├── .circleci └── config.yml ├── .gitignore ├── .gitmodules ├── .metadata ├── LICENSE ├── README.md ├── analysis_options.yaml ├── android ├── app │ ├── build.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ └── dev │ │ │ └── flutter │ │ │ └── devrpg │ │ │ └── MainActivity.kt │ │ └── res │ │ ├── drawable │ │ └── launch_background.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── artwork └── speech bubble.pxm ├── assets ├── docs │ ├── code_chomper_alpha.dart │ ├── code_chomper_beta.dart │ ├── dev-rpg-research-tree.graphml │ └── research-tree.png ├── flare │ ├── Chomper FUI Type.flr │ ├── Chomper.flr │ ├── CodeIcon.flr │ ├── Coin.flr │ ├── CoordinationIcon.flr │ ├── CowboyCoder.flr │ ├── Designer.flr │ ├── EngineeringIcon.flr │ ├── Joy.flr │ ├── NotificationIcon.flr │ ├── ProgramManager.flr │ ├── SelectArrow.flr │ ├── Sourcerer.flr │ ├── Stars.flr │ ├── TasksIcon.flr │ ├── TeamIcon.flr │ ├── Tester.flr │ ├── TheArchitect.flr │ ├── TheHacker.flr │ ├── TheJack.flr │ ├── TheRefactorer.flr │ ├── UXResearcher.flr │ ├── Users.flr │ └── UxIcon.flr ├── fonts │ ├── Gotham XNarrow Medium.otf │ ├── Montserrat-Bold.otf │ ├── Montserrat-Medium.otf │ ├── Montserrat-Regular.otf │ ├── Roboto-Regular.ttf │ ├── RobotoCondensed-Bold.ttf │ ├── SpaceMono-Bold.ttf │ └── SpaceMono-Regular.ttf ├── images │ ├── 2.0x │ │ ├── 2dimensions.png │ │ ├── 2dimensions_wide.png │ │ └── flare_logo.png │ ├── 2dimensions.png │ ├── 2dimensions_wide.png │ ├── 3.0x │ │ ├── 2dimensions.png │ │ ├── 2dimensions_wide.png │ │ └── flare_logo.png │ ├── flare_logo.png │ └── flutter_logo.png └── style_sphinx │ ├── green_bed.png │ ├── orange_cat.png │ ├── pyramid.png │ ├── red_bed.png │ ├── sphinx.png │ ├── sphinx_no_glasses.png │ ├── start_background.png │ ├── sunglasses.png │ └── yellow_cat.png ├── ios ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon.png │ │ ├── icon_20pt.png │ │ ├── icon_20pt@2x.png │ │ ├── icon_20pt@3x.png │ │ ├── icon_29pt.png │ │ ├── icon_29pt@2x.png │ │ ├── icon_29pt@3x.png │ │ ├── icon_40pt.png │ │ ├── icon_40pt@2x.png │ │ ├── icon_40pt@3x.png │ │ ├── icon_60pt@2x.png │ │ ├── icon_60pt@3x.png │ │ ├── icon_76pt.png │ │ ├── icon_76pt@2x.png │ │ └── icon_83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── Info.plist │ └── Runner-Bridging-Header.h ├── lib ├── main.dart └── src │ ├── about_screen.dart │ ├── code_chomper │ ├── code_chomper.dart │ └── code_chomper_screen.dart │ ├── game_screen.dart │ ├── game_screen │ ├── add_task_button.dart │ ├── bug_picker_modal.dart │ ├── character_modal.dart │ ├── character_pool_page.dart │ ├── character_style.dart │ ├── project_picker_modal.dart │ ├── task_pool_page.dart │ └── team_picker_modal.dart │ ├── game_screen_slim.dart │ ├── game_screen_wide.dart │ ├── rpg_layout_builder.dart │ ├── shared_state │ ├── game │ │ ├── bug.dart │ │ ├── character.dart │ │ ├── character_pool.dart │ │ ├── company.dart │ │ ├── skill.dart │ │ ├── src │ │ │ ├── aspect.dart │ │ │ ├── aspect_container.dart │ │ │ └── child_aspect.dart │ │ ├── task.dart │ │ ├── task_blueprint.dart │ │ ├── task_pool.dart │ │ ├── task_prerequisite.dart │ │ ├── task_tree │ │ │ ├── animations.dart │ │ │ ├── backend_infrastructure.dart │ │ │ ├── beta.dart │ │ │ ├── design.dart │ │ │ ├── geolocation.dart │ │ │ ├── image_messaging.dart │ │ │ ├── launch.dart │ │ │ ├── natural_language.dart │ │ │ ├── pre_alpha.dart │ │ │ ├── pre_launch.dart │ │ │ ├── responsive_design.dart │ │ │ ├── task_tree.dart │ │ │ ├── theme.dart │ │ │ ├── tree_hierarchy.dart │ │ │ └── ux_testing.dart │ │ ├── work_item.dart │ │ └── world.dart │ └── user.dart │ ├── style.dart │ ├── style_sphinx │ ├── axis_questions.dart │ ├── breathing_animations.dart │ ├── flex_questions.dart │ ├── kittens.dart │ ├── question_arguments.dart │ ├── question_scaffold.dart │ ├── sphinx_buttton.dart │ ├── sphinx_image.dart │ ├── sphinx_screen.dart │ ├── success_route.dart │ └── text_bubble.dart │ ├── welcome_screen.dart │ └── widgets │ ├── app_bar │ ├── coin_badge.dart │ ├── joy_badge.dart │ ├── stat_badge.dart │ ├── stat_separator.dart │ └── users_badge.dart │ ├── buttons │ ├── welcome_button.dart │ └── wide_button.dart │ ├── flare │ ├── desaturated_actor.dart │ ├── hiring_bust.dart │ ├── hiring_particles.dart │ ├── skill_icon.dart │ ├── start_screen_hero.dart │ ├── warmup_flare.dart │ └── work_team.dart │ ├── game_over.dart │ ├── keyboard.dart │ ├── prowess_progress.dart │ ├── screen_layout_builder.dart │ ├── skill_badge.dart │ ├── task_picker │ ├── task_picker_header.dart │ └── task_picker_item.dart │ └── work_items │ ├── bug_header.dart │ ├── bug_list_item.dart │ ├── skill_dot.dart │ ├── task_header.dart │ ├── task_list_item.dart │ ├── tasks_button_header.dart │ ├── tasks_section_header.dart │ ├── work_list_item.dart │ └── work_list_progress.dart ├── pubspec.yaml ├── test ├── character_test.dart ├── style_sphinx │ ├── question_arguments_test.dart │ └── sphinx_button_test.dart ├── widget_test.dart └── world_test.dart ├── test_driver ├── .gitignore ├── durations.tsv ├── generate-graphs.R ├── parse_timeline.dart ├── perf_stats.tsv ├── performance.dart └── performance_test.dart └── tool └── lock_android_scaling.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | test: 4 | 5 | docker: 6 | - image: circleci/android:api-28 7 | 8 | environment: 9 | JVM_OPTS: -Xmx3200m 10 | 11 | steps: 12 | - checkout 13 | - run: 14 | name: "Update Submodules" 15 | command: | 16 | git submodule init 17 | git submodule update --recursive 18 | - run: 19 | name: Install Flutter 20 | command: | 21 | git clone https://github.com/flutter/flutter.git -b v1.5.4 22 | ./flutter/bin/flutter doctor 23 | ./flutter/bin/flutter packages get 24 | 25 | - run: 26 | name: Check formatting 27 | command: | 28 | ./flutter/bin/cache/dart-sdk/bin/dartfmt -n --set-exit-if-changed ./lib 29 | 30 | - run: 31 | name: Static Analysis 32 | command: | 33 | ./flutter/bin/cache/dart-sdk/bin/dartanalyzer --fatal-infos --fatal-warnings ./lib 34 | 35 | - run: 36 | name: Test App 37 | command: | 38 | ./flutter/bin/flutter test 39 | 40 | - run: 41 | name: Build App 42 | command: | 43 | ./flutter/bin/flutter build apk 44 | 45 | workflows: 46 | version: 2 47 | build: 48 | jobs: 49 | - test 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | **/ios/**/profile 55 | **/ios/**/xcuserdata 56 | **/ios/.generated/ 57 | **/ios/Flutter/App.framework 58 | **/ios/Flutter/Flutter.framework 59 | **/ios/Flutter/Generated.xcconfig 60 | **/ios/Flutter/app.flx 61 | **/ios/Flutter/app.zip 62 | **/ios/Flutter/flutter_assets/ 63 | **/ios/ServiceDefinitions.json 64 | **/ios/Runner/GeneratedPluginRegistrant.* 65 | 66 | # Exceptions to above rules. 67 | !**/ios/**/default.mode1v3 68 | !**/ios/**/default.mode2v3 69 | !**/ios/**/default.pbxuser 70 | !**/ios/**/default.perspectivev3 71 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 72 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/.gitmodules -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 2D, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Developer Quest 2 | 3 | Become a tech lead, slay bugs, and don't get fired. 4 | 5 | All in Flutter. 6 | 7 | ## Research tree 8 | 9 | The game progression is based on a "research tree" of tasks. The tree is defined in code 10 | in `lib/src/shared_state/task_tree` but for clarity it is also kept as a diagram 11 | in `assets/docs`. Here's the PNG. 12 | 13 | ![The task "research tree"](https://github.com/2d-inc/dev_rpg/blob/master/assets/docs/research-tree.png) 14 | 15 | ## Performance testing 16 | 17 | Attach a real device and run the following command from the root of the repo: 18 | 19 | ```sh 20 | flutter drive --target=test_driver/performance.dart --profile 21 | ``` 22 | 23 | This will do an automated run-through of the app, and will save the output to files. 24 | 25 | * Look into to `build/walkthrough-*.json` files for detailed summaries of each run. 26 | * Look at `test_driver/perf_stats.tsv` to compare latest runs with historical data. 27 | * Run `Rscript test_driver/generate-graphs.R` (assuming you have R installed) to generate 28 | boxplots of the latest runs. This will show up as `test_driver/*.pdf` files. 29 | * Peruse the raw data file (used by R to generate the boxplots) by opening the 30 | `durations.tsv` file. These files contain build and rasterization times for each frame 31 | for every run. 32 | 33 | If you want to get several runs at once, you can use something like the following command: 34 | 35 | ```sh 36 | DESC="my change" bash -c 'for i in {1..5}; do flutter drive --target=test_driver/performance.dart --profile; sleep 1; done' 37 | ``` 38 | 39 | Why run several times when we get so many data points on each walkthrough? With several identical 40 | walkthroughs it's possible to visually check variance between runs. Even with box plots, 41 | these nuances get lost in the summary stats, so it's hard to see whether a change actually 42 | brought any performance improvement or not. Running several times also eliminates 43 | the effect of extremely bad luck, like for example when Android decides to update some app while 44 | test is running. 45 | 46 | ### Lock CPU and GPU speed for your performance test device 47 | 48 | Run the following command when your performance test device is attached via USB. 49 | 50 | ```bash 51 | ./tool/lock_android_scaling.sh 52 | ``` 53 | 54 | WARNING: 55 | 56 | * This only works for rooted devices. 57 | * This only works for Nexus 5. The specifics of scaling lock are different from device to device. 58 | You can modify the script to your needs, following 59 | [this template](https://github.com/google/skia/blob/master/infra/bots/recipe_modules/flavor/android.py) 60 | and 61 | [/sys/devices/system/cpu documentation](https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-system-cpu). 62 | 63 | ### Where to store the profiling data 64 | 65 | You probably don't want to check the `*.tsv` output files into the repo. For that, 66 | run `git update-index --assume-unchanged test_driver/*.tsv` in the root dir. This is a one time 67 | command per machine. 68 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: 3 | implicit-casts: false 4 | implicit-dynamic: false 5 | 6 | linter: 7 | rules: 8 | - always_put_required_named_parameters_first 9 | - always_require_non_null_named_parameters 10 | - annotate_overrides 11 | - avoid_annotating_with_dynamic 12 | - avoid_bool_literals_in_conditional_expressions 13 | - avoid_catches_without_on_clauses 14 | - avoid_catching_errors 15 | - avoid_classes_with_only_static_members 16 | - avoid_double_and_int_checks 17 | - avoid_empty_else 18 | - avoid_field_initializers_in_const_classes 19 | - avoid_implementing_value_types 20 | - avoid_init_to_null 21 | - avoid_js_rounded_ints 22 | - avoid_null_checks_in_equality_operators 23 | - avoid_relative_lib_imports 24 | - avoid_return_types_on_setters 25 | - avoid_returning_null 26 | - avoid_returning_null_for_future 27 | - avoid_returning_null_for_void 28 | - avoid_returning_this 29 | - avoid_setters_without_getters 30 | - avoid_shadowing_type_parameters 31 | - avoid_single_cascade_in_expression_statements 32 | - avoid_slow_async_io 33 | - avoid_types_as_parameter_names 34 | - avoid_unused_constructor_parameters 35 | - avoid_void_async 36 | - await_only_futures 37 | - camel_case_types 38 | - cancel_subscriptions 39 | - close_sinks 40 | - constant_identifier_names 41 | - control_flow_in_finally 42 | - curly_braces_in_flow_control_structures 43 | - directives_ordering 44 | - empty_catches 45 | - empty_constructor_bodies 46 | - empty_statements 47 | - file_names 48 | - hash_and_equals 49 | - implementation_imports 50 | - invariant_booleans 51 | - iterable_contains_unrelated_type 52 | - join_return_with_assignment 53 | - library_names 54 | - library_prefixes 55 | - lines_longer_than_80_chars 56 | - list_remove_unrelated_type 57 | - literal_only_boolean_expressions 58 | - no_adjacent_strings_in_list 59 | - no_duplicate_case_values 60 | - non_constant_identifier_names 61 | - null_closures 62 | - one_member_abstracts 63 | - only_throw_errors 64 | - overridden_fields 65 | - package_api_docs 66 | - package_names 67 | - package_prefixed_library_names 68 | - parameter_assignments 69 | - prefer_adjacent_string_concatenation 70 | - prefer_asserts_in_initializer_lists 71 | - prefer_collection_literals 72 | - prefer_conditional_assignment 73 | - prefer_const_constructors 74 | - prefer_const_constructors_in_immutables 75 | - prefer_const_declarations 76 | - prefer_const_literals_to_create_immutables 77 | - prefer_constructors_over_static_methods 78 | - prefer_contains 79 | - prefer_equal_for_default_values 80 | - prefer_final_fields 81 | - prefer_final_in_for_each 82 | - prefer_foreach 83 | - prefer_function_declarations_over_variables 84 | - prefer_initializing_formals 85 | - prefer_is_empty 86 | - prefer_is_not_empty 87 | - prefer_iterable_whereType 88 | - prefer_mixin 89 | - prefer_null_aware_operators 90 | - prefer_typing_uninitialized_variables 91 | - prefer_void_to_null 92 | - recursive_getters 93 | - slash_for_doc_comments 94 | - sort_pub_dependencies 95 | - sort_unnamed_constructors_first 96 | - test_types_in_equals 97 | - throw_in_finally 98 | - type_annotate_public_apis 99 | - type_init_formals 100 | - unawaited_futures 101 | - unnecessary_await_in_return 102 | - unnecessary_brace_in_string_interps 103 | - unnecessary_const 104 | - unnecessary_getters_setters 105 | - unnecessary_lambdas 106 | - unnecessary_new 107 | - unnecessary_null_aware_assignments 108 | - unnecessary_null_in_if_null_operators 109 | - unnecessary_overrides 110 | - unnecessary_parenthesis 111 | - unnecessary_statements 112 | - unnecessary_this 113 | - unrelated_type_equality_checks 114 | - use_full_hex_values_for_flutter_colors 115 | - use_rethrow_when_possible 116 | - use_setters_to_change_properties 117 | - use_string_buffers 118 | - use_to_and_as_if_applicable 119 | - valid_regexps 120 | - void_checks 121 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | def keystoreProperties = new Properties() 25 | if (System.getenv()["CI"] && System.getenv()["CIRCLECI"] == null) { 26 | def keystorePropertiesFile = rootProject.file("key.properties") 27 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 28 | } 29 | 30 | apply plugin: 'com.android.application' 31 | apply plugin: 'kotlin-android' 32 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 33 | 34 | android { 35 | compileSdkVersion 27 36 | 37 | sourceSets { 38 | main.java.srcDirs += 'src/main/kotlin' 39 | } 40 | 41 | lintOptions { 42 | disable 'InvalidPackage' 43 | } 44 | 45 | defaultConfig { 46 | applicationId "dev.flutter.devRpg" 47 | minSdkVersion 16 48 | targetSdkVersion 27 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 52 | } 53 | signingConfigs { 54 | release { 55 | if (System.getenv()["CI"] && System.getenv()["CIRCLECI"] == null) { 56 | keyAlias keystoreProperties['keyAlias'] 57 | keyPassword keystoreProperties['keyPassword'] 58 | storeFile file(keystoreProperties['storeFile']) 59 | storePassword keystoreProperties['storePassword'] 60 | } 61 | } 62 | } 63 | buildTypes { 64 | release { 65 | // TODO: Add your own signing config for the release build. 66 | // Signing with the debug keys for now, so `flutter run --release` works. 67 | if (System.getenv()["CI"] && System.getenv()["CIRCLECI"] == null) { 68 | signingConfig signingConfigs.release 69 | } 70 | else { 71 | signingConfig signingConfigs.debug 72 | } 73 | } 74 | } 75 | } 76 | 77 | flutter { 78 | source '../..' 79 | } 80 | 81 | dependencies { 82 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 83 | testImplementation 'junit:junit:4.12' 84 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 85 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 86 | } 87 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 19 | 26 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/dev/flutter/devrpg/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package dev.flutter.devrpg 2 | 3 | import android.os.Bundle 4 | 5 | import io.flutter.app.FlutterActivity 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | GeneratedPluginRegistrant.registerWith(this) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.2.71' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.2.1' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /artwork/speech bubble.pxm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/artwork/speech bubble.pxm -------------------------------------------------------------------------------- /assets/docs/code_chomper_alpha.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class RpgButton extends StatelessWidget { 4 | final VoidCallback onPressed; 5 | final Widget child; 6 | 7 | const RpgButton({ 8 | @required this.onPressed, 9 | @required this.child, 10 | Key key, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final radius = BorderRadius.circular(10); 16 | 17 | return Material( 18 | shape: RoundedRectangleBorder(borderRadius: radius), 19 | color: const Color.fromRGBO(242, 124, 78, 1), 20 | child: InkWell( 21 | borderRadius: radius, 22 | splashColor: const Color.fromRGBO(242, 124, 78, 1), 23 | child: Container( 24 | padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 16), 25 | child: DefaultTextStyle( 26 | child: child, 27 | style: const TextStyle( 28 | fontFamily: 'MontserratRegular', 29 | fontSize: 16, 30 | fontWeight: FontWeight.bold, 31 | color: Color.fromRGBO(85, 34, 34, 1)), 32 | ), 33 | ), 34 | onTap: onPressed, 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /assets/docs/code_chomper_beta.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | void main() => runApp(MyApp()); 4 | 5 | class MyApp extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return MaterialApp( 9 | title: 'Flutter Demo', 10 | theme: ThemeData( 11 | primarySwatch: Colors.blue, 12 | ), 13 | home: MyHomePage(title: 'Flutter Demo Home Page'), 14 | ); 15 | } 16 | } 17 | 18 | class MyHomePage extends StatefulWidget { 19 | MyHomePage({Key key, this.title}) : super(key: key); 20 | 21 | final String title; 22 | 23 | @override 24 | _MyHomePageState createState() => _MyHomePageState(); 25 | } 26 | 27 | class _MyHomePageState extends State { 28 | int _counter = 0; 29 | 30 | void _incrementCounter() { 31 | setState(() { 32 | _counter++; 33 | }); 34 | } 35 | 36 | @override 37 | Widget build(BuildContext context) { 38 | return Scaffold( 39 | appBar: AppBar( 40 | title: Text(widget.title), 41 | ), 42 | body: Center( 43 | child: Column( 44 | mainAxisAlignment: MainAxisAlignment.center, 45 | children: [ 46 | const Text( 47 | 'You have pushed the button this many times:', 48 | ), 49 | Text( 50 | '$_counter', 51 | style: Theme.of(context).textTheme.display1, 52 | ), 53 | ], 54 | ), 55 | ), 56 | floatingActionButton: FloatingActionButton( 57 | onPressed: _incrementCounter, 58 | tooltip: 'Increment', 59 | child: const Icon(Icons.add), 60 | ), 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /assets/docs/research-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/docs/research-tree.png -------------------------------------------------------------------------------- /assets/flare/Chomper FUI Type.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/Chomper FUI Type.flr -------------------------------------------------------------------------------- /assets/flare/Chomper.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/Chomper.flr -------------------------------------------------------------------------------- /assets/flare/CodeIcon.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/CodeIcon.flr -------------------------------------------------------------------------------- /assets/flare/Coin.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/Coin.flr -------------------------------------------------------------------------------- /assets/flare/CoordinationIcon.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/CoordinationIcon.flr -------------------------------------------------------------------------------- /assets/flare/CowboyCoder.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/CowboyCoder.flr -------------------------------------------------------------------------------- /assets/flare/Designer.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/Designer.flr -------------------------------------------------------------------------------- /assets/flare/EngineeringIcon.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/EngineeringIcon.flr -------------------------------------------------------------------------------- /assets/flare/Joy.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/Joy.flr -------------------------------------------------------------------------------- /assets/flare/NotificationIcon.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/NotificationIcon.flr -------------------------------------------------------------------------------- /assets/flare/ProgramManager.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/ProgramManager.flr -------------------------------------------------------------------------------- /assets/flare/SelectArrow.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/SelectArrow.flr -------------------------------------------------------------------------------- /assets/flare/Sourcerer.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/Sourcerer.flr -------------------------------------------------------------------------------- /assets/flare/Stars.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/Stars.flr -------------------------------------------------------------------------------- /assets/flare/TasksIcon.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/TasksIcon.flr -------------------------------------------------------------------------------- /assets/flare/TeamIcon.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/TeamIcon.flr -------------------------------------------------------------------------------- /assets/flare/Tester.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/Tester.flr -------------------------------------------------------------------------------- /assets/flare/TheArchitect.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/TheArchitect.flr -------------------------------------------------------------------------------- /assets/flare/TheHacker.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/TheHacker.flr -------------------------------------------------------------------------------- /assets/flare/TheJack.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/TheJack.flr -------------------------------------------------------------------------------- /assets/flare/TheRefactorer.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/TheRefactorer.flr -------------------------------------------------------------------------------- /assets/flare/UXResearcher.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/UXResearcher.flr -------------------------------------------------------------------------------- /assets/flare/Users.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/Users.flr -------------------------------------------------------------------------------- /assets/flare/UxIcon.flr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/flare/UxIcon.flr -------------------------------------------------------------------------------- /assets/fonts/Gotham XNarrow Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/fonts/Gotham XNarrow Medium.otf -------------------------------------------------------------------------------- /assets/fonts/Montserrat-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/fonts/Montserrat-Bold.otf -------------------------------------------------------------------------------- /assets/fonts/Montserrat-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/fonts/Montserrat-Medium.otf -------------------------------------------------------------------------------- /assets/fonts/Montserrat-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/fonts/Montserrat-Regular.otf -------------------------------------------------------------------------------- /assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/RobotoCondensed-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/fonts/RobotoCondensed-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/fonts/SpaceMono-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /assets/images/2.0x/2dimensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/images/2.0x/2dimensions.png -------------------------------------------------------------------------------- /assets/images/2.0x/2dimensions_wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/images/2.0x/2dimensions_wide.png -------------------------------------------------------------------------------- /assets/images/2.0x/flare_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/images/2.0x/flare_logo.png -------------------------------------------------------------------------------- /assets/images/2dimensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/images/2dimensions.png -------------------------------------------------------------------------------- /assets/images/2dimensions_wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/images/2dimensions_wide.png -------------------------------------------------------------------------------- /assets/images/3.0x/2dimensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/images/3.0x/2dimensions.png -------------------------------------------------------------------------------- /assets/images/3.0x/2dimensions_wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/images/3.0x/2dimensions_wide.png -------------------------------------------------------------------------------- /assets/images/3.0x/flare_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/images/3.0x/flare_logo.png -------------------------------------------------------------------------------- /assets/images/flare_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/images/flare_logo.png -------------------------------------------------------------------------------- /assets/images/flutter_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/images/flutter_logo.png -------------------------------------------------------------------------------- /assets/style_sphinx/green_bed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/style_sphinx/green_bed.png -------------------------------------------------------------------------------- /assets/style_sphinx/orange_cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/style_sphinx/orange_cat.png -------------------------------------------------------------------------------- /assets/style_sphinx/pyramid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/style_sphinx/pyramid.png -------------------------------------------------------------------------------- /assets/style_sphinx/red_bed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/style_sphinx/red_bed.png -------------------------------------------------------------------------------- /assets/style_sphinx/sphinx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/style_sphinx/sphinx.png -------------------------------------------------------------------------------- /assets/style_sphinx/sphinx_no_glasses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/style_sphinx/sphinx_no_glasses.png -------------------------------------------------------------------------------- /assets/style_sphinx/start_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/style_sphinx/start_background.png -------------------------------------------------------------------------------- /assets/style_sphinx/sunglasses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/style_sphinx/sunglasses.png -------------------------------------------------------------------------------- /assets/style_sphinx/yellow_cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/assets/style_sphinx/yellow_cat.png -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildSystemType 6 | Original 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "icon_20pt@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "icon_20pt@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "icon_29pt.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "icon_29pt@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "icon_29pt@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "icon_40pt@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "icon_40pt@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "icon_60pt@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "icon_60pt@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "icon_20pt.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "icon_20pt@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "icon_29pt.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "icon_29pt@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "icon_40pt.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "icon_40pt@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "icon_76pt.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "icon_76pt@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "icon_83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_76pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_76pt.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/2d-inc/developer_quest/5574fd17c641207bfa898c4c3301334ca4650d38/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | Developer Quest 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | dev_rpg 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dev_rpg/src/about_screen.dart'; 4 | import 'package:dev_rpg/src/code_chomper/code_chomper.dart'; 5 | import 'package:dev_rpg/src/game_screen.dart'; 6 | import 'package:dev_rpg/src/shared_state/game/world.dart'; 7 | import 'package:dev_rpg/src/shared_state/user.dart'; 8 | import 'package:dev_rpg/src/style_sphinx/axis_questions.dart'; 9 | import 'package:dev_rpg/src/style_sphinx/flex_questions.dart'; 10 | import 'package:dev_rpg/src/style_sphinx/kittens.dart'; 11 | import 'package:dev_rpg/src/style_sphinx/sphinx_image.dart'; 12 | import 'package:dev_rpg/src/style_sphinx/sphinx_screen.dart'; 13 | import 'package:dev_rpg/src/welcome_screen.dart'; 14 | import 'package:flare_flutter/flare_cache.dart'; 15 | import 'package:flutter/material.dart'; 16 | import 'package:flutter/services.dart'; 17 | import 'package:provider/provider.dart'; 18 | 19 | void main() { 20 | // Don't prune the Flare cache, keep loaded Flare files warm and ready 21 | // to be re-displayed. 22 | FlareCache.doesPrune = false; 23 | 24 | runApp(MyApp()); 25 | } 26 | 27 | class MyApp extends StatefulWidget { 28 | @override 29 | _MyAppState createState() => _MyAppState(); 30 | } 31 | 32 | class _MyAppState extends State { 33 | final World world = World(); 34 | 35 | @override 36 | Widget build(BuildContext context) { 37 | return MultiProvider( 38 | providers: [ 39 | ChangeNotifierProvider(builder: (_) => User()), 40 | ChangeNotifierProvider.value(notifier: world), 41 | ChangeNotifierProvider.value(notifier: world.characterPool), 42 | ChangeNotifierProvider.value(notifier: world.taskPool), 43 | ChangeNotifierProvider.value(notifier: world.company), 44 | ChangeNotifierProvider.value(notifier: world.company.users), 45 | ChangeNotifierProvider.value(notifier: world.company.joy), 46 | ChangeNotifierProvider.value(notifier: world.company.coin), 47 | ], 48 | child: MaterialApp( 49 | title: 'Flutter Demo', 50 | theme: ThemeData( 51 | brightness: Brightness.dark, 52 | primarySwatch: Colors.orange, 53 | canvasColor: Colors.transparent), 54 | routes: { 55 | '/': (context) => WelcomeScreen(), 56 | '/gameloop': (context) => GameScreen(), 57 | '/about': (context) => AboutScreen(), 58 | CodeChomper.miniGameRouteName: (context) { 59 | String filename = 60 | ModalRoute.of(context).settings.arguments as String; 61 | return CodeChomper(filename); 62 | }, 63 | SphinxScreen.miniGameRouteName: (context) => const SphinxScreen(), 64 | SphinxScreen.fullGameRouteName: (context) => 65 | const SphinxScreen(fullGame: true), 66 | ColumnQuestion.routeName: (context) => const ColumnQuestion(), 67 | RowQuestion.routeName: (context) => const RowQuestion(), 68 | StackQuestion.routeName: (context) => const StackQuestion(), 69 | MainAxisCenterQuestion.routeName: (context) => 70 | const MainAxisCenterQuestion(), 71 | MainAxisSpaceAroundQuestion.routeName: (context) => 72 | const MainAxisSpaceAroundQuestion(), 73 | MainAxisSpaceBetweenQuestion.routeName: (context) => 74 | const MainAxisSpaceBetweenQuestion(), 75 | MainAxisStartQuestion.routeName: (context) => 76 | const MainAxisStartQuestion(), 77 | MainAxisEndQuestion.routeName: (context) => 78 | const MainAxisEndQuestion(), 79 | MainAxisSpaceEvenlyQuestion.routeName: (context) => 80 | const MainAxisSpaceEvenlyQuestion(), 81 | RowMainAxisEndQuestion.routeName: (context) => 82 | const RowMainAxisEndQuestion(), 83 | RowMainAxisStartQuestion.routeName: (context) => 84 | const RowMainAxisStartQuestion(), 85 | RowMainAxisSpaceBetween.routeName: (context) => 86 | const RowMainAxisSpaceBetween(), 87 | }, 88 | )); 89 | } 90 | 91 | @override 92 | void initState() { 93 | // Schedule a microtask that warms up the image cache with all of the style 94 | // sphinx images. This will run after the build method is executed, but 95 | // before the style sphinx is displayed. 96 | scheduleMicrotask(() { 97 | precacheImage(SphinxScreen.pyramid, context); 98 | precacheImage(SphinxScreen.background, context); 99 | precacheImage(SphinxImage.provider, context); 100 | precacheImage(SphinxWithoutGlassesImage.provider, context); 101 | precacheImage(SphinxGlassesImage.provider, context); 102 | precacheImage(KittyBed.redProvider, context); 103 | precacheImage(KittyBed.greenProvider, context); 104 | precacheImage(Kitty.orangeProvider, context); 105 | precacheImage(Kitty.yellowProvider, context); 106 | }); 107 | super.initState(); 108 | } 109 | 110 | @override 111 | void dispose() { 112 | world.dispose(); 113 | super.dispose(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/src/game_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/rpg_layout_builder.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'game_screen_slim.dart'; 4 | import 'game_screen_wide.dart'; 5 | 6 | class GameScreen extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | return RpgLayoutBuilder( 10 | builder: (context, layout) => 11 | layout == RpgLayout.slim ? GameScreenSlim() : GameScreenWide(), 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/game_screen/add_task_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/style.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | // A stylized button meant to be used for adding tasks to the task pool. 5 | class AddTaskButton extends StatefulWidget { 6 | final IconData icon; 7 | final Color color; 8 | final String label; 9 | final int count; 10 | final VoidCallback onPressed; 11 | final double scale; 12 | 13 | const AddTaskButton( 14 | this.label, { 15 | Key key, 16 | this.count = 0, 17 | this.icon, 18 | this.color, 19 | this.onPressed, 20 | this.scale = 1.0, 21 | }) : super(key: key); 22 | 23 | @override 24 | _AddTaskButtonState createState() => _AddTaskButtonState(); 25 | } 26 | 27 | class _AddTaskButtonState extends State { 28 | bool _isPressed; 29 | @override 30 | void initState() { 31 | _isPressed = false; 32 | super.initState(); 33 | } 34 | 35 | void onTapDown(TapDownDetails details) { 36 | setState(() { 37 | _isPressed = true; 38 | }); 39 | } 40 | 41 | void onTapUp(TapUpDetails details) { 42 | setState(() { 43 | _isPressed = false; 44 | }); 45 | } 46 | 47 | void onTap() { 48 | if (widget.onPressed != null) { 49 | widget.onPressed(); 50 | } 51 | } 52 | 53 | bool get isDisabled => widget.count == 0; 54 | 55 | @override 56 | Widget build(BuildContext context) { 57 | return GestureDetector( 58 | onTapDown: isDisabled ? null : onTapDown, 59 | onTapUp: isDisabled ? null : onTapUp, 60 | onTap: isDisabled ? null : onTap, 61 | child: AnimatedContainer( 62 | duration: const Duration(milliseconds: 100), 63 | height: 40 * widget.scale, 64 | padding: const EdgeInsets.symmetric(horizontal: 8), 65 | decoration: BoxDecoration( 66 | boxShadow: isDisabled 67 | ? null 68 | : [ 69 | BoxShadow( 70 | color: widget.color.withOpacity(0.3), 71 | offset: const Offset(0, 10), 72 | blurRadius: _isPressed ? 10 : 15, 73 | spreadRadius: 0), 74 | ], 75 | borderRadius: BorderRadius.all(Radius.circular(20 * widget.scale)), 76 | color: isDisabled 77 | ? disabledTaskColor.withOpacity(0.10) 78 | : _isPressed ? widget.color.withOpacity(0.8) : widget.color, 79 | ), 80 | child: Row( 81 | children: [ 82 | Icon(widget.icon, 83 | color: isDisabled ? disabledTaskColor : Colors.white), 84 | const SizedBox(width: 4), 85 | Expanded( 86 | child: Text( 87 | widget.label, 88 | style: buttonTextStyle.apply( 89 | fontSizeDelta: -2, 90 | fontSizeFactor: widget.scale, 91 | color: isDisabled ? disabledTaskColor : Colors.white), 92 | ), 93 | ), 94 | isDisabled 95 | ? Container() 96 | : Container( 97 | constraints: BoxConstraints( 98 | minWidth: 26 * widget.scale, 99 | minHeight: 26 * widget.scale, 100 | maxHeight: 26 * widget.scale, 101 | ), 102 | decoration: const BoxDecoration( 103 | shape: BoxShape.circle, 104 | color: Colors.white, 105 | ), 106 | child: Center( 107 | child: Text(widget.count.toString(), 108 | style: buttonTextStyle.apply( 109 | fontSizeDelta: -2, 110 | fontSizeFactor: widget.scale, 111 | color: widget.color)), 112 | ), 113 | ), 114 | ], 115 | ), 116 | ), 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/src/game_screen/bug_picker_modal.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/bug.dart'; 2 | import 'package:dev_rpg/src/shared_state/game/task_pool.dart'; 3 | import 'package:dev_rpg/src/style.dart'; 4 | import 'package:dev_rpg/src/widgets/work_items/bug_header.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:provider/provider.dart'; 7 | 8 | /// Displays a list of the currently available [Bug]s. 9 | class BugPickerModal extends StatelessWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | var taskPool = Provider.of(context); 13 | var bugs = taskPool.availableBugs.toList(growable: false) 14 | ..sort((a, b) => -a.priority.drainOfJoy.compareTo(b.priority.drainOfJoy)); 15 | 16 | return Center( 17 | child: ConstrainedBox( 18 | constraints: const BoxConstraints(maxWidth: modalMaxWidth), 19 | child: ClipRRect( 20 | borderRadius: const BorderRadius.only( 21 | topLeft: Radius.circular(10), topRight: Radius.circular(10)), 22 | child: Container( 23 | color: modalBackgroundColor, 24 | child: ListView.builder( 25 | padding: const EdgeInsets.only(top: 30, left: 15, right: 15), 26 | itemCount: bugs.length, 27 | itemBuilder: (context, index) { 28 | Bug bug = bugs[index]; 29 | 30 | return Padding( 31 | padding: const EdgeInsets.only(bottom: 15), 32 | child: InkWell( 33 | onTap: () => Navigator.pop(context, bug), 34 | child: Container( 35 | padding: const EdgeInsets.all(15), 36 | decoration: const BoxDecoration( 37 | boxShadow: [ 38 | BoxShadow( 39 | color: Color.fromRGBO(0, 0, 0, 0.03), 40 | offset: Offset(0, 10), 41 | blurRadius: 10, 42 | spreadRadius: 0), 43 | ], 44 | borderRadius: BorderRadius.all(Radius.circular(9)), 45 | color: Colors.white, 46 | ), 47 | child: Column( 48 | crossAxisAlignment: CrossAxisAlignment.start, 49 | children: [ 50 | BugHeader(bug), 51 | const SizedBox(height: 12), 52 | Text(bug.name, style: contentStyle) 53 | ], 54 | ), 55 | ), 56 | ), 57 | ); 58 | }, 59 | ), 60 | ), 61 | ), 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/src/game_screen/character_style.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:ui'; 3 | 4 | import 'package:dev_rpg/src/shared_state/game/character.dart'; 5 | 6 | /// UI style properties for [Character]s. [Character] to [CharacterStyle] 7 | /// mapping is done via [Character.id] values. 8 | class CharacterStyle { 9 | final String flare; 10 | final Color accent; 11 | final String name; 12 | final String description; 13 | 14 | static final Map _all = { 15 | 'jack': CharacterStyle( 16 | name: 'The Jack-of-All-Trades', 17 | flare: 'assets/flare/TheJack.flr', 18 | accent: const Color.fromRGBO(29, 202, 34, 1), 19 | description: 'Got a problem? Jack can help! Carries a snorkel ' 20 | 'everywhere he goes since he\'s always prepared.'), 21 | 'sourcerer': CharacterStyle( 22 | name: 'The Sourcerer', 23 | flare: 'assets/flare/Sourcerer.flr', 24 | accent: const Color.fromRGBO(82, 183, 216, 1), 25 | description: 26 | 'Accomplished problem-solver and coder who is able to find the ' 27 | 'answer to any and all problems by traversing codebases.'), 28 | 'refactorer': CharacterStyle( 29 | name: 'The Refactorer', 30 | flare: 'assets/flare/TheRefactorer.flr', 31 | accent: const Color.fromRGBO(75, 58, 185, 1), 32 | description: 33 | 'A Digital Druid. She has a sixth sense when it comes to code ' 34 | 'health. Need to restructure your code? Is your code made up of a ' 35 | 'bunch of copy-paste snippets? Send in The Refactorer to clean up ' 36 | 'your codebase and make it shine!'), 37 | 'architect': CharacterStyle( 38 | name: 'The Architect', 39 | flare: 'assets/flare/TheArchitect.flr', 40 | accent: const Color.fromRGBO(236, 41, 117, 1), 41 | description: 42 | 'Helps provide structure in large codebases, which can improve ' 43 | 'code health. Has a ton of books and a head full of ideas.'), 44 | 'pm': CharacterStyle( 45 | name: 'Program Manager', 46 | flare: 'assets/flare/ProgramManager.flr', 47 | accent: const Color.fromRGBO(84, 209, 88, 1), 48 | description: 49 | 'Promotes communication and group harmony. He has the superpower ' 50 | 'of increasing everyone else\'s abilities if assigned to a task ' 51 | 'with others.'), 52 | 'avant_garde_designer': CharacterStyle( 53 | name: 'Avant Garde Designer', 54 | flare: 'assets/flare/Designer.flr', 55 | accent: const Color.fromRGBO(236, 148, 0, 1), 56 | description: 57 | 'Improves team execution by inspiring them with great designs for ' 58 | 'the app. Her designs win over more customers and spark joy when ' 59 | 'users interact with the app.'), 60 | 'cowboy': CharacterStyle( 61 | name: 'The Cowboy Coder', 62 | flare: 'assets/flare/CowboyCoder.flr', 63 | accent: const Color.fromRGBO(75, 58, 185, 1), 64 | description: 65 | 'An extremely prolific coder who doesn\'t like structure. He can ' 66 | 'write a whole lot of code very quickly... hopefully everyone else ' 67 | 'can read it. Yeehaw!'), 68 | 'tester': CharacterStyle( 69 | name: 'The Test Engineer', 70 | flare: 'assets/flare/Tester.flr', 71 | accent: const Color.fromRGBO(75, 58, 185, 1), 72 | description: 73 | 'An excellent developer in their own right, the Test Engineer ' 74 | 'creates invaluable frameworks for continuous integration testing ' 75 | 'and fixes bugs at lightning speed.'), 76 | 'uxr': CharacterStyle( 77 | name: 'User Experience Researcher', 78 | flare: 'assets/flare/UXResearcher.flr', 79 | accent: const Color.fromRGBO(222, 165, 88, 1), 80 | description: 81 | 'They design enlightening experiments to better understand user ' 82 | 'needs, resulting in a delightful user experience.'), 83 | 'hacker': CharacterStyle( 84 | name: 'The Hacker', 85 | flare: 'assets/flare/TheHacker.flr', 86 | accent: const Color.fromRGBO(236, 41, 117, 1), 87 | description: 88 | 'A strong coder on her own, but excels at finding and fixing ' 89 | 'security flaws and also discovering unique solutions to problems. ' 90 | 'She may have sniffed your email password while you read this ' 91 | 'description.'), 92 | }; 93 | 94 | CharacterStyle( 95 | {this.flare, this.accent, this.name, this.description = 'N/A'}); 96 | 97 | static CharacterStyle from(Character character) { 98 | return _all[character.id]; 99 | } 100 | 101 | static CharacterStyle random() { 102 | Random rand = Random(); 103 | return _all.values.elementAt(rand.nextInt(_all.values.length)); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /lib/src/game_screen/task_pool_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/rpg_layout_builder.dart'; 2 | import 'package:dev_rpg/src/shared_state/game/bug.dart'; 3 | import 'package:dev_rpg/src/shared_state/game/task.dart'; 4 | import 'package:dev_rpg/src/shared_state/game/task_pool.dart'; 5 | import 'package:dev_rpg/src/shared_state/game/work_item.dart'; 6 | import 'package:dev_rpg/src/widgets/work_items/bug_list_item.dart'; 7 | import 'package:dev_rpg/src/widgets/work_items/task_list_item.dart'; 8 | import 'package:dev_rpg/src/widgets/work_items/tasks_button_header.dart'; 9 | import 'package:dev_rpg/src/widgets/work_items/tasks_section_header.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:provider/provider.dart'; 12 | 13 | enum TaskPoolDisplay { all, inProgress, completed } 14 | 15 | /// Displays a list of the [Task]s the player has interacted with. 16 | /// These are [Task]s that have been added into the game, are being 17 | /// actively worked on, or have been completed and/or archived. 18 | class TaskPoolPage extends StatelessWidget { 19 | final TaskPoolDisplay display; 20 | const TaskPoolPage({this.display = TaskPoolDisplay.all}); 21 | 22 | /// Builds a section of the task list with [title] and a list of [workItems]. 23 | /// This returns slivers to be used in a [SliverList]. 24 | void _buildSection(List slivers, String title, double scale, 25 | List workItems) { 26 | if (workItems.isNotEmpty) { 27 | slivers.add(SliverPersistentHeader( 28 | pinned: false, 29 | delegate: TasksSectionHeader(title, scale), 30 | )); 31 | slivers.add(SliverList( 32 | delegate: SliverChildBuilderDelegate((context, index) { 33 | WorkItem item = workItems[index]; 34 | return ChangeNotifierProvider.value( 35 | notifier: item, 36 | key: ValueKey(item), 37 | child: item is Bug ? BugListItem() : TaskListItem(), 38 | ); 39 | }, childCount: workItems.length), 40 | )); 41 | } 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | return RpgLayoutBuilder(builder: (context, layout) { 47 | double scale = layout == RpgLayout.ultrawide ? 1.25 : 1; 48 | return Container( 49 | color: display == TaskPoolDisplay.completed 50 | ? const Color.fromRGBO(229, 229, 229, 1) 51 | : const Color.fromRGBO(241, 241, 241, 1), 52 | child: Consumer( 53 | builder: (context, taskPool, _) { 54 | var slivers = []; 55 | 56 | // Add the header only if we show the in progress tasks. 57 | if (display == TaskPoolDisplay.all || 58 | display == TaskPoolDisplay.inProgress) { 59 | slivers.add( 60 | SliverPersistentHeader( 61 | pinned: false, 62 | delegate: TasksButtonHeader(taskPool: taskPool, scale: scale), 63 | ), 64 | ); 65 | _buildSection(slivers, 'IN PROGRESS', scale, taskPool.workItems); 66 | } 67 | 68 | if (display == TaskPoolDisplay.all || 69 | display == TaskPoolDisplay.completed) { 70 | _buildSection( 71 | slivers, 72 | 'COMPLETED', 73 | scale, 74 | taskPool.completedTasks 75 | .followedBy(taskPool.archivedTasks) 76 | .toList(growable: false)); 77 | } 78 | return CustomScrollView(slivers: slivers); 79 | }, 80 | ), 81 | ); 82 | }); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/game_screen_wide.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:dev_rpg/src/game_screen/character_pool_page.dart'; 4 | import 'package:dev_rpg/src/rpg_layout_builder.dart'; 5 | import 'package:dev_rpg/src/shared_state/game/company.dart'; 6 | import 'package:dev_rpg/src/style.dart'; 7 | import 'package:dev_rpg/src/widgets/app_bar/coin_badge.dart'; 8 | import 'package:dev_rpg/src/widgets/app_bar/joy_badge.dart'; 9 | import 'package:dev_rpg/src/widgets/app_bar/stat_separator.dart'; 10 | import 'package:dev_rpg/src/widgets/app_bar/users_badge.dart'; 11 | import 'package:flutter/material.dart'; 12 | import 'package:provider/provider.dart'; 13 | 14 | import 'game_screen/task_pool_page.dart'; 15 | 16 | class GameScreenWide extends StatelessWidget { 17 | @override 18 | Widget build(BuildContext context) { 19 | var availableWidth = MediaQuery.of(context).size.width; 20 | var taskColumnWidth = min(modalMaxWidth, availableWidth / 3); 21 | var charactersWidth = availableWidth - taskColumnWidth * 2; 22 | var numCharacterColumns = 23 | (charactersWidth / idealCharacterWidth).round().clamp(2, 4).toInt(); 24 | 25 | return RpgLayoutBuilder( 26 | builder: (context, layout) { 27 | double statsScale = layout == RpgLayout.ultrawide ? 1.25 : 1; 28 | double statsWidth = layout == RpgLayout.ultrawide ? 300 : 125; 29 | return Scaffold( 30 | backgroundColor: const Color.fromRGBO(59, 59, 73, 1), 31 | appBar: AppBar( 32 | automaticallyImplyLeading: false, 33 | titleSpacing: 0, 34 | title: Consumer( 35 | builder: (context, company, _) { 36 | // Using RepaintBoundary here because this part of the UI 37 | // changes frequently. 38 | return RepaintBoundary( 39 | child: Container( 40 | decoration: BoxDecoration( 41 | border: Border( 42 | top: const BorderSide( 43 | color: statsSeparatorColor, 44 | style: BorderStyle.solid, 45 | ), 46 | ), 47 | ), 48 | child: Row( 49 | children: [ 50 | Container( 51 | width: statsWidth, 52 | child: UsersBadge( 53 | company.users, 54 | scale: statsScale, 55 | isWide: layout == RpgLayout.ultrawide, 56 | ), 57 | ), 58 | StatSeparator(), 59 | Container( 60 | width: statsWidth, 61 | child: JoyBadge( 62 | company.joy, 63 | scale: statsScale, 64 | isWide: layout == RpgLayout.ultrawide, 65 | ), 66 | ), 67 | StatSeparator(), 68 | layout == RpgLayout.ultrawide 69 | ? Container( 70 | width: statsWidth, 71 | child: CoinBadge( 72 | company.coin, 73 | scale: statsScale, 74 | isWide: layout == RpgLayout.ultrawide, 75 | ), 76 | ) 77 | : Expanded( 78 | child: 79 | CoinBadge(company.coin, scale: statsScale)), 80 | ], 81 | ), 82 | ), 83 | ); 84 | }, 85 | ), 86 | ), 87 | body: Row( 88 | children: [ 89 | SizedBox( 90 | width: charactersWidth, 91 | child: 92 | CharacterPoolPage(numColumns: numCharacterColumns.toInt()), 93 | ), 94 | SizedBox( 95 | width: taskColumnWidth, 96 | child: const TaskPoolPage(display: TaskPoolDisplay.inProgress), 97 | ), 98 | SizedBox( 99 | width: taskColumnWidth, 100 | child: const TaskPoolPage(display: TaskPoolDisplay.completed), 101 | ), 102 | ], 103 | ), 104 | ); 105 | }, 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/src/rpg_layout_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/style.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// Layout for the dev_rpg game. 5 | enum RpgLayout { slim, wide, ultrawide } 6 | 7 | /// Signature for a function that builds a widget given an [RpgLayout]. 8 | /// 9 | /// Used by [RpgLayoutBuilder.builder]. 10 | typedef RpgLayoutWidgetBuilder = Widget Function( 11 | BuildContext context, RpgLayout layout); 12 | 13 | /// Builds a widget tree that can depend on the parent widget's width 14 | class RpgLayoutBuilder extends StatelessWidget { 15 | const RpgLayoutBuilder({ 16 | @required this.builder, 17 | Key key, 18 | }) : assert(builder != null), 19 | super(key: key); 20 | 21 | /// Builds the widgets below this widget given this widget's layout width. 22 | final RpgLayoutWidgetBuilder builder; 23 | 24 | Widget _build(BuildContext context, BoxConstraints constraints) { 25 | var mediaWidth = MediaQuery.of(context).size.width; 26 | final RpgLayout layout = mediaWidth >= ultraWideLayoutThreshold 27 | ? RpgLayout.ultrawide 28 | : mediaWidth > wideLayoutThreshold ? RpgLayout.wide : RpgLayout.slim; 29 | return builder(context, layout); 30 | } 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | return LayoutBuilder(builder: _build); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/bug.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:dev_rpg/src/shared_state/game/skill.dart'; 4 | import 'package:dev_rpg/src/shared_state/game/task_pool.dart'; 5 | import 'package:dev_rpg/src/shared_state/game/work_item.dart'; 6 | 7 | /// Build weighted list of priorities. Put each BugPriority type in the list 8 | /// n times, where n is the value in bugFrequency. 9 | List bugChances = bugFrequency.keys 10 | .expand((bug) => List.generate(bugFrequency[bug], (_) => bug)) 11 | .toList(); 12 | 13 | /// Map of Bug types to the frequency that they appear. 14 | Map bugFrequency = { 15 | BugPriority('P0', 0.9): 1, 16 | BugPriority('P1', 0.6): 2, 17 | BugPriority('P2', 0.3): 2, 18 | BugPriority('P3', 0.2): 3, 19 | BugPriority('P4', 0.1): 3, 20 | }; 21 | 22 | /// A bug that randomly shows up in the work queue. 23 | class Bug extends WorkItem { 24 | static Random randomizer = Random(); 25 | final BugPriority priority; 26 | 27 | Bug(this.priority, Map difficulty) 28 | : super(priority.name + ' Bug!!', difficulty); 29 | 30 | Bug.random(Set availableSkills) 31 | : this(bugChances[randomizer.nextInt(bugChances.length)], 32 | randomDifficulty(randomizer, availableSkills)); 33 | 34 | @override 35 | void onCompleted() { 36 | get().squashBug(this); 37 | super.onCompleted(); 38 | markDirty(); 39 | } 40 | } 41 | 42 | class BugPriority { 43 | final double drainOfJoy; 44 | final String name; 45 | 46 | BugPriority(this.name, this.drainOfJoy); 47 | 48 | @override 49 | String toString() => 'BugPriority($drainOfJoy, $name)'; 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/character.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/company.dart'; 2 | import 'package:dev_rpg/src/shared_state/game/skill.dart'; 3 | import 'package:dev_rpg/src/shared_state/game/src/aspect.dart'; 4 | import 'package:dev_rpg/src/shared_state/game/src/child_aspect.dart'; 5 | import 'package:dev_rpg/src/shared_state/game/task_pool.dart'; 6 | import 'package:dev_rpg/src/shared_state/game/world.dart'; 7 | 8 | /// A single task for the player and her team to complete. 9 | /// 10 | /// The definition of the task is in [blueprint]. This class holds the runtime 11 | /// state (like [percentComplete]). 12 | class Character extends Aspect with ChildAspect { 13 | static const int maxSkillProwess = 5; 14 | 15 | final String id; 16 | 17 | final Map prowess; 18 | 19 | // basically a upgrade counter 20 | int _level = 1; 21 | int get level => _level; 22 | 23 | bool _isHired = false; 24 | bool get isHired => _isHired; 25 | 26 | final int customHiringCost; 27 | final int costMultiplier; 28 | 29 | /// This value will get summed with [TaskPool.featureBugChance] 30 | /// after completing work to compute the total bug chance. 31 | /// This value can be negative, meaning this character is so 32 | /// attentive that they will actually reduce the overall bug 33 | /// chance. 34 | final double bugChanceOffset; 35 | 36 | /// If this character produces bugs, the number of bugs that they often 37 | /// tend to produce at once. 38 | final int bugQuantity; 39 | 40 | bool _isBusy = false; 41 | 42 | Character( 43 | this.id, 44 | this.prowess, { 45 | this.customHiringCost, 46 | this.costMultiplier = 1, 47 | this.bugChanceOffset = 0, 48 | this.bugQuantity = TaskPool.defaultBugNumber, 49 | }); 50 | 51 | bool get isBusy => _isBusy; 52 | 53 | set isBusy(bool value) { 54 | _isBusy = value; 55 | markDirty(); 56 | } 57 | 58 | double getProwessProgress(Skill skill) => 59 | (prowess[skill] ?? 0) / maxSkillProwess; 60 | 61 | bool contributes(List skills) { 62 | for (final Skill skill in skills) { 63 | if (prowess.containsKey(skill)) { 64 | return true; 65 | } 66 | } 67 | return false; 68 | } 69 | 70 | @override 71 | String toString() => id; 72 | 73 | int get upgradeCost => !_isHired && customHiringCost != null 74 | ? customHiringCost 75 | : prowess.values.fold(0, (int previous, int value) => previous + value) * 76 | (_isHired ? 110 : 220) * 77 | costMultiplier; 78 | 79 | bool get canUpgradeOrHire { 80 | Company company = get().company; 81 | // Make sure there's some skill that's below max (meaning we can bump 82 | // it up). 83 | if (prowess.values.firstWhere((value) => value < maxSkillProwess, 84 | orElse: () => maxSkillProwess) == 85 | maxSkillProwess) { 86 | return false; 87 | } 88 | return company.coin.number >= upgradeCost; 89 | } 90 | 91 | bool upgradeOrHire() => _isHired ? upgrade() : hire(); 92 | 93 | bool hire() { 94 | assert(!_isHired); 95 | Company company = get().company; 96 | if (!company.spend(upgradeCost)) { 97 | return false; 98 | } 99 | _isHired = true; 100 | markDirty(); 101 | return true; 102 | } 103 | 104 | bool upgrade() { 105 | assert(_isHired); 106 | Company company = get().company; 107 | if (!company.spend(upgradeCost)) { 108 | return false; 109 | } 110 | for (final Skill skill in prowess.keys) { 111 | prowess[skill] += 1; 112 | } 113 | _level++; 114 | markDirty(); 115 | return true; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/character_pool.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/character.dart'; 2 | import 'package:dev_rpg/src/shared_state/game/skill.dart'; 3 | import 'package:dev_rpg/src/shared_state/game/src/aspect_container.dart'; 4 | import 'package:dev_rpg/src/shared_state/game/src/child_aspect.dart'; 5 | import 'package:dev_rpg/src/shared_state/game/world.dart'; 6 | 7 | /// A list of [Character]s. 8 | class CharacterPool extends AspectContainer with ChildAspect { 9 | CharacterPool() { 10 | initializeCharacters(); 11 | } 12 | 13 | void initializeCharacters() => setAspects([ 14 | Character('jack', {Skill.coding: 1, Skill.coordination: 1, Skill.ux: 1}, 15 | customHiringCost: 220, costMultiplier: 4), 16 | Character('sourcerer', 17 | {Skill.coding: 1, Skill.coordination: 1, Skill.engineering: 1}, 18 | customHiringCost: 220, costMultiplier: 4, bugChanceOffset: -0.02), 19 | Character('refactorer', {Skill.coding: 1, Skill.engineering: 3}), 20 | Character('architect', {Skill.coding: 1, Skill.engineering: 4}, 21 | bugChanceOffset: -0.09), 22 | Character('hacker', {Skill.coding: 3, Skill.engineering: 1}, 23 | bugChanceOffset: 0.03), 24 | Character('cowboy', {Skill.coding: 4, Skill.engineering: 1}, 25 | bugChanceOffset: 0.6, bugQuantity: 6), 26 | Character('pm', {Skill.coordination: 3, Skill.ux: 1}), 27 | Character('uxr', {Skill.coordination: 1, Skill.ux: 3}), 28 | Character('avant_garde_designer', {Skill.ux: 4}), 29 | Character('tester', {Skill.coding: 2, Skill.coordination: 1}, 30 | bugChanceOffset: -0.05), 31 | ]); 32 | 33 | /// Get all the characters that have been hired and are part of the 34 | /// player's team! 35 | List get fullTeam => children.where((c) => c.isHired).toList(); 36 | 37 | // get the set of available skills available with the player's hired team 38 | Set get availableSkills => children 39 | .expand((character) => character.isHired 40 | ? character.prowess.keys 41 | : const Iterable.empty()) 42 | .toSet(); 43 | 44 | bool _isUpgradeAvailable = false; 45 | bool get isUpgradeAvailable => _isUpgradeAvailable; 46 | 47 | @override 48 | void update() { 49 | int coin = get().company.coin.number; 50 | bool upgradeAvailable = 51 | children.any((character) => character.upgradeCost <= coin); 52 | if (upgradeAvailable != _isUpgradeAvailable) { 53 | _isUpgradeAvailable = upgradeAvailable; 54 | markDirty(); 55 | } 56 | super.update(); 57 | } 58 | 59 | // Mark this Aspect dirty when any child is dirty. 60 | @override 61 | bool get inheritsDirt => true; 62 | 63 | void reset() => initializeCharacters(); 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/company.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:dev_rpg/src/shared_state/game/src/aspect.dart'; 4 | import 'package:dev_rpg/src/shared_state/game/src/aspect_container.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:intl/intl.dart'; 7 | 8 | /// The company the user is playing on behalf of. 9 | /// 10 | /// The company owns [coin], and generates [joy] for its [users]. 11 | class Company extends AspectContainer { 12 | static const int _initialCoin = 540; 13 | 14 | final StatValue users; 15 | 16 | final StatValue joy; 17 | 18 | final StatValue coin; 19 | 20 | Company() 21 | : users = StatValue(0), 22 | joy = StatValue(0), 23 | coin = StatValue(_initialCoin) { 24 | addAspect(users); 25 | addAspect(joy); 26 | addAspect(coin); 27 | } 28 | 29 | double maxUsers = 0; 30 | 31 | /// Star rating out of 5. Based on users lost, which is a consequence of 32 | /// bugs not resolved in a timely manner. 33 | /// Since it's inevitable to lose some, we consider losing 5% 34 | /// exceptional. 35 | int get starRating => 36 | ((users.number / maxUsers / 0.95).clamp(0, 1) * 4).round(); 37 | 38 | void award(int newUsers, int coinReward) { 39 | users.number += newUsers; 40 | coin.number += coinReward; 41 | } 42 | 43 | bool spend(int cost) { 44 | if (cost > coin.number) { 45 | return false; 46 | } 47 | coin.number -= cost; 48 | markDirty(); 49 | return true; 50 | } 51 | 52 | @override 53 | void update() { 54 | super.update(); 55 | 56 | // Generous denorm. 57 | if (joy.number.abs() < 0.01) { 58 | joy.number = 0; 59 | } 60 | 61 | users.number = max(0, users.number + joy.number); 62 | maxUsers = max(maxUsers, users.number); 63 | } 64 | 65 | void reset() { 66 | joy.number = 0; 67 | users.number = 0; 68 | maxUsers = 0; 69 | coin.number = _initialCoin; 70 | } 71 | } 72 | 73 | /// A value that is shown to the user. It is backed by a [number], and for 74 | /// the purposes of UI repainting it is represented by a [String] 75 | /// (via [string]). 76 | class StatValue extends Aspect 77 | implements ValueListenable { 78 | /// The default formatter of stats. It uses `package:intl`'s 79 | /// [NumberFormat.compact]. 80 | static final Formatter defaultFormatter = NumberFormat.compact().format; 81 | 82 | /// The current value. 83 | V _number; 84 | 85 | /// The value that listeners have been provided with most recently. 86 | /// 87 | /// This field is used to make sure we don't notify listeners when they're 88 | /// already showing the most recent value. 89 | String _shownValue; 90 | 91 | /// The formatter to be used to convert [number] into a String [string]. 92 | final String Function(V) formatter; 93 | 94 | StatValue(this._number, {Formatter statFormatter}) 95 | : assert(_number != null), 96 | formatter = statFormatter ?? StatValue.defaultFormatter { 97 | _shownValue = formatter(_number); 98 | } 99 | 100 | V get number => _number; 101 | 102 | set number(V newValue) { 103 | if ((newValue == 0 && _number != 0) || (newValue.sign != _number.sign)) { 104 | // Special case for significant value changes. 105 | _number = newValue; 106 | _shownValue = formatter(newValue); 107 | notifyListeners(); 108 | return; 109 | } 110 | 111 | var newShownValue = formatter(newValue); 112 | 113 | if (newShownValue == _shownValue) { 114 | // Only update value, don't notify listeners. 115 | _number = newValue; 116 | return; 117 | } 118 | 119 | _number = newValue; 120 | _shownValue = newShownValue; 121 | notifyListeners(); 122 | } 123 | 124 | @override 125 | String get value => _shownValue; 126 | } 127 | 128 | /// A function that takes a number and returns its string representation. 129 | typedef Formatter = String Function(T); 130 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/skill.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | enum Skill { coding, engineering, ux, coordination } 4 | 5 | // get a random set of skills and difficutly values 6 | Map randomDifficulty( 7 | Random randomizer, Set availableSkills, 8 | {double maxDifficulty = 100}) { 9 | Map difficulty = {}; 10 | 11 | List availableSkillList = availableSkills.toList(); 12 | 13 | // Add one or two skills. 14 | do { 15 | var skill = 16 | availableSkillList[randomizer.nextInt(availableSkillList.length)]; 17 | difficulty[skill] = randomizer.nextDouble() * maxDifficulty; 18 | } while (difficulty.length < 2 && randomizer.nextBool()); 19 | 20 | return difficulty; 21 | } 22 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/src/aspect.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | /// An aspect of the game world that can be listened to. 4 | /// 5 | /// This is like a [ChangeNotifier], but it uses the concept of dirtiness. 6 | /// 7 | /// This is a very game-centric approach, geared toward state that is heavily 8 | /// interdependent (like in a game simulation) and that is changed all the time 9 | /// (again, like in a game simulation). You probably do not need this 10 | /// for a regular app. Use regular [ChangeNotifier], which is cleaner. 11 | abstract class Aspect extends ChangeNotifier { 12 | bool _dirty = false; 13 | 14 | /// The aspect has changed since the last time [update] was called. 15 | bool get isDirty => _dirty; 16 | 17 | /// Marks the aspect dirty (changed). 18 | /// 19 | /// This can be called from outside the class (unlike [notifyListeners], which 20 | /// is [protected]). Also unlike [notifyListeners], this does not immediately 21 | /// notify. Listeners will be called on next call of [update]. 22 | void markDirty() { 23 | _dirty = true; 24 | } 25 | 26 | /// Called every "logical frame" (physical update in game parlance), to update 27 | /// the state of this aspect. 28 | /// 29 | /// Subclasses must call `super.update()`. 30 | @mustCallSuper 31 | void update() { 32 | if (_dirty) { 33 | notifyListeners(); 34 | _dirty = false; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/src/aspect_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/src/aspect.dart'; 2 | import 'package:dev_rpg/src/shared_state/game/src/child_aspect.dart'; 3 | 4 | /// A general class for [Aspect]s that contain other [Aspect]s 5 | abstract class AspectContainer extends Aspect { 6 | final List children; 7 | AspectContainer() : children = []; 8 | bool _guardRemove = false; 9 | List _queuedRemoval; 10 | 11 | // Whether or not this container gets marked dirty when a child is dirty. 12 | bool get inheritsDirt => false; 13 | 14 | @override 15 | void update() { 16 | // guard against changing the list while iterating it 17 | _guardRemove = true; 18 | for (final T child in children) { 19 | if (inheritsDirt && child.isDirty) { 20 | markDirty(); 21 | } 22 | child.update(); 23 | } 24 | _guardRemove = false; 25 | 26 | // Process any queued removals. 27 | if (_queuedRemoval != null) { 28 | _queuedRemoval.forEach(children.remove); 29 | _queuedRemoval = null; 30 | markDirty(); 31 | } 32 | 33 | super.update(); 34 | } 35 | 36 | void addAspects(Iterable aspects) => aspects.forEach(addAspect); 37 | void clearAspects() { 38 | _queuedRemoval?.clear(); 39 | children.clear(); 40 | } 41 | 42 | void setAspects(Iterable aspects) { 43 | clearAspects(); 44 | addAspects(aspects); 45 | } 46 | 47 | void addAspect(T aspect) { 48 | children.add(aspect); 49 | if (aspect is ChildAspect) { 50 | (aspect as ChildAspect).parent = this; 51 | } 52 | markDirty(); 53 | } 54 | 55 | void removeAspect(T aspect) { 56 | // If we attempt to remove an aspect while the list is being iterated, 57 | // queue the removal and process it on our next update. 58 | if (_guardRemove) { 59 | _queuedRemoval ??= []; 60 | _queuedRemoval.add(aspect); 61 | return; 62 | } 63 | children.remove(aspect); 64 | markDirty(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/src/child_aspect.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/src/aspect.dart'; 2 | 3 | /// A mixin that allows for hierarchical [Aspect]s 4 | /// and also allows searching for specific parent [Aspect]s 5 | mixin ChildAspect { 6 | Aspect parent; 7 | 8 | T get() { 9 | Aspect looking = parent; 10 | while (looking != null) { 11 | if (looking is T) { 12 | return looking as T; 13 | } 14 | looking = looking is ChildAspect ? (looking as ChildAspect).parent : null; 15 | } 16 | 17 | return null; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/task_blueprint.dart'; 2 | import 'package:dev_rpg/src/shared_state/game/task_pool.dart'; 3 | import 'package:dev_rpg/src/shared_state/game/work_item.dart'; 4 | import 'package:dev_rpg/src/shared_state/game/world.dart'; 5 | 6 | enum TaskState { working, completed, rewarded } 7 | 8 | /// A single task for the player and her team to complete. 9 | /// 10 | /// The definition of the task is in [blueprint]. This class holds the runtime 11 | /// state (like [percentComplete]). 12 | class Task extends WorkItem { 13 | final TaskBlueprint blueprint; 14 | TaskState _state = TaskState.working; 15 | TaskState get state => _state; 16 | 17 | Task(this.blueprint) : super(blueprint.name, blueprint.difficulty); 18 | 19 | @override 20 | void onCompleted() { 21 | _state = TaskState.completed; 22 | 23 | get().completeTask(this); 24 | super.onCompleted(); 25 | } 26 | 27 | void shipFeature() { 28 | assert(_state == TaskState.completed); 29 | _state = TaskState.rewarded; 30 | markDirty(); 31 | 32 | get().shipFeature(this); 33 | get().archiveTask(this); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_blueprint.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/skill.dart'; 2 | import 'package:dev_rpg/src/shared_state/game/task_prerequisite.dart'; 3 | import 'package:meta/meta.dart'; 4 | 5 | enum MiniGame { none, chomp, sphinx } 6 | 7 | /// A blueprint of a task. 8 | /// 9 | /// This class is immutable. Put runtime state into [Task]. 10 | @immutable 11 | class TaskBlueprint implements Prerequisite { 12 | final String name; 13 | 14 | final Map difficulty; 15 | 16 | /// The tasks (and their combinations) that the player must have completed 17 | /// before this task is available. 18 | final Prerequisite requirements; 19 | 20 | final int userReward; 21 | 22 | final int coinReward; 23 | 24 | final MiniGame miniGame; 25 | 26 | /// When sorting several blueprints, the ones with higher priority 27 | /// come first. This is `0` by default. 28 | final int priority; 29 | 30 | /// List of names of tasks that, when started, immediately make this task 31 | /// not selectable. For example, starting a 'Red Design' disallows 32 | /// 'Blue Design'. 33 | final List mutuallyExclusive; 34 | 35 | const TaskBlueprint(this.name, this.difficulty, 36 | {@required this.requirements, 37 | this.userReward = 100, 38 | this.coinReward = 80, 39 | this.priority = 0, 40 | this.mutuallyExclusive = const [], 41 | this.miniGame = MiniGame.none}) 42 | : assert(name != null), 43 | assert(difficulty != null), 44 | assert(requirements != null), 45 | assert(priority != null), 46 | assert(mutuallyExclusive != null); 47 | 48 | List get skillsNeeded => difficulty.keys.toList(growable: false); 49 | 50 | /// The task is satisfied if, and only if, it has already been done. 51 | @override 52 | bool isSatisfiedIn(Iterable done) => done.contains(this); 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_prerequisite.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/task_blueprint.dart'; 2 | 3 | /// A prerequisite that is always satisfied. 4 | const Prerequisite none = _None(); 5 | 6 | /// A prerequisite that is satisfied when all of the [prerequisites] 7 | /// are satisfied. 8 | class AllOf implements Prerequisite { 9 | final List prerequisites; 10 | 11 | const AllOf(this.prerequisites); 12 | 13 | @override 14 | bool isSatisfiedIn(Iterable done) { 15 | for (final prerequisite in prerequisites) { 16 | if (!prerequisite.isSatisfiedIn(done)) return false; 17 | } 18 | return true; 19 | } 20 | } 21 | 22 | /// A prerequisite that is satisfied when any of the [prerequisites] 23 | /// is satisfied. 24 | class AnyOf implements Prerequisite { 25 | final List prerequisites; 26 | 27 | const AnyOf(this.prerequisites); 28 | 29 | @override 30 | bool isSatisfiedIn(Iterable done) { 31 | for (final prerequisite in prerequisites) { 32 | if (prerequisite.isSatisfiedIn(done)) return true; 33 | } 34 | return false; 35 | } 36 | } 37 | 38 | /// An abstract class representing something that can be satisfied or not. 39 | /// 40 | /// Implemented by operators ([AnyOf], [AllOf]) and by [TaskBlueprint]. 41 | // ignore: one_member_abstracts 42 | abstract class Prerequisite { 43 | bool isSatisfiedIn(Iterable done); 44 | } 45 | 46 | /// A prerequisite that is always satisfied. Only the first tasks in the tree 47 | /// should have this prerequisite. 48 | class _None implements Prerequisite { 49 | const _None(); 50 | 51 | @override 52 | bool isSatisfiedIn(Iterable done) => true; 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/animations.dart: -------------------------------------------------------------------------------- 1 | part of task_tree; 2 | 3 | const _basicAnimations = TaskBlueprint( 4 | 'Basic Animations', 5 | {Skill.ux: 100}, 6 | coinReward: 200, 7 | requirements: AllOf([_alpha]), 8 | ); 9 | 10 | const _advancedMotionDesign = TaskBlueprint( 11 | 'Advanced Motion Design', 12 | {Skill.ux: 200, Skill.coordination: 50}, 13 | coinReward: 400, 14 | requirements: AllOf([_basicAnimations, _basicDesign, _uxTesting]), 15 | ); 16 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/backend_infrastructure.dart: -------------------------------------------------------------------------------- 1 | part of task_tree; 2 | 3 | const _backendInfrastructure = TaskBlueprint( 4 | 'Backend Infrastructure', 5 | {Skill.engineering: 100, Skill.coding: 100}, 6 | coinReward: 200, 7 | requirements: AllOf([_alpha]), 8 | ); 9 | 10 | const _fastBackend = TaskBlueprint( 11 | 'Fast Backend', 12 | {Skill.coding: 100}, 13 | coinReward: 250, 14 | requirements: AllOf([_backendInfrastructure]), 15 | ); 16 | 17 | const _scalableBackend = TaskBlueprint( 18 | 'Scalable backend', 19 | {Skill.coding: 100}, 20 | coinReward: 250, 21 | requirements: AllOf([_backendInfrastructure]), 22 | ); 23 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/beta.dart: -------------------------------------------------------------------------------- 1 | part of task_tree; 2 | 3 | const _beta = TaskBlueprint('Beta', {Skill.coordination: 100}, 4 | requirements: AllOf([ 5 | _alpha, 6 | // Mascot & icon. 7 | AnyOf([_dinosaurMascot, _birdMascot, _catMascot]), 8 | // Design philosophy. 9 | AnyOf([_retroDesign, _scifiDesign, _mainstreamDesign]), 10 | // Color theme. 11 | AnyOf([_redTheme, _greenTheme, _blueTheme]), 12 | ]), 13 | priority: 100, 14 | coinReward: 800, 15 | miniGame: MiniGame.chomp); 16 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/design.dart: -------------------------------------------------------------------------------- 1 | part of task_tree; 2 | 3 | const _basicDesign = TaskBlueprint( 4 | 'Basic Design', 5 | {Skill.ux: 100, Skill.coordination: 50}, 6 | requirements: AllOf([_alpha]), 7 | priority: 50, 8 | ); 9 | 10 | const _dinosaurMascot = TaskBlueprint( 11 | 'Dinosaur Mascot & Icon', 12 | {Skill.coordination: 100, Skill.ux: 50}, 13 | coinReward: 250, 14 | requirements: AllOf([_basicDesign]), 15 | mutuallyExclusive: ['Bird Mascot & Icon', 'Cat Mascot & Icon'], 16 | priority: 10, 17 | ); 18 | 19 | const _birdMascot = TaskBlueprint( 20 | 'Bird Mascot & Icon', 21 | {Skill.coordination: 100, Skill.ux: 50}, 22 | coinReward: 250, 23 | requirements: AllOf([_basicDesign]), 24 | mutuallyExclusive: ['Cat Mascot & Icon', 'Dinosaur Mascot & Icon'], 25 | priority: 10, 26 | ); 27 | 28 | const _catMascot = TaskBlueprint( 29 | 'Cat Mascot & Icon', 30 | {Skill.coordination: 100, Skill.ux: 50}, 31 | coinReward: 250, 32 | requirements: AllOf([_basicDesign]), 33 | mutuallyExclusive: ['Bird Mascot & Icon', 'Dinosaur Mascot & Icon'], 34 | priority: 10, 35 | ); 36 | 37 | const _retroDesign = TaskBlueprint( 38 | 'Retro Design', 39 | {Skill.ux: 100}, 40 | coinReward: 250, 41 | requirements: AllOf([_basicDesign]), 42 | mutuallyExclusive: ['Sci-Fi Design', 'Mainstream Design'], 43 | priority: 10, 44 | ); 45 | 46 | const _scifiDesign = TaskBlueprint( 47 | 'Sci-Fi Design', 48 | {Skill.ux: 100}, 49 | coinReward: 250, 50 | requirements: AllOf([_basicDesign]), 51 | mutuallyExclusive: ['Retro Design', 'Mainstream Design'], 52 | priority: 10, 53 | ); 54 | 55 | const _mainstreamDesign = TaskBlueprint( 56 | 'Mainstream Design', 57 | {Skill.ux: 50}, 58 | coinReward: 250, 59 | requirements: AllOf([_basicDesign]), 60 | mutuallyExclusive: ['Sci-Fi Design', 'Retro Design'], 61 | priority: 10, 62 | ); 63 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/geolocation.dart: -------------------------------------------------------------------------------- 1 | part of task_tree; 2 | 3 | const _geolocation = TaskBlueprint( 4 | 'Geolocation', 5 | {Skill.engineering: 100, Skill.coding: 50}, 6 | coinReward: 300, 7 | requirements: AllOf([_alpha]), 8 | ); 9 | 10 | const _arMessages = TaskBlueprint( 11 | 'AR Messages', 12 | {Skill.engineering: 50, Skill.coding: 100}, 13 | coinReward: 300, 14 | requirements: AllOf([_geolocation]), 15 | ); 16 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/image_messaging.dart: -------------------------------------------------------------------------------- 1 | part of task_tree; 2 | 3 | const _simpleImageMessaging = TaskBlueprint( 4 | 'Simple Image Messaging', 5 | {Skill.engineering: 100, Skill.coding: 50, Skill.ux: 50}, 6 | coinReward: 200, 7 | requirements: AllOf([_alpha]), 8 | ); 9 | 10 | const _animatedGifSupport = TaskBlueprint( 11 | 'Animated GIF Support', 12 | {Skill.engineering: 50, Skill.coding: 100}, 13 | coinReward: 200, 14 | requirements: AllOf([_simpleImageMessaging]), 15 | ); 16 | 17 | const _backendImageProcessing = TaskBlueprint( 18 | 'Backend Image Processing', 19 | {Skill.engineering: 50, Skill.coding: 100}, 20 | coinReward: 200, 21 | requirements: AllOf([_simpleImageMessaging, _backendInfrastructure]), 22 | ); 23 | 24 | const _memeGenerator = TaskBlueprint( 25 | 'Meme Generator', 26 | {Skill.coding: 50, Skill.ux: 50, Skill.coordination: 50}, 27 | coinReward: 200, 28 | requirements: AllOf([_simpleImageMessaging, _backendImageProcessing]), 29 | ); 30 | 31 | const _imageFilters = TaskBlueprint( 32 | 'Image Filters', 33 | {Skill.coding: 50, Skill.ux: 50}, 34 | coinReward: 200, 35 | requirements: AllOf([_backendImageProcessing, _backendInfrastructure]), 36 | ); 37 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/launch.dart: -------------------------------------------------------------------------------- 1 | part of task_tree; 2 | 3 | const _launch = TaskBlueprint('1.0', {Skill.coordination: 100}, 4 | requirements: AllOf([ 5 | _beta, 6 | // At least one post-beta polish feature. 7 | AnyOf([ 8 | _backendPerformanceOptimization, 9 | _backendScalabilityOptimization, 10 | _prelaunchMarketing, 11 | _backendHardening, 12 | _uiPerformanceOptimization, 13 | _uiPolish, 14 | ]), 15 | ]), 16 | priority: 100, 17 | coinReward: 5000, 18 | miniGame: MiniGame.sphinx); 19 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/natural_language.dart: -------------------------------------------------------------------------------- 1 | part of task_tree; 2 | 3 | const _naturalLanguageGeneration = TaskBlueprint( 4 | 'Natural Language Generation', 5 | {Skill.engineering: 100, Skill.coding: 100}, 6 | coinReward: 350, 7 | requirements: AllOf([_alpha]), 8 | ); 9 | 10 | const _naturalLanguageUnderstanding = TaskBlueprint( 11 | 'Natural Language Understanding', 12 | {Skill.engineering: 200, Skill.coding: 100}, 13 | coinReward: 350, 14 | requirements: AllOf([_alpha]), 15 | ); 16 | 17 | const _automatedBots = TaskBlueprint( 18 | 'Automated Bots', 19 | {Skill.coding: 100, Skill.ux: 50}, 20 | coinReward: 350, 21 | requirements: AllOf([_naturalLanguageGeneration]), 22 | ); 23 | 24 | const _conversationalChatbots = TaskBlueprint( 25 | 'Conversational Chatbots', 26 | {Skill.coding: 200, Skill.ux: 50}, 27 | coinReward: 350, 28 | requirements: 29 | AllOf([_naturalLanguageGeneration, _naturalLanguageUnderstanding]), 30 | ); 31 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/pre_alpha.dart: -------------------------------------------------------------------------------- 1 | part of task_tree; 2 | 3 | const _prototype = TaskBlueprint( 4 | 'Prototype', 5 | {Skill.coding: 30}, 6 | requirements: none, 7 | ); 8 | 9 | const _basicBackend = TaskBlueprint( 10 | 'Basic Backend', 11 | {Skill.coding: 100}, 12 | coinReward: 100, 13 | requirements: AllOf([_prototype]), 14 | priority: 10, 15 | ); 16 | 17 | const _basicUI = TaskBlueprint( 18 | 'Basic UI', 19 | {Skill.ux: 100}, 20 | coinReward: 100, 21 | requirements: AllOf([_prototype]), 22 | mutuallyExclusive: ['Programmer Art UI'], 23 | ); 24 | 25 | const _programmerArtUI = TaskBlueprint( 26 | 'Programmer Art UI', 27 | {Skill.coding: 100}, 28 | requirements: AllOf([_prototype]), 29 | mutuallyExclusive: ['Basic UI'], 30 | ); 31 | 32 | const _alpha = 33 | TaskBlueprint('Alpha release', {Skill.coordination: 100, Skill.coding: 100}, 34 | requirements: AllOf([ 35 | AnyOf([_programmerArtUI, _basicUI]), 36 | _basicBackend, 37 | ]), 38 | priority: 100, 39 | coinReward: 500, 40 | miniGame: MiniGame.chomp); 41 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/pre_launch.dart: -------------------------------------------------------------------------------- 1 | part of task_tree; 2 | 3 | const _backendPerformanceOptimization = TaskBlueprint( 4 | 'Backend Performance Optimization', 5 | {Skill.engineering: 100, Skill.coding: 100}, 6 | requirements: AllOf([_beta, _fastBackend]), 7 | coinReward: 350, 8 | priority: 50, 9 | ); 10 | 11 | const _backendScalabilityOptimization = TaskBlueprint( 12 | 'Backend Scalability Optimization', 13 | {Skill.engineering: 100, Skill.coding: 100}, 14 | requirements: AllOf([_beta, _scalableBackend]), 15 | coinReward: 350, 16 | priority: 50, 17 | ); 18 | 19 | const _prelaunchMarketing = TaskBlueprint( 20 | 'Pre-launch Marketing', 21 | {Skill.coordination: 100}, 22 | requirements: AllOf([_beta]), 23 | coinReward: 350, 24 | priority: 50, 25 | ); 26 | 27 | const _backendHardening = TaskBlueprint( 28 | 'Backend Hardening', 29 | {Skill.engineering: 100, Skill.coding: 100}, 30 | requirements: AllOf([_beta]), 31 | coinReward: 350, 32 | priority: 50, 33 | ); 34 | 35 | const _uiPerformanceOptimization = TaskBlueprint( 36 | 'UI Performance Optimization', 37 | {Skill.coding: 100, Skill.ux: 50}, 38 | requirements: AllOf([_beta]), 39 | coinReward: 350, 40 | priority: 50, 41 | ); 42 | 43 | const _uiPolish = TaskBlueprint( 44 | 'UI Polish', 45 | {Skill.coding: 100, Skill.ux: 100}, 46 | requirements: AllOf([_beta]), 47 | coinReward: 350, 48 | priority: 50, 49 | ); 50 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/responsive_design.dart: -------------------------------------------------------------------------------- 1 | part of task_tree; 2 | 3 | const _responsiveDesign = TaskBlueprint( 4 | 'Responsive Design', 5 | {Skill.ux: 100, Skill.coordination: 50, Skill.engineering: 50}, 6 | requirements: AllOf([_basicDesign]), 7 | coinReward: 150, 8 | ); 9 | 10 | const _tabletUI = TaskBlueprint( 11 | 'Tablet UI', 12 | {Skill.ux: 100, Skill.coding: 50}, 13 | requirements: AllOf([_basicDesign]), 14 | coinReward: 150, 15 | ); 16 | 17 | const _desktopUI = TaskBlueprint( 18 | 'Desktop UI', 19 | {Skill.ux: 100, Skill.coding: 50}, 20 | requirements: AllOf([_basicDesign]), 21 | coinReward: 250, 22 | ); 23 | 24 | const _iOSDesign = TaskBlueprint( 25 | 'Custom iOS Design', 26 | {Skill.ux: 100, Skill.coding: 50}, 27 | requirements: AllOf([_basicDesign]), 28 | coinReward: 150, 29 | ); 30 | 31 | const _webVersion = TaskBlueprint( 32 | 'Web Version', 33 | {Skill.coding: 100, Skill.ux: 50}, 34 | requirements: AllOf([_desktopUI]), 35 | coinReward: 350, 36 | ); 37 | 38 | const _desktopVersion = TaskBlueprint( 39 | 'Desktop Version', 40 | {Skill.coding: 100, Skill.ux: 50}, 41 | requirements: AllOf([_desktopUI]), 42 | coinReward: 350, 43 | ); 44 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/task_tree.dart: -------------------------------------------------------------------------------- 1 | library task_tree; 2 | 3 | import 'package:dev_rpg/src/shared_state/game/skill.dart'; 4 | import 'package:dev_rpg/src/shared_state/game/task_blueprint.dart'; 5 | import 'package:dev_rpg/src/shared_state/game/task_prerequisite.dart'; 6 | import 'package:dev_rpg/src/shared_state/game/task_tree/tree_hierarchy.dart'; 7 | 8 | part 'animations.dart'; 9 | part 'backend_infrastructure.dart'; 10 | part 'beta.dart'; 11 | part 'design.dart'; 12 | part 'geolocation.dart'; 13 | part 'image_messaging.dart'; 14 | part 'launch.dart'; 15 | part 'natural_language.dart'; 16 | part 'pre_alpha.dart'; 17 | part 'pre_launch.dart'; 18 | part 'responsive_design.dart'; 19 | part 'theme.dart'; 20 | part 'ux_testing.dart'; 21 | 22 | /// The set of all tasks. 23 | const Set taskTree = { 24 | // animations.dart 25 | _basicAnimations, 26 | _advancedMotionDesign, 27 | // backend_infrastructure.dart 28 | _backendInfrastructure, 29 | _fastBackend, 30 | _scalableBackend, 31 | // beta.dart 32 | _beta, 33 | // design.dart 34 | _basicDesign, 35 | _dinosaurMascot, 36 | _birdMascot, 37 | _catMascot, 38 | _retroDesign, 39 | _scifiDesign, 40 | _mainstreamDesign, 41 | // geolocation.dart 42 | _geolocation, 43 | _arMessages, 44 | // image_messaging.dart 45 | _simpleImageMessaging, 46 | _animatedGifSupport, 47 | _backendImageProcessing, 48 | _memeGenerator, 49 | _imageFilters, 50 | // launch.dart 51 | _launch, 52 | // natural_language.dart 53 | _naturalLanguageGeneration, 54 | _naturalLanguageUnderstanding, 55 | _automatedBots, 56 | _conversationalChatbots, 57 | // pre_alpha.dart 58 | _prototype, 59 | _basicUI, 60 | _basicBackend, 61 | _programmerArtUI, 62 | _alpha, 63 | // pre_launch.dart 64 | _backendPerformanceOptimization, 65 | _backendScalabilityOptimization, 66 | _prelaunchMarketing, 67 | _backendHardening, 68 | _uiPerformanceOptimization, 69 | _uiPolish, 70 | // responsive_design.dart 71 | _responsiveDesign, 72 | _tabletUI, 73 | _desktopUI, 74 | _iOSDesign, 75 | _webVersion, 76 | _desktopVersion, 77 | // theme.dart 78 | _basicTheme, 79 | _greenTheme, 80 | _redTheme, 81 | _blueTheme, 82 | // ux_testing.dart 83 | _uxTesting, 84 | _foreignLanguageUx, 85 | _accessibilityUx, 86 | _internationalization, 87 | _accessibility, 88 | }; 89 | 90 | // Store which tasks have been processed as we go to avoid finding 91 | // multiple dependencies which would cause a stack overflow. 92 | // Automatically exclude top level nodes from being found as dependencies 93 | // to other nodes. 94 | Set _processedTaskTree = {_prototype, _alpha, _beta, _launch}; 95 | 96 | // Build the top down top level categories. 97 | class TaskNode implements TreeData { 98 | final TaskBlueprint blueprint; 99 | @override 100 | final List children = []; 101 | 102 | TaskNode(this.blueprint, [bool isTop = false]) { 103 | _processedTaskTree.add(blueprint); 104 | // Find tasks that are direct dependents of this task. 105 | for (final otherBlueprint in taskTree) { 106 | if (_processedTaskTree.contains(otherBlueprint) || 107 | otherBlueprint == blueprint || 108 | !otherBlueprint.requirements.isSatisfiedIn([blueprint])) { 109 | continue; 110 | } 111 | children.add(TaskNode(otherBlueprint)); 112 | } 113 | 114 | if (isTop) { 115 | // Patch remaining items that are satisfied by this full set. 116 | // We have to do this because some items (like _advancedMotionDesign) 117 | // are only satisfied when multiple non-direct descendent items in the 118 | // tree are satisfied. 119 | // 120 | // ignore: literal_only_boolean_expressions 121 | while (true) { 122 | var patched = false; 123 | 124 | final allPrereqs = allPrerequisites; 125 | for (final otherBlueprint in taskTree) { 126 | if (_processedTaskTree.contains(otherBlueprint) || 127 | otherBlueprint == blueprint || 128 | !otherBlueprint.requirements.isSatisfiedIn(allPrereqs)) { 129 | continue; 130 | } 131 | // This changes the full list of prereqs, so patch again. 132 | children.add(TaskNode(otherBlueprint)); 133 | patched = true; 134 | break; 135 | } 136 | if (!patched) { 137 | break; 138 | } 139 | } 140 | } 141 | 142 | children.sort((TaskNode a, TaskNode b) => 143 | a.blueprint.priority.compareTo(b.blueprint.priority)); 144 | } 145 | 146 | List get allPrerequisites => [blueprint] 147 | ..addAll(children.expand((child) => child.allPrerequisites).toList()); 148 | } 149 | 150 | TaskNode prototypeTaskNode = TaskNode(_prototype, true); 151 | TaskNode alphaTaskNode = TaskNode(_alpha, true); 152 | TaskNode betaTaskNode = TaskNode(_beta, true); 153 | TaskNode launchTaskNode = TaskNode(_launch, true); 154 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/theme.dart: -------------------------------------------------------------------------------- 1 | part of task_tree; 2 | 3 | const _basicTheme = TaskBlueprint( 4 | 'Basic Theme', 5 | {Skill.ux: 100, Skill.coordination: 50}, 6 | requirements: AllOf([_alpha]), 7 | coinReward: 250, 8 | priority: 50, 9 | ); 10 | 11 | const _greenTheme = TaskBlueprint( 12 | 'Green Theme', 13 | {Skill.ux: 50}, 14 | requirements: AllOf([_basicTheme]), 15 | mutuallyExclusive: ['Red Theme', 'Blue Theme'], 16 | coinReward: 250, 17 | priority: 10, 18 | ); 19 | 20 | const _redTheme = TaskBlueprint( 21 | 'Red Theme', 22 | {Skill.ux: 50}, 23 | requirements: AllOf([_basicTheme]), 24 | mutuallyExclusive: ['Green Theme', 'Blue Theme'], 25 | coinReward: 250, 26 | priority: 10, 27 | ); 28 | 29 | const _blueTheme = TaskBlueprint( 30 | 'Blue Theme', 31 | {Skill.ux: 50}, 32 | requirements: AllOf([_basicTheme]), 33 | mutuallyExclusive: ['Green Theme', 'Red Theme'], 34 | coinReward: 250, 35 | priority: 10, 36 | ); 37 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/tree_hierarchy.dart: -------------------------------------------------------------------------------- 1 | /// An interface for hierarchical data. 2 | abstract class TreeData { 3 | List get children; 4 | } 5 | 6 | /// Flattened representation of the tree, making it possible 7 | /// to render the tree from virtualized (one dimensional) array. 8 | class FlattenedTreeData { 9 | /// Reference back to the hierarchical node represented by this 10 | /// flat structure. 11 | final TreeData data; 12 | 13 | /// Whether this node has a sibling right after it 14 | bool hasNextSibling = false; 15 | 16 | /// Whether this node contains a child and hence must draw an extra line 17 | /// under itself to that child. 18 | bool hasNextChild = false; 19 | 20 | /// The list of horizontal spaces (indentation) and whether or not they 21 | /// contain a line. 22 | List lines = []; 23 | 24 | FlattenedTreeData(this.data); 25 | } 26 | 27 | /// Flatten the hierarchical tree and store properties necessary to help 28 | /// define where connecting lines need to be drawn. 29 | List flattenTree(List list, 30 | [List flattened, List depth]) { 31 | flattened ??= []; 32 | depth ??= [true]; 33 | for (int i = 0; i < list.length; i++) { 34 | final item = list[i]; 35 | final flat = FlattenedTreeData(item); 36 | flat.lines = depth; 37 | flattened.add(flat); 38 | flat.hasNextSibling = i != list.length - 1; 39 | flat.hasNextChild = item.children.isNotEmpty; 40 | if (item.children.isNotEmpty) { 41 | List childDepth = List.from(depth); 42 | if (!flat.hasNextSibling && childDepth.length != 1) { 43 | childDepth[childDepth.length - 1] = false; 44 | } 45 | childDepth.add(true); 46 | flattenTree(item.children, flattened, childDepth); 47 | } 48 | } 49 | return flattened; 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/task_tree/ux_testing.dart: -------------------------------------------------------------------------------- 1 | part of task_tree; 2 | 3 | const _uxTesting = TaskBlueprint( 4 | 'UX Testing', 5 | {Skill.ux: 100}, 6 | coinReward: 120, 7 | requirements: AllOf([_alpha]), 8 | ); 9 | 10 | const _foreignLanguageUx = TaskBlueprint( 11 | 'Foreign Language UX', 12 | {Skill.ux: 100}, 13 | coinReward: 120, 14 | requirements: AllOf([_uxTesting]), 15 | ); 16 | 17 | const _accessibilityUx = TaskBlueprint( 18 | 'Accessibility UX', 19 | {Skill.ux: 100}, 20 | coinReward: 120, 21 | requirements: AllOf([_uxTesting]), 22 | ); 23 | 24 | const _internationalization = TaskBlueprint( 25 | 'Internationalization', 26 | {Skill.coding: 100, Skill.engineering: 100, Skill.coordination: 50}, 27 | requirements: AllOf([_foreignLanguageUx]), 28 | coinReward: 120, 29 | ); 30 | 31 | const _accessibility = TaskBlueprint( 32 | 'Accessibility', 33 | {Skill.coding: 100, Skill.engineering: 10, Skill.coordination: 50}, 34 | requirements: AllOf([_accessibilityUx]), 35 | coinReward: 120, 36 | ); 37 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/work_item.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:dev_rpg/src/shared_state/game/character.dart'; 4 | import 'package:dev_rpg/src/shared_state/game/skill.dart'; 5 | import 'package:dev_rpg/src/shared_state/game/src/aspect.dart'; 6 | import 'package:dev_rpg/src/shared_state/game/src/child_aspect.dart'; 7 | 8 | /// An item that a set of [Character]s with specific [Skill]s can work on. 9 | abstract class WorkItem extends Aspect with ChildAspect { 10 | final String name; 11 | final Map difficulty; 12 | final Map completion; 13 | 14 | List _assignedTeam; 15 | List get assignedTeam => _assignedTeam; 16 | 17 | bool get isComplete => percentComplete == 1; 18 | List get skillsNeeded => difficulty.keys.toList(growable: false); 19 | 20 | bool get isBeingWorkedOn => _assignedTeam?.isNotEmpty ?? false; 21 | 22 | /// Reduce the length of time to complete a task with a boost. 23 | double _boost = 1; 24 | 25 | WorkItem(this.name, this.difficulty) 26 | : completion = difficulty.map((Skill s, _) => MapEntry(s, 0)); 27 | 28 | void assignTeam(List team) { 29 | if (_assignedTeam != null) { 30 | // First, mark member who were unassigned as not busy. 31 | _assignedTeam.forEach((character) => character.isBusy = false); 32 | } 33 | _assignedTeam = team; 34 | _assignedTeam.forEach((character) => character.isBusy = true); 35 | markDirty(); 36 | } 37 | 38 | void freeTeam() { 39 | if (_assignedTeam == null) { 40 | return; 41 | } 42 | for (final character in _assignedTeam) { 43 | character.isBusy = false; 44 | } 45 | _assignedTeam = null; 46 | } 47 | 48 | void onCompleted() { 49 | // Free up the workers if they are done! 50 | freeTeam(); 51 | } 52 | 53 | @override 54 | void update() { 55 | if (isComplete || _assignedTeam == null) { 56 | super.update(); 57 | return; 58 | } 59 | 60 | for (final character in _assignedTeam) { 61 | for (final skill in character.prowess.keys) { 62 | if (!skillsNeeded.contains(skill)) continue; 63 | var prowess = character.prowess[skill]; 64 | completion[skill] += prowess * _boost; 65 | } 66 | } 67 | _boost = 1; 68 | if (percentComplete >= 1) { 69 | onCompleted(); 70 | } 71 | 72 | markDirty(); 73 | super.update(); 74 | } 75 | 76 | bool addBoost() { 77 | if (!isBeingWorkedOn) { 78 | return false; 79 | } 80 | _boost += 2.5; 81 | return true; 82 | } 83 | 84 | /// Get progress of this task. 85 | double get percentComplete { 86 | double required = 0; 87 | double completed = 0; 88 | 89 | // accumulate the required skills and the amount completed for each one 90 | difficulty.forEach((Skill skill, double amount) { 91 | required += amount; 92 | // Make sure to not count one skill overworking as another :) 93 | completed += min(amount, completion[skill] ?? 0); 94 | }); 95 | 96 | return completed / required; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/src/shared_state/game/world.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dev_rpg/src/shared_state/game/character_pool.dart'; 4 | import 'package:dev_rpg/src/shared_state/game/company.dart'; 5 | import 'package:dev_rpg/src/shared_state/game/src/aspect_container.dart'; 6 | import 'package:dev_rpg/src/shared_state/game/task.dart'; 7 | import 'package:dev_rpg/src/shared_state/game/task_pool.dart'; 8 | 9 | /// The state of the game world. 10 | /// 11 | /// Widgets should subscribe to aspects of the world (such as [projectPool]) 12 | /// instead of this whole world, unless they care about the most high-level 13 | /// stuff (like whether the simulation is running). 14 | class World extends AspectContainer { 15 | static const Duration newFeatureJoyDuration = Duration(seconds: 5); 16 | 17 | static Duration tickDuration = const Duration(milliseconds: 200); 18 | 19 | Timer _timer; 20 | double _joyAccumulation = 0; 21 | Timer _joyResetTimer; 22 | 23 | final CharacterPool characterPool; 24 | 25 | final TaskPool taskPool; 26 | 27 | final Company company; 28 | 29 | bool _isRunning = false; 30 | 31 | World() 32 | : characterPool = CharacterPool(), 33 | taskPool = TaskPool(), 34 | company = Company() { 35 | addAspect(characterPool); 36 | addAspect(taskPool); 37 | addAspect(company); 38 | } 39 | 40 | /// Returns `true` when the simulation is currently running. 41 | bool get isRunning => _isRunning; 42 | 43 | void pause() { 44 | if (_joyResetTimer?.isActive ?? false) { 45 | _joyResetTimer.cancel(); 46 | _resetJoy(); 47 | } 48 | 49 | _timer.cancel(); 50 | _isRunning = false; 51 | markDirty(); 52 | } 53 | 54 | void start() { 55 | _timer?.cancel(); 56 | _timer = Timer.periodic(tickDuration, _performTick); 57 | _isRunning = true; 58 | markDirty(); 59 | } 60 | 61 | void _performTick(Timer timer) { 62 | update(); 63 | } 64 | 65 | /// TODO: Feature joy should probably depend on the feature 66 | /// (might be another stat for the feature/task). 67 | static const double featureJoy = 5; 68 | 69 | void _resetJoy() { 70 | company.joy.number -= _joyAccumulation; 71 | _joyResetTimer = null; 72 | _joyAccumulation = 0; 73 | } 74 | 75 | void shipFeature(Task task) { 76 | // Todo: modify these values by how quickly the user completed the task 77 | // some bonus system? 78 | 79 | // Give some joy for the new feature, at least for a while. 80 | company.joy.number += featureJoy; 81 | 82 | _joyAccumulation += featureJoy; 83 | _joyResetTimer?.cancel(); 84 | _joyResetTimer = Timer(newFeatureJoyDuration, _resetJoy); 85 | 86 | company.award(task.blueprint.userReward, task.blueprint.coinReward); 87 | } 88 | 89 | void reset() { 90 | _joyResetTimer?.cancel(); 91 | _timer?.cancel(); 92 | _joyAccumulation = 0; 93 | company.reset(); 94 | characterPool.reset(); 95 | taskPool.reset(); 96 | start(); 97 | } 98 | 99 | @override 100 | void dispose() { 101 | _joyResetTimer?.cancel(); 102 | if (_timer.isActive) { 103 | _timer.cancel(); 104 | } 105 | super.dispose(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/src/shared_state/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | class User extends ChangeNotifier { 4 | final String name = 'Daring Developer'; 5 | 6 | @override 7 | String toString() => name; 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/style.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/skill.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | const Color contentColor = Color.fromRGBO(38, 38, 47, 1); 5 | const Color modalBackgroundColor = Color.fromRGBO(241, 241, 241, 1); 6 | const Color secondaryContentColor = Color.fromRGBO(111, 111, 118, 1); 7 | const Color skillTextColor = Color.fromRGBO(5, 59, 73, 1); 8 | const Color attentionColor = Color.fromRGBO(0, 152, 255, 1); 9 | const Color disabledColor = Color.fromRGBO(116, 116, 126, 1); 10 | const Color disabledTaskColor = Color.fromRGBO(38, 38, 47, 0.25); 11 | const Color treeLineColor = Color.fromRGBO(215, 215, 215, 1); 12 | const Color bugColor = Color.fromRGBO(236, 41, 117, 1); 13 | const Color statsSeparatorColor = Color.fromRGBO(57, 57, 71, 1); 14 | 15 | /// Maximum logical pixel width for a modal window. 16 | const double modalMaxWidth = 400; 17 | 18 | /// Once the logic screen pixel width exceeds this number, show the ultrawide 19 | /// layout. 20 | const double ultraWideLayoutThreshold = 1920; 21 | 22 | /// Once the logic screen pixel width exceeds this number, show the wide layout. 23 | const double wideLayoutThreshold = 800; 24 | 25 | /// Devices with a logical screen pixel width less than this value 26 | /// will not be permitted to rotate into landscape mode. 27 | const double blockLandscapeThreshold = 750; 28 | 29 | /// Ideal width of a character cell in the character hiring grid. This is used 30 | /// to compute the number of columns to display when viewing the character grid. 31 | const double idealCharacterWidth = 165; 32 | 33 | /// Ideal diameter of a particle in the hiring effect. The actual drawn particle 34 | /// size is computed based on a ratio of this diameter to the ideal character 35 | /// multiplied by the actual character width displayed. 36 | const double idealParticleSize = 10; 37 | 38 | const TextStyle contentSmallStyle = TextStyle( 39 | fontFamily: 'MontserratRegular', 40 | fontSize: 14, 41 | color: secondaryContentColor, 42 | ); 43 | 44 | const TextStyle contentStyle = TextStyle( 45 | fontFamily: 'MontserratRegular', 46 | fontSize: 16, 47 | color: contentColor, 48 | ); 49 | 50 | const TextStyle contentLargeStyle = TextStyle( 51 | fontFamily: 'MontserratRegular', 52 | fontSize: 24, 53 | color: contentColor, 54 | ); 55 | 56 | const TextStyle buttonTextStyle = TextStyle( 57 | fontFamily: 'MontserratMedium', 58 | fontSize: 16, 59 | color: contentColor, 60 | ); 61 | 62 | Map skillColor = { 63 | Skill.coding: const Color.fromRGBO(0, 179, 184, 1), 64 | Skill.engineering: const Color.fromRGBO(84, 114, 239, 1), 65 | Skill.ux: const Color.fromRGBO(184, 56, 72, 1), 66 | Skill.coordination: Colors.lightGreen 67 | }; 68 | 69 | Map skillFlareIcon = { 70 | Skill.coding: 'assets/flare/CodeIcon.flr', 71 | Skill.engineering: 'assets/flare/EngineeringIcon.flr', 72 | Skill.ux: 'assets/flare/UxIcon.flr', 73 | Skill.coordination: 'assets/flare/CoordinationIcon.flr' 74 | }; 75 | -------------------------------------------------------------------------------- /lib/src/style_sphinx/breathing_animations.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | // The base class for Breathing animations. Creates the necessary animation 4 | // controller that can be configured using a provided tween. 5 | class _BreathingBase extends StatefulWidget { 6 | final Widget Function(BuildContext, Animation) builder; 7 | final Tween tween; 8 | 9 | const _BreathingBase({ 10 | @required this.builder, 11 | @required this.tween, 12 | Key key, 13 | }) : super(key: key); 14 | 15 | @override 16 | _BreathingBaseState createState() => _BreathingBaseState(); 17 | } 18 | 19 | class _BreathingBaseState extends State<_BreathingBase> 20 | with SingleTickerProviderStateMixin { 21 | AnimationController _controller; 22 | Animation _animation; 23 | 24 | @override 25 | void initState() { 26 | // Create the animation controller to drive the offset animation 27 | _controller = AnimationController( 28 | vsync: this, 29 | duration: const Duration(milliseconds: 600), 30 | ); 31 | 32 | // Create an animation using the provided tween 33 | _animation = 34 | widget.tween.chain(CurveTween(curve: Curves.ease)).animate(_controller); 35 | 36 | // Start the animation and ensure it repeats indefinitely 37 | _controller 38 | ..forward() 39 | ..repeat(reverse: true); 40 | 41 | super.initState(); 42 | } 43 | 44 | @override 45 | void dispose() { 46 | _controller.dispose(); 47 | super.dispose(); 48 | } 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | return widget.builder(context, _animation); 53 | } 54 | } 55 | 56 | // Creates a Bouncing "Breathing Animation." It does so using an Offset Tween 57 | // and a SlideTransition. 58 | class Bouncy extends StatelessWidget { 59 | final Widget child; 60 | 61 | const Bouncy({@required this.child, Key key}) : super(key: key); 62 | 63 | @override 64 | Widget build(BuildContext context) { 65 | return _BreathingBase( 66 | tween: Tween( 67 | begin: const Offset(0, -0.05), 68 | end: const Offset(0, 0.05), 69 | ), 70 | builder: (context, animation) { 71 | return SlideTransition( 72 | position: animation, 73 | child: child, 74 | ); 75 | }, 76 | ); 77 | } 78 | } 79 | 80 | // Creates a Breathing animation that fades a Widget in and out. It does so 81 | // using a Tween with a FadeTransition Widget 82 | class Faded extends StatelessWidget { 83 | final Widget child; 84 | final double begin; 85 | final double end; 86 | 87 | const Faded({ 88 | @required this.child, 89 | this.begin = 1, 90 | this.end = 0.8, 91 | Key key, 92 | }) : super(key: key); 93 | 94 | @override 95 | Widget build(BuildContext context) { 96 | return _BreathingBase( 97 | tween: Tween( 98 | begin: begin, 99 | end: end, 100 | ), 101 | builder: (context, animation) { 102 | return FadeTransition( 103 | opacity: animation, 104 | child: child, 105 | ); 106 | }, 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/src/style_sphinx/kittens.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | enum KittyType { orange, yellow } 4 | 5 | class KittyBed extends StatelessWidget { 6 | static const ImageProvider redProvider = 7 | AssetImage('assets/style_sphinx/red_bed.png'); 8 | static const ImageProvider greenProvider = 9 | AssetImage('assets/style_sphinx/green_bed.png'); 10 | 11 | final KittyType type; 12 | 13 | const KittyBed({@required this.type, Key key}) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) => 17 | _SizedItem(child: Image(image: _provider)); 18 | 19 | ImageProvider get _provider { 20 | switch (type) { 21 | case KittyType.orange: 22 | return redProvider; 23 | case KittyType.yellow: 24 | default: 25 | return greenProvider; 26 | } 27 | } 28 | } 29 | 30 | class Kitty extends StatelessWidget { 31 | static const ImageProvider orangeProvider = 32 | AssetImage('assets/style_sphinx/orange_cat.png'); 33 | static const ImageProvider yellowProvider = 34 | AssetImage('assets/style_sphinx/yellow_cat.png'); 35 | 36 | final KittyType type; 37 | 38 | const Kitty({@required this.type, Key key}) : super(key: key); 39 | 40 | @override 41 | Widget build(BuildContext context) => 42 | _SizedItem(child: Image(image: _provider)); 43 | 44 | ImageProvider get _provider { 45 | switch (type) { 46 | case KittyType.orange: 47 | return orangeProvider; 48 | case KittyType.yellow: 49 | default: 50 | return yellowProvider; 51 | } 52 | } 53 | } 54 | 55 | class _SizedItem extends StatelessWidget { 56 | final Widget child; 57 | 58 | const _SizedItem({@required this.child, Key key}) : super(key: key); 59 | 60 | @override 61 | Widget build(BuildContext context) { 62 | final screenSize = MediaQuery.of(context).size; 63 | final width = screenSize.width; 64 | final height = screenSize.height; 65 | 66 | return ConstrainedBox( 67 | constraints: BoxConstraints( 68 | maxWidth: width < 600 ? 120 : width < 900 ? 160 : 200, 69 | maxHeight: height < 600 ? 120 : height < 900 ? 160 : 200, 70 | ), 71 | child: child, 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/style_sphinx/question_arguments.dart: -------------------------------------------------------------------------------- 1 | /// Arguments that drive the game from one screen to the next. 2 | class QuestionArguments { 3 | final List questionRoutes; 4 | final int currentIndex; 5 | 6 | QuestionArguments({ 7 | this.questionRoutes = const [], 8 | this.currentIndex = 0, 9 | }); 10 | 11 | bool get hasNextQuestion => currentIndex + 1 < questionRoutes.length; 12 | 13 | String get routeName => questionRoutes[currentIndex]; 14 | 15 | QuestionArguments nextQuestion() { 16 | if (!hasNextQuestion) throw StateError('No next question available'); 17 | 18 | return QuestionArguments( 19 | questionRoutes: questionRoutes, 20 | currentIndex: currentIndex + 1, 21 | ); 22 | } 23 | 24 | @override 25 | String toString() => 26 | '''QuestionArguments{questionRoutes: $questionRoutes, currentIndex: $currentIndex}'''; 27 | 28 | @override 29 | bool operator ==(Object other) => 30 | identical(this, other) || 31 | other is QuestionArguments && 32 | runtimeType == other.runtimeType && 33 | questionRoutes == other.questionRoutes && 34 | currentIndex == other.currentIndex; 35 | 36 | @override 37 | int get hashCode => questionRoutes.hashCode ^ currentIndex.hashCode; 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/style_sphinx/question_scaffold.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class QuestionScaffold extends StatelessWidget { 4 | final Widget question; 5 | final Widget expected; 6 | final Widget actual; 7 | 8 | const QuestionScaffold({ 9 | Key key, 10 | this.question, 11 | this.expected, 12 | this.actual, 13 | }) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Scaffold( 18 | backgroundColor: const Color.fromRGBO(246, 216, 204, 1), 19 | body: Column( 20 | crossAxisAlignment: CrossAxisAlignment.start, 21 | children: [ 22 | SafeArea( 23 | child: Padding( 24 | padding: const EdgeInsets.only( 25 | left: 24, 26 | right: 24, 27 | bottom: 24, 28 | top: 8, 29 | ), 30 | child: question, 31 | ), 32 | ), 33 | Expanded( 34 | child: Stack( 35 | children: [ 36 | Positioned.fill( 37 | child: Container( 38 | color: const Color.fromRGBO(252, 235, 227, 1), 39 | ), 40 | ), 41 | Positioned.fill( 42 | child: Padding( 43 | padding: const EdgeInsets.only( 44 | left: 24, 45 | right: 24, 46 | top: 0, 47 | bottom: 48, 48 | ), 49 | child: expected, 50 | ), 51 | ), 52 | Positioned.fill( 53 | child: Padding( 54 | padding: const EdgeInsets.only( 55 | left: 24, 56 | right: 24, 57 | top: 0, 58 | bottom: 48, 59 | ), 60 | child: actual, 61 | ), 62 | ), 63 | ], 64 | ), 65 | ), 66 | ], 67 | ), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/src/style_sphinx/sphinx_buttton.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SphinxButton extends StatelessWidget { 4 | final VoidCallback onPressed; 5 | final Widget child; 6 | 7 | const SphinxButton({ 8 | @required this.onPressed, 9 | @required this.child, 10 | Key key, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final radius = BorderRadius.circular(10); 16 | 17 | return Material( 18 | shape: RoundedRectangleBorder(borderRadius: radius), 19 | color: const Color.fromRGBO(242, 124, 78, 1), 20 | child: InkWell( 21 | borderRadius: radius, 22 | splashColor: const Color.fromRGBO(242, 124, 78, 1), 23 | child: Container( 24 | padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 16), 25 | child: DefaultTextStyle( 26 | child: child, 27 | style: const TextStyle( 28 | fontFamily: 'MontserratMedium', 29 | fontSize: 16, 30 | fontWeight: FontWeight.bold, 31 | color: Color.fromRGBO(85, 34, 34, 1)), 32 | ), 33 | ), 34 | onTap: onPressed, 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/style_sphinx/sphinx_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | class SphinxImage extends StatelessWidget { 4 | static const ImageProvider provider = 5 | AssetImage('assets/style_sphinx/sphinx.png'); 6 | 7 | const SphinxImage(); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return Image(image: provider); 12 | } 13 | } 14 | 15 | class SphinxWithoutGlassesImage extends StatelessWidget { 16 | static const ImageProvider provider = 17 | AssetImage('assets/style_sphinx/sphinx_no_glasses.png'); 18 | 19 | const SphinxWithoutGlassesImage(); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | return Image(image: provider); 24 | } 25 | } 26 | 27 | class SphinxGlassesImage extends StatelessWidget { 28 | static const ImageProvider provider = 29 | AssetImage('assets/style_sphinx/sunglasses.png'); 30 | 31 | const SphinxGlassesImage(); 32 | 33 | @override 34 | Widget build(BuildContext context) { 35 | return Image(image: provider); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/src/style_sphinx/text_bubble.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | 4 | /// Determines whether the indicator should point left or right 5 | enum TextBubbleDirection { left, right } 6 | 7 | /// Place content inside a Text Bubble 8 | class TextBubble extends StatelessWidget { 9 | final Widget child; 10 | final TextBubbleDirection direction; 11 | final Radius radius; 12 | final Size indicatorSize; 13 | final double shadowOffset; 14 | final EdgeInsets padding; 15 | 16 | const TextBubble({ 17 | @required this.child, 18 | Key key, 19 | this.padding = const EdgeInsets.all(16), 20 | this.direction = TextBubbleDirection.left, 21 | this.radius = const Radius.circular(10), 22 | this.indicatorSize = const Size(35, 20), 23 | this.shadowOffset = 10, 24 | }) : super(key: key); 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Stack( 29 | children: [ 30 | Positioned.fill( 31 | child: CustomPaint( 32 | painter: _TextBubbleBackgroundPainter( 33 | direction: direction, 34 | radius: radius, 35 | indicatorSize: indicatorSize, 36 | shadowOffset: shadowOffset, 37 | ), 38 | ), 39 | ), 40 | // Text 41 | Column( 42 | children: [ 43 | Padding( 44 | padding: EdgeInsets.only( 45 | left: padding.left, 46 | right: padding.right, 47 | top: padding.top, 48 | bottom: padding.bottom + indicatorSize.height + shadowOffset, 49 | ), 50 | child: DefaultTextStyle( 51 | child: child, 52 | style: const TextStyle( 53 | color: Colors.black, 54 | fontFamily: 'MontserratMedium', 55 | fontSize: 16, 56 | ), 57 | ), 58 | ), 59 | ], 60 | ), 61 | ], 62 | ); 63 | } 64 | } 65 | 66 | // Create a "Text Bubble" using a CustomPainter. 67 | class _TextBubbleBackgroundPainter extends CustomPainter { 68 | final TextBubbleDirection direction; 69 | final Radius radius; 70 | final Size indicatorSize; 71 | final double shadowOffset; 72 | 73 | _TextBubbleBackgroundPainter({ 74 | @required this.direction, 75 | @required this.radius, 76 | @required this.indicatorSize, 77 | @required this.shadowOffset, 78 | }); 79 | 80 | @override 81 | void paint(Canvas canvas, Size size) { 82 | final bubbleBottom = size.height - indicatorSize.height - shadowOffset; 83 | 84 | // Start the path and create the first two rounded corners 85 | final path = Path() 86 | ..moveTo( 87 | radius.x, 88 | 0, 89 | ) 90 | ..lineTo(size.width - radius.x, 0) 91 | ..arcToPoint( 92 | Offset(size.width, radius.x), 93 | radius: radius, 94 | ) 95 | ..lineTo(size.width, bubbleBottom - radius.y) 96 | ..arcToPoint( 97 | Offset(size.width - radius.x, bubbleBottom), 98 | radius: radius, 99 | ); 100 | 101 | // After drawing the top and bottom-right corner, use the direction to 102 | // determine which indicator to draw: Facing left or right. 103 | if (direction == TextBubbleDirection.left) { 104 | final startX = size.width - radius.x - size.width / 6; 105 | final endX = startX - indicatorSize.width; 106 | 107 | path 108 | ..lineTo(startX, bubbleBottom) 109 | ..lineTo(endX, bubbleBottom + indicatorSize.height) 110 | ..lineTo(endX, bubbleBottom); 111 | } else { 112 | final startX = radius.x + indicatorSize.width + size.width / 6; 113 | final endX = startX - indicatorSize.width; 114 | 115 | path 116 | ..lineTo(startX, bubbleBottom) 117 | ..lineTo(startX, bubbleBottom + indicatorSize.height) 118 | ..lineTo(endX, bubbleBottom); 119 | } 120 | 121 | // Complete the path by creating the last two rounded corners 122 | path 123 | ..lineTo(radius.x, bubbleBottom) 124 | ..arcToPoint( 125 | Offset(0, bubbleBottom - radius.y), 126 | radius: radius, 127 | ) 128 | ..lineTo(0, radius.y) 129 | ..arcToPoint( 130 | Offset(radius.x, 0), 131 | radius: radius, 132 | ); 133 | 134 | // Paint the shadow 135 | canvas.save(); 136 | canvas.translate(0, shadowOffset); 137 | canvas.drawPath( 138 | path, 139 | Paint() 140 | ..color = const Color.fromRGBO(85, 34, 34, 0.29) 141 | ..style = PaintingStyle.fill, 142 | ); 143 | 144 | // Then, paint the white background 145 | canvas.restore(); 146 | canvas.drawPath( 147 | path, 148 | Paint() 149 | ..color = Colors.white 150 | ..style = PaintingStyle.fill, 151 | ); 152 | } 153 | 154 | @override 155 | bool shouldRepaint(_TextBubbleBackgroundPainter old) { 156 | return direction != old.direction; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib/src/widgets/app_bar/coin_badge.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/company.dart'; 2 | import 'package:dev_rpg/src/widgets/app_bar/stat_badge.dart'; 3 | 4 | /// Visually indicates the amount of capital the [Company] has amassed for this 5 | /// game session. 6 | class CoinBadge extends StatBadge { 7 | CoinBadge(StatValue statValue, {double scale = 1, bool isWide = false}) 8 | : super('Capital', statValue, 9 | flare: 'assets/flare/Coin.flr', scale: scale, isWide: isWide); 10 | 11 | /// Play the indicator animation after this value changes by at least 5 coins. 12 | @override 13 | int get celebrateAfter => 5; 14 | } 15 | -------------------------------------------------------------------------------- /lib/src/widgets/app_bar/joy_badge.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/company.dart'; 2 | import 'package:dev_rpg/src/widgets/app_bar/stat_badge.dart'; 3 | 4 | /// Visually indicates the level of joy via a Flare animation with distinct 5 | /// sad, happy, neutral states and a call to attention animation whenever 6 | /// the numeric value changes. 7 | class JoyBadge extends StatBadge { 8 | JoyBadge(StatValue statValue, {double scale = 1, bool isWide = false}) 9 | : super('Joy', statValue, 10 | flare: 'assets/flare/Joy.flr', scale: scale, isWide: isWide); 11 | 12 | /// Play the celebration animation whenever there's a change 13 | /// 0 is a flag for always in this case 14 | @override 15 | double get celebrateAfter => 0; 16 | 17 | @override 18 | JoyBadgeState createState() => JoyBadgeState(); 19 | } 20 | 21 | /// The three emotions this widget can display 22 | enum _Emotion { sad, happy, neutral } 23 | 24 | class JoyBadgeState extends StatBadgeState { 25 | _Emotion _emotion; 26 | 27 | _Emotion get emotion => _emotion; 28 | set emotion(_Emotion value) { 29 | if (value == _emotion) { 30 | return; 31 | } 32 | _emotion = value; 33 | switch (_emotion) { 34 | case _Emotion.sad: 35 | controls.play('sad'); 36 | break; 37 | case _Emotion.happy: 38 | controls.play('happy'); 39 | break; 40 | case _Emotion.neutral: 41 | controls.play('neutral'); 42 | break; 43 | } 44 | } 45 | 46 | @override 47 | void valueChanged() { 48 | double joy = widget.statValue.number; 49 | if (joy < 0) { 50 | emotion = _Emotion.sad; 51 | } else if (joy > 3) { 52 | emotion = _Emotion.happy; 53 | } else { 54 | emotion = _Emotion.neutral; 55 | } 56 | super.valueChanged(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/widgets/app_bar/stat_separator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../../style.dart'; 4 | 5 | /// A widget to place between [StatBadge] widgets to show clear 6 | /// separation between them. 7 | class StatSeparator extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | return Container( 11 | width: 1, 12 | color: statsSeparatorColor, 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/src/widgets/app_bar/users_badge.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/company.dart'; 2 | import 'package:dev_rpg/src/widgets/app_bar/stat_badge.dart'; 3 | 4 | /// Visually indicates the number of users the [Company] has amassed for this 5 | /// game session. 6 | class UsersBadge extends StatBadge { 7 | UsersBadge(StatValue statValue, 8 | {double scale = 1, bool isWide = false}) 9 | : super('Users', statValue, 10 | flare: 'assets/flare/Users.flr', scale: scale, isWide: isWide); 11 | 12 | /// Play a celebration/call to attention animation after the value changes 13 | /// by 100 users. 14 | @override 15 | double get celebrateAfter => 100; 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/widgets/buttons/welcome_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/style.dart'; 2 | import 'package:dev_rpg/src/widgets/buttons/wide_button.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// A styled button that animates its accent color. 6 | class WelcomeButton extends StatefulWidget { 7 | final Widget child; 8 | final Color background; 9 | final IconData icon; 10 | final String label; 11 | final double fontSize; 12 | @required 13 | final VoidCallback onPressed; 14 | const WelcomeButton({ 15 | Key key, 16 | this.child, 17 | this.onPressed, 18 | this.background, 19 | this.icon, 20 | this.label, 21 | this.fontSize = 16, 22 | }) : super(key: key); 23 | 24 | @override 25 | _WelcomeButtonState createState() => _WelcomeButtonState(); 26 | } 27 | 28 | class _WelcomeButtonState extends State 29 | with SingleTickerProviderStateMixin { 30 | AnimationController _animationController; 31 | Animation _colorTween; 32 | 33 | @override 34 | void initState() { 35 | _animationController = AnimationController( 36 | vsync: this, 37 | duration: const Duration(milliseconds: 3100), 38 | ); 39 | _colorTween = ColorTween(begin: widget.background, end: widget.background) 40 | .animate(_animationController); 41 | super.initState(); 42 | } 43 | 44 | @override 45 | void didUpdateWidget(WelcomeButton oldWidget) { 46 | setState(() { 47 | _colorTween = ColorTween(begin: _colorTween.value, end: widget.background) 48 | .animate(_animationController); 49 | _animationController.reset(); 50 | _animationController.forward(); 51 | }); 52 | super.didUpdateWidget(oldWidget); 53 | } 54 | 55 | @override 56 | void dispose() { 57 | _animationController?.dispose(); 58 | super.dispose(); 59 | } 60 | 61 | @override 62 | Widget build(BuildContext context) { 63 | return AnimatedBuilder( 64 | animation: _animationController, 65 | builder: (context, child) => WideButton( 66 | onPressed: widget.onPressed, 67 | background: _colorTween.value, 68 | child: Align( 69 | alignment: Alignment.centerLeft, 70 | child: Row( 71 | children: [ 72 | widget.icon == null 73 | ? Container() 74 | : Padding( 75 | padding: const EdgeInsets.only(right: 13), 76 | child: Icon( 77 | widget.icon, 78 | size: widget.fontSize, 79 | color: Colors.white.withOpacity(0.5), 80 | ), 81 | ), 82 | Text(widget.label.toUpperCase(), 83 | style: buttonTextStyle.apply( 84 | color: Colors.white, 85 | fontSizeDelta: 86 | widget.fontSize - buttonTextStyle.fontSize)) 87 | ], 88 | ), 89 | ), 90 | ), 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/src/widgets/buttons/wide_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A styled button that takes up all of the available horizontal space. 4 | class WideButton extends StatelessWidget { 5 | final Key buttonKey; 6 | final Widget child; 7 | final Color background; 8 | final Color shadowColor; 9 | final bool enabled; 10 | @required 11 | final VoidCallback onPressed; 12 | 13 | /// Use the padding tweak to allow negative adjustments to padding. 14 | final EdgeInsets paddingTweak; 15 | 16 | const WideButton({ 17 | this.child, 18 | this.onPressed, 19 | this.background, 20 | this.paddingTweak = const EdgeInsets.all(0), 21 | this.buttonKey, 22 | this.shadowColor, 23 | this.enabled = true, 24 | }); 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | return Container( 29 | constraints: const BoxConstraints(minWidth: double.infinity), 30 | decoration: shadowColor != null 31 | ? BoxDecoration( 32 | boxShadow: [ 33 | BoxShadow( 34 | color: shadowColor, 35 | offset: const Offset(0, 10), 36 | blurRadius: 10), 37 | ], 38 | ) 39 | : null, 40 | child: FlatButton( 41 | key: buttonKey, 42 | padding: EdgeInsets.only( 43 | left: 20 + paddingTweak.left, 44 | right: 20 + paddingTweak.right, 45 | top: 11 + paddingTweak.top, 46 | bottom: 11 + paddingTweak.bottom), 47 | shape: RoundedRectangleBorder( 48 | borderRadius: BorderRadius.circular(9), 49 | ), 50 | onPressed: enabled ? onPressed : null, 51 | color: background, 52 | child: child), 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/widgets/flare/desaturated_actor.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | import 'package:flare_dart/actor_flags.dart'; 3 | import 'package:flare_dart/actor_image.dart'; 4 | import 'package:flare_flutter/flare.dart'; 5 | import 'package:vector_math/vector_math.dart'; 6 | 7 | // This is the custom actor shape we create with custom painting 8 | class DesaturatedActor extends FlutterActor { 9 | bool _desaturate = true; 10 | bool get desaturate => _desaturate; 11 | set desaturate(bool value) { 12 | if (_desaturate == value) { 13 | return; 14 | } 15 | _desaturate = value; 16 | artboard.addDirt(artboard.root, DirtyFlags.paintDirty, true); 17 | } 18 | 19 | @override 20 | ActorImage makeImageNode() { 21 | return DesaturatedActorImage(); 22 | } 23 | } 24 | 25 | /// Custom ActorImage to draw in place of regular Flare ActorImage 26 | /// We use this to override the paint and do custom color filtering. 27 | class DesaturatedActorImage extends FlutterActorImage { 28 | @override 29 | void onPaintUpdated(ui.Paint paint) { 30 | if (!(artboard.actor as DesaturatedActor).desaturate) { 31 | paint.colorFilter = null; 32 | return; 33 | } 34 | // Light blue tinge. 35 | ui.Color tint = const ui.Color.fromRGBO(222, 222, 255, 1); 36 | double rf = tint.red / 255; 37 | double gf = tint.green / 255; 38 | double bf = tint.blue / 255; 39 | 40 | Matrix3 greyMatrix = Matrix3.fromList([ 41 | 0.21, 42 | 0.75, 43 | 0.077, 44 | 0.21, 45 | 0.75, 46 | 0.077, 47 | 0.21, 48 | 0.75, 49 | 0.077, 50 | ]); 51 | 52 | Matrix3 tintMatrix = Matrix3.fromList([ 53 | rf, 54 | 0, 55 | 0, 56 | 0, 57 | gf, 58 | 0, 59 | 0, 60 | 0, 61 | bf, 62 | ]); 63 | 64 | greyMatrix.multiply(tintMatrix); 65 | paint.colorFilter = ui.ColorFilter.matrix([ 66 | greyMatrix[0], 67 | greyMatrix[1], 68 | greyMatrix[2], 69 | 0, 70 | 0, 71 | greyMatrix[3], 72 | greyMatrix[4], 73 | greyMatrix[5], 74 | 0, 75 | 0, 76 | greyMatrix[6], 77 | greyMatrix[7], 78 | greyMatrix[8], 79 | 0, 80 | 0, 81 | 0, 82 | 0, 83 | 0, 84 | 1, 85 | 0 86 | ]); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/src/widgets/flare/hiring_particles.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:ui'; 3 | 4 | /// Data for each particle in the [HiringParticles] system. 5 | /// [HiringParticles] will create and destroy as many [HiringParticle] 6 | /// instances as it needs to show the effect. 7 | class HiringParticle { 8 | /// The position of the particle. 9 | Offset offset; 10 | 11 | /// The brightness of the particle. 12 | double opacity; 13 | 14 | /// The scale in 0-1 range, gets multiplied by the desired particle display 15 | /// size by [HiringParticles] at draw time. 16 | double scale; 17 | 18 | /// The phase the particle is in for its horizontal motion which is driven 19 | /// by an LFO (simple sin wave). 20 | double phase; 21 | } 22 | 23 | /// This is the class that manages the list of particles that are displayed 24 | /// for the hiring bust. It's reponsible for instancing, destroying, moving, and 25 | /// painting the particles. 26 | class HiringParticles { 27 | final Color color; 28 | final List _particles = []; 29 | static const int particleCount = 20; 30 | double particleSize = 10; 31 | final Random _random = Random(); 32 | double elapsedSinceEmission = 0; 33 | 34 | HiringParticles({this.color}); 35 | void advance(double elapsedSeconds, Size size) { 36 | if (_particles.isEmpty) { 37 | while (_particles.length < particleCount) { 38 | _particles.add(HiringParticle() 39 | ..offset = Offset(_random.nextDouble() * size.width, 40 | _random.nextDouble() * size.height) 41 | ..opacity = 0 42 | ..phase = _random.nextDouble() 43 | ..scale = _random.nextDouble()); 44 | } 45 | } 46 | 47 | List deadParticles = []; 48 | for (final HiringParticle particle in _particles) { 49 | particle.phase += elapsedSeconds; 50 | particle.offset = Offset(particle.offset.dx, 51 | particle.offset.dy - size.height * elapsedSeconds * 0.5); 52 | if (particle.offset.dy < size.height / 2) { 53 | particle.opacity -= elapsedSeconds; 54 | } else { 55 | particle.opacity += elapsedSeconds; 56 | } 57 | particle.scale -= min(1, elapsedSeconds / 5); 58 | if (particle.opacity < 0 || particle.scale < 0) { 59 | deadParticles.add(particle); 60 | } 61 | } 62 | 63 | elapsedSinceEmission += elapsedSeconds; 64 | // no more than two per advance 65 | if (elapsedSinceEmission > 0.1 && _particles.length < particleCount) { 66 | elapsedSinceEmission = 0; 67 | _particles.add(HiringParticle() 68 | ..offset = Offset(_random.nextDouble() * size.width, size.height) 69 | ..opacity = 0 70 | ..phase = _random.nextDouble() 71 | ..scale = 0.5 + 0.5 * _random.nextDouble()); 72 | } 73 | 74 | deadParticles.forEach(_particles.remove); 75 | } 76 | 77 | void paint(Canvas canvas, Offset offset) { 78 | double fullRadius = particleSize / 2; 79 | 80 | for (final HiringParticle particle in _particles) { 81 | Offset po = offset + particle.offset; 82 | double radius = fullRadius * particle.scale; 83 | double size = radius * 2; 84 | double ox = sin(particle.phase * 2) * particleSize * particle.scale; 85 | canvas.drawOval( 86 | Rect.fromLTWH( 87 | ox + po.dx - radius, po.dy + fullRadius - radius, size, size), 88 | Paint() 89 | ..style = PaintingStyle.fill 90 | ..color = 91 | color.withOpacity(particle.opacity.clamp(0, 1).toDouble())); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/src/widgets/flare/skill_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/skill.dart'; 2 | import 'package:dev_rpg/src/style.dart'; 3 | import 'package:flare_flutter/flare_actor.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class SkillIcon extends StatelessWidget { 7 | final double width; 8 | final double height; 9 | final Skill skill; 10 | final double opacity; 11 | final Color color; 12 | const SkillIcon(this.skill, 13 | {this.width = 19, 14 | this.height = 16, 15 | this.opacity = 1, 16 | this.color = Colors.white}); 17 | @override 18 | Widget build(BuildContext context) { 19 | return SizedBox( 20 | width: width, 21 | height: height, 22 | child: FlareActor(skillFlareIcon[skill], 23 | color: color.withOpacity(opacity), 24 | alignment: Alignment.topCenter, 25 | shouldClip: false, 26 | fit: BoxFit.contain, 27 | animation: 'idle'), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/widgets/flare/warmup_flare.dart: -------------------------------------------------------------------------------- 1 | import 'package:flare_flutter/flare_cache.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | const _filesToWarmup = [ 5 | "assets/flare/CodeIcon.flr", 6 | "assets/flare/Coin.flr", 7 | "assets/flare/CoordinationIcon.flr", 8 | "assets/flare/CowboyCoder.flr", 9 | "assets/flare/Designer.flr", 10 | "assets/flare/EngineeringIcon.flr", 11 | "assets/flare/Joy.flr", 12 | "assets/flare/NotificationIcon.flr", 13 | "assets/flare/ProgramManager.flr", 14 | "assets/flare/SelectArrow.flr", 15 | "assets/flare/Sourcerer.flr", 16 | "assets/flare/Tester.flr", 17 | "assets/flare/TheArchitect.flr", 18 | "assets/flare/TheHacker.flr", 19 | "assets/flare/TheJack.flr", 20 | "assets/flare/TheRefactorer.flr", 21 | "assets/flare/Users.flr", 22 | "assets/flare/UxIcon.flr", 23 | "assets/flare/UXResearcher.flr", 24 | ]; 25 | 26 | /// Ensure all Flare assets used by this app are cached and ready to 27 | /// be displayed as quickly as possible. 28 | Future warmupFlare() async { 29 | for (final filename in _filesToWarmup) { 30 | await cachedActor(rootBundle, filename); 31 | await Future.delayed(const Duration(milliseconds: 16)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/widgets/flare/work_team.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/game_screen/character_style.dart'; 2 | import 'package:dev_rpg/src/shared_state/game/character.dart'; 3 | import 'package:dev_rpg/src/shared_state/game/skill.dart'; 4 | import 'package:dev_rpg/src/widgets/flare/hiring_bust.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/rendering.dart'; 7 | 8 | /// Widget that shows the team that is actively working on a work item. 9 | class WorkTeam extends StatefulWidget { 10 | final List team; 11 | final List skillsNeeded; 12 | final bool isComplete; 13 | const WorkTeam({this.skillsNeeded, this.team, this.isComplete}); 14 | @override 15 | _WorkTeamState createState() => _WorkTeamState(); 16 | } 17 | 18 | /// A helper to store the animation state to display for the Character 19 | /// in the [WorkTeam] widget. 20 | class _WorkTeamMember { 21 | final CharacterStyle style; 22 | HiringBustState state; 23 | 24 | _WorkTeamMember(this.style, this.state); 25 | } 26 | 27 | class _WorkTeamState extends State { 28 | final List<_WorkTeamMember> _workTeam = []; 29 | @override 30 | void didUpdateWidget(WorkTeam oldWidget) { 31 | setState(updateCharacterStyles); 32 | super.didUpdateWidget(oldWidget); 33 | } 34 | 35 | @override 36 | void initState() { 37 | updateCharacterStyles(); 38 | super.initState(); 39 | } 40 | 41 | void updateCharacterStyles() { 42 | if (widget?.team == null) { 43 | if (widget.isComplete) { 44 | for (final _WorkTeamMember member in _workTeam) { 45 | member.state = HiringBustState.success; 46 | } 47 | } 48 | return; 49 | } 50 | _workTeam.clear(); 51 | for (final Character character in widget.team) { 52 | CharacterStyle style = CharacterStyle.from(character); 53 | if (style == null) { 54 | continue; 55 | } 56 | 57 | _workTeam.add(_WorkTeamMember( 58 | style, 59 | widget.isComplete 60 | ? HiringBustState.success 61 | : character.contributes(widget.skillsNeeded) 62 | ? HiringBustState.working 63 | : HiringBustState.hired)); 64 | } 65 | } 66 | 67 | @override 68 | Widget build(BuildContext context) { 69 | return Wrap( 70 | spacing: 10, 71 | runSpacing: 10, 72 | alignment: WrapAlignment.end, 73 | crossAxisAlignment: WrapCrossAlignment.end, 74 | children: _workTeam.map((member) { 75 | return Container( 76 | width: 71, 77 | height: 71, 78 | decoration: BoxDecoration( 79 | borderRadius: BorderRadius.circular(10), 80 | color: const Color.fromRGBO(69, 69, 82, 1), 81 | ), 82 | child: HiringBust( 83 | filename: member.style.flare, 84 | fit: BoxFit.contain, 85 | alignment: Alignment.bottomCenter, 86 | hiringState: member.state, 87 | isPlaying: true, 88 | )); 89 | }).toList()); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/src/widgets/keyboard.dart: -------------------------------------------------------------------------------- 1 | /// Convenience class for giving names to key identifiers. 2 | class KeyCode { 3 | static const int escape = 0x100070029; 4 | static const int backspace = 0x10007002a; 5 | } 6 | -------------------------------------------------------------------------------- /lib/src/widgets/prowess_progress.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// A progress bar with rounded corners and custom colors. 4 | class ProwessProgress extends StatefulWidget { 5 | const ProwessProgress( 6 | {@required this.progress, 7 | Key key, 8 | this.color, 9 | this.background = const Color.fromRGBO(0, 0, 0, 0.06), 10 | this.height = 7, 11 | this.borderRadius, 12 | this.innerPadding = const EdgeInsets.all(0)}) 13 | : super(key: key); 14 | 15 | final double progress; 16 | final Color background; 17 | final Color color; 18 | final double height; 19 | final EdgeInsets innerPadding; 20 | final BorderRadius borderRadius; 21 | 22 | @override 23 | _ProwessProgressState createState() => _ProwessProgressState(); 24 | } 25 | 26 | class _ProwessProgressState extends State 27 | with SingleTickerProviderStateMixin { 28 | AnimationController _animationController; 29 | Animation _progressTween; 30 | 31 | @override 32 | void initState() { 33 | _animationController = AnimationController( 34 | vsync: this, duration: const Duration(milliseconds: 200)); 35 | _progressTween = Tween(begin: widget.progress, end: widget.progress) 36 | .animate(_animationController); 37 | super.initState(); 38 | } 39 | 40 | @override 41 | void dispose() { 42 | _animationController.dispose(); 43 | super.dispose(); 44 | } 45 | 46 | @override 47 | void didUpdateWidget(ProwessProgress oldWidget) { 48 | setState(() { 49 | _progressTween = 50 | Tween(begin: _progressTween.value, end: widget.progress) 51 | .animate(_animationController); 52 | _animationController.reset(); 53 | _animationController.forward(); 54 | }); 55 | super.didUpdateWidget(oldWidget); 56 | } 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return Container( 61 | height: widget.height, 62 | child: Stack( 63 | children: [ 64 | ConstrainedBox( 65 | constraints: const BoxConstraints(minWidth: double.infinity), 66 | child: Container( 67 | decoration: BoxDecoration( 68 | color: widget.background, borderRadius: widget.borderRadius), 69 | ), 70 | ), 71 | AnimatedBuilder( 72 | animation: _animationController, 73 | builder: (context, child) => FractionallySizedBox( 74 | widthFactor: _progressTween.value, 75 | child: Padding( 76 | padding: widget.innerPadding, 77 | child: Container( 78 | height: widget.height, 79 | decoration: BoxDecoration( 80 | color: widget.color, 81 | borderRadius: widget.borderRadius), 82 | ), 83 | ), 84 | ), 85 | ) 86 | ], 87 | ), 88 | ); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/src/widgets/screen_layout_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | /// Create a mobile or tablet layout depending on the screen size. 4 | class ResponsiveLayoutBuilder extends StatelessWidget { 5 | final Widget Function(BuildContext) buildMobile; 6 | final Widget Function(BuildContext) buildTablet; 7 | 8 | const ResponsiveLayoutBuilder({Key key, this.buildMobile, this.buildTablet}) 9 | : super(key: key); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return MediaQuery.of(context).size.shortestSide < 600 14 | ? buildMobile(context) 15 | : buildTablet(context); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/widgets/skill_badge.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/skill.dart'; 2 | import 'package:dev_rpg/src/style.dart'; 3 | import 'package:dev_rpg/src/widgets/flare/skill_icon.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | Map skillDisplayName = { 7 | Skill.coding: 'Coding', 8 | Skill.engineering: 'Engineering', 9 | Skill.ux: 'UX', 10 | Skill.coordination: 'Coordination' 11 | }; 12 | 13 | /// Displays a skill in a nicely readable format along with 14 | /// the value if present. 15 | class SkillBadge extends StatelessWidget { 16 | final Skill skill; 17 | 18 | const SkillBadge(this.skill); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return Padding( 23 | padding: const EdgeInsets.only(right: 10), 24 | child: Container( 25 | padding: const EdgeInsets.all(8), 26 | decoration: BoxDecoration( 27 | color: skillColor[skill], 28 | borderRadius: const BorderRadius.all( 29 | Radius.circular(5), 30 | ), 31 | ), 32 | child: Row( 33 | children: [ 34 | SkillIcon(skill), 35 | const SizedBox(width: 5), 36 | Text( 37 | skillDisplayName[skill].toUpperCase(), 38 | style: 39 | buttonTextStyle.apply(fontSizeDelta: -4, color: Colors.white), 40 | ), 41 | ], 42 | ), 43 | ), 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/src/widgets/task_picker/task_picker_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/style.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// This is a simple text header for the tasks list. 5 | class TaskPickerHeader extends SliverPersistentHeaderDelegate { 6 | final String title; 7 | final bool showLine; 8 | const TaskPickerHeader(this.title, {this.showLine = true}); 9 | 10 | @override 11 | Widget build( 12 | BuildContext context, double shrinkOffset, bool overlapsContent) { 13 | return Stack(overflow: Overflow.visible, children: [ 14 | showLine 15 | ? Positioned.fromRect( 16 | rect: Rect.fromLTWH(26, 25, 2, 35 - shrinkOffset), 17 | child: SizedOverflowBox( 18 | size: const Size.fromHeight(0), 19 | child: Container(color: treeLineColor), 20 | ), 21 | ) 22 | : Container(), 23 | Row( 24 | children: [ 25 | const SizedBox(width: 15), 26 | Container( 27 | width: 25, 28 | height: 25, 29 | decoration: const BoxDecoration( 30 | boxShadow: [ 31 | BoxShadow( 32 | color: Color.fromRGBO(84, 114, 244, 0.25), 33 | offset: Offset(0, 10), 34 | blurRadius: 10, 35 | spreadRadius: 0), 36 | ], 37 | borderRadius: BorderRadius.all(Radius.circular(12.5)), 38 | color: Color.fromRGBO(84, 114, 239, 1), 39 | ), 40 | child: const Icon(Icons.keyboard_arrow_down)), 41 | const SizedBox(width: 15), 42 | Text( 43 | title, 44 | style: buttonTextStyle, 45 | ), 46 | ], 47 | ), 48 | ]); 49 | } 50 | 51 | @override 52 | double get maxExtent => 60; 53 | 54 | @override 55 | double get minExtent => 60; 56 | 57 | @override 58 | bool shouldRebuild(TaskPickerHeader oldDelegate) { 59 | return title != oldDelegate.title; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/src/widgets/work_items/bug_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/bug.dart'; 2 | import 'package:dev_rpg/src/shared_state/game/skill.dart'; 3 | import 'package:dev_rpg/src/style.dart'; 4 | import 'package:dev_rpg/src/widgets/work_items/skill_dot.dart'; 5 | import 'package:flutter/material.dart'; 6 | 7 | /// Indicator for bug list items. Shows skills necessary to fix the bug. 8 | class BugHeader extends StatelessWidget { 9 | final Bug bug; 10 | 11 | const BugHeader(this.bug); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Row( 16 | children: [ 17 | const Icon(Icons.bug_report, color: bugColor), 18 | Expanded(child: Container()), 19 | for (Skill skill in bug.skillsNeeded) SkillDot(skill) 20 | ], 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/widgets/work_items/bug_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/bug.dart'; 2 | import 'package:dev_rpg/src/shared_state/game/work_item.dart'; 3 | import 'package:dev_rpg/src/style.dart'; 4 | import 'package:dev_rpg/src/widgets/work_items/bug_header.dart'; 5 | import 'package:dev_rpg/src/widgets/work_items/work_list_item.dart'; 6 | import 'package:flutter/material.dart'; 7 | import 'package:provider/provider.dart'; 8 | 9 | /// Displays a [Bug] that can be tapped on to assign it to a team. 10 | class BugListItem extends StatelessWidget { 11 | @override 12 | Widget build(BuildContext context) { 13 | var bug = Provider.of(context) as Bug; 14 | 15 | return WorkListItem( 16 | workItem: bug, 17 | isExpanded: bug.isBeingWorkedOn, 18 | progressColor: bugColor, 19 | heading: !bug.isComplete 20 | ? BugHeader(bug) 21 | : const Icon(Icons.bug_report, color: disabledColor), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/widgets/work_items/skill_dot.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/skill.dart'; 2 | import 'package:dev_rpg/src/style.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | /// Displays a skill as a colored dot. 6 | class SkillDot extends StatelessWidget { 7 | final Skill skill; 8 | 9 | const SkillDot(this.skill); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | return Padding( 14 | padding: const EdgeInsets.only(left: 10), 15 | child: Container( 16 | width: 10, 17 | height: 10, 18 | decoration: BoxDecoration( 19 | color: skillColor[skill], 20 | borderRadius: const BorderRadius.all( 21 | Radius.circular(5), 22 | ), 23 | ), 24 | ), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/widgets/work_items/task_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/rpg_layout_builder.dart'; 2 | import 'package:dev_rpg/src/shared_state/game/skill.dart'; 3 | import 'package:dev_rpg/src/shared_state/game/task_blueprint.dart'; 4 | import 'package:dev_rpg/src/style.dart'; 5 | import 'package:dev_rpg/src/widgets/work_items/skill_dot.dart'; 6 | import 'package:flare_flutter/flare_actor.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:dev_rpg/src/shared_state/game/task.dart'; 9 | 10 | /// A header for [Task] indicating the rewarded coin and skills 11 | /// necessary to work on the task. 12 | class TaskHeader extends StatelessWidget { 13 | final TaskBlueprint blueprint; 14 | const TaskHeader(this.blueprint); 15 | @override 16 | Widget build(BuildContext context) { 17 | return RpgLayoutBuilder(builder: (context, layout) { 18 | double scale = layout == RpgLayout.ultrawide ? 1.25 : 1.0; 19 | return Row( 20 | children: [ 21 | Container( 22 | width: 20 * scale, 23 | height: 20 * scale, 24 | child: const FlareActor('assets/flare/Coin.flr'), 25 | ), 26 | const SizedBox(width: 4), 27 | Text(blueprint.coinReward.toString(), 28 | style: contentSmallStyle.apply(fontSizeFactor: scale)), 29 | Expanded(child: Container()), 30 | for (Skill skill in blueprint.skillsNeeded) SkillDot(skill) 31 | ], 32 | ); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/src/widgets/work_items/task_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/code_chomper/code_chomper.dart'; 2 | import 'package:dev_rpg/src/shared_state/game/task.dart'; 3 | import 'package:dev_rpg/src/shared_state/game/task_blueprint.dart'; 4 | import 'package:dev_rpg/src/shared_state/game/work_item.dart'; 5 | import 'package:dev_rpg/src/shared_state/game/world.dart'; 6 | import 'package:dev_rpg/src/style.dart'; 7 | import 'package:dev_rpg/src/style_sphinx/sphinx_screen.dart'; 8 | import 'package:dev_rpg/src/widgets/work_items/task_header.dart'; 9 | import 'package:dev_rpg/src/widgets/work_items/work_list_item.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:provider/provider.dart'; 12 | 13 | import '../game_over.dart'; 14 | 15 | /// Displays a [Task] that can be tapped on to assign it to a team. 16 | /// The task can also be tapped on to award points once it is completed. 17 | class TaskListItem extends StatelessWidget { 18 | bool _handleTap(Task task, BuildContext context) { 19 | if (task.state == TaskState.completed) { 20 | // N.B. we ship the feature only once a minigame has 21 | // completed (if there is one). 22 | // This ensures that the BuildContext is still valid after 23 | // the game completes. 24 | switch (task.blueprint.miniGame) { 25 | case MiniGame.none: 26 | task.shipFeature(); 27 | break; 28 | case MiniGame.chomp: 29 | // Time to face chompy, temporarily pause the game. 30 | var world = Provider.of(context); 31 | world.pause(); 32 | Navigator.of(context) 33 | .pushNamed(CodeChomper.miniGameRouteName, 34 | arguments: task.blueprint.name == 'Alpha release' 35 | ? 'assets/docs/code_chomper_alpha.dart' 36 | : 'assets/docs/code_chomper_beta.dart') 37 | .then((_) { 38 | world.start(); 39 | task.shipFeature(); 40 | }); 41 | break; 42 | case MiniGame.sphinx: 43 | { 44 | // Time to face the Sphinx, game is effectively over. 45 | var world = Provider.of(context); 46 | world.pause(); 47 | 48 | Navigator.of(context) 49 | .pushNamed(SphinxScreen.miniGameRouteName) 50 | .then((_) { 51 | // Escaped the Sphinx. 52 | task.shipFeature(); 53 | showDialog( 54 | barrierDismissible: false, 55 | context: context, 56 | builder: (BuildContext context) { 57 | return GameOver(world); 58 | }); 59 | }); 60 | break; 61 | } 62 | } 63 | 64 | return true; 65 | } 66 | return false; 67 | } 68 | 69 | @override 70 | Widget build(BuildContext context) { 71 | var task = Provider.of(context) as Task; 72 | 73 | bool isExpanded = task.isBeingWorkedOn || task.state == TaskState.completed; 74 | return WorkListItem( 75 | workItem: task, 76 | isExpanded: isExpanded, 77 | handleTap: () => _handleTap(task, context), 78 | progressColor: const Color.fromRGBO(0, 152, 255, 1), 79 | heading: task.state != TaskState.rewarded 80 | ? TaskHeader(task.blueprint) 81 | : const Icon(Icons.check_circle, color: disabledColor), 82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/widgets/work_items/tasks_button_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/game_screen/add_task_button.dart'; 2 | import 'package:dev_rpg/src/game_screen/bug_picker_modal.dart'; 3 | import 'package:dev_rpg/src/game_screen/project_picker_modal.dart'; 4 | import 'package:dev_rpg/src/game_screen/team_picker_modal.dart'; 5 | import 'package:dev_rpg/src/shared_state/game/bug.dart'; 6 | import 'package:dev_rpg/src/shared_state/game/character.dart'; 7 | import 'package:dev_rpg/src/shared_state/game/task_blueprint.dart'; 8 | import 'package:dev_rpg/src/shared_state/game/task_pool.dart'; 9 | import 'package:dev_rpg/src/shared_state/game/work_item.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:provider/provider.dart'; 12 | 13 | /// This is a header providing two buttons, one for selecting tasks and another 14 | /// for selecting active bugs to add to the working list. 15 | class TasksButtonHeader extends SliverPersistentHeaderDelegate { 16 | final TaskPool taskPool; 17 | final double scale; 18 | const TasksButtonHeader({this.taskPool, this.scale}); 19 | 20 | Future _pickTeam(BuildContext context, WorkItem item) async { 21 | // immediately show the character picker for this newly 22 | // created task. 23 | var characters = await showModalBottomSheet>( 24 | context: context, 25 | builder: (context) => TeamPickerModal(item), 26 | ); 27 | if (characters != null && !item.isComplete) { 28 | item.assignTeam(characters.toList()); 29 | } 30 | } 31 | 32 | @override 33 | Widget build( 34 | BuildContext context, double shrinkOffset, bool overlapsContent) { 35 | return Padding( 36 | padding: const EdgeInsets.only(top: 15, left: 15, right: 15), 37 | child: Row( 38 | children: [ 39 | Expanded( 40 | child: AddTaskButton( 41 | 'Tasks', 42 | scale: scale, 43 | key: const Key('add_task'), 44 | count: taskPool.availableTasks.length, 45 | icon: Icons.add, 46 | color: const Color(0xff5472ee), 47 | onPressed: () async { 48 | var project = await showModalBottomSheet( 49 | context: context, 50 | builder: (context) => ProjectPickerModal(), 51 | ); 52 | if (project != null) { 53 | var task = Provider.of(context, listen: false) 54 | .startTask(project); 55 | await _pickTeam(context, task); 56 | } 57 | }, 58 | ), 59 | ), 60 | const SizedBox(width: 10), 61 | Expanded( 62 | child: AddTaskButton( 63 | 'Bugs', 64 | scale: scale, 65 | count: taskPool.availableBugs.length, 66 | icon: Icons.bug_report, 67 | color: const Color(0xffeb2875), 68 | onPressed: () async { 69 | var bug = await showModalBottomSheet( 70 | context: context, 71 | builder: (context) => BugPickerModal(), 72 | ); 73 | if (bug != null) { 74 | Provider.of(context, listen: false) 75 | .addWorkItem(bug); 76 | await _pickTeam(context, bug); 77 | } 78 | }, 79 | ), 80 | ), 81 | ], 82 | ), 83 | ); 84 | } 85 | 86 | @override 87 | double get maxExtent => 55 * scale; 88 | 89 | @override 90 | double get minExtent => 55 * scale; 91 | 92 | @override 93 | bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) { 94 | return true; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/src/widgets/work_items/tasks_section_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/style.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | /// This is a simple text header for the tasks list. 5 | class TasksSectionHeader extends SliverPersistentHeaderDelegate { 6 | final String title; 7 | final double scale; 8 | const TasksSectionHeader(this.title, this.scale); 9 | 10 | @override 11 | Widget build( 12 | BuildContext context, double shrinkOffset, bool overlapsContent) { 13 | return Padding( 14 | padding: const EdgeInsets.all(15), 15 | child: Text( 16 | title, 17 | style: buttonTextStyle.apply( 18 | fontSizeDelta: -4, 19 | color: secondaryContentColor, 20 | fontSizeFactor: scale), 21 | ), 22 | ); 23 | } 24 | 25 | @override 26 | double get maxExtent => 45; 27 | 28 | @override 29 | double get minExtent => 45; 30 | 31 | @override 32 | bool shouldRebuild(TasksSectionHeader oldDelegate) { 33 | return title != oldDelegate.title || scale != oldDelegate.scale; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dev_rpg 2 | description: Become a tech lead, slay bugs, and don't get fired. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # Read more about versioning at semver.org. 10 | version: 1.0.0+1 11 | 12 | environment: 13 | # For Google I/O we are being very specific about pinning to specific branch revision. 14 | # This is flutter commit hash b593f5167bce84fb3cad5c258477bf3abc1b14eb, tagged 15 | # as Flutter version 1.5.4. 16 | sdk: ">=2.3.0-dev.0.1 <3.0.0" 17 | 18 | dependencies: 19 | flutter: 20 | sdk: flutter 21 | intl: any 22 | provider: ^2.0.0 23 | 24 | # The following adds the Cupertino Icons font to your application. 25 | # Use with the CupertinoIcons class for iOS style icons. 26 | cupertino_icons: ^0.1.2 27 | flare_flutter: ^1.8.3 28 | auto_size_text: ^1.1.2 29 | 30 | dev_dependencies: 31 | flutter_test: 32 | sdk: flutter 33 | flutter_driver: 34 | sdk: flutter 35 | logging: ^0.11.3+2 36 | git: ^0.5.1+1 37 | t_stats: ^2.0.0 38 | test: ^1.6.1 39 | 40 | 41 | # For information on the generic Dart part of this file, see the 42 | # following page: https://www.dartlang.org/tools/pub/pubspec 43 | 44 | # The following section is specific to Flutter. 45 | flutter: 46 | 47 | # The following line ensures that the Material Icons font is 48 | # included with your application, so that you can use the icons in 49 | # the material Icons class. 50 | uses-material-design: true 51 | 52 | # To add assets to your application, add an assets section, like this: 53 | assets: 54 | - assets/style_sphinx/ 55 | - assets/images/ 56 | - assets/images/2.0x/ 57 | - assets/images/3.0x/ 58 | - assets/flare/ 59 | - assets/docs/code_chomper_alpha.dart 60 | - assets/docs/code_chomper_beta.dart 61 | 62 | fonts: 63 | - family: SpaceMonoRegular 64 | fonts: 65 | - asset: assets/fonts/SpaceMono-Regular.ttf 66 | - family: SpaceMonoBold 67 | fonts: 68 | - asset: assets/fonts/SpaceMono-Bold.ttf 69 | - family: RobotoCondensedBold 70 | fonts: 71 | - asset: assets/fonts/RobotoCondensed-Bold.ttf 72 | - family: RobotoRegular 73 | fonts: 74 | - asset: assets/fonts/Roboto-Regular.ttf 75 | - family: MontserratMedium 76 | fonts: 77 | - asset: assets/fonts/Montserrat-Medium.otf 78 | - family: MontserratRegular 79 | fonts: 80 | - asset: assets/fonts/Montserrat-Regular.otf 81 | - family: MontserratBold 82 | fonts: 83 | - asset: assets/fonts/Montserrat-Bold.otf 84 | -------------------------------------------------------------------------------- /test/character_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/world.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | test('cannot hire without money', () { 6 | var world = World()..company.coin.number = 0; 7 | var newHire = world.characterPool.children.firstWhere((ch) => !ch.isHired); 8 | expect(newHire.hire(), false); 9 | expect(newHire.isHired, false); 10 | }); 11 | 12 | test('can hire', () { 13 | var world = World()..company.coin.number = 1000; 14 | var newHire = world.characterPool.children.firstWhere((ch) => !ch.isHired); 15 | expect(newHire.hire(), true); 16 | expect(newHire.isHired, true); 17 | }); 18 | 19 | test('cannot be upgraded when not hired', () { 20 | var world = World(); 21 | var unhired = world.characterPool.children.firstWhere((ch) => !ch.isHired); 22 | var previous = unhired.level; 23 | expect(() => unhired.upgrade(), throwsA(isA())); 24 | expect(unhired.level, previous); 25 | }); 26 | 27 | test('level goes up when upgraded', () { 28 | var world = World()..company.coin.number = 10000; 29 | // Find the first available character that we can hire. 30 | var character = world.characterPool.children 31 | .firstWhere((ch) => !ch.isHired && ch.canUpgradeOrHire); 32 | character.hire(); 33 | var previous = character.level; 34 | expect(character.upgrade(), true); 35 | expect(character.level, greaterThan(previous)); 36 | }); 37 | 38 | test('skill goes up when upgraded', () { 39 | var world = World()..company.coin.number = 10000; 40 | // Find the first available character that we can hire. 41 | var character = world.characterPool.children 42 | .firstWhere((ch) => !ch.isHired && ch.canUpgradeOrHire); 43 | character.hire(); 44 | var previous = character.prowess.values.fold(0, (a, b) => a + b); 45 | expect(character.upgrade(), true); 46 | expect(character.prowess.values.fold(0, (a, b) => a + b), 47 | greaterThan(previous)); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /test/style_sphinx/question_arguments_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/style_sphinx/question_arguments.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | /// This test suite demonstrates how you can test business logic independent of 5 | /// the Widget tree. 6 | void main() { 7 | group('QuestionArguments', () { 8 | test('should have a next question if one exists', () { 9 | expect( 10 | QuestionArguments(questionRoutes: ['a', 'b']).hasNextQuestion, 11 | isTrue, 12 | ); 13 | 14 | expect( 15 | QuestionArguments( 16 | questionRoutes: ['a', 'b', 'c'], 17 | currentIndex: 1, 18 | ).hasNextQuestion, 19 | isTrue, 20 | ); 21 | }); 22 | 23 | test('should not have a next question if one does not exist', () { 24 | expect(QuestionArguments().hasNextQuestion, isFalse); 25 | expect( 26 | QuestionArguments( 27 | questionRoutes: ['a'], 28 | currentIndex: 0, 29 | ).hasNextQuestion, 30 | isFalse, 31 | ); 32 | }); 33 | 34 | test('should produce next arguments based on the current arguments', () { 35 | final routes = ['a', 'b']; 36 | 37 | expect( 38 | QuestionArguments(questionRoutes: routes).nextQuestion(), 39 | QuestionArguments(questionRoutes: routes, currentIndex: 1), 40 | ); 41 | }); 42 | 43 | test('should throw if the next question does not exist', () { 44 | expect( 45 | QuestionArguments().nextQuestion, 46 | throwsStateError, 47 | ); 48 | }); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /test/style_sphinx/sphinx_button_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/style_sphinx/sphinx_buttton.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | /// This test suite demonstrates how you can test your own custom Widgets 6 | void main() { 7 | group('SphinxButton', () { 8 | testWidgets('displays the child', (tester) async { 9 | await tester.pumpWidget( 10 | MaterialApp( 11 | home: SphinxButton( 12 | onPressed: () {}, 13 | child: const Text('A'), 14 | ), 15 | ), 16 | ); 17 | 18 | expect(find.text('A'), findsOneWidget); 19 | }); 20 | 21 | testWidgets('executes a callback on press', (tester) async { 22 | int count = 0; 23 | 24 | await tester.pumpWidget( 25 | MaterialApp( 26 | home: SphinxButton( 27 | onPressed: () => count++, 28 | child: const Text('A'), 29 | ), 30 | ), 31 | ); 32 | 33 | await tester.tap(find.text('A')); 34 | 35 | expect(count, 1); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:dev_rpg/main.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | 12 | void main() { 13 | testWidgets( 14 | 'Start the game', 15 | (WidgetTester tester) async { 16 | final startFinder = find.text('START'); 17 | 18 | // Build our app and trigger a frame. 19 | await tester.pumpWidget(MyApp()); 20 | 21 | // Find the start text 22 | expect(startFinder, findsOneWidget); 23 | 24 | // Start the game. 25 | expect(find.byType(FlatButton), findsNWidgets(2)); 26 | await tester.tap(startFinder); 27 | await tester.pumpAndSettle(); 28 | 29 | expect(find.text('Tasks'), findsOneWidget); 30 | }, 31 | // This currently fails with a hanging Future. Not because of code 32 | // in this app. Skipping until this is resolved. TODO. 33 | skip: true, 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /test/world_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:dev_rpg/src/shared_state/game/world.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | void main() { 7 | testWidgets('world can be started', (WidgetTester tester) async { 8 | var buttonKey = const ValueKey('start'); 9 | 10 | await tester.pumpWidget( 11 | ChangeNotifierProvider( 12 | builder: (_) => World(), 13 | child: MaterialApp( 14 | home: Consumer( 15 | builder: (context, world, child) => FlatButton( 16 | key: buttonKey, 17 | onPressed: () => world.start(), 18 | child: Text(world.isRunning ? 'Stop' : 'Start'), 19 | ), 20 | ), 21 | ), 22 | ), 23 | ); 24 | 25 | expect(find.text('Stop'), findsNothing); 26 | await tester.tap(find.byKey(buttonKey)); 27 | await tester.pumpAndSettle(); 28 | expect(find.text('Stop'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /test_driver/.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf 2 | *.tsv 3 | download_results.sh 4 | -------------------------------------------------------------------------------- /test_driver/durations.tsv: -------------------------------------------------------------------------------- 1 | id build rasterizer frameRequest sha description 2 | -------------------------------------------------------------------------------- /test_driver/generate-graphs.R: -------------------------------------------------------------------------------- 1 | # Load data from disk. 2 | durations <- read.csv("test_driver/durations.tsv", sep = "\t") 3 | 4 | # Uncomment and modify the following line to focus on just a few selected runs. 5 | # durations <- durations[durations$description == "baseline" | durations$description == "statvalue-always-notifies" ,] 6 | 7 | # Get just the IDs as a vector. 8 | ids <- aggregate(durations$id, by=list(durations$id), FUN=head)[1]$Group.1 9 | # Get just the date and time from the id. 10 | labels <- substr(ids, 18, 33) 11 | 12 | # Setting plot margins. 13 | old.par <- par(no.readonly = TRUE) 14 | 15 | # Percentiles 16 | build90 <- aggregate(build ~ description, durations, function(x) quantile(x, c(0.5,0.90))) 17 | build95 <- aggregate(build ~ description, durations, function(x) quantile(x, c(0.5,0.95))) 18 | build99 <- aggregate(build ~ description, durations, function(x) quantile(x, c(0.5,0.99))) 19 | len <- length(build95$description) 20 | labels <- gsub("-", "\n", build95$description) 21 | 22 | # Jank chart 23 | pdf("test_driver/builds_bad.pdf", width = 16, height = 9) 24 | par(mar = c(15,6,4,2)+0.1, las = 2) 25 | x <- barplot(t(build95$build), xlim = c(0, max(build99$build)), density = len:1 * 2, angle = len:1 * 90 + 45, names.arg = labels, horiz = TRUE, beside = TRUE) 26 | #arrows(x, build90$build, x, build99$build, length=0.05, angle=90, code=3) 27 | title("Build times worst case") 28 | dev.off() 29 | 30 | # CPU time chart 31 | pdf("test_driver/cpu_time.pdf", width = 4, height = 8) 32 | stats <- read.csv("test_driver/perf_stats.tsv", sep = "\t") 33 | mean_with_moe <- function(x) { 34 | m <- mean(x) 35 | std_dev <- sd(x)/sqrt(length(x)) 36 | crit_val <- qt(0.975, df = length(x) - 1) 37 | moe <- std_dev * crit_val 38 | df <- data.frame(m, m - moe, m + moe) 39 | colnames(df) <- c("mean", "lower", "upper") 40 | return(df) 41 | } 42 | m <- aggregate(expiredTasksDuration ~ description, stats, function(x) mean_with_moe(x)$mean) / 1000 43 | low <- aggregate(expiredTasksDuration ~ description, stats, function(x) mean_with_moe(x)$lower) / 1000 44 | high <- aggregate(expiredTasksDuration ~ description, stats, function(x) mean_with_moe(x)$upper) / 1000 45 | x <- barplot(m$expiredTasksDuration, ylim = c(0, max(high$expiredTasksDuration) * 1.1), density = 5, names.arg = m$description, ylab = "CPU time (ms)") 46 | arrows(x, low$expiredTasksDuration, x, high$expiredTasksDuration, length=0.10, angle=90, code=3) 47 | title("CPU time (lower is better)") 48 | dev.off() 49 | 50 | # Build durations 51 | pdf("test_driver/builds.pdf", width = 10, height = 10) 52 | par(mar = c(15,6,4,2)+0.1, las = 2) 53 | boxplot(build ~ description, durations, notch = TRUE, boxwex = 0.8) 54 | #axis(1, at = 1:length(ids), labels = labels, las = 2, cex.axis=0.5) 55 | title("Build times") 56 | dev.off() 57 | 58 | pdf("test_driver/rasterizations.pdf", width = 10, height = 10) 59 | par(mar = c(15,6,4,2)+0.1, las = 2) 60 | boxplot(rasterizer ~ description, durations, notch = TRUE, boxwex = 0.8, las = 2) 61 | #axis(1, at = 1:length(ids), labels = labels, las = 2, cex.axis=0.5) 62 | title("Rasterizer times") 63 | dev.off() 64 | 65 | pdf("test_driver/frame_requests.pdf", width = 10, height = 10) 66 | par(mar = c(15,6,4,2)+0.1, las = 2) 67 | boxplot(frameRequest ~ description, durations, notch = TRUE, boxwex = 0.8, las = 2) 68 | #axis(1, at = 1:length(ids), labels = labels, las = 2, cex.axis=0.5) 69 | title("Frame request pending times") 70 | dev.off() 71 | 72 | par(old.par) 73 | -------------------------------------------------------------------------------- /test_driver/perf_stats.tsv: -------------------------------------------------------------------------------- 1 | name mean lowerBound upperBound marginOfError stdDeviation stdError min max n description 90thPercentile 99thPercentile worstFrame missedFrames length frames fps frameRequestDurationMean dartPercentage dartPhaseEvents dartPhaseDuration expiredTasksEvents expiredTasksDuration timestamp 2 | -------------------------------------------------------------------------------- /test_driver/performance.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:dev_rpg/main.dart' as app; 4 | import 'package:dev_rpg/src/shared_state/game/bug.dart'; 5 | import 'package:dev_rpg/src/shared_state/game/task_pool.dart'; 6 | import 'package:dev_rpg/src/shared_state/game/world.dart'; 7 | import 'package:flutter_driver/driver_extension.dart'; 8 | 9 | void main() { 10 | enableFlutterDriverExtension(); 11 | // Make the simulation faster, about one update every 2 frames. 12 | World.tickDuration = const Duration(milliseconds: 1000 ~/ 30); 13 | Bug.randomizer = Random(42); 14 | TaskPool.bugRandom = Random(24); 15 | app.main(); 16 | } 17 | -------------------------------------------------------------------------------- /tool/lock_android_scaling.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This is adapted from Skia's recipe at: 4 | # https://github.com/google/skia/blob/e25b4472cdd9f09cd393c9c34651218507c9847b/infra/bots/recipe_modules/flavor/android.py 5 | # 6 | # Use that recipe to modify this for your test device. This shell script assumes Nexus 5. 7 | # Every device will have slightly different numbers and processes. 8 | 9 | set -e 10 | 11 | # Remove whitespace characters from string. 12 | # From https://stackoverflow.com/a/3352015/1416886 13 | trim() { 14 | local var="$*" 15 | # remove leading whitespace characters 16 | var="${var#"${var%%[![:space:]]*}"}" 17 | # remove trailing whitespace characters 18 | var="${var%"${var##*[![:space:]]}"}" 19 | echo -n "$var" 20 | } 21 | 22 | TARGET_DEVICE="Nexus 5" 23 | echo "This assumes a rooted ${TARGET_DEVICE} attached via USB." 24 | ACTUAL_DEVICE=`adb shell getprop ro.product.model` 25 | ACTUAL_DEVICE=`trim ${ACTUAL_DEVICE}` 26 | echo "${ACTUAL_DEVICE} detected." 27 | 28 | if [[ "${TARGET_DEVICE}" != "${ACTUAL_DEVICE}" ]]; then 29 | echo "Error: the attached device ${ACTUAL_DEVICE} is not ${TARGET_DEVICE}." 30 | echo "Aborting. If you have another device attached, unplug it." 31 | exit 1 32 | fi 33 | 34 | # Nexus 5 has only one CPU to scale, cpu0. 35 | # https://github.com/google/skia/blob/e25b4472cdd9f09cd393c9c34651218507c9847b/infra/bots/recipe_modules/flavor/android.py#L53 36 | CPU_NO="0" 37 | 38 | # Root path to CPU scaling virtual files. 39 | # https://github.com/google/skia/blob/e25b4472cdd9f09cd393c9c34651218507c9847b/infra/bots/recipe_modules/flavor/android.py#L283 40 | ROOT="/sys/devices/system/cpu/cpu${CPU_NO}/cpufreq" 41 | 42 | echo 43 | ACTUAL_CPU_FREQ=`adb shell "cat ${ROOT}/scaling_cur_freq"` 44 | echo "Current CPU frequency: ${ACTUAL_CPU_FREQ}" 45 | ACTUAL_GOV=`adb shell "cat /sys/devices/system/cpu/cpu${CPU_NO}/cpufreq/scaling_governor"` 46 | echo "Current governor: ${ACTUAL_GOV}" 47 | echo 48 | 49 | echo "This script will set frequencies of your device's CPU and GPU." 50 | echo; read -n 1 -s -r -p "Press any key to continue, or Ctrl-C to cancel"; echo 51 | 52 | # https://github.com/google/skia/blob/e25b4472cdd9f09cd393c9c34651218507c9847b/infra/bots/recipe_modules/flavor/android.py#L151 53 | GPU_FREQ="320000000" 54 | IDLE_TIMER="10000" 55 | 56 | adb root 57 | 58 | # --- CPU --- 59 | 60 | # Set userspace governor for cpu0 61 | # https://github.com/google/skia/blob/e25b4472cdd9f09cd393c9c34651218507c9847b/infra/bots/recipe_modules/flavor/android.py#L205 62 | GOV="userspace" 63 | echo "Setting CPU governor to: ${GOV}" 64 | adb shell "echo ${GOV} > /sys/devices/system/cpu/cpu${CPU_NO}/cpufreq/scaling_governor" 65 | ACTUAL_GOV=`adb shell "cat /sys/devices/system/cpu/cpu${CPU_NO}/cpufreq/scaling_governor"` 66 | echo " - actual: ${ACTUAL_GOV}" 67 | 68 | 69 | # If you want to check available frequencies: 70 | # cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies 71 | # 72 | # Since we are hardcoding this script for Nexus 5, we can just select the frequency we want. 73 | # 74 | # Nexus 5 max frequency: 2,265,600 Hz 75 | # 60% : 1,359,360 Hz 76 | # closest available: 1,267,200 Hz 77 | CPU_FREQ="1267200" 78 | MAX_FREQ="2265600" 79 | 80 | echo "Setting CPU frequency to: ${CPU_FREQ}" 81 | # https://github.com/google/skia/blob/e25b4472cdd9f09cd393c9c34651218507c9847b/infra/bots/recipe_modules/flavor/android.py#L326 82 | # If scaling_max_freq is lower than our attempted setting, it won't take. 83 | # We must set min first, because if we try to set max to be less than min 84 | # (which sometimes happens after certain devices reboot) it returns a 85 | # perplexing permissions error. 86 | adb shell "echo 0 > ${ROOT}/scaling_min_freq" 87 | adb shell "echo ${CPU_FREQ} > ${ROOT}/scaling_max_freq" 88 | adb shell "echo ${CPU_FREQ} > ${ROOT}/scaling_setspeed" 89 | sleep 5 90 | ACTUAL_CPU_FREQ=`adb shell "cat ${ROOT}/scaling_cur_freq"` 91 | echo " - actual: ${ACTUAL_CPU_FREQ}" 92 | 93 | # According to Skia, no need to disable CPUs on a Nexus 5. 94 | # https://github.com/google/skia/blob/e25b4472cdd9f09cd393c9c34651218507c9847b/infra/bots/recipe_modules/flavor/android.py#L145 95 | # But actually, Nexus 5 does scale other CPUs. 96 | 97 | CPU_ONLINE=0 98 | for n in 1 2 3 99 | do 100 | echo "Turning CPU ${n} to: ${CPU_ONLINE}" 101 | adb shell "echo ${CPU_ONLINE} > /sys/devices/system/cpu/cpu${n}/online" 102 | ACTUAL=`adb shell "cat /sys/devices/system/cpu/cpu${n}/online"` 103 | echo " - actual: ${ACTUAL}" 104 | done 105 | 106 | 107 | # --- GPU --- 108 | 109 | # https://github.com/google/skia/blob/e25b4472cdd9f09cd393c9c34651218507c9847b/infra/bots/recipe_modules/flavor/android.py#L153 110 | echo "Stopping thermald" 111 | adb shell "stop thermald" 112 | 113 | echo "Setting GPU frequency to: ${GPU_FREQ}" 114 | adb shell "echo ${GPU_FREQ} > /sys/class/kgsl/kgsl-3d0/gpuclk" 115 | ACTUAL_GPU_FREQ=`adb shell "cat /sys/class/kgsl/kgsl-3d0/gpuclk"` 116 | echo " - actual: ${ACTUAL_GPU_FREQ}" 117 | 118 | echo "Setting GPU idle timer to: ${IDLE_TIMER}" 119 | adb shell "echo ${IDLE_TIMER} > /sys/class/kgsl/kgsl-3d0/idle_timer" 120 | ACTUAL_TIMER=`adb shell "cat /sys/class/kgsl/kgsl-3d0/idle_timer"` 121 | echo " - actual: ${ACTUAL_TIMER}" 122 | 123 | echo "Setting force_bus_on, force_rail_on, force_clk_on" 124 | adb shell "echo 1 > /sys/class/kgsl/kgsl-3d0/force_bus_on" 125 | adb shell "echo 1 > /sys/class/kgsl/kgsl-3d0/force_rail_on" 126 | adb shell "echo 1 > /sys/class/kgsl/kgsl-3d0/force_clk_on" 127 | --------------------------------------------------------------------------------