├── .env.empty ├── .gitignore ├── .metadata ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── README_DEVELOP.md ├── TODO.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── flutter_puzzle │ │ │ │ └── MainActivity.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── 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-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── settings.gradle ├── assets ├── audio │ ├── music │ │ ├── bensound-extremeaction.mp3 │ │ ├── bensound-punky.mp3 │ │ └── bensound-scifi.mp3 │ └── sounds │ │ ├── awesome.mp3 │ │ ├── beep.mp3 │ │ ├── beep_long.mp3 │ │ ├── click.wav │ │ ├── excellent.mp3 │ │ ├── game_over.mp3 │ │ ├── great.mp3 │ │ ├── keep_it_up.mp3 │ │ ├── swish.wav │ │ ├── that_was_close.mp3 │ │ ├── too_easy.mp3 │ │ └── well_done.mp3 └── launcher │ ├── icon.png │ └── icon_rounded.png ├── build_web.sh ├── docs └── private_policy.txt ├── fonts ├── AzeretMono-Black.ttf ├── AzeretMono-Bold.ttf ├── AzeretMono-ExtraBold.ttf ├── AzeretMono-SemiBold.ttf ├── Rowdies-Bold.ttf ├── Rowdies-Light.ttf └── Rowdies-Regular.ttf ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-50x50@1x.png │ │ ├── Icon-App-50x50@2x.png │ │ ├── Icon-App-57x57@1x.png │ │ ├── Icon-App-57x57@2x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-72x72@1x.png │ │ ├── Icon-App-72x72@2x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.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 ├── app_lifecycle.dart ├── audio.dart ├── audio_dialog.dart ├── background.dart ├── build_context_extension.dart ├── config.dart ├── env.dart ├── game │ ├── countdown.dart │ ├── game.dart │ ├── game_time.dart │ ├── grid.dart │ ├── puzzle.dart │ ├── save_game.dart │ └── tile.dart ├── highscore_dialog.dart ├── leaderboard.dart ├── leaderboard_dialog.dart ├── lerp_value.dart ├── main.dart ├── music_credits.dart ├── scenes │ ├── cubes.dart │ ├── particle.dart │ ├── rays.dart │ ├── scene.dart │ └── start_screen.dart └── state_transition.dart ├── macos ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── app_icon_1024.png │ │ ├── app_icon_128.png │ │ ├── app_icon_16.png │ │ ├── app_icon_256.png │ │ ├── app_icon_32.png │ │ ├── app_icon_512.png │ │ └── app_icon_64.png │ ├── Base.lproj │ └── MainMenu.xib │ ├── Configs │ ├── AppInfo.xcconfig │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── Warnings.xcconfig │ ├── DebugProfile.entitlements │ ├── Info.plist │ ├── MainFlutterWindow.swift │ └── Release.entitlements ├── pubspec.lock ├── pubspec.yaml ├── source_assets └── loading │ ├── loading_00.png │ ├── loading_00_small.png │ ├── loading_01.png │ ├── loading_01_small.png │ ├── loading_02.png │ ├── loading_02_small.png │ ├── loading_03.png │ └── loading_03_small.png ├── test └── widget_test.dart ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── img │ ├── loading.gif │ ├── loading_small.gif │ └── splash.png ├── index.html └── manifest.json └── windows ├── .gitignore ├── CMakeLists.txt ├── flutter ├── CMakeLists.txt ├── generated_plugin_registrant.cc ├── generated_plugin_registrant.h └── generated_plugins.cmake └── runner ├── CMakeLists.txt ├── Runner.rc ├── flutter_window.cpp ├── flutter_window.h ├── main.cpp ├── resource.h ├── resources └── app_icon.ico ├── runner.exe.manifest ├── utils.cpp ├── utils.h ├── win32_window.cpp └── win32_window.h /.env.empty: -------------------------------------------------------------------------------- 1 | HASH= 2 | SALT= 3 | SUPABASE_URL= 4 | SUPABASE_ANON_KEY= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | temp/ 12 | 13 | # Secrets 14 | env.g.dart 15 | .env 16 | 17 | # IntelliJ related 18 | *.iml 19 | *.ipr 20 | *.iws 21 | .idea/ 22 | 23 | # The .vscode folder contains launch configuration and tasks you configure in 24 | # VS Code which you may wish to be included in version control, so this line 25 | # is commented out by default. 26 | #.vscode/ 27 | 28 | # Flutter/Dart/Pub related 29 | **/doc/api/ 30 | **/ios/Flutter/.last_build_id 31 | .dart_tool/ 32 | .flutter-plugins 33 | .flutter-plugins-dependencies 34 | .packages 35 | .pub-cache/ 36 | .pub/ 37 | /build/ 38 | 39 | # Web related 40 | 41 | # Symbolication related 42 | app.*.symbols 43 | 44 | # Obfuscation related 45 | app.*.map.json 46 | 47 | # Android Studio will place build artifacts here 48 | /android/app/debug 49 | /android/app/profile 50 | /android/app/release 51 | 52 | # Generated Dart files 53 | **/*.g.dart 54 | -------------------------------------------------------------------------------- /.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. 5 | 6 | version: 7 | revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 17 | base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 18 | - platform: windows 19 | create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 20 | base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "pushtrix", 9 | "request": "launch", 10 | "type": "dart" 11 | }, 12 | { 13 | "name": "pushtrix (profile mode)", 14 | "request": "launch", 15 | "type": "dart", 16 | "flutterMode": "profile" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.configureOnOpen": false 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Business Source License 1.1 2 | 3 | Parameters 4 | 5 | Licensor: Gottfried Chen 6 | Licensed Work: Pushtrix 7 | The Licensed Work is (c) 2022 Gottfried Chen. 8 | Additional Use Grant: None 9 | 10 | Change Date: 2026-06-04 11 | 12 | Change License: Apache License, Version 2.0 13 | 14 | Notice 15 | 16 | The Business Source License (this document, or the "License") is not an Open 17 | Source license. However, the Licensed Work will eventually be made available 18 | under an Open Source License, as stated in this License. 19 | 20 | License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. 21 | "Business Source License" is a trademark of MariaDB Corporation Ab. 22 | 23 | ----------------------------------------------------------------------------- 24 | 25 | Business Source License 1.1 26 | 27 | Terms 28 | 29 | The Licensor hereby grants you the right to copy, modify, create derivative 30 | works, redistribute, and make non-production use of the Licensed Work. The 31 | Licensor may make an Additional Use Grant, above, permitting limited 32 | production use. 33 | 34 | Effective on the Change Date, or the fourth anniversary of the first publicly 35 | available distribution of a specific version of the Licensed Work under this 36 | License, whichever comes first, the Licensor hereby grants you rights under 37 | the terms of the Change License, and the rights granted in the paragraph 38 | above terminate. 39 | 40 | If your use of the Licensed Work does not comply with the requirements 41 | currently in effect as described in this License, you must purchase a 42 | commercial license from the Licensor, its affiliated entities, or authorized 43 | resellers, or you must refrain from using the Licensed Work. 44 | 45 | All copies of the original and modified Licensed Work, and derivative works 46 | of the Licensed Work, are subject to this License. This License applies 47 | separately for each version of the Licensed Work and the Change Date may vary 48 | for each version of the Licensed Work released by Licensor. 49 | 50 | You must conspicuously display this License on each original or modified copy 51 | of the Licensed Work. If you receive the Licensed Work in original or 52 | modified form from a third party, the terms and conditions set forth in this 53 | License apply to your use of that work. 54 | 55 | Any use of the Licensed Work in violation of this License will automatically 56 | terminate your rights under this License for the current and all other 57 | versions of the Licensed Work. 58 | 59 | This License does not grant you any right in any trademark or logo of 60 | Licensor or its affiliates (provided that you may use a trademark or logo of 61 | Licensor as expressly required by this License). 62 | 63 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON 64 | AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, 65 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF 66 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND 67 | TITLE. 68 | 69 | MariaDB hereby grants you permission to use this License’s text to license 70 | your works, and to refer to it using the trademark "Business Source License", 71 | as long as you comply with the Covenants of Licensor below. 72 | 73 | Covenants of Licensor 74 | 75 | In consideration of the right to use this License’s text and the "Business 76 | Source License" name and trademark, Licensor covenants to MariaDB, and to all 77 | other recipients of the licensed work to be provided by Licensor: 78 | 79 | 1. To specify as the Change License the GPL Version 2.0 or any later version, 80 | or a license that is compatible with GPL Version 2.0 or a later version, 81 | where "compatible" means that software provided under the Change License can 82 | be included in a program with software provided under GPL Version 2.0 or a 83 | later version. Licensor may specify additional Change Licenses without 84 | limitation. 85 | 86 | 2. To either: (a) specify an additional grant of rights to use that does not 87 | impose any additional restriction on the right granted in this License, as 88 | the Additional Use Grant; or (b) insert the text "None". 89 | 90 | 3. To specify a Change Date. 91 | 92 | 4. Not to modify this License in any other way. 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pushtrix 2 | 3 | A fast paced puzzle game. 4 | 5 | Try it out at https://gottfired.github.io/pushtrix 6 | 7 | This app still has a few issues (mainly when build for iOS/Android native - see TODO.md). 8 | 9 | Won Runner Up in category multiplatform in the Flutter Puzzle Hack: https://flutterhack.devpost.com/ 10 | 11 | # How to build 12 | 13 | - `flutter pub get` 14 | - make a copy of `.env.empty` and name it `.env` 15 | - `flutter pub run build_runner build --delete-conflicting-outputs` 16 | - `flutter run` 17 | - Leaderboard will not work when building the game this way, since the keys needed for hashing highscore entries are not part of this repo. This method for encrypting the leaderboard is not secure at all but at least provides some basic obfuscation to prevent casual cheaters from entering bogus scores. I deliberately didn't want to lock highscore entries behind a login to keep the old school arcade feel. So if you figure out how to enter a highscore without playing, congrats you're a 1337 h4x0r! But please don't ruin the game for others. 18 | 19 | # Known Issues 20 | 21 | - Uploading an iOS build triggers a warning email regarding push notifications that can be ignored: https://stackoverflow.com/a/55167613/677910 22 | -------------------------------------------------------------------------------- /README_DEVELOP.md: -------------------------------------------------------------------------------- 1 | - add windows support `flutter create --platforms=windows .` 2 | - `flutter run` to start or play button in VScode debug pane 3 | - play button adds hot reload 4 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | - fix missing PWA highscore keyboard on iOS 4 | - fix iOS web sound 5 | - fix shrinking load screen (only on iOS mobile web) 6 | - deploy to stores 7 | - add more scenes 8 | - decouple ticker from background -> general refactor into better structure 9 | - fix pausing app when in start menu 10 | - fix android audio 11 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /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 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 33 30 | 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = '1.8' 38 | } 39 | 40 | sourceSets { 41 | main.java.srcDirs += 'src/main/kotlin' 42 | } 43 | 44 | defaultConfig { 45 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 46 | applicationId "com.chengine.pushtrix" 47 | minSdkVersion 19 48 | targetSdkVersion flutter.targetSdkVersion 49 | versionCode flutterVersionCode.toInteger() 50 | versionName flutterVersionName 51 | } 52 | 53 | buildTypes { 54 | release { 55 | // TODO: Add your own signing config for the release build. 56 | // Signing with the debug keys for now, so `flutter run --release` works. 57 | signingConfig signingConfigs.debug 58 | } 59 | } 60 | } 61 | 62 | flutter { 63 | source '../..' 64 | } 65 | 66 | dependencies { 67 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 68 | } 69 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/flutter_puzzle/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.chengine.pushtrix 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 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.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /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-6.7-all.zip 7 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /assets/audio/music/bensound-extremeaction.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/music/bensound-extremeaction.mp3 -------------------------------------------------------------------------------- /assets/audio/music/bensound-punky.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/music/bensound-punky.mp3 -------------------------------------------------------------------------------- /assets/audio/music/bensound-scifi.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/music/bensound-scifi.mp3 -------------------------------------------------------------------------------- /assets/audio/sounds/awesome.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/sounds/awesome.mp3 -------------------------------------------------------------------------------- /assets/audio/sounds/beep.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/sounds/beep.mp3 -------------------------------------------------------------------------------- /assets/audio/sounds/beep_long.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/sounds/beep_long.mp3 -------------------------------------------------------------------------------- /assets/audio/sounds/click.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/sounds/click.wav -------------------------------------------------------------------------------- /assets/audio/sounds/excellent.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/sounds/excellent.mp3 -------------------------------------------------------------------------------- /assets/audio/sounds/game_over.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/sounds/game_over.mp3 -------------------------------------------------------------------------------- /assets/audio/sounds/great.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/sounds/great.mp3 -------------------------------------------------------------------------------- /assets/audio/sounds/keep_it_up.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/sounds/keep_it_up.mp3 -------------------------------------------------------------------------------- /assets/audio/sounds/swish.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/sounds/swish.wav -------------------------------------------------------------------------------- /assets/audio/sounds/that_was_close.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/sounds/that_was_close.mp3 -------------------------------------------------------------------------------- /assets/audio/sounds/too_easy.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/sounds/too_easy.mp3 -------------------------------------------------------------------------------- /assets/audio/sounds/well_done.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/audio/sounds/well_done.mp3 -------------------------------------------------------------------------------- /assets/launcher/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/launcher/icon.png -------------------------------------------------------------------------------- /assets/launcher/icon_rounded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/assets/launcher/icon_rounded.png -------------------------------------------------------------------------------- /build_web.sh: -------------------------------------------------------------------------------- 1 | flutter build web --base-href /pushtrix/ --web-renderer canvaskit 2 | echo "Now copy ..." 3 | cp -a build/web/. ../pushtrix/docs/ 4 | -------------------------------------------------------------------------------- /docs/private_policy.txt: -------------------------------------------------------------------------------- 1 | Pushtrix does not collect any personal data. 2 | -------------------------------------------------------------------------------- /fonts/AzeretMono-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/fonts/AzeretMono-Black.ttf -------------------------------------------------------------------------------- /fonts/AzeretMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/fonts/AzeretMono-Bold.ttf -------------------------------------------------------------------------------- /fonts/AzeretMono-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/fonts/AzeretMono-ExtraBold.ttf -------------------------------------------------------------------------------- /fonts/AzeretMono-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/fonts/AzeretMono-SemiBold.ttf -------------------------------------------------------------------------------- /fonts/Rowdies-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/fonts/Rowdies-Bold.ttf -------------------------------------------------------------------------------- /fonts/Rowdies-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/fonts/Rowdies-Light.ttf -------------------------------------------------------------------------------- /fonts/Rowdies-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/fonts/Rowdies-Regular.ttf -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /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 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '14.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | end 36 | 37 | post_install do |installer| 38 | installer.pods_project.targets.each do |target| 39 | flutter_additional_ios_build_settings(target) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - app_links (0.0.1): 3 | - Flutter 4 | - audioplayers_darwin (0.0.1): 5 | - Flutter 6 | - Flutter (1.0.0) 7 | - path_provider_ios (0.0.1): 8 | - Flutter 9 | - shared_preferences_ios (0.0.1): 10 | - Flutter 11 | - url_launcher_ios (0.0.1): 12 | - Flutter 13 | 14 | DEPENDENCIES: 15 | - app_links (from `.symlinks/plugins/app_links/ios`) 16 | - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) 17 | - Flutter (from `Flutter`) 18 | - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) 19 | - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) 20 | - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) 21 | 22 | EXTERNAL SOURCES: 23 | app_links: 24 | :path: ".symlinks/plugins/app_links/ios" 25 | audioplayers_darwin: 26 | :path: ".symlinks/plugins/audioplayers_darwin/ios" 27 | Flutter: 28 | :path: Flutter 29 | path_provider_ios: 30 | :path: ".symlinks/plugins/path_provider_ios/ios" 31 | shared_preferences_ios: 32 | :path: ".symlinks/plugins/shared_preferences_ios/ios" 33 | url_launcher_ios: 34 | :path: ".symlinks/plugins/url_launcher_ios/ios" 35 | 36 | SPEC CHECKSUMS: 37 | app_links: ab4ba54d10a13d45825336bc9707b5eadee81191 38 | audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 39 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 40 | path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 41 | shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad 42 | url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de 43 | 44 | PODFILE CHECKSUM: 775997f741c536251164e3eacf6e34abf2eb7a17 45 | 46 | COCOAPODS: 1.11.2 47 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 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: [UIApplication.LaunchOptionsKey: 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-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.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/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/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 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Pushtrix 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | pushtrix 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UIStatusBarHidden 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /lib/app_lifecycle.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppLifecycle extends WidgetsBindingObserver { 4 | AppLifecycle({this.onResume, this.onPause}); 5 | 6 | final void Function()? onResume; 7 | final void Function()? onPause; 8 | 9 | @override 10 | Future didChangeAppLifecycleState(AppLifecycleState state) async { 11 | switch (state) { 12 | case AppLifecycleState.inactive: 13 | case AppLifecycleState.paused: 14 | case AppLifecycleState.detached: 15 | onPause?.call(); 16 | break; 17 | case AppLifecycleState.resumed: 18 | onResume?.call(); 19 | break; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/audio.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:audioplayers/audioplayers.dart'; 5 | import 'package:flame_audio/flame_audio.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:pushtrix/config.dart'; 8 | 9 | const praises = [ 10 | "sounds/well_done.mp3", 11 | "sounds/great.mp3", 12 | "sounds/keep_it_up.mp3", 13 | "sounds/excellent.mp3", 14 | "sounds/awesome.mp3", 15 | ]; 16 | 17 | class Audio { 18 | static final Audio instance = Audio(); 19 | 20 | bool settingEnabled = true; 21 | 22 | List _praiseIndexes = []; 23 | int _currentPraise = 0; 24 | 25 | AudioPlayer? _player; 26 | Timer? _timer; 27 | 28 | bool isIosWebDisabled = false; 29 | 30 | void init() async { 31 | isIosWebDisabled = kIsWeb && defaultTargetPlatform == TargetPlatform.iOS && !enableIosWebAudio; 32 | 33 | if (!isIosWebDisabled) { 34 | await FlameAudio.audioCache.loadAll([ 35 | "sounds/swish.wav", 36 | "sounds/click.wav", 37 | ...praises, 38 | "sounds/game_over.mp3", 39 | "sounds/too_easy.mp3", 40 | "sounds/that_was_close.mp3", 41 | "sounds/beep.mp3", 42 | "sounds/beep_long.mp3", 43 | ]); 44 | 45 | FlameAudio.bgm.initialize(); 46 | await FlameAudio.audioCache.loadAll(["music/bensound-scifi.mp3", "music/bensound-punky.mp3"]); 47 | } 48 | 49 | _praiseIndexes = List.generate(praises.length, (i) => i); 50 | _praiseIndexes.shuffle(); 51 | } 52 | 53 | void dispose() { 54 | if (!isIosWebDisabled) { 55 | FlameAudio.bgm.dispose(); 56 | } 57 | } 58 | 59 | bool get _enabled => settingEnabled && !isIosWebDisabled; 60 | 61 | void click() { 62 | if (!_enabled) return; 63 | 64 | FlameAudio.play("sounds/click.wav"); 65 | } 66 | 67 | void swish() async { 68 | if (!_enabled) return; 69 | 70 | await FlameAudio.play("sounds/swish.wav", volume: 0.5); 71 | } 72 | 73 | void praise() async { 74 | if (!_enabled) return; 75 | 76 | await FlameAudio.play(praises[_praiseIndexes[_currentPraise++]]); 77 | if (_currentPraise >= praises.length) { 78 | _currentPraise = 0; 79 | _praiseIndexes.shuffle(); 80 | } 81 | } 82 | 83 | void thatWasClose() async { 84 | if (!_enabled) return; 85 | 86 | await FlameAudio.play("sounds/that_was_close.mp3"); 87 | } 88 | 89 | void gameOver() async { 90 | if (!_enabled) return; 91 | 92 | await FlameAudio.play("sounds/game_over.mp3"); 93 | } 94 | 95 | void tooEasy() async { 96 | if (!_enabled) return; 97 | 98 | await FlameAudio.play("sounds/too_easy.mp3"); 99 | } 100 | 101 | void beep() async { 102 | if (!_enabled) return; 103 | await FlameAudio.play("sounds/beep.mp3"); 104 | } 105 | 106 | void beepLong() async { 107 | if (!_enabled) return; 108 | 109 | await FlameAudio.play("sounds/beep_long.mp3"); 110 | } 111 | 112 | void playMusic(String title, {double volume = 1}) async { 113 | await FlameAudio.bgm.stop(); 114 | Timer(const Duration(milliseconds: 500), () async { 115 | await FlameAudio.bgm.play(title, volume: volume); 116 | }); 117 | } 118 | 119 | void menuMusic() { 120 | if (!_enabled) return; 121 | 122 | if (_timer != null) { 123 | _timer!.cancel(); 124 | _timer = null; 125 | } 126 | 127 | playMusic("music/bensound-scifi.mp3"); 128 | } 129 | 130 | void gameMusic() { 131 | if (!_enabled) return; 132 | 133 | playMusic("music/bensound-punky.mp3", volume: 0.4); 134 | 135 | // TODO: Fix timer starting fast music when app in background 136 | // _timer = Timer(const Duration(seconds: 125), () { 137 | // gameMusicFast(); 138 | // _timer = null; 139 | // }); 140 | } 141 | 142 | void gameMusicFast() async { 143 | if (!_enabled) return; 144 | 145 | playMusic("music/bensound-extremeaction.mp3"); 146 | } 147 | 148 | void enable(bool enabled) async { 149 | settingEnabled = enabled; 150 | if (!enabled) { 151 | await FlameAudio.bgm.stop(); 152 | await _player?.stop(); 153 | _player = null; 154 | _timer?.cancel(); 155 | } else { 156 | menuMusic(); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /lib/audio_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AudioDialog extends StatelessWidget { 4 | const AudioDialog({ 5 | Key? key, 6 | }) : super(key: key); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return Dialog( 11 | backgroundColor: Colors.white, 12 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), 13 | child: Container( 14 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 24), 15 | child: ConstrainedBox( 16 | constraints: const BoxConstraints(minWidth: 320), // TODO: Why is minWidth not working? 17 | child: Column( 18 | mainAxisSize: MainAxisSize.min, 19 | children: [ 20 | RichText( 21 | text: TextSpan( 22 | text: "La ", 23 | style: const TextStyle(fontSize: 32, fontFamily: "Rowdies", fontWeight: FontWeight.w300, color: Colors.red), 24 | children: [ 25 | TextSpan(text: 'La ', style: TextStyle(color: Colors.yellow.shade600)), 26 | const TextSpan(text: 'La ', style: TextStyle(color: Colors.blue)), 27 | const TextSpan(text: 'or ', style: TextStyle(color: Colors.black)), 28 | TextSpan(text: 'Hush?', style: TextStyle(color: Colors.grey.shade400)), 29 | ], 30 | ), 31 | ), 32 | const SizedBox(height: 16), 33 | RichText( 34 | text: TextSpan( 35 | text: "This game was designed with audio in mind.\nYou decide: ", 36 | style: const TextStyle(fontSize: 20, fontFamily: "Rowdies", fontWeight: FontWeight.w300, color: Colors.black), 37 | children: [ 38 | TextSpan(text: 'fun ', style: TextStyle(color: Colors.red.shade600)), 39 | TextSpan(text: 'and ', style: TextStyle(color: Colors.yellow.shade800)), 40 | TextSpan(text: 'funky ', style: TextStyle(color: Colors.blue.shade700)), 41 | const TextSpan(text: 'or ', style: TextStyle(color: Colors.black)), 42 | TextSpan(text: 'quiet and boring?', style: TextStyle(color: Colors.grey.shade500)), 43 | ], 44 | ), 45 | ), 46 | const SizedBox(height: 48), 47 | ElevatedButton( 48 | onPressed: () { 49 | Navigator.pop(context, true); 50 | }, 51 | child: const Text("Thank you for the music!", style: TextStyle(fontSize: 20, fontWeight: FontWeight.w300)), 52 | style: ElevatedButton.styleFrom( 53 | padding: const EdgeInsets.all(20), 54 | shape: const RoundedRectangleBorder( 55 | borderRadius: BorderRadius.all(Radius.circular(10)), 56 | ), 57 | ), 58 | ), 59 | const SizedBox(height: 8), 60 | TextButton( 61 | onPressed: () { 62 | Navigator.pop(context, false); 63 | }, 64 | child: Text("Shh, everyone's asleep.", style: TextStyle(fontSize: 18, color: Colors.grey.shade500)), 65 | style: TextButton.styleFrom( 66 | padding: const EdgeInsets.all(20), 67 | ), 68 | ), 69 | ], 70 | ), 71 | ), 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/background.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/scheduler.dart'; 3 | import 'package:pushtrix/scenes/cubes.dart'; 4 | import 'package:pushtrix/game/game.dart'; 5 | import 'package:pushtrix/scenes/particle.dart'; 6 | import 'package:pushtrix/scenes/rays.dart'; 7 | import 'package:pushtrix/scenes/scene.dart'; 8 | import 'package:pushtrix/scenes/start_screen.dart'; 9 | 10 | import 'config.dart'; 11 | import 'game/game_time.dart'; 12 | 13 | class BackgroundPainter extends CustomPainter { 14 | final double value; 15 | Scene? scene; 16 | Scene? nextScene; 17 | BackgroundState state; 18 | 19 | BackgroundPainter({required this.value, required this.state, this.scene, this.nextScene}) { 20 | if (nextScene != null) debugPrint('BackgroundPainter $nextScene'); 21 | } 22 | 23 | /* 24 | void dummyPaint(Canvas canvas, Size size) { 25 | var paint = Paint() 26 | ..color = Colors.red 27 | ..strokeWidth = 50 28 | ..strokeCap = StrokeCap.round; 29 | 30 | final center = Offset(size.width / 2, size.height / 2); 31 | final length = min(size.width, size.height); 32 | 33 | // Paint line moving up/down 34 | final range = size.height / 3; 35 | final y = size.height / 2 + sin(value) * range; 36 | canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); 37 | 38 | // Paint center pulsing circle 39 | paint = Paint() 40 | ..color = Colors.blue.shade50 41 | ..strokeWidth = 5 42 | ..strokeCap = StrokeCap.round; 43 | 44 | final rMin = length * 0.4; 45 | final rMax = length * 0.45; 46 | canvas.drawCircle(Offset(center.dx, center.dy), rMin + (rMax - rMin) * sin(4 * value), paint); 47 | 48 | // Paint rotating circles 49 | paint = Paint()..color = Colors.blue.shade100; 50 | 51 | final r2 = length / 2; 52 | canvas.drawCircle(Offset(center.dx + r2 * cos(2 * value), center.dy + r2 * sin(2 * value)), 50, paint); 53 | 54 | paint = Paint()..color = Colors.blue.shade200; 55 | 56 | final r3 = length / 3; 57 | canvas.drawCircle(Offset(center.dx + r3 * cos(2 * value), center.dy + r3 * sin(2 * value)), 20, paint); 58 | } 59 | 60 | void paintPulsingCenter(Canvas canvas, Size size) { 61 | var paint = Paint() 62 | ..color = Colors.red 63 | ..strokeWidth = 50 64 | ..strokeCap = StrokeCap.round; 65 | 66 | final center = Offset(size.width / 2, size.height / 2); 67 | final length = min(size.width, size.height); 68 | 69 | paint = Paint() 70 | ..color = Colors.blue.shade50 71 | ..strokeWidth = 5 72 | ..strokeCap = StrokeCap.round; 73 | 74 | final rMin = length * 0.3; 75 | final rMax = length * 0.35; 76 | canvas.drawCircle(Offset(center.dx, center.dy), rMin + (rMax - rMin) * sin(4 * value), paint); 77 | } 78 | */ 79 | 80 | @override 81 | void paint(Canvas canvas, Size size) { 82 | if (scene == null) { 83 | return; 84 | } 85 | 86 | if (nextScene != null) { 87 | nextScene!.render(canvas, size); 88 | } 89 | 90 | scene!.render(canvas, size); 91 | } 92 | 93 | @override 94 | bool shouldRepaint(CustomPainter oldDelegate) => true; 95 | } 96 | 97 | class Background extends StatefulWidget { 98 | const Background({Key? key}) : super(key: key); 99 | 100 | @override 101 | State createState() => BackgroundState(); 102 | } 103 | 104 | class BackgroundState extends State with TickerProviderStateMixin { 105 | late final AnimationController _controller; 106 | 107 | late final Ticker ticker; 108 | int frame = 0; 109 | 110 | static late BackgroundState instance; 111 | 112 | final _scenes = [ 113 | Rays(), 114 | ParticleSystem(), 115 | Cubes(), 116 | ]; 117 | 118 | final startScreen = StartScreen(); 119 | 120 | int _currentSceneIndex = 0; 121 | 122 | BackgroundState() { 123 | BackgroundState.instance = this; 124 | 125 | if (useAnimationController) { 126 | _controller = AnimationController( 127 | duration: const Duration(seconds: 3600), 128 | upperBound: 3600, 129 | vsync: this, 130 | )..repeat(); 131 | } 132 | 133 | double lastDuration = 0; 134 | ticker = Ticker((duration) { 135 | final current = duration.inMilliseconds / 1000.0; 136 | frame++; 137 | 138 | if (frame & 1 == 0) { 139 | final dt = current - lastDuration; 140 | GameTime.instance.tick(dt); 141 | lastDuration = current; 142 | 143 | _currentScene.tick(); 144 | if (_currentScene.state == SceneState.fadeOut || _currentScene.state == SceneState.done) { 145 | final nextScene = _nextScene; 146 | if (nextScene != null) { 147 | nextScene.tick(); 148 | } 149 | 150 | if (_currentScene.state == SceneState.done) { 151 | _currentScene.reset(); 152 | _currentSceneIndex = (_currentSceneIndex + 1) % _scenes.length; 153 | } 154 | } 155 | 156 | setState(() { 157 | // debugPrint("setState"); 158 | }); 159 | } 160 | }); 161 | 162 | ticker.start(); 163 | } 164 | 165 | void pause() { 166 | ticker.stop(); 167 | } 168 | 169 | void resume() { 170 | ticker.start(); 171 | } 172 | 173 | Scene get _currentScene { 174 | if (Game.instance.state == GameState.startScreen) { 175 | return startScreen; 176 | } else { 177 | return _scenes[_currentSceneIndex]; 178 | } 179 | } 180 | 181 | Scene? get _nextScene { 182 | return _scenes[(_currentSceneIndex + 1) % _scenes.length]; 183 | } 184 | 185 | @override 186 | void dispose() { 187 | super.dispose(); 188 | ticker.dispose(); 189 | if (useAnimationController) { 190 | _controller.dispose(); 191 | } 192 | } 193 | 194 | @override 195 | Widget build(BuildContext context) { 196 | final size = MediaQuery.of(context).size; 197 | 198 | final scene = _currentScene; 199 | final nextScene = _nextScene; 200 | final crossFade = _currentScene.state == SceneState.fadeOut; 201 | 202 | // if (crossFade) { 203 | // debugPrint("$crossFade $nextScene"); 204 | // } 205 | 206 | if (useAnimationController) { 207 | return AnimatedBuilder( 208 | animation: _controller, 209 | builder: (context, _) { 210 | return CustomPaint( 211 | painter: BackgroundPainter( 212 | scene: scene, 213 | nextScene: crossFade ? nextScene : null, 214 | value: _controller.value, 215 | state: this, 216 | ), 217 | size: size, 218 | ); 219 | }); 220 | } else { 221 | return CustomPaint( 222 | size: size, 223 | painter: BackgroundPainter( 224 | value: GameTime.instance.current, 225 | state: this, 226 | scene: scene, 227 | nextScene: crossFade ? nextScene : null, 228 | ), 229 | ); 230 | } 231 | } 232 | 233 | reset() { 234 | _currentSceneIndex = 0; 235 | for (final scene in _scenes) { 236 | scene.reset(); 237 | scene.gameOver(); 238 | } 239 | } 240 | 241 | // continue -> rays restart 242 | } 243 | -------------------------------------------------------------------------------- /lib/build_context_extension.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension BuildContextExtension on BuildContext { 4 | bool isMobile() { 5 | return MediaQuery.of(this).size.width < 600; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/config.dart: -------------------------------------------------------------------------------- 1 | const debugEnabled = false; 2 | const debugBanner = true; 3 | 4 | const screenshotIcon = false; 5 | const sizeFactor = screenshotIcon ? 4.0 : 1.0; 6 | 7 | const useAnimationController = false; 8 | 9 | const alwaysSmallPuzzles = false; 10 | 11 | const tileSize = sizeFactor * 60.0; 12 | const tileBorderSize = sizeFactor * 1.0; 13 | const puzzleBorderSize = sizeFactor * 4.0; 14 | const slideTimeMs = 100; 15 | const dropInAnimMs = 300; 16 | const resetMs = 50; 17 | const levelDurationSeconds = 10.0; 18 | 19 | const leaderboardSize = 20; 20 | const leaderboardDeleteInvalid = false; 21 | 22 | const infiniteTime = false; 23 | const enableIosWebAudio = false; 24 | -------------------------------------------------------------------------------- /lib/env.dart: -------------------------------------------------------------------------------- 1 | import 'package:envied/envied.dart'; 2 | 3 | part 'env.g.dart'; 4 | 5 | @Envied(path: '.env') 6 | abstract class Env { 7 | @EnviedField(obfuscate: true) 8 | static final HASH = _Env.HASH; 9 | 10 | @EnviedField(obfuscate: true) 11 | static final SALT = _Env.SALT; 12 | 13 | @EnviedField() 14 | static const SUPABASE_URL = _Env.SUPABASE_URL; 15 | 16 | @EnviedField() 17 | static const SUPABASE_ANON_KEY = _Env.SUPABASE_ANON_KEY; 18 | } 19 | -------------------------------------------------------------------------------- /lib/game/countdown.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | import 'game.dart'; 6 | 7 | class Countdown extends StatefulWidget { 8 | final Game game; 9 | 10 | const Countdown(this.game, {Key? key}) : super(key: key); 11 | 12 | @override 13 | CountdownState createState() => CountdownState(); 14 | } 15 | 16 | class CountdownState extends State { 17 | double _timerValue = 0; 18 | 19 | double _levelTime = 0; 20 | Timer? _timer; 21 | 22 | @override 23 | void initState() { 24 | super.initState(); 25 | widget.game.setCountdownState(this); 26 | } 27 | 28 | void start(double levelTime) { 29 | stopTimer(); 30 | startTimer(levelTime); 31 | } 32 | 33 | void startTimer([double? remainingTime]) { 34 | _timer = Timer.periodic( 35 | const Duration(milliseconds: 10), 36 | (Timer timer) { 37 | // debugPrint('timer tick ${timer.tick}'); 38 | setState(() { 39 | if (_timerValue <= 0) { 40 | timer.cancel(); 41 | widget.game.onTimerFinished(); 42 | } else { 43 | _timerValue = _levelTime - timer.tick * 0.01; 44 | if (_timerValue < 0) { 45 | _timerValue = 0; 46 | } 47 | 48 | widget.game.tick(_timerValue); 49 | } 50 | }); 51 | // onTimerTick?.call(_timerValue); 52 | }, 53 | ); 54 | 55 | setState(() { 56 | double remaining = remainingTime ?? _timerValue; 57 | _timerValue = remaining; 58 | _levelTime = remaining; 59 | }); 60 | } 61 | 62 | void stopTimer() { 63 | _timer?.cancel(); 64 | } 65 | 66 | void solved() { 67 | stopTimer(); 68 | } 69 | 70 | @override 71 | Widget build(BuildContext context) { 72 | final screenSize = MediaQuery.of(context).size; 73 | const duration = Duration(milliseconds: 200); 74 | 75 | return AnimatedPositioned( 76 | top: screenSize.height / 2 - 77 | widget.game.getPuzzleScreenSize() / 2 - 78 | 70 - 79 | ((widget.game.showCountdown() || widget.game.levelTime == 0) ? 0 : 40), 80 | duration: duration, 81 | child: AnimatedOpacity( 82 | opacity: widget.game.showCountdown() ? 1 : 0, 83 | duration: duration, 84 | child: Container( 85 | width: 120, 86 | decoration: BoxDecoration( 87 | borderRadius: BorderRadius.circular(10), 88 | color: Colors.white, 89 | boxShadow: [ 90 | BoxShadow( 91 | color: Colors.blue.shade400.withOpacity(0.5), 92 | spreadRadius: 3, 93 | blurRadius: 6, 94 | offset: const Offset(0, 3), // changes position of shadow 95 | ), 96 | ], 97 | ), 98 | child: Padding( 99 | padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), 100 | child: Column( 101 | children: [ 102 | Row( 103 | mainAxisAlignment: MainAxisAlignment.center, 104 | children: [ 105 | Text( 106 | _timerValue.toInt().toString(), 107 | style: TextStyle( 108 | fontSize: 30, 109 | fontWeight: FontWeight.bold, 110 | fontFamily: "AzeretMono", 111 | color: Colors.blue.shade600, 112 | ), 113 | ), 114 | const SizedBox(width: 10), 115 | Text( 116 | (_timerValue - _timerValue.toInt()).toStringAsFixed(2).substring(2), 117 | style: TextStyle( 118 | fontSize: 20, 119 | fontWeight: FontWeight.bold, 120 | fontFamily: "AzeretMono", 121 | color: Colors.blue.shade600, 122 | ), 123 | ), 124 | ], 125 | ), 126 | if (Game.instance.levelTime > 0) 127 | LinearProgressIndicator( 128 | value: _timerValue / Game.instance.levelTime, 129 | backgroundColor: Colors.blue.shade50, 130 | ), 131 | ], 132 | ), 133 | ), 134 | ), 135 | ), 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /lib/game/game_time.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | class GameTime { 4 | static late GameTime instance; 5 | 6 | static void init() { 7 | GameTime.instance = GameTime(); 8 | } 9 | 10 | double current = 0; 11 | double dt = 0; 12 | 13 | void tick(double delta) { 14 | if (delta < 0) { 15 | return; 16 | } 17 | 18 | current += delta; 19 | dt = delta; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/game/grid.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pushtrix/game/puzzle.dart'; 3 | 4 | import '../config.dart'; 5 | import 'tile.dart'; 6 | 7 | class Grid extends StatelessWidget { 8 | final Puzzle _puzzle; 9 | final Function(int number)? onTap; 10 | 11 | final bool? withShadow; 12 | 13 | const Grid(this._puzzle, {Key? key, this.onTap, this.withShadow}) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | // Fill list of tiles 18 | List tiles = []; 19 | for (int i = 0; i < _puzzle.tiles.length; i++) { 20 | final number = _puzzle.tiles[i]; 21 | final row = i ~/ _puzzle.size; 22 | final col = i % _puzzle.size; 23 | // 0 is the empty space 24 | if (number != 0) { 25 | tiles.add( 26 | // Animated so tile pushing is smooth 27 | AnimatedPositioned( 28 | key: ValueKey(number), 29 | left: col * tileSize, 30 | top: row * tileSize, 31 | duration: const Duration(milliseconds: slideTimeMs), 32 | child: Tile( 33 | number, 34 | onTap, 35 | _puzzle.canMoveHorizontal(number), 36 | _puzzle.canMoveVertical(number), 37 | ), 38 | ), 39 | ); 40 | } 41 | } 42 | 43 | // Render the container for the tiles 44 | return Container( 45 | decoration: BoxDecoration( 46 | border: Border.all( 47 | color: Colors.blue, 48 | width: puzzleBorderSize, 49 | ), 50 | borderRadius: BorderRadius.circular(tileSize / 5), 51 | boxShadow: [ 52 | if (withShadow != false) 53 | BoxShadow( 54 | color: Colors.blue.shade400.withOpacity(0.5), 55 | spreadRadius: 2, 56 | blurRadius: 4, 57 | offset: const Offset(0, 3), // changes position of shadow 58 | ), 59 | ], 60 | ), 61 | child: Stack( 62 | // Render tiles 63 | children: [ 64 | Container(color: Colors.blue, width: tileSize * _puzzle.size, height: tileSize * _puzzle.size), 65 | ...tiles, 66 | ], 67 | ), 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/game/puzzle.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:pushtrix/config.dart'; 6 | 7 | class Puzzle { 8 | final List tiles = []; 9 | List lastTiles = []; 10 | final List _empty; 11 | final int size; 12 | double _animTime = 0; 13 | 14 | static double getScreenSize(int puzzleHeight) { 15 | return puzzleHeight * tileSize + 2 * puzzleBorderSize; 16 | } 17 | 18 | double get screenSize { 19 | return size * tileSize + 2 * puzzleBorderSize; 20 | } 21 | 22 | Puzzle(this.size, int shuffleCount) : _empty = [size - 1, size - 1] { 23 | reset(); 24 | 25 | if (shuffleCount > 0) { 26 | while (isSolved()) { 27 | reset(); 28 | shuffle(shuffleCount); 29 | } 30 | } 31 | } 32 | 33 | void reset() { 34 | tiles.clear(); 35 | for (int i = 0; i < size * size - 1; i++) { 36 | tiles.add(i + 1); 37 | } 38 | 39 | // Empty is marked with 0 40 | tiles.add(0); 41 | } 42 | 43 | void shuffle(int shuffleCount) { 44 | for (var i = 0; i < shuffleCount; ++i) { 45 | doRandomMove(); 46 | } 47 | } 48 | 49 | void move(int number) { 50 | if (number <= 0 || number > size * size - 1) { 51 | return; 52 | } 53 | 54 | // find coords of number 55 | final coords = _getCoords(number); 56 | final x = coords[0]; 57 | final y = coords[1]; 58 | 59 | if (x == _empty[0]) { 60 | _moveColumn(y); 61 | _debugOutput(); 62 | } else if (y == _empty[1]) { 63 | _moveRow(x); 64 | _debugOutput(); 65 | } 66 | } 67 | 68 | bool isSolved() { 69 | for (int i = 0; i < size * size - 1; i++) { 70 | if (tiles[i] != i + 1) { 71 | return false; 72 | } 73 | } 74 | 75 | return true; 76 | } 77 | 78 | void _startTileMoveAnim() { 79 | lastTiles = tiles.toList(growable: false); 80 | _animTime = 0; 81 | } 82 | 83 | void tickTileMoveAnim(double dt) { 84 | _animTime += dt * 5; 85 | } 86 | 87 | Offset getTileOffset(int number) { 88 | final coords = _getCoords(number); 89 | if (_animTime >= 1) { 90 | return Offset(coords[0] * tileSize + puzzleBorderSize, coords[1] * tileSize + puzzleBorderSize); 91 | } 92 | 93 | final lastCoords = _getCoords(number, true); 94 | final lastX = lastCoords[0]; 95 | final lastY = lastCoords[1]; 96 | 97 | if (coords[0] == lastX && coords[1] == lastY) { 98 | return Offset(coords[0] * tileSize + puzzleBorderSize, coords[1] * tileSize + puzzleBorderSize); 99 | } 100 | 101 | // Animate from last position to current position using animTime 102 | final x = lerpDouble(lastX, coords[0], _animTime) ?? coords[0]; 103 | final y = lerpDouble(lastY, coords[1], _animTime) ?? coords[1]; 104 | 105 | return Offset(x * tileSize + puzzleBorderSize, y * tileSize + puzzleBorderSize); 106 | } 107 | 108 | void _moveRow(int newX) { 109 | _startTileMoveAnim(); 110 | final y = _empty[1]; 111 | if (newX < _empty[0]) { 112 | // Move tiles right 113 | for (var i = _empty[0]; i > newX; i--) { 114 | tiles[y * size + i] = tiles[y * size + i - 1]; 115 | } 116 | } else { 117 | // Move tiles left 118 | for (var i = _empty[0]; i < newX; i++) { 119 | tiles[y * size + i] = tiles[y * size + i + 1]; 120 | } 121 | } 122 | 123 | // Mark the new empty space 124 | _empty[0] = newX; 125 | tiles[y * size + newX] = 0; 126 | } 127 | 128 | void _moveColumn(int newY) { 129 | _startTileMoveAnim(); 130 | final x = _empty[0]; 131 | if (newY < _empty[1]) { 132 | // Move tiles down 133 | for (var i = _empty[1]; i > newY; i--) { 134 | tiles[i * size + x] = tiles[(i - 1) * size + x]; 135 | } 136 | } else { 137 | // Move tiles up 138 | for (var i = _empty[1]; i < newY; i++) { 139 | tiles[i * size + x] = tiles[(i + 1) * size + x]; 140 | } 141 | } 142 | 143 | // Mark the new empty space 144 | _empty[1] = newY; 145 | tiles[newY * size + x] = 0; 146 | } 147 | 148 | List _getCoords(int number, [bool last = false]) { 149 | List t = last && lastTiles.isNotEmpty ? lastTiles : tiles; 150 | for (var y = 0; y < size; ++y) { 151 | for (var x = 0; x < size; ++x) { 152 | if (t[y * size + x] == number) { 153 | return [x, y]; 154 | } 155 | } 156 | } 157 | return []; 158 | } 159 | 160 | bool canMoveHorizontal(int number) { 161 | if (number <= 0 || number > size * size - 1) { 162 | return false; 163 | } 164 | 165 | final coords = _getCoords(number); 166 | return coords[1] == _empty[1]; 167 | } 168 | 169 | bool canMoveVertical(int number) { 170 | if (number <= 0 || number > size * size - 1) { 171 | return false; 172 | } 173 | 174 | final coords = _getCoords(number); 175 | return coords[0] == _empty[0]; 176 | } 177 | 178 | // Debug print to console 179 | void _debugOutput() { 180 | if (!debugEnabled) { 181 | return; 182 | } 183 | 184 | var line = ""; 185 | for (var i = 0; i < size; i++) { 186 | for (var j = 0; j < size; j++) { 187 | if (tiles[i * size + j] != 0) { 188 | line += tiles[i * size + j].toString().padLeft(2) + " "; 189 | } else { 190 | line += " "; 191 | } 192 | } 193 | debugPrint(line); 194 | line = ""; 195 | } 196 | 197 | debugPrint(line); 198 | } 199 | 200 | void doRandomMove() { 201 | final xOrY = Random().nextInt(2); 202 | if (xOrY == 0) { 203 | // Fill array with empty x values 204 | final empty = [for (var j = 0; j < size - 1; j++) j < _empty[0] ? j : j + 1]; 205 | final newEmpty = empty[Random().nextInt(size - 1)]; 206 | // Move row starting from newEmpty value 207 | _moveRow(newEmpty); 208 | } else { 209 | // Fill array with empty y values 210 | final empty = [for (var j = 0; j < size - 1; j++) j < _empty[1] ? j : j + 1]; 211 | final newEmpty = empty[Random().nextInt(size - 1)]; 212 | // Move column starting from newEmpty value 213 | _moveColumn(newEmpty); 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /lib/game/save_game.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shared_preferences/shared_preferences.dart'; 3 | 4 | class SaveGame { 5 | late final SharedPreferences _prefs; 6 | 7 | int maxLevel = 0; 8 | bool finishedOnce = false; 9 | bool soundEnabled = true; 10 | 11 | static late final SaveGame instance; 12 | 13 | SaveGame() { 14 | SaveGame.instance = this; 15 | } 16 | 17 | Future init() async { 18 | _prefs = await SharedPreferences.getInstance(); 19 | maxLevel = _prefs.getInt("maxLevel") ?? 0; 20 | finishedOnce = _prefs.getBool("finishedOnce") ?? false; 21 | soundEnabled = _prefs.getBool("soundEnabled") ?? true; 22 | debugPrint("load sound: $soundEnabled"); 23 | } 24 | 25 | Future saveLevel(int level) async { 26 | if (level > maxLevel) { 27 | maxLevel = level; 28 | await _prefs.setInt("maxLevel", level); 29 | } 30 | } 31 | 32 | Future enableSound(bool enabled) async { 33 | soundEnabled = enabled; 34 | debugPrint("save sound: $enabled"); 35 | await _prefs.setBool("soundEnabled", enabled); 36 | } 37 | 38 | Future gameOver(int level) async { 39 | finishedOnce = true; 40 | await _prefs.setBool("finishedOnce", true); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/game/tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | import '../audio.dart'; 4 | import '../config.dart'; 5 | 6 | class Tile extends StatelessWidget { 7 | final int number; 8 | final Function(int number)? onTap; 9 | final bool canMoveHorizontal; 10 | final bool canMoveVertical; 11 | final bool? red; 12 | 13 | const Tile(this.number, this.onTap, this.canMoveHorizontal, this.canMoveVertical, {Key? key, this.red}) : super(key: key); 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return GestureDetector( 18 | onPanDown: (_) { 19 | onTap?.call(number); 20 | Audio.instance.swish(); 21 | }, 22 | // onVerticalDragStart: (details) { 23 | // if (canMoveVertical) { 24 | // onTap?.call(number); 25 | // } 26 | // }, 27 | // onHorizontalDragStart: (details) { 28 | // if (canMoveHorizontal) { 29 | // onTap?.call(number); 30 | // } 31 | // }, 32 | child: MouseRegion( 33 | cursor: SystemMouseCursors.click, 34 | child: Container( 35 | width: tileSize, 36 | height: tileSize, 37 | decoration: BoxDecoration( 38 | color: Colors.white, 39 | border: Border.all( 40 | color: Colors.blue, 41 | width: tileBorderSize, 42 | ), 43 | borderRadius: BorderRadius.circular(tileSize / 5), 44 | ), 45 | child: Center( 46 | child: Text( 47 | '$number', 48 | style: TextStyle( 49 | fontSize: tileSize / 2, 50 | fontWeight: FontWeight.bold, 51 | color: Colors.blue.shade800, 52 | ), 53 | ), 54 | ), 55 | ), 56 | ), 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/highscore_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:pushtrix/build_context_extension.dart'; 4 | import 'package:pushtrix/config.dart'; 5 | import 'package:pushtrix/leaderboard.dart'; 6 | import 'package:pushtrix/leaderboard_dialog.dart'; 7 | 8 | class UpperCaseTextFormatter extends TextInputFormatter { 9 | @override 10 | TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { 11 | return TextEditingValue( 12 | text: newValue.text.toUpperCase(), 13 | selection: newValue.selection, 14 | ); 15 | } 16 | } 17 | 18 | class HighScoreDialog extends StatefulWidget { 19 | final int position; 20 | final int score; 21 | 22 | HighScoreDialog(this.position, this.score, {Key? key}) : super(key: key); 23 | 24 | @override 25 | State createState() => _HighScoreDialogState(); 26 | } 27 | 28 | class _HighScoreDialogState extends State { 29 | String name = ""; 30 | bool sendEnabled = false; 31 | bool sending = false; 32 | 33 | void send() async { 34 | sending = true; 35 | await saveHighScore(name, widget.score); 36 | Navigator.pop(context, true); 37 | sending = false; 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | final textStyle = getLeaderboardTextStyle(context); 43 | 44 | const range = 2; 45 | const first = 0; 46 | const last = leaderboardSize - 1; 47 | var start = widget.position - range; 48 | var end = widget.position + range; 49 | 50 | if (start < first) { 51 | final diff = -start; 52 | start = first; 53 | end += diff; 54 | } else if (end > last) { 55 | final diff = end - last; 56 | start -= diff; 57 | end = last; 58 | } 59 | 60 | List entries = []; 61 | for (int i = start; i < widget.position; i++) { 62 | final widget = LeaderboardEntryWidget( 63 | name: leaderboard[i].name, 64 | score: leaderboard[i].score, 65 | marginBottom: 8, 66 | ); 67 | entries.add(widget); 68 | } 69 | 70 | final rankColor = getRankColor(getRank(widget.score)); 71 | final rankStyle = textStyle.copyWith(color: rankColor); 72 | entries.add( 73 | Container( 74 | width: 320, 75 | margin: const EdgeInsets.only(bottom: 8), 76 | child: Row( 77 | mainAxisSize: MainAxisSize.max, 78 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 79 | crossAxisAlignment: CrossAxisAlignment.center, 80 | children: [ 81 | Expanded( 82 | child: Padding( 83 | padding: const EdgeInsets.only(top: 8, right: 32), 84 | child: TextField( 85 | maxLength: 14, 86 | style: rankStyle, 87 | inputFormatters: [ 88 | FilteringTextInputFormatter.deny(RegExp("[ ]")), 89 | UpperCaseTextFormatter(), 90 | ], 91 | autofocus: true, 92 | decoration: const InputDecoration( 93 | hintText: 'Enter Name', 94 | counterText: '', 95 | isCollapsed: true, 96 | contentPadding: EdgeInsets.only(bottom: 8), 97 | ), 98 | onChanged: (value) { 99 | name = value; 100 | setState(() { 101 | name.isNotEmpty ? sendEnabled = true : sendEnabled = false; 102 | }); 103 | }, 104 | onEditingComplete: name.isNotEmpty ? send : null, 105 | ), 106 | ), 107 | ), 108 | Text(widget.score > 0 ? "LVL ${getScoreString(widget.score)}" : "-", style: rankStyle), 109 | ], 110 | ), 111 | ), 112 | ); 113 | 114 | for (int i = widget.position; i < end; i++) { 115 | final widget = LeaderboardEntryWidget( 116 | name: leaderboard[i].name, 117 | score: leaderboard[i].score, 118 | marginBottom: 8, 119 | ); 120 | entries.add(widget); 121 | } 122 | 123 | return Center( 124 | child: SingleChildScrollView( 125 | child: Dialog( 126 | backgroundColor: Colors.white, 127 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), 128 | child: Container( 129 | padding: EdgeInsets.symmetric(horizontal: context.isMobile() ? 16 : 32, vertical: 24), 130 | child: ConstrainedBox( 131 | constraints: const BoxConstraints(minWidth: 320), 132 | child: Column( 133 | mainAxisSize: MainAxisSize.min, 134 | children: [ 135 | Text( 136 | "New High Score!", 137 | style: textStyle.copyWith(fontSize: 24), 138 | ), 139 | const SizedBox(height: 16), 140 | ...entries, 141 | const SizedBox(height: 32), 142 | buildSendButton(context), 143 | ], 144 | ), 145 | ), 146 | ), 147 | ), 148 | ), 149 | ); 150 | } 151 | 152 | ElevatedButton buildSendButton(BuildContext context) { 153 | return ElevatedButton( 154 | onPressed: (sendEnabled && !sending) ? send : null, 155 | child: const Text("Send", style: TextStyle(fontSize: 20, fontWeight: FontWeight.w300)), 156 | style: ElevatedButton.styleFrom( 157 | padding: const EdgeInsets.all(20), 158 | shape: const RoundedRectangleBorder( 159 | borderRadius: BorderRadius.all(Radius.circular(10)), 160 | ), 161 | ), 162 | ); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lib/leaderboard.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:crypto/crypto.dart'; 4 | import 'package:eval_ex/expression.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:pushtrix/config.dart'; 7 | import 'package:supabase_flutter/supabase_flutter.dart'; 8 | 9 | import 'env.dart'; 10 | 11 | final supabase = Supabase.instance.client; 12 | 13 | class LeaderboardEntry { 14 | String name = ""; 15 | int score = 0; 16 | } 17 | 18 | List leaderboard = []; 19 | List topScores = []; 20 | 21 | String scoreToHash(String name, int score, double salt) { 22 | final lower = name.toLowerCase(); 23 | var saltedScore = utf8.encode("$lower$score${Env.HASH}$salt"); 24 | final hash = sha256.convert(saltedScore).toString(); 25 | 26 | return hash; 27 | } 28 | 29 | int scoreFromHash(String name, String hash, double salt) { 30 | final lower = name.toLowerCase(); 31 | 32 | // reverse hash 33 | for (int i = 0; i < 1000; ++i) { 34 | var saltedScore = utf8.encode("$lower$i${Env.HASH}$salt"); 35 | final value = sha256.convert(saltedScore).toString(); 36 | if (value == hash) { 37 | return i; 38 | } 39 | } 40 | 41 | return -1; 42 | } 43 | 44 | Future refreshLeaderboard([int newScore = 0]) async { 45 | leaderboard.clear(); 46 | try { 47 | debugPrint("Refresh leaderboard"); 48 | final data = await supabase 49 | .from("leaderboard") 50 | .select("id,name,salt,hash") 51 | .order("salt", ascending: true) 52 | .order("created_at", ascending: true) 53 | .order("hash") 54 | .limit(leaderboardSize + 10) // +10 to add some buffer for illegal score entries 55 | ; 56 | 57 | for (int i = 0; i < leaderboardSize; ++i) { 58 | final value = data[i]; 59 | final name = value['name']; 60 | final salt = value["salt"]; 61 | final hash = value['hash']; 62 | final score = hash != null ? scoreFromHash(name, hash, salt) : -1; 63 | final scoreValid = score >= 0; 64 | if (scoreValid) { 65 | LeaderboardEntry leaderboardEntry = LeaderboardEntry(); 66 | leaderboardEntry.name = name; 67 | leaderboardEntry.score = score; 68 | leaderboard.add(leaderboardEntry); 69 | } else { 70 | // Delete invalid entry 71 | if (leaderboardDeleteInvalid) { 72 | await supabase.from("leaderboard").delete().eq("id", value["id"]); 73 | } 74 | } 75 | } 76 | } catch (error) { 77 | // TODO: Proper error handling 78 | debugPrint("Error refreshing leaderboard: $error"); 79 | } 80 | 81 | // If leaderboard is not full -> fill up with "PUSHTRIX" entries 82 | if (leaderboard.length < leaderboardSize) { 83 | for (int i = leaderboard.length; i < leaderboardSize; i++) { 84 | LeaderboardEntry leaderboardEntry = LeaderboardEntry(); 85 | leaderboardEntry.name = "PUSHTRIX"; 86 | leaderboardEntry.score = 0; 87 | leaderboard.add(leaderboardEntry); 88 | } 89 | } 90 | 91 | List scores = []; 92 | for (LeaderboardEntry entry in leaderboard) { 93 | scores.add(entry.score); 94 | } 95 | 96 | scores.add(newScore); 97 | 98 | topScores = scores.toSet().toList(); 99 | topScores.sort((a, b) => b.compareTo(a)); 100 | } 101 | 102 | String getScoreString(int score) { 103 | int top = topScores[0]; 104 | int numDigits = top.toString().length; 105 | return score.toString().padLeft(numDigits, "0"); 106 | } 107 | 108 | // Returns index in leaderboard or -1 109 | Future isHighScore(int score) async { 110 | await refreshLeaderboard(score); 111 | if (score <= leaderboard.last.score) { 112 | return -1; 113 | } 114 | 115 | for (int i = 0; i < leaderboard.length; i++) { 116 | if (leaderboard[i].score < score) { 117 | return i; 118 | } 119 | } 120 | 121 | return -1; 122 | } 123 | 124 | int getRank(int score) { 125 | for (int i = 0; i < topScores.length; ++i) { 126 | if (topScores[i] == score) { 127 | return i; 128 | } 129 | } 130 | 131 | return -1; 132 | } 133 | 134 | Future saveHighScore(String name, int score) async { 135 | try { 136 | final salt = Expression(Env.SALT).setStringVariable("x", score.toString()).eval()?.toDouble() ?? 0.0; 137 | await supabase.from("leaderboard").insert({ 138 | "name": name.toUpperCase(), 139 | "salt": salt, 140 | "hash": scoreToHash(name, score, salt), 141 | }); 142 | } on PostgrestException catch (error) { 143 | // 23505 is unique key violation, that's ok since we only allow once name/score combo in the leaderboard 144 | if (error.code != "23505") { 145 | // TODO: Proper error handling 146 | print("Database error saving highscore: $error"); 147 | } 148 | } catch (error) { 149 | // TODO: Proper error handling 150 | print("Error saving highscore: $error"); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /lib/leaderboard_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:pushtrix/build_context_extension.dart'; 3 | import 'package:pushtrix/leaderboard.dart'; 4 | 5 | Color getRankColor(int rank) { 6 | if (rank == 0) { 7 | return Colors.red.shade600; 8 | } else if (rank == 1) { 9 | return Colors.yellow.shade800; 10 | } else if (rank == 2) { 11 | return Colors.blue.shade700; 12 | } 13 | 14 | return Colors.black; 15 | } 16 | 17 | TextStyle getLeaderboardTextStyle(BuildContext context) { 18 | return TextStyle(fontSize: context.isMobile() ? 18 : 20, fontFamily: "AzeretMono"); 19 | } 20 | 21 | class LeaderboardDialog extends StatelessWidget { 22 | const LeaderboardDialog({ 23 | Key? key, 24 | }) : super(key: key); 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | final entries = leaderboard.map((e) { 29 | return LeaderboardEntryWidget(name: e.name, score: e.score); 30 | }).toList(); 31 | 32 | final textStyle = getLeaderboardTextStyle(context); 33 | 34 | return Dialog( 35 | backgroundColor: Colors.white, 36 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), 37 | child: Container( 38 | padding: EdgeInsets.symmetric(horizontal: context.isMobile() ? 0 : 16, vertical: 24), 39 | child: ConstrainedBox( 40 | constraints: const BoxConstraints(minWidth: 320), 41 | child: Column( 42 | mainAxisSize: MainAxisSize.min, 43 | children: [ 44 | Text( 45 | "HIGH SCORES", 46 | style: textStyle.copyWith(fontSize: 24), 47 | ), 48 | const SizedBox(height: 16), 49 | buildContent(entries), 50 | const SizedBox(height: 32), 51 | buildCloseButton(context), 52 | ], 53 | ), 54 | ), 55 | ), 56 | ); 57 | } 58 | 59 | Flexible buildContent(List entries) { 60 | return Flexible( 61 | child: Theme( 62 | data: ThemeData(scrollbarTheme: const ScrollbarThemeData(thumbVisibility: MaterialStatePropertyAll(true))), 63 | child: SingleChildScrollView( 64 | child: Padding( 65 | padding: const EdgeInsets.symmetric(horizontal: 16), 66 | child: Column(children: [ 67 | ...entries, 68 | ]), 69 | ), 70 | ), 71 | ), 72 | ); 73 | } 74 | 75 | ElevatedButton buildCloseButton(BuildContext context) { 76 | return ElevatedButton( 77 | onPressed: () { 78 | Navigator.pop(context, true); 79 | }, 80 | child: const Text("Close", style: TextStyle(fontSize: 20, fontWeight: FontWeight.w300)), 81 | style: ElevatedButton.styleFrom( 82 | padding: const EdgeInsets.all(20), 83 | shape: const RoundedRectangleBorder( 84 | borderRadius: BorderRadius.all(Radius.circular(10)), 85 | ), 86 | ), 87 | ); 88 | } 89 | } 90 | 91 | class LeaderboardEntryWidget extends StatelessWidget { 92 | const LeaderboardEntryWidget({ 93 | Key? key, 94 | required this.name, 95 | required this.score, 96 | this.marginBottom = 4, 97 | }) : super(key: key); 98 | 99 | final String name; 100 | final int score; 101 | final double marginBottom; 102 | 103 | @override 104 | Widget build(BuildContext context) { 105 | final color = getRankColor(getRank(score)); 106 | final textStyle = getLeaderboardTextStyle(context).copyWith(color: color); 107 | 108 | return Container( 109 | width: 320, 110 | margin: EdgeInsets.only(bottom: marginBottom), 111 | child: Row( 112 | mainAxisSize: MainAxisSize.max, 113 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 114 | children: [ 115 | Flexible(child: Text(name.toUpperCase(), style: textStyle)), 116 | Text(score > 0 ? "LVL ${getScoreString(score)}" : "-", style: textStyle), 117 | ], 118 | ), 119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/lerp_value.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | class LerpValue { 6 | double value = 0; 7 | double currentTime = 0; 8 | double from = 0; 9 | double to = 0; 10 | double durationSeconds = 0; 11 | final curve = Curves.easeInOut; 12 | 13 | LerpValue(double? value) { 14 | if (value != null) { 15 | this.value = value; 16 | from = value; 17 | to = value; 18 | } 19 | } 20 | 21 | void set(double value) { 22 | this.value = value; 23 | from = value; 24 | to = value; 25 | durationSeconds = 0; 26 | } 27 | 28 | void lerpTo(double to, [double durationSeconds = 1]) { 29 | if (to == this.to && durationSeconds == this.durationSeconds) { 30 | return; 31 | } 32 | 33 | from = value; 34 | this.to = to; 35 | this.durationSeconds = durationSeconds; 36 | currentTime = 0; 37 | } 38 | 39 | void tick(double dt) { 40 | if (durationSeconds == 0) { 41 | value = to; 42 | return; 43 | } 44 | 45 | currentTime += dt; 46 | if (currentTime >= durationSeconds) { 47 | value = to; 48 | durationSeconds = 0; 49 | } else { 50 | final t = max(0.0, min(1.0, currentTime / durationSeconds)); 51 | value = from + curve.transform(t) * (to - from); 52 | } 53 | } 54 | 55 | bool get isLerping => durationSeconds > 0; 56 | } 57 | -------------------------------------------------------------------------------- /lib/music_credits.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MusicCredits extends StatelessWidget { 4 | const MusicCredits({ 5 | Key? key, 6 | required bool creditsShown, 7 | }) : _creditsShown = creditsShown, 8 | super(key: key); 9 | 10 | final bool _creditsShown; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return AnimatedOpacity( 15 | duration: const Duration(milliseconds: 500), 16 | opacity: _creditsShown ? 1 : 0, 17 | child: Container( 18 | decoration: BoxDecoration( 19 | color: Colors.white, 20 | borderRadius: BorderRadius.circular(8), 21 | boxShadow: [ 22 | BoxShadow( 23 | color: Colors.black.withOpacity(0.3), 24 | blurRadius: 8, 25 | offset: const Offset(2, 4), 26 | ), 27 | ], 28 | ), 29 | padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), 30 | child: const Text("Music by Bensound.com"), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/scenes/cubes.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:pushtrix/game/game_time.dart'; 5 | import 'package:pushtrix/lerp_value.dart'; 6 | import 'package:pushtrix/scenes/scene.dart'; 7 | import 'package:vector_math/vector_math_64.dart'; 8 | 9 | final cube = [ 10 | // Back face 11 | Vector4(1, 1, -1, 1), 12 | Vector4(1, -1, -1, 1), 13 | Vector4(-1, -1, -1, 1), 14 | Vector4(-1, 1, -1, 1), 15 | 16 | // Front face 17 | Vector4(1, 1, 1, 1), 18 | Vector4(1, -1, 1, 1), 19 | Vector4(-1, -1, 1, 1), 20 | Vector4(-1, 1, 1, 1), 21 | ]; 22 | 23 | final cubeFaces = [ 24 | // back 25 | Vector4(0, 1, 2, 3), 26 | // front 27 | Vector4(7, 6, 5, 4), 28 | // sides 29 | Vector4(0, 4, 5, 1), 30 | Vector4(1, 5, 6, 2), 31 | Vector4(2, 6, 7, 3), 32 | Vector4(3, 7, 4, 0), 33 | ]; 34 | 35 | const numCubes = 9; 36 | const start1 = 0; 37 | const start3 = 1; 38 | const start5 = 4; 39 | const start7 = 7; 40 | const start9 = 10; 41 | const startFade = 8; 42 | const fadeOut = 18; 43 | 44 | class Cubes extends Scene { 45 | List _lerpValues = []; 46 | 47 | double time = 0; 48 | final _backFaceFade = LerpValue(0); 49 | final _frontFaceFade = LerpValue(0); 50 | 51 | late final List _faceNormals = []; 52 | 53 | Cubes() { 54 | reset(); 55 | for (final face in cubeFaces) { 56 | final v1 = cube[face[0].toInt()]; 57 | final v2 = cube[face[1].toInt()]; 58 | final v3 = cube[face[2].toInt()]; 59 | 60 | final v1v2 = v2 - v1; 61 | final v1v3 = v3 - v1; 62 | 63 | final faceNormal = v1v2.xyz.cross(v1v3.xyz); 64 | faceNormal.normalize(); 65 | debugPrint("faceNormal: $faceNormal"); 66 | _faceNormals.add(Vector4(faceNormal.x, faceNormal.y, faceNormal.z, 0)); 67 | } 68 | } 69 | 70 | void drawFace(Canvas canvas, Paint paint, List vertices, Vector4 face, double centerX, double centerY) { 71 | final v1 = vertices[face[0].toInt()]; 72 | final v2 = vertices[face[1].toInt()]; 73 | final v3 = vertices[face[2].toInt()]; 74 | final v4 = vertices[face[3].toInt()]; 75 | 76 | canvas.drawPath( 77 | Path() 78 | ..moveTo(v1.x * centerX / v1.w + centerX, v1.y * centerY / v1.w + centerY) 79 | ..lineTo(v2.x * centerX / v2.w + centerX, v2.y * centerY / v2.w + centerY) 80 | ..lineTo(v3.x * centerX / v3.w + centerX, v3.y * centerY / v3.w + centerY) 81 | ..lineTo(v4.x * centerX / v4.w + centerX, v4.y * centerY / v4.w + centerY) 82 | ..close(), 83 | paint, 84 | ); 85 | } 86 | 87 | @override 88 | void render(Canvas canvas, Size size) { 89 | final rz = Matrix4.rotationZ(time * 1); 90 | final ry = Matrix4.rotationY(time * 0.0); 91 | final rx = Matrix4.rotationX(time * 4); 92 | 93 | final camPos = Vector3(0, 0, (sin(time * 2) + 1) * 3 + 2.0); 94 | final camUp = Vector3(cos(time), sin(time), 0); 95 | final view = makeViewMatrix(camPos, Vector3(0, 0, 0), camUp); 96 | 97 | // final rz = Matrix4.rotationZ(time * 0.0); 98 | // final ry = Matrix4.rotationY(time * 0.0); 99 | // final rx = Matrix4.rotationX(time * 0.0); 100 | 101 | // final camPos = Vector3(0, 0, 5); 102 | // final view = makeViewMatrix(Vector3(0, 0, 5), Vector3(0, 0, 0), Vector3(0, 1, 0)); 103 | 104 | final projection = makePerspectiveMatrix(pi / 3, size.width / size.height, 0.1, 100); 105 | final vp = projection * view; 106 | 107 | // calculate center 108 | final cx = size.width / 2; 109 | final cy = size.height / 2; 110 | 111 | final paint = Paint()..strokeWidth = 4.0; 112 | 113 | const dist = 3.5; 114 | 115 | for (var i = 0; i < numCubes; i++) { 116 | for (var j = 0; j < numCubes; j++) { 117 | final value = _lerpValues[i * numCubes + j].value; 118 | 119 | final pos = Vector3( 120 | (i - numCubes ~/ 2) * dist, 121 | (j - numCubes ~/ 2) * dist, 122 | -8 - value * 200, 123 | ); 124 | final translation = Matrix4.translation(pos); 125 | 126 | paint.style = PaintingStyle.stroke; 127 | paint.strokeWidth = 3.0; 128 | 129 | final mvp = vp * translation * rz * ry * rx; 130 | List out = []; 131 | for (final v in cube) { 132 | out.add(mvp.transformed(v)); 133 | } 134 | 135 | final mv = view * rz * ry * rx; 136 | List outNormals = []; 137 | for (var i = 0; i < _faceNormals.length; i++) { 138 | outNormals.add(mv.transformed(_faceNormals[i])); 139 | } 140 | 141 | List frontFaces = []; 142 | 143 | // Draw back faces 144 | paint.color = Color.lerp(const Color(0xffff0000), const Color(0x00ffffff), value + _backFaceFade.value)!; 145 | for (var i = 0; i < cubeFaces.length; i++) { 146 | final face = cubeFaces[i]; 147 | final faceNormal = outNormals[i]; 148 | 149 | final viewPos = view * (pos - camPos); 150 | final dot = viewPos.dot(faceNormal.xyz); 151 | if (dot < 0) { 152 | frontFaces.add(i); 153 | continue; 154 | } 155 | 156 | drawFace(canvas, paint, out, face, cx, cy); 157 | } 158 | 159 | // Draw front faces 160 | paint.color = Color.lerp(const Color(0xffff0000), const Color(0x00ffffff), value + _frontFaceFade.value)!; 161 | for (var i = 0; i < frontFaces.length; i++) { 162 | final face = cubeFaces[frontFaces[i]]; 163 | drawFace(canvas, paint, out, face, cx, cy); 164 | } 165 | } 166 | } 167 | } 168 | 169 | @override 170 | void reset() { 171 | super.reset(); 172 | time = 0; 173 | _backFaceFade.set(0); 174 | _frontFaceFade.set(0); 175 | _lerpValues = []; 176 | for (int i = 0; i < numCubes * numCubes; i++) { 177 | _lerpValues.add(LerpValue(1.0)); 178 | } 179 | } 180 | 181 | @override 182 | void tick() { 183 | final dt = GameTime.instance.dt; 184 | time += dt; 185 | 186 | const mid = numCubes ~/ 2; 187 | bool isInRange(i, j, r) => (i >= mid - r && i <= mid + r) && (j >= mid - r && j <= mid + r); 188 | bool is1(int i, int j) => i == mid && j == mid; 189 | bool is3(int i, int j) => !is1(i, j) && isInRange(i, j, 1); 190 | bool is5(int i, int j) => !is3(i, j) && isInRange(i, j, 2); 191 | bool is7(int i, int j) => !is5(i, j) && isInRange(i, j, 3); 192 | bool is9(int i, int j) => !is7(i, j) && isInRange(i, j, 4); 193 | 194 | for (int i = 0; i < numCubes; i++) { 195 | for (int j = 0; j < numCubes; j++) { 196 | final index = i * numCubes + j; 197 | _lerpValues[index].tick(dt); 198 | if ((time > start1 && is1(i, j)) || 199 | (time > start3 && is3(i, j)) || 200 | (time > start5 && is5(i, j)) || 201 | (time > start7 && is7(i, j)) || 202 | (time > start9 && is9(i, j))) { 203 | _lerpValues[index].lerpTo(0.0, 1.0); 204 | } 205 | } 206 | } 207 | 208 | if (time > startFade) { 209 | _backFaceFade.lerpTo(0.7, 1.0); 210 | _backFaceFade.tick(dt); 211 | } 212 | 213 | if (time > fadeOut) { 214 | state = SceneState.fadeOut; 215 | _frontFaceFade.lerpTo(1.0, 1.0); 216 | _frontFaceFade.tick(dt); 217 | if (_frontFaceFade.value == 1.0) { 218 | state = SceneState.done; 219 | } 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /lib/scenes/particle.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart' hide Colors; 4 | import 'package:pushtrix/game/game_time.dart'; 5 | import 'package:pushtrix/lerp_value.dart'; 6 | import 'package:pushtrix/scenes/scene.dart'; 7 | import 'package:vector_math/vector_math.dart'; 8 | 9 | const rotateStartAtSec = 0.2; 10 | const rotateDuration = 2.0; 11 | 12 | const swirlStartAtSec = 2; 13 | const swirlDuration = 4.0; 14 | 15 | const flickerStartsAtSec = 10; 16 | const flickerDuration = 1.0; 17 | 18 | const colorsStartAtSec = 22; 19 | const colorDuration = 2.0; 20 | 21 | const toBlackStartAtSec = 50; 22 | const toBlackDuration = 0.5; 23 | 24 | const fadeOutStartAtSec = 60; 25 | 26 | const fadeInDuration = 2; 27 | const fadeOutDuration = 2.0; 28 | 29 | class Particle { 30 | Vector3 position = Vector3.zero(); 31 | Vector3 originalPosition = Vector3.zero(); 32 | double originalDistance = 0.0; 33 | Vector3 targetPosition = Vector3.zero(); 34 | Vector4 color = Vector4.zero(); 35 | double size = 0.0; 36 | LerpValue alpha = LerpValue(0.0); 37 | } 38 | 39 | double wrap(double x, double min, double max) { 40 | return x - (max - min) * (x / (max - min)).floor(); 41 | } 42 | 43 | class ParticleSystem extends Scene { 44 | List particles = []; 45 | double width = 0; 46 | double height = 0; 47 | double maxDistance = 0; 48 | double time = 0; 49 | 50 | final colorLerp = LerpValue(0); 51 | final zFlickerLerp = LerpValue(0); 52 | final rotateLerp = LerpValue(0); 53 | final swirlLerp = LerpValue(0); 54 | final goToBlack = LerpValue(1); 55 | final fadeOut = LerpValue(0); 56 | 57 | ParticleSystem({int? count}) { 58 | if (count != null) { 59 | particles = List.generate(count, (index) => Particle()); 60 | } 61 | 62 | _init2dGrid(); 63 | } 64 | 65 | @override 66 | void render(Canvas canvas, Size size) { 67 | final maxSide = max(size.width, size.height) * 1.3; 68 | final centerX = size.width / 2; 69 | final centerY = size.height / 2; 70 | final factorX = maxSide / width; 71 | final factorY = maxSide / height; 72 | 73 | final timeSwing = sin(time * 0.3) * 0.0015; 74 | 75 | Paint paint = Paint(); 76 | 77 | var brightness = (goToBlack.value * 255).toInt(); 78 | if (state == SceneState.fadeOut) { 79 | brightness = (fadeOut.value * 255).toInt(); 80 | } 81 | paint.color = Color.fromRGBO(brightness, brightness, brightness, 1 - fadeOut.value); 82 | canvas.drawPaint(paint); 83 | 84 | for (var p in particles) { 85 | // Fade in 86 | if (time < fadeInDuration && p.originalDistance > (time / fadeInDuration) * maxDistance) { 87 | continue; 88 | } 89 | 90 | // fade in/out 91 | p.alpha.lerpTo(state == SceneState.fadeOut ? 0 : 1, 1.0); 92 | p.alpha.tick(GameTime.instance.dt); 93 | 94 | final angle = swirlLerp.value * timeSwing * p.originalDistance; 95 | 96 | final rot = Quaternion.euler(0, 0, angle); 97 | final pos = rot.rotated(p.position); 98 | 99 | paint.color = Color.fromRGBO( 100 | (p.color.x * 255).toInt(), 101 | (p.color.y * 255).toInt(), 102 | (p.color.z * 255).toInt(), 103 | (p.position.z + 1) * 0.75 * p.alpha.value, 104 | ); 105 | 106 | double x = pos.x * factorX + centerX; 107 | double y = pos.y * factorY + centerY; 108 | double offset = (0.55 * factorX) / (pos.z * 0.8 + 1.1); 109 | double radius = p.size * offset; 110 | 111 | // canvas.save(); 112 | // canvas.rotate(angle * 0.3); 113 | canvas.drawCircle(Offset(x, y), radius, paint); 114 | // canvas.restore(); 115 | } 116 | } 117 | 118 | void _init2dGrid() { 119 | const widthHalf = 800.0; 120 | const heightHalf = 800.0; 121 | 122 | const dx = 60.0; 123 | const dy = 60.0; 124 | 125 | for (double x = -widthHalf; x < widthHalf; x += dx) { 126 | for (double y = -heightHalf; y < heightHalf + dy; y += dy) { 127 | final particle = Particle(); 128 | particle.position.x = x; 129 | particle.position.y = y; 130 | particle.targetPosition.x = x; 131 | particle.targetPosition.y = y; 132 | particle.originalPosition.x = x; 133 | particle.originalPosition.y = y; 134 | particle.originalDistance = particle.position.distanceTo(Vector3.zero()); 135 | maxDistance = max(particle.originalDistance, maxDistance); 136 | 137 | particle.size = 50.0; 138 | 139 | particles.add(particle); 140 | } 141 | } 142 | 143 | width = 2 * widthHalf; 144 | height = 2 * heightHalf; 145 | } 146 | 147 | @override 148 | void tick() { 149 | final dt = GameTime.instance.dt; 150 | time += dt; 151 | final cosine = cos(time); 152 | final sine = sin(time); 153 | 154 | for (var particle in particles) { 155 | particle.position.x = particle.originalPosition.x + (0.2 + rotateLerp.value) * cosine * 30; 156 | particle.position.y = particle.originalPosition.y + (0.2 + rotateLerp.value) * sine * 100; 157 | particle.position.z = zFlickerLerp.value * 0.3 * sin(5 * time + particle.position.x * 0.3 + particle.position.y * 0.1); 158 | Vector4 color = Vector4.zero(); 159 | 160 | // Lerp between red and hsl shift 161 | Colors.hslToRgb(Vector4(sine * particle.originalDistance / 900, 1.0, 0.5, 1.0), color); 162 | particle.color.x = (1 - colorLerp.value) + (colorLerp.value) * color.x; 163 | particle.color.y = (1 - colorLerp.value) * 50 / 255 + (colorLerp.value) * color.y; 164 | particle.color.z = (1 - colorLerp.value) * 50 / 255 + (colorLerp.value) * color.z; 165 | particle.color.w = 1; 166 | } 167 | 168 | if (time < colorsStartAtSec) { 169 | colorLerp.set(0); 170 | } else { 171 | colorLerp.lerpTo(1, colorDuration); 172 | colorLerp.tick(dt); 173 | } 174 | 175 | if (time < flickerStartsAtSec) { 176 | zFlickerLerp.set(0); 177 | } else { 178 | zFlickerLerp.lerpTo(1, flickerDuration); 179 | zFlickerLerp.tick(dt); 180 | } 181 | 182 | if (time < rotateStartAtSec) { 183 | rotateLerp.set(0); 184 | } else { 185 | rotateLerp.lerpTo(0.8, rotateDuration); 186 | rotateLerp.tick(dt); 187 | } 188 | 189 | if (time < swirlStartAtSec) { 190 | swirlLerp.set(0); 191 | } else { 192 | swirlLerp.lerpTo(1, swirlDuration); 193 | swirlLerp.tick(dt); 194 | } 195 | 196 | if (time < toBlackStartAtSec) { 197 | goToBlack.set(1); 198 | } else { 199 | goToBlack.lerpTo(0, toBlackDuration); 200 | goToBlack.tick(dt); 201 | } 202 | 203 | if (time > fadeOutStartAtSec) { 204 | if (state != SceneState.fadeOut) { 205 | state = SceneState.fadeOut; 206 | } 207 | 208 | fadeOut.lerpTo(1, fadeOutDuration); 209 | fadeOut.tick(dt); 210 | if (fadeOut.value == 1) { 211 | state = SceneState.done; 212 | } 213 | } 214 | } 215 | 216 | @override 217 | void reset() { 218 | super.reset(); 219 | time = 0; 220 | for (Particle p in particles) { 221 | p.alpha.set(0); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /lib/scenes/rays.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:pushtrix/config.dart'; 5 | import 'package:pushtrix/lerp_value.dart'; 6 | import 'package:pushtrix/scenes/scene.dart'; 7 | 8 | import '../game/game_time.dart'; 9 | 10 | const _numSections = 8; 11 | const lerpTime = 2.0; 12 | 13 | const switchFirst = 4; 14 | const switchSecond = 8; 15 | const switchThird = 12; 16 | 17 | class Rays extends Scene { 18 | LerpValue ray2Angle = LerpValue(pi / 8); 19 | LerpValue ray4Angle = LerpValue(pi / 8); 20 | LerpValue ray8Angle = LerpValue(pi / 8); 21 | double angle = 0; 22 | double radius = 0; 23 | double time = 0; 24 | final color = LerpValue(1); 25 | 26 | @override 27 | void tick() { 28 | if (!screenshotIcon) { 29 | time += GameTime.instance.dt; 30 | } 31 | 32 | angle = time * 0.4; 33 | 34 | color.lerpTo(1, 2); 35 | color.tick(GameTime.instance.dt); 36 | 37 | if (time < switchFirst) { 38 | ray2Angle.set(pi / 8); 39 | ray4Angle.set(pi / 8); 40 | ray8Angle.set(pi / 8); 41 | } else if (time < switchSecond) { 42 | ray2Angle.lerpTo(pi / 4, lerpTime); 43 | ray4Angle.lerpTo(pi / 4, lerpTime); 44 | ray8Angle.lerpTo(0, lerpTime); 45 | } else if (time < switchThird) { 46 | ray2Angle.lerpTo(pi / 2, lerpTime); 47 | ray4Angle.lerpTo(0, lerpTime); 48 | } else { 49 | ray2Angle.lerpTo(0, lerpTime); 50 | 51 | if (ray2Angle.value == 0) { 52 | state = SceneState.done; 53 | } else { 54 | state = SceneState.fadeOut; 55 | } 56 | } 57 | 58 | ray2Angle.tick(GameTime.instance.dt); 59 | ray4Angle.tick(GameTime.instance.dt); 60 | ray8Angle.tick(GameTime.instance.dt); 61 | } 62 | 63 | @override 64 | void render(Canvas canvas, Size size) { 65 | Paint paint = Paint(); 66 | 67 | final cx = size.width / 2; 68 | final cy = size.height / 2; 69 | final maxSize = max(size.width, size.height); 70 | 71 | radius += GameTime.instance.dt * maxSize * 0.5; 72 | if (radius > maxSize && state == SceneState.running) { 73 | radius = 0; 74 | } 75 | 76 | final circleColor = Color.lerp(const Color(0x00ffffff), Colors.red.shade100, color.value)!; 77 | paint.color = Color.lerp(circleColor, const Color.fromARGB(0, 255, 255, 255), radius / maxSize)!; 78 | if (!screenshotIcon) { 79 | canvas.drawCircle(Offset(cx, cy), radius, paint); 80 | } 81 | 82 | const sectionDelta = 2 * pi / _numSections; 83 | paint.color = Color.lerp(const Color(0x00ffffff), Colors.red, color.value)!; 84 | for (int i = 0; i < _numSections; i++) { 85 | final angle = this.angle + i * sectionDelta; 86 | double width = 0; 87 | if (i == 0 || i == 4) { 88 | width = ray2Angle.value / 2; 89 | } else if (i == 2 || i == 6) { 90 | width = ray4Angle.value / 2; 91 | } else if (i == 1 || i == 3 || i == 5 || i == 7) { 92 | width = ray8Angle.value / 2; 93 | } 94 | 95 | if (width > 0) { 96 | final x = cx + maxSize * sin(angle - 2 * width); 97 | final y = cy + maxSize * cos(angle - 2 * width); 98 | 99 | final x2 = cx + maxSize * sin(angle); 100 | final y2 = cy + maxSize * cos(angle); 101 | 102 | var path = Path(); 103 | path.moveTo(cx, cy); 104 | path.lineTo(x, y); 105 | path.lineTo(x2, y2); 106 | path.close(); 107 | canvas.drawPath(path, paint); 108 | } 109 | } 110 | } 111 | 112 | @override 113 | void reset() { 114 | super.reset(); 115 | time = 0; 116 | color.set(0); 117 | } 118 | 119 | @override 120 | void gameOver() { 121 | super.gameOver(); 122 | color.set(1); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/scenes/scene.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/rendering.dart'; 2 | 3 | enum SceneState { 4 | running, 5 | fadeOut, 6 | done, 7 | } 8 | 9 | // Base class for background scenes 10 | // A scene switches to the next one by entering 11 | // the fadeOut and then done state. Once it is done 12 | // the scene gets removed 13 | abstract class Scene { 14 | void tick(); 15 | void render(Canvas canvas, Size size); 16 | void reset() { 17 | state = SceneState.running; 18 | } 19 | 20 | void gameOver() { 21 | // override if needed 22 | } 23 | 24 | SceneState state = SceneState.running; 25 | } 26 | -------------------------------------------------------------------------------- /lib/scenes/start_screen.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:pushtrix/config.dart'; 5 | import 'package:pushtrix/game/puzzle.dart'; 6 | import 'package:pushtrix/scenes/scene.dart'; 7 | 8 | import '../game/game_time.dart'; 9 | 10 | class StartScreen extends Scene { 11 | final Puzzle _puzzle2 = Puzzle(2, 0); 12 | final Puzzle _puzzle3 = Puzzle(3, 0); 13 | final Puzzle _puzzle4 = Puzzle(4, 0); 14 | final Puzzle _puzzle5 = Puzzle(5, 0); 15 | double lastRandom2 = 0; 16 | double lastRandom3 = 0.33; 17 | double lastRandom4 = 0.66; 18 | 19 | List textPainters = []; 20 | 21 | // ATTENTION: Has to be done every frame for some reason. 22 | void cacheTextPainters() { 23 | textPainters.clear(); 24 | // Cache the text painters because TextPainter.layout() is slow. 25 | for (int i = 0; i < 25; ++i) { 26 | TextSpan ts = TextSpan( 27 | style: TextStyle( 28 | fontSize: tileSize / 2, 29 | fontWeight: FontWeight.bold, 30 | fontFamily: "Rowdies", 31 | color: Colors.blue.shade800, 32 | ), 33 | text: "$i", 34 | ); 35 | 36 | TextPainter tp = TextPainter(text: ts, textAlign: TextAlign.center, textDirection: TextDirection.ltr); 37 | tp.layout(); 38 | textPainters.add(tp); 39 | } 40 | } 41 | 42 | @override 43 | void tick() { 44 | if (GameTime.instance.current > lastRandom2) { 45 | _puzzle2.doRandomMove(); 46 | _puzzle5.doRandomMove(); 47 | while (GameTime.instance.current > lastRandom2) { 48 | lastRandom2++; 49 | } 50 | } 51 | 52 | if (GameTime.instance.current > lastRandom3) { 53 | _puzzle3.doRandomMove(); 54 | while (GameTime.instance.current > lastRandom3) { 55 | lastRandom3++; 56 | } 57 | } 58 | if (GameTime.instance.current > lastRandom4) { 59 | _puzzle4.doRandomMove(); 60 | while (GameTime.instance.current > lastRandom4) { 61 | lastRandom4++; 62 | } 63 | } 64 | 65 | final dt = GameTime.instance.dt; 66 | 67 | _puzzle2.tickTileMoveAnim(dt); 68 | _puzzle3.tickTileMoveAnim(dt); 69 | _puzzle4.tickTileMoveAnim(dt); 70 | _puzzle5.tickTileMoveAnim(dt); 71 | } 72 | 73 | void paintPuzzle(Puzzle puzzle, Canvas canvas) { 74 | Paint paint = Paint(); 75 | paint.color = Colors.blue; 76 | final sizeHalf = puzzle.screenSize / 2; 77 | canvas.drawRRect( 78 | RRect.fromLTRBR( 79 | -sizeHalf, 80 | -sizeHalf, 81 | sizeHalf, 82 | sizeHalf, 83 | const Radius.circular(tileSize / 5), 84 | ), 85 | paint, 86 | ); 87 | 88 | for (int i = 1; i < puzzle.size * puzzle.size; ++i) { 89 | final offset = puzzle.getTileOffset(i); 90 | final left = offset.dx - sizeHalf; 91 | final top = offset.dy - sizeHalf; 92 | final right = left + tileSize; 93 | final bottom = top + tileSize; 94 | 95 | paint.color = Colors.white; 96 | canvas.drawRRect( 97 | RRect.fromLTRBR( 98 | left + tileBorderSize, 99 | top + tileBorderSize, 100 | right - tileBorderSize, 101 | bottom - tileBorderSize, 102 | const Radius.circular(tileSize / 5), 103 | ), 104 | paint, 105 | ); 106 | 107 | TextPainter tp = textPainters[i]; 108 | tp.paint(canvas, Offset(left + (tileSize - tp.width) / 2, top + (tileSize - tp.height) / 2)); 109 | } 110 | } 111 | 112 | void paintPuzzleRing(Canvas canvas, Size size, Puzzle puzzle, int count, double radius, double frequency) { 113 | final cx = size.width / 2; 114 | final cy = size.height / 2; 115 | final d = 2 * pi / count; 116 | for (var i = 0; i < count; i++) { 117 | final angle = GameTime.instance.current * frequency + i * d; 118 | final x = cx + radius * cos(angle); 119 | final y = cy + radius * sin(angle); 120 | canvas.save(); 121 | canvas.translate(x, y); 122 | canvas.rotate(angle + pi / 2); 123 | paintPuzzle(puzzle, canvas); 124 | canvas.restore(); 125 | } 126 | } 127 | 128 | void paintRays(Canvas canvas, Size size) { 129 | Paint paint = Paint(); 130 | paint.color = Colors.red; 131 | 132 | final cx = size.width / 2; 133 | final cy = size.height / 2; 134 | final maxSize = max(size.width, size.height); 135 | 136 | const numSections = 10; 137 | const sectionDelta = 2 * pi / numSections; 138 | 139 | for (int i = 0; i < numSections; i++) { 140 | final angle = GameTime.instance.current * 0.2 + i * sectionDelta; 141 | final x = cx + maxSize * sin(angle); 142 | final y = cy + maxSize * cos(angle); 143 | 144 | final x2 = cx + maxSize * sin(angle + sectionDelta / 2); 145 | final y2 = cy + maxSize * cos(angle + sectionDelta / 2); 146 | 147 | var path = Path(); 148 | path.moveTo(cx, cy); 149 | path.lineTo(x, y); 150 | path.lineTo(x2, y2); 151 | path.close(); 152 | canvas.drawPath(path, paint); 153 | } 154 | } 155 | 156 | @override 157 | void render(Canvas canvas, Size size) { 158 | cacheTextPainters(); 159 | 160 | paintRays(canvas, size); 161 | 162 | const num2 = 6; 163 | final r2 = _puzzle2.screenSize * 1.2; 164 | const f2 = 0.3; 165 | 166 | const num3 = 10; 167 | final r3 = r2 + _puzzle3.screenSize * 1; 168 | const f3 = 0.2; 169 | 170 | const num4 = 13; 171 | final r4 = r3 + _puzzle4.screenSize * 1; 172 | const f4 = 0.1; 173 | 174 | const num5 = 15; 175 | final r5 = r4 + _puzzle5.screenSize * 1; 176 | const f5 = 0.1; 177 | 178 | paintPuzzleRing(canvas, size, _puzzle2, num2, r2, f2); 179 | paintPuzzleRing(canvas, size, _puzzle3, num3, r3, f3); 180 | 181 | final maxSize = max(size.width, size.height); 182 | if (maxSize > r4 + _puzzle4.screenSize) { 183 | paintPuzzleRing(canvas, size, _puzzle4, num4, r4, f4); 184 | } 185 | 186 | if (maxSize > r5 + _puzzle5.screenSize) { 187 | paintPuzzleRing(canvas, size, _puzzle5, num5, r5, f5); 188 | } 189 | 190 | canvas.drawRect(Rect.fromLTRB(0, 0, size.width, size.height), Paint()..color = Colors.white.withOpacity(0.4)); 191 | } 192 | 193 | @override 194 | void reset() { 195 | // pass 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /lib/state_transition.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:flutter/scheduler.dart'; 5 | import 'package:pushtrix/game/game.dart'; 6 | 7 | enum TransitionState { 8 | stateChange, 9 | finished, 10 | } 11 | 12 | class TransitionPainter extends CustomPainter { 13 | final double value; 14 | final bool closed; 15 | 16 | TransitionPainter({required this.value, required this.closed}); 17 | 18 | @override 19 | void paint(Canvas canvas, Size size) { 20 | final center = Offset(size.width / 2, size.height / 2); 21 | // below one is big circle and instead of this circle you can draw your shape here. 22 | final paint = Paint() 23 | ..color = Colors.blue.shade800 24 | ..strokeWidth = 5 25 | ..strokeCap = StrokeCap.round; 26 | 27 | final Path path = Path(); 28 | path.fillType = PathFillType.evenOdd; 29 | path.addRect(Rect.fromLTWH(0, 0, size.width, size.height)); 30 | final radius = !closed ? max(0, 1 - value) : value - 1; 31 | path.addOval(Rect.fromCircle(center: center, radius: radius * max(size.width, size.height))); 32 | canvas.drawPath(path, paint); 33 | } 34 | 35 | @override 36 | bool shouldRepaint(CustomPainter oldDelegate) => true; 37 | } 38 | 39 | class StateTransition extends StatefulWidget { 40 | final Function(TransitionState state) onTransition; 41 | const StateTransition(this.onTransition, {Key? key}) : super(key: key); 42 | 43 | @override 44 | State createState() => _StateTransitionState(); 45 | } 46 | 47 | class _StateTransitionState extends State { 48 | late final Ticker ticker; 49 | double time = 0; 50 | double dt = 0; 51 | double value = 0; 52 | bool _isClosed = false; 53 | 54 | _StateTransitionState() { 55 | ticker = Ticker((duration) { 56 | final current = duration.inMilliseconds / 1000.0; 57 | setState(() { 58 | dt = current - time; 59 | time = current; 60 | 61 | // Calculate seconds passed since the start of the animation. 62 | value = (DateTime.now().millisecondsSinceEpoch - (Game.instance.transitionStarted ?? 0)) / 1000; 63 | if (value > 1 && !_isClosed) { 64 | _isClosed = true; 65 | widget.onTransition(TransitionState.stateChange); 66 | } 67 | 68 | if (value > 2) { 69 | debugPrint("transition finished"); 70 | ticker.stop(); 71 | Game.instance.transitionStarted = null; 72 | widget.onTransition(TransitionState.finished); 73 | } 74 | }); 75 | }); 76 | 77 | ticker.start(); 78 | } 79 | 80 | @override 81 | void dispose() { 82 | debugPrint("_StateTransitionState dispose"); 83 | super.dispose(); 84 | ticker.dispose(); 85 | } 86 | 87 | @override 88 | Widget build(BuildContext context) { 89 | final size = MediaQuery.of(context).size; 90 | return CustomPaint( 91 | size: size, 92 | painter: TransitionPainter(value: value, closed: _isClosed), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import app_links_macos 9 | import audioplayers_darwin 10 | import path_provider_macos 11 | import shared_preferences_macos 12 | import url_launcher_macos 13 | 14 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 15 | AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) 16 | AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) 17 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 18 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 19 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 20 | } 21 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.12' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | end 35 | 36 | post_install do |installer| 37 | installer.pods_project.targets.each do |target| 38 | flutter_additional_macos_build_settings(target) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /macos/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - app_links_macos (1.0.0): 3 | - FlutterMacOS 4 | - audioplayers_darwin (0.0.1): 5 | - FlutterMacOS 6 | - FlutterMacOS (1.0.0) 7 | - path_provider_macos (0.0.1): 8 | - FlutterMacOS 9 | - shared_preferences_macos (0.0.1): 10 | - FlutterMacOS 11 | - url_launcher_macos (0.0.1): 12 | - FlutterMacOS 13 | 14 | DEPENDENCIES: 15 | - app_links_macos (from `Flutter/ephemeral/.symlinks/plugins/app_links_macos/macos`) 16 | - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`) 17 | - FlutterMacOS (from `Flutter/ephemeral`) 18 | - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) 19 | - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) 20 | - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) 21 | 22 | EXTERNAL SOURCES: 23 | app_links_macos: 24 | :path: Flutter/ephemeral/.symlinks/plugins/app_links_macos/macos 25 | audioplayers_darwin: 26 | :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos 27 | FlutterMacOS: 28 | :path: Flutter/ephemeral 29 | path_provider_macos: 30 | :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos 31 | shared_preferences_macos: 32 | :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos 33 | url_launcher_macos: 34 | :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos 35 | 36 | SPEC CHECKSUMS: 37 | app_links_macos: 15e554f46b367713cb8d1db37cee3d1701d22a27 38 | audioplayers_darwin: dcad41de4fbd0099cb3749f7ab3b0cb8f70b810c 39 | FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 40 | path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 41 | shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 42 | url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 43 | 44 | PODFILE CHECKSUM: c7161fcf45d4fd9025dc0f48a76d6e64e52f8176 45 | 46 | COCOAPODS: 1.11.2 47 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "version": 1, 4 | "author": "xcode" 5 | }, 6 | "images": [ 7 | { 8 | "size": "16x16", 9 | "idiom": "mac", 10 | "filename": "app_icon_16.png", 11 | "scale": "1x" 12 | }, 13 | { 14 | "size": "16x16", 15 | "idiom": "mac", 16 | "filename": "app_icon_32.png", 17 | "scale": "2x" 18 | }, 19 | { 20 | "size": "32x32", 21 | "idiom": "mac", 22 | "filename": "app_icon_32.png", 23 | "scale": "1x" 24 | }, 25 | { 26 | "size": "32x32", 27 | "idiom": "mac", 28 | "filename": "app_icon_64.png", 29 | "scale": "2x" 30 | }, 31 | { 32 | "size": "128x128", 33 | "idiom": "mac", 34 | "filename": "app_icon_128.png", 35 | "scale": "1x" 36 | }, 37 | { 38 | "size": "128x128", 39 | "idiom": "mac", 40 | "filename": "app_icon_256.png", 41 | "scale": "2x" 42 | }, 43 | { 44 | "size": "256x256", 45 | "idiom": "mac", 46 | "filename": "app_icon_256.png", 47 | "scale": "1x" 48 | }, 49 | { 50 | "size": "256x256", 51 | "idiom": "mac", 52 | "filename": "app_icon_512.png", 53 | "scale": "2x" 54 | }, 55 | { 56 | "size": "512x512", 57 | "idiom": "mac", 58 | "filename": "app_icon_512.png", 59 | "scale": "1x" 60 | }, 61 | { 62 | "size": "512x512", 63 | "idiom": "mac", 64 | "filename": "app_icon_1024.png", 65 | "scale": "2x" 66 | } 67 | ] 68 | } -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = pushtrix 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.chengine.pushtrix 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2022 com.chengine. All rights reserved. 15 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | com.apple.security.network.client 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController.init() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: pushtrix 2 | description: A fast paced puzzle game. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 1.0.0+1 19 | 20 | environment: 21 | sdk: ">=2.15.1 <3.0.0" 22 | 23 | # Dependencies specify other packages that your package needs in order to work. 24 | # To automatically upgrade your package dependencies to the latest versions 25 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 26 | # dependencies can be manually updated by changing the version numbers below to 27 | # the latest version available on pub.dev. To see which dependencies have newer 28 | # versions available, run `flutter pub outdated`. 29 | dependencies: 30 | flutter: 31 | sdk: flutter 32 | 33 | # The following adds the Cupertino Icons font to your application. 34 | # Use with the CupertinoIcons class for iOS style icons. 35 | cupertino_icons: ^1.0.2 36 | animated_button: ^0.2.0 37 | shared_preferences: ^2.0.13 38 | flame_audio: ^1.3.0 39 | supabase_flutter: ^1.0.1 40 | eval_ex: ^1.1.7 41 | envied: ^0.2.3 42 | 43 | dev_dependencies: 44 | flutter_test: 45 | sdk: flutter 46 | 47 | # The "flutter_lints" package below contains a set of recommended lints to 48 | # encourage good coding practices. The lint set provided by the package is 49 | # activated in the `analysis_options.yaml` file located at the root of your 50 | # package. See that file for information about deactivating specific lint 51 | # rules and activating additional ones. 52 | flutter_lints: ^1.0.0 53 | build_runner: ^2.3.2 54 | envied_generator: ^0.2.3 55 | flutter_launcher_icons: ^0.11.0 56 | 57 | # For information on the generic Dart part of this file, see the 58 | # following page: https://dart.dev/tools/pub/pubspec 59 | 60 | flutter_icons: 61 | ios: true 62 | image_path_ios: "assets/launcher/icon.png" 63 | android: true 64 | image_path_android: "assets/launcher/icon_rounded.png" 65 | web: 66 | generate: true 67 | image_path: "assets/launcher/icon_rounded.png" 68 | #background_color: "#hexcode" 69 | #theme_color: "#hexcode" 70 | windows: 71 | generate: true 72 | image_path: "assets/launcher/icon_rounded.png" 73 | icon_size: 256 # min:48, max:256, default: 48 74 | macos: 75 | generate: true 76 | image_path: "assets/launcher/icon.png" 77 | 78 | # The following section is specific to Flutter. 79 | flutter: 80 | # The following line ensures that the Material Icons font is 81 | # included with your application, so that you can use the icons in 82 | # the material Icons class. 83 | uses-material-design: true 84 | 85 | # To add assets to your application, add an assets section, like this: 86 | assets: 87 | - assets/audio/sounds/click.wav 88 | - assets/audio/sounds/swish.wav 89 | - assets/audio/sounds/excellent.mp3 90 | - assets/audio/sounds/great.mp3 91 | - assets/audio/sounds/keep_it_up.mp3 92 | - assets/audio/sounds/well_done.mp3 93 | - assets/audio/sounds/awesome.mp3 94 | - assets/audio/sounds/game_over.mp3 95 | - assets/audio/sounds/that_was_close.mp3 96 | - assets/audio/sounds/too_easy.mp3 97 | - assets/audio/sounds/beep.mp3 98 | - assets/audio/sounds/beep_long.mp3 99 | - assets/audio/music/bensound-extremeaction.mp3 100 | - assets/audio/music/bensound-punky.mp3 101 | - assets/audio/music/bensound-scifi.mp3 102 | 103 | fonts: 104 | - family: Rowdies 105 | fonts: 106 | - asset: fonts/Rowdies-Regular.ttf 107 | weight: 400 108 | - asset: fonts/Rowdies-Light.ttf 109 | weight: 300 110 | - asset: fonts/Rowdies-Bold.ttf 111 | weight: 700 112 | - family: AzeretMono 113 | fonts: 114 | - asset: fonts/AzeretMono-Bold.ttf 115 | weight: 700 116 | 117 | # derry scripts 118 | scripts: 119 | build_runner: flutter pub run build_runner build --delete-conflicting-outputs 120 | build_launcher_icons: flutter pub run flutter_launcher_icons 121 | build: 122 | web: 123 | - flutter build web --base-href /pushtrix/ --web-renderer canvaskit 124 | - echo "Now copy ..." 125 | - cp -a build/web/. ../pushtrix/docs/ 126 | ios: 127 | - flutter build ipa --obfuscate --split-debug-info=ios/debug_info 128 | -------------------------------------------------------------------------------- /source_assets/loading/loading_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/source_assets/loading/loading_00.png -------------------------------------------------------------------------------- /source_assets/loading/loading_00_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/source_assets/loading/loading_00_small.png -------------------------------------------------------------------------------- /source_assets/loading/loading_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/source_assets/loading/loading_01.png -------------------------------------------------------------------------------- /source_assets/loading/loading_01_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/source_assets/loading/loading_01_small.png -------------------------------------------------------------------------------- /source_assets/loading/loading_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/source_assets/loading/loading_02.png -------------------------------------------------------------------------------- /source_assets/loading/loading_02_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/source_assets/loading/loading_02_small.png -------------------------------------------------------------------------------- /source_assets/loading/loading_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/source_assets/loading/loading_03.png -------------------------------------------------------------------------------- /source_assets/loading/loading_03_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/source_assets/loading/loading_03_small.png -------------------------------------------------------------------------------- /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:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:pushtrix/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const App()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/web/img/loading.gif -------------------------------------------------------------------------------- /web/img/loading_small.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/web/img/loading_small.gif -------------------------------------------------------------------------------- /web/img/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/web/img/splash.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Pushtrix 33 | 34 | 35 | 36 | 51 | 54 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pushtrix", 3 | "short_name": "Pushtrix", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A fast paced puzzle game.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(flutter_puzzle LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "flutter_puzzle") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(SET CMP0063 NEW) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | # Generated plugin build rules, which manage building the plugins and adding 56 | # them to the application. 57 | include(flutter/generated_plugins.cmake) 58 | 59 | 60 | # === Installation === 61 | # Support files are copied into place next to the executable, so that it can 62 | # run in place. This is done instead of making a separate bundle (as on Linux) 63 | # so that building and running from within Visual Studio will work. 64 | set(BUILD_BUNDLE_DIR "$") 65 | # Make the "install" step default, as it's required to run. 66 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 67 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 68 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 69 | endif() 70 | 71 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 72 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 73 | 74 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 75 | COMPONENT Runtime) 76 | 77 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 78 | COMPONENT Runtime) 79 | 80 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 81 | COMPONENT Runtime) 82 | 83 | if(PLUGIN_BUNDLED_LIBRARIES) 84 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 85 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 86 | COMPONENT Runtime) 87 | endif() 88 | 89 | # Fully re-copy the assets directory on each build to avoid having stale files 90 | # from a previous install. 91 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 92 | install(CODE " 93 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 94 | " COMPONENT Runtime) 95 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 96 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 97 | 98 | # Install the AOT library on non-Debug builds only. 99 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 100 | CONFIGURATIONS Profile;Release 101 | COMPONENT Runtime) 102 | -------------------------------------------------------------------------------- /windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # === Flutter Library === 14 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 15 | 16 | # Published to parent scope for install step. 17 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 18 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 19 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 20 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 21 | 22 | list(APPEND FLUTTER_LIBRARY_HEADERS 23 | "flutter_export.h" 24 | "flutter_windows.h" 25 | "flutter_messenger.h" 26 | "flutter_plugin_registrar.h" 27 | "flutter_texture_registrar.h" 28 | ) 29 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 30 | add_library(flutter INTERFACE) 31 | target_include_directories(flutter INTERFACE 32 | "${EPHEMERAL_DIR}" 33 | ) 34 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 35 | add_dependencies(flutter flutter_assemble) 36 | 37 | # === Wrapper === 38 | list(APPEND CPP_WRAPPER_SOURCES_CORE 39 | "core_implementations.cc" 40 | "standard_codec.cc" 41 | ) 42 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 43 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 44 | "plugin_registrar.cc" 45 | ) 46 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 47 | list(APPEND CPP_WRAPPER_SOURCES_APP 48 | "flutter_engine.cc" 49 | "flutter_view_controller.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 52 | 53 | # Wrapper sources needed for a plugin. 54 | add_library(flutter_wrapper_plugin STATIC 55 | ${CPP_WRAPPER_SOURCES_CORE} 56 | ${CPP_WRAPPER_SOURCES_PLUGIN} 57 | ) 58 | apply_standard_settings(flutter_wrapper_plugin) 59 | set_target_properties(flutter_wrapper_plugin PROPERTIES 60 | POSITION_INDEPENDENT_CODE ON) 61 | set_target_properties(flutter_wrapper_plugin PROPERTIES 62 | CXX_VISIBILITY_PRESET hidden) 63 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 64 | target_include_directories(flutter_wrapper_plugin PUBLIC 65 | "${WRAPPER_ROOT}/include" 66 | ) 67 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 68 | 69 | # Wrapper sources needed for the runner. 70 | add_library(flutter_wrapper_app STATIC 71 | ${CPP_WRAPPER_SOURCES_CORE} 72 | ${CPP_WRAPPER_SOURCES_APP} 73 | ) 74 | apply_standard_settings(flutter_wrapper_app) 75 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 76 | target_include_directories(flutter_wrapper_app PUBLIC 77 | "${WRAPPER_ROOT}/include" 78 | ) 79 | add_dependencies(flutter_wrapper_app flutter_assemble) 80 | 81 | # === Flutter tool backend === 82 | # _phony_ is a non-existent file to force this command to run every time, 83 | # since currently there's no way to get a full input/output list from the 84 | # flutter tool. 85 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 86 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 87 | add_custom_command( 88 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 89 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 90 | ${CPP_WRAPPER_SOURCES_APP} 91 | ${PHONY_OUTPUT} 92 | COMMAND ${CMAKE_COMMAND} -E env 93 | ${FLUTTER_TOOL_ENVIRONMENT} 94 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 95 | windows-x64 $ 96 | VERBATIM 97 | ) 98 | add_custom_target(flutter_assemble DEPENDS 99 | "${FLUTTER_LIBRARY}" 100 | ${FLUTTER_LIBRARY_HEADERS} 101 | ${CPP_WRAPPER_SOURCES_CORE} 102 | ${CPP_WRAPPER_SOURCES_PLUGIN} 103 | ${CPP_WRAPPER_SOURCES_APP} 104 | ) 105 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | void RegisterPlugins(flutter::PluginRegistry* registry) { 14 | AppLinksWindowsPluginRegisterWithRegistrar( 15 | registry->GetRegistrarForPlugin("AppLinksWindowsPlugin")); 16 | AudioplayersWindowsPluginRegisterWithRegistrar( 17 | registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); 18 | UrlLauncherWindowsRegisterWithRegistrar( 19 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 20 | } 21 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | app_links_windows 7 | audioplayers_windows 8 | url_launcher_windows 9 | ) 10 | 11 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 12 | ) 13 | 14 | set(PLUGIN_BUNDLED_LIBRARIES) 15 | 16 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 17 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 18 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 19 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 21 | endforeach(plugin) 22 | 23 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 24 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 25 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 26 | endforeach(ffi_plugin) 27 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Disable Windows macros that collide with C++ standard library functions. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 25 | 26 | # Add dependency libraries and include directories. Add any application-specific 27 | # dependencies here. 28 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 29 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 30 | 31 | # Run the Flutter tool portions of the build. This must not be removed. 32 | add_dependencies(${BINARY_NAME} flutter_assemble) 33 | -------------------------------------------------------------------------------- /windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #ifdef FLUTTER_BUILD_NUMBER 64 | #define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0 67 | #endif 68 | 69 | #ifdef FLUTTER_BUILD_NAME 70 | #define VERSION_AS_STRING #FLUTTER_BUILD_NAME 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.chengine" "\0" 93 | VALUE "FileDescription", "flutter_puzzle" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "flutter_puzzle" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2022 com.chengine. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "flutter_puzzle.exe" "\0" 98 | VALUE "ProductName", "flutter_puzzle" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | return true; 30 | } 31 | 32 | void FlutterWindow::OnDestroy() { 33 | if (flutter_controller_) { 34 | flutter_controller_ = nullptr; 35 | } 36 | 37 | Win32Window::OnDestroy(); 38 | } 39 | 40 | LRESULT 41 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 42 | WPARAM const wparam, 43 | LPARAM const lparam) noexcept { 44 | // Give Flutter, including plugins, an opportunity to handle window messages. 45 | if (flutter_controller_) { 46 | std::optional result = 47 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 48 | lparam); 49 | if (result) { 50 | return *result; 51 | } 52 | } 53 | 54 | switch (message) { 55 | case WM_FONTCHANGE: 56 | flutter_controller_->engine()->ReloadSystemFonts(); 57 | break; 58 | } 59 | 60 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 61 | } 62 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.CreateAndShow(L"flutter_puzzle", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottfired/flutter_puzzle/422ea096c9cc2a6e1ca652c07834b4a896bd23d8/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | std::string utf8_string; 52 | if (target_length == 0 || target_length > utf8_string.max_size()) { 53 | return utf8_string; 54 | } 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /windows/runner/win32_window.cpp: -------------------------------------------------------------------------------- 1 | #include "win32_window.h" 2 | 3 | #include 4 | 5 | #include "resource.h" 6 | 7 | namespace { 8 | 9 | constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; 10 | 11 | // The number of Win32Window objects that currently exist. 12 | static int g_active_window_count = 0; 13 | 14 | using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); 15 | 16 | // Scale helper to convert logical scaler values to physical using passed in 17 | // scale factor 18 | int Scale(int source, double scale_factor) { 19 | return static_cast(source * scale_factor); 20 | } 21 | 22 | // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. 23 | // This API is only needed for PerMonitor V1 awareness mode. 24 | void EnableFullDpiSupportIfAvailable(HWND hwnd) { 25 | HMODULE user32_module = LoadLibraryA("User32.dll"); 26 | if (!user32_module) { 27 | return; 28 | } 29 | auto enable_non_client_dpi_scaling = 30 | reinterpret_cast( 31 | GetProcAddress(user32_module, "EnableNonClientDpiScaling")); 32 | if (enable_non_client_dpi_scaling != nullptr) { 33 | enable_non_client_dpi_scaling(hwnd); 34 | FreeLibrary(user32_module); 35 | } 36 | } 37 | 38 | } // namespace 39 | 40 | // Manages the Win32Window's window class registration. 41 | class WindowClassRegistrar { 42 | public: 43 | ~WindowClassRegistrar() = default; 44 | 45 | // Returns the singleton registar instance. 46 | static WindowClassRegistrar* GetInstance() { 47 | if (!instance_) { 48 | instance_ = new WindowClassRegistrar(); 49 | } 50 | return instance_; 51 | } 52 | 53 | // Returns the name of the window class, registering the class if it hasn't 54 | // previously been registered. 55 | const wchar_t* GetWindowClass(); 56 | 57 | // Unregisters the window class. Should only be called if there are no 58 | // instances of the window. 59 | void UnregisterWindowClass(); 60 | 61 | private: 62 | WindowClassRegistrar() = default; 63 | 64 | static WindowClassRegistrar* instance_; 65 | 66 | bool class_registered_ = false; 67 | }; 68 | 69 | WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; 70 | 71 | const wchar_t* WindowClassRegistrar::GetWindowClass() { 72 | if (!class_registered_) { 73 | WNDCLASS window_class{}; 74 | window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); 75 | window_class.lpszClassName = kWindowClassName; 76 | window_class.style = CS_HREDRAW | CS_VREDRAW; 77 | window_class.cbClsExtra = 0; 78 | window_class.cbWndExtra = 0; 79 | window_class.hInstance = GetModuleHandle(nullptr); 80 | window_class.hIcon = 81 | LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); 82 | window_class.hbrBackground = 0; 83 | window_class.lpszMenuName = nullptr; 84 | window_class.lpfnWndProc = Win32Window::WndProc; 85 | RegisterClass(&window_class); 86 | class_registered_ = true; 87 | } 88 | return kWindowClassName; 89 | } 90 | 91 | void WindowClassRegistrar::UnregisterWindowClass() { 92 | UnregisterClass(kWindowClassName, nullptr); 93 | class_registered_ = false; 94 | } 95 | 96 | Win32Window::Win32Window() { 97 | ++g_active_window_count; 98 | } 99 | 100 | Win32Window::~Win32Window() { 101 | --g_active_window_count; 102 | Destroy(); 103 | } 104 | 105 | bool Win32Window::CreateAndShow(const std::wstring& title, 106 | const Point& origin, 107 | const Size& size) { 108 | Destroy(); 109 | 110 | const wchar_t* window_class = 111 | WindowClassRegistrar::GetInstance()->GetWindowClass(); 112 | 113 | const POINT target_point = {static_cast(origin.x), 114 | static_cast(origin.y)}; 115 | HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); 116 | UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); 117 | double scale_factor = dpi / 96.0; 118 | 119 | HWND window = CreateWindow( 120 | window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, 121 | Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), 122 | Scale(size.width, scale_factor), Scale(size.height, scale_factor), 123 | nullptr, nullptr, GetModuleHandle(nullptr), this); 124 | 125 | if (!window) { 126 | return false; 127 | } 128 | 129 | return OnCreate(); 130 | } 131 | 132 | // static 133 | LRESULT CALLBACK Win32Window::WndProc(HWND const window, 134 | UINT const message, 135 | WPARAM const wparam, 136 | LPARAM const lparam) noexcept { 137 | if (message == WM_NCCREATE) { 138 | auto window_struct = reinterpret_cast(lparam); 139 | SetWindowLongPtr(window, GWLP_USERDATA, 140 | reinterpret_cast(window_struct->lpCreateParams)); 141 | 142 | auto that = static_cast(window_struct->lpCreateParams); 143 | EnableFullDpiSupportIfAvailable(window); 144 | that->window_handle_ = window; 145 | } else if (Win32Window* that = GetThisFromHandle(window)) { 146 | return that->MessageHandler(window, message, wparam, lparam); 147 | } 148 | 149 | return DefWindowProc(window, message, wparam, lparam); 150 | } 151 | 152 | LRESULT 153 | Win32Window::MessageHandler(HWND hwnd, 154 | UINT const message, 155 | WPARAM const wparam, 156 | LPARAM const lparam) noexcept { 157 | switch (message) { 158 | case WM_DESTROY: 159 | window_handle_ = nullptr; 160 | Destroy(); 161 | if (quit_on_close_) { 162 | PostQuitMessage(0); 163 | } 164 | return 0; 165 | 166 | case WM_DPICHANGED: { 167 | auto newRectSize = reinterpret_cast(lparam); 168 | LONG newWidth = newRectSize->right - newRectSize->left; 169 | LONG newHeight = newRectSize->bottom - newRectSize->top; 170 | 171 | SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, 172 | newHeight, SWP_NOZORDER | SWP_NOACTIVATE); 173 | 174 | return 0; 175 | } 176 | case WM_SIZE: { 177 | RECT rect = GetClientArea(); 178 | if (child_content_ != nullptr) { 179 | // Size and position the child window. 180 | MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, 181 | rect.bottom - rect.top, TRUE); 182 | } 183 | return 0; 184 | } 185 | 186 | case WM_ACTIVATE: 187 | if (child_content_ != nullptr) { 188 | SetFocus(child_content_); 189 | } 190 | return 0; 191 | } 192 | 193 | return DefWindowProc(window_handle_, message, wparam, lparam); 194 | } 195 | 196 | void Win32Window::Destroy() { 197 | OnDestroy(); 198 | 199 | if (window_handle_) { 200 | DestroyWindow(window_handle_); 201 | window_handle_ = nullptr; 202 | } 203 | if (g_active_window_count == 0) { 204 | WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); 205 | } 206 | } 207 | 208 | Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { 209 | return reinterpret_cast( 210 | GetWindowLongPtr(window, GWLP_USERDATA)); 211 | } 212 | 213 | void Win32Window::SetChildContent(HWND content) { 214 | child_content_ = content; 215 | SetParent(content, window_handle_); 216 | RECT frame = GetClientArea(); 217 | 218 | MoveWindow(content, frame.left, frame.top, frame.right - frame.left, 219 | frame.bottom - frame.top, true); 220 | 221 | SetFocus(child_content_); 222 | } 223 | 224 | RECT Win32Window::GetClientArea() { 225 | RECT frame; 226 | GetClientRect(window_handle_, &frame); 227 | return frame; 228 | } 229 | 230 | HWND Win32Window::GetHandle() { 231 | return window_handle_; 232 | } 233 | 234 | void Win32Window::SetQuitOnClose(bool quit_on_close) { 235 | quit_on_close_ = quit_on_close; 236 | } 237 | 238 | bool Win32Window::OnCreate() { 239 | // No-op; provided for subclasses. 240 | return true; 241 | } 242 | 243 | void Win32Window::OnDestroy() { 244 | // No-op; provided for subclasses. 245 | } 246 | -------------------------------------------------------------------------------- /windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates and shows a win32 window with |title| and position and size using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | --------------------------------------------------------------------------------