diff --git a/android/app/build.gradle b/android/app/build.gradle index 205d4b68c1e6b433e0475d1ae26e334dd0314f57..c837737204ea3688c7128f45e28ae9851b79bd66 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -37,7 +37,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 33 + compileSdkVersion 34 namespace "org.benoitharrault.tetrisdual" defaultConfig { diff --git a/android/gradle.properties b/android/gradle.properties index d9abd55731010fe508f39321892e8002f10e79ef..6e2124a9b333ebbcb6e52e0ae6e7080553f5887a 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -app.versionName=0.0.12 -app.versionCode=12 +app.versionName=0.1.0 +app.versionCode=13 diff --git a/assets/fonts/Nunito-Bold.ttf b/assets/fonts/Nunito-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6519feb781449ebe0015cbc74dfd9e13110fbba9 Binary files /dev/null and b/assets/fonts/Nunito-Bold.ttf differ diff --git a/assets/fonts/Nunito-Light.ttf b/assets/fonts/Nunito-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8a0736c41cd6c2a1225d356bf274de1d0afc3497 Binary files /dev/null and b/assets/fonts/Nunito-Light.ttf differ diff --git a/assets/fonts/Nunito-Medium.ttf b/assets/fonts/Nunito-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..88fccdc0638b6f5d6ac49d9d269dc3d518618ad1 Binary files /dev/null and b/assets/fonts/Nunito-Medium.ttf differ diff --git a/assets/fonts/Nunito-Regular.ttf b/assets/fonts/Nunito-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e7b8375a896ef0cd8e06730a78c84532b377e784 Binary files /dev/null and b/assets/fonts/Nunito-Regular.ttf differ diff --git a/assets/translations/en.json b/assets/translations/en.json new file mode 100644 index 0000000000000000000000000000000000000000..83738f6c34524dd01113e8f896084aeab7f00612 --- /dev/null +++ b/assets/translations/en.json @@ -0,0 +1,12 @@ +{ + "app_name": "Tetris dual", + + "settings_title": "Settings", + "settings_label_theme": "Theme mode", + + "about_title": "Informations", + "about_content": "Tetris dual", + "about_version": "Version: {version}", + + "": "" +} diff --git a/assets/translations/fr.json b/assets/translations/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..94b86c938fe2285a50df060d5869596324da81a9 --- /dev/null +++ b/assets/translations/fr.json @@ -0,0 +1,12 @@ +{ + "app_name": "Tetris duel", + + "settings_title": "Réglages", + "settings_label_theme": "Thème de couleurs", + + "about_title": "Informations", + "about_content": "Tetris duel.", + "about_version": "Version : {version}", + + "": "" +} diff --git a/assets/icons/button_back.png b/assets/ui/button_back.png similarity index 100% rename from assets/icons/button_back.png rename to assets/ui/button_back.png diff --git a/assets/ui/button_delete_saved_game.png b/assets/ui/button_delete_saved_game.png new file mode 100644 index 0000000000000000000000000000000000000000..5e4f217689b11e444b7163557d7e5d68f3bbfe7d Binary files /dev/null and b/assets/ui/button_delete_saved_game.png differ diff --git a/assets/ui/button_resume_game.png b/assets/ui/button_resume_game.png new file mode 100644 index 0000000000000000000000000000000000000000..b2ea0a02d05e42377eb551a4b51428b511a32f5d Binary files /dev/null and b/assets/ui/button_resume_game.png differ diff --git a/assets/icons/button_start.png b/assets/ui/button_start.png similarity index 100% rename from assets/icons/button_start.png rename to assets/ui/button_start.png diff --git a/assets/icons/placeholder.png b/assets/ui/placeholder.png similarity index 100% rename from assets/icons/placeholder.png rename to assets/ui/placeholder.png diff --git a/fastlane/metadata/android/en-US/changelogs/13.txt b/fastlane/metadata/android/en-US/changelogs/13.txt new file mode 100644 index 0000000000000000000000000000000000000000..d4afd512e55b3fd8ffbfd795adb9b00832e5aaef --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/13.txt @@ -0,0 +1 @@ +Improve/normalize game architecture. diff --git a/fastlane/metadata/android/fr-FR/changelogs/13.txt b/fastlane/metadata/android/fr-FR/changelogs/13.txt new file mode 100644 index 0000000000000000000000000000000000000000..6a9871a5eb8eb3c6e9106520f1cbf1f39f9e5ef7 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/13.txt @@ -0,0 +1 @@ +Amélioration/normalisation de l'architecture du jeu. diff --git a/icons/build_application_icons.sh b/icons/build_application_icons.sh deleted file mode 100755 index 2ae92914464ceb9984af27c4b0ad1a4ff920bd55..0000000000000000000000000000000000000000 --- a/icons/build_application_icons.sh +++ /dev/null @@ -1,82 +0,0 @@ -#! /bin/bash - -# Check dependencies -command -v inkscape >/dev/null 2>&1 || { echo >&2 "I require inkscape but it's not installed. Aborting."; exit 1; } -command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not installed. Aborting."; exit 1; } -command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; } - -CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" -BASE_DIR="$(dirname "${CURRENT_DIR}")" -ASSETS_DIR="${BASE_DIR}/assets" - -OPTIPNG_OPTIONS="-preserve -quiet -o7" -ICON_SIZE=192 - -####################################################### - -# Game images -AVAILABLE_GAME_IMAGES=" - button_back - button_start - placeholder -" - -# Settings images -AVAILABLES_GAME_SETTINGS=" -" - -####################################################### - -# optimize svg -function optimize_svg() { - SOURCE="$1" - - cp ${SOURCE} ${SOURCE}.tmp - scour \ - --remove-descriptive-elements \ - --enable-id-stripping \ - --enable-viewboxing \ - --enable-comment-stripping \ - --nindent=4 \ - --quiet \ - -i ${SOURCE}.tmp \ - -o ${SOURCE} - rm ${SOURCE}.tmp -} - -# build icons -function build_icon() { - SOURCE="$1" - TARGET="$2" - - echo "Building ${TARGET}" - - if [ ! -f "${SOURCE}" ]; then - echo "Missing file: ${SOURCE}" - exit 1 - fi - - optimize_svg "${SOURCE}" - - inkscape \ - --export-width=${ICON_SIZE} \ - --export-height=${ICON_SIZE} \ - --export-filename=${TARGET} \ - ${SOURCE} - - optipng ${OPTIPNG_OPTIONS} ${TARGET} -} - -####################################################### - -# Create output folders -mkdir -p ${ASSETS_DIR}/icons - -# Delete existing generated images -find ${ASSETS_DIR}/icons -type f -name "*.png" -delete - -# build game images -for GAME_IMAGE in ${AVAILABLE_GAME_IMAGES} -do - build_icon ${CURRENT_DIR}/${GAME_IMAGE}.svg ${ASSETS_DIR}/icons/${GAME_IMAGE}.png -done diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..7341459a4da26baa921b0b5586be6728fdbcd047 --- /dev/null +++ b/lib/config/default_game_settings.dart @@ -0,0 +1,33 @@ +import 'package:tetrisdual/utils/tools.dart'; + +class DefaultGameSettings { + // available game parameters codes + static const String parameterCodeGameType = 'gameType'; + static const List<String> availableParameters = [ + parameterCodeGameType, + ]; + + // game mode: available values + static const String gameTypeValueDefault = 'default'; + static const List<String> allowedGameTypeValues = [ + gameTypeValueDefault, + ]; + // game mode: default value + static const String defaultGameTypeValue = gameTypeValueDefault; + + // available values from parameter code + static List<String> getAvailableValues(String parameterCode) { + switch (parameterCode) { + case parameterCodeGameType: + return DefaultGameSettings.allowedGameTypeValues; + } + + printlog('Did not find any available value for game parameter "$parameterCode".'); + return []; + } + + // parameters displayed with assets (instead of painter) + static List<String> displayedWithAssets = [ + // + ]; +} diff --git a/lib/config/default_global_settings.dart b/lib/config/default_global_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..3ab4eb7dc9f1803c925174cfdde182839e403842 --- /dev/null +++ b/lib/config/default_global_settings.dart @@ -0,0 +1,33 @@ +import 'package:tetrisdual/utils/tools.dart'; + +class DefaultGlobalSettings { + // available global parameters codes + static const String parameterCodeSkin = 'skin'; + static const List<String> availableParameters = [ + parameterCodeSkin, + ]; + + // skin: available values + static const String skinValueDefault = 'default'; + static const List<String> allowedSkinValues = [ + skinValueDefault, + ]; + // skin: default value + static const String defaultSkinValue = skinValueDefault; + + // available values from parameter code + static List<String> getAvailableValues(String parameterCode) { + switch (parameterCode) { + case parameterCodeSkin: + return DefaultGlobalSettings.allowedSkinValues; + } + + printlog('Did not find any available value for global parameter "$parameterCode".'); + return []; + } + + // parameters displayed with assets (instead of painter) + static List<String> displayedWithAssets = [ + // + ]; +} diff --git a/lib/config/menu.dart b/lib/config/menu.dart new file mode 100644 index 0000000000000000000000000000000000000000..78c099415746da7c1c87b7e104c31fe49759f6e4 --- /dev/null +++ b/lib/config/menu.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:tetrisdual/ui/screens/page_about.dart'; +import 'package:tetrisdual/ui/screens/page_game.dart'; +import 'package:tetrisdual/ui/screens/page_settings.dart'; + +class MenuItem { + final Icon icon; + final Widget page; + + const MenuItem({ + required this.icon, + required this.page, + }); +} + +class Menu { + static const indexGame = 0; + static const menuItemGame = MenuItem( + icon: Icon(UniconsLine.home), + page: PageGame(), + ); + + static const indexSettings = 1; + static const menuItemSettings = MenuItem( + icon: Icon(UniconsLine.setting), + page: PageSettings(), + ); + + static const indexAbout = 2; + static const menuItemAbout = MenuItem( + icon: Icon(UniconsLine.info_circle), + page: PageAbout(), + ); + + static Map<int, MenuItem> items = { + indexGame: menuItemGame, + indexSettings: menuItemSettings, + indexAbout: menuItemAbout, + }; + + static bool isIndexAllowed(int pageIndex) { + return items.keys.contains(pageIndex); + } + + static Widget getPageWidget(int pageIndex) { + return items[pageIndex]?.page ?? menuItemGame.page; + } + + static int itemsCount = Menu.items.length; +} diff --git a/lib/config/theme.dart b/lib/config/theme.dart new file mode 100644 index 0000000000000000000000000000000000000000..74f532fd5abf693979118609564d29167e902009 --- /dev/null +++ b/lib/config/theme.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; + +/// Colors from Tailwind CSS (v3.0) - June 2022 +/// +/// https://tailwindcss.com/docs/customizing-colors + +const int _primaryColor = 0xFF6366F1; +const MaterialColor primarySwatch = MaterialColor(_primaryColor, <int, Color>{ + 50: Color(0xFFEEF2FF), // indigo-50 + 100: Color(0xFFE0E7FF), // indigo-100 + 200: Color(0xFFC7D2FE), // indigo-200 + 300: Color(0xFFA5B4FC), // indigo-300 + 400: Color(0xFF818CF8), // indigo-400 + 500: Color(_primaryColor), // indigo-500 + 600: Color(0xFF4F46E5), // indigo-600 + 700: Color(0xFF4338CA), // indigo-700 + 800: Color(0xFF3730A3), // indigo-800 + 900: Color(0xFF312E81), // indigo-900 +}); + +const int _textColor = 0xFF64748B; +const MaterialColor textSwatch = MaterialColor(_textColor, <int, Color>{ + 50: Color(0xFFF8FAFC), // slate-50 + 100: Color(0xFFF1F5F9), // slate-100 + 200: Color(0xFFE2E8F0), // slate-200 + 300: Color(0xFFCBD5E1), // slate-300 + 400: Color(0xFF94A3B8), // slate-400 + 500: Color(_textColor), // slate-500 + 600: Color(0xFF475569), // slate-600 + 700: Color(0xFF334155), // slate-700 + 800: Color(0xFF1E293B), // slate-800 + 900: Color(0xFF0F172A), // slate-900 +}); + +const Color errorColor = Color(0xFFDC2626); // red-600 + +final ColorScheme lightColorScheme = ColorScheme.light( + primary: primarySwatch.shade500, + secondary: primarySwatch.shade500, + onSecondary: Colors.white, + error: errorColor, + onSurface: textSwatch.shade500, + surface: textSwatch.shade50, + surfaceContainerHighest: Colors.white, + shadow: textSwatch.shade900.withOpacity(.1), +); + +final ColorScheme darkColorScheme = ColorScheme.dark( + primary: primarySwatch.shade500, + secondary: primarySwatch.shade500, + onSecondary: Colors.white, + error: errorColor, + onSurface: textSwatch.shade300, + surface: const Color(0xFF262630), + surfaceContainerHighest: const Color(0xFF282832), + shadow: textSwatch.shade900.withOpacity(.2), +); + +final ThemeData lightTheme = ThemeData( + colorScheme: lightColorScheme, + fontFamily: 'Nunito', + textTheme: TextTheme( + displayLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + displayMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + displaySmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + headlineLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + headlineMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + headlineSmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + titleLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + titleMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + titleSmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + bodyLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + bodyMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + bodySmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + labelLarge: TextStyle( + color: textSwatch.shade700, + fontFamily: 'Nunito', + ), + labelMedium: TextStyle( + color: textSwatch.shade600, + fontFamily: 'Nunito', + ), + labelSmall: TextStyle( + color: textSwatch.shade500, + fontFamily: 'Nunito', + ), + ), +); + +final ThemeData darkTheme = lightTheme.copyWith( + colorScheme: darkColorScheme, + textTheme: TextTheme( + displayLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + displayMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + displaySmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + headlineLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + headlineMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + headlineSmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + titleLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + titleMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + titleSmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + bodyLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + bodyMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + bodySmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + labelLarge: TextStyle( + color: textSwatch.shade200, + fontFamily: 'Nunito', + ), + labelMedium: TextStyle( + color: textSwatch.shade300, + fontFamily: 'Nunito', + ), + labelSmall: TextStyle( + color: textSwatch.shade400, + fontFamily: 'Nunito', + ), + ), +); diff --git a/lib/cubit/game_cubit.dart b/lib/cubit/game_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..c8dd323c9b9e16e470454b917349ae09d3c54733 --- /dev/null +++ b/lib/cubit/game_cubit.dart @@ -0,0 +1,127 @@ +import 'dart:math'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:tetrisdual/models/game/game.dart'; +import 'package:tetrisdual/models/game/player.dart'; +import 'package:tetrisdual/models/settings/settings_game.dart'; +import 'package:tetrisdual/models/settings/settings_global.dart'; + +part 'game_state.dart'; + +class GameCubit extends HydratedCubit<GameState> { + GameCubit() + : super(GameState( + currentGame: Game.createEmpty(), + )); + + void updateState(Game game) { + emit(GameState( + currentGame: game, + )); + } + + void refresh() { + final Game game = Game( + // Settings + gameSettings: state.currentGame.gameSettings, + globalSettings: state.currentGame.globalSettings, + // State + isRunning: state.currentGame.isRunning, + isStarted: state.currentGame.isStarted, + isFinished: state.currentGame.isFinished, + animationInProgress: state.currentGame.animationInProgress, + // Base data + players: state.currentGame.players, + // Game data + currentPlayer: state.currentGame.currentPlayer, + ); + // game.dump(); + + updateState(game); + } + + void startNewGame({ + required GameSettings gameSettings, + required GlobalSettings globalSettings, + }) { + final Game newGame = Game.createNew( + // Settings + gameSettings: gameSettings, + globalSettings: globalSettings, + ); + + newGame.dump(); + + updateState(newGame); + enableRandomPlayer(); + refresh(); + } + + void quitGame() { + state.currentGame.isRunning = false; + refresh(); + } + + void resumeSavedGame() { + state.currentGame.isRunning = true; + refresh(); + } + + void deleteSavedGame() { + state.currentGame.isRunning = false; + state.currentGame.isFinished = true; + refresh(); + } + + void toggleCurrentPlayer() { + state.currentGame.isStarted = true; + + // brand new game + if (state.currentGame.currentPlayer == 0) { + state.currentGame.currentPlayer = 1; + } else { + // Reset current player tetrimino + getCurrentPlayer().resetTetrimino(); + + // toggle: 1 -> 2 ; 2 -> 1 + state.currentGame.currentPlayer = 3 - state.currentGame.currentPlayer; + } + + // Pick new tetrimino + getCurrentPlayer().pickRandomTetrimino(); + + refresh(); + } + + void enableRandomPlayer() { + state.currentGame.currentPlayer = Random().nextInt(2) + 1; + toggleCurrentPlayer(); + } + + Player getPlayer(int playerId) { + return state.currentGame.players[playerId - 1]; + } + + Player getCurrentPlayer() { + return getPlayer(state.currentGame.currentPlayer); + } + + @override + GameState? fromJson(Map<String, dynamic> json) { + final Game currentGame = json['currentGame'] as Game; + + return GameState( + currentGame: currentGame, + ); + } + + @override + Map<String, dynamic>? toJson(GameState state) { + return <String, dynamic>{ + 'currentGame': state.currentGame.toJson(), + }; + } +} diff --git a/lib/cubit/game_state.dart b/lib/cubit/game_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..00e211668c3269255926939324355792abd61c41 --- /dev/null +++ b/lib/cubit/game_state.dart @@ -0,0 +1,15 @@ +part of 'game_cubit.dart'; + +@immutable +class GameState extends Equatable { + const GameState({ + required this.currentGame, + }); + + final Game currentGame; + + @override + List<dynamic> get props => <dynamic>[ + currentGame, + ]; +} diff --git a/lib/cubit/nav_cubit.dart b/lib/cubit/nav_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..e65528cb3ae94660e1b6f4d9bec221619c5c596b --- /dev/null +++ b/lib/cubit/nav_cubit.dart @@ -0,0 +1,37 @@ +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:tetrisdual/config/menu.dart'; + +class NavCubit extends HydratedCubit<int> { + NavCubit() : super(0); + + void updateIndex(int index) { + if (Menu.isIndexAllowed(index)) { + emit(index); + } else { + goToGamePage(); + } + } + + void goToGamePage() { + emit(Menu.indexGame); + } + + void goToSettingsPage() { + emit(Menu.indexSettings); + } + + void goToAboutPage() { + emit(Menu.indexAbout); + } + + @override + int fromJson(Map<String, dynamic> json) { + return Menu.indexGame; + } + + @override + Map<String, dynamic>? toJson(int state) { + return <String, int>{'pageIndex': state}; + } +} diff --git a/lib/cubit/settings_game_cubit.dart b/lib/cubit/settings_game_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..406db47d441bccc6974d8d5e351e1e609a56bc3d --- /dev/null +++ b/lib/cubit/settings_game_cubit.dart @@ -0,0 +1,61 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:tetrisdual/config/default_game_settings.dart'; +import 'package:tetrisdual/models/settings/settings_game.dart'; + +part 'settings_game_state.dart'; + +class GameSettingsCubit extends HydratedCubit<GameSettingsState> { + GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault())); + + void setValues({ + String? gameType, + }) { + emit( + GameSettingsState( + settings: GameSettings( + gameType: gameType ?? state.settings.gameType, + ), + ), + ); + } + + String getParameterValue(String code) { + switch (code) { + case DefaultGameSettings.parameterCodeGameType: + return GameSettings.getGameTypeValueFromUnsafe(state.settings.gameType); + } + + return ''; + } + + void setParameterValue(String code, String value) { + final String gameType = code == DefaultGameSettings.parameterCodeGameType + ? value + : getParameterValue(DefaultGameSettings.parameterCodeGameType); + + setValues( + gameType: gameType, + ); + } + + @override + GameSettingsState? fromJson(Map<String, dynamic> json) { + final String gameType = json[DefaultGameSettings.parameterCodeGameType] as String; + + return GameSettingsState( + settings: GameSettings( + gameType: gameType, + ), + ); + } + + @override + Map<String, dynamic>? toJson(GameSettingsState state) { + return <String, dynamic>{ + DefaultGameSettings.parameterCodeGameType: state.settings.gameType, + }; + } +} diff --git a/lib/cubit/settings_game_state.dart b/lib/cubit/settings_game_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..5acd85b44ba541e1c5e9c26af1c4be26a385b9ed --- /dev/null +++ b/lib/cubit/settings_game_state.dart @@ -0,0 +1,15 @@ +part of 'settings_game_cubit.dart'; + +@immutable +class GameSettingsState extends Equatable { + const GameSettingsState({ + required this.settings, + }); + + final GameSettings settings; + + @override + List<dynamic> get props => <dynamic>[ + settings, + ]; +} diff --git a/lib/cubit/settings_global_cubit.dart b/lib/cubit/settings_global_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..19b8d182f69c1b63c4f3567ffd1d952b4e8a4011 --- /dev/null +++ b/lib/cubit/settings_global_cubit.dart @@ -0,0 +1,60 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:tetrisdual/config/default_global_settings.dart'; +import 'package:tetrisdual/models/settings/settings_global.dart'; + +part 'settings_global_state.dart'; + +class GlobalSettingsCubit extends HydratedCubit<GlobalSettingsState> { + GlobalSettingsCubit() : super(GlobalSettingsState(settings: GlobalSettings.createDefault())); + + void setValues({ + String? skin, + }) { + emit( + GlobalSettingsState( + settings: GlobalSettings( + skin: skin ?? state.settings.skin, + ), + ), + ); + } + + String getParameterValue(String code) { + switch (code) { + case DefaultGlobalSettings.parameterCodeSkin: + return GlobalSettings.getSkinValueFromUnsafe(state.settings.skin); + } + return ''; + } + + void setParameterValue(String code, String value) { + final String skin = (code == DefaultGlobalSettings.parameterCodeSkin) + ? value + : getParameterValue(DefaultGlobalSettings.parameterCodeSkin); + + setValues( + skin: skin, + ); + } + + @override + GlobalSettingsState? fromJson(Map<String, dynamic> json) { + final String skin = json[DefaultGlobalSettings.parameterCodeSkin] as String; + + return GlobalSettingsState( + settings: GlobalSettings( + skin: skin, + ), + ); + } + + @override + Map<String, dynamic>? toJson(GlobalSettingsState state) { + return <String, dynamic>{ + DefaultGlobalSettings.parameterCodeSkin: state.settings.skin, + }; + } +} diff --git a/lib/cubit/settings_global_state.dart b/lib/cubit/settings_global_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..ebcddd700f252257223ca8e16c85202b04f3ff24 --- /dev/null +++ b/lib/cubit/settings_global_state.dart @@ -0,0 +1,15 @@ +part of 'settings_global_cubit.dart'; + +@immutable +class GlobalSettingsState extends Equatable { + const GlobalSettingsState({ + required this.settings, + }); + + final GlobalSettings settings; + + @override + List<dynamic> get props => <dynamic>[ + settings, + ]; +} diff --git a/lib/cubit/theme_cubit.dart b/lib/cubit/theme_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..b793e895dbb0c672d451cd403e0036c3d9ac9b42 --- /dev/null +++ b/lib/cubit/theme_cubit.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +part 'theme_state.dart'; + +class ThemeCubit extends HydratedCubit<ThemeModeState> { + ThemeCubit() : super(const ThemeModeState()); + + void getTheme(ThemeModeState state) { + emit(state); + } + + @override + ThemeModeState? fromJson(Map<String, dynamic> json) { + switch (json['themeMode']) { + case 'ThemeMode.dark': + return const ThemeModeState(themeMode: ThemeMode.dark); + case 'ThemeMode.light': + return const ThemeModeState(themeMode: ThemeMode.light); + case 'ThemeMode.system': + default: + return const ThemeModeState(themeMode: ThemeMode.system); + } + } + + @override + Map<String, String>? toJson(ThemeModeState state) { + return <String, String>{'themeMode': state.themeMode.toString()}; + } +} diff --git a/lib/cubit/theme_state.dart b/lib/cubit/theme_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..e479a50f12fe72a35a1fd1722ff72afbb692a136 --- /dev/null +++ b/lib/cubit/theme_state.dart @@ -0,0 +1,15 @@ +part of 'theme_cubit.dart'; + +@immutable +class ThemeModeState extends Equatable { + const ThemeModeState({ + this.themeMode, + }); + + final ThemeMode? themeMode; + + @override + List<Object?> get props => <Object?>[ + themeMode, + ]; +} diff --git a/lib/entity/player.dart b/lib/entity/player.dart deleted file mode 100644 index cef7f92cdc452487d61b7e6acbfda837020e75cf..0000000000000000000000000000000000000000 --- a/lib/entity/player.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; - -import 'package:tetrisdual/entity/counter.dart'; -import 'package:tetrisdual/layout/board_painter.dart'; -import 'package:tetrisdual/provider/data.dart'; - -class Player { - Player(this.playerId); - int playerId; - - int _score = 0; - int _currentTetrimino = 0; - final Counter _counter = Counter(); - - Widget buildTetriminoWidget(Data myProvider, double width) { - return GestureDetector( - onTapUp: (details) { - if (playerId == myProvider.getCurrentPlayer().playerId) { - pickRandomTetrimino(); - myProvider.redraw(); - } - }, - child: CustomPaint( - size: Size(width, width), - willChange: false, - painter: BoardPainter(_currentTetrimino), - isComplex: true, - key: Key(_currentTetrimino.toString()), - ), - ); - } - - Widget buildManagerWidget(Data myProvider) { - return Expanded( - child: Container( - margin: const EdgeInsets.all(5), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: myProvider.currentPlayer == playerId - ? [ - _counter.buildCounterWidget(myProvider), - buildSubmitWidget(myProvider), - ] - : [], - ), - ), - ); - } - - Widget buildSubmitWidget(Data myProvider) { - const double gainFontSize = 70; - - const gainTestStyle = TextStyle( - fontFamily: 'Blocks', - fontSize: gainFontSize, - fontWeight: FontWeight.bold, - ); - const submitIcon = Icon( - Icons.done_all, - color: Colors.orange, - size: gainFontSize / 2, - ); - - const topBorderBlack = BoxDecoration( - border: Border( - top: BorderSide( - color: Colors.black, - width: 2, - ), - ), - ); - - return Container( - decoration: topBorderBlack, - padding: const EdgeInsets.only( - top: 10, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - '+${_counter.computePoints()}', - style: gainTestStyle, - textHeightBehavior: const TextHeightBehavior( - applyHeightToFirstAscent: false, - applyHeightToLastDescent: false, - ), - ), - const SizedBox(width: 10), - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - style: const ButtonStyle( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - icon: submitIcon, - onPressed: () { - _score = _score + _counter.computePoints(); - _counter.reset(); - myProvider.toggleCurrentPlayer(); - }, - ), - const SizedBox(width: 10), - ], - ), - ); - } - - Widget buildPlayerBoard(Data myProvider, double screenWidth, bool isActive) { - final double tetriminoBlockWidth = screenWidth / 2.3; - final Color borderColor = isActive ? Colors.greenAccent : Colors.blueGrey; - const double borderWidth = 10; - - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - _score.toString(), - style: const TextStyle( - fontFamily: 'Blocks', - fontSize: 130, - fontWeight: FontWeight.bold, - ), - textHeightBehavior: const TextHeightBehavior( - applyHeightToFirstAscent: false, - applyHeightToLastDescent: false, - ), - ), - Container( - decoration: BoxDecoration( - border: Border.all( - color: borderColor, - width: borderWidth, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - isActive - ? buildTetriminoWidget(myProvider, tetriminoBlockWidth) - : SizedBox( - height: tetriminoBlockWidth, - ), - buildManagerWidget(myProvider), - ], - ), - ) - ], - ); - } - - void pickRandomTetrimino() { - // ensure new tetrimino is not same as current one - int newTetrimino = _currentTetrimino; - while (newTetrimino == _currentTetrimino) { - newTetrimino = Random().nextInt(5) + 1; - } - - _currentTetrimino = newTetrimino; - } - - void resetTetrimino() { - _currentTetrimino = 0; - } - - void submitPoints() { - _score = _score + _counter.computePoints(); - _counter.reset(); - } -} diff --git a/lib/layout/board.dart b/lib/layout/board.dart deleted file mode 100644 index b324c6ff4d0dfd583761f8e95a157ae889dfbf46..0000000000000000000000000000000000000000 --- a/lib/layout/board.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:tetrisdual/provider/data.dart'; - -class Board { - static Widget buildGameBoard(Data myProvider, double screenWidth) { - final Widget player1 = RotatedBox( - quarterTurns: 2, - child: myProvider - .getPlayer(1) - .buildPlayerBoard(myProvider, screenWidth, myProvider.currentPlayer == 1), - ); - - final Widget player2 = myProvider - .getPlayer(2) - .buildPlayerBoard(myProvider, screenWidth, myProvider.currentPlayer == 2); - - final Widget togglePlayerWidget = GestureDetector( - onTapUp: (details) { - myProvider.toggleCurrentPlayer(); - }, - child: const Text( - '🔄', - style: TextStyle( - fontSize: 50, - ), - textHeightBehavior: TextHeightBehavior( - applyHeightToFirstAscent: false, - applyHeightToLastDescent: false, - ), - ), - ); - - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - player1, - togglePlayerWidget, - player2, - ], - ), - ); - } -} diff --git a/lib/layout/game.dart b/lib/layout/game.dart deleted file mode 100644 index b8a54c803d424b0059b7c509f2d2e82072d453cf..0000000000000000000000000000000000000000 --- a/lib/layout/game.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:tetrisdual/layout/board.dart'; -import 'package:tetrisdual/provider/data.dart'; -import 'package:tetrisdual/utils/game_utils.dart'; - -class Game { - static Widget buildGameWidget(Data myProvider, double screenWidth) { - return !myProvider.isGameFinished - ? Board.buildGameBoard(myProvider, screenWidth) - : Game.buildEndGameMessage(myProvider); - } - - static TextButton buildQuitGameButton(Data myProvider) { - return TextButton( - child: const Image( - image: AssetImage('assets/icons/button_back.png'), - fit: BoxFit.fill, - ), - onPressed: () => GameUtils.quitGame(myProvider), - ); - } - - static Container buildEndGameMessage(Data myProvider) { - const String decorationImageAssetName = 'assets/icons/game_fail.png'; - - final Widget decorationWidget = TextButton( - child: const Image( - image: AssetImage(decorationImageAssetName), - fit: BoxFit.fill, - ), - onPressed: () {}, - ); - - return Container( - margin: const EdgeInsets.all(2), - padding: const EdgeInsets.all(2), - child: Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - children: [ - TableRow( - children: [ - Column( - children: [decorationWidget], - ), - Column( - children: [buildQuitGameButton(myProvider)], - ), - Column( - children: [decorationWidget], - ), - ], - ), - ], - ), - ); - } -} diff --git a/lib/layout/parameters.dart b/lib/layout/parameters.dart deleted file mode 100644 index 97f81cfd2939f44abb01b4b754ca3a139b477f9a..0000000000000000000000000000000000000000 --- a/lib/layout/parameters.dart +++ /dev/null @@ -1,150 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:tetrisdual/provider/data.dart'; -import 'package:tetrisdual/utils/game_utils.dart'; - -class Parameters { - static const double separatorHeight = 2.0; - static const double blockMargin = 3.0; - static const double blockPadding = 2.0; - static const Color buttonBackgroundColor = Colors.white; - static const Color buttonBorderColorActive = Colors.blue; - static const Color buttonBorderColorInactive = Colors.white; - static const double buttonBorderWidth = 10.0; - static const double buttonBorderRadius = 8.0; - static const double buttonPadding = 0.0; - static const double buttonMargin = 0.0; - - static Widget buildParametersSelector(Data myProvider) { - final List<Widget> lines = []; - - final List<String> parameters = myProvider.availableParameters; - for (int index = 0; index < parameters.length; index++) { - lines.add(buildParameterSelector(myProvider, parameters[index])); - lines.add(const SizedBox(height: separatorHeight)); - } - - final Widget buttonsBlock = buildStartNewGameButton(myProvider); - - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: separatorHeight), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: lines, - ), - ), - const SizedBox(height: separatorHeight), - Container( - child: buttonsBlock, - ), - ], - ); - } - - static Image buildImageWidget(String imageAssetCode) { - return Image( - image: AssetImage('assets/icons/$imageAssetCode.png'), - fit: BoxFit.fill, - ); - } - - static Container buildImageContainerWidget(String imageAssetCode) { - return Container( - child: buildImageWidget(imageAssetCode), - ); - } - - static Column buildDecorationImageWidget() { - return Column( - children: [ - TextButton( - child: buildImageContainerWidget('placeholder'), - onPressed: () {}, - ), - ], - ); - } - - static Container buildStartNewGameButton(Data myProvider) { - return Container( - margin: const EdgeInsets.all(blockMargin), - padding: const EdgeInsets.all(blockPadding), - child: Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - children: [ - TableRow( - children: [ - buildDecorationImageWidget(), - Column( - children: [ - TextButton( - child: buildImageContainerWidget('button_start'), - onPressed: () => GameUtils.startNewGame(myProvider), - ), - ], - ), - buildDecorationImageWidget(), - ], - ), - ], - ), - ); - } - - static Widget buildParameterSelector(Data myProvider, String parameterCode) { - final List<String> availableValues = myProvider.getParameterAvailableValues(parameterCode); - - if (availableValues.length == 1) { - return const SizedBox(height: 0.0); - } - - return Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - children: [ - TableRow( - children: [ - for (int index = 0; index < availableValues.length; index++) - Column( - children: [ - _buildParameterButton(myProvider, parameterCode, availableValues[index]) - ], - ), - ], - ), - ], - ); - } - - static Widget _buildParameterButton( - Data myProvider, - String parameterCode, - String parameterValue, - ) { - final String currentValue = myProvider.getParameterValue(parameterCode).toString(); - - final bool isActive = (parameterValue == currentValue); - final String imageAsset = '${parameterCode}_$parameterValue'; - - return TextButton( - child: Container( - margin: const EdgeInsets.all(buttonMargin), - padding: const EdgeInsets.all(buttonPadding), - decoration: BoxDecoration( - color: buttonBackgroundColor, - borderRadius: BorderRadius.circular(buttonBorderRadius), - border: Border.all( - color: isActive ? buttonBorderColorActive : buttonBorderColorInactive, - width: buttonBorderWidth, - ), - ), - child: buildImageWidget(imageAsset), - ), - onPressed: () => myProvider.setParameterValue(parameterCode, parameterValue), - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index 121d1109f25290bd210df2442c310a169db8ab28..5677b41cb14516a35418cfff53d60eacbe5382b0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,15 +1,42 @@ +import 'dart:io'; + +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; -import 'package:overlay_support/overlay_support.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hive/hive.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; +import 'package:path_provider/path_provider.dart'; -import 'package:tetrisdual/provider/data.dart'; -import 'package:tetrisdual/screens/home.dart'; +import 'package:tetrisdual/config/theme.dart'; +import 'package:tetrisdual/cubit/game_cubit.dart'; +import 'package:tetrisdual/cubit/nav_cubit.dart'; +import 'package:tetrisdual/cubit/settings_game_cubit.dart'; +import 'package:tetrisdual/cubit/settings_global_cubit.dart'; +import 'package:tetrisdual/cubit/theme_cubit.dart'; +import 'package:tetrisdual/ui/skeleton.dart'; -void main() { +void main() async { + // Initialize packages WidgetsFlutterBinding.ensureInitialized(); + await EasyLocalization.ensureInitialized(); + final Directory tmpDir = await getTemporaryDirectory(); + Hive.init(tmpDir.toString()); + HydratedBloc.storage = await HydratedStorage.build( + storageDirectory: tmpDir, + ); + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]) - .then((value) => runApp(const MyApp())); + .then((value) => runApp(EasyLocalization( + path: 'assets/translations', + supportedLocales: const <Locale>[ + Locale('en'), + Locale('fr'), + ], + fallbackLocale: const Locale('en'), + useFallbackTranslations: true, + child: const MyApp(), + ))); } class MyApp extends StatelessWidget { @@ -17,22 +44,56 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (BuildContext context) => Data(), - child: Consumer<Data>(builder: (context, data, child) { - return OverlaySupport( - child: MaterialApp( - theme: ThemeData( - primaryColor: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: const Home(), - routes: { - Home.id: (context) => const Home(), - }, - ), - ); - }), + final List<String> assets = getImagesAssets(); + for (String asset in assets) { + precacheImage(AssetImage(asset), context); + } + + return MultiBlocProvider( + providers: [ + BlocProvider<NavCubit>(create: (context) => NavCubit()), + BlocProvider<ThemeCubit>(create: (context) => ThemeCubit()), + BlocProvider<GameCubit>(create: (context) => GameCubit()), + BlocProvider<GlobalSettingsCubit>(create: (context) => GlobalSettingsCubit()), + BlocProvider<GameSettingsCubit>(create: (context) => GameSettingsCubit()), + ], + child: BlocBuilder<ThemeCubit, ThemeModeState>( + builder: (BuildContext context, ThemeModeState state) { + return MaterialApp( + title: 'Tetris', + home: const SkeletonScreen(), + + // Theme stuff + theme: lightTheme, + darkTheme: darkTheme, + themeMode: state.themeMode, + + // Localization stuff + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: context.locale, + debugShowCheckedModeBanner: false, + ); + }, + ), ); } + + List<String> getImagesAssets() { + final List<String> assets = []; + + final List<String> gameImages = [ + 'button_back', + 'button_delete_saved_game', + 'button_resume_game', + 'button_start', + 'placeholder', + ]; + + for (String image in gameImages) { + assets.add('assets/ui/$image.png'); + } + + return assets; + } } diff --git a/lib/models/game/counter.dart b/lib/models/game/counter.dart new file mode 100644 index 0000000000000000000000000000000000000000..b991b9e239c8be90968b5fd57de60ce13267bd05 --- /dev/null +++ b/lib/models/game/counter.dart @@ -0,0 +1,35 @@ +class Counter { + // Current counter + bool touch = false; // Does this new tetrimino touch an other tetrimino of same player + int lines = 0; // Count lines fully filled by this new tetrimino + int holes = 0; // Count non fillable holes caused by this new tetrimino + + // Points definitions + static const int base = 50; + static const int pointsIfTouch = 50; + static const int pointsPerLine = 60; + static const int pointsPerHole = -10; + + int computePoints() { + return base + (touch ? pointsIfTouch : 0) + lines * pointsPerLine + holes * pointsPerHole; + } + + void reset() { + touch = false; + lines = 0; + holes = 0; + } + + @override + String toString() { + return '$Counter(${toJson()})'; + } + + Map<String, dynamic> toJson() { + return { + 'touch': touch, + 'lines': lines, + 'holes': holes, + }; + } +} diff --git a/lib/models/game/game.dart b/lib/models/game/game.dart new file mode 100644 index 0000000000000000000000000000000000000000..afb0a573f5a4c284656073388421562bfb4c35ac --- /dev/null +++ b/lib/models/game/game.dart @@ -0,0 +1,115 @@ +import 'package:tetrisdual/models/game/player.dart'; +import 'package:tetrisdual/models/settings/settings_game.dart'; +import 'package:tetrisdual/models/settings/settings_global.dart'; +import 'package:tetrisdual/utils/tools.dart'; + +class Game { + Game({ + // Settings + required this.gameSettings, + required this.globalSettings, + + // State + this.isRunning = false, + this.isStarted = false, + this.isFinished = false, + this.animationInProgress = false, + + // Base data + this.players = const [], + + // Game data + this.currentPlayer = 0, + }); + + // Settings + final GameSettings gameSettings; + final GlobalSettings globalSettings; + + // State + bool isRunning; + bool isStarted; + bool isFinished; + bool animationInProgress; + + // Base data + List<Player> players; + + // Game data + int currentPlayer; + + factory Game.createEmpty() { + return Game( + // Settings + gameSettings: GameSettings.createDefault(), + globalSettings: GlobalSettings.createDefault(), + ); + } + + factory Game.createNew({ + GameSettings? gameSettings, + GlobalSettings? globalSettings, + }) { + final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault(); + final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault(); + + return Game( + // Settings + gameSettings: newGameSettings, + globalSettings: newGlobalSettings, + // State + isRunning: true, + // Base data + players: [ + Player(1), + Player(2), + ], + // Game data + currentPlayer: 0, + ); + } + + bool get canBeResumed => isStarted && !isFinished; + + void dump() { + printlog(''); + printlog('## Current game dump:'); + printlog(''); + printlog('$Game:'); + printlog(' Settings'); + gameSettings.dump(); + globalSettings.dump(); + printlog(' State'); + printlog(' isRunning: $isRunning'); + printlog(' isStarted: $isStarted'); + printlog(' isFinished: $isFinished'); + printlog(' animationInProgress: $animationInProgress'); + printlog(' Base data'); + printlog(' players: $players'); + printlog(' Game data'); + printlog(' currentPlayer: $currentPlayer'); + printlog(''); + } + + @override + String toString() { + return '$Game(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + // Settings + 'gameSettings': gameSettings.toJson(), + 'globalSettings': globalSettings.toJson(), + // State + 'isRunning': isRunning, + 'isStarted': isStarted, + 'isFinished': isFinished, + 'animationInProgress': animationInProgress, + // Base data + 'players': players, + // Game data + 'currentPlayer': currentPlayer, + }; + } +} diff --git a/lib/models/game/player.dart b/lib/models/game/player.dart new file mode 100644 index 0000000000000000000000000000000000000000..4d5aed113bb74fab5d0b8dbb336082e168754656 --- /dev/null +++ b/lib/models/game/player.dart @@ -0,0 +1,45 @@ +import 'dart:math'; + +import 'package:tetrisdual/models/game/counter.dart'; + +class Player { + Player(this.playerId); + int playerId; + + int score = 0; + int currentTetrimino = 0; + final Counter counter = Counter(); + + void pickRandomTetrimino() { + // ensure new tetrimino is not same as current one + int newTetrimino = currentTetrimino; + while (newTetrimino == currentTetrimino) { + newTetrimino = Random().nextInt(5) + 1; + } + + currentTetrimino = newTetrimino; + } + + void resetTetrimino() { + currentTetrimino = 0; + } + + void submitPoints() { + score = score + counter.computePoints(); + counter.reset(); + } + + @override + String toString() { + return '$Player(${toJson()})'; + } + + Map<String, dynamic> toJson() { + return { + 'playerId': playerId, + 'score': score, + 'currentTetrimino': currentTetrimino, + 'counter': counter, + }; + } +} diff --git a/lib/models/settings/settings_game.dart b/lib/models/settings/settings_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..e94f857dc46a4e148f6c85891497cd7167d0284b --- /dev/null +++ b/lib/models/settings/settings_game.dart @@ -0,0 +1,41 @@ +import 'package:tetrisdual/config/default_game_settings.dart'; +import 'package:tetrisdual/utils/tools.dart'; + +class GameSettings { + final String gameType; + + GameSettings({ + required this.gameType, + }); + + static String getGameTypeValueFromUnsafe(String gameType) { + if (DefaultGameSettings.allowedGameTypeValues.contains(gameType)) { + return gameType; + } + + return DefaultGameSettings.defaultGameTypeValue; + } + + factory GameSettings.createDefault() { + return GameSettings( + gameType: DefaultGameSettings.defaultGameTypeValue, + ); + } + + void dump() { + printlog('$GameSettings:'); + printlog(' ${DefaultGameSettings.parameterCodeGameType}: $gameType'); + printlog(''); + } + + @override + String toString() { + return '$GameSettings(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + DefaultGameSettings.parameterCodeGameType: gameType, + }; + } +} diff --git a/lib/models/settings/settings_global.dart b/lib/models/settings/settings_global.dart new file mode 100644 index 0000000000000000000000000000000000000000..ac327d732f6bfd50de5900a3f39e10e6b0785c14 --- /dev/null +++ b/lib/models/settings/settings_global.dart @@ -0,0 +1,41 @@ +import 'package:tetrisdual/config/default_global_settings.dart'; +import 'package:tetrisdual/utils/tools.dart'; + +class GlobalSettings { + String skin; + + GlobalSettings({ + required this.skin, + }); + + static String getSkinValueFromUnsafe(String skin) { + if (DefaultGlobalSettings.allowedSkinValues.contains(skin)) { + return skin; + } + + return DefaultGlobalSettings.defaultSkinValue; + } + + factory GlobalSettings.createDefault() { + return GlobalSettings( + skin: DefaultGlobalSettings.defaultSkinValue, + ); + } + + void dump() { + printlog('$GlobalSettings:'); + printlog(' ${DefaultGlobalSettings.parameterCodeSkin}: $skin'); + printlog(''); + } + + @override + String toString() { + return '$GlobalSettings(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + DefaultGlobalSettings.parameterCodeSkin: skin, + }; + } +} diff --git a/lib/provider/data.dart b/lib/provider/data.dart deleted file mode 100644 index b1f4ad0e5d4c79c0252a7762b9fa900f5da04412..0000000000000000000000000000000000000000 --- a/lib/provider/data.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/foundation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import 'package:tetrisdual/entity/player.dart'; - -class Data extends ChangeNotifier { - // Configuration available parameters - final List<String> _availableParameters = []; - - List<String> get availableParameters => _availableParameters; - - // Application default configuration - - // Application current configuration - String getParameterValue(String parameterCode) { - switch (parameterCode) {} - return ''; - } - - List<String> getParameterAvailableValues(String parameterCode) { - switch (parameterCode) {} - return []; - } - - void setParameterValue(String parameterCode, String parameterValue) async { - switch (parameterCode) {} - final prefs = await SharedPreferences.getInstance(); - prefs.setString(parameterCode, parameterValue); - } - - // Game data - bool _gameIsRunning = false; - bool _gameIsFinished = false; - int _currentPlayer = 0; - List<Player?> _players = [null, null]; - - bool get isGameRunning => _gameIsRunning; - void updateGameIsRunning(bool gameIsRunning) { - _gameIsRunning = gameIsRunning; - notifyListeners(); - } - - bool get isGameFinished => _gameIsFinished; - void updateGameIsFinished(bool gameIsFinished) { - _gameIsFinished = gameIsFinished; - notifyListeners(); - } - - int get currentPlayer => _currentPlayer; - void toggleCurrentPlayer() { - if (_currentPlayer == 0) { - // start game - _currentPlayer = 1; - } else { - // Reset current player tetrimino - getCurrentPlayer().resetTetrimino(); - - // 1 -> 2 ; 2 -> 1 - _currentPlayer = 3 - _currentPlayer; - } - - // Pick new tetrimino - getCurrentPlayer().pickRandomTetrimino(); - - notifyListeners(); - } - - void enableRandomPlayer() { - _currentPlayer = Random().nextInt(2) + 1; - toggleCurrentPlayer(); - } - - Player getPlayer(int playerId) { - final int playerIndex = playerId - 1; - Player? player = _players[playerIndex]; - - // Create new player if none - if (null == player) { - player = Player(playerId); - _players[playerIndex] = player; - } - - return player; - } - - Player getCurrentPlayer() { - return getPlayer(currentPlayer); - } - - void redraw() { - notifyListeners(); - } - - void resetGame() { - _gameIsRunning = false; - _gameIsFinished = false; - _players = [Player(1), Player(2)]; - notifyListeners(); - } -} diff --git a/lib/screens/home.dart b/lib/screens/home.dart deleted file mode 100644 index 34cdc098057ec2fe06739f673a9aec55382fbbf2..0000000000000000000000000000000000000000 --- a/lib/screens/home.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:overlay_support/overlay_support.dart'; - -import 'package:tetrisdual/layout/game.dart'; -import 'package:tetrisdual/layout/parameters.dart'; -import 'package:tetrisdual/provider/data.dart'; -import 'package:tetrisdual/utils/game_utils.dart'; - -class Home extends StatefulWidget { - const Home({super.key}); - - static const String id = 'home'; - - @override - HomeState createState() => HomeState(); -} - -class HomeState extends State<Home> { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - final Data myProvider = Provider.of<Data>(context); - final double screenWidth = MediaQuery.of(context).size.width; - - final List<Widget> menuActions = []; - - if (myProvider.isGameRunning) { - menuActions.add(TextButton( - child: const Image( - image: AssetImage('assets/icons/button_back.png'), - fit: BoxFit.fill, - ), - onPressed: () => toast('Long press to quit game...'), - onLongPress: () => GameUtils.quitGame(myProvider), - )); - } - - return Scaffold( - appBar: AppBar( - actions: menuActions, - ), - body: myProvider.isGameRunning - ? Game.buildGameWidget(myProvider, screenWidth) - : Parameters.buildParametersSelector(myProvider), - ); - } -} diff --git a/lib/ui/helpers/app_titles.dart b/lib/ui/helpers/app_titles.dart new file mode 100644 index 0000000000000000000000000000000000000000..b98107b12fabc3114ebfbec994166b588abcf1ad --- /dev/null +++ b/lib/ui/helpers/app_titles.dart @@ -0,0 +1,32 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +class AppHeader extends StatelessWidget { + const AppHeader({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Text( + tr(text), + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2), + ); + } +} + +class AppTitle extends StatelessWidget { + const AppTitle({super.key, required this.text}); + + final String text; + + @override + Widget build(BuildContext context) { + return Text( + tr(text), + textAlign: TextAlign.start, + style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2), + ); + } +} diff --git a/lib/ui/helpers/outlined_text_widget.dart b/lib/ui/helpers/outlined_text_widget.dart new file mode 100644 index 0000000000000000000000000000000000000000..7cecab58dec9b861d573ae93127ac27387e22d6a --- /dev/null +++ b/lib/ui/helpers/outlined_text_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:tetrisdual/utils/color_extensions.dart'; + +class OutlinedText extends StatelessWidget { + const OutlinedText({ + super.key, + required this.text, + required this.fontSize, + required this.textColor, + this.outlineColor, + }); + + final String text; + final double fontSize; + final Color textColor; + final Color? outlineColor; + + @override + Widget build(BuildContext context) { + final double delta = fontSize / 30; + + return Text( + text, + style: TextStyle( + inherit: true, + fontSize: fontSize, + fontWeight: FontWeight.w600, + color: textColor, + shadows: [ + Shadow( + offset: Offset(-delta, -delta), + color: outlineColor ?? textColor.darken(), + ), + Shadow( + offset: Offset(delta, -delta), + color: outlineColor ?? textColor.darken(), + ), + Shadow( + offset: Offset(delta, delta), + color: outlineColor ?? textColor.darken(), + ), + Shadow( + offset: Offset(-delta, delta), + color: outlineColor ?? textColor.darken(), + ), + ], + ), + ); + } +} diff --git a/lib/ui/layouts/game_layout.dart b/lib/ui/layouts/game_layout.dart new file mode 100644 index 0000000000000000000000000000000000000000..d49b3119d04bd0500e34ad003d100013b83ca2ea --- /dev/null +++ b/lib/ui/layouts/game_layout.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +import 'package:tetrisdual/ui/widgets/game/game_board.dart'; + +class GameLayout extends StatelessWidget { + const GameLayout({super.key}); + + @override + Widget build(BuildContext context) { + return const GameBoardWidget(); + } +} diff --git a/lib/ui/layouts/parameters_layout.dart b/lib/ui/layouts/parameters_layout.dart new file mode 100644 index 0000000000000000000000000000000000000000..87c48ad8562d6d67b482a11074d4fb55c177cc77 --- /dev/null +++ b/lib/ui/layouts/parameters_layout.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/config/default_game_settings.dart'; +import 'package:tetrisdual/config/default_global_settings.dart'; +import 'package:tetrisdual/cubit/settings_game_cubit.dart'; +import 'package:tetrisdual/cubit/settings_global_cubit.dart'; +import 'package:tetrisdual/ui/parameters/parameter_image.dart'; +import 'package:tetrisdual/ui/parameters/parameter_painter.dart'; +import 'package:tetrisdual/ui/widgets/actions/button_delete_saved_game.dart'; +import 'package:tetrisdual/ui/widgets/actions/button_game_start_new.dart'; +import 'package:tetrisdual/ui/widgets/actions/button_resume_saved_game.dart'; + +class ParametersLayout extends StatelessWidget { + const ParametersLayout({super.key, required this.canResume}); + + final bool canResume; + + final double separatorHeight = 8.0; + + @override + Widget build(BuildContext context) { + final List<Widget> lines = []; + + // Game settings + for (String code in DefaultGameSettings.availableParameters) { + lines.add(Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: buildParametersLine( + code: code, + isGlobal: false, + ), + )); + + lines.add(SizedBox(height: separatorHeight)); + } + + lines.add(SizedBox(height: separatorHeight)); + + if (canResume == false) { + // Start new game + lines.add(const Expanded( + child: StartNewGameButton(), + )); + } else { + // Resume game + lines.add(const Expanded( + child: ResumeSavedGameButton(), + )); + // Delete saved game + lines.add(SizedBox.square( + dimension: MediaQuery.of(context).size.width / 4, + child: const DeleteSavedGameButton(), + )); + } + + lines.add(SizedBox(height: separatorHeight)); + + // Global settings + for (String code in DefaultGlobalSettings.availableParameters) { + lines.add(Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: buildParametersLine( + code: code, + isGlobal: true, + ), + )); + + lines.add(SizedBox(height: separatorHeight)); + } + + return Column( + children: lines, + ); + } + + List<Widget> buildParametersLine({ + required String code, + required bool isGlobal, + }) { + final List<Widget> parameterButtons = []; + + final List<String> availableValues = isGlobal + ? DefaultGlobalSettings.getAvailableValues(code) + : DefaultGameSettings.getAvailableValues(code); + + if (availableValues.length <= 1) { + return []; + } + + for (String value in availableValues) { + final Widget parameterButton = BlocBuilder<GameSettingsCubit, GameSettingsState>( + builder: (BuildContext context, GameSettingsState gameSettingsState) { + return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>( + builder: (BuildContext context, GlobalSettingsState globalSettingsState) { + final GameSettingsCubit gameSettingsCubit = + BlocProvider.of<GameSettingsCubit>(context); + final GlobalSettingsCubit globalSettingsCubit = + BlocProvider.of<GlobalSettingsCubit>(context); + + final String currentValue = isGlobal + ? globalSettingsCubit.getParameterValue(code) + : gameSettingsCubit.getParameterValue(code); + + final bool isActive = (value == currentValue); + + final double displayWidth = MediaQuery.of(context).size.width; + final double itemWidth = displayWidth / availableValues.length - 26; + + final bool displayedWithAssets = + DefaultGlobalSettings.displayedWithAssets.contains(code) || + DefaultGameSettings.displayedWithAssets.contains(code); + + return TextButton( + child: Container( + child: displayedWithAssets + ? SizedBox.square( + dimension: itemWidth, + child: ParameterImage( + code: code, + value: value, + isSelected: isActive, + ), + ) + : CustomPaint( + size: Size(itemWidth, itemWidth), + willChange: false, + painter: ParameterPainter( + code: code, + value: value, + isSelected: isActive, + gameSettings: gameSettingsState.settings, + globalSettings: globalSettingsState.settings, + ), + isComplex: true, + ), + ), + onPressed: () { + isGlobal + ? globalSettingsCubit.setParameterValue(code, value) + : gameSettingsCubit.setParameterValue(code, value); + }, + ); + }, + ); + }, + ); + + parameterButtons.add(parameterButton); + } + + return parameterButtons; + } +} diff --git a/lib/ui/parameters/parameter_image.dart b/lib/ui/parameters/parameter_image.dart new file mode 100644 index 0000000000000000000000000000000000000000..fc4b576f85b01158b74548400d11a4d027c57fbe --- /dev/null +++ b/lib/ui/parameters/parameter_image.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +class ParameterImage extends StatelessWidget { + const ParameterImage({ + super.key, + required this.code, + required this.value, + required this.isSelected, + }); + + final String code; + final String value; + final bool isSelected; + + static const Color buttonBackgroundColor = Colors.white; + static const Color buttonBorderColorActive = Colors.blue; + static const Color buttonBorderColorInactive = Colors.white; + static const double buttonBorderWidth = 8.0; + static const double buttonBorderRadius = 8.0; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: buttonBackgroundColor, + borderRadius: BorderRadius.circular(buttonBorderRadius), + border: Border.all( + color: isSelected ? buttonBorderColorActive : buttonBorderColorInactive, + width: buttonBorderWidth, + ), + ), + child: Image( + image: AssetImage('assets/ui/${code}_$value.png'), + fit: BoxFit.fill, + ), + ); + } +} diff --git a/lib/ui/parameters/parameter_painter.dart b/lib/ui/parameters/parameter_painter.dart new file mode 100644 index 0000000000000000000000000000000000000000..b4f633bef1dbf286c582376011caa51e92e8bf07 --- /dev/null +++ b/lib/ui/parameters/parameter_painter.dart @@ -0,0 +1,90 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:tetrisdual/models/settings/settings_game.dart'; +import 'package:tetrisdual/models/settings/settings_global.dart'; +import 'package:tetrisdual/utils/tools.dart'; + +class ParameterPainter extends CustomPainter { + const ParameterPainter({ + required this.code, + required this.value, + required this.isSelected, + required this.gameSettings, + required this.globalSettings, + }); + + final String code; + final String value; + final bool isSelected; + final GameSettings gameSettings; + final GlobalSettings globalSettings; + + @override + void paint(Canvas canvas, Size size) { + // force square + final double canvasSize = min(size.width, size.height); + + const Color borderColorEnabled = Colors.blue; + const Color borderColorDisabled = Colors.white; + + // "enabled/disabled" border + final paint = Paint(); + paint.style = PaintingStyle.stroke; + paint.color = isSelected ? borderColorEnabled : borderColorDisabled; + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 10; + canvas.drawRect( + Rect.fromPoints(const Offset(0, 0), Offset(canvasSize, canvasSize)), paint); + + // content + switch (code) { + default: + printlog('Unknown parameter: $code/$value'); + paintUnknownParameterItem(value, canvas, canvasSize); + } + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } + + // "unknown" parameter -> simple block with text + void paintUnknownParameterItem( + final String value, + final Canvas canvas, + final double size, + ) { + final paint = Paint(); + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 3; + + paint.color = Colors.grey; + paint.style = PaintingStyle.fill; + canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint); + + final textSpan = TextSpan( + text: '?\n$value', + style: const TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ); + final textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + textAlign: TextAlign.center, + ); + textPainter.layout(); + textPainter.paint( + canvas, + Offset( + (size - textPainter.width) * 0.5, + (size - textPainter.height) * 0.5, + ), + ); + } +} diff --git a/lib/ui/screens/page_about.dart b/lib/ui/screens/page_about.dart new file mode 100644 index 0000000000000000000000000000000000000000..284a45b0cdb6ea2220584a303903783f9c5f709b --- /dev/null +++ b/lib/ui/screens/page_about.dart @@ -0,0 +1,41 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +import 'package:tetrisdual/ui/helpers/app_titles.dart'; + +class PageAbout extends StatelessWidget { + const PageAbout({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + const SizedBox(height: 8), + const AppTitle(text: 'about_title'), + const Text('about_content').tr(), + FutureBuilder<PackageInfo>( + future: PackageInfo.fromPlatform(), + builder: (context, snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.done: + return const Text('about_version').tr( + namedArgs: { + 'version': snapshot.data!.version, + }, + ); + default: + return const SizedBox(); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/ui/screens/page_game.dart b/lib/ui/screens/page_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..8bf1a663aa5c8347a268351a5f6437a1bfca5209 --- /dev/null +++ b/lib/ui/screens/page_game.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/cubit/game_cubit.dart'; +import 'package:tetrisdual/models/game/game.dart'; +import 'package:tetrisdual/ui/layouts/game_layout.dart'; +import 'package:tetrisdual/ui/layouts/parameters_layout.dart'; + +class PageGame extends StatelessWidget { + const PageGame({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + return currentGame.isRunning + ? const GameLayout() + : ParametersLayout(canResume: currentGame.canBeResumed); + }, + ); + } +} diff --git a/lib/ui/screens/page_settings.dart b/lib/ui/screens/page_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..1813ba69b216956034a94631aebed2a21a8141bb --- /dev/null +++ b/lib/ui/screens/page_settings.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import 'package:tetrisdual/ui/helpers/app_titles.dart'; +import 'package:tetrisdual/ui/settings/settings_form.dart'; + +class PageSettings extends StatelessWidget { + const PageSettings({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + SizedBox(height: 8), + AppTitle(text: 'settings_title'), + SizedBox(height: 8), + SettingsForm(), + ], + ), + ); + } +} diff --git a/lib/ui/settings/settings_form.dart b/lib/ui/settings/settings_form.dart new file mode 100644 index 0000000000000000000000000000000000000000..55b4a7f3e1a13c7ab667bd7639efc0104bc0317c --- /dev/null +++ b/lib/ui/settings/settings_form.dart @@ -0,0 +1,63 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:tetrisdual/ui/settings/theme_card.dart'; + +class SettingsForm extends StatefulWidget { + const SettingsForm({super.key}); + + @override + State<SettingsForm> createState() => _SettingsFormState(); +} + +class _SettingsFormState extends State<SettingsForm> { + @override + void dispose() { + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: <Widget>[ + // Light/dark theme + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: <Widget>[ + const Text('settings_label_theme').tr(), + const Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ThemeCard( + mode: ThemeMode.system, + icon: UniconsLine.cog, + ), + ThemeCard( + mode: ThemeMode.light, + icon: UniconsLine.sun, + ), + ThemeCard( + mode: ThemeMode.dark, + icon: UniconsLine.moon, + ) + ], + ), + ], + ), + + const SizedBox(height: 16), + ], + ); + } +} diff --git a/lib/ui/settings/theme_card.dart b/lib/ui/settings/theme_card.dart new file mode 100644 index 0000000000000000000000000000000000000000..6aef993934224bb47e87940ade39dcc3e6fd5502 --- /dev/null +++ b/lib/ui/settings/theme_card.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/cubit/theme_cubit.dart'; + +class ThemeCard extends StatelessWidget { + const ThemeCard({ + super.key, + required this.mode, + required this.icon, + }); + + final IconData icon; + final ThemeMode mode; + + @override + Widget build(BuildContext context) { + return BlocBuilder<ThemeCubit, ThemeModeState>( + builder: (BuildContext context, ThemeModeState state) { + return Card( + elevation: 2, + shadowColor: Theme.of(context).colorScheme.shadow, + color: state.themeMode == mode + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surface, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + margin: const EdgeInsets.all(5), + child: InkWell( + onTap: () => BlocProvider.of<ThemeCubit>(context).getTheme( + ThemeModeState(themeMode: mode), + ), + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Icon( + icon, + size: 32, + color: state.themeMode != mode + ? Theme.of(context).colorScheme.primary + : Colors.white, + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/skeleton.dart b/lib/ui/skeleton.dart new file mode 100644 index 0000000000000000000000000000000000000000..0e4b38fceaac1e8e3ab1819d1cee06e752436d6c --- /dev/null +++ b/lib/ui/skeleton.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/config/menu.dart'; +import 'package:tetrisdual/cubit/nav_cubit.dart'; +import 'package:tetrisdual/ui/widgets/global_app_bar.dart'; + +class SkeletonScreen extends StatelessWidget { + const SkeletonScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const GlobalAppBar(), + extendBodyBehindAppBar: false, + body: Material( + color: Theme.of(context).colorScheme.surface, + child: BlocBuilder<NavCubit, int>( + builder: (BuildContext context, int pageIndex) { + return Padding( + padding: const EdgeInsets.only( + top: 8, + left: 2, + right: 2, + ), + child: Menu.getPageWidget(pageIndex), + ); + }, + ), + ), + backgroundColor: Theme.of(context).colorScheme.surface, + ); + } +} diff --git a/lib/ui/widgets/actions/button_delete_saved_game.dart b/lib/ui/widgets/actions/button_delete_saved_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..6e701828ab1daab181edaf08e89e0bc5a376a7ed --- /dev/null +++ b/lib/ui/widgets/actions/button_delete_saved_game.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/cubit/game_cubit.dart'; + +class DeleteSavedGameButton extends StatelessWidget { + const DeleteSavedGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return TextButton( + child: const Image( + image: AssetImage('assets/ui/button_delete_saved_game.png'), + fit: BoxFit.fill, + ), + onPressed: () { + BlocProvider.of<GameCubit>(context).deleteSavedGame(); + }, + ); + } +} diff --git a/lib/ui/widgets/actions/button_game_quit.dart b/lib/ui/widgets/actions/button_game_quit.dart new file mode 100644 index 0000000000000000000000000000000000000000..b16e72645ef71bb772c961a94064ef120db699ff --- /dev/null +++ b/lib/ui/widgets/actions/button_game_quit.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/cubit/game_cubit.dart'; + +class QuitGameButton extends StatelessWidget { + const QuitGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return TextButton( + child: const Image( + image: AssetImage('assets/ui/button_back.png'), + fit: BoxFit.fill, + ), + onPressed: () { + BlocProvider.of<GameCubit>(context).quitGame(); + }, + ); + } +} diff --git a/lib/ui/widgets/actions/button_game_start_new.dart b/lib/ui/widgets/actions/button_game_start_new.dart new file mode 100644 index 0000000000000000000000000000000000000000..6e609bea6941f02e65fb84f3700762f1ca6ccf19 --- /dev/null +++ b/lib/ui/widgets/actions/button_game_start_new.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/cubit/game_cubit.dart'; +import 'package:tetrisdual/cubit/settings_game_cubit.dart'; +import 'package:tetrisdual/cubit/settings_global_cubit.dart'; + +class StartNewGameButton extends StatelessWidget { + const StartNewGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameSettingsCubit, GameSettingsState>( + builder: (BuildContext context, GameSettingsState gameSettingsState) { + return BlocBuilder<GlobalSettingsCubit, GlobalSettingsState>( + builder: (BuildContext context, GlobalSettingsState globalSettingsState) { + return TextButton( + child: const Image( + image: AssetImage('assets/ui/button_start.png'), + fit: BoxFit.fill, + ), + onPressed: () { + BlocProvider.of<GameCubit>(context).startNewGame( + gameSettings: gameSettingsState.settings, + globalSettings: globalSettingsState.settings, + ); + }, + ); + }, + ); + }, + ); + } +} diff --git a/lib/ui/widgets/actions/button_resume_saved_game.dart b/lib/ui/widgets/actions/button_resume_saved_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..bbd0c93f29dc238ad23b283d310c29e02fd80e7f --- /dev/null +++ b/lib/ui/widgets/actions/button_resume_saved_game.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/cubit/game_cubit.dart'; + +class ResumeSavedGameButton extends StatelessWidget { + const ResumeSavedGameButton({super.key}); + + @override + Widget build(BuildContext context) { + return TextButton( + child: const Image( + image: AssetImage('assets/ui/button_resume_game.png'), + fit: BoxFit.fill, + ), + onPressed: () { + BlocProvider.of<GameCubit>(context).resumeSavedGame(); + }, + ); + } +} diff --git a/lib/layout/board_painter.dart b/lib/ui/widgets/game/board_painter.dart similarity index 100% rename from lib/layout/board_painter.dart rename to lib/ui/widgets/game/board_painter.dart diff --git a/lib/entity/counter.dart b/lib/ui/widgets/game/counter.dart similarity index 69% rename from lib/entity/counter.dart rename to lib/ui/widgets/game/counter.dart index 31fedae0d2c1f1bf3858fc36569b109c3caacbc0..0105014f7a20472fc7cbfdbbc9d000392c4aee14 100644 --- a/lib/entity/counter.dart +++ b/lib/ui/widgets/game/counter.dart @@ -1,20 +1,15 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:tetrisdual/provider/data.dart'; +import 'package:tetrisdual/cubit/game_cubit.dart'; +import 'package:tetrisdual/models/game/counter.dart'; -class Counter { - // Current counter - bool _match = false; // Does this new tetrimino touch an other tetrimino of same player - int _lines = 0; // Count lines fully filled by this new tetrimino - int _holes = 0; // Count non fillable holes caused by this new tetrimino +class CounterWidget extends StatelessWidget { + const CounterWidget({super.key, required this.counter}); - // Points definitions - static const int _base = 50; - static const int _pointsIfMatch = 50; - static const int _pointsPerLine = 60; - static const int _pointsPerHole = -10; + final Counter counter; static const double iconSize = 30.0; static const double fontSize = 50.0; @@ -51,29 +46,30 @@ class Counter { size: iconSize, ); - Widget buildCounterWidget(Data myProvider) { - return Table( - children: [ - buildMatchWidget(myProvider), - buildSpacerRow(), - buildLinesWidget(myProvider), - buildSpacerRow(), - buildHolesWidget(myProvider), - buildSpacerRow(), - ], - ); - } + @override + Widget build(BuildContext context) { + final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); - TableRow buildSpacerRow() { - return const TableRow(children: [ + const spacer = TableRow(children: [ + SizedBox(height: spacerHeight), SizedBox(height: spacerHeight), SizedBox(height: spacerHeight), SizedBox(height: spacerHeight), - SizedBox(height: spacerHeight) ]); + + return Table( + children: [ + buildTouchWidget(gameCubit), + spacer, + buildLinesWidget(gameCubit), + spacer, + buildHolesWidget(gameCubit), + spacer, + ], + ); } - TableRow buildMatchWidget(Data myProvider) { + TableRow buildTouchWidget(GameCubit gameCubit) { return TableRow( children: [ iconTouchingColor, @@ -85,13 +81,13 @@ class Counter { ), icon: iconRemove, onPressed: () { - _match = false; - myProvider.redraw(); + counter.touch = false; + gameCubit.refresh(); }, ), Center( child: Icon( - _match ? Icons.radio_button_checked : Icons.radio_button_unchecked, + counter.touch ? Icons.radio_button_checked : Icons.radio_button_unchecked, color: categoryIconColor, size: iconSize, ), @@ -104,15 +100,15 @@ class Counter { ), icon: iconAdd, onPressed: () { - _match = true; - myProvider.redraw(); + counter.touch = true; + gameCubit.refresh(); }, ), ], ); } - TableRow buildLinesWidget(Data myProvider) { + TableRow buildLinesWidget(GameCubit gameCubit) { return TableRow( children: [ iconRowsCount, @@ -124,13 +120,13 @@ class Counter { ), icon: iconRemove, onPressed: () { - _lines = max(_lines - 1, 0); - myProvider.redraw(); + counter.lines = max(counter.lines - 1, 0); + gameCubit.refresh(); }, ), Center( child: Text( - _lines.toString(), + counter.lines.toString(), style: const TextStyle( fontFamily: 'Blocks', fontSize: fontSize, @@ -149,15 +145,15 @@ class Counter { ), icon: iconAdd, onPressed: () { - _lines = min(_lines + 1, 4); - myProvider.redraw(); + counter.lines = min(counter.lines + 1, 4); + gameCubit.refresh(); }, ), ], ); } - TableRow buildHolesWidget(Data myProvider) { + TableRow buildHolesWidget(GameCubit gameCubit) { return TableRow( children: [ iconHolesCount, @@ -169,13 +165,13 @@ class Counter { ), icon: iconRemove, onPressed: () { - _holes = max(_holes - 1, 0); - myProvider.redraw(); + counter.holes = max(counter.holes - 1, 0); + gameCubit.refresh(); }, ), Center( child: Text( - _holes.toString(), + counter.holes.toString(), style: const TextStyle( fontFamily: 'Blocks', fontSize: fontSize, @@ -194,24 +190,11 @@ class Counter { ), icon: iconAdd, onPressed: () { - _holes = min(_holes + 1, 9); - myProvider.redraw(); + counter.holes = min(counter.holes + 1, 9); + gameCubit.refresh(); }, ), ], ); } - - int computePoints() { - return _base + - (_match ? _pointsIfMatch : 0) + - _lines * _pointsPerLine + - _holes * _pointsPerHole; - } - - void reset() { - _match = false; - _lines = 0; - _holes = 0; - } } diff --git a/lib/ui/widgets/game/game_board.dart b/lib/ui/widgets/game/game_board.dart new file mode 100644 index 0000000000000000000000000000000000000000..5a46f801dbe6410ebb65507398238c981ea948ee --- /dev/null +++ b/lib/ui/widgets/game/game_board.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/cubit/game_cubit.dart'; +import 'package:tetrisdual/ui/widgets/game/player.dart'; +import 'package:tetrisdual/ui/widgets/game/toggle_player.dart'; + +class GameBoardWidget extends StatelessWidget { + const GameBoardWidget({super.key}); + + @override + Widget build(BuildContext context) { + final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + RotatedBox( + quarterTurns: 2, + child: PlayerWidget( + player: gameCubit.getPlayer(1), + ), + ), + const TogglePlayerWidget(), + PlayerWidget( + player: gameCubit.getPlayer(2), + ), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/game/manager.dart b/lib/ui/widgets/game/manager.dart new file mode 100644 index 0000000000000000000000000000000000000000..e02062a7b20f989443d427827aa8293b95b0a863 --- /dev/null +++ b/lib/ui/widgets/game/manager.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/cubit/game_cubit.dart'; +import 'package:tetrisdual/models/game/player.dart'; +import 'package:tetrisdual/ui/widgets/game/counter.dart'; +import 'package:tetrisdual/ui/widgets/game/submit.dart'; + +class ManagerWidget extends StatelessWidget { + const ManagerWidget({super.key, required this.player}); + + final Player player; + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + return Expanded( + child: Container( + margin: const EdgeInsets.all(5), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: gameState.currentGame.currentPlayer == player.playerId + ? [ + CounterWidget(counter: player.counter), + SubmitWidget(player: player), + ] + : [], + ), + ), + ); + }, + ); + } +} diff --git a/lib/ui/widgets/game/player.dart b/lib/ui/widgets/game/player.dart new file mode 100644 index 0000000000000000000000000000000000000000..92109dd8a245737f4cca9c2d32811aecf11afddb --- /dev/null +++ b/lib/ui/widgets/game/player.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/cubit/game_cubit.dart'; +import 'package:tetrisdual/models/game/player.dart'; +import 'package:tetrisdual/ui/widgets/game/manager.dart'; +import 'package:tetrisdual/ui/widgets/game/tetrimino.dart'; + +class PlayerWidget extends StatelessWidget { + const PlayerWidget({super.key, required this.player}); + + final Player player; + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final bool isActive = (gameState.currentGame.currentPlayer == player.playerId); + final double screenWidth = MediaQuery.of(context).size.width; + final double tetriminoBlockWidth = screenWidth / 2.3; + + final Color borderColor = isActive ? Colors.greenAccent : Colors.blueGrey; + const double borderWidth = 10; + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + player.score.toString(), + style: const TextStyle( + fontFamily: 'Blocks', + fontSize: 130, + fontWeight: FontWeight.bold, + ), + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), + ), + Container( + decoration: BoxDecoration( + border: Border.all( + color: borderColor, + width: borderWidth, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox.square( + dimension: tetriminoBlockWidth, + child: + isActive ? TetriminoWidget(player: player) : const SizedBox.shrink(), + ), + ManagerWidget(player: player), + ], + ), + ) + ], + ); + }, + ); + } +} diff --git a/lib/ui/widgets/game/submit.dart b/lib/ui/widgets/game/submit.dart new file mode 100644 index 0000000000000000000000000000000000000000..875001b384306279dd9edbe1fa3b3b8710ed2834 --- /dev/null +++ b/lib/ui/widgets/game/submit.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tetrisdual/cubit/game_cubit.dart'; +import 'package:tetrisdual/models/game/player.dart'; + +class SubmitWidget extends StatelessWidget { + const SubmitWidget({super.key, required this.player}); + + final Player player; + + @override + Widget build(BuildContext context) { + const double gainFontSize = 70; + + const gainTestStyle = TextStyle( + fontFamily: 'Blocks', + fontSize: gainFontSize, + fontWeight: FontWeight.bold, + ); + const submitIcon = Icon( + Icons.done_all, + color: Colors.orange, + size: gainFontSize / 2, + ); + + const topBorderBlack = BoxDecoration( + border: Border( + top: BorderSide( + color: Colors.black, + width: 2, + ), + ), + ); + + return Container( + decoration: topBorderBlack, + padding: const EdgeInsets.only( + top: 10, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '+${player.counter.computePoints()}', + style: gainTestStyle, + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), + ), + const SizedBox(width: 10), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + icon: submitIcon, + onPressed: () { + player.score = player.score + player.counter.computePoints(); + player.counter.reset(); + + BlocProvider.of<GameCubit>(context).toggleCurrentPlayer(); + }, + ), + const SizedBox(width: 10), + ], + ), + ); + } +} diff --git a/lib/ui/widgets/game/tetrimino.dart b/lib/ui/widgets/game/tetrimino.dart new file mode 100644 index 0000000000000000000000000000000000000000..d883487c746c21cf6f0ea09e875408f2aa98efd7 --- /dev/null +++ b/lib/ui/widgets/game/tetrimino.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/cubit/game_cubit.dart'; +import 'package:tetrisdual/models/game/player.dart'; +import 'package:tetrisdual/ui/widgets/game/board_painter.dart'; + +class TetriminoWidget extends StatelessWidget { + const TetriminoWidget({super.key, required this.player}); + + final Player player; + + @override + Widget build(BuildContext context) { + final GameCubit gameCubit = BlocProvider.of<GameCubit>(context); + final double width = MediaQuery.of(context).size.width; + + return GestureDetector( + onTapUp: (details) { + if (player.playerId == gameCubit.getCurrentPlayer().playerId) { + player.pickRandomTetrimino(); + gameCubit.refresh(); + } + }, + child: CustomPaint( + size: Size(width, width), + willChange: false, + painter: BoardPainter(player.currentTetrimino), + isComplex: true, + key: Key(player.currentTetrimino.toString()), + ), + ); + } +} diff --git a/lib/ui/widgets/game/toggle_player.dart b/lib/ui/widgets/game/toggle_player.dart new file mode 100644 index 0000000000000000000000000000000000000000..1d15b942250e1d4d21fbf210e8d11b5e0c7bad57 --- /dev/null +++ b/lib/ui/widgets/game/toggle_player.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/cubit/game_cubit.dart'; + +class TogglePlayerWidget extends StatelessWidget { + const TogglePlayerWidget({super.key}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTapUp: (details) { + BlocProvider.of<GameCubit>(context).toggleCurrentPlayer(); + }, + child: const Text( + '🔄', + style: TextStyle( + fontSize: 50, + ), + textHeightBehavior: TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), + ), + ); + } +} diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart new file mode 100644 index 0000000000000000000000000000000000000000..240c5f2e7f2f9bd708fb4bc0d36b57a61d1c6b9c --- /dev/null +++ b/lib/ui/widgets/global_app_bar.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:tetrisdual/config/menu.dart'; +import 'package:tetrisdual/cubit/game_cubit.dart'; +import 'package:tetrisdual/cubit/nav_cubit.dart'; +import 'package:tetrisdual/models/game/game.dart'; +import 'package:tetrisdual/ui/helpers/app_titles.dart'; + +class GlobalAppBar extends StatelessWidget implements PreferredSizeWidget { + const GlobalAppBar({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + return BlocBuilder<NavCubit, int>( + builder: (BuildContext context, int pageIndex) { + final Game currentGame = gameState.currentGame; + + final List<Widget> menuActions = []; + + if (currentGame.isRunning && !currentGame.isFinished) { + menuActions.add(TextButton( + child: const Image( + image: AssetImage('assets/ui/button_back.png'), + fit: BoxFit.fill, + ), + onPressed: () {}, + onLongPress: () { + BlocProvider.of<GameCubit>(context).quitGame(); + }, + )); + } else { + if (pageIndex == Menu.indexGame) { + // go to Settings page + menuActions.add(ElevatedButton( + onPressed: () { + context.read<NavCubit>().goToSettingsPage(); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + ), + child: Menu.menuItemSettings.icon, + )); + + // go to About page + menuActions.add(ElevatedButton( + onPressed: () { + context.read<NavCubit>().goToAboutPage(); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + ), + child: Menu.menuItemAbout.icon, + )); + } else { + // back to Home page + menuActions.add(ElevatedButton( + onPressed: () { + context.read<NavCubit>().goToGamePage(); + }, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + ), + child: Menu.menuItemGame.icon, + )); + } + } + + return AppBar( + title: const AppHeader(text: 'app_name'), + actions: menuActions, + ); + }, + ); + }, + ); + } + + @override + Size get preferredSize => const Size.fromHeight(50); +} diff --git a/lib/utils/color_extensions.dart b/lib/utils/color_extensions.dart new file mode 100644 index 0000000000000000000000000000000000000000..4e55e338f0d3ed98b233d1ef887b7b3e17e29d97 --- /dev/null +++ b/lib/utils/color_extensions.dart @@ -0,0 +1,33 @@ +import 'dart:ui'; + +extension ColorExtension on Color { + Color darken([int percent = 40]) { + assert(1 <= percent && percent <= 100); + final value = 1 - percent / 100; + return Color.fromARGB( + alpha, + (red * value).round(), + (green * value).round(), + (blue * value).round(), + ); + } + + Color lighten([int percent = 40]) { + assert(1 <= percent && percent <= 100); + final value = percent / 100; + return Color.fromARGB( + alpha, + (red + ((255 - red) * value)).round(), + (green + ((255 - green) * value)).round(), + (blue + ((255 - blue) * value)).round(), + ); + } + + Color avg(Color other) { + final red = (this.red + other.red) ~/ 2; + final green = (this.green + other.green) ~/ 2; + final blue = (this.blue + other.blue) ~/ 2; + final alpha = (this.alpha + other.alpha) ~/ 2; + return Color.fromARGB(alpha, red, green, blue); + } +} diff --git a/lib/utils/game_utils.dart b/lib/utils/game_utils.dart deleted file mode 100644 index fed7b66ae2d25e27d9b6e4fc1ef9d702228f0ca2..0000000000000000000000000000000000000000 --- a/lib/utils/game_utils.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:tetrisdual/provider/data.dart'; - -class GameUtils { - static Future<void> quitGame(Data myProvider) async { - myProvider.updateGameIsRunning(false); - } - - static Future<void> startNewGame(Data myProvider) async { - myProvider.resetGame(); - myProvider.enableRandomPlayer(); - myProvider.getCurrentPlayer().pickRandomTetrimino(); - myProvider.updateGameIsRunning(true); - } -} diff --git a/pubspec.lock b/pubspec.lock index 6b1fdfef808496ebd4ff8ce5bb9e51bfbc6469e8..e0ab96ebb656b1260018d45af586d9ec14ba4a7e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" async: dependency: transitive description: @@ -9,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" characters: dependency: transitive description: @@ -17,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" collection: dependency: transitive description: @@ -25,6 +49,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + easy_localization: + dependency: "direct main" + description: + name: easy_localization + sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 + url: "https://pub.dev" + source: hosted + version: "3.0.7" + easy_logger: + dependency: transitive + description: + name: easy_logger + sha256: c764a6e024846f33405a2342caf91c62e357c24b02c04dbc712ef232bf30ffb7 + url: "https://pub.dev" + source: hosted + version: "0.0.2" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" ffi: dependency: transitive description: @@ -46,27 +102,80 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "4.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + http: + dependency: transitive + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + hydrated_bloc: + dependency: "direct main" + description: + name: hydrated_bloc + sha256: af35b357739fe41728df10bec03aad422cdc725a1e702e03af9d2a41ea05160c + url: "https://pub.dev" + source: hosted + version: "9.1.5" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" lints: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" material_color_utilities: dependency: transitive description: @@ -79,10 +188,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" nested: dependency: transitive description: @@ -91,14 +200,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - overlay_support: + package_info_plus: dependency: "direct main" description: - name: overlay_support - sha256: fc39389bfd94e6985e1e13b2a88a125fc4027608485d2d4e2847afe1b2bb339c + name: package_info_plus + sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "8.0.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + url: "https://pub.dev" + source: hosted + version: "3.0.0" path: dependency: transitive description: @@ -107,6 +224,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" + url: "https://pub.dev" + source: hosted + version: "2.2.5" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -135,10 +276,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -148,7 +289,7 @@ packages: source: hosted version: "2.1.8" provider: - dependency: "direct main" + dependency: transitive description: name: provider sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c @@ -156,29 +297,29 @@ packages: source: hosted version: "6.1.2" shared_preferences: - dependency: "direct main" + dependency: transitive description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.3" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.4.0" shared_preferences_linux: dependency: transitive description: @@ -216,6 +357,54 @@ packages: description: flutter source: sdk version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + unicons: + dependency: "direct main" + description: + name: unicons + sha256: dbfcf93ff4d4ea19b324113857e358e4882115ab85db04417a4ba1c72b17a670 + url: "https://pub.dev" + source: hosted + version: "2.1.1" vector_math: dependency: transitive description: @@ -228,18 +417,18 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.5.1" win32: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.5.1" xdg_directories: dependency: transitive description: @@ -249,5 +438,5 @@ packages: source: hosted version: "1.0.4" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 553c373d9b1a925596874ad75fe8c4fb5a246534..ddd843645aaf7b8c8166d73aafb04c5607b6f715 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,26 +1,51 @@ name: tetrisdual description: Tetris Dual Game -publish_to: 'none' -version: 0.0.12+12 + +publish_to: "none" + +version: 0.1.0+13 environment: - sdk: '^3.0.0' + sdk: "^3.0.0" dependencies: flutter: sdk: flutter - provider: ^6.0.5 - shared_preferences: ^2.2.1 - overlay_support: ^2.1.0 + + # base + easy_localization: ^3.0.1 + equatable: ^2.0.5 + flutter_bloc: ^8.1.1 + hive: ^2.2.3 + hydrated_bloc: ^9.0.0 + package_info_plus: ^8.0.0 + path_provider: ^2.0.11 + unicons: ^2.1.1 + + # specific + # (none) dev_dependencies: - flutter_lints: ^3.0.1 + flutter_lints: ^4.0.0 flutter: uses-material-design: true assets: - - assets/icons/ + - assets/ui/ + - assets/translations/ + fonts: - family: Blocks fonts: - asset: assets/fonts/blocks.ttf + - family: Nunito + fonts: + - asset: assets/fonts/Nunito-Bold.ttf + weight: 700 + - asset: assets/fonts/Nunito-Medium.ttf + weight: 500 + - asset: assets/fonts/Nunito-Regular.ttf + weight: 400 + - asset: assets/fonts/Nunito-Light.ttf + weight: 300 + diff --git a/icons/build_repository_icons.sh b/resources/app/build_application_resources.sh similarity index 98% rename from icons/build_repository_icons.sh rename to resources/app/build_application_resources.sh index 27dbe2647fe4e6d562fbd99451716d1b7d448570..6d67b8f4f9eca701d1aed7331ef41dfb0bd44f20 100755 --- a/icons/build_repository_icons.sh +++ b/resources/app/build_application_resources.sh @@ -6,7 +6,7 @@ command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not ins command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; } CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" -BASE_DIR="$(dirname "${CURRENT_DIR}")" +BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")" SOURCE_ICON="${CURRENT_DIR}/icon.svg" SOURCE_FASTLANE="${CURRENT_DIR}/featureGraphic.svg" diff --git a/icons/featureGraphic.svg b/resources/app/featureGraphic.svg similarity index 100% rename from icons/featureGraphic.svg rename to resources/app/featureGraphic.svg diff --git a/icons/icon.svg b/resources/app/icon.svg similarity index 100% rename from icons/icon.svg rename to resources/app/icon.svg diff --git a/icons/build_icons.sh b/resources/build_resources.sh similarity index 50% rename from icons/build_icons.sh rename to resources/build_resources.sh index 915a16cb0b0b9b3a7109ef6686af811fc93cb8bb..659697a1c043cfe1c7654635cfaec3e4a0ff8a1a 100755 --- a/icons/build_icons.sh +++ b/resources/build_resources.sh @@ -2,5 +2,6 @@ CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" -"${CURRENT_DIR}/build_application_icons.sh" -"${CURRENT_DIR}/build_repository_icons.sh" +${CURRENT_DIR}/app/build_application_resources.sh +${CURRENT_DIR}/ui/build_ui_resources.sh + diff --git a/resources/ui/build_ui_resources.sh b/resources/ui/build_ui_resources.sh new file mode 100755 index 0000000000000000000000000000000000000000..4f365ede7d83140ce6309a3083580f2662b30990 --- /dev/null +++ b/resources/ui/build_ui_resources.sh @@ -0,0 +1,110 @@ +#! /bin/bash + +# Check dependencies +command -v inkscape >/dev/null 2>&1 || { echo >&2 "I require inkscape but it's not installed. Aborting."; exit 1; } +command -v scour >/dev/null 2>&1 || { echo >&2 "I require scour but it's not installed. Aborting."; exit 1; } +command -v optipng >/dev/null 2>&1 || { echo >&2 "I require optipng but it's not installed. Aborting."; exit 1; } + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +BASE_DIR="$(dirname "$(dirname "${CURRENT_DIR}")")" +ASSETS_DIR="${BASE_DIR}/assets" + +OPTIPNG_OPTIONS="-preserve -quiet -o7" +ICON_SIZE=192 + +####################################################### + +# Game images (svg files found in `images` folder) +AVAILABLE_GAME_IMAGES="" +if [ -d "${CURRENT_DIR}/images" ]; then + AVAILABLE_GAME_IMAGES="$(find "${CURRENT_DIR}/images" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort)" +fi + +# Skins (subfolders found in `skins` folder) +AVAILABLE_SKINS="" +if [ -d "${CURRENT_DIR}/skins" ]; then + AVAILABLE_SKINS="$(find "${CURRENT_DIR}/skins" -mindepth 1 -type d | awk -F/ '{print $NF}')" +fi + +# Images per skin (svg files found recursively in `skins` folder and subfolders) +SKIN_IMAGES="" +if [ -d "${CURRENT_DIR}/skins" ]; then + SKIN_IMAGES="$(find "${CURRENT_DIR}/skins" -type f -name "*.svg" | awk -F/ '{print $NF}' | cut -d"." -f1 | sort | uniq)" +fi + +####################################################### + +# optimize svg +function optimize_svg() { + SOURCE="$1" + + cp ${SOURCE} ${SOURCE}.tmp + scour \ + --remove-descriptive-elements \ + --enable-id-stripping \ + --enable-viewboxing \ + --enable-comment-stripping \ + --nindent=4 \ + --quiet \ + -i ${SOURCE}.tmp \ + -o ${SOURCE} + rm ${SOURCE}.tmp +} + +# build icons +function build_image() { + SOURCE="$1" + TARGET="$2" + + echo "Building ${TARGET}" + + if [ ! -f "${SOURCE}" ]; then + echo "Missing file: ${SOURCE}" + exit 1 + fi + + optimize_svg "${SOURCE}" + + mkdir -p "$(dirname "${TARGET}")" + + inkscape \ + --export-width=${ICON_SIZE} \ + --export-height=${ICON_SIZE} \ + --export-filename=${TARGET} \ + "${SOURCE}" + + optipng ${OPTIPNG_OPTIONS} "${TARGET}" +} + +function build_image_for_skin() { + SKIN_CODE="$1" + + # skin images + for SKIN_IMAGE in ${SKIN_IMAGES} + do + build_image ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_IMAGE}.svg ${ASSETS_DIR}/skins/${SKIN_CODE}_${SKIN_IMAGE}.png + done +} + +####################################################### + +# Delete existing generated images +if [ -d "${ASSETS_DIR}/ui" ]; then + find ${ASSETS_DIR}/ui -type f -name "*.png" -delete +fi +if [ -d "${ASSETS_DIR}/skins" ]; then + find ${ASSETS_DIR}/skins -type f -name "*.png" -delete +fi + +# build game images +for GAME_IMAGE in ${AVAILABLE_GAME_IMAGES} +do + build_image ${CURRENT_DIR}/images/${GAME_IMAGE}.svg ${ASSETS_DIR}/ui/${GAME_IMAGE}.png +done + +# build skins images +for SKIN in ${AVAILABLE_SKINS} +do + build_image_for_skin "${SKIN}" +done + diff --git a/icons/button_back.svg b/resources/ui/images/button_back.svg similarity index 100% rename from icons/button_back.svg rename to resources/ui/images/button_back.svg diff --git a/resources/ui/images/button_delete_saved_game.svg b/resources/ui/images/button_delete_saved_game.svg new file mode 100644 index 0000000000000000000000000000000000000000..ac7eefef476f761903fe781b8c86d0c94323550a --- /dev/null +++ b/resources/ui/images/button_delete_saved_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#ee7d49" stroke="#fff" stroke-width=".238"/><path d="m61.07 35.601-1.7399 27.837c-0.13442 2.1535-1.9205 3.8312-4.0781 3.8312h-16.84c-2.1576 0-3.9437-1.6777-4.0781-3.8312l-1.7399-27.837h-2.6176c-0.84621 0-1.5323-0.68613-1.5323-1.5323 0-0.84655 0.68613-1.5323 1.5323-1.5323h33.711c0.84621 0 1.5323 0.68578 1.5323 1.5323 0 0.84621-0.68613 1.5323-1.5323 1.5323zm-3.2617 0h-21.953l1.4715 26.674c0.05985 1.0829 0.95531 1.9305 2.0403 1.9305h14.929c1.085 0 1.9804-0.84757 2.0403-1.9305zm-10.977 3.0647c0.78977 0 1.4301 0.6403 1.4301 1.4301v19.614c0 0.78977-0.6403 1.4301-1.4301 1.4301s-1.4301-0.6403-1.4301-1.4301v-19.614c0-0.78977 0.6403-1.4301 1.4301-1.4301zm-6.1293 0c0.80004 0 1.4588 0.62935 1.495 1.4286l0.89647 19.719c0.03182 0.70016-0.50998 1.2933-1.2101 1.3255-0.01915 7.02e-4 -0.03831 1e-3 -0.05781 1e-3 -0.74462 0-1.3596-0.58215-1.4003-1.3261l-1.0757-19.719c-0.0407-0.74701 0.53188-1.3852 1.2786-1.4259 0.02462-0.0014 0.04926-2e-3 0.07388-2e-3zm12.259 0c0.74804 0 1.3541 0.60609 1.3541 1.3541 0 0.02462-3.28e-4 0.04926-0.0017 0.07388l-1.0703 19.618c-0.04379 0.80106-0.70597 1.4281-1.5081 1.4281-0.74804 0-1.3541-0.60609-1.3541-1.3541 0-0.02462 3.49e-4 -0.04925 0.0017-0.07388l1.0703-19.618c0.04379-0.80106 0.70597-1.4281 1.5081-1.4281zm-10.216-12.259h8.1728c2.2567 0 4.086 1.8293 4.086 4.086v2.0433h-16.344v-2.0433c0-2.2567 1.8293-4.086 4.086-4.086zm0.20453 3.0647c-0.67725 0-1.2259 0.54863-1.2259 1.2259v1.8388h10.215v-1.8388c0-0.67725-0.54863-1.2259-1.2259-1.2259z" fill="#fff" fill-rule="evenodd" stroke="#bd4812" stroke-width=".75383"/></svg> diff --git a/resources/ui/images/button_resume_game.svg b/resources/ui/images/button_resume_game.svg new file mode 100644 index 0000000000000000000000000000000000000000..6ad8b64202d0e70f898c16c520e756fe8a934add --- /dev/null +++ b/resources/ui/images/button_resume_game.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 93.665 93.676" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x=".44662" y=".89101" width="92.772" height="91.894" ry="11.689" fill="#49a1ee" stroke="#fff" stroke-width=".238"/><path d="m39.211 31.236c-0.84086-0.84489-2.9911-0.84489-2.9911 0v34.329c0 0.84594 2.1554 0.84594 2.9993 0l28.178-15.637c0.84392-0.84086 0.85812-2.2091 0.01623-3.053z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="6.1726"/><path d="m40.355 33.714c-0.71948-0.72294-2.5594-0.72294-2.5594 0v29.373c0 0.72383 1.8442 0.72383 2.5663 0l24.11-13.38c0.7221-0.71948 0.73426-1.8902 0.01389-2.6124z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.225"/><path d="m28.369 66.919v-37.591" fill="#105ca2" stroke="#105ca2" stroke-linecap="round" stroke-width="4.0337"/></svg> diff --git a/icons/button_start.svg b/resources/ui/images/button_start.svg similarity index 100% rename from icons/button_start.svg rename to resources/ui/images/button_start.svg diff --git a/icons/placeholder.svg b/resources/ui/images/placeholder.svg similarity index 100% rename from icons/placeholder.svg rename to resources/ui/images/placeholder.svg