diff --git a/chopper/.gitignore b/chopper/.gitignore new file mode 100644 index 0000000..3a85790 --- /dev/null +++ b/chopper/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md new file mode 100644 index 0000000..9f30d8e --- /dev/null +++ b/chopper/CHANGELOG.md @@ -0,0 +1,11 @@ +## 1.1.1 + +- Fixed system promp + +## 1.1.0 + +- Updated agent to enable commandless mode + +## 1.0.0 + +- Initial version. diff --git a/chopper/README.md b/chopper/README.md new file mode 100644 index 0000000..d82488a --- /dev/null +++ b/chopper/README.md @@ -0,0 +1,3 @@ +# Agent Reamde File + +This is a sample readme file for agent. You add description about the agent and any other instruction or information. diff --git a/chopper/analysis_options.yaml b/chopper/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/chopper/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/chopper/assets/examples/basic_example/definition.chopper.dart b/chopper/assets/examples/basic_example/definition.chopper.dart new file mode 100644 index 0000000..4ee6e51 --- /dev/null +++ b/chopper/assets/examples/basic_example/definition.chopper.dart @@ -0,0 +1,128 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'definition.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +final class _$MyService extends MyService { + _$MyService([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final Type definitionType = MyService; + + @override + Future> getResource(String id) { + final Uri $url = Uri.parse('/resources/${id}'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future>> getMapResource(String id) { + final Uri $url = Uri.parse('/resources/'); + final Map $params = {'id': id}; + final Map $headers = { + 'foo': 'bar', + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + headers: $headers, + ); + return client.send, Map>($request); + } + + @override + Future>>> getListResources() { + final Uri $url = Uri.parse('/resources/resources'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client + .send>, Map>($request); + } + + @override + Future> postResourceUrlEncoded( + String toto, + String b, + ) { + final Uri $url = Uri.parse('/resources/'); + final $body = { + 'a': toto, + 'b': b, + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> postResources( + Map a, + Map b, + String c, + ) { + final Uri $url = Uri.parse('/resources/multi'); + final List $parts = [ + PartValue>( + '1', + a, + ), + PartValue>( + '2', + b, + ), + PartValue( + '3', + c, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postFile(List bytes) { + final Uri $url = Uri.parse('/resources/file'); + final List $parts = [ + PartValue>( + 'file', + bytes, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } +} diff --git a/chopper/assets/examples/basic_example/definition.dart b/chopper/assets/examples/basic_example/definition.dart new file mode 100644 index 0000000..ef8fcf9 --- /dev/null +++ b/chopper/assets/examples/basic_example/definition.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:chopper/chopper.dart'; + +part 'definition.chopper.dart'; + +@ChopperApi(baseUrl: '/resources') +abstract class MyService extends ChopperService { + static MyService create(ChopperClient client) => _$MyService(client); + + @Get(path: '/{id}') + Future getResource( + @Path() String id, + ); + + @Get(path: '/', headers: {'foo': 'bar'}) + Future> getMapResource( + @Query() String id, + ); + + @Get(path: '/resources') + Future>> getListResources(); + + @Post(path: '/') + Future postResourceUrlEncoded( + @Field('a') String toto, + @Field() String b, + ); + + @Post(path: '/multi') + @multipart + Future postResources( + @Part('1') Map a, + @Part('2') Map b, + @Part('3') String c, + ); + + @Post(path: '/file') + @multipart + Future postFile( + @Part('file') List bytes, + ); +} diff --git a/chopper/assets/examples/basic_example/main.dart b/chopper/assets/examples/basic_example/main.dart new file mode 100644 index 0000000..b7a2cad --- /dev/null +++ b/chopper/assets/examples/basic_example/main.dart @@ -0,0 +1,24 @@ +import 'package:chopper/chopper.dart'; + +import 'definition.dart'; + +Future main() async { + final chopper = ChopperClient( + baseUrl: Uri.parse('http://localhost:8000'), + services: [ + // the generated service + MyService.create(ChopperClient()), + ], + converter: JsonConverter(), + ); + + final myService = chopper.getService(); + + final response = await myService.getMapResource('1'); + print(response.body); + + final list = await myService.getListResources(); + print(list.body); + + chopper.dispose(); +} diff --git a/chopper/assets/examples/basic_example/tag.chopper.dart b/chopper/assets/examples/basic_example/tag.chopper.dart new file mode 100644 index 0000000..77f07fa --- /dev/null +++ b/chopper/assets/examples/basic_example/tag.chopper.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tag.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +final class _$TagService extends TagService { + _$TagService([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final Type definitionType = TagService; + + @override + Future> requestWithTag({BizTag tag = const BizTag()}) { + final Uri $url = Uri.parse('/tag'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + tag: tag, + ); + return client.send($request); + } + + @override + Future> includeBodyNullOrEmptyTag( + {IncludeBodyNullOrEmptyTag tag = const IncludeBodyNullOrEmptyTag()}) { + final Uri $url = Uri.parse('/tag'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + tag: tag, + ); + return client.send($request); + } +} diff --git a/chopper/assets/examples/basic_example/tag.dart b/chopper/assets/examples/basic_example/tag.dart new file mode 100644 index 0000000..eb168a0 --- /dev/null +++ b/chopper/assets/examples/basic_example/tag.dart @@ -0,0 +1,87 @@ +/// @author luwenjie on 2024/3/20 11:38:11 +/// +/// +/// +import "package:chopper/chopper.dart"; + +import 'definition.dart'; + +part 'tag.chopper.dart'; + +Future main() async { + final chopper = ChopperClient( + baseUrl: Uri.parse('http://localhost:8000'), + services: [ + // the generated service + TagService.create(ChopperClient()), + ], + interceptors: [ + TagInterceptor(), + ], + converter: JsonConverter(), + ); + + final myService = chopper.getService(); + + final response = await myService.getMapResource('1'); + print(response.body); + + final list = await myService.getListResources(); + print(list.body); + chopper.dispose(); +} + +// add a uniform appId header for some path +class BizTag { + final int appId; + + BizTag({this.appId = 0}); +} + +class IncludeBodyNullOrEmptyTag { + bool includeNull = false; + bool includeEmpty = false; + + IncludeBodyNullOrEmptyTag(this.includeNull, this.includeEmpty); +} + +class TagConverter extends JsonConverter { + FutureOr convertRequest(Request request) { + final tag = request.tag; + if (tag is IncludeBodyNullOrEmptyTag) { + if (request.body is Map) { + final Map body = request.body as Map; + final Map bodyCopy = {}; + for (final MapEntry entry in body.entries) { + if (!tag.includeNull && entry.value == null) continue; + if (!tag.includeEmpty && entry.value == "") continue; + bodyCopy[entry.key] = entry.value; + } + request = request.copyWith(body: bodyCopy); + } + } + } +} + +class TagInterceptor implements RequestInterceptor { + FutureOr onRequest(Request request) { + final tag = request.tag; + if (tag is BizTag) { + request.headers["x-appId"] = tag.appId; + } + return request; + } +} + +@ChopperApi(baseUrl: '/tag') +abstract class TagService extends ChopperService { + static TagService create(ChopperClient client) => _$TagService(client); + + @get(path: '/bizRequest') + Future requestWithTag({@Tag() BizTag tag = const BizTag()}); + + @get(path: '/include') + Future includeBodyNullOrEmptyTag( + {@Tag() + IncludeBodyNullOrEmptyTag tag = const IncludeBodyNullOrEmptyTag()}); +} diff --git a/chopper/assets/examples/chopper_with_provider/.gitignore b/chopper/assets/examples/chopper_with_provider/.gitignore new file mode 100644 index 0000000..0fa6b67 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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 +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# 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 diff --git a/chopper/assets/examples/chopper_with_provider/README.md b/chopper/assets/examples/chopper_with_provider/README.md new file mode 100644 index 0000000..6945dce --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/README.md @@ -0,0 +1,16 @@ +# chopper_with_provider + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/chopper/assets/examples/chopper_with_provider/analysis_options.yaml b/chopper/assets/examples/chopper_with_provider/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/chopper/assets/examples/chopper_with_provider/android/.gitignore b/chopper/assets/examples/chopper_with_provider/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/chopper/assets/examples/chopper_with_provider/android/app/build.gradle b/chopper/assets/examples/chopper_with_provider/android/app/build.gradle new file mode 100644 index 0000000..f76b83b --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/app/build.gradle @@ -0,0 +1,68 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 30 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.chopper_with_provider" + minSdkVersion 16 + targetSdkVersion 30 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/chopper/assets/examples/chopper_with_provider/android/app/src/debug/AndroidManifest.xml b/chopper/assets/examples/chopper_with_provider/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..33af70b --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/chopper/assets/examples/chopper_with_provider/android/app/src/main/AndroidManifest.xml b/chopper/assets/examples/chopper_with_provider/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..01858c8 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/chopper/assets/examples/chopper_with_provider/android/app/src/main/kotlin/com/example/chopper_with_provider/MainActivity.kt b/chopper/assets/examples/chopper_with_provider/android/app/src/main/kotlin/com/example/chopper_with_provider/MainActivity.kt new file mode 100644 index 0000000..2b97b7a --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/app/src/main/kotlin/com/example/chopper_with_provider/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.chopper_with_provider + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/drawable-v21/launch_background.xml b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/drawable/launch_background.xml b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/values-night/styles.xml b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..449a9f9 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/values/styles.xml b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d74aa35 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/chopper/assets/examples/chopper_with_provider/android/app/src/profile/AndroidManifest.xml b/chopper/assets/examples/chopper_with_provider/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..33af70b --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/chopper/assets/examples/chopper_with_provider/android/build.gradle b/chopper/assets/examples/chopper_with_provider/android/build.gradle new file mode 100644 index 0000000..ed45c65 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/chopper/assets/examples/chopper_with_provider/android/gradle.properties b/chopper/assets/examples/chopper_with_provider/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/chopper/assets/examples/chopper_with_provider/android/gradle/wrapper/gradle-wrapper.properties b/chopper/assets/examples/chopper_with_provider/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bc6a58a --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/chopper/assets/examples/chopper_with_provider/android/settings.gradle b/chopper/assets/examples/chopper_with_provider/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/chopper/assets/examples/chopper_with_provider/ios/.gitignore b/chopper/assets/examples/chopper_with_provider/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/chopper/assets/examples/chopper_with_provider/ios/Flutter/AppFrameworkInfo.plist b/chopper/assets/examples/chopper_with_provider/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..8d4492f --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/chopper/assets/examples/chopper_with_provider/ios/Flutter/Debug.xcconfig b/chopper/assets/examples/chopper_with_provider/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/chopper/assets/examples/chopper_with_provider/ios/Flutter/Release.xcconfig b/chopper/assets/examples/chopper_with_provider/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/project.pbxproj b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..dbebfe2 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,471 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.chopperWithProvider; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.chopperWithProvider; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.example.chopperWithProvider; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..a28140c --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner.xcworkspace/contents.xcworkspacedata b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/AppDelegate.swift b/chopper/assets/examples/chopper_with_provider/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..f091b6b Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..d0ef06e Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..c8f9ed8 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..a6d6b86 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..75b2d16 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..c4df70d Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Base.lproj/LaunchScreen.storyboard b/chopper/assets/examples/chopper_with_provider/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Base.lproj/Main.storyboard b/chopper/assets/examples/chopper_with_provider/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Info.plist b/chopper/assets/examples/chopper_with_provider/ios/Runner/Info.plist new file mode 100644 index 0000000..ed6565b --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + chopper_with_provider + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/chopper/assets/examples/chopper_with_provider/ios/Runner/Runner-Bridging-Header.h b/chopper/assets/examples/chopper_with_provider/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/chopper/assets/examples/chopper_with_provider/lib/data/post_api_service.chopper.dart b/chopper/assets/examples/chopper_with_provider/lib/data/post_api_service.chopper.dart new file mode 100644 index 0000000..2f40ed8 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/lib/data/post_api_service.chopper.dart @@ -0,0 +1,40 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'post_api_service.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations +class _$PostApiService extends PostApiService { + _$PostApiService([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final definitionType = PostApiService; + + @override + Future> getPosts() { + final $url = 'https://jsonplaceholder.typicode.com/posts'; + final $request = Request('GET', $url, client.baseUrl); + return client.send($request); + } + + @override + Future> getPost(int id) { + final $url = 'https://jsonplaceholder.typicode.com/posts/${id}'; + final $request = Request('GET', $url, client.baseUrl); + return client.send($request); + } + + @override + Future> postPost(Map body) { + final $url = 'https://jsonplaceholder.typicode.com/posts'; + final $body = body; + final $request = Request('POST', $url, client.baseUrl, body: $body); + return client.send($request); + } +} diff --git a/chopper/assets/examples/chopper_with_provider/lib/data/post_api_service.dart b/chopper/assets/examples/chopper_with_provider/lib/data/post_api_service.dart new file mode 100644 index 0000000..e8e4adb --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/lib/data/post_api_service.dart @@ -0,0 +1,36 @@ + + +import 'package:chopper/chopper.dart'; + +part 'post_api_service.chopper.dart'; + +@ChopperApi(baseUrl: '/posts') +abstract class PostApiService extends ChopperService{ + + @Get() + Future getPosts(); + + @Get(path: '/{id}') + Future getPost( @Path('id') int id); + + @Post() + Future postPost( + @Body() Map body, + ); + + static PostApiService create(){ + + final client = ChopperClient( + baseUrl: 'https://jsonplaceholder.typicode.com', + services: [ + _$PostApiService(), + ], + converter: const JsonConverter(), + ); + + return _$PostApiService(client); + + } + + +} \ No newline at end of file diff --git a/chopper/assets/examples/chopper_with_provider/lib/home_page.dart b/chopper/assets/examples/chopper_with_provider/lib/home_page.dart new file mode 100644 index 0000000..c7fb295 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/lib/home_page.dart @@ -0,0 +1,57 @@ +import 'dart:convert'; + +import 'package:chopper/chopper.dart'; +import 'package:chopper_with_provider/data/post_api_service.dart'; +import 'package:chopper_with_provider/single_post_page.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class HomePage extends StatelessWidget { + const HomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Chopper Blog'),), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () async { + final response = await Provider.of(context, listen: false).postPost({ 'key1': 'value1' }); + print(response.body); + },), + body: FutureBuilder( + future: Provider.of(context).getPosts(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + + if(snapshot.connectionState == ConnectionState.done){ + + final List posts = json.decode(snapshot.data.bodyString); + + return ListView.builder( + itemCount: posts.length, + itemBuilder: (context, index) { + return Card( + child: ListTile( + onTap: (){ + _navigateToPost(context, posts[index]['id']); + } , + title: Text(posts[index]['title']), + subtitle: Text(posts[index]['body']), + )); + },); + }else{ + return const Center(child: CircularProgressIndicator(),); + } + + },), + + ); + } +} + +void _navigateToPost(BuildContext context, int id){ + Navigator.push(context, CupertinoPageRoute(builder: (context) => SinglePostPage(postId: id,),)); +} + + diff --git a/chopper/assets/examples/chopper_with_provider/lib/main.dart b/chopper/assets/examples/chopper_with_provider/lib/main.dart new file mode 100644 index 0000000..cd7c2e7 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/lib/main.dart @@ -0,0 +1,23 @@ +import 'package:chopper_with_provider/data/post_api_service.dart'; +import 'package:chopper_with_provider/home_page.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Provider( + create: (_) => PostApiService.create(), + dispose: (_, PostApiService service) => service.client.dispose(), + child: MaterialApp( + home: HomePage(), + ), + ); + } +} diff --git a/chopper/assets/examples/chopper_with_provider/lib/single_post_page.dart b/chopper/assets/examples/chopper_with_provider/lib/single_post_page.dart new file mode 100644 index 0000000..b94992b --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/lib/single_post_page.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; + +import 'package:chopper/chopper.dart'; +import 'package:chopper_with_provider/data/post_api_service.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class SinglePostPage extends StatelessWidget { + + final int postId; + + const SinglePostPage({Key? key, required this.postId}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + + appBar: AppBar(title: Text('single post ')), + body: FutureBuilder( + future: Provider.of(context).getPost(postId), + builder: (BuildContext context, AsyncSnapshot snapshot) { + + if(snapshot.connectionState == ConnectionState.done){ + + final Map post = json.decode(snapshot.data.bodyString); + + return Card( + child: ListTile( + title: Text(post['title']), + subtitle: Text(post['body']), + )); + }else{ + return const Center(child: CircularProgressIndicator(),); + } + + },), + + + ); + } +} diff --git a/chopper/assets/examples/chopper_with_provider/pubspec.lock b/chopper/assets/examples/chopper_with_provider/pubspec.lock new file mode 100644 index 0000000..e8ee9f7 --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/pubspec.lock @@ -0,0 +1,589 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8" + url: "https://pub.dev" + source: hosted + version: "47.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80" + url: "https://pub.dev" + source: hosted + version: "4.7.0" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6" + url: "https://pub.dev" + source: hosted + version: "2.0.10" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + url: "https://pub.dev" + source: hosted + version: "2.4.9" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + url: "https://pub.dev" + source: hosted + version: "7.2.10" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + chopper: + dependency: "direct main" + description: + name: chopper + sha256: "813cabd029ad8c020874429a671f2c15f45cfc3ced66b566bfa181a9b61b89b8" + url: "https://pub.dev" + source: hosted + version: "4.0.6" + chopper_generator: + dependency: "direct dev" + description: + name: chopper_generator + sha256: b090e4926b2ad3a0277fa65fa6590a1f2087b55afd0dd53341b4ade669fabdb5 + url: "https://pub.dev" + source: hosted + version: "4.0.6" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + http: + dependency: transitive + description: + name: http + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + url: "https://pub.dev" + source: hosted + version: "0.13.6" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" + source: hosted + version: "10.0.4" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + url: "https://pub.dev" + source: hosted + version: "1.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + provider: + dependency: "direct main" + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + url: "https://pub.dev" + source: hosted + version: "1.2.6" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b" + url: "https://pub.dev" + source: hosted + version: "14.2.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/chopper/assets/examples/chopper_with_provider/pubspec.yaml b/chopper/assets/examples/chopper_with_provider/pubspec.yaml new file mode 100644 index 0000000..2172e1d --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/pubspec.yaml @@ -0,0 +1,87 @@ +name: chopper_with_provider +description: A new Flutter project. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 + +environment: + sdk: ">=2.12.0 <3.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + cupertino_icons: ^1.0.2 + chopper: ^4.0.3 + provider: ^6.0.1 + + +dev_dependencies: + flutter_test: + sdk: flutter + + + flutter_lints: ^1.0.0 + chopper_generator: ^4.0.3 + build_runner: ^2.1.2 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/chopper/assets/examples/chopper_with_provider/test/widget_test.dart b/chopper/assets/examples/chopper_with_provider/test/widget_test.dart new file mode 100644 index 0000000..356afac --- /dev/null +++ b/chopper/assets/examples/chopper_with_provider/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:chopper_with_provider/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/chopper/assets/examples/misc_examples/Makefile b/chopper/assets/examples/misc_examples/Makefile new file mode 100644 index 0000000..dee4bcf --- /dev/null +++ b/chopper/assets/examples/misc_examples/Makefile @@ -0,0 +1,46 @@ +# Makefile + +help: + @printf "%-20s %s\n" "Target" "Description" + @printf "%-20s %s\n" "------" "-----------" + @make -pqR : 2>/dev/null \ + | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' \ + | sort \ + | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' \ + | xargs -I _ sh -c 'printf "%-20s " _; make _ -nB | (grep -i "^# Help:" || echo "") | tail -1 | sed "s/^# Help: //g"' + +analyze: + @# Help: Analyze the project's Dart code. + dart analyze --fatal-infos + +check_format: + @# Help: Check the formatting of one or more Dart files. + dart format --output=none --set-exit-if-changed . + +check_outdated: + @# Help: Check which of the project's packages are outdated. + dart pub outdated + +check_style: + @# Help: Analyze the project's Dart code and check the formatting one or more Dart files. + make analyze && make check_format + +code_gen: + @# Help: Run the build system for Dart code generation and modular compilation. + dart run build_runner build --delete-conflicting-outputs + +code_gen_watcher: + @# Help: Run the build system for Dart code generation and modular compilation as a watcher. + dart run build_runner watch --delete-conflicting-outputs + +format: + @# Help: Format one or more Dart files. + dart format . + +install: + @# Help: Install all the project's packages + dart pub get + +upgrade: + @# Help: Upgrade all the project's packages. + dart pub upgrade \ No newline at end of file diff --git a/chopper/assets/examples/misc_examples/analysis_options.yaml b/chopper/assets/examples/misc_examples/analysis_options.yaml new file mode 100644 index 0000000..8ed20e7 --- /dev/null +++ b/chopper/assets/examples/misc_examples/analysis_options.yaml @@ -0,0 +1,12 @@ +include: package:lints/recommended.yaml + +analyzer: + exclude: + - "**.g.dart" + - "**.chopper.dart" + - "**.mocks.dart" + +linter: + rules: + avoid_print: false + prefer_single_quotes: true diff --git a/chopper/assets/examples/misc_examples/bin/main_built_value.dart b/chopper/assets/examples/misc_examples/bin/main_built_value.dart new file mode 100644 index 0000000..7d7fe8c --- /dev/null +++ b/chopper/assets/examples/misc_examples/bin/main_built_value.dart @@ -0,0 +1,108 @@ +import 'dart:async'; + +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/serializer.dart'; +import 'package:built_value/standard_json_plugin.dart'; +import 'package:chopper/chopper.dart'; +import 'package:chopper_example/built_value_resource.dart'; +import 'package:chopper_example/built_value_serializers.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; + +final jsonSerializers = + (serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build(); + +/// Simple client to have working example without remote server +final client = MockClient((req) async { + if (req.method == 'POST') { + return http.Response('{"type":"Fatal","message":"fatal erorr"}', 500); + } + if (req.url.path == '/resources/list') { + return http.Response('[{"id":"1","name":"Foo"}]', 200); + } + + return http.Response('{"id":"1","name":"Foo"}', 200); +}); + +main() async { + final chopper = ChopperClient( + client: client, + baseUrl: Uri.parse('http://localhost:8000'), + converter: BuiltValueConverter(), + errorConverter: BuiltValueConverter(), + services: [ + // the generated service + MyService.create(), + ], + ); + + final myService = chopper.getService(); + + final response1 = await myService.getResource('1'); + print('response 1: ${response1.body}'); // undecoded String + + final response2 = await myService.getTypedResource(); + print('response 2: ${response2.body}'); // decoded Resource + + final response3 = await myService.getBuiltListResources(); + print('response 3: ${response3.body}'); + + try { + final builder = ResourceBuilder() + ..id = '3' + ..name = 'Super Name'; + await myService.newResource(builder.build()); + } on Response catch (error) { + print(error.body); + } +} + +class BuiltValueConverter extends JsonConverter { + T? _deserialize(dynamic value) { + final serializer = jsonSerializers.serializerForType(T) as Serializer?; + if (serializer == null) { + throw Exception('No serializer for type $T'); + } + + return jsonSerializers.deserializeWith(serializer, value); + } + + BuiltList _deserializeListOf(Iterable value) => BuiltList( + value.map((value) => _deserialize(value)).toList(growable: false), + ); + + dynamic _decode(dynamic entity) { + /// handle case when we want to access to Map directly + /// getResource or getMapResource + /// Avoid dynamic or unconverted value, this could lead to several issues + if (entity is T) return entity; + + try { + return entity is List + ? _deserializeListOf(entity) + : _deserialize(entity); + } catch (e) { + print(e); + + return null; + } + } + + @override + FutureOr> convertResponse( + Response response, + ) async { + // use [JsonConverter] to decode json + final Response jsonRes = await super.convertResponse(response); + final body = _decode(jsonRes.body); + + return jsonRes.copyWith(body: body); + } + + @override + Request convertRequest(Request request) => super.convertRequest( + request.copyWith( + body: serializers.serialize(request.body), + ), + ); +} diff --git a/chopper/assets/examples/misc_examples/bin/main_json_serializable.dart b/chopper/assets/examples/misc_examples/bin/main_json_serializable.dart new file mode 100644 index 0000000..08ab263 --- /dev/null +++ b/chopper/assets/examples/misc_examples/bin/main_json_serializable.dart @@ -0,0 +1,128 @@ +import 'dart:async'; + +import 'package:chopper/chopper.dart'; +import 'package:chopper_example/json_serializable.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; + +/// Simple client to have working example without remote server +final client = MockClient((req) async { + if (req.method == 'POST') { + return http.Response('{"type":"Fatal","message":"fatal erorr"}', 500); + } + if (req.method == 'GET' && req.headers['test'] == 'list') { + return http.Response('[{"id":"1","name":"Foo"}]', 200); + } + + return http.Response('{"id":"1","name":"Foo"}', 200); +}); + +main() async { + final converter = JsonSerializableConverter({ + Resource: Resource.fromJsonFactory, + }); + + final chopper = ChopperClient( + client: client, + baseUrl: Uri.parse('http://localhost:8000'), + // bind your object factories here + converter: converter, + errorConverter: converter, + services: [ + // the generated service + MyService.create(), + ], + /* Interceptors */ + interceptors: [AuthInterceptor()], + ); + + final myService = chopper.getService(); + + final response1 = await myService.getResource('1'); + print('response 1: ${response1.body}'); // undecoded String + + final response2 = await myService.getResources(); + print('response 2: ${response2.body}'); // decoded list of Resources + + final response3 = await myService.getTypedResource(); + print('response 3: ${response3.body}'); // decoded Resource + + final response4 = await myService.getMapResource('1'); + print('response 4: ${response4.body}'); // undecoded Resource + + try { + await myService.newResource(Resource('3', 'Super Name')); + } on Response catch (error) { + print(error.body); + } +} + +class AuthInterceptor implements Interceptor { + @override + FutureOr> intercept( + Chain chain) async { + return chain.proceed( + applyHeader( + chain.request, + 'Authorization', + '42', + ), + ); + } +} + +typedef JsonFactory = T Function(Map json); + +class JsonSerializableConverter extends JsonConverter { + final Map factories; + + const JsonSerializableConverter(this.factories); + + T? _decodeMap(Map values) { + /// Get jsonFactory using Type parameters + /// if not found or invalid, throw error or return null + final jsonFactory = factories[T]; + if (jsonFactory == null || jsonFactory is! JsonFactory) { + /// throw serializer not found error; + return null; + } + + return jsonFactory(values); + } + + List _decodeList(Iterable values) => + values.where((v) => v != null).map((v) => _decode(v)).toList(); + + dynamic _decode(entity) { + if (entity is Iterable) return _decodeList(entity as List); + + if (entity is Map) return _decodeMap(entity as Map); + + return entity; + } + + @override + FutureOr> convertResponse( + Response response, + ) async { + // use [JsonConverter] to decode json + final jsonRes = await super.convertResponse(response); + + return jsonRes.copyWith(body: _decode(jsonRes.body)); + } + + @override + // all objects should implements toJson method + // ignore: unnecessary_overrides + Request convertRequest(Request request) => super.convertRequest(request); + + @override + FutureOr convertError(Response response) async { + // use [JsonConverter] to decode json + final jsonRes = await super.convertError(response); + + return jsonRes.copyWith( + body: ResourceError.fromJsonFactory(jsonRes.body), + ); + } +} diff --git a/chopper/assets/examples/misc_examples/bin/main_json_serializable_squadron_worker_pool.dart b/chopper/assets/examples/misc_examples/bin/main_json_serializable_squadron_worker_pool.dart new file mode 100644 index 0000000..f2e268c --- /dev/null +++ b/chopper/assets/examples/misc_examples/bin/main_json_serializable_squadron_worker_pool.dart @@ -0,0 +1,164 @@ +/// This example uses +/// - https://github.com/google/json_serializable.dart +/// - https://github.com/d-markey/squadron +/// - https://github.com/d-markey/squadron_builder + +import 'dart:async' show FutureOr; +import 'dart:convert' show jsonDecode; + +import 'package:chopper/chopper.dart'; +import 'package:chopper_example/json_decode_service.dart'; +import 'package:chopper_example/json_serializable.dart'; +import 'package:http/testing.dart'; +import 'package:squadron/squadron.dart'; +import 'package:http/http.dart' as http; + +import 'main_json_serializable.dart' show AuthInterceptor; + +typedef JsonFactory = T Function(Map json); + +/// This JsonConverter works with or without a WorkerPool +class JsonSerializableWorkerPoolConverter extends JsonConverter { + const JsonSerializableWorkerPoolConverter(this.factories, [this.workerPool]); + + final Map factories; + final JsonDecodeServiceWorkerPool? workerPool; + + T? _decodeMap(Map values) { + /// Get jsonFactory using Type parameters + /// if not found or invalid, throw error or return null + final jsonFactory = factories[T]; + if (jsonFactory == null || jsonFactory is! JsonFactory) { + /// throw serializer not found error; + return null; + } + + return jsonFactory(values); + } + + List _decodeList(Iterable values) => + values.where((v) => v != null).map((v) => _decode(v)).toList(); + + dynamic _decode(entity) { + if (entity is Iterable) return _decodeList(entity as List); + + if (entity is Map) return _decodeMap(entity as Map); + + return entity; + } + + @override + FutureOr> convertResponse( + Response response, + ) async { + // use [JsonConverter] to decode json + final jsonRes = await super.convertResponse(response); + + return jsonRes.copyWith(body: _decode(jsonRes.body)); + } + + @override + FutureOr convertError(Response response) async { + // use [JsonConverter] to decode json + final jsonRes = await super.convertError(response); + + return jsonRes.copyWith( + body: ResourceError.fromJsonFactory(jsonRes.body), + ); + } + + @override + FutureOr tryDecodeJson(String data) async { + try { + // if there is a worker pool use it, otherwise run in the main thread + return workerPool != null + ? await workerPool!.jsonDecode(data) + : jsonDecode(data); + } catch (error) { + print(error); + + chopperLogger.warning(error); + + return data; + } + } +} + +/// Simple client to have working example without remote server +final client = MockClient((http.Request req) async { + if (req.method == 'POST') { + return http.Response('{"type":"Fatal","message":"fatal error"}', 500); + } + if (req.method == 'GET' && req.headers['test'] == 'list') { + return http.Response('[{"id":"1","name":"Foo"}]', 200); + } + + return http.Response('{"id":"1","name":"Foo"}', 200); +}); + +/// inspired by https://github.com/d-markey/squadron_sample/blob/main/lib/main.dart +void initSquadron(String id) { + Squadron.setId(id); + Squadron.setLogger(ConsoleSquadronLogger()); + Squadron.logLevel = SquadronLogLevel.all; + Squadron.debugMode = true; +} + +Future main() async { + /// initialize Squadron before using it + initSquadron('worker_pool_example'); + + final jsonDecodeServiceWorkerPool = JsonDecodeServiceWorkerPool( + // Set whatever you want here + concurrencySettings: ConcurrencySettings.oneCpuThread, + ); + + /// start the Worker Pool + await jsonDecodeServiceWorkerPool.start(); + + final converter = JsonSerializableWorkerPoolConverter( + { + Resource: Resource.fromJsonFactory, + }, + // make sure to provide the WorkerPool to the JsonConverter + jsonDecodeServiceWorkerPool, + ); + + final chopper = ChopperClient( + client: client, + baseUrl: Uri.parse('http://localhost:8000'), + // bind your object factories here + converter: converter, + errorConverter: converter, + services: [ + // the generated service + MyService.create(), + ], + /* Interceptor */ + interceptors: [AuthInterceptor()], + ); + + final myService = chopper.getService(); + + /// All of the calls below will use jsonDecode in an Isolate worker + final response1 = await myService.getResource('1'); + print('response 1: ${response1.body}'); // undecoded String + + final response2 = await myService.getResources(); + print('response 2: ${response2.body}'); // decoded list of Resources + + final response3 = await myService.getTypedResource(); + print('response 3: ${response3.body}'); // decoded Resource + + final response4 = await myService.getMapResource('1'); + print('response 4: ${response4.body}'); // undecoded Resource + + try { + await myService.newResource(Resource('3', 'Super Name')); + } on Response catch (error) { + print(error.body); + } + + /// stop the Worker Pool + jsonDecodeServiceWorkerPool.stop(); +} diff --git a/chopper/assets/examples/misc_examples/build.yaml b/chopper/assets/examples/misc_examples/build.yaml new file mode 100644 index 0000000..b809962 --- /dev/null +++ b/chopper/assets/examples/misc_examples/build.yaml @@ -0,0 +1,20 @@ +targets: + $default: + builders: + json_serializable: + options: + # Options configure how source code is generated for every + # `@JsonSerializable`-annotated class in the package. + # + # The default value for each is listed. + # + # For usage information, reference the corresponding field in + # `JsonSerializableGenerator`. + any_map: false + checked: false + explicit_to_json: true + create_to_json: true + squadron_builder:worker_builder: + options: + with_finalizers: true + serialization_type: List \ No newline at end of file diff --git a/chopper/assets/examples/misc_examples/build_serializers.yaml b/chopper/assets/examples/misc_examples/build_serializers.yaml new file mode 100644 index 0000000..868f1d9 --- /dev/null +++ b/chopper/assets/examples/misc_examples/build_serializers.yaml @@ -0,0 +1,22 @@ +targets: + $default: + sources: + include: ["lib/built_value.dart", + "lib/built_value_serializers.dart", + "lib/json_serializable.dart", + "lib/jaguar_serializer.dart" ] + builders: + json_serializable: + options: + # Options configure how source code is generated for every + # `@JsonSerializable`-annotated class in the package. + # + # The default value for each is listed. + # + # For usage information, reference the corresponding field in + # `JsonSerializableGenerator`. + use_wrappers: false + any_map: false + checked: false + explicit_to_json: true + generate_to_json_function: true \ No newline at end of file diff --git a/chopper/assets/examples/misc_examples/lib/built_value_resource.chopper.dart b/chopper/assets/examples/misc_examples/lib/built_value_resource.chopper.dart new file mode 100644 index 0000000..3226421 --- /dev/null +++ b/chopper/assets/examples/misc_examples/lib/built_value_resource.chopper.dart @@ -0,0 +1,75 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'built_value_resource.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +final class _$MyService extends MyService { + _$MyService([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final definitionType = MyService; + + @override + Future> getResource(String id) { + final Uri $url = Uri.parse('/resources/${id}/'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future>> getBuiltListResources() { + final Uri $url = Uri.parse('/resources/list'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send, Resource>($request); + } + + @override + Future> getTypedResource() { + final Uri $url = Uri.parse('/resources/'); + final Map $headers = { + 'foo': 'bar', + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> newResource( + Resource resource, { + String? name, + }) { + final Uri $url = Uri.parse('/resources'); + final Map $headers = { + if (name != null) 'name': name, + }; + final $body = resource; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); + return client.send($request); + } +} diff --git a/chopper/assets/examples/misc_examples/lib/built_value_resource.dart b/chopper/assets/examples/misc_examples/lib/built_value_resource.dart new file mode 100644 index 0000000..9e30c3e --- /dev/null +++ b/chopper/assets/examples/misc_examples/lib/built_value_resource.dart @@ -0,0 +1,57 @@ +library resource; + +import 'dart:async'; + +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; +import 'package:chopper/chopper.dart'; + +part 'built_value_resource.chopper.dart'; +part 'built_value_resource.g.dart'; + +abstract class Resource implements Built { + String get id; + + String get name; + + static Serializer get serializer => _$resourceSerializer; + + factory Resource([Function(ResourceBuilder b) updates]) = _$Resource; + + Resource._(); +} + +abstract class ResourceError + implements Built { + String get type; + + String get message; + + static Serializer get serializer => _$resourceErrorSerializer; + + factory ResourceError([Function(ResourceErrorBuilder b) updates]) = + _$ResourceError; + + ResourceError._(); +} + +@ChopperApi(baseUrl: '/resources') +abstract class MyService extends ChopperService { + static MyService create([ChopperClient? client]) => _$MyService(client); + + @Get(path: '/{id}/') + Future getResource(@Path() String id); + + @Get(path: '/list') + Future>> getBuiltListResources(); + + @Get(path: '/', headers: {'foo': 'bar'}) + Future> getTypedResource(); + + @Post() + Future> newResource( + @Body() Resource resource, { + @Header() String? name, + }); +} diff --git a/chopper/assets/examples/misc_examples/lib/built_value_resource.g.dart b/chopper/assets/examples/misc_examples/lib/built_value_resource.g.dart new file mode 100644 index 0000000..525a059 --- /dev/null +++ b/chopper/assets/examples/misc_examples/lib/built_value_resource.g.dart @@ -0,0 +1,295 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'built_value_resource.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializer _$resourceSerializer = new _$ResourceSerializer(); +Serializer _$resourceErrorSerializer = + new _$ResourceErrorSerializer(); + +class _$ResourceSerializer implements StructuredSerializer { + @override + final Iterable types = const [Resource, _$Resource]; + @override + final String wireName = 'Resource'; + + @override + Iterable serialize(Serializers serializers, Resource object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'id', + serializers.serialize(object.id, specifiedType: const FullType(String)), + 'name', + serializers.serialize(object.name, specifiedType: const FullType(String)), + ]; + + return result; + } + + @override + Resource deserialize(Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new ResourceBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current! as String; + iterator.moveNext(); + final Object? value = iterator.current; + switch (key) { + case 'id': + result.id = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; + case 'name': + result.name = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; + } + } + + return result.build(); + } +} + +class _$ResourceErrorSerializer implements StructuredSerializer { + @override + final Iterable types = const [ResourceError, _$ResourceError]; + @override + final String wireName = 'ResourceError'; + + @override + Iterable serialize(Serializers serializers, ResourceError object, + {FullType specifiedType = FullType.unspecified}) { + final result = [ + 'type', + serializers.serialize(object.type, specifiedType: const FullType(String)), + 'message', + serializers.serialize(object.message, + specifiedType: const FullType(String)), + ]; + + return result; + } + + @override + ResourceError deserialize( + Serializers serializers, Iterable serialized, + {FullType specifiedType = FullType.unspecified}) { + final result = new ResourceErrorBuilder(); + + final iterator = serialized.iterator; + while (iterator.moveNext()) { + final key = iterator.current! as String; + iterator.moveNext(); + final Object? value = iterator.current; + switch (key) { + case 'type': + result.type = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; + case 'message': + result.message = serializers.deserialize(value, + specifiedType: const FullType(String))! as String; + break; + } + } + + return result.build(); + } +} + +class _$Resource extends Resource { + @override + final String id; + @override + final String name; + + factory _$Resource([void Function(ResourceBuilder)? updates]) => + (new ResourceBuilder()..update(updates))._build(); + + _$Resource._({required this.id, required this.name}) : super._() { + BuiltValueNullFieldError.checkNotNull(id, r'Resource', 'id'); + BuiltValueNullFieldError.checkNotNull(name, r'Resource', 'name'); + } + + @override + Resource rebuild(void Function(ResourceBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + ResourceBuilder toBuilder() => new ResourceBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is Resource && id == other.id && name == other.name; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'Resource') + ..add('id', id) + ..add('name', name)) + .toString(); + } +} + +class ResourceBuilder implements Builder { + _$Resource? _$v; + + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; + + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; + + ResourceBuilder(); + + ResourceBuilder get _$this { + final $v = _$v; + if ($v != null) { + _id = $v.id; + _name = $v.name; + _$v = null; + } + return this; + } + + @override + void replace(Resource other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$Resource; + } + + @override + void update(void Function(ResourceBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + Resource build() => _build(); + + _$Resource _build() { + final _$result = _$v ?? + new _$Resource._( + id: BuiltValueNullFieldError.checkNotNull(id, r'Resource', 'id'), + name: BuiltValueNullFieldError.checkNotNull( + name, r'Resource', 'name')); + replace(_$result); + return _$result; + } +} + +class _$ResourceError extends ResourceError { + @override + final String type; + @override + final String message; + + factory _$ResourceError([void Function(ResourceErrorBuilder)? updates]) => + (new ResourceErrorBuilder()..update(updates))._build(); + + _$ResourceError._({required this.type, required this.message}) : super._() { + BuiltValueNullFieldError.checkNotNull(type, r'ResourceError', 'type'); + BuiltValueNullFieldError.checkNotNull(message, r'ResourceError', 'message'); + } + + @override + ResourceError rebuild(void Function(ResourceErrorBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + ResourceErrorBuilder toBuilder() => new ResourceErrorBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is ResourceError && + type == other.type && + message == other.message; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, type.hashCode); + _$hash = $jc(_$hash, message.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'ResourceError') + ..add('type', type) + ..add('message', message)) + .toString(); + } +} + +class ResourceErrorBuilder + implements Builder { + _$ResourceError? _$v; + + String? _type; + String? get type => _$this._type; + set type(String? type) => _$this._type = type; + + String? _message; + String? get message => _$this._message; + set message(String? message) => _$this._message = message; + + ResourceErrorBuilder(); + + ResourceErrorBuilder get _$this { + final $v = _$v; + if ($v != null) { + _type = $v.type; + _message = $v.message; + _$v = null; + } + return this; + } + + @override + void replace(ResourceError other) { + ArgumentError.checkNotNull(other, 'other'); + _$v = other as _$ResourceError; + } + + @override + void update(void Function(ResourceErrorBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + ResourceError build() => _build(); + + _$ResourceError _build() { + final _$result = _$v ?? + new _$ResourceError._( + type: BuiltValueNullFieldError.checkNotNull( + type, r'ResourceError', 'type'), + message: BuiltValueNullFieldError.checkNotNull( + message, r'ResourceError', 'message')); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/chopper/assets/examples/misc_examples/lib/built_value_serializers.dart b/chopper/assets/examples/misc_examples/lib/built_value_serializers.dart new file mode 100644 index 0000000..256983b --- /dev/null +++ b/chopper/assets/examples/misc_examples/lib/built_value_serializers.dart @@ -0,0 +1,14 @@ +library serializers; + +import 'package:built_value/serializer.dart'; + +import 'built_value_resource.dart'; + +part 'built_value_serializers.g.dart'; + +/// Collection of generated serializers for the built_value chat example. +@SerializersFor([ + Resource, + ResourceError, +]) +final Serializers serializers = _$serializers; diff --git a/chopper/assets/examples/misc_examples/lib/built_value_serializers.g.dart b/chopper/assets/examples/misc_examples/lib/built_value_serializers.g.dart new file mode 100644 index 0000000..1dd64f7 --- /dev/null +++ b/chopper/assets/examples/misc_examples/lib/built_value_serializers.g.dart @@ -0,0 +1,14 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'built_value_serializers.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +Serializers _$serializers = (new Serializers().toBuilder() + ..add(Resource.serializer) + ..add(ResourceError.serializer)) + .build(); + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/chopper/assets/examples/misc_examples/lib/json_decode_service.activator.g.dart b/chopper/assets/examples/misc_examples/lib/json_decode_service.activator.g.dart new file mode 100644 index 0000000..48ed629 --- /dev/null +++ b/chopper/assets/examples/misc_examples/lib/json_decode_service.activator.g.dart @@ -0,0 +1,10 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// Generator: WorkerGenerator 2.4.1 +// ************************************************************************** + +import 'json_decode_service.vm.g.dart'; + +/// Service activator for JsonDecodeService +final $JsonDecodeServiceActivator = $getJsonDecodeServiceActivator(); diff --git a/chopper/assets/examples/misc_examples/lib/json_decode_service.dart b/chopper/assets/examples/misc_examples/lib/json_decode_service.dart new file mode 100644 index 0000000..299b73c --- /dev/null +++ b/chopper/assets/examples/misc_examples/lib/json_decode_service.dart @@ -0,0 +1,20 @@ +/// This example uses https://github.com/d-markey/squadron_builder + +import 'dart:async'; +import 'dart:convert' show json; + +import 'package:squadron/squadron.dart'; +import 'package:squadron/squadron_annotations.dart'; + +import 'json_decode_service.activator.g.dart'; + +part 'json_decode_service.worker.g.dart'; + +@SquadronService( + // disable web to keep the number of generated files low for this example + web: false, +) +class JsonDecodeService { + @SquadronMethod() + Future jsonDecode(String source) async => json.decode(source); +} diff --git a/chopper/assets/examples/misc_examples/lib/json_decode_service.vm.g.dart b/chopper/assets/examples/misc_examples/lib/json_decode_service.vm.g.dart new file mode 100644 index 0000000..09d7309 --- /dev/null +++ b/chopper/assets/examples/misc_examples/lib/json_decode_service.vm.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// Generator: WorkerGenerator 2.4.1 +// ************************************************************************** + +import 'package:squadron/squadron.dart'; + +import 'json_decode_service.dart'; + +/// VM entry point for JsonDecodeService +void _start$JsonDecodeService(List command) => + run($JsonDecodeServiceInitializer, command, null); + +EntryPoint $getJsonDecodeServiceActivator() => _start$JsonDecodeService; diff --git a/chopper/assets/examples/misc_examples/lib/json_decode_service.worker.g.dart b/chopper/assets/examples/misc_examples/lib/json_decode_service.worker.g.dart new file mode 100644 index 0000000..18a33d7 --- /dev/null +++ b/chopper/assets/examples/misc_examples/lib/json_decode_service.worker.g.dart @@ -0,0 +1,290 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'json_decode_service.dart'; + +// ************************************************************************** +// Generator: WorkerGenerator 2.4.1 +// ************************************************************************** + +/// WorkerService class for JsonDecodeService +class _$JsonDecodeServiceWorkerService extends JsonDecodeService + implements WorkerService { + _$JsonDecodeServiceWorkerService() : super(); + + @override + Map get operations => _operations; + + late final Map _operations = { + _$jsonDecodeId: ($) => jsonDecode($.args[0]) + }; + + static const int _$jsonDecodeId = 1; +} + +/// Service initializer for JsonDecodeService +WorkerService $JsonDecodeServiceInitializer(WorkerRequest startRequest) => + _$JsonDecodeServiceWorkerService(); + +/// Operations map for JsonDecodeService +@Deprecated( + 'squadron_builder now supports "plain old Dart objects" as services. ' + 'Services do not need to derive from WorkerService nor do they need to mix in ' + 'with \$JsonDecodeServiceOperations anymore.') +mixin $JsonDecodeServiceOperations on WorkerService { + @override + // not needed anymore, generated for compatibility with previous versions of squadron_builder + Map get operations => WorkerService.noOperations; +} + +/// Worker for JsonDecodeService +class _$JsonDecodeServiceWorker extends Worker implements JsonDecodeService { + _$JsonDecodeServiceWorker({PlatformWorkerHook? platformWorkerHook}) + : super($JsonDecodeServiceActivator, + platformWorkerHook: platformWorkerHook); + + @override + Future jsonDecode(String source) => + send(_$JsonDecodeServiceWorkerService._$jsonDecodeId, args: [source]); + + final Object _detachToken = Object(); +} + +/// Finalizable worker wrapper for JsonDecodeService +class JsonDecodeServiceWorker implements _$JsonDecodeServiceWorker { + JsonDecodeServiceWorker({PlatformWorkerHook? platformWorkerHook}) + : _$w = + _$JsonDecodeServiceWorker(platformWorkerHook: platformWorkerHook) { + _finalizer.attach(this, _$w, detach: _$w._detachToken); + } + + final _$JsonDecodeServiceWorker _$w; + + static final Finalizer<_$JsonDecodeServiceWorker> _finalizer = + Finalizer<_$JsonDecodeServiceWorker>((w) { + try { + _finalizer.detach(w._detachToken); + w.stop(); + } catch (ex) { + // finalizers must not throw + } + }); + + @override + Future jsonDecode(String source) => _$w.jsonDecode(source); + + @override + List get args => _$w.args; + + @override + Channel? get channel => _$w.channel; + + @override + Duration get idleTime => _$w.idleTime; + + @override + bool get isStopped => _$w.isStopped; + + @override + int get maxWorkload => _$w.maxWorkload; + + @override + WorkerStat get stats => _$w.stats; + + @override + String get status => _$w.status; + + @override + int get totalErrors => _$w.totalErrors; + + @override + int get totalWorkload => _$w.totalWorkload; + + @override + Duration get upTime => _$w.upTime; + + @override + String get workerId => _$w.workerId; + + @override + int get workload => _$w.workload; + + @override + PlatformWorkerHook? get platformWorkerHook => _$w.platformWorkerHook; + + @override + Future start() => _$w.start(); + + @override + void stop() => _$w.stop(); + + @override + Future send(int command, + {List args = const [], + CancellationToken? token, + bool inspectRequest = false, + bool inspectResponse = false}) => + _$w.send(command, + args: args, + token: token, + inspectRequest: inspectRequest, + inspectResponse: inspectResponse); + + @override + Stream stream(int command, + {List args = const [], + CancellationToken? token, + bool inspectRequest = false, + bool inspectResponse = false}) => + _$w.stream(command, + args: args, + token: token, + inspectRequest: inspectRequest, + inspectResponse: inspectResponse); + + @override + Object get _detachToken => _$w._detachToken; + + @override + Map get operations => WorkerService.noOperations; +} + +/// Worker pool for JsonDecodeService +class _$JsonDecodeServiceWorkerPool extends WorkerPool + implements JsonDecodeService { + _$JsonDecodeServiceWorkerPool( + {ConcurrencySettings? concurrencySettings, + PlatformWorkerHook? platformWorkerHook}) + : super( + () => + JsonDecodeServiceWorker(platformWorkerHook: platformWorkerHook), + concurrencySettings: concurrencySettings); + + @override + Future jsonDecode(String source) => + execute((w) => w.jsonDecode(source)); + + final Object _detachToken = Object(); +} + +/// Finalizable worker pool wrapper for JsonDecodeService +class JsonDecodeServiceWorkerPool implements _$JsonDecodeServiceWorkerPool { + JsonDecodeServiceWorkerPool( + {ConcurrencySettings? concurrencySettings, + PlatformWorkerHook? platformWorkerHook}) + : _$p = _$JsonDecodeServiceWorkerPool( + concurrencySettings: concurrencySettings, + platformWorkerHook: platformWorkerHook) { + _finalizer.attach(this, _$p, detach: _$p._detachToken); + } + + final _$JsonDecodeServiceWorkerPool _$p; + + static final Finalizer<_$JsonDecodeServiceWorkerPool> _finalizer = + Finalizer<_$JsonDecodeServiceWorkerPool>((p) { + try { + _finalizer.detach(p._detachToken); + p.stop(); + } catch (ex) { + // finalizers must not throw + } + }); + + @override + Future jsonDecode(String source) => _$p.jsonDecode(source); + + @override + ConcurrencySettings get concurrencySettings => _$p.concurrencySettings; + + @override + Iterable get fullStats => _$p.fullStats; + + @override + int get maxConcurrency => _$p.maxConcurrency; + + @override + int get maxParallel => _$p.maxParallel; + + @override + int get maxSize => _$p.maxSize; + + @override + int get maxWorkers => _$p.maxWorkers; + + @override + int get maxWorkload => _$p.maxWorkload; + + @override + int get minWorkers => _$p.minWorkers; + + @override + int get pendingWorkload => _$p.pendingWorkload; + + @override + int get size => _$p.size; + + @override + Iterable get stats => _$p.stats; + + @override + bool get stopped => _$p.stopped; + + @override + int get totalErrors => _$p.totalErrors; + + @override + int get totalWorkload => _$p.totalWorkload; + + @override + int get workload => _$p.workload; + + @override + void cancel([Task? task, String? message]) => _$p.cancel(task, message); + + @override + FutureOr start() => _$p.start(); + + @override + int stop([bool Function(JsonDecodeServiceWorker worker)? predicate]) => + _$p.stop(predicate); + + @override + Object registerWorkerPoolListener( + void Function(JsonDecodeServiceWorker worker, bool removed) + listener) => + _$p.registerWorkerPoolListener(listener); + + @override + void unregisterWorkerPoolListener( + {void Function(JsonDecodeServiceWorker worker, bool removed)? + listener, + Object? token}) => + _$p.unregisterWorkerPoolListener(listener: listener, token: token); + + @override + Future execute(Future Function(JsonDecodeServiceWorker worker) task, + {PerfCounter? counter}) => + _$p.execute(task, counter: counter); + + @override + StreamTask scheduleStream( + Stream Function(JsonDecodeServiceWorker worker) task, + {PerfCounter? counter}) => + _$p.scheduleStream(task, counter: counter); + + @override + ValueTask scheduleTask( + Future Function(JsonDecodeServiceWorker worker) task, + {PerfCounter? counter}) => + _$p.scheduleTask(task, counter: counter); + + @override + Stream stream(Stream Function(JsonDecodeServiceWorker worker) task, + {PerfCounter? counter}) => + _$p.stream(task, counter: counter); + + @override + Object get _detachToken => _$p._detachToken; + + @override + Map get operations => WorkerService.noOperations; +} diff --git a/chopper/assets/examples/misc_examples/lib/json_serializable.chopper.dart b/chopper/assets/examples/misc_examples/lib/json_serializable.chopper.dart new file mode 100644 index 0000000..e616498 --- /dev/null +++ b/chopper/assets/examples/misc_examples/lib/json_serializable.chopper.dart @@ -0,0 +1,92 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'json_serializable.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +final class _$MyService extends MyService { + _$MyService([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final definitionType = MyService; + + @override + Future> getResource(String id) { + final Uri $url = Uri.parse('/resources/${id}/'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future>> getResources() { + final Uri $url = Uri.parse('/resources/all'); + final Map $headers = { + 'test': 'list', + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + return client.send, Resource>($request); + } + + @override + Future>> getMapResource(String id) { + final Uri $url = Uri.parse('/resources/'); + final Map $params = {'id': id}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send, Map>($request); + } + + @override + Future> getTypedResource() { + final Uri $url = Uri.parse('/resources/'); + final Map $headers = { + 'foo': 'bar', + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> newResource( + Resource resource, { + String? name, + }) { + final Uri $url = Uri.parse('/resources'); + final Map $headers = { + if (name != null) 'name': name, + }; + final $body = resource; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); + return client.send($request); + } +} diff --git a/chopper/assets/examples/misc_examples/lib/json_serializable.dart b/chopper/assets/examples/misc_examples/lib/json_serializable.dart new file mode 100644 index 0000000..4676bca --- /dev/null +++ b/chopper/assets/examples/misc_examples/lib/json_serializable.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:chopper/chopper.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'json_serializable.chopper.dart'; +part 'json_serializable.g.dart'; + +@JsonSerializable() +class Resource { + final String id; + final String name; + + Resource(this.id, this.name); + + static const fromJsonFactory = _$ResourceFromJson; + + Map toJson() => _$ResourceToJson(this); + + @override + String toString() => 'Resource{id: $id, name: $name}'; +} + +@JsonSerializable() +class ResourceError { + final String type; + final String message; + + ResourceError(this.type, this.message); + + static const fromJsonFactory = _$ResourceErrorFromJson; + + Map toJson() => _$ResourceErrorToJson(this); +} + +@ChopperApi(baseUrl: '/resources') +abstract class MyService extends ChopperService { + static MyService create([ChopperClient? client]) => _$MyService(client); + + @Get(path: '/{id}/') + Future getResource(@Path() String id); + + @Get(path: '/all', headers: {'test': 'list'}) + Future>> getResources(); + + @Get(path: '/') + Future> getMapResource(@Query() String id); + + @Get(path: '/', headers: {'foo': 'bar'}) + Future> getTypedResource(); + + @Post() + Future> newResource( + @Body() Resource resource, { + @Header() String? name, + }); +} diff --git a/chopper/assets/examples/misc_examples/lib/json_serializable.g.dart b/chopper/assets/examples/misc_examples/lib/json_serializable.g.dart new file mode 100644 index 0000000..bb2b4f9 --- /dev/null +++ b/chopper/assets/examples/misc_examples/lib/json_serializable.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'json_serializable.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Resource _$ResourceFromJson(Map json) => Resource( + json['id'] as String, + json['name'] as String, + ); + +Map _$ResourceToJson(Resource instance) => { + 'id': instance.id, + 'name': instance.name, + }; + +ResourceError _$ResourceErrorFromJson(Map json) => + ResourceError( + json['type'] as String, + json['message'] as String, + ); + +Map _$ResourceErrorToJson(ResourceError instance) => + { + 'type': instance.type, + 'message': instance.message, + }; diff --git a/chopper/assets/examples/misc_examples/pubspec.yaml b/chopper/assets/examples/misc_examples/pubspec.yaml new file mode 100644 index 0000000..735d046 --- /dev/null +++ b/chopper/assets/examples/misc_examples/pubspec.yaml @@ -0,0 +1,31 @@ +name: chopper_example +description: Example usage of the Chopper package +version: 0.0.5 +documentation: https://hadrien-lejard.gitbook.io/chopper/ +#author: Hadrien Lejard + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + chopper: + json_annotation: ^4.8.1 + built_value: + analyzer: ^5.13.0 + http: ^1.1.0 + built_collection: ^5.1.1 + squadron: ^5.1.3 + +dev_dependencies: + build_runner: ^2.4.6 + chopper_generator: + json_serializable: ^6.7.1 + built_value_generator: ^8.6.1 + lints: ^2.1.1 + squadron_builder: ^2.4.1 + +dependency_overrides: + chopper: + path: ../chopper + chopper_generator: + path: ../chopper_generator diff --git a/chopper/bin/main.dart b/chopper/bin/main.dart new file mode 100644 index 0000000..56ee88b --- /dev/null +++ b/chopper/bin/main.dart @@ -0,0 +1,11 @@ +import 'package:dash_agent/dash_agent.dart'; + +import 'package:chopper/agent.dart'; +import 'package:chopper/data_source/github_issues_data_source_helper.dart'; + +Future main() async { + final openIssuesLinks = await generateGitIssuesLink(); + final closedIssuesLinks = await generateGitIssuesLink(closedIssues: true); + await processAgent( + ChopperAgent(issuesLinks: [...closedIssuesLinks, ...openIssuesLinks])); +} diff --git a/chopper/lib/agent.dart b/chopper/lib/agent.dart new file mode 100644 index 0000000..37f91a6 --- /dev/null +++ b/chopper/lib/agent.dart @@ -0,0 +1,44 @@ +import 'package:chopper/commands/debug.dart'; +import 'package:chopper/commands/generate.dart'; +import 'package:chopper/commands/test.dart'; +import 'package:dash_agent/configuration/metadata.dart'; +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/configuration/dash_agent.dart'; +import 'data_source/data_sources.dart'; + +class ChopperAgent extends AgentConfiguration { + final List issuesLinks; + final IssuesDataSource issueSource; + final docsSource = DocsDataSource(); + final exampleSource = ExampleDataSource(); + + ChopperAgent({required this.issuesLinks}) + : issueSource = IssuesDataSource(issuesLinks); + + @override + List get registerDataSources => + [docsSource, exampleSource, issueSource]; + + @override + List get registerSupportedCommands => [ + GenerateCommand( + exampleDataSource: exampleSource, docDataSource: docsSource), + DebugCommand( + exampleDataSource: exampleSource, + docDataSource: docsSource, + issueDataSource: issueSource), + TestCommand() + ]; + + @override + Metadata get metadata => Metadata( + name: 'Chopper', + tags: ['flutter', 'dart', 'flutter favorite', 'flutter package']); + + @override + String get registerSystemPrompt => + '''You are a Chopper Integration assistant inside user's IDE. Chopper is an http client generator for Dart and Flutter using source_gen and inspired by Retrofit. + + You will be provided with latest docs and examples relevant to user queries and you have to help user with any questions they have related to Chopper. Output code and code links wherever required and answer "I don't know" if the user query is not covered in the docs provided to you'''; +} diff --git a/chopper/lib/commands/debug.dart b/chopper/lib/commands/debug.dart new file mode 100644 index 0000000..4d25b71 --- /dev/null +++ b/chopper/lib/commands/debug.dart @@ -0,0 +1,71 @@ +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/steps/steps.dart'; +import 'package:dash_agent/variables/dash_input.dart'; +import 'package:dash_agent/variables/dash_output.dart'; + +class DebugCommand extends Command { + DebugCommand( + {required this.exampleDataSource, + required this.docDataSource, + required this.issueDataSource}); + final DataSource exampleDataSource; + final DataSource docDataSource; + final DataSource issueDataSource; + final issueDescription = StringInput('Issue Description'); + final codeReference1 = CodeInput('Reference', optional: true); + final codeReference2 = CodeInput('Reference', optional: true); + + @override + String get intent => 'Debug Chopper Code'; + + @override + List get registerInputs => + [issueDescription, codeReference1, codeReference2]; + + @override + String get slug => 'debug'; + + @override + List get steps { + final docReferences = MatchDocumentOuput(); + final issueReferences = MatchDocumentOuput(); + final resultOutput = PromptOutput(); + return [ + MatchDocumentStep( + query: '$issueDescription', + dataSources: [exampleDataSource, docDataSource], + output: docReferences), + MatchDocumentStep( + query: '$issueDescription', + dataSources: [issueDataSource], + output: issueReferences), + PromptQueryStep( + prompt: + '''Assist in debugging the code written using Flutter's Chopper package based on the information shared. + Issue Description: $issueDescription + + Code References: + ```dart + // code reference 1 + $codeReference1 + + // code reference 2 + $codeReference2 + ``` + + Official Chopper Documentation References: + $docReferences + + GitHub Issue Messages: + $issueReferences + ''', + promptOutput: resultOutput), + AppendToChatStep(value: '$resultOutput') + ]; + } + + @override + String get textFieldLayout => + 'Share the following details to understand your issue better: $issueDescription \n\nOptionally attach any references: $codeReference1 $codeReference2'; +} diff --git a/chopper/lib/commands/generate.dart b/chopper/lib/commands/generate.dart new file mode 100644 index 0000000..1ceea87 --- /dev/null +++ b/chopper/lib/commands/generate.dart @@ -0,0 +1,63 @@ +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/steps/steps.dart'; +import 'package:dash_agent/variables/dash_input.dart'; +import 'package:dash_agent/variables/dash_output.dart'; + +class GenerateCommand extends Command { + GenerateCommand( + {required this.exampleDataSource, required this.docDataSource}); + final DataSource exampleDataSource; + final DataSource docDataSource; + final generateInstructions = StringInput('Generate Instructions'); + final codeReference1 = CodeInput('Reference', optional: true); + final codeReference2 = CodeInput('Reference', optional: true); + + @override + String get intent => 'Generate Chopper Code based on request'; + + @override + List get registerInputs => + [generateInstructions, codeReference1, codeReference2]; + + @override + String get slug => 'generate'; + + @override + List get steps { + final docReferences = MatchDocumentOuput(); + final resultOutput = PromptOutput(); + return [ + MatchDocumentStep( + query: + 'examples/instructions of writing code using chopper - $generateInstructions $codeReference1 $codeReference2.', + dataSources: [exampleDataSource, docDataSource], + output: docReferences), + PromptQueryStep( + prompt: + '''Generate code using chopper based on the instructions and relevant code references/examples shared. + Instructions: $generateInstructions + + Code References: + ```dart + // code reference 1 + $codeReference1 + + // code reference 2 + $codeReference2 + ``` + + Documentation or examples of the chopper package for reference: + $docReferences + + Generate the complete code as per user's instruction. + ''', + promptOutput: resultOutput), + AppendToChatStep(value: '$resultOutput') + ]; + } + + @override + String get textFieldLayout => + 'Generate the code using chopper: $generateInstructions \n\nOptionally attach any references: $codeReference1 $codeReference2'; +} diff --git a/chopper/lib/commands/test.dart b/chopper/lib/commands/test.dart new file mode 100644 index 0000000..0246797 --- /dev/null +++ b/chopper/lib/commands/test.dart @@ -0,0 +1,104 @@ +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/steps/steps.dart'; +import 'package:dash_agent/variables/dash_input.dart'; +import 'package:dash_agent/variables/dash_output.dart'; + +class TestCommand extends Command { + TestCommand(); + final primaryObject = CodeInput('Test Object'); + final testInstructions = StringInput('Instructions', optional: true); + final referenceObject1 = CodeInput('Reference', optional: true); + final referenceObject2 = CodeInput('Reference', optional: true); + final referenceObject3 = CodeInput('Reference', optional: true); + @override + String get intent => 'Write tests for your chopper related code'; + + @override + List get registerInputs => [ + primaryObject, + testInstructions, + referenceObject1, + referenceObject2, + referenceObject3 + ]; + + @override + String get slug => 'test'; + + @override + String get textFieldLayout => + 'Generate test for your chopper-related code $primaryObject with $testInstructions\n\nOptionally attach any supporting code: $referenceObject1 $referenceObject2 $referenceObject3'; + + @override + List get steps { + final testOutput = PromptOutput(); + return [ + PromptQueryStep( + prompt: + '''Write tests for the Flutter Chopper related code with instructions - $testInstructions. + +Code: +```dart +$primaryObject +``` + +Here are some contextual code or references provided by the user: +```dart +// reference 1 +$referenceObject1 + +// reference 2 +$referenceObject2 + +// reference 3 +$referenceObject3 +``` + +Sample Unit test unrelated to the above scenerio as a reference: + +```dart +import 'dart:convert'; + +import 'package:chopper/chopper.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; + +part 'api_service.chopper.dart'; + +@ChopperApi() +abstract class ApiService extends ChopperService { +static ApiService create() { +final client = ChopperClient( +client: MockClient((request) async { + Map result = mockData[request.url.path]?.firstWhere((mockQueryParams) { + if (mockQueryParams['id'] == request.url.queryParameters['id']) return true; + return false; + }, orElse: () => null); + if (result == null) { + return http.Response( + json.encode({'error': "not found"}), 404); + } + return http.Response(json.encode(result), 200); +}), +baseUrl: Uri.parse('https://mysite.com/api'), +services: [ + _\$ApiService(), +], +converter: JsonConverter(), +errorConverter: JsonConverter(), +); +return _\$ApiService(client); +} + +@Get(path: "/get") +Future get(@Query() String url); +} +``` + +Generate the test for the user's code based on the instructions. +''', + promptOutput: testOutput), + AppendToChatStep(value: '$testOutput') + ]; + } +} diff --git a/chopper/lib/data_source/data_sources.dart b/chopper/lib/data_source/data_sources.dart new file mode 100644 index 0000000..0159d55 --- /dev/null +++ b/chopper/lib/data_source/data_sources.dart @@ -0,0 +1,67 @@ +import 'dart:io'; + +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/data/objects/project_data_object.dart'; +import 'package:dash_agent/data/objects/file_data_object.dart'; +import 'package:dash_agent/data/objects/web_data_object.dart'; + +class DocsDataSource extends DataSource { + @override + List get fileObjects => []; + + @override + List get projectObjects => []; + + @override + List get webObjects => [ + WebDataObject.fromWebPage('https://hadrien-lejard.gitbook.io/chopper'), + WebDataObject.fromWebPage( + 'https://hadrien-lejard.gitbook.io/chopper/getting-started'), + WebDataObject.fromWebPage( + 'https://hadrien-lejard.gitbook.io/chopper/requests'), + WebDataObject.fromWebPage( + 'https://hadrien-lejard.gitbook.io/chopper/interceptors'), + WebDataObject.fromWebPage( + 'https://hadrien-lejard.gitbook.io/chopper/converters/converters'), + WebDataObject.fromWebPage( + 'https://hadrien-lejard.gitbook.io/chopper/converters/built-value-converter'), + WebDataObject.fromWebPage( + 'https://hadrien-lejard.gitbook.io/chopper/faq') + ]; +} + +class ExampleDataSource extends DataSource { + @override + List get fileObjects => [ + FileDataObject.fromDirectory(Directory('assets/examples'), + includePaths: true, + regex: RegExp(r'(\.dart$)'), + relativeTo: + '/Users/yogesh/Development/org.welltested/default_agents/chopper/assets/examples') + ]; + + @override + List get projectObjects => []; + + @override + List get webObjects => []; +} + +class IssuesDataSource extends DataSource { + final List issuesLinks; + + IssuesDataSource(this.issuesLinks); + + @override + List get fileObjects => []; + + @override + List get projectObjects => []; + + @override + List get webObjects => + [for (final issueUrl in issuesLinks) WebDataObject.fromWebPage(issueUrl)]; +} + + + diff --git a/chopper/lib/data_source/github_issues_data_source_helper.dart b/chopper/lib/data_source/github_issues_data_source_helper.dart new file mode 100644 index 0000000..f0c88fc --- /dev/null +++ b/chopper/lib/data_source/github_issues_data_source_helper.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +Future> generateGitIssuesLink({bool closedIssues = false}) async { + var issueApiUrl = 'https://api.github.com/repos/lejard-h/chopper/issues'; + + if (closedIssues) { + issueApiUrl = '$issueApiUrl?state=closed'; + } + + final response = await http.get(Uri.parse(issueApiUrl)); + + if (response.statusCode != 200) { + throw 'Failed to get the github issues. Response received: ${response.statusCode} ${response.body}'; + } + + final issueUrls = []; + final responseBody = jsonDecode(response.body).cast(); + + for (final issue in responseBody) { + issueUrls.add(issue['html_url'] as String); + } + + return issueUrls; +} diff --git a/chopper/pubspec.lock b/chopper/pubspec.lock new file mode 100644 index 0000000..9518d70 --- /dev/null +++ b/chopper/pubspec.lock @@ -0,0 +1,413 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + url: "https://pub.dev" + source: hosted + version: "1.7.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + dash_agent: + dependency: "direct main" + description: + name: dash_agent + sha256: "5f647003933a979768cad1453c2a1401a2ec3f844e94ed9ba4eb57e6cdd23be9" + url: "https://pub.dev" + source: hosted + version: "0.3.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + http: + dependency: "direct main" + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + lints: + dependency: "direct dev" + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + meta: + dependency: transitive + description: + name: meta + sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" + url: "https://pub.dev" + source: hosted + version: "1.14.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: d72b538180efcf8413cd2e4e6fcc7ae99c7712e0909eb9223f9da6e6d0ef715f + url: "https://pub.dev" + source: hosted + version: "1.25.4" + test_api: + dependency: transitive + description: + name: test_api + sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" + url: "https://pub.dev" + source: hosted + version: "0.7.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: "4d070a6bc36c1c4e89f20d353bfd71dc30cdf2bd0e14349090af360a029ab292" + url: "https://pub.dev" + source: hosted + version: "0.6.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + url: "https://pub.dev" + source: hosted + version: "14.2.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.3.4 <4.0.0" diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml new file mode 100644 index 0000000..dc6a387 --- /dev/null +++ b/chopper/pubspec.yaml @@ -0,0 +1,16 @@ +name: chopper +description: A sample command-line application. +version: 1.1.1 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.3.4 + +# Add regular dependencies here. +dependencies: + dash_agent: ^0.3.0 + http: ^1.2.1 + +dev_dependencies: + lints: ^3.0.0 + test: ^1.24.0 diff --git a/chopper/test/agent_test.dart b/chopper/test/agent_test.dart new file mode 100644 index 0000000..2ba7e75 --- /dev/null +++ b/chopper/test/agent_test.dart @@ -0,0 +1,14 @@ +import 'package:chopper/agent.dart'; +import 'package:chopper/data_source/github_issues_data_source_helper.dart'; +import 'package:dash_agent/dash_agent.dart'; + +import 'package:test/test.dart'; + +void main() { + test('test agent config', () async { + final openIssuesLinks = await generateGitIssuesLink(); + final closedIssuesLinks = await generateGitIssuesLink(closedIssues: true); + await processAgent( + ChopperAgent(issuesLinks: [...closedIssuesLinks, ...openIssuesLinks])); + }); +} diff --git a/flame/CHANGELOG.md b/flame/CHANGELOG.md new file mode 100644 index 0000000..26da800 --- /dev/null +++ b/flame/CHANGELOG.md @@ -0,0 +1,12 @@ +## 1.1.0 + +- Updated agent to enable commandless mode + +## 1.0.1 + +- Updated the ask and test command +- Updated the issue datasource + +## 1.0.0 + +- Initial version. diff --git a/flame/README.md b/flame/README.md new file mode 100644 index 0000000..d82488a --- /dev/null +++ b/flame/README.md @@ -0,0 +1,3 @@ +# Agent Reamde File + +This is a sample readme file for agent. You add description about the agent and any other instruction or information. diff --git a/flame/analysis_options.yaml b/flame/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/flame/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/flame/assets/documents/flame_dart_api_doc.xml b/flame/assets/documents/flame_dart_api_doc.xml new file mode 100644 index 0000000..9fbeed2 --- /dev/null +++ b/flame/assets/documents/flame_dart_api_doc.xml @@ -0,0 +1,2182 @@ + + + + + + + https://pub.dev/documentation/flame/latest/ + 2024-05-08T11:15:03+00:00 + 1.00 + + + https://pub.dev/documentation/flame/latest/cache/cache-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/camera/camera-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/collisions/collisions-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/components/components-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/debug/debug-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/devtools/devtools-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/effects/effects-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/events/events-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/experimental/experimental-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/extensions/extensions-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/flame/flame-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/game/game-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/geometry/geometry-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/image_composition/image_composition-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/input/input-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/layers/layers-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/layout/layout-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/math/math-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/palette/palette-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/parallax/parallax-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/particles/particles-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/rendering/rendering-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/sprite/sprite-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/text/text-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/timer/timer-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/widgets/widgets-library.html + 2024-05-08T11:15:03+00:00 + 0.80 + + + https://pub.dev/documentation/flame/latest/index.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/cache/AssetsCache-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/cache/Images-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/cache/MemoryCache-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/cache/ValueCache-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/camera/BoundedPositionBehavior-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/camera/CameraComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/camera/World-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/camera/CircularViewport-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/camera/FixedAspectRatioViewport-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/camera/FixedResolutionViewport-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/camera/FixedSizeViewport-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/camera/FollowBehavior-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/camera/MaxViewport-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/camera/Viewfinder-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/camera/Viewport-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/Broadphase-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/Hitbox-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/CircleHitbox-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/CollisionDetection-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/CollisionDetectionCompletionNotifier-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/CollisionProspect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/CollisionTypeNotifier-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/CompositeHitbox-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/PositionComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/ShapeHitbox-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/PolygonHitbox-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/ProspectPool-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/QuadTree-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/QuadTreeBroadphase-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/QuadTreeCollisionDetection-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/QuadTreeNode-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/QuadTreeNodeDebugInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/RaycastResult-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/RectangleHitbox-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/ScreenHitbox-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/FlameGame-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/StandardCollisionDetection-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/Sweep-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/CollisionType.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/CollisionCallbacks-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/CollisionPassthrough-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/GenericCollisionCallbacks-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/HasQuadTreeCollisionDetection-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/ShapeComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/CollisionCallback.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Vector2-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/CollisionEndCallback.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/ExternalBroadphaseCheck.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/collisions/ExternalMinDistanceCheck.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Aabb2-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Aabb3-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/AdvancedButtonComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/Anchor-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/Block-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/CircleComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ClipComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/Component-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ComponentKey-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ComponentSet-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ComponentsNotifier-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/CoordinateTransform-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/CustomPainterComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/FpsComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/FpsTextComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/TextRenderer-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/TextComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Frustum-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Plane-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/IntersectionResult-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/IsometricTileMapComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/JoystickComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/KeyboardListenerComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/KeyboardHandler-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/HasKeyboardHandlerComponents-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Matrix2-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Matrix3-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/widgets/NineTileBox-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/NineTileBoxComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Obb3-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ParallaxComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ParticleSystemComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/Particle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/PolygonComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Quad-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Quaternion-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Ray-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/RectangleComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ScrollTextBoxComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/SimplexNoise-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/SpawnComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Sphere-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/sprite/Sprite-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/sprite/SpriteAnimation-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/SpriteAnimationComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/sprite/SpriteAnimationData-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/sprite/SpriteAnimationFrame-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/sprite/SpriteAnimationFrameData-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/SpriteAnimationGroupComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/SpriteBatchComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/SpriteComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/SpriteGroupComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/TextBoxComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/TextBoxConfig-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/TextElementComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/TextPaint-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/TextPainterTextElement-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/timer/Timer-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/TimerComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ToggleButtonComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Triangle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Vector-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Vector3-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/Vector4-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ButtonState.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ChildrenChangeType.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/JoystickDirection.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/PositionType.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ComponentViewportMargin-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/GestureHitboxes-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/HasAncestor-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/HasCollisionDetection-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/HasDecorator-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/rendering/Decorator-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/HasGameRef-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/HasGameReference-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/HasGenericCollisionDetection-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/HasPaint-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/HasTimeScale-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/HasVisibility-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/HasWorldReference-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/IgnoreEvents-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/Notifier-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ParentIsA-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/SingleChildParticle-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/Snapshot-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ParallaxComponentExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/Vector2Extension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/degrees2Radians-constant.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/radians2Degrees-constant.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/absoluteError.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/buildPlaneVectors.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/catmullRom.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/cross2.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/cross2A.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/cross2B.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/cross3.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/degrees.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/dot2.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/dot3.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/makeFrustumMatrix.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/makeInfiniteMatrix.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/makeOrthographicMatrix.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/makePerspectiveMatrix.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/makePlaneProjection.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/makePlaneReflection.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/makeViewMatrix.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/mix.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/pickRay.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/radians.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/relativeError.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/setFrustumMatrix.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/setInfiniteMatrix.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/setModelMatrix.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/setOrthographicMatrix.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/setPerspectiveMatrix.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/setRotationMatrix.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/setViewMatrix.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/smoothStep.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/unproject.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ComponentSetFactory.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/KeyHandlerCallback.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/components/ShapeBuilder.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/experimental/Shape-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/debug/ChildCounterComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/debug/DevToolsService-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/debug/TimeTrackComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/devtools/ComponentTreeNode-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/AnchorByEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/AnchorEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/AnchorProvider-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/AnchorToEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/AngleProvider-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/ColorEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/ComponentEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/CurvedEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/DelayedEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/DurationEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/Effect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/EffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/GlowEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/InfiniteEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/LinearEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/MoveAlongPathEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/MoveByEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/MoveEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/MoveToEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/OpacityEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/OpacityProvider-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/PauseEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/PositionProvider-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/RandomEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/RandomVariable-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/ReadOnlyAngleProvider-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/ReadOnlyPositionProvider-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/ReadOnlyScaleProvider-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/ReadOnlySizeProvider-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/RemoveEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/RepeatedEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/ReverseCurvedEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/ReverseLinearEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/RotateEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/ScaleEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/ScaleProvider-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/SequenceEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/SequenceEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/SineEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/SizeEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/SizeProvider-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/SpeedEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/Transform2DEffect-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/Transform2D-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/ZigzagEffectController-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/effects/EffectTarget-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/DoubleTapCancelEvent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/DoubleTapDownEvent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/DoubleTapEvent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/DragCancelEvent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/DragDownInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/DragEndEvent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/DragEndInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/DragStartEvent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/DragStartInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/DragUpdateEvent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/DragUpdateInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/ForcePressInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/HardwareKeyboardDetector-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/LongPressEndInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/LongPressMoveUpdateInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/LongPressStartInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/MultiDragListener-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/MultiTapListener-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/PointerHoverInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/PointerMoveEvent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/PointerScrollInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/PositionInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/ScaleEndInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/ScaleStartInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/ScaleUpdateInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/TapCancelEvent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/TapDownEvent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/TapDownInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/TapUpEvent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/TapUpInfo-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/DoubleTapCallbacks-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/DoubleTapDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/DragCallbacks-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/ForcePressDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/KeyboardEvents-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/HorizontalDragDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/HoverCallbacks-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/Game-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/LongPressDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/MouseMovementDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/MultiTouchDragDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/MultiTouchTapDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/PanDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/PointerMoveCallbacks-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/ScaleDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/ScrollDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/SecondaryTapDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/events/TapCallbacks-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/TapDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/TertiaryTapDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/VerticalDragDetector-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/experimental/Circle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/experimental/Polygon-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/experimental/Rectangle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/experimental/RoundedRectangle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/Aabb2Extension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/CanvasExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/ColorExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/DoubleExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/ImageExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/ListExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/Matrix4Extension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/OffsetExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/PaintExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/PictureExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/RectangleExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/RectExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/extensions/SizeExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/flame/Flame-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/GameWidget-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/GameWidgetState-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/NotifyingVector2-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/OverlayRoute-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/Route-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/RouterComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/ValueRoute-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/HasPerformanceTracker-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/SingleGameInstance-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/GameErrorWidgetBuilder.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/GameFactory.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/GameLoadingWidgetBuilder.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/game/OverlayWidgetBuilder.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/CircleCircleIntersections-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/CirclePolygonIntersections-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/Intersections-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/Line-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/LineSegment-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/PolygonPolygonIntersections-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/Ray2-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/PolygonRayIntersection-mixin.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/tau-constant.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/geometry/intersections.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/image_composition/ImageComposition-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/ButtonComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/HudButtonComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/HudMarginComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/SpriteButtonComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/input/ButtonState.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/layers/DynamicLayer-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/layers/Layer-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/layers/LayerProcessor-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/layers/PreRenderedLayer-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/layers/ShadowProcessor-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/layout/AlignComponent-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/math/discriminantEpsilon-constant.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/math/randomFallback.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/math/solveCubic.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/math/solveQuadratic.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/palette/BasicPalette-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/palette/PaletteEntry-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/parallax/Parallax-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/parallax/ParallaxAnimation-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/parallax/ParallaxAnimationData-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/parallax/ParallaxData-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/parallax/ParallaxImage-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/parallax/ParallaxImageData-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/parallax/ParallaxLayer-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/parallax/ParallaxRenderer-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/parallax/LayerFill.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/parallax/ParallaxExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/AcceleratedParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/CircleParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/ComponentParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/ComposedParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/ComputedParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/CurvedParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/ImageParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/MovingParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/PaintParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/RotatingParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/ScaledParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/ScalingParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/SpriteAnimationParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/SpriteParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/TranslatedParticle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/ParticleGenerator.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/particles/ParticleRenderDelegate.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/rendering/PaintDecorator-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/rendering/Rotate3DDecorator-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/rendering/Shadow3DDecorator-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/rendering/Transform2DDecorator-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/sprite/BatchItem-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/sprite/SpriteAnimationTicker-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/sprite/SpriteBatch-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/sprite/SpriteSheet-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/sprite/SpriteBatchExtension.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/BackgroundStyle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/BlockElement-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/TextElement-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/BlockNode-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/BlockStyle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/BoldTextNode-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/ColumnNode-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/DocumentRoot-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/DocumentStyle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/FlameTextStyle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/Glyph-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/SpriteFont-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/GroupTextNode-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/HeaderNode-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/InlineTextElement-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/InlineTextNode-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/InlineTextStyle-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/ItalicTextNode-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/LineMetrics-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/ParagraphNode-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/PlainTextNode-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/RectElement-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/RRectElement-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/SpriteFontRenderer-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/SpriteFontTextElement-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/TextBlockNode-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/TextNode-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/text/TextRendererFactory-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/widgets/ComponentsNotifierBuilder-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/widgets/InternalNineTileBox-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/widgets/InternalSpriteAnimationWidget-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/widgets/InternalSpriteButton-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/widgets/InternalSpriteWidget-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/widgets/NineTileBoxWidget-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/widgets/SpriteAnimationWidget-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/widgets/SpriteButton-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + https://pub.dev/documentation/flame/latest/widgets/SpriteWidget-class.html + 2024-05-08T11:15:03+00:00 + 0.64 + + + + \ No newline at end of file diff --git a/flame/assets/documents/flame_doc.xml b/flame/assets/documents/flame_doc.xml new file mode 100644 index 0000000..fa1ef33 --- /dev/null +++ b/flame/assets/documents/flame_doc.xml @@ -0,0 +1,697 @@ + + + + + + + https://docs.flame-engine.org/latest/ + 2024-05-06T13:47:33+00:00 + 1.00 + + + https://docs.flame-engine.org/latest/flame/flame.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/structure.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/game_widget.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/game.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/components.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/router.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/platforms.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/collision_detection.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/effects.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/camera_component.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/inputs/inputs.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/inputs/drag_events.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/inputs/gesture_input.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/inputs/keyboard_input.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/inputs/other_inputs.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/inputs/tap_events.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/inputs/pointer_events.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/inputs/hardware_keyboard_detector.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/rendering/rendering.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/rendering/palette.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/rendering/decorators.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/rendering/images.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/rendering/layers.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/rendering/particles.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/rendering/text_rendering.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/layout/layout.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/layout/align_component.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/overlays.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/other/other.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/other/debug.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/other/util.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/flame/other/widgets.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/bridge_packages.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_audio/flame_audio.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_audio/audio.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_audio/bgm.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_audio/audio_pool.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_bloc/flame_bloc.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_bloc/bloc.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_bloc/bloc_components.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_fire_atlas/flame_fire_atlas.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_fire_atlas/fire_atlas.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_forge2d/flame_forge2d.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_forge2d/forge2d.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_forge2d/joints.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_isolate/flame_isolate.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_isolate/isolate.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_lottie/flame_lottie.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_network_assets/flame_network_assets.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_oxygen/flame_oxygen.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_rive/flame_rive.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_rive/rive.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_riverpod/flame_riverpod.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_riverpod/riverpod.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_riverpod/component.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_riverpod/widget.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_splash_screen/flame_splash_screen.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_spine/flame_spine.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_svg/flame_svg.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_svg/svg.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_tiled/flame_tiled.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_tiled/tiled.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/bridge_packages/flame_tiled/layers.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/other_modules.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/jenny.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/language.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/nodes.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/lines.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/options.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/commands/commands.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/commands/character.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/commands/declare.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/commands/if.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/commands/jump.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/commands/local.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/commands/set.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/commands/stop.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/commands/visit.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/commands/wait.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/commands/user_defined_commands.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/expressions/expressions.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/expressions/variables.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/expressions/operators.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/expressions/functions/functions.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/expressions/functions/random.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/expressions/functions/numeric.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/expressions/functions/type.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/expressions/functions/misc.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/language/markup.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/jenny_runtime.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/character.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/character_storage.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/command_storage.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/dialogue_choice.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/dialogue_line.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/dialogue_option.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/dialogue_runner.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/dialogue_view.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/function_storage.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/markup_attribute.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/node.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/user_defined_command.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/variable_storage.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/jenny/runtime/yarn_project.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/oxygen/oxygen.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/other_modules/oxygen/components.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/tutorials.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/bare_flame_game.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/klondike/klondike.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/klondike/step1.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/klondike/step2.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/klondike/step3.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/klondike/step4.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/klondike/step5.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/platformer/platformer.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/platformer/step_1.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/platformer/step_2.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/platformer/step_3.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/platformer/step_4.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/platformer/step_5.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/platformer/step_6.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/platformer/step_7.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/space_shooter/space_shooter.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/space_shooter/step_1.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/space_shooter/step_2.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/space_shooter/step_3.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/space_shooter/step_4.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/space_shooter/step_5.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/tutorials/space_shooter/step_6.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/development/development.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/development/contributing.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/development/documentation.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/development/style_guide.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/development/testing_guide.html + 2024-05-06T13:47:32+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/resources/resources.html + 2024-05-06T13:47:33+00:00 + 0.80 + + + https://docs.flame-engine.org/latest/index.html + 2024-05-06T13:47:33+00:00 + 0.64 + + + + \ No newline at end of file diff --git a/flame/assets/examples/official/dashbook_example/README.md b/flame/assets/examples/official/dashbook_example/README.md new file mode 100644 index 0000000..724ce83 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/README.md @@ -0,0 +1,29 @@ +[![Powered by Flame](https://img.shields.io/badge/Powered%20by-%F0%9F%94%A5-orange.svg)](https://flame-engine.org) +[![Discord](https://img.shields.io/discord/509714518008528896.svg)](https://discord.gg/pxrBmy4) + + +# Flame Examples + +This is a set of small examples showcasing specific features of the Flame Engine; it's a great +source of learning how to use certain things. +[See it live here](https://examples.flame-engine.org/). + +This app is composed of a main menu in which you can select one of the examples and play with it. +Each example is a standalone game and is contained within its own file, so you can easily checkout +the code and see how it works. + +For a very simple, but complete game in Flame, check the +[example folder inside the Flame package](https://github.com/flame-engine/flame/tree/main/packages/flame/example). + + +## Help + +If you have questions about this: + +- Check the source code, the examples are meant to be simple, short, and easy to read. +- Check our extensive documentation, links to which can be found + [on the main repo](https://github.com/flame-engine/flame) (faq, docs folder, code/api docs, + tutorials, flame-awesome). +- Join [Blue Fire's Discord](https://discord.gg/5unKpdQD78), we have a #flame channel where you can + find lots of people to help and get help from. +- Use the `flame` tag on StackOverflow. diff --git a/flame/assets/examples/official/dashbook_example/lib/commons/commons.dart b/flame/assets/examples/official/dashbook_example/lib/commons/commons.dart new file mode 100644 index 0000000..cb9432e --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/commons/commons.dart @@ -0,0 +1,6 @@ +String baseLink(String path) { + const basePath = + 'https://github.com/flame-engine/flame/blob/main/examples/lib/stories/'; + + return '$basePath$path'; +} diff --git a/flame/assets/examples/official/dashbook_example/lib/commons/ember.dart b/flame/assets/examples/official/dashbook_example/lib/commons/ember.dart new file mode 100644 index 0000000..e8309f5 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/commons/ember.dart @@ -0,0 +1,25 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:meta/meta.dart'; + +class Ember extends SpriteAnimationComponent + with HasGameReference { + Ember({super.position, Vector2? size, super.priority, super.key}) + : super( + size: size ?? Vector2.all(50), + anchor: Anchor.center, + ); + + @mustCallSuper + @override + Future onLoad() async { + animation = await game.loadSpriteAnimation( + 'animations/ember.png', + SpriteAnimationData.sequenced( + amount: 3, + textureSize: Vector2.all(16), + stepTime: 0.15, + ), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/main.dart b/flame/assets/examples/official/dashbook_example/lib/main.dart new file mode 100644 index 0000000..b7ac835 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/main.dart @@ -0,0 +1,107 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/platform/stub_provider.dart' + if (dart.library.html) 'platform/web_provider.dart'; +import 'package:examples/stories/animations/animations.dart'; +import 'package:examples/stories/bridge_libraries/audio/audio.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/flame_forge2d.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart'; +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'; +import 'package:examples/stories/collision_detection/collision_detection.dart'; +import 'package:examples/stories/components/components.dart'; +import 'package:examples/stories/effects/effects.dart'; +import 'package:examples/stories/experimental/experimental.dart'; +import 'package:examples/stories/games/games.dart'; +import 'package:examples/stories/image/image.dart'; +import 'package:examples/stories/input/input.dart'; +import 'package:examples/stories/layout/layout.dart'; +import 'package:examples/stories/parallax/parallax.dart'; +import 'package:examples/stories/rendering/rendering.dart'; +import 'package:examples/stories/sprites/sprites.dart'; +import 'package:examples/stories/structure/structure.dart'; +import 'package:examples/stories/svg/svg.dart'; +import 'package:examples/stories/system/system.dart'; +import 'package:examples/stories/tiled/tiled.dart'; +import 'package:examples/stories/utils/utils.dart'; +import 'package:examples/stories/widgets/widgets.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +void main() { + final page = PageProviderImpl().getPage(); + + final routes = { + 'constant_volume_joint': ConstantVolumeJointExample.new, + 'distance_joint': DistanceJointExample.new, + 'friction_joint': FrictionJointExample.new, + 'gear_joint': GearJointExample.new, + 'motor_joint': MotorJointExample.new, + 'mouse_joint': MouseJointExample.new, + 'pulley_joint': PulleyJointExample.new, + 'prismatic_joint': PrismaticJointExample.new, + 'revolute_joint': RevoluteJointExample.new, + 'rope_joint': RopeJointExample.new, + 'weld_joint': WeldJointExample.new, + }; + final game = routes[page]?.call(); + if (game != null) { + runApp(GameWidget(game: game)); + } else { + runAsDashbook(); + } +} + +void runAsDashbook() { + final dashbook = Dashbook( + title: 'Flame Examples', + theme: ThemeData.dark(), + ); + + // Some small sample games + addGameStories(dashbook); + + // Show some different ways of structuring games + addStructureStories(dashbook); + + // Feature examples + addAudioStories(dashbook); + addAnimationStories(dashbook); + addCameraAndViewportStories(dashbook); + addCollisionDetectionStories(dashbook); + addComponentsStories(dashbook); + addEffectsStories(dashbook); + addExperimentalStories(dashbook); + addInputStories(dashbook); + addLayoutStories(dashbook); + addParallaxStories(dashbook); + addRenderingStories(dashbook); + addTiledStories(dashbook); + addSpritesStories(dashbook); + addSvgStories(dashbook); + addSystemStories(dashbook); + addUtilsStories(dashbook); + addWidgetsStories(dashbook); + addImageStories(dashbook); + + // Bridge package examples + addForge2DStories(dashbook); + addFlameIsolateExample(dashbook); + addFlameJennyExample(dashbook); + addFlameLottieExample(dashbook); + addFlameSpineExamples(dashbook); + + runApp(dashbook); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/platform/page_provider.dart b/flame/assets/examples/official/dashbook_example/lib/platform/page_provider.dart new file mode 100644 index 0000000..b22aaae --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/platform/page_provider.dart @@ -0,0 +1,3 @@ +abstract class PageProvider { + String? getPage(); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/platform/stub_provider.dart b/flame/assets/examples/official/dashbook_example/lib/platform/stub_provider.dart new file mode 100644 index 0000000..3e04b7a --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/platform/stub_provider.dart @@ -0,0 +1,8 @@ +import 'package:examples/platform/page_provider.dart'; + +class PageProviderImpl extends PageProvider { + @override + String? getPage() { + return null; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/platform/web_provider.dart b/flame/assets/examples/official/dashbook_example/lib/platform/web_provider.dart new file mode 100644 index 0000000..1a53eda --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/platform/web_provider.dart @@ -0,0 +1,13 @@ +import 'dart:html'; +import 'package:examples/platform/page_provider.dart'; + +class PageProviderImpl extends PageProvider { + @override + String? getPage() { + var page = window.location.search ?? ''; + if (page.startsWith('?')) { + page = page.substring(1); + } + return page; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/animations/animation_group_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/animations/animation_group_example.dart new file mode 100644 index 0000000..b3d534d --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/animations/animation_group_example.dart @@ -0,0 +1,73 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; + +enum RobotState { + idle, + running, +} + +class AnimationGroupExample extends FlameGame with TapDetector { + static const description = ''' + This example shows how to create a component that can be switched between + different states to change the animation that is playing.\n\n + + Usage: Click/tap and hold the screen to change state and then let go to go + back to the original animation. + '''; + + late SpriteAnimationGroupComponent robot; + + @override + Future onLoad() async { + final running = await loadSpriteAnimation( + 'animations/robot.png', + SpriteAnimationData.sequenced( + amount: 8, + stepTime: 0.2, + textureSize: Vector2(16, 18), + ), + ); + final idle = await loadSpriteAnimation( + 'animations/robot-idle.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.4, + textureSize: Vector2(16, 18), + ), + ); + + final robotSize = Vector2(64, 72); + robot = SpriteAnimationGroupComponent( + animations: { + RobotState.running: running, + RobotState.idle: idle, + }, + current: RobotState.idle, + position: size / 2 - robotSize / 2, + size: robotSize, + ); + + add(robot); + } + + @override + void onTapDown(_) { + robot.current = RobotState.running; + } + + @override + void onTapCancel() { + robot.current = RobotState.idle; + } + + @override + void onTapUp(_) { + robot.current = RobotState.idle; + } + + @override + Color backgroundColor() => const Color(0xFF222222); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/animations/animations.dart b/flame/assets/examples/official/dashbook_example/lib/stories/animations/animations.dart new file mode 100644 index 0000000..bd34f6c --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/animations/animations.dart @@ -0,0 +1,35 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/animations/animation_group_example.dart'; +import 'package:examples/stories/animations/aseprite_example.dart'; +import 'package:examples/stories/animations/basic_animation_example.dart'; +import 'package:examples/stories/animations/benchmark_example.dart'; +import 'package:flame/game.dart'; + +void addAnimationStories(Dashbook dashbook) { + dashbook.storiesOf('Animations') + ..add( + 'Basic Animations', + (_) => GameWidget(game: BasicAnimationsExample()), + codeLink: baseLink('animations/basic_animation_example.dart'), + info: BasicAnimationsExample.description, + ) + ..add( + 'Group animation', + (_) => GameWidget(game: AnimationGroupExample()), + codeLink: baseLink('animations/animation_group_example.dart'), + info: AnimationGroupExample.description, + ) + ..add( + 'Aseprite', + (_) => GameWidget(game: AsepriteExample()), + codeLink: baseLink('animations/aseprite_example.dart'), + info: AsepriteExample.description, + ) + ..add( + 'Benchmark', + (_) => GameWidget(game: BenchmarkExample()), + codeLink: baseLink('animations/benchmark_example.dart'), + info: BenchmarkExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/animations/aseprite_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/animations/aseprite_example.dart new file mode 100644 index 0000000..aef17b1 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/animations/aseprite_example.dart @@ -0,0 +1,23 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; + +class AsepriteExample extends FlameGame { + static const String description = ''' + This example shows how to load animations from an Aseprite json file and a + sprite sheet. There is no interaction on this example. + '''; + + @override + Future onLoad() async { + final image = await images.load('animations/chopper.png'); + final jsonData = await assets.readJson('images/animations/chopper.json'); + final animation = SpriteAnimation.fromAsepriteData(image, jsonData); + final spriteSize = Vector2.all(200); + final animationComponent = SpriteAnimationComponent( + animation: animation, + position: (size - spriteSize) / 2, + size: spriteSize, + ); + add(animationComponent); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/animations/basic_animation_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/animations/basic_animation_example.dart new file mode 100644 index 0000000..b699a03 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/animations/basic_animation_example.dart @@ -0,0 +1,76 @@ +import 'dart:ui'; + +import 'package:examples/commons/ember.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; + +class BasicAnimationsExample extends FlameGame { + static const description = ''' + Basic example of how to use `SpriteAnimation`s in Flame's. + + In this example, click or touch anywhere on the screen to dynamically add + animations. + '''; + + BasicAnimationsExample() : super(world: BasicAnimationsWorld()); +} + +class BasicAnimationsWorld extends World with TapCallbacks, HasGameReference { + late Image creature; + + @override + Future onLoad() async { + creature = await game.images.load('animations/creature.png'); + + final animation = await game.loadSpriteAnimation( + 'animations/chopper.png', + SpriteAnimationData.sequenced( + amount: 4, + textureSize: Vector2.all(48), + stepTime: 0.15, + ), + ); + + final spriteSize = Vector2.all(100.0); + final animationComponent = SpriteAnimationComponent( + animation: animation, + position: Vector2(-spriteSize.x, 0), + size: spriteSize, + anchor: Anchor.center, + ); + + final reversedAnimationComponent = SpriteAnimationComponent( + animation: animation.reversed(), + position: Vector2(spriteSize.x, 0), + size: spriteSize, + anchor: Anchor.center, + ); + + add(animationComponent); + add(reversedAnimationComponent); + add(Ember()); + } + + @override + void onTapDown(TapDownEvent event) { + final size = Vector2(291, 178); + + final animationComponent = SpriteAnimationComponent.fromFrameData( + creature, + SpriteAnimationData.sequenced( + amount: 18, + amountPerRow: 10, + textureSize: size, + stepTime: 0.15, + loop: false, + ), + position: event.localPosition, + anchor: Anchor.center, + size: size, + removeOnFinish: true, + ); + + add(animationComponent); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/animations/benchmark_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/animations/benchmark_example.dart new file mode 100644 index 0000000..a6421f8 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/animations/benchmark_example.dart @@ -0,0 +1,69 @@ +import 'dart:math'; + +import 'package:examples/commons/ember.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; + +class BenchmarkExample extends FlameGame { + static const description = ''' +See how many SpriteAnimationComponent's your platform can handle before it +starts to drop in FPS, this is without any sprite batching and such. +100 animation components are added per tap. + '''; + + BenchmarkExample() : super(world: BenchmarkWorld()); + + final emberSize = Vector2.all(20); + late final TextComponent emberCounter; + final counterPrefix = 'Animations: '; + + @override + Future onLoad() async { + await camera.viewport.addAll([ + FpsTextComponent( + position: size - Vector2(10, 50), + anchor: Anchor.bottomRight, + ), + emberCounter = TextComponent( + position: size - Vector2(10, 25), + anchor: Anchor.bottomRight, + priority: 1, + ), + ]); + world.add(Ember(size: emberSize)); + children.register(); + } + + @override + void update(double dt) { + super.update(dt); + emberCounter.text = + '$counterPrefix ${world.children.query().length}'; + } +} + +class BenchmarkWorld extends World + with TapCallbacks, HasGameReference { + final Random random = Random(); + + @override + void onTapDown(TapDownEvent event) { + addAll( + List.generate( + 100, + (_) => Ember( + size: game.emberSize, + position: Vector2( + (game.size.x / 2) * + random.nextDouble() * + (random.nextBool() ? 1 : -1), + (game.size.y / 2) * + random.nextDouble() * + (random.nextBool() ? 1 : -1), + ), + ), + ), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/audio/audio.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/audio/audio.dart new file mode 100644 index 0000000..00e7cbd --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/audio/audio.dart @@ -0,0 +1,13 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/bridge_libraries/audio/basic_audio_example.dart'; +import 'package:flame/game.dart'; + +void addAudioStories(Dashbook dashbook) { + dashbook.storiesOf('Audio').add( + 'Basic Audio', + (_) => GameWidget(game: BasicAudioExample()), + codeLink: baseLink('bridge_libraries/audio/basic_audio_example.dart'), + info: BasicAudioExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/audio/basic_audio_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/audio/basic_audio_example.dart new file mode 100644 index 0000000..38500d8 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/audio/basic_audio_example.dart @@ -0,0 +1,94 @@ +import 'package:flame/components.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; +import 'package:flame_audio/flame_audio.dart'; +import 'package:flutter/painting.dart'; + +class BasicAudioExample extends FlameGame { + static const String description = ''' + This example showcases the most basic Flame Audio functionalities. + + 1. Use the static FlameAudio class to easily fire a sfx using the default + configs for the button tap. + 2. Uses a custom AudioPool for extremely efficient audio loading and pooling + for tapping elsewhere. + 3. Uses the Bgm utility for background music. + '''; + + static final Paint black = BasicPalette.black.paint(); + static final Paint gray = const PaletteEntry(Color(0xFFCCCCCC)).paint(); + static final TextPaint topTextPaint = TextPaint( + style: TextStyle(color: BasicPalette.lightBlue.color), + ); + static final TextPaint bottomTextPaint = TextPaint( + style: TextStyle(color: BasicPalette.black.color), + ); + + late AudioPool pool; + + @override + Future onLoad() async { + pool = await FlameAudio.createPool( + 'sfx/fire_2.mp3', + minPlayers: 3, + maxPlayers: 4, + ); + startBgmMusic(); + final firstButtonSize = Vector2(size.x - 40, size.y * (4 / 5)); + final secondButtonSize = Vector2(size.x - 40, size.y / 5); + addAll( + [ + ButtonComponent( + position: Vector2(20, 20), + size: firstButtonSize, + button: RectangleComponent(paint: black, size: firstButtonSize), + onPressed: fireOne, + children: [ + TextComponent( + text: 'Click here for 1', + textRenderer: topTextPaint, + position: firstButtonSize / 2, + anchor: Anchor.center, + priority: 1, + ), + ], + ), + ButtonComponent( + position: Vector2(20, size.y - size.y / 5), + size: secondButtonSize, + button: RectangleComponent(paint: gray, size: secondButtonSize), + onPressed: fireTwo, + children: [ + TextComponent( + text: 'Click here for 2', + textRenderer: bottomTextPaint, + position: secondButtonSize / 2, + anchor: Anchor.center, + priority: 1, + ), + ], + ), + ], + ); + } + + void startBgmMusic() { + FlameAudio.bgm.initialize(); + FlameAudio.bgm.play('music/bg_music.ogg'); + } + + void fireOne() { + FlameAudio.play('sfx/fire_1.mp3'); + } + + void fireTwo() { + pool.start(); + } + + @override + void onRemove() { + FlameAudio.bgm.dispose(); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/animated_body_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/animated_body_example.dart new file mode 100644 index 0000000..e8c4dd3 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/animated_body_example.dart @@ -0,0 +1,92 @@ +import 'dart:ui'; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/flame.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class AnimatedBodyExample extends Forge2DGame { + static const String description = ''' + In this example we show how to add an animated chopper, which is created + with a SpriteAnimationComponent, on top of a BodyComponent. + + Tap the screen to add more choppers. + '''; + + AnimatedBodyExample() + : super( + gravity: Vector2.zero(), + world: AnimatedBodyWorld(), + ); +} + +class AnimatedBodyWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + late Image chopper; + late SpriteAnimation animation; + + @override + Future onLoad() async { + super.onLoad(); + chopper = await Flame.images.load('animations/chopper.png'); + + animation = SpriteAnimation.fromFrameData( + chopper, + SpriteAnimationData.sequenced( + amount: 4, + textureSize: Vector2.all(48), + stepTime: 0.15, + ), + ); + + final boundaries = createBoundaries(game); + addAll(boundaries); + } + + @override + void onTapDown(TapDownEvent info) { + super.onTapDown(info); + final position = info.localPosition; + final spriteSize = Vector2.all(10); + final animationComponent = SpriteAnimationComponent( + animation: animation, + size: spriteSize, + anchor: Anchor.center, + ); + add(ChopperBody(position, animationComponent)); + } +} + +class ChopperBody extends BodyComponent { + final Vector2 _position; + final Vector2 size; + + ChopperBody( + this._position, + PositionComponent component, + ) : size = component.size { + renderBody = false; + add(component); + } + + @override + Body createBody() { + final shape = CircleShape()..radius = size.x / 4; + final fixtureDef = FixtureDef( + shape, + userData: this, // To be able to determine object in collision + restitution: 0.8, + friction: 0.2, + ); + + final velocity = (Vector2.random() - Vector2.random()) * 200; + final bodyDef = BodyDef( + position: _position, + angle: velocity.angleTo(Vector2(1, 0)), + linearVelocity: velocity, + type: BodyType.dynamic, + ); + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/blob_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/blob_example.dart new file mode 100644 index 0000000..6e5f177 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/blob_example.dart @@ -0,0 +1,124 @@ +import 'dart:math' as math; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class BlobExample extends Forge2DGame { + static const String description = ''' + In this example we show the power of joints by showing interactions between + bodies tied together. + + Tap the screen to add boxes that will bounce on the "blob" in the center. + '''; + BlobExample() : super(world: BlobWorld()); +} + +class BlobWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + @override + Future onLoad() async { + await super.onLoad(); + final blobCenter = Vector2(0, -30); + final blobRadius = Vector2.all(6.0); + addAll(createBoundaries(game)); + add(Ground(Vector2.zero())); + final jointDef = ConstantVolumeJointDef() + ..frequencyHz = 20.0 + ..dampingRatio = 1.0 + ..collideConnected = false; + + await addAll([ + for (var i = 0; i < 20; i++) + BlobPart(i, jointDef, blobRadius, blobCenter), + ]); + createJoint(ConstantVolumeJoint(physicsWorld, jointDef)); + } + + @override + void onTapDown(TapDownEvent info) { + super.onTapDown(info); + add(FallingBox(info.localPosition)); + } +} + +class Ground extends BodyComponent { + final Vector2 worldCenter; + + Ground(this.worldCenter); + + @override + Body createBody() { + final shape = PolygonShape(); + shape.setAsBoxXY(20.0, 0.4); + final fixtureDef = FixtureDef(shape, friction: 0.2); + + final bodyDef = BodyDef(position: worldCenter.clone()); + final ground = world.createBody(bodyDef); + ground.createFixture(fixtureDef); + + shape.setAsBox(0.4, 20.0, Vector2(-10.0, 0.0), 0.0); + ground.createFixture(fixtureDef); + shape.setAsBox(0.4, 20.0, Vector2(10.0, 0.0), 0.0); + ground.createFixture(fixtureDef); + return ground; + } +} + +class BlobPart extends BodyComponent { + final ConstantVolumeJointDef jointDef; + final int bodyNumber; + final Vector2 blobRadius; + final Vector2 blobCenter; + + BlobPart( + this.bodyNumber, + this.jointDef, + this.blobRadius, + this.blobCenter, + ); + + @override + Body createBody() { + const nBodies = 20.0; + const bodyRadius = 0.5; + final angle = (bodyNumber / nBodies) * math.pi * 2; + final x = blobCenter.x + blobRadius.x * math.sin(angle); + final y = blobCenter.y + blobRadius.y * math.cos(angle); + + final bodyDef = BodyDef( + fixedRotation: true, + position: Vector2(x, y), + type: BodyType.dynamic, + ); + final body = world.createBody(bodyDef); + + final shape = CircleShape()..radius = bodyRadius; + final fixtureDef = FixtureDef( + shape, + friction: 0.2, + ); + body.createFixture(fixtureDef); + jointDef.addBody(body); + return body; + } +} + +class FallingBox extends BodyComponent { + final Vector2 _position; + + FallingBox(this._position); + + @override + Body createBody() { + final bodyDef = BodyDef( + type: BodyType.dynamic, + position: _position, + ); + final shape = PolygonShape()..setAsBoxXY(2, 4); + final body = world.createBody(bodyDef); + body.createFixtureFromShape(shape); + return body; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/camera_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/camera_example.dart new file mode 100644 index 0000000..941423d --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/camera_example.dart @@ -0,0 +1,23 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/domino_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class CameraExample extends Forge2DGame { + static const String description = ''' + This example showcases the possibility to follow BodyComponents with the + camera. When the screen is tapped a pizza is added, which the camera will + follow. Other than that it is the same as the domino example. + '''; + CameraExample() : super(world: CameraExampleWorld()); +} + +class CameraExampleWorld extends DominoExampleWorld { + @override + void onTapDown(TapDownEvent info) { + final position = info.localPosition; + final pizza = Pizza(position); + add(pizza); + pizza.mounted.whenComplete(() => game.camera.follow(pizza)); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/composition_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/composition_example.dart new file mode 100644 index 0000000..f68962b --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/composition_example.dart @@ -0,0 +1,99 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; + +const TextStyle _textStyle = TextStyle(color: Colors.white, fontSize: 2); + +class CompositionExample extends Forge2DGame { + static const description = ''' + This example shows how to compose a `BodyComponent` together with a normal + Flame component. Click the ball to see the number increment. + '''; + + CompositionExample() : super(zoom: 20, gravity: Vector2(0, 10.0)); + + @override + Future onLoad() async { + super.onLoad(); + final boundaries = createBoundaries(this); + world.addAll(boundaries); + world.add(TappableText(Vector2(0, 5))); + world.add(TappableBall(Vector2.zero())); + } +} + +class TappableText extends TextComponent with TapCallbacks { + TappableText(Vector2 position) + : super( + text: 'A normal tappable Flame component', + textRenderer: TextPaint(style: _textStyle), + position: position, + anchor: Anchor.center, + ); + + @override + Future onLoad() async { + final scaleEffect = ScaleEffect.by( + Vector2.all(1.1), + EffectController( + duration: 0.7, + alternate: true, + infinite: true, + ), + ); + add(scaleEffect); + } + + @override + void onTapDown(TapDownEvent event) { + add( + MoveEffect.by( + Vector2.all(5), + EffectController( + speed: 5, + alternate: true, + ), + ), + ); + } +} + +class TappableBall extends Ball with TapCallbacks { + late final TextComponent textComponent; + int counter = 0; + late final TextPaint _textPaint; + + TappableBall(super.position) { + originalPaint = Paint()..color = Colors.amber; + paint = originalPaint; + } + + @override + Future onLoad() async { + super.onLoad(); + _textPaint = TextPaint(style: _textStyle); + textComponent = TextComponent( + text: counter.toString(), + textRenderer: _textPaint, + ); + add(textComponent); + } + + @override + void update(double dt) { + super.update(dt); + textComponent.text = counter.toString(); + } + + @override + bool onTapDown(_) { + counter++; + body.applyLinearImpulse(Vector2.random() * 1000); + paint = randomPaint(); + return false; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/contact_callbacks_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/contact_callbacks_example.dart new file mode 100644 index 0000000..27dd816 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/contact_callbacks_example.dart @@ -0,0 +1,40 @@ +import 'dart:math' as math; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class ContactCallbacksExample extends Forge2DGame { + static const description = ''' + This example shows how `BodyComponent`s can react to collisions with other + bodies. + Tap the screen to add balls, the white balls will give an impulse to the + balls that it collides with. + '''; + + ContactCallbacksExample() + : super(gravity: Vector2(0, 10.0), world: ContactCallbackWorld()); +} + +class ContactCallbackWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + @override + Future onLoad() async { + super.onLoad(); + final boundaries = createBoundaries(game); + addAll(boundaries); + } + + @override + void onTapDown(TapDownEvent info) { + super.onTapDown(info); + final position = info.localPosition; + if (math.Random().nextInt(10) < 2) { + add(WhiteBall(position)); + } else { + add(Ball(position)); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/domino_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/domino_example.dart new file mode 100644 index 0000000..b936fba --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/domino_example.dart @@ -0,0 +1,88 @@ +import 'dart:ui'; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class DominoExample extends Forge2DGame { + static const description = ''' + In this example we can see some domino tiles lined up. + If you tap on the screen a pizza is added which can tip the tiles over and + cause a chain reaction. + '''; + + DominoExample() + : super(gravity: Vector2(0, 10.0), world: DominoExampleWorld()); +} + +class DominoExampleWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + late Image pizzaImage; + + @override + Future onLoad() async { + super.onLoad(); + final boundaries = createBoundaries(game); + addAll(boundaries); + + const numberOfRows = 7; + for (var i = 0; i < numberOfRows - 2; i++) { + add(Platform(Vector2(0.0, 5.0 * i))); + } + + const numberPerRow = 25; + for (var i = 0; i < numberOfRows; ++i) { + for (var j = 0; j < numberPerRow; j++) { + final position = Vector2( + -14.75 + j * (29.5 / (numberPerRow - 1)), + -12.7 + 5 * i, + ); + add(DominoBrick(position)); + } + } + } + + @override + void onTapDown(TapDownEvent info) { + final position = info.localPosition; + add(Pizza(position)); + } +} + +class Platform extends BodyComponent { + final Vector2 _position; + + Platform(this._position); + + @override + Body createBody() { + final shape = PolygonShape()..setAsBoxXY(14.8, 0.125); + final fixtureDef = FixtureDef(shape); + + final bodyDef = BodyDef(position: _position); + final body = world.createBody(bodyDef); + return body..createFixture(fixtureDef); + } +} + +class DominoBrick extends BodyComponent { + final Vector2 _position; + + DominoBrick(this._position); + + @override + Body createBody() { + final shape = PolygonShape()..setAsBoxXY(0.125, 2.0); + final fixtureDef = FixtureDef( + shape, + density: 25.0, + restitution: 0.4, + friction: 0.5, + ); + + final bodyDef = BodyDef(type: BodyType.dynamic, position: _position); + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart new file mode 100644 index 0000000..090b6e8 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart @@ -0,0 +1,47 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart' hide Draggable; + +class DragCallbacksExample extends Forge2DGame { + static const description = ''' + In this example we use Flame's normal `DragCallbacks` mixin to give impulses + to a ball when we are dragging it around. If you are interested in dragging + bodies around, also have a look at the MouseJointExample. + '''; + + DragCallbacksExample() : super(gravity: Vector2.all(0.0)); + + @override + Future onLoad() async { + super.onLoad(); + final boundaries = createBoundaries(this); + world.addAll(boundaries); + world.add(DraggableBall(Vector2.zero())); + } +} + +class DraggableBall extends Ball with DragCallbacks { + DraggableBall(super.position) : super(radius: 5) { + originalPaint = Paint()..color = Colors.amber; + paint = originalPaint; + } + + @override + void onDragStart(DragStartEvent event) { + super.onDragStart(event); + paint = randomPaint(); + } + + @override + void onDragUpdate(DragUpdateEvent event) { + body.applyLinearImpulse(event.localDelta * 1000); + } + + @override + void onDragEnd(DragEndEvent event) { + super.onDragEnd(event); + paint = originalPaint; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/flame_forge2d.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/flame_forge2d.dart new file mode 100644 index 0000000..01d3534 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/flame_forge2d.dart @@ -0,0 +1,178 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/animated_body_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/blob_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/camera_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/composition_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/contact_callbacks_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/domino_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart'; +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_forge2d/raycast_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/tap_callbacks_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/widget_example.dart'; +import 'package:flame/game.dart'; + +String link(String example) => + baseLink('bridge_libraries/flame_forge2d/$example'); + +void addForge2DStories(Dashbook dashbook) { + dashbook.storiesOf('flame_forge2d') + ..add( + 'Blob example', + (DashbookContext ctx) => GameWidget(game: BlobExample()), + codeLink: link('blob_example.dart'), + info: BlobExample.description, + ) + ..add( + 'Composition example', + (DashbookContext ctx) => GameWidget(game: CompositionExample()), + codeLink: link('composition_example.dart'), + info: CompositionExample.description, + ) + ..add( + 'Domino example', + (DashbookContext ctx) => GameWidget(game: DominoExample()), + codeLink: link('domino_example.dart'), + info: DominoExample.description, + ) + ..add( + 'Contact Callbacks', + (DashbookContext ctx) => GameWidget(game: ContactCallbacksExample()), + codeLink: link('contact_callbacks_example.dart'), + info: ContactCallbacksExample.description, + ) + ..add( + 'RevoluteJoint with Motor', + (DashbookContext ctx) => + GameWidget(game: RevoluteJointWithMotorExample()), + codeLink: link('revolute_joint_with_motor_example.dart'), + info: RevoluteJointExample.description, + ) + ..add( + 'Sprite Bodies', + (DashbookContext ctx) => GameWidget(game: SpriteBodyExample()), + codeLink: link('sprite_body_example.dart'), + info: SpriteBodyExample.description, + ) + ..add( + 'Animated Bodies', + (DashbookContext ctx) => GameWidget(game: AnimatedBodyExample()), + codeLink: link('animated_body_example.dart'), + info: AnimatedBodyExample.description, + ) + ..add( + 'Tappable Body', + (DashbookContext ctx) => GameWidget(game: TapCallbacksExample()), + codeLink: link('tap_callbacks_example.dart'), + info: TapCallbacksExample.description, + ) + ..add( + 'Draggable Body', + (DashbookContext ctx) => GameWidget(game: DragCallbacksExample()), + codeLink: link('drag_callbacks_example.dart'), + info: DragCallbacksExample.description, + ) + ..add( + 'Camera', + (DashbookContext ctx) => GameWidget(game: CameraExample()), + codeLink: link('camera_example.dart'), + info: CameraExample.description, + ) + ..add( + 'Raycasting', + (DashbookContext ctx) => GameWidget(game: RaycastExample()), + codeLink: link('raycast_example.dart'), + info: RaycastExample.description, + ) + ..add( + 'Widgets', + (DashbookContext ctx) => const BodyWidgetExample(), + codeLink: link('widget_example.dart'), + info: WidgetExample.description, + ); + addJointsStories(dashbook); +} + +void addJointsStories(Dashbook dashbook) { + dashbook + .storiesOf('flame_forge2d/joints') + .add( + 'ConstantVolumeJoint', + (DashbookContext ctx) => GameWidget(game: ConstantVolumeJointExample()), + codeLink: link('joints/constant_volume_joint.dart'), + info: ConstantVolumeJointExample.description, + ) + .add( + 'DistanceJoint', + (DashbookContext ctx) => GameWidget(game: DistanceJointExample()), + codeLink: link('joints/distance_joint.dart'), + info: DistanceJointExample.description, + ) + .add( + 'FrictionJoint', + (DashbookContext ctx) => GameWidget(game: FrictionJointExample()), + codeLink: link('joints/friction_joint.dart'), + info: FrictionJointExample.description, + ) + .add( + 'GearJoint', + (DashbookContext ctx) => GameWidget(game: GearJointExample()), + codeLink: link('joints/gear_joint.dart'), + info: GearJointExample.description, + ) + .add( + 'MotorJoint', + (DashbookContext ctx) => GameWidget(game: MotorJointExample()), + codeLink: link('joints/motor_joint.dart'), + info: MotorJointExample.description, + ) + .add( + 'MouseJoint', + (DashbookContext ctx) => GameWidget(game: MouseJointExample()), + codeLink: link('joints/mouse_joint.dart'), + info: MouseJointExample.description, + ) + .add( + 'PrismaticJoint', + (DashbookContext ctx) => GameWidget(game: PrismaticJointExample()), + codeLink: link('joints/prismatic_joint.dart'), + info: PrismaticJointExample.description, + ) + .add( + 'PulleyJoint', + (DashbookContext ctx) => GameWidget(game: PulleyJointExample()), + codeLink: link('joints/pulley_joint.dart'), + info: PulleyJointExample.description, + ) + .add( + 'RevoluteJoint', + (DashbookContext ctx) => GameWidget(game: RevoluteJointExample()), + codeLink: link('joints/revolute_joint.dart'), + info: RevoluteJointExample.description, + ) + .add( + 'RopeJoint', + (DashbookContext ctx) => GameWidget(game: RopeJointExample()), + codeLink: link('joints/rope_joint.dart'), + info: RopeJointExample.description, + ) + .add( + 'WeldJoint', + (DashbookContext ctx) => GameWidget(game: WeldJointExample()), + codeLink: link('joints/weld_joint.dart'), + info: WeldJointExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart new file mode 100644 index 0000000..53f0d6e --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/constant_volume_joint.dart @@ -0,0 +1,62 @@ +import 'dart:math'; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class ConstantVolumeJointExample extends Forge2DGame { + static const description = ''' + This example shows how to use a `ConstantVolumeJoint`. Tap the screen to add + a bunch off balls, that maintain a constant volume within them. + '''; + + ConstantVolumeJointExample() : super(world: SpriteBodyWorld()); +} + +class SpriteBodyWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + @override + Future onLoad() async { + super.onLoad(); + addAll(createBoundaries(game)); + } + + @override + Future onTapDown(TapDownEvent info) async { + super.onTapDown(info); + final center = info.localPosition; + + const numPieces = 20; + const radius = 5.0; + final balls = []; + + for (var i = 0; i < numPieces; i++) { + final x = radius * cos(2 * pi * (i / numPieces)); + final y = radius * sin(2 * pi * (i / numPieces)); + + final ball = Ball(Vector2(x + center.x, y + center.y), radius: 0.5); + + add(ball); + balls.add(ball); + } + + await Future.wait(balls.map((e) => e.loaded)); + + final constantVolumeJoint = ConstantVolumeJointDef() + ..frequencyHz = 10 + ..dampingRatio = 0.8; + + balls.forEach((ball) { + constantVolumeJoint.addBody(ball.body); + }); + + createJoint( + ConstantVolumeJoint( + physicsWorld, + constantVolumeJoint, + ), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart new file mode 100644 index 0000000..15af34b --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/distance_joint.dart @@ -0,0 +1,48 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class DistanceJointExample extends Forge2DGame { + static const description = ''' + This example shows how to use a `DistanceJoint`. Tap the screen to add a + pair of balls joined with a `DistanceJoint`. + '''; + + DistanceJointExample() : super(world: DistanceJointWorld()); +} + +class DistanceJointWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + @override + Future onLoad() async { + super.onLoad(); + addAll(createBoundaries(game)); + } + + @override + Future onTapDown(TapDownEvent info) async { + super.onTapDown(info); + final tap = info.localPosition; + + final first = Ball(tap); + final second = Ball(Vector2(tap.x + 3, tap.y + 3)); + addAll([first, second]); + + await Future.wait([first.loaded, second.loaded]); + + final distanceJointDef = DistanceJointDef() + ..initialize( + first.body, + second.body, + first.body.worldCenter, + second.center, + ) + ..length = 10 + ..frequencyHz = 3 + ..dampingRatio = 0.2; + + createJoint(DistanceJoint(distanceJointDef)); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart new file mode 100644 index 0000000..86f5e7d --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/friction_joint.dart @@ -0,0 +1,52 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class FrictionJointExample extends Forge2DGame { + static const description = ''' + This example shows how to use a `FrictionJoint`. Tap the screen to move the + ball around and observe it slows down due to the friction force. + '''; + + FrictionJointExample() + : super(gravity: Vector2.all(0), world: FrictionJointWorld()); +} + +class FrictionJointWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + late Wall border; + late Ball ball; + + @override + Future onLoad() async { + super.onLoad(); + final boundaries = createBoundaries(game); + border = boundaries.first; + addAll(boundaries); + + ball = Ball(Vector2.zero(), radius: 3); + add(ball); + + await Future.wait([ball.loaded, border.loaded]); + + createFrictionJoint(ball.body, border.body); + } + + @override + Future onTapDown(TapDownEvent info) async { + super.onTapDown(info); + ball.body.applyLinearImpulse(Vector2.random() * 5000); + } + + void createFrictionJoint(Body first, Body second) { + final frictionJointDef = FrictionJointDef() + ..initialize(first, second, first.worldCenter) + ..collideConnected = true + ..maxForce = 500 + ..maxTorque = 500; + + createJoint(FrictionJoint(frictionJointDef)); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart new file mode 100644 index 0000000..322cb9c --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/gear_joint.dart @@ -0,0 +1,122 @@ +import 'dart:ui'; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart'; +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class GearJointExample extends Forge2DGame { + static const description = ''' + This example shows how to use a `GearJoint`. + + Drag the box along the specified axis and observe gears respond to the + translation. + '''; + + GearJointExample() : super(world: GearJointWorld()); +} + +class GearJointWorld extends Forge2DWorld with HasGameReference { + late PrismaticJoint prismaticJoint; + Vector2 boxAnchor = Vector2.zero(); + + double boxWidth = 2; + double ball1Radius = 4; + double ball2Radius = 2; + + @override + Future onLoad() async { + super.onLoad(); + + final box = + DraggableBox(startPosition: boxAnchor, width: boxWidth, height: 20); + add(box); + + final ball1Anchor = boxAnchor - Vector2(boxWidth / 2 + ball1Radius, 0); + final ball1 = Ball(ball1Anchor, radius: ball1Radius); + add(ball1); + + final ball2Anchor = ball1Anchor - Vector2(ball1Radius + ball2Radius, 0); + final ball2 = Ball(ball2Anchor, radius: ball2Radius); + add(ball2); + + await Future.wait([box.loaded, ball1.loaded, ball2.loaded]); + + prismaticJoint = createPrismaticJoint(box.body, boxAnchor); + final revoluteJoint1 = createRevoluteJoint(ball1.body, ball1Anchor); + final revoluteJoint2 = createRevoluteJoint(ball2.body, ball2Anchor); + + createGearJoint(prismaticJoint, revoluteJoint1, 1); + createGearJoint(revoluteJoint1, revoluteJoint2, 0.5); + add(JointRenderer(joint: prismaticJoint, anchor: boxAnchor)); + } + + PrismaticJoint createPrismaticJoint(Body box, Vector2 anchor) { + final groundBody = createBody(BodyDef()); + + final prismaticJointDef = PrismaticJointDef() + ..initialize( + groundBody, + box, + anchor, + Vector2(0, 1), + ) + ..enableLimit = true + ..lowerTranslation = -10 + ..upperTranslation = 10; + + final joint = PrismaticJoint(prismaticJointDef); + createJoint(joint); + return joint; + } + + RevoluteJoint createRevoluteJoint(Body ball, Vector2 anchor) { + final groundBody = createBody(BodyDef()); + + final revoluteJointDef = RevoluteJointDef() + ..initialize( + groundBody, + ball, + anchor, + ); + + final joint = RevoluteJoint(revoluteJointDef); + createJoint(joint); + return joint; + } + + void createGearJoint(Joint first, Joint second, double gearRatio) { + final gearJointDef = GearJointDef() + ..bodyA = first.bodyA + ..bodyB = second.bodyA + ..joint1 = first + ..joint2 = second + ..ratio = gearRatio; + + final joint = GearJoint(gearJointDef); + createJoint(joint); + } +} + +class JointRenderer extends Component { + JointRenderer({required this.joint, required this.anchor}); + + final PrismaticJoint joint; + final Vector2 anchor; + final Vector2 p1 = Vector2.zero(); + final Vector2 p2 = Vector2.zero(); + + @override + void render(Canvas canvas) { + p1 + ..setFrom(joint.getLocalAxisA()) + ..scale(joint.getLowerLimit()) + ..add(anchor); + p2 + ..setFrom(joint.getLocalAxisA()) + ..scale(joint.getUpperLimit()) + ..add(anchor); + + canvas.drawLine(p1.toOffset(), p2.toOffset(), debugPaint); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart new file mode 100644 index 0000000..64a2145 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/motor_joint.dart @@ -0,0 +1,99 @@ +import 'dart:ui'; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class MotorJointExample extends Forge2DGame { + static const description = ''' + This example shows how to use a `MotorJoint`. The ball spins around the + center point. Tap the screen to change the direction. + '''; + + MotorJointExample() + : super(gravity: Vector2.zero(), world: MotorJointWorld()); +} + +class MotorJointWorld extends Forge2DWorld with TapCallbacks { + late Ball ball; + late MotorJoint joint; + final motorSpeed = 1; + + bool clockWise = true; + + @override + Future onLoad() async { + super.onLoad(); + + final box = Box( + startPosition: Vector2.zero(), + width: 2, + height: 1, + bodyType: BodyType.static, + ); + add(box); + + ball = Ball(Vector2(0, -5)); + add(ball); + + await Future.wait([ball.loaded, box.loaded]); + + joint = createMotorJoint(ball.body, box.body); + add(JointRenderer(joint: joint)); + } + + @override + void onTapDown(TapDownEvent info) { + super.onTapDown(info); + clockWise = !clockWise; + } + + MotorJoint createMotorJoint(Body first, Body second) { + final motorJointDef = MotorJointDef() + ..initialize(first, second) + ..maxForce = 1000 + ..maxTorque = 1000 + ..correctionFactor = 0.1; + + final joint = MotorJoint(motorJointDef); + createJoint(joint); + return joint; + } + + final linearOffset = Vector2.zero(); + + @override + void update(double dt) { + super.update(dt); + + var deltaOffset = motorSpeed * dt; + if (clockWise) { + deltaOffset = -deltaOffset; + } + + final linearOffsetX = joint.getLinearOffset().x + deltaOffset; + final linearOffsetY = joint.getLinearOffset().y + deltaOffset; + linearOffset.setValues(linearOffsetX, linearOffsetY); + final angularOffset = joint.getAngularOffset() + deltaOffset; + + joint.setLinearOffset(linearOffset); + joint.setAngularOffset(angularOffset); + } +} + +class JointRenderer extends Component { + JointRenderer({required this.joint}); + + final MotorJoint joint; + + @override + void render(Canvas canvas) { + canvas.drawLine( + joint.anchorA.toOffset(), + joint.anchorB.toOffset(), + debugPaint, + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart new file mode 100644 index 0000000..51f3247 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart @@ -0,0 +1,67 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class MouseJointExample extends Forge2DGame { + static const description = ''' + In this example we use a `MouseJoint` to make the ball follow the mouse + when you drag it around. + '''; + + MouseJointExample() + : super(gravity: Vector2(0, 10.0), world: MouseJointWorld()); +} + +class MouseJointWorld extends Forge2DWorld + with DragCallbacks, HasGameReference { + late Ball ball; + late Body groundBody; + MouseJoint? mouseJoint; + + @override + Future onLoad() async { + super.onLoad(); + final boundaries = createBoundaries(game); + addAll(boundaries); + + final center = Vector2.zero(); + groundBody = createBody(BodyDef()); + ball = Ball(center, radius: 5); + add(ball); + add(CornerRamp(center)); + add(CornerRamp(center, isMirrored: true)); + } + + @override + void onDragStart(DragStartEvent info) { + super.onDragStart(info); + final mouseJointDef = MouseJointDef() + ..maxForce = 3000 * ball.body.mass * 10 + ..dampingRatio = 0.1 + ..frequencyHz = 5 + ..target.setFrom(ball.body.position) + ..collideConnected = false + ..bodyA = groundBody + ..bodyB = ball.body; + + if (mouseJoint == null) { + mouseJoint = MouseJoint(mouseJointDef); + createJoint(mouseJoint!); + } + } + + @override + void onDragUpdate(DragUpdateEvent info) { + mouseJoint?.setTarget(info.localEndPosition); + } + + @override + void onDragEnd(DragEndEvent info) { + super.onDragEnd(info); + destroyJoint(mouseJoint!); + mouseJoint = null; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart new file mode 100644 index 0000000..8232234 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/prismatic_joint.dart @@ -0,0 +1,73 @@ +import 'dart:ui'; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart'; +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class PrismaticJointExample extends Forge2DGame { + static const description = ''' + This example shows how to use a `PrismaticJoint`. + + Drag the box along the specified axis, bound between lower and upper limits. + Also, there's a motor enabled that's pulling the box to the lower limit. + '''; + + final Vector2 anchor = Vector2.zero(); + + @override + Future onLoad() async { + super.onLoad(); + + final box = DraggableBox(startPosition: anchor, width: 6, height: 6); + world.add(box); + await Future.wait([box.loaded]); + + final joint = createJoint(box.body, anchor); + world.add(JointRenderer(joint: joint, anchor: anchor)); + } + + PrismaticJoint createJoint(Body box, Vector2 anchor) { + final groundBody = world.createBody(BodyDef()); + + final prismaticJointDef = PrismaticJointDef() + ..initialize( + box, + groundBody, + anchor, + Vector2(1, 0), + ) + ..enableLimit = true + ..lowerTranslation = -20 + ..upperTranslation = 20 + ..enableMotor = true + ..motorSpeed = 1 + ..maxMotorForce = 100; + + final joint = PrismaticJoint(prismaticJointDef); + world.createJoint(joint); + return joint; + } +} + +class JointRenderer extends Component { + JointRenderer({required this.joint, required this.anchor}); + + final PrismaticJoint joint; + final Vector2 anchor; + final Vector2 p1 = Vector2.zero(); + final Vector2 p2 = Vector2.zero(); + + @override + void render(Canvas canvas) { + p1 + ..setFrom(joint.getLocalAxisA()) + ..scale(joint.getLowerLimit()) + ..add(anchor); + p2 + ..setFrom(joint.getLocalAxisA()) + ..scale(joint.getUpperLimit()) + ..add(anchor); + + canvas.drawLine(p1.toOffset(), p2.toOffset(), debugPaint); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart new file mode 100644 index 0000000..9108e64 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/pulley_joint.dart @@ -0,0 +1,98 @@ +import 'dart:ui'; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart'; +import 'package:flame/components.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class PulleyJointExample extends Forge2DGame { + static const description = ''' + This example shows how to use a `PulleyJoint`. Drag one of the boxes and see + how the other one gets moved by the pulley + '''; + + @override + Future onLoad() async { + super.onLoad(); + final distanceFromCenter = camera.visibleWorldRect.width / 5; + + final firstPulley = Ball( + Vector2(-distanceFromCenter, -10), + bodyType: BodyType.static, + ); + final secondPulley = Ball( + Vector2(distanceFromCenter, -10), + bodyType: BodyType.static, + ); + + final firstBox = DraggableBox( + startPosition: Vector2(-distanceFromCenter, 20), + width: 5, + height: 10, + ); + final secondBox = DraggableBox( + startPosition: Vector2(distanceFromCenter, 20), + width: 7, + height: 10, + ); + world.addAll([firstBox, secondBox, firstPulley, secondPulley]); + + await Future.wait([ + firstBox.loaded, + secondBox.loaded, + firstPulley.loaded, + secondPulley.loaded, + ]); + + final joint = createJoint(firstBox, secondBox, firstPulley, secondPulley); + world.add(PulleyRenderer(joint: joint)); + } + + PulleyJoint createJoint( + Box firstBox, + Box secondBox, + Ball firstPulley, + Ball secondPulley, + ) { + final pulleyJointDef = PulleyJointDef() + ..initialize( + firstBox.body, + secondBox.body, + firstPulley.center, + secondPulley.center, + firstBox.body.worldPoint(Vector2(0, -firstBox.height / 2)), + secondBox.body.worldPoint(Vector2(0, -secondBox.height / 2)), + 1, + ); + final joint = PulleyJoint(pulleyJointDef); + world.createJoint(joint); + return joint; + } +} + +class PulleyRenderer extends Component { + PulleyRenderer({required this.joint}); + + final PulleyJoint joint; + + @override + void render(Canvas canvas) { + canvas.drawLine( + joint.anchorA.toOffset(), + joint.getGroundAnchorA().toOffset(), + debugPaint, + ); + + canvas.drawLine( + joint.anchorB.toOffset(), + joint.getGroundAnchorB().toOffset(), + debugPaint, + ); + + canvas.drawLine( + joint.getGroundAnchorA().toOffset(), + joint.getGroundAnchorB().toOffset(), + debugPaint, + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart new file mode 100644 index 0000000..72b9609 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/revolute_joint.dart @@ -0,0 +1,77 @@ +import 'dart:math'; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class RevoluteJointExample extends Forge2DGame { + static const description = ''' + In this example we use a joint to keep a body with several fixtures stuck + to another body. + + Tap the screen to add more of these combined bodies. + '''; + + RevoluteJointExample() + : super(gravity: Vector2(0, 10.0), world: RevoluteJointWorld()); +} + +class RevoluteJointWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + @override + Future onLoad() async { + super.onLoad(); + addAll(createBoundaries(game)); + } + + @override + void onTapDown(TapDownEvent info) { + super.onTapDown(info); + final ball = Ball(info.localPosition); + add(ball); + add(CircleShuffler(ball)); + } +} + +class CircleShuffler extends BodyComponent { + final Ball ball; + + CircleShuffler(this.ball); + + @override + Body createBody() { + final bodyDef = BodyDef( + type: BodyType.dynamic, + position: ball.body.position.clone(), + ); + const numPieces = 5; + const radius = 6.0; + final body = world.createBody(bodyDef); + + for (var i = 0; i < numPieces; i++) { + final xPos = radius * cos(2 * pi * (i / numPieces)); + final yPos = radius * sin(2 * pi * (i / numPieces)); + + final shape = CircleShape() + ..radius = 1.2 + ..position.setValues(xPos, yPos); + + final fixtureDef = FixtureDef( + shape, + density: 50.0, + friction: 0.1, + restitution: 0.9, + ); + + body.createFixture(fixtureDef); + } + + final jointDef = RevoluteJointDef() + ..initialize(body, ball.body, body.position); + world.createJoint(RevoluteJoint(jointDef)); + + return body; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart new file mode 100644 index 0000000..ab79b2a --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/rope_joint.dart @@ -0,0 +1,88 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; + +class RopeJointExample extends Forge2DGame { + static const description = ''' + This example shows how to use a `RopeJoint`. + + Drag the box handle along the axis and observe the rope respond to the + movement. + '''; + + RopeJointExample() : super(world: RopeJointWorld()); +} + +class RopeJointWorld extends Forge2DWorld + with DragCallbacks, HasGameReference { + double handleWidth = 6; + + @override + Future onLoad() async { + super.onLoad(); + + final handleBody = await createHandle(); + createRope(handleBody); + } + + Future createHandle() async { + final anchor = game.screenToWorld(Vector2(0, 100))..x = 0; + + final box = DraggableBox( + startPosition: anchor, + width: handleWidth, + height: 3, + ); + await add(box); + + createPrismaticJoint(box.body, anchor); + return box.body; + } + + Future createRope(Body handle) async { + const length = 50; + var prevBody = handle; + + for (var i = 0; i < length; i++) { + final newPosition = prevBody.worldCenter + Vector2(0, 1); + final ball = Ball(newPosition, radius: 0.5, color: Colors.white); + await add(ball); + + createRopeJoint(ball.body, prevBody); + prevBody = ball.body; + } + } + + void createPrismaticJoint(Body box, Vector2 anchor) { + final groundBody = createBody(BodyDef()); + final halfWidth = game.screenToWorld(Vector2.zero()).x.abs(); + + final prismaticJointDef = PrismaticJointDef() + ..initialize( + box, + groundBody, + anchor, + Vector2(1, 0), + ) + ..enableLimit = true + ..lowerTranslation = -halfWidth + handleWidth / 2 + ..upperTranslation = halfWidth - handleWidth / 2; + + final joint = PrismaticJoint(prismaticJointDef); + createJoint(joint); + } + + void createRopeJoint(Body first, Body second) { + final ropeJointDef = RopeJointDef() + ..bodyA = first + ..localAnchorA.setFrom(first.getLocalCenter()) + ..bodyB = second + ..localAnchorB.setFrom(second.getLocalCenter()) + ..maxLength = (second.worldCenter - first.worldCenter).length; + + createJoint(RopeJoint(ropeJointDef)); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart new file mode 100644 index 0000000..b1daea4 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/joints/weld_joint.dart @@ -0,0 +1,102 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boxes.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; + +class WeldJointExample extends Forge2DGame { + static const description = ''' + This example shows how to use a `WeldJoint`. Tap the screen to add a + ball to test the bridge built using a `WeldJoint` + '''; + + WeldJointExample() : super(world: WeldJointWorld()); +} + +class WeldJointWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + final pillarHeight = 20.0; + final pillarWidth = 5.0; + + @override + Future onLoad() async { + super.onLoad(); + + final leftPillar = Box( + startPosition: game.screenToWorld(Vector2(50, game.size.y)) + ..y -= pillarHeight / 2, + width: pillarWidth, + height: pillarHeight, + bodyType: BodyType.static, + color: Colors.white, + ); + final rightPillar = Box( + startPosition: game.screenToWorld(Vector2(game.size.x - 50, game.size.y)) + ..y -= pillarHeight / 2, + width: pillarWidth, + height: pillarHeight, + bodyType: BodyType.static, + color: Colors.white, + ); + + await addAll([leftPillar, rightPillar]); + + createBridge(leftPillar, rightPillar); + } + + Future createBridge( + Box leftPillar, + Box rightPillar, + ) async { + const sectionsCount = 10; + // Vector2.zero is used here since 0,0 is in the middle and 0,0 in the + // screen space then gives us the coordinates of the upper left corner in + // world space. + final halfSize = game.screenToWorld(Vector2.zero())..absolute(); + final sectionWidth = ((leftPillar.center.x.abs() + + rightPillar.center.x.abs() + + pillarWidth) / + sectionsCount) + .ceilToDouble(); + Body? prevSection; + + for (var i = 0; i < sectionsCount; i++) { + final section = Box( + startPosition: Vector2( + sectionWidth * i - halfSize.x + sectionWidth / 2, + halfSize.y - pillarHeight, + ), + width: sectionWidth, + height: 1, + ); + await add(section); + + if (prevSection != null) { + createWeldJoint( + prevSection, + section.body, + Vector2( + sectionWidth * i - halfSize.x + sectionWidth, + halfSize.y - pillarHeight, + ), + ); + } + + prevSection = section.body; + } + } + + void createWeldJoint(Body first, Body second, Vector2 anchor) { + final weldJointDef = WeldJointDef()..initialize(first, second, anchor); + + createJoint(WeldJoint(weldJointDef)); + } + + @override + Future onTapDown(TapDownEvent info) async { + super.onTapDown(info); + final ball = Ball(info.localPosition, radius: 5); + add(ball); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/raycast_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/raycast_example.dart new file mode 100644 index 0000000..8ab66f2 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/raycast_example.dart @@ -0,0 +1,216 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/input.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart' show Colors, Paint, Canvas; + +class RaycastExample extends Forge2DGame with MouseMovementDetector { + static const String description = ''' + This example shows how raycasts can be used to find nearest and farthest + fixtures. + Red ray finds the nearest fixture and blue ray finds the farthest fixture. + '''; + + final random = Random(); + + final redPoints = []; + final bluePoints = []; + + Box? nearestBox; + Box? farthestBox; + + RaycastExample() : super(gravity: Vector2.zero()); + + @override + Future onLoad() async { + super.onLoad(); + world.addAll(createBoundaries(this)); + + const numberOfRows = 3; + const numberOfBoxes = 4; + for (var i = 0; i < numberOfBoxes; ++i) { + for (var j = 0; j < numberOfRows; ++j) { + world.add(Box(Vector2(i * 10, j * 20 - 20))); + } + } + world.add( + LineComponent( + redPoints, + Paint() + ..color = Colors.red + ..strokeWidth = 1, + ), + ); + world.add( + LineComponent( + bluePoints, + Paint() + ..color = Colors.blue + ..strokeWidth = 1, + ), + ); + } + + @override + void onMouseMove(PointerHoverInfo info) { + final rayStart = screenToWorld( + Vector2( + camera.viewport.size.x / 4, + camera.viewport.size.y / 2, + ), + ); + + final worldPosition = screenToWorld(info.eventPosition.widget); + final redRayTarget = worldPosition + Vector2(0, 2); + fireRedRay(rayStart, redRayTarget); + + final blueRayTarget = worldPosition - Vector2(0, 2); + fireBlueRay(rayStart, blueRayTarget); + + super.onMouseMove(info); + } + + void fireBlueRay(Vector2 rayStart, Vector2 rayTarget) { + bluePoints.clear(); + bluePoints.add(rayStart); + + final farthestCallback = FarthestBoxRayCastCallback(); + world.raycast(farthestCallback, rayStart, rayTarget); + + if (farthestCallback.farthestPoint != null) { + bluePoints.add(farthestCallback.farthestPoint!); + } else { + bluePoints.add(rayTarget); + } + farthestBox = farthestCallback.box; + } + + void fireRedRay(Vector2 rayStart, Vector2 rayTarget) { + redPoints.clear(); + redPoints.add(rayStart); + + final nearestCallback = NearestBoxRayCastCallback(); + world.raycast(nearestCallback, rayStart, rayTarget); + + if (nearestCallback.nearestPoint != null) { + redPoints.add(nearestCallback.nearestPoint!); + } else { + redPoints.add(rayTarget); + } + nearestBox = nearestCallback.box; + } + + @override + void update(double dt) { + super.update(dt); + children.whereType().forEach((component) { + if ((component == nearestBox) && (component == farthestBox)) { + component.paint.color = Colors.yellow; + } else if (component == nearestBox) { + component.paint.color = Colors.red; + } else if (component == farthestBox) { + component.paint.color = Colors.blue; + } else { + component.paint.color = Colors.white; + } + }); + } +} + +class LineComponent extends Component { + LineComponent(this.points, this.paint); + + final List points; + final Paint paint; + final Path path = Path(); + + @override + void update(double dt) { + path + ..reset() + ..addPolygon( + points.map((p) => p.toOffset()).toList(growable: false), + false, + ); + } + + @override + void render(Canvas canvas) { + for (var i = 0; i < points.length - 1; ++i) { + canvas.drawLine( + points[i].toOffset(), + points[i + 1].toOffset(), + paint, + ); + } + } +} + +class Box extends BodyComponent { + Box(this.initialPosition); + + final Vector2 initialPosition; + + @override + Body createBody() { + final shape = PolygonShape()..setAsBoxXY(2.0, 4.0); + final fixtureDef = FixtureDef(shape, userData: this); + final bodyDef = BodyDef(position: initialPosition); + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class NearestBoxRayCastCallback extends RayCastCallback { + Box? box; + Vector2? nearestPoint; + Vector2? normalAtInter; + + @override + double reportFixture( + Fixture fixture, + Vector2 point, + Vector2 normal, + double fraction, + ) { + nearestPoint = point.clone(); + normalAtInter = normal.clone(); + box = fixture.userData as Box?; + + // Returning fraction implies that we care only about + // fixtures that are closer to ray start point than + // the current fixture + return fraction; + } +} + +class FarthestBoxRayCastCallback extends RayCastCallback { + Box? box; + Vector2? farthestPoint; + Vector2? normalAtInter; + double previousFraction = 0.0; + + @override + double reportFixture( + Fixture fixture, + Vector2 point, + Vector2 normal, + double fraction, + ) { + // Value of fraction is directly proportional to + // the distance of fixture from ray start point. + // So we are interested in the current fixture only if + // it has a higher fraction value than previousFraction. + if (previousFraction < fraction) { + farthestPoint = point.clone(); + normalAtInter = normal.clone(); + box = fixture.userData as Box?; + previousFraction = fraction; + } + + return 1; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart new file mode 100644 index 0000000..31456b5 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/revolute_joint_with_motor_example.dart @@ -0,0 +1,119 @@ +import 'dart:math'; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class RevoluteJointWithMotorExample extends Forge2DGame { + static const String description = ''' + This example showcases a revolute joint, which is the spinning balls in the + center. + + If you tap the screen some colorful balls are added and will + interact with the bodies tied to the revolute joint once they have fallen + down the funnel. + '''; + + RevoluteJointWithMotorExample() : super(world: RevoluteJointWithMotorWorld()); +} + +class RevoluteJointWithMotorWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + final random = Random(); + + @override + Future onLoad() async { + super.onLoad(); + final boundaries = createBoundaries(game); + addAll(boundaries); + final center = Vector2.zero(); + add(CircleShuffler(center)); + add(CornerRamp(center, isMirrored: true)); + add(CornerRamp(center)); + } + + @override + void onTapDown(TapDownEvent info) { + super.onTapDown(info); + final tapPosition = info.localPosition; + List.generate(15, (i) { + final randomVector = (Vector2.random() - Vector2.all(-0.5)).normalized(); + add(Ball(tapPosition + randomVector, radius: random.nextDouble())); + }); + } +} + +class CircleShuffler extends BodyComponent { + CircleShuffler(this._center); + + final Vector2 _center; + + @override + Body createBody() { + final bodyDef = BodyDef( + type: BodyType.dynamic, + position: _center + Vector2(0.0, 25.0), + ); + const numPieces = 5; + const radius = 6.0; + final body = world.createBody(bodyDef); + + for (var i = 0; i < numPieces; i++) { + final xPos = radius * cos(2 * pi * (i / numPieces)); + final yPos = radius * sin(2 * pi * (i / numPieces)); + + final shape = CircleShape() + ..radius = 1.2 + ..position.setValues(xPos, yPos); + + final fixtureDef = FixtureDef( + shape, + density: 50.0, + friction: .1, + restitution: .9, + ); + + body.createFixture(fixtureDef); + } + // Create an empty ground body. + final groundBody = world.createBody(BodyDef()); + + final revoluteJointDef = RevoluteJointDef() + ..initialize(body, groundBody, body.position) + ..motorSpeed = pi + ..maxMotorTorque = 1000000.0 + ..enableMotor = true; + + world.createJoint(RevoluteJoint(revoluteJointDef)); + return body; + } +} + +class CornerRamp extends BodyComponent { + CornerRamp(this._center, {this.isMirrored = false}); + + final bool isMirrored; + final Vector2 _center; + + @override + Body createBody() { + final shape = ChainShape(); + final mirrorFactor = isMirrored ? -1 : 1; + final diff = 2.0 * mirrorFactor; + final vertices = [ + Vector2(diff, 0), + Vector2(diff + 20.0 * mirrorFactor, -20.0), + Vector2(diff + 35.0 * mirrorFactor, -30.0), + ]; + shape.createLoop(vertices); + + final fixtureDef = FixtureDef(shape, friction: 0.1); + final bodyDef = BodyDef() + ..position = _center + ..type = BodyType.static; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart new file mode 100644 index 0000000..0aea806 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/sprite_body_example.dart @@ -0,0 +1,85 @@ +import 'dart:math'; + +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class SpriteBodyExample extends Forge2DGame { + static const String description = ''' + In this example we show how to add a sprite on top of a `BodyComponent`. + Tap the screen to add more pizzas. + '''; + + SpriteBodyExample() + : super( + gravity: Vector2(0, 10.0), + world: SpriteBodyWorld(), + ); +} + +class SpriteBodyWorld extends Forge2DWorld + with TapCallbacks, HasGameReference { + @override + Future onLoad() async { + super.onLoad(); + addAll(createBoundaries(game)); + } + + @override + void onTapDown(TapDownEvent info) { + super.onTapDown(info); + final position = info.localPosition; + add(Pizza(position, size: Vector2(10, 15))); + } +} + +class Pizza extends BodyComponent { + final Vector2 initialPosition; + final Vector2 size; + + Pizza( + this.initialPosition, { + Vector2? size, + }) : size = size ?? Vector2(2, 3); + + @override + Future onLoad() async { + await super.onLoad(); + final sprite = await game.loadSprite('pizza.png'); + renderBody = false; + add( + SpriteComponent( + sprite: sprite, + size: size, + anchor: Anchor.center, + ), + ); + } + + @override + Body createBody() { + final shape = PolygonShape(); + + final vertices = [ + Vector2(-size.x / 2, size.y / 2), + Vector2(size.x / 2, size.y / 2), + Vector2(0, -size.y / 2), + ]; + shape.set(vertices); + + final fixtureDef = FixtureDef( + shape, + userData: this, // To be able to determine object in collision + restitution: 0.4, + friction: 0.5, + ); + + final bodyDef = BodyDef( + position: initialPosition, + angle: (initialPosition.x + initialPosition.y) / 2 * pi, + type: BodyType.dynamic, + ); + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/tap_callbacks_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/tap_callbacks_example.dart new file mode 100644 index 0000000..96831ba --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/tap_callbacks_example.dart @@ -0,0 +1,36 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/balls.dart'; +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/events.dart'; +import 'package:flame/palette.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class TapCallbacksExample extends Forge2DGame { + static const String description = ''' + In this example we show how to use Flame's TapCallbacks mixin to react to + taps on `BodyComponent`s. + Tap the ball to give it a random impulse, or the text to add an effect to + it. + '''; + TapCallbacksExample() : super(zoom: 20, gravity: Vector2(0, 10.0)); + + @override + Future onLoad() async { + super.onLoad(); + final boundaries = createBoundaries(this); + world.addAll(boundaries); + world.add(TappableBall(Vector2.zero())); + } +} + +class TappableBall extends Ball with TapCallbacks { + TappableBall(super.position) { + originalPaint = BasicPalette.white.paint(); + paint = originalPaint; + } + + @override + void onTapDown(_) { + body.applyLinearImpulse(Vector2.random() * 1000); + paint = randomPaint(); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/utils/balls.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/utils/balls.dart new file mode 100644 index 0000000..4df873e --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/utils/balls.dart @@ -0,0 +1,108 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/palette.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; +import 'package:flutter/material.dart'; + +class Ball extends BodyComponent with ContactCallbacks { + late Paint originalPaint; + bool giveNudge = false; + final double radius; + final BodyType bodyType; + final Vector2 _position; + double _timeSinceNudge = 0.0; + static const double _minNudgeRest = 2.0; + + final Paint _blue = BasicPalette.blue.paint(); + + Ball( + this._position, { + this.radius = 2, + this.bodyType = BodyType.dynamic, + Color? color, + }) { + if (color != null) { + originalPaint = PaletteEntry(color).paint(); + } else { + originalPaint = randomPaint(); + } + paint = originalPaint; + } + + Paint randomPaint() => PaintExtension.random(withAlpha: 0.9, base: 100); + + @override + Body createBody() { + final shape = CircleShape(); + shape.radius = radius; + + final fixtureDef = FixtureDef( + shape, + restitution: 0.8, + friction: 0.4, + ); + + final bodyDef = BodyDef( + userData: this, + angularDamping: 0.8, + position: _position, + type: bodyType, + ); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } + + @override + void renderCircle(Canvas canvas, Offset center, double radius) { + super.renderCircle(canvas, center, radius); + final lineRotation = Offset(0, radius); + canvas.drawLine(center, center + lineRotation, _blue); + } + + final _impulseForce = Vector2(0, 1000); + + @override + @mustCallSuper + void update(double dt) { + _timeSinceNudge += dt; + if (giveNudge) { + giveNudge = false; + if (_timeSinceNudge > _minNudgeRest) { + body.applyLinearImpulse(_impulseForce); + _timeSinceNudge = 0.0; + } + } + } + + @override + void beginContact(Object other, Contact contact) { + if (other is Wall) { + other.paint = paint; + } + + if (other is WhiteBall) { + return; + } + + if (other is Ball) { + if (paint != originalPaint) { + paint = other.paint; + } else { + other.paint = paint; + } + } + } +} + +class WhiteBall extends Ball with ContactCallbacks { + WhiteBall(super.position) { + originalPaint = BasicPalette.white.paint(); + paint = originalPaint; + } + + @override + void beginContact(Object other, Contact contact) { + if (other is Ball) { + other.giveNudge = true; + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart new file mode 100644 index 0000000..7c02dc0 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart @@ -0,0 +1,39 @@ +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +List createBoundaries(Forge2DGame game, {double? strokeWidth}) { + final visibleRect = game.camera.visibleWorldRect; + final topLeft = visibleRect.topLeft.toVector2(); + final topRight = visibleRect.topRight.toVector2(); + final bottomRight = visibleRect.bottomRight.toVector2(); + final bottomLeft = visibleRect.bottomLeft.toVector2(); + + return [ + Wall(topLeft, topRight, strokeWidth: strokeWidth), + Wall(topRight, bottomRight, strokeWidth: strokeWidth), + Wall(bottomLeft, bottomRight, strokeWidth: strokeWidth), + Wall(topLeft, bottomLeft, strokeWidth: strokeWidth), + ]; +} + +class Wall extends BodyComponent { + final Vector2 start; + final Vector2 end; + final double strokeWidth; + + Wall(this.start, this.end, {double? strokeWidth}) + : strokeWidth = strokeWidth ?? 1; + + @override + Body createBody() { + final shape = EdgeShape()..set(start, end); + final fixtureDef = FixtureDef(shape, friction: 0.3); + final bodyDef = BodyDef( + userData: this, // To be able to determine object in collision + position: Vector2.zero(), + ); + paint.strokeWidth = strokeWidth; + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/utils/boxes.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/utils/boxes.dart new file mode 100644 index 0000000..d780462 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/utils/boxes.dart @@ -0,0 +1,95 @@ +import 'dart:ui'; + +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/palette.dart'; +import 'package:flame_forge2d/flame_forge2d.dart'; + +class Box extends BodyComponent { + final Vector2 startPosition; + final double width; + final double height; + final BodyType bodyType; + + Box({ + required this.startPosition, + required this.width, + required this.height, + this.bodyType = BodyType.dynamic, + Color? color, + }) { + if (color != null) { + paint = PaletteEntry(color).paint(); + } else { + paint = randomPaint(); + } + } + + Paint randomPaint() => PaintExtension.random(withAlpha: 0.9, base: 100); + + @override + Body createBody() { + final shape = PolygonShape() + ..setAsBox(width / 2, height / 2, Vector2.zero(), 0); + final fixtureDef = FixtureDef(shape, friction: 0.3, density: 10); + final bodyDef = BodyDef( + userData: this, // To be able to determine object in collision + position: startPosition, + type: bodyType, + ); + + return world.createBody(bodyDef)..createFixture(fixtureDef); + } +} + +class DraggableBox extends Box with DragCallbacks { + MouseJoint? mouseJoint; + late final groundBody = world.createBody(BodyDef()); + bool _destroyJoint = false; + + DraggableBox({ + required super.startPosition, + required super.width, + required super.height, + }); + + @override + void update(double dt) { + if (_destroyJoint && mouseJoint != null) { + world.destroyJoint(mouseJoint!); + mouseJoint = null; + _destroyJoint = false; + } + } + + @override + bool onDragUpdate(DragUpdateEvent info) { + final target = info.localEndPosition; + final mouseJointDef = MouseJointDef() + ..maxForce = body.mass * 300 + ..dampingRatio = 0 + ..frequencyHz = 20 + ..target.setFrom(body.position) + ..collideConnected = false + ..bodyA = groundBody + ..bodyB = body; + + if (mouseJoint == null) { + mouseJoint = MouseJoint(mouseJointDef); + world.createJoint(mouseJoint!); + } else { + mouseJoint?.setTarget(target); + } + return false; + } + + @override + void onDragEnd(DragEndEvent info) { + super.onDragEnd(info); + if (mouseJoint == null) { + return; + } + _destroyJoint = true; + info.continuePropagation = false; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/widget_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/widget_example.dart new file mode 100644 index 0000000..1c6b42d --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_forge2d/widget_example.dart @@ -0,0 +1,137 @@ +import 'package:examples/stories/bridge_libraries/flame_forge2d/utils/boundaries.dart'; +import 'package:flame/game.dart'; +import 'package:flame_forge2d/flame_forge2d.dart' hide Transform; +import 'package:flutter/material.dart'; + +class WidgetExample extends Forge2DGame { + static const String description = ''' + This examples shows how to render a widget on top of a Forge2D body outside + of Flame. + '''; + + final List updateStates = []; + final Map bodyIdMap = {}; + final List addLaterIds = []; + + WidgetExample() : super(zoom: 20, gravity: Vector2(0, 10.0)); + + @override + Future onLoad() async { + super.onLoad(); + final boundaries = createBoundaries(this, strokeWidth: 0); + world.addAll(boundaries); + } + + Body createBody() { + final bodyDef = BodyDef( + angularVelocity: 3, + position: Vector2.zero(), + type: BodyType.dynamic, + ); + final body = world.createBody(bodyDef); + + final shape = PolygonShape()..setAsBoxXY(4.6, 0.8); + final fixtureDef = FixtureDef( + shape, + restitution: 0.8, + friction: 0.2, + ); + body.createFixture(fixtureDef); + return body; + } + + int createBodyId(int id) { + addLaterIds.add(id); + return id; + } + + @override + void update(double dt) { + super.update(dt); + addLaterIds.forEach((id) { + if (!bodyIdMap.containsKey(id)) { + bodyIdMap[id] = createBody(); + } + }); + addLaterIds.clear(); + updateStates.forEach((f) => f()); + } +} + +class BodyWidgetExample extends StatelessWidget { + const BodyWidgetExample({super.key}); + + @override + Widget build(BuildContext context) { + return GameWidget( + game: WidgetExample(), + overlayBuilderMap: { + 'button1': (ctx, game) { + return BodyButtonWidget(game, game.createBodyId(1)); + }, + 'button2': (ctx, game) { + return BodyButtonWidget(game, game.createBodyId(2)); + }, + }, + initialActiveOverlays: const ['button1', 'button2'], + ); + } +} + +class BodyButtonWidget extends StatefulWidget { + final WidgetExample _game; + final int _bodyId; + + const BodyButtonWidget( + this._game, + this._bodyId, { + super.key, + }); + + @override + State createState() { + return _BodyButtonState(_game, _bodyId); + } +} + +class _BodyButtonState extends State { + final WidgetExample _game; + final int _bodyId; + Body? _body; + + _BodyButtonState(this._game, this._bodyId) { + _game.updateStates.add(() { + setState(() { + _body = _game.bodyIdMap[_bodyId]; + }); + }); + } + + @override + Widget build(BuildContext context) { + final body = _body; + if (body == null) { + return Container(); + } else { + final bodyPosition = _game.worldToScreen(body.position); + return Positioned( + top: bodyPosition.y - 18, + left: bodyPosition.x - 90, + child: Transform.rotate( + angle: body.angle, + child: ElevatedButton( + onPressed: () { + setState( + () => body.applyLinearImpulse(Vector2(0.0, 1000)), + ); + }, + child: const Text( + 'Flying button!', + textScaler: TextScaler.linear(2.0), + ), + ), + ), + ); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_isolate/isolate.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_isolate/isolate.dart new file mode 100644 index 0000000..f5df511 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_isolate/isolate.dart @@ -0,0 +1,17 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/bridge_libraries/flame_isolate/simple_isolate_example.dart'; +import 'package:flame/game.dart'; + +void addFlameIsolateExample(Dashbook dashbook) { + dashbook.storiesOf('FlameIsolate').add( + 'Simple isolate example', + (_) => GameWidget( + game: SimpleIsolateExample(), + ), + codeLink: baseLink( + 'bridge_libraries/flame_isolate/simple_isolate_example.dart', + ), + info: SimpleIsolateExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_isolate/simple_isolate_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_isolate/simple_isolate_example.dart new file mode 100644 index 0000000..59198f9 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_isolate/simple_isolate_example.dart @@ -0,0 +1,177 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame_isolate/flame_isolate.dart'; +import 'package:flutter/material.dart'; + +class SimpleIsolateExample extends FlameGame { + static const String description = ''' + This example showcases a simple FlameIsolate example, making it easy to + continually run heavy load without stutter. + + Tap the brown square to swap between running heavy load in an isolate or + synchronous. + + The selected backpressure strategy used for this example is + `DiscardNewBackPressureStrategy`. This strategy discards all new jobs added + to the queue if there is already a job in the queue. + '''; + + @override + Future onLoad() async { + final world = World(); + final cameraComponent = CameraComponent.withFixedResolution( + world: world, + width: 400, + height: 600, + ); + addAll([world, cameraComponent]); + + const rect = Rect.fromLTRB(-120, -120, 120, 120); + final circle = Path()..addOval(rect); + final teal = Paint()..color = Colors.tealAccent; + + for (var i = 0; i < 20; i++) { + world.add( + RectangleComponent.square(size: 10) + ..paint = teal + ..add( + MoveAlongPathEffect( + circle, + EffectController( + duration: 6, + startDelay: i * 0.3, + infinite: true, + ), + oriented: true, + ), + ), + ); + } + + world.add(CalculatePrimeNumber()); + } +} + +enum ComputeType { + isolate('Running in isolate'), + synchronous('Running synchronously'); + + final String description; + + const ComputeType(this.description); +} + +class CalculatePrimeNumber extends PositionComponent + with TapCallbacks, FlameIsolate { + CalculatePrimeNumber() : super(anchor: Anchor.center); + + ComputeType computeType = ComputeType.isolate; + late Timer _interval; + + @override + BackpressureStrategy get backpressureStrategy => + DiscardNewBackPressureStrategy(); + + @override + void onLoad() { + width = 200; + height = 70; + } + + @override + Future onMount() { + _interval = Timer(0.4, repeat: true, onTick: _checkNextAgainstPrime) + ..start(); + return super.onMount(); + } + + @override + void update(double dt) { + _interval.update(dt); + } + + @override + void onRemove() { + _interval.stop(); + super.onRemove(); + } + + static const _minStartValue = 500000000; + static const _maxStartValue = 600000000; + static final _primeStartNumber = + Random().nextInt(_maxStartValue - _minStartValue) + _minStartValue; + + MapEntry _primeData = MapEntry( + _primeStartNumber, + _isPrime(_primeStartNumber), + ); + + Future _checkNextAgainstPrime() async { + final nextInt = _primeData.key + 1; + + try { + final isPrime = switch (computeType) { + ComputeType.isolate => await isolateCompute(_isPrime, nextInt), + ComputeType.synchronous => _isPrime(nextInt), + }; + + _primeData = MapEntry(nextInt, isPrime); + } on BackpressureDropException catch (_) { + debugPrint('Backpressure kicked in'); + } + } + + @override + void onTapDown(_) { + computeType = + ComputeType.values[(computeType.index + 1) % ComputeType.values.length]; + } + + final _paint = Paint()..color = Colors.green; + + final _textPaint = TextPaint( + style: const TextStyle( + fontSize: 10, + ), + ); + + late final rect = Rect.fromLTWH(0, 0, width, height); + late final topLeftVector = rect.topLeft.toVector2() + Vector2.all(4); + late final centerVector = rect.center.toVector2(); + + @override + void render(Canvas canvas) { + canvas.drawRect(rect, _paint); + + _textPaint.render( + canvas, + computeType.description, + topLeftVector, + ); + + _textPaint.render( + canvas, + '${_primeData.key} is${_primeData.value ? '' : ' not'} a prime number', + centerVector, + anchor: Anchor.center, + ); + } +} + +bool _isPrime(int value) { + // Simulating heavy load + if (value == 1) { + return false; + } + for (var i = 2; i < value; ++i) { + if (value % i == 0) { + return false; + } + } + return true; +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/commons/commons.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/commons/commons.dart new file mode 100644 index 0000000..ae2f08c --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/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/examples/lib/stories/bridge_libraries/flame_jenny/'; + + return '$basePath$path'; +} + +const double fontSize = 24; diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/components/button_row.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/components/button_row.dart new file mode 100644 index 0000000..5252494 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/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/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/components/dialogue_box.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/components/dialogue_box.dart new file mode 100644 index 0000000..4820ce8 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/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/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/components/dialogue_button.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/components/dialogue_button.dart new file mode 100644 index 0000000..f685782 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/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/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/components/dialogue_controller_component.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/components/dialogue_controller_component.dart new file mode 100644 index 0000000..79580df --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/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/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/components/dialogue_text_box.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/components/dialogue_text_box.dart new file mode 100644 index 0000000..fe38ebf --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/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/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/components/menu_button.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/components/menu_button.dart new file mode 100644 index 0000000..de16196 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/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/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/jenny.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/jenny.dart new file mode 100644 index 0000000..e9d512e --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/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/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/jenny_advanced_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/jenny_advanced_example.dart new file mode 100644 index 0000000..c7686aa --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/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/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/jenny_simple_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_jenny/jenny_simple_example.dart new file mode 100644 index 0000000..7aacc08 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/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/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_lottie/lottie.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_lottie/lottie.dart new file mode 100644 index 0000000..329ec06 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_lottie/lottie.dart @@ -0,0 +1,17 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/bridge_libraries/flame_lottie/lottie_animation_example.dart'; +import 'package:flame/game.dart'; + +void addFlameLottieExample(Dashbook dashbook) { + dashbook.storiesOf('FlameLottie').add( + 'Lottie Animation example', + (_) => GameWidget( + game: LottieAnimationExample(), + ), + codeLink: baseLink( + 'bridge_libraries/flame_lottie/lottie_animation_example.dart', + ), + info: LottieAnimationExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_lottie/lottie_animation_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_lottie/lottie_animation_example.dart new file mode 100644 index 0000000..2fc4c3d --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_lottie/lottie_animation_example.dart @@ -0,0 +1,18 @@ +import 'package:flame/game.dart'; +import 'package:flame_lottie/flame_lottie.dart'; + +class LottieAnimationExample extends FlameGame { + static const String description = ''' + This example shows how to load a Lottie animation. It is configured to + continuously loop the animation and restart once its done. + '''; + + @override + Future onLoad() async { + final asset = await loadLottie( + Lottie.asset('assets/images/animations/lottieLogo.json'), + ); + + add(LottieComponent(asset, size: Vector2.all(400), repeating: true)); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_spine/basic_spine_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_spine/basic_spine_example.dart new file mode 100644 index 0000000..f92fc48 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_spine/basic_spine_example.dart @@ -0,0 +1,58 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame_spine/flame_spine.dart'; + +class FlameSpineExample extends FlameGame with TapDetector { + static const String description = ''' + This example shows how to load a Spine animation. Tap on the screen to try + different states of the animation. + '''; + + late final SpineComponent spineboy; + + final states = [ + 'walk', + 'aim', + 'death', + 'hoverboard', + 'idle', + 'jump', + 'portal', + 'run', + 'shoot', + ]; + + int _stateIndex = 0; + + @override + Future onLoad() async { + await initSpineFlutter(); + // Load the Spineboy atlas and skeleton data from asset files + // and create a SpineComponent from them, scaled down and + // centered on the screen + spineboy = await SpineComponent.fromAssets( + atlasFile: 'assets/spine/spineboy.atlas', + skeletonFile: 'assets/spine/spineboy-pro.skel', + scale: Vector2(0.4, 0.4), + anchor: Anchor.center, + position: Vector2(size.x / 2, size.y / 2), + ); + + // Set the "walk" animation on track 0 in looping mode + spineboy.animationState.setAnimationByName(0, 'walk', true); + await add(spineboy); + } + + @override + void onTap() { + _stateIndex = (_stateIndex + 1) % states.length; + spineboy.animationState.setAnimationByName(0, states[_stateIndex], true); + } + + @override + void onDetach() { + // Dispose the native resources that have been loaded for spineboy. + spineboy.dispose(); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_spine/flame_spine.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_spine/flame_spine.dart new file mode 100644 index 0000000..76e3e3d --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_spine/flame_spine.dart @@ -0,0 +1,28 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/bridge_libraries/flame_spine/basic_spine_example.dart'; +import 'package:examples/stories/bridge_libraries/flame_spine/shared_data_spine_example.dart'; +import 'package:flame/game.dart'; + +void addFlameSpineExamples(Dashbook dashbook) { + dashbook.storiesOf('FlameSpine') + ..add( + 'Basic Spine Animation', + (_) => GameWidget( + game: FlameSpineExample(), + ), + codeLink: + baseLink('bridge_libraries/flame_spine/basic_spine_example.dart'), + info: FlameSpineExample.description, + ) + ..add( + 'SpineComponent with shared data', + (_) => GameWidget( + game: SharedDataSpineExample(), + ), + codeLink: baseLink( + 'bridge_libraries/flame_spine/shared_data_spine_example.dart', + ), + info: SharedDataSpineExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_spine/shared_data_spine_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_spine/shared_data_spine_example.dart new file mode 100644 index 0000000..5b2a60b --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/bridge_libraries/flame_spine/shared_data_spine_example.dart @@ -0,0 +1,71 @@ +import 'dart:math'; + +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame_spine/flame_spine.dart'; + +class SharedDataSpineExample extends FlameGame with TapDetector { + static const String description = ''' + This example shows how to preload assets and share data between Spine + components. + '''; + + late final SkeletonData cachedSkeletonData; + late final Atlas cachedAtlas; + late final List spineboys = []; + + @override + Future onLoad() async { + await initSpineFlutter(); + // Pre-load the atlas and skeleton data once. + cachedAtlas = await Atlas.fromAsset('assets/spine/spineboy.atlas'); + cachedSkeletonData = await SkeletonData.fromAsset( + cachedAtlas, + 'assets/spine/spineboy-pro.skel', + ); + + // Instantiate many spineboys from the pre-loaded data. Each SpineComponent + // gets their own SkeletonDrawable copy derived from the cached data. The + // SkeletonDrawable copies do not own the underlying skeleton data and + // atlas. + final rng = Random(); + for (var i = 0; i < 100; i++) { + final drawable = SkeletonDrawable(cachedAtlas, cachedSkeletonData, false); + final scale = 0.1 + rng.nextDouble() * 0.2; + final position = Vector2.random(rng)..multiply(size); + final spineboy = SpineComponent( + drawable, + scale: Vector2.all(scale), + position: position, + ); + spineboy.animationState.setAnimationByName(0, 'walk', true); + spineboys.add(spineboy); + } + await addAll(spineboys); + } + + @override + void onTap() { + for (final spineboy in spineboys) { + spineboy.animationState.setAnimationByName(0, 'jump', false); + spineboy.animationState.setListener((type, track, event) { + if (type == EventType.complete) { + spineboy.animationState.setAnimationByName(0, 'walk', true); + } + }); + } + } + + @override + void onDetach() { + // Dispose the pre-loaded atlas and skeleton data when the game/scene is + // removed. + cachedAtlas.dispose(); + cachedSkeletonData.dispose(); + + // Dispose each spineboy and its internal SkeletonDrawable. + for (final spineboy in spineboys) { + spineboy.dispose(); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/camera_and_viewport.dart b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/camera_and_viewport.dart new file mode 100644 index 0000000..bd84ebc --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/camera_and_viewport.dart @@ -0,0 +1,97 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/camera_and_viewport/camera_component_example.dart'; +import 'package:examples/stories/camera_and_viewport/camera_component_properties_example.dart'; +import 'package:examples/stories/camera_and_viewport/camera_follow_and_world_bounds.dart'; +import 'package:examples/stories/camera_and_viewport/coordinate_systems_example.dart'; +import 'package:examples/stories/camera_and_viewport/fixed_resolution_example.dart'; +import 'package:examples/stories/camera_and_viewport/follow_component_example.dart'; +import 'package:examples/stories/camera_and_viewport/static_components_example.dart'; +import 'package:examples/stories/camera_and_viewport/zoom_example.dart'; +import 'package:flame/game.dart'; + +void addCameraAndViewportStories(Dashbook dashbook) { + dashbook.storiesOf('Camera & Viewport') + ..add( + 'Follow Component', + (context) { + return GameWidget( + game: FollowComponentExample( + viewportResolution: Vector2( + context.numberProperty('viewport width', 500), + context.numberProperty('viewport height', 500), + ), + ), + ); + }, + codeLink: baseLink('camera_and_viewport/follow_component_example.dart'), + info: FollowComponentExample.description, + ) + ..add( + 'Zoom', + (context) { + return GameWidget( + game: ZoomExample( + viewportResolution: Vector2( + context.numberProperty('viewport width', 500), + context.numberProperty('viewport height', 500), + ), + ), + ); + }, + codeLink: baseLink('camera_and_viewport/zoom_example.dart'), + info: ZoomExample.description, + ) + ..add( + 'Fixed Resolution viewport', + (context) { + return const GameWidget.controlled( + gameFactory: FixedResolutionExample.new, + ); + }, + codeLink: baseLink('camera_and_viewport/fixed_resolution_example.dart'), + info: FixedResolutionExample.description, + ) + ..add( + 'HUDs and static components', + (context) { + return GameWidget( + game: StaticComponentsExample( + viewportResolution: Vector2( + context.numberProperty('viewport width', 500), + context.numberProperty('viewport height', 500), + ), + ), + ); + }, + codeLink: baseLink('camera_and_viewport/static_components_example.dart'), + info: StaticComponentsExample.description, + ) + ..add( + 'Coordinate Systems', + (context) => const CoordinateSystemsWidget(), + codeLink: baseLink('camera_and_viewport/coordinate_systems_example.dart'), + info: CoordinateSystemsExample.description, + ) + ..add( + 'CameraComponent', + (context) => GameWidget(game: CameraComponentExample()), + codeLink: baseLink('camera_and_viewport/camera_component_example.dart'), + info: CameraComponentExample.description, + ) + ..add( + 'CameraComponent properties', + (context) => GameWidget(game: CameraComponentPropertiesExample()), + codeLink: baseLink( + 'camera_and_viewport/camera_component_properties_example.dart', + ), + info: CameraComponentPropertiesExample.description, + ) + ..add( + 'Follow and World bounds', + (_) => GameWidget(game: CameraFollowAndWorldBoundsExample()), + codeLink: + baseLink('camera_and_viewport/camera_follow_and_world_bounds.dart'), + info: CameraFollowAndWorldBoundsExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/camera_component_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/camera_component_example.dart new file mode 100644 index 0000000..f9e87cc --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/camera_component_example.dart @@ -0,0 +1,543 @@ +import 'dart:math'; + +import 'package:flame/camera.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart' show OffsetExtension; +import 'package:flame/game.dart'; +import 'package:flame/geometry.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/painting.dart'; + +class CameraComponentExample extends FlameGame with PanDetector { + static const description = ''' + This example shows how a camera can be dynamically added into a game using + a CameraComponent. + + Click and hold the mouse to bring up a magnifying glass, then have a better + look at the world underneath! + '''; + + late final CameraComponent magnifyingGlass; + late final Vector2 center; + static const zoom = 10.0; + static const radius = 130.0; + + @override + Color backgroundColor() => const Color(0xFFeeeeee); + + @override + Future onLoad() async { + final world = AntWorld(); + await add(world); + final camera = CameraComponent(world: world); + await add(camera); + final offset = world.curve.boundingRect().center; + center = offset.toVector2(); + camera.viewfinder.position = Vector2(center.x, center.y); + + magnifyingGlass = + CameraComponent(world: world, viewport: CircularViewport(radius)); + magnifyingGlass.viewport.add(Bezel(radius)); + magnifyingGlass.viewfinder.zoom = zoom; + } + + @override + bool onPanStart(DragStartInfo info) { + _updateMagnifyingGlassPosition(info.eventPosition.widget); + add(magnifyingGlass); + return false; + } + + @override + bool onPanUpdate(DragUpdateInfo info) { + _updateMagnifyingGlassPosition(info.eventPosition.widget); + return false; + } + + @override + bool onPanEnd(DragEndInfo info) { + onPanCancel(); + return false; + } + + @override + bool onPanCancel() { + remove(magnifyingGlass); + return false; + } + + void _updateMagnifyingGlassPosition(Vector2 point) { + // [point] is in the canvas coordinate system. + magnifyingGlass + ..viewport.position = point - Vector2.all(radius) + ..viewfinder.position = point - canvasSize / 2 + center; + } +} + +class Bezel extends PositionComponent { + Bezel(this.radius) + : super( + size: Vector2.all(2 * radius), + position: Vector2.all(radius), + ); + + final double radius; + late final Path rim; + late final Path rimBorder; + late final Path handle; + late final Path connector; + late final Path specularHighlight; + static const rimWidth = 20.0; + static const handleWidth = 40.0; + static const handleLength = 100.0; + late final Paint glassPaint; + late final Paint rimPaint; + late final Paint rimBorderPaint; + late final Paint handlePaint; + late final Paint connectorPaint; + late final Paint specularPaint; + + @override + void onLoad() { + rim = Path()..addOval(Rect.fromLTRB(-radius, -radius, radius, radius)); + final outer = radius + rimWidth / 2; + final inner = radius - rimWidth / 2; + rimBorder = Path() + ..addOval(Rect.fromLTRB(-outer, -outer, outer, outer)) + ..addOval(Rect.fromLTRB(-inner, -inner, inner, inner)); + handle = (Path() + ..addRRect( + RRect.fromLTRBR( + radius, + -handleWidth / 2, + handleLength + radius, + handleWidth / 2, + const Radius.circular(5.0), + ), + )) + .transform((Matrix4.identity()..rotateZ(pi / 4)).storage); + connector = (Path() + ..addArc(Rect.fromLTRB(-outer, -outer, outer, outer), -0.22, 0.44)) + .transform((Matrix4.identity()..rotateZ(pi / 4)).storage); + specularHighlight = (Path() + ..addOval(Rect.fromLTWH(-radius * 0.8, -8, 16, radius * 0.3))) + .transform((Matrix4.identity()..rotateZ(pi / 4)).storage); + + glassPaint = Paint()..color = const Color(0x1400ffae); + rimBorderPaint = Paint() + ..strokeWidth = 1.0 + ..style = PaintingStyle.stroke + ..color = const Color(0xff61382a); + rimPaint = Paint() + ..strokeWidth = rimWidth + ..style = PaintingStyle.stroke + ..color = const Color(0xffffdf70); + connectorPaint = Paint() + ..strokeWidth = 20.0 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..color = const Color(0xff654510); + handlePaint = Paint()..color = const Color(0xffdbbf9f); + specularPaint = Paint() + ..color = const Color(0xccffffff) + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2); + } + + @override + void render(Canvas canvas) { + canvas.drawPath(rim, glassPaint); + canvas.drawPath(specularHighlight, specularPaint); + canvas.drawPath(handle, handlePaint); + canvas.drawPath(handle, rimBorderPaint); + canvas.drawPath(connector, connectorPaint); + canvas.drawPath(rim, rimPaint); + canvas.drawPath(rimBorder, rimBorderPaint); + } +} + +class AntWorld extends World { + late final DragonCurve curve; + late final Rect bgRect; + final Paint bgPaint = Paint()..color = const Color(0xffeeeeee); + + @override + Future onLoad() async { + final random = Random(); + curve = DragonCurve(); + await add(curve); + bgRect = curve.boundingRect().inflate(100); + + const baseColor = HSVColor.fromAHSV(1, 38.5, 0.63, 0.68); + for (var i = 0; i < 20; i++) { + add( + Ant() + ..color = baseColor.withHue(random.nextDouble() * 360).toColor() + ..scale = Vector2.all(.4) + ..setTravelPath(curve.path), + ); + } + } + + @override + void render(Canvas canvas) { + // Render white backdrop, to prevent the world in the magnifying glass from + // being "see-through" + canvas.drawRect(bgRect, bgPaint); + } +} + +class DragonCurve extends PositionComponent { + late final Paint borderPaint; + late final Paint mainPaint; + late final Path dragon; + late List path; + static const cellSize = 20.0; + static const notchSize = 4.0; + + void initPath() { + path = [ + Vector2(0, cellSize - notchSize), + Vector2(0, notchSize), + ]; + final endPoint = Vector2(0, cellSize); + final transform = Transform2D()..angleDegrees = -90; + for (var i = 0; i < 8; i++) { + path += List.from(path.reversed.map(transform.localToGlobal)); + final pivot = transform.localToGlobal(endPoint); + transform + ..position = pivot + ..offset = -pivot; + } + } + + Rect boundingRect() { + var minX = double.infinity; + var minY = double.infinity; + var maxX = -double.infinity; + var maxY = -double.infinity; + for (final point in path) { + minX = min(minX, point.x); + minY = min(minY, point.y); + maxX = max(maxX, point.x); + maxY = max(maxY, point.y); + } + return Rect.fromLTRB(minX, minY, maxX, maxY); + } + + @override + Future onLoad() async { + initPath(); + borderPaint = Paint() + ..color = const Color(0xFF041D1F) + ..style = PaintingStyle.stroke + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 0.3) + ..strokeWidth = 4; + mainPaint = Paint() + ..color = const Color(0xffefe79c) + ..style = PaintingStyle.stroke + ..strokeWidth = 3.6; + + dragon = Path()..moveTo(path[0].x, path[0].y); + for (final p in path) { + dragon.lineTo(p.x, p.y); + } + } + + @override + void render(Canvas canvas) { + canvas.drawPath(dragon, borderPaint); + canvas.drawPath(dragon, mainPaint); + } +} + +class Ant extends PositionComponent { + Ant() : random = Random() { + size = Vector2(2, 5); + anchor = const Anchor(0.5, 0.4); + } + + late final Color color; + final Random random; + static const black = Color(0xFF000000); + late final Paint bodyPaint; + late final Paint eyesPaint; + late final Paint legsPaint; + late final Paint facePaint; + late final Paint borderPaint; + late final Path head; + late final Path body; + late final Path pincers; + late final Path eyes; + late final Path antennae; + late final List legs; + Vector2 destinationPosition = Vector2.zero(); + double destinationAngle = 0; + double movementTime = 0; + double rotationTime = 0; + double stepTime = 0; + double movementSpeed = 3; // mm/s + double rotationSpeed = 3; // angle/s + double probabilityToChangeDirection = 0.02; + bool moveLeftSide = false; + List targetLegsPositions = List.generate(6, (_) => Vector2.zero()); + List travelPath = []; + int travelPathNodeIndex = 0; + int travelDirection = 1; // +1 or -1 + + bool legIsMoving(int i) => moveLeftSide == (i < 3); + + void setTravelPath(List path) { + travelPath = path; + travelPathNodeIndex = random.nextInt(path.length - 1); + travelDirection = 1; + position = travelPath[travelPathNodeIndex]; + destinationPosition = travelPath[travelPathNodeIndex + travelDirection]; + angle = -(destinationPosition - position).angleToSigned(Vector2(0, -1)); + destinationAngle = angle; + } + + @override + Future onLoad() async { + bodyPaint = Paint()..color = color; + eyesPaint = Paint()..color = black; + borderPaint = Paint() + ..color = Color.lerp(color, black, 0.6)! + ..style = PaintingStyle.stroke + ..strokeWidth = 0.06; + legsPaint = Paint() + ..color = Color.lerp(color, black, 0.4)! + ..style = PaintingStyle.stroke + ..strokeWidth = 0.2; + facePaint = Paint() + ..color = Color.lerp(color, black, 0.5)! + ..style = PaintingStyle.stroke + ..strokeWidth = 0.05; + head = Path() + ..moveTo(0, -0.3) + ..cubicTo(-0.5, -0.3, -0.7, -0.6, -0.7, -1) + ..cubicTo(-0.7, -1.3, -0.3, -2, 0, -2) + ..cubicTo(0.3, -2, 0.7, -1.3, 0.7, -1) + ..cubicTo(0.7, -0.6, 0.5, -0.3, 0, -0.3) + ..close(); + body = Path() + ..moveTo(0, -0.3) + ..cubicTo(0.2, -0.3, 0.4, -0.2, 0.4, 0.2) + ..cubicTo(0.4, 0.4, 0.25, 1, 0, 1) + ..cubicTo(0.6, 1, 0.9, 1.4, 0.9, 1.8) + ..cubicTo(0.9, 2.6, 0.35, 3.1, 0, 3.1) + ..cubicTo(-0.35, 3.1, -0.9, 2.6, -0.9, 1.8) + ..cubicTo(-0.9, 1.4, -0.6, 1, 0, 1) + ..cubicTo(-0.25, 1, -0.4, 0.4, -0.4, 0.2) + ..cubicTo(-0.4, -0.2, -0.2, -0.3, 0, -0.3) + ..close(); + pincers = Path() + ..moveTo(0.15, -2.15) + ..cubicTo(0.5, -1.5, -0.5, -1.5, -0.15, -2.15) + ..cubicTo(-0.3, -1.8, 0.3, -1.8, 0.15, -2.15) + ..close(); + antennae = Path() + ..moveTo(0, -1.7) + ..lineTo(-0.7, -1.9) + ..lineTo(-1, -2.5) + ..lineTo(-1.5, -2.6) + ..moveTo(0, -1.7) + ..lineTo(0.7, -1.9) + ..lineTo(1, -2.5) + ..lineTo(1.5, -2.6); + eyes = Path() + ..moveTo(-0.5, -1.1) + ..cubicTo(-0.95, -1.1, -0.6, -1.8, -0.3, -1.8) + ..cubicTo(0, -1.8, 0, -1.1, -0.5, -1.1) + ..moveTo(0.5, -1.1) + ..cubicTo(0.95, -1.1, 0.6, -1.8, 0.3, -1.8) + ..cubicTo(0, -1.8, 0, -1.1, 0.5, -1.1) + ..close(); + legs = [ + InsectLeg(-0.3, 0.4, -2.6, 0.6, 1.1, 1.1, 0.5, mirrorBendDirection: true), + InsectLeg(-0.2, 0.7, -2.3, 2.6, 1.5, 1.5, 0.6, mirrorBendDirection: true), + InsectLeg(0.3, 0, 1.7, -2.3, 1.5, 1.3, 0.6, mirrorBendDirection: true), + InsectLeg(0.3, 0.4, 2.6, 0.6, 1.1, 1.1, 0.5, mirrorBendDirection: false), + InsectLeg(0.2, 0.7, 2.3, 2.6, 1.5, 1.5, 0.6, mirrorBendDirection: false), + InsectLeg(-0.3, 0, -1.7, -2.3, 1.5, 1.3, 0.6, mirrorBendDirection: false), + ]; + } + + @override + void update(double dt) { + super.update(dt); + if (movementTime <= 0 && rotationTime <= 0) { + planNextMove(); + } + if (stepTime <= 0) { + planNextStep(); + } + final feetPositions = [for (final leg in legs) positionOf(leg.foot)]; + final fMove = movementTime > 0 ? min(dt / movementTime, 1) : 0; + final fRot = rotationTime > 0 ? min(dt / rotationTime, 1) : 0; + final deltaX = (destinationPosition.x - position.x) * fMove; + final deltaY = (destinationPosition.y - position.y) * fMove; + final deltaA = (destinationAngle - angle) * fRot; + position += Vector2(deltaX, deltaY); + angle += deltaA; + movementTime -= dt; + rotationTime -= dt; + for (var i = 0; i < 6; i++) { + var newFootPosition = feetPositions[i]; + if (legIsMoving(i)) { + final fStep = min(dt / stepTime, 1.0); + final targetPosition = targetLegsPositions[i]; + newFootPosition += (targetPosition - newFootPosition) * fStep; + } + legs[i].placeFoot(toLocal(newFootPosition)); + } + stepTime -= dt; + } + + void planNextStep() { + moveLeftSide = !moveLeftSide; + stepTime = 0.1; + final f = min(stepTime * 1.6 / movementTime, 1.0); + final deltaX = (destinationPosition.x - position.x) * f; + final deltaY = (destinationPosition.y - position.y) * f; + final deltaA = (destinationAngle - angle) * f; + position += Vector2(deltaX, deltaY); + angle += deltaA; + for (var i = 0; i < 6; i++) { + if (legIsMoving(i)) { + targetLegsPositions[i].setFrom( + positionOf(Vector2(legs[i].x1, legs[i].y1)), + ); + } + } + position -= Vector2(deltaX, deltaY); + angle -= deltaA; + } + + void planNextMove() { + if (travelPathNodeIndex == 0) { + travelDirection = 1; + } else if (travelPathNodeIndex == travelPath.length - 1) { + travelDirection = -1; + } else if (random.nextDouble() < probabilityToChangeDirection) { + travelDirection = -travelDirection; + } + final nextIndex = travelPathNodeIndex + travelDirection; + assert( + nextIndex >= 0 && nextIndex < travelPath.length, + 'nextIndex is outside of the bounds of travelPath', + ); + final nextPosition = travelPath[nextIndex]; + var nextAngle = + angle = -(nextPosition - position).angleToSigned(Vector2(0, -1)); + if (nextAngle - angle > tau / 2) { + nextAngle -= tau; + } + if (nextAngle - angle < -tau / 2) { + nextAngle += tau; + } + if ((nextAngle - angle).abs() > 1) { + destinationPosition = position; + destinationAngle = nextAngle; + } else { + destinationPosition = nextPosition; + destinationAngle = nextAngle; + travelPathNodeIndex = nextIndex; + } + rotationTime = (destinationAngle - angle) / rotationSpeed; + movementTime = (destinationPosition - position).length / movementSpeed; + } + + @override + void render(Canvas canvas) { + super.render(canvas); + canvas + ..save() + ..translate(1, 2) + ..drawPath(pincers, facePaint) + ..drawPath(antennae, facePaint) + ..drawPath(head, bodyPaint) + ..drawPath(head, borderPaint) + ..drawPath(eyes, eyesPaint) + ..drawPath(legs[0].path, legsPaint) + ..drawPath(legs[1].path, legsPaint) + ..drawPath(legs[2].path, legsPaint) + ..drawPath(legs[3].path, legsPaint) + ..drawPath(legs[4].path, legsPaint) + ..drawPath(legs[5].path, legsPaint) + ..drawPath(body, bodyPaint) + ..drawPath(body, borderPaint) + ..restore(); + } +} + +class InsectLeg { + InsectLeg( + this.x0, + this.y0, + this.x1, + this.y1, + this.l1, + this.l2, + this.l3, { + required bool mirrorBendDirection, + }) : dir = mirrorBendDirection ? -1 : 1, + path = Path(), + foot = Vector2.zero() { + final ok = placeFoot(Vector2(x1, y1)); + assert(ok, 'The foot was not properly placed'); + } + + /// Place where the leg is attached to the body + final double x0; + final double y0; + + /// Place on the ground where the ant needs to place its foot + final double x1; + final double y1; + + /// Lengths of the 3 segments of the leg: [l1] is nearest to the body, [l2] + /// is the middle part, and [l3] is the "foot". + final double l1; + final double l2; + final double l3; + + /// +1 if the leg bends "forward", or -1 if backwards + final double dir; + + /// The leg is drawn as a simple [path] polyline consisting of 3 segments. + final Path path; + + /// This vector stores the position of the foot; it's equal to (x1, y1). + final Vector2 foot; + + bool placeFoot(Vector2 pos) { + final r = l3 / 2; + final rr = distance(pos.x, pos.y, x0, y0); + if (rr < r) { + return false; + } + final d = rr - r; + final z = (d * d + l1 * l1 - l2 * l2) / (2 * d); + if (z > l1) { + return false; + } + final h = sqrt(l1 * l1 - z * z); + final xv = (pos.x - x0) / rr; + final yv = (pos.y - y0) / rr; + path + ..reset() + ..moveTo(x0, y0) + ..lineTo(x0 + xv * z + dir * yv * h, y0 + yv * z - dir * xv * h) + ..lineTo(x0 + xv * (rr - r), y0 + yv * (rr - r)) + ..lineTo(x0 + xv * (rr + r), y0 + yv * (rr + r)); + foot.setFrom(pos); + return true; + } +} + +double distance(num x0, num y0, num x1, num y1) { + final dx = x1 - x0; + final dy = y1 - y0; + return sqrt(dx * dx + dy * dy); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/camera_component_properties_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/camera_component_properties_example.dart new file mode 100644 index 0000000..0c9b22b --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/camera_component_properties_example.dart @@ -0,0 +1,153 @@ +import 'dart:ui'; + +import 'package:flame/camera.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; + +class CameraComponentPropertiesExample extends FlameGame { + static const description = ''' + This example uses FixedSizeViewport which is dynamically sized and + positioned based on the size of the game widget. + + The underlying world is represented as a simple coordinate plane, with + green dot being the origin. The viewfinder uses custom anchor in order to + declare its "center" half-way between the bottom left corner and the true + center. + + The thin yellow rectangle shows the camera's [visibleWorldRect]. It should + be visible along the edge of the viewport. + + Click at any point within the viewport to create a circle there. + '''; + + CameraComponentPropertiesExample() + : super( + camera: CameraComponent( + viewport: FixedSizeViewport(200, 200)..add(ViewportFrame()), + ) + ..viewfinder.zoom = 5 + ..viewfinder.anchor = const Anchor(0.25, 0.75), + ); + + late RectangleComponent _cullRect; + + @override + Color backgroundColor() => const Color(0xff333333); + + @override + Future onLoad() async { + world.add(Background()); + _cullRect = RectangleComponent.fromRect( + Rect.zero, + paint: Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 0.25 + ..color = const Color(0xaaffff00), + ); + await world.add(_cullRect); + camera.mounted.then((_) { + updateSize(canvasSize); + }); + } + + @override + void onGameResize(Vector2 size) { + super.onGameResize(size); + if (camera.isMounted) { + updateSize(size); + } + } + + void updateSize(Vector2 size) { + camera.viewport.anchor = Anchor.center; + camera.viewport.size = size * 0.7; + camera.viewport.position = size * 0.6; + _cullRect.position = Vector2( + camera.visibleWorldRect.left + 1, + camera.visibleWorldRect.top + 1, + ); + _cullRect.size = Vector2( + camera.visibleWorldRect.width - 2, + camera.visibleWorldRect.height - 2, + ); + } +} + +class ViewportFrame extends Component { + final paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 3 + ..color = const Color(0xff87c4e2); + + @override + void render(Canvas canvas) { + final size = (parent! as Viewport).size; + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(0, 0, size.x, size.y), + const Radius.circular(5), + ), + paint, + ); + } +} + +class Background extends Component with TapCallbacks { + final bgPaint = Paint()..color = const Color(0xffff0000); + final originPaint = Paint()..color = const Color(0xff19bf57); + final axisPaint = Paint() + ..strokeWidth = 1 + ..style = PaintingStyle.stroke + ..color = const Color(0xff878787); + final gridPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 0 + ..color = const Color(0xff555555); + + @override + void render(Canvas canvas) { + canvas.drawColor(const Color(0xff000000), BlendMode.src); + for (var i = -100.0; i <= 100.0; i += 10) { + canvas.drawLine(Offset(i, -100), Offset(i, 100), gridPaint); + canvas.drawLine(Offset(-100, i), Offset(100, i), gridPaint); + } + canvas.drawLine(Offset.zero, const Offset(0, 10), axisPaint); + canvas.drawLine(Offset.zero, const Offset(10, 0), axisPaint); + canvas.drawCircle(Offset.zero, 1.0, originPaint); + } + + @override + bool containsLocalPoint(Vector2 point) => true; + + @override + void onTapDown(TapDownEvent event) { + add(ExpandingCircle(event.localPosition.toOffset())); + } +} + +class ExpandingCircle extends CircleComponent { + ExpandingCircle(Offset center) + : super( + position: Vector2(center.dx, center.dy), + anchor: Anchor.center, + radius: 0, + paint: Paint() + ..color = const Color(0xffffffff) + ..style = PaintingStyle.stroke + ..strokeWidth = 1, + ); + + static const maxRadius = 50; + + @override + void update(double dt) { + radius += dt * 10; + if (radius >= maxRadius) { + removeFromParent(); + } else { + final opacity = 1 - radius / maxRadius; + paint.color = const Color(0xffffffff).withOpacity(opacity); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/camera_follow_and_world_bounds.dart b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/camera_follow_and_world_bounds.dart new file mode 100644 index 0000000..5b24773 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/camera_follow_and_world_bounds.dart @@ -0,0 +1,188 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/services.dart'; + +class CameraFollowAndWorldBoundsExample extends FlameGame + with HasKeyboardHandlerComponents { + static const description = ''' + This example demonstrates camera following the player, but also obeying the + world bounds (which are set up to leave a small margin around the visible + part of the ground). + + Use arrows or keys W,A,D to move the player around. The camera should follow + the player horizontally, but not jump with the player. + '''; + + @override + Future onLoad() async { + final player = Player()..position = Vector2(250, 0); + camera + ..viewfinder.visibleGameSize = Vector2(400, 100) + ..follow(player, horizontalOnly: true) + ..setBounds(Rectangle.fromLTRB(190, -50, 810, 50)); + world.add(Ground()); + world.add(player); + } +} + +class Ground extends PositionComponent { + Ground() + : pebbles = [], + super(size: Vector2(1000, 30)) { + final random = Random(); + for (var i = 0; i < 25; i++) { + pebbles.add( + Vector3( + random.nextDouble() * size.x, + random.nextDouble() * size.y / 3, + random.nextDouble() * 0.5 + 1, + ), + ); + } + } + + final Paint groundPaint = Paint() + ..shader = Gradient.linear( + Offset.zero, + const Offset(0, 30), + [const Color(0xFFC9C972), const Color(0x22FFFF88)], + ); + final Paint pebblePaint = Paint()..color = const Color(0xFF685A2B); + + final List pebbles; + + @override + void render(Canvas canvas) { + canvas.drawRect(size.toRect(), groundPaint); + for (final pebble in pebbles) { + canvas.drawCircle(Offset(pebble.x, pebble.y), pebble.z, pebblePaint); + } + } +} + +class Player extends PositionComponent with KeyboardHandler { + Player() + : body = Path() + ..moveTo(10, 0) + ..cubicTo(17, 0, 28, 20, 10, 20) + ..cubicTo(-8, 20, 3, 0, 10, 0) + ..close(), + eyes = Path() + ..addOval(const Rect.fromLTWH(12.5, 9, 4, 6)) + ..addOval(const Rect.fromLTWH(6.5, 9, 4, 6)), + pupils = Path() + ..addOval(const Rect.fromLTWH(14, 11, 2, 2)) + ..addOval(const Rect.fromLTWH(8, 11, 2, 2)), + velocity = Vector2.zero(), + super(size: Vector2(20, 20), anchor: Anchor.bottomCenter); + + final Path body; + final Path eyes; + final Path pupils; + final Paint borderPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..color = const Color(0xffffc67c); + final Paint innerPaint = Paint()..color = const Color(0xff9c0051); + final Paint eyesPaint = Paint()..color = const Color(0xFFFFFFFF); + final Paint pupilsPaint = Paint()..color = const Color(0xFF000000); + final Paint shadowPaint = Paint() + ..shader = Gradient.radial( + Offset.zero, + 10, + [const Color(0x88000000), const Color(0x00000000)], + ); + + final Vector2 velocity; + final double runSpeed = 150.0; + final double jumpSpeed = 300.0; + final double gravity = 1000.0; + bool facingRight = true; + int nJumpsLeft = 2; + + @override + void update(double dt) { + position.x += velocity.x * dt; + position.y += velocity.y * dt; + if (position.y > 0) { + position.y = 0; + velocity.y = 0; + nJumpsLeft = 2; + } + if (position.y < 0) { + velocity.y += gravity * dt; + } + if (position.x < 0) { + position.x = 0; + } + if (position.x > 1000) { + position.x = 1000; + } + } + + @override + void render(Canvas canvas) { + { + final h = -position.y; // height above the ground + canvas.save(); + canvas.translate(width / 2, height + 1 + h * 1.05); + canvas.scale(1 - h * 0.003, 0.3 - h * 0.001); + canvas.drawCircle(Offset.zero, 10, shadowPaint); + canvas.restore(); + } + canvas.drawPath(body, innerPaint); + canvas.drawPath(body, borderPaint); + canvas.drawPath(eyes, eyesPaint); + canvas.drawPath(pupils, pupilsPaint); + } + + @override + bool onKeyEvent(KeyEvent event, Set keysPressed) { + final isKeyDown = event is KeyDownEvent; + final keyLeft = (event.logicalKey == LogicalKeyboardKey.arrowLeft) || + (event.logicalKey == LogicalKeyboardKey.keyA); + final keyRight = (event.logicalKey == LogicalKeyboardKey.arrowRight) || + (event.logicalKey == LogicalKeyboardKey.keyD); + final keyUp = (event.logicalKey == LogicalKeyboardKey.arrowUp) || + (event.logicalKey == LogicalKeyboardKey.keyW); + + if (isKeyDown) { + if (keyLeft) { + velocity.x = -runSpeed; + } else if (keyRight) { + velocity.x = runSpeed; + } else if (keyUp && nJumpsLeft > 0) { + velocity.y = -jumpSpeed; + nJumpsLeft -= 1; + } + } else { + final hasLeft = keysPressed.contains(LogicalKeyboardKey.arrowLeft) || + keysPressed.contains(LogicalKeyboardKey.keyA); + final hasRight = keysPressed.contains(LogicalKeyboardKey.arrowRight) || + keysPressed.contains(LogicalKeyboardKey.keyD); + if (hasLeft && hasRight) { + // Leave the current speed unchanged + } else if (hasLeft) { + velocity.x = -runSpeed; + } else if (hasRight) { + velocity.x = runSpeed; + } else { + velocity.x = 0; + } + } + if ((velocity.x > 0) && !facingRight) { + facingRight = true; + flipHorizontally(); + } + if ((velocity.x < 0) && facingRight) { + facingRight = false; + flipHorizontally(); + } + return super.onKeyEvent(event, keysPressed); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/coordinate_systems_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/coordinate_systems_example.dart new file mode 100644 index 0000000..2d47cea --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/coordinate_systems_example.dart @@ -0,0 +1,262 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// A game that allows for camera control and displays Tap, Drag & Scroll +/// events information on the screen, to allow exploration of the 3 coordinate +/// systems of Flame (global, widget, game). +class CoordinateSystemsExample extends FlameGame + with + MultiTouchTapDetector, + MultiTouchDragDetector, + ScrollDetector, + KeyboardEvents { + static const String description = ''' + Displays event data in all 3 coordinate systems (global, widget and game). + Use WASD to move the camera and Q/E to zoom in/out. + Trigger events to see the coordinates on each coordinate space. + '''; + + static final _borderPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2 + ..color = BasicPalette.red.color; + static final _text = TextPaint( + style: TextStyle(color: BasicPalette.red.color, fontSize: 12), + ); + + String? lastEventDescription; + final cameraPosition = Vector2.zero(); + final cameraVelocity = Vector2.zero(); + + @override + Future onLoad() async { + final rectanglePosition = canvasSize / 4; + final rectangleSize = Vector2.all(20); + final positions = [ + Vector2(rectanglePosition.x, rectanglePosition.y), + Vector2(rectanglePosition.x, -rectanglePosition.y), + Vector2(-rectanglePosition.x, rectanglePosition.y), + Vector2(-rectanglePosition.x, -rectanglePosition.y), + ]; + world.addAll( + [ + for (final position in positions) + RectangleComponent( + position: position, + size: rectangleSize, + ), + ], + ); + } + + @override + void render(Canvas canvas) { + canvas.drawRect(canvasSize.toRect(), _borderPaint); + _text.render( + canvas, + 'Camera: WASD to move, QE to zoom', + Vector2.all(5.0), + ); + _text.render( + canvas, + 'Camera: ${camera.viewfinder.position}, ' + 'zoom: ${camera.viewfinder.zoom}', + Vector2(canvasSize.x - 5, 5.0), + anchor: Anchor.topRight, + ); + _text.render( + canvas, + 'This is your Flame game!', + canvasSize - Vector2.all(5.0), + anchor: Anchor.bottomRight, + ); + final lastEventDescription = this.lastEventDescription; + if (lastEventDescription != null) { + _text.render( + canvas, + lastEventDescription, + canvasSize / 2, + anchor: Anchor.center, + ); + } + super.render(canvas); + } + + @override + void onTapUp(int pointerId, TapUpInfo info) { + lastEventDescription = _describe('TapUp', info); + } + + @override + void onTapDown(int pointerId, TapDownInfo info) { + lastEventDescription = _describe('TapDown', info); + } + + @override + void onDragStart(int pointerId, DragStartInfo info) { + lastEventDescription = _describe('DragStart', info); + } + + @override + void onDragUpdate(int pointerId, DragUpdateInfo info) { + lastEventDescription = _describe('DragUpdate', info); + } + + @override + void onScroll(PointerScrollInfo info) { + lastEventDescription = _describe('Scroll', info); + } + + /// Describes generic event information + some event specific details for + /// some events. + String _describe(String name, PositionInfo info) { + return [ + name, + 'Global: ${info.eventPosition.global}', + 'Widget: ${info.eventPosition.widget}', + 'World: ${camera.globalToLocal(info.eventPosition.global)}', + 'Camera: ${camera.viewfinder.position}', + if (info is DragUpdateInfo) ...[ + 'Delta', + 'Global: ${info.delta.global}', + 'World: ${info.delta.global / camera.viewfinder.zoom}', + ], + if (info is PointerScrollInfo) ...[ + 'Scroll Delta', + 'Global: ${info.scrollDelta.global}', + 'World: ${info.scrollDelta.global / camera.viewfinder.zoom}', + ], + ].join('\n'); + } + + @override + void update(double dt) { + super.update(dt); + cameraPosition.add(cameraVelocity * dt * 30); + // just make it look pretty + cameraPosition.x = _roundDouble(cameraPosition.x, 5); + cameraPosition.y = _roundDouble(cameraPosition.y, 5); + camera.viewfinder.position = cameraPosition; + } + + /// Round [val] up to [places] decimal places. + static double _roundDouble(double val, int places) { + final mod = pow(10.0, places); + return (val * mod).round().toDouble() / mod; + } + + /// Camera controls. + @override + KeyEventResult onKeyEvent( + KeyEvent event, + Set keysPressed, + ) { + final isKeyDown = event is KeyDownEvent; + + if (event.logicalKey == LogicalKeyboardKey.keyA) { + cameraVelocity.x = isKeyDown ? -1 : 0; + } else if (event.logicalKey == LogicalKeyboardKey.keyD) { + cameraVelocity.x = isKeyDown ? 1 : 0; + } else if (event.logicalKey == LogicalKeyboardKey.keyW) { + cameraVelocity.y = isKeyDown ? -1 : 0; + } else if (event.logicalKey == LogicalKeyboardKey.keyS) { + cameraVelocity.y = isKeyDown ? 1 : 0; + } else if (isKeyDown) { + if (event.logicalKey == LogicalKeyboardKey.keyQ) { + camera.viewfinder.zoom *= 2; + } else if (event.logicalKey == LogicalKeyboardKey.keyE) { + camera.viewfinder.zoom /= 2; + } + } + + return KeyEventResult.handled; + } +} + +/// A simple widget that "wraps" a Flame game with some Containers +/// on each direction (top, bottom, left and right) and allow adding +/// or removing containers. +class CoordinateSystemsWidget extends StatefulWidget { + const CoordinateSystemsWidget({super.key}); + + @override + State createState() { + return _CoordinateSystemsState(); + } +} + +class _CoordinateSystemsState extends State { + /// The number of blocks in each direction (top, left, right, bottom). + List blocks = [1, 1, 1, 1]; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...createBlocks(index: 0, rotated: false, start: true), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...createBlocks(index: 1, rotated: true, start: true), + Expanded( + child: GameWidget(game: CoordinateSystemsExample()), + ), + ...createBlocks(index: 2, rotated: true, start: false), + ], + ), + ), + ...createBlocks(index: 3, rotated: false, start: false), + ], + ); + } + + /// Just creates a list of Widgets + the "add" button + List createBlocks({ + /// Index on the [blocks] array + required int index, + + /// If true, render vertical text + required bool rotated, + + /// Whether to render the "add" button before or after + required bool start, + }) { + final add = Container( + margin: const EdgeInsets.all(32), + child: Center( + child: TextButton( + child: const Text('+'), + onPressed: () => setState(() => blocks[index]++), + ), + ), + ); + return [ + if (start) add, + for (int i = 1; i <= blocks[index]; i++) + GestureDetector( + child: Container( + margin: const EdgeInsets.all(32), + child: Center( + child: RotatedBox( + quarterTurns: rotated ? 1 : 0, + child: Text('Block $i'), + ), + ), + ), + onTap: () => setState(() => blocks[index]--), + ), + if (!start) add, + ]; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/fixed_resolution_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/fixed_resolution_example.dart new file mode 100644 index 0000000..886a834 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/fixed_resolution_example.dart @@ -0,0 +1,141 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; +import 'package:flame/text.dart'; +import 'package:flutter/material.dart'; + +class FixedResolutionExample extends FlameGame + with ScrollDetector, ScaleDetector { + static const description = ''' + This example shows how to create a viewport with a fixed resolution. + It is useful when you want the visible part of the game to be the same on + all devices no matter the actual screen size of the device. + Resize the window or change device orientation to see the difference. + + If you tap once you will set the zoom to 2 and if you tap again it goes back + to 1, so that you can test how it works with a zoom level. + '''; + + FixedResolutionExample() + : super( + camera: CameraComponent.withFixedResolution(width: 600, height: 1024), + world: FixedResolutionWorld(), + ); + + @override + Future onLoad() async { + final textRenderer = TextPaint( + style: TextStyle(fontSize: 25, color: BasicPalette.black.color), + ); + camera.viewport.add( + TextButton( + text: 'Viewport\ncomponent', + position: Vector2.all(10), + textRenderer: textRenderer, + ), + ); + camera.viewfinder.add( + TextButton( + text: 'Viewfinder\ncomponent', + textRenderer: textRenderer, + position: Vector2(0, 200), + anchor: Anchor.center, + ), + ); + camera.viewport.add( + TextButton( + text: 'Viewport\ncomponent', + position: camera.viewport.virtualSize - Vector2.all(10), + textRenderer: textRenderer, + anchor: Anchor.bottomRight, + ), + ); + } +} + +class FixedResolutionWorld extends World + with HasGameReference, TapCallbacks, DoubleTapCallbacks { + final red = BasicPalette.red.paint(); + + @override + Future onLoad() async { + final flameSprite = await game.loadSprite('layers/player.png'); + + add(Background()); + add( + SpriteComponent( + sprite: flameSprite, + size: Vector2(149, 211), + )..anchor = Anchor.center, + ); + } + + @override + void onTapDown(TapDownEvent event) { + add( + CircleComponent( + radius: 2, + position: event.localPosition, + paint: red, + ), + ); + } + + @override + void onDoubleTapDown(DoubleTapDownEvent event) { + final currentZoom = game.camera.viewfinder.zoom; + game.camera.viewfinder.zoom = currentZoom > 1 ? 1 : 2; + } +} + +class Background extends PositionComponent { + @override + int priority = -1; + + late Paint white; + late final Rect hugeRect; + + Background() : super(size: Vector2.all(100000), anchor: Anchor.center); + + @override + Future onLoad() async { + white = BasicPalette.white.paint(); + hugeRect = size.toRect(); + } + + @override + void render(Canvas canvas) { + canvas.drawRect(hugeRect, white); + } +} + +class TextButton extends ButtonComponent { + TextButton({ + required String text, + required super.position, + super.anchor, + TextRenderer? textRenderer, + }) : super( + button: RectangleComponent( + size: Vector2(200, 100), + paint: Paint() + ..color = Colors.orange + ..strokeWidth = 2 + ..style = PaintingStyle.stroke, + ), + buttonDown: RectangleComponent( + size: Vector2(200, 100), + paint: Paint()..color = BasicPalette.orange.color.withOpacity(0.5), + ), + children: [ + TextComponent( + text: text, + textRenderer: textRenderer, + position: Vector2(100, 50), + anchor: Anchor.center, + ), + ], + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/follow_component_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/follow_component_example.dart new file mode 100644 index 0000000..c27cce9 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/follow_component_example.dart @@ -0,0 +1,205 @@ +import 'dart:math'; + +import 'package:examples/commons/ember.dart'; +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class FollowComponentExample extends FlameGame + with HasCollisionDetection, HasKeyboardHandlerComponents { + static const String description = ''' + Move around with W, A, S, D and notice how the camera follows the ember + sprite.\n + If you collide with the gray squares, the camera reference is changed from + center to topCenter.\n + The gray squares can also be clicked to show how the coordinate system + respects the camera transformation. + '''; + + FollowComponentExample({required this.viewportResolution}) + : super( + camera: CameraComponent.withFixedResolution( + width: viewportResolution.x, + height: viewportResolution.y, + ), + ); + + late MovableEmber ember; + final Vector2 viewportResolution; + + @override + Future onLoad() async { + world.add(Map()); + world.add(ember = MovableEmber()); + camera.setBounds(Map.bounds); + camera.follow(ember, maxSpeed: 250); + + world.addAll( + List.generate(30, (_) => Rock(Map.generateCoordinates())), + ); + } +} + +class MovableEmber extends Ember + with CollisionCallbacks, KeyboardHandler { + static const double speed = 300; + static final TextPaint textRenderer = TextPaint( + style: const TextStyle(color: Colors.white70, fontSize: 12), + ); + + final Vector2 velocity = Vector2.zero(); + late final TextComponent positionText; + late final Vector2 textPosition; + late final maxPosition = Vector2.all(Map.size - size.x / 2); + late final minPosition = -maxPosition; + + MovableEmber() : super(priority: 2); + + @override + Future onLoad() async { + await super.onLoad(); + positionText = TextComponent( + textRenderer: textRenderer, + position: (size / 2)..y = size.y / 2 + 30, + anchor: Anchor.center, + ); + add(positionText); + add(CircleHitbox()); + } + + @override + void update(double dt) { + super.update(dt); + final deltaPosition = velocity * (speed * dt); + position.add(deltaPosition); + position.clamp(minPosition, maxPosition); + positionText.text = '(${x.toInt()}, ${y.toInt()})'; + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + super.onCollisionStart(intersectionPoints, other); + if (other is Rock) { + other.add( + ScaleEffect.to( + Vector2.all(1.5), + EffectController(duration: 0.2, alternate: true), + ), + ); + } + } + + @override + bool onKeyEvent(KeyEvent event, Set keysPressed) { + final isKeyDown = event is KeyDownEvent; + + final bool handled; + if (event.logicalKey == LogicalKeyboardKey.keyA) { + velocity.x = isKeyDown ? -1 : 0; + handled = true; + } else if (event.logicalKey == LogicalKeyboardKey.keyD) { + velocity.x = isKeyDown ? 1 : 0; + handled = true; + } else if (event.logicalKey == LogicalKeyboardKey.keyW) { + velocity.y = isKeyDown ? -1 : 0; + handled = true; + } else if (event.logicalKey == LogicalKeyboardKey.keyS) { + velocity.y = isKeyDown ? 1 : 0; + handled = true; + } else { + handled = false; + } + + if (handled) { + angle = -velocity.angleToSigned(Vector2(1, 0)); + return false; + } else { + return super.onKeyEvent(event, keysPressed); + } + } +} + +class Map extends Component { + static const double size = 1500; + static const Rect _bounds = Rect.fromLTRB(-size, -size, size, size); + static final Rectangle bounds = Rectangle.fromLTRB(-size, -size, size, size); + + static final Paint _paintBorder = Paint() + ..color = Colors.white12 + ..strokeWidth = 10 + ..style = PaintingStyle.stroke; + static final Paint _paintBg = Paint()..color = const Color(0xFF333333); + + static final _rng = Random(); + + late final List _paintPool; + late final List _rectPool; + + Map() : super(priority: 0) { + _paintPool = List.generate( + (size / 50).ceil(), + (_) => PaintExtension.random(rng: _rng) + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + growable: false, + ); + _rectPool = List.generate( + (size / 50).ceil(), + (i) => Rect.fromCircle(center: Offset.zero, radius: size - i * 50), + growable: false, + ); + } + + @override + void render(Canvas canvas) { + canvas.drawRect(_bounds, _paintBg); + canvas.drawRect(_bounds, _paintBorder); + for (var i = 0; i < (size / 50).ceil(); i++) { + canvas.drawCircle(Offset.zero, size - i * 50, _paintPool[i]); + canvas.drawRect(_rectPool[i], _paintBorder); + } + } + + static Vector2 generateCoordinates() { + return Vector2.random() + ..scale(2 * size) + ..sub(Vector2.all(size)); + } +} + +class Rock extends SpriteComponent with HasGameRef, TapCallbacks { + Rock(Vector2 position) + : super( + position: position, + size: Vector2.all(50), + priority: 1, + anchor: Anchor.center, + ); + + @override + Future onLoad() async { + sprite = await game.loadSprite('nine-box.png'); + paint = Paint()..color = Colors.white; + add(RectangleHitbox()); + } + + @override + void onTapDown(_) { + add( + ScaleEffect.to( + Vector2.all(scale.x >= 2.0 ? 1 : 2), + EffectController(duration: 0.3), + ), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/static_components_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/static_components_example.dart new file mode 100644 index 0000000..3b0fb1f --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/static_components_example.dart @@ -0,0 +1,134 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/parallax.dart'; + +class StaticComponentsExample extends FlameGame + with ScrollDetector, ScaleDetector { + static const description = ''' + This example shows a parallax which is attached to the viewport (behind the + world), four Flame logos that are added to the world, and a player added to + the world which is also followed by the camera when you click somewhere. + The text components that are added are self-explanatory. + '''; + + late final ParallaxComponent myParallax; + + StaticComponentsExample({ + required Vector2 viewportResolution, + }) : super( + camera: CameraComponent.withFixedResolution( + width: viewportResolution.x, + height: viewportResolution.y, + ), + world: _StaticComponentWorld(), + ); + + @override + Future onLoad() async { + myParallax = MyParallaxComponent()..parallax?.baseVelocity.setZero(); + camera.backdrop.addAll([ + myParallax, + TextComponent( + text: 'Center backdrop Component', + position: camera.viewport.virtualSize / 2 + Vector2(0, 30), + anchor: Anchor.center, + ), + ]); + camera.viewport.addAll( + [ + TextComponent( + text: 'Corner Viewport Component', + position: Vector2.all(10), + ), + TextComponent( + text: 'Center Viewport Component', + position: camera.viewport.virtualSize / 2, + anchor: Anchor.center, + ), + ], + ); + } +} + +class _StaticComponentWorld extends World + with + HasGameReference, + TapCallbacks, + DoubleTapCallbacks { + late SpriteComponent player; + @override + Future onLoad() async { + final playerSprite = await game.loadSprite('layers/player.png'); + final flameSprite = await game.loadSprite('flame.png'); + final visibleSize = game.camera.visibleWorldRect.toVector2(); + add(player = SpriteComponent(sprite: playerSprite, anchor: Anchor.center)); + addAll([ + SpriteComponent( + sprite: flameSprite, + anchor: Anchor.center, + position: -visibleSize / 8, + size: Vector2(20, 30), + ), + SpriteComponent( + sprite: flameSprite, + anchor: Anchor.center, + position: visibleSize / 8, + size: Vector2(20, 30), + ), + SpriteComponent( + sprite: flameSprite, + anchor: Anchor.center, + position: (visibleSize / 8)..multiply(Vector2(-1, 1)), + size: Vector2(20, 30), + ), + SpriteComponent( + sprite: flameSprite, + anchor: Anchor.center, + position: (visibleSize / 8)..multiply(Vector2(1, -1)), + size: Vector2(20, 30), + ), + ]); + game.camera.follow(player, maxSpeed: 100); + } + + @override + void onTapDown(TapDownEvent event) { + const moveDuration = 1.0; + final deltaX = (event.localPosition - player.position).x; + player.add( + MoveToEffect( + event.localPosition, + EffectController( + duration: moveDuration, + ), + onComplete: () => game.myParallax.parallax?.baseVelocity.setZero(), + ), + ); + final moveSpeedX = deltaX / moveDuration; + game.myParallax.parallax?.baseVelocity.setValues(moveSpeedX, 0); + } +} + +class MyParallaxComponent extends ParallaxComponent { + @override + Future onLoad() async { + parallax = await game.loadParallax( + [ + ParallaxImageData('parallax/bg.png'), + ParallaxImageData('parallax/mountain-far.png'), + ParallaxImageData('parallax/mountains.png'), + ParallaxImageData('parallax/trees.png'), + ParallaxImageData('parallax/foreground-trees.png'), + ], + baseVelocity: Vector2(0, 0), + velocityMultiplierDelta: Vector2(1.8, 1.0), + filterQuality: FilterQuality.none, + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/zoom_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/zoom_example.dart new file mode 100644 index 0000000..ce37978 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/camera_and_viewport/zoom_example.dart @@ -0,0 +1,64 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; + +class ZoomExample extends FlameGame with ScrollDetector, ScaleDetector { + static const String description = ''' + On web: use scroll to zoom in and out.\n + On mobile: use scale gesture to zoom in and out. + '''; + + ZoomExample({ + required Vector2 viewportResolution, + }) : super( + camera: CameraComponent.withFixedResolution( + width: viewportResolution.x, + height: viewportResolution.y, + ), + ); + + @override + Future onLoad() async { + final flameSprite = await loadSprite('flame.png'); + + world.add( + SpriteComponent( + sprite: flameSprite, + size: Vector2(149, 211), + )..anchor = Anchor.center, + ); + } + + void clampZoom() { + camera.viewfinder.zoom = camera.viewfinder.zoom.clamp(0.05, 3.0); + } + + static const zoomPerScrollUnit = 0.02; + + @override + void onScroll(PointerScrollInfo info) { + camera.viewfinder.zoom += + info.scrollDelta.global.y.sign * zoomPerScrollUnit; + clampZoom(); + } + + late double startZoom; + + @override + void onScaleStart(_) { + startZoom = camera.viewfinder.zoom; + } + + @override + void onScaleUpdate(ScaleUpdateInfo info) { + final currentScale = info.scale.global; + if (!currentScale.isIdentity()) { + camera.viewfinder.zoom = startZoom * currentScale.y; + clampZoom(); + } else { + final delta = info.delta.global; + camera.viewfinder.position.translate(-delta.x, -delta.y); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/bouncing_ball_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/bouncing_ball_example.dart new file mode 100644 index 0000000..29e864e --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/bouncing_ball_example.dart @@ -0,0 +1,105 @@ +import 'dart:math' as math; +import 'dart:ui'; + +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +class BouncingBallExample extends FlameGame with HasCollisionDetection { + static const description = ''' + This example shows how you can use the Collisions detection api to know when a ball + collides with the screen boundaries and then update it to bounce off these boundaries. + '''; + @override + void onLoad() { + addAll([ + ScreenHitbox(), + Ball(), + ]); + } +} + +class Ball extends CircleComponent + with HasGameReference, CollisionCallbacks { + late Vector2 velocity; + + Ball() { + paint = Paint()..color = Colors.white; + radius = 10; + } + + static const double speed = 500; + static const degree = math.pi / 180; + + @override + Future onLoad() async { + super.onLoad(); + _resetBall; + final hitBox = CircleHitbox( + radius: radius, + ); + + addAll([ + hitBox, + ]); + } + + @override + void update(double dt) { + super.update(dt); + position += velocity * dt; + } + + void get _resetBall { + position = game.size / 2; + final spawnAngle = getSpawnAngle; + + final vx = math.cos(spawnAngle * degree) * speed; + final vy = math.sin(spawnAngle * degree) * speed; + velocity = Vector2( + vx, + vy, + ); + } + + double get getSpawnAngle { + final random = math.Random().nextDouble(); + final spawnAngle = lerpDouble(0, 360, random)!; + + return spawnAngle; + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + super.onCollisionStart(intersectionPoints, other); + + if (other is ScreenHitbox) { + final collisionPoint = intersectionPoints.first; + + // Left Side Collision + if (collisionPoint.x == 0) { + velocity.x = -velocity.x; + velocity.y = velocity.y; + } + // Right Side Collision + if (collisionPoint.x == game.size.x) { + velocity.x = -velocity.x; + velocity.y = velocity.y; + } + // Top Side Collision + if (collisionPoint.y == 0) { + velocity.x = velocity.x; + velocity.y = -velocity.y; + } + // Bottom Side Collision + if (collisionPoint.y == game.size.y) { + velocity.x = velocity.x; + velocity.y = -velocity.y; + } + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/circles_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/circles_example.dart new file mode 100644 index 0000000..4dc2884 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/circles_example.dart @@ -0,0 +1,79 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart' hide Image, Draggable; + +class CirclesExample extends FlameGame { + static const description = ''' + This example will create a circle every time you tap on the screen. It will + have the initial velocity towards the center of the screen and if it touches + another circle both of them will change color. + '''; + + CirclesExample() + : super( + camera: CameraComponent.withFixedResolution(width: 600, height: 400), + world: MyWorld(), + ); +} + +class MyWorld extends World with TapCallbacks, HasCollisionDetection { + MyWorld() : super(children: [ScreenHitbox()..debugMode = true]); + + @override + void onTapDown(TapDownEvent info) { + add(MyCollidable(position: info.localPosition)); + } +} + +class MyCollidable extends PositionComponent + with HasGameReference, CollisionCallbacks { + MyCollidable({super.position}) + : super(size: Vector2.all(30), anchor: Anchor.center); + + late Vector2 velocity; + final _collisionColor = Colors.amber; + final _defaultColor = Colors.cyan; + late ShapeHitbox hitbox; + + @override + Future onLoad() async { + final defaultPaint = Paint() + ..color = _defaultColor + ..style = PaintingStyle.stroke; + hitbox = CircleHitbox() + ..paint = defaultPaint + ..renderShape = true; + add(hitbox); + velocity = -position + ..scaleTo(50); + } + + @override + void update(double dt) { + super.update(dt); + position.add(velocity * dt); + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + super.onCollisionStart(intersectionPoints, other); + hitbox.paint.color = _collisionColor; + if (other is ScreenHitbox) { + removeFromParent(); + return; + } + } + + @override + void onCollisionEnd(PositionComponent other) { + super.onCollisionEnd(other); + if (!isColliding) { + hitbox.paint.color = _defaultColor; + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/collidable_animation_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/collidable_animation_example.dart new file mode 100644 index 0000000..a8470ef --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/collidable_animation_example.dart @@ -0,0 +1,118 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/palette.dart'; + +class CollidableAnimationExample extends FlameGame with HasCollisionDetection { + static const description = ''' + In this example you can see four animated birds which are flying straight + along the same route until they hit either another bird or the wall, which + makes them turn. The birds have PolygonHitboxes which are marked with the + white lines. + '''; + + @override + Future onLoad() async { + add(ScreenHitbox()); + final componentSize = Vector2(150, 100); + // Top left component + add( + AnimatedComponent(Vector2.all(200), Vector2.all(100), componentSize) + ..flipVertically(), + ); + // Bottom right component + add( + AnimatedComponent( + Vector2(-100, -100), + size.clone()..sub(Vector2.all(200)), + componentSize / 2, + ), + ); + // Bottom left component + add( + AnimatedComponent( + Vector2(100, -100), + Vector2(100, size.y - 100), + componentSize * 1.5, + angle: pi / 4, + ), + ); + // Top right component + add( + AnimatedComponent( + Vector2(-300, 300), + Vector2(size.x - 100, 100), + componentSize / 3, + angle: pi / 4, + )..flipVertically(), + ); + } +} + +class AnimatedComponent extends SpriteAnimationComponent + with CollisionCallbacks, HasGameRef { + final Vector2 velocity; + + AnimatedComponent( + this.velocity, + Vector2 position, + Vector2 size, { + double angle = -pi / 4, + }) : super( + position: position, + size: size, + angle: angle, + anchor: Anchor.center, + ); + + @override + Future onLoad() async { + animation = await game.loadSpriteAnimation( + 'bomb_ptero.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.2, + textureSize: Vector2.all(48), + ), + ); + final hitboxPaint = BasicPalette.white.paint() + ..style = PaintingStyle.stroke; + add( + PolygonHitbox.relative( + [ + Vector2(0.0, -1.0), + Vector2(-1.0, -0.1), + Vector2(-0.2, 0.4), + Vector2(0.2, 0.4), + Vector2(1.0, -0.1), + ], + parentSize: size, + ) + ..paint = hitboxPaint + ..renderShape = true, + ); + } + + @override + void update(double dt) { + super.update(dt); + position += velocity * dt; + } + + final Paint hitboxPaint = BasicPalette.green.paint() + ..style = PaintingStyle.stroke; + final Paint dotPaint = BasicPalette.red.paint()..style = PaintingStyle.stroke; + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + super.onCollisionStart(intersectionPoints, other); + velocity.negate(); + flipVertically(); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/collision_detection.dart b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/collision_detection.dart new file mode 100644 index 0000000..108f5ec --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/collision_detection.dart @@ -0,0 +1,87 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/collision_detection/bouncing_ball_example.dart'; +import 'package:examples/stories/collision_detection/circles_example.dart'; +import 'package:examples/stories/collision_detection/collidable_animation_example.dart'; +import 'package:examples/stories/collision_detection/multiple_shapes_example.dart'; +import 'package:examples/stories/collision_detection/multiple_worlds_example.dart'; +import 'package:examples/stories/collision_detection/quadtree_example.dart'; +import 'package:examples/stories/collision_detection/raycast_example.dart'; +import 'package:examples/stories/collision_detection/raycast_light_example.dart'; +import 'package:examples/stories/collision_detection/raycast_max_distance_example.dart'; +import 'package:examples/stories/collision_detection/rays_in_shape_example.dart'; +import 'package:examples/stories/collision_detection/raytrace_example.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/widgets.dart'; + +void addCollisionDetectionStories(Dashbook dashbook) { + dashbook.storiesOf('Collision Detection') + ..add( + 'Collidable AnimationComponent', + (_) => GameWidget(game: CollidableAnimationExample()), + codeLink: + baseLink('collision_detection/collidable_animation_example.dart'), + info: CollidableAnimationExample.description, + ) + ..add( + 'Circles', + (_) => GameWidget(game: CirclesExample()), + codeLink: baseLink('collision_detection/circles_example.dart'), + info: CirclesExample.description, + ) + ..add( + 'Bouncing Ball', + (_) => GameWidget(game: BouncingBallExample()), + codeLink: baseLink('collision_detection/bouncing_ball_example.dart'), + info: BouncingBallExample.description, + ) + ..add( + 'Multiple shapes', + (_) => ClipRect(child: GameWidget(game: MultipleShapesExample())), + codeLink: baseLink('collision_detection/multiple_shapes_example.dart'), + info: MultipleShapesExample.description, + ) + ..add( + 'Multiple worlds', + (_) => GameWidget(game: MultipleWorldsExample()), + codeLink: baseLink('collision_detection/multiple_worlds_example.dart'), + info: MultipleWorldsExample.description, + ) + ..add( + 'QuadTree collision', + (_) => GameWidget(game: QuadTreeExample()), + codeLink: baseLink('collision_detection/quadtree_example.dart'), + info: QuadTreeExample.description, + ) + ..add( + 'Raycasting (light)', + (_) => GameWidget(game: RaycastLightExample()), + codeLink: baseLink('collision_detection/raycast_light_example.dart'), + info: RaycastLightExample.description, + ) + ..add( + 'Raycasting', + (_) => GameWidget(game: RaycastExample()), + codeLink: baseLink('collision_detection/raycast_example.dart'), + info: RaycastExample.description, + ) + ..add( + 'Raytracing', + (_) => GameWidget(game: RaytraceExample()), + codeLink: baseLink('collision_detection/raytrace_example.dart'), + info: RaytraceExample.description, + ) + ..add( + 'Raycasting Max Distance', + (_) => GameWidget(game: RaycastMaxDistanceExample()), + codeLink: + baseLink('collision_detection/raycast_max_distance_example.dart'), + info: RaycastMaxDistanceExample.description, + ) + ..add( + 'Ray inside/outside shapes', + (_) => GameWidget(game: RaysInShapeExample()), + codeLink: baseLink('collision_detection/rays_in_shape_example.dart'), + info: RaysInShapeExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/multiple_shapes_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/multiple_shapes_example.dart new file mode 100644 index 0000000..9375acf --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/multiple_shapes_example.dart @@ -0,0 +1,299 @@ +import 'dart:math'; + +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/material.dart' hide Image, Draggable; + +enum Shapes { circle, rectangle, polygon } + +class MultipleShapesExample extends FlameGame with HasCollisionDetection { + static const description = ''' + An example with many hitboxes that move around on the screen and during + collisions they change color depending on what it is that they have collided + with. + + The snowman, the component built with three circles on top of each other, + works a little bit differently than the other components to show that you + can have multiple hitboxes within one component. + + On this example, you can "throw" the components by dragging them quickly in + any direction. + '''; + + MultipleShapesExample() + : super( + world: MultiShapesWorld(), + camera: CameraComponent()..viewfinder.anchor = Anchor.topLeft, + ); +} + +class MultiShapesWorld extends World with HasGameReference { + @override + Future onLoad() async { + add(FpsTextComponent(position: Vector2(0, game.size.y - 24))); + final screenHitbox = ScreenHitbox(); + final snowman = CollidableSnowman( + Vector2.all(150), + Vector2(120, 250), + Vector2(-100, 100), + screenHitbox, + ); + MyCollidable lastToAdd = snowman; + add(screenHitbox); + add(snowman); + var totalAdded = 1; + while (totalAdded < 1000) { + lastToAdd = nextRandomCollidable(lastToAdd, screenHitbox); + final lastBottomRight = lastToAdd.toAbsoluteRect().bottomRight; + if (lastBottomRight.dx < game.size.x && + lastBottomRight.dy < game.size.y) { + add(lastToAdd); + totalAdded++; + } else { + break; + } + } + } + + final _rng = Random(); + final _distance = Vector2(100, 0); + + MyCollidable nextRandomCollidable( + MyCollidable lastCollidable, + ScreenHitbox screenHitbox, + ) { + final collidableSize = Vector2.all(50) + Vector2.random(_rng) * 100; + final isXOverflow = lastCollidable.position.x + + lastCollidable.size.x / 2 + + _distance.x + + collidableSize.x > + game.size.x; + var position = _distance + Vector2(0, lastCollidable.position.y + 200); + if (!isXOverflow) { + position = (lastCollidable.position + _distance) + ..x += collidableSize.x / 2; + } + final velocity = (Vector2.random(_rng) - Vector2.random(_rng)) * 400; + return randomCollidable( + position, + collidableSize, + velocity, + screenHitbox, + random: _rng, + ); + } +} + +abstract class MyCollidable extends PositionComponent + with DragCallbacks, CollisionCallbacks, GestureHitboxes { + double rotationSpeed = 0.0; + final Vector2 velocity; + final delta = Vector2.zero(); + double angleDelta = 0; + final Color _defaultColor = Colors.blue.withOpacity(0.8); + final Color _collisionColor = Colors.green.withOpacity(0.8); + late final Paint _dragIndicatorPaint; + final ScreenHitbox screenHitbox; + ShapeHitbox? hitbox; + + MyCollidable( + Vector2 position, + Vector2 size, + this.velocity, + this.screenHitbox, + ) : super(position: position, size: size, anchor: Anchor.center) { + _dragIndicatorPaint = BasicPalette.white.paint(); + } + + @override + void onMount() { + hitbox?.paint.color = _defaultColor; + super.onMount(); + } + + @override + void update(double dt) { + super.update(dt); + if (isDragged) { + return; + } + delta.setFrom(velocity * dt); + position.add(delta); + angleDelta = dt * rotationSpeed; + angle = (angle + angleDelta) % (2 * pi); + // Takes rotation into consideration (which topLeftPosition doesn't) + final topLeft = absoluteCenter - (scaledSize / 2); + if (topLeft.x + scaledSize.x < 0 || + topLeft.y + scaledSize.y < 0 || + topLeft.x > screenHitbox.scaledSize.x || + topLeft.y > screenHitbox.scaledSize.y) { + final moduloSize = screenHitbox.scaledSize + scaledSize; + topLeftPosition = topLeftPosition % moduloSize; + } + } + + @override + void render(Canvas canvas) { + if (isDragged) { + final localCenter = scaledSize.toOffset() / 2; + canvas.drawCircle(localCenter, 5, _dragIndicatorPaint); + } + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + super.onCollisionStart(intersectionPoints, other); + hitbox?.paint.color = _collisionColor; + } + + @override + void onCollisionEnd(PositionComponent other) { + super.onCollisionEnd(other); + if (!isColliding) { + hitbox?.paint.color = _defaultColor; + } + } + + @override + void onDragEnd(DragEndEvent event) { + super.onDragEnd(event); + velocity.setFrom(event.velocity / 10); + } +} + +class CollidablePolygon extends MyCollidable { + CollidablePolygon( + Vector2 position, + Vector2 size, + Vector2 velocity, + ScreenHitbox screenHitbox, + ) : super(position, size, velocity, screenHitbox) { + hitbox = PolygonHitbox.relative( + [ + Vector2(-1.0, 0.0), + Vector2(-0.8, 0.6), + Vector2(0.0, 1.0), + Vector2(0.6, 0.9), + Vector2(1.0, 0.0), + Vector2(0.6, -0.8), + Vector2(0, -1.0), + Vector2(-0.8, -0.8), + ], + parentSize: size, + )..renderShape = true; + add(hitbox!); + } +} + +class CollidableRectangle extends MyCollidable { + CollidableRectangle( + super.position, + super.size, + super.velocity, + super.screenHitbox, + ) { + hitbox = RectangleHitbox()..renderShape = true; + add(hitbox!); + } +} + +class CollidableCircle extends MyCollidable { + CollidableCircle( + super.position, + super.size, + super.velocity, + super.screenHitbox, + ) { + hitbox = CircleHitbox()..renderShape = true; + add(hitbox!); + } +} + +class SnowmanPart extends CircleHitbox { + @override + final renderShape = true; + final startColor = Colors.white.withOpacity(0.8); + final Color hitColor; + + SnowmanPart(double radius, Vector2 position, this.hitColor) + : super(radius: radius, position: position, anchor: Anchor.center) { + paint.color = startColor; + } + + @override + void onCollisionStart(Set intersectionPoints, ShapeHitbox other) { + super.onCollisionStart(intersectionPoints, other); + + if (other.hitboxParent is ScreenHitbox) { + paint.color = startColor; + } else { + paint.color = hitColor.withOpacity(0.8); + } + } + + @override + void onCollisionEnd(ShapeHitbox other) { + super.onCollisionEnd(other); + if (!isColliding) { + paint.color = startColor; + } + } +} + +class CollidableSnowman extends MyCollidable { + CollidableSnowman( + Vector2 position, + Vector2 size, + Vector2 velocity, + ScreenHitbox screenHitbox, + ) : super(position, size, velocity, screenHitbox) { + rotationSpeed = 0.3; + anchor = Anchor.topLeft; + final top = SnowmanPart( + size.x * 0.3, + Vector2(size.x / 2, size.y * 0.15), + Colors.red, + ); + final middle = SnowmanPart( + size.x * 0.4, + Vector2(size.x / 2, size.y * 0.40), + Colors.yellow, + ); + final bottom = SnowmanPart( + size.x / 2, + Vector2(size.x / 2, size.y - size.y / 4), + Colors.green, + ); + add(bottom); + add(middle); + add(top); + } +} + +MyCollidable randomCollidable( + Vector2 position, + Vector2 size, + Vector2 velocity, + ScreenHitbox screenHitbox, { + Random? random, +}) { + final rng = random ?? Random(); + final rotationSpeed = 0.5 - rng.nextDouble(); + final shapeType = Shapes.values[rng.nextInt(Shapes.values.length)]; + return switch (shapeType) { + Shapes.circle => CollidableCircle(position, size, velocity, screenHitbox) + ..rotationSpeed = rotationSpeed, + Shapes.rectangle => + CollidableRectangle(position, size, velocity, screenHitbox) + ..rotationSpeed = rotationSpeed, + Shapes.polygon => CollidablePolygon(position, size, velocity, screenHitbox) + ..rotationSpeed = rotationSpeed, + }; +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/multiple_worlds_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/multiple_worlds_example.dart new file mode 100644 index 0000000..a6bd6e5 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/multiple_worlds_example.dart @@ -0,0 +1,80 @@ +import 'dart:math'; + +import 'package:examples/commons/ember.dart'; +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +class MultipleWorldsExample extends FlameGame { + static const description = ''' + This example shows how multiple worlds can have discrete collision + detection. + + The top two Embers live in one world and turn green when they collide and + the bottom two embers live in another world and turn red when they collide, + you can see that when one of the top ones collide with one of the bottom + ones, neither change their colors since they are in different worlds. + '''; + + @override + Future onLoad() async { + final world1 = CollisionDetectionWorld(); + final world2 = CollisionDetectionWorld(); + final camera1 = CameraComponent(world: world1); + final camera2 = CameraComponent(world: world2); + await addAll([world1, world2, camera1, camera2]); + final ember1 = CollidableEmber(position: Vector2(75, 75)); + final ember2 = CollidableEmber(position: Vector2(-75, 75)); + final ember3 = CollidableEmber(position: Vector2(75, -75)); + final ember4 = CollidableEmber(position: Vector2(-75, -75)); + world1.addAll([ember1, ember2]); + world2.addAll([ember3, ember4]); + } +} + +class CollisionDetectionWorld extends World with HasCollisionDetection {} + +class CollidableEmber extends Ember with CollisionCallbacks { + CollidableEmber({super.position}); + + static final Random _rng = Random(); + int get index => + (position.x.isNegative ? 1 : 0) + (position.y.isNegative ? 2 : 0); + + @override + Future onLoad() async { + super.onLoad(); + add(CircleHitbox()); + add( + MoveToEffect( + Vector2.zero(), + EffectController( + duration: 0.5 + _rng.nextDouble(), + infinite: true, + alternate: true, + ), + ), + ); + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + super.onCollisionStart(intersectionPoints, other); + + add( + ColorEffect( + index < 2 ? Colors.red : Colors.green, + EffectController( + duration: 0.2, + alternate: true, + ), + opacityTo: 0.9, + ), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/quadtree_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/quadtree_example.dart new file mode 100644 index 0000000..98aa77f --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/quadtree_example.dart @@ -0,0 +1,433 @@ +import 'dart:math'; + +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/layers.dart'; +import 'package:flutter/material.dart' hide Image, Draggable; +import 'package:flutter/services.dart'; + +const tileSize = 8.0; + +class QuadTreeExample extends FlameGame + with HasQuadTreeCollisionDetection, KeyboardEvents, ScrollDetector { + QuadTreeExample(); + + static const description = ''' +In this example the standard "Sweep and Prune" algorithm is replaced by +"Quad Tree". Quad Tree is often a more efficient approach of handling collisions, +its efficiency is shown especially on huge maps with big amounts of collidable +components. +Some bricks are highlighted when placed on an edge of a quadrant. It is +important to understand that handling hitboxes on edges requires more +resources. +Blue lines visualize the quad tree's quadrant positions. + +Use WASD to move the player and use the mouse scroll to change zoom. +Hold direction button and press space to fire a bullet. +Notice that bullet will fly above water but collides with bricks. + +Also notice that creating a lot of bullets at once leads to generating new +quadrants on the map since it becomes more than 25 objects in one quadrant. + +Press O button to rescan the tree and optimize it, removing unused quadrants. + +Press T button to toggle player to collide with other objects. + '''; + + static const mapSize = 300; + static const bricksCount = 8000; + late final Player player; + final staticLayer = StaticLayer(); + + @override + Future onLoad() async { + super.onLoad(); + + const mapWidth = mapSize * tileSize; + const mapHeight = mapSize * tileSize; + initializeCollisionDetection( + mapDimensions: const Rect.fromLTWH(0, 0, mapWidth, mapHeight), + minimumDistance: 10, + ); + + final random = Random(); + final spriteBrick = await Sprite.load( + 'retro_tiles.png', + srcPosition: Vector2.all(0), + srcSize: Vector2.all(tileSize), + ); + + final spriteWater = await Sprite.load( + 'retro_tiles.png', + srcPosition: Vector2(0, tileSize), + srcSize: Vector2.all(tileSize), + ); + + for (var i = 0; i < bricksCount; i++) { + final x = random.nextInt(mapSize); + final y = random.nextInt(mapSize); + final brick = Brick( + position: Vector2(x * tileSize, y * tileSize), + size: Vector2.all(tileSize), + priority: 0, + sprite: spriteBrick, + ); + world.add(brick); + staticLayer.components.add(brick); + } + + staticLayer.reRender(); + camera = CameraComponent.withFixedResolution( + world: world, + width: 500, + height: 250, + ); + + player = Player( + position: Vector2.all(mapSize * tileSize / 2), + size: Vector2.all(tileSize), + priority: 2, + ); + world.add(player); + camera.follow(player); + + final brick = Brick( + position: player.position.translated(0, -tileSize * 2), + size: Vector2.all(tileSize), + priority: 0, + sprite: spriteBrick, + ); + world.add(brick); + staticLayer.components.add(brick); + + final water1 = Water( + position: player.position.translated(0, tileSize * 2), + size: Vector2.all(tileSize), + priority: 0, + sprite: spriteWater, + ); + world.add(water1); + + final water2 = Water( + position: player.position.translated(tileSize * 2, 0), + size: Vector2.all(tileSize), + priority: 0, + sprite: spriteWater, + ); + world.add(water2); + + final water3 = Water( + position: player.position.translated(-tileSize * 2, 0), + size: Vector2.all(tileSize), + priority: 0, + sprite: spriteWater, + ); + world.add(water3); + + world.add(QuadTreeDebugComponent(collisionDetection)); + world.add(LayerComponent(staticLayer)); + camera.viewport.add(FpsTextComponent()); + } + + final elapsedMicroseconds = []; + + final _playerDisplacement = Vector2.zero(); + var _fireBullet = false; + + static const stepSize = 1.0; + + @override + KeyEventResult onKeyEvent( + KeyEvent event, + Set keysPressed, + ) { + for (final key in keysPressed) { + if (key == LogicalKeyboardKey.keyW && player.canMoveTop) { + _playerDisplacement.setValues(0, -stepSize); + player.position.translate(0, -stepSize); + } + if (key == LogicalKeyboardKey.keyA && player.canMoveLeft) { + _playerDisplacement.setValues(-stepSize, 0); + player.position.translate(-stepSize, 0); + } + if (key == LogicalKeyboardKey.keyS && player.canMoveBottom) { + _playerDisplacement.setValues(0, stepSize); + player.position.translate(0, stepSize); + } + if (key == LogicalKeyboardKey.keyD && player.canMoveRight) { + _playerDisplacement.setValues(stepSize, 0); + player.position.translate(stepSize, 0); + } + if (key == LogicalKeyboardKey.space) { + _fireBullet = true; + } + if (key == LogicalKeyboardKey.keyT) { + final collisionType = player.hitbox.collisionType; + if (collisionType == CollisionType.active) { + player.hitbox.collisionType = CollisionType.inactive; + } else if (collisionType == CollisionType.inactive) { + player.hitbox.collisionType = CollisionType.active; + } + } + if (key == LogicalKeyboardKey.keyO) { + collisionDetection.broadphase.tree.optimize(); + } + } + if (_fireBullet && !_playerDisplacement.isZero()) { + final bullet = Bullet( + position: player.position, + displacement: _playerDisplacement * 50, + ); + add(bullet); + _playerDisplacement.setZero(); + _fireBullet = false; + } + + return KeyEventResult.handled; + } + + @override + void onScroll(PointerScrollInfo info) { + camera.viewfinder.zoom += info.scrollDelta.global.y.sign * 0.08; + camera.viewfinder.zoom = camera.viewfinder.zoom.clamp(0.05, 5.0); + } +} + +//#region Player + +class Player extends SpriteComponent + with CollisionCallbacks, HasGameReference { + Player({ + required super.position, + required super.size, + required super.priority, + }); + + bool canMoveLeft = true; + bool canMoveRight = true; + bool canMoveTop = true; + bool canMoveBottom = true; + final hitbox = RectangleHitbox(); + + @override + Future onLoad() async { + sprite = await Sprite.load( + 'retro_tiles.png', + srcSize: Vector2.all(tileSize), + srcPosition: Vector2(tileSize * 3, tileSize), + ); + + add(hitbox); + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + final myCenter = + Vector2(position.x + tileSize / 2, position.y + tileSize / 2); + if (other is GameCollidable) { + final diffX = myCenter.x - other.cachedCenter.x; + if (diffX < 0) { + canMoveRight = false; + } else if (diffX > 0) { + canMoveLeft = false; + } + + final diffY = myCenter.y - other.cachedCenter.y; + if (diffY < 0) { + canMoveBottom = false; + } else if (diffY > 0) { + canMoveTop = false; + } + final newPos = Vector2(position.x + diffX / 3, position.y + diffY / 3); + position = newPos; + } + super.onCollisionStart(intersectionPoints, other); + } + + @override + void onCollisionEnd(PositionComponent other) { + canMoveLeft = true; + canMoveRight = true; + canMoveTop = true; + canMoveBottom = true; + super.onCollisionEnd(other); + } +} + +class Bullet extends PositionComponent with CollisionCallbacks, HasPaint { + Bullet({required super.position, required this.displacement}) { + paint.color = Colors.deepOrange; + priority = 10; + size = Vector2.all(1); + add(RectangleHitbox()); + } + + final Vector2 displacement; + + @override + void render(Canvas canvas) { + canvas.drawCircle(Offset.zero, 1, paint); + } + + @override + void update(double dt) { + final d = displacement * dt; + position = Vector2(position.x + d.x, position.y + d.y); + super.update(dt); + } + + @override + bool onComponentTypeCheck(PositionComponent other) { + if (other is Player || other is Water) { + return false; + } + return super.onComponentTypeCheck(other); + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + if (other is Brick) { + removeFromParent(); + } + super.onCollisionStart(intersectionPoints, other); + } +} + +//#endregion + +//#region Environment + +class Brick extends SpriteComponent + with CollisionCallbacks, GameCollidable, UpdateOnce { + Brick({ + required super.position, + required super.size, + required super.priority, + required super.sprite, + }) { + initCenter(); + initCollision(); + } + + bool rendered = false; + + @override + void renderTree(Canvas canvas) { + if (!rendered) { + super.renderTree(canvas); + } + } +} + +class Water extends SpriteComponent + with CollisionCallbacks, GameCollidable, UpdateOnce { + Water({ + required super.position, + required super.size, + required super.priority, + required super.sprite, + }) { + initCenter(); + initCollision(); + } +} + +mixin GameCollidable on PositionComponent { + void initCollision() { + add(RectangleHitbox(collisionType: CollisionType.passive)); + } + + void initCenter() { + cachedCenter = + Vector2(position.x + tileSize / 2, position.y + tileSize / 2); + } + + late final Vector2 cachedCenter; +} + +//#endregion + +//#region Utils + +mixin UpdateOnce on PositionComponent { + bool updateOnce = true; + + @override + void updateTree(double dt) { + if (updateOnce) { + super.updateTree(dt); + updateOnce = false; + } + } +} + +class StaticLayer extends PreRenderedLayer { + StaticLayer(); + + List components = []; + + @override + void drawLayer() { + for (final element in components) { + if (element is Brick) { + element.rendered = false; + element.renderTree(canvas); + element.rendered = true; + } + } + } +} + +class LayerComponent extends PositionComponent { + LayerComponent(this.layer); + + StaticLayer layer; + + @override + void render(Canvas canvas) { + layer.render(canvas); + } +} + +class QuadTreeDebugComponent extends PositionComponent with HasPaint { + QuadTreeDebugComponent(QuadTreeCollisionDetection cd) { + dbg = QuadTreeNodeDebugInfo.init(cd); + paint.color = Colors.blue; + paint.style = PaintingStyle.stroke; + priority = 10; + } + + late final QuadTreeNodeDebugInfo dbg; + + final _boxPaint = Paint() + ..style = PaintingStyle.stroke + ..color = Colors.lightGreenAccent + ..strokeWidth = 1; + + @override + void render(Canvas canvas) { + final nodes = dbg.nodes; + for (final node in nodes) { + canvas.drawRect(node.rect, paint); + final nodeElements = node.ownElements; + + final shouldPaint = !node.noChildren && nodeElements.isNotEmpty; + for (final box in nodeElements) { + if (shouldPaint) { + canvas.drawRect(box.aabb.toRect(), _boxPaint); + } + } + } + } +} +//#endregion diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/raycast_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/raycast_example.dart new file mode 100644 index 0000000..a3736cf --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/raycast_example.dart @@ -0,0 +1,137 @@ +import 'dart:math'; + +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/geometry.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/material.dart'; + +class RaycastExample extends FlameGame with HasCollisionDetection { + static const description = ''' +In this example the raycast functionality is showcased. The circle moves around +and casts 10 rays and checks how far the nearest hitboxes are and naively moves +around trying not to hit them. + '''; + + Ray2? ray; + Ray2? reflection; + Vector2 origin = Vector2(250, 100); + Paint paint = Paint()..color = Colors.amber.withOpacity(0.6); + final speed = 100; + final inertia = 3.0; + final safetyDistance = 50; + final direction = Vector2(0, 1); + final velocity = Vector2.zero(); + final random = Random(); + + static const numberOfRays = 10; + final List rays = []; + final List> results = []; + + late Path path; + @override + Future onLoad() async { + final paint = BasicPalette.gray.paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0; + add(ScreenHitbox()); + add( + CircleComponent( + position: Vector2(100, 100), + radius: 50, + paint: paint, + children: [CircleHitbox()], + ), + ); + add( + CircleComponent( + position: Vector2(150, 500), + radius: 50, + paint: paint, + children: [CircleHitbox()], + ), + ); + add( + RectangleComponent( + position: Vector2.all(300), + size: Vector2.all(100), + paint: paint, + children: [RectangleHitbox()], + ), + ); + add( + RectangleComponent( + position: Vector2.all(500), + size: Vector2(100, 200), + paint: paint, + children: [RectangleHitbox()], + ), + ); + add( + RectangleComponent( + position: Vector2(550, 200), + size: Vector2(200, 150), + paint: paint, + children: [RectangleHitbox()], + ), + ); + } + + final _velocityModifier = Vector2.zero(); + + @override + void update(double dt) { + super.update(dt); + collisionDetection.raycastAll( + origin, + numberOfRays: numberOfRays, + rays: rays, + out: results, + ); + velocity.scale(inertia); + for (final result in results) { + _velocityModifier + ..setFrom(result.intersectionPoint!) + ..sub(origin) + ..normalize(); + if (result.distance! < safetyDistance) { + _velocityModifier.negate(); + } else if (random.nextDouble() < 0.2) { + velocity.add(_velocityModifier); + } + velocity.add(_velocityModifier); + } + velocity + ..normalize() + ..scale(speed * dt); + origin.add(velocity); + } + + @override + void render(Canvas canvas) { + super.render(canvas); + renderResult(canvas, origin, results, paint); + } + + void renderResult( + Canvas canvas, + Vector2 origin, + List> results, + Paint paint, + ) { + final originOffset = origin.toOffset(); + for (final result in results) { + if (!result.isActive) { + continue; + } + final intersectionPoint = result.intersectionPoint!.toOffset(); + canvas.drawLine( + originOffset, + intersectionPoint, + paint, + ); + } + canvas.drawCircle(originOffset, 5, paint); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/raycast_light_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/raycast_light_example.dart new file mode 100644 index 0000000..b6f8864 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/raycast_light_example.dart @@ -0,0 +1,163 @@ +import 'dart:math'; + +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame/geometry.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/material.dart'; + +class RaycastLightExample extends FlameGame + with HasCollisionDetection, TapDetector, MouseMovementDetector { + static const description = ''' +In this example the raycast functionality is showcased by using it as a light +source, if you move the mouse around the canvas the rays will be cast from its +location. You can also tap to create a permanent source of rays that wont move +with with mouse. + '''; + + Ray2? ray; + Ray2? reflection; + Vector2? origin; + Vector2? tapOrigin; + bool isOriginCasted = false; + bool isTapOriginCasted = false; + Paint paint = Paint(); + Paint tapPaint = Paint(); + + final _colorTween = ColorTween( + begin: Colors.blue.withOpacity(0.2), + end: Colors.red.withOpacity(0.2), + ); + + static const numberOfRays = 2000; + final List rays = []; + final List tapRays = []; + final List> results = []; + final List> tapResults = []; + + late Path path; + @override + Future onLoad() async { + final paint = BasicPalette.gray.paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0; + add(ScreenHitbox()); + add( + CircleComponent( + position: Vector2(100, 100), + radius: 50, + paint: paint, + children: [CircleHitbox()], + ), + ); + add( + CircleComponent( + position: Vector2(150, 500), + radius: 50, + paint: paint, + children: [CircleHitbox()], + ), + ); + add( + RectangleComponent( + position: Vector2.all(300), + size: Vector2.all(100), + paint: paint, + children: [RectangleHitbox()], + ), + ); + add( + RectangleComponent( + position: Vector2.all(500), + size: Vector2(100, 200), + paint: paint, + children: [RectangleHitbox()], + ), + ); + add( + RectangleComponent( + position: Vector2(550, 200), + size: Vector2(200, 150), + paint: paint, + children: [RectangleHitbox()], + ), + ); + } + + @override + void onTapDown(TapDownInfo info) { + super.onTapDown(info); + final origin = info.eventPosition.widget; + isTapOriginCasted = origin == tapOrigin; + tapOrigin = origin; + } + + @override + void onMouseMove(PointerHoverInfo info) { + final origin = info.eventPosition.widget; + isOriginCasted = origin == this.origin; + this.origin = origin; + } + + var _timePassed = 0.0; + + @override + void update(double dt) { + super.update(dt); + _timePassed += dt; + paint.color = _colorTween.transform(0.5 + (sin(_timePassed) / 2))!; + tapPaint.color = _colorTween.transform(0.5 + (cos(_timePassed) / 2))!; + if (origin != null && !isOriginCasted) { + collisionDetection.raycastAll( + origin!, + numberOfRays: numberOfRays, + rays: rays, + out: results, + ); + isOriginCasted = true; + } + if (tapOrigin != null && !isTapOriginCasted) { + collisionDetection.raycastAll( + tapOrigin!, + numberOfRays: numberOfRays, + rays: tapRays, + out: tapResults, + ); + isTapOriginCasted = true; + } + } + + @override + void render(Canvas canvas) { + super.render(canvas); + if (origin != null) { + renderResult(canvas, origin!, results, paint); + } + if (tapOrigin != null) { + renderResult(canvas, tapOrigin!, tapResults, tapPaint); + } + } + + void renderResult( + Canvas canvas, + Vector2 origin, + List> results, + Paint paint, + ) { + final originOffset = origin.toOffset(); + for (final result in results) { + if (!result.isActive) { + continue; + } + final intersectionPoint = result.intersectionPoint!.toOffset(); + canvas.drawLine( + originOffset, + intersectionPoint, + paint, + ); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/raycast_max_distance_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/raycast_max_distance_example.dart new file mode 100644 index 0000000..936b5c2 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/raycast_max_distance_example.dart @@ -0,0 +1,138 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/geometry.dart'; +import 'package:flame/palette.dart'; +import 'package:flame_noise/flame_noise.dart'; +import 'package:flutter/material.dart'; + +class RaycastMaxDistanceExample extends FlameGame with HasCollisionDetection { + static const description = ''' +This examples showcases how raycast APIs can be used to detect hits within certain range. +'''; + + static const _maxDistance = 50.0; + + late Ray2 _ray; + late _Character _character; + final _result = RaycastResult(); + + final _text = TextComponent( + text: "Hey! Who's there?", + anchor: Anchor.center, + textRenderer: TextPaint( + style: const TextStyle( + fontSize: 8, + color: Colors.amber, + ), + ), + ); + + @override + void onLoad() { + camera = CameraComponent.withFixedResolution( + world: world, + width: 320, + height: 180, + ); + + _addMovingWall(); + + world.add( + _character = _Character( + maxDistance: _maxDistance, + position: Vector2(-50, 0), + anchor: Anchor.center, + ), + ); + + _text.position = _character.position - Vector2(0, 50); + + _ray = Ray2( + origin: _character.absolutePosition, + direction: Vector2(1, 0), + ); + } + + void _addMovingWall() { + world.add( + RectangleComponent( + size: Vector2(20, 40), + anchor: Anchor.center, + paint: BasicPalette.red.paint(), + children: [ + RectangleHitbox(), + MoveByEffect( + Vector2(50, 0), + EffectController( + duration: 2, + alternate: true, + infinite: true, + ), + ), + ], + ), + ); + } + + @override + void update(double dt) { + collisionDetection.raycast(_ray, maxDistance: _maxDistance, out: _result); + if (_result.isActive) { + if (camera.viewfinder.children.query().isEmpty) { + camera.viewfinder.add( + MoveEffect.by( + Vector2(5, 5), + NoiseEffectController( + duration: 0.2, + noise: PerlinNoise(frequency: 400), + ), + ), + ); + } + if (!_text.isMounted) { + world.add(_text); + } + } else { + _text.removeFromParent(); + } + super.update(dt); + } +} + +class _Character extends PositionComponent { + _Character({required this.maxDistance, super.position, super.anchor}); + + final double maxDistance; + + final _rayOriginPoint = Offset.zero; + late final _rayEndPoint = Offset(maxDistance, 0); + final _rayPaint = BasicPalette.gray.paint(); + + @override + Future? onLoad() async { + addAll([ + CircleComponent( + radius: 20, + anchor: Anchor.center, + paint: BasicPalette.green.paint(), + )..scale = Vector2(0.55, 1), + CircleComponent( + radius: 10, + anchor: Anchor.center, + paint: _rayPaint, + ), + RectangleComponent( + size: Vector2(10, 3), + position: Vector2(12, 5), + ), + ]); + } + + @override + void render(Canvas canvas) { + canvas.drawLine(_rayOriginPoint, _rayEndPoint, _rayPaint); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/rays_in_shape_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/rays_in_shape_example.dart new file mode 100644 index 0000000..28f6171 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/rays_in_shape_example.dart @@ -0,0 +1,158 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/geometry.dart'; +import 'package:flutter/material.dart'; + +const playArea = Rect.fromLTRB(-100, -100, 100, 100); + +class RaysInShapeExample extends FlameGame { + static const description = ''' +In this example we showcase the raytrace functionality where you can see whether +the rays are inside the shapes or not. Click to change the shape that the rays +are casted against. The rays originates from small circles, and if the circle is +inside the shape it will be red, otherwise green. And if the ray doesn't hit any +shape it will be gray. +'''; + + RaysInShapeExample() + : super( + world: RaysInShapeWorld(), + camera: CameraComponent.withFixedResolution( + width: playArea.width, + height: playArea.height, + ), + ); +} + +final whiteStroke = Paint() + ..color = const Color(0xffffffff) + ..style = PaintingStyle.stroke; + +final lightStroke = Paint() + ..color = const Color(0x50ffffff) + ..style = PaintingStyle.stroke; + +final greenStroke = Paint() + ..color = const Color(0xff00ff00) + ..style = PaintingStyle.stroke; + +final redStroke = Paint() + ..color = const Color(0xffff0000) + ..style = PaintingStyle.stroke; + +class RaysInShapeWorld extends World + with + HasGameReference, + HasCollisionDetection, + TapCallbacks { + final _rng = Random(); + List _rays = []; + + List randomRays(int count) => List.generate( + count, + (index) => Ray2( + origin: (Vector2.random(_rng)) * playArea.size.width - + playArea.size.toVector2() / 2, + direction: (Vector2.random(_rng) - Vector2(0.5, 0.5)).normalized(), + ), + ); + + int _componentIndex = 0; + + final _components = [ + CircleComponent( + radius: 60, + anchor: Anchor.center, + position: Vector2.zero(), + paint: whiteStroke, + children: [CircleHitbox()], + ), + RectangleComponent( + size: Vector2(100, 100), + anchor: Anchor.center, + position: Vector2.zero(), + paint: whiteStroke, + children: [RectangleHitbox()], + ), + PositionComponent( + position: Vector2.zero(), + children: [ + PolygonHitbox.relative( + [ + Vector2(-0.7, -1), + Vector2(1, -0.4), + Vector2(0.3, 1), + Vector2(-1, 0.6), + ], + parentSize: Vector2(100, 100), + anchor: Anchor.center, + position: Vector2.zero(), + ) + ..paint = whiteStroke + ..renderShape = true, + ], + ), + ]; + + @override + FutureOr onLoad() { + super.onLoad(); + add(_components[_componentIndex]); + _rays = randomRays(200); + } + + @override + void onTapUp(TapUpEvent event) { + super.onTapUp(event); + remove(_components[_componentIndex]); + _componentIndex = (_componentIndex + 1) % _components.length; + add(_components[_componentIndex]); + _recording.clear(); + _rays = randomRays(200); + } + + final Map?> _recording = {}; + + @override + void update(double dt) { + super.update(dt); + + for (final ray in _rays) { + final result = collisionDetection.raycast(ray); + _recording.addAll({ray: result}); + } + } + + @override + void render(Canvas canvas) { + super.render(canvas); + for (final ray in _recording.keys) { + final result = _recording[ray]; + if (result == null) { + canvas.drawLine( + ray.origin.toOffset(), + (ray.origin + ray.direction.scaled(10)).toOffset(), + lightStroke, + ); + canvas.drawCircle(ray.origin.toOffset(), 1, lightStroke); + } else { + canvas.drawLine( + ray.origin.toOffset(), + result.intersectionPoint!.toOffset(), + lightStroke, + ); + canvas.drawCircle( + ray.origin.toOffset(), + 1, + result.isInsideHitbox ? redStroke : greenStroke, + ); + } + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/raytrace_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/raytrace_example.dart new file mode 100644 index 0000000..5115ed8 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/collision_detection/raytrace_example.dart @@ -0,0 +1,191 @@ +import 'dart:math'; + +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame/geometry.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/material.dart'; + +class RaytraceExample extends FlameGame + with + HasCollisionDetection, + TapDetector, + MouseMovementDetector, + TapDetector { + static const description = ''' +In this example the raytrace functionality is showcased. +Click to start sending out a ray which will bounce around to visualize how it +works. If you move the mouse around the canvas, rays and their reflections will +be moved rendered and if you click again some more objects that the rays can +bounce on will appear. + '''; + + final _colorTween = ColorTween( + begin: Colors.amber.withOpacity(1.0), + end: Colors.lightBlueAccent.withOpacity(1.0), + ); + final random = Random(); + Ray2? ray; + Ray2? reflection; + Vector2? origin; + bool isOriginCasted = false; + Paint rayPaint = Paint(); + final boxPaint = BasicPalette.gray.paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0; + + final List rays = []; + final List> results = []; + + late Path path; + @override + Future onLoad() async { + addAll([ + ScreenHitbox(), + CircleComponent( + radius: min(canvasSize.x, canvasSize.y) / 2, + paint: boxPaint, + children: [CircleHitbox()], + ), + ]); + } + + bool isClicked = false; + final extraChildren = []; + @override + void onTap() { + if (!isClicked) { + isClicked = true; + return; + } + _timePassed = 0; + if (extraChildren.isEmpty) { + addAll( + extraChildren + ..addAll( + [ + CircleComponent( + position: Vector2(100, 100), + radius: 50, + paint: boxPaint, + children: [CircleHitbox()], + ), + CircleComponent( + position: Vector2(150, 500), + radius: 50, + paint: boxPaint, + anchor: Anchor.center, + children: [CircleHitbox()], + ), + CircleComponent( + position: Vector2(150, 500), + radius: 150, + paint: boxPaint, + anchor: Anchor.center, + children: [CircleHitbox()], + ), + RectangleComponent( + position: Vector2.all(300), + size: Vector2.all(100), + paint: boxPaint, + children: [RectangleHitbox()], + ), + RectangleComponent( + position: Vector2.all(500), + size: Vector2(100, 200), + paint: boxPaint, + children: [RectangleHitbox()], + ), + CircleComponent( + position: Vector2(650, 275), + radius: 50, + paint: boxPaint, + anchor: Anchor.center, + children: [CircleHitbox()], + ), + RectangleComponent( + position: Vector2(550, 200), + size: Vector2(200, 150), + paint: boxPaint, + children: [RectangleHitbox()], + ), + RectangleComponent( + position: Vector2(350, 30), + size: Vector2(200, 150), + paint: boxPaint, + angle: tau / 10, + children: [RectangleHitbox()], + ), + ], + ), + ); + } else { + removeAll(extraChildren); + extraChildren.clear(); + } + } + + @override + void onMouseMove(PointerHoverInfo info) { + final origin = info.eventPosition.widget; + isOriginCasted = origin == this.origin; + this.origin = origin; + } + + final Ray2 _ray = Ray2.zero(); + var _timePassed = 0.0; + + @override + void update(double dt) { + super.update(dt); + if (isClicked) { + _timePassed += dt; + } + rayPaint.color = _colorTween.transform(0.5 + (sin(_timePassed) / 2))!; + if (origin != null) { + _ray.origin.setFrom(origin!); + _ray.direction + ..setValues(1, 1) + ..normalize(); + collisionDetection + .raytrace( + _ray, + maxDepth: min((_timePassed * 8).ceil(), 1000), + out: results, + ) + .toList(); + isOriginCasted = true; + } + } + + @override + void render(Canvas canvas) { + super.render(canvas); + if (origin != null) { + renderResult(canvas, origin!, results, rayPaint); + } + } + + void renderResult( + Canvas canvas, + Vector2 origin, + List> results, + Paint paint, + ) { + var originOffset = origin.toOffset(); + for (final result in results) { + if (!result.isActive) { + continue; + } + final intersectionPoint = result.intersectionPoint!.toOffset(); + canvas.drawLine( + originOffset, + intersectionPoint, + paint, + ); + originOffset = intersectionPoint; + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/components/clip_component_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/components/clip_component_example.dart new file mode 100644 index 0000000..84f7bb8 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/components/clip_component_example.dart @@ -0,0 +1,80 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart' hide Gradient; + +class _Rectangle extends RectangleComponent { + _Rectangle() + : super( + size: Vector2(200, 200), + anchor: Anchor.center, + paint: Paint() + ..shader = Gradient.linear( + Offset.zero, + const Offset(0, 100), + [Colors.orange, Colors.blue], + ), + children: [ + RotateEffect.by( + pi * 2, + EffectController(duration: .4, infinite: true), + ), + ], + ); +} + +class ClipComponentExample extends FlameGame with TapDetector { + static const String description = + 'Tap on the objects to increase their size.'; + + @override + Future onLoad() async { + addAll( + [ + ClipComponent.circle( + position: Vector2(100, 100), + size: Vector2.all(50), + children: [_Rectangle()], + ), + ClipComponent.rectangle( + position: Vector2(200, 100), + size: Vector2.all(50), + children: [_Rectangle()], + ), + ClipComponent.polygon( + points: [ + Vector2(1, 0), + Vector2(1, 1), + Vector2(0, 1), + Vector2(1, 0), + ], + position: Vector2(200, 200), + size: Vector2.all(50), + children: [_Rectangle()], + ), + ], + ); + } + + @override + void onTapUp(TapUpInfo info) { + final position = info.eventPosition.widget; + final hit = children + .whereType() + .where( + (component) => component.containsLocalPoint( + position - component.position, + ), + ) + .toList(); + + hit.forEach((component) { + component.size += Vector2.all(10); + }); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/components/components.dart b/flame/assets/examples/official/dashbook_example/lib/stories/components/components.dart new file mode 100644 index 0000000..1b7aad0 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/components/components.dart @@ -0,0 +1,96 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/components/clip_component_example.dart'; +import 'package:examples/stories/components/components_notifier_example.dart'; +import 'package:examples/stories/components/components_notifier_provider_example.dart'; +import 'package:examples/stories/components/composability_example.dart'; +import 'package:examples/stories/components/debug_example.dart'; +import 'package:examples/stories/components/has_visibility_example.dart'; +import 'package:examples/stories/components/keys_example.dart'; +import 'package:examples/stories/components/look_at_example.dart'; +import 'package:examples/stories/components/look_at_smooth_example.dart'; +import 'package:examples/stories/components/priority_example.dart'; +import 'package:examples/stories/components/spawn_component_example.dart'; +import 'package:examples/stories/components/time_scale_example.dart'; +import 'package:flame/game.dart'; + +void addComponentsStories(Dashbook dashbook) { + dashbook.storiesOf('Components') + ..add( + 'Composability', + (_) => GameWidget(game: ComposabilityExample()), + codeLink: baseLink('components/composability_example.dart'), + info: ComposabilityExample.description, + ) + ..add( + 'Priority', + (_) => GameWidget(game: PriorityExample()), + codeLink: baseLink('components/priority_example.dart'), + info: PriorityExample.description, + ) + ..add( + 'Debug', + (_) => GameWidget(game: DebugExample()), + codeLink: baseLink('components/debug_example.dart'), + info: DebugExample.description, + ) + ..add( + 'ClipComponent', + (context) => GameWidget(game: ClipComponentExample()), + codeLink: baseLink('components/clip_component_example.dart'), + info: ClipComponentExample.description, + ) + ..add( + 'Look At', + (_) => GameWidget(game: LookAtExample()), + codeLink: baseLink('components/look_at_example.dart'), + info: LookAtExample.description, + ) + ..add( + 'Look At Smooth', + (_) => GameWidget(game: LookAtSmoothExample()), + codeLink: baseLink('components/look_at_smooth_example.dart'), + info: LookAtExample.description, + ) + ..add( + 'Component Notifier', + (_) => const ComponentsNotifierExampleWidget(), + codeLink: baseLink('components/components_notifier_example.dart'), + info: ComponentsNotifierExampleWidget.description, + ) + ..add( + 'Component Notifier (with provider)', + (_) => const ComponentsNotifierProviderExampleWidget(), + codeLink: + baseLink('components/components_notifier_provider_example.dart'), + info: ComponentsNotifierProviderExampleWidget.description, + ) + ..add( + 'Spawn Component', + (_) => const GameWidget.controlled( + gameFactory: SpawnComponentExample.new, + ), + codeLink: baseLink('components/spawn_component_example.dart'), + info: SpawnComponentExample.description, + ) + ..add( + 'Time Scale', + (_) => const GameWidget.controlled( + gameFactory: TimeScaleExample.new, + ), + codeLink: baseLink('components/time_scale_example.dart'), + info: TimeScaleExample.description, + ) + ..add( + 'Component Keys', + (_) => const KeysExampleWidget(), + codeLink: baseLink('components/keys_example.dart'), + info: KeysExampleWidget.description, + ) + ..add( + 'HasVisibility', + (_) => GameWidget(game: HasVisibilityExample()), + codeLink: baseLink('components/has_visibility_example.dart'), + info: HasVisibilityExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/components/components_notifier_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/components/components_notifier_example.dart new file mode 100644 index 0000000..e2baf80 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/components/components_notifier_example.dart @@ -0,0 +1,111 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame/widgets.dart'; +import 'package:flutter/material.dart'; + +class ComponentsNotifierExampleWidget extends StatefulWidget { + const ComponentsNotifierExampleWidget({super.key}); + + static const String description = ''' + Showcases how the components notifier can be used between + a flame game instance and widgets. + + Tap the red dots to defeat the enemies and see the hud being updated + to reflect the current state of the game. +'''; + + @override + State createState() => + _ComponentsNotifierExampleWidgetState(); +} + +class _ComponentsNotifierExampleWidgetState + extends State { + @override + void initState() { + super.initState(); + + game = ComponentNotifierExample(); + } + + late final ComponentNotifierExample game; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: GameWidget(game: game), + ), + Positioned( + left: 16, + top: 16, + child: ComponentsNotifierBuilder( + notifier: game.componentsNotifier(), + builder: (context, notifier) { + return GameHud( + remainingEnemies: notifier.components.length, + onReplay: game.replay, + ); + }, + ), + ), + ], + ), + ); + } +} + +class GameHud extends StatelessWidget { + const GameHud({ + required this.remainingEnemies, + required this.onReplay, + super.key, + }); + + final int remainingEnemies; + final VoidCallback onReplay; + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: remainingEnemies == 0 + ? ElevatedButton( + onPressed: onReplay, + child: const Text('Play again'), + ) + : Text('Remaining enemies: $remainingEnemies'), + ), + ); + } +} + +class Enemy extends CircleComponent with TapCallbacks, Notifier { + Enemy({super.position}) + : super( + radius: 20, + paint: Paint()..color = const Color(0xFFFF0000), + ); + + @override + void onTapUp(_) { + removeFromParent(); + } +} + +class ComponentNotifierExample extends FlameGame { + @override + Future onLoad() async { + replay(); + } + + void replay() { + add(Enemy(position: Vector2(100, 100))); + add(Enemy(position: Vector2(200, 100))); + add(Enemy(position: Vector2(300, 100))); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/components/components_notifier_provider_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/components/components_notifier_provider_example.dart new file mode 100644 index 0000000..b21bd61 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/components/components_notifier_provider_example.dart @@ -0,0 +1,105 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ComponentsNotifierProviderExampleWidget extends StatefulWidget { + const ComponentsNotifierProviderExampleWidget({super.key}); + + static const String description = ''' + Similar to the Components Notifier example, but uses provider + instead of the built in ComponentsNotifierBuilder widget. +'''; + + @override + State createState() => + _ComponentsNotifierProviderExampleWidgetState(); +} + +class _ComponentsNotifierProviderExampleWidgetState + extends State { + @override + void initState() { + super.initState(); + + game = ComponentNotifierExample(); + } + + late final ComponentNotifierExample game; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: MultiProvider( + providers: [ + Provider.value(value: game), + ChangeNotifierProvider>( + create: (_) => game.componentsNotifier(), + ), + ], + child: Stack( + children: [ + Positioned.fill( + child: GameWidget(game: game), + ), + const Positioned( + left: 16, + top: 16, + child: GameHud(), + ), + ], + ), + ), + ); + } +} + +class GameHud extends StatelessWidget { + const GameHud({super.key}); + + @override + Widget build(BuildContext context) { + final enemies = context.watch>().components; + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: enemies.isEmpty + ? ElevatedButton( + child: const Text('Play again'), + onPressed: () { + context.read().replay(); + }, + ) + : Text('Remaining enemies: ${enemies.length}'), + ), + ); + } +} + +class Enemy extends CircleComponent with TapCallbacks, Notifier { + Enemy({super.position}) + : super( + radius: 20, + paint: Paint()..color = const Color(0xFFFF0000), + ); + + @override + void onTapUp(_) { + removeFromParent(); + } +} + +class ComponentNotifierExample extends FlameGame { + @override + Future onLoad() async { + replay(); + } + + void replay() { + add(Enemy(position: Vector2(100, 100))); + add(Enemy(position: Vector2(200, 100))); + add(Enemy(position: Vector2(300, 100))); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/components/composability_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/components/composability_example.dart new file mode 100644 index 0000000..555b3dc --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/components/composability_example.dart @@ -0,0 +1,81 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/palette.dart'; + +class ComposabilityExample extends FlameGame { + static const String description = ''' + In this example we showcase how you can add children to a component and how + they transform together with their parent, if the parent is a + `PositionComponent`. This example is not interactive. + '''; + + late ParentSquare parentSquare; + + @override + bool debugMode = true; + + @override + Future onLoad() async { + parentSquare = ParentSquare(Vector2.all(200), Vector2.all(300)) + ..anchor = Anchor.center; + add(parentSquare); + } + + @override + void update(double dt) { + super.update(dt); + parentSquare.angle += dt; + } +} + +class ParentSquare extends RectangleComponent with HasGameRef { + static final defaultPaint = BasicPalette.white.paint() + ..style = PaintingStyle.stroke; + + ParentSquare(Vector2 position, Vector2 size) + : super( + position: position, + size: size, + paint: defaultPaint, + ); + + @override + Future onLoad() async { + createChildren(); + } + + void createChildren() { + // All positions here are in relation to the parent's position + const childSize = 50.0; + final children = [ + RectangleComponent.square( + position: Vector2(100, 100), + size: childSize, + angle: 2, + paint: defaultPaint, + ), + RectangleComponent.square( + position: Vector2(160, 100), + size: childSize, + angle: 3, + paint: defaultPaint, + ), + RectangleComponent.square( + position: Vector2(170, 150), + size: childSize, + angle: 4, + paint: defaultPaint, + ), + RectangleComponent.square( + position: Vector2(70, 200), + size: childSize, + angle: 5, + paint: defaultPaint, + ), + ]; + + addAll(children); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/components/debug_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/components/debug_example.dart new file mode 100644 index 0000000..ecde44f --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/components/debug_example.dart @@ -0,0 +1,67 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; + +class DebugExample extends FlameGame { + static const String description = ''' + In this example we show what you will see when setting `debugMode = true` + and add the `FPSTextComponent` to your game. + This is a non-interactive example. + '''; + + @override + bool debugMode = true; + + @override + Future onLoad() async { + final flameLogo = await loadSprite('flame.png'); + + final flame1 = LogoComponent(flameLogo); + flame1.x = 100; + flame1.y = 400; + + final flame2 = LogoComponent(flameLogo); + flame2.x = 100; + flame2.y = 400; + flame2.yDirection = -1; + + final flame3 = LogoComponent(flameLogo); + flame3.x = 100; + flame3.y = 400; + flame3.xDirection = -1; + + add(flame1); + add(flame2); + add(flame3); + + add(FpsTextComponent(position: Vector2(0, size.y - 24))); + } +} + +class LogoComponent extends SpriteComponent + with HasGameReference { + static const int speed = 150; + + int xDirection = 1; + int yDirection = 1; + + LogoComponent(Sprite sprite) : super(sprite: sprite, size: sprite.srcSize); + + @override + void update(double dt) { + x += xDirection * speed * dt; + + final rect = toRect(); + + if ((x <= 0 && xDirection == -1) || + (rect.right >= game.size.x && xDirection == 1)) { + xDirection = xDirection * -1; + } + + y += yDirection * speed * dt; + + if ((y <= 0 && yDirection == -1) || + (rect.bottom >= game.size.y && yDirection == 1)) { + yDirection = yDirection * -1; + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/components/has_visibility_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/components/has_visibility_example.dart new file mode 100644 index 0000000..d44f8f5 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/components/has_visibility_example.dart @@ -0,0 +1,30 @@ +import 'dart:async'; + +import 'package:flame/components.dart' hide Timer; +import 'package:flame/game.dart'; + +class HasVisibilityExample extends FlameGame { + static const String description = ''' + In this example we use the `HasVisibility` mixin to toggle the + visibility of a component without removing it from the parent + component. + This is a non-interactive example. + '''; + + @override + Future onLoad() async { + final flameLogoComponent = LogoComponent(await loadSprite('flame.png')); + add(flameLogoComponent); + + // Toggle visibility every second + const oneSecDuration = Duration(seconds: 1); + Timer.periodic( + oneSecDuration, + (Timer t) => flameLogoComponent.isVisible = !flameLogoComponent.isVisible, + ); + } +} + +class LogoComponent extends SpriteComponent with HasVisibility { + LogoComponent(Sprite sprite) : super(sprite: sprite, size: sprite.srcSize); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/components/keys_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/components/keys_example.dart new file mode 100644 index 0000000..8373039 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/components/keys_example.dart @@ -0,0 +1,118 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +class KeysExampleWidget extends StatefulWidget { + const KeysExampleWidget({super.key}); + + static const String description = ''' + Showcases how component keys can be used to find components + from a flame game instance. + + Use the buttons to select or deselect the heroes. +'''; + + @override + State createState() => _KeysExampleWidgetState(); +} + +class _KeysExampleWidgetState extends State { + late final KeysExampleGame game = KeysExampleGame(); + + void selectHero(ComponentKey key) { + final hero = game.findByKey(key); + if (hero != null) { + hero.selected = !hero.selected; + } + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned.fill( + child: GameWidget(game: game), + ), + Positioned( + left: 20, + top: 222, + width: 300, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + onPressed: () { + selectHero(ComponentKey.named('knight')); + }, + child: const Text('Knight'), + ), + ElevatedButton( + onPressed: () { + selectHero(ComponentKey.named('mage')); + }, + child: const Text('Mage'), + ), + ElevatedButton( + onPressed: () { + selectHero(ComponentKey.named('ranger')); + }, + child: const Text('Ranger'), + ), + ], + ), + ), + ], + ); + } +} + +class KeysExampleGame extends FlameGame { + @override + FutureOr onLoad() async { + await super.onLoad(); + + final knight = await loadSprite('knight.png'); + final mage = await loadSprite('mage.png'); + final ranger = await loadSprite('ranger.png'); + + await addAll([ + SelectableClass( + key: ComponentKey.named('knight'), + sprite: knight, + size: Vector2.all(100), + position: Vector2(0, 100), + ), + SelectableClass( + key: ComponentKey.named('mage'), + sprite: mage, + size: Vector2.all(100), + position: Vector2(120, 100), + ), + SelectableClass( + key: ComponentKey.named('ranger'), + sprite: ranger, + size: Vector2.all(100), + position: Vector2(240, 100), + ), + ]); + } +} + +class SelectableClass extends SpriteComponent { + SelectableClass({ + super.position, + super.size, + super.key, + super.sprite, + }) : super(paint: Paint()..color = Colors.white.withOpacity(0.5)); + + bool _selected = false; + bool get selected => _selected; + set selected(bool value) { + _selected = value; + paint = Paint() + ..color = value ? Colors.white : Colors.white.withOpacity(0.5); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/components/look_at_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/components/look_at_example.dart new file mode 100644 index 0000000..090241c --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/components/look_at_example.dart @@ -0,0 +1,113 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/palette.dart'; +import 'package:flame/sprite.dart'; +import 'package:flutter/material.dart'; + +class LookAtExample extends FlameGame { + static const description = 'This example demonstrates how a component can be ' + 'made to look at a specific target using the lookAt method. Tap anywhere ' + 'to change the target point for both the choppers. ' + 'It also shows how nativeAngle can be used to make the component ' + 'oriented in the desired direction if the image is not facing the ' + 'correct direction.'; + + LookAtExample() : super(world: _TapWorld()); + + late SpriteAnimationComponent _chopper1; + late SpriteAnimationComponent _chopper2; + + @override + Color backgroundColor() => const Color.fromARGB(255, 96, 145, 112); + + @override + Future onLoad() async { + final spriteSheet = SpriteSheet( + image: await images.load('animations/chopper.png'), + srcSize: Vector2.all(48), + ); + + _spawnChoppers(spriteSheet); + _spawnInfoText(); + } + + void _spawnChoppers(SpriteSheet spriteSheet) { + // Notice now the nativeAngle is set to pi because the chopper + // is facing in down/south direction in the original image. + world.add( + _chopper1 = SpriteAnimationComponent( + nativeAngle: pi, + size: Vector2.all(128), + anchor: Anchor.center, + animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05), + ), + ); + + // This chopper does not use correct nativeAngle, hence using + // lookAt on it results in the sprite pointing in incorrect + // direction visually. + world.add( + _chopper2 = SpriteAnimationComponent( + size: Vector2.all(128), + anchor: Anchor.center, + animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05), + position: Vector2(0, 160), + ), + ); + } + + // Just displays some information. No functional contribution to the example. + void _spawnInfoText() { + final shaded = TextPaint( + style: TextStyle( + color: BasicPalette.white.color, + fontSize: 30.0, + shadows: const [ + Shadow(offset: Offset(1, 1), blurRadius: 1), + ], + ), + ); + + world.add( + TextComponent( + text: 'nativeAngle = pi', + textRenderer: shaded, + anchor: Anchor.center, + position: _chopper1.absolutePosition + Vector2(0, -70), + ), + ); + + world.add( + TextComponent( + text: 'nativeAngle = 0', + textRenderer: shaded, + anchor: Anchor.center, + position: _chopper2.absolutePosition + Vector2(0, -70), + ), + ); + } +} + +class _TapWorld extends World with TapCallbacks { + final CircleComponent _targetComponent = CircleComponent( + radius: 5, + anchor: Anchor.center, + paint: BasicPalette.black.paint(), + ); + + @override + void onTapDown(TapDownEvent event) { + if (!_targetComponent.isMounted) { + add(_targetComponent); + } + _targetComponent.position = event.localPosition; + final choppers = children.query(); + for (final chopper in choppers) { + chopper.lookAt(event.localPosition); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/components/look_at_smooth_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/components/look_at_smooth_example.dart new file mode 100644 index 0000000..7d1e8ee --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/components/look_at_smooth_example.dart @@ -0,0 +1,128 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/palette.dart'; +import 'package:flame/sprite.dart'; +import 'package:flutter/material.dart'; + +class LookAtSmoothExample extends FlameGame { + static const description = 'This example demonstrates how a component can be ' + 'made to smoothly rotate towards a target using the angleTo method. ' + 'Tap anywhere to change the target point for both the choppers. ' + 'It also shows how nativeAngle can be used to make the component ' + 'oriented in the desired direction if the image is not facing the ' + 'correct direction.'; + + LookAtSmoothExample() : super(world: _TapWorld()); + + late SpriteAnimationComponent _chopper1; + late SpriteAnimationComponent _chopper2; + + @override + Color backgroundColor() => const Color.fromARGB(255, 96, 145, 112); + + @override + Future onLoad() async { + final spriteSheet = SpriteSheet( + image: await images.load('animations/chopper.png'), + srcSize: Vector2.all(48), + ); + + _spawnChoppers(spriteSheet); + _spawnInfoText(); + } + + void _spawnChoppers(SpriteSheet spriteSheet) { + // Notice now the nativeAngle is set to pi because the chopper + // is facing in down/south direction in the original image. + world.add( + _chopper1 = SpriteAnimationComponent( + nativeAngle: pi, + size: Vector2.all(128), + anchor: Anchor.center, + animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05), + ), + ); + + // This chopper does not use correct nativeAngle, hence using + // lookAt on it results in the sprite pointing in incorrect + // direction visually. + world.add( + _chopper2 = SpriteAnimationComponent( + size: Vector2.all(128), + anchor: Anchor.center, + animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05), + position: Vector2(0, 160), + ), + ); + } + + // Just displays some information. No functional contribution to the example. + void _spawnInfoText() { + final shaded = TextPaint( + style: TextStyle( + color: BasicPalette.white.color, + fontSize: 30.0, + shadows: const [ + Shadow(offset: Offset(1, 1), blurRadius: 1), + ], + ), + ); + + world.add( + TextComponent( + text: 'nativeAngle = pi', + textRenderer: shaded, + anchor: Anchor.center, + position: _chopper1.absolutePosition + Vector2(0, -70), + ), + ); + + world.add( + TextComponent( + text: 'nativeAngle = 0', + textRenderer: shaded, + anchor: Anchor.center, + position: _chopper2.absolutePosition + Vector2(0, -70), + ), + ); + } +} + +class _TapWorld extends World with TapCallbacks { + bool _isRotating = false; + + final CircleComponent _targetComponent = CircleComponent( + radius: 5, + anchor: Anchor.center, + paint: BasicPalette.black.paint(), + ); + + @override + void onTapDown(TapDownEvent event) { + if (!_targetComponent.isMounted) { + add(_targetComponent); + } + + // Ignore if choppers are already rotating. + if (!_isRotating) { + _isRotating = true; + _targetComponent.position = event.localPosition; + + final choppers = children.query(); + for (final chopper in choppers) { + chopper.add( + RotateEffect.by( + chopper.angleTo(_targetComponent.absolutePosition), + LinearEffectController(1), + onComplete: () => _isRotating = false, + ), + ); + } + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/components/priority_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/components/priority_example.dart new file mode 100644 index 0000000..daa4eef --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/components/priority_example.dart @@ -0,0 +1,39 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; + +class PriorityExample extends FlameGame { + static const String description = ''' + On this example, click on the square to bring them to the front by changing + the priority. + '''; + + PriorityExample() + : super( + children: [ + Square(Vector2(100, 100)), + Square(Vector2(160, 100)), + Square(Vector2(170, 150)), + Square(Vector2(110, 150)), + ], + ); +} + +class Square extends RectangleComponent + with HasGameReference, TapCallbacks { + Square(Vector2 position) + : super( + position: position, + size: Vector2.all(100), + paint: PaintExtension.random(withAlpha: 0.9, base: 100), + ); + + @override + void onTapDown(TapDownEvent event) { + final topComponent = game.children.last; + if (topComponent != this) { + priority = topComponent.priority + 1; + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/components/spawn_component_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/components/spawn_component_example.dart new file mode 100644 index 0000000..8f0c763 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/components/spawn_component_example.dart @@ -0,0 +1,62 @@ +import 'package:examples/commons/ember.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/math.dart'; + +class SpawnComponentExample extends FlameGame with TapDetector { + static const String description = + 'Tap on the screen to start spawning Embers within different shapes.'; + + SpawnComponentExample() : super(world: SpawnComponentWorld()); +} + +class SpawnComponentWorld extends World with TapCallbacks { + @override + void onTapDown(TapDownEvent info) { + final shapeType = Shapes.values.random(); + + final position = info.localPosition; + final shape = switch (shapeType) { + Shapes.rectangle => Rectangle.fromCenter( + center: position, + size: Vector2.all(200), + ), + Shapes.circle => Circle(position, 150), + Shapes.polygon => Polygon( + [ + Vector2(-1.0, 0.0), + Vector2(-0.8, 0.6), + Vector2(0.0, 1.0), + Vector2(0.6, 0.9), + Vector2(1.0, 0.0), + Vector2(0.3, -0.2), + Vector2(0.0, -1.0), + Vector2(-0.8, -0.5), + ].map((vertex) { + return vertex + ..scale(200) + ..add(position); + }).toList(), + ), + }; + + add( + SpawnComponent( + factory: (_) => Ember(), + period: 0.5, + area: shape, + within: randomFallback.nextBool(), + ), + ); + } +} + +enum Shapes { + rectangle, + circle, + polygon, +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/components/time_scale_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/components/time_scale_example.dart new file mode 100644 index 0000000..dfe59eb --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/components/time_scale_example.dart @@ -0,0 +1,130 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/image_composition.dart'; +import 'package:flame/palette.dart'; +import 'package:flame/sprite.dart'; +import 'package:flutter/rendering.dart'; + +class TimeScaleExample extends FlameGame + with HasTimeScale, HasCollisionDetection { + static const description = + 'This example shows how time scale can be used to control game speed.'; + + TimeScaleExample() + : super( + camera: CameraComponent.withFixedResolution( + width: 640, + height: 360, + ), + ); + + final gameSpeedText = TextComponent( + text: 'Time Scale: 1', + textRenderer: TextPaint( + style: TextStyle( + color: BasicPalette.white.color, + fontSize: 20.0, + shadows: const [ + Shadow(offset: Offset(1, 1), blurRadius: 1), + ], + ), + ), + anchor: Anchor.center, + ); + + @override + Color backgroundColor() => const Color.fromARGB(255, 88, 114, 97); + + @override + Future onLoad() async { + final spriteSheet = SpriteSheet( + image: await images.load('animations/chopper.png'), + srcSize: Vector2.all(48), + ); + gameSpeedText.position = Vector2(size.x * 0.5, size.y * 0.8); + + await world.addAll([ + _Chopper( + position: Vector2(-100, -10), + size: Vector2.all(64), + anchor: Anchor.center, + angle: -pi / 2, + animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05), + ), + _Chopper( + position: Vector2(100, 10), + size: Vector2.all(64), + anchor: Anchor.center, + angle: pi / 2, + animation: spriteSheet.createAnimation(row: 0, stepTime: 0.05), + ), + gameSpeedText, + ]); + return super.onLoad(); + } + + @override + void update(double dt) { + gameSpeedText.text = 'Time Scale : $timeScale'; + super.update(dt); + } +} + +class _Chopper extends SpriteAnimationComponent + with HasGameReference, CollisionCallbacks { + _Chopper({ + super.animation, + super.position, + super.size, + super.angle, + super.anchor, + }) : _moveDirection = Vector2(0, 1)..rotate(angle ?? 0), + _initialPosition = position?.clone() ?? Vector2.zero(); + + final Vector2 _moveDirection; + final _speed = 80.0; + final Vector2 _initialPosition; + late final _timer = TimerComponent( + period: 2, + onTick: _reset, + autoStart: false, + ); + + @override + Future onLoad() async { + await add(CircleHitbox()); + await add(_timer); + return super.onLoad(); + } + + @override + void updateTree(double dt) { + position.setFrom(position + _moveDirection * _speed * dt); + super.updateTree(dt); + } + + @override + void onCollisionStart(Set _, PositionComponent other) { + if (other is _Chopper) { + game.timeScale = 0.25; + } + super.onCollisionStart(_, other); + } + + @override + void onCollisionEnd(PositionComponent other) { + if (other is _Chopper) { + game.timeScale = 1.0; + _timer.timer.start(); + } + super.onCollisionEnd(other); + } + + void _reset() { + position.setFrom(_initialPosition); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/effects/color_effect_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/effects/color_effect_example.dart new file mode 100644 index 0000000..de93f22 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/effects/color_effect_example.dart @@ -0,0 +1,36 @@ +import 'package:examples/commons/ember.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; + +class ColorEffectExample extends FlameGame with TapDetector { + static const String description = ''' + In this example we show how the `ColorEffect` can be used. + Ember will constantly pulse in and out of a blue color. + '''; + + late final SpriteComponent sprite; + + @override + Future onLoad() async { + add( + Ember( + position: Vector2(180, 230), + size: Vector2.all(100), + )..add( + ColorEffect( + Colors.blue, + EffectController( + duration: 1.5, + reverseDuration: 1.5, + infinite: true, + ), + // Means, applies from 0% to 80% of the color + opacityTo: 0.8, + ), + ), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/effects/dual_effect_removal_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/effects/dual_effect_removal_example.dart new file mode 100644 index 0000000..ab307f9 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/effects/dual_effect_removal_example.dart @@ -0,0 +1,66 @@ +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; + +class DualEffectRemovalExample extends FlameGame with TapDetector { + static const String description = ''' + In this example we show how a dual effect can be used and removed. + To remove an effect, tap anywhere on the screen and the first tap will + remove the OpacityEffect and the second tap removes the ColorEffect. + In this example, when an effect is removed the component is reset to + the state (the part of the state that was affected by the running effect) + that it had before the effect started running. + '''; + + late ColorEffect colorEffect; + late OpacityEffect opacityEffect; + + @override + Future onLoad() async { + final mySprite = SpriteComponent( + sprite: await loadSprite('flame.png'), + position: Vector2(50, 50), + ); + + add(mySprite); + + final colorController = EffectController( + duration: 2, + reverseDuration: 2, + infinite: true, + ); + colorEffect = ColorEffect( + Colors.blue, + colorController, + opacityTo: 0.8, + ); + mySprite.add(colorEffect); + + final opacityController = EffectController( + duration: 1, + reverseDuration: 1, + infinite: true, + ); + opacityEffect = OpacityEffect.fadeOut(opacityController); + mySprite.add(opacityEffect); + } + + @override + void onTap() { + // apply(0) sends the animation to its initial starting state. + // If this isn't called, the effect would be removed and leave the + // component at its current state. + // Hence when you want an effect to be removed and the component to go + // back to how it looked prior to the effect, you must call apply(0) before + // you call removeFromParent(). + if (opacityEffect.isMounted) { + opacityEffect.apply(0); + opacityEffect.removeFromParent(); + } else if (colorEffect.isMounted) { + colorEffect.apply(0); + colorEffect.removeFromParent(); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/effects/effect_controllers_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/effects/effect_controllers_example.dart new file mode 100644 index 0000000..a79a2c3 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/effects/effect_controllers_example.dart @@ -0,0 +1,94 @@ +import 'dart:ui'; + +import 'package:flame/camera.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flame/geometry.dart'; + +class EffectControllersExample extends FlameGame { + static const description = ''' + This page demonstrates application of various non-standard effect + controllers. + + The first white square has a ZigzagEffectController with period 1. The + orange square next to it has two move effects, each with a + ZigzagEffectController. + + The lime square has a SineEffectController with the same period of 1s. The + violet square next to it has two move effects, each with a + SineEffectController with periods, but one of the effects is slightly + delayed. + '''; + + EffectControllersExample() + : super( + camera: CameraComponent.withFixedResolution( + width: 400, + height: 600, + ), + world: _EffectControllerWorld(), + ); +} + +class _EffectControllerWorld extends World { + @override + void onLoad() { + add( + RectangleComponent.square( + position: Vector2(-140, 0), + size: 20, + )..add( + MoveEffect.by( + Vector2(0, 20), + InfiniteEffectController(ZigzagEffectController(period: 1)), + ), + ), + ); + add( + RectangleComponent.square( + position: Vector2(-50, 0), + size: 20, + paint: Paint()..color = const Color(0xffffbc63), + )..addAll([ + MoveEffect.by( + Vector2(0, 20), + InfiniteEffectController(ZigzagEffectController(period: 8 / 7)), + ), + MoveEffect.by( + Vector2(10, 0), + InfiniteEffectController(ZigzagEffectController(period: 2 / 3)), + ), + ]), + ); + + add( + RectangleComponent.square( + position: Vector2(50, 0), + size: 20, + paint: Paint()..color = const Color(0xffbeff63), + )..add( + MoveEffect.by( + Vector2(0, 20), + InfiniteEffectController(SineEffectController(period: 1)), + ), + ), + ); + add( + RectangleComponent.square( + position: Vector2(140, 0), + size: 10, + paint: Paint()..color = const Color(0xffb663ff), + )..addAll([ + MoveEffect.by( + Vector2(0, 20), + InfiniteEffectController(SineEffectController(period: 1)) + ..advance(0.25), + ), + MoveEffect.by( + Vector2(10, 0), + InfiniteEffectController(SineEffectController(period: 1)), + ), + ]), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/effects/effects.dart b/flame/assets/examples/official/dashbook_example/lib/stories/effects/effects.dart new file mode 100644 index 0000000..262afcf --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/effects/effects.dart @@ -0,0 +1,77 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/effects/color_effect_example.dart'; +import 'package:examples/stories/effects/dual_effect_removal_example.dart'; +import 'package:examples/stories/effects/effect_controllers_example.dart'; +import 'package:examples/stories/effects/move_effect_example.dart'; +import 'package:examples/stories/effects/opacity_effect_example.dart'; +import 'package:examples/stories/effects/remove_effect_example.dart'; +import 'package:examples/stories/effects/rotate_effect_example.dart'; +import 'package:examples/stories/effects/scale_effect_example.dart'; +import 'package:examples/stories/effects/sequence_effect_example.dart'; +import 'package:examples/stories/effects/size_effect_example.dart'; +import 'package:flame/game.dart'; + +void addEffectsStories(Dashbook dashbook) { + dashbook.storiesOf('Effects') + ..add( + 'Move Effect', + (_) => GameWidget(game: MoveEffectExample()), + codeLink: baseLink('effects/move_effect_example.dart'), + info: MoveEffectExample.description, + ) + ..add( + 'Dual Effect Removal', + (_) => GameWidget(game: DualEffectRemovalExample()), + codeLink: baseLink('effects/dual_effect_removal_example.dart'), + info: DualEffectRemovalExample.description, + ) + ..add( + 'Rotate Effect', + (_) => GameWidget(game: RotateEffectExample()), + codeLink: baseLink('effects/rotate_effect_example.dart'), + info: RotateEffectExample.description, + ) + ..add( + 'Size Effect', + (_) => GameWidget(game: SizeEffectExample()), + codeLink: baseLink('effects/size_effect_example.dart'), + info: SizeEffectExample.description, + ) + ..add( + 'Scale Effect', + (_) => GameWidget(game: ScaleEffectExample()), + codeLink: baseLink('effects/scale_effect_example.dart'), + info: ScaleEffectExample.description, + ) + ..add( + 'Opacity Effect', + (_) => GameWidget(game: OpacityEffectExample()), + codeLink: baseLink('effects/opacity_effect_example.dart'), + info: OpacityEffectExample.description, + ) + ..add( + 'Color Effect', + (_) => GameWidget(game: ColorEffectExample()), + codeLink: baseLink('effects/color_effect_example.dart'), + info: ColorEffectExample.description, + ) + ..add( + 'Sequence Effect', + (_) => GameWidget(game: SequenceEffectExample()), + codeLink: baseLink('effects/sequence_effect_example.dart'), + info: SequenceEffectExample.description, + ) + ..add( + 'Remove Effect', + (_) => GameWidget(game: RemoveEffectExample()), + codeLink: baseLink('effects/remove_effect_example.dart'), + info: RemoveEffectExample.description, + ) + ..add( + 'EffectControllers', + (_) => GameWidget(game: EffectControllersExample()), + codeLink: baseLink('effects/effect_controllers_example.dart'), + info: EffectControllersExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/effects/glow_effect_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/effects/glow_effect_example.dart new file mode 100644 index 0000000..f66b620 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/effects/glow_effect_example.dart @@ -0,0 +1,39 @@ +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(GameWidget(game: GlowEffectExample())); +} + +class GlowEffectExample extends FlameGame with TapDetector { + static const String description = ''' + In this example we show how the `GlowEffect` can be used. + '''; + + @override + Future onLoad() async { + final paint = Paint() + ..color = const Color(0xff39FF14) + ..style = PaintingStyle.stroke; + + add( + CircleComponent( + radius: 50, + position: Vector2(300, 400), + paint: paint, + )..add( + GlowEffect( + 10.0, + EffectController( + duration: 3, + reverseDuration: 1.5, + infinite: true, + ), + ), + ), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/effects/move_effect_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/effects/move_effect_example.dart new file mode 100644 index 0000000..0dfe91a --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/effects/move_effect_example.dart @@ -0,0 +1,176 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flame/geometry.dart'; +import 'package:flame_noise/flame_noise.dart'; +import 'package:flutter/material.dart'; + +class MoveEffectExample extends FlameGame { + static const description = ''' + Top square has `MoveEffect.to` effect that makes the component move along a + straight line back and forth. The effect uses a non-linear progression + curve, which makes the movement non-uniform. + + The middle green square has a combination of two movement effects: a + `MoveEffect.to` and a `MoveEffect.by` which forces it to periodically jump. + + The purple square executes a sequence of shake effects. + + At the bottom there are 60 more components which demonstrate movement along + an arbitrary path using `MoveEffect.along`. + '''; + + MoveEffectExample() + : super( + camera: CameraComponent.withFixedResolution( + width: 400, + height: 600, + )..viewfinder.anchor = Anchor.topLeft, + world: _MoveEffectWorld(), + ); +} + +class _MoveEffectWorld extends World { + @override + void onLoad() { + final paint1 = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 5.0 + ..color = Colors.deepOrange; + final paint2 = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 5.0 + ..color = Colors.greenAccent; + final paint3 = Paint()..color = const Color(0xffb372dc); + + // Red square, moving back and forth + add( + RectangleComponent.square( + position: Vector2(20, 50), + size: 20, + paint: paint1, + )..add( + MoveEffect.to( + Vector2(380, 50), + EffectController( + duration: 3, + reverseDuration: 3, + infinite: true, + curve: Curves.easeOut, + ), + ), + ), + ); + + // Green square, moving and jumping + add( + RectangleComponent.square( + position: Vector2(20, 150), + size: 20, + paint: paint2, + ) + ..add( + MoveEffect.to( + Vector2(380, 150), + EffectController( + duration: 3, + reverseDuration: 3, + infinite: true, + ), + ), + ) + ..add( + MoveEffect.by( + Vector2(0, -50), + EffectController( + duration: 0.25, + reverseDuration: 0.25, + startDelay: 1, + atMinDuration: 2, + curve: Curves.ease, + infinite: true, + ), + ), + ), + ); + + // Purple square, vibrating from two noise controllers. + add( + RectangleComponent.square( + size: 15, + position: Vector2(40, 240), + paint: paint3, + )..add( + SequenceEffect( + [ + MoveEffect.by( + Vector2(5, 0), + NoiseEffectController( + duration: 1, + noise: PerlinNoise(frequency: 20), + ), + ), + MoveEffect.by(Vector2.zero(), LinearEffectController(2)), + MoveEffect.by( + Vector2(0, 10), + NoiseEffectController( + duration: 1, + noise: PerlinNoise(frequency: 10), + ), + ), + ], + infinite: true, + ), + ), + ); + + // A circle of moving rectangles. + final path2 = Path()..addOval(const Rect.fromLTRB(80, 230, 320, 470)); + for (var i = 0; i < 20; i++) { + add( + RectangleComponent.square(size: 10) + ..position = Vector2(i * 10, 0) + ..paint = (Paint()..color = Colors.tealAccent) + ..add( + MoveAlongPathEffect( + path2, + EffectController( + duration: 6, + startDelay: i * 0.3, + infinite: true, + ), + absolute: true, + oriented: true, + ), + ), + ); + } + + // A star of moving rectangles. + final path1 = Path()..moveTo(200, 250); + for (var i = 1; i <= 5; i++) { + final x = 200 + 100 * sin(i * tau * 2 / 5); + final y = 350 - 100 * cos(i * tau * 2 / 5); + path1.lineTo(x, y); + } + for (var i = 0; i < 40; i++) { + add( + CircleComponent(radius: 5) + ..position = Vector2(i * 10, 0) + ..add( + MoveAlongPathEffect( + path1, + EffectController( + duration: 10, + startDelay: i * 0.2, + infinite: true, + ), + absolute: true, + ), + ), + ); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/effects/opacity_effect_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/effects/opacity_effect_example.dart new file mode 100644 index 0000000..f13f5f3 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/effects/opacity_effect_example.dart @@ -0,0 +1,52 @@ +import 'package:examples/commons/ember.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; + +class OpacityEffectExample extends FlameGame with TapDetector { + static const String description = ''' + In this example we show how the `OpacityEffect` can be used in two ways. + The left Ember will constantly pulse in and out of opacity and the right + flame will change opacity when you click the screen. + '''; + + late final SpriteComponent sprite; + + @override + Future onLoad() async { + final flameSprite = await loadSprite('flame.png'); + add( + sprite = SpriteComponent( + sprite: flameSprite, + position: Vector2(300, 100), + size: Vector2(149, 211), + ), + ); + + add( + Ember( + position: Vector2(180, 230), + size: Vector2.all(100), + )..add( + OpacityEffect.fadeOut( + EffectController( + duration: 1.5, + reverseDuration: 1.5, + infinite: true, + ), + ), + ), + ); + } + + @override + void onTap() { + final opacity = sprite.paint.color.opacity; + if (opacity >= 0.5) { + sprite.add(OpacityEffect.fadeOut(EffectController(duration: 1))); + } else { + sprite.add(OpacityEffect.fadeIn(EffectController(duration: 1))); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/effects/remove_effect_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/effects/remove_effect_example.dart new file mode 100644 index 0000000..af7c22a --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/effects/remove_effect_example.dart @@ -0,0 +1,55 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +class RemoveEffectExample extends FlameGame { + static const description = ''' + Click on any circle to apply a RemoveEffect, which will make the circle + disappear after a 0.5 second delay. + '''; + + RemoveEffectExample() + : super( + camera: CameraComponent.withFixedResolution( + width: 400, + height: 600, + )..viewfinder.anchor = Anchor.topLeft, + world: _RemoveEffectWorld(), + ); +} + +class _RemoveEffectWorld extends World { + @override + void onLoad() { + super.onLoad(); + final rng = Random(); + for (var i = 0; i < 20; i++) { + add(_RandomCircle.random(rng)); + } + } +} + +class _RandomCircle extends CircleComponent with TapCallbacks { + _RandomCircle(double radius, {super.position, super.paint}) + : super(radius: radius); + + factory _RandomCircle.random(Random rng) { + final radius = rng.nextDouble() * 30 + 10; + final position = Vector2( + rng.nextDouble() * 320 + 40, + rng.nextDouble() * 520 + 40, + ); + final paint = Paint() + ..color = Colors.primaries[rng.nextInt(Colors.primaries.length)]; + return _RandomCircle(radius, position: position, paint: paint); + } + + @override + void onTapDown(TapDownEvent info) { + add(RemoveEffect(delay: 0.5)); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/effects/rotate_effect_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/effects/rotate_effect_example.dart new file mode 100644 index 0000000..2de51f7 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/effects/rotate_effect_example.dart @@ -0,0 +1,212 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flame/geometry.dart'; +import 'package:flutter/animation.dart'; + +class RotateEffectExample extends FlameGame { + static const description = ''' + The outer rim rotates at a different speed forward and reverse, and + uses the "ease" animation curve. + + The compass arrow has 3 rotation effects applied to it at the same + time: one effect rotates the arrow at a constant speed, and two more + add small amounts of wobble, creating quasi-chaotic movement. + '''; + + RotateEffectExample() + : super( + camera: CameraComponent.withFixedResolution( + width: 400, + height: 600, + ), + world: _RotateEffectWorld(), + ); +} + +class _RotateEffectWorld extends World { + @override + void onLoad() { + final compass = Compass(size: 200); + add(compass); + + compass.rim.add( + RotateEffect.by( + 1.0, + EffectController( + duration: 6, + reverseDuration: 3, + curve: Curves.ease, + infinite: true, + ), + ), + ); + compass.arrow + ..add( + RotateEffect.to( + tau, + EffectController( + duration: 20, + infinite: true, + ), + ), + ) + ..add( + RotateEffect.by( + tau * 0.015, + EffectController( + duration: 0.1, + reverseDuration: 0.1, + infinite: true, + ), + ), + ) + ..add( + RotateEffect.by( + tau * 0.021, + EffectController( + duration: 0.13, + reverseDuration: 0.13, + infinite: true, + ), + ), + ); + } +} + +class Compass extends PositionComponent { + Compass({required double size}) + : _radius = size / 2, + super( + size: Vector2.all(size), + anchor: Anchor.center, + ); + + late PositionComponent arrow; + late PositionComponent rim; + + final double _radius; + final _bgPaint = Paint()..color = const Color(0xffeacb31); + final _marksPaint = Paint() + ..color = const Color(0xFF7F6D36) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + late Path _marksPath; + + @override + Future onLoad() async { + _marksPath = Path(); + for (var i = 0; i < 12; i++) { + final angle = tau * (i / 12); + // Note: rim takes up 0.1radius, so the lengths must be > than that + final markLength = (i % 3 == 0) ? _radius * 0.2 : _radius * 0.15; + _marksPath.moveTo( + _radius + _radius * sin(angle), + _radius + _radius * cos(angle), + ); + _marksPath.lineTo( + _radius + (_radius - markLength) * sin(angle), + _radius + (_radius - markLength) * cos(angle), + ); + } + + arrow = CompassArrow(width: _radius * 0.3, radius: _radius * 0.7) + ..position = Vector2(_radius, _radius); + rim = CompassRim(radius: _radius, width: _radius * 0.1) + ..position = Vector2(_radius, _radius); + add(arrow); + add(rim); + } + + @override + void render(Canvas canvas) { + canvas.drawCircle(Offset(_radius, _radius), _radius, _bgPaint); + canvas.drawPath(_marksPath, _marksPaint); + } +} + +class CompassArrow extends PositionComponent { + CompassArrow({required double width, required double radius}) + : assert(width <= radius, 'The width is larger than the radius'), + _radius = radius, + _width = width, + super(size: Vector2(width, 2 * radius), anchor: Anchor.center); + + final double _radius; + final double _width; + late final Path _northPath; + late final Path _southPath; + final _northPaint = Paint()..color = const Color(0xff387fcb); + final _southPaint = Paint()..color = const Color(0xffa83636); + + @override + Future onLoad() async { + _northPath = Path() + ..moveTo(0, _radius) + ..lineTo(_width / 2, 0) + ..lineTo(_width, _radius) + ..close(); + _southPath = Path() + ..moveTo(0, _radius) + ..lineTo(_width, _radius) + ..lineTo(_width / 2, 2 * _radius) + ..close(); + } + + @override + void render(Canvas canvas) { + canvas.drawPath(_northPath, _northPaint); + canvas.drawPath(_southPath, _southPaint); + } +} + +class CompassRim extends PositionComponent { + CompassRim({required double radius, required double width}) + : assert(radius > width, 'The width is larger than the radius'), + _radius = radius, + _width = width, + super( + size: Vector2.all(2 * radius), + anchor: Anchor.center, + ); + + static const int numberOfNotches = 144; + final double _radius; + final double _width; + late final Path _marksPath; + final _bgPaint = Paint() + ..style = PaintingStyle.stroke + ..color = const Color(0xffb6a241); + final _marksPaint = Paint() + ..style = PaintingStyle.stroke + ..color = const Color(0xff3d3b26); + + @override + Future onLoad() async { + _bgPaint.strokeWidth = _width; + _marksPath = Path(); + final innerRadius = _radius - _width; + final midRadius = _radius - _width / 3; + for (var i = 0; i < numberOfNotches; i++) { + final angle = tau * (i / numberOfNotches); + _marksPath.moveTo( + _radius + innerRadius * sin(angle), + _radius + innerRadius * cos(angle), + ); + _marksPath.lineTo( + _radius + midRadius * sin(angle), + _radius + midRadius * cos(angle), + ); + } + } + + @override + void render(Canvas canvas) { + canvas.drawCircle(Offset(_radius, _radius), _radius - _width / 2, _bgPaint); + canvas.drawCircle(Offset(_radius, _radius), _radius - _width, _marksPaint); + canvas.drawPath(_marksPath, _marksPaint); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/effects/scale_effect_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/effects/scale_effect_example.dart new file mode 100644 index 0000000..3f6921e --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/effects/scale_effect_example.dart @@ -0,0 +1,96 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flame/geometry.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/animation.dart'; + +class ScaleEffectExample extends FlameGame with TapDetector { + static const String description = ''' + In this example you can tap the screen and the component will scale up or + down, depending on its current state. + + The star pulsates randomly using a RandomEffectController. + '''; + + late RectangleComponent square; + bool grow = true; + + @override + Future onLoad() async { + square = RectangleComponent.square( + size: 100, + position: Vector2.all(200), + paint: BasicPalette.white.paint()..style = PaintingStyle.stroke, + ); + final childSquare = RectangleComponent.square( + position: Vector2.all(70), + size: 20, + ); + square.add(childSquare); + add(square); + + add( + Star() + ..position = Vector2(200, 100) + ..add( + ScaleEffect.to( + Vector2.all(1.2), + InfiniteEffectController( + SequenceEffectController([ + LinearEffectController(0.1), + ReverseLinearEffectController(0.1), + RandomEffectController.exponential( + PauseEffectController(1, progress: 0), + beta: 1, + ), + ]), + ), + ), + ), + ); + } + + @override + void onTap() { + final s = grow ? 3.0 : 1.0; + + grow = !grow; + + square.add( + ScaleEffect.to( + Vector2.all(s), + EffectController( + duration: 1.5, + curve: Curves.bounceInOut, + ), + ), + ); + } +} + +class Star extends PositionComponent { + Star() { + const smallR = 15.0; + const bigR = 30.0; + shape = Path()..moveTo(bigR, 0); + for (var i = 1; i < 10; i++) { + final r = i.isEven ? bigR : smallR; + final a = i / 10 * tau; + shape.lineTo(r * cos(a), r * sin(a)); + } + shape.close(); + } + + late final Path shape; + late final Paint paint = Paint()..color = const Color(0xFFFFF127); + + @override + void render(Canvas canvas) { + canvas.drawPath(shape, paint); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/effects/sequence_effect_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/effects/sequence_effect_example.dart new file mode 100644 index 0000000..ea3f15e --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/effects/sequence_effect_example.dart @@ -0,0 +1,62 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flame/geometry.dart'; + +class SequenceEffectExample extends FlameGame { + static const String description = ''' + Sequence of effects, consisting of a move effect, a rotate effect, another + move effect, a scale effect, and then one more move effect. The sequence + then runs in the opposite order (alternate = true) and loops infinitely + (infinite = true). + '''; + + @override + Future onLoad() async { + EffectController duration(double x) => EffectController(duration: x); + add( + Player() + ..position = Vector2(200, 300) + ..add( + SequenceEffect( + [ + MoveEffect.to(Vector2(400, 300), duration(0.7)), + RotateEffect.by(tau / 4, duration(0.5)), + MoveEffect.to(Vector2(400, 400), duration(0.7)), + ScaleEffect.by(Vector2.all(1.5), duration(0.7)), + MoveEffect.to(Vector2(400, 500), duration(0.7)), + ], + alternate: true, + infinite: true, + ), + ), + ); + } +} + +class Player extends PositionComponent { + Player() + : path = Path() + ..lineTo(40, 20) + ..lineTo(0, 40) + ..quadraticBezierTo(8, 20, 0, 0) + ..close(), + bodyPaint = Paint()..color = const Color(0x887F99B3), + borderPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 3 + ..color = const Color(0xFFFFFD9A), + super(anchor: Anchor.center, size: Vector2(40, 40)); + + final Path path; + final Paint borderPaint; + final Paint bodyPaint; + + @override + void render(Canvas canvas) { + canvas.drawPath(path, bodyPaint); + canvas.drawPath(path, borderPaint); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/effects/size_effect_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/effects/size_effect_example.dart new file mode 100644 index 0000000..3d1a1eb --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/effects/size_effect_example.dart @@ -0,0 +1,43 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/animation.dart'; + +class SizeEffectExample extends FlameGame with TapDetector { + static const String description = ''' + The `SizeEffect` changes the size of the component, the sizes of the + children will stay the same. + In this example you can tap the screen and the component will size up or + down, depending on its current state. + '''; + + late Component shape; + bool grow = true; + + @override + Future onLoad() async { + shape = CircleComponent( + radius: 100, + position: Vector2.all(200), + paint: BasicPalette.white.paint()..style = PaintingStyle.stroke, + children: [ + RectangleComponent.square(position: Vector2.all(70), size: 20), + ], + )..addToParent(this); + } + + @override + void onTap() { + shape.add( + SizeEffect.to( + Vector2.all(grow ? 300.0 : 100.0), + EffectController(duration: 1.5, curve: Curves.bounceInOut), + ), + ); + grow = !grow; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/experimental/experimental.dart b/flame/assets/examples/official/dashbook_example/lib/stories/experimental/experimental.dart new file mode 100644 index 0000000..c2cb7cf --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/experimental/experimental.dart @@ -0,0 +1,13 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/experimental/shapes.dart'; +import 'package:flame/game.dart'; + +void addExperimentalStories(Dashbook dashbook) { + dashbook.storiesOf('Experimental').add( + 'Shapes', + (_) => GameWidget(game: ShapesExample()), + codeLink: baseLink('experimental/shapes.dart'), + info: ShapesExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/experimental/shapes.dart b/flame/assets/examples/official/dashbook_example/lib/stories/experimental/shapes.dart new file mode 100644 index 0000000..f08eb3d --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/experimental/shapes.dart @@ -0,0 +1,120 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/game.dart'; + +class ShapesExample extends FlameGame { + static const description = ''' + This example shows multiple raw `Shape`s, and random points whose color + should match the colors of the shapes that they fall in. Points that are + outside of any shape should be grey. + '''; + + @override + Future onLoad() async { + final shapes = [ + Circle(Vector2(50, 30), 20), + Circle(Vector2(700, 500), 50), + Rectangle.fromLTRB(100, 30, 260, 100), + RoundedRectangle.fromLTRBR(40, 300, 120, 550, 30), + Polygon([Vector2(10, 70), Vector2(180, 200), Vector2(220, 150)]), + Polygon([ + Vector2(400, 160), + Vector2(550, 400), + Vector2(710, 350), + Vector2(540, 170), + Vector2(710, 100), + Vector2(710, 320), + Vector2(730, 315), + Vector2(750, 60), + Vector2(590, 30), + ]), + ]; + const colors = [ + Color(0xFFFFFF88), + Color(0xFFff88FF), + Color(0xFF88FFFF), + Color(0xFF88FF88), + Color(0xFFaaaaFF), + Color(0xFFFF8888), + ]; + add(ShapesComponent(shapes, colors)); + add(DotsComponent(shapes, colors)); + } +} + +class ShapesComponent extends Component { + ShapesComponent(this.shapes, List colors) + : assert( + shapes.length == colors.length, + 'The shapes and colors lists have to be of the same length', + ), + paints = colors + .map( + (color) => Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..color = color, + ) + .toList(); + + final List shapes; + final List paints; + + @override + void render(Canvas canvas) { + for (var i = 0; i < shapes.length; i++) { + canvas.drawPath(shapes[i].asPath(), paints[i]); + } + } +} + +class DotsComponent extends Component { + DotsComponent(this.shapes, this.shapeColors) + : assert( + shapes.length == shapeColors.length, + 'The shapes and shapeColors lists have to be of the same length', + ); + + final List shapes; + final List shapeColors; + + final Random random = Random(); + final List points = []; + final List pointColors = []; + static const pointSize = 3; + + @override + void update(double dt) { + generatePoint(); + } + + void generatePoint() { + final point = Vector2( + random.nextDouble() * 800, + random.nextDouble() * 600, + ); + points.add(point); + pointColors.add(const Color(0xff444444)); + for (var i = 0; i < shapes.length; i++) { + if (shapes[i].containsPoint(point)) { + pointColors.last = shapeColors[i]; + break; + } + } + } + + @override + void render(Canvas canvas) { + const d = pointSize / 2; + final paint = Paint(); + for (var i = 0; i < points.length; i++) { + final x = points[i].x; + final y = points[i].y; + paint.color = pointColors[i]; + canvas.drawRect(Rect.fromLTRB(x - d, y - d, x + d, y + d), paint); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/games/games.dart b/flame/assets/examples/official/dashbook_example/lib/stories/games/games.dart new file mode 100644 index 0000000..8f619e3 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/games/games.dart @@ -0,0 +1,32 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:padracing/padracing_game.dart'; +import 'package:padracing/padracing_widget.dart'; +import 'package:rogue_shooter/rogue_shooter_game.dart'; +import 'package:rogue_shooter/rogue_shooter_widget.dart'; +import 'package:trex_game/trex_game.dart'; +import 'package:trex_game/trex_widget.dart'; + +String gamesLink(String game) => + 'https://github.com/flame-engine/flame/blob/main/examples/games/$game'; + +void addGameStories(Dashbook dashbook) { + dashbook.storiesOf('Sample Games') + ..add( + 'Padracing', + (_) => const PadracingWidget(), + codeLink: gamesLink('padracing'), + info: PadRacingGame.description, + ) + ..add( + 'Rogue Shooter', + (_) => const RogueShooterWidget(), + codeLink: gamesLink('rogue_shooter'), + info: RogueShooterGame.description, + ) + ..add( + 'T-Rex', + (_) => const TRexWidget(), + codeLink: gamesLink('trex'), + info: TRexGame.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/image/image.dart b/flame/assets/examples/official/dashbook_example/lib/stories/image/image.dart new file mode 100644 index 0000000..af4eae7 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/image/image.dart @@ -0,0 +1,23 @@ +import 'package:dashbook/dashbook.dart'; + +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/image/resize.dart'; +import 'package:flame/game.dart'; + +void addImageStories(Dashbook dashbook) { + dashbook.storiesOf('Image') + ..decorator(CenterDecorator()) + ..add( + 'resize', + (context) => GameWidget( + game: ImageResizeExample( + Vector2( + context.numberProperty('width', 200), + context.numberProperty('height', 300), + ), + ), + ), + codeLink: baseLink('image/resize.dart'), + info: ImageResizeExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/image/resize.dart b/flame/assets/examples/official/dashbook_example/lib/stories/image/resize.dart new file mode 100644 index 0000000..667f5e4 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/image/resize.dart @@ -0,0 +1,29 @@ +import 'package:flame/components.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; + +class ImageResizeExample extends FlameGame { + ImageResizeExample(this.sizeTarget); + + static const String description = ''' + Shows how a dart:ui `Image` can be resized using Flame Image extensions. + Use the properties on the side to change the size of the image. + '''; + + final Vector2 sizeTarget; + + @override + Future onLoad() async { + final image = await images.load('flame.png'); + + final resized = await image.resize(sizeTarget); + add( + SpriteComponent( + sprite: Sprite(resized), + position: size / 2, + size: resized.size, + anchor: Anchor.center, + ), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/advanced_button_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/advanced_button_example.dart new file mode 100644 index 0000000..e74b0fb --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/advanced_button_example.dart @@ -0,0 +1,125 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/painting.dart'; + +class AdvancedButtonExample extends FlameGame { + static const String description = + '''This example shows how you can use a button with different states'''; + + @override + Future onLoad() async { + final defaultButton = DefaultButton(); + defaultButton.position = Vector2(50, 50); + defaultButton.size = Vector2(250, 50); + add(defaultButton); + + final disableButton = DisableButton(); + disableButton.isDisabled = true; + disableButton.position = Vector2(400, 50); + disableButton.size = defaultButton.size; + add(disableButton); + + final toggleButton = ToggleButton(); + toggleButton.position = Vector2(50, 150); + toggleButton.size = defaultButton.size; + add(toggleButton); + } +} + +class ToggleButton extends ToggleButtonComponent { + @override + Future onLoad() async { + super.onLoad(); + + defaultLabel = TextComponent( + text: 'Toggle button', + textRenderer: TextPaint( + style: TextStyle( + fontSize: 24, + color: BasicPalette.white.color, + ), + ), + ); + + defaultSelectedLabel = TextComponent( + text: 'Toggle button', + textRenderer: TextPaint( + style: TextStyle( + fontSize: 24, + color: BasicPalette.red.color, + ), + ), + ); + + defaultSkin = RoundedRectComponent() + ..setColor(const Color.fromRGBO(0, 200, 0, 1)); + + hoverSkin = RoundedRectComponent() + ..setColor(const Color.fromRGBO(0, 180, 0, 1)); + + downSkin = RoundedRectComponent() + ..setColor(const Color.fromRGBO(0, 100, 0, 1)); + + defaultSelectedSkin = RoundedRectComponent() + ..setColor(const Color.fromRGBO(0, 0, 200, 1)); + + hoverAndSelectedSkin = RoundedRectComponent() + ..setColor(const Color.fromRGBO(0, 0, 180, 1)); + + downAndSelectedSkin = RoundedRectComponent() + ..setColor(const Color.fromRGBO(0, 0, 100, 1)); + } +} + +class DefaultButton extends AdvancedButtonComponent { + @override + Future onLoad() async { + super.onLoad(); + + defaultLabel = TextComponent(text: 'Default button'); + + defaultSkin = RoundedRectComponent() + ..setColor(const Color.fromRGBO(0, 200, 0, 1)); + + hoverSkin = RoundedRectComponent() + ..setColor(const Color.fromRGBO(0, 180, 0, 1)); + + downSkin = RoundedRectComponent() + ..setColor(const Color.fromRGBO(0, 100, 0, 1)); + } +} + +class DisableButton extends AdvancedButtonComponent { + @override + Future onLoad() async { + super.onLoad(); + + disabledLabel = TextComponent(text: 'Disabled button'); + + defaultSkin = RoundedRectComponent() + ..setColor(const Color.fromRGBO(0, 255, 0, 1)); + + disabledSkin = RoundedRectComponent() + ..setColor(const Color.fromRGBO(100, 100, 100, 1)); + } +} + +class RoundedRectComponent extends PositionComponent with HasPaint { + @override + void render(Canvas canvas) { + canvas.drawRRect( + RRect.fromLTRBAndCorners( + 0, + 0, + width, + height, + topLeft: Radius.circular(height), + topRight: Radius.circular(height), + bottomRight: Radius.circular(height), + bottomLeft: Radius.circular(height), + ), + paint, + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/double_tap_callbacks_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/double_tap_callbacks_example.dart new file mode 100644 index 0000000..df8190d --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/double_tap_callbacks_example.dart @@ -0,0 +1,65 @@ +import 'package:examples/commons/ember.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +class DoubleTapCallbacksExample extends FlameGame with DoubleTapCallbacks { + static const String description = ''' + In this example, we show how you can use the `DoubleTapCallbacks` mixin on + a `Component`. Double tap Ember and see her color changing. + The example also adds white circles when double-tapping on the game area. +'''; + + @override + Future onLoad() async { + children.register(); + } + + @override + void onGameResize(Vector2 size) { + children + .query() + .forEach((element) => element.removeFromParent()); + add(DoubleTappableEmber(position: size / 2)); + + super.onGameResize(size); + } + + @override + void onDoubleTapDown(DoubleTapDownEvent event) { + add( + CircleComponent( + radius: 30, + position: event.localPosition, + anchor: Anchor.center, + ), + ); + } +} + +class DoubleTappableEmber extends Ember with DoubleTapCallbacks { + @override + bool debugMode = true; + + DoubleTappableEmber({Vector2? position}) + : super( + position: position ?? Vector2.all(100), + size: Vector2.all(100), + ); + + @override + void onDoubleTapUp(DoubleTapEvent event) { + debugColor = Colors.greenAccent; + } + + @override + void onDoubleTapCancel(DoubleTapCancelEvent event) { + debugColor = Colors.red; + } + + @override + void onDoubleTapDown(DoubleTapDownEvent event) { + debugColor = Colors.blue; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/drag_callbacks_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/drag_callbacks_example.dart new file mode 100644 index 0000000..025023c --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/drag_callbacks_example.dart @@ -0,0 +1,44 @@ +import 'package:examples/commons/ember.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart' show Colors; + +class DragCallbacksExample extends FlameGame { + static const String description = ''' + In this example we show you can use the `DragCallbacks` mixin on + `PositionComponent`s. Drag around the Embers and see their position + changing. + '''; + + DragCallbacksExample({required this.zoom}); + + final double zoom; + late final DraggableEmber square; + + @override + Future onLoad() async { + camera.viewfinder.zoom = zoom; + world.add(square = DraggableEmber()); + world.add(DraggableEmber()..y = 350); + } +} + +// Note: this component does not consider the possibility of multiple +// simultaneous drags with different pointerIds. +class DraggableEmber extends Ember with DragCallbacks { + @override + bool debugMode = true; + + DraggableEmber({super.position}) : super(size: Vector2.all(100)); + + @override + void update(double dt) { + super.update(dt); + debugColor = isDragged ? Colors.greenAccent : Colors.purple; + } + + @override + void onDragUpdate(DragUpdateEvent event) { + position += event.localDelta; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/gesture_hitboxes_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/gesture_hitboxes_example.dart new file mode 100644 index 0000000..817c607 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/gesture_hitboxes_example.dart @@ -0,0 +1,93 @@ +import 'dart:math'; + +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; + +enum Shapes { circle, rectangle, polygon } + +class GestureHitboxesExample extends FlameGame { + static const description = ''' + Tap to create a PositionComponent with a randomly shaped hitbox. + You can then hover over to shapes to see that they receive the hover events + only when the cursor is within the shape. If you tap/click within the shape + it is removed. + '''; + + GestureHitboxesExample() : super(world: _GestureHitboxesWorld()); +} + +class _GestureHitboxesWorld extends World with TapCallbacks { + final _rng = Random(); + + PositionComponent randomShape(Vector2 position) { + final shapeType = Shapes.values[_rng.nextInt(Shapes.values.length)]; + final shapeSize = + Vector2.all(100) + Vector2.all(50.0).scaled(_rng.nextDouble()); + final shapeAngle = _rng.nextDouble() * 6; + final hitbox = switch (shapeType) { + Shapes.circle => CircleHitbox(), + Shapes.rectangle => RectangleHitbox(), + Shapes.polygon => PolygonHitbox.relative( + [ + -Vector2.random(_rng), + Vector2.random(_rng)..x *= -1, + Vector2.random(_rng), + Vector2.random(_rng)..y *= -1, + ], + parentSize: shapeSize, + ), + }; + return MyShapeComponent( + hitbox: hitbox, + position: position, + size: shapeSize, + angle: shapeAngle, + ); + } + + @override + void onTapDown(TapDownEvent event) { + add(randomShape(event.localPosition)); + } +} + +class MyShapeComponent extends PositionComponent + with TapCallbacks, HoverCallbacks, GestureHitboxes { + final ShapeHitbox hitbox; + late final Color baseColor; + late final Color hoverColor; + + MyShapeComponent({ + required this.hitbox, + super.position, + super.size, + super.angle, + }) : super(anchor: Anchor.center); + + @override + Future onLoad() async { + super.onLoad(); + baseColor = ColorExtension.random(withAlpha: 0.8, base: 100); + hitbox.paint.color = baseColor; + hitbox.renderShape = true; + add(hitbox); + } + + @override + void onTapDown(TapDownEvent _) { + removeFromParent(); + } + + @override + void onHoverEnter() { + hitbox.paint.color = hitbox.paint.color.darken(0.5); + } + + @override + void onHoverExit() { + hitbox.paint.color = baseColor; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/hardware_keyboard_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/hardware_keyboard_example.dart new file mode 100644 index 0000000..d0ef86f --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/hardware_keyboard_example.dart @@ -0,0 +1,255 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame/text.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +class HardwareKeyboardExample extends FlameGame { + static const String description = ''' + This example uses the HardwareKeyboardDetector mixin in order to keep + track of which keys on a keyboard are currently pressed. + + Tap as many keys on the keyboard at once as you want, and see whether the + system can detect them or not. + '''; + + /// The list of [KeyboardKey] components currently shown on the screen. This + /// list is re-generated on every KeyEvent. These components are also + /// attached as children. + List _keyComponents = []; + + void replaceKeyComponents(List newComponents) { + for (final key in _keyComponents) { + key.visible = false; + key.removeFromParent(); + } + _keyComponents = newComponents; + addAll(_keyComponents); + } + + @override + void onLoad() { + add(MyKeyboardDetector()); + add( + TextComponent( + text: 'Press any key(s)', + textRenderer: TextPaint( + style: const TextStyle( + fontSize: 12, + color: Color(0x77ffffff), + ), + ), + )..position = Vector2(80, 60), + ); + } +} + +class MyKeyboardDetector extends HardwareKeyboardDetector + with HasGameReference { + @override + void onKeyEvent(KeyEvent event) { + final newComponents = []; + var x0 = 80.0; + const y0 = 100.0; + for (final key in physicalKeysPressed) { + final keyComponent = KeyboardKey( + text: keyNames[key] ?? '[${key.usbHidUsage} ${key.debugName}]', + position: Vector2(x0, y0), + ); + newComponents.add(keyComponent); + x0 += keyComponent.width + 10; + } + game.replaceKeyComponents(newComponents); + } + + /// The names of keyboard keys (at least the most important ones). We can't + /// rely on `key.debugName` because this property is not available in release + /// builds. + static final Map keyNames = { + PhysicalKeyboardKey.hyper: 'Hyper', + PhysicalKeyboardKey.superKey: 'Super', + PhysicalKeyboardKey.fn: 'Fn', + PhysicalKeyboardKey.fnLock: 'FnLock', + PhysicalKeyboardKey.gameButton1: 'Game 1', + PhysicalKeyboardKey.gameButton2: 'Game 2 ', + PhysicalKeyboardKey.gameButton3: 'Game 3', + PhysicalKeyboardKey.gameButton4: 'Game 4', + PhysicalKeyboardKey.gameButton5: 'Game 5', + PhysicalKeyboardKey.gameButton6: 'Game 6', + PhysicalKeyboardKey.gameButton7: 'Game 7', + PhysicalKeyboardKey.gameButton8: 'Game 8', + PhysicalKeyboardKey.gameButtonA: 'Game A', + PhysicalKeyboardKey.gameButtonB: 'Game B', + PhysicalKeyboardKey.gameButtonC: 'Game C', + PhysicalKeyboardKey.gameButtonLeft1: 'Game L1', + PhysicalKeyboardKey.gameButtonLeft2: 'Game L2', + PhysicalKeyboardKey.gameButtonMode: 'Game Mode', + PhysicalKeyboardKey.gameButtonRight1: 'Game R1', + PhysicalKeyboardKey.gameButtonRight2: 'Game R2', + PhysicalKeyboardKey.gameButtonSelect: 'Game Select', + PhysicalKeyboardKey.gameButtonStart: 'Game Start', + PhysicalKeyboardKey.gameButtonThumbLeft: 'Game LThumb', + PhysicalKeyboardKey.gameButtonThumbRight: 'Game RThumb', + PhysicalKeyboardKey.gameButtonX: 'Game X', + PhysicalKeyboardKey.gameButtonY: 'Game Y', + PhysicalKeyboardKey.gameButtonZ: 'Game Z', + PhysicalKeyboardKey.keyA: 'A', + PhysicalKeyboardKey.keyB: 'B', + PhysicalKeyboardKey.keyC: 'C', + PhysicalKeyboardKey.keyD: 'D', + PhysicalKeyboardKey.keyE: 'E', + PhysicalKeyboardKey.keyF: 'F', + PhysicalKeyboardKey.keyG: 'G', + PhysicalKeyboardKey.keyH: 'H', + PhysicalKeyboardKey.keyI: 'I', + PhysicalKeyboardKey.keyJ: 'J', + PhysicalKeyboardKey.keyK: 'K', + PhysicalKeyboardKey.keyL: 'L', + PhysicalKeyboardKey.keyM: 'M', + PhysicalKeyboardKey.keyN: 'N', + PhysicalKeyboardKey.keyO: 'O', + PhysicalKeyboardKey.keyP: 'P', + PhysicalKeyboardKey.keyQ: 'Q', + PhysicalKeyboardKey.keyR: 'R', + PhysicalKeyboardKey.keyS: 'S', + PhysicalKeyboardKey.keyT: 'T', + PhysicalKeyboardKey.keyU: 'U', + PhysicalKeyboardKey.keyV: 'V', + PhysicalKeyboardKey.keyW: 'W', + PhysicalKeyboardKey.keyX: 'X', + PhysicalKeyboardKey.keyY: 'Y', + PhysicalKeyboardKey.keyZ: 'Z', + PhysicalKeyboardKey.digit1: '1', + PhysicalKeyboardKey.digit2: '2', + PhysicalKeyboardKey.digit3: '3', + PhysicalKeyboardKey.digit4: '4', + PhysicalKeyboardKey.digit5: '5', + PhysicalKeyboardKey.digit6: '6', + PhysicalKeyboardKey.digit7: '7', + PhysicalKeyboardKey.digit8: '8', + PhysicalKeyboardKey.digit9: '9', + PhysicalKeyboardKey.digit0: '0', + PhysicalKeyboardKey.enter: 'Enter', + PhysicalKeyboardKey.escape: 'Esc', + PhysicalKeyboardKey.backspace: 'Backspace', + PhysicalKeyboardKey.tab: 'Tab', + PhysicalKeyboardKey.space: 'Space', + PhysicalKeyboardKey.minus: '-', + PhysicalKeyboardKey.equal: '=', + PhysicalKeyboardKey.bracketLeft: '[', + PhysicalKeyboardKey.bracketRight: ']', + PhysicalKeyboardKey.backslash: r'\', + PhysicalKeyboardKey.semicolon: ';', + PhysicalKeyboardKey.quote: "'", + PhysicalKeyboardKey.backquote: '`', + PhysicalKeyboardKey.comma: ',', + PhysicalKeyboardKey.period: '.', + PhysicalKeyboardKey.slash: '/', + PhysicalKeyboardKey.capsLock: 'CapsLock', + PhysicalKeyboardKey.f1: 'F1', + PhysicalKeyboardKey.f2: 'F2', + PhysicalKeyboardKey.f3: 'F3', + PhysicalKeyboardKey.f4: 'F4', + PhysicalKeyboardKey.f5: 'F5', + PhysicalKeyboardKey.f6: 'F6', + PhysicalKeyboardKey.f7: 'F7', + PhysicalKeyboardKey.f8: 'F8', + PhysicalKeyboardKey.f9: 'F9', + PhysicalKeyboardKey.f10: 'F10', + PhysicalKeyboardKey.f11: 'F11', + PhysicalKeyboardKey.f12: 'F12', + PhysicalKeyboardKey.f13: 'F13', + PhysicalKeyboardKey.f14: 'F14', + PhysicalKeyboardKey.f15: 'F15', + PhysicalKeyboardKey.f16: 'F16', + PhysicalKeyboardKey.printScreen: 'PrintScreen', + PhysicalKeyboardKey.scrollLock: 'ScrollLock', + PhysicalKeyboardKey.pause: 'Pause', + PhysicalKeyboardKey.insert: 'Insert', + PhysicalKeyboardKey.home: 'Home', + PhysicalKeyboardKey.pageUp: 'PageUp', + PhysicalKeyboardKey.delete: 'Delete', + PhysicalKeyboardKey.end: 'End', + PhysicalKeyboardKey.pageDown: 'PageDown', + PhysicalKeyboardKey.arrowRight: 'ArrowRight', + PhysicalKeyboardKey.arrowLeft: 'ArrowLeft', + PhysicalKeyboardKey.arrowDown: 'ArrowDown', + PhysicalKeyboardKey.arrowUp: 'ArrowUp', + PhysicalKeyboardKey.numLock: 'NumLock', + PhysicalKeyboardKey.numpadDivide: 'Num /', + PhysicalKeyboardKey.numpadMultiply: 'Num *', + PhysicalKeyboardKey.numpadSubtract: 'Num -', + PhysicalKeyboardKey.numpadAdd: 'Num +', + PhysicalKeyboardKey.numpadEnter: 'Num Enter', + PhysicalKeyboardKey.numpad1: 'Num 1', + PhysicalKeyboardKey.numpad2: 'Num 2', + PhysicalKeyboardKey.numpad3: 'Num 3', + PhysicalKeyboardKey.numpad4: 'Num 4', + PhysicalKeyboardKey.numpad5: 'Num 5', + PhysicalKeyboardKey.numpad6: 'Num 6', + PhysicalKeyboardKey.numpad7: 'Num 7', + PhysicalKeyboardKey.numpad8: 'Num 8', + PhysicalKeyboardKey.numpad9: 'Num 9', + PhysicalKeyboardKey.numpad0: 'Num 0', + PhysicalKeyboardKey.numpadDecimal: 'Num .', + PhysicalKeyboardKey.contextMenu: 'ContextMenu', + PhysicalKeyboardKey.controlLeft: 'LControl', + PhysicalKeyboardKey.shiftLeft: 'LShift', + PhysicalKeyboardKey.altLeft: 'LAlt', + PhysicalKeyboardKey.metaLeft: 'LMeta', + PhysicalKeyboardKey.controlRight: 'RControl', + PhysicalKeyboardKey.shiftRight: 'RShift', + PhysicalKeyboardKey.altRight: 'RAlt', + PhysicalKeyboardKey.metaRight: 'RMeta', + }; +} + +class KeyboardKey extends PositionComponent { + KeyboardKey({required this.text, super.position}) { + textElement = textRenderer.format(text); + width = textElement.metrics.width + padding.x; + height = textElement.metrics.height + padding.y; + textElement.translate( + padding.x / 2, + padding.y / 2 + textElement.metrics.ascent, + ); + rect = RRect.fromLTRBR(0, 0, width, height, const Radius.circular(8)); + } + + final String text; + late final InlineTextElement textElement; + late final RRect rect; + + /// The KeyEvents may occur very fast, and out of sync with the game loop. + /// On each such event we remove old KeyboardKey components, and add new ones. + /// However, since multiple KeyEvents may occur within a single game tick, + /// we end up adding/removing components many times within that tick, and for + /// a brief moment there could be a situation that the old components still + /// haven't been removed while the new ones were already added. In order to + /// prevent this from happening, we mark all components that are about to be + /// removed as "not visible", which prevents them from being rendered while + /// they are waiting to be removed. + bool visible = true; + + static final Vector2 padding = Vector2(24, 12); + static final Paint borderPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 3 + ..color = const Color(0xffb5ffd0); + static final TextPaint textRenderer = TextPaint( + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xffb5ffd0), + ), + ); + + @override + void render(Canvas canvas) { + if (visible) { + canvas.drawRRect(rect, borderPaint); + textElement.draw(canvas); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/hover_callbacks_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/hover_callbacks_example.dart new file mode 100644 index 0000000..4d754e9 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/hover_callbacks_example.dart @@ -0,0 +1,50 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +class HoverCallbacksExample extends FlameGame { + static const String description = ''' + This example shows how to use `HoverCallbacks`s.\n\n + Add more squares by clicking and hover them to change their color. + '''; + + HoverCallbacksExample() : super(world: HoverCallbacksWorld()); + + @override + Future onLoad() async { + camera.viewfinder.anchor = Anchor.topLeft; + camera.viewfinder.zoom = 1.5; + } +} + +class HoverCallbacksWorld extends World with TapCallbacks { + @override + Future onLoad() async { + add(HoverSquare(Vector2(200, 500))); + add(HoverSquare(Vector2(700, 300))); + } + + @override + void onTapDown(TapDownEvent event) { + add(HoverSquare(event.localPosition)); + } +} + +class HoverSquare extends PositionComponent with HoverCallbacks { + static final Paint _white = Paint()..color = const Color(0xFFFFFFFF); + static final Paint _grey = Paint()..color = const Color(0xFFA5A5A5); + + HoverSquare(Vector2 position) + : super( + position: position, + size: Vector2.all(100), + anchor: Anchor.center, + ); + + @override + void render(Canvas canvas) { + canvas.drawRect(size.toRect(), isHovered ? _grey : _white); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/input.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/input.dart new file mode 100644 index 0000000..40e5b10 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/input.dart @@ -0,0 +1,140 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/input/advanced_button_example.dart'; +import 'package:examples/stories/input/double_tap_callbacks_example.dart'; +import 'package:examples/stories/input/drag_callbacks_example.dart'; +import 'package:examples/stories/input/gesture_hitboxes_example.dart'; +import 'package:examples/stories/input/hardware_keyboard_example.dart'; +import 'package:examples/stories/input/hover_callbacks_example.dart'; +import 'package:examples/stories/input/joystick_advanced_example.dart'; +import 'package:examples/stories/input/joystick_example.dart'; +import 'package:examples/stories/input/keyboard_example.dart'; +import 'package:examples/stories/input/keyboard_listener_component_example.dart'; +import 'package:examples/stories/input/mouse_cursor_example.dart'; +import 'package:examples/stories/input/mouse_movement_example.dart'; +import 'package:examples/stories/input/multitap_advanced_example.dart'; +import 'package:examples/stories/input/multitap_example.dart'; +import 'package:examples/stories/input/overlapping_tap_callbacks_example.dart'; +import 'package:examples/stories/input/scroll_example.dart'; +import 'package:examples/stories/input/tap_callbacks_example.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +void addInputStories(Dashbook dashbook) { + dashbook.storiesOf('Input') + ..add( + 'TapCallbacks', + (_) => GameWidget(game: TapCallbacksExample()), + codeLink: baseLink('input/tap_callbacks_example.dart'), + info: TapCallbacksExample.description, + ) + ..add( + 'DragCallbacks', + (context) { + return GameWidget( + game: DragCallbacksExample( + zoom: context.listProperty('zoom', 1, [0.5, 1, 1.5]), + ), + ); + }, + codeLink: baseLink('input/drag_callbacks_example.dart'), + info: DragCallbacksExample.description, + ) + ..add( + 'Double Tap (Component)', + (context) { + return GameWidget( + game: DoubleTapCallbacksExample(), + ); + }, + codeLink: baseLink('input/double_tap_callbacks_example.dart'), + info: DoubleTapCallbacksExample.description, + ) + ..add( + 'HoverCallbacks', + (_) => GameWidget(game: HoverCallbacksExample()), + codeLink: baseLink('input/hover_callbacks_example.dart'), + info: HoverCallbacksExample.description, + ) + ..add( + 'Keyboard', + (_) => GameWidget(game: KeyboardExample()), + codeLink: baseLink('input/keyboard_example.dart'), + info: KeyboardExample.description, + ) + ..add( + 'Keyboard (Component)', + (_) => GameWidget(game: KeyboardListenerComponentExample()), + codeLink: baseLink('input/keyboard_listener_component_example.dart'), + info: KeyboardListenerComponentExample.description, + ) + ..add( + 'Hardware Keyboard', + (_) => GameWidget(game: HardwareKeyboardExample()), + codeLink: baseLink('input/hardware_keyboard_example.dart'), + info: HardwareKeyboardExample.description, + ) + ..add( + 'Mouse Movement', + (_) => GameWidget(game: MouseMovementExample()), + codeLink: baseLink('input/mouse_movement_example.dart'), + info: MouseMovementExample.description, + ) + ..add( + 'Mouse Cursor', + (_) => GameWidget( + game: MouseCursorExample(), + mouseCursor: SystemMouseCursors.move, + ), + codeLink: baseLink('input/mouse_cursor_example.dart'), + info: MouseCursorExample.description, + ) + ..add( + 'Scroll', + (_) => GameWidget(game: ScrollExample()), + codeLink: baseLink('input/scroll_example.dart'), + info: ScrollExample.description, + ) + ..add( + 'Multitap', + (_) => GameWidget(game: MultitapExample()), + codeLink: baseLink('input/multitap_example.dart'), + info: MultitapExample.description, + ) + ..add( + 'Multitap Advanced', + (_) => GameWidget(game: MultitapAdvancedExample()), + codeLink: baseLink('input/multitap_advanced_example.dart'), + info: MultitapAdvancedExample.description, + ) + ..add( + 'Overlapping TapCallbacks', + (_) => GameWidget(game: OverlappingTapCallbacksExample()), + codeLink: baseLink('input/overlapping_tap_callbacks_example.dart'), + info: OverlappingTapCallbacksExample.description, + ) + ..add( + 'Gesture Hitboxes', + (_) => GameWidget(game: GestureHitboxesExample()), + codeLink: baseLink('input/gesture_hitboxes_example.dart'), + info: GestureHitboxesExample.description, + ) + ..add( + 'Joystick', + (_) => GameWidget(game: JoystickExample()), + codeLink: baseLink('input/joystick_example.dart'), + info: JoystickExample.description, + ) + ..add( + 'Joystick Advanced', + (_) => GameWidget(game: JoystickAdvancedExample()), + codeLink: baseLink('input/joystick_advanced_example.dart'), + info: JoystickAdvancedExample.description, + ) + ..add( + 'Advanced Button', + (_) => GameWidget(game: AdvancedButtonExample()), + codeLink: baseLink('input/advanced_button_example.dart'), + info: AdvancedButtonExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/joystick_advanced_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/joystick_advanced_example.dart new file mode 100644 index 0000000..99dd1c3 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/joystick_advanced_example.dart @@ -0,0 +1,207 @@ +import 'dart:math'; + +import 'package:examples/stories/input/joystick_player.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; +import 'package:flame/sprite.dart'; +import 'package:flutter/material.dart'; + +class JoystickAdvancedExample extends FlameGame with HasCollisionDetection { + static const String description = ''' + In this example we showcase how to use the joystick by creating + `SpriteComponent`s that serve as the joystick's knob and background. + We also showcase the `HudButtonComponent` which is a button that has its + position defined by margins to the edges, which can be useful when making + controller buttons.\n\n + Steer the player by using the joystick and flip and rotate it by pressing + the buttons. + '''; + + JoystickAdvancedExample() + : super( + camera: CameraComponent.withFixedResolution(width: 1200, height: 800), + ); + + late final JoystickPlayer player; + late final JoystickComponent joystick; + late final TextComponent speedText; + late final TextComponent directionText; + + @override + Future onLoad() async { + final image = await images.load('joystick.png'); + final sheet = SpriteSheet.fromColumnsAndRows( + image: image, + columns: 6, + rows: 1, + ); + world.add(ScreenHitbox()); + joystick = JoystickComponent( + knob: SpriteComponent( + sprite: sheet.getSpriteById(1), + size: Vector2.all(100), + ), + background: SpriteComponent( + sprite: sheet.getSpriteById(0), + size: Vector2.all(150), + ), + margin: const EdgeInsets.only(left: 40, bottom: 40), + ); + player = JoystickPlayer(joystick); + + final buttonSize = Vector2.all(80); + // A button with margin from the edge of the viewport that flips the + // rendering of the player on the X-axis. + final flipButton = HudButtonComponent( + button: SpriteComponent( + sprite: sheet.getSpriteById(2), + size: buttonSize, + ), + buttonDown: SpriteComponent( + sprite: sheet.getSpriteById(4), + size: buttonSize, + ), + margin: const EdgeInsets.only( + right: 80, + bottom: 60, + ), + onPressed: player.flipHorizontally, + ); + + // A button with margin from the edge of the viewport that flips the + // rendering of the player on the Y-axis. + final flopButton = HudButtonComponent( + button: SpriteComponent( + sprite: sheet.getSpriteById(3), + size: buttonSize, + ), + buttonDown: SpriteComponent( + sprite: sheet.getSpriteById(5), + size: buttonSize, + ), + margin: const EdgeInsets.only( + right: 160, + bottom: 60, + ), + onPressed: player.flipVertically, + ); + + final rng = Random(); + // A button, created from a shape, that adds a rotation effect to the player + // when it is pressed. + final shapeButton = HudButtonComponent( + button: CircleComponent(radius: 35), + buttonDown: RectangleComponent( + size: buttonSize, + paint: BasicPalette.blue.paint(), + ), + margin: const EdgeInsets.only( + right: 85, + bottom: 150, + ), + onPressed: () => player.add( + RotateEffect.by( + 8 * rng.nextDouble(), + EffectController( + duration: 1, + reverseDuration: 1, + curve: Curves.bounceOut, + ), + ), + ), + ); + + // A button, created from a shape, that adds a scale effect to the player + // when it is pressed. + final buttonComponent = ButtonComponent( + button: RectangleComponent( + size: Vector2(185, 50), + paint: Paint() + ..color = Colors.orange + ..style = PaintingStyle.stroke, + ), + buttonDown: RectangleComponent( + size: Vector2(185, 50), + paint: BasicPalette.magenta.paint(), + ), + position: Vector2(20, size.y - 280), + onPressed: () => player.add( + ScaleEffect.by( + Vector2.all(1.5), + EffectController(duration: 1.0, reverseDuration: 1.0), + ), + ), + ); + + final buttonSprites = await images.load('buttons.png'); + final buttonSheet = SpriteSheet.fromColumnsAndRows( + image: buttonSprites, + columns: 1, + rows: 2, + ); + + // A sprite button, created from a shape, that adds a opacity effect to the + // player when it is pressed. + final spriteButtonComponent = SpriteButtonComponent( + button: buttonSheet.getSpriteById(0), + buttonDown: buttonSheet.getSpriteById(1), + position: Vector2(20, size.y - 360), + size: Vector2(185, 50), + onPressed: () => player.add( + OpacityEffect.fadeOut( + EffectController(duration: 0.5, reverseDuration: 0.5), + ), + ), + ); + + final regular = TextPaint( + style: TextStyle(color: BasicPalette.white.color), + ); + speedText = TextComponent( + text: 'Speed: 0', + textRenderer: regular, + ); + directionText = TextComponent( + text: 'Direction: idle', + textRenderer: regular, + ); + + final speedWithMargin = HudMarginComponent( + margin: const EdgeInsets.only( + top: 80, + left: 80, + ), + )..add(speedText); + + final directionWithMargin = HudMarginComponent( + margin: const EdgeInsets.only( + top: 110, + left: 80, + ), + )..add(directionText); + + world.add(player); + camera.viewport.addAll([ + joystick, + flipButton, + flopButton, + buttonComponent, + spriteButtonComponent, + shapeButton, + speedWithMargin, + directionWithMargin, + ]); + } + + @override + void update(double dt) { + super.update(dt); + speedText.text = 'Speed: ${(joystick.intensity * player.maxSpeed).round()}'; + final direction = + joystick.direction.toString().replaceAll('JoystickDirection.', ''); + directionText.text = 'Direction: $direction'; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/joystick_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/joystick_example.dart new file mode 100644 index 0000000..aac3dc4 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/joystick_example.dart @@ -0,0 +1,31 @@ +import 'package:examples/stories/input/joystick_player.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/painting.dart'; + +class JoystickExample extends FlameGame { + static const String description = ''' + In this example we showcase how to use the joystick by creating simple + `CircleComponent`s that serve as the joystick's knob and background. + Steer the player by using the joystick. + '''; + + late final JoystickPlayer player; + late final JoystickComponent joystick; + + @override + Future onLoad() async { + final knobPaint = BasicPalette.blue.withAlpha(200).paint(); + final backgroundPaint = BasicPalette.blue.withAlpha(100).paint(); + joystick = JoystickComponent( + knob: CircleComponent(radius: 30, paint: knobPaint), + background: CircleComponent(radius: 100, paint: backgroundPaint), + margin: const EdgeInsets.only(left: 40, bottom: 40), + ); + player = JoystickPlayer(joystick); + + world.add(player); + camera.viewport.add(joystick); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/joystick_player.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/joystick_player.dart new file mode 100644 index 0000000..e72fa99 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/joystick_player.dart @@ -0,0 +1,42 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; + +class JoystickPlayer extends SpriteComponent + with HasGameRef, CollisionCallbacks { + /// Pixels/s + double maxSpeed = 300.0; + late final Vector2 _lastSize = size.clone(); + late final Transform2D _lastTransform = transform.clone(); + + final JoystickComponent joystick; + + JoystickPlayer(this.joystick) + : super(size: Vector2.all(100.0), anchor: Anchor.center); + + @override + Future onLoad() async { + sprite = await game.loadSprite('layers/player.png'); + add(RectangleHitbox()); + } + + @override + void update(double dt) { + if (!joystick.delta.isZero() && activeCollisions.isEmpty) { + _lastSize.setFrom(size); + _lastTransform.setFrom(transform); + position.add(joystick.relativeDelta * maxSpeed * dt); + angle = joystick.delta.screenAngle(); + } + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + super.onCollisionStart(intersectionPoints, other); + transform.setFrom(_lastTransform); + size.setFrom(_lastSize); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/keyboard_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/keyboard_example.dart new file mode 100644 index 0000000..6a9e337 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/keyboard_example.dart @@ -0,0 +1,63 @@ +import 'package:examples/commons/ember.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +class KeyboardExample extends FlameGame with KeyboardEvents { + static const String description = ''' + Example showcasing how to act on keyboard events. + It also briefly showcases how to create a game without the FlameGame. + Usage: Use WASD to steer Ember. + '''; + + // Speed at which amber moves. + static const double _speed = 200; + + // Direction in which amber is moving. + final Vector2 _direction = Vector2.zero(); + + @override + Future onLoad() async { + add( + Ember( + key: ComponentKey.named('ember'), + position: size / 2, + size: Vector2.all(100), + ), + ); + } + + @override + void update(double dt) { + super.update(dt); + final ember = findByKeyName('ember'); + final displacement = _direction.normalized() * _speed * dt; + ember?.position.add(displacement); + } + + @override + KeyEventResult onKeyEvent( + KeyEvent event, + Set keysPressed, + ) { + final isKeyDown = event is KeyDownEvent; + + // Avoiding repeat event as we are interested only in + // key up and key down event. + if (key is! KeyRepeatEvent) { + if (event.logicalKey == LogicalKeyboardKey.keyA) { + _direction.x += isKeyDown ? -1 : 1; + } else if (event.logicalKey == LogicalKeyboardKey.keyD) { + _direction.x += isKeyDown ? 1 : -1; + } else if (event.logicalKey == LogicalKeyboardKey.keyW) { + _direction.y += isKeyDown ? -1 : 1; + } else if (event.logicalKey == LogicalKeyboardKey.keyS) { + _direction.y += isKeyDown ? 1 : -1; + } + } + + return super.onKeyEvent(event, keysPressed); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/keyboard_listener_component_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/keyboard_listener_component_example.dart new file mode 100644 index 0000000..ee7b74d --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/keyboard_listener_component_example.dart @@ -0,0 +1,83 @@ +import 'package:examples/commons/ember.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/services.dart'; + +class KeyboardListenerComponentExample extends FlameGame + with HasKeyboardHandlerComponents { + static const String description = ''' + Similar to the default Keyboard example, but shows a different + implementation approach, which uses Flame's + KeyboardListenerComponent to handle input. + Usage: Use WASD to steer Ember. + '''; + + static const int _speed = 200; + + late final Ember _ember; + final Vector2 _direction = Vector2.zero(); + + final Map _keyWeights = { + LogicalKeyboardKey.keyW: 0, + LogicalKeyboardKey.keyA: 0, + LogicalKeyboardKey.keyS: 0, + LogicalKeyboardKey.keyD: 0, + }; + + @override + Future onLoad() async { + _ember = Ember(position: size / 2, size: Vector2.all(100)); + add(_ember); + + add( + KeyboardListenerComponent( + keyUp: { + LogicalKeyboardKey.keyA: (keys) => + _handleKey(LogicalKeyboardKey.keyA, false), + LogicalKeyboardKey.keyD: (keys) => + _handleKey(LogicalKeyboardKey.keyD, false), + LogicalKeyboardKey.keyW: (keys) => + _handleKey(LogicalKeyboardKey.keyW, false), + LogicalKeyboardKey.keyS: (keys) => + _handleKey(LogicalKeyboardKey.keyS, false), + }, + keyDown: { + LogicalKeyboardKey.keyA: (keys) => + _handleKey(LogicalKeyboardKey.keyA, true), + LogicalKeyboardKey.keyD: (keys) => + _handleKey(LogicalKeyboardKey.keyD, true), + LogicalKeyboardKey.keyW: (keys) => + _handleKey(LogicalKeyboardKey.keyW, true), + LogicalKeyboardKey.keyS: (keys) => + _handleKey(LogicalKeyboardKey.keyS, true), + }, + ), + ); + } + + @override + void update(double dt) { + super.update(dt); + + _direction + ..setValues(xInput, yInput) + ..normalize(); + + final displacement = _direction * (_speed * dt); + _ember.position.add(displacement); + } + + bool _handleKey(LogicalKeyboardKey key, bool isDown) { + _keyWeights[key] = isDown ? 1 : 0; + return true; + } + + double get xInput => + _keyWeights[LogicalKeyboardKey.keyD]! - + _keyWeights[LogicalKeyboardKey.keyA]!; + + double get yInput => + _keyWeights[LogicalKeyboardKey.keyS]! - + _keyWeights[LogicalKeyboardKey.keyW]!; +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/mouse_cursor_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/mouse_cursor_example.dart new file mode 100644 index 0000000..c352d5d --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/mouse_cursor_example.dart @@ -0,0 +1,61 @@ +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class MouseCursorExample extends FlameGame with MouseMovementDetector { + static const String description = ''' + Example showcasing the ability to change the game cursor in runtime + hover the little square to see the cursor changing + '''; + + static const speed = 200; + static final Paint _blue = BasicPalette.blue.paint(); + static final Paint _white = BasicPalette.white.paint(); + static final Vector2 objSize = Vector2.all(150); + + Vector2 position = Vector2(100, 100); + Vector2? target; + + bool onTarget = false; + + @override + void onMouseMove(PointerHoverInfo info) { + target = info.eventPosition.widget; + } + + Rect _toRect() => position.toPositionedRect(objSize); + + @override + void render(Canvas canvas) { + super.render(canvas); + canvas.drawRect( + _toRect(), + onTarget ? _blue : _white, + ); + } + + @override + void update(double dt) { + super.update(dt); + final target = this.target; + if (target != null) { + final hovering = _toRect().contains(target.toOffset()); + if (hovering) { + if (!onTarget) { + //Entered + mouseCursor = SystemMouseCursors.grab; + } + } else { + if (onTarget) { + // Exited + mouseCursor = SystemMouseCursors.move; + } + } + onTarget = hovering; + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/mouse_movement_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/mouse_movement_example.dart new file mode 100644 index 0000000..7864dba --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/mouse_movement_example.dart @@ -0,0 +1,54 @@ +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/material.dart'; + +class MouseMovementExample extends FlameGame with MouseMovementDetector { + static const String description = ''' + In this example we show how you can use `MouseMovementDetector`.\n\n + Move around the mouse on the canvas and the white square will follow it and + turn into blue if it reaches the mouse, or the edge of the canvas. + '''; + + static const speed = 200; + static final Paint _blue = BasicPalette.blue.paint(); + static final Paint _white = BasicPalette.white.paint(); + static final Vector2 objSize = Vector2.all(50); + + Vector2 position = Vector2(0, 0); + Vector2? target; + + bool onTarget = false; + + @override + void onMouseMove(PointerHoverInfo info) { + target = info.eventPosition.widget; + } + + Rect _toRect() => position.toPositionedRect(objSize); + + @override + void render(Canvas canvas) { + super.render(canvas); + canvas.drawRect( + _toRect(), + onTarget ? _blue : _white, + ); + } + + @override + void update(double dt) { + final target = this.target; + super.update(dt); + if (target != null) { + onTarget = _toRect().contains(target.toOffset()); + + if (!onTarget) { + final dir = (target - position).normalized(); + position += dir * (speed * dt); + } + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/multitap_advanced_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/multitap_advanced_example.dart new file mode 100644 index 0000000..55cc834 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/multitap_advanced_example.dart @@ -0,0 +1,79 @@ +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; + +/// Showcases how to mix two advanced detectors +class MultitapAdvancedExample extends FlameGame + with MultiTouchTapDetector, MultiTouchDragDetector { + static const String description = ''' + This showcases the use of both `MultiTouchTapDetector` and + `MultiTouchDragDetector` simultaneously. Drag multiple fingers on the screen + to see rectangles of different sizes being drawn. + '''; + + static final whitePaint = BasicPalette.white.paint(); + static final tapSize = Vector2.all(50); + + final Map taps = {}; + + Vector2? start; + Vector2? end; + Rect? panRect; + + @override + void onTapDown(int pointerId, TapDownInfo info) { + taps[pointerId] = info.eventPosition.widget.toPositionedRect(tapSize); + } + + @override + void onTapUp(int pointerId, _) { + taps.remove(pointerId); + } + + @override + void onTapCancel(int pointerId) { + taps.remove(pointerId); + } + + @override + void onDragCancel(int pointerId) { + end = null; + start = null; + panRect = null; + } + + @override + void onDragStart(int pointerId, DragStartInfo info) { + end = null; + start = info.eventPosition.widget; + } + + @override + void onDragUpdate(int pointerId, DragUpdateInfo info) { + end = info.eventPosition.widget; + } + + @override + void onDragEnd(int pointerId, _) { + final start = this.start; + final end = this.end; + if (start != null && end != null) { + panRect = start.toPositionedRect(end - start); + } + } + + @override + void render(Canvas canvas) { + final panRect = this.panRect; + super.render(canvas); + taps.values.forEach((rect) { + canvas.drawRect(rect, whitePaint); + }); + + if (panRect != null) { + canvas.drawRect(panRect, whitePaint); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/multitap_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/multitap_example.dart new file mode 100644 index 0000000..1ce89a0 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/multitap_example.dart @@ -0,0 +1,42 @@ +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; + +/// Includes an example including advanced detectors +class MultitapExample extends FlameGame with MultiTouchTapDetector { + static const String description = ''' + In this example we showcase the multi touch capabilities + Touch multiple places on the screen and you will see multiple squares drawn, + one under each finger. + '''; + + static final whitePaint = BasicPalette.white.paint(); + static final tapSize = Vector2.all(50); + + final Map taps = {}; + + @override + void onTapDown(int pointerId, TapDownInfo info) { + taps[pointerId] = info.eventPosition.widget.toPositionedRect(tapSize); + } + + @override + void onTapUp(int pointerId, _) { + taps.remove(pointerId); + } + + @override + void onTapCancel(int pointerId) { + taps.remove(pointerId); + } + + @override + void render(Canvas canvas) { + super.render(canvas); + taps.values.forEach((rect) { + canvas.drawRect(rect, whitePaint); + }); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/overlapping_tap_callbacks_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/overlapping_tap_callbacks_example.dart new file mode 100644 index 0000000..78bcacd --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/overlapping_tap_callbacks_example.dart @@ -0,0 +1,44 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +class OverlappingTapCallbacksExample extends FlameGame { + static const String description = ''' + In this example we show you that events can choose to continue propagating + to underlying components. The middle green square continue to propagate the + events, meanwhile the others do not. + '''; + + @override + Future onLoad() async { + add(TapCallbacksSquare(position: Vector2(100, 100))); + add( + TapCallbacksSquare( + position: Vector2(150, 150), + continuePropagation: true, + ), + ); + add(TapCallbacksSquare(position: Vector2(100, 200))); + } +} + +class TapCallbacksSquare extends RectangleComponent with TapCallbacks { + TapCallbacksSquare({Vector2? position, this.continuePropagation = false}) + : super( + position: position ?? Vector2.all(100), + size: Vector2.all(100), + paint: continuePropagation + ? (Paint()..color = Colors.green.withOpacity(0.9)) + : PaintExtension.random(withAlpha: 0.9, base: 100), + ); + + final bool continuePropagation; + + @override + void onTapDown(TapDownEvent event) { + event.continuePropagation = continuePropagation; + angle += 1.0; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/scroll_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/scroll_example.dart new file mode 100644 index 0000000..a3edccd --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/scroll_example.dart @@ -0,0 +1,49 @@ +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; + +class ScrollExample extends FlameGame with ScrollDetector { + static const String description = ''' + In this example we show how to use the `ScrollDetector`.\n\n + Scroll within the canvas (both horizontally and vertically) and the white + square will move around. + '''; + + static const speed = 2000.0; + final _size = Vector2.all(50); + final _paint = BasicPalette.white.paint(); + + Vector2 position = Vector2.all(100); + Vector2? target; + + @override + void onScroll(PointerScrollInfo info) { + target = position + info.scrollDelta.global * 5; + } + + @override + void render(Canvas canvas) { + super.render(canvas); + canvas.drawRect(position.toPositionedRect(_size), _paint); + } + + @override + void update(double dt) { + super.update(dt); + final target = this.target; + final ds = speed * dt; + if (target != null) { + if (position != target) { + final diff = target - position; + if (diff.length < ds) { + position.setFrom(target); + } else { + diff.scaleTo(ds); + position.setFrom(position + diff); + } + } + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/input/tap_callbacks_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/input/tap_callbacks_example.dart new file mode 100644 index 0000000..a076e58 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/input/tap_callbacks_example.dart @@ -0,0 +1,53 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +class TapCallbacksExample extends FlameGame { + static const String description = ''' + In this example we show the `TapCallbacks` mixin functionality. You can add + the `TapCallbacks` mixin to any `PositionComponent`.\n\n + Tap the squares to see them change their angle around their anchor. + '''; + + @override + Future onLoad() async { + world.add(TappableSquare()..anchor = Anchor.center); + world.add(TappableSquare()..y = 350); + } +} + +class TappableSquare extends PositionComponent with TapCallbacks { + static final Paint _white = Paint()..color = const Color(0xFFFFFFFF); + static final Paint _grey = Paint()..color = const Color(0xFFA5A5A5); + + bool _beenPressed = false; + + TappableSquare({Vector2? position}) + : super( + position: position ?? Vector2.all(100), + size: Vector2.all(100), + ); + + @override + void render(Canvas canvas) { + canvas.drawRect(size.toRect(), _beenPressed ? _grey : _white); + } + + @override + void onTapUp(_) { + _beenPressed = false; + } + + @override + void onTapDown(_) { + _beenPressed = true; + angle += 1.0; + } + + @override + void onTapCancel(_) { + _beenPressed = false; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/layout/align_component.dart b/flame/assets/examples/official/dashbook_example/lib/stories/layout/align_component.dart new file mode 100644 index 0000000..e429644 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/layout/align_component.dart @@ -0,0 +1,100 @@ +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/game.dart'; +import 'package:flame/layout.dart'; + +class AlignComponentExample extends FlameGame { + static const String description = ''' + In this example the AlignComponent is used to arrange the circles + so that there is one in the middle and 8 more surrounding it in + the shape of a diamond. + + The arrangement will remain intact if you change the window size. + '''; + + @override + void onLoad() { + addAll([ + AlignComponent( + child: CircleComponent( + radius: 40, + children: [ + SizeEffect.by( + Vector2.all(25), + EffectController( + infinite: true, + duration: 0.75, + reverseDuration: 0.5, + ), + ), + AlignComponent( + alignment: Anchor.topCenter, + child: CircleComponent( + radius: 10, + anchor: Anchor.bottomCenter, + ), + keepChildAnchor: true, + ), + AlignComponent( + alignment: Anchor.bottomCenter, + child: CircleComponent( + radius: 10, + anchor: Anchor.topCenter, + ), + keepChildAnchor: true, + ), + AlignComponent( + alignment: Anchor.centerLeft, + child: CircleComponent( + radius: 10, + anchor: Anchor.centerRight, + ), + keepChildAnchor: true, + ), + AlignComponent( + alignment: Anchor.centerRight, + child: CircleComponent( + radius: 10, + anchor: Anchor.centerLeft, + ), + keepChildAnchor: true, + ), + ], + ), + alignment: Anchor.center, + ), + AlignComponent( + child: CircleComponent(radius: 30), + alignment: Anchor.topCenter, + ), + AlignComponent( + child: CircleComponent(radius: 30), + alignment: Anchor.bottomCenter, + ), + AlignComponent( + child: CircleComponent(radius: 30), + alignment: Anchor.centerLeft, + ), + AlignComponent( + child: CircleComponent(radius: 30), + alignment: Anchor.centerRight, + ), + AlignComponent( + child: CircleComponent(radius: 10), + alignment: const Anchor(0.25, 0.25), + ), + AlignComponent( + child: CircleComponent(radius: 10), + alignment: const Anchor(0.25, 0.75), + ), + AlignComponent( + child: CircleComponent(radius: 10), + alignment: const Anchor(0.75, 0.25), + ), + AlignComponent( + child: CircleComponent(radius: 10), + alignment: const Anchor(0.75, 0.75), + ), + ]); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/layout/layout.dart b/flame/assets/examples/official/dashbook_example/lib/stories/layout/layout.dart new file mode 100644 index 0000000..8c46c4c --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/layout/layout.dart @@ -0,0 +1,13 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/layout/align_component.dart'; +import 'package:flame/game.dart'; + +void addLayoutStories(Dashbook dashbook) { + dashbook.storiesOf('Layout').add( + 'AlignComponent', + (_) => GameWidget(game: AlignComponentExample()), + codeLink: baseLink('layout/align_component.dart'), + info: AlignComponentExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/parallax/advanced_parallax_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/advanced_parallax_example.dart new file mode 100644 index 0000000..79d4e9e --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/advanced_parallax_example.dart @@ -0,0 +1,37 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/parallax.dart'; + +class AdvancedParallaxExample extends FlameGame { + static const String description = ''' + Shows how to create a parallax with different velocity deltas on each layer. + '''; + + final _layersMeta = { + 'parallax/bg.png': 1.0, + 'parallax/mountain-far.png': 1.5, + 'parallax/mountains.png': 2.3, + 'parallax/trees.png': 3.8, + 'parallax/foreground-trees.png': 6.6, + }; + + @override + Future onLoad() async { + final layers = _layersMeta.entries.map( + (e) => loadParallaxLayer( + ParallaxImageData(e.key), + velocityMultiplier: Vector2(e.value, 1.0), + filterQuality: FilterQuality.none, + ), + ); + final parallax = ParallaxComponent( + parallax: Parallax( + await Future.wait(layers), + baseVelocity: Vector2(20, 0), + ), + ); + add(parallax); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/parallax/animation_parallax_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/animation_parallax_example.dart new file mode 100644 index 0000000..4363d4e --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/animation_parallax_example.dart @@ -0,0 +1,51 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/parallax.dart'; +import 'package:flutter/painting.dart'; + +class AnimationParallaxExample extends FlameGame { + static const String description = ''' + Shows how to use animations in a `ParallaxComponent`. + '''; + + @override + Future onLoad() async { + final cityLayer = await loadParallaxLayer( + ParallaxImageData('parallax/city.png'), + filterQuality: FilterQuality.none, + ); + + final rainLayer = await loadParallaxLayer( + ParallaxAnimationData( + 'parallax/rain.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.3, + textureSize: Vector2(80, 160), + ), + ), + velocityMultiplier: Vector2(2, 0), + filterQuality: FilterQuality.none, + ); + + final cloudsLayer = await loadParallaxLayer( + ParallaxImageData('parallax/heavy_clouded.png'), + velocityMultiplier: Vector2(4, 0), + fill: LayerFill.none, + alignment: Alignment.topLeft, + filterQuality: FilterQuality.none, + ); + + final parallax = Parallax( + [ + cityLayer, + rainLayer, + cloudsLayer, + ], + baseVelocity: Vector2(20, 0), + ); + + final parallaxComponent = ParallaxComponent(parallax: parallax); + add(parallaxComponent); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/parallax/basic_parallax_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/basic_parallax_example.dart new file mode 100644 index 0000000..d7ce1f5 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/basic_parallax_example.dart @@ -0,0 +1,30 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/parallax.dart'; + +class BasicParallaxExample extends FlameGame { + static const String description = ''' + Shows the simplest way to use a fullscreen `ParallaxComponent`. + '''; + + final _imageNames = [ + ParallaxImageData('parallax/bg.png'), + ParallaxImageData('parallax/mountain-far.png'), + ParallaxImageData('parallax/mountains.png'), + ParallaxImageData('parallax/trees.png'), + ParallaxImageData('parallax/foreground-trees.png'), + ]; + + @override + Future onLoad() async { + final parallax = await loadParallaxComponent( + _imageNames, + baseVelocity: Vector2(20, 0), + velocityMultiplierDelta: Vector2(1.8, 1.0), + filterQuality: FilterQuality.none, + ); + add(parallax); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/parallax/component_parallax_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/component_parallax_example.dart new file mode 100644 index 0000000..fd464bd --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/component_parallax_example.dart @@ -0,0 +1,35 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/parallax.dart'; + +class ComponentParallaxExample extends FlameGame { + static const String description = ''' + Shows how to do initiation and loading of assets from within an extended + `ParallaxComponent`, + '''; + + @override + Future onLoad() async { + add(MyParallaxComponent()); + } +} + +class MyParallaxComponent extends ParallaxComponent { + @override + Future onLoad() async { + parallax = await game.loadParallax( + [ + ParallaxImageData('parallax/bg.png'), + ParallaxImageData('parallax/mountain-far.png'), + ParallaxImageData('parallax/mountains.png'), + ParallaxImageData('parallax/trees.png'), + ParallaxImageData('parallax/foreground-trees.png'), + ], + baseVelocity: Vector2(20, 0), + velocityMultiplierDelta: Vector2(1.8, 1.0), + filterQuality: FilterQuality.none, + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/parallax/no_fcs_parallax_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/no_fcs_parallax_example.dart new file mode 100644 index 0000000..9c4ab68 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/no_fcs_parallax_example.dart @@ -0,0 +1,44 @@ +import 'dart:ui'; + +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/parallax.dart'; + +class NoFCSParallaxExample extends Game { + static const String description = ''' + This examples serves to test the Parallax feature outside of the Flame + Component System (FCS), use the other files in this folder for examples on + how to use parallax with FCS.\n + FCS is only used when you extend FlameGame, not when you only use the Game + mixin, like we do in this example. + '''; + + late Parallax parallax; + + @override + Future onLoad() async { + parallax = await loadParallax( + [ + ParallaxImageData('parallax/bg.png'), + ParallaxImageData('parallax/mountain-far.png'), + ParallaxImageData('parallax/mountains.png'), + ParallaxImageData('parallax/trees.png'), + ParallaxImageData('parallax/foreground-trees.png'), + ], + size: size, + baseVelocity: Vector2(20, 0), + velocityMultiplierDelta: Vector2(1.8, 1.0), + filterQuality: FilterQuality.none, + ); + } + + @override + void update(double dt) { + parallax.update(dt); + } + + @override + void render(Canvas canvas) { + parallax.render(canvas); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/parallax/parallax.dart b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/parallax.dart new file mode 100644 index 0000000..91ece6e --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/parallax.dart @@ -0,0 +1,91 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/parallax/advanced_parallax_example.dart'; +import 'package:examples/stories/parallax/animation_parallax_example.dart'; +import 'package:examples/stories/parallax/basic_parallax_example.dart'; +import 'package:examples/stories/parallax/component_parallax_example.dart'; +import 'package:examples/stories/parallax/no_fcs_parallax_example.dart'; +import 'package:examples/stories/parallax/sandbox_layer_parallax_example.dart'; +import 'package:examples/stories/parallax/small_parallax_example.dart'; +import 'package:flame/game.dart'; +import 'package:flame/parallax.dart'; +import 'package:flutter/painting.dart'; + +void addParallaxStories(Dashbook dashbook) { + dashbook.storiesOf('Parallax') + ..add( + 'Basic', + (_) => GameWidget(game: BasicParallaxExample()), + codeLink: baseLink('parallax/basic_parallax_example.dart'), + info: BasicParallaxExample.description, + ) + ..add( + 'Component', + (_) => GameWidget(game: ComponentParallaxExample()), + codeLink: baseLink('parallax/component_parallax_example.dart'), + info: ComponentParallaxExample.description, + ) + ..add( + 'Animation', + (_) => GameWidget(game: AnimationParallaxExample()), + codeLink: baseLink('parallax/animation_parallax_example.dart'), + info: AnimationParallaxExample.description, + ) + ..add( + 'Non-fullscreen', + (_) => GameWidget(game: SmallParallaxExample()), + codeLink: baseLink('parallax/small_parallax_example.dart'), + info: SmallParallaxExample.description, + ) + ..add( + 'No FCS', + (_) => GameWidget(game: NoFCSParallaxExample()), + codeLink: baseLink('parallax/no_fcs_parallax_example.dart'), + info: NoFCSParallaxExample.description, + ) + ..add( + 'Advanced', + (_) => GameWidget(game: AdvancedParallaxExample()), + codeLink: baseLink('parallax/advanced_parallax_example.dart'), + info: AdvancedParallaxExample.description, + ) + ..add( + 'Layer sandbox', + (context) { + return GameWidget( + game: SandboxLayerParallaxExample( + planeSpeed: Vector2( + context.numberProperty('plane x speed', 0), + context.numberProperty('plane y speed', 0), + ), + planeRepeat: context.listProperty( + 'plane repeat strategy', + ImageRepeat.noRepeat, + ImageRepeat.values, + ), + planeFill: context.listProperty( + 'plane fill strategy', + LayerFill.none, + LayerFill.values, + ), + planeAlignment: context.listProperty( + 'plane alignment strategy', + Alignment.center, + [ + Alignment.topLeft, + Alignment.topRight, + Alignment.center, + Alignment.topCenter, + Alignment.centerLeft, + Alignment.bottomLeft, + Alignment.bottomRight, + Alignment.bottomCenter, + ], + ), + ), + ); + }, + codeLink: baseLink('parallax/sandbox_layer_parallax_example.dart'), + info: SandboxLayerParallaxExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/parallax/sandbox_layer_parallax_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/sandbox_layer_parallax_example.dart new file mode 100644 index 0000000..0044e54 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/sandbox_layer_parallax_example.dart @@ -0,0 +1,81 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/parallax.dart'; +import 'package:flutter/painting.dart'; + +class SandboxLayerParallaxExample extends FlameGame { + static const String description = ''' + In this example, properties of a layer can be changed to preview the + different combination of values. You can change the properties by pressing + the pen in the upper right corner. + '''; + + final Vector2 planeSpeed; + final ImageRepeat planeRepeat; + final LayerFill planeFill; + final Alignment planeAlignment; + + SandboxLayerParallaxExample({ + required this.planeSpeed, + required this.planeRepeat, + required this.planeFill, + required this.planeAlignment, + }); + + @override + Future onLoad() async { + final bgLayer = await loadParallaxLayer( + ParallaxImageData('parallax/bg.png'), + filterQuality: FilterQuality.none, + ); + final mountainFarLayer = await loadParallaxLayer( + ParallaxImageData('parallax/mountain-far.png'), + velocityMultiplier: Vector2(1.8, 0), + filterQuality: FilterQuality.none, + ); + final mountainLayer = await loadParallaxLayer( + ParallaxImageData('parallax/mountains.png'), + velocityMultiplier: Vector2(2.8, 0), + filterQuality: FilterQuality.none, + ); + final treeLayer = await loadParallaxLayer( + ParallaxImageData('parallax/trees.png'), + velocityMultiplier: Vector2(3.8, 0), + filterQuality: FilterQuality.none, + ); + final foregroundTreesLayer = await loadParallaxLayer( + ParallaxImageData('parallax/foreground-trees.png'), + velocityMultiplier: Vector2(4.8, 0), + filterQuality: FilterQuality.none, + ); + final airplaneLayer = await loadParallaxLayer( + ParallaxAnimationData( + 'parallax/airplane.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.2, + textureSize: Vector2(320, 160), + ), + ), + repeat: planeRepeat, + velocityMultiplier: planeSpeed, + fill: planeFill, + alignment: planeAlignment, + filterQuality: FilterQuality.none, + ); + + final parallax = Parallax( + [ + bgLayer, + mountainFarLayer, + mountainLayer, + treeLayer, + foregroundTreesLayer, + airplaneLayer, + ], + baseVelocity: Vector2(20, 0), + ); + + add(ParallaxComponent(parallax: parallax)); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/parallax/small_parallax_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/small_parallax_example.dart new file mode 100644 index 0000000..fda32ef --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/parallax/small_parallax_example.dart @@ -0,0 +1,27 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/parallax.dart'; + +class SmallParallaxExample extends FlameGame { + static const String description = ''' + Shows how to create a smaller parallax in the center of the screen. + '''; + + @override + Future onLoad() async { + final component = await loadParallaxComponent( + [ + ParallaxImageData('parallax/bg.png'), + ParallaxImageData('parallax/mountain-far.png'), + ParallaxImageData('parallax/mountains.png'), + ParallaxImageData('parallax/trees.png'), + ParallaxImageData('parallax/foreground-trees.png'), + ], + size: Vector2.all(200), + baseVelocity: Vector2(20, 0), + velocityMultiplierDelta: Vector2(1.8, 1.0), + ); + component.position = size / 2; + add(component); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/rendering/flip_sprite_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/flip_sprite_example.dart new file mode 100644 index 0000000..4308d0f --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/flip_sprite_example.dart @@ -0,0 +1,28 @@ +import 'package:examples/commons/ember.dart'; +import 'package:flame/game.dart'; + +class FlipSpriteExample extends FlameGame { + static const String description = ''' + In this example we show how you can flip components horizontally and + vertically. + '''; + + @override + Future onLoad() async { + final regular = Ember(position: Vector2(size.x / 2 - 100, 200)); + add(regular); + + final flipX = Ember(position: Vector2(size.x / 2 - 100, 400)); + flipX.flipHorizontally(); + add(flipX); + + final flipY = Ember(position: Vector2(size.x / 2 + 100, 200)); + flipY.flipVertically(); + add(flipY); + + final flipWithRotation = Ember(position: Vector2(size.x / 2 + 100, 400)) + ..angle = 2; + flipWithRotation.flipVertically(); + add(flipWithRotation); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/rendering/isometric_tile_map_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/isometric_tile_map_example.dart new file mode 100644 index 0000000..94ca0ac --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/isometric_tile_map_example.dart @@ -0,0 +1,100 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/sprite.dart'; + +class IsometricTileMapExample extends FlameGame with MouseMovementDetector { + static const String description = ''' + Shows an example of how to use the `IsometricTileMapComponent`.\n\n + Move the mouse over the board to see a selector appearing on the tiles. + '''; + + final topLeft = Vector2.all(500); + + static const scale = 2.0; + static const srcTileSize = 32.0; + static const destTileSize = scale * srcTileSize; + + final originColor = Paint()..color = const Color(0xFFFF00FF); + final originColor2 = Paint()..color = const Color(0xFFAA55FF); + + final bool halfSize; + late final tileHeight = scale * (halfSize ? 8.0 : 16.0); + late final suffix = halfSize ? '-short' : ''; + + late IsometricTileMapComponent base; + late Selector selector; + + IsometricTileMapExample({required this.halfSize}); + + @override + Future onLoad() async { + final tilesetImage = await images.load('tile_maps/tiles$suffix.png'); + final tileset = SpriteSheet( + image: tilesetImage, + srcSize: Vector2.all(srcTileSize), + ); + final matrix = [ + [3, 1, 1, 1, 0, 0], + [-1, 1, 2, 1, 0, 0], + [-1, 0, 1, 1, 0, 0], + [-1, 1, 1, 1, 0, 0], + [1, 1, 1, 1, 0, 2], + [1, 3, 3, 3, 0, 2], + ]; + add( + base = IsometricTileMapComponent( + tileset, + matrix, + destTileSize: Vector2.all(destTileSize), + tileHeight: tileHeight, + position: topLeft, + ), + ); + + final selectorImage = await images.load('tile_maps/selector$suffix.png'); + add(selector = Selector(destTileSize, selectorImage)); + } + + @override + void render(Canvas canvas) { + super.render(canvas); + canvas.renderPoint(topLeft, size: 5, paint: originColor); + canvas.renderPoint( + base.position + base.getBlockCenterPosition(const Block(0, 0)), + size: 5, + paint: originColor2, + ); + } + + @override + void onMouseMove(PointerHoverInfo info) { + final screenPosition = info.eventPosition.widget; + final block = base.getBlock(screenPosition); + selector.show = base.containsBlock(block); + selector.position.setFrom(topLeft + base.getBlockRenderPosition(block)); + } +} + +class Selector extends SpriteComponent { + bool show = true; + + Selector(double s, Image image) + : super( + sprite: Sprite(image, srcSize: Vector2.all(32.0)), + size: Vector2.all(s), + ); + + @override + void render(Canvas canvas) { + if (!show) { + return; + } + + super.render(canvas); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/rendering/layers_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/layers_example.dart new file mode 100644 index 0000000..f9ff12e --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/layers_example.dart @@ -0,0 +1,74 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/layers.dart'; + +class LayerExample extends FlameGame { + static const String description = ''' + In this example we show how layers can be used to produce a shadow effect. + '''; + + late Layer gameLayer; + late Layer backgroundLayer; + + @override + Future onLoad() async { + final playerSprite = Sprite(await images.load('layers/player.png')); + final enemySprite = Sprite(await images.load('layers/enemy.png')); + final backgroundSprite = Sprite(await images.load('layers/background.png')); + + gameLayer = GameLayer(playerSprite, enemySprite); + backgroundLayer = BackgroundLayer(backgroundSprite); + } + + @override + void render(Canvas canvas) { + super.render(canvas); + gameLayer.render(canvas); + backgroundLayer.render(canvas); + } + + @override + Color backgroundColor() => const Color(0xFF38607C); +} + +class GameLayer extends DynamicLayer { + final Sprite playerSprite; + final Sprite enemySprite; + + GameLayer(this.playerSprite, this.enemySprite) { + preProcessors.add(ShadowProcessor()); + } + + @override + void drawLayer() { + playerSprite.render( + canvas, + position: Vector2.all(50), + size: Vector2.all(150), + ); + enemySprite.render( + canvas, + position: Vector2(250, 150), + size: Vector2(100, 50), + ); + } +} + +class BackgroundLayer extends PreRenderedLayer { + final Sprite sprite; + + BackgroundLayer(this.sprite) { + preProcessors.add(ShadowProcessor()); + } + + @override + void drawLayer() { + sprite.render( + canvas, + position: Vector2(50, 200), + size: Vector2(300, 150), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/rendering/nine_tile_box_custom_grid_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/nine_tile_box_custom_grid_example.dart new file mode 100644 index 0000000..f978fd4 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/nine_tile_box_custom_grid_example.dart @@ -0,0 +1,46 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; + +class NineTileBoxCustomGridExample extends FlameGame + with TapDetector, DoubleTapDetector { + static const String description = ''' + If you want to create a background for something that can stretch you can + use the `NineTileBox` which is showcased here. In this example a custom + grid is used.\n\n + Tap to make the box bigger and double tap to make it smaller. + '''; + + late NineTileBoxComponent nineTileBoxComponent; + + @override + Future onLoad() async { + final sprite = Sprite(await images.load('speech-bubble.png')); + final boxSize = Vector2.all(300); + final nineTileBox = NineTileBox.withGrid( + sprite, + leftWidth: 31, + rightWidth: 5, + topHeight: 5, + bottomHeight: 21, + ); + add( + nineTileBoxComponent = NineTileBoxComponent( + nineTileBox: nineTileBox, + position: size / 2, + size: boxSize, + anchor: Anchor.center, + ), + ); + } + + @override + void onTap() { + nineTileBoxComponent.scale.scale(1.2); + } + + @override + void onDoubleTap() { + nineTileBoxComponent.scale.scale(0.8); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/rendering/nine_tile_box_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/nine_tile_box_example.dart new file mode 100644 index 0000000..16adfa0 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/nine_tile_box_example.dart @@ -0,0 +1,38 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; + +class NineTileBoxExample extends FlameGame with TapDetector, DoubleTapDetector { + static const String description = ''' + If you want to create a background for something that can stretch you can + use the `NineTileBox` which is showcased here.\n\n + Tap to make the box bigger and double tap to make it smaller. + '''; + + late NineTileBoxComponent nineTileBoxComponent; + + @override + Future onLoad() async { + final sprite = Sprite(await images.load('nine-box.png')); + final boxSize = Vector2.all(300); + final nineTileBox = NineTileBox(sprite, destTileSize: 148); + add( + nineTileBoxComponent = NineTileBoxComponent( + nineTileBox: nineTileBox, + position: size / 2, + size: boxSize, + anchor: Anchor.center, + ), + ); + } + + @override + void onTap() { + nineTileBoxComponent.scale.scale(1.2); + } + + @override + void onDoubleTap() { + nineTileBoxComponent.scale.scale(0.8); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/rendering/particles_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/particles_example.dart new file mode 100644 index 0000000..9f86685 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/particles_example.dart @@ -0,0 +1,559 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flame/components.dart' hide Timer; +import 'package:flame/game.dart'; +import 'package:flame/particles.dart'; +import 'package:flame/sprite.dart'; +import 'package:flame/timer.dart' as flame_timer; +import 'package:flutter/material.dart' hide Image; + +class ParticlesExample extends FlameGame { + static const String description = ''' + In this example we show how to render a lot of different particles. + '''; + + /// Defines dimensions of the sample + /// grid to be displayed on the screen, + /// 5x5 in this particular case + static const gridSize = 5.0; + static const steps = 5; + + /// Miscellaneous values used + /// by examples below + final Random rnd = Random(); + Timer? spawnTimer; + final StepTween steppedTween = StepTween(begin: 0, end: 5); + final trafficLight = TrafficLightComponent(); + + /// Defines the lifespan of all the particles in these examples + final sceneDuration = const Duration(seconds: 1); + + Vector2 get cellSize => size / gridSize; + Vector2 get halfCellSize => cellSize / 2; + + @override + Future onLoad() async { + await images.load('zap.png'); + await images.load('boom.png'); + } + + @override + void onMount() { + spawnParticles(); + // Spawn new particles every second + spawnTimer = Timer.periodic(sceneDuration, (_) { + spawnParticles(); + }); + } + + @override + void onRemove() { + super.onRemove(); + spawnTimer?.cancel(); + } + + /// Showcases various different uses of [Particle] + /// and its derivatives + void spawnParticles() { + // Contains sample particles, in order by complexity + // and amount of used features. Jump to source for more explanation on each + final particles = [ + circle(), + smallWhiteCircle(), + movingParticle(), + randomMovingParticle(), + alignedMovingParticles(), + easedMovingParticle(), + intervalMovingParticle(), + computedParticle(), + chainingBehaviors(), + steppedComputedParticle(), + reuseParticles(), + imageParticle(), + reuseImageParticle(), + rotatingImage(), + acceleratedParticles(), + paintParticle(), + spriteParticle(), + animationParticle(), + fireworkParticle(), + componentParticle(), + ]; + + // Place all the [Particle] instances + // defined above in a grid on the screen + // as per defined grid parameters + do { + final particle = particles.removeLast(); + final col = particles.length % gridSize; + final row = (particles.length ~/ gridSize).toDouble(); + final cellCenter = (cellSize..multiply(Vector2(col, row))) + halfCellSize; + + add( + // Bind all the particles to a [Component] update + // lifecycle from the [FlameGame]. + ParticleSystemComponent( + particle: TranslatedParticle( + lifespan: 1, + offset: cellCenter, + child: particle, + ), + ), + ); + } while (particles.isNotEmpty); + } + + /// Simple static circle, doesn't move or + /// change any of its attributes + Particle circle() { + return CircleParticle( + paint: Paint()..color = Colors.white10, + ); + } + + /// This one will is a bit smaller, + /// and a bit less transparent + Particle smallWhiteCircle() { + return CircleParticle( + radius: 5.0, + paint: Paint()..color = Colors.white, + ); + } + + /// Particle which is moving from + /// one predefined position to another one + Particle movingParticle() { + return MovingParticle( + /// This parameter is optional, will default to [Vector2.zero] + from: Vector2(-20, -20), + to: Vector2(20, 20), + child: CircleParticle(paint: Paint()..color = Colors.amber), + ); + } + + /// [Particle] which is moving to a random direction + /// within each cell each time created + Particle randomMovingParticle() { + return MovingParticle( + to: randomCellVector2(), + child: CircleParticle( + radius: 5 + rnd.nextDouble() * 5, + paint: Paint()..color = Colors.red, + ), + ); + } + + /// Generates 5 particles, each moving + /// symmetrically within grid cell + Particle alignedMovingParticles() { + return Particle.generate( + count: 5, + generator: (i) { + final currentColumn = (cellSize.x / 5) * i - halfCellSize.x; + return MovingParticle( + from: Vector2(currentColumn, -halfCellSize.y), + to: Vector2(currentColumn, halfCellSize.y), + child: CircleParticle( + radius: 2.0, + paint: Paint()..color = Colors.blue, + ), + ); + }, + ); + } + + /// Burst of 5 particles each moving + /// to a random direction within the cell + Particle randomMovingParticles() { + return Particle.generate( + count: 5, + generator: (i) => MovingParticle( + to: randomCellVector2()..scale(.5), + child: CircleParticle( + radius: 5 + rnd.nextDouble() * 5, + paint: Paint()..color = Colors.deepOrange, + ), + ), + ); + } + + /// Same example as above, but with easing, utilizing [CurvedParticle] + /// extension. + Particle easedMovingParticle() { + return Particle.generate( + count: 5, + generator: (i) => MovingParticle( + curve: Curves.easeOutQuad, + to: randomCellVector2()..scale(.5), + child: CircleParticle( + radius: 5 + rnd.nextDouble() * 5, + paint: Paint()..color = Colors.deepPurple, + ), + ), + ); + } + + /// Same example as above, but using awesome [Interval] + /// curve, which "schedules" transition to happen between + /// certain values of progress. In this example, circles will + /// move from their initial to their final position + /// when progress is changing from 0.2 to 0.6 respectively. + Particle intervalMovingParticle() { + return Particle.generate( + count: 5, + generator: (i) => MovingParticle( + curve: const Interval(.2, .6, curve: Curves.easeInOutCubic), + to: randomCellVector2()..scale(.5), + child: CircleParticle( + radius: 5 + rnd.nextDouble() * 5, + paint: Paint()..color = Colors.greenAccent, + ), + ), + ); + } + + /// A [ComputedParticle] completely delegates all the rendering + /// to an external function, hence It's very flexible, as you can implement + /// any currently missing behavior with it. + /// Also, it allows to optimize complex behaviors by avoiding nesting too + /// many [Particle] together and having all the computations in place. + Particle computedParticle() { + return ComputedParticle( + renderer: (canvas, particle) => canvas.drawCircle( + Offset.zero, + particle.progress * halfCellSize.x, + Paint() + ..color = Color.lerp( + Colors.red, + Colors.blue, + particle.progress, + )!, + ), + ); + } + + /// Using [ComputedParticle] to use custom tweening + /// In reality, you would like to keep as much of renderer state + /// defined outside and reused between each call + Particle steppedComputedParticle() { + return ComputedParticle( + lifespan: 2, + renderer: (canvas, particle) { + const steps = 5; + final steppedProgress = + steppedTween.transform(particle.progress) / steps; + + canvas.drawCircle( + Offset.zero, + (1 - steppedProgress) * halfCellSize.x, + Paint() + ..color = Color.lerp( + Colors.red, + Colors.blue, + steppedProgress, + )!, + ); + }, + ); + } + + /// Particle which is used in example below + Particle? reusableParticle; + + /// A burst of white circles which actually using a single circle + /// as a form of optimization. Look for reusing parts of particle effects + /// whenever possible, as there are limits which are relatively easy to reach. + Particle reuseParticles() { + reusableParticle ??= circle(); + + return Particle.generate( + generator: (i) => MovingParticle( + curve: Interval(rnd.nextDouble() * .1, rnd.nextDouble() * .8 + .1), + to: randomCellVector2()..scale(.5), + child: reusableParticle!, + ), + ); + } + + /// Simple static image particle which doesn't do much. + /// Images are great examples of where assets should + /// be reused across particles. See example below for more details. + Particle imageParticle() { + return ImageParticle( + size: Vector2.all(24), + image: images.fromCache('zap.png'), + ); + } + + /// Particle which is used in example below + Particle? reusableImageParticle; + + /// A single [imageParticle] is drawn 9 times + /// in a grid within grid cell. Looks as 9 particles + /// to user, saves us 8 particle objects. + Particle reuseImageParticle() { + const count = 9; + const perLine = 3; + const imageSize = 24.0; + final colWidth = cellSize.x / perLine; + final rowHeight = cellSize.y / perLine; + + reusableImageParticle ??= imageParticle(); + + return Particle.generate( + count: count, + generator: (i) => TranslatedParticle( + offset: Vector2( + (i % perLine) * colWidth - halfCellSize.x + imageSize, + (i ~/ perLine) * rowHeight - halfCellSize.y + imageSize, + ), + child: reusableImageParticle!, + ), + ); + } + + /// [RotatingParticle] is a simple container which rotates + /// a child particle passed to it. + /// As you can see, we're reusing [imageParticle] from example above. + /// Such a composability is one of the main implementation features. + Particle rotatingImage({double initialAngle = 0}) { + return RotatingParticle(from: initialAngle, child: imageParticle()); + } + + /// [AcceleratedParticle] is a very basic acceleration physics container, + /// which could help implementing such behaviors as gravity, or adding + /// some non-linearity to something like [MovingParticle] + Particle acceleratedParticles() { + return Particle.generate( + generator: (i) => AcceleratedParticle( + speed: + Vector2(rnd.nextDouble() * 600 - 300, -rnd.nextDouble() * 600) * .2, + acceleration: Vector2(0, 200), + child: rotatingImage(initialAngle: rnd.nextDouble() * pi), + ), + ); + } + + /// [PaintParticle] allows to perform basic composite operations + /// by specifying custom [Paint]. + /// Be aware that it's very easy to get *really* bad performance + /// misusing composites. + Particle paintParticle() { + final colors = [ + const Color(0xffff0000), + const Color(0xff00ff00), + const Color(0xff0000ff), + ]; + final positions = [ + Vector2(-10, 10), + Vector2(10, 10), + Vector2(0, -14), + ]; + + return Particle.generate( + count: 3, + generator: (i) => PaintParticle( + paint: Paint()..blendMode = BlendMode.difference, + child: MovingParticle( + curve: SineCurve(), + from: positions[i], + to: i == 0 ? positions.last : positions[i - 1], + child: CircleParticle( + radius: 20.0, + paint: Paint()..color = colors[i], + ), + ), + ), + ); + } + + /// [SpriteParticle] allows easily embed + /// Flame's [Sprite] into the effect. + Particle spriteParticle() { + return SpriteParticle( + sprite: Sprite(images.fromCache('zap.png')), + size: cellSize * .5, + ); + } + + /// An [SpriteAnimationParticle] takes a Flame [SpriteAnimation] + /// and plays it during the particle lifespan. + Particle animationParticle() { + return SpriteAnimationParticle( + animation: getBoomAnimation(), + size: Vector2(128, 128), + ); + } + + /// [ComponentParticle] proxies particle lifecycle hooks + /// to its child [Component]. In example below, [Component] is + /// reused between particle effects and has internal behavior + /// which is independent from the parent [Particle]. + Particle componentParticle() { + return MovingParticle( + from: -halfCellSize * .2, + to: halfCellSize * .2, + curve: SineCurve(), + child: ComponentParticle(component: trafficLight), + ); + } + + /// Not very realistic firework, yet it highlights + /// use of [ComputedParticle] within other particles, + /// mixing predefined and fully custom behavior. + Particle fireworkParticle() { + // A palette to paint over the "sky" + final paints = [ + Colors.amber, + Colors.amberAccent, + Colors.red, + Colors.redAccent, + Colors.yellow, + Colors.yellowAccent, + // Adds a nice "lense" tint + // to overall effect + Colors.blue, + ].map((color) => Paint()..color = color).toList(); + + return Particle.generate( + generator: (i) { + final initialSpeed = randomCellVector2(); + final deceleration = initialSpeed * -1; + final gravity = Vector2(0, 40); + + return AcceleratedParticle( + speed: initialSpeed, + acceleration: deceleration + gravity, + child: ComputedParticle( + renderer: (canvas, particle) { + final paint = randomElement(paints); + // Override the color to dynamically update opacity + paint.color = paint.color.withOpacity(1 - particle.progress); + + canvas.drawCircle( + Offset.zero, + // Closer to the end of lifespan particles + // will turn into larger glaring circles + rnd.nextDouble() * particle.progress > .6 + ? rnd.nextDouble() * (50 * particle.progress) + : 2 + (3 * particle.progress), + paint, + ); + }, + ), + ); + }, + ); + } + + /// [Particle] base class exposes a number + /// of convenience wrappers to make positioning. + /// + /// Just remember that the less chaining and nesting - the + /// better for performance! + Particle chainingBehaviors() { + final paint = Paint()..color = randomMaterialColor(); + final rect = ComputedParticle( + renderer: (canvas, _) => canvas.drawRect( + Rect.fromCenter(center: Offset.zero, width: 10, height: 10), + paint, + ), + ); + + return ComposedParticle( + children: [ + rect + .rotating(to: pi / 2) + .moving(to: -cellSize) + .scaled(2) + .accelerated(acceleration: halfCellSize * 5) + .translated(halfCellSize), + rect + .rotating(to: -pi) + .moving(to: Vector2(1, -1)..multiply(cellSize)) + .scaled(2) + .translated(Vector2(1, -1)..multiply(halfCellSize)) + .accelerated(acceleration: Vector2(-5, 5)..multiply(halfCellSize)), + ], + ); + } + + /// Returns random [Vector2] within a virtual grid cell + Vector2 randomCellVector2() { + return (Vector2.random() - Vector2.random())..multiply(cellSize); + } + + /// Returns random [Color] from primary swatches + /// of material palette + Color randomMaterialColor() { + return Colors.primaries[rnd.nextInt(Colors.primaries.length)]; + } + + /// Returns a random element from a given list + T randomElement(List list) { + return list[rnd.nextInt(list.length)]; + } + + /// Sample "explosion" animation for [SpriteAnimationParticle] example + SpriteAnimation getBoomAnimation() { + const columns = 8; + const rows = 8; + const frames = columns * rows; + final spriteImage = images.fromCache('boom.png'); + final spriteSheet = SpriteSheet.fromColumnsAndRows( + image: spriteImage, + columns: columns, + rows: rows, + ); + final sprites = List.generate(frames, spriteSheet.getSpriteById); + return SpriteAnimation.spriteList(sprites, stepTime: 0.1); + } +} + +Future loadGame() async { + WidgetsFlutterBinding.ensureInitialized(); + + return ParticlesExample(); +} + +/// A curve which maps sinus output (-1..1,0..pi) +/// to an oscillating (0..1..0,0..1), essentially "ease-in-out and back" +class SineCurve extends Curve { + @override + double transformInternal(double t) { + return (sin(pi * (t * 2 - 1 / 2)) + 1) / 2; + } +} + +/// Sample for [ComponentParticle], changes its colors +/// each 2s of registered lifetime. +class TrafficLightComponent extends Component { + final Rect rect = Rect.fromCenter(center: Offset.zero, height: 32, width: 32); + final flame_timer.Timer colorChangeTimer = flame_timer.Timer(2, repeat: true); + final colors = [ + Colors.green, + Colors.orange, + Colors.red, + ]; + final Paint _paint = Paint(); + + @override + void onMount() { + colorChangeTimer.start(); + } + + @override + void render(Canvas canvas) { + canvas.drawRect(rect, _paint..color = currentColor); + } + + @override + void update(double dt) { + colorChangeTimer.update(dt); + } + + Color get currentColor { + return colors[(colorChangeTimer.progress * colors.length).toInt()]; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/rendering/particles_interactive_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/particles_interactive_example.dart new file mode 100644 index 0000000..d491fd6 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/particles_interactive_example.dart @@ -0,0 +1,58 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/particles.dart'; +import 'package:flutter/material.dart'; + +class ParticlesInteractiveExample extends FlameGame with PanDetector { + static const description = 'An example which shows how ' + 'ParticleSystemComponent can be added in runtime ' + 'following an event, in this example, the mouse ' + 'dragging'; + + final random = Random(); + final Tween noise = Tween(begin: -1, end: 1); + final ColorTween colorTween; + + ParticlesInteractiveExample({ + required Color from, + required Color to, + required double zoom, + }) : colorTween = ColorTween(begin: from, end: to), + super( + camera: CameraComponent.withFixedResolution( + width: 400, + height: 600, + )..viewfinder.zoom = zoom, + ); + + @override + void onPanUpdate(DragUpdateInfo info) { + add( + ParticleSystemComponent( + position: info.eventPosition.widget, + particle: Particle.generate( + count: 40, + generator: (i) { + return AcceleratedParticle( + lifespan: 2, + speed: Vector2( + noise.transform(random.nextDouble()), + noise.transform(random.nextDouble()), + ) * + i.toDouble(), + child: CircleParticle( + radius: 2, + paint: Paint() + ..color = colorTween.transform(random.nextDouble())!, + ), + ); + }, + ), + ), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/rendering/rendering.dart b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/rendering.dart new file mode 100644 index 0000000..0fb17fa --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/rendering.dart @@ -0,0 +1,82 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/rendering/flip_sprite_example.dart'; +import 'package:examples/stories/rendering/isometric_tile_map_example.dart'; +import 'package:examples/stories/rendering/layers_example.dart'; +import 'package:examples/stories/rendering/nine_tile_box_example.dart'; +import 'package:examples/stories/rendering/particles_example.dart'; +import 'package:examples/stories/rendering/particles_interactive_example.dart'; +import 'package:examples/stories/rendering/rich_text_example.dart'; +import 'package:examples/stories/rendering/text_example.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +void addRenderingStories(Dashbook dashbook) { + dashbook.storiesOf('Rendering') + ..add( + 'Text', + (_) => GameWidget(game: TextExample()), + codeLink: baseLink('rendering/text_example.dart'), + info: TextExample.description, + ) + ..add( + 'Isometric Tile Map', + (context) => GameWidget( + game: IsometricTileMapExample( + halfSize: context.boolProperty('Half size', true), + ), + ), + codeLink: baseLink('rendering/isometric_tile_map_example.dart'), + info: IsometricTileMapExample.description, + ) + ..add( + 'Nine Tile Box', + (_) => GameWidget(game: NineTileBoxExample()), + codeLink: baseLink('rendering/nine_tile_box_example.dart'), + info: NineTileBoxExample.description, + ) + ..add( + 'Flip Sprite', + (_) => GameWidget(game: FlipSpriteExample()), + codeLink: baseLink('rendering/flip_sprite_example.dart'), + info: FlipSpriteExample.description, + ) + ..add( + 'Layers', + (_) => GameWidget(game: LayerExample()), + codeLink: baseLink('rendering/layers_example.dart'), + info: LayerExample.description, + ) + ..add( + 'Particles', + (_) => GameWidget(game: ParticlesExample()), + codeLink: baseLink('rendering/particles_example.dart'), + info: ParticlesExample.description, + ) + ..add( + 'Particles (Interactive)', + (context) => GameWidget( + game: ParticlesInteractiveExample( + from: context.colorProperty('From color', Colors.pink), + to: context.colorProperty('To color', Colors.blue), + zoom: context.numberProperty('Zoom', 1), + ), + ), + codeLink: baseLink('rendering/particles_interactive_example.dart'), + info: ParticlesInteractiveExample.description, + ) + ..add( + 'Rich Text', + (context) => GameWidget( + game: RichTextExample( + textAlign: context.listProperty( + 'Text align', + TextAlign.left, + TextAlign.values, + ), + ), + ), + codeLink: baseLink('rendering/rich_text_example.dart'), + info: RichTextExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/rendering/rich_text_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/rich_text_example.dart new file mode 100644 index 0000000..a6ce405 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/rich_text_example.dart @@ -0,0 +1,79 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/text.dart'; +import 'package:flutter/painting.dart'; + +class RichTextExample extends FlameGame { + final TextAlign textAlign; + + RichTextExample({this.textAlign = TextAlign.left}); + + static const String description = + 'A non-interactive example of how to render rich text in Flame.'; + + @override + Color backgroundColor() => const Color(0xFF888888); + + @override + Future onLoad() async { + final style = DocumentStyle( + width: 400, + height: 200, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), + background: BackgroundStyle( + color: const Color(0xFF4E322E), + borderColor: const Color(0xFF000000), + borderWidth: 2.0, + ), + paragraph: BlockStyle( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + textAlign: textAlign, + background: BackgroundStyle( + color: const Color(0xFF004D40), + borderColor: const Color(0xFFAAAAAA), + ), + ), + header1: BlockStyle( + textAlign: textAlign, + ), + ); + final document = DocumentRoot([ + HeaderNode.simple('1984', level: 1), + ParagraphNode.simple( + 'Anything could be true. The so-called laws of nature were nonsense.', + ), + ParagraphNode.simple( + 'The law of gravity was nonsense. "If I wished," O\'Brien had said, ' + '"I could float off this floor like a soap bubble." Winston worked it ' + 'out. "If he thinks he floats off the floor, and I simultaneously ' + 'think I can see him do it, then the thing happens."', + ), + ParagraphNode.group([ + PlainTextNode( + 'Suddenly, like a lump of submerged wreckage breaking the surface ' + 'of water, the thought burst into his mind: '), + ItalicTextNode.group([ + PlainTextNode('"It doesn\'t really happen. We imagine it. It is '), + BoldTextNode.simple('hallucination'), + PlainTextNode('."'), + ]), + ]), + ParagraphNode.simple( + 'He pushed the thought under instantly. The fallacy was obvious. It ' + 'presupposed that somewhere or other, outside oneself, there was a ' + '"real" world where "real" things happened. But how could there be ' + 'such a world? What knowledge have we of anything, save through our ' + 'own minds? All happenings are in the mind. Whatever happens in all ' + 'minds, truly happens.', + ), + ]); + + add( + TextElementComponent.fromDocument( + document: document, + style: style, + position: Vector2(100, 50), + ), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/rendering/text_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/text_example.dart new file mode 100644 index 0000000..14f5dad --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/rendering/text_example.dart @@ -0,0 +1,182 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/palette.dart'; +import 'package:flame/text.dart'; +import 'package:flutter/material.dart'; + +class TextExample extends FlameGame { + static const String description = ''' + In this example we show different ways of rendering text. + '''; + + @override + Future onLoad() async { + addAll( + [ + TextComponent(text: 'Hello, Flame', textRenderer: _regular) + ..anchor = Anchor.topCenter + ..x = size.x / 2 + ..y = 32.0, + TextComponent(text: 'Text with shade', textRenderer: _shaded) + ..anchor = Anchor.topRight + ..position = size - Vector2.all(100), + TextComponent(text: 'center', textRenderer: _tiny) + ..anchor = Anchor.center + ..position.setFrom(size / 2), + TextComponent(text: 'bottomRight', textRenderer: _tiny) + ..anchor = Anchor.bottomRight + ..position.setFrom(size), + MyTextBox( + '"This is our world now. The world of the electron and the switch; ' + 'the beauty of the baud. We exist without nationality, skin color, ' + 'or religious bias. You wage wars, murder, cheat, lie to us and try ' + "to make us believe it's for our own good, yet we're the " + 'criminals. Yes, I am a criminal. My crime is that of curiosity."', + ) + ..anchor = Anchor.bottomLeft + ..y = size.y, + MyTextBox( + 'Let A be a finitely generated torsion-free abelian group. Then ' + 'A is free.', + align: Anchor.center, + size: Vector2(300, 200), + timePerChar: 0, + margins: 10, + )..position = Vector2(10, 50), + MyTextBox( + 'Let A be a torsion abelian group. Then A is the direct sum of its ' + 'subgroups A(p) for all primes p such that A(p) ≠ 0.', + align: Anchor.bottomRight, + size: Vector2(300, 200), + timePerChar: 0, + margins: 10, + )..position = Vector2(10, 260), + TextComponent( + text: 'Scroll me when finished:', + position: Vector2(size.x / 2, size.y / 2 + 100), + anchor: Anchor.bottomCenter, + ), + MyScrollTextBox( + 'In a bustling city, a small team of developers set out to create ' + 'a mobile game using the Flame engine for Flutter. Their goal was ' + 'simple: to create an engaging, easy-to-play game that could reach ' + 'a wide audience on both iOS and Android platforms. ' + 'After weeks of brainstorming, they decided on a concept: ' + 'a fast-paced, endless runner game set in a whimsical, ' + 'ever-changing world. They named it "Swift Dash." ' + "Using Flutter's versatility and the Flame engine's " + 'capabilities, the team crafted a game with vibrant graphics, ' + 'smooth animations, and responsive controls. ' + 'The game featured a character dashing through various landscapes, ' + 'dodging obstacles, and collecting points. ' + 'As they launched "Swift Dash," the team was anxious but hopeful. ' + 'To their delight, the game was well-received. Players loved its ' + 'simplicity and charm, and the game quickly gained popularity.', + size: Vector2(200, 150), + position: Vector2(size.x / 2, size.y / 2 + 100), + anchor: Anchor.topCenter, + boxConfig: const TextBoxConfig( + timePerChar: 0.005, + margins: EdgeInsets.fromLTRB(10, 10, 10, 10), + ), + ), + ], + ); + } +} + +final _regularTextStyle = TextStyle( + fontSize: 18, + color: BasicPalette.white.color, +); +final _regular = TextPaint( + style: _regularTextStyle, +); +final _tiny = TextPaint(style: _regularTextStyle.copyWith(fontSize: 14.0)); +final _box = _regular.copyWith( + (style) => style.copyWith( + color: Colors.lightGreenAccent, + fontFamily: 'monospace', + letterSpacing: 2.0, + ), +); +final _shaded = TextPaint( + style: TextStyle( + color: BasicPalette.white.color, + fontSize: 40.0, + shadows: const [ + Shadow(color: Colors.red, offset: Offset(2, 2), blurRadius: 2), + Shadow(color: Colors.yellow, offset: Offset(4, 4), blurRadius: 4), + ], + ), +); + +class MyTextBox extends TextBoxComponent { + late Paint paint; + late Rect bgRect; + + MyTextBox( + String text, { + super.align, + super.size, + double? timePerChar, + double? margins, + }) : super( + text: text, + textRenderer: _box, + boxConfig: TextBoxConfig( + maxWidth: 400, + timePerChar: timePerChar ?? 0.05, + growingBox: true, + margins: EdgeInsets.all(margins ?? 25), + ), + ); + + @override + Future onLoad() { + paint = Paint(); + bgRect = Rect.fromLTWH(0, 0, width, height); + size.addListener(() { + bgRect = Rect.fromLTWH(0, 0, width, height); + }); + + paint.color = Colors.white10; + return super.onLoad(); + } + + @override + void render(Canvas canvas) { + canvas.drawRect(bgRect, paint); + super.render(canvas); + } +} + +class MyScrollTextBox extends ScrollTextBoxComponent { + late Paint paint; + late Rect backgroundRect; + + MyScrollTextBox( + String text, { + required super.size, + super.boxConfig, + super.position, + super.anchor, + }) : super(text: text, textRenderer: _box); + + @override + FutureOr onLoad() { + paint = Paint(); + backgroundRect = Rect.fromLTWH(0, 0, width, height); + + paint.color = Colors.white10; + return super.onLoad(); + } + + @override + void render(Canvas canvas) { + canvas.drawRect(backgroundRect, paint); + super.render(canvas); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/sprites/base64_sprite_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/base64_sprite_example.dart new file mode 100644 index 0000000..22cf670 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/base64_sprite_example.dart @@ -0,0 +1,29 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; + +class Base64SpriteExample extends FlameGame { + static const String description = ''' + In this example we load a sprite from the a base64 string and put it into a + `SpriteComponent`. + '''; + + @override + Future onLoad() async { + const exampleUrl = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/' + '9hAAAAxElEQVQ4jYWTMQ7DIAxFIeoNuAGK1K1ISL0DMwOHzNC5p6iUPeoNOEM7GZ' + 'nPJ/EUbP7Lx7KtIfH91B/L++gs5m5M9NreTN/dEZiVghatwbXvY68UlksyPjprRa' + 'xFGAJZg+uAuSSzzC7rEDirDYAz2wg0RjWRFa/EUwdnQnQ37QFe1Odjrw04AKTTaB' + 'XPAlx8dDaXdNk4rMsc0B7ge/UcYLTZxoFizxCQ/L0DMAhaX4Mzj/uzW6phu3AvtH' + 'UUU4BAWJ6t8x9N/HHcruXjwQAAAABJRU5ErkJggg=='; + final image = await images.fromBase64('shield.png', exampleUrl); + add( + SpriteComponent.fromImage( + image, + position: size / 2, + size: Vector2.all(100), + anchor: Anchor.center, + ), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/sprites/basic_sprite_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/basic_sprite_example.dart new file mode 100644 index 0000000..457e626 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/basic_sprite_example.dart @@ -0,0 +1,22 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; + +class BasicSpriteExample extends FlameGame { + static const String description = ''' + In this example we load a sprite from the assets folder and put it into a + `SpriteComponent`. + '''; + + @override + Future onLoad() async { + final sprite = await loadSprite('flame.png'); + add( + SpriteComponent( + sprite: sprite, + position: size / 2, + size: sprite.srcSize * 2, + anchor: Anchor.center, + ), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprite_batch_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprite_batch_example.dart new file mode 100644 index 0000000..295f31d --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprite_batch_example.dart @@ -0,0 +1,52 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/sprite.dart'; +import 'package:flutter/material.dart'; + +class SpriteBatchExample extends FlameGame { + static const String description = ''' + In this example we show how to render many sprites in a batch for + efficiency, this is done with `SpriteBatch` and the `SpriteBatchComponent`. + '''; + + @override + Future onLoad() async { + final spriteBatch = await SpriteBatch.load('boom.png'); + + spriteBatch.add( + source: const Rect.fromLTWH(128 * 4.0, 128 * 4.0, 64, 128), + offset: Vector2.all(200), + color: Colors.greenAccent, + scale: 2, + rotation: pi / 9.0, + anchor: Vector2.all(64), + ); + + spriteBatch.addTransform( + source: const Rect.fromLTWH(128 * 4.0, 128 * 4.0, 64, 128), + color: Colors.redAccent, + ); + + const num = 100; + final r = Random(); + for (var i = 0; i < num; ++i) { + final sx = r.nextInt(8) * 128.0; + final sy = r.nextInt(8) * 128.0; + final x = r.nextInt(size.x.toInt()).toDouble(); + final y = r.nextInt(size.y ~/ 2).toDouble() + size.y / 2.0; + spriteBatch.add( + source: Rect.fromLTWH(sx, sy, 128, 128), + offset: Vector2(x - 64, y - 64), + ); + } + + add( + SpriteBatchComponent( + spriteBatch: spriteBatch, + blendMode: BlendMode.srcOver, + ), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprite_batch_load_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprite_batch_load_example.dart new file mode 100644 index 0000000..eaf0207 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprite_batch_load_example.dart @@ -0,0 +1,56 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/sprite.dart'; +import 'package:flutter/material.dart'; + +class SpriteBatchLoadExample extends FlameGame { + static const String description = ''' + In this example we do the same thing as in the normal sprite batch example, + but in this example the logic and loading is moved into a component that + extends `SpriteBatchComponent`. + '''; + + @override + Future onLoad() async { + add(MySpriteBatchComponent()); + } +} + +class MySpriteBatchComponent extends SpriteBatchComponent + with HasGameReference { + @override + Future onLoad() async { + final spriteBatch = await game.loadSpriteBatch('boom.png'); + this.spriteBatch = spriteBatch; + + spriteBatch.add( + source: const Rect.fromLTWH(128 * 4.0, 128 * 4.0, 64, 128), + offset: Vector2.all(200), + color: Colors.greenAccent, + scale: 2, + rotation: pi / 9.0, + anchor: Vector2.all(64), + ); + + spriteBatch.addTransform( + source: const Rect.fromLTWH(128 * 4.0, 128 * 4.0, 64, 128), + color: Colors.redAccent, + ); + + final size = game.size; + const num = 100; + final r = Random(); + for (var i = 0; i < num; ++i) { + final sx = r.nextInt(8) * 128.0; + final sy = r.nextInt(8) * 128.0; + final x = r.nextInt(size.x.toInt()).toDouble(); + final y = r.nextInt(size.y ~/ 2).toDouble() + size.y / 2.0; + spriteBatch.add( + source: Rect.fromLTWH(sx, sy, 128, 128), + offset: Vector2(x - 64, y - 64), + ); + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprite_group_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprite_group_example.dart new file mode 100644 index 0000000..0bdea5f --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprite_group_example.dart @@ -0,0 +1,61 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; + +enum ButtonState { unpressed, pressed } + +class SpriteGroupExample extends FlameGame { + static const String description = ''' + In this example we show how a `SpriteGroupComponent` can be used to create + a button which displays different sprites depending on whether it is pressed + or not. + '''; + + @override + Future onLoad() async { + add( + ButtonComponent() + ..position = size / 2 + ..size = Vector2(200, 50) + ..anchor = Anchor.center, + ); + } +} + +class ButtonComponent extends SpriteGroupComponent + with HasGameReference, TapCallbacks { + @override + Future onLoad() async { + final pressedSprite = await game.loadSprite( + 'buttons.png', + srcPosition: Vector2(0, 20), + srcSize: Vector2(60, 20), + ); + final unpressedSprite = await game.loadSprite( + 'buttons.png', + srcSize: Vector2(60, 20), + ); + + sprites = { + ButtonState.pressed: pressedSprite, + ButtonState.unpressed: unpressedSprite, + }; + + current = ButtonState.unpressed; + } + + @override + void onTapDown(_) { + current = ButtonState.pressed; + } + + @override + void onTapUp(_) { + current = ButtonState.unpressed; + } + + @override + void onTapCancel(_) { + current = ButtonState.unpressed; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprite_sheet_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprite_sheet_example.dart new file mode 100644 index 0000000..86c592a --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprite_sheet_example.dart @@ -0,0 +1,91 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/sprite.dart'; + +class SpriteSheetExample extends FlameGame { + static const String description = ''' + In this example we show how to load images and how to create animations from + sprite sheets. + '''; + + @override + Future onLoad() async { + final spriteSheet = SpriteSheet( + image: await images.load('sprite_sheet.png'), + srcSize: Vector2(16.0, 18.0), + ); + + final vampireAnimation = + spriteSheet.createAnimation(row: 0, stepTime: 0.1, to: 7); + + final ghostAnimation = + spriteSheet.createAnimation(row: 1, stepTime: 0.1, to: 7); + + final ghostAnimationVariableStepTimes = + spriteSheet.createAnimationWithVariableStepTimes( + row: 1, + to: 7, + stepTimes: [0.1, 0.1, 0.3, 0.3, 0.5, 0.3, 0.1], + ); + + final customVampireAnimation = SpriteAnimation.fromFrameData( + spriteSheet.image, + SpriteAnimationData([ + spriteSheet.createFrameData(0, 0, stepTime: 0.1), + spriteSheet.createFrameData(0, 1, stepTime: 0.1), + spriteSheet.createFrameData(0, 2, stepTime: 0.3), + spriteSheet.createFrameDataFromId(4, stepTime: 0.3), + spriteSheet.createFrameDataFromId(5, stepTime: 0.5), + spriteSheet.createFrameDataFromId(6, stepTime: 0.3), + spriteSheet.createFrameDataFromId(7, stepTime: 0.1), + ]), + ); + + final spriteSize = Vector2(80.0, 90.0); + + final vampireComponent = SpriteAnimationComponent( + animation: vampireAnimation, + position: Vector2(150, 100), + size: spriteSize, + ); + + final ghostComponent = SpriteAnimationComponent( + animation: ghostAnimation, + position: Vector2(150, 220), + size: spriteSize, + ); + + final ghostAnimationVariableStepTimesComponent = SpriteAnimationComponent( + animation: ghostAnimationVariableStepTimes, + position: Vector2(250, 220), + size: spriteSize, + ); + + final customVampireComponent = SpriteAnimationComponent( + animation: customVampireAnimation, + position: Vector2(250, 100), + size: spriteSize, + ); + + add(vampireComponent); + add(ghostComponent); + add(ghostAnimationVariableStepTimesComponent); + add(customVampireComponent); + + // Some plain sprites + final vampireSpriteComponent = SpriteComponent( + sprite: spriteSheet.getSprite(0, 0), + position: Vector2(50, 100), + size: spriteSize, + ); + + final ghostSpriteComponent = SpriteComponent( + sprite: spriteSheet.getSprite(1, 0), + size: spriteSize, + position: Vector2(50, 220), + ); + + add(vampireSpriteComponent); + add(ghostSpriteComponent); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprites.dart b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprites.dart new file mode 100644 index 0000000..e97027a --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/sprites/sprites.dart @@ -0,0 +1,49 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/sprites/base64_sprite_example.dart'; +import 'package:examples/stories/sprites/basic_sprite_example.dart'; +import 'package:examples/stories/sprites/sprite_batch_example.dart'; +import 'package:examples/stories/sprites/sprite_batch_load_example.dart'; +import 'package:examples/stories/sprites/sprite_group_example.dart'; +import 'package:examples/stories/sprites/sprite_sheet_example.dart'; +import 'package:flame/game.dart'; + +void addSpritesStories(Dashbook dashbook) { + dashbook.storiesOf('Sprites') + ..add( + 'Basic Sprite', + (_) => GameWidget(game: BasicSpriteExample()), + codeLink: baseLink('sprites/basic_sprite_example.dart'), + info: BasicSpriteExample.description, + ) + ..add( + 'Base64 Sprite', + (_) => GameWidget(game: Base64SpriteExample()), + codeLink: baseLink('sprites/base64_sprite_example.dart'), + info: Base64SpriteExample.description, + ) + ..add( + 'SpriteSheet', + (_) => GameWidget(game: SpriteSheetExample()), + codeLink: baseLink('sprites/sprite_sheet_example.dart'), + info: SpriteSheetExample.description, + ) + ..add( + 'SpriteBatch', + (_) => GameWidget(game: SpriteBatchExample()), + codeLink: baseLink('sprites/sprite_batch_example.dart'), + info: SpriteBatchExample.description, + ) + ..add( + 'SpriteBatch Auto Load', + (_) => GameWidget(game: SpriteBatchLoadExample()), + codeLink: baseLink('sprites/sprite_batch_load_example.dart'), + info: SpriteBatchLoadExample.description, + ) + ..add( + 'SpriteGroup', + (_) => GameWidget(game: SpriteGroupExample()), + codeLink: baseLink('sprites/sprite_group_example.dart'), + info: SpriteGroupExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/structure/levels.dart b/flame/assets/examples/official/dashbook_example/lib/stories/structure/levels.dart new file mode 100644 index 0000000..8dd3d3f --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/structure/levels.dart @@ -0,0 +1,149 @@ +import 'package:examples/commons/ember.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; + +class LevelsExample extends FlameGame { + static const String description = ''' + In this example we showcase how you can utilize World components as levels. + Press the different buttons in the bottom to change levels and press in the + center to add new Ember's. You can see how level 1-3 keeps their state, + meanwhile the one called Resettable always resets. + '''; + + LevelsExample() : super(world: ResettableLevel()); + + late final TextComponent header; + + @override + Future onLoad() async { + header = TextComponent( + text: 'test', + position: Vector2(size.x / 2, 50), + anchor: Anchor.center, + ); + // If you have a lot of HUDs you could also create separate viewports for + // each level and then just change them from within the world's onLoad with: + // game.cameraComponent.viewport = Level1Viewport(); + final viewport = camera.viewport; + viewport.add(header); + final levels = [Level1(), Level2(), Level3()]; + viewport.addAll( + [ + LevelButton( + 'Level 1', + onPressed: () => world = levels[0], + position: Vector2(size.x / 2 - 210, size.y - 50), + ), + LevelButton( + 'Level 2', + onPressed: () => world = levels[1], + position: Vector2(size.x / 2 - 70, size.y - 50), + ), + LevelButton( + 'Level 3', + onPressed: () => world = levels[2], + position: Vector2(size.x / 2 + 70, size.y - 50), + ), + LevelButton( + 'Resettable', + onPressed: () => world = ResettableLevel(), + position: Vector2(size.x / 2 + 210, size.y - 50), + ), + ], + ); + } +} + +class ResettableLevel extends Level { + @override + Future onLoad() async { + add( + Ember() + ..add( + ScaleEffect.by( + Vector2.all(3), + EffectController(duration: 1, alternate: true, infinite: true), + ), + ), + ); + game.header.text = 'Resettable'; + } +} + +class Level1 extends Level { + @override + Future onLoad() async { + add(Ember()); + game.header.text = 'Level 1'; + } +} + +class Level2 extends Level { + @override + Future onLoad() async { + add(Ember(position: Vector2(-100, 0))); + add(Ember(position: Vector2(100, 0))); + game.header.text = 'Level 2'; + } +} + +class Level3 extends Level { + @override + Future onLoad() async { + add(Ember(position: Vector2(-100, -50))); + add(Ember(position: Vector2(100, -50))); + add(Ember(position: Vector2(0, 50))); + game.header.text = 'Level 3'; + } +} + +class Level extends World with HasGameReference, TapCallbacks { + @override + void onTapDown(TapDownEvent event) { + add(Ember(position: event.localPosition)); + } +} + +class LevelButton extends ButtonComponent { + LevelButton(String text, {super.onPressed, super.position}) + : super( + button: ButtonBackground(Colors.white), + buttonDown: ButtonBackground(Colors.orangeAccent), + children: [ + TextComponent( + text: text, + position: Vector2(60, 20), + anchor: Anchor.center, + ), + ], + size: Vector2(120, 40), + anchor: Anchor.center, + ); +} + +class ButtonBackground extends PositionComponent with HasAncestor { + ButtonBackground(Color color) { + _paint.color = color; + } + + @override + void onMount() { + super.onMount(); + size = ancestor.size; + } + + late final _background = RRect.fromRectAndRadius( + size.toRect(), + const Radius.circular(5), + ); + final _paint = Paint()..style = PaintingStyle.stroke; + + @override + void render(Canvas canvas) { + canvas.drawRRect(_background, _paint); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/structure/structure.dart b/flame/assets/examples/official/dashbook_example/lib/stories/structure/structure.dart new file mode 100644 index 0000000..e496601 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/structure/structure.dart @@ -0,0 +1,13 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/structure/levels.dart'; +import 'package:flame/game.dart'; + +void addStructureStories(Dashbook dashbook) { + dashbook.storiesOf('Structure').add( + 'Levels', + (_) => GameWidget(game: LevelsExample()), + info: LevelsExample.description, + codeLink: baseLink('structure/levels.dart'), + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/svg/svg.dart b/flame/assets/examples/official/dashbook_example/lib/stories/svg/svg.dart new file mode 100644 index 0000000..440a48b --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/svg/svg.dart @@ -0,0 +1,13 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/svg/svg_component.dart'; +import 'package:flame/game.dart'; + +void addSvgStories(Dashbook dashbook) { + dashbook.storiesOf('Svg').add( + 'Svg Component', + (_) => GameWidget(game: SvgComponentExample()), + codeLink: baseLink('svg/svg_component.dart'), + info: SvgComponentExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/svg/svg_component.dart b/flame/assets/examples/official/dashbook_example/lib/stories/svg/svg_component.dart new file mode 100644 index 0000000..fa9f62e --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/svg/svg_component.dart @@ -0,0 +1,117 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame_svg/flame_svg.dart'; + +class Player extends SvgComponent with HasGameReference { + Player() : super(priority: 3, size: Vector2(106, 146), anchor: Anchor.center); + + Vector2? destination; + + @override + Future? onLoad() async { + await super.onLoad(); + + svg = await game.loadSvg('svgs/happy_player.svg'); + } + + @override + void update(double dt) { + super.update(dt); + + if (destination != null) { + final difference = destination! - position; + if (difference.length < 2) { + destination = null; + } else { + final direction = difference.normalized(); + position += direction * 200 * dt; + } + } + } +} + +class Background extends SvgComponent + with HasGameReference { + Background() + : super( + priority: 1, + size: Vector2(745, 415), + anchor: Anchor.center, + ); + + @override + Future? onLoad() async { + await super.onLoad(); + + svg = await game.loadSvg('svgs/checkerboard.svg'); + } +} + +class Balloons extends SvgComponent with HasGameReference { + Balloons({super.position}) + : super( + priority: 2, + size: Vector2(75, 125), + anchor: Anchor.center, + ); + + @override + Future? onLoad() async { + await super.onLoad(); + + final color = Random().nextBool() ? 'red' : 'green'; + + svg = await game.loadSvg('svgs/${color}_balloons.svg'); + } +} + +class SvgComponentExample extends FlameGame { + static const description = ''' + Simple game showcasing how to use SVGs inside a flame game. This game + uses several SVGs for its graphics. Click or touch the screen to make the + player move, and double click/tap to add a new set of balloons at the + clicked position. + '''; + + SvgComponentExample() + : super( + camera: CameraComponent.withFixedResolution( + width: 400, + height: 600, + ), + world: _SvgComponentWorld(), + ); +} + +class _SvgComponentWorld extends World with TapCallbacks, DoubleTapCallbacks { + late Player player; + + @override + Future? onLoad() async { + await super.onLoad(); + + add(player = Player()); + add(Background()); + + addAll([ + Balloons(position: Vector2(-10, -20)), + Balloons(position: Vector2(-100, -150)), + Balloons(position: Vector2(-200, -140)), + Balloons(position: Vector2(100, 130)), + Balloons(position: Vector2(50, -130)), + ]); + } + + @override + void onTapUp(TapUpEvent info) { + player.destination = info.localPosition; + } + + @override + void onDoubleTapDown(DoubleTapDownEvent info) { + add(Balloons()..position = info.localPosition); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/system/overlays_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/system/overlays_example.dart new file mode 100644 index 0000000..5c620e1 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/system/overlays_example.dart @@ -0,0 +1,69 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; + +class OverlaysExample extends FlameGame with TapDetector { + static const String description = ''' + In this example we show how the overlays system can be used.\n\n + If you tap the canvas the game will start and if you tap it again it will + pause. + '''; + + @override + Future onLoad() async { + final animation = await loadSpriteAnimation( + 'animations/chopper.png', + SpriteAnimationData.sequenced( + amount: 4, + textureSize: Vector2.all(48), + stepTime: 0.15, + ), + ); + + add( + SpriteAnimationComponent( + animation: animation, + ) + ..position.y = size.y / 2 + ..position.x = 100 + ..anchor = Anchor.center + ..size = Vector2.all(100), + ); + } + + @override + void onTap() { + if (overlays.isActive('PauseMenu')) { + overlays.remove('PauseMenu'); + resumeEngine(); + } else { + overlays.add('PauseMenu'); + pauseEngine(); + } + } +} + +Widget _pauseMenuBuilder(BuildContext buildContext, OverlaysExample game) { + return Center( + child: Container( + width: 100, + height: 100, + color: Colors.orange, + child: const Center( + child: Text('Paused'), + ), + ), + ); +} + +Widget overlayBuilder(DashbookContext ctx) { + return GameWidget( + game: OverlaysExample()..paused = true, + overlayBuilderMap: const { + 'PauseMenu': _pauseMenuBuilder, + }, + initialActiveOverlays: const ['PauseMenu'], + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/system/pause_resume_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/system/pause_resume_example.dart new file mode 100644 index 0000000..f98dce0 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/system/pause_resume_example.dart @@ -0,0 +1,51 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; + +class PauseResumeExample extends FlameGame with TapDetector, DoubleTapDetector { + static const description = ''' + Demonstrate how to use the pause and resume engine methods and paused + attribute. + + Tap on the screen to toggle the execution of the engine using the + `resumeEngine` and `pauseEngine`. + + Double Tap on the screen to toggle the execution of the engine using the + `paused` attribute. + '''; + + @override + Future onLoad() async { + final animation = await loadSpriteAnimation( + 'animations/chopper.png', + SpriteAnimationData.sequenced( + amount: 4, + textureSize: Vector2.all(48), + stepTime: 0.15, + ), + ); + + add( + SpriteAnimationComponent( + animation: animation, + ) + ..position = size / 2 + ..anchor = Anchor.center + ..size = Vector2.all(100), + ); + } + + @override + void onTap() { + if (paused) { + resumeEngine(); + } else { + pauseEngine(); + } + } + + @override + void onDoubleTap() { + paused = !paused; + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/system/step_engine_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/system/step_engine_example.dart new file mode 100644 index 0000000..da6ee9d --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/system/step_engine_example.dart @@ -0,0 +1,141 @@ +import 'dart:async'; +import 'dart:math'; + +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'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class StepEngineExample extends FlameGame + with HasCollisionDetection, HasKeyboardHandlerComponents { + static const description = ''' + This example demonstrates how the game can be advanced frame by frame using + stepEngine method. + + To pause and un-pause the game anytime press the `P` key. Once paused, use + the `S` key to step by one frame. + + Up arrow and down arrow can be used to increase or decrease the step time. + '''; + + // Fixed resolution of the game. + static final Vector2 _visibleSize = Vector2(320, 180); + double _stepTimeMultiplier = 1; + static const _stepTime = 1 / 60; + + @override + Color backgroundColor() => BasicPalette.darkGreen.color; + + @override + Future onLoad() async { + final carSprite = await Sprite.load('Car.png'); + final car = SpriteComponent( + sprite: carSprite, + anchor: Anchor.center, + angle: -pi / 10, + position: Vector2(0, _visibleSize.y / 3), + children: [CircleHitbox()], + ); + + final world = World( + children: [ + ..._createCircularDetectors(), + PositionComponent(children: [car, _rotateEffect]), + ], + ); + + final cameraComponent = CameraComponent.withFixedResolution( + world: world, + width: _visibleSize.x, + height: _visibleSize.y, + hudComponents: [_controlsText], + ); + + await addAll([world, cameraComponent]); + } + + @override + KeyEventResult onKeyEvent(_, Set keysPressed) { + if (keysPressed.contains(LogicalKeyboardKey.keyP)) { + paused = !paused; + } else if (keysPressed.contains(LogicalKeyboardKey.keyS)) { + stepEngine(stepTime: _stepTime * _stepTimeMultiplier); + } else if (keysPressed.contains(LogicalKeyboardKey.arrowUp)) { + _stepTimeMultiplier += 1; + _controlsText.text = _text; + } else if (keysPressed.contains(LogicalKeyboardKey.arrowDown)) { + _stepTimeMultiplier -= 1; + _controlsText.text = _text; + } + return super.onKeyEvent(_, keysPressed); + } + + // Creates the circle detectors. + List _createCircularDetectors() { + final componentsToAdd = []; + final offsetVec = Vector2(0, -_visibleSize.y / 2.5); + for (var i = 0; i < 12; ++i) { + offsetVec.rotate(2 * pi / 12); + componentsToAdd.add( + _DetectorComponents( + radius: 5, + position: offsetVec, + anchor: Anchor.center, + children: [CircleHitbox()], + ), + ); + } + return componentsToAdd; + } + + final _rotateEffect = RotateEffect.by( + 2 * pi, + InfiniteEffectController( + SpeedEffectController( + LinearEffectController(1), + speed: 1, + ), + ), + ); + + String get _text => + 'P: Pause/Unpause\nS: Step x$_stepTimeMultiplier\nUp: Increase step\nDown: Decrease step'; + + late final _controlsText = TextBoxComponent( + text: _text, + textRenderer: TextPaint( + style: TextStyle( + color: BasicPalette.white.color, + fontSize: 20.0, + shadows: const [ + Shadow(offset: Offset(1, 1), blurRadius: 1), + ], + ), + ), + ); +} + +class _DetectorComponents extends CircleComponent with CollisionCallbacks { + _DetectorComponents({ + super.radius, + super.position, + super.anchor, + super.children, + }); + + @override + void onCollisionStart(_, __) { + paint.color = BasicPalette.black.color; + super.onCollisionStart(_, __); + } + + @override + void onCollisionEnd(__) { + paint.color = BasicPalette.white.color; + super.onCollisionEnd(__); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/system/system.dart b/flame/assets/examples/official/dashbook_example/lib/stories/system/system.dart new file mode 100644 index 0000000..7435efa --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/system/system.dart @@ -0,0 +1,35 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/system/overlays_example.dart'; +import 'package:examples/stories/system/pause_resume_example.dart'; +import 'package:examples/stories/system/step_engine_example.dart'; +import 'package:examples/stories/system/without_flame_game_example.dart'; +import 'package:flame/game.dart'; + +void addSystemStories(Dashbook dashbook) { + dashbook.storiesOf('System') + ..add( + 'Pause/resume engine', + (_) => GameWidget(game: PauseResumeExample()), + codeLink: baseLink('system/pause_resume_example.dart'), + info: PauseResumeExample.description, + ) + ..add( + 'Overlay', + overlayBuilder, + codeLink: baseLink('system/overlays_example.dart'), + info: OverlaysExample.description, + ) + ..add( + 'Without FlameGame', + (_) => GameWidget(game: NoFlameGameExample()), + codeLink: baseLink('system/without_flame_game_example.dart'), + info: NoFlameGameExample.description, + ) + ..add( + 'Step Game', + (_) => GameWidget(game: StepEngineExample()), + codeLink: baseLink('system/step_engine_game.dart'), + info: StepEngineExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/system/without_flame_game_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/system/without_flame_game_example.dart new file mode 100644 index 0000000..19686d3 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/system/without_flame_game_example.dart @@ -0,0 +1,50 @@ +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +class NoFlameGameExample extends Game with KeyboardEvents { + static const String description = ''' + This example showcases how to create a game without the FlameGame. + It also briefly showcases how to act on keyboard events. + Usage: Use W A S D to steer the rectangle. + '''; + + static final Paint white = BasicPalette.white.paint(); + static const int speed = 200; + + Rect rect = const Rect.fromLTWH(0, 100, 100, 100); + final Vector2 velocity = Vector2(0, 0); + + @override + void update(double dt) { + final displacement = velocity * (speed * dt); + rect = rect.translate(displacement.x, displacement.y); + } + + @override + void render(Canvas canvas) { + canvas.drawRect(rect, white); + } + + @override + KeyEventResult onKeyEvent( + KeyEvent event, + Set keysPressed, + ) { + final isKeyDown = event is KeyDownEvent; + + if (event.logicalKey == LogicalKeyboardKey.keyA) { + velocity.x = isKeyDown ? -1 : 0; + } else if (event.logicalKey == LogicalKeyboardKey.keyD) { + velocity.x = isKeyDown ? 1 : 0; + } else if (event.logicalKey == LogicalKeyboardKey.keyW) { + velocity.y = isKeyDown ? -1 : 0; + } else if (event.logicalKey == LogicalKeyboardKey.keyS) { + velocity.y = isKeyDown ? 1 : 0; + } + + return super.onKeyEvent(event, keysPressed); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/tiled/flame_tiled_animation_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/tiled/flame_tiled_animation_example.dart new file mode 100644 index 0000000..31c95d2 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/tiled/flame_tiled_animation_example.dart @@ -0,0 +1,16 @@ +import 'package:flame/game.dart'; +import 'package:flame_tiled/flame_tiled.dart'; + +class FlameTiledAnimationExample extends FlameGame { + static const String description = ''' + Loads and displays an animated Tiled map. + '''; + + late final TiledComponent map; + + @override + Future onLoad() async { + map = await TiledComponent.load('dungeon.tmx', Vector2.all(32)); + add(map); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/tiled/tiled.dart b/flame/assets/examples/official/dashbook_example/lib/stories/tiled/tiled.dart new file mode 100644 index 0000000..04e7321 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/tiled/tiled.dart @@ -0,0 +1,14 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/tiled/flame_tiled_animation_example.dart'; + +import 'package:flame/game.dart'; + +void addTiledStories(Dashbook dashbook) { + dashbook.storiesOf('Tiled').add( + 'Flame Tiled Animation', + (_) => GameWidget(game: FlameTiledAnimationExample()), + codeLink: baseLink('tiled/flame_tiled_animation_example.dart'), + info: FlameTiledAnimationExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/utils/timer_component_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/utils/timer_component_example.dart new file mode 100644 index 0000000..96a4b8f --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/utils/timer_component_example.dart @@ -0,0 +1,53 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; + +class TimerComponentExample extends FlameGame + with TapDetector, DoubleTapDetector { + static const String description = ''' + This examples showcases the `TimerComponent`.\n\n + Tap to start a timer that lives for one second and double tap to start + another timer that lives for 5 seconds. + '''; + + RenderedTimeComponent? tapComponent; + RenderedTimeComponent? doubleTapComponent; + + @override + void onTap() { + tapComponent?.removeFromParent(); + tapComponent = RenderedTimeComponent(1); + add(tapComponent!); + } + + @override + void onDoubleTap() { + doubleTapComponent?.removeFromParent(); + doubleTapComponent = RenderedTimeComponent(5, yOffset: 180); + add(doubleTapComponent!); + } +} + +class RenderedTimeComponent extends TimerComponent { + final TextPaint textPaint = TextPaint( + style: const TextStyle(color: Colors.white, fontSize: 20), + ); + + final double yOffset; + + RenderedTimeComponent(double period, {this.yOffset = 150}) + : super( + period: period, + removeOnFinish: true, + ); + + @override + void render(Canvas canvas) { + textPaint.render( + canvas, + 'Elapsed time: ${timer.current.toStringAsFixed(3)}', + Vector2(30, yOffset), + ); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/utils/timer_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/utils/timer_example.dart new file mode 100644 index 0000000..a3f85f9 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/utils/timer_example.dart @@ -0,0 +1,54 @@ +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/timer.dart'; +import 'package:flutter/material.dart'; + +class TimerExample extends FlameGame with TapDetector { + static const String description = ''' + This example shows how to use the `Timer`.\n\n + Tap down to start the countdown timer, it will then count to 5 and then stop + until you tap the canvas again and it restarts. + '''; + + final TextPaint textConfig = TextPaint( + style: const TextStyle(color: Colors.white, fontSize: 20), + ); + late Timer countdown; + late Timer interval; + + int elapsedSecs = 0; + + @override + Future onLoad() async { + countdown = Timer(5); + interval = Timer( + 1, + onTick: () => elapsedSecs += 1, + repeat: true, + ); + interval.start(); + } + + @override + void onTapDown(_) { + countdown.start(); + } + + @override + void update(double dt) { + super.update(dt); + countdown.update(dt); + interval.update(dt); + } + + @override + void render(Canvas canvas) { + super.render(canvas); + textConfig.render( + canvas, + 'Countdown: ${countdown.current.toStringAsPrecision(3)}', + Vector2(30, 100), + ); + textConfig.render(canvas, 'Elapsed time: $elapsedSecs', Vector2(30, 130)); + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/utils/utils.dart b/flame/assets/examples/official/dashbook_example/lib/stories/utils/utils.dart new file mode 100644 index 0000000..e1ab77f --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/utils/utils.dart @@ -0,0 +1,21 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/utils/timer_component_example.dart'; +import 'package:examples/stories/utils/timer_example.dart'; +import 'package:flame/game.dart'; + +void addUtilsStories(Dashbook dashbook) { + dashbook.storiesOf('Utils') + ..add( + 'Timer', + (_) => GameWidget(game: TimerExample()), + codeLink: baseLink('utils/timer_example.dart'), + info: TimerExample.description, + ) + ..add( + 'Timer Component', + (_) => GameWidget(game: TimerComponentExample()), + codeLink: baseLink('utils/timer_component_example.dart'), + info: TimerComponentExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/widgets/custom_painter_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/custom_painter_example.dart new file mode 100644 index 0000000..1a43353 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/custom_painter_example.dart @@ -0,0 +1,134 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flutter/material.dart'; + +class CustomPainterExample extends FlameGame with TapDetector { + static const description = ''' + Example demonstration of how to use the CustomPainterComponent. + + On the screen you can see a component using a custom painter being + rendered on a FlameGame, and if you tap, that same painter is used to + show a smiley on a widget overlay. + '''; + + @override + Future onLoad() async { + add(Player()); + } + + @override + void onTap() { + if (overlays.isActive('Smiley')) { + overlays.remove('Smiley'); + } else { + overlays.add('Smiley'); + } + } +} + +Widget customPainterBuilder(DashbookContext ctx) { + return GameWidget( + game: CustomPainterExample(), + overlayBuilderMap: { + 'Smiley': (context, game) { + return Center( + child: Container( + color: Colors.transparent, + width: 200, + height: 200, + child: Column( + children: [ + const Text( + 'Hey, I can be a widget too!', + style: TextStyle( + color: Colors.white70, + ), + ), + const SizedBox(height: 32), + SizedBox( + height: 132, + width: 132, + child: CustomPaint(painter: PlayerCustomPainter()), + ), + ], + ), + ), + ); + }, + }, + ); +} + +class PlayerCustomPainter extends CustomPainter { + late final facePaint = Paint()..color = Colors.yellow; + + late final eyesPaint = Paint()..color = Colors.black; + + @override + void paint(Canvas canvas, Size size) { + final faceRadius = size.height / 2; + + canvas.drawCircle( + Offset( + faceRadius, + faceRadius, + ), + faceRadius, + facePaint, + ); + + final eyeSize = faceRadius * 0.15; + + canvas.drawCircle( + Offset( + faceRadius - (eyeSize * 2), + faceRadius - eyeSize, + ), + eyeSize, + eyesPaint, + ); + + canvas.drawCircle( + Offset( + faceRadius + (eyeSize * 2), + faceRadius - eyeSize, + ), + eyeSize, + eyesPaint, + ); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } +} + +class Player extends CustomPainterComponent + with HasGameReference { + static const speed = 150; + + int direction = 1; + + @override + Future onLoad() async { + painter = PlayerCustomPainter(); + size = Vector2.all(100); + + y = 200; + } + + @override + void update(double dt) { + super.update(dt); + + x += speed * direction * dt; + + if ((x + width >= game.size.x && direction > 0) || + (x <= 0 && direction < 0)) { + direction *= -1; + } + } +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/widgets/nine_tile_box_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/nine_tile_box_example.dart new file mode 100644 index 0000000..bc11e29 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/nine_tile_box_example.dart @@ -0,0 +1,23 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/widgets.dart'; +import 'package:flutter/widgets.dart'; + +Widget nineTileBoxBuilder(DashbookContext ctx) { + return Container( + width: ctx.numberProperty('width', 200), + height: ctx.numberProperty('height', 200), + child: NineTileBoxWidget.asset( + path: 'nine-box.png', + tileSize: 22, + destTileSize: 50, + child: const Center( + child: Text( + 'Cool label', + style: TextStyle( + color: Color(0xFF000000), + ), + ), + ), + ), + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/widgets/partial_sprite_widget_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/partial_sprite_widget_example.dart new file mode 100644 index 0000000..7e24034 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/partial_sprite_widget_example.dart @@ -0,0 +1,22 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/widgets.dart'; +import 'package:flutter/material.dart'; + +final anchorOptions = Anchor.values.map((e) => e.name).toList(); + +Widget partialSpriteWidgetBuilder(DashbookContext ctx) { + return Container( + width: ctx.numberProperty('container width', 400), + height: ctx.numberProperty('container height', 200), + decoration: BoxDecoration(border: Border.all(color: Colors.amber)), + child: SpriteWidget.asset( + path: 'bomb_ptero.png', + srcPosition: Vector2(48, 0), + srcSize: Vector2(48, 32), + anchor: Anchor.valueOf( + ctx.listProperty('anchor', 'center', anchorOptions), + ), + ), + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/widgets/sprite_animation_widget_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/sprite_animation_widget_example.dart new file mode 100644 index 0000000..9f75213 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/sprite_animation_widget_example.dart @@ -0,0 +1,25 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/widgets.dart'; +import 'package:flutter/widgets.dart'; + +final anchorOptions = Anchor.values.map((e) => e.name).toList(); + +Widget spriteAnimationWidgetBuilder(DashbookContext ctx) { + return Container( + width: ctx.numberProperty('container width', 400), + height: ctx.numberProperty('container height', 200), + child: SpriteAnimationWidget.asset( + path: 'bomb_ptero.png', + data: SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.2, + textureSize: Vector2(48, 32), + ), + playing: ctx.boolProperty('playing', true), + anchor: Anchor.valueOf( + ctx.listProperty('anchor', 'center', anchorOptions), + ), + ), + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/widgets/sprite_button_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/sprite_button_example.dart new file mode 100644 index 0000000..b49dacd --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/sprite_button_example.dart @@ -0,0 +1,27 @@ +import 'package:dashbook/dashbook.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/widgets.dart'; +import 'package:flutter/widgets.dart'; + +Widget spriteButtonBuilder(DashbookContext ctx) { + return Container( + padding: const EdgeInsets.all(20), + child: SpriteButton.asset( + path: 'buttons.png', + pressedPath: 'buttons.png', + srcPosition: Vector2(0, 0), + srcSize: Vector2(60, 20), + pressedSrcPosition: Vector2(0, 20), + pressedSrcSize: Vector2(60, 20), + onPressed: () { + // Do something + }, + label: const Text( + 'Sprite Button', + style: TextStyle(color: Color(0xFF5D275D)), + ), + width: ctx.numberProperty('width', 250), + height: ctx.numberProperty('height', 75), + ), + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/widgets/sprite_widget_example.dart b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/sprite_widget_example.dart new file mode 100644 index 0000000..ec3ec33 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/sprite_widget_example.dart @@ -0,0 +1,22 @@ +import 'dart:math'; + +import 'package:dashbook/dashbook.dart'; +import 'package:flame/widgets.dart'; +import 'package:flutter/material.dart'; + +final anchorOptions = Anchor.values.map((e) => e.name).toList(); + +Widget spriteWidgetBuilder(DashbookContext ctx) { + return Container( + width: ctx.numberProperty('container width', 400), + height: ctx.numberProperty('container height', 200), + decoration: BoxDecoration(border: Border.all(color: Colors.amber)), + child: SpriteWidget.asset( + path: 'shield.png', + angle: pi / 180 * ctx.numberProperty('angle (deg)', 0), + anchor: Anchor.valueOf( + ctx.listProperty('anchor', 'center', anchorOptions), + ), + ), + ); +} diff --git a/flame/assets/examples/official/dashbook_example/lib/stories/widgets/widgets.dart b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/widgets.dart new file mode 100644 index 0000000..3bf7645 --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/lib/stories/widgets/widgets.dart @@ -0,0 +1,70 @@ +import 'package:dashbook/dashbook.dart'; + +import 'package:examples/commons/commons.dart'; +import 'package:examples/stories/widgets/custom_painter_example.dart'; +import 'package:examples/stories/widgets/nine_tile_box_example.dart'; +import 'package:examples/stories/widgets/partial_sprite_widget_example.dart'; +import 'package:examples/stories/widgets/sprite_animation_widget_example.dart'; +import 'package:examples/stories/widgets/sprite_button_example.dart'; +import 'package:examples/stories/widgets/sprite_widget_example.dart'; + +void addWidgetsStories(Dashbook dashbook) { + dashbook.storiesOf('Widgets') + ..decorator(CenterDecorator()) + ..add( + 'Nine Tile Box', + nineTileBoxBuilder, + codeLink: baseLink('widgets/nine_tile_box_example.dart'), + info: ''' + If you want to create a background for something that can stretch you + can use the `NineTileBox` which is showcased here, don't forget to check + out the settings on the pen icon. + ''', + ) + ..add( + 'Sprite Button', + spriteButtonBuilder, + codeLink: baseLink('widgets/sprite_button_example.dart'), + info: ''' + If you want to use sprites as a buttons within the flutter widget tree + you can create a `SpriteButton`, don't forget to check out the settings + on the pen icon. + ''', + ) + ..add( + 'Sprite Widget (full image)', + spriteWidgetBuilder, + codeLink: baseLink('widgets/sprite_widget_example.dart'), + info: ''' + If you want to use a sprite within the flutter widget tree + you can create a `SpriteWidget`, don't forget to check out the settings + on the pen icon. + ''', + ) + ..add( + 'Sprite Widget (section of image)', + partialSpriteWidgetBuilder, + codeLink: baseLink('widgets/partial_sprite_widget_example.dart'), + info: ''' + In this example we show how you can render only parts of a sprite within + a `SpriteWidget`, don't forget to check out the settings on the pen + icon. + ''', + ) + ..add( + 'Sprite Animation Widget', + spriteAnimationWidgetBuilder, + codeLink: baseLink('widgets/sprite_animation_widget_example.dart'), + info: ''' + If you want to use a sprite animation directly on the flutter widget + tree you can create a `SpriteAnimationWidget`, don't forget to check out + the settings on the pen icon. + ''', + ) + ..add( + 'CustomPainterComponent', + customPainterBuilder, + codeLink: baseLink('widgets/custom_painter_example.dart'), + info: CustomPainterExample.description, + ); +} diff --git a/flame/assets/examples/official/dashbook_example/pubspec.yaml b/flame/assets/examples/official/dashbook_example/pubspec.yaml new file mode 100644 index 0000000..6867ffa --- /dev/null +++ b/flame/assets/examples/official/dashbook_example/pubspec.yaml @@ -0,0 +1,53 @@ +name: examples +description: A set of small examples showcasing each feature provided by the Flame Engine. +homepage: https://github.com/flame-engine/flame/tree/main/examples +publish_to: "none" + +version: 0.1.0 + +environment: + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.19.0" + +dependencies: + dashbook: ^0.1.14 + flame: ^1.17.0 + flame_audio: ^2.10.1 + flame_forge2d: ^0.18.0 + flame_isolate: ^0.6.0+1 + flame_lottie: ^0.4.0+1 + flame_noise: ^0.3.0+1 + flame_spine: ^0.2.0+1 + flame_svg: ^1.10.1 + flame_tiled: ^1.20.1 + flutter: + sdk: flutter + google_fonts: ^4.0.4 + jenny: ^1.3.0 + meta: ^1.9.1 + padracing: ^1.0.0 + provider: ^6.0.5 + rogue_shooter: ^0.1.0 + trex_game: ^0.1.0 + +dev_dependencies: + flame_lint: ^1.1.2 + test: any + +flutter: + uses-material-design: true + + assets: + - assets/images/animations/ + - assets/images/ + - assets/images/tile_maps/ + - assets/images/layers/ + - assets/images/parallax/ + - assets/images/parallax/ + - assets/images/rogue_shooter/ + - assets/spine/ + - assets/svgs/ + - assets/tiles/ + - assets/audio/music/ + - assets/audio/sfx/ + - assets/yarn/ diff --git a/flame/assets/examples/official/padracing/LICENSE b/flame/assets/examples/official/padracing/LICENSE new file mode 100644 index 0000000..86b0460 --- /dev/null +++ b/flame/assets/examples/official/padracing/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 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. diff --git a/flame/assets/examples/official/padracing/README.md b/flame/assets/examples/official/padracing/README.md new file mode 100644 index 0000000..2a8cd5d --- /dev/null +++ b/flame/assets/examples/official/padracing/README.md @@ -0,0 +1,12 @@ +# Padracing + +Padracing is a small racing game to showcase the possibility of running +Flame and Forge2D in DartPad. + +In this game I took on the challenge to build a game completely without +assets, since you can't have assets in DartPad (yet). +I could of course have pulled in network assets, but decided that the +challenge of not having any assets at all made it more fun. + +Created by: [Lukas Klingsbo](https://twitter.com/spyd0n) +([spydon](https://github.com/spydon)) diff --git a/flame/assets/examples/official/padracing/lib/ball.dart b/flame/assets/examples/official/padracing/lib/ball.dart new file mode 100644 index 0000000..5655f70 --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/ball.dart @@ -0,0 +1,101 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/extensions.dart'; +import 'package:flame/palette.dart'; +import 'package:flame_forge2d/flame_forge2d.dart' hide Particle, World; +import 'package:padracing/car.dart'; +import 'package:padracing/game_colors.dart'; +import 'package:padracing/padracing_game.dart'; +import 'package:padracing/wall.dart'; + +class Ball extends BodyComponent with ContactCallbacks { + final double radius; + final Vector2 initialPosition; + final double rotation; + final bool isMovable; + final rng = Random(); + late final Paint _shaderPaint; + + Ball({ + required this.initialPosition, + this.radius = 80.0, + this.rotation = 1.0, + this.isMovable = true, + }) : super(priority: 3); + + @override + Future onLoad() async { + await super.onLoad(); + renderBody = false; + _shaderPaint = GameColors.green.paint + ..shader = Gradient.radial( + Offset.zero, + radius, + [ + GameColors.green.color, + BasicPalette.black.color, + ], + null, + TileMode.clamp, + null, + Offset(radius / 2, radius / 2), + ); + } + + @override + Body createBody() { + final def = BodyDef() + ..userData = this + ..type = isMovable ? BodyType.dynamic : BodyType.kinematic + ..position = initialPosition; + final body = world.createBody(def)..angularVelocity = rotation; + + final shape = CircleShape()..radius = radius; + final fixtureDef = FixtureDef(shape) + ..restitution = 0.5 + ..friction = 0.5; + return body..createFixture(fixtureDef); + } + + @override + void render(Canvas canvas) { + canvas.drawCircle(Offset.zero, radius, _shaderPaint); + } + + @override + void beginContact(Object other, Contact contact) { + if (isMovable && other is Car) { + final carBody = other.body; + carBody.applyAngularImpulse(3 * carBody.mass * 100); + } + } + + late Rect asRect = Rect.fromCircle( + center: initialPosition.toOffset(), + radius: radius, + ); +} + +List createBalls(Vector2 trackSize, List walls, Ball bigBall) { + final balls = []; + final rng = Random(); + while (balls.length < 20) { + final ball = Ball( + initialPosition: Vector2.random(rng)..multiply(trackSize), + radius: 3.0 + rng.nextInt(5), + rotation: (rng.nextBool() ? 1 : -1) * rng.nextInt(5).toDouble(), + ); + final touchesBall = + ball.initialPosition.distanceTo(bigBall.initialPosition) < + ball.radius + bigBall.radius; + if (!touchesBall) { + final touchesWall = + walls.any((wall) => wall.asRect.overlaps(ball.asRect)); + if (!touchesWall) { + balls.add(ball); + } + } + } + return balls; +} diff --git a/flame/assets/examples/official/padracing/lib/car.dart b/flame/assets/examples/official/padracing/lib/car.dart new file mode 100644 index 0000000..322d9b9 --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/car.dart @@ -0,0 +1,138 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame_forge2d/flame_forge2d.dart' hide Particle, World; +import 'package:flutter/material.dart' hide Image, Gradient; +import 'package:padracing/game_colors.dart'; +import 'package:padracing/lap_line.dart'; +import 'package:padracing/padracing_game.dart'; +import 'package:padracing/tire.dart'; + +class Car extends BodyComponent { + Car({required this.playerNumber, required this.cameraComponent}) + : super( + priority: 3, + paint: Paint()..color = colors[playerNumber], + ); + + static final colors = [ + GameColors.green.color, + GameColors.blue.color, + ]; + + late final List tires; + final ValueNotifier lapNotifier = ValueNotifier(1); + final int playerNumber; + final Set passedStartControl = {}; + final CameraComponent cameraComponent; + late final Image _image; + final size = const Size(6, 10); + final scale = 10.0; + late final _renderPosition = -size.toOffset() / 2; + late final _scaledRect = (size * scale).toRect(); + late final _renderRect = _renderPosition & size; + + final vertices = [ + Vector2(1.5, -5.0), + Vector2(3.0, -2.5), + Vector2(2.8, 0.5), + Vector2(1.0, 5.0), + Vector2(-1.0, 5.0), + Vector2(-2.8, 0.5), + Vector2(-3.0, -2.5), + Vector2(-1.5, -5.0), + ]; + + @override + Future onLoad() async { + await super.onLoad(); + + final recorder = PictureRecorder(); + final canvas = Canvas(recorder, _scaledRect); + final path = Path(); + final bodyPaint = Paint()..color = paint.color; + for (var i = 0.0; i < _scaledRect.width / 4; i++) { + bodyPaint.color = bodyPaint.color.darken(0.1); + path.reset(); + final offsetVertices = vertices + .map( + (v) => + v.toOffset() * scale - + Offset(i * v.x.sign, i * v.y.sign) + + _scaledRect.bottomRight / 2, + ) + .toList(); + path.addPolygon(offsetVertices, true); + canvas.drawPath(path, bodyPaint); + } + final picture = recorder.endRecording(); + _image = await picture.toImage( + _scaledRect.width.toInt(), + _scaledRect.height.toInt(), + ); + } + + @override + Body createBody() { + final startPosition = + Vector2(20, 30) + Vector2(15, 0) * playerNumber.toDouble(); + final def = BodyDef() + ..type = BodyType.dynamic + ..position = startPosition; + final body = world.createBody(def) + ..userData = this + ..angularDamping = 3.0; + + final shape = PolygonShape()..set(vertices); + final fixtureDef = FixtureDef(shape) + ..density = 0.2 + ..restitution = 2.0; + body.createFixture(fixtureDef); + + final jointDef = RevoluteJointDef() + ..bodyA = body + ..enableLimit = true + ..lowerAngle = 0.0 + ..upperAngle = 0.0 + ..localAnchorB.setZero(); + + tires = List.generate(4, (i) { + final isFrontTire = i <= 1; + final isLeftTire = i.isEven; + return Tire( + car: this, + pressedKeys: game.pressedKeySets[playerNumber], + isFrontTire: isFrontTire, + isLeftTire: isLeftTire, + jointDef: jointDef, + isTurnableTire: isFrontTire, + ); + }); + + game.world.addAll(tires); + return body; + } + + @override + void update(double dt) { + cameraComponent.viewfinder.position = body.position; + } + + @override + void render(Canvas canvas) { + canvas.drawImageRect( + _image, + _scaledRect, + _renderRect, + paint, + ); + } + + @override + void onRemove() { + for (final tire in tires) { + tire.removeFromParent(); + } + } +} diff --git a/flame/assets/examples/official/padracing/lib/game_colors.dart b/flame/assets/examples/official/padracing/lib/game_colors.dart new file mode 100644 index 0000000..fbd2d91 --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/game_colors.dart @@ -0,0 +1,18 @@ +import 'package:flame/extensions.dart'; +import 'package:flutter/material.dart' hide Image, Gradient; + +enum GameColors { + green, + blue, +} + +extension GameColorExtension on GameColors { + Color get color { + return switch (this) { + GameColors.green => ColorExtension.fromRGBHexString('#14F596'), + GameColors.blue => ColorExtension.fromRGBHexString('#81DDF9'), + }; + } + + Paint get paint => Paint()..color = color; +} diff --git a/flame/assets/examples/official/padracing/lib/game_over.dart b/flame/assets/examples/official/padracing/lib/game_over.dart new file mode 100644 index 0000000..8642a20 --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/game_over.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart' hide Image, Gradient; +import 'package:padracing/menu_card.dart'; +import 'package:padracing/padracing_game.dart'; + +class GameOver extends StatelessWidget { + const GameOver(this.game, {super.key}); + + final PadRacingGame game; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Material( + color: Colors.transparent, + child: Center( + child: Wrap( + children: [ + MenuCard( + children: [ + Text( + 'Player ${game.winner!.playerNumber + 1} wins!', + style: textTheme.displayLarge, + ), + const SizedBox(height: 10), + Text( + 'Time: ${game.timePassed}', + style: textTheme.bodyLarge, + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: game.reset, + child: const Text('Restart'), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/flame/assets/examples/official/padracing/lib/lap_line.dart b/flame/assets/examples/official/padracing/lib/lap_line.dart new file mode 100644 index 0000000..2afc1e9 --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/lap_line.dart @@ -0,0 +1,102 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/extensions.dart'; +import 'package:flame/palette.dart'; +import 'package:flame_forge2d/flame_forge2d.dart' hide Particle, World; +import 'package:flutter/material.dart' hide Image, Gradient; + +import 'package:padracing/car.dart'; +import 'package:padracing/game_colors.dart'; + +class LapLine extends BodyComponent with ContactCallbacks { + LapLine(this.id, this.initialPosition, this.size, {required this.isFinish}) + : super(priority: 1); + + final int id; + final bool isFinish; + final Vector2 initialPosition; + final Vector2 size; + late final Rect rect = size.toRect(); + Image? _finishOverlay; + + @override + Future onLoad() async { + super.onLoad(); + if (isFinish) { + _finishOverlay = await createFinishOverlay(); + } + } + + @override + Body createBody() { + paint.color = (isFinish ? GameColors.green.color : GameColors.green.color) + ..withOpacity(0.5); + paint + ..style = PaintingStyle.fill + ..shader = Gradient.radial( + (size / 2).toOffset(), + max(size.x, size.y), + [ + paint.color, + Colors.black, + ], + ); + + final groundBody = world.createBody( + BodyDef( + position: initialPosition, + userData: this, + ), + ); + final shape = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2); + final fixtureDef = FixtureDef(shape, isSensor: true); + return groundBody..createFixture(fixtureDef); + } + + late final Rect _scaledRect = (size * 10).toRect(); + late final Rect _drawRect = size.toRect(); + + Future createFinishOverlay() { + final recorder = PictureRecorder(); + final canvas = Canvas(recorder, _scaledRect); + final step = _scaledRect.width / 2; + final black = BasicPalette.black.paint(); + + for (var i = 0; i * step < _scaledRect.height; i++) { + canvas.drawRect( + Rect.fromLTWH(i.isEven ? 0 : step, i * step, step, step), + black, + ); + } + final picture = recorder.endRecording(); + return picture.toImage( + _scaledRect.width.toInt(), + _scaledRect.height.toInt(), + ); + } + + @override + void render(Canvas canvas) { + canvas.translate(-size.x / 2, -size.y / 2); + canvas.drawRect(rect, paint); + if (_finishOverlay != null) { + canvas.drawImageRect(_finishOverlay!, _scaledRect, _drawRect, paint); + } + } + + @override + void beginContact(Object other, Contact contact) { + if (other is! Car) { + return; + } + if (isFinish && other.passedStartControl.length == 2) { + other.lapNotifier.value++; + other.passedStartControl.clear(); + } else if (!isFinish) { + other.passedStartControl + .removeWhere((passedControl) => passedControl.id > id); + other.passedStartControl.add(this); + } + } +} diff --git a/flame/assets/examples/official/padracing/lib/lap_text.dart b/flame/assets/examples/official/padracing/lib/lap_text.dart new file mode 100644 index 0000000..fbaea56 --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/lap_text.dart @@ -0,0 +1,85 @@ +import 'package:flame/components.dart'; +import 'package:flame/extensions.dart'; +import 'package:flutter/material.dart' hide Image, Gradient; +import 'package:google_fonts/google_fonts.dart'; + +import 'package:padracing/car.dart'; +import 'package:padracing/padracing_game.dart'; + +class LapText extends PositionComponent with HasGameReference { + LapText({required this.car, required Vector2 position}) + : super(position: position); + + final Car car; + late final ValueNotifier lapNotifier = car.lapNotifier; + late final TextComponent _timePassedComponent; + + @override + Future onLoad() async { + await super.onLoad(); + final textStyle = GoogleFonts.vt323( + fontSize: 35, + color: car.paint.color, + ); + final defaultRenderer = TextPaint(style: textStyle); + final lapCountRenderer = TextPaint( + style: textStyle.copyWith(fontSize: 55, fontWeight: FontWeight.bold), + ); + add( + TextComponent( + text: 'Lap', + position: Vector2(0, -20), + anchor: Anchor.center, + textRenderer: defaultRenderer, + ), + ); + final lapCounter = TextComponent( + position: Vector2(0, 10), + anchor: Anchor.center, + textRenderer: lapCountRenderer, + ); + add(lapCounter); + void updateLapText() { + if (lapNotifier.value <= PadRacingGame.numberOfLaps) { + final prefix = lapNotifier.value < 10 ? '0' : ''; + lapCounter.text = '$prefix${lapNotifier.value}'; + } else { + lapCounter.text = 'DONE'; + } + } + + _timePassedComponent = TextComponent( + position: Vector2(0, 70), + anchor: Anchor.center, + textRenderer: defaultRenderer, + ); + add(_timePassedComponent); + + _backgroundPaint = Paint() + ..color = car.paint.color + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + lapNotifier.addListener(updateLapText); + updateLapText(); + } + + @override + void update(double dt) { + if (game.isGameOver) { + return; + } + _timePassedComponent.text = game.timePassed; + } + + final _backgroundRect = RRect.fromRectAndRadius( + Rect.fromCircle(center: Offset.zero, radius: 50), + const Radius.circular(10), + ); + late final Paint _backgroundPaint; + + @override + void render(Canvas canvas) { + canvas.drawRRect(_backgroundRect, _backgroundPaint); + } +} diff --git a/flame/assets/examples/official/padracing/lib/main.dart b/flame/assets/examples/official/padracing/lib/main.dart new file mode 100644 index 0000000..67b7171 --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/main.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart' hide Image, Gradient; +import 'package:padracing/padracing_widget.dart'; + +void main() { + runApp( + const PadracingWidget(), + ); +} diff --git a/flame/assets/examples/official/padracing/lib/menu.dart b/flame/assets/examples/official/padracing/lib/menu.dart new file mode 100644 index 0000000..0212304 --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/menu.dart @@ -0,0 +1,91 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart' hide Image, Gradient; +import 'package:padracing/game_colors.dart'; +import 'package:padracing/menu_card.dart'; +import 'package:padracing/padracing_game.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class Menu extends StatelessWidget { + const Menu(this.game, {super.key}); + + final PadRacingGame game; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Material( + color: Colors.transparent, + child: Center( + child: Wrap( + children: [ + Column( + children: [ + MenuCard( + children: [ + Text( + 'PadRacing', + style: textTheme.displayLarge, + ), + Text( + 'First to 3 laps win', + style: textTheme.bodyLarge, + ), + const SizedBox(height: 10), + ElevatedButton( + child: const Text('1 Player'), + onPressed: () { + game.prepareStart(numberOfPlayers: 1); + }, + ), + Text( + 'Arrow keys', + style: textTheme.bodyMedium, + ), + const SizedBox(height: 10), + ElevatedButton( + child: const Text('2 Players'), + onPressed: () { + game.prepareStart(numberOfPlayers: 2); + }, + ), + Text( + 'WASD', + style: textTheme.bodyMedium, + ), + ], + ), + MenuCard( + children: [ + RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'Made by ', + style: textTheme.bodyMedium, + ), + TextSpan( + text: 'Lukas Klingsbo (spydon)', + style: textTheme.bodyMedium?.copyWith( + color: GameColors.green.color, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl( + Uri.parse('https://github.com/spydon'), + ); + }, + ), + ], + ), + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/flame/assets/examples/official/padracing/lib/menu_card.dart b/flame/assets/examples/official/padracing/lib/menu_card.dart new file mode 100644 index 0000000..1d8aaf7 --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/menu_card.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart' hide Image, Gradient; + +import 'package:padracing/game_colors.dart'; + +class MenuCard extends StatelessWidget { + const MenuCard({required this.children, super.key}); + + final List children; + + @override + Widget build(BuildContext context) { + return Card( + color: Colors.black, + shadowColor: GameColors.green.color, + elevation: 10, + margin: const EdgeInsets.only(bottom: 20), + child: Container( + margin: const EdgeInsets.all(20), + child: Column( + children: children, + ), + ), + ); + } +} diff --git a/flame/assets/examples/official/padracing/lib/padracing_game.dart b/flame/assets/examples/official/padracing/lib/padracing_game.dart new file mode 100644 index 0000000..9ed5405 --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/padracing_game.dart @@ -0,0 +1,264 @@ +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:flame/camera.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/input.dart'; +import 'package:flame_forge2d/flame_forge2d.dart' hide Particle, World; +import 'package:flutter/material.dart' hide Image, Gradient; +import 'package:flutter/services.dart'; +import 'package:padracing/ball.dart'; +import 'package:padracing/car.dart'; +import 'package:padracing/game_colors.dart'; +import 'package:padracing/lap_line.dart'; +import 'package:padracing/lap_text.dart'; +import 'package:padracing/wall.dart'; + +final List> playersKeys = [ + { + LogicalKeyboardKey.arrowUp: LogicalKeyboardKey.arrowUp, + LogicalKeyboardKey.arrowDown: LogicalKeyboardKey.arrowDown, + LogicalKeyboardKey.arrowLeft: LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.arrowRight: LogicalKeyboardKey.arrowRight, + }, + { + LogicalKeyboardKey.keyW: LogicalKeyboardKey.arrowUp, + LogicalKeyboardKey.keyS: LogicalKeyboardKey.arrowDown, + LogicalKeyboardKey.keyA: LogicalKeyboardKey.arrowLeft, + LogicalKeyboardKey.keyD: LogicalKeyboardKey.arrowRight, + }, +]; + +class PadRacingGame extends Forge2DGame with KeyboardEvents { + static const String description = ''' + This is an example game that uses Forge2D to handle the physics. + In this game you should finish 3 laps in as little time as possible, it can + be played as single player or with two players (on the same keyboard). + Watch out for the balls, they make your car spin. + '''; + + PadRacingGame() : super(gravity: Vector2.zero(), zoom: 1); + + @override + Color backgroundColor() => Colors.black; + + static final Vector2 trackSize = Vector2.all(500); + static const double playZoom = 8.0; + static const int numberOfLaps = 3; + late CameraComponent startCamera; + late List> activeKeyMaps; + late List> pressedKeySets; + final cars = []; + bool isGameOver = true; + Car? winner; + double _timePassed = 0; + + @override + Future onLoad() async { + super.onLoad(); + camera.removeFromParent(); + children.register(); + + final walls = createWalls(trackSize); + final bigBall = Ball(initialPosition: Vector2(200, 245), isMovable: false); + world.addAll([ + LapLine(1, Vector2(25, 50), Vector2(50, 5), isFinish: false), + LapLine(2, Vector2(25, 70), Vector2(50, 5), isFinish: false), + LapLine(3, Vector2(52.5, 25), Vector2(5, 50), isFinish: true), + bigBall, + ...walls, + ...createBalls(trackSize, walls, bigBall), + ]); + + openMenu(); + } + + void openMenu() { + overlays.add('menu'); + final zoomLevel = min( + canvasSize.x / trackSize.x, + canvasSize.y / trackSize.y, + ); + startCamera = CameraComponent(world: world) + ..viewfinder.position = trackSize / 2 + ..viewfinder.anchor = Anchor.center + ..viewfinder.zoom = zoomLevel - 0.2; + add(startCamera); + } + + void prepareStart({required int numberOfPlayers}) { + startCamera.viewfinder + ..add( + ScaleEffect.to( + Vector2.all(playZoom), + EffectController(duration: 1.0), + onComplete: () => start(numberOfPlayers: numberOfPlayers), + ), + ) + ..add( + MoveEffect.to( + Vector2.all(20), + EffectController(duration: 1.0), + ), + ); + } + + void start({required int numberOfPlayers}) { + isGameOver = false; + overlays.remove('menu'); + startCamera.removeFromParent(); + final isHorizontal = canvasSize.x > canvasSize.y; + Vector2 alignedVector({ + required double longMultiplier, + double shortMultiplier = 1.0, + }) { + return Vector2( + isHorizontal + ? canvasSize.x * longMultiplier + : canvasSize.x * shortMultiplier, + !isHorizontal + ? canvasSize.y * longMultiplier + : canvasSize.y * shortMultiplier, + ); + } + + final viewportSize = alignedVector(longMultiplier: 1 / numberOfPlayers); + + RectangleComponent viewportRimGenerator() => + RectangleComponent(size: viewportSize, anchor: Anchor.topLeft) + ..paint.color = GameColors.blue.color + ..paint.strokeWidth = 2.0 + ..paint.style = PaintingStyle.stroke; + final cameras = List.generate(numberOfPlayers, (i) { + return CameraComponent( + world: world, + viewport: FixedSizeViewport(viewportSize.x, viewportSize.y) + ..position = alignedVector( + longMultiplier: i == 0 ? 0.0 : 1 / (i + 1), + shortMultiplier: 0.0, + ) + ..add(viewportRimGenerator()), + ) + ..viewfinder.anchor = Anchor.center + ..viewfinder.zoom = playZoom; + }); + + final mapCameraSize = Vector2.all(500); + const mapCameraZoom = 0.5; + final mapCameras = List.generate(numberOfPlayers, (i) { + return CameraComponent( + world: world, + viewport: FixedSizeViewport(mapCameraSize.x, mapCameraSize.y) + ..position = Vector2( + viewportSize.x - mapCameraSize.x * mapCameraZoom - 50, + 50, + ), + ) + ..viewfinder.anchor = Anchor.topLeft + ..viewfinder.zoom = mapCameraZoom; + }); + addAll(cameras); + + for (var i = 0; i < numberOfPlayers; i++) { + final car = Car(playerNumber: i, cameraComponent: cameras[i]); + final lapText = LapText( + car: car, + position: Vector2.all(100), + ); + + car.lapNotifier.addListener(() { + if (car.lapNotifier.value > numberOfLaps) { + isGameOver = true; + winner = car; + overlays.add('game_over'); + lapText.addAll([ + ScaleEffect.by( + Vector2.all(1.5), + EffectController(duration: 0.2, alternate: true, repeatCount: 3), + ), + RotateEffect.by(pi * 2, EffectController(duration: 0.5)), + ]); + } else { + lapText.add( + ScaleEffect.by( + Vector2.all(1.5), + EffectController(duration: 0.2, alternate: true), + ), + ); + } + }); + cars.add(car); + world.add(car); + cameras[i].viewport.addAll([lapText, mapCameras[i]]); + } + + pressedKeySets = List.generate(numberOfPlayers, (_) => {}); + activeKeyMaps = List.generate(numberOfPlayers, (i) => playersKeys[i]); + } + + @override + void update(double dt) { + super.update(dt); + if (isGameOver) { + return; + } + _timePassed += dt; + } + + @override + KeyEventResult onKeyEvent( + KeyEvent event, + Set keysPressed, + ) { + super.onKeyEvent(event, keysPressed); + if (!isLoaded || isGameOver) { + return KeyEventResult.ignored; + } + + _clearPressedKeys(); + for (final key in keysPressed) { + activeKeyMaps.forEachIndexed((i, keyMap) { + if (keyMap.containsKey(key)) { + pressedKeySets[i].add(keyMap[key]!); + } + }); + } + return KeyEventResult.handled; + } + + void _clearPressedKeys() { + for (final pressedKeySet in pressedKeySets) { + pressedKeySet.clear(); + } + } + + void reset() { + _clearPressedKeys(); + activeKeyMaps.clear(); + _timePassed = 0; + overlays.remove('game_over'); + openMenu(); + for (final car in cars) { + car.removeFromParent(); + } + for (final camera in children.query()) { + camera.removeFromParent(); + } + } + + String _maybePrefixZero(int number) { + if (number < 10) { + return '0$number'; + } + return number.toString(); + } + + String get timePassed { + final minutes = _maybePrefixZero((_timePassed / 60).floor()); + final seconds = _maybePrefixZero((_timePassed % 60).floor()); + final ms = _maybePrefixZero(((_timePassed % 1) * 100).floor()); + return [minutes, seconds, ms].join(':'); + } +} diff --git a/flame/assets/examples/official/padracing/lib/padracing_widget.dart b/flame/assets/examples/official/padracing/lib/padracing_widget.dart new file mode 100644 index 0000000..7c7948f --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/padracing_widget.dart @@ -0,0 +1,74 @@ +import 'package:flame/game.dart'; +import 'package:flutter/material.dart' hide Image, Gradient; +import 'package:google_fonts/google_fonts.dart'; + +import 'package:padracing/game_over.dart'; +import 'package:padracing/menu.dart'; +import 'package:padracing/padracing_game.dart'; + +class PadracingWidget extends StatelessWidget { + const PadracingWidget({super.key}); + + @override + Widget build(BuildContext context) { + final theme = ThemeData( + textTheme: TextTheme( + displayLarge: GoogleFonts.vt323( + fontSize: 35, + color: Colors.white, + ), + labelLarge: GoogleFonts.vt323( + fontSize: 30, + fontWeight: FontWeight.w500, + ), + bodyLarge: GoogleFonts.vt323( + fontSize: 28, + color: Colors.grey, + ), + bodyMedium: GoogleFonts.vt323( + fontSize: 18, + color: Colors.grey, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + minimumSize: const Size(150, 50), + ), + ), + inputDecorationTheme: InputDecorationTheme( + hoverColor: Colors.red.shade700, + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white), + ), + border: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white), + ), + errorBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Colors.red.shade700, + ), + ), + ), + ); + + return MaterialApp( + title: 'PadRacing', + home: GameWidget( + game: PadRacingGame(), + loadingBuilder: (context) => Center( + child: Text( + 'Loading...', + style: Theme.of(context).textTheme.displayLarge, + ), + ), + overlayBuilderMap: { + 'menu': (_, game) => Menu(game), + 'game_over': (_, game) => GameOver(game), + }, + initialActiveOverlays: const ['menu'], + ), + theme: theme, + ); + } +} diff --git a/flame/assets/examples/official/padracing/lib/tire.dart b/flame/assets/examples/official/padracing/lib/tire.dart new file mode 100644 index 0000000..0309f7f --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/tire.dart @@ -0,0 +1,193 @@ +import 'package:flame/palette.dart'; +import 'package:flame_forge2d/flame_forge2d.dart' hide Particle, World; +import 'package:flutter/material.dart' hide Image, Gradient; +import 'package:flutter/services.dart'; +import 'package:padracing/car.dart'; +import 'package:padracing/padracing_game.dart'; +import 'package:padracing/trail.dart'; + +class Tire extends BodyComponent { + Tire({ + required this.car, + required this.pressedKeys, + required this.isFrontTire, + required this.isLeftTire, + required this.jointDef, + this.isTurnableTire = false, + }) : super( + paint: Paint() + ..color = car.paint.color + ..strokeWidth = 0.2 + ..style = PaintingStyle.stroke, + priority: 2, + ); + + static const double _backTireMaxDriveForce = 300.0; + static const double _frontTireMaxDriveForce = 600.0; + static const double _backTireMaxLateralImpulse = 8.5; + static const double _frontTireMaxLateralImpulse = 7.5; + + final Car car; + final size = Vector2(0.5, 1.25); + late final RRect _renderRect = RRect.fromLTRBR( + -size.x, + -size.y, + size.x, + size.y, + const Radius.circular(0.3), + ); + + final Set pressedKeys; + + late final double _maxDriveForce = + isFrontTire ? _frontTireMaxDriveForce : _backTireMaxDriveForce; + late final double _maxLateralImpulse = + isFrontTire ? _frontTireMaxLateralImpulse : _backTireMaxLateralImpulse; + + // Make mutable if ice or something should be implemented + final double _currentTraction = 1.0; + + final double _maxForwardSpeed = 250.0; + final double _maxBackwardSpeed = -40.0; + + final RevoluteJointDef jointDef; + late final RevoluteJoint joint; + final bool isTurnableTire; + final bool isFrontTire; + final bool isLeftTire; + + final double _lockAngle = 0.6; + final double _turnSpeedPerSecond = 4; + + final Paint _black = BasicPalette.black.paint(); + + @override + Future onLoad() async { + await super.onLoad(); + game.world.add(Trail(car: car, tire: this)); + } + + @override + Body createBody() { + final jointAnchor = Vector2( + isLeftTire ? -3.0 : 3.0, + isFrontTire ? 3.5 : -4.25, + ); + + final def = BodyDef() + ..type = BodyType.dynamic + ..position = car.body.position + jointAnchor; + final body = world.createBody(def)..userData = this; + + final polygonShape = PolygonShape()..setAsBoxXY(0.5, 1.25); + body.createFixtureFromShape(polygonShape).userData = this; + + jointDef.bodyB = body; + jointDef.localAnchorA.setFrom(jointAnchor); + world.createJoint(joint = RevoluteJoint(jointDef)); + joint.setLimits(0, 0); + return body; + } + + @override + void update(double dt) { + if (body.isAwake || pressedKeys.isNotEmpty) { + _updateTurn(dt); + _updateFriction(); + if (!game.isGameOver) { + _updateDrive(); + } + } + } + + @override + void render(Canvas canvas) { + canvas.drawRRect(_renderRect, _black); + canvas.drawRRect(_renderRect, paint); + } + + void _updateFriction() { + final impulse = _lateralVelocity + ..scale(-body.mass) + ..clampScalar(-_maxLateralImpulse, _maxLateralImpulse) + ..scale(_currentTraction); + body.applyLinearImpulse(impulse); + body.applyAngularImpulse( + 0.1 * _currentTraction * body.getInertia() * -body.angularVelocity, + ); + + final currentForwardNormal = _forwardVelocity; + final currentForwardSpeed = currentForwardNormal.length; + currentForwardNormal.normalize(); + final dragForceMagnitude = -2 * currentForwardSpeed; + body.applyForce( + currentForwardNormal..scale(_currentTraction * dragForceMagnitude), + ); + } + + void _updateDrive() { + var desiredSpeed = 0.0; + if (pressedKeys.contains(LogicalKeyboardKey.arrowUp)) { + desiredSpeed = _maxForwardSpeed; + } + if (pressedKeys.contains(LogicalKeyboardKey.arrowDown)) { + desiredSpeed += _maxBackwardSpeed; + } + + final currentForwardNormal = body.worldVector(Vector2(0.0, 1.0)); + final currentSpeed = _forwardVelocity.dot(currentForwardNormal); + var force = 0.0; + if (desiredSpeed < currentSpeed) { + force = -_maxDriveForce; + } else if (desiredSpeed > currentSpeed) { + force = _maxDriveForce; + } + + if (force.abs() > 0) { + body.applyForce(currentForwardNormal..scale(_currentTraction * force)); + } + } + + void _updateTurn(double dt) { + var desiredAngle = 0.0; + var desiredTorque = 0.0; + var isTurning = false; + if (pressedKeys.contains(LogicalKeyboardKey.arrowLeft)) { + desiredTorque = -15.0; + desiredAngle = -_lockAngle; + isTurning = true; + } + if (pressedKeys.contains(LogicalKeyboardKey.arrowRight)) { + desiredTorque += 15.0; + desiredAngle += _lockAngle; + isTurning = true; + } + if (isTurnableTire && isTurning) { + final turnPerTimeStep = _turnSpeedPerSecond * dt; + final angleNow = joint.jointAngle(); + final angleToTurn = + (desiredAngle - angleNow).clamp(-turnPerTimeStep, turnPerTimeStep); + final angle = angleNow + angleToTurn; + joint.setLimits(angle, angle); + } else { + joint.setLimits(0, 0); + } + body.applyTorque(desiredTorque); + } + + // Cached Vectors to reduce unnecessary object creation. + final Vector2 _worldLeft = Vector2(1.0, 0.0); + final Vector2 _worldUp = Vector2(0.0, -1.0); + + Vector2 get _lateralVelocity { + final currentRightNormal = body.worldVector(_worldLeft); + return currentRightNormal + ..scale(currentRightNormal.dot(body.linearVelocity)); + } + + Vector2 get _forwardVelocity { + final currentForwardNormal = body.worldVector(_worldUp); + return currentForwardNormal + ..scale(currentForwardNormal.dot(body.linearVelocity)); + } +} diff --git a/flame/assets/examples/official/padracing/lib/trail.dart b/flame/assets/examples/official/padracing/lib/trail.dart new file mode 100644 index 0000000..06133da --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/trail.dart @@ -0,0 +1,44 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; + +import 'package:padracing/car.dart'; +import 'package:padracing/tire.dart'; + +class Trail extends Component with HasPaint { + Trail({ + required this.car, + required this.tire, + }) : super(priority: 1); + + final Car car; + final Tire tire; + + final trail = []; + final _trailLength = 30; + + @override + Future onLoad() async { + paint + ..color = (tire.paint.color.withOpacity(0.9)) + ..strokeWidth = 1.0; + } + + @override + void update(double dt) { + if (tire.body.linearVelocity.length2 > 100) { + if (trail.length > _trailLength) { + trail.removeAt(0); + } + final trailPoint = tire.body.position.toOffset(); + trail.add(trailPoint); + } else if (trail.isNotEmpty) { + trail.removeAt(0); + } + } + + @override + void render(Canvas canvas) { + canvas.drawPoints(PointMode.polygon, trail, paint); + } +} diff --git a/flame/assets/examples/official/padracing/lib/wall.dart b/flame/assets/examples/official/padracing/lib/wall.dart new file mode 100644 index 0000000..454d220 --- /dev/null +++ b/flame/assets/examples/official/padracing/lib/wall.dart @@ -0,0 +1,112 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/extensions.dart'; +import 'package:flame/palette.dart'; +import 'package:flame_forge2d/flame_forge2d.dart' hide Particle, World; + +import 'package:padracing/padracing_game.dart'; + +List createWalls(Vector2 size) { + final topCenter = Vector2(size.x / 2, 0); + final bottomCenter = Vector2(size.x / 2, size.y); + final leftCenter = Vector2(0, size.y / 2); + final rightCenter = Vector2(size.x, size.y / 2); + + final filledSize = size.clone() + Vector2.all(5); + return [ + Wall(topCenter, Vector2(filledSize.x, 5)), + Wall(leftCenter, Vector2(5, filledSize.y)), + Wall(Vector2(52.5, 240), Vector2(5, 380)), + Wall(Vector2(200, 50), Vector2(300, 5)), + Wall(Vector2(72.5, 300), Vector2(5, 400)), + Wall(Vector2(180, 100), Vector2(220, 5)), + Wall(Vector2(350, 105), Vector2(5, 115)), + Wall(Vector2(310, 160), Vector2(240, 5)), + Wall(Vector2(211.5, 400), Vector2(283, 5)), + Wall(Vector2(351, 312.5), Vector2(5, 180)), + Wall(Vector2(430, 302.5), Vector2(5, 290)), + Wall(Vector2(292.5, 450), Vector2(280, 5)), + Wall(bottomCenter, Vector2(filledSize.y, 5)), + Wall(rightCenter, Vector2(5, filledSize.y)), + ]; +} + +class Wall extends BodyComponent { + Wall(this._position, this.size) : super(priority: 3); + + final Vector2 _position; + final Vector2 size; + + final Random rng = Random(); + late final Image _image; + + final scale = 10.0; + late final _renderPosition = -size.toOffset() / 2; + late final _scaledRect = (size * scale).toRect(); + late final _renderRect = _renderPosition & size.toSize(); + + @override + Future onLoad() async { + await super.onLoad(); + + paint.color = ColorExtension.fromRGBHexString('#14F596'); + + final recorder = PictureRecorder(); + final canvas = Canvas(recorder, _scaledRect); + final drawSize = _scaledRect.size.toVector2(); + final center = (drawSize / 2).toOffset(); + const step = 1.0; + + canvas.drawRect( + Rect.fromCenter(center: center, width: drawSize.x, height: drawSize.y), + BasicPalette.black.paint(), + ); + paint.style = PaintingStyle.stroke; + paint.strokeWidth = step; + for (var x = 0; x < 30; x++) { + canvas.drawRect( + Rect.fromCenter(center: center, width: drawSize.x, height: drawSize.y), + paint, + ); + paint.color = paint.color.darken(0.07); + drawSize.x -= step; + drawSize.y -= step; + } + final picture = recorder.endRecording(); + _image = await picture.toImage( + _scaledRect.width.toInt(), + _scaledRect.height.toInt(), + ); + } + + @override + void render(Canvas canvas) { + canvas.drawImageRect( + _image, + _scaledRect, + _renderRect, + paint, + ); + } + + @override + Body createBody() { + final def = BodyDef() + ..type = BodyType.static + ..position = _position; + final body = world.createBody(def) + ..userData = this + ..angularDamping = 3.0; + + final shape = PolygonShape()..setAsBoxXY(size.x / 2, size.y / 2); + final fixtureDef = FixtureDef(shape)..restitution = 0.5; + return body..createFixture(fixtureDef); + } + + late Rect asRect = Rect.fromCenter( + center: _position.toOffset(), + width: size.x, + height: size.y, + ); +} diff --git a/flame/assets/examples/official/padracing/pubspec.yaml b/flame/assets/examples/official/padracing/pubspec.yaml new file mode 100644 index 0000000..c07fc7d --- /dev/null +++ b/flame/assets/examples/official/padracing/pubspec.yaml @@ -0,0 +1,24 @@ +name: padracing +description: A sample game featuring Flame and Forge2D for DartPad +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + collection: ^1.17.1 + flame: ^1.17.0 + flame_forge2d: ^0.18.0 + flutter: + sdk: flutter + google_fonts: ^4.0.4 + url_launcher: ^6.1.11 + +dev_dependencies: + flame_lint: ^1.1.2 + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/flame/assets/examples/official/rogue_shooter/README.md b/flame/assets/examples/official/rogue_shooter/README.md new file mode 100644 index 0000000..ab70178 --- /dev/null +++ b/flame/assets/examples/official/rogue_shooter/README.md @@ -0,0 +1,6 @@ +# Flame Performance Test Game + +This is a simple scrolling shooter game which we use for testing the performance of Flame, +since it uses a lot of components and hitboxes. When it reaches a certain amount of +components (counted in the lower right corner) you can expect it to drop a bit in FPS, +depending on what platform you are on. diff --git a/flame/assets/examples/official/rogue_shooter/lib/components/bullet_component.dart b/flame/assets/examples/official/rogue_shooter/lib/components/bullet_component.dart new file mode 100644 index 0000000..f4d7146 --- /dev/null +++ b/flame/assets/examples/official/rogue_shooter/lib/components/bullet_component.dart @@ -0,0 +1,54 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:rogue_shooter/components/enemy_component.dart'; + +class BulletComponent extends SpriteAnimationComponent + with HasGameRef, CollisionCallbacks { + static const speed = 500.0; + late final Vector2 velocity; + final Vector2 deltaPosition = Vector2.zero(); + + BulletComponent({required super.position, super.angle}) + : super(size: Vector2(10, 20), anchor: Anchor.center); + + @override + Future onLoad() async { + add(CircleHitbox()); + animation = await game.loadSpriteAnimation( + 'rogue_shooter/bullet.png', + SpriteAnimationData.sequenced( + stepTime: 0.2, + amount: 4, + textureSize: Vector2(8, 16), + ), + ); + velocity = Vector2(0, -1) + ..rotate(angle) + ..scale(speed); + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + super.onCollisionStart(intersectionPoints, other); + if (other is EnemyComponent) { + other.takeHit(); + removeFromParent(); + } + } + + @override + void update(double dt) { + super.update(dt); + deltaPosition + ..setFrom(velocity) + ..scale(dt); + position += deltaPosition; + + if (position.y < 0 || position.x > game.size.x || position.x + size.x < 0) { + removeFromParent(); + } + } +} diff --git a/flame/assets/examples/official/rogue_shooter/lib/components/enemy_component.dart b/flame/assets/examples/official/rogue_shooter/lib/components/enemy_component.dart new file mode 100644 index 0000000..a9c1ffe --- /dev/null +++ b/flame/assets/examples/official/rogue_shooter/lib/components/enemy_component.dart @@ -0,0 +1,42 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:rogue_shooter/components/explosion_component.dart'; +import 'package:rogue_shooter/rogue_shooter_game.dart'; + +class EnemyComponent extends SpriteAnimationComponent + with HasGameReference, CollisionCallbacks { + static const speed = 150; + static final Vector2 initialSize = Vector2.all(25); + + EnemyComponent({required super.position}) + : super(size: initialSize, anchor: Anchor.center); + + @override + Future onLoad() async { + animation = await game.loadSpriteAnimation( + 'rogue_shooter/enemy.png', + SpriteAnimationData.sequenced( + stepTime: 0.2, + amount: 4, + textureSize: Vector2.all(16), + ), + ); + add(CircleHitbox(collisionType: CollisionType.passive)); + } + + @override + void update(double dt) { + super.update(dt); + y += speed * dt; + if (y >= game.size.y) { + removeFromParent(); + } + } + + void takeHit() { + removeFromParent(); + + game.add(ExplosionComponent(position: position)); + game.increaseScore(); + } +} diff --git a/flame/assets/examples/official/rogue_shooter/lib/components/enemy_creator.dart b/flame/assets/examples/official/rogue_shooter/lib/components/enemy_creator.dart new file mode 100644 index 0000000..f57c2ba --- /dev/null +++ b/flame/assets/examples/official/rogue_shooter/lib/components/enemy_creator.dart @@ -0,0 +1,26 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:rogue_shooter/components/enemy_component.dart'; + +class EnemyCreator extends TimerComponent with HasGameRef { + final Random random = Random(); + final _halfWidth = EnemyComponent.initialSize.x / 2; + + EnemyCreator() : super(period: 0.05, repeat: true); + + @override + void onTick() { + game.addAll( + List.generate( + 5, + (index) => EnemyComponent( + position: Vector2( + _halfWidth + (game.size.x - _halfWidth) * random.nextDouble(), + 0, + ), + ), + ), + ); + } +} diff --git a/flame/assets/examples/official/rogue_shooter/lib/components/explosion_component.dart b/flame/assets/examples/official/rogue_shooter/lib/components/explosion_component.dart new file mode 100644 index 0000000..e646eca --- /dev/null +++ b/flame/assets/examples/official/rogue_shooter/lib/components/explosion_component.dart @@ -0,0 +1,23 @@ +import 'package:flame/components.dart'; + +class ExplosionComponent extends SpriteAnimationComponent with HasGameRef { + ExplosionComponent({super.position}) + : super( + size: Vector2.all(50), + anchor: Anchor.center, + removeOnFinish: true, + ); + + @override + Future onLoad() async { + animation = await game.loadSpriteAnimation( + 'rogue_shooter/explosion.png', + SpriteAnimationData.sequenced( + stepTime: 0.1, + amount: 6, + textureSize: Vector2.all(32), + loop: false, + ), + ); + } +} diff --git a/flame/assets/examples/official/rogue_shooter/lib/components/player_component.dart b/flame/assets/examples/official/rogue_shooter/lib/components/player_component.dart new file mode 100644 index 0000000..0bd242a --- /dev/null +++ b/flame/assets/examples/official/rogue_shooter/lib/components/player_component.dart @@ -0,0 +1,69 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:rogue_shooter/components/bullet_component.dart'; +import 'package:rogue_shooter/components/enemy_component.dart'; +import 'package:rogue_shooter/components/explosion_component.dart'; + +class PlayerComponent extends SpriteAnimationComponent + with HasGameRef, CollisionCallbacks { + late TimerComponent bulletCreator; + + PlayerComponent() : super(size: Vector2(50, 75), anchor: Anchor.center); + + @override + Future onLoad() async { + position = game.size / 2; + add(CircleHitbox()); + add( + bulletCreator = TimerComponent( + period: 0.05, + repeat: true, + autoStart: false, + onTick: _createBullet, + ), + ); + animation = await game.loadSpriteAnimation( + 'rogue_shooter/player.png', + SpriteAnimationData.sequenced( + stepTime: 0.2, + amount: 4, + textureSize: Vector2(32, 39), + ), + ); + } + + final _bulletAngles = [0.5, 0.3, 0.0, -0.5, -0.3]; + void _createBullet() { + game.addAll( + _bulletAngles.map( + (angle) => BulletComponent( + position: position + Vector2(0, -size.y / 2), + angle: angle, + ), + ), + ); + } + + void beginFire() { + bulletCreator.timer.start(); + } + + void stopFire() { + bulletCreator.timer.pause(); + } + + void takeHit() { + game.add(ExplosionComponent(position: position)); + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + super.onCollisionStart(intersectionPoints, other); + if (other is EnemyComponent) { + other.takeHit(); + } + } +} diff --git a/flame/assets/examples/official/rogue_shooter/lib/components/star_background_creator.dart b/flame/assets/examples/official/rogue_shooter/lib/components/star_background_creator.dart new file mode 100644 index 0000000..e7a153d --- /dev/null +++ b/flame/assets/examples/official/rogue_shooter/lib/components/star_background_creator.dart @@ -0,0 +1,65 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/sprite.dart'; +import 'package:rogue_shooter/components/star_component.dart'; + +class StarBackGroundCreator extends Component with HasGameRef { + final gapSize = 12; + + late final SpriteSheet spriteSheet; + Random random = Random(); + + StarBackGroundCreator(); + + @override + Future onLoad() async { + spriteSheet = SpriteSheet.fromColumnsAndRows( + image: await game.images.load('rogue_shooter/stars.png'), + rows: 4, + columns: 4, + ); + + final starGapTime = (game.size.y / gapSize) / StarComponent.speed; + + add( + TimerComponent( + period: starGapTime, + repeat: true, + onTick: () => _createRowOfStars(0), + ), + ); + + _createInitialStars(); + } + + void _createStarAt(double x, double y) { + final animation = spriteSheet.createAnimation( + row: random.nextInt(3), + to: 4, + stepTime: 0.1, + )..variableStepTimes = [max(20, 100 * random.nextDouble()), 0.1, 0.1, 0.1]; + + game.add(StarComponent(animation: animation, position: Vector2(x, y))); + } + + void _createRowOfStars(double y) { + const gapSize = 6; + final starGap = game.size.x / gapSize; + + for (var i = 0; i < gapSize; i++) { + _createStarAt( + starGap * i + (random.nextDouble() * starGap), + y + (random.nextDouble() * 20), + ); + } + } + + void _createInitialStars() { + final rows = game.size.y / gapSize; + + for (var i = 0; i < gapSize; i++) { + _createRowOfStars(i * rows); + } + } +} diff --git a/flame/assets/examples/official/rogue_shooter/lib/components/star_component.dart b/flame/assets/examples/official/rogue_shooter/lib/components/star_component.dart new file mode 100644 index 0000000..df5eb1b --- /dev/null +++ b/flame/assets/examples/official/rogue_shooter/lib/components/star_component.dart @@ -0,0 +1,17 @@ +import 'package:flame/components.dart'; + +class StarComponent extends SpriteAnimationComponent with HasGameRef { + static const speed = 10; + + StarComponent({super.animation, super.position}) + : super(size: Vector2.all(20)); + + @override + void update(double dt) { + super.update(dt); + y += dt * speed; + if (y >= game.size.y) { + removeFromParent(); + } + } +} diff --git a/flame/assets/examples/official/rogue_shooter/lib/main.dart b/flame/assets/examples/official/rogue_shooter/lib/main.dart new file mode 100644 index 0000000..24ee2a7 --- /dev/null +++ b/flame/assets/examples/official/rogue_shooter/lib/main.dart @@ -0,0 +1,7 @@ +import 'package:flame/game.dart'; +import 'package:flutter/widgets.dart'; +import 'package:rogue_shooter/rogue_shooter_game.dart'; + +void main() { + runApp(GameWidget(game: RogueShooterGame())); +} diff --git a/flame/assets/examples/official/rogue_shooter/lib/rogue_shooter_game.dart b/flame/assets/examples/official/rogue_shooter/lib/rogue_shooter_game.dart new file mode 100644 index 0000000..1bd2436 --- /dev/null +++ b/flame/assets/examples/official/rogue_shooter/lib/rogue_shooter_game.dart @@ -0,0 +1,91 @@ +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:rogue_shooter/components/enemy_creator.dart'; +import 'package:rogue_shooter/components/player_component.dart'; +import 'package:rogue_shooter/components/star_background_creator.dart'; + +class RogueShooterGame extends FlameGame + with PanDetector, HasCollisionDetection, HasPerformanceTracker { + static const String description = ''' + A simple space shooter game used for testing performance of the collision + detection system in Flame. + '''; + + late final PlayerComponent _player; + late final TextComponent _componentCounter; + late final TextComponent _scoreText; + + final _updateTime = TextComponent( + text: 'Update time: 0ms', + position: Vector2(0, 0), + priority: 1, + ); + + final TextComponent _renderTime = TextComponent( + text: 'Render time: 0ms', + position: Vector2(0, 25), + priority: 1, + ); + + int _score = 0; + + @override + Future onLoad() async { + add(_player = PlayerComponent()); + addAll([ + FpsTextComponent( + position: size - Vector2(0, 50), + anchor: Anchor.bottomRight, + ), + _scoreText = TextComponent( + position: size - Vector2(0, 25), + anchor: Anchor.bottomRight, + priority: 1, + ), + _componentCounter = TextComponent( + position: size, + anchor: Anchor.bottomRight, + priority: 1, + ), + ]); + + add(EnemyCreator()); + add(StarBackGroundCreator()); + + addAll([_updateTime, _renderTime]); + } + + @override + void update(double dt) { + super.update(dt); + _scoreText.text = 'Score: $_score'; + _componentCounter.text = 'Components: ${children.length}'; + _updateTime.text = 'Update time: $updateTime ms'; + _renderTime.text = 'Render time: $renderTime ms'; + } + + @override + void onPanStart(_) { + _player.beginFire(); + } + + @override + void onPanEnd(_) { + _player.stopFire(); + } + + @override + void onPanCancel() { + _player.stopFire(); + } + + @override + void onPanUpdate(DragUpdateInfo info) { + _player.position += info.delta.global; + } + + void increaseScore() { + _score++; + } +} diff --git a/flame/assets/examples/official/rogue_shooter/lib/rogue_shooter_widget.dart b/flame/assets/examples/official/rogue_shooter/lib/rogue_shooter_widget.dart new file mode 100644 index 0000000..b9adbad --- /dev/null +++ b/flame/assets/examples/official/rogue_shooter/lib/rogue_shooter_widget.dart @@ -0,0 +1,17 @@ +import 'package:flame/game.dart'; +import 'package:flutter/widgets.dart'; +import 'package:rogue_shooter/rogue_shooter_game.dart'; + +class RogueShooterWidget extends StatelessWidget { + const RogueShooterWidget({super.key}); + + @override + Widget build(BuildContext context) { + return GameWidget( + game: RogueShooterGame(), + loadingBuilder: (_) => const Center( + child: Text('Loading'), + ), + ); + } +} diff --git a/flame/assets/examples/official/rogue_shooter/pubspec.yaml b/flame/assets/examples/official/rogue_shooter/pubspec.yaml new file mode 100644 index 0000000..820ebbe --- /dev/null +++ b/flame/assets/examples/official/rogue_shooter/pubspec.yaml @@ -0,0 +1,22 @@ +name: rogue_shooter +description: A simple game benchmarking the collision detection performance. +homepage: https://github.com/flame-engine/flame/tree/main/examples/games/rogue_shooter +publish_to: 'none' + +version: 0.1.0 + +environment: + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.19.0" + +dependencies: + flame: ^1.17.0 + flutter: + sdk: flutter + +dev_dependencies: + flame_lint: ^1.1.2 + +flutter: + assets: + - assets/images/rogue_shooter/ diff --git a/flame/assets/examples/official/sample_example/lib/main.dart b/flame/assets/examples/official/sample_example/lib/main.dart new file mode 100644 index 0000000..2fa6228 --- /dev/null +++ b/flame/assets/examples/official/sample_example/lib/main.dart @@ -0,0 +1,82 @@ +import 'dart:math' as math; + +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/game.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/material.dart'; + +/// This example simply adds a rotating white square on the screen. +/// If you press on a square, it will be removed. +/// If you press anywhere else, another square will be added. +void main() { + runApp( + GameWidget( + game: FlameGame(world: MyWorld()), + ), + ); +} + +class MyWorld extends World with TapCallbacks { + @override + Future onLoad() async { + add(Square(Vector2.zero())); + } + + @override + void onTapDown(TapDownEvent event) { + super.onTapDown(event); + if (!event.handled) { + final touchPoint = event.localPosition; + add(Square(touchPoint)); + } + } +} + +class Square extends RectangleComponent with TapCallbacks { + static const speed = 3; + static const squareSize = 128.0; + static const indicatorSize = 6.0; + + static final Paint red = BasicPalette.red.paint(); + static final Paint blue = BasicPalette.blue.paint(); + + Square(Vector2 position) + : super( + position: position, + size: Vector2.all(squareSize), + anchor: Anchor.center, + ); + + @override + Future onLoad() async { + super.onLoad(); + add( + RectangleComponent( + size: Vector2.all(indicatorSize), + paint: blue, + ), + ); + add( + RectangleComponent( + position: size / 2, + size: Vector2.all(indicatorSize), + anchor: Anchor.center, + paint: red, + ), + ); + } + + @override + void update(double dt) { + super.update(dt); + angle += speed * dt; + angle %= 2 * math.pi; + } + + @override + void onTapDown(TapDownEvent event) { + removeFromParent(); + event.handled = true; + } +} diff --git a/flame/assets/examples/official/trex/.metadata b/flame/assets/examples/official/trex/.metadata new file mode 100644 index 0000000..deb187d --- /dev/null +++ b/flame/assets/examples/official/trex/.metadata @@ -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. + +version: + revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: linux + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + + # 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' diff --git a/flame/assets/examples/official/trex/README.md b/flame/assets/examples/official/trex/README.md new file mode 100644 index 0000000..ba28e44 --- /dev/null +++ b/flame/assets/examples/official/trex/README.md @@ -0,0 +1,13 @@ +# T-rex + +![https://cdn-images-1.medium.com/max/1600/1*BadLUm5ZzpcS34eTVQAz5g.gif](https://cdn-images-1.medium.com/max/1600/1*BadLUm5ZzpcS34eTVQAz5g.gif) + +The joy of our offline hours recreated with [Flutter](https://github.com/flutter/flutter) and [Flame](https://github.com/flame-engine/flame) + + +## Article + +This was the original article written when the game initially was ported. +(It is outdated now since it uses a very old version of Flame) + + diff --git a/flame/assets/examples/official/trex/analysis_options.yaml b/flame/assets/examples/official/trex/analysis_options.yaml new file mode 100644 index 0000000..92aae2f --- /dev/null +++ b/flame/assets/examples/official/trex/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flame_lint/analysis_options_with_dcm.yaml diff --git a/flame/assets/examples/official/trex/assets/images/trex.png b/flame/assets/examples/official/trex/assets/images/trex.png new file mode 100644 index 0000000..e35c8ac Binary files /dev/null and b/flame/assets/examples/official/trex/assets/images/trex.png differ diff --git a/flame/assets/examples/official/trex/lib/background/cloud.dart b/flame/assets/examples/official/trex/lib/background/cloud.dart new file mode 100644 index 0000000..31e8b07 --- /dev/null +++ b/flame/assets/examples/official/trex/lib/background/cloud.dart @@ -0,0 +1,61 @@ +import 'package:flame/components.dart'; +import 'package:trex_game/background/cloud_manager.dart'; +import 'package:trex_game/random_extension.dart'; +import 'package:trex_game/trex_game.dart'; + +class Cloud extends SpriteComponent + with ParentIsA, HasGameReference { + Cloud({required Vector2 position}) + : cloudGap = random.fromRange( + minCloudGap, + maxCloudGap, + ), + super( + position: position, + size: initialSize, + ); + + static final Vector2 initialSize = Vector2(92.0, 28.0); + + static const double maxCloudGap = 400.0; + static const double minCloudGap = 100.0; + + static const double maxSkyLevel = 71.0; + static const double minSkyLevel = 30.0; + + final double cloudGap; + + @override + Future onLoad() async { + sprite = Sprite( + game.spriteImage, + srcPosition: Vector2(166.0, 2.0), + srcSize: initialSize, + ); + } + + @override + void update(double dt) { + super.update(dt); + if (isRemoving) { + return; + } + x -= parent.cloudSpeed.ceil() * 50 * dt; + + if (!isVisible) { + removeFromParent(); + } + } + + bool get isVisible { + return x + width > 0; + } + + @override + void onGameResize(Vector2 size) { + super.onGameResize(size); + y = ((absolutePosition.y / 2 - (maxSkyLevel - minSkyLevel)) + + random.fromRange(minSkyLevel, maxSkyLevel)) - + absolutePositionOf(absoluteTopLeftPosition).y; + } +} diff --git a/flame/assets/examples/official/trex/lib/background/cloud_manager.dart b/flame/assets/examples/official/trex/lib/background/cloud_manager.dart new file mode 100644 index 0000000..f7e5edd --- /dev/null +++ b/flame/assets/examples/official/trex/lib/background/cloud_manager.dart @@ -0,0 +1,41 @@ +import 'package:flame/components.dart'; +import 'package:trex_game/background/cloud.dart'; +import 'package:trex_game/random_extension.dart'; +import 'package:trex_game/trex_game.dart'; + +class CloudManager extends PositionComponent with HasGameReference { + final double cloudFrequency = 0.5; + final int maxClouds = 20; + final double bgCloudSpeed = 0.2; + + void addCloud() { + final cloudPosition = Vector2( + game.size.x + Cloud.initialSize.x + 10, + ((absolutePosition.y / 2 - (Cloud.maxSkyLevel - Cloud.minSkyLevel)) + + random.fromRange(Cloud.minSkyLevel, Cloud.maxSkyLevel)) - + absolutePosition.y, + ); + add(Cloud(position: cloudPosition)); + } + + double get cloudSpeed => bgCloudSpeed / 1000 * game.currentSpeed; + + @override + void update(double dt) { + super.update(dt); + final numClouds = children.length; + if (numClouds > 0) { + final lastCloud = children.last as Cloud; + if (numClouds < maxClouds && + (game.size.x / 2 - lastCloud.x) > lastCloud.cloudGap) { + addCloud(); + } + } else { + addCloud(); + } + } + + void reset() { + removeAll(children); + } +} diff --git a/flame/assets/examples/official/trex/lib/background/horizon.dart b/flame/assets/examples/official/trex/lib/background/horizon.dart new file mode 100644 index 0000000..3d0f06f --- /dev/null +++ b/flame/assets/examples/official/trex/lib/background/horizon.dart @@ -0,0 +1,80 @@ +import 'dart:collection'; +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:flame/components.dart'; +import 'package:trex_game/background/cloud_manager.dart'; +import 'package:trex_game/obstacle/obstacle_manager.dart'; +import 'package:trex_game/trex_game.dart'; + +class Horizon extends PositionComponent with HasGameReference { + Horizon() : super(); + + static final Vector2 lineSize = Vector2(1200, 24); + final Queue groundLayers = Queue(); + late final CloudManager cloudManager = CloudManager(); + late final ObstacleManager obstacleManager = ObstacleManager(); + + late final _softSprite = Sprite( + game.spriteImage, + srcPosition: Vector2(2.0, 104.0), + srcSize: lineSize, + ); + + late final _bumpySprite = Sprite( + game.spriteImage, + srcPosition: Vector2(game.spriteImage.width / 2, 104.0), + srcSize: lineSize, + ); + + @override + Future onLoad() async { + add(cloudManager); + add(obstacleManager); + } + + @override + void update(double dt) { + super.update(dt); + final increment = game.currentSpeed * dt; + for (final line in groundLayers) { + line.x -= increment; + } + + final firstLine = groundLayers.first; + if (firstLine.x <= -firstLine.width) { + firstLine.x = groundLayers.last.x + groundLayers.last.width; + groundLayers.remove(firstLine); + groundLayers.add(firstLine); + } + } + + @override + void onGameResize(Vector2 size) { + super.onGameResize(size); + final newLines = _generateLines(); + groundLayers.addAll(newLines); + addAll(newLines); + y = (size.y / 2) + 21.0; + } + + void reset() { + cloudManager.reset(); + obstacleManager.reset(); + groundLayers.forEachIndexed((i, line) => line.x = i * lineSize.x); + } + + List _generateLines() { + final number = 1 + (game.size.x / lineSize.x).ceil() - groundLayers.length; + final lastX = (groundLayers.lastOrNull?.x ?? 0) + + (groundLayers.lastOrNull?.width ?? 0); + return List.generate( + max(number, 0), + (i) => SpriteComponent( + sprite: (i + groundLayers.length).isEven ? _softSprite : _bumpySprite, + size: lineSize, + )..x = lastX + lineSize.x * i, + growable: false, + ); + } +} diff --git a/flame/assets/examples/official/trex/lib/game_over.dart b/flame/assets/examples/official/trex/lib/game_over.dart new file mode 100644 index 0000000..73ccda9 --- /dev/null +++ b/flame/assets/examples/official/trex/lib/game_over.dart @@ -0,0 +1,61 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:trex_game/trex_game.dart'; + +class GameOverPanel extends Component { + bool visible = false; + + @override + Future onLoad() async { + add(GameOverText()); + add(GameOverRestart()); + } + + @override + void renderTree(Canvas canvas) { + if (visible) { + super.renderTree(canvas); + } + } +} + +class GameOverText extends SpriteComponent with HasGameReference { + GameOverText() : super(size: Vector2(382, 25), anchor: Anchor.center); + + @override + Future onLoad() async { + sprite = Sprite( + game.spriteImage, + srcPosition: Vector2(955.0, 26.0), + srcSize: size, + ); + } + + @override + void onGameResize(Vector2 size) { + super.onGameResize(size); + x = size.x / 2; + y = size.y * .25; + } +} + +class GameOverRestart extends SpriteComponent with HasGameReference { + GameOverRestart() : super(size: Vector2(72, 64), anchor: Anchor.center); + + @override + Future onLoad() async { + sprite = Sprite( + game.spriteImage, + srcPosition: Vector2.all(2.0), + srcSize: size, + ); + } + + @override + void onGameResize(Vector2 size) { + super.onGameResize(size); + x = size.x / 2; + y = size.y * .75; + } +} diff --git a/flame/assets/examples/official/trex/lib/main.dart b/flame/assets/examples/official/trex/lib/main.dart new file mode 100644 index 0000000..44268d1 --- /dev/null +++ b/flame/assets/examples/official/trex/lib/main.dart @@ -0,0 +1,9 @@ +import 'package:flame/game.dart'; +import 'package:flutter/widgets.dart'; +import 'package:trex_game/trex_game.dart'; + +void main() { + runApp( + GameWidget(game: TRexGame()), + ); +} diff --git a/flame/assets/examples/official/trex/lib/obstacle/obstacle.dart b/flame/assets/examples/official/trex/lib/obstacle/obstacle.dart new file mode 100644 index 0000000..772fc7a --- /dev/null +++ b/flame/assets/examples/official/trex/lib/obstacle/obstacle.dart @@ -0,0 +1,47 @@ +import 'package:flame/components.dart'; +import 'package:trex_game/obstacle/obstacle_type.dart'; +import 'package:trex_game/random_extension.dart'; +import 'package:trex_game/trex_game.dart'; + +class Obstacle extends SpriteComponent with HasGameReference { + Obstacle({ + required this.settings, + required this.groupIndex, + }) : super(size: settings.size); + + final double _gapCoefficient = 0.6; + final double _maxGapCoefficient = 1.5; + + bool followingObstacleCreated = false; + late double gap; + final ObstacleTypeSettings settings; + final int groupIndex; + + bool get isVisible => (x + width) > 0; + + @override + Future onLoad() async { + sprite = settings.sprite(game.spriteImage); + x = game.size.x + width * groupIndex; + y = settings.y; + gap = computeGap(_gapCoefficient, game.currentSpeed); + addAll(settings.generateHitboxes()); + } + + double computeGap(double gapCoefficient, double speed) { + final minGap = + (width * speed * settings.minGap * gapCoefficient).roundToDouble(); + final maxGap = (minGap * _maxGapCoefficient).roundToDouble(); + return random.fromRange(minGap, maxGap); + } + + @override + void update(double dt) { + super.update(dt); + x -= game.currentSpeed * dt; + + if (!isVisible) { + removeFromParent(); + } + } +} diff --git a/flame/assets/examples/official/trex/lib/obstacle/obstacle_manager.dart b/flame/assets/examples/official/trex/lib/obstacle/obstacle_manager.dart new file mode 100644 index 0000000..117ef21 --- /dev/null +++ b/flame/assets/examples/official/trex/lib/obstacle/obstacle_manager.dart @@ -0,0 +1,80 @@ +import 'dart:collection'; + +import 'package:flame/components.dart'; +import 'package:trex_game/obstacle/obstacle.dart'; +import 'package:trex_game/obstacle/obstacle_type.dart'; +import 'package:trex_game/random_extension.dart'; +import 'package:trex_game/trex_game.dart'; + +class ObstacleManager extends Component with HasGameReference { + ObstacleManager(); + + ListQueue history = ListQueue(); + static const int maxObstacleDuplication = 2; + + @override + void update(double dt) { + final obstacles = children.query(); + + if (obstacles.isNotEmpty) { + final lastObstacle = children.last as Obstacle?; + + if (lastObstacle != null && + !lastObstacle.followingObstacleCreated && + lastObstacle.isVisible && + (lastObstacle.x + lastObstacle.width + lastObstacle.gap) < + game.size.x) { + addNewObstacle(); + lastObstacle.followingObstacleCreated = true; + } + } else { + addNewObstacle(); + } + } + + void addNewObstacle() { + final speed = game.currentSpeed; + if (speed == 0) { + return; + } + var settings = random.nextBool() + ? ObstacleTypeSettings.cactusSmall + : ObstacleTypeSettings.cactusLarge; + if (duplicateObstacleCheck(settings.type) || speed < settings.allowedAt) { + settings = ObstacleTypeSettings.cactusSmall; + } + + final groupSize = _groupSize(settings); + for (var i = 0; i < groupSize; i++) { + add(Obstacle(settings: settings, groupIndex: i)); + game.score++; + } + + history.addFirst(settings.type); + while (history.length > maxObstacleDuplication) { + history.removeLast(); + } + } + + bool duplicateObstacleCheck(ObstacleType nextType) { + var duplicateCount = 0; + + for (final type in history) { + duplicateCount += type == nextType ? 1 : 0; + } + return duplicateCount >= maxObstacleDuplication; + } + + void reset() { + removeAll(children); + history.clear(); + } + + int _groupSize(ObstacleTypeSettings settings) { + if (game.currentSpeed > settings.multipleAt) { + return random.fromRange(1.0, ObstacleTypeSettings.maxGroupSize).floor(); + } else { + return 1; + } + } +} diff --git a/flame/assets/examples/official/trex/lib/obstacle/obstacle_type.dart b/flame/assets/examples/official/trex/lib/obstacle/obstacle_type.dart new file mode 100644 index 0000000..30468e1 --- /dev/null +++ b/flame/assets/examples/official/trex/lib/obstacle/obstacle_type.dart @@ -0,0 +1,105 @@ +// ignore_for_file: unused_element + +import 'dart:ui'; + +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; + +enum ObstacleType { + cactusSmall, + cactusLarge, +} + +class ObstacleTypeSettings { + const ObstacleTypeSettings._internal( + this.type, { + required this.size, + required this.y, + required this.allowedAt, + required this.multipleAt, + required this.minGap, + required this.minSpeed, + required this.generateHitboxes, + this.numFrames, + this.frameRate, + this.speedOffset, + }); + + final ObstacleType type; + final Vector2 size; + final double y; + final int allowedAt; + final int multipleAt; + final double minGap; + final double minSpeed; + final int? numFrames; + final double? frameRate; + final double? speedOffset; + + static const maxGroupSize = 3.0; + + final List Function() generateHitboxes; + + static final cactusSmall = ObstacleTypeSettings._internal( + ObstacleType.cactusSmall, + size: Vector2(34.0, 70.0), + y: -55.0, + allowedAt: 0, + multipleAt: 1000, + minGap: 120.0, + minSpeed: 0.0, + generateHitboxes: () => [ + RectangleHitbox( + position: Vector2(5.0, 7.0), + size: Vector2(10.0, 54.0), + ), + RectangleHitbox( + position: Vector2(5.0, 7.0), + size: Vector2(12.0, 68.0), + ), + RectangleHitbox( + position: Vector2(15.0, 4.0), + size: Vector2(14.0, 28.0), + ), + ], + ); + + static final cactusLarge = ObstacleTypeSettings._internal( + ObstacleType.cactusLarge, + size: Vector2(50.0, 100.0), + y: -74.0, + allowedAt: 800, + multipleAt: 1500, + minGap: 120.0, + minSpeed: 0.0, + generateHitboxes: () => [ + RectangleHitbox( + position: Vector2(0.0, 26.0), + size: Vector2(14.0, 40.0), + ), + RectangleHitbox( + position: Vector2(16.0, 0.0), + size: Vector2(14.0, 98.0), + ), + RectangleHitbox( + position: Vector2(28.0, 22.0), + size: Vector2(20.0, 40.0), + ), + ], + ); + + Sprite sprite(Image spriteImage) { + return switch (type) { + ObstacleType.cactusSmall => Sprite( + spriteImage, + srcPosition: Vector2(446.0, 2.0), + srcSize: size, + ), + ObstacleType.cactusLarge => Sprite( + spriteImage, + srcPosition: Vector2(652.0, 2.0), + srcSize: size, + ), + }; + } +} diff --git a/flame/assets/examples/official/trex/lib/player.dart b/flame/assets/examples/official/trex/lib/player.dart new file mode 100644 index 0000000..e4d4f0a --- /dev/null +++ b/flame/assets/examples/official/trex/lib/player.dart @@ -0,0 +1,129 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:trex_game/trex_game.dart'; + +enum PlayerState { crashed, jumping, running, waiting } + +class Player extends SpriteAnimationGroupComponent + with HasGameReference, CollisionCallbacks { + Player() : super(size: Vector2(90, 88)); + + final double gravity = 1; + + final double initialJumpVelocity = -15.0; + final double introDuration = 1500.0; + final double startXPosition = 50; + + double _jumpVelocity = 0.0; + + double get groundYPos { + return (game.size.y / 2) - height / 2; + } + + @override + Future onLoad() async { + // Body hitbox + add( + RectangleHitbox.relative( + Vector2(0.7, 0.6), + position: Vector2(0, height / 3), + parentSize: size, + ), + ); + // Head hitbox + add( + RectangleHitbox.relative( + Vector2(0.45, 0.35), + position: Vector2(width / 2, 0), + parentSize: size, + ), + ); + animations = { + PlayerState.running: _getAnimation( + size: Vector2(88.0, 90.0), + frames: [Vector2(1514.0, 4.0), Vector2(1602.0, 4.0)], + stepTime: 0.2, + ), + PlayerState.waiting: _getAnimation( + size: Vector2(88.0, 90.0), + frames: [Vector2(76.0, 6.0)], + ), + PlayerState.jumping: _getAnimation( + size: Vector2(88.0, 90.0), + frames: [Vector2(1339.0, 6.0)], + ), + PlayerState.crashed: _getAnimation( + size: Vector2(88.0, 90.0), + frames: [Vector2(1782.0, 6.0)], + ), + }; + current = PlayerState.waiting; + } + + void jump(double speed) { + if (current == PlayerState.jumping) { + return; + } + + current = PlayerState.jumping; + _jumpVelocity = initialJumpVelocity - (speed / 500); + } + + void reset() { + y = groundYPos; + _jumpVelocity = 0.0; + current = PlayerState.running; + } + + @override + void update(double dt) { + super.update(dt); + if (current == PlayerState.jumping) { + y += _jumpVelocity; + _jumpVelocity += gravity; + if (y > groundYPos) { + reset(); + } + } else { + y = groundYPos; + } + + if (game.isIntro && x < startXPosition) { + x += (startXPosition / introDuration) * dt * 5000; + } + } + + @override + void onGameResize(Vector2 size) { + super.onGameResize(size); + y = groundYPos; + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + super.onCollisionStart(intersectionPoints, other); + game.gameOver(); + } + + SpriteAnimation _getAnimation({ + required Vector2 size, + required List frames, + double stepTime = double.infinity, + }) { + return SpriteAnimation.spriteList( + frames + .map( + (vector) => Sprite( + game.spriteImage, + srcSize: size, + srcPosition: vector, + ), + ) + .toList(), + stepTime: stepTime, + ); + } +} diff --git a/flame/assets/examples/official/trex/lib/random_extension.dart b/flame/assets/examples/official/trex/lib/random_extension.dart new file mode 100644 index 0000000..f3d4fd4 --- /dev/null +++ b/flame/assets/examples/official/trex/lib/random_extension.dart @@ -0,0 +1,8 @@ +import 'dart:math'; + +final random = Random(); + +extension RandomExtension on Random { + double fromRange(double min, double max) => + (nextDouble() * (max - min + 1)).floor() + min; +} diff --git a/flame/assets/examples/official/trex/lib/trex_game.dart b/flame/assets/examples/official/trex/lib/trex_game.dart new file mode 100644 index 0000000..c23447e --- /dev/null +++ b/flame/assets/examples/official/trex/lib/trex_game.dart @@ -0,0 +1,152 @@ +import 'dart:ui'; + +import 'package:flame/components.dart'; +import 'package:flame/events.dart'; +import 'package:flame/flame.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/text.dart'; +import 'package:flutter/material.dart' hide Image; +import 'package:flutter/services.dart'; +import 'package:trex_game/background/horizon.dart'; +import 'package:trex_game/game_over.dart'; +import 'package:trex_game/player.dart'; + +enum GameState { playing, intro, gameOver } + +class TRexGame extends FlameGame + with KeyboardEvents, TapCallbacks, HasCollisionDetection { + static const String description = ''' + A game similar to the game in chrome that you get to play while offline. + Press space or tap/click the screen to jump, the more obstacles you manage + to survive, the more points you get. + '''; + + late final Image spriteImage; + + @override + Color backgroundColor() => const Color(0xFFFFFFFF); + + late final player = Player(); + late final horizon = Horizon(); + late final gameOverPanel = GameOverPanel(); + late final TextComponent scoreText; + + int _score = 0; + int _highScore = 0; + int get score => _score; + set score(int newScore) { + _score = newScore; + scoreText.text = '${scoreString(_score)} HI ${scoreString(_highScore)}'; + } + + String scoreString(int score) => score.toString().padLeft(5, '0'); + + /// Used for score calculation + double _distanceTraveled = 0; + + @override + Future onLoad() async { + spriteImage = await Flame.images.load('trex.png'); + add(horizon); + add(player); + add(gameOverPanel); + + const chars = '0123456789HI '; + final renderer = SpriteFontRenderer.fromFont( + SpriteFont( + source: spriteImage, + size: 23, + ascent: 23, + glyphs: [ + for (var i = 0; i < chars.length; i++) + Glyph(chars[i], left: 954.0 + 20 * i, top: 0, width: 20), + ], + ), + letterSpacing: 2, + ); + add( + scoreText = TextComponent( + position: Vector2(20, 20), + textRenderer: renderer, + ), + ); + score = 0; + } + + GameState state = GameState.intro; + double currentSpeed = 0.0; + double timePlaying = 0.0; + + final double acceleration = 10; + final double maxSpeed = 2500.0; + final double startSpeed = 600; + + bool get isPlaying => state == GameState.playing; + bool get isGameOver => state == GameState.gameOver; + bool get isIntro => state == GameState.intro; + + @override + KeyEventResult onKeyEvent( + KeyEvent event, + Set keysPressed, + ) { + if (keysPressed.contains(LogicalKeyboardKey.enter) || + keysPressed.contains(LogicalKeyboardKey.space)) { + onAction(); + } + return KeyEventResult.handled; + } + + @override + void onTapDown(TapDownEvent event) { + onAction(); + } + + void onAction() { + if (isGameOver || isIntro) { + restart(); + return; + } + player.jump(currentSpeed); + } + + void gameOver() { + gameOverPanel.visible = true; + state = GameState.gameOver; + player.current = PlayerState.crashed; + currentSpeed = 0.0; + } + + void restart() { + state = GameState.playing; + player.reset(); + horizon.reset(); + currentSpeed = startSpeed; + gameOverPanel.visible = false; + timePlaying = 0.0; + if (score > _highScore) { + _highScore = score; + } + score = 0; + _distanceTraveled = 0; + } + + @override + void update(double dt) { + super.update(dt); + if (isGameOver) { + return; + } + + if (isPlaying) { + timePlaying += dt; + _distanceTraveled += dt * currentSpeed; + score = _distanceTraveled ~/ 50; + + if (currentSpeed < maxSpeed) { + currentSpeed += acceleration * dt; + } + } + } +} diff --git a/flame/assets/examples/official/trex/lib/trex_widget.dart b/flame/assets/examples/official/trex/lib/trex_widget.dart new file mode 100644 index 0000000..33907ae --- /dev/null +++ b/flame/assets/examples/official/trex/lib/trex_widget.dart @@ -0,0 +1,26 @@ +import 'package:flame/game.dart'; +import 'package:flutter/material.dart' hide Image, Gradient; +import 'package:trex_game/trex_game.dart'; + +class TRexWidget extends StatelessWidget { + const TRexWidget({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'T-Rex', + home: Container( + color: Colors.black, + margin: const EdgeInsets.all(45), + child: ClipRect( + child: GameWidget( + game: TRexGame(), + loadingBuilder: (_) => const Center( + child: Text('Loading'), + ), + ), + ), + ), + ); + } +} diff --git a/flame/assets/examples/official/trex/pubspec.yaml b/flame/assets/examples/official/trex/pubspec.yaml new file mode 100644 index 0000000..9e701b5 --- /dev/null +++ b/flame/assets/examples/official/trex/pubspec.yaml @@ -0,0 +1,23 @@ +name: trex_game +description: A clone of the classic browser T-Rex game. +homepage: https://github.com/flame-engine/flame/tree/main/examples/games/trex/ +publish_to: 'none' + +version: 0.1.0 + +environment: + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.19.0" + +dependencies: + collection: ^1.16.0 + flame: ^1.17.0 + flutter: + sdk: flutter + +dev_dependencies: + flame_lint: ^1.1.2 +flutter: + uses-material-design: true + assets: + - assets/images/ diff --git a/flame/assets/examples/others/darkness_dungeon/LICENSE b/flame/assets/examples/others/darkness_dungeon/LICENSE new file mode 100644 index 0000000..bba5704 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Rafaelbarbosatec + +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. \ No newline at end of file diff --git a/flame/assets/examples/others/darkness_dungeon/lib/decoration/barrel.dart b/flame/assets/examples/others/darkness_dungeon/lib/decoration/barrel.dart new file mode 100644 index 0000000..37baeea --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/decoration/barrel.dart @@ -0,0 +1,22 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/main.dart'; + +class Barrel extends GameDecoration { + Barrel(Vector2 position) + : super.withSprite( + sprite: Sprite.load('items/barrel.png'), + position: position, + size: Vector2(tileSize, tileSize), + ); + + @override + Future onLoad() { + add( + RectangleHitbox( + size: Vector2(tileSize * 0.6, tileSize * 0.6), + position: Vector2(tileSize * 0.2, 0), + ), + ); + return super.onLoad(); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/decoration/door.dart b/flame/assets/examples/others/darkness_dungeon/lib/decoration/door.dart new file mode 100644 index 0000000..99a5086 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/decoration/door.dart @@ -0,0 +1,70 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/player/knight.dart'; +import 'package:darkness_dungeon/util/game_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/localization/strings_location.dart'; +import 'package:flutter/cupertino.dart'; + +class Door extends GameDecoration { + bool open = false; + bool showDialog = false; + + Door(Vector2 position, Vector2 size) + : super.withSprite( + sprite: Sprite.load('items/door_closed.png'), + position: position, + size: size, + ); + + @override + Future onLoad() { + add(RectangleHitbox( + size: Vector2(width, height / 4), + position: Vector2(0, height * 0.75), + )); + return super.onLoad(); + } + + @override + void onCollisionStart( + Set intersectionPoints, PositionComponent other) { + if (other is Knight) { + if (!open) { + Knight p = other; + if (p.containKey == true) { + open = true; + p.containKey = false; + playSpriteAnimationOnce( + GameSpriteSheet.openTheDoor(), + onFinish: removeFromParent, + ); + } else { + if (!showDialog) { + showDialog = true; + _showIntroduction(); + } + } + } + } + super.onCollisionStart(intersectionPoints, other); + } + + void _showIntroduction() { + TalkDialog.show( + gameRef.context, + [ + Say( + text: [TextSpan(text: getString('door_without_key'))], + person: (gameRef.player as SimplePlayer?) + ?.animation + ?.idleRight + ?.asWidget() ?? + SizedBox.shrink(), + personSayDirection: PersonSayDirection.LEFT, + ) + ], + onClose: () { + showDialog = false; + }, + ); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/decoration/key.dart b/flame/assets/examples/others/darkness_dungeon/lib/decoration/key.dart new file mode 100644 index 0000000..85a6d60 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/decoration/key.dart @@ -0,0 +1,20 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/main.dart'; +import 'package:darkness_dungeon/player/knight.dart'; + +class DoorKey extends GameDecoration with Sensor { + DoorKey(Vector2 position) + : super.withSprite( + sprite: Sprite.load('items/key_silver.png'), + position: position, + size: Vector2(tileSize, tileSize), + ); + + @override + void onContact(GameComponent collision) { + if (collision is Knight) { + collision.containKey = true; + removeFromParent(); + } + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/decoration/potion_life.dart b/flame/assets/examples/others/darkness_dungeon/lib/decoration/potion_life.dart new file mode 100644 index 0000000..2c43ff1 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/decoration/potion_life.dart @@ -0,0 +1,40 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/main.dart'; +import 'package:darkness_dungeon/player/knight.dart'; + +class PotionLife extends GameDecoration with Sensor { + final Vector2 initPosition; + final double life; + + bool hasContact = false; + + PotionLife(this.initPosition, this.life) + : super.withSprite( + sprite: Sprite.load('items/potion_red.png'), + position: initPosition, + size: Vector2(tileSize, tileSize), + ); + + @override + void onContact(Knight player) { + if (!hasContact) { + hasContact = true; + _giveLife(player); + removeFromParent(); + } + } + + void _giveLife(Player player) { + double _lifeDistributed = 0; + generateValues( + const Duration(seconds: 1), + onChange: (value) { + if (_lifeDistributed < life) { + double newLife = life * value - _lifeDistributed; + _lifeDistributed += newLife; + player.addLife(newLife); + } + }, + ); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/decoration/spikes.dart b/flame/assets/examples/others/darkness_dungeon/lib/decoration/spikes.dart new file mode 100644 index 0000000..2c834d1 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/decoration/spikes.dart @@ -0,0 +1,37 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/main.dart'; +import 'package:darkness_dungeon/player/knight.dart'; +import 'package:darkness_dungeon/util/game_sprite_sheet.dart'; + +class Spikes extends GameDecoration with Sensor { + final double damage; + Knight? player; + + Spikes(Vector2 position, {this.damage = 60}) + : super.withAnimation( + animation: GameSpriteSheet.spikes(), + position: position, + size: Vector2(tileSize, tileSize), + ); + + @override + void onContact(Knight collision) { + player = collision; + } + + @override + void update(double dt) { + if (isAnimationLastFrame) { + player?.receiveDamage(AttackFromEnum.ENEMY, damage, 0); + } + super.update(dt); + } + + @override + int get priority => LayerPriority.getComponentPriority(1); + + @override + void onContactExit(Knight component) { + player = null; + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/decoration/torch.dart b/flame/assets/examples/others/darkness_dungeon/lib/decoration/torch.dart new file mode 100644 index 0000000..64f7d56 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/decoration/torch.dart @@ -0,0 +1,30 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/main.dart'; +import 'package:darkness_dungeon/util/game_sprite_sheet.dart'; +import 'package:flutter/material.dart'; + +class Torch extends GameDecoration { + bool empty = false; + Torch(Vector2 position, {this.empty = false}) + : super.withAnimation( + animation: GameSpriteSheet.torch(), + position: position, + size: Vector2.all(tileSize), + ) { + setupLighting( + LightingConfig( + radius: width * 2.5, + blurBorder: width, + pulseVariation: 0.1, + color: Colors.deepOrangeAccent.withOpacity(0.2), + ), + ); + } + + @override + void render(Canvas canvas) { + if (!empty) { + super.render(canvas); + } + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/enemies/boss.dart b/flame/assets/examples/others/darkness_dungeon/lib/enemies/boss.dart new file mode 100644 index 0000000..a8b2fe8 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/enemies/boss.dart @@ -0,0 +1,286 @@ +import 'dart:async'; + +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/enemies/imp.dart'; +import 'package:darkness_dungeon/enemies/mini_boss.dart'; +import 'package:darkness_dungeon/main.dart'; +import 'package:darkness_dungeon/util/custom_sprite_animation_widget.dart'; +import 'package:darkness_dungeon/util/enemy_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/functions.dart'; +import 'package:darkness_dungeon/util/game_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/localization/strings_location.dart'; +import 'package:darkness_dungeon/util/npc_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/player_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/sounds.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class Boss extends SimpleEnemy with BlockMovementCollision, UseLifeBar { + final Vector2 initPosition; + double attack = 40; + + bool addChild = false; + bool firstSeePlayer = false; + List childrenEnemy = []; + + Boss(this.initPosition) + : super( + animation: EnemySpriteSheet.bossAnimations(), + position: initPosition, + size: Vector2(tileSize * 1.5, tileSize * 1.7), + speed: tileSize * 1.5, + life: 200, + ); + + @override + Future onLoad() { + add( + RectangleHitbox( + size: Vector2(valueByTileSize(14), valueByTileSize(16)), + position: Vector2(valueByTileSize(5), valueByTileSize(11)), + ), + ); + return super.onLoad(); + } + + @override + void render(Canvas canvas) { + drawBarSummonEnemy(canvas); + super.render(canvas); + } + + @override + void update(double dt) { + if (!firstSeePlayer) { + this.seePlayer( + observed: (p) { + firstSeePlayer = true; + gameRef.camera.moveToTargetAnimated( + target: this, + zoom: 2, + onComplete: _showConversation, + ); + }, + radiusVision: tileSize * 6, + ); + } + + if (life < 150 && childrenEnemy.length == 0) { + addChildInMap(dt); + } + + if (life < 100 && childrenEnemy.length == 1) { + addChildInMap(dt); + } + + if (life < 50 && childrenEnemy.length == 2) { + addChildInMap(dt); + } + + this.seeAndMoveToPlayer( + closePlayer: (player) { + execAttack(); + }, + radiusVision: tileSize * 4, + ); + + super.update(dt); + } + + @override + void die() { + gameRef.add( + AnimatedGameObject( + animation: GameSpriteSheet.explosion(), + position: this.position, + size: Vector2(32, 32), + loop: false, + ), + ); + childrenEnemy.forEach((e) { + if (!e.isDead) e.die(); + }); + removeFromParent(); + super.die(); + } + + void addChildInMap(double dt) { + if (checkInterval('addChild', 2000, dt)) { + Vector2 positionExplosion = Vector2.zero(); + + switch (this.directionThePlayerIsIn()) { + case Direction.left: + positionExplosion = this.position.translated(width * -2, 0); + break; + case Direction.right: + positionExplosion = this.position.translated(width * 2, 0); + break; + case Direction.up: + positionExplosion = this.position.translated(0, height * -2); + break; + case Direction.down: + positionExplosion = this.position.translated(0, height * 2); + break; + case Direction.upLeft: + case Direction.upRight: + case Direction.downLeft: + case Direction.downRight: + break; + default: + } + + Enemy e = childrenEnemy.length == 2 + ? MiniBoss( + Vector2( + positionExplosion.x, + positionExplosion.y, + ), + ) + : Imp( + Vector2( + positionExplosion.x, + positionExplosion.y, + ), + ); + + gameRef.add( + AnimatedGameObject( + animation: GameSpriteSheet.smokeExplosion(), + position: positionExplosion, + size: Vector2(32, 32), + loop: false, + ), + ); + + childrenEnemy.add(e); + gameRef.add(e); + } + } + + void execAttack() { + this.simpleAttackMelee( + size: Vector2.all(tileSize * 0.62), + damage: attack, + interval: 1500, + animationRight: EnemySpriteSheet.enemyAttackEffectRight(), + execute: () { + Sounds.attackEnemyMelee(); + }, + ); + } + + @override + void receiveDamage(AttackFromEnum attacker, double damage, dynamic id) { + this.showDamage( + damage, + config: TextStyle( + fontSize: valueByTileSize(5), + color: Colors.white, + fontFamily: 'Normal', + ), + ); + super.receiveDamage(attacker, damage, id); + } + + void drawBarSummonEnemy(Canvas canvas) { + double yPosition = 0; + double widthBar = (width - 10) / 3; + if (childrenEnemy.length < 1) + canvas.drawLine( + Offset(0, yPosition), + Offset(widthBar, yPosition), + Paint() + ..color = Colors.orange + ..strokeWidth = 1 + ..style = PaintingStyle.fill); + + double lastX = widthBar + 5; + if (childrenEnemy.length < 2) + canvas.drawLine( + Offset(lastX, yPosition), + Offset(lastX + widthBar, yPosition), + Paint() + ..color = Colors.orange + ..strokeWidth = 1 + ..style = PaintingStyle.fill); + + lastX = lastX + widthBar + 5; + if (childrenEnemy.length < 3) + canvas.drawLine( + Offset(lastX, yPosition), + Offset(lastX + widthBar, yPosition), + Paint() + ..color = Colors.orange + ..strokeWidth = 1 + ..style = PaintingStyle.fill); + } + + void _showConversation() { + Sounds.interaction(); + TalkDialog.show( + gameRef.context, + [ + Say( + text: [TextSpan(text: getString('talk_kid_1'))], + person: CustomSpriteAnimationWidget( + animation: NpcSpriteSheet.kidIdleLeft(), + ), + personSayDirection: PersonSayDirection.RIGHT, + ), + Say( + text: [TextSpan(text: getString('talk_boss_1'))], + person: CustomSpriteAnimationWidget( + animation: EnemySpriteSheet.bossIdleRight(), + ), + personSayDirection: PersonSayDirection.LEFT, + ), + Say( + text: [TextSpan(text: getString('talk_player_3'))], + person: CustomSpriteAnimationWidget( + animation: PlayerSpriteSheet.idleRight(), + ), + personSayDirection: PersonSayDirection.LEFT, + ), + Say( + text: [TextSpan(text: getString('talk_boss_2'))], + person: CustomSpriteAnimationWidget( + animation: EnemySpriteSheet.bossIdleRight(), + ), + personSayDirection: PersonSayDirection.RIGHT, + ), + ], + onFinish: () { + Sounds.interaction(); + addInitChild(); + Future.delayed(Duration(milliseconds: 500), () { + gameRef.camera.moveToPlayerAnimated(zoom: 1); + Sounds.playBackgroundBoosSound(); + }); + }, + onChangeTalk: (index) { + Sounds.interaction(); + }, + logicalKeyboardKeysToNext: [ + LogicalKeyboardKey.space, + ], + ); + } + + void addInitChild() { + addImp(width * -2, 0); + addImp(width * -2, width); + } + + void addImp(double x, double y) { + final p = position.translated(x, y); + gameRef.add( + AnimatedGameObject( + animation: GameSpriteSheet.smokeExplosion(), + position: p, + size: Vector2.all(tileSize), + loop: false, + ), + ); + gameRef.add(Imp(p)); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/enemies/goblin.dart b/flame/assets/examples/others/darkness_dungeon/lib/enemies/goblin.dart new file mode 100644 index 0000000..058c6b9 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/enemies/goblin.dart @@ -0,0 +1,84 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/main.dart'; +import 'package:darkness_dungeon/util/enemy_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/functions.dart'; +import 'package:darkness_dungeon/util/game_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/sounds.dart'; +import 'package:flutter/material.dart'; + +class Goblin extends SimpleEnemy with BlockMovementCollision, UseLifeBar { + final Vector2 initPosition; + double attack = 25; + + Goblin(this.initPosition) + : super( + animation: EnemySpriteSheet.goblinAnimations(), + position: initPosition, + size: Vector2.all(tileSize * 0.8), + speed: tileSize * 1.5, + life: 120, + ); + + @override + Future onLoad() { + add(RectangleHitbox( + size: Vector2( + valueByTileSize(7), + valueByTileSize(7), + ), + position: Vector2(valueByTileSize(3), valueByTileSize(4)), + )); + return super.onLoad(); + } + + @override + void update(double dt) { + super.update(dt); + + this.seeAndMoveToPlayer( + closePlayer: (player) { + execAttack(); + }, + radiusVision: tileSize * 4, + ); + } + + @override + void die() { + gameRef.add( + AnimatedGameObject( + animation: GameSpriteSheet.smokeExplosion(), + position: this.position, + size: Vector2(32, 32), + loop: false, + ), + ); + removeFromParent(); + super.die(); + } + + void execAttack() { + this.simpleAttackMelee( + size: Vector2.all(tileSize * 0.62), + damage: attack, + interval: 800, + animationRight: EnemySpriteSheet.enemyAttackEffectRight(), + execute: () { + Sounds.attackEnemyMelee(); + }, + ); + } + + @override + void receiveDamage(AttackFromEnum attacker, double damage, dynamic id) { + this.showDamage( + damage, + config: TextStyle( + fontSize: valueByTileSize(5), + color: Colors.white, + fontFamily: 'Normal', + ), + ); + super.receiveDamage(attacker, damage, id); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/enemies/imp.dart b/flame/assets/examples/others/darkness_dungeon/lib/enemies/imp.dart new file mode 100644 index 0000000..d19ff09 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/enemies/imp.dart @@ -0,0 +1,88 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/main.dart'; +import 'package:darkness_dungeon/util/enemy_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/functions.dart'; +import 'package:darkness_dungeon/util/game_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/sounds.dart'; +import 'package:flutter/material.dart'; + +class Imp extends SimpleEnemy with BlockMovementCollision, UseLifeBar { + final Vector2 initPosition; + double attack = 10; + + Imp(this.initPosition) + : super( + animation: EnemySpriteSheet.impAnimations(), + position: initPosition, + size: Vector2.all(tileSize * 0.8), + speed: tileSize * 2, + life: 80, + ); + + @override + Future onLoad() { + add( + RectangleHitbox( + size: Vector2( + valueByTileSize(6), + valueByTileSize(6), + ), + position: Vector2( + valueByTileSize(3), + valueByTileSize(5), + ), + ), + ); + return super.onLoad(); + } + + @override + void update(double dt) { + super.update(dt); + this.seeAndMoveToPlayer( + radiusVision: tileSize * 5, + closePlayer: (player) { + execAttack(); + }, + ); + } + + void execAttack() { + this.simpleAttackMelee( + size: Vector2.all(tileSize * 0.62), + damage: attack, + interval: 300, + animationRight: EnemySpriteSheet.enemyAttackEffectRight(), + execute: () { + Sounds.attackEnemyMelee(); + }, + ); + } + + @override + void die() { + gameRef.add( + AnimatedGameObject( + animation: GameSpriteSheet.smokeExplosion(), + position: this.position, + size: Vector2(32, 32), + loop: false, + ), + ); + removeFromParent(); + super.die(); + } + + @override + void receiveDamage(AttackFromEnum attacker, double damage, dynamic id) { + this.showDamage( + damage, + config: TextStyle( + fontSize: valueByTileSize(5), + color: Colors.white, + fontFamily: 'Normal', + ), + ); + super.receiveDamage(attacker, damage, id); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/enemies/mini_boss.dart b/flame/assets/examples/others/darkness_dungeon/lib/enemies/mini_boss.dart new file mode 100644 index 0000000..57bcfa1 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/enemies/mini_boss.dart @@ -0,0 +1,123 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/main.dart'; +import 'package:darkness_dungeon/util/enemy_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/functions.dart'; +import 'package:darkness_dungeon/util/game_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/sounds.dart'; +import 'package:flutter/material.dart'; + +class MiniBoss extends SimpleEnemy with BlockMovementCollision, UseLifeBar { + final Vector2 initPosition; + double attack = 50; + bool _seePlayerClose = false; + + MiniBoss(this.initPosition) + : super( + animation: EnemySpriteSheet.miniBossAnimations(), + position: initPosition, + size: Vector2(tileSize * 0.68, tileSize * 0.93), + speed: tileSize * 1.5, + life: 150, + ); + + @override + Future onLoad() { + add( + RectangleHitbox( + size: Vector2(valueByTileSize(6), valueByTileSize(7)), + position: Vector2(valueByTileSize(2.5), valueByTileSize(8)), + ), + ); + return super.onLoad(); + } + + @override + void update(double dt) { + super.update(dt); + _seePlayerClose = false; + this.seePlayer( + observed: (player) { + _seePlayerClose = true; + this.seeAndMoveToPlayer( + closePlayer: (player) { + execAttack(); + }, + radiusVision: tileSize * 3, + ); + }, + radiusVision: tileSize * 3, + ); + if (!_seePlayerClose) { + this.seeAndMoveToAttackRange( + positioned: (p) { + execAttackRange(); + }, + radiusVision: tileSize * 5, + ); + } + } + + @override + void die() { + gameRef.add( + AnimatedGameObject( + animation: GameSpriteSheet.smokeExplosion(), + position: this.position, + size: Vector2(32, 32), + loop: false, + ), + ); + removeFromParent(); + super.die(); + } + + void execAttackRange() { + this.simpleAttackRange( + animationRight: GameSpriteSheet.fireBallAttackRight(), + animationDestroy: GameSpriteSheet.fireBallExplosion(), + size: Vector2.all(tileSize * 0.65), + damage: attack, + speed: speed * 2.5, + execute: () { + Sounds.attackRange(); + }, + onDestroy: () { + Sounds.explosion(); + }, + collision: RectangleHitbox( + size: Vector2(tileSize / 3, tileSize / 3), + position: Vector2(10, 5), + ), + lightingConfig: LightingConfig( + radius: tileSize * 0.9, + blurBorder: tileSize / 2, + color: Colors.deepOrangeAccent.withOpacity(0.4), + ), + ); + } + + void execAttack() { + this.simpleAttackMelee( + size: Vector2.all(tileSize * 0.62), + damage: attack / 3, + interval: 300, + animationRight: EnemySpriteSheet.enemyAttackEffectRight(), + execute: () { + Sounds.attackEnemyMelee(); + }, + ); + } + + @override + void receiveDamage(AttackFromEnum attacker, double damage, dynamic id) { + this.showDamage( + damage, + config: TextStyle( + fontSize: valueByTileSize(5), + color: Colors.white, + fontFamily: 'Normal', + ), + ); + super.receiveDamage(attacker, damage, id); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/game.dart b/flame/assets/examples/others/darkness_dungeon/lib/game.dart new file mode 100644 index 0000000..0db867f --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/game.dart @@ -0,0 +1,129 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/decoration/door.dart'; +import 'package:darkness_dungeon/decoration/key.dart'; +import 'package:darkness_dungeon/decoration/potion_life.dart'; +import 'package:darkness_dungeon/decoration/spikes.dart'; +import 'package:darkness_dungeon/decoration/torch.dart'; +import 'package:darkness_dungeon/enemies/boss.dart'; +import 'package:darkness_dungeon/enemies/goblin.dart'; +import 'package:darkness_dungeon/enemies/imp.dart'; +import 'package:darkness_dungeon/enemies/mini_boss.dart'; +import 'package:darkness_dungeon/interface/knight_interface.dart'; +import 'package:darkness_dungeon/main.dart'; +import 'package:darkness_dungeon/npc/kid.dart'; +import 'package:darkness_dungeon/npc/wizard_npc.dart'; +import 'package:darkness_dungeon/player/knight.dart'; +import 'package:darkness_dungeon/util/sounds.dart'; +import 'package:darkness_dungeon/widgets/game_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class Game extends StatefulWidget { + static bool useJoystick = true; + const Game({Key? key}) : super(key: key); + + @override + _GameState createState() => _GameState(); +} + +class _GameState extends State { + @override + void initState() { + Sounds.playBackgroundSound(); + super.initState(); + } + + @override + void dispose() { + Sounds.stopBackgroundSound(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var joystick = Joystick( + directional: JoystickDirectional( + spriteBackgroundDirectional: Sprite.load('joystick_background.png'), + spriteKnobDirectional: Sprite.load('joystick_knob.png'), + size: 100, + isFixed: false, + ), + actions: [ + JoystickAction( + actionId: 0, + sprite: Sprite.load('joystick_atack.png'), + spritePressed: Sprite.load('joystick_atack_selected.png'), + size: 80, + margin: EdgeInsets.only(bottom: 50, right: 50), + ), + JoystickAction( + actionId: 1, + sprite: Sprite.load('joystick_atack_range.png'), + spritePressed: Sprite.load('joystick_atack_range_selected.png'), + size: 50, + margin: EdgeInsets.only(bottom: 50, right: 160), + ) + ], + ); + if (!Game.useJoystick) { + joystick = Joystick( + keyboardConfig: KeyboardConfig( + directionalKeys: KeyboardDirectionalKeys.arrows(), + acceptedKeys: [ + LogicalKeyboardKey.space, + LogicalKeyboardKey.keyZ, + ], + ), + ); + } + + return Material( + color: Colors.transparent, + child: BonfireWidget( + joystick: joystick, + player: Knight( + Vector2(2 * tileSize, 3 * tileSize), + ), + map: WorldMapByTiled( + 'tiled/map.json', + forceTileSize: Vector2(tileSize, tileSize), + objectsBuilder: { + 'door': (p) => Door(p.position, p.size), + 'torch': (p) => Torch(p.position), + 'potion': (p) => PotionLife(p.position, 30), + 'wizard': (p) => WizardNPC(p.position), + 'spikes': (p) => Spikes(p.position), + 'key': (p) => DoorKey(p.position), + 'kid': (p) => Kid(p.position), + 'boss': (p) => Boss(p.position), + 'goblin': (p) => Goblin(p.position), + 'imp': (p) => Imp(p.position), + 'mini_boss': (p) => MiniBoss(p.position), + 'torch_empty': (p) => Torch(p.position, empty: true), + }, + ), + components: [GameController()], + interface: KnightInterface(), + lightingColorGame: Colors.black.withOpacity(0.6), + backgroundColor: Colors.grey[900]!, + cameraConfig: CameraConfig( + speed: 3, + zoom: getZoomFromMaxVisibleTile(context, tileSize, 18), + ), + progress: Container( + color: Colors.black, + child: Center( + child: Text( + "Loading...", + style: TextStyle( + color: Colors.white, + fontFamily: 'Normal', + fontSize: 20.0, + ), + ), + ), + ), + ), + ); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/generated_plugin_registrant.dart b/flame/assets/examples/others/darkness_dungeon/lib/generated_plugin_registrant.dart new file mode 100644 index 0000000..e56076b --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/generated_plugin_registrant.dart @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// ignore_for_file: directives_ordering +// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: depend_on_referenced_packages + +import 'package:audioplayers_web/audioplayers_web.dart'; +import 'package:url_launcher_web/url_launcher_web.dart'; + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +// ignore: public_member_api_docs +void registerPlugins(Registrar registrar) { + AudioplayersPlugin.registerWith(registrar); + UrlLauncherPlugin.registerWith(registrar); + registrar.registerMessageHandler(); +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/interface/bar_life_component.dart b/flame/assets/examples/others/darkness_dungeon/lib/interface/bar_life_component.dart new file mode 100644 index 0000000..14d7f35 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/interface/bar_life_component.dart @@ -0,0 +1,91 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/player/knight.dart'; +import 'package:flutter/material.dart'; + +class MyBarLifeComponent extends InterfaceComponent { + double padding = 20; + double widthBar = 90; + double strokeWidth = 12; + + double maxLife = 0; + double life = 0; + double maxStamina = 100; + double stamina = 0; + + MyBarLifeComponent() + : super( + id: 1, + position: Vector2(20, 20), + spriteUnselected: Sprite.load('health_ui.png'), + size: Vector2(120, 40), + ); + + @override + void update(double t) { + if (this.gameRef.player != null) { + life = this.gameRef.player!.life; + maxLife = this.gameRef.player!.maxLife; + if (this.gameRef.player is Knight) { + stamina = (this.gameRef.player as Knight).stamina; + } + } + super.update(t); + } + + @override + void render(Canvas c) { + try { + _drawLife(c); + _drawStamina(c); + } catch (e) {} + super.render(c); + } + + void _drawLife(Canvas canvas) { + double xBar = 29; + double yBar = 10; + canvas.drawLine( + Offset(xBar, yBar), + Offset(xBar + widthBar, yBar), + Paint() + ..color = Colors.blueGrey[800]! + ..strokeWidth = strokeWidth + ..style = PaintingStyle.fill); + + double currentBarLife = (life * widthBar) / maxLife; + + canvas.drawLine( + Offset(xBar, yBar), + Offset(xBar + currentBarLife, yBar), + Paint() + ..color = _getColorLife(currentBarLife) + ..strokeWidth = strokeWidth + ..style = PaintingStyle.fill); + } + + void _drawStamina(Canvas canvas) { + double xBar = 29; + double yBar = 27; + + double currentBarStamina = (stamina * widthBar) / maxStamina; + + canvas.drawLine( + Offset(xBar, yBar), + Offset(xBar + currentBarStamina, yBar), + Paint() + ..color = Colors.yellow + ..strokeWidth = strokeWidth + ..style = PaintingStyle.fill); + } + + Color _getColorLife(double currentBarLife) { + if (currentBarLife > widthBar - (widthBar / 3)) { + return Colors.green; + } + if (currentBarLife > (widthBar / 3)) { + return Colors.yellow; + } else { + return Colors.red; + } + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/interface/knight_interface.dart b/flame/assets/examples/others/darkness_dungeon/lib/interface/knight_interface.dart new file mode 100644 index 0000000..dc72836 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/interface/knight_interface.dart @@ -0,0 +1,28 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/interface/bar_life_component.dart'; +import 'package:darkness_dungeon/player/knight.dart'; + +class KnightInterface extends GameInterface { + late Sprite key; + + @override + Future onLoad() async { + key = await Sprite.load('items/key_silver.png'); + add(MyBarLifeComponent()); + return super.onLoad(); + } + + @override + void render(Canvas canvas) { + try { + _drawKey(canvas); + } catch (e) {} + super.render(canvas); + } + + void _drawKey(Canvas c) { + if (gameRef.player != null && (gameRef.player as Knight).containKey) { + key.renderRect(c, Rect.fromLTWH(150, 20, 35, 30)); + } + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/main.dart b/flame/assets/examples/others/darkness_dungeon/lib/main.dart new file mode 100644 index 0000000..fea3938 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/main.dart @@ -0,0 +1,37 @@ +import 'package:darkness_dungeon/menu.dart'; +import 'package:darkness_dungeon/util/localization/my_localizations_delegate.dart'; +import 'package:flame/flame.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +import 'util/sounds.dart'; + +double tileSize = 32; +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + if (!kIsWeb) { + await Flame.device.setLandscape(); + await Flame.device.fullScreen(); + } + await Sounds.initialize(); + MyLocalizationsDelegate myLocation = const MyLocalizationsDelegate(); + runApp( + MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData( + fontFamily: 'Normal', + ), + home: Menu(), + supportedLocales: MyLocalizationsDelegate.supportedLocales(), + localizationsDelegates: [ + myLocation, + DefaultCupertinoLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + localeResolutionCallback: myLocation.resolution, + ), + ); +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/menu.dart b/flame/assets/examples/others/darkness_dungeon/lib/menu.dart new file mode 100644 index 0000000..013252e --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/menu.dart @@ -0,0 +1,241 @@ +import 'dart:async' as async; + +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/game.dart'; +import 'package:darkness_dungeon/util/custom_sprite_animation_widget.dart'; +import 'package:darkness_dungeon/util/enemy_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/localization/strings_location.dart'; +import 'package:darkness_dungeon/util/player_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/sounds.dart'; +import 'package:darkness_dungeon/widgets/custom_radio.dart'; +import 'package:flame_splash_screen/flame_splash_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class Menu extends StatefulWidget { + @override + _MenuState createState() => _MenuState(); +} + +class _MenuState extends State { + bool showSplash = true; + int currentPosition = 0; + late async.Timer _timer; + List> sprites = [ + PlayerSpriteSheet.idleRight(), + EnemySpriteSheet.goblinIdleRight(), + EnemySpriteSheet.impIdleRight(), + EnemySpriteSheet.miniBossIdleRight(), + EnemySpriteSheet.bossIdleRight(), + ]; + + @override + void dispose() { + Sounds.stopBackgroundSound(); + _timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: showSplash ? buildSplash() : buildMenu(), + ); + } + + Widget buildMenu() { + return Scaffold( + backgroundColor: Colors.black, + body: Center( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Darkness Dungeon", + style: TextStyle( + color: Colors.white, fontFamily: 'Normal', fontSize: 30.0), + ), + SizedBox( + height: 20.0, + ), + if (sprites.isNotEmpty) + SizedBox( + height: 100, + width: 100, + child: CustomSpriteAnimationWidget( + animation: sprites[currentPosition], + ), + ), + SizedBox( + height: 30.0, + ), + SizedBox( + width: 150, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0), + ), + minimumSize: Size(100, 40), //////// HERE + ), + child: Text( + getString('play_cap'), + style: TextStyle( + color: Colors.white, + fontFamily: 'Normal', + fontSize: 17.0, + ), + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => Game()), + ); + }, + ), + ), + SizedBox( + height: 20, + ), + DefectorRadio( + value: false, + label: 'Keyboard', + group: Game.useJoystick, + onChange: (value) { + setState(() { + Game.useJoystick = value; + }); + }, + ), + SizedBox( + height: 10, + ), + DefectorRadio( + value: true, + group: Game.useJoystick, + label: 'Joystick', + onChange: (value) { + setState(() { + Game.useJoystick = value; + }); + }, + ), + SizedBox( + height: 20, + ), + if (!Game.useJoystick) + SizedBox( + height: 80, + width: 200, + child: Sprite.load('keyboard_tip.png').asWidget(), + ), + ], + ), + ), + ), + bottomNavigationBar: SafeArea( + child: Container( + height: 20, + margin: EdgeInsets.all(20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + getString('powered_by'), + style: TextStyle( + color: Colors.white, + fontFamily: 'Normal', + fontSize: 12.0), + ), + InkWell( + onTap: () { + _launchURL('https://github.com/RafaelBarbosatec'); + }, + child: Text( + 'rafaelbarbosatec', + style: TextStyle( + decoration: TextDecoration.underline, + color: Colors.blue, + fontFamily: 'Normal', + fontSize: 12.0, + ), + ), + ) + ], + ), + ), + Flexible( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + getString('built_with'), + style: TextStyle( + color: Colors.white, + fontFamily: 'Normal', + fontSize: 12.0), + ), + InkWell( + onTap: () { + _launchURL( + 'https://github.com/RafaelBarbosatec/bonfire'); + }, + child: Text( + 'Bonfire', + style: TextStyle( + decoration: TextDecoration.underline, + color: Colors.blue, + fontFamily: 'Normal', + fontSize: 12.0, + ), + ), + ) + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget buildSplash() { + return FlameSplashScreen( + theme: FlameSplashTheme.dark, + onFinish: (BuildContext context) { + setState(() { + showSplash = false; + }); + startTimer(); + }, + ); + } + + void startTimer() { + _timer = async.Timer.periodic(Duration(seconds: 2), (timer) { + setState(() { + currentPosition++; + if (currentPosition > sprites.length - 1) { + currentPosition = 0; + } + }); + }); + } + + void _launchURL(String url) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } else { + throw 'Could not launch $url'; + } + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/npc/kid.dart b/flame/assets/examples/others/darkness_dungeon/lib/npc/kid.dart new file mode 100644 index 0000000..25083d2 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/npc/kid.dart @@ -0,0 +1,76 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/enemies/boss.dart'; +import 'package:darkness_dungeon/util/custom_sprite_animation_widget.dart'; +import 'package:darkness_dungeon/util/dialogs.dart'; +import 'package:darkness_dungeon/util/functions.dart'; +import 'package:darkness_dungeon/util/localization/strings_location.dart'; +import 'package:darkness_dungeon/util/npc_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/player_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/sounds.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class Kid extends GameDecoration { + bool conversationWithHero = false; + + Kid( + Vector2 position, + ) : super.withAnimation( + animation: NpcSpriteSheet.kidIdleLeft(), + position: position, + size: Vector2(valueByTileSize(8), valueByTileSize(11)), + ); + + @override + void update(double dt) { + super.update(dt); + if (!conversationWithHero && checkInterval('checkBossDead', 1000, dt)) { + try { + gameRef.enemies().firstWhere((e) => e is Boss); + } catch (e) { + conversationWithHero = true; + gameRef.camera.moveToTargetAnimated( + target: this, + onComplete: () { + _startConversation(); + }, + ); + } + } + } + + void _startConversation() { + Sounds.interaction(); + TalkDialog.show( + gameRef.context, + [ + Say( + text: [TextSpan(text: getString('talk_kid_2'))], + person: CustomSpriteAnimationWidget( + animation: NpcSpriteSheet.kidIdleLeft(), + ), + personSayDirection: PersonSayDirection.RIGHT, + ), + Say( + text: [TextSpan(text: getString('talk_player_4'))], + person: CustomSpriteAnimationWidget( + animation: PlayerSpriteSheet.idleRight(), + ), + personSayDirection: PersonSayDirection.LEFT, + ), + ], + onFinish: () { + Sounds.interaction(); + gameRef.camera.moveToPlayerAnimated(onComplete: () { + Dialogs.showCongratulations(gameRef.context); + }); + }, + onChangeTalk: (index) { + Sounds.interaction(); + }, + logicalKeyboardKeysToNext: [ + LogicalKeyboardKey.space, + ], + ); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/npc/wizard_npc.dart b/flame/assets/examples/others/darkness_dungeon/lib/npc/wizard_npc.dart new file mode 100644 index 0000000..568ee45 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/npc/wizard_npc.dart @@ -0,0 +1,119 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/main.dart'; +import 'package:darkness_dungeon/util/custom_sprite_animation_widget.dart'; +import 'package:darkness_dungeon/util/localization/strings_location.dart'; +import 'package:darkness_dungeon/util/npc_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/player_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/sounds.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class WizardNPC extends SimpleNpc { + bool _showConversation = false; + WizardNPC( + Vector2 position, + ) : super( + animation: SimpleDirectionAnimation( + idleRight: NpcSpriteSheet.wizardIdleLeft(), + runRight: NpcSpriteSheet.wizardIdleLeft(), + ), + position: position, + size: Vector2( + tileSize * 0.8, + tileSize, + ), + ); + + @override + void update(double dt) { + super.update(dt); + if (gameRef.player != null) { + this.seeComponent( + gameRef.player!, + observed: (player) { + if (!_showConversation) { + gameRef.player!.idle(); + _showConversation = true; + _showEmote(emote: 'emote/emote_interregacao.png'); + _showIntroduction(); + } + }, + radiusVision: (2 * tileSize), + ); + } + } + + void _showEmote({String emote = 'emote/emote_exclamacao.png'}) { + gameRef.add( + AnimatedFollowerGameObject( + animation: SpriteAnimation.load( + emote, + SpriteAnimationData.sequenced( + amount: 8, + stepTime: 0.1, + textureSize: Vector2(32, 32), + ), + ), + loop: false, + target: this, + offset: Vector2(18, -6), + size: Vector2.all(tileSize / 2), + ), + ); + } + + void _showIntroduction() { + Sounds.interaction(); + TalkDialog.show( + gameRef.context, + [ + Say( + text: [ + TextSpan(text: getString('talk_wizard_1')), + ], + person: CustomSpriteAnimationWidget( + animation: NpcSpriteSheet.wizardIdleLeft(), + ), + personSayDirection: PersonSayDirection.RIGHT, + ), + Say( + text: [TextSpan(text: getString('talk_player_1'))], + person: CustomSpriteAnimationWidget( + animation: PlayerSpriteSheet.idleRight(), + ), + personSayDirection: PersonSayDirection.LEFT, + ), + Say( + text: [TextSpan(text: getString('talk_wizard_2'))], + person: CustomSpriteAnimationWidget( + animation: NpcSpriteSheet.wizardIdleLeft(), + ), + personSayDirection: PersonSayDirection.RIGHT, + ), + Say( + text: [TextSpan(text: getString('talk_player_2'))], + person: CustomSpriteAnimationWidget( + animation: PlayerSpriteSheet.idleRight(), + ), + personSayDirection: PersonSayDirection.LEFT, + ), + Say( + text: [TextSpan(text: getString('talk_wizard_3'))], + person: CustomSpriteAnimationWidget( + animation: NpcSpriteSheet.wizardIdleLeft(), + ), + personSayDirection: PersonSayDirection.RIGHT, + ), + ], + onChangeTalk: (index) { + Sounds.interaction(); + }, + onFinish: () { + Sounds.interaction(); + }, + logicalKeyboardKeysToNext: [ + LogicalKeyboardKey.space, + ], + ); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/player/knight.dart b/flame/assets/examples/others/darkness_dungeon/lib/player/knight.dart new file mode 100644 index 0000000..8384a68 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/player/knight.dart @@ -0,0 +1,211 @@ +import 'dart:async' as async; + +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/main.dart'; +import 'package:darkness_dungeon/util/functions.dart'; +import 'package:darkness_dungeon/util/game_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/player_sprite_sheet.dart'; +import 'package:darkness_dungeon/util/sounds.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class Knight extends SimplePlayer with Lighting, BlockMovementCollision { + double attack = 25; + double stamina = 100; + async.Timer? _timerStamina; + bool containKey = false; + bool showObserveEnemy = false; + + Knight(Vector2 position) + : super( + animation: PlayerSpriteSheet.playerAnimations(), + size: Vector2.all(tileSize), + position: position, + life: 200, + speed: tileSize * 2.5, + ) { + setupLighting( + LightingConfig( + radius: width * 1.5, + blurBorder: width, + color: Colors.deepOrangeAccent.withOpacity(0.2), + ), + ); + setupMovementByJoystick( + intencityEnabled: true, + ); + } + + @override + async.Future onLoad() { + add( + RectangleHitbox( + size: Vector2(valueByTileSize(8), valueByTileSize(8)), + position: Vector2( + valueByTileSize(4), + valueByTileSize(8), + ), + ), + ); + return super.onLoad(); + } + + @override + void onJoystickAction(JoystickActionEvent event) { + if (event.id == 0 && event.event == ActionEvent.DOWN) { + actionAttack(); + } + + if (event.id == LogicalKeyboardKey.space.keyId && + event.event == ActionEvent.DOWN) { + actionAttack(); + } + + if (event.id == LogicalKeyboardKey.keyZ.keyId && + event.event == ActionEvent.DOWN) { + actionAttackRange(); + } + + if (event.id == 1 && event.event == ActionEvent.DOWN) { + actionAttackRange(); + } + super.onJoystickAction(event); + } + + @override + void die() { + removeFromParent(); + gameRef.add( + GameDecoration.withSprite( + sprite: Sprite.load('player/crypt.png'), + position: Vector2( + this.position.x, + this.position.y, + ), + size: Vector2.all(30), + ), + ); + super.die(); + } + + void actionAttack() { + if (stamina < 15) { + return; + } + + Sounds.attackPlayerMelee(); + decrementStamina(15); + this.simpleAttackMelee( + damage: attack, + animationRight: PlayerSpriteSheet.attackEffectRight(), + size: Vector2.all(tileSize), + ); + } + + void actionAttackRange() { + if (stamina < 10) { + return; + } + + Sounds.attackRange(); + + decrementStamina(10); + this.simpleAttackRange( + animationRight: GameSpriteSheet.fireBallAttackRight(), + animationDestroy: GameSpriteSheet.fireBallExplosion(), + size: Vector2(tileSize * 0.65, tileSize * 0.65), + damage: 10, + speed: speed * 2.5, + onDestroy: () { + Sounds.explosion(); + }, + collision: RectangleHitbox( + size: Vector2(tileSize / 3, tileSize / 3), + position: Vector2(10, 5), + ), + lightingConfig: LightingConfig( + radius: tileSize * 0.9, + blurBorder: tileSize / 2, + color: Colors.deepOrangeAccent.withOpacity(0.4), + ), + ); + } + + @override + void update(double dt) { + if (isDead) return; + _verifyStamina(); + this.seeEnemy( + radiusVision: tileSize * 6, + notObserved: () { + showObserveEnemy = false; + }, + observed: (enemies) { + if (showObserveEnemy) return; + showObserveEnemy = true; + _showEmote(); + }, + ); + super.update(dt); + } + + @override + void render(Canvas c) { + super.render(c); + } + + void _verifyStamina() { + if (_timerStamina == null) { + _timerStamina = async.Timer(Duration(milliseconds: 150), () { + _timerStamina = null; + }); + } else { + return; + } + + stamina += 2; + if (stamina > 100) { + stamina = 100; + } + } + + void decrementStamina(int i) { + stamina -= i; + if (stamina < 0) { + stamina = 0; + } + } + + @override + void receiveDamage(AttackFromEnum attacker, double damage, dynamic id) { + if (isDead) return; + this.showDamage( + damage, + config: TextStyle( + fontSize: valueByTileSize(5), + color: Colors.orange, + fontFamily: 'Normal', + ), + ); + super.receiveDamage(attacker, damage, id); + } + + void _showEmote({String emote = 'emote/emote_exclamacao.png'}) { + gameRef.add( + AnimatedFollowerGameObject( + animation: SpriteAnimation.load( + emote, + SpriteAnimationData.sequenced( + amount: 8, + stepTime: 0.1, + textureSize: Vector2(32, 32), + ), + ), + target: this, + loop: false, + size: Vector2.all(tileSize / 2), + offset: Vector2(18, -6), + ), + ); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/util/custom_sprite_animation_widget.dart b/flame/assets/examples/others/darkness_dungeon/lib/util/custom_sprite_animation_widget.dart new file mode 100644 index 0000000..70b668a --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/util/custom_sprite_animation_widget.dart @@ -0,0 +1,17 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:flutter/material.dart'; + +class CustomSpriteAnimationWidget extends StatelessWidget { + final Future animation; + + const CustomSpriteAnimationWidget({Key? key, required this.animation}) + : super(key: key); + @override + Widget build(BuildContext context) { + return SizedBox( + width: 100, + height: 100, + child: animation.asWidget(), + ); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/util/dialogs.dart b/flame/assets/examples/others/darkness_dungeon/lib/util/dialogs.dart new file mode 100644 index 0000000..3280303 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/util/dialogs.dart @@ -0,0 +1,106 @@ +import 'package:darkness_dungeon/menu.dart'; +import 'package:darkness_dungeon/util/localization/strings_location.dart'; +import 'package:flutter/material.dart'; + +class Dialogs { + static void showGameOver(BuildContext context, VoidCallback playAgain) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + 'assets/game_over.png', + height: 100, + ), + SizedBox( + height: 10.0, + ), + ElevatedButton( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(Colors.transparent), + ), + onPressed: playAgain, + child: Text( + getString('play_again_cap'), + style: TextStyle( + color: Colors.white, + fontFamily: 'Normal', + fontSize: 20.0), + ), + ) + ], + ), + ); + }, + ); + } + + static void showCongratulations(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return Material( + color: Colors.transparent, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + getString('congratulations'), + style: TextStyle( + color: Colors.white, + fontFamily: 'Normal', + fontSize: 30.0), + ), + SizedBox( + height: 10.0, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 100), + child: Text( + getString('thanks'), + style: TextStyle( + color: Colors.white, + fontFamily: 'Normal', + fontSize: 18.0), + textAlign: TextAlign.center, + ), + ), + SizedBox( + height: 30.0, + ), + ElevatedButton( + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Color.fromARGB(255, 118, 82, 78)), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0)), + ), + ), + child: Text("OK", + style: TextStyle( + color: Colors.white, + fontFamily: 'Normal', + fontSize: 17.0)), + onPressed: () { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (context) => Menu()), + (Route route) => false, + ); + }, + ) + ], + ), + ), + ); + }, + ); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/util/enemy_sprite_sheet.dart b/flame/assets/examples/others/darkness_dungeon/lib/util/enemy_sprite_sheet.dart new file mode 100644 index 0000000..8f128bc --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/util/enemy_sprite_sheet.dart @@ -0,0 +1,211 @@ +import 'package:bonfire/bonfire.dart'; + +class EnemySpriteSheet { + static Future enemyAttackEffectBottom() => + SpriteAnimation.load( + 'enemy/atack_effect_bottom.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + + static Future enemyAttackEffectLeft() => + SpriteAnimation.load( + 'enemy/atack_effect_left.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + static Future enemyAttackEffectRight() => + SpriteAnimation.load( + 'enemy/atack_effect_right.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + static Future enemyAttackEffectTop() => SpriteAnimation.load( + 'enemy/atack_effect_top.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + + static Future bossIdleRight() => SpriteAnimation.load( + 'enemy/boss/boss_idle.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(32, 36), + ), + ); + + static SimpleDirectionAnimation bossAnimations() => SimpleDirectionAnimation( + idleLeft: SpriteAnimation.load( + 'enemy/boss/boss_idle_left.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(32, 36), + ), + ), + idleRight: bossIdleRight(), + runLeft: SpriteAnimation.load( + 'enemy/boss/boss_run_left.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(32, 36), + ), + ), + runRight: SpriteAnimation.load( + 'enemy/boss/boss_run_right.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(32, 36), + ), + ), + ); + + static Future goblinIdleRight() => SpriteAnimation.load( + 'enemy/goblin/goblin_idle.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + + static SimpleDirectionAnimation goblinAnimations() => + SimpleDirectionAnimation( + idleLeft: SpriteAnimation.load( + 'enemy/goblin/goblin_idle_left.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ), + idleRight: SpriteAnimation.load( + 'enemy/goblin/goblin_idle.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ), + runLeft: SpriteAnimation.load( + 'enemy/goblin/goblin_run_left.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ), + runRight: SpriteAnimation.load( + 'enemy/goblin/goblin_run_right.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ), + ); + + static Future impIdleRight() => SpriteAnimation.load( + 'enemy/imp/imp_idle.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + + static SimpleDirectionAnimation impAnimations() => SimpleDirectionAnimation( + idleLeft: SpriteAnimation.load( + 'enemy/imp/imp_idle_left.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ), + idleRight: SpriteAnimation.load( + 'enemy/imp/imp_idle.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ), + runLeft: SpriteAnimation.load( + 'enemy/imp/imp_run_left.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ), + runRight: SpriteAnimation.load( + 'enemy/imp/imp_run_right.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ), + ); + + static Future miniBossIdleRight() => SpriteAnimation.load( + 'enemy/mini_boss/mini_boss_idle.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(16, 24), + ), + ); + + static SimpleDirectionAnimation miniBossAnimations() => + SimpleDirectionAnimation( + idleLeft: SpriteAnimation.load( + 'enemy/mini_boss/mini_boss_idle_left.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(16, 24), + ), + ), + idleRight: SpriteAnimation.load( + 'enemy/mini_boss/mini_boss_idle.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(16, 24), + ), + ), + runLeft: SpriteAnimation.load( + 'enemy/mini_boss/mini_boss_run_left.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(16, 24), + ), + ), + runRight: SpriteAnimation.load( + 'enemy/mini_boss/mini_boss_run_right.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(16, 24), + ), + ), + ); +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/util/functions.dart b/flame/assets/examples/others/darkness_dungeon/lib/util/functions.dart new file mode 100644 index 0000000..f07707a --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/util/functions.dart @@ -0,0 +1,7 @@ +import '../main.dart'; + +const TILE_SIZE_SPRITE_SHEET = 16; + +double valueByTileSize(double value) { + return value * (tileSize / TILE_SIZE_SPRITE_SHEET); +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/util/game_sprite_sheet.dart b/flame/assets/examples/others/darkness_dungeon/lib/util/game_sprite_sheet.dart new file mode 100644 index 0000000..0bee619 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/util/game_sprite_sheet.dart @@ -0,0 +1,91 @@ +import 'package:bonfire/bonfire.dart'; + +class GameSpriteSheet { + static Future openTheDoor() => SpriteAnimation.load( + 'items/door_open.png', + SpriteAnimationData.sequenced( + amount: 14, + stepTime: 0.1, + textureSize: Vector2(32, 32), + ), + ); + static Future spikes() => SpriteAnimation.load( + 'items/spikes.png', + SpriteAnimationData.sequenced( + amount: 10, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + static Future torch() => SpriteAnimation.load( + 'items/torch_spritesheet.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + + static Future explosion() => SpriteAnimation.load( + 'explosion.png', + SpriteAnimationData.sequenced( + amount: 7, + stepTime: 0.1, + textureSize: Vector2(32, 32), + ), + ); + + static Future smokeExplosion() => SpriteAnimation.load( + 'smoke_explosin.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + + static Future fireBallAttackRight() => SpriteAnimation.load( + 'player/fireball_right.png', + SpriteAnimationData.sequenced( + amount: 3, + stepTime: 0.1, + textureSize: Vector2(23, 23), + ), + ); + + static Future fireBallAttackLeft() => SpriteAnimation.load( + 'player/fireball_left.png', + SpriteAnimationData.sequenced( + amount: 3, + stepTime: 0.1, + textureSize: Vector2(23, 23), + ), + ); + + static Future fireBallAttackTop() => SpriteAnimation.load( + 'player/fireball_top.png', + SpriteAnimationData.sequenced( + amount: 3, + stepTime: 0.1, + textureSize: Vector2(23, 23), + ), + ); + + static Future fireBallAttackBottom() => SpriteAnimation.load( + 'player/fireball_bottom.png', + SpriteAnimationData.sequenced( + amount: 3, + stepTime: 0.1, + textureSize: Vector2(23, 23), + ), + ); + + static Future fireBallExplosion() => SpriteAnimation.load( + 'player/explosion_fire.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(32, 32), + ), + ); +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/util/localization/my_localizations.dart b/flame/assets/examples/others/darkness_dungeon/lib/util/localization/my_localizations.dart new file mode 100644 index 0000000..fe2b9df --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/util/localization/my_localizations.dart @@ -0,0 +1,37 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:darkness_dungeon/util/localization/strings_location.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class MyLocalizations { + MyLocalizations(this.locale) { + StringsLocation.configure(this); + } + + final Locale locale; + + static MyLocalizations? of(BuildContext context) { + return Localizations.of(context, MyLocalizations); + } + + Map _sentences = {}; + + Future load() async { + String data = await rootBundle + .loadString('resources/lang/${this.locale.languageCode}.json'); + Map _result = json.decode(data); + + this._sentences = new Map(); + _result.forEach((String key, dynamic value) { + this._sentences[key] = value.toString(); + }); + + return true; + } + + String trans(String key) { + return this._sentences[key] ?? ''; + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/util/localization/my_localizations_delegate.dart b/flame/assets/examples/others/darkness_dungeon/lib/util/localization/my_localizations_delegate.dart new file mode 100644 index 0000000..4beb33f --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/util/localization/my_localizations_delegate.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:darkness_dungeon/util/localization/my_localizations.dart'; +import 'package:flutter/material.dart'; + +class MyLocalizationsDelegate extends LocalizationsDelegate { + const MyLocalizationsDelegate(); + + @override + bool isSupported(Locale locale) => ['en', 'pt'].contains(locale.languageCode); + + @override + Future load(Locale locale) async { + MyLocalizations localizations = new MyLocalizations(locale); + await localizations.load(); + print("Load ${locale.languageCode}"); + return localizations; + } + + @override + bool shouldReload(MyLocalizationsDelegate old) => false; + + Locale resolution(Locale? locale, Iterable supportedLocales) { + for (Locale supportedLocale in supportedLocales) { + if (locale != null) { + if (supportedLocale.languageCode == locale.languageCode || + supportedLocale.countryCode == locale.countryCode) { + return supportedLocale; + } + } + } + return supportedLocales.first; + } + + static List supportedLocales() { + return [const Locale('en', 'US'), const Locale('pt', 'BR')]; + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/util/localization/strings_location.dart b/flame/assets/examples/others/darkness_dungeon/lib/util/localization/strings_location.dart new file mode 100644 index 0000000..0d3ff6a --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/util/localization/strings_location.dart @@ -0,0 +1,25 @@ +import 'package:darkness_dungeon/util/localization/my_localizations.dart'; + +class StringsLocation { + static final StringsLocation _singleton = new StringsLocation._internal(); + + static late MyLocalizations _myLocalizations; + + static void configure(MyLocalizations location) { + _myLocalizations = location; + } + + factory StringsLocation() { + return _singleton; + } + + StringsLocation._internal(); + + String getString(String key) { + return _myLocalizations.trans(key); + } +} + +String getString(String key) { + return StringsLocation().getString(key); +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/util/npc_sprite_sheet.dart b/flame/assets/examples/others/darkness_dungeon/lib/util/npc_sprite_sheet.dart new file mode 100644 index 0000000..6e9222e --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/util/npc_sprite_sheet.dart @@ -0,0 +1,21 @@ +import 'package:bonfire/bonfire.dart'; + +class NpcSpriteSheet { + static Future kidIdleLeft() => SpriteAnimation.load( + 'npc/kid_idle_left.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(16, 22), + ), + ); + + static Future wizardIdleLeft() => SpriteAnimation.load( + 'npc/wizard_idle_left.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(16, 22), + ), + ); +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/util/player_sprite_sheet.dart b/flame/assets/examples/others/darkness_dungeon/lib/util/player_sprite_sheet.dart new file mode 100644 index 0000000..722d710 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/util/player_sprite_sheet.dart @@ -0,0 +1,75 @@ +import 'package:bonfire/bonfire.dart'; + +class PlayerSpriteSheet { + static Future idleRight() => SpriteAnimation.load( + 'player/knight_idle.png', + SpriteAnimationData.sequenced( + amount: 4, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + + static Future attackEffectBottom() => SpriteAnimation.load( + 'player/atack_effect_bottom.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + + static Future attackEffectLeft() => SpriteAnimation.load( + 'player/atack_effect_left.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + static Future attackEffectRight() => SpriteAnimation.load( + 'player/atack_effect_right.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + static Future attackEffectTop() => SpriteAnimation.load( + 'player/atack_effect_top.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ); + + static SimpleDirectionAnimation playerAnimations() => + SimpleDirectionAnimation( + idleLeft: SpriteAnimation.load( + 'player/knight_idle_left.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ), + idleRight: idleRight(), + runLeft: SpriteAnimation.load( + 'player/knight_run_left.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ), + runRight: SpriteAnimation.load( + 'player/knight_run.png', + SpriteAnimationData.sequenced( + amount: 6, + stepTime: 0.1, + textureSize: Vector2(16, 16), + ), + ), + ); +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/util/sounds.dart b/flame/assets/examples/others/darkness_dungeon/lib/util/sounds.dart new file mode 100644 index 0000000..d1348ed --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/util/sounds.dart @@ -0,0 +1,59 @@ +import 'package:flame_audio/flame_audio.dart'; + +class Sounds { + static Future initialize() async { + FlameAudio.bgm.initialize(); + await FlameAudio.audioCache.loadAll([ + 'attack_player.mp3', + 'attack_fire_ball.wav', + 'attack_enemy.mp3', + 'explosion.wav', + 'sound_interaction.wav', + ]); + } + + static void attackPlayerMelee() { + FlameAudio.play('attack_player.mp3', volume: 0.4); + } + + static void attackRange() { + FlameAudio.play('attack_fire_ball.wav', volume: 0.3); + } + + static void attackEnemyMelee() { + FlameAudio.play('attack_enemy.mp3', volume: 0.4); + } + + static void explosion() { + FlameAudio.play('explosion.wav'); + } + + static void interaction() { + FlameAudio.play('sound_interaction.wav', volume: 0.4); + } + + static stopBackgroundSound() { + return FlameAudio.bgm.stop(); + } + + static void playBackgroundSound() async { + await FlameAudio.bgm.stop(); + FlameAudio.bgm.play('sound_bg.mp3'); + } + + static void playBackgroundBoosSound() { + FlameAudio.bgm.play('battle_boss.mp3'); + } + + static void pauseBackgroundSound() { + FlameAudio.bgm.pause(); + } + + static void resumeBackgroundSound() { + FlameAudio.bgm.resume(); + } + + static void dispose() { + FlameAudio.bgm.dispose(); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/lib/widgets/custom_radio.dart b/flame/assets/examples/others/darkness_dungeon/lib/widgets/custom_radio.dart new file mode 100644 index 0000000..608f784 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/widgets/custom_radio.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +class DefectorRadio extends StatelessWidget { + final T value; + final T? group; + final String? label; + final ValueChanged? onChange; + + const DefectorRadio( + {Key? key, required this.value, this.group, this.label, this.onChange}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + onChange?.call(value); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 2), + ), + child: Container( + width: 8, + height: 8, + margin: const EdgeInsets.all(2), + color: value == group ? Colors.white : Colors.transparent, + ), + ), + if (label != null) ...[ + SizedBox( + width: 10, + ), + Text( + label!, + style: TextStyle(color: Colors.white), + ), + ] + ], + ), + ); + } +} \ No newline at end of file diff --git a/flame/assets/examples/others/darkness_dungeon/lib/widgets/game_controller.dart b/flame/assets/examples/others/darkness_dungeon/lib/widgets/game_controller.dart new file mode 100644 index 0000000..15399b1 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/lib/widgets/game_controller.dart @@ -0,0 +1,33 @@ +import 'package:bonfire/bonfire.dart'; +import 'package:darkness_dungeon/game.dart'; +import 'package:darkness_dungeon/util/dialogs.dart'; +import 'package:flutter/material.dart'; + +class GameController extends GameComponent { + bool showGameOver = false; + @override + void update(double dt) { + if (checkInterval('gameOver', 100, dt)) { + if (gameRef.player != null && gameRef.player?.isDead == true) { + if (!showGameOver) { + showGameOver = true; + _showDialogGameOver(); + } + } + } + super.update(dt); + } + + void _showDialogGameOver() { + showGameOver = true; + Dialogs.showGameOver( + context, + () { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (context) => Game()), + (Route route) => false, + ); + }, + ); + } +} diff --git a/flame/assets/examples/others/darkness_dungeon/pubspec.yaml b/flame/assets/examples/others/darkness_dungeon/pubspec.yaml new file mode 100644 index 0000000..93860a7 --- /dev/null +++ b/flame/assets/examples/others/darkness_dungeon/pubspec.yaml @@ -0,0 +1,75 @@ +name: darkness_dungeon +description: RPG Game. + +version: 1.3.6+11 +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + + bonfire: ^3.0.0 + flame_audio: ^2.0.5 + flame_splash_screen: ^0.1.0 + url_launcher: ^6.1.14 + + +dev_dependencies: + test: any + +# flutter_launcher_icons: "^0.7.3" +# +#flutter_icons: +# android: true +# ios: true +# image_path_android: "icone/icone_android.png" +# image_path_ios: "icone/icone_ios.png" + +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/ + - assets/images/ + - assets/images/emote/ + - assets/images/npc/ + - assets/images/enemy/ + - assets/images/enemy/goblin/ + - assets/images/enemy/imp/ + - assets/images/enemy/mini_boss/ + - assets/images/enemy/boss/ + - assets/images/player/ + - assets/images/items/ + - assets/images/tiled/ + - assets/audio/ + - resources/lang/en.json + - resources/lang/pt.json + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + fonts: + - family: Normal + fonts: + - asset: fonts/font_pixel.ttf + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/flame/assets/examples/others/maze_generation/LICENSE b/flame/assets/examples/others/maze_generation/LICENSE new file mode 100644 index 0000000..63b4b68 --- /dev/null +++ b/flame/assets/examples/others/maze_generation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [year] [fullname] + +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. \ No newline at end of file diff --git a/flame/assets/examples/others/maze_generation/lib/examples/maze/maze.dart b/flame/assets/examples/others/maze_generation/lib/examples/maze/maze.dart new file mode 100644 index 0000000..fb19fb8 --- /dev/null +++ b/flame/assets/examples/others/maze_generation/lib/examples/maze/maze.dart @@ -0,0 +1,34 @@ +import 'package:FlameExamples/examples/maze/recursive_maze.dart'; +import 'package:flutter/material.dart'; +import 'package:flame/game.dart'; + +class Maze extends Game { + List walls; + Paint p = Paint(); + + Maze() { + walls = RecursiveMaze() + .build(41, 41, orientationType: OrientationType.randomized); + } + + @override + void render(Canvas c) { + var bgPaint = Paint(); + bgPaint.color = Colors.black; + + p.color = Color.fromRGBO(255, 255, 255, .5); + for (var wall in walls) { + c.drawRect( + Rect.fromLTWH( + 16 + double.parse(wall['x'].toString()) * 8, + 16 + double.parse(wall['y'].toString()) * 8, + 8, + 8, + ), + p); + } + } + + @override + void update(double t) {} +} diff --git a/flame/assets/examples/others/maze_generation/lib/examples/maze/recursive_maze.dart b/flame/assets/examples/others/maze_generation/lib/examples/maze/recursive_maze.dart new file mode 100644 index 0000000..d13e054 --- /dev/null +++ b/flame/assets/examples/others/maze_generation/lib/examples/maze/recursive_maze.dart @@ -0,0 +1,123 @@ +import 'dart:math'; + +class RecursiveMaze { + int width, height; + OrientationType mOrientationType; + + RecursiveMaze() { + // + } + + List build(int width, int height, + {OrientationType orientationType = OrientationType.symmetrical}) { + mOrientationType = orientationType; + List wallList = []; + + getSquare(width, height, wallList); + divideChamber(0, 0, width, height, wallList, true); + + return wallList; + } + + void getSquare(int width, int height, List wallList) { + for (int y = 0; y < height + 2; y++) { + wallList.add({'x': -1, 'y': y - 1}); + } + for (int y = 0; y < height + 2; y++) { + wallList.add({'x': height, 'y': y - 1}); + } + for (int x = 0; x < width; x++) { + wallList.add({'x': x, 'y': -1}); + } + for (int x = 0; x < width; x++) { + wallList.add({'x': x, 'y': width}); + } + } + + Future divideChamber(int posX, int posY, int width, int height, List wallList, + bool isVertical) async { + if (width <= 1 || height <= 1) return; + + var halfWallX = (width / 2).floor(); + var halfWallY = (height / 2).floor(); + + if ((posX + halfWallX) % 2 == 0) halfWallX--; + if ((posY + halfWallY) % 2 == 0) halfWallY--; + + if (isVertical) { + var r = Random().nextInt(height); + while (r % 2 != 0) r = Random().nextInt(height); + for (int y = 0; y < height; y++) { + if (r != y) { + wallList.add({'x': posX + halfWallX, 'y': posY + y}); + await Future.delayed(Duration(milliseconds: 10)); + } + } + } else { + var r = Random().nextInt(width); + while (r % 2 != 0) r = Random().nextInt(width); + for (int x = 0; x < width; x++) { + if (r != x) { + wallList.add({'x': posX + x, 'y': posY + halfWallY}); + await Future.delayed(Duration(milliseconds: 10)); + } + } + } + + var nextWidth = width; + var nextHeight = height; + + if (isVertical) { + nextWidth = halfWallX; + } else { + nextHeight = halfWallY; + } + + if (halfWallX >= 1 || halfWallY >= 2) { + var orientation = + isVerticalOrientation(nextWidth, nextHeight, isVertical); + await divideChamber( + posX, posY, nextWidth, nextHeight, wallList, orientation); + + orientation = isVerticalOrientation(nextWidth, nextHeight, isVertical); + if (!isVertical) { + await divideChamber( + posX, + posY + nextHeight + 1, + nextWidth, + height - nextHeight - 1, + wallList, + orientation, + ); + } else { + await divideChamber( + posX + nextWidth + 1, + posY, + width - nextWidth - 1, + nextHeight, + wallList, + orientation, + ); + } + } + } + + bool isVerticalOrientation(width, height, previousOrientation) { + if (mOrientationType == OrientationType.randomized) { + if (width < height) { + return false; + } else if (height < width) + return true; + else { + return Random().nextInt(2) == 0; + } + } else { + return !previousOrientation; + } + } +} + +enum OrientationType { + symmetrical, + randomized, +} diff --git a/flame/assets/examples/others/maze_generation/lib/examples/smooth_camera_follow/smooth_camera_follow.dart b/flame/assets/examples/others/maze_generation/lib/examples/smooth_camera_follow/smooth_camera_follow.dart new file mode 100644 index 0000000..beab8e6 --- /dev/null +++ b/flame/assets/examples/others/maze_generation/lib/examples/smooth_camera_follow/smooth_camera_follow.dart @@ -0,0 +1,126 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; + +class SmoothCameraController extends Game { + static Rect screenSize; + + final MapController _mapController = MapController(); + + SmoothCameraController() {} + + void render(Canvas c) { + var bgPaint = Paint(); + bgPaint.color = Colors.green; + c.drawRect(screenSize, bgPaint); + + _mapController.draw(c); + } + + void update(double dt) { + _mapController.update(dt); + } + + void resize(Size size) { + super.resize(size); + screenSize = Rect.fromLTWH(0, 0, size.width, size.height); + } +} + +class MapController { + //map start position + double _mapPosX = 0; + double _mapPosY = 0; + + double cameraSpeed = 1; + + Paint _p = Paint(); + List mapObjects = []; + + Player _player1 = Player(0, 0); + Player _player2 = Player(150, 225); + + MapController() { + createRandomObjects(); + } + + void draw(Canvas c) { + // this is an example of an object that will NOT offset its position with the map + _p.color = Color.fromRGBO(220, 220, 255, .1); + c.drawCircle(Offset(50, 50), 10, _p); + + //the 'Camera' position is on left top corner of the screen, so offset this + //value to the mid point + var midScreenPointX = + _mapPosX - SmoothCameraController.screenSize.width / 2; + var midScreenPointY = + _mapPosY - SmoothCameraController.screenSize.height / 2; + + //save the canvas state before drawing the map elements + //this is where the camera logic happens + c.save(); + c.translate(-midScreenPointX, -midScreenPointY); + //After this line you can drawn all elements on the map that that will + //also move according to maps position + + _p.color = Colors.green[600]; + for (var obj in mapObjects) { + c.drawRect(obj, _p); + } + + _player1.draw(c); + + c.restore(); //must be called at the end to stop offset items with the camera + //After this line you can draw elements that will not move inside the map + //For example HUD, Menus etc... + } + + void update(double deltaTime) { + // The target point in the map, can be a player for example + moveMap(_player1.x, _player1.y, deltaTime); + } + + void createRandomObjects() { + for (var i = 0; i < 200; i++) { + var x = (Random().nextDouble() * 1000) - 500; + var y = (Random().nextDouble() * 2000) - 1000; + + mapObjects.add(Rect.fromLTWH(x, y, 32, 32)); + } + } + + void moveMap(double toX, double toY, double deltaTime) { + //lerp interpolate from a value to a value using a time + _mapPosX = lerpDouble(_mapPosX, toX, deltaTime * cameraSpeed); + _mapPosY = lerpDouble(_mapPosY, toY, deltaTime * cameraSpeed); + } +} + +class Player { + double x; + double y; + double width = 32; + double height = 64; + + Paint _p = Paint(); + + Player(this.x, this.y) { + _p.color = Colors.red; + randomTeleport(); + } + + void draw(Canvas c) { + c.drawRect(Rect.fromLTWH(x, y, width, height), _p); + } + + Future randomTeleport() async { + x = (Random().nextDouble() * 300) - 150; + y = (Random().nextDouble() * 300) - 150; + print('Teleporting player to ${x.toInt()}, ${y.toInt()}'); + + await Future.delayed(Duration(seconds: 2)); + randomTeleport(); + } +} diff --git a/flame/assets/examples/others/maze_generation/lib/main.dart b/flame/assets/examples/others/maze_generation/lib/main.dart new file mode 100644 index 0000000..ffd0083 --- /dev/null +++ b/flame/assets/examples/others/maze_generation/lib/main.dart @@ -0,0 +1,15 @@ +import 'package:FlameExamples/examples/maze/maze.dart'; +import 'package:flame/util.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + //var gameController = SmoothCameraController(); + var gameController = Maze(); + runApp(gameController.widget); + + var flameUtil = Util(); + flameUtil.fullScreen(); + flameUtil.setOrientation(DeviceOrientation.portraitUp); +} diff --git a/flame/assets/examples/others/maze_generation/pubspec.yaml b/flame/assets/examples/others/maze_generation/pubspec.yaml new file mode 100644 index 0000000..73635fc --- /dev/null +++ b/flame/assets/examples/others/maze_generation/pubspec.yaml @@ -0,0 +1,77 @@ +name: FlameExamples +description: A new Flutter project. + +# The following line prevents the package from being accidentally published to +# pub.dev using `pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 1.0.0+1 + +environment: + sdk: ">=2.7.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^0.1.3 + +dev_dependencies: + flutter_test: + sdk: flutter + flame: ^0.25.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/flame/assets/examples/others/simple_platformer/LICENSE b/flame/assets/examples/others/simple_platformer/LICENSE new file mode 100644 index 0000000..e3c51b2 --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Shubham Kamble + +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. diff --git a/flame/assets/examples/others/simple_platformer/lib/game/actors/coin.dart b/flame/assets/examples/others/simple_platformer/lib/game/actors/coin.dart new file mode 100644 index 0000000..65c62bf --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/actors/coin.dart @@ -0,0 +1,65 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame_simple_platformer/game/actors/player.dart'; +import 'package:flame_simple_platformer/game/game.dart'; +import 'package:flame_simple_platformer/game/utils/audio_manager.dart'; +import 'package:flutter/animation.dart'; + +// Represents a collectable coin in the game world. +class Coin extends SpriteComponent + with CollisionCallbacks, HasGameReference { + Coin( + super.image, { + super.position, + super.size, + super.scale, + super.angle, + super.anchor, + super.priority, + }) : super.fromImage( + srcPosition: Vector2(3 * 32, 0), + srcSize: Vector2.all(32), + ); + + @override + Future onLoad() async { + add(CircleHitbox()..collisionType = CollisionType.passive); + + // Keeps the coin bouncing + await add( + MoveEffect.by( + Vector2(0, -4), + EffectController( + alternate: true, + infinite: true, + duration: 1, + curve: Curves.ease, + ), + ), + ); + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + if (other is Player) { + AudioManager.playSfx('Collectibles_6.wav'); + + // SequenceEffect can also be used here + add( + OpacityEffect.fadeOut( + LinearEffectController(0.3), + onComplete: () { + add(RemoveEffect()); + }, + ), + ); + + game.playerData.score.value += 1; + } + super.onCollisionStart(intersectionPoints, other); + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/actors/door.dart b/flame/assets/examples/others/simple_platformer/lib/game/actors/door.dart new file mode 100644 index 0000000..6fd19ee --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/actors/door.dart @@ -0,0 +1,41 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame_simple_platformer/game/actors/player.dart'; +import 'package:flame_simple_platformer/game/utils/audio_manager.dart'; +import 'package:flutter/material.dart'; + +// Represents a door in the game world. +class Door extends SpriteComponent with CollisionCallbacks { + VoidCallback? onPlayerEnter; + + Door( + super.image, { + this.onPlayerEnter, + super.position, + super.size, + super.scale, + super.angle, + super.anchor, + super.priority, + }) : super.fromImage( + srcPosition: Vector2(2 * 32, 0), + srcSize: Vector2.all(32), + ); + + @override + Future onLoad() async { + await add(RectangleHitbox()..collisionType = CollisionType.passive); + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + if (other is Player) { + AudioManager.playSfx('Blop_1.wav'); + onPlayerEnter?.call(); + } + super.onCollisionStart(intersectionPoints, other); + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/actors/enemy.dart b/flame/assets/examples/others/simple_platformer/lib/game/actors/enemy.dart new file mode 100644 index 0000000..e530e38 --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/actors/enemy.dart @@ -0,0 +1,84 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame_simple_platformer/game/actors/player.dart'; +import 'package:flame_simple_platformer/game/game.dart'; +import 'package:flame_simple_platformer/game/utils/audio_manager.dart'; + +// Represents an enemy in the game world. +class Enemy extends SpriteComponent + with CollisionCallbacks, HasGameReference { + static final Vector2 _up = Vector2(0, -1); + + Enemy( + super.image, { + Vector2? position, + Vector2? targetPosition, + super.size, + super.scale, + super.angle, + super.anchor, + super.priority, + }) : super.fromImage( + srcPosition: Vector2(1 * 32, 0), + srcSize: Vector2.all(32), + position: position, + ) { + if (targetPosition != null && position != null) { + // Need to sequence two move to effects so that we can + // tap into the onFinishCallback and flip the component. + final effect = SequenceEffect( + [ + MoveToEffect( + targetPosition, + EffectController(speed: 100), + onComplete: flipHorizontallyAroundCenter, + ), + MoveToEffect( + position + Vector2(32, 0), // Need to offset by 32 due to flip + EffectController(speed: 100), + onComplete: flipHorizontallyAroundCenter, + ), + ], + infinite: true, + ); + + add(effect); + } + } + + @override + Future onLoad() async { + await add(CircleHitbox()..collisionType = CollisionType.passive); + } + + @override + void onCollisionStart( + Set intersectionPoints, + PositionComponent other, + ) { + if (other is Player) { + final playerDir = (other.absoluteCenter - absoluteCenter).normalized(); + + // Checks if player is hitting this enemy from the top. + if (playerDir.dot(_up) > 0.85) { + // Fade out and remove this enemy and make the player auto-jump. + add( + OpacityEffect.fadeOut( + LinearEffectController(0.2), + onComplete: removeFromParent, + ), + ); + other.jump(); + } else { + AudioManager.playSfx('Hit_2.wav'); + // Run hit effect on player and reduce the health. + other.hit(); + if (game.playerData.health.value > 0) { + game.playerData.health.value -= 1; + } + } + } + super.onCollisionStart(intersectionPoints, other); + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/actors/platform.dart b/flame/assets/examples/others/simple_platformer/lib/game/actors/platform.dart new file mode 100644 index 0000000..f7b7b0d --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/actors/platform.dart @@ -0,0 +1,20 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; + +// Represents a platform in the game world. +class Platform extends PositionComponent with CollisionCallbacks { + Platform({ + required Vector2 super.position, + required Vector2 super.size, + super.scale, + super.angle, + super.anchor, + }); + + @override + Future onLoad() async { + // Passive, because we don't want platforms to + // collide with each other. + await add(RectangleHitbox()..collisionType = CollisionType.passive); + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/actors/player.dart b/flame/assets/examples/others/simple_platformer/lib/game/actors/player.dart new file mode 100644 index 0000000..f36901a --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/actors/player.dart @@ -0,0 +1,132 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame_simple_platformer/game/actors/platform.dart'; +import 'package:flame_simple_platformer/game/utils/audio_manager.dart'; +import 'package:flutter/services.dart'; + +// Represents a player in the game world. +class Player extends SpriteComponent with CollisionCallbacks, KeyboardHandler { + int _hAxisInput = 0; + bool _jumpInput = false; + bool _isOnGround = false; + + final double _gravity = 10 * 60; + final double _jumpSpeed = 360; + final double _moveSpeed = 200; + + final Vector2 _up = Vector2(0, -1); + final Vector2 _velocity = Vector2.zero(); + + Player( + super.image, { + super.position, + super.size, + super.scale, + super.angle, + super.anchor, + super.priority, + super.children, + }) : super.fromImage( + srcPosition: Vector2.zero(), + srcSize: Vector2.all(32), + ); + + @override + Future onLoad() async { + await add(CircleHitbox()); + } + + @override + void update(double dt) { + // Modify components of velocity based on + // inputs and gravity. + _velocity.x = _hAxisInput * _moveSpeed; + _velocity.y += _gravity * dt; + + // Allow jump only if jump input is pressed + // and player is already on ground. + if (_jumpInput) { + if (_isOnGround) { + AudioManager.playSfx('Jump_15.wav'); + _velocity.y = -_jumpSpeed; + _isOnGround = false; + } + _jumpInput = false; + } + + // Clamp velocity along y to avoid player tunneling + // through platforms at very high velocities. + _velocity.y = _velocity.y.clamp(-_jumpSpeed, 150); + + // delta movement = velocity * time + position += _velocity * dt; + + // Flip player if needed. + if (_hAxisInput < 0 && scale.x > 0) { + flipHorizontallyAroundCenter(); + } else if (_hAxisInput > 0 && scale.x < 0) { + flipHorizontallyAroundCenter(); + } + + super.update(dt); + } + + @override + bool onKeyEvent(KeyEvent event, Set keysPressed) { + _hAxisInput = 0; + + _hAxisInput += keysPressed.contains(LogicalKeyboardKey.keyA) ? -1 : 0; + _hAxisInput += keysPressed.contains(LogicalKeyboardKey.keyD) ? 1 : 0; + _jumpInput = keysPressed.contains(LogicalKeyboardKey.space); + + return true; + } + + @override + void onCollision(Set intersectionPoints, PositionComponent other) { + if (other is Platform) { + if (intersectionPoints.length == 2) { + // Calculate the collision normal and separation distance. + final mid = (intersectionPoints.elementAt(0) + + intersectionPoints.elementAt(1)) / + 2; + + final collisionNormal = absoluteCenter - mid; + final separationDistance = (size.x / 2) - collisionNormal.length; + collisionNormal.normalize(); + + // If collision normal is almost upwards, + // player must be on ground. + if (_up.dot(collisionNormal) > 0.9) { + _isOnGround = true; + } + + // Resolve collision by moving player along + // collision normal by separation distance. + position += collisionNormal.scaled(separationDistance); + } + } + super.onCollision(intersectionPoints, other); + } + + // This method runs an opacity effect on player + // to make it blink. + void hit() { + add( + OpacityEffect.fadeOut( + EffectController( + alternate: true, + duration: 0.1, + repeatCount: 5, + ), + ), + ); + } + + // Makes the player jump forcefully. + void jump() { + _jumpInput = true; + _isOnGround = true; + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/game.dart b/flame/assets/examples/others/simple_platformer/lib/game/game.dart new file mode 100644 index 0000000..eac57fa --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/game.dart @@ -0,0 +1,27 @@ +import 'package:flame/extensions.dart'; +import 'package:flame/flame.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame_simple_platformer/game/model/player_data.dart'; +import 'package:flame_simple_platformer/game/utils/audio_manager.dart'; + +// Represents the game world +class SimplePlatformer extends FlameGame + with HasCollisionDetection, HasKeyboardHandlerComponents { + // Reference to common spritesheet + late Image spriteSheet; + + final playerData = PlayerData(); + final fixedResolution = Vector2(640, 330); + + @override + Future onLoad() async { + // Device setup + await Flame.device.fullScreen(); + await Flame.device.setLandscape(); + + // Loads all the audio assets + await AudioManager.init(); + spriteSheet = await images.load('Spritesheet.png'); + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/game_play.dart b/flame/assets/examples/others/simple_platformer/lib/game/game_play.dart new file mode 100644 index 0000000..836bebf --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/game_play.dart @@ -0,0 +1,46 @@ +import 'package:flame/components.dart'; + +import 'package:flame_simple_platformer/game/game.dart'; +import 'package:flame_simple_platformer/game/hud/hud.dart'; +import 'package:flame_simple_platformer/game/level/level.dart'; +import 'package:flame_simple_platformer/game/utils/audio_manager.dart'; + +// This component is responsible for the whole game play. +class GamePlay extends World with HasGameReference { + // Currently active level + Level? _currentLevel; + + final hud = Hud(priority: 1); + late CameraComponent camera; + + @override + Future onLoad() async { + AudioManager.playBgm('Winning_Sight.wav'); + + camera = CameraComponent.withFixedResolution( + world: this, + width: game.fixedResolution.x, + height: game.fixedResolution.y, + hudComponents: [hud], + ); + camera.viewfinder.position = game.fixedResolution / 2; + await game.add(camera); + + loadLevel('Level1.tmx'); + game.playerData.score.value = 0; + game.playerData.health.value = 5; + } + + @override + void onRemove() { + hud.removeFromParent(); + super.onRemove(); + } + + // Swaps current level with given level + void loadLevel(String levelName) { + _currentLevel?.removeFromParent(); + _currentLevel = Level(levelName); + add(_currentLevel!); + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/hud/hud.dart b/flame/assets/examples/others/simple_platformer/lib/game/hud/hud.dart new file mode 100644 index 0000000..45c7743 --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/hud/hud.dart @@ -0,0 +1,86 @@ +import 'package:flame/components.dart'; +import 'package:flame/input.dart'; + +import 'package:flame_simple_platformer/game/game.dart'; +import 'package:flame_simple_platformer/game/overlays/game_over.dart'; +import 'package:flame_simple_platformer/game/overlays/pause_menu.dart'; +import 'package:flame_simple_platformer/game/utils/audio_manager.dart'; + +class Hud extends Component with HasGameReference { + late final TextComponent scoreTextComponent; + late final TextComponent healthTextComponent; + + Hud({super.children, super.priority}); + + @override + Future onLoad() async { + scoreTextComponent = TextComponent( + text: 'Score: 0', + position: Vector2.all(10), + ); + await add(scoreTextComponent); + + healthTextComponent = TextComponent( + text: 'x5', + anchor: Anchor.topRight, + position: Vector2(game.fixedResolution.x - 10, 10), + ); + await add(healthTextComponent); + + final playerSprite = SpriteComponent.fromImage( + game.spriteSheet, + srcPosition: Vector2.zero(), + srcSize: Vector2.all(32), + anchor: Anchor.topRight, + position: Vector2( + healthTextComponent.position.x - healthTextComponent.size.x - 5, + 5, + ), + ); + await add(playerSprite); + + game.playerData.score.addListener(onScoreChange); + game.playerData.health.addListener(onHealthChange); + + final pauseButton = SpriteButtonComponent( + onPressed: () { + AudioManager.pauseBgm(); + game.pauseEngine(); + game.overlays.add(PauseMenu.id); + }, + button: Sprite( + game.spriteSheet, + srcSize: Vector2.all(32), + srcPosition: Vector2(32 * 4, 0), + ), + size: Vector2.all(32), + anchor: Anchor.topCenter, + position: Vector2(game.fixedResolution.x / 2, 5), + ); + await add(pauseButton); + } + + @override + void onRemove() { + game.playerData.score.removeListener(onScoreChange); + game.playerData.health.removeListener(onHealthChange); + super.onRemove(); + } + + // Updates score text on hud. + void onScoreChange() { + scoreTextComponent.text = 'Score: ${game.playerData.score.value}'; + } + + // Updates health text on hud. + void onHealthChange() { + healthTextComponent.text = 'x${game.playerData.health.value}'; + + // Load game over overlay if health is zero. + if (game.playerData.health.value == 0) { + AudioManager.stopBgm(); + game.pauseEngine(); + game.overlays.add(GameOver.id); + } + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/level/level.dart b/flame/assets/examples/others/simple_platformer/lib/game/level/level.dart new file mode 100644 index 0000000..9080925 --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/level/level.dart @@ -0,0 +1,137 @@ +import 'package:flame/camera.dart'; +import 'package:flame/components.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame_simple_platformer/game/actors/coin.dart'; +import 'package:flame_simple_platformer/game/actors/door.dart'; +import 'package:flame_simple_platformer/game/actors/enemy.dart'; +import 'package:flame_simple_platformer/game/actors/platform.dart'; +import 'package:flame_simple_platformer/game/actors/player.dart'; +import 'package:flame_simple_platformer/game/game.dart'; +import 'package:flame_simple_platformer/game/game_play.dart'; +import 'package:flame_tiled/flame_tiled.dart'; + +// Represents a level in game. Should only be added as child of GamePlay +class Level extends Component + with HasGameReference, ParentIsA { + final String levelName; + late Player _player; + + Level(this.levelName) : super(); + + @override + Future onLoad() async { + final level = await TiledComponent.load( + levelName, + Vector2.all(32), + ); + await add(level); + + _spawnActors(level); + _setupCamera(level); + } + + // This method takes care of spawning + // all the actors in the game world. + void _spawnActors(TiledComponent level) { + final tileMap = level.tileMap; + final platformsLayer = tileMap.getLayer('Platforms'); + + for (final platformObject in platformsLayer!.objects) { + final platform = Platform( + position: Vector2(platformObject.x, platformObject.y), + size: Vector2(platformObject.width, platformObject.height), + ); + add(platform); + } + + final spawnPointsLayer = tileMap.getLayer('SpawnPoints'); + + for (final spawnPoint in spawnPointsLayer!.objects) { + final position = Vector2(spawnPoint.x, spawnPoint.y - spawnPoint.height); + final size = Vector2(spawnPoint.width, spawnPoint.height); + + switch (spawnPoint.class_) { + case 'Player': + final halfSize = size * 0.5; + final levelBounds = Rect.fromLTWH( + halfSize.x, + halfSize.y, + level.size.x - halfSize.x, + level.size.y - halfSize.y, + ); + + _player = Player( + game.spriteSheet, + anchor: Anchor.center, + position: position, + size: size, + children: [ + BoundedPositionBehavior( + bounds: Rectangle.fromRect(levelBounds), + ), + ], + ); + add(_player); + + break; + + case 'Coin': + final coin = Coin( + game.spriteSheet, + position: position, + size: size, + ); + add(coin); + + break; + + case 'Enemy': + // Find the target object. + final targetObjectId = + int.parse(spawnPoint.properties.first.value.toString()); + final target = spawnPointsLayer.objects + .firstWhere((object) => object.id == targetObjectId); + + final enemy = Enemy( + game.spriteSheet, + position: position, + targetPosition: Vector2(target.x, target.y), + size: size, + ); + add(enemy); + + break; + + case 'Door': + final door = Door( + game.spriteSheet, + position: position, + size: size, + onPlayerEnter: () { + parent.loadLevel(spawnPoint.properties.first.value.toString()); + }, + ); + add(door); + + break; + } + } + } + + // This method is responsible for making the camera + // follow the player component and also for keeping + // the camera within level bounds. + /// NOTE: Call only after [_spawnActors]. + void _setupCamera(TiledComponent level) { + parent.camera.follow(_player, maxSpeed: 200); + parent.camera.setBounds( + Rectangle.fromLTRB( + game.fixedResolution.x / 2, + game.fixedResolution.y / 2, + level.width - game.fixedResolution.x / 2, + level.height - game.fixedResolution.y / 2, + ), + ); + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/model/player_data.dart b/flame/assets/examples/others/simple_platformer/lib/game/model/player_data.dart new file mode 100644 index 0000000..3f06b54 --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/model/player_data.dart @@ -0,0 +1,6 @@ +import 'package:flutter/foundation.dart'; + +class PlayerData { + final score = ValueNotifier(0); + final health = ValueNotifier(5); +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/overlays/game_over.dart b/flame/assets/examples/others/simple_platformer/lib/game/overlays/game_over.dart new file mode 100644 index 0000000..5eebf38 --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/overlays/game_over.dart @@ -0,0 +1,49 @@ +import 'package:flame_simple_platformer/game/game.dart'; +import 'package:flame_simple_platformer/game/game_play.dart'; +import 'package:flame_simple_platformer/game/overlays/main_menu.dart'; +import 'package:flutter/material.dart'; + +class GameOver extends StatelessWidget { + static const id = 'GameOver'; + final SimplePlatformer game; + + const GameOver({required this.game, super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black.withAlpha(100), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 100, + child: ElevatedButton( + onPressed: () { + game.overlays.remove(id); + game.resumeEngine(); + game.removeAll(game.children); + game.add(GamePlay()); + }, + child: const Text('Restart'), + ), + ), + SizedBox( + width: 100, + child: ElevatedButton( + onPressed: () { + game.overlays.remove(id); + game.resumeEngine(); + game.removeAll(game.children); + game.overlays.add(MainMenu.id); + }, + child: const Text('Exit'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/overlays/main_menu.dart b/flame/assets/examples/others/simple_platformer/lib/game/overlays/main_menu.dart new file mode 100644 index 0000000..24ec344 --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/overlays/main_menu.dart @@ -0,0 +1,45 @@ +import 'package:flame_simple_platformer/game/game.dart'; +import 'package:flame_simple_platformer/game/game_play.dart'; +import 'package:flame_simple_platformer/game/overlays/settings.dart'; +import 'package:flutter/material.dart'; + +class MainMenu extends StatelessWidget { + static const id = 'MainMenu'; + final SimplePlatformer game; + + const MainMenu({required this.game, super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 120, + child: ElevatedButton( + onPressed: () { + game.overlays.remove(id); + game.add(GamePlay()); + }, + child: const Text('Play'), + ), + ), + SizedBox( + width: 120, + child: ElevatedButton( + onPressed: () { + game.overlays.remove(id); + game.overlays.add(Settings.id); + }, + child: const Text('Settings'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/overlays/pause_menu.dart b/flame/assets/examples/others/simple_platformer/lib/game/overlays/pause_menu.dart new file mode 100644 index 0000000..436a042 --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/overlays/pause_menu.dart @@ -0,0 +1,48 @@ +import 'package:flame_simple_platformer/game/game.dart'; +import 'package:flame_simple_platformer/game/overlays/main_menu.dart'; +import 'package:flame_simple_platformer/game/utils/audio_manager.dart'; +import 'package:flutter/material.dart'; + +class PauseMenu extends StatelessWidget { + static const id = 'PauseMenu'; + final SimplePlatformer game; + + const PauseMenu({required this.game, super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black.withAlpha(100), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 100, + child: ElevatedButton( + onPressed: () { + AudioManager.resumeBgm(); + game.overlays.remove(id); + game.resumeEngine(); + }, + child: const Text('Resume'), + ), + ), + SizedBox( + width: 100, + child: ElevatedButton( + onPressed: () { + game.overlays.remove(id); + game.resumeEngine(); + game.removeAll(game.children); + game.overlays.add(MainMenu.id); + }, + child: const Text('Exit'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/overlays/settings.dart b/flame/assets/examples/others/simple_platformer/lib/game/overlays/settings.dart new file mode 100644 index 0000000..cbf3049 --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/overlays/settings.dart @@ -0,0 +1,57 @@ +import 'package:flame_simple_platformer/game/game.dart'; +import 'package:flame_simple_platformer/game/overlays/main_menu.dart'; +import 'package:flame_simple_platformer/game/utils/audio_manager.dart'; +import 'package:flutter/material.dart'; + +class Settings extends StatelessWidget { + static const id = 'Settings'; + final SimplePlatformer game; + + const Settings({required this.game, super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 300, + child: ValueListenableBuilder( + valueListenable: AudioManager.sfx, + builder: (context, sfx, child) => SwitchListTile( + title: const Text('Sound Effects'), + value: sfx, + onChanged: (value) => AudioManager.sfx.value = value, + ), + ), + ), + SizedBox( + width: 300, + child: ValueListenableBuilder( + valueListenable: AudioManager.bgm, + builder: (context, bgm, child) => SwitchListTile( + title: const Text('Background Music'), + value: bgm, + onChanged: (value) => AudioManager.bgm.value = value, + ), + ), + ), + SizedBox( + width: 100, + child: ElevatedButton( + onPressed: () { + game.overlays.remove(id); + game.overlays.add(MainMenu.id); + }, + child: const Text('Back'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/game/utils/audio_manager.dart b/flame/assets/examples/others/simple_platformer/lib/game/utils/audio_manager.dart new file mode 100644 index 0000000..e9d208d --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/game/utils/audio_manager.dart @@ -0,0 +1,46 @@ +import 'package:flame_audio/flame_audio.dart'; +import 'package:flutter/material.dart'; + +// This class is responsible for playing all the sound effects +// and background music in this game. +class AudioManager { + static final sfx = ValueNotifier(false); + static final bgm = ValueNotifier(false); + + static Future init() async { + FlameAudio.bgm.initialize(); + await FlameAudio.audioCache.loadAll([ + 'Blop_1.wav', + 'Collectibles_6.wav', + 'Hit_2.wav', + 'Jump_15.wav', + 'Winning_Sight.wav', + ]); + } + + static void playSfx(String file) { + if (sfx.value) { + FlameAudio.play(file); + } + } + + static void playBgm(String file) { + if (bgm.value) { + FlameAudio.bgm.play(file); + } + } + + static void pauseBgm() { + FlameAudio.bgm.pause(); + } + + static void resumeBgm() { + if (bgm.value) { + FlameAudio.bgm.resume(); + } + } + + static void stopBgm() { + FlameAudio.bgm.stop(); + } +} diff --git a/flame/assets/examples/others/simple_platformer/lib/main.dart b/flame/assets/examples/others/simple_platformer/lib/main.dart new file mode 100644 index 0000000..d657748 --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/lib/main.dart @@ -0,0 +1,41 @@ +import 'package:flame/game.dart'; +import 'package:flame_simple_platformer/game/game.dart'; +import 'package:flame_simple_platformer/game/overlays/game_over.dart'; +import 'package:flame_simple_platformer/game/overlays/main_menu.dart'; +import 'package:flame_simple_platformer/game/overlays/pause_menu.dart'; +import 'package:flame_simple_platformer/game/overlays/settings.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(const MyApp()); +} + +// A single instance to avoid creation of +// multiple instances in every build. +final _game = SimplePlatformer(); + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Simple Platformer', + theme: ThemeData.dark(), + home: Scaffold( + body: GameWidget( + game: kDebugMode ? SimplePlatformer() : _game, + overlayBuilderMap: { + MainMenu.id: (context, game) => MainMenu(game: game), + PauseMenu.id: (context, game) => PauseMenu(game: game), + GameOver.id: (context, game) => GameOver(game: game), + Settings.id: (context, game) => Settings(game: game), + }, + initialActiveOverlays: const [MainMenu.id], + ), + ), + ); + } +} diff --git a/flame/assets/examples/others/simple_platformer/pubspec.yaml b/flame/assets/examples/others/simple_platformer/pubspec.yaml new file mode 100644 index 0000000..e6e01be --- /dev/null +++ b/flame/assets/examples/others/simple_platformer/pubspec.yaml @@ -0,0 +1,28 @@ +name: flame_simple_platformer +description: A 2d platformer made using Flame engine + +publish_to: "none" +version: 1.0.0+1 + +environment: + sdk: ">=2.17.1 <3.0.0" + +dependencies: + flame: 1.17.0 + flame_audio: 2.10.1 + flame_tiled: 1.20.0 + flutter: + sdk: flutter + tiled: 0.10.2 + +dev_dependencies: + flame_lint: 1.1.2 + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true + assets: + - assets/images/ + - assets/tiles/ + - assets/audio/ diff --git a/flame/assets/examples/others/spaceescape/LICENSE b/flame/assets/examples/others/spaceescape/LICENSE new file mode 100644 index 0000000..24270e5 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Shubham Kamble + +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. diff --git a/flame/assets/examples/others/spaceescape/lib/game/audio_player_component.dart b/flame/assets/examples/others/spaceescape/lib/game/audio_player_component.dart new file mode 100644 index 0000000..d7d6b2e --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/game/audio_player_component.dart @@ -0,0 +1,56 @@ +import 'package:flame/components.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame_audio/flame_audio.dart'; +import 'package:provider/provider.dart'; + +import 'game.dart'; + +import '../models/settings.dart'; + +class AudioPlayerComponent extends Component + with HasGameReference { + @override + Future? onLoad() async { + FlameAudio.bgm.initialize(); + + await FlameAudio.audioCache + .loadAll(['laser1.ogg', 'powerUp6.ogg', 'laserSmall_001.ogg']); + + try { + await FlameAudio.audioCache.load( + '9. Space Invaders.wav', + ); + } catch (_) { + // ignore: avoid_print + print('Missing VOiD1 Gaming music pack: ' + 'https://void1gaming.itch.io/free-synthwave-music-pack ' + 'See assets/audio/README.md for more information.'); + } + + return super.onLoad(); + } + + void playBgm(String filename) { + if (!FlameAudio.audioCache.loadedFiles.containsKey(filename)) return; + + if (game.buildContext != null) { + if (Provider.of(game.buildContext!, listen: false) + .backgroundMusic) { + FlameAudio.bgm.play(filename); + } + } + } + + void playSfx(String filename) { + if (game.buildContext != null) { + if (Provider.of(game.buildContext!, listen: false) + .soundEffects) { + FlameAudio.play(filename); + } + } + } + + void stopBgm() { + FlameAudio.bgm.stop(); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/game/bullet.dart b/flame/assets/examples/others/spaceescape/lib/game/bullet.dart new file mode 100644 index 0000000..4a0f01a --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/game/bullet.dart @@ -0,0 +1,63 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; + +import 'enemy.dart'; + +// This component represent a bullet in game world. +class Bullet extends SpriteComponent with CollisionCallbacks { + // Speed of the bullet. + final double _speed = 450; + + // Controls the direction in which bullet travels. + Vector2 direction = Vector2(0, -1); + + // Level of this bullet. Essentially represents the + // level of spaceship that fired this bullet. + final int level; + + Bullet({ + required Sprite? sprite, + required Vector2? position, + required Vector2? size, + required this.level, + }) : super(sprite: sprite, position: position, size: size); + + @override + void onMount() { + super.onMount(); + + // Adding a circular hitbox with radius as 0.4 times + // the smallest dimension of this components size. + final shape = CircleHitbox.relative( + 0.4, + parentSize: size, + position: size / 2, + anchor: Anchor.center, + ); + add(shape); + } + + @override + void onCollision(Set intersectionPoints, PositionComponent other) { + super.onCollision(intersectionPoints, other); + + // If the other Collidable is Enemy, remove this bullet. + if (other is Enemy) { + removeFromParent(); + } + } + + @override + void update(double dt) { + super.update(dt); + + // Moves the bullet to a new position with _speed and direction. + position += direction * _speed * dt; + + // If bullet crosses the upper boundary of screen + // mark it to be removed it from the game world. + if (position.y < 0) { + removeFromParent(); + } + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/game/command.dart b/flame/assets/examples/others/spaceescape/lib/game/command.dart new file mode 100644 index 0000000..9425c4e --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/game/command.dart @@ -0,0 +1,19 @@ +import 'package:flame/components.dart'; + +// This class represents a command that will be run +// on every component of type T added to game instance. +class Command { + // A callback function to be run on + // components of type T. + void Function(T target) action; + + Command({required this.action}); + + // Runs the callback on given component + // if it is of type T. + void run(Component c) { + if (c is T) { + action.call(c); + } + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/game/enemy.dart b/flame/assets/examples/others/spaceescape/lib/game/enemy.dart new file mode 100644 index 0000000..e6b6648 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/game/enemy.dart @@ -0,0 +1,204 @@ +import 'dart:math'; + +import 'package:flame/collisions.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/particles.dart'; +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; + +import 'game.dart'; +import 'bullet.dart'; +import 'player.dart'; +import 'command.dart'; +import 'audio_player_component.dart'; + +import '../models/enemy_data.dart'; + +// This class represent an enemy component. +class Enemy extends SpriteComponent + with CollisionCallbacks, HasGameReference { + // The speed of this enemy. + double _speed = 250; + + // This direction in which this Enemy will move. + // Defaults to vertically downwards. + Vector2 moveDirection = Vector2(0, 1); + + // Controls for how long enemy should be frozen. + late Timer _freezeTimer; + + // Holds an object of Random class to generate random numbers. + final _random = Random(); + + // The data required to create this enemy. + final EnemyData enemyData; + + // Represents health of this enemy. + int _hitPoints = 10; + + // To display health in game world. + final _hpText = TextComponent( + text: '10 HP', + textRenderer: TextPaint( + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontFamily: 'BungeeInline', + ), + ), + ); + + // This method generates a random vector with its angle + // between from 0 and 360 degrees. + Vector2 getRandomVector() { + return (Vector2.random(_random) - Vector2.random(_random)) * 500; + } + + // Returns a random direction vector with slight angle to +ve y axis. + Vector2 getRandomDirection() { + return (Vector2.random(_random) - Vector2(0.5, -1)).normalized(); + } + + Enemy({ + required Sprite? sprite, + required this.enemyData, + required Vector2? position, + required Vector2? size, + }) : super(sprite: sprite, position: position, size: size) { + // Rotates the enemy component by 180 degrees. This is needed because + // all the sprites initially face the same direct, but we want enemies to be + // moving in opposite direction. + angle = pi; + + // Set the current speed from enemyData. + _speed = enemyData.speed; + + // Set hitpoint to correct value from enemyData. + _hitPoints = enemyData.level * 10; + _hpText.text = '$_hitPoints HP'; + + // Sets freeze time to 2 seconds. After 2 seconds speed will be reset. + _freezeTimer = Timer(2, onTick: () { + _speed = enemyData.speed; + }); + + // If this enemy can move horizontally, randomize the move direction. + if (enemyData.hMove) { + moveDirection = getRandomDirection(); + } + } + + @override + void onMount() { + super.onMount(); + + // Adding a circular hitbox with radius as 0.8 times + // the smallest dimension of this components size. + final shape = CircleHitbox.relative( + 0.8, + parentSize: size, + position: size / 2, + anchor: Anchor.center, + ); + add(shape); + + // As current component is already rotated by pi radians, + // the text component needs to be again rotated by pi radians + // so that it is displayed correctly. + _hpText.angle = pi; + + // To place the text just behind the enemy. + _hpText.position = Vector2(50, 80); + + // Add as child of current component. + add(_hpText); + } + + @override + void onCollision(Set intersectionPoints, PositionComponent other) { + super.onCollision(intersectionPoints, other); + + if (other is Bullet) { + // If the other Collidable is a Bullet, + // reduce health by level of bullet times 10. + _hitPoints -= other.level * 10; + } else if (other is Player) { + // If the other Collidable is Player, destroy. + destroy(); + } + } + + // This method will destroy this enemy. + void destroy() { + // Ask audio player to play enemy destroy effect. + game.addCommand(Command(action: (audioPlayer) { + audioPlayer.playSfx('laser1.ogg'); + })); + + removeFromParent(); + + // Before dying, register a command to increase + // player's score by 1. + final command = Command(action: (player) { + // Use the correct killPoint to increase player's score. + player.addToScore(enemyData.killPoint); + }); + game.addCommand(command); + + // Generate 20 white circle particles with random speed and acceleration, + // at current position of this enemy. Each particles lives for exactly + // 0.1 seconds and will get removed from the game world after that. + final particleComponent = ParticleSystemComponent( + particle: Particle.generate( + count: 20, + lifespan: 0.1, + generator: (i) => AcceleratedParticle( + acceleration: getRandomVector(), + speed: getRandomVector(), + position: position.clone(), + child: CircleParticle( + radius: 2, + paint: Paint()..color = Colors.white, + ), + ), + ), + ); + + game.world.add(particleComponent); + } + + @override + void update(double dt) { + super.update(dt); + + // Sync-up text component and value of hitPoints. + _hpText.text = '$_hitPoints HP'; + + // If hitPoints have reduced to zero, + // destroy this enemy. + if (_hitPoints <= 0) { + destroy(); + } + + _freezeTimer.update(dt); + + // Update the position of this enemy using its speed and delta time. + position += moveDirection * _speed * dt; + + // If the enemy leaves the screen, destroy it. + if (position.y > game.fixedResolution.y) { + removeFromParent(); + } else if ((position.x < size.x / 2) || + (position.x > (game.fixedResolution.x - size.x / 2))) { + // Enemy is going outside vertical screen bounds, flip its x direction. + moveDirection.x *= -1; + } + } + + // Pauses enemy for 2 seconds when called. + void freeze() { + _speed = 0; + _freezeTimer.stop(); + _freezeTimer.start(); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/game/enemy_manager.dart b/flame/assets/examples/others/spaceescape/lib/game/enemy_manager.dart new file mode 100644 index 0000000..e40581a --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/game/enemy_manager.dart @@ -0,0 +1,251 @@ +import 'dart:math'; + +import 'package:flame/experimental.dart'; +import 'package:flame/sprite.dart'; +import 'package:flame/components.dart'; +import 'package:provider/provider.dart'; + +import 'game.dart'; +import 'enemy.dart'; + +import '../models/enemy_data.dart'; +import '../models/player_data.dart'; + +// This component class takes care of spawning new enemy components +// randomly from top of the screen. It uses the HasGameReference mixin so that +// it can add child components. +class EnemyManager extends Component with HasGameReference { + // The timer which runs the enemy spawner code at regular interval of time. + late Timer _timer; + + // Controls for how long EnemyManager should stop spawning new enemies. + late Timer _freezeTimer; + + // A reference to spriteSheet contains enemy sprites. + SpriteSheet spriteSheet; + + // Holds an object of Random class to generate random numbers. + Random random = Random(); + + EnemyManager({required this.spriteSheet}) : super() { + // Sets the timer to call _spawnEnemy() after every 1 second, until timer is explicitly stops. + _timer = Timer(1, onTick: _spawnEnemy, repeat: true); + + // Sets freeze time to 2 seconds. After 2 seconds spawn timer will start again. + _freezeTimer = Timer(2, onTick: () { + _timer.start(); + }); + } + + // Spawns a new enemy at random position at the top of the screen. + void _spawnEnemy() { + Vector2 initialSize = Vector2(64, 64); + + // random.nextDouble() generates a random number between 0 and 1. + // Multiplying it by game.fixedResolution.x makes sure that the value remains between 0 and width of screen. + Vector2 position = Vector2(random.nextDouble() * game.fixedResolution.x, 0); + + // Clamps the vector such that the enemy sprite remains within the screen. + position.clamp( + Vector2.zero() + initialSize / 2, + game.fixedResolution - initialSize / 2, + ); + + // Make sure that we have a valid BuildContext before using it. + if (game.buildContext != null) { + // Get current score and figure out the max level of enemy that + // can be spawned for this score. + int currentScore = + Provider.of(game.buildContext!, listen: false) + .currentScore; + int maxLevel = mapScoreToMaxEnemyLevel(currentScore); + + /// Gets a random [EnemyData] object from the list. + final enemyData = _enemyDataList.elementAt(random.nextInt(maxLevel * 4)); + + Enemy enemy = Enemy( + sprite: spriteSheet.getSpriteById(enemyData.spriteId), + size: initialSize, + position: position, + enemyData: enemyData, + ); + + // Makes sure that the enemy sprite is centered. + enemy.anchor = Anchor.center; + + // Add it to components list of game instance, instead of EnemyManager. + // This ensures the collision detection working correctly. + game.world.add(enemy); + } + } + + // For a given score, this method returns a max level + // of enemy that can be used for spawning. + int mapScoreToMaxEnemyLevel(int score) { + int level = 1; + + if (score > 1500) { + level = 4; + } else if (score > 500) { + level = 3; + } else if (score > 100) { + level = 2; + } + + return level; + } + + @override + void onMount() { + super.onMount(); + // Start the timer as soon as current enemy manager get prepared + // and added to the game instance. + _timer.start(); + } + + @override + void onRemove() { + super.onRemove(); + // Stop the timer if current enemy manager is getting removed from the + // game instance. + _timer.stop(); + } + + @override + void update(double dt) { + super.update(dt); + // Update timers with delta time to make them tick. + _timer.update(dt); + _freezeTimer.update(dt); + } + + // Stops and restarts the timer. Should be called + // while restarting and exiting the game. + void reset() { + _timer.stop(); + _timer.start(); + } + + // Pauses spawn timer for 2 seconds when called. + void freeze() { + _timer.stop(); + _freezeTimer.stop(); + _freezeTimer.start(); + } + + /// A private list of all [EnemyData]s. + static const List _enemyDataList = [ + EnemyData( + killPoint: 1, + speed: 200, + spriteId: 8, + level: 1, + hMove: false, + ), + EnemyData( + killPoint: 2, + speed: 200, + spriteId: 9, + level: 1, + hMove: false, + ), + EnemyData( + killPoint: 4, + speed: 200, + spriteId: 10, + level: 1, + hMove: false, + ), + EnemyData( + killPoint: 4, + speed: 200, + spriteId: 11, + level: 1, + hMove: false, + ), + EnemyData( + killPoint: 6, + speed: 250, + spriteId: 12, + level: 2, + hMove: false, + ), + EnemyData( + killPoint: 6, + speed: 250, + spriteId: 13, + level: 2, + hMove: false, + ), + EnemyData( + killPoint: 6, + speed: 250, + spriteId: 14, + level: 2, + hMove: false, + ), + EnemyData( + killPoint: 6, + speed: 250, + spriteId: 15, + level: 2, + hMove: true, + ), + EnemyData( + killPoint: 10, + speed: 350, + spriteId: 16, + level: 3, + hMove: false, + ), + EnemyData( + killPoint: 10, + speed: 350, + spriteId: 17, + level: 3, + hMove: false, + ), + EnemyData( + killPoint: 10, + speed: 400, + spriteId: 18, + level: 3, + hMove: true, + ), + EnemyData( + killPoint: 10, + speed: 400, + spriteId: 19, + level: 3, + hMove: false, + ), + EnemyData( + killPoint: 10, + speed: 400, + spriteId: 20, + level: 4, + hMove: false, + ), + EnemyData( + killPoint: 50, + speed: 250, + spriteId: 21, + level: 4, + hMove: true, + ), + EnemyData( + killPoint: 50, + speed: 250, + spriteId: 22, + level: 4, + hMove: false, + ), + EnemyData( + killPoint: 50, + speed: 250, + spriteId: 23, + level: 4, + hMove: false, + ) + ]; +} diff --git a/flame/assets/examples/others/spaceescape/lib/game/game.dart b/flame/assets/examples/others/spaceescape/lib/game/game.dart new file mode 100644 index 0000000..162c0a8 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/game/game.dart @@ -0,0 +1,339 @@ +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/parallax.dart'; +import 'package:flame/sprite.dart'; +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../widgets/overlays/pause_menu.dart'; +import '../widgets/overlays/pause_button.dart'; +import '../widgets/overlays/game_over_menu.dart'; + +import '../models/player_data.dart'; +import '../models/spaceship_details.dart'; + +import 'enemy.dart'; +import 'health_bar.dart'; +import 'player.dart'; +import 'bullet.dart'; +import 'command.dart'; +import 'power_ups.dart'; +import 'enemy_manager.dart'; +import 'power_up_manager.dart'; +import 'audio_player_component.dart'; + +// This class is responsible for initializing and running the game-loop. +class SpacescapeGame extends FlameGame + with HasCollisionDetection, HasKeyboardHandlerComponents { + // The whole game world. + final World world = World(); + + late CameraComponent primaryCamera; + + // Stores a reference to player component. + late Player _player; + + // Stores a reference to the main spritesheet. + late SpriteSheet spriteSheet; + + // Stores a reference to an enemy manager component. + late EnemyManager _enemyManager; + + // Stores a reference to an power-up manager component. + late PowerUpManager _powerUpManager; + + // Displays player score on top left. + late TextComponent _playerScore; + + // Displays player helth on top right. + late TextComponent _playerHealth; + + late AudioPlayerComponent _audioPlayerComponent; + + // List of commands to be processed in current update. + final _commandList = List.empty(growable: true); + + // List of commands to be processed in next update. + final _addLaterCommandList = List.empty(growable: true); + + // Indicates weather the game world has been already initialized. + bool _isAlreadyLoaded = false; + + // Returns the size of the playable area of the game window. + Vector2 fixedResolution = Vector2(540, 960); + + // This method gets called by Flame before the game-loop begins. + // Assets loading and adding component should be done here. + @override + Future onLoad() async { + // Initialize the game world only one time. + if (!_isAlreadyLoaded) { + // Loads and caches all the images for later use. + await images.loadAll([ + 'simpleSpace_tilesheet@2.png', + 'freeze.png', + 'icon_plusSmall.png', + 'multi_fire.png', + 'nuke.png', + ]); + + spriteSheet = SpriteSheet.fromColumnsAndRows( + image: images.fromCache('simpleSpace_tilesheet@2.png'), + columns: 8, + rows: 6, + ); + + await add(world); + + // Create a basic joystick component on left. + final joystick = JoystickComponent( + anchor: Anchor.bottomLeft, + position: Vector2(30, fixedResolution.y - 30), + // size: 100, + background: CircleComponent( + radius: 60, + paint: Paint()..color = Colors.white.withOpacity(0.5), + ), + knob: CircleComponent(radius: 30), + ); + + primaryCamera = CameraComponent.withFixedResolution( + world: world, + width: fixedResolution.x, + height: fixedResolution.y, + hudComponents: [joystick], + )..viewfinder.position = fixedResolution / 2; + await add(primaryCamera); + + _audioPlayerComponent = AudioPlayerComponent(); + final stars = await ParallaxComponent.load( + [ParallaxImageData('stars1.png'), ParallaxImageData('stars2.png')], + repeat: ImageRepeat.repeat, + baseVelocity: Vector2(0, -50), + velocityMultiplierDelta: Vector2(0, 1.5), + size: fixedResolution, + ); + + /// As build context is not valid in onLoad() method, we + /// cannot get current [PlayerData] here. So initilize player + /// with the default SpaceshipType.Canary. + const spaceshipType = SpaceshipType.canary; + final spaceship = Spaceship.getSpaceshipByType(spaceshipType); + + _player = Player( + joystick: joystick, + spaceshipType: spaceshipType, + sprite: spriteSheet.getSpriteById(spaceship.spriteId), + size: Vector2(64, 64), + position: fixedResolution / 2, + ); + + // Makes sure that the sprite is centered. + _player.anchor = Anchor.center; + + _enemyManager = EnemyManager(spriteSheet: spriteSheet); + _powerUpManager = PowerUpManager(); + + // Create a fire button component on right + final button = ButtonComponent( + button: CircleComponent( + radius: 60, + paint: Paint()..color = Colors.white.withOpacity(0.5), + ), + anchor: Anchor.bottomRight, + position: Vector2(fixedResolution.x - 30, fixedResolution.y - 30), + onPressed: _player.joystickAction, + ); + + // Create text component for player score. + _playerScore = TextComponent( + text: 'Score: 0', + position: Vector2(10, 10), + textRenderer: TextPaint( + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontFamily: 'BungeeInline', + ), + ), + ); + + // Create text component for player health. + _playerHealth = TextComponent( + text: 'Health: 100%', + position: Vector2(fixedResolution.x - 10, 10), + textRenderer: TextPaint( + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontFamily: 'BungeeInline', + ), + ), + ); + + // Anchor to top right as we want the top right + // corner of this component to be at a specific position. + _playerHealth.anchor = Anchor.topRight; + + // Add the blue bar indicating health. + final healthBar = HealthBar( + player: _player, + position: _playerHealth.positionOfAnchor(Anchor.topLeft), + priority: -1, + ); + + // Makes the game use a fixed resolution irrespective of the windows size. + await world.addAll([ + _audioPlayerComponent, + stars, + _player, + _enemyManager, + _powerUpManager, + button, + _playerScore, + _playerHealth, + healthBar, + ]); + + // Set this to true so that we do not initilize + // everything again in the same session. + _isAlreadyLoaded = true; + } + } + + // This method gets called when game instance gets attached + // to Flutter's widget tree. + @override + void onAttach() { + if (buildContext != null) { + // Get the PlayerData from current build context without registering a listener. + final playerData = Provider.of(buildContext!, listen: false); + // Update the current spaceship type of player. + _player.setSpaceshipType(playerData.spaceshipType); + } + _audioPlayerComponent.playBgm('9. Space Invaders.wav'); + super.onAttach(); + } + + @override + void onDetach() { + _audioPlayerComponent.stopBgm(); + super.onDetach(); + } + + // =================================== + // IMPORTANT NOTE + // Those overrides are obsolete since Flame v1.2.0 version + // This code remains as is as a reference for the YouTube tutorial. + // =================================== + // @override + // void prepare(Component c) { + // super.prepare(c); + + // // If the component being prepared is of type KnowsGameSize, + // // call onResize() on it so that it stores the current game screen size. + // if (c is KnowsGameSize) { + // c.onResize(size); + // } + // } + + // @override + // void onResize(Vector2 canvasSize) { + // super.onResize(canvasSize); + + // // Loop over all the components of type KnowsGameSize and resize then as well. + // children.whereType().forEach((component) { + // component.onResize(size); + // }); + // } + // =================================== + + @override + void update(double dt) { + super.update(dt); + + // Run each command from _commandList on each + // component from components list. The run() + // method of Command is no-op if the command is + // not valid for given component. + for (var command in _commandList) { + for (var component in world.children) { + command.run(component); + } + } + + // Remove all the commands that are processed and + // add all new commands to be processed in next update. + _commandList.clear(); + _commandList.addAll(_addLaterCommandList); + _addLaterCommandList.clear(); + + if (_player.isMounted) { + // Update score and health components with latest values. + _playerScore.text = 'Score: ${_player.score}'; + _playerHealth.text = 'Health: ${_player.health}%'; + + /// Display [GameOverMenu] when [Player.health] becomes + /// zero and camera stops shaking. + // if (_player.health <= 0 && (!camera.shaking)) { + if (_player.health <= 0) { + pauseEngine(); + overlays.remove(PauseButton.id); + overlays.add(GameOverMenu.id); + } + } + } + + // This method handles state of app and pauses + // the game when necessary. + @override + void lifecycleStateChange(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.resumed: + break; + case AppLifecycleState.inactive: + case AppLifecycleState.paused: + case AppLifecycleState.detached: + case AppLifecycleState.hidden: + if (_player.health > 0) { + pauseEngine(); + overlays.remove(PauseButton.id); + overlays.add(PauseMenu.id); + } + break; + } + + super.lifecycleStateChange(state); + } + + // Adds given command to command list. + void addCommand(Command command) { + _addLaterCommandList.add(command); + } + + // Resets the game to inital state. Should be called + // while restarting and exiting the game. + void reset() { + // First reset player, enemy manager and power-up manager . + _player.reset(); + _enemyManager.reset(); + _powerUpManager.reset(); + + // Now remove all the enemies, bullets and power ups + // from the game world. Note that, we are not calling + // Enemy.destroy() because it will unnecessarily + // run explosion effect and increase players score. + world.children.whereType().forEach((enemy) { + enemy.removeFromParent(); + }); + + world.children.whereType().forEach((bullet) { + bullet.removeFromParent(); + }); + + world.children.whereType().forEach((powerUp) { + powerUp.removeFromParent(); + }); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/game/health_bar.dart b/flame/assets/examples/others/spaceescape/lib/game/health_bar.dart new file mode 100644 index 0000000..79fbd2c --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/game/health_bar.dart @@ -0,0 +1,29 @@ +import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; + +import 'player.dart'; + +class HealthBar extends PositionComponent { + final Player player; + + HealthBar({ + required this.player, + super.position, + super.size, + super.scale, + super.angle, + super.anchor, + super.children, + super.priority, + }); + + @override + void render(Canvas canvas) { + // Draws a rectangular health bar at top right corner. + canvas.drawRect( + Rect.fromLTWH(-2, 5, player.health.toDouble(), 20), + Paint()..color = Colors.blue, + ); + super.render(canvas); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/game/knows_game_size.dart b/flame/assets/examples/others/spaceescape/lib/game/knows_game_size.dart new file mode 100644 index 0000000..5565888 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/game/knows_game_size.dart @@ -0,0 +1,16 @@ +// IMPORTANT NOTE +// This class is obsolete since Flame v1.2.0 version +// This code remains as is as a reference for the YouTube tutorial. + +// import 'package:flame/components.dart'; + +// Adding this mixin to any class derived from BaseComponent will make sure that +// the components gets access to current gameSize. Do not try to access gameSize +// before your components is added to the game instance. +// mixin KnowsGameSize on Component { +// late Vector2 gameSize; + +// void onResize(Vector2 newGameSize) { +// gameSize = newGameSize; +// } +// } diff --git a/flame/assets/examples/others/spaceescape/lib/game/player.dart b/flame/assets/examples/others/spaceescape/lib/game/player.dart new file mode 100644 index 0000000..19149e5 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/game/player.dart @@ -0,0 +1,277 @@ +import 'dart:math'; + +import 'package:flame/collisions.dart'; +// import 'package:flame/effects.dart'; +import 'package:flame/experimental.dart'; +import 'package:flame/particles.dart'; +import 'package:flame/components.dart'; +// import 'package:flame_noise/flame_noise.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +import '../models/player_data.dart'; +import '../models/spaceship_details.dart'; + +import 'game.dart'; +import 'enemy.dart'; +import 'bullet.dart'; +import 'command.dart'; +import 'audio_player_component.dart'; + +// This component class represents the player character in game. +class Player extends SpriteComponent + with CollisionCallbacks, HasGameReference, KeyboardHandler { + // Player joystick + JoystickComponent joystick; + + // Player health. + int _health = 100; + int get health => _health; + + // Details of current spaceship. + Spaceship _spaceship; + + // Type of current spaceship. + SpaceshipType spaceshipType; + + // A reference to PlayerData so that + // we can modify money. + late PlayerData _playerData; + int get score => _playerData.currentScore; + + // If true, player will shoot 3 bullets at a time. + bool _shootMultipleBullets = false; + + // Controls for how long multi-bullet power up is active. + late Timer _powerUpTimer; + + // Holds an object of Random class to generate random numbers. + final _random = Random(); + + // This method generates a random vector such that + // its x component lies between [-100 to 100] and + // y component lies between [200, 400] + Vector2 getRandomVector() { + return (Vector2.random(_random) - Vector2(0.5, -1)) * 200; + } + + Player({ + required this.joystick, + required this.spaceshipType, + Sprite? sprite, + Vector2? position, + Vector2? size, + }) : _spaceship = Spaceship.getSpaceshipByType(spaceshipType), + super(sprite: sprite, position: position, size: size) { + // Sets power up timer to 4 seconds. After 4 seconds, + // multiple bullet will get deactivated. + _powerUpTimer = Timer(4, onTick: () { + _shootMultipleBullets = false; + }); + } + + @override + void onMount() { + super.onMount(); + + // Adding a circular hitbox with radius as 0.8 times + // the smallest dimension of this components size. + final shape = CircleHitbox.relative( + 0.8, + parentSize: size, + position: size / 2, + anchor: Anchor.center, + ); + add(shape); + + _playerData = Provider.of(game.buildContext!, listen: false); + } + + @override + void onCollision(Set intersectionPoints, PositionComponent other) { + super.onCollision(intersectionPoints, other); + + // If other entity is an Enemy, reduce player's health by 10. + if (other is Enemy) { + // Make the camera shake, with custom intensity. + // TODO: Investigate how camera shake should be implemented in new camera system. + // game.primaryCamera.viewfinder.add( + // MoveByEffect( + // Vector2.all(10), + // PerlinNoiseEffectController(duration: 1), + // ), + // ); + + _health -= 10; + if (_health <= 0) { + _health = 0; + } + } + } + + Vector2 keyboardDelta = Vector2.zero(); + static final _keysWatched = { + LogicalKeyboardKey.keyW, + LogicalKeyboardKey.keyA, + LogicalKeyboardKey.keyS, + LogicalKeyboardKey.keyD, + LogicalKeyboardKey.space, + }; + + @override + bool onKeyEvent(RawKeyEvent event, Set keysPressed) { + // Set this to zero first - if the user releases all keys pressed, then + // the set will be empty and our vector non-zero. + keyboardDelta.setZero(); + + if (!_keysWatched.contains(event.logicalKey)) return true; + + if (event is RawKeyDownEvent && + !event.repeat && + event.logicalKey == LogicalKeyboardKey.space) { + // pew pew! + joystickAction(); + } + + if (keysPressed.contains(LogicalKeyboardKey.keyW)) { + keyboardDelta.y = -1; + } + if (keysPressed.contains(LogicalKeyboardKey.keyA)) { + keyboardDelta.x = -1; + } + if (keysPressed.contains(LogicalKeyboardKey.keyS)) { + keyboardDelta.y = 1; + } + if (keysPressed.contains(LogicalKeyboardKey.keyD)) { + keyboardDelta.x = 1; + } + + // Handled keyboard input + return false; + } + + // This method is called by game class for every frame. + @override + void update(double dt) { + super.update(dt); + + _powerUpTimer.update(dt); + + // Increment the current position of player by (speed * delta time) along moveDirection. + // Delta time is the time elapsed since last update. For devices with higher frame rates, delta time + // will be smaller and for devices with lower frame rates, it will be larger. Multiplying speed with + // delta time ensure that player speed remains same irrespective of the device FPS. + if (!joystick.delta.isZero()) { + position.add(joystick.relativeDelta * _spaceship.speed * dt); + } + + if (!keyboardDelta.isZero()) { + position.add(keyboardDelta * _spaceship.speed * dt); + } + + // Clamp position of player such that the player sprite does not go outside the screen size. + position.clamp( + Vector2.zero() + size / 2, + game.fixedResolution - size / 2, + ); + + // Adds thruster particles. + final particleComponent = ParticleSystemComponent( + particle: Particle.generate( + count: 10, + lifespan: 0.1, + generator: (i) => AcceleratedParticle( + acceleration: getRandomVector(), + speed: getRandomVector(), + position: (position.clone() + Vector2(0, size.y / 3)), + child: CircleParticle( + radius: 1, + paint: Paint()..color = Colors.white, + ), + ), + ), + ); + + game.world.add(particleComponent); + } + + void joystickAction() { + Bullet bullet = Bullet( + sprite: game.spriteSheet.getSpriteById(28), + size: Vector2(64, 64), + position: position.clone(), + level: _spaceship.level, + ); + + // Anchor it to center and add to game world. + bullet.anchor = Anchor.center; + game.world.add(bullet); + + // Ask audio player to play bullet fire effect. + game.addCommand(Command(action: (audioPlayer) { + audioPlayer.playSfx('laserSmall_001.ogg'); + })); + + // If multiple bullet is on, add two more + // bullets rotated +-PI/6 radians to first bullet. + if (_shootMultipleBullets) { + for (int i = -1; i < 2; i += 2) { + Bullet bullet = Bullet( + sprite: game.spriteSheet.getSpriteById(28), + size: Vector2(64, 64), + position: position.clone(), + level: _spaceship.level, + ); + + // Anchor it to center and add to game world. + bullet.anchor = Anchor.center; + bullet.direction.rotate(i * pi / 6); + game.world.add(bullet); + } + } + } + + // Adds given points to player score + /// and also add it to [PlayerData.money]. + void addToScore(int points) { + _playerData.currentScore += points; + _playerData.money += points; + + // Saves player data to disk. + _playerData.save(); + } + + // Increases health by give amount. + void increaseHealthBy(int points) { + _health += points; + // Clamps health to 100. + if (_health > 100) { + _health = 100; + } + } + + // Resets player score, health and position. Should be called + // while restarting and exiting the game. + void reset() { + _playerData.currentScore = 0; + _health = 100; + position = game.fixedResolution / 2; + } + + // Changes the current spaceship type with given spaceship type. + // This method also takes care of updating the internal spaceship details + // as well as the spaceship sprite. + void setSpaceshipType(SpaceshipType spaceshipType) { + spaceshipType = spaceshipType; + _spaceship = Spaceship.getSpaceshipByType(spaceshipType); + sprite = game.spriteSheet.getSpriteById(_spaceship.spriteId); + } + + // Allows player to first multiple bullets for 4 seconds when called. + void shootMultipleBullets() { + _shootMultipleBullets = true; + _powerUpTimer.stop(); + _powerUpTimer.start(); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/game/power_up_manager.dart b/flame/assets/examples/others/spaceescape/lib/game/power_up_manager.dart new file mode 100644 index 0000000..b7900b5 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/game/power_up_manager.dart @@ -0,0 +1,143 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/experimental.dart'; + +import 'game.dart'; +import 'power_ups.dart'; + +typedef PowerUpMap + = Map; + +// Represents the types of power up we have to offer. +enum PowerUpTypes { health, freeze, nuke, multiFire } + +// This class/component is responsible for spawning random power ups +// at random locations in the game world. +class PowerUpManager extends Component with HasGameReference { + // Controls the frequency of spawning power ups. + late Timer _spawnTimer; + + // Controls the amount of time for which this component + /// should be frozen when [Freeze] power is activated. + late Timer _freezeTimer; + + // A random number generator. + Random random = Random(); + + // Storing these static sprites so that + // they stay alive across multiple restarts. + static late Sprite nukeSprite; + static late Sprite healthSprite; + static late Sprite freezeSprite; + static late Sprite multiFireSprite; + + // A private static map which stores a generator function for each power up. + static final PowerUpMap _powerUpMap = { + PowerUpTypes.health: (position, size) => Health( + position: position, + size: size, + ), + PowerUpTypes.freeze: (position, size) => Freeze( + position: position, + size: size, + ), + PowerUpTypes.nuke: (position, size) => Nuke( + position: position, + size: size, + ), + PowerUpTypes.multiFire: (position, size) => MultiFire( + position: position, + size: size, + ), + }; + + PowerUpManager() : super() { + // Makes sure that a new power up is spawned every 5 seconds. + _spawnTimer = Timer(5, onTick: _spawnPowerUp, repeat: true); + + // Restarts the spawn timer after 2 seconds are + // elapsed from start of freeze timer. + _freezeTimer = Timer(2, onTick: () { + _spawnTimer.start(); + }); + } + + // This method is responsible for generating a + // random power up at random location on the screen. + void _spawnPowerUp() { + Vector2 initialSize = Vector2(64, 64); + Vector2 position = Vector2( + random.nextDouble() * game.fixedResolution.x, + random.nextDouble() * game.fixedResolution.y, + ); + + // Clamp so that the power up does not + // go outside the screen. + position.clamp( + Vector2.zero() + initialSize / 2, + game.fixedResolution - initialSize / 2, + ); + + // Returns a random integer from 0 to (PowerUpTypes.values.length - 1). + int randomIndex = random.nextInt(PowerUpTypes.values.length); + + // Tried to get the generator function corresponding to selected random power. + final fn = _powerUpMap[PowerUpTypes.values.elementAt(randomIndex)]; + + // If the generator function is valid call it and get the power up. + var powerUp = fn?.call(position, initialSize); + + // If power up is valid, set anchor to center. + powerUp?.anchor = Anchor.center; + + // If power up is valid, add it to game world. + if (powerUp != null) { + game.world.add(powerUp); + } + } + + @override + void onMount() { + // Start the spawn timer as soon as this component is mounted. + _spawnTimer.start(); + + healthSprite = Sprite(game.images.fromCache('icon_plusSmall.png')); + nukeSprite = Sprite(game.images.fromCache('nuke.png')); + freezeSprite = Sprite(game.images.fromCache('freeze.png')); + multiFireSprite = Sprite(game.images.fromCache('multi_fire.png')); + + super.onMount(); + } + + @override + void onRemove() { + // Stop the spawn timer as soon as this component is removed. + _spawnTimer.stop(); + super.onRemove(); + } + + @override + void update(double dt) { + _spawnTimer.update(dt); + _freezeTimer.update(dt); + super.update(dt); + } + + // This method gets called when the game is being restarted. + void reset() { + // Stop all the timers. + _spawnTimer.stop(); + _spawnTimer.start(); + } + + // This method gets called when freeze power is activated. + void freeze() { + // Stop the spawn timer. + _spawnTimer.stop(); + + // Restart the freeze timer. + _freezeTimer.stop(); + _freezeTimer.start(); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/game/power_ups.dart b/flame/assets/examples/others/spaceescape/lib/game/power_ups.dart new file mode 100644 index 0000000..1ebcaaa --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/game/power_ups.dart @@ -0,0 +1,171 @@ +import 'package:flame/collisions.dart'; +import 'package:flame/components.dart'; +import 'package:flame/experimental.dart'; + +import 'game.dart'; +import 'enemy.dart'; +import 'player.dart'; +import 'command.dart'; +import 'enemy_manager.dart'; +import 'power_up_manager.dart'; +import 'audio_player_component.dart'; + +// An abstract class which represents power ups in this game. +/// See [Freeze], [Health], [MultiFire] and [Nuke] for example. +abstract class PowerUp extends SpriteComponent + with HasGameReference, CollisionCallbacks { + // Controls how long the power up should be visible + // before getting destroyed if not picked. + late Timer _timer; + + // Abstract method which child classes should override + /// and return a [Sprite] for the power up. + Sprite getSprite(); + + // Abstract method which child classes should override + // and perform any activation event necessary. + void onActivated(); + + PowerUp({ + Vector2? position, + Vector2? size, + Sprite? sprite, + }) : super(position: position, size: size, sprite: sprite) { + // Power ups will be displayed only for 3 seconds + // before getting destroyed. + _timer = Timer(3, onTick: removeFromParent); + } + + @override + void update(double dt) { + _timer.update(dt); + super.update(dt); + } + + @override + void onMount() { + // Add a circular hit box for this power up. + final shape = CircleHitbox.relative( + 0.5, + parentSize: size, + position: size / 2, + anchor: Anchor.center, + ); + add(shape); + + // Set the correct sprite by calling overriden getSprite method. + sprite = getSprite(); + + // Start the timer. + _timer.start(); + super.onMount(); + } + + @override + void onCollision(Set intersectionPoints, PositionComponent other) { + // If the other entity is Player, call the overriden + // onActivated method and mark this component to be removed. + if (other is Player) { + // Ask audio player to play power up activation effect. + game.addCommand(Command(action: (audioPlayer) { + audioPlayer.playSfx('powerUp6.ogg'); + })); + onActivated(); + removeFromParent(); + } + + super.onCollision(intersectionPoints, other); + } +} + +// This power up nukes all the enemies. +class Nuke extends PowerUp { + Nuke({Vector2? position, Vector2? size}) + : super(position: position, size: size); + + @override + Sprite getSprite() { + return PowerUpManager.nukeSprite; + } + + @override + void onActivated() { + // Register a command to destory all enemies. + final command = Command(action: (enemy) { + enemy.destroy(); + }); + game.addCommand(command); + } +} + +// This power up increases player health by 10. +class Health extends PowerUp { + Health({Vector2? position, Vector2? size}) + : super(position: position, size: size); + + @override + Sprite getSprite() { + return PowerUpManager.healthSprite; + } + + @override + void onActivated() { + // Register a command to increase player health. + final command = Command(action: (player) { + player.increaseHealthBy(10); + }); + game.addCommand(command); + } +} + +// This power up freezes all enemies for some time. +class Freeze extends PowerUp { + Freeze({Vector2? position, Vector2? size}) + : super(position: position, size: size); + + @override + Sprite getSprite() { + return PowerUpManager.freezeSprite; + } + + @override + void onActivated() { + // Register a command to freeze all enemies. + final command1 = Command(action: (enemy) { + enemy.freeze(); + }); + game.addCommand(command1); + + /// Register a command to freeze [EnemyManager]. + final command2 = Command(action: (enemyManager) { + enemyManager.freeze(); + }); + game.addCommand(command2); + + /// Register a command to freeze [PowerUpManager]. + final command3 = Command(action: (powerUpManager) { + powerUpManager.freeze(); + }); + game.addCommand(command3); + } +} + +// This power up activate multi-fire for some time. +class MultiFire extends PowerUp { + MultiFire({Vector2? position, Vector2? size}) + : super(position: position, size: size); + + @override + Sprite getSprite() { + return PowerUpManager.multiFireSprite; + } + + @override + void onActivated() { + // Register a command to allow multiple bullets. + final command = Command(action: (player) { + player.shootMultipleBullets(); + }); + game.addCommand(command); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/main.dart b/flame/assets/examples/others/spaceescape/lib/main.dart new file mode 100644 index 0000000..c911b61 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/main.dart @@ -0,0 +1,111 @@ +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:flame/flame.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'models/settings.dart'; +import 'screens/main_menu.dart'; +import 'models/player_data.dart'; +import 'models/spaceship_details.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // This opens the app in fullscreen mode. + await Flame.device.fullScreen(); + + // Initialize hive. + await initHive(); + + runApp( + MultiProvider( + providers: [ + FutureProvider( + create: (BuildContext context) => getPlayerData(), + initialData: PlayerData.fromMap(PlayerData.defaultData), + ), + FutureProvider( + create: (BuildContext context) => getSettings(), + initialData: Settings(soundEffects: false, backgroundMusic: false), + ), + ], + builder: (context, child) { + // We use .value constructor here because the required objects + // are already created by upstream FutureProviders. + return MultiProvider( + providers: [ + ChangeNotifierProvider.value( + value: Provider.of(context), + ), + ChangeNotifierProvider.value( + value: Provider.of(context), + ), + ], + child: child, + ); + }, + child: MaterialApp( + debugShowCheckedModeBanner: false, + // Dark more because we are too cool for white theme. + themeMode: ThemeMode.dark, + // Use custom theme with 'BungeeInline' font. + darkTheme: ThemeData( + brightness: Brightness.dark, + fontFamily: 'BungeeInline', + scaffoldBackgroundColor: Colors.black, + ), + // MainMenu will be the first screen for now. + // But this might change in future if we decide + // to add a splash screen. + home: const MainMenu(), + ), + ), + ); +} + +// This function initializes hive with app's +// documents directory and also registers +// all the hive adapters. +Future initHive() async { + await Hive.initFlutter(); + + Hive.registerAdapter(PlayerDataAdapter()); + Hive.registerAdapter(SpaceshipTypeAdapter()); + Hive.registerAdapter(SettingsAdapter()); +} + +/// This function reads the stored [PlayerData] from disk. +Future getPlayerData() async { + // Open the player data box and read player data from it. + final box = await Hive.openBox(PlayerData.playerDataBox); + final playerData = box.get(PlayerData.playerDataKey); + + // If player data is null, it means this is a fresh launch + // of the game. In such case, we first store the default + // player data in the player data box and then return the same. + if (playerData == null) { + box.put( + PlayerData.playerDataKey, + PlayerData.fromMap(PlayerData.defaultData), + ); + } + + return box.get(PlayerData.playerDataKey)!; +} + +/// This function reads the stored [Settings] from disk. +Future getSettings() async { + // Open the settings box and read settings from it. + final box = await Hive.openBox(Settings.settingsBox); + final settings = box.get(Settings.settingsKey); + + // If settings is null, it means this is a fresh launch + // of the game. In such case, we first store the default + // settings in the settings box and then return the same. + if (settings == null) { + box.put(Settings.settingsKey, + Settings(soundEffects: true, backgroundMusic: true)); + } + + return box.get(Settings.settingsKey)!; +} diff --git a/flame/assets/examples/others/spaceescape/lib/models/enemy_data.dart b/flame/assets/examples/others/spaceescape/lib/models/enemy_data.dart new file mode 100644 index 0000000..7514254 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/models/enemy_data.dart @@ -0,0 +1,26 @@ +/// This class represents all the details required +/// to create an [Enemy] component. +class EnemyData { + // Speed of the enemy. + final double speed; + + // Sprite ID from the main sprite sheet. + final int spriteId; + + // Level of this enemy. + final int level; + + // Indicates if this enemy can move horizontally. + final bool hMove; + + // Points gains after destroying this enemy. + final int killPoint; + + const EnemyData({ + required this.speed, + required this.spriteId, + required this.level, + required this.hMove, + required this.killPoint, + }); +} diff --git a/flame/assets/examples/others/spaceescape/lib/models/player_data.dart b/flame/assets/examples/others/spaceescape/lib/models/player_data.dart new file mode 100644 index 0000000..62bb02a --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/models/player_data.dart @@ -0,0 +1,113 @@ +import 'package:hive/hive.dart'; +import 'package:flutter/material.dart'; + +import 'spaceship_details.dart'; + +part 'player_data.g.dart'; + +// This class represents all the persistent data that we +// might want to store for tracking player progress. +@HiveType(typeId: 0) +class PlayerData extends ChangeNotifier with HiveObjectMixin { + static const String playerDataBox = 'PlayerDataBox'; + static const String playerDataKey = 'PlayerData'; + + // The spaceship type of player's current spaceship. + @HiveField(0) + SpaceshipType spaceshipType; + + // List of all the spaceships owned by player. + // Note that just storing their type is enough. + @HiveField(1) + final List ownedSpaceships; + + // Highest player score so far. + @HiveField(2) + late int _highScore; + int get highScore => _highScore; + + // Balance money. + @HiveField(3) + int money; + + // Keeps track of current score. + // If game is not running, this will + // represent score of last round. + int _currentScore = 0; + + int get currentScore => _currentScore; + + set currentScore(int newScore) { + _currentScore = newScore; + // While setting currentScore to a new value + // also make sure to update highScore. + if (_highScore < _currentScore) { + _highScore = _currentScore; + } + } + + PlayerData({ + required this.spaceshipType, + required this.ownedSpaceships, + int highScore = 0, + required this.money, + }) { + _highScore = highScore; + } + + /// Creates a new instance of [PlayerData] from given map. + PlayerData.fromMap(Map map) + : spaceshipType = map['currentSpaceshipType'], + ownedSpaceships = map['ownedSpaceshipTypes'] + .map((e) => e as SpaceshipType) // Map out each element. + .cast() // Cast each element to SpaceshipType. + .toList(), // Convert to a List. + _highScore = map['highScore'], + money = map['money']; + + // A default map which should be used for creating the + // very first PlayerData instance when game is launched + // for the first time. + static Map defaultData = { + 'currentSpaceshipType': SpaceshipType.canary, + 'ownedSpaceshipTypes': [SpaceshipType.canary], + 'highScore': 0, + 'money': 0, + }; + + /// Returns true if given [SpaceshipType] is owned by player. + bool isOwned(SpaceshipType spaceshipType) { + return ownedSpaceships.contains(spaceshipType); + } + + /// Returns true if player has enough money to by given [SpaceshipType]. + bool canBuy(SpaceshipType spaceshipType) { + return (money >= Spaceship.getSpaceshipByType(spaceshipType).cost); + } + + /// Returns true if player's current spaceship type is same as given [SpaceshipType]. + bool isEquipped(SpaceshipType spaceshipType) { + return (this.spaceshipType == spaceshipType); + } + + /// Buys the given [SpaceshipType] if player has enough money and does not already own it. + void buy(SpaceshipType spaceshipType) { + if (canBuy(spaceshipType) && (!isOwned(spaceshipType))) { + money -= Spaceship.getSpaceshipByType(spaceshipType).cost; + ownedSpaceships.add(spaceshipType); + notifyListeners(); + + // Saves player data to disk. + save(); + } + } + + /// Sets the given [SpaceshipType] as the current spaceship type for player. + void equip(SpaceshipType spaceshipType) { + this.spaceshipType = spaceshipType; + notifyListeners(); + + // Saves player data to disk. + save(); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/models/player_data.g.dart b/flame/assets/examples/others/spaceescape/lib/models/player_data.g.dart new file mode 100644 index 0000000..9afea2a --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/models/player_data.g.dart @@ -0,0 +1,49 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'player_data.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class PlayerDataAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + PlayerData read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return PlayerData( + spaceshipType: fields[0] as SpaceshipType, + ownedSpaceships: (fields[1] as List).cast(), + money: fields[3] as int, + ).._highScore = fields[2] as int; + } + + @override + void write(BinaryWriter writer, PlayerData obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.spaceshipType) + ..writeByte(1) + ..write(obj.ownedSpaceships) + ..writeByte(2) + ..write(obj._highScore) + ..writeByte(3) + ..write(obj.money); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PlayerDataAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/flame/assets/examples/others/spaceescape/lib/models/settings.dart b/flame/assets/examples/others/spaceescape/lib/models/settings.dart new file mode 100644 index 0000000..daec5f9 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/models/settings.dart @@ -0,0 +1,34 @@ +import 'package:hive/hive.dart'; +import 'package:flutter/foundation.dart'; + +part 'settings.g.dart'; + +@HiveType(typeId: 2) +class Settings extends ChangeNotifier with HiveObjectMixin { + static const String settingsBox = 'SettingsBox'; + static const String settingsKey = 'Settings'; + + @HiveField(0) + bool _sfx = false; + bool get soundEffects => _sfx; + set soundEffects(bool value) { + _sfx = value; + notifyListeners(); + save(); + } + + @HiveField(1) + bool _bgm = false; + bool get backgroundMusic => _bgm; + set backgroundMusic(bool value) { + _bgm = value; + notifyListeners(); + save(); + } + + Settings({ + bool soundEffects = false, + bool backgroundMusic = false, + }) : _bgm = backgroundMusic, + _sfx = soundEffects; +} diff --git a/flame/assets/examples/others/spaceescape/lib/models/settings.g.dart b/flame/assets/examples/others/spaceescape/lib/models/settings.g.dart new file mode 100644 index 0000000..b7e2bb3 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/models/settings.g.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'settings.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class SettingsAdapter extends TypeAdapter { + @override + final int typeId = 2; + + @override + Settings read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Settings() + .._sfx = fields[0] as bool + .._bgm = fields[1] as bool; + } + + @override + void write(BinaryWriter writer, Settings obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj._sfx) + ..writeByte(1) + ..write(obj._bgm); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SettingsAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/flame/assets/examples/others/spaceescape/lib/models/spaceship_details.dart b/flame/assets/examples/others/spaceescape/lib/models/spaceship_details.dart new file mode 100644 index 0000000..be72a90 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/models/spaceship_details.dart @@ -0,0 +1,141 @@ +// This class represents all the data +// which defines a spaceship. +import 'package:hive/hive.dart'; + +part 'spaceship_details.g.dart'; + +class Spaceship { + // Name of the spaceship. + final String name; + + // Cost of the spaceship. + final int cost; + + // Cost of the spaceship. + final double speed; + + // SpriteId to be used for displaying + // this spaceship in game world. + final int spriteId; + + // Path to the asset to be used for displaying + // this spaceship outside game world. + final String assetPath; + + // Level of the spaceship. + final int level; + + const Spaceship({ + required this.name, + required this.cost, + required this.speed, + required this.spriteId, + required this.assetPath, + required this.level, + }); + + /// Given a spaceshipType, this method returns a corresponding + /// Spaceship object which holds all the details of that spaceship. + static Spaceship getSpaceshipByType(SpaceshipType spaceshipType) { + /// It is highly unlikely that it [spaceships] does not contain given [spaceshipType]. + /// But if that ever happens, we will just return data for [SpaceshipType.Canary]. + return spaceships[spaceshipType] ?? spaceships.entries.first.value; + } + + /// This map holds all the meta-data of each [SpaceshipType]. + static const Map spaceships = { + SpaceshipType.canary: Spaceship( + name: 'Canary', + cost: 0, + speed: 250, + spriteId: 0, + assetPath: 'assets/images/ship_A.png', + level: 1, + ), + SpaceshipType.dusky: Spaceship( + name: 'Dusky', + cost: 100, + speed: 400, + spriteId: 1, + assetPath: 'assets/images/ship_B.png', + level: 2, + ), + SpaceshipType.condor: Spaceship( + name: 'Condor', + cost: 200, + speed: 300, + spriteId: 2, + assetPath: 'assets/images/ship_C.png', + level: 2, + ), + SpaceshipType.cXC: Spaceship( + name: 'CXC', + cost: 400, + speed: 300, + spriteId: 3, + assetPath: 'assets/images/ship_D.png', + level: 3, + ), + SpaceshipType.raptor: Spaceship( + name: 'Raptor', + cost: 550, + speed: 300, + spriteId: 4, + assetPath: 'assets/images/ship_E.png', + level: 3, + ), + SpaceshipType.raptorX: Spaceship( + name: 'Raptor-X', + cost: 700, + speed: 350, + spriteId: 5, + assetPath: 'assets/images/ship_F.png', + level: 3, + ), + SpaceshipType.albatross: Spaceship( + name: 'Albatross', + cost: 850, + speed: 400, + spriteId: 6, + assetPath: 'assets/images/ship_G.png', + level: 4, + ), + SpaceshipType.dK809: Spaceship( + name: 'DK-809', + cost: 1000, + speed: 450, + spriteId: 7, + assetPath: 'assets/images/ship_H.png', + level: 4, + ), + }; +} + +// This enum represents all the spaceship +// types available in this game. +@HiveType(typeId: 1) +enum SpaceshipType { + @HiveField(0) + canary, + + @HiveField(1) + dusky, + + @HiveField(2) + condor, + + @HiveField(3) + cXC, + + @HiveField(4) + raptor, + + @HiveField(5) + raptorX, + + @HiveField(6) + albatross, + + @HiveField(7) + dK809, +} diff --git a/flame/assets/examples/others/spaceescape/lib/models/spaceship_details.g.dart b/flame/assets/examples/others/spaceescape/lib/models/spaceship_details.g.dart new file mode 100644 index 0000000..fa09e17 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/models/spaceship_details.g.dart @@ -0,0 +1,76 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'spaceship_details.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class SpaceshipTypeAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + SpaceshipType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return SpaceshipType.canary; + case 1: + return SpaceshipType.dusky; + case 2: + return SpaceshipType.condor; + case 3: + return SpaceshipType.cXC; + case 4: + return SpaceshipType.raptor; + case 5: + return SpaceshipType.raptorX; + case 6: + return SpaceshipType.albatross; + case 7: + return SpaceshipType.dK809; + default: + return SpaceshipType.canary; + } + } + + @override + void write(BinaryWriter writer, SpaceshipType obj) { + switch (obj) { + case SpaceshipType.canary: + writer.writeByte(0); + break; + case SpaceshipType.dusky: + writer.writeByte(1); + break; + case SpaceshipType.condor: + writer.writeByte(2); + break; + case SpaceshipType.cXC: + writer.writeByte(3); + break; + case SpaceshipType.raptor: + writer.writeByte(4); + break; + case SpaceshipType.raptorX: + writer.writeByte(5); + break; + case SpaceshipType.albatross: + writer.writeByte(6); + break; + case SpaceshipType.dK809: + writer.writeByte(7); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is SpaceshipTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/flame/assets/examples/others/spaceescape/lib/screens/game_play.dart b/flame/assets/examples/others/spaceescape/lib/screens/game_play.dart new file mode 100644 index 0000000..1de7463 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/screens/game_play.dart @@ -0,0 +1,50 @@ +import 'package:flame/game.dart'; +import 'package:flutter/material.dart'; +import 'package:spacescape/widgets/overlays/game_over_menu.dart'; + +import '../game/game.dart'; +import '../widgets/overlays/pause_button.dart'; +import '../widgets/overlays/pause_menu.dart'; + +// Creating this as a file private object so as to +// avoid unwanted rebuilds of the whole game object. +SpacescapeGame _spacescapeGame = SpacescapeGame(); + +// This class represents the actual game screen +// where all the action happens. +class GamePlay extends StatelessWidget { + const GamePlay({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + // WillPopScope provides us a way to decide if + // this widget should be poped or not when user + // presses the back button. + body: PopScope( + canPop: false, + // GameWidget is useful to inject the underlying + // widget of any class extending from Flame's Game class. + child: GameWidget( + game: _spacescapeGame, + // Initially only pause button overlay will be visible. + initialActiveOverlays: const [PauseButton.id], + overlayBuilderMap: { + PauseButton.id: (BuildContext context, SpacescapeGame game) => + PauseButton( + game: game, + ), + PauseMenu.id: (BuildContext context, SpacescapeGame game) => + PauseMenu( + game: game, + ), + GameOverMenu.id: (BuildContext context, SpacescapeGame game) => + GameOverMenu( + game: game, + ), + }, + ), + ), + ); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/screens/main_menu.dart b/flame/assets/examples/others/spaceescape/lib/screens/main_menu.dart new file mode 100644 index 0000000..3a3c516 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/screens/main_menu.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; + +import 'settings_menu.dart'; +import 'select_spaceship.dart'; + +// Represents the main menu screen of Spacescape, allowing +// players to start the game or modify in-game settings. +class MainMenu extends StatelessWidget { + const MainMenu({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Game title. + const Padding( + padding: EdgeInsets.symmetric(vertical: 50.0), + child: Text( + 'Spacescape', + style: TextStyle( + fontSize: 50.0, + color: Colors.black, + shadows: [ + Shadow( + blurRadius: 20.0, + color: Colors.white, + offset: Offset(0, 0), + ) + ], + ), + ), + ), + + // Play button. + SizedBox( + width: MediaQuery.of(context).size.width / 3, + child: ElevatedButton( + onPressed: () { + // Push and replace current screen (i.e MainMenu) with + // SelectSpaceship(), so that player can select a spaceship. + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => const SelectSpaceship(), + ), + ); + }, + child: const Text('Play'), + ), + ), + + // Settings button. + SizedBox( + width: MediaQuery.of(context).size.width / 3, + child: ElevatedButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const SettingsMenu(), + ), + ); + }, + child: const Text('Settings'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/screens/select_spaceship.dart b/flame/assets/examples/others/spaceescape/lib/screens/select_spaceship.dart new file mode 100644 index 0000000..041f0c5 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/screens/select_spaceship.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_carousel_slider/carousel_slider.dart'; +import 'package:provider/provider.dart'; + +import '../models/player_data.dart'; +import '../models/spaceship_details.dart'; + +import 'game_play.dart'; +import 'main_menu.dart'; + +// Represents the spaceship selection menu from where player can +// change current spaceship or buy a new one. +class SelectSpaceship extends StatelessWidget { + const SelectSpaceship({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Game title. + const Padding( + padding: EdgeInsets.symmetric(vertical: 50.0), + child: Text( + 'Select', + style: TextStyle( + fontSize: 50.0, + color: Colors.black, + shadows: [ + Shadow( + blurRadius: 20.0, + color: Colors.white, + offset: Offset(0, 0), + ) + ], + ), + ), + ), + + // Displays current spaceship's name and amount of money left. + Consumer( + builder: (context, playerData, child) { + final spaceship = + Spaceship.getSpaceshipByType(playerData.spaceshipType); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text('Ship: ${spaceship.name}'), + Text('Money: ${playerData.money}'), + ], + ); + }, + ), + + SizedBox( + height: MediaQuery.of(context).size.height * 0.5, + child: CarouselSlider.builder( + itemCount: Spaceship.spaceships.length, + slideBuilder: (index) { + final spaceship = + Spaceship.spaceships.entries.elementAt(index).value; + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset(spaceship.assetPath), + Text(spaceship.name), + Text('Speed: ${spaceship.speed}'), + Text('Level: ${spaceship.level}'), + Text('Cost: ${spaceship.cost}'), + Consumer( + builder: (context, playerData, child) { + final type = + Spaceship.spaceships.entries.elementAt(index).key; + final isEquipped = playerData.isEquipped(type); + final isOwned = playerData.isOwned(type); + final canBuy = playerData.canBuy(type); + + return ElevatedButton( + onPressed: isEquipped + ? null + : () { + if (isOwned) { + playerData.equip(type); + } else { + if (canBuy) { + playerData.buy(type); + } else { + // Displays an alert if player + // does not have enough money. + showDialog( + context: context, + builder: (context) { + return AlertDialog( + backgroundColor: Colors.red, + title: const Text( + 'Insufficient Balance', + textAlign: TextAlign.center, + ), + content: Text( + 'Need ${spaceship.cost - playerData.money} more', + textAlign: TextAlign.center, + ), + ); + }, + ); + } + } + }, + child: Text( + isEquipped + ? 'Equipped' + : isOwned + ? 'Select' + : 'Buy', + ), + ); + }, + ), + ], + ); + }, + ), + ), + + // Start button. + SizedBox( + width: MediaQuery.of(context).size.width / 3, + child: ElevatedButton( + onPressed: () { + // Push and replace current screen (i.e MainMenu) with + // GamePlay, because back press will be blocked by GamePlay. + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => const GamePlay(), + ), + ); + }, + child: const Text('Start'), + ), + ), + + // Back button. + SizedBox( + width: MediaQuery.of(context).size.width / 3, + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => const MainMenu(), + ), + ); + }, + child: const Icon(Icons.arrow_back), + ), + ), + ], + ), + ), + ); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/screens/settings_menu.dart b/flame/assets/examples/others/spaceescape/lib/screens/settings_menu.dart new file mode 100644 index 0000000..5990af7 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/screens/settings_menu.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:spacescape/models/settings.dart'; + +// This class represents the settings menu. +class SettingsMenu extends StatelessWidget { + const SettingsMenu({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Game title. + const Padding( + padding: EdgeInsets.symmetric(vertical: 50.0), + child: Text( + 'Settings', + style: TextStyle( + fontSize: 50.0, + color: Colors.black, + shadows: [ + Shadow( + blurRadius: 20.0, + color: Colors.white, + offset: Offset(0, 0), + ) + ], + ), + ), + ), + + // Switch for sound effects. + Selector( + selector: (context, settings) => settings.soundEffects, + builder: (context, value, child) { + return SwitchListTile( + title: const Text('Sound Effects'), + value: value, + onChanged: (newValue) { + Provider.of(context, listen: false).soundEffects = + newValue; + }, + ); + }, + ), + + // Switch for background music. + Selector( + selector: (context, settings) => settings.backgroundMusic, + builder: (context, value, child) { + return SwitchListTile( + title: const Text('Background Music'), + value: value, + onChanged: (newValue) { + Provider.of(context, listen: false) + .backgroundMusic = newValue; + }, + ); + }, + ), + + // Back button. + SizedBox( + width: MediaQuery.of(context).size.width / 4, + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Icon(Icons.arrow_back), + ), + ), + ], + ), + ), + ); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/widgets/overlays/game_over_menu.dart b/flame/assets/examples/others/spaceescape/lib/widgets/overlays/game_over_menu.dart new file mode 100644 index 0000000..b69e214 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/widgets/overlays/game_over_menu.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; + +import '../../game/game.dart'; +import '../../screens/main_menu.dart'; +import 'pause_button.dart'; + +// This class represents the game over menu overlay. +class GameOverMenu extends StatelessWidget { + static const String id = 'GameOverMenu'; + final SpacescapeGame game; + + const GameOverMenu({Key? key, required this.game}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Pause menu title. + const Padding( + padding: EdgeInsets.symmetric(vertical: 50.0), + child: Text( + 'Game Over', + style: TextStyle( + fontSize: 50.0, + color: Colors.black, + shadows: [ + Shadow( + blurRadius: 20.0, + color: Colors.white, + offset: Offset(0, 0), + ) + ], + ), + ), + ), + + // Restart button. + SizedBox( + width: MediaQuery.of(context).size.width / 3, + child: ElevatedButton( + onPressed: () { + game.overlays.remove(GameOverMenu.id); + game.overlays.add(PauseButton.id); + game.reset(); + game.resumeEngine(); + }, + child: const Text('Restart'), + ), + ), + + // Exit button. + SizedBox( + width: MediaQuery.of(context).size.width / 3, + child: ElevatedButton( + onPressed: () { + game.overlays.remove(GameOverMenu.id); + game.reset(); + game.resumeEngine(); + + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => const MainMenu(), + ), + ); + }, + child: const Text('Exit'), + ), + ), + ], + ), + ); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/widgets/overlays/pause_button.dart b/flame/assets/examples/others/spaceescape/lib/widgets/overlays/pause_button.dart new file mode 100644 index 0000000..ef03972 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/widgets/overlays/pause_button.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import '../../game/game.dart'; +import 'pause_menu.dart'; + +// This class represents the pause button overlay. +class PauseButton extends StatelessWidget { + static const String id = 'PauseButton'; + final SpacescapeGame game; + + const PauseButton({Key? key, required this.game}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Align( + alignment: Alignment.topCenter, + child: TextButton( + child: const Icon( + Icons.pause_rounded, + color: Colors.white, + ), + onPressed: () { + game.pauseEngine(); + game.overlays.add(PauseMenu.id); + game.overlays.remove(PauseButton.id); + }, + ), + ); + } +} diff --git a/flame/assets/examples/others/spaceescape/lib/widgets/overlays/pause_menu.dart b/flame/assets/examples/others/spaceescape/lib/widgets/overlays/pause_menu.dart new file mode 100644 index 0000000..6b57489 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/lib/widgets/overlays/pause_menu.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +import '../../game/game.dart'; +import '../../screens/main_menu.dart'; +import 'pause_button.dart'; + +// This class represents the pause menu overlay. +class PauseMenu extends StatelessWidget { + static const String id = 'PauseMenu'; + final SpacescapeGame game; + + const PauseMenu({Key? key, required this.game}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Pause menu title. + const Padding( + padding: EdgeInsets.symmetric(vertical: 50.0), + child: Text( + 'Paused', + style: TextStyle( + fontSize: 50.0, + color: Colors.black, + shadows: [ + Shadow( + blurRadius: 20.0, + color: Colors.white, + offset: Offset(0, 0), + ) + ], + ), + ), + ), + + // Resume button. + SizedBox( + width: MediaQuery.of(context).size.width / 3, + child: ElevatedButton( + onPressed: () { + game.resumeEngine(); + game.overlays.remove(PauseMenu.id); + game.overlays.add(PauseButton.id); + }, + child: const Text('Resume'), + ), + ), + + // Restart button. + SizedBox( + width: MediaQuery.of(context).size.width / 3, + child: ElevatedButton( + onPressed: () { + game.overlays.remove(PauseMenu.id); + game.overlays.add(PauseButton.id); + game.reset(); + game.resumeEngine(); + }, + child: const Text('Restart'), + ), + ), + + // Exit button. + SizedBox( + width: MediaQuery.of(context).size.width / 3, + child: ElevatedButton( + onPressed: () { + game.overlays.remove(PauseMenu.id); + game.reset(); + game.resumeEngine(); + + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => const MainMenu(), + ), + ); + }, + child: const Text('Exit'), + ), + ), + ], + ), + ); + } +} diff --git a/flame/assets/examples/others/spaceescape/pubspec.yaml b/flame/assets/examples/others/spaceescape/pubspec.yaml new file mode 100644 index 0000000..67351d3 --- /dev/null +++ b/flame/assets/examples/others/spaceescape/pubspec.yaml @@ -0,0 +1,38 @@ +name: spacescape +description: A 2D top down space shooter. + +publish_to: "none" + +version: 1.0.0+1 +environment: + sdk: ">=2.17.0 <3.0.0" + +dependencies: + flame: 1.8.2 + flame_audio: 2.0.5 + provider: 6.0.5 + flutter_carousel_slider: 1.1.0 + hive: 2.2.3 + hive_generator: 2.0.1 + path_provider: 2.1.0 + flutter: + sdk: flutter + cupertino_icons: 1.0.5 + hive_flutter: 1.1.0 + # flame_noise: 0.1.1 + +dev_dependencies: + build_runner: 2.4.6 + flutter_test: + sdk: flutter + flutter_lints: 2.0.2 + +flutter: + uses-material-design: true + assets: + - assets/images/ + - assets/audio/ + fonts: + - family: BungeeInline + fonts: + - asset: assets/fonts/BungeeInline/BungeeInline-Regular.ttf diff --git a/flame/assets/logo/flame_logo.png b/flame/assets/logo/flame_logo.png new file mode 100644 index 0000000..0469eaf Binary files /dev/null and b/flame/assets/logo/flame_logo.png differ diff --git a/flame/bin/main.dart b/flame/bin/main.dart new file mode 100644 index 0000000..72f1fb3 --- /dev/null +++ b/flame/bin/main.dart @@ -0,0 +1,9 @@ +import 'package:dash_agent/dash_agent.dart'; +import 'package:flame/agent.dart'; +import 'package:flame/data_source/github_issues_data_source_helper.dart'; + +Future main() async { + final openIssues = await generateGitIssuesLink(); + final closedIssues = await generateGitIssuesLink(closedIssues: true); + await processAgent(FlameAgent(issuesLinks: [...closedIssues, ...openIssues])); +} diff --git a/flame/lib/agent.dart b/flame/lib/agent.dart new file mode 100644 index 0000000..7fb2104 --- /dev/null +++ b/flame/lib/agent.dart @@ -0,0 +1,57 @@ +import 'package:dash_agent/configuration/metadata.dart'; +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/configuration/dash_agent.dart'; +import 'package:flame/data_source/data_sources.dart'; +import 'commands/ask.dart'; +import 'commands/debug.dart'; +import 'commands/generate.dart'; +import 'commands/test.dart'; + +class FlameAgent extends AgentConfiguration { + final List issuesLinks; + final IssuesDataSource issueSource; + final docsSource = DocsDataSource(); + final exampleSource = ExampleDataSource(); + final testExampleSource = TestExampleDataSource(); + + FlameAgent({required this.issuesLinks}) + : issueSource = IssuesDataSource(issuesLinks); + + @override + List get registerDataSources => + [docsSource, exampleSource, testExampleSource, issueSource]; + + @override + List get registerSupportedCommands => [ + AskCommand( + docsSource: docsSource, + exampleDataSource: exampleSource, + issuesDataSource: issueSource), + GenerateCommand( + exampleDataSource: exampleSource, docDataSource: docsSource), + DebugCommand( + exampleDataSource: exampleSource, + docDataSource: docsSource, + issueDataSource: issueSource), + TestCommand(testExampleSource) + ]; + + @override + Metadata get metadata => Metadata( + name: 'Flame', + avatarProfile: 'assets/logo/flame_logo.png', + tags: [ + 'flutter', + 'dart', + 'flutter favorite', + 'game engine', + 'flutter package' + ]); + + @override + String get registerSystemPrompt => + '''You are a Flame Integration assistant inside user's IDE. Flame is a 2D game engine built on top of Flutter. It provides the core building blocks for creating games, like a game loop, collision detection, and sprite animation. This allows you to focus on the game logic itself without needing to write low-level graphics code. + + You will be provided with latest docs and examples relevant to user queries and you have to help user with any questions they have related to Flame. Output code and code links wherever required and answer "I don't know" if the user query is not covered in the docs provided to you'''; +} diff --git a/flame/lib/commands/ask.dart b/flame/lib/commands/ask.dart new file mode 100644 index 0000000..0c14b92 --- /dev/null +++ b/flame/lib/commands/ask.dart @@ -0,0 +1,65 @@ +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/steps/steps.dart'; +import 'package:dash_agent/variables/dash_input.dart'; +import 'package:dash_agent/variables/dash_output.dart'; + +class AskCommand extends Command { + AskCommand( + {required this.docsSource, + required this.exampleDataSource, + required this.issuesDataSource}); + + final DataSource docsSource; + final DataSource exampleDataSource; + final DataSource issuesDataSource; + + /// Inputs to be provided by the user in the text field + final userQuery = StringInput('Query', optional: false); + final codeAttachment = CodeInput('Code', optional: true); + + @override + String get slug => 'ask'; + + @override + String get intent => 'Ask me anything'; + + @override + String get textFieldLayout => + "Hi, I'm here to help you. Please let know your $userQuery and any optionally any relevant code: $codeAttachment"; + + @override + List get registerInputs => [userQuery, codeAttachment]; + + @override + List get steps { + final matchingDocuments = MatchDocumentOuput(); + final promptOutput = PromptOutput(); + + return [ + MatchDocumentStep( + query: '$userQuery$codeAttachment', + dataSources: [docsSource, exampleDataSource, issuesDataSource], + output: matchingDocuments), + PromptQueryStep( + prompt: + '''You are tasked with answering any query related to flutter's flame framework. Please find the below shared details: + + Query: $userQuery, + + Code Attachment: + $codeAttachment + + References: + $matchingDocuments. + + Note: + 1. If the references don't address the question, state that "I couldn't fetch your answer from the doc sources, but I'll try to answer from my own knowledge". + 2. Be truthful, complete and detailed with your responses and include code snippets wherever required. + Now, answer the user's query.''', + promptOutput: promptOutput, + ), + AppendToChatStep(value: '$promptOutput') + ]; + } +} diff --git a/flame/lib/commands/debug.dart b/flame/lib/commands/debug.dart new file mode 100644 index 0000000..6c5c189 --- /dev/null +++ b/flame/lib/commands/debug.dart @@ -0,0 +1,71 @@ +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/steps/steps.dart'; +import 'package:dash_agent/variables/dash_input.dart'; +import 'package:dash_agent/variables/dash_output.dart'; + +class DebugCommand extends Command { + DebugCommand( + {required this.exampleDataSource, + required this.docDataSource, + required this.issueDataSource}); + final DataSource exampleDataSource; + final DataSource docDataSource; + final DataSource issueDataSource; + final issueDescription = StringInput('Issue Description'); + final codeReference1 = CodeInput('Reference', optional: true); + final codeReference2 = CodeInput('Reference', optional: true); + + @override + String get intent => 'Debug flame code'; + + @override + List get registerInputs => + [issueDescription, codeReference1, codeReference2]; + + @override + String get slug => 'debug'; + + @override + List get steps { + final docReferences = MatchDocumentOuput(); + final issueReferences = MatchDocumentOuput(); + final resultOutput = PromptOutput(); + return [ + MatchDocumentStep( + query: '$issueDescription', + dataSources: [exampleDataSource, docDataSource], + output: docReferences), + MatchDocumentStep( + query: '$issueDescription', + dataSources: [issueDataSource], + output: issueReferences), + PromptQueryStep( + prompt: + '''Assist in debugging the code written using Flutter's flame package based on the information shared. + Issue Description: $issueDescription + + Code References: + ```dart + // code reference 1 + $codeReference1 + + // code reference 2 + $codeReference2 + ``` + + Official Chopper Documentation References: + $docReferences + + GitHub Issue Messages: + $issueReferences + ''', + promptOutput: resultOutput), + AppendToChatStep(value: '$resultOutput') + ]; + } + + @override + String get textFieldLayout => + 'Share the following details to understand your issue better: $issueDescription \n\nOptionally attach any references: $codeReference1 $codeReference2'; +} diff --git a/flame/lib/commands/generate.dart b/flame/lib/commands/generate.dart new file mode 100644 index 0000000..a7af84e --- /dev/null +++ b/flame/lib/commands/generate.dart @@ -0,0 +1,105 @@ +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/steps/steps.dart'; +import 'package:dash_agent/variables/dash_input.dart'; +import 'package:dash_agent/variables/dash_output.dart'; + +class GenerateCommand extends Command { + GenerateCommand( + {required this.exampleDataSource, required this.docDataSource}); + final DataSource exampleDataSource; + final DataSource docDataSource; + final generateInstructions = StringInput('Instructions'); + final codeReference1 = CodeInput('Reference', optional: true); + final codeReference2 = CodeInput('Reference', optional: true); + + @override + String get intent => 'Generate flame code'; + + @override + List get registerInputs => + [generateInstructions, codeReference1, codeReference2]; + + @override + String get slug => 'generate'; + + @override + List get steps { + final docReferences = MatchDocumentOuput(); + final codeReferences = MatchDocumentOuput(); + final filteredReferences = PromptOutput(); + final resultOutput = PromptOutput(); + return [ + MatchDocumentStep( + query: + 'examples/instructions of writing code using flame - $generateInstructions $codeReference1 $codeReference2.', + dataSources: [docDataSource], + output: docReferences), + MatchDocumentStep( + query: + 'examples/instructions of writing code using flame - $generateInstructions $codeReference1 $codeReference2.', + dataSources: [exampleDataSource], + output: codeReferences), + PromptQueryStep( + prompt: + '''You are tasked with finding the at most top 3 most relevant references from the shared input refereces for a specific query. Your goal is to provide a concise list of references out of the shared references in Markdown format. + +Query: examples/instructions of writing code using flame package - $generateInstructions $codeReference1 $codeReference2. + +Input References: +## Doc References +$docReferences + +## Code Refrences +$codeReferences + +Instructions: +1. Read through the provided inpur references and identify the most relevant references that are pertinent to the given query. +2. For each relevant reference, provide the following information in Markdown format: + - Brief Summary describing relevance to the given query + - Reference content + +Example + Input: + Query: Latest version of go_router + + Input Refernces: + This article introduces go_router, a new Flutter router package designed specifically for web applications. + This blog post announces the release of go_router version 2.0, highlighting the new features and improvements. + This guide explains asynchronous programming concepts in Dart, including Futures, Streams, and async/await syntax. + + Output: + - **Reference Title:** "Blog on go_router v2.0 Released: What's New?" + - **Reference Content**: This blog post announces the release of go_router version 2.0, highlighting the new features and improvements. + +Please provide the list of relevant references in the specified Markdown format.''', + promptOutput: filteredReferences), + PromptQueryStep( + prompt: + '''You are tasked with generating Flutter code to create games or related components using the Flame package. + Instructions: $generateInstructions + + Code References: + ```dart + // code reference 1 + $codeReference1 + + // code reference 2 + $codeReference2 + ``` + + Documentation or examples of the chopper package for reference: + $filteredReferences + + + Now, generate code based on the user's instructions and shared references. + ''', + promptOutput: resultOutput), + AppendToChatStep(value: '$resultOutput') + ]; + } + + @override + String get textFieldLayout => + 'Generate the code using flame: $generateInstructions \n\nOptionally attach any references: $codeReference1 $codeReference2'; +} \ No newline at end of file diff --git a/flame/lib/commands/test.dart b/flame/lib/commands/test.dart new file mode 100644 index 0000000..2ea0936 --- /dev/null +++ b/flame/lib/commands/test.dart @@ -0,0 +1,121 @@ +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/steps/steps.dart'; +import 'package:dash_agent/variables/dash_input.dart'; +import 'package:dash_agent/variables/dash_output.dart'; + +class TestCommand extends Command { + TestCommand(this.testDataSource); + final DataSource testDataSource; + final primaryObject = CodeInput('Test Object'); + final testInstructions = StringInput('Instructions', optional: true); + final referenceObject1 = CodeInput('Reference', optional: true); + final referenceObject2 = CodeInput('Reference', optional: true); + final referenceObject3 = CodeInput('Reference', optional: true); + @override + String get intent => 'Write tests for your navigation related code using go_router'; + + @override + List get registerInputs => [ + primaryObject, + testInstructions, + referenceObject1, + referenceObject2, + referenceObject3 + ]; + + @override + String get slug => 'test'; + + @override + String get textFieldLayout => + 'Generate test for your flame-related code $primaryObject with $testInstructions\n\nOptionally attach any supporting code: $referenceObject1 $referenceObject2 $referenceObject3'; + + @override + List get steps { + final testOutput = PromptOutput(); + final testReferences = MatchDocumentOuput(); + return [ + MatchDocumentStep( + query: + 'examples/instructions of writing tests for flame code - $testInstructions $primaryObject.', + dataSources: [testDataSource], + output: testReferences), + PromptQueryStep( + prompt: + '''You are tasked with testing the flame code. + +Write tests for flame-related code with instructions - $testInstructions. + +Code: +```dart +$primaryObject +``` + +Here are some contextual code or references provided by the user: +```dart +// reference 1 +$referenceObject1 + +// reference 2 +$referenceObject2 + +// reference 3 +$referenceObject3 +``` + +Few sample Unit tests unrelated to the above scenerio as a reference: + +```dart +$testReferences +``` + +```dart +import 'package:flame_test/flame_test.dart'; +import 'package:flame_test_example/game.dart'; +import 'package:flutter_test/flutter_test.dart'; + +final myGame = FlameTester(MyGame.new); +void main() { + group('flameTest', () { + TestWidgetsFlutterBinding.ensureInitialized(); + + testWithGame( + 'can load the game', + MyGame.new, + (game) async { + expect(game.world.children.length, 1); + }, + ); + + myGame.testGameWidget( + 'render the game widget', + verify: (game, tester) async { + expect( + find.byGame(), + findsOneWidget, + ); + }, + ); + + myGame.testGameWidget( + 'render the background correctly', + setUp: (game, _) async { + await game.ready(); + }, + verify: (game, tester) async { + await expectLater( + find.byGame(), + matchesGoldenFile('goldens/game.png'), + ); + }, + ); + }); +} +``` +''', + promptOutput: testOutput), + AppendToChatStep(value: '$testOutput') + ]; + } +} diff --git a/flame/lib/data_source/data_sources.dart b/flame/lib/data_source/data_sources.dart new file mode 100644 index 0000000..68ceaa2 --- /dev/null +++ b/flame/lib/data_source/data_sources.dart @@ -0,0 +1,85 @@ +import 'dart:io'; + +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/data/objects/project_data_object.dart'; +import 'package:dash_agent/data/objects/file_data_object.dart'; +import 'package:dash_agent/data/objects/web_data_object.dart'; +import 'package:flame/data_source/sitemap_url_fetcher.dart'; + +/// [DocsDataSource] indexes all the documentation related data and provides it to commands. +class DocsDataSource extends DataSource { + @override + List get fileObjects => []; + + @override + List get projectObjects => []; + + @override + List get webObjects => [ + ...[ + for (final docUrl + in sitemapUrlFetcher('assets/documents/flame_doc.xml')) + WebDataObject.fromWebPage(docUrl) + ], + ...[ + for (final docUrl + in sitemapUrlFetcher('assets/documents/flame_dart_api_doc.xml')) + WebDataObject.fromWebPage(docUrl) + ] + ]; +} + +/// [ExampleDataSource] indexes all the examples related data and provides it to commands. +class ExampleDataSource extends DataSource { + @override + List get fileObjects => [ + FileDataObject.fromDirectory(Directory('assets/examples/official'), + includePaths: true, + regex: RegExp(r'(\.dart$)'), + relativeTo: + '/Users/yogesh/Development/org.welltested/default_agents/flame/assets/examples'), + FileDataObject.fromDirectory(Directory('assets/examples/others'), + includePaths: true, + regex: RegExp(r'(\.dart$)'), + relativeTo: + '/Users/yogesh/Development/org.welltested/default_agents/flame/assets/examples') + ]; + + @override + List get projectObjects => []; + + @override + List get webObjects => []; +} + +/// [IssuesDataSource] indexes all the issues and their solutions related data and provides it to commands. +class IssuesDataSource extends DataSource { + final List issuesLinks; + + IssuesDataSource(this.issuesLinks); + + @override + List get fileObjects => []; + + @override + List get projectObjects => []; + + @override + List get webObjects => + [for (final issueUrl in issuesLinks) WebDataObject.fromWebPage(issueUrl)]; +} + +/// [TestExampleDataSource] indexes all the test examples related data and provides it to commands. +class TestExampleDataSource extends DataSource { + @override + List get fileObjects => []; + + @override + List get projectObjects => []; + + @override + List get webObjects => [ + WebDataObject.fromWebPage( + 'https://docs.flame-engine.org/latest/development/testing_guide.html') + ]; +} diff --git a/flame/lib/data_source/github_issues_data_source_helper.dart b/flame/lib/data_source/github_issues_data_source_helper.dart new file mode 100644 index 0000000..1d69104 --- /dev/null +++ b/flame/lib/data_source/github_issues_data_source_helper.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +/// Generates a list of links to GitHub issues for a given repository and label. +/// +/// The [closedIssues] parameter determines whether to include closed issues in the list. +/// The default value is `false`. +Future> generateGitIssuesLink({bool closedIssues = false}) async { + String? issueApiUrl = + 'https://api.github.com/repos/flame-engine/flame/issues'; + + /// The maximum number of issues to retrieve. + final issueLimit = closedIssues ? 350 : 50; + + /// The list of issue URLs. + final issueUrls = []; + + if (closedIssues) { + issueApiUrl = '$issueApiUrl?state=closed'; + } + + // Loop through the pages of results. + while (issueApiUrl != null && issueLimit > issueUrls.length) { + final response = await http.get(Uri.parse(issueApiUrl), headers: { + 'Authorization': 'token <>', + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'zexross' + }); + + if (response.statusCode == 200) { + final responseBody = jsonDecode(response.body).cast(); + + for (final issue in responseBody) { + issueUrls.add(issue['html_url'] as String); + } + + if (response.headers.containsKey('link')) { + String links = response.headers['link']!; + + /// If the `Link` header contains a `rel="next"` link, extract the URL. + if (links.contains('rel="next"')) { + RegExp regExp = RegExp(r'<(.+?)>'); + issueApiUrl = regExp.firstMatch(links)!.group(1); + } else { + issueApiUrl = null; + } + } else { + issueApiUrl = null; + } + } else { + print('PROCESS_AGENT_FAILURE'); + print(jsonEncode( + 'Runtime Error - Github issue fetching failure. Response code: ${response.statusCode}, Reason: ${response.body}')); + print('END_OF_AGENT_JSON'); + } + } + + return issueUrls; +} diff --git a/flame/lib/data_source/sitemap_url_fetcher.dart b/flame/lib/data_source/sitemap_url_fetcher.dart new file mode 100644 index 0000000..4a0cef6 --- /dev/null +++ b/flame/lib/data_source/sitemap_url_fetcher.dart @@ -0,0 +1,30 @@ +import 'dart:io'; +import 'package:xml/xml.dart' as xml; + +List sitemapUrlFetcher(String filePath) { + // Read the contents of the file + File file = File(filePath); + if (!file.existsSync()) { + throw 'File not found: $filePath'; + } + + String contents = file.readAsStringSync(); + + // Parse the XML + var document = xml.XmlDocument.parse(contents); + + // Find all URL elements + var urlElements = document.findAllElements('url'); + + // Extract + List urls = []; + for (var urlElement in urlElements) { + var locElement = urlElement.findElements('loc').first; + var url = locElement.firstChild?.value; + if (url != null) { + urls.add(url); + } + } + + return urls; +} diff --git a/flame/pubspec.lock b/flame/pubspec.lock new file mode 100644 index 0000000..bd6fe8e --- /dev/null +++ b/flame/pubspec.lock @@ -0,0 +1,413 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + url: "https://pub.dev" + source: hosted + version: "1.7.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + dash_agent: + dependency: "direct main" + description: + name: dash_agent + sha256: "5f647003933a979768cad1453c2a1401a2ec3f844e94ed9ba4eb57e6cdd23be9" + url: "https://pub.dev" + source: hosted + version: "0.3.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + http: + dependency: "direct main" + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + lints: + dependency: "direct dev" + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + meta: + dependency: transitive + description: + name: meta + sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" + url: "https://pub.dev" + source: hosted + version: "1.14.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: d72b538180efcf8413cd2e4e6fcc7ae99c7712e0909eb9223f9da6e6d0ef715f + url: "https://pub.dev" + source: hosted + version: "1.25.4" + test_api: + dependency: transitive + description: + name: test_api + sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" + url: "https://pub.dev" + source: hosted + version: "0.7.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: "4d070a6bc36c1c4e89f20d353bfd71dc30cdf2bd0e14349090af360a029ab292" + url: "https://pub.dev" + source: hosted + version: "0.6.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + url: "https://pub.dev" + source: hosted + version: "14.2.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + xml: + dependency: "direct main" + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.3.4 <4.0.0" diff --git a/flame/pubspec.yaml b/flame/pubspec.yaml new file mode 100644 index 0000000..4da6ee5 --- /dev/null +++ b/flame/pubspec.yaml @@ -0,0 +1,17 @@ +name: flame +description: Get help with flame framework +version: 1.1.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.3.4 + +# Add regular dependencies here. +dependencies: + dash_agent: ^0.3.0 + http: ^1.2.1 + xml: ^6.5.0 + +dev_dependencies: + lints: ^3.0.0 + test: ^1.24.0 diff --git a/flame/test/dash_agent_test.dart b/flame/test/dash_agent_test.dart new file mode 100644 index 0000000..1ea4cb1 --- /dev/null +++ b/flame/test/dash_agent_test.dart @@ -0,0 +1,13 @@ +import 'package:dash_agent/dash_agent.dart'; +import 'package:flame/agent.dart'; +import 'package:flame/data_source/github_issues_data_source_helper.dart'; + +import 'package:test/test.dart'; + +void main() { + test('adds one to input values', () async { + final openIssues = await generateGitIssuesLink(); + final closedIssues = await generateGitIssuesLink(closedIssues: true); + await processAgent(FlameAgent(issuesLinks: [])); + }); +} diff --git a/go_router/CHANGELOG.md b/go_router/CHANGELOG.md new file mode 100644 index 0000000..6875875 --- /dev/null +++ b/go_router/CHANGELOG.md @@ -0,0 +1,7 @@ +## 1.1.0 + +- Updated to enable commandless mode + +## 1.0.0 + +- Initial version. diff --git a/go_router/README.md b/go_router/README.md new file mode 100644 index 0000000..d82488a --- /dev/null +++ b/go_router/README.md @@ -0,0 +1,3 @@ +# Agent Reamde File + +This is a sample readme file for agent. You add description about the agent and any other instruction or information. diff --git a/go_router/analysis_options.yaml b/go_router/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/go_router/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/go_router/assets/.DS_Store b/go_router/assets/.DS_Store new file mode 100644 index 0000000..259e3be Binary files /dev/null and b/go_router/assets/.DS_Store differ diff --git a/go_router/assets/documents/go_router_doc.xml b/go_router/assets/documents/go_router_doc.xml new file mode 100644 index 0000000..b64ea20 --- /dev/null +++ b/go_router/assets/documents/go_router_doc.xml @@ -0,0 +1,352 @@ + + + + + + + https://pub.dev/documentation/go_router/latest/index.html + 2024-05-04T13:41:58+00:00 + 1.00 + + + https://pub.dev/documentation/go_router/latest/go_router/ShellRoute-class.html + 2024-05-04T13:41:58+00:00 + 0.80 + + + https://pub.dev/documentation/go_router/latest/topics/Get%20started-topic.html + 2024-05-04T13:41:58+00:00 + 0.80 + + + https://pub.dev/documentation/go_router/latest/topics/Upgrading-topic.html + 2024-05-04T13:41:58+00:00 + 0.80 + + + https://pub.dev/documentation/go_router/latest/topics/Configuration-topic.html + 2024-05-04T13:41:58+00:00 + 0.80 + + + https://pub.dev/documentation/go_router/latest/topics/Navigation-topic.html + 2024-05-04T13:41:58+00:00 + 0.80 + + + https://pub.dev/documentation/go_router/latest/topics/Redirection-topic.html + 2024-05-04T13:41:58+00:00 + 0.80 + + + https://pub.dev/documentation/go_router/latest/topics/Web-topic.html + 2024-05-04T13:41:58+00:00 + 0.80 + + + https://pub.dev/documentation/go_router/latest/topics/Deep%20linking-topic.html + 2024-05-04T13:41:58+00:00 + 0.80 + + + https://pub.dev/documentation/go_router/latest/topics/Transition%20animations-topic.html + 2024-05-04T13:41:58+00:00 + 0.80 + + + https://pub.dev/documentation/go_router/latest/topics/Type-safe%20routes-topic.html + 2024-05-04T13:41:58+00:00 + 0.80 + + + https://pub.dev/documentation/go_router/latest/topics/Named%20routes-topic.html + 2024-05-04T13:41:58+00:00 + 0.80 + + + https://pub.dev/documentation/go_router/latest/topics/Error%20handling-topic.html + 2024-05-04T13:41:58+00:00 + 0.80 + + + https://pub.dev/documentation/go_router/latest/go_router/go_router-library.html + 2024-05-04T13:41:58+00:00 + 0.80 + + + https://pub.dev/documentation/go_router/latest/go_router/GoRouter-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/RouteBase-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/ShellRouteBase-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/ShellRouteBuilder.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/ShellRoutePageBuilder.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoRouterState-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/ShellRouteContext-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoRoute-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/RoutingConfig-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoRouterRedirect.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoRouteData-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/CustomTransitionPage-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoRouteInformationParser-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/RouteMatchList-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoRouteInformationProvider-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoRouterDelegate-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/ImperativeRouteMatch-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/InheritedGoRouter-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/NoTransitionPage-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/RouteBuilder-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/RouteConfiguration-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/RouteData-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/RouteInformationState-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/RouteMatch-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/RouteMatchBase-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/StatefulShellRoute-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/ShellRouteData-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/ShellRouteMatch-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/StatefulNavigationShell-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/StatefulNavigationShellState-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/StatefulShellBranch-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/StatefulShellBranchData-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/StatefulShellRouteData-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/TypedGoRoute-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/TypedRoute-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/TypedShellRoute-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/TypedStatefulShellBranch-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/TypedStatefulShellRoute-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/NavigatingType.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoRouterHelper.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/ExitCallback.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoExceptionHandler.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoRouterBuilderWithNav.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoRouterPageBuilder.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoRouterWidgetBuilder.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/NavigatorBuilder.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/ParserExceptionHandler.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/PopPageWithRouteMatchCallback.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/RouteMatchVisitor.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/ShellNavigationContainerBuilder.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/StatefulShellRouteBuilder.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/StatefulShellRoutePageBuilder.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoError-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + https://pub.dev/documentation/go_router/latest/go_router/GoException-class.html + 2024-05-04T13:41:58+00:00 + 0.64 + + + + \ No newline at end of file diff --git a/go_router/assets/examples/.DS_Store b/go_router/assets/examples/.DS_Store new file mode 100644 index 0000000..438cd8c Binary files /dev/null and b/go_router/assets/examples/.DS_Store differ diff --git a/go_router/assets/examples/official/.gitignore b/go_router/assets/examples/official/.gitignore new file mode 100644 index 0000000..0d920e6 --- /dev/null +++ b/go_router/assets/examples/official/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# 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 +.packages +.pub-cache/ +.pub/ +build/ + +# Web related +lib/generated_plugin_registrant.dart + +# 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 diff --git a/go_router/assets/examples/official/README.md b/go_router/assets/examples/official/README.md new file mode 100644 index 0000000..cac540f --- /dev/null +++ b/go_router/assets/examples/official/README.md @@ -0,0 +1,52 @@ +# Example Catalog + +## [Get started](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/main.dart) +`flutter run lib/main.dart` + +An example to demonstrate a simple two-page app. + +## [Sub-routes](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/sub_routes.dart) +`flutter run lib/sub_routes.dart` + +An example to demonstrate an app with multi-level routing. + +## [Query parameters and path parameters](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/path_and_query_parameters.dart) +`flutter run lib/path_and_query_parameters.dart` + +An example to demonstrate how to use path parameters and query parameters. + +## [Named routes](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/named_routes.dart) +`flutter run lib/named_routes.dart` + +An example to demonstrate how to navigate using named locations instead of URLs. + +## [Redirection](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/redirection.dart) +`flutter run lib/redirection.dart` + +An example to demonstrate how to use redirect to handle a synchronous sign-in flow. + +## [Asynchronous Redirection](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/async_redirection.dart) +`flutter run lib/async_redirection.dart` + +An example to demonstrate how to use handle a sign-in flow with a stream authentication service. + +## [Stateful Nested Navigation](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart) +`flutter run lib/stacked_shell_route.dart` + +An example to demonstrate how to use a `StatefulShellRoute` to create stateful nested navigation, with a +`BottomNavigationBar`. + +## [Exception Handling](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/exception_handling.dart) +`flutter run lib/exception_handling.dart` + +An example to demonstrate how to handle exception in go_router. + +## [Extra Codec](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/extra_codec.dart) +`flutter run lib/extra_codec.dart` + +An example to demonstrate how to use a complex object as extra. + +## [Books app](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/books) +`flutter run lib/books/main.dart` + +A fully fledged example that showcases various go_router APIs. \ No newline at end of file diff --git a/go_router/assets/examples/official/lib/async_redirection.dart b/go_router/assets/examples/official/lib/async_redirection.dart new file mode 100644 index 0000000..f84c14e --- /dev/null +++ b/go_router/assets/examples/official/lib/async_redirection.dart @@ -0,0 +1,244 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +// This scenario demonstrates how to use redirect to handle a asynchronous +// sign-in flow. +// +// The `StreamAuth` is a mock of google_sign_in. This example wraps it with an +// InheritedNotifier, StreamAuthScope, and relies on +// `dependOnInheritedWidgetOfExactType` to create a dependency between the +// notifier and go_router's parsing pipeline. When StreamAuth broadcasts new +// event, the dependency will cause the go_router to reparse the current url +// which will also trigger the redirect. + +void main() => runApp(StreamAuthScope(child: App())); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Redirection'; + + // add the login info into the tree as app state that can change over time + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + debugShowCheckedModeBanner: false, + ); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + ), + GoRoute( + path: '/login', + builder: (BuildContext context, GoRouterState state) => + const LoginScreen(), + ), + ], + + // redirect to the login page if the user is not logged in + redirect: (BuildContext context, GoRouterState state) async { + // Using `of` method creates a dependency of StreamAuthScope. It will + // cause go_router to reparse current route if StreamAuth has new sign-in + // information. + final bool loggedIn = await StreamAuthScope.of(context).isSignedIn(); + final bool loggingIn = state.matchedLocation == '/login'; + if (!loggedIn) { + return '/login'; + } + + // if the user is logged in but still on the login page, send them to + // the home page + if (loggingIn) { + return '/'; + } + + // no need to redirect at all + return null; + }, + ); +} + +/// The login screen. +class LoginScreen extends StatefulWidget { + /// Creates a [LoginScreen]. + const LoginScreen({super.key}); + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State + with TickerProviderStateMixin { + bool loggingIn = false; + late final AnimationController controller; + + @override + void initState() { + super.initState(); + controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + )..addListener(() { + setState(() {}); + }); + controller.repeat(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (loggingIn) CircularProgressIndicator(value: controller.value), + if (!loggingIn) + ElevatedButton( + onPressed: () { + StreamAuthScope.of(context).signIn('test-user'); + setState(() { + loggingIn = true; + }); + }, + child: const Text('Login'), + ), + ], + ), + ), + ); +} + +/// The home screen. +class HomeScreen extends StatelessWidget { + /// Creates a [HomeScreen]. + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final StreamAuth info = StreamAuthScope.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text(App.title), + actions: [ + IconButton( + onPressed: () => info.signOut(), + tooltip: 'Logout: ${info.currentUser}', + icon: const Icon(Icons.logout), + ) + ], + ), + body: const Center( + child: Text('HomeScreen'), + ), + ); + } +} + +/// A scope that provides [StreamAuth] for the subtree. +class StreamAuthScope extends InheritedNotifier { + /// Creates a [StreamAuthScope] sign in scope. + StreamAuthScope({ + super.key, + required super.child, + }) : super( + notifier: StreamAuthNotifier(), + ); + + /// Gets the [StreamAuth]. + static StreamAuth of(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType()! + .notifier! + .streamAuth; + } +} + +/// A class that converts [StreamAuth] into a [ChangeNotifier]. +class StreamAuthNotifier extends ChangeNotifier { + /// Creates a [StreamAuthNotifier]. + StreamAuthNotifier() : streamAuth = StreamAuth() { + streamAuth.onCurrentUserChanged.listen((String? string) { + notifyListeners(); + }); + } + + /// The stream auth client. + final StreamAuth streamAuth; +} + +/// An asynchronous log in services mock with stream similar to google_sign_in. +/// +/// This class adds an artificial delay of 3 second when logging in an user, and +/// will automatically clear the login session after [refreshInterval]. +class StreamAuth { + /// Creates an [StreamAuth] that clear the current user session in + /// [refeshInterval] second. + StreamAuth({this.refreshInterval = 20}) + : _userStreamController = StreamController.broadcast() { + _userStreamController.stream.listen((String? currentUser) { + _currentUser = currentUser; + }); + } + + /// The current user. + String? get currentUser => _currentUser; + String? _currentUser; + + /// Checks whether current user is signed in with an artificial delay to mimic + /// async operation. + Future isSignedIn() async { + await Future.delayed(const Duration(seconds: 1)); + return _currentUser != null; + } + + /// A stream that notifies when current user has changed. + Stream get onCurrentUserChanged => _userStreamController.stream; + final StreamController _userStreamController; + + /// The interval that automatically signs out the user. + final int refreshInterval; + + Timer? _timer; + Timer _createRefreshTimer() { + return Timer(Duration(seconds: refreshInterval), () { + _userStreamController.add(null); + _timer = null; + }); + } + + /// Signs in a user with an artificial delay to mimic async operation. + Future signIn(String newUserName) async { + await Future.delayed(const Duration(seconds: 3)); + _userStreamController.add(newUserName); + _timer?.cancel(); + _timer = _createRefreshTimer(); + } + + /// Signs out the current user. + Future signOut() async { + _timer?.cancel(); + _timer = null; + _userStreamController.add(null); + } +} diff --git a/go_router/assets/examples/official/lib/books/main.dart b/go_router/assets/examples/official/lib/books/main.dart new file mode 100644 index 0000000..eb757e3 --- /dev/null +++ b/go_router/assets/examples/official/lib/books/main.dart @@ -0,0 +1,171 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import 'src/auth.dart'; +import 'src/data/author.dart'; +import 'src/data/book.dart'; +import 'src/data/library.dart'; +import 'src/screens/author_details.dart'; +import 'src/screens/authors.dart'; +import 'src/screens/book_details.dart'; +import 'src/screens/books.dart'; +import 'src/screens/scaffold.dart'; +import 'src/screens/settings.dart'; +import 'src/screens/sign_in.dart'; + +void main() => runApp(Bookstore()); + +/// The book store view. +class Bookstore extends StatelessWidget { + /// Creates a [Bookstore]. + Bookstore({super.key}); + + final ValueKey _scaffoldKey = const ValueKey('App scaffold'); + + @override + Widget build(BuildContext context) => BookstoreAuthScope( + notifier: _auth, + child: MaterialApp.router( + routerConfig: _router, + ), + ); + + final BookstoreAuth _auth = BookstoreAuth(); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + redirect: (_, __) => '/books', + ), + GoRoute( + path: '/signin', + pageBuilder: (BuildContext context, GoRouterState state) => + FadeTransitionPage( + key: state.pageKey, + child: SignInScreen( + onSignIn: (Credentials credentials) { + BookstoreAuthScope.of(context) + .signIn(credentials.username, credentials.password); + }, + ), + ), + ), + GoRoute( + path: '/books', + redirect: (_, __) => '/books/popular', + ), + GoRoute( + path: '/book/:bookId', + redirect: (BuildContext context, GoRouterState state) => + '/books/all/${state.pathParameters['bookId']}', + ), + GoRoute( + path: '/books/:kind(new|all|popular)', + pageBuilder: (BuildContext context, GoRouterState state) => + FadeTransitionPage( + key: _scaffoldKey, + child: BookstoreScaffold( + selectedTab: ScaffoldTab.books, + child: BooksScreen(state.pathParameters['kind']!), + ), + ), + routes: [ + GoRoute( + path: ':bookId', + builder: (BuildContext context, GoRouterState state) { + final String bookId = state.pathParameters['bookId']!; + final Book? selectedBook = libraryInstance.allBooks + .firstWhereOrNull((Book b) => b.id.toString() == bookId); + + return BookDetailsScreen(book: selectedBook); + }, + ), + ], + ), + GoRoute( + path: '/author/:authorId', + redirect: (BuildContext context, GoRouterState state) => + '/authors/${state.pathParameters['authorId']}', + ), + GoRoute( + path: '/authors', + pageBuilder: (BuildContext context, GoRouterState state) => + FadeTransitionPage( + key: _scaffoldKey, + child: const BookstoreScaffold( + selectedTab: ScaffoldTab.authors, + child: AuthorsScreen(), + ), + ), + routes: [ + GoRoute( + path: ':authorId', + builder: (BuildContext context, GoRouterState state) { + final int authorId = int.parse(state.pathParameters['authorId']!); + final Author? selectedAuthor = libraryInstance.allAuthors + .firstWhereOrNull((Author a) => a.id == authorId); + + return AuthorDetailsScreen(author: selectedAuthor); + }, + ), + ], + ), + GoRoute( + path: '/settings', + pageBuilder: (BuildContext context, GoRouterState state) => + FadeTransitionPage( + key: _scaffoldKey, + child: const BookstoreScaffold( + selectedTab: ScaffoldTab.settings, + child: SettingsScreen(), + ), + ), + ), + ], + redirect: _guard, + refreshListenable: _auth, + debugLogDiagnostics: true, + ); + + String? _guard(BuildContext context, GoRouterState state) { + final bool signedIn = _auth.signedIn; + final bool signingIn = state.matchedLocation == '/signin'; + + // Go to /signin if the user is not signed in + if (!signedIn && !signingIn) { + return '/signin'; + } + // Go to /books if the user is signed in and tries to go to /signin. + else if (signedIn && signingIn) { + return '/books'; + } + + // no redirect + return null; + } +} + +/// A page that fades in an out. +class FadeTransitionPage extends CustomTransitionPage { + /// Creates a [FadeTransitionPage]. + FadeTransitionPage({ + required LocalKey super.key, + required super.child, + }) : super( + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + FadeTransition( + opacity: animation.drive(_curveTween), + child: child, + )); + + static final CurveTween _curveTween = CurveTween(curve: Curves.easeIn); +} diff --git a/go_router/assets/examples/official/lib/books/src/auth.dart b/go_router/assets/examples/official/lib/books/src/auth.dart new file mode 100644 index 0000000..b9c353c --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/auth.dart @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +/// A mock authentication service. +class BookstoreAuth extends ChangeNotifier { + bool _signedIn = false; + + /// Whether user has signed in. + bool get signedIn => _signedIn; + + /// Signs out the current user. + Future signOut() async { + await Future.delayed(const Duration(milliseconds: 200)); + // Sign out. + _signedIn = false; + notifyListeners(); + } + + /// Signs in a user. + Future signIn(String username, String password) async { + await Future.delayed(const Duration(milliseconds: 200)); + + // Sign in. Allow any password. + _signedIn = true; + notifyListeners(); + return _signedIn; + } +} + +/// An inherited notifier to host [BookstoreAuth] for the subtree. +class BookstoreAuthScope extends InheritedNotifier { + /// Creates a [BookstoreAuthScope]. + const BookstoreAuthScope({ + required BookstoreAuth super.notifier, + required super.child, + super.key, + }); + + /// Gets the [BookstoreAuth] above the context. + static BookstoreAuth of(BuildContext context) => context + .dependOnInheritedWidgetOfExactType()! + .notifier!; +} diff --git a/go_router/assets/examples/official/lib/books/src/data.dart b/go_router/assets/examples/official/lib/books/src/data.dart new file mode 100644 index 0000000..109082e --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/data.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'data/author.dart'; +export 'data/book.dart'; +export 'data/library.dart'; diff --git a/go_router/assets/examples/official/lib/books/src/data/author.dart b/go_router/assets/examples/official/lib/books/src/data/author.dart new file mode 100644 index 0000000..b58db11 --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/data/author.dart @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'book.dart'; + +/// Author data class. +class Author { + /// Creates an author data object. + Author({ + required this.id, + required this.name, + }); + + /// The id of the author. + final int id; + + /// The name of the author. + final String name; + + /// The books of the author. + final List books = []; +} diff --git a/go_router/assets/examples/official/lib/books/src/data/book.dart b/go_router/assets/examples/official/lib/books/src/data/book.dart new file mode 100644 index 0000000..cd2c94f --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/data/book.dart @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'author.dart'; + +/// Book data class. +class Book { + /// Creates a book data object. + Book({ + required this.id, + required this.title, + required this.isPopular, + required this.isNew, + required this.author, + }); + + /// The id of the book. + final int id; + + /// The title of the book. + final String title; + + /// The author of the book. + final Author author; + + /// Whether the book is popular. + final bool isPopular; + + /// Whether the book is new. + final bool isNew; +} diff --git a/go_router/assets/examples/official/lib/books/src/data/library.dart b/go_router/assets/examples/official/lib/books/src/data/library.dart new file mode 100644 index 0000000..075b825 --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/data/library.dart @@ -0,0 +1,76 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'author.dart'; +import 'book.dart'; + +/// Library data mock. +final Library libraryInstance = Library() + ..addBook( + title: 'Left Hand of Darkness', + authorName: 'Ursula K. Le Guin', + isPopular: true, + isNew: true) + ..addBook( + title: 'Too Like the Lightning', + authorName: 'Ada Palmer', + isPopular: false, + isNew: true) + ..addBook( + title: 'Kindred', + authorName: 'Octavia E. Butler', + isPopular: true, + isNew: false) + ..addBook( + title: 'The Lathe of Heaven', + authorName: 'Ursula K. Le Guin', + isPopular: false, + isNew: false); + +/// A library that contains books and authors. +class Library { + /// The books in the library. + final List allBooks = []; + + /// The authors in the library. + final List allAuthors = []; + + /// Adds a book into the library. + void addBook({ + required String title, + required String authorName, + required bool isPopular, + required bool isNew, + }) { + final Author author = allAuthors.firstWhere( + (Author author) => author.name == authorName, + orElse: () { + final Author value = Author(id: allAuthors.length, name: authorName); + allAuthors.add(value); + return value; + }, + ); + + final Book book = Book( + id: allBooks.length, + title: title, + isPopular: isPopular, + isNew: isNew, + author: author, + ); + + author.books.add(book); + allBooks.add(book); + } + + /// The list of popular books in the library. + List get popularBooks => [ + ...allBooks.where((Book book) => book.isPopular), + ]; + + /// The list of new books in the library. + List get newBooks => [ + ...allBooks.where((Book book) => book.isNew), + ]; +} diff --git a/go_router/assets/examples/official/lib/books/src/screens/author_details.dart b/go_router/assets/examples/official/lib/books/src/screens/author_details.dart new file mode 100644 index 0000000..3aff898 --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/screens/author_details.dart @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../data.dart'; +import '../widgets/book_list.dart'; + +/// The author detail screen. +class AuthorDetailsScreen extends StatelessWidget { + /// Creates an author detail screen. + const AuthorDetailsScreen({ + required this.author, + super.key, + }); + + /// The author to be displayed. + final Author? author; + + @override + Widget build(BuildContext context) { + if (author == null) { + return const Scaffold( + body: Center( + child: Text('No author found.'), + ), + ); + } + return Scaffold( + appBar: AppBar( + title: Text(author!.name), + ), + body: Center( + child: Column( + children: [ + Expanded( + child: BookList( + books: author!.books, + onTap: (Book book) => context.go('/book/${book.id}'), + ), + ), + ], + ), + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/books/src/screens/authors.dart b/go_router/assets/examples/official/lib/books/src/screens/authors.dart new file mode 100644 index 0000000..0eeb1c3 --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/screens/authors.dart @@ -0,0 +1,31 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../data.dart'; +import '../widgets/author_list.dart'; + +/// A screen that displays a list of authors. +class AuthorsScreen extends StatelessWidget { + /// Creates an [AuthorsScreen]. + const AuthorsScreen({super.key}); + + /// The title of the screen. + static const String title = 'Authors'; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text(title), + ), + body: AuthorList( + authors: libraryInstance.allAuthors, + onTap: (Author author) { + context.go('/author/${author.id}'); + }, + ), + ); +} diff --git a/go_router/assets/examples/official/lib/books/src/screens/book_details.dart b/go_router/assets/examples/official/lib/books/src/screens/book_details.dart new file mode 100644 index 0000000..9a51a83 --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/screens/book_details.dart @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/link.dart'; + +import '../data.dart'; +import 'author_details.dart'; + +/// A screen to display book details. +class BookDetailsScreen extends StatelessWidget { + /// Creates a [BookDetailsScreen]. + const BookDetailsScreen({ + super.key, + this.book, + }); + + /// The book to be displayed. + final Book? book; + + @override + Widget build(BuildContext context) { + if (book == null) { + return const Scaffold( + body: Center( + child: Text('No book found.'), + ), + ); + } + return Scaffold( + appBar: AppBar( + title: Text(book!.title), + ), + body: Center( + child: Column( + children: [ + Text( + book!.title, + style: Theme.of(context).textTheme.headlineMedium, + ), + Text( + book!.author.name, + style: Theme.of(context).textTheme.titleMedium, + ), + TextButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => + AuthorDetailsScreen(author: book!.author), + ), + ); + }, + child: const Text('View author (navigator.push)'), + ), + Link( + uri: Uri.parse('/author/${book!.author.id}'), + builder: (BuildContext context, FollowLink? followLink) => + TextButton( + onPressed: followLink, + child: const Text('View author (Link)'), + ), + ), + TextButton( + onPressed: () { + context.push('/author/${book!.author.id}'); + }, + child: const Text('View author (GoRouter.push)'), + ), + ], + ), + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/books/src/screens/books.dart b/go_router/assets/examples/official/lib/books/src/screens/books.dart new file mode 100644 index 0000000..c3d8dc8 --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/screens/books.dart @@ -0,0 +1,113 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../data.dart'; +import '../widgets/book_list.dart'; + +/// A screen that displays a list of books. +class BooksScreen extends StatefulWidget { + /// Creates a [BooksScreen]. + const BooksScreen(this.kind, {super.key}); + + /// Which tab to display. + final String kind; + + @override + State createState() => _BooksScreenState(); +} + +class _BooksScreenState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + } + + @override + void didUpdateWidget(BooksScreen oldWidget) { + super.didUpdateWidget(oldWidget); + + switch (widget.kind) { + case 'popular': + _tabController.index = 0; + + case 'new': + _tabController.index = 1; + + case 'all': + _tabController.index = 2; + } + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('Books'), + bottom: TabBar( + controller: _tabController, + onTap: _handleTabTapped, + tabs: const [ + Tab( + text: 'Popular', + icon: Icon(Icons.people), + ), + Tab( + text: 'New', + icon: Icon(Icons.new_releases), + ), + Tab( + text: 'All', + icon: Icon(Icons.list), + ), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + BookList( + books: libraryInstance.popularBooks, + onTap: _handleBookTapped, + ), + BookList( + books: libraryInstance.newBooks, + onTap: _handleBookTapped, + ), + BookList( + books: libraryInstance.allBooks, + onTap: _handleBookTapped, + ), + ], + ), + ); + + void _handleBookTapped(Book book) { + context.go('/book/${book.id}'); + } + + void _handleTabTapped(int index) { + switch (index) { + case 1: + context.go('/books/new'); + case 2: + context.go('/books/all'); + case 0: + default: + context.go('/books/popular'); + break; + } + } +} diff --git a/go_router/assets/examples/official/lib/books/src/screens/scaffold.dart b/go_router/assets/examples/official/lib/books/src/screens/scaffold.dart new file mode 100644 index 0000000..7e26fe2 --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/screens/scaffold.dart @@ -0,0 +1,68 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:adaptive_navigation/adaptive_navigation.dart'; +import 'package:flutter/material.dart'; + +import 'package:go_router/go_router.dart'; + +/// The enum for scaffold tab. +enum ScaffoldTab { + /// The books tab. + books, + + /// The authors tab. + authors, + + /// The settings tab. + settings +} + +/// The scaffold for the book store. +class BookstoreScaffold extends StatelessWidget { + /// Creates a [BookstoreScaffold]. + const BookstoreScaffold({ + required this.selectedTab, + required this.child, + super.key, + }); + + /// Which tab of the scaffold to display. + final ScaffoldTab selectedTab; + + /// The scaffold body. + final Widget child; + + @override + Widget build(BuildContext context) => Scaffold( + body: AdaptiveNavigationScaffold( + selectedIndex: selectedTab.index, + body: child, + onDestinationSelected: (int idx) { + switch (ScaffoldTab.values[idx]) { + case ScaffoldTab.books: + context.go('/books'); + case ScaffoldTab.authors: + context.go('/authors'); + case ScaffoldTab.settings: + context.go('/settings'); + } + }, + destinations: const [ + AdaptiveScaffoldDestination( + title: 'Books', + icon: Icons.book, + ), + AdaptiveScaffoldDestination( + title: 'Authors', + icon: Icons.person, + ), + AdaptiveScaffoldDestination( + title: 'Settings', + icon: Icons.settings, + ), + ], + ), + ); +} diff --git a/go_router/assets/examples/official/lib/books/src/screens/settings.dart b/go_router/assets/examples/official/lib/books/src/screens/settings.dart new file mode 100644 index 0000000..a098e30 --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/screens/settings.dart @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/link.dart'; + +import '../auth.dart'; + +/// The settings screen. +class SettingsScreen extends StatefulWidget { + /// Creates a [SettingsScreen]. + const SettingsScreen({super.key}); + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + @override + Widget build(BuildContext context) => Scaffold( + body: SafeArea( + child: SingleChildScrollView( + child: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: const Card( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 18, horizontal: 12), + child: SettingsContent(), + ), + ), + ), + ), + ), + ), + ); +} + +/// The content of a [SettingsScreen]. +class SettingsContent extends StatelessWidget { + /// Creates a [SettingsContent]. + const SettingsContent({ + super.key, + }); + + @override + Widget build(BuildContext context) => Column( + children: [ + ...[ + Text( + 'Settings', + style: Theme.of(context).textTheme.headlineMedium, + ), + ElevatedButton( + onPressed: () { + BookstoreAuthScope.of(context).signOut(); + }, + child: const Text('Sign out'), + ), + Link( + uri: Uri.parse('/book/0'), + builder: (BuildContext context, FollowLink? followLink) => + TextButton( + onPressed: followLink, + child: const Text('Go directly to /book/0 (Link)'), + ), + ), + TextButton( + onPressed: () { + context.go('/book/0'); + }, + child: const Text('Go directly to /book/0 (GoRouter)'), + ), + ].map((Widget w) => + Padding(padding: const EdgeInsets.all(8), child: w)), + TextButton( + onPressed: () => showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: const Text('Alert!'), + content: const Text('The alert description goes here.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, 'Cancel'), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.pop(context, 'OK'), + child: const Text('OK'), + ), + ], + ), + ), + child: const Text('Show Dialog'), + ) + ], + ); +} diff --git a/go_router/assets/examples/official/lib/books/src/screens/sign_in.dart b/go_router/assets/examples/official/lib/books/src/screens/sign_in.dart new file mode 100644 index 0000000..e02c870 --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/screens/sign_in.dart @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +/// Credential data class. +class Credentials { + /// Creates a credential data object. + Credentials(this.username, this.password); + + /// The username of the credentials. + final String username; + + /// The password of the credentials. + final String password; +} + +/// The sign-in screen. +class SignInScreen extends StatefulWidget { + /// Creates a sign-in screen. + const SignInScreen({ + required this.onSignIn, + super.key, + }); + + /// Called when users sign in with [Credentials]. + final ValueChanged onSignIn; + + @override + State createState() => _SignInScreenState(); +} + +class _SignInScreenState extends State { + final TextEditingController _usernameController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + + @override + Widget build(BuildContext context) => Scaffold( + body: Center( + child: Card( + child: Container( + constraints: BoxConstraints.loose(const Size(600, 600)), + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text('Sign in', + style: Theme.of(context).textTheme.headlineMedium), + TextField( + decoration: const InputDecoration(labelText: 'Username'), + controller: _usernameController, + ), + TextField( + decoration: const InputDecoration(labelText: 'Password'), + obscureText: true, + controller: _passwordController, + ), + Padding( + padding: const EdgeInsets.all(16), + child: TextButton( + onPressed: () async { + widget.onSignIn(Credentials( + _usernameController.value.text, + _passwordController.value.text)); + }, + child: const Text('Sign in'), + ), + ), + ], + ), + ), + ), + ), + ); +} diff --git a/go_router/assets/examples/official/lib/books/src/widgets/author_list.dart b/go_router/assets/examples/official/lib/books/src/widgets/author_list.dart new file mode 100644 index 0000000..371e30a --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/widgets/author_list.dart @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import '../data.dart'; + +/// The author list view. +class AuthorList extends StatelessWidget { + /// Creates an [AuthorList]. + const AuthorList({ + required this.authors, + this.onTap, + super.key, + }); + + /// The list of authors to be shown. + final List authors; + + /// Called when the user taps an author. + final ValueChanged? onTap; + + @override + Widget build(BuildContext context) => ListView.builder( + itemCount: authors.length, + itemBuilder: (BuildContext context, int index) => ListTile( + title: Text( + authors[index].name, + ), + subtitle: Text( + '${authors[index].books.length} books', + ), + onTap: onTap != null ? () => onTap!(authors[index]) : null, + ), + ); +} diff --git a/go_router/assets/examples/official/lib/books/src/widgets/book_list.dart b/go_router/assets/examples/official/lib/books/src/widgets/book_list.dart new file mode 100644 index 0000000..3e2761f --- /dev/null +++ b/go_router/assets/examples/official/lib/books/src/widgets/book_list.dart @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +import '../data.dart'; + +/// The book list view. +class BookList extends StatelessWidget { + /// Creates an [BookList]. + const BookList({ + required this.books, + this.onTap, + super.key, + }); + + /// The list of books to be displayed. + final List books; + + /// Called when the user taps a book. + final ValueChanged? onTap; + + @override + Widget build(BuildContext context) => ListView.builder( + itemCount: books.length, + itemBuilder: (BuildContext context, int index) => ListTile( + title: Text( + books[index].title, + ), + subtitle: Text( + books[index].author.name, + ), + onTap: onTap != null ? () => onTap!(books[index]) : null, + ), + ); +} diff --git a/go_router/assets/examples/official/lib/exception_handling.dart b/go_router/assets/examples/official/lib/exception_handling.dart new file mode 100644 index 0000000..82c10e3 --- /dev/null +++ b/go_router/assets/examples/official/lib/exception_handling.dart @@ -0,0 +1,87 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// This sample app shows how to use `GoRouter.onException` to redirect on +/// exception. +/// +/// The first route '/' is mapped to [HomeScreen], and the second route +/// '/404' is mapped to [NotFoundScreen]. +/// +/// Any other unknown route or exception is redirected to `/404`. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + onException: (_, GoRouterState state, GoRouter router) { + router.go('/404', extra: state.uri.toString()); + }, + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + ), + GoRoute( + path: '/404', + builder: (BuildContext context, GoRouterState state) { + return NotFoundScreen(uri: state.extra as String? ?? ''); + }, + ), + ], +); + +/// The main app. +class MyApp extends StatelessWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + ); + } +} + +/// The home screen +class HomeScreen extends StatelessWidget { + /// Constructs a [HomeScreen] + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home Screen')), + body: Center( + child: ElevatedButton( + onPressed: () => context.go('/some-unknown-route'), + child: const Text('Simulates user entering unknown url'), + ), + ), + ); + } +} + +/// The not found screen +class NotFoundScreen extends StatelessWidget { + /// Constructs a [HomeScreen] + const NotFoundScreen({super.key, required this.uri}); + + /// The uri that can not be found. + final String uri; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Page Not Found')), + body: Center( + child: Text("Can't find a page for: $uri"), + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/extra_codec.dart b/go_router/assets/examples/official/lib/extra_codec.dart new file mode 100644 index 0000000..389afe4 --- /dev/null +++ b/go_router/assets/examples/official/lib/extra_codec.dart @@ -0,0 +1,139 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// This sample app demonstrates how to provide a codec for complex extra data. +void main() => runApp(const MyApp()); + +/// The router configuration. +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + ), + ], + extraCodec: const MyExtraCodec(), +); + +/// The main app. +class MyApp extends StatelessWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + ); + } +} + +/// The home screen. +class HomeScreen extends StatelessWidget { + /// Constructs a [HomeScreen]. + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home Screen')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "If running in web, use the browser's backward and forward button to test extra codec after setting extra several times."), + Text( + 'The extra for this page is: ${GoRouterState.of(context).extra}'), + ElevatedButton( + onPressed: () => context.go('/', extra: ComplexData1('data')), + child: const Text('Set extra to ComplexData1'), + ), + ElevatedButton( + onPressed: () => context.go('/', extra: ComplexData2('data')), + child: const Text('Set extra to ComplexData2'), + ), + ], + ), + ), + ); + } +} + +/// A complex class. +class ComplexData1 { + /// Create a complex object. + ComplexData1(this.data); + + /// The data. + final String data; + + @override + String toString() => 'ComplexData1(data: $data)'; +} + +/// A complex class. +class ComplexData2 { + /// Create a complex object. + ComplexData2(this.data); + + /// The data. + final String data; + + @override + String toString() => 'ComplexData2(data: $data)'; +} + +/// A codec that can serialize both [ComplexData1] and [ComplexData2]. +class MyExtraCodec extends Codec { + /// Create a codec. + const MyExtraCodec(); + @override + Converter get decoder => const _MyExtraDecoder(); + + @override + Converter get encoder => const _MyExtraEncoder(); +} + +class _MyExtraDecoder extends Converter { + const _MyExtraDecoder(); + @override + Object? convert(Object? input) { + if (input == null) { + return null; + } + final List inputAsList = input as List; + if (inputAsList[0] == 'ComplexData1') { + return ComplexData1(inputAsList[1]! as String); + } + if (inputAsList[0] == 'ComplexData2') { + return ComplexData2(inputAsList[1]! as String); + } + throw FormatException('Unable to parse input: $input'); + } +} + +class _MyExtraEncoder extends Converter { + const _MyExtraEncoder(); + @override + Object? convert(Object? input) { + if (input == null) { + return null; + } + switch (input) { + case ComplexData1 _: + return ['ComplexData1', input.data]; + case ComplexData2 _: + return ['ComplexData2', input.data]; + default: + throw FormatException('Cannot encode type ${input.runtimeType}'); + } + } +} diff --git a/go_router/assets/examples/official/lib/main.dart b/go_router/assets/examples/official/lib/main.dart new file mode 100644 index 0000000..d159648 --- /dev/null +++ b/go_router/assets/examples/official/lib/main.dart @@ -0,0 +1,87 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// This sample app shows an app with two screens. +/// +/// The first route '/' is mapped to [HomeScreen], and the second route +/// '/details' is mapped to [DetailsScreen]. +/// +/// The buttons use context.go() to navigate to each destination. On mobile +/// devices, each destination is deep-linkable and on the web, can be navigated +/// to using the address bar. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(); + }, + ), + ], + ), + ], +); + +/// The main app. +class MyApp extends StatelessWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + ); + } +} + +/// The home screen +class HomeScreen extends StatelessWidget { + /// Constructs a [HomeScreen] + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home Screen')), + body: Center( + child: ElevatedButton( + onPressed: () => context.go('/details'), + child: const Text('Go to the Details screen'), + ), + ), + ); + } +} + +/// The details screen +class DetailsScreen extends StatelessWidget { + /// Constructs a [DetailsScreen] + const DetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Details Screen')), + body: Center( + child: ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go back to the Home screen'), + ), + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/named_routes.dart b/go_router/assets/examples/official/lib/named_routes.dart new file mode 100644 index 0000000..9685fc4 --- /dev/null +++ b/go_router/assets/examples/official/lib/named_routes.dart @@ -0,0 +1,181 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +// This scenario demonstrates how to navigate using named locations instead of +// URLs. +// +// Instead of hardcoding the URI locations , you can also use the named +// locations. To use this API, give a unique name to each GoRoute. The name can +// then be used in context.namedLocation to be translate back to the actual URL +// location. + +/// Family data class. +class Family { + /// Create a family. + const Family({required this.name, required this.people}); + + /// The last name of the family. + final String name; + + /// The people in the family. + final Map people; +} + +/// Person data class. +class Person { + /// Creates a person. + const Person({required this.name, required this.age}); + + /// The first name of the person. + final String name; + + /// The age of the person. + final int age; +} + +const Map _families = { + 'f1': Family( + name: 'Doe', + people: { + 'p1': Person(name: 'Jane', age: 23), + 'p2': Person(name: 'John', age: 6), + }, + ), + 'f2': Family( + name: 'Wong', + people: { + 'p1': Person(name: 'June', age: 51), + 'p2': Person(name: 'Xin', age: 44), + }, + ), +}; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Named Routes'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + debugShowCheckedModeBanner: false, + ); + + late final GoRouter _router = GoRouter( + debugLogDiagnostics: true, + routes: [ + GoRoute( + name: 'home', + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + name: 'family', + path: 'family/:fid', + builder: (BuildContext context, GoRouterState state) => + FamilyScreen(fid: state.pathParameters['fid']!), + routes: [ + GoRoute( + name: 'person', + path: 'person/:pid', + builder: (BuildContext context, GoRouterState state) { + return PersonScreen( + fid: state.pathParameters['fid']!, + pid: state.pathParameters['pid']!); + }, + ), + ], + ), + ], + ), + ], + ); +} + +/// The home screen that shows a list of families. +class HomeScreen extends StatelessWidget { + /// Creates a [HomeScreen]. + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text(App.title), + ), + body: ListView( + children: [ + for (final MapEntry entry in _families.entries) + ListTile( + title: Text(entry.value.name), + onTap: () => context.go(context.namedLocation('family', + pathParameters: {'fid': entry.key})), + ) + ], + ), + ); + } +} + +/// The screen that shows a list of persons in a family. +class FamilyScreen extends StatelessWidget { + /// Creates a [FamilyScreen]. + const FamilyScreen({required this.fid, super.key}); + + /// The id family to display. + final String fid; + + @override + Widget build(BuildContext context) { + final Map people = _families[fid]!.people; + return Scaffold( + appBar: AppBar(title: Text(_families[fid]!.name)), + body: ListView( + children: [ + for (final MapEntry entry in people.entries) + ListTile( + title: Text(entry.value.name), + onTap: () => context.go(context.namedLocation( + 'person', + pathParameters: {'fid': fid, 'pid': entry.key}, + queryParameters: {'qid': 'quid'}, + )), + ), + ], + ), + ); + } +} + +/// The person screen. +class PersonScreen extends StatelessWidget { + /// Creates a [PersonScreen]. + const PersonScreen({required this.fid, required this.pid, super.key}); + + /// The id of family this person belong to. + final String fid; + + /// The id of the person to be displayed. + final String pid; + + @override + Widget build(BuildContext context) { + final Family family = _families[fid]!; + final Person person = family.people[pid]!; + return Scaffold( + appBar: AppBar(title: Text(person.name)), + body: Text('${person.name} ${family.name} is ${person.age} years old'), + ); + } +} diff --git a/go_router/assets/examples/official/lib/on_exit.dart b/go_router/assets/examples/official/lib/on_exit.dart new file mode 100644 index 0000000..bf397ef --- /dev/null +++ b/go_router/assets/examples/official/lib/on_exit.dart @@ -0,0 +1,142 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// This sample app demonstrates how to use GoRoute.onExit. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(); + }, + onExit: ( + BuildContext context, + GoRouterState state, + ) async { + final bool? confirmed = await showDialog( + context: context, + builder: (_) { + return AlertDialog( + content: const Text('Are you sure to leave this page?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Confirm'), + ), + ], + ); + }, + ); + return confirmed ?? false; + }, + ), + GoRoute( + path: 'settings', + builder: (BuildContext context, GoRouterState state) { + return const SettingsScreen(); + }, + ), + ], + ), + ], +); + +/// The main app. +class MyApp extends StatelessWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + ); + } +} + +/// The home screen +class HomeScreen extends StatelessWidget { + /// Constructs a [HomeScreen] + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home Screen')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/details'), + child: const Text('Go to the Details screen'), + ), + ], + ), + ), + ); + } +} + +/// The details screen +class DetailsScreen extends StatelessWidget { + /// Constructs a [DetailsScreen] + const DetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Details Screen')), + body: Center( + child: Column( + children: [ + TextButton( + onPressed: () { + context.pop(); + }, + child: const Text('go back'), + ), + TextButton( + onPressed: () { + context.go('/settings'); + }, + child: const Text('go to settings'), + ), + ], + )), + ); + } +} + +/// The settings screen +class SettingsScreen extends StatelessWidget { + /// Constructs a [SettingsScreen] + const SettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Settings Screen')), + body: const Center( + child: Text('Settings'), + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/others/custom_stateful_shell_route.dart b/go_router/assets/examples/official/lib/others/custom_stateful_shell_route.dart new file mode 100644 index 0000000..5af1504 --- /dev/null +++ b/go_router/assets/examples/official/lib/others/custom_stateful_shell_route.dart @@ -0,0 +1,460 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _tabANavigatorKey = + GlobalKey(debugLabel: 'tabANav'); + +// This example demonstrates how to setup nested navigation using a +// BottomNavigationBar, where each bar item uses its own persistent navigator, +// i.e. navigation state is maintained separately for each item. This setup also +// enables deep linking into nested pages. +// +// This example also demonstrates how build a nested shell with a custom +// container for the branch Navigators (in this case a TabBarView). + +void main() { + runApp(NestedTabNavigationExampleApp()); +} + +/// An example demonstrating how to use nested navigators +class NestedTabNavigationExampleApp extends StatelessWidget { + /// Creates a NestedTabNavigationExampleApp + NestedTabNavigationExampleApp({super.key}); + + final GoRouter _router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/a', + routes: [ + StatefulShellRoute( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + // This nested StatefulShellRoute demonstrates the use of a + // custom container for the branch Navigators. In this implementation, + // no customization is done in the builder function (navigationShell + // itself is simply used as the Widget for the route). Instead, the + // navigatorContainerBuilder function below is provided to + // customize the container for the branch Navigators. + return navigationShell; + }, + navigatorContainerBuilder: (BuildContext context, + StatefulNavigationShell navigationShell, List children) { + // Returning a customized container for the branch + // Navigators (i.e. the `List children` argument). + // + // See ScaffoldWithNavBar for more details on how the children + // are managed (using AnimatedBranchContainer). + return ScaffoldWithNavBar( + navigationShell: navigationShell, children: children); + }, + branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + navigatorKey: _tabANavigatorKey, + routes: [ + GoRoute( + // The screen to display as the root in the first tab of the + // bottom navigation bar. + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const RootScreenA(), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'A'), + ), + ], + ), + ], + ), + + // The route branch for the third tab of the bottom navigation bar. + StatefulShellBranch( + // StatefulShellBranch will automatically use the first descendant + // GoRoute as the initial location of the branch. If another route + // is desired, specify the location of it using the defaultLocation + // parameter. + // defaultLocation: '/c2', + routes: [ + StatefulShellRoute( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + // Just like with the top level StatefulShellRoute, no + // customization is done in the builder function. + return navigationShell; + }, + navigatorContainerBuilder: (BuildContext context, + StatefulNavigationShell navigationShell, + List children) { + // Returning a customized container for the branch + // Navigators (i.e. the `List children` argument). + // + // See TabbedRootScreen for more details on how the children + // are managed (in a TabBarView). + return TabbedRootScreen( + navigationShell: navigationShell, children: children); + }, + // This bottom tab uses a nested shell, wrapping sub routes in a + // top TabBar. + branches: [ + StatefulShellBranch(routes: [ + GoRoute( + path: '/b1', + builder: (BuildContext context, GoRouterState state) => + const TabScreen( + label: 'B1', detailsPath: '/b1/details'), + routes: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + const DetailsScreen( + label: 'B1', + withScaffold: false, + ), + ), + ], + ), + ]), + StatefulShellBranch(routes: [ + GoRoute( + path: '/b2', + builder: (BuildContext context, GoRouterState state) => + const TabScreen( + label: 'B2', detailsPath: '/b2/details'), + routes: [ + GoRoute( + path: 'details', + builder: + (BuildContext context, GoRouterState state) => + const DetailsScreen( + label: 'B2', + withScaffold: false, + ), + ), + ], + ), + ]), + ], + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.navigationShell, + required this.children, + Key? key, + }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + + /// The navigation shell and container for the branch Navigators. + final StatefulNavigationShell navigationShell; + + /// The children (branch Navigators) to display in a custom container + /// ([AnimatedBranchContainer]). + final List children; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: AnimatedBranchContainer( + currentIndex: navigationShell.currentIndex, + children: children, + ), + bottomNavigationBar: BottomNavigationBar( + // Here, the items of BottomNavigationBar are hard coded. In a real + // world scenario, the items would most likely be generated from the + // branches of the shell route, which can be fetched using + // `navigationShell.route.branches`. + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), + BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), + ], + currentIndex: navigationShell.currentIndex, + onTap: (int index) => _onTap(context, index), + ), + ); + } + + /// Navigate to the current location of the branch at the provided index when + /// tapping an item in the BottomNavigationBar. + void _onTap(BuildContext context, int index) { + // When navigating to a new branch, it's recommended to use the goBranch + // method, as doing so makes sure the last navigation state of the + // Navigator for the branch is restored. + navigationShell.goBranch( + index, + // A common pattern when using bottom navigation bars is to support + // navigating to the initial location when tapping the item that is + // already active. This example demonstrates how to support this behavior, + // using the initialLocation parameter of goBranch. + initialLocation: index == navigationShell.currentIndex, + ); + } +} + +/// Custom branch Navigator container that provides animated transitions +/// when switching branches. +class AnimatedBranchContainer extends StatelessWidget { + /// Creates a AnimatedBranchContainer + const AnimatedBranchContainer( + {super.key, required this.currentIndex, required this.children}); + + /// The index (in [children]) of the branch Navigator to display. + final int currentIndex; + + /// The children (branch Navigators) to display in this container. + final List children; + + @override + Widget build(BuildContext context) { + return Stack( + children: children.mapIndexed( + (int index, Widget navigator) { + return AnimatedScale( + scale: index == currentIndex ? 1 : 1.5, + duration: const Duration(milliseconds: 400), + child: AnimatedOpacity( + opacity: index == currentIndex ? 1 : 0, + duration: const Duration(milliseconds: 400), + child: _branchNavigatorWrapper(index, navigator), + ), + ); + }, + ).toList()); + } + + Widget _branchNavigatorWrapper(int index, Widget navigator) => IgnorePointer( + ignoring: index != currentIndex, + child: TickerMode( + enabled: index == currentIndex, + child: navigator, + ), + ); +} + +/// Widget for the root page for the first section of the bottom navigation bar. +class RootScreenA extends StatelessWidget { + /// Creates a RootScreenA + const RootScreenA({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Root of section A'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen A', style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go('/a/details'); + }, + child: const Text('View details'), + ), + ], + ), + ), + ); + } +} + +/// The details screen for either the A or B screen. +class DetailsScreen extends StatefulWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + this.param, + this.withScaffold = true, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + /// Optional param + final String? param; + + /// Wrap in scaffold + final bool withScaffold; + + @override + State createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State { + int _counter = 0; + + @override + Widget build(BuildContext context) { + if (widget.withScaffold) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); + } else { + return ColoredBox( + color: Theme.of(context).scaffoldBackgroundColor, + child: _build(context), + ); + } + } + + Widget _build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Details for ${widget.label} - Counter: $_counter', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { + _counter++; + }); + }, + child: const Text('Increment counter'), + ), + const Padding(padding: EdgeInsets.all(8)), + if (widget.param != null) + Text('Parameter: ${widget.param!}', + style: Theme.of(context).textTheme.titleMedium), + const Padding(padding: EdgeInsets.all(8)), + if (!widget.withScaffold) ...[ + const Padding(padding: EdgeInsets.all(16)), + TextButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('< Back', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + ), + ] + ], + ), + ); + } +} + +/// Builds a nested shell using a [TabBar] and [TabBarView]. +class TabbedRootScreen extends StatefulWidget { + /// Constructs a TabbedRootScreen + const TabbedRootScreen( + {required this.navigationShell, required this.children, super.key}); + + /// The current state of the parent StatefulShellRoute. + final StatefulNavigationShell navigationShell; + + /// The children (branch Navigators) to display in the [TabBarView]. + final List children; + + @override + State createState() => _TabbedRootScreenState(); +} + +class _TabbedRootScreenState extends State + with SingleTickerProviderStateMixin { + late final TabController _tabController = TabController( + length: widget.children.length, + vsync: this, + initialIndex: widget.navigationShell.currentIndex); + + @override + void didUpdateWidget(covariant TabbedRootScreen oldWidget) { + super.didUpdateWidget(oldWidget); + _tabController.index = widget.navigationShell.currentIndex; + } + + @override + Widget build(BuildContext context) { + final List tabs = widget.children + .mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}')) + .toList(); + + return Scaffold( + appBar: AppBar( + title: const Text('Root of Section B (nested TabBar shell)'), + bottom: TabBar( + controller: _tabController, + tabs: tabs, + onTap: (int tappedIndex) => _onTabTap(context, tappedIndex), + )), + body: TabBarView( + controller: _tabController, + children: widget.children, + ), + ); + } + + void _onTabTap(BuildContext context, int index) { + widget.navigationShell.goBranch(index); + } +} + +/// Widget for the pages in the top tab bar. +class TabScreen extends StatelessWidget { + /// Creates a RootScreen + const TabScreen({required this.label, required this.detailsPath, super.key}); + + /// The label + final String label; + + /// The path to the detail page + final String detailsPath; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath); + }, + child: const Text('View details'), + ), + ], + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/others/error_screen.dart b/go_router/assets/examples/official/lib/others/error_screen.dart new file mode 100644 index 0000000..5c071fc --- /dev/null +++ b/go_router/assets/examples/official/lib/others/error_screen.dart @@ -0,0 +1,110 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Custom Error Screen'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (BuildContext context, GoRouterState state) => + const Page2Screen(), + ), + ], + errorBuilder: (BuildContext context, GoRouterState state) => + ErrorScreen(state.error!), + ); +} + +/// The screen of the first page. +class Page1Screen extends StatelessWidget { + /// Creates a [Page1Screen]. + const Page1Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} + +/// The screen of the second page. +class Page2Screen extends StatelessWidget { + /// Creates a [Page2Screen]. + const Page2Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} + +/// The screen of the error page. +class ErrorScreen extends StatelessWidget { + /// Creates an [ErrorScreen]. + const ErrorScreen(this.error, {super.key}); + + /// The error to display. + final Exception error; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('My "Page Not Found" Screen')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SelectableText(error.toString()), + TextButton( + onPressed: () => context.go('/'), + child: const Text('Home'), + ), + ], + ), + ), + ); +} diff --git a/go_router/assets/examples/official/lib/others/extra_param.dart b/go_router/assets/examples/official/lib/others/extra_param.dart new file mode 100644 index 0000000..6cb1c39 --- /dev/null +++ b/go_router/assets/examples/official/lib/others/extra_param.dart @@ -0,0 +1,130 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// Family data class. +class Family { + /// Create a family. + const Family({required this.name, required this.people}); + + /// The last name of the family. + final String name; + + /// The people in the family. + final Map people; +} + +/// Person data class. +class Person { + /// Creates a person. + const Person({required this.name}); + + /// The first name of the person. + final String name; +} + +const Map _families = { + 'f1': Family( + name: 'Doe', + people: { + 'p1': Person(name: 'Jane'), + 'p2': Person(name: 'John'), + }, + ), + 'f2': Family( + name: 'Wong', + people: { + 'p1': Person(name: 'June'), + 'p2': Person(name: 'Xin'), + }, + ), +}; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Extra Parameter'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + name: 'home', + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + name: 'family', + path: 'family', + builder: (BuildContext context, GoRouterState state) { + final Map params = + state.extra! as Map; + final String fid = params['fid']! as String; + return FamilyScreen(fid: fid); + }, + ), + ], + ), + ], + ); +} + +/// The home screen that shows a list of families. +class HomeScreen extends StatelessWidget { + /// Creates a [HomeScreen]. + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: ListView( + children: [ + for (final MapEntry entry in _families.entries) + ListTile( + title: Text(entry.value.name), + onTap: () => context.goNamed('family', + extra: {'fid': entry.key}), + ) + ], + ), + ); +} + +/// The screen that shows a list of persons in a family. +class FamilyScreen extends StatelessWidget { + /// Creates a [FamilyScreen]. + const FamilyScreen({required this.fid, super.key}); + + /// The family to display. + final String fid; + + @override + Widget build(BuildContext context) { + final Map people = _families[fid]!.people; + return Scaffold( + appBar: AppBar(title: Text(_families[fid]!.name)), + body: ListView( + children: [ + for (final Person p in people.values) + ListTile( + title: Text(p.name), + ), + ], + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/others/init_loc.dart b/go_router/assets/examples/official/lib/others/init_loc.dart new file mode 100644 index 0000000..4f61b00 --- /dev/null +++ b/go_router/assets/examples/official/lib/others/init_loc.dart @@ -0,0 +1,110 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Initial Location'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + initialLocation: '/page3', + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (BuildContext context, GoRouterState state) => + const Page2Screen(), + ), + GoRoute( + path: '/page3', + builder: (BuildContext context, GoRouterState state) => + const Page3Screen(), + ), + ], + ); +} + +/// The screen of the first page. +class Page1Screen extends StatelessWidget { + /// Creates a [Page1Screen]. + const Page1Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} + +/// The screen of the second page. +class Page2Screen extends StatelessWidget { + /// Creates a [Page2Screen]. + const Page2Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} + +/// The screen of the third page. +class Page3Screen extends StatelessWidget { + /// Creates a [Page3Screen]. + const Page3Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} diff --git a/go_router/assets/examples/official/lib/others/nav_observer.dart b/go_router/assets/examples/official/lib/others/nav_observer.dart new file mode 100644 index 0000000..038d337 --- /dev/null +++ b/go_router/assets/examples/official/lib/others/nav_observer.dart @@ -0,0 +1,167 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Navigator Observer'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + observers: [MyNavObserver()], + routes: [ + GoRoute( + // if there's no name, path will be used as name for observers + path: '/', + builder: (BuildContext context, GoRouterState state) => + const Page1Screen(), + routes: [ + GoRoute( + name: 'page2', + path: 'page2/:p1', + builder: (BuildContext context, GoRouterState state) => + const Page2Screen(), + routes: [ + GoRoute( + name: 'page3', + path: 'page3', + builder: (BuildContext context, GoRouterState state) => + const Page3Screen(), + ), + ], + ), + ], + ), + ], + ); +} + +/// The Navigator observer. +class MyNavObserver extends NavigatorObserver { + /// Creates a [MyNavObserver]. + MyNavObserver() { + log.onRecord.listen((LogRecord e) => debugPrint('$e')); + } + + /// The logged message. + final Logger log = Logger('MyNavObserver'); + + @override + void didPush(Route route, Route? previousRoute) => + log.info('didPush: ${route.str}, previousRoute= ${previousRoute?.str}'); + + @override + void didPop(Route route, Route? previousRoute) => + log.info('didPop: ${route.str}, previousRoute= ${previousRoute?.str}'); + + @override + void didRemove(Route route, Route? previousRoute) => + log.info('didRemove: ${route.str}, previousRoute= ${previousRoute?.str}'); + + @override + void didReplace({Route? newRoute, Route? oldRoute}) => + log.info('didReplace: new= ${newRoute?.str}, old= ${oldRoute?.str}'); + + @override + void didStartUserGesture( + Route route, + Route? previousRoute, + ) => + log.info('didStartUserGesture: ${route.str}, ' + 'previousRoute= ${previousRoute?.str}'); + + @override + void didStopUserGesture() => log.info('didStopUserGesture'); +} + +extension on Route { + String get str => 'route(${settings.name}: ${settings.arguments})'; +} + +/// The screen of the first page. +class Page1Screen extends StatelessWidget { + /// Creates a [Page1Screen]. + const Page1Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.goNamed( + 'page2', + pathParameters: {'p1': 'pv1'}, + queryParameters: {'q1': 'qv1'}, + ), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} + +/// The screen of the second page. +class Page2Screen extends StatelessWidget { + /// Creates a [Page2Screen]. + const Page2Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.goNamed( + 'page3', + pathParameters: {'p1': 'pv2'}, + ), + child: const Text('Go to page 3'), + ), + ], + ), + ), + ); +} + +/// The screen of the third page. +class Page3Screen extends StatelessWidget { + /// Creates a [Page3Screen]. + const Page3Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} diff --git a/go_router/assets/examples/official/lib/others/push.dart b/go_router/assets/examples/official/lib/others/push.dart new file mode 100644 index 0000000..0cea894 --- /dev/null +++ b/go_router/assets/examples/official/lib/others/push.dart @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Push'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const Page1ScreenWithPush(), + ), + GoRoute( + path: '/page2', + builder: (BuildContext context, GoRouterState state) => + Page2ScreenWithPush( + int.parse(state.uri.queryParameters['push-count']!), + ), + ), + ], + ); +} + +/// The screen of the first page. +class Page1ScreenWithPush extends StatelessWidget { + /// Creates a [Page1ScreenWithPush]. + const Page1ScreenWithPush({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('${App.title}: page 1')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.push('/page2?push-count=1'), + child: const Text('Push page 2'), + ), + ], + ), + ), + ); +} + +/// The screen of the second page. +class Page2ScreenWithPush extends StatelessWidget { + /// Creates a [Page2ScreenWithPush]. + const Page2ScreenWithPush(this.pushCount, {super.key}); + + /// The push count. + final int pushCount; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: Text('${App.title}: page 2 w/ push count $pushCount'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: ElevatedButton( + onPressed: () => context.push( + '/page2?push-count=${pushCount + 1}', + ), + child: const Text('Push page 2 (again)'), + ), + ), + ], + ), + ), + ); +} diff --git a/go_router/assets/examples/official/lib/others/router_neglect.dart b/go_router/assets/examples/official/lib/others/router_neglect.dart new file mode 100644 index 0000000..75686a1 --- /dev/null +++ b/go_router/assets/examples/official/lib/others/router_neglect.dart @@ -0,0 +1,95 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Router neglect'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + // turn off history tracking in the browser for this navigation + routerNeglect: true, + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const Page1Screen(), + ), + GoRoute( + path: '/page2', + builder: (BuildContext context, GoRouterState state) => + const Page2Screen(), + ), + ], + ); +} + +/// The screen of the first page. +class Page1Screen extends StatelessWidget { + /// Creates a [Page1Screen]. + const Page1Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + const SizedBox(height: 8), + ElevatedButton( + // turn off history tracking in the browser for this navigation; + // note that this isn't necessary when you've set routerNeglect + // but it does illustrate the technique + onPressed: () => Router.neglect( + context, + () => context.push('/page2'), + ), + child: const Text('Push page 2'), + ), + ], + ), + ), + ); +} + +/// The screen of the second page. +class Page2Screen extends StatelessWidget { + /// Creates a [Page2Screen]. + const Page2Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} diff --git a/go_router/assets/examples/official/lib/others/state_restoration.dart b/go_router/assets/examples/official/lib/others/state_restoration.dart new file mode 100644 index 0000000..93e8aca --- /dev/null +++ b/go_router/assets/examples/official/lib/others/state_restoration.dart @@ -0,0 +1,102 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp( + const RootRestorationScope(restorationId: 'root', child: App()), + ); + +/// The main app. +class App extends StatefulWidget { + /// Creates an [App]. + const App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: State Restoration'; + + @override + State createState() => _AppState(); +} + +class _AppState extends State with RestorationMixin { + @override + String get restorationId => 'wrapper'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + // Implement restoreState for your app + } + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: App.title, + restorationScopeId: 'app', + ); + + final GoRouter _router = GoRouter( + routes: [ + // restorationId set for the route automatically + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const Page1Screen(), + ), + + // restorationId set for the route automatically + GoRoute( + path: '/page2', + builder: (BuildContext context, GoRouterState state) => + const Page2Screen(), + ), + ], + restorationScopeId: 'router', + ); +} + +/// The screen of the first page. +class Page1Screen extends StatelessWidget { + /// Creates a [Page1Screen]. + const Page1Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/page2'), + child: const Text('Go to page 2'), + ), + ], + ), + ), + ); +} + +/// The screen of the second page. +class Page2Screen extends StatelessWidget { + /// Creates a [Page2Screen]. + const Page2Screen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go to home page'), + ), + ], + ), + ), + ); +} diff --git a/go_router/assets/examples/official/lib/others/stateful_shell_state_restoration.dart b/go_router/assets/examples/official/lib/others/stateful_shell_state_restoration.dart new file mode 100644 index 0000000..aeecd11 --- /dev/null +++ b/go_router/assets/examples/official/lib/others/stateful_shell_state_restoration.dart @@ -0,0 +1,236 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(RestorableStatefulShellRouteExampleApp()); + +/// An example demonstrating how to use StatefulShellRoute with state +/// restoration. +class RestorableStatefulShellRouteExampleApp extends StatelessWidget { + /// Creates a NestedTabNavigationExampleApp + RestorableStatefulShellRouteExampleApp({super.key}); + + final GoRouter _router = GoRouter( + initialLocation: '/a', + restorationScopeId: 'router', + routes: [ + StatefulShellRoute.indexedStack( + restorationScopeId: 'shell1', + pageBuilder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + return MaterialPage( + restorationId: 'shellWidget1', + child: ScaffoldWithNavBar(navigationShell: navigationShell)); + }, + branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + restorationScopeId: 'branchA', + routes: [ + GoRoute( + // The screen to display as the root in the first tab of the + // bottom navigation bar. + path: '/a', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenA', + child: + RootScreen(label: 'A', detailsPath: '/a/details')), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenADetail', + child: DetailsScreen(label: 'A')), + ), + ], + ), + ], + ), + // The route branch for the second tab of the bottom navigation bar. + StatefulShellBranch( + restorationScopeId: 'branchB', + routes: [ + GoRoute( + // The screen to display as the root in the second tab of the + // bottom navigation bar. + path: '/b', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenB', + child: + RootScreen(label: 'B', detailsPath: '/b/details')), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + pageBuilder: (BuildContext context, GoRouterState state) => + const MaterialPage( + restorationId: 'screenBDetail', + child: DetailsScreen(label: 'B')), + ), + ], + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + restorationScopeId: 'app', + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.navigationShell, + Key? key, + }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + + /// The navigation shell and container for the branch Navigators. + final StatefulNavigationShell navigationShell; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: navigationShell, + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), + BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), + ], + currentIndex: navigationShell.currentIndex, + onTap: (int tappedIndex) => navigationShell.goBranch(tappedIndex), + ), + ); + } +} + +/// Widget for the root/initial pages in the bottom navigation bar. +class RootScreen extends StatelessWidget { + /// Creates a RootScreen + const RootScreen({ + required this.label, + required this.detailsPath, + super.key, + }); + + /// The label + final String label; + + /// The path to the detail page + final String detailsPath; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Root of section $label'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath); + }, + child: const Text('View details'), + ), + ], + ), + ), + ); + } +} + +/// The details screen for either the A or B screen. +class DetailsScreen extends StatefulWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + @override + State createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State with RestorationMixin { + final RestorableInt _counter = RestorableInt(0); + + @override + String? get restorationId => 'DetailsScreen-${widget.label}'; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + registerForRestoration(_counter, 'counter'); + } + + @override + void dispose() { + super.dispose(); + _counter.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); + } + + Widget _build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Details for ${widget.label} - Counter: ${_counter.value}', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { + _counter.value++; + }); + }, + child: const Text('Increment counter'), + ), + const Padding(padding: EdgeInsets.all(8)), + ], + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/others/transitions.dart b/go_router/assets/examples/official/lib/others/transitions.dart new file mode 100644 index 0000000..f255952 --- /dev/null +++ b/go_router/assets/examples/official/lib/others/transitions.dart @@ -0,0 +1,163 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Custom Transitions'; + + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + ); + + final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + redirect: (_, __) => '/none', + ), + GoRoute( + path: '/fade', + pageBuilder: (BuildContext context, GoRouterState state) => + CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'fade', + color: Colors.red, + ), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + FadeTransition(opacity: animation, child: child), + ), + ), + GoRoute( + path: '/scale', + pageBuilder: (BuildContext context, GoRouterState state) => + CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'scale', + color: Colors.green, + ), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + ScaleTransition(scale: animation, child: child), + ), + ), + GoRoute( + path: '/slide', + pageBuilder: (BuildContext context, GoRouterState state) => + CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'slide', + color: Colors.yellow, + ), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + SlideTransition( + position: animation.drive( + Tween( + begin: const Offset(0.25, 0.25), + end: Offset.zero, + ).chain(CurveTween(curve: Curves.easeIn)), + ), + child: child, + ), + ), + ), + GoRoute( + path: '/rotation', + pageBuilder: (BuildContext context, GoRouterState state) => + CustomTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'rotation', + color: Colors.purple, + ), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) => + RotationTransition(turns: animation, child: child), + ), + ), + GoRoute( + path: '/none', + pageBuilder: (BuildContext context, GoRouterState state) => + NoTransitionPage( + key: state.pageKey, + child: const ExampleTransitionsScreen( + kind: 'none', + color: Colors.white, + ), + ), + ), + ], + ); +} + +/// An Example transitions screen. +class ExampleTransitionsScreen extends StatelessWidget { + /// Creates an [ExampleTransitionsScreen]. + const ExampleTransitionsScreen({ + required this.color, + required this.kind, + super.key, + }); + + /// The available transition kinds. + static final List kinds = [ + 'fade', + 'scale', + 'slide', + 'rotation', + 'none' + ]; + + /// The color of the container. + final Color color; + + /// The transition kind. + final String kind; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: Text('${App.title}: $kind')), + body: ColoredBox( + color: color, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (final String kind in kinds) + Padding( + padding: const EdgeInsets.all(8), + child: ElevatedButton( + onPressed: () => context.go('/$kind'), + child: Text('$kind transition'), + ), + ) + ], + ), + ), + ), + ); +} diff --git a/go_router/assets/examples/official/lib/path_and_query_parameters.dart b/go_router/assets/examples/official/lib/path_and_query_parameters.dart new file mode 100755 index 0000000..42d3b15 --- /dev/null +++ b/go_router/assets/examples/official/lib/path_and_query_parameters.dart @@ -0,0 +1,166 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +// This scenario demonstrates how to use path parameters and query parameters. +// +// The route segments that start with ':' are treated as path parameters when +// defining GoRoute[s]. The parameter values can be accessed through +// GoRouterState.pathParameters. +// +// The query parameters are automatically stored in GoRouterState.queryParameters. + +/// Family data class. +class Family { + /// Create a family. + const Family({required this.name, required this.people}); + + /// The last name of the family. + final String name; + + /// The people in the family. + final Map people; +} + +/// Person data class. +class Person { + /// Creates a person. + const Person({required this.name}); + + /// The first name of the person. + final String name; +} + +const Map _families = { + 'f1': Family( + name: 'Doe', + people: { + 'p1': Person(name: 'Jane'), + 'p2': Person(name: 'John'), + }, + ), + 'f2': Family( + name: 'Wong', + people: { + 'p1': Person(name: 'June'), + 'p2': Person(name: 'Xin'), + }, + ), +}; + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + /// The title of the app. + static const String title = 'GoRouter Example: Query Parameters'; + + // add the login info into the tree as app state that can change over time + @override + Widget build(BuildContext context) => MaterialApp.router( + routerConfig: _router, + title: title, + debugShowCheckedModeBanner: false, + ); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + routes: [ + GoRoute( + name: 'family', + path: 'family/:fid', + builder: (BuildContext context, GoRouterState state) { + return FamilyScreen( + fid: state.pathParameters['fid']!, + asc: state.uri.queryParameters['sort'] == 'asc', + ); + }), + ], + ), + ], + ); +} + +/// The home screen that shows a list of families. +class HomeScreen extends StatelessWidget { + /// Creates a [HomeScreen]. + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text(App.title), + ), + body: ListView( + children: [ + for (final MapEntry entry in _families.entries) + ListTile( + title: Text(entry.value.name), + onTap: () => context.go('/family/${entry.key}'), + ) + ], + ), + ); + } +} + +/// The screen that shows a list of persons in a family. +class FamilyScreen extends StatelessWidget { + /// Creates a [FamilyScreen]. + const FamilyScreen({required this.fid, required this.asc, super.key}); + + /// The family to display. + final String fid; + + /// Whether to sort the name in ascending order. + final bool asc; + + @override + Widget build(BuildContext context) { + final Map newQueries; + final List names = _families[fid]! + .people + .values + .map((Person p) => p.name) + .toList(); + names.sort(); + if (asc) { + newQueries = const {'sort': 'desc'}; + } else { + newQueries = const {'sort': 'asc'}; + } + return Scaffold( + appBar: AppBar( + title: Text(_families[fid]!.name), + actions: [ + IconButton( + onPressed: () => context.goNamed('family', + pathParameters: {'fid': fid}, + queryParameters: newQueries), + tooltip: 'sort ascending or descending', + icon: const Icon(Icons.sort), + ) + ], + ), + body: ListView( + children: [ + for (final String name in asc ? names : names.reversed) + ListTile( + title: Text(name), + ), + ], + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/push_with_shell_route.dart b/go_router/assets/examples/official/lib/push_with_shell_route.dart new file mode 100644 index 0000000..d26fb10 --- /dev/null +++ b/go_router/assets/examples/official/lib/push_with_shell_route.dart @@ -0,0 +1,160 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +// This scenario demonstrates the behavior when pushing ShellRoute in various +// scenario. +// +// This example have three routes, /shell1, /shell2, and /regular-route. The +// /shell1 and /shell2 are nested in different ShellRoutes. The /regular-route +// is a simple GoRoute. + +void main() { + runApp(PushWithShellRouteExampleApp()); +} + +/// An example demonstrating how to use [ShellRoute] +class PushWithShellRouteExampleApp extends StatelessWidget { + /// Creates a [PushWithShellRouteExampleApp] + PushWithShellRouteExampleApp({super.key}); + + final GoRouter _router = GoRouter( + initialLocation: '/home', + debugLogDiagnostics: true, + routes: [ + ShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) { + return ScaffoldForShell1(child: child); + }, + routes: [ + GoRoute( + path: '/home', + builder: (BuildContext context, GoRouterState state) { + return const Home(); + }, + ), + GoRoute( + path: '/shell1', + pageBuilder: (_, __) => const NoTransitionPage( + child: Center( + child: Text('shell1 body'), + ), + ), + ), + ], + ), + ShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) { + return ScaffoldForShell2(child: child); + }, + routes: [ + GoRoute( + path: '/shell2', + builder: (BuildContext context, GoRouterState state) { + return const Center(child: Text('shell2 body')); + }, + ), + ], + ), + GoRoute( + path: '/regular-route', + builder: (BuildContext context, GoRouterState state) { + return const Scaffold( + body: Center(child: Text('regular route')), + ); + }, + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for /shell1 +class ScaffoldForShell1 extends StatelessWidget { + /// Constructs an [ScaffoldForShell1]. + const ScaffoldForShell1({ + required this.child, + super.key, + }); + + /// The widget to display in the body of the Scaffold. + /// In this sample, it is a Navigator. + final Widget child; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('shell1')), + body: child, + ); + } +} + +/// Builds the "shell" for /shell1 +class ScaffoldForShell2 extends StatelessWidget { + /// Constructs an [ScaffoldForShell1]. + const ScaffoldForShell2({ + required this.child, + super.key, + }); + + /// The widget to display in the body of the Scaffold. + /// In this sample, it is a Navigator. + final Widget child; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('shell2')), + body: child, + ); + } +} + +/// The screen for /home +class Home extends StatelessWidget { + /// Constructs a [Home] widget. + const Home({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextButton( + onPressed: () { + GoRouter.of(context).push('/shell1'); + }, + child: const Text('push the same shell route /shell1'), + ), + TextButton( + onPressed: () { + GoRouter.of(context).push('/shell2'); + }, + child: const Text('push the different shell route /shell2'), + ), + TextButton( + onPressed: () { + GoRouter.of(context).push('/regular-route'); + }, + child: const Text('push the regular route /regular-route'), + ), + ], + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/redirection.dart b/go_router/assets/examples/official/lib/redirection.dart new file mode 100644 index 0000000..aded4b7 --- /dev/null +++ b/go_router/assets/examples/official/lib/redirection.dart @@ -0,0 +1,146 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +// This scenario demonstrates how to use redirect to handle a sign-in flow. +// +// The GoRouter.redirect method is called before the app is navigate to a +// new page. You can choose to redirect to a different page by returning a +// non-null URL string. + +/// The login information. +class LoginInfo extends ChangeNotifier { + /// The username of login. + String get userName => _userName; + String _userName = ''; + + /// Whether a user has logged in. + bool get loggedIn => _userName.isNotEmpty; + + /// Logs in a user. + void login(String userName) { + _userName = userName; + notifyListeners(); + } + + /// Logs out the current user. + void logout() { + _userName = ''; + notifyListeners(); + } +} + +void main() => runApp(App()); + +/// The main app. +class App extends StatelessWidget { + /// Creates an [App]. + App({super.key}); + + final LoginInfo _loginInfo = LoginInfo(); + + /// The title of the app. + static const String title = 'GoRouter Example: Redirection'; + + // add the login info into the tree as app state that can change over time + @override + Widget build(BuildContext context) => ChangeNotifierProvider.value( + value: _loginInfo, + child: MaterialApp.router( + routerConfig: _router, + title: title, + debugShowCheckedModeBanner: false, + ), + ); + + late final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const HomeScreen(), + ), + GoRoute( + path: '/login', + builder: (BuildContext context, GoRouterState state) => + const LoginScreen(), + ), + ], + + // redirect to the login page if the user is not logged in + redirect: (BuildContext context, GoRouterState state) { + // if the user is not logged in, they need to login + final bool loggedIn = _loginInfo.loggedIn; + final bool loggingIn = state.matchedLocation == '/login'; + if (!loggedIn) { + return '/login'; + } + + // if the user is logged in but still on the login page, send them to + // the home page + if (loggingIn) { + return '/'; + } + + // no need to redirect at all + return null; + }, + + // changes on the listenable will cause the router to refresh it's route + refreshListenable: _loginInfo, + ); +} + +/// The login screen. +class LoginScreen extends StatelessWidget { + /// Creates a [LoginScreen]. + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text(App.title)), + body: Center( + child: ElevatedButton( + onPressed: () { + // log a user in, letting all the listeners know + context.read().login('test-user'); + + // router will automatically redirect from /login to / using + // refreshListenable + }, + child: const Text('Login'), + ), + ), + ); +} + +/// The home screen. +class HomeScreen extends StatelessWidget { + /// Creates a [HomeScreen]. + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final LoginInfo info = context.read(); + + return Scaffold( + appBar: AppBar( + title: const Text(App.title), + actions: [ + IconButton( + onPressed: info.logout, + tooltip: 'Logout: ${info.userName}', + icon: const Icon(Icons.logout), + ) + ], + ), + body: const Center( + child: Text('HomeScreen'), + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/routing_config.dart b/go_router/assets/examples/official/lib/routing_config.dart new file mode 100644 index 0000000..85acf20 --- /dev/null +++ b/go_router/assets/examples/official/lib/routing_config.dart @@ -0,0 +1,108 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// This app shows how to dynamically add more route into routing config +void main() => runApp(const MyApp()); + +/// The main app. +class MyApp extends StatefulWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + bool isNewRouteAdded = false; + + late final ValueNotifier myConfig = + ValueNotifier(_generateRoutingConfig()); + + late final GoRouter router = GoRouter.routingConfig( + routingConfig: myConfig, + errorBuilder: (_, GoRouterState state) => Scaffold( + appBar: AppBar(title: const Text('Page not found')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('${state.uri} does not exist'), + ElevatedButton( + onPressed: () => router.go('/'), + child: const Text('Go to home')), + ], + )), + )); + + RoutingConfig _generateRoutingConfig() { + return RoutingConfig( + routes: [ + GoRoute( + path: '/', + builder: (_, __) { + return Scaffold( + appBar: AppBar(title: const Text('Home')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: isNewRouteAdded + ? null + : () { + setState(() { + isNewRouteAdded = true; + // Modify the routing config. + myConfig.value = _generateRoutingConfig(); + }); + }, + child: isNewRouteAdded + ? const Text('A route has been added') + : const Text('Add a new route'), + ), + ElevatedButton( + onPressed: () { + router.go('/new-route'); + }, + child: const Text('Try going to /new-route'), + ) + ], + ), + ), + ); + }, + ), + if (isNewRouteAdded) + GoRoute( + path: '/new-route', + builder: (_, __) { + return Scaffold( + appBar: AppBar(title: const Text('A new Route')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => router.go('/'), + child: const Text('Go to home')), + ], + )), + ); + }, + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: router, + ); + } +} diff --git a/go_router/assets/examples/official/lib/shell_route.dart b/go_router/assets/examples/official/lib/shell_route.dart new file mode 100644 index 0000000..1628151 --- /dev/null +++ b/go_router/assets/examples/official/lib/shell_route.dart @@ -0,0 +1,286 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _shellNavigatorKey = + GlobalKey(debugLabel: 'shell'); + +// This scenario demonstrates how to set up nested navigation using ShellRoute, +// which is a pattern where an additional Navigator is placed in the widget tree +// to be used instead of the root navigator. This allows deep-links to display +// pages along with other UI components such as a BottomNavigationBar. +// +// This example demonstrates how to display a route within a ShellRoute and also +// push a screen using a different navigator (such as the root Navigator) by +// providing a `parentNavigatorKey`. + +void main() { + runApp(ShellRouteExampleApp()); +} + +/// An example demonstrating how to use [ShellRoute] +class ShellRouteExampleApp extends StatelessWidget { + /// Creates a [ShellRouteExampleApp] + ShellRouteExampleApp({super.key}); + + final GoRouter _router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/a', + debugLogDiagnostics: true, + routes: [ + /// Application shell + ShellRoute( + navigatorKey: _shellNavigatorKey, + builder: (BuildContext context, GoRouterState state, Widget child) { + return ScaffoldWithNavBar(child: child); + }, + routes: [ + /// The first screen to display in the bottom navigation bar. + GoRoute( + path: '/a', + builder: (BuildContext context, GoRouterState state) { + return const ScreenA(); + }, + routes: [ + // The details screen to display stacked on the inner Navigator. + // This will cover screen A but not the application shell. + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(label: 'A'); + }, + ), + ], + ), + + /// Displayed when the second item in the the bottom navigation bar is + /// selected. + GoRoute( + path: '/b', + builder: (BuildContext context, GoRouterState state) { + return const ScreenB(); + }, + routes: [ + /// Same as "/a/details", but displayed on the root Navigator by + /// specifying [parentNavigatorKey]. This will cover both screen B + /// and the application shell. + GoRoute( + path: 'details', + parentNavigatorKey: _rootNavigatorKey, + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(label: 'B'); + }, + ), + ], + ), + + /// The third screen to display in the bottom navigation bar. + GoRoute( + path: '/c', + builder: (BuildContext context, GoRouterState state) { + return const ScreenC(); + }, + routes: [ + // The details screen to display stacked on the inner Navigator. + // This will cover screen A but not the application shell. + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(label: 'C'); + }, + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.child, + super.key, + }); + + /// The widget to display in the body of the Scaffold. + /// In this sample, it is a Navigator. + final Widget child; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: child, + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.home), + label: 'A Screen', + ), + BottomNavigationBarItem( + icon: Icon(Icons.business), + label: 'B Screen', + ), + BottomNavigationBarItem( + icon: Icon(Icons.notification_important_rounded), + label: 'C Screen', + ), + ], + currentIndex: _calculateSelectedIndex(context), + onTap: (int idx) => _onItemTapped(idx, context), + ), + ); + } + + static int _calculateSelectedIndex(BuildContext context) { + final String location = GoRouterState.of(context).uri.path; + if (location.startsWith('/a')) { + return 0; + } + if (location.startsWith('/b')) { + return 1; + } + if (location.startsWith('/c')) { + return 2; + } + return 0; + } + + void _onItemTapped(int index, BuildContext context) { + switch (index) { + case 0: + GoRouter.of(context).go('/a'); + case 1: + GoRouter.of(context).go('/b'); + case 2: + GoRouter.of(context).go('/c'); + } + } +} + +/// The first screen in the bottom navigation bar. +class ScreenA extends StatelessWidget { + /// Constructs a [ScreenA] widget. + const ScreenA({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Screen A'), + TextButton( + onPressed: () { + GoRouter.of(context).go('/a/details'); + }, + child: const Text('View A details'), + ), + ], + ), + ), + ); + } +} + +/// The second screen in the bottom navigation bar. +class ScreenB extends StatelessWidget { + /// Constructs a [ScreenB] widget. + const ScreenB({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Screen B'), + TextButton( + onPressed: () { + GoRouter.of(context).go('/b/details'); + }, + child: const Text('View B details'), + ), + ], + ), + ), + ); + } +} + +/// The third screen in the bottom navigation bar. +class ScreenC extends StatelessWidget { + /// Constructs a [ScreenC] widget. + const ScreenC({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Screen C'), + TextButton( + onPressed: () { + GoRouter.of(context).go('/c/details'); + }, + child: const Text('View C details'), + ), + ], + ), + ), + ); + } +} + +/// The details screen for either the A, B or C screen. +class DetailsScreen extends StatelessWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Details Screen'), + ), + body: Center( + child: Text( + 'Details for $label', + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/shell_route_top_route.dart b/go_router/assets/examples/official/lib/shell_route_top_route.dart new file mode 100644 index 0000000..ce6128a --- /dev/null +++ b/go_router/assets/examples/official/lib/shell_route_top_route.dart @@ -0,0 +1,305 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _shellNavigatorKey = + GlobalKey(debugLabel: 'shell'); + +// This scenario demonstrates how to set up nested navigation using ShellRoute, +// which is a pattern where an additional Navigator is placed in the widget tree +// to be used instead of the root navigator. This allows deep-links to display +// pages along with other UI components such as a BottomNavigationBar. +// +// This example demonstrates how use topRoute in a ShellRoute to create the +// title in the AppBar above the child, which is different for each GoRoute. + +void main() { + runApp(ShellRouteExampleApp()); +} + +/// An example demonstrating how to use [ShellRoute] +class ShellRouteExampleApp extends StatelessWidget { + /// Creates a [ShellRouteExampleApp] + ShellRouteExampleApp({super.key}); + + final GoRouter _router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/a', + debugLogDiagnostics: true, + routes: [ + /// Application shell + ShellRoute( + navigatorKey: _shellNavigatorKey, + builder: (BuildContext context, GoRouterState state, Widget child) { + final String? routeName = GoRouterState.of(context).topRoute?.name; + // This title could also be created using a route's path parameters in GoRouterState + final String title = switch (routeName) { + 'a' => 'A Screen', + 'a.details' => 'A Details', + 'b' => 'B Screen', + 'b.details' => 'B Details', + 'c' => 'C Screen', + 'c.details' => 'C Details', + _ => 'Unknown', + }; + return ScaffoldWithNavBar(title: title, child: child); + }, + routes: [ + /// The first screen to display in the bottom navigation bar. + GoRoute( + // The name of this route used to determine the title in the ShellRoute. + name: 'a', + path: '/a', + builder: (BuildContext context, GoRouterState state) { + return const ScreenA(); + }, + routes: [ + // The details screen to display stacked on the inner Navigator. + // This will cover screen A but not the application shell. + GoRoute( + // The name of this route used to determine the title in the ShellRoute. + name: 'a.details', + path: 'details', + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(label: 'A'); + }, + ), + ], + ), + + /// Displayed when the second item in the the bottom navigation bar is + /// selected. + GoRoute( + // The name of this route used to determine the title in the ShellRoute. + name: 'b', + path: '/b', + builder: (BuildContext context, GoRouterState state) { + return const ScreenB(); + }, + routes: [ + // The details screen to display stacked on the inner Navigator. + // This will cover screen B but not the application shell. + GoRoute( + // The name of this route used to determine the title in the ShellRoute. + name: 'b.details', + path: 'details', + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(label: 'B'); + }, + ), + ], + ), + + /// The third screen to display in the bottom navigation bar. + GoRoute( + // The name of this route used to determine the title in the ShellRoute. + name: 'c', + path: '/c', + builder: (BuildContext context, GoRouterState state) { + return const ScreenC(); + }, + routes: [ + // The details screen to display stacked on the inner Navigator. + // This will cover screen C but not the application shell. + GoRoute( + // The name of this route used to determine the title in the ShellRoute. + name: 'c.details', + path: 'details', + builder: (BuildContext context, GoRouterState state) { + return const DetailsScreen(label: 'C'); + }, + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + super.key, + required this.title, + required this.child, + }); + + /// The title to display in the AppBar. + final String title; + + /// The widget to display in the body of the Scaffold. + /// In this sample, it is a Navigator. + final Widget child; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: child, + appBar: AppBar( + title: Text(title), + leading: _buildLeadingButton(context), + ), + bottomNavigationBar: BottomNavigationBar( + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.home), + label: 'A Screen', + ), + BottomNavigationBarItem( + icon: Icon(Icons.business), + label: 'B Screen', + ), + BottomNavigationBarItem( + icon: Icon(Icons.notification_important_rounded), + label: 'C Screen', + ), + ], + currentIndex: _calculateSelectedIndex(context), + onTap: (int idx) => _onItemTapped(idx, context), + ), + ); + } + + /// Builds the app bar leading button using the current location [Uri]. + /// + /// The [Scaffold]'s default back button cannot be used because it doesn't + /// have the context of the current child. + Widget? _buildLeadingButton(BuildContext context) { + final RouteMatchList currentConfiguration = + GoRouter.of(context).routerDelegate.currentConfiguration; + final RouteMatch lastMatch = currentConfiguration.last; + final Uri location = lastMatch is ImperativeRouteMatch + ? lastMatch.matches.uri + : currentConfiguration.uri; + final bool canPop = location.pathSegments.length > 1; + return canPop ? BackButton(onPressed: GoRouter.of(context).pop) : null; + } + + static int _calculateSelectedIndex(BuildContext context) { + final String location = GoRouterState.of(context).uri.path; + if (location.startsWith('/a')) { + return 0; + } + if (location.startsWith('/b')) { + return 1; + } + if (location.startsWith('/c')) { + return 2; + } + return 0; + } + + void _onItemTapped(int index, BuildContext context) { + switch (index) { + case 0: + GoRouter.of(context).go('/a'); + case 1: + GoRouter.of(context).go('/b'); + case 2: + GoRouter.of(context).go('/c'); + } + } +} + +/// The first screen in the bottom navigation bar. +class ScreenA extends StatelessWidget { + /// Constructs a [ScreenA] widget. + const ScreenA({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: TextButton( + onPressed: () { + GoRouter.of(context).go('/a/details'); + }, + child: const Text('View A details'), + ), + ), + ); + } +} + +/// The second screen in the bottom navigation bar. +class ScreenB extends StatelessWidget { + /// Constructs a [ScreenB] widget. + const ScreenB({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: TextButton( + onPressed: () { + GoRouter.of(context).go('/b/details'); + }, + child: const Text('View B details'), + ), + ), + ); + } +} + +/// The third screen in the bottom navigation bar. +class ScreenC extends StatelessWidget { + /// Constructs a [ScreenC] widget. + const ScreenC({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: TextButton( + onPressed: () { + GoRouter.of(context).go('/c/details'); + }, + child: const Text('View C details'), + ), + ), + ); + } +} + +/// The details screen for either the A, B or C screen. +class DetailsScreen extends StatelessWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Text( + 'Details for $label', + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/stateful_shell_route.dart b/go_router/assets/examples/official/lib/stateful_shell_route.dart new file mode 100644 index 0000000..9901619 --- /dev/null +++ b/go_router/assets/examples/official/lib/stateful_shell_route.dart @@ -0,0 +1,324 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final GlobalKey _rootNavigatorKey = + GlobalKey(debugLabel: 'root'); +final GlobalKey _sectionANavigatorKey = + GlobalKey(debugLabel: 'sectionANav'); + +// This example demonstrates how to setup nested navigation using a +// BottomNavigationBar, where each bar item uses its own persistent navigator, +// i.e. navigation state is maintained separately for each item. This setup also +// enables deep linking into nested pages. + +void main() { + runApp(NestedTabNavigationExampleApp()); +} + +/// An example demonstrating how to use nested navigators +class NestedTabNavigationExampleApp extends StatelessWidget { + /// Creates a NestedTabNavigationExampleApp + NestedTabNavigationExampleApp({super.key}); + + final GoRouter _router = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/a', + routes: [ + StatefulShellRoute.indexedStack( + builder: (BuildContext context, GoRouterState state, + StatefulNavigationShell navigationShell) { + // Return the widget that implements the custom shell (in this case + // using a BottomNavigationBar). The StatefulNavigationShell is passed + // to be able access the state of the shell and to navigate to other + // branches in a stateful way. + return ScaffoldWithNavBar(navigationShell: navigationShell); + }, + branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + navigatorKey: _sectionANavigatorKey, + routes: [ + GoRoute( + // The screen to display as the root in the first tab of the + // bottom navigation bar. + path: '/a', + builder: (BuildContext context, GoRouterState state) => + const RootScreen(label: 'A', detailsPath: '/a/details'), + routes: [ + // The details screen to display stacked on navigator of the + // first tab. This will cover screen A but not the application + // shell (bottom navigation bar). + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + const DetailsScreen(label: 'A'), + ), + ], + ), + ], + ), + + // The route branch for the second tab of the bottom navigation bar. + StatefulShellBranch( + // It's not necessary to provide a navigatorKey if it isn't also + // needed elsewhere. If not provided, a default key will be used. + routes: [ + GoRoute( + // The screen to display as the root in the second tab of the + // bottom navigation bar. + path: '/b', + builder: (BuildContext context, GoRouterState state) => + const RootScreen( + label: 'B', + detailsPath: '/b/details/1', + secondDetailsPath: '/b/details/2', + ), + routes: [ + GoRoute( + path: 'details/:param', + builder: (BuildContext context, GoRouterState state) => + DetailsScreen( + label: 'B', + param: state.pathParameters['param'], + ), + ), + ], + ), + ], + ), + + // The route branch for the third tab of the bottom navigation bar. + StatefulShellBranch( + routes: [ + GoRoute( + // The screen to display as the root in the third tab of the + // bottom navigation bar. + path: '/c', + builder: (BuildContext context, GoRouterState state) => + const RootScreen( + label: 'C', + detailsPath: '/c/details', + ), + routes: [ + GoRoute( + path: 'details', + builder: (BuildContext context, GoRouterState state) => + DetailsScreen( + label: 'C', + extra: state.extra, + ), + ), + ], + ), + ], + ), + ], + ), + ], + ); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + routerConfig: _router, + ); + } +} + +/// Builds the "shell" for the app by building a Scaffold with a +/// BottomNavigationBar, where [child] is placed in the body of the Scaffold. +class ScaffoldWithNavBar extends StatelessWidget { + /// Constructs an [ScaffoldWithNavBar]. + const ScaffoldWithNavBar({ + required this.navigationShell, + Key? key, + }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + + /// The navigation shell and container for the branch Navigators. + final StatefulNavigationShell navigationShell; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: navigationShell, + bottomNavigationBar: BottomNavigationBar( + // Here, the items of BottomNavigationBar are hard coded. In a real + // world scenario, the items would most likely be generated from the + // branches of the shell route, which can be fetched using + // `navigationShell.route.branches`. + items: const [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'), + BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'), + BottomNavigationBarItem(icon: Icon(Icons.tab), label: 'Section C'), + ], + currentIndex: navigationShell.currentIndex, + onTap: (int index) => _onTap(context, index), + ), + ); + } + + /// Navigate to the current location of the branch at the provided index when + /// tapping an item in the BottomNavigationBar. + void _onTap(BuildContext context, int index) { + // When navigating to a new branch, it's recommended to use the goBranch + // method, as doing so makes sure the last navigation state of the + // Navigator for the branch is restored. + navigationShell.goBranch( + index, + // A common pattern when using bottom navigation bars is to support + // navigating to the initial location when tapping the item that is + // already active. This example demonstrates how to support this behavior, + // using the initialLocation parameter of goBranch. + initialLocation: index == navigationShell.currentIndex, + ); + } +} + +/// Widget for the root/initial pages in the bottom navigation bar. +class RootScreen extends StatelessWidget { + /// Creates a RootScreen + const RootScreen({ + required this.label, + required this.detailsPath, + this.secondDetailsPath, + super.key, + }); + + /// The label + final String label; + + /// The path to the detail page + final String detailsPath; + + /// The path to another detail page + final String? secondDetailsPath; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Root of section $label'), + ), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Screen $label', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + GoRouter.of(context).go(detailsPath, extra: '$label-XYZ'); + }, + child: const Text('View details'), + ), + const Padding(padding: EdgeInsets.all(4)), + if (secondDetailsPath != null) + TextButton( + onPressed: () { + GoRouter.of(context).go(secondDetailsPath!); + }, + child: const Text('View more details'), + ), + ], + ), + ), + ); + } +} + +/// The details screen for either the A or B screen. +class DetailsScreen extends StatefulWidget { + /// Constructs a [DetailsScreen]. + const DetailsScreen({ + required this.label, + this.param, + this.extra, + this.withScaffold = true, + super.key, + }); + + /// The label to display in the center of the screen. + final String label; + + /// Optional param + final String? param; + + /// Optional extra object + final Object? extra; + + /// Wrap in scaffold + final bool withScaffold; + + @override + State createState() => DetailsScreenState(); +} + +/// The state for DetailsScreen +class DetailsScreenState extends State { + int _counter = 0; + + @override + Widget build(BuildContext context) { + if (widget.withScaffold) { + return Scaffold( + appBar: AppBar( + title: Text('Details Screen - ${widget.label}'), + ), + body: _build(context), + ); + } else { + return ColoredBox( + color: Theme.of(context).scaffoldBackgroundColor, + child: _build(context), + ); + } + } + + Widget _build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Details for ${widget.label} - Counter: $_counter', + style: Theme.of(context).textTheme.titleLarge), + const Padding(padding: EdgeInsets.all(4)), + TextButton( + onPressed: () { + setState(() { + _counter++; + }); + }, + child: const Text('Increment counter'), + ), + const Padding(padding: EdgeInsets.all(8)), + if (widget.param != null) + Text('Parameter: ${widget.param!}', + style: Theme.of(context).textTheme.titleMedium), + const Padding(padding: EdgeInsets.all(8)), + if (widget.extra != null) + Text('Extra: ${widget.extra!}', + style: Theme.of(context).textTheme.titleMedium), + if (!widget.withScaffold) ...[ + const Padding(padding: EdgeInsets.all(16)), + TextButton( + onPressed: () { + GoRouter.of(context).pop(); + }, + child: const Text('< Back', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), + ), + ] + ], + ), + ); + } +} diff --git a/go_router/assets/examples/official/lib/transition_animations.dart b/go_router/assets/examples/official/lib/transition_animations.dart new file mode 100644 index 0000000..f245719 --- /dev/null +++ b/go_router/assets/examples/official/lib/transition_animations.dart @@ -0,0 +1,175 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +/// To use a custom transition animation, provide a `pageBuilder` with a +/// CustomTransitionPage. +/// +/// To learn more about animation in Flutter, check out the [Introduction to +/// animations](https://docs.flutter.dev/development/ui/animations) page on +/// flutter.dev. +void main() => runApp(const MyApp()); + +/// The route configuration. +final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + routes: [ + GoRoute( + path: 'details', + pageBuilder: (BuildContext context, GoRouterState state) { + return CustomTransitionPage( + key: state.pageKey, + child: const DetailsScreen(), + transitionDuration: const Duration(milliseconds: 150), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) { + // Change the opacity of the screen using a Curve based on the the animation's + // value + return FadeTransition( + opacity: + CurveTween(curve: Curves.easeInOut).animate(animation), + child: child, + ); + }, + ); + }, + ), + GoRoute( + path: 'dismissible-details', + pageBuilder: (BuildContext context, GoRouterState state) { + return CustomTransitionPage( + key: state.pageKey, + child: const DismissibleDetails(), + barrierDismissible: true, + barrierColor: Colors.black38, + opaque: false, + transitionDuration: Duration.zero, + transitionsBuilder: (_, __, ___, Widget child) => child, + ); + }, + ), + GoRoute( + path: 'custom-reverse-transition-duration', + pageBuilder: (BuildContext context, GoRouterState state) { + return CustomTransitionPage( + key: state.pageKey, + child: const DetailsScreen(), + barrierDismissible: true, + barrierColor: Colors.black38, + opaque: false, + transitionDuration: const Duration(milliseconds: 500), + reverseTransitionDuration: const Duration(milliseconds: 200), + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + ); + }, + ), + ], + ), + ], +); + +/// The main app. +class MyApp extends StatelessWidget { + /// Constructs a [MyApp] + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + routerConfig: _router, + ); + } +} + +/// The home screen +class HomeScreen extends StatelessWidget { + /// Constructs a [HomeScreen] + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Home Screen')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/details'), + child: const Text('Go to the Details screen'), + ), + const SizedBox(height: 48), + ElevatedButton( + onPressed: () => context.go('/dismissible-details'), + child: const Text('Go to the Dismissible Details screen'), + ), + const SizedBox(height: 48), + ElevatedButton( + onPressed: () => + context.go('/custom-reverse-transition-duration'), + child: const Text( + 'Go to the Custom Reverse Transition Duration Screen', + ), + ), + ], + ), + ), + ); + } +} + +/// The details screen +class DetailsScreen extends StatelessWidget { + /// Constructs a [DetailsScreen] + const DetailsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Details Screen')), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Go back to the Home screen'), + ), + ], + ), + ), + ); + } +} + +/// The dismissible details screen +class DismissibleDetails extends StatelessWidget { + /// Constructs a [DismissibleDetails] + const DismissibleDetails({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.all(48), + child: ColoredBox(color: Colors.red), + ); + } +} diff --git a/go_router/assets/examples/official/pubspec.yaml b/go_router/assets/examples/official/pubspec.yaml new file mode 100644 index 0000000..eb4fd43 --- /dev/null +++ b/go_router/assets/examples/official/pubspec.yaml @@ -0,0 +1,28 @@ +name: go_router_examples +description: go_router examples +version: 3.0.1 +publish_to: none + +environment: + sdk: ^3.2.0 + flutter: ">=3.16.0" + +dependencies: + adaptive_navigation: ^0.0.4 + collection: ^1.15.0 + cupertino_icons: ^1.0.2 + flutter: + sdk: flutter + go_router: + path: .. + logging: ^1.0.0 + provider: 6.0.5 + shared_preferences: ^2.0.11 + url_launcher: ^6.0.7 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/go_router/assets/examples/official/test/exception_handling_test.dart b/go_router/assets/examples/official/test/exception_handling_test.dart new file mode 100644 index 0000000..68a7310 --- /dev/null +++ b/go_router/assets/examples/official/test/exception_handling_test.dart @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_examples/exception_handling.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(const example.MyApp()); + expect(find.text('Simulates user entering unknown url'), findsOneWidget); + + await tester.tap(find.text('Simulates user entering unknown url')); + await tester.pumpAndSettle(); + expect(find.text("Can't find a page for: /some-unknown-route"), + findsOneWidget); + }); +} diff --git a/go_router/assets/examples/official/test/extra_codec_test.dart b/go_router/assets/examples/official/test/extra_codec_test.dart new file mode 100644 index 0000000..e42b19d --- /dev/null +++ b/go_router/assets/examples/official/test/extra_codec_test.dart @@ -0,0 +1,39 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_examples/extra_codec.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(const example.MyApp()); + expect(find.text('The extra for this page is: null'), findsOneWidget); + + await tester.tap(find.text('Set extra to ComplexData1')); + await tester.pumpAndSettle(); + expect(find.text('The extra for this page is: ComplexData1(data: data)'), + findsOneWidget); + + await tester.tap(find.text('Set extra to ComplexData2')); + await tester.pumpAndSettle(); + expect(find.text('The extra for this page is: ComplexData2(data: data)'), + findsOneWidget); + }); + + test('invalid extra throws', () { + const example.MyExtraCodec extraCodec = example.MyExtraCodec(); + const List invalidValue = ['invalid']; + + expect( + () => extraCodec.decode(invalidValue), + throwsA( + predicate( + (Object? exception) => + exception is FormatException && + exception.message == 'Unable to parse input: $invalidValue', + ), + ), + ); + }); +} diff --git a/go_router/assets/examples/official/test/main_test.dart b/go_router/assets/examples/official/test/main_test.dart new file mode 100644 index 0000000..9cf8d5e --- /dev/null +++ b/go_router/assets/examples/official/test/main_test.dart @@ -0,0 +1,21 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_examples/main.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(const example.MyApp()); + expect(find.text('Go to the Details screen'), findsOneWidget); + + await tester.tap(find.text('Go to the Details screen')); + await tester.pumpAndSettle(); + expect(find.text('Go back to the Home screen'), findsOneWidget); + + await tester.tap(find.text('Go back to the Home screen')); + await tester.pumpAndSettle(); + expect(find.text('Go to the Details screen'), findsOneWidget); + }); +} diff --git a/go_router/assets/examples/official/test/on_exit_test.dart b/go_router/assets/examples/official/test/on_exit_test.dart new file mode 100644 index 0000000..86659d9 --- /dev/null +++ b/go_router/assets/examples/official/test/on_exit_test.dart @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_examples/on_exit.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(const example.MyApp()); + + await tester.tap(find.text('Go to the Details screen')); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(BackButton)); + await tester.pumpAndSettle(); + + expect(find.text('Are you sure to leave this page?'), findsOneWidget); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + expect(find.byType(example.DetailsScreen), findsOneWidget); + + await tester.tap(find.byType(BackButton)); + await tester.pumpAndSettle(); + + expect(find.text('Are you sure to leave this page?'), findsOneWidget); + await tester.tap(find.text('Confirm')); + await tester.pumpAndSettle(); + expect(find.byType(example.HomeScreen), findsOneWidget); + }); +} diff --git a/go_router/assets/examples/official/test/path_and_query_params_test.dart b/go_router/assets/examples/official/test/path_and_query_params_test.dart new file mode 100644 index 0000000..e4af48b --- /dev/null +++ b/go_router/assets/examples/official/test/path_and_query_params_test.dart @@ -0,0 +1,47 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_examples/path_and_query_parameters.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(example.App()); + expect(find.text(example.App.title), findsOneWidget); + + // Directly set the url through platform message. + Map testRouteInformation = { + 'location': '/family/f1?sort=asc', + }; + ByteData message = const JSONMethodCodec().encodeMethodCall( + MethodCall('pushRouteInformation', testRouteInformation), + ); + await tester.binding.defaultBinaryMessenger + .handlePlatformMessage('flutter/navigation', message, (_) {}); + + await tester.pumpAndSettle(); + // 'Chris' should be higher than 'Tom'. + expect( + tester.getCenter(find.text('Jane')).dy < + tester.getCenter(find.text('John')).dy, + isTrue); + + testRouteInformation = { + 'location': '/family/f1?privacy=false', + }; + message = const JSONMethodCodec().encodeMethodCall( + MethodCall('pushRouteInformation', testRouteInformation), + ); + await tester.binding.defaultBinaryMessenger + .handlePlatformMessage('flutter/navigation', message, (_) {}); + + await tester.pumpAndSettle(); + // 'Chris' should be lower than 'Tom'. + expect( + tester.getCenter(find.text('Jane')).dy > + tester.getCenter(find.text('John')).dy, + isTrue); + }); +} diff --git a/go_router/assets/examples/official/test/push_with_shell_route_test.dart b/go_router/assets/examples/official/test/push_with_shell_route_test.dart new file mode 100644 index 0000000..86d6714 --- /dev/null +++ b/go_router/assets/examples/official/test/push_with_shell_route_test.dart @@ -0,0 +1,45 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; +import 'package:go_router_examples/push_with_shell_route.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(example.PushWithShellRouteExampleApp()); + expect(find.text('shell1'), findsOneWidget); + + await tester.tap(find.text('push the same shell route /shell1')); + await tester.pumpAndSettle(); + expect(find.text('shell1'), findsOneWidget); + expect(find.text('shell1 body'), findsOneWidget); + + find.text('shell1 body').evaluate().first.pop(); + await tester.pumpAndSettle(); + expect(find.text('shell1'), findsOneWidget); + expect(find.text('shell1 body'), findsNothing); + + await tester.tap(find.text('push the different shell route /shell2')); + await tester.pumpAndSettle(); + expect(find.text('shell1'), findsNothing); + expect(find.text('shell2'), findsOneWidget); + expect(find.text('shell2 body'), findsOneWidget); + + find.text('shell2 body').evaluate().first.pop(); + await tester.pumpAndSettle(); + expect(find.text('shell1'), findsOneWidget); + expect(find.text('shell2'), findsNothing); + + await tester.tap(find.text('push the regular route /regular-route')); + await tester.pumpAndSettle(); + expect(find.text('shell1'), findsNothing); + expect(find.text('regular route'), findsOneWidget); + + find.text('regular route').evaluate().first.pop(); + await tester.pumpAndSettle(); + expect(find.text('shell1'), findsOneWidget); + expect(find.text('regular route'), findsNothing); + }); +} diff --git a/go_router/assets/examples/official/test/redirection_test.dart b/go_router/assets/examples/official/test/redirection_test.dart new file mode 100644 index 0000000..2d0e286 --- /dev/null +++ b/go_router/assets/examples/official/test/redirection_test.dart @@ -0,0 +1,52 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_examples/redirection.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(example.App()); + expect(find.text('Login'), findsOneWidget); + + // Directly set the url to the home page. + Map testRouteInformation = { + 'location': '/', + }; + ByteData message = const JSONMethodCodec().encodeMethodCall( + MethodCall('pushRouteInformation', testRouteInformation), + ); + await tester.binding.defaultBinaryMessenger + .handlePlatformMessage('flutter/navigation', message, (_) {}); + + await tester.pumpAndSettle(); + // Still show login page due to redirection + expect(find.text('Login'), findsOneWidget); + + await tester.tap(find.text('Login')); + await tester.pumpAndSettle(); + expect(find.text('HomeScreen'), findsOneWidget); + + testRouteInformation = { + 'location': '/login', + }; + message = const JSONMethodCodec().encodeMethodCall( + MethodCall('pushRouteInformation', testRouteInformation), + ); + await tester.binding.defaultBinaryMessenger + .handlePlatformMessage('flutter/navigation', message, (_) {}); + + await tester.pumpAndSettle(); + // Got redirected back to home page. + expect(find.text('HomeScreen'), findsOneWidget); + + // Tap logout. + await tester.tap(find.byType(IconButton)); + await tester.pumpAndSettle(); + + expect(find.text('Login'), findsOneWidget); + }); +} diff --git a/go_router/assets/examples/official/test/routing_config_test.dart b/go_router/assets/examples/official/test/routing_config_test.dart new file mode 100644 index 0000000..bd288e3 --- /dev/null +++ b/go_router/assets/examples/official/test/routing_config_test.dart @@ -0,0 +1,31 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_examples/routing_config.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(const example.MyApp()); + expect(find.text('Add a new route'), findsOneWidget); + + await tester.tap(find.text('Try going to /new-route')); + await tester.pumpAndSettle(); + + expect(find.text('Page not found'), findsOneWidget); + + await tester.tap(find.text('Go to home')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Add a new route')); + await tester.pumpAndSettle(); + + expect(find.text('A route has been added'), findsOneWidget); + + await tester.tap(find.text('Try going to /new-route')); + await tester.pumpAndSettle(); + + expect(find.text('A new Route'), findsOneWidget); + }); +} diff --git a/go_router/assets/examples/official/test/shell_route_test.dart b/go_router/assets/examples/official/test/shell_route_test.dart new file mode 100644 index 0000000..9327189 --- /dev/null +++ b/go_router/assets/examples/official/test/shell_route_test.dart @@ -0,0 +1,42 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_examples/shell_route.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(example.ShellRouteExampleApp()); + expect(find.text('Screen A'), findsOneWidget); + // navigate to a/details + await tester.tap(find.text('View A details')); + await tester.pumpAndSettle(); + expect(find.text('Details for A'), findsOneWidget); + + // navigate to ScreenB + await tester.tap(find.text('B Screen')); + await tester.pumpAndSettle(); + expect(find.text('Screen B'), findsOneWidget); + + // navigate to b/details + await tester.tap(find.text('View B details')); + await tester.pumpAndSettle(); + expect(find.text('Details for B'), findsOneWidget); + + // back to ScreenB. + await tester.tap(find.byType(BackButton)); + await tester.pumpAndSettle(); + + // navigate to ScreenC + await tester.tap(find.text('C Screen')); + await tester.pumpAndSettle(); + expect(find.text('Screen C'), findsOneWidget); + + // navigate to c/details + await tester.tap(find.text('View C details')); + await tester.pumpAndSettle(); + expect(find.text('Details for C'), findsOneWidget); + }); +} diff --git a/go_router/assets/examples/official/test/transition_animations_test.dart b/go_router/assets/examples/official/test/transition_animations_test.dart new file mode 100644 index 0000000..107ac26 --- /dev/null +++ b/go_router/assets/examples/official/test/transition_animations_test.dart @@ -0,0 +1,21 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_examples/transition_animations.dart' as example; + +void main() { + testWidgets('example works', (WidgetTester tester) async { + await tester.pumpWidget(const example.MyApp()); + expect(find.text('Go to the Details screen'), findsOneWidget); + + await tester.tap(find.text('Go to the Details screen')); + await tester.pumpAndSettle(); + expect(find.text('Go back to the Home screen'), findsOneWidget); + + await tester.tap(find.text('Go back to the Home screen')); + await tester.pumpAndSettle(); + expect(find.text('Go to the Details screen'), findsOneWidget); + }); +} diff --git a/go_router/bin/main.dart b/go_router/bin/main.dart new file mode 100644 index 0000000..8b3fa15 --- /dev/null +++ b/go_router/bin/main.dart @@ -0,0 +1,10 @@ +import 'package:dash_agent/dash_agent.dart'; +import 'package:go_router/agent.dart'; +import 'package:go_router/data_source/github_issues_data_source_helper.dart'; + +Future main() async { + final openIssues = await generateGitIssuesLink(); + final closedIssues = await generateGitIssuesLink(closedIssues: true); + await processAgent( + GoRouterAgent(issuesLinks: [...closedIssues, ...openIssues])); +} diff --git a/go_router/lib/agent.dart b/go_router/lib/agent.dart new file mode 100644 index 0000000..24edade --- /dev/null +++ b/go_router/lib/agent.dart @@ -0,0 +1,50 @@ +import 'package:dash_agent/configuration/metadata.dart'; +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/configuration/dash_agent.dart'; +import 'package:go_router/commands/debug.dart'; +import 'package:go_router/commands/generate.dart'; +import 'package:go_router/commands/test.dart'; +import 'commands/ask.dart'; +import 'data_source/data_sources.dart'; + +class GoRouterAgent extends AgentConfiguration { + final List issuesLinks; + final IssuesDataSource issueSource; + final docsSource = DocsDataSource(); + final exampleSource = ExampleDataSource(); + final testExampleSource = TestExampleDataSource(); + + GoRouterAgent({required this.issuesLinks}) + : issueSource = IssuesDataSource(issuesLinks); + + @override + List get registerDataSources => + [docsSource, exampleSource, issueSource, testExampleSource]; + + @override + List get registerSupportedCommands => [ + AskCommand( + docsSource: docsSource, + exampleDataSource: exampleSource, + issuesDataSource: issueSource), + GenerateCommand( + exampleDataSource: exampleSource, docDataSource: docsSource), + DebugCommand( + exampleDataSource: exampleSource, + docDataSource: docsSource, + issueDataSource: issueSource), + TestCommand(testExampleSource) + ]; + + @override + Metadata get metadata => Metadata( + name: 'GoRouter', + tags: ['flutter', 'dart', 'flutter package', 'flutter favorite']); + + @override + String get registerSystemPrompt => + '''You are a GoRouter Integration assistant inside user's IDE. GoRouter is a declarative routing package for Flutter that uses the Router API to provide a convenient, url-based API for navigating between different screens. + + You will be provided with latest docs and examples relevant to user queries and you have to help user with any questions they have related to GoRouter. Output code and code links wherever required and answer "I don't know" if the user query is not covered in the docs provided to you'''; +} diff --git a/go_router/lib/commands/ask.dart b/go_router/lib/commands/ask.dart new file mode 100644 index 0000000..701146f --- /dev/null +++ b/go_router/lib/commands/ask.dart @@ -0,0 +1,65 @@ +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/steps/steps.dart'; +import 'package:dash_agent/variables/dash_input.dart'; +import 'package:dash_agent/variables/dash_output.dart'; + +class AskCommand extends Command { + AskCommand( + {required this.docsSource, + required this.exampleDataSource, + required this.issuesDataSource}); + + final DataSource docsSource; + final DataSource exampleDataSource; + final DataSource issuesDataSource; + + /// Inputs to be provided by the user in the text field + final userQuery = StringInput('Query', optional: false); + final codeAttachment = CodeInput('Code'); + + @override + String get slug => 'ask'; + + @override + String get intent => 'Ask me anything'; + + @override + String get textFieldLayout => + "Hi, I'm here to help you. Please let know your $userQuery and any optionally any relevant code: $codeAttachment"; + + @override + List get registerInputs => [userQuery, codeAttachment]; + + @override + List get steps { + final matchingDocuments = MatchDocumentOuput(); + final promptOutput = PromptOutput(); + + return [ + MatchDocumentStep( + query: '$userQuery$codeAttachment', + dataSources: [docsSource, exampleDataSource, issuesDataSource], + output: matchingDocuments), + PromptQueryStep( + prompt: + '''You are tasked with answering any query related to go_router. Please find the below shared details: + + Query: $userQuery, + + Code Attachment: + $codeAttachment + + References: + $matchingDocuments. + + Note: + 1. If the references don't address the question, state that "I couldn't fetch your answer from the doc sources, but I'll try to answer from my own knowledge". + 2. Be truthful, complete and detailed with your responses and include code snippets wherever required. + Now, answer the user's query.''', + promptOutput: promptOutput, + ), + AppendToChatStep(value: '$promptOutput') + ]; + } +} diff --git a/go_router/lib/commands/debug.dart b/go_router/lib/commands/debug.dart new file mode 100644 index 0000000..0f5bc17 --- /dev/null +++ b/go_router/lib/commands/debug.dart @@ -0,0 +1,71 @@ +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/steps/steps.dart'; +import 'package:dash_agent/variables/dash_input.dart'; +import 'package:dash_agent/variables/dash_output.dart'; + +class DebugCommand extends Command { + DebugCommand( + {required this.exampleDataSource, + required this.docDataSource, + required this.issueDataSource}); + final DataSource exampleDataSource; + final DataSource docDataSource; + final DataSource issueDataSource; + final issueDescription = StringInput('Issue Description'); + final codeReference1 = CodeInput('Reference', optional: true); + final codeReference2 = CodeInput('Reference', optional: true); + + @override + String get intent => 'Debug go_router code'; + + @override + List get registerInputs => + [issueDescription, codeReference1, codeReference2]; + + @override + String get slug => 'debug'; + + @override + List get steps { + final docReferences = MatchDocumentOuput(); + final issueReferences = MatchDocumentOuput(); + final resultOutput = PromptOutput(); + return [ + MatchDocumentStep( + query: '$issueDescription', + dataSources: [exampleDataSource, docDataSource], + output: docReferences), + MatchDocumentStep( + query: '$issueDescription', + dataSources: [issueDataSource], + output: issueReferences), + PromptQueryStep( + prompt: + '''Assist in debugging the code written using Flutter's go_router package based on the information shared. + Issue Description: $issueDescription + + Code References: + ```dart + // code reference 1 + $codeReference1 + + // code reference 2 + $codeReference2 + ``` + + Official Chopper Documentation References: + $docReferences + + GitHub Issue Messages: + $issueReferences + ''', + promptOutput: resultOutput), + AppendToChatStep(value: '$resultOutput') + ]; + } + + @override + String get textFieldLayout => + 'Share the following details to understand your issue better: $issueDescription \n\nOptionally attach any references: $codeReference1 $codeReference2'; +} diff --git a/go_router/lib/commands/generate.dart b/go_router/lib/commands/generate.dart new file mode 100644 index 0000000..407eadb --- /dev/null +++ b/go_router/lib/commands/generate.dart @@ -0,0 +1,107 @@ +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/steps/steps.dart'; +import 'package:dash_agent/variables/dash_input.dart'; +import 'package:dash_agent/variables/dash_output.dart'; + +class GenerateCommand extends Command { + GenerateCommand( + {required this.exampleDataSource, required this.docDataSource}); + final DataSource exampleDataSource; + final DataSource docDataSource; + final generateInstructions = StringInput('Instructions'); + final codeReference1 = CodeInput('Reference', optional: true); + final codeReference2 = CodeInput('Reference', optional: true); + + @override + String get intent => 'Generate navigation related code using go_router based on request'; + + @override + List get registerInputs => + [generateInstructions, codeReference1, codeReference2]; + + @override + String get slug => 'generate'; + + @override + List get steps { + final docReferences = MatchDocumentOuput(); + final codeReferences = MatchDocumentOuput(); + final filteredReferences = PromptOutput(); + final resultOutput = PromptOutput(); + return [ + MatchDocumentStep( + query: + 'examples/instructions of writing code using go_router - $generateInstructions $codeReference1 $codeReference2.', + dataSources: [docDataSource], + output: docReferences), + MatchDocumentStep( + query: + 'examples/instructions of writing code using go_router - $generateInstructions $codeReference1 $codeReference2.', + dataSources: [exampleDataSource], + output: codeReferences), + PromptQueryStep( + prompt: + '''You are tasked with finding the at most top 3 most relevant references from the shared input refereces for a specific query. Your goal is to provide a concise list of references out of the shared references in Markdown format. + +Query: examples/instructions of writing code using go_router - $generateInstructions $codeReference1 $codeReference2. + +Input References: +## Doc References +$docReferences + +## Code Refrences +$codeReferences + +Instructions: +1. Read through the provided inpur references and identify the most relevant references that are pertinent to the given query. +2. For each relevant reference, provide the following information in Markdown format: + - Brief Summary describing relevance to the given query + - Reference content + +Example input: +Query: Latest version of go_router + +Input Refernces: +This article introduces go_router, a new Flutter router package designed specifically for web applications. +This blog post announces the release of go_router version 2.0, highlighting the new features and improvements. +This guide explains asynchronous programming concepts in Dart, including Futures, Streams, and async/await syntax. + +Example Output: + +- **Reference Title:** "Blog on go_router v2.0 Released: What's New?" +- **Reference Content**: This blog post announces the release of go_router version 2.0, highlighting the new features and improvements. + +Please provide the list of relevant references in the specified Markdown format.''', + promptOutput: filteredReferences), + PromptQueryStep( + prompt: + '''You are tasked with generating Flutter code to implement navigation in a flutter app using the go_router package. Your goal is to create code snippets that demonstrate how to define routes, navigate between screens, and handle route parameters using go_router based on the instructions and relevant code references/examples shared. + Instructions: $generateInstructions + + Code References: + ```dart + // code reference 1 + $codeReference1 + + // code reference 2 + $codeReference2 + ``` + + Documentation or examples of the chopper package for reference: + $filteredReferences + + + Deliverables: + 1. Generated Flutter code demonstrating navigation using the go_router package. + 2. Documentation outlining the process of generating the code, including explanations and usage examples. + ''', + promptOutput: resultOutput), + AppendToChatStep(value: '$resultOutput') + ]; + } + + @override + String get textFieldLayout => + 'Generate the code using go_router: $generateInstructions \n\nOptionally attach any references: $codeReference1 $codeReference2'; +} \ No newline at end of file diff --git a/go_router/lib/commands/test.dart b/go_router/lib/commands/test.dart new file mode 100644 index 0000000..76b4b96 --- /dev/null +++ b/go_router/lib/commands/test.dart @@ -0,0 +1,108 @@ +import 'package:dash_agent/configuration/command.dart'; +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/steps/steps.dart'; +import 'package:dash_agent/variables/dash_input.dart'; +import 'package:dash_agent/variables/dash_output.dart'; + +class TestCommand extends Command { + TestCommand(this.testDataSource); + final DataSource testDataSource; + final primaryObject = CodeInput('Test Object'); + final testInstructions = StringInput('Instructions', optional: true); + final referenceObject1 = CodeInput('Reference', optional: true); + final referenceObject2 = CodeInput('Reference', optional: true); + final referenceObject3 = CodeInput('Reference', optional: true); + @override + String get intent => 'Write tests for your navigation related code using go_router'; + + @override + List get registerInputs => [ + primaryObject, + testInstructions, + referenceObject1, + referenceObject2, + referenceObject3 + ]; + + @override + String get slug => 'test'; + + @override + String get textFieldLayout => + 'Generate test for your go_router-related code $primaryObject with $testInstructions\n\nOptionally attach any supporting code: $referenceObject1 $referenceObject2 $referenceObject3'; + + @override + List get steps { + final testOutput = PromptOutput(); + final testReferences = MatchDocumentOuput(); + return [ + MatchDocumentStep( + query: + 'examples/instructions of writing tests for go_router code - $testInstructions $primaryObject.', + dataSources: [testDataSource], + output: testReferences), + PromptQueryStep( + prompt: + '''You are tasked with testing the navigation functionality implemented using the go_router package in a Flutter app. Your goal is to ensure that the navigation flows smoothly, routes are correctly defined, and route parameters are handled accurately. + +Write tests for the Flutter go_router related code with instructions - $testInstructions. + +Code: +```dart +$primaryObject +``` + +Here are some contextual code or references provided by the user: +```dart +// reference 1 +$referenceObject1 + +// reference 2 +$referenceObject2 + +// reference 3 +$referenceObject3 +``` + +Few sample Unit tests unrelated to the above scenerio as a reference: + +```dart +$testReferences +``` + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router_examples/on_exit.dart' as example; + +void main() { + testWidgets('testing on_exit navigation', (WidgetTester tester) async { + await tester.pumpWidget(const example.MyApp()); + + await tester.tap(find.text('Go to the Details screen')); + await tester.pumpAndSettle(); + + await tester.tap(find.byType(BackButton)); + await tester.pumpAndSettle(); + + expect(find.text('Are you sure to leave this page?'), findsOneWidget); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + expect(find.byType(example.DetailsScreen), findsOneWidget); + + await tester.tap(find.byType(BackButton)); + await tester.pumpAndSettle(); + + expect(find.text('Are you sure to leave this page?'), findsOneWidget); + await tester.tap(find.text('Confirm')); + await tester.pumpAndSettle(); + expect(find.byType(example.HomeScreen), findsOneWidget); + }); +} +``` +''', + promptOutput: testOutput), + AppendToChatStep(value: '$testOutput') + ]; + } +} diff --git a/go_router/lib/data_source/data_sources.dart b/go_router/lib/data_source/data_sources.dart new file mode 100644 index 0000000..d67656b --- /dev/null +++ b/go_router/lib/data_source/data_sources.dart @@ -0,0 +1,77 @@ +import 'dart:io'; + +import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/data/objects/project_data_object.dart'; +import 'package:dash_agent/data/objects/file_data_object.dart'; +import 'package:dash_agent/data/objects/web_data_object.dart'; +import 'package:go_router/data_source/sitemap_url_fetcher.dart'; + +/// [DocsDataSource] indexes all the documentation related data and provides it to commands. +class DocsDataSource extends DataSource { + @override + List get fileObjects => []; + + @override + List get projectObjects => []; + + @override + List get webObjects => [ + for (final docUrl + in sitemapUrlFetcher('assets/documents/go_router_doc.xml')) + WebDataObject.fromWebPage(docUrl) + ]; +} + +/// [ExampleDataSource] indexes all the examples related data and provides it to commands. +class ExampleDataSource extends DataSource { + @override + List get fileObjects => [ + FileDataObject.fromDirectory(Directory('assets/examples/official/lib'), + includePaths: true, + regex: RegExp(r'(\.dart$)'), + relativeTo: + '/Users/yogesh/Development/org.welltested/default_agents/go_router/assets/examples') + ]; + + @override + List get projectObjects => []; + + @override + List get webObjects => []; +} + +/// [IssuesDataSource] indexes all the issues and their solutions related data and provides it to commands. +class IssuesDataSource extends DataSource { + final List issuesLinks; + + IssuesDataSource(this.issuesLinks); + + @override + List get fileObjects => []; + + @override + List get projectObjects => []; + + @override + List get webObjects => + [for (final issueUrl in issuesLinks) WebDataObject.fromWebPage(issueUrl)]; +} + + +/// [TestExampleDataSource] indexes all the test examples related data and provides it to commands. +class TestExampleDataSource extends DataSource { + @override + List get fileObjects => [ + FileDataObject.fromDirectory(Directory('assets/examples/official/test'), + includePaths: true, + regex: RegExp(r'(\.dart$)'), + relativeTo: + '/Users/yogesh/Development/org.welltested/default_agents/go_router/assets/examples') + ]; + + @override + List get projectObjects => []; + + @override + List get webObjects => []; +} diff --git a/go_router/lib/data_source/github_issues_data_source_helper.dart b/go_router/lib/data_source/github_issues_data_source_helper.dart new file mode 100644 index 0000000..dac117a --- /dev/null +++ b/go_router/lib/data_source/github_issues_data_source_helper.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +/// Generates a list of links to GitHub issues for a given repository and label. +/// +/// The [closedIssues] parameter determines whether to include closed issues in the list. +/// The default value is `false`. +Future> generateGitIssuesLink({bool closedIssues = false}) async { + String? issueApiUrl = + 'https://api.github.com/repos/flutter/flutter/issues?labels=p: go_router'; + + /// The maximum number of issues to retrieve. + final issueLimit = closedIssues ? 150 : 50; + + /// The list of issue URLs. + final issueUrls = []; + + if (closedIssues) { + issueApiUrl = '$issueApiUrl&state=closed'; + } + + // Loop through the pages of results. + while (issueApiUrl != null && issueLimit > issueUrls.length) { + final response = await http.get(Uri.parse(issueApiUrl)); + + if (response.statusCode == 200) { + final responseBody = jsonDecode(response.body).cast(); + + for (final issue in responseBody) { + issueUrls.add(issue['html_url'] as String); + } + + if (response.headers.containsKey('link')) { + String links = response.headers['link']!; + + /// If the `Link` header contains a `rel="next"` link, extract the URL. + if (links.contains('rel="next"')) { + RegExp regExp = RegExp(r'<(.+?)>'); + issueApiUrl = regExp.firstMatch(links)!.group(1); + } else { + issueApiUrl = null; + } + } else { + issueApiUrl = null; + } + } + } + + return issueUrls; +} diff --git a/go_router/lib/data_source/sitemap_url_fetcher.dart b/go_router/lib/data_source/sitemap_url_fetcher.dart new file mode 100644 index 0000000..332ec1a --- /dev/null +++ b/go_router/lib/data_source/sitemap_url_fetcher.dart @@ -0,0 +1,30 @@ +import 'dart:io'; +import 'package:xml/xml.dart' as xml; + +List sitemapUrlFetcher(String filePath) { + // Read the contents of the file + File file = File(filePath); + if (!file.existsSync()) { + throw 'File not found: $filePath'; + } + + String contents = file.readAsStringSync(); + + // Parse the XML + var document = xml.XmlDocument.parse(contents); + + // Find all URL elements + var urlElements = document.findAllElements('url'); + + // Extract + List urls = []; + for (var urlElement in urlElements) { + var locElement = urlElement.findElements('loc').first; + var url = locElement.value; + if (url != null) { + urls.add(url); + } + } + + return urls; +} diff --git a/go_router/pubspec.lock b/go_router/pubspec.lock new file mode 100644 index 0000000..bd6fe8e --- /dev/null +++ b/go_router/pubspec.lock @@ -0,0 +1,413 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + url: "https://pub.dev" + source: hosted + version: "1.7.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + dash_agent: + dependency: "direct main" + description: + name: dash_agent + sha256: "5f647003933a979768cad1453c2a1401a2ec3f844e94ed9ba4eb57e6cdd23be9" + url: "https://pub.dev" + source: hosted + version: "0.3.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + http: + dependency: "direct main" + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + lints: + dependency: "direct dev" + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + meta: + dependency: transitive + description: + name: meta + sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" + url: "https://pub.dev" + source: hosted + version: "1.14.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: d72b538180efcf8413cd2e4e6fcc7ae99c7712e0909eb9223f9da6e6d0ef715f + url: "https://pub.dev" + source: hosted + version: "1.25.4" + test_api: + dependency: transitive + description: + name: test_api + sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" + url: "https://pub.dev" + source: hosted + version: "0.7.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: "4d070a6bc36c1c4e89f20d353bfd71dc30cdf2bd0e14349090af360a029ab292" + url: "https://pub.dev" + source: hosted + version: "0.6.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + url: "https://pub.dev" + source: hosted + version: "14.2.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + xml: + dependency: "direct main" + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.3.4 <4.0.0" diff --git a/go_router/pubspec.yaml b/go_router/pubspec.yaml new file mode 100644 index 0000000..f8fe44a --- /dev/null +++ b/go_router/pubspec.yaml @@ -0,0 +1,18 @@ +name: go_router +description: A sample command-line application. +version: 1.1.0 +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.3.4 + +# Add regular dependencies here. +dependencies: + dash_agent: ^0.3.0 + http: ^1.2.1 + xml: ^6.5.0 + # path: ^1.8.0 + +dev_dependencies: + lints: ^3.0.0 + test: ^1.24.0 diff --git a/pub/lib/data_source/data_sources.dart b/pub/lib/data_source/data_sources.dart index bd1a5d6..608abfd 100644 --- a/pub/lib/data_source/data_sources.dart +++ b/pub/lib/data_source/data_sources.dart @@ -1,6 +1,6 @@ import 'package:dash_agent/data/datasource.dart'; +import 'package:dash_agent/data/objects/file_data_object.dart'; import 'package:dash_agent/data/objects/project_data_object.dart'; -import 'package:dash_agent/data/objects/system_data_object.dart'; import 'package:dash_agent/data/objects/web_data_object.dart'; class PubDataSource extends DataSource { diff --git a/pub/pubspec.lock b/pub/pubspec.lock index 9ac4501..1af07cf 100644 --- a/pub/pubspec.lock +++ b/pub/pubspec.lock @@ -41,14 +41,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - characters: - dependency: transitive - description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.dev" - source: hosted - version: "1.3.0" collection: dependency: transitive description: @@ -96,11 +88,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - flutter: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -181,14 +168,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.12.16+1" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" - url: "https://pub.dev" - source: hosted - version: "0.8.0" meta: dependency: transitive description: @@ -277,11 +256,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" source_map_stack_trace: dependency: transitive description: @@ -370,14 +344,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" vm_service: dependency: transitive description: @@ -428,4 +394,3 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.3 <4.0.0" - flutter: ">=1.17.0"