diff --git a/android/app/build.gradle b/android/app/build.gradle index 1ca98bf74ac4f1f4b95fec382837dc31683a161f..8ae92ccdf2edbf3e07f07e356e92fc5d63297945 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.snake" 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/icons/level_easy.png b/assets/icons/level_easy.png deleted file mode 100644 index 6e53deb037691967b94968c8a546ec3ebb48d158..0000000000000000000000000000000000000000 Binary files a/assets/icons/level_easy.png and /dev/null differ diff --git a/assets/icons/level_hard.png b/assets/icons/level_hard.png deleted file mode 100644 index 7c32513e555f15eed877f8e3bcc5dd0ce06c038d..0000000000000000000000000000000000000000 Binary files a/assets/icons/level_hard.png and /dev/null differ diff --git a/assets/icons/level_nightmare.png b/assets/icons/level_nightmare.png deleted file mode 100644 index 006b51ea98b3d1331a70bf8219d4fa66a324a87b..0000000000000000000000000000000000000000 Binary files a/assets/icons/level_nightmare.png and /dev/null differ diff --git a/assets/icons/level_normal.png b/assets/icons/level_normal.png deleted file mode 100644 index f5f175d6286aac3f31a8525377dd3a9b342856f7..0000000000000000000000000000000000000000 Binary files a/assets/icons/level_normal.png and /dev/null differ diff --git a/assets/icons/skin_colors.png b/assets/icons/skin_colors.png deleted file mode 100644 index 77bff67518ce7920a1f5b51cd00127c368d84e1f..0000000000000000000000000000000000000000 Binary files a/assets/icons/skin_colors.png and /dev/null differ diff --git a/assets/icons/skin_retro.png b/assets/icons/skin_retro.png deleted file mode 100644 index d84a10bc6e64fc34bc686cd2607aeef8e64f4c4b..0000000000000000000000000000000000000000 Binary files a/assets/icons/skin_retro.png and /dev/null differ diff --git a/assets/translations/en.json b/assets/translations/en.json new file mode 100644 index 0000000000000000000000000000000000000000..c1512a695a6f4dce50e4b0f15dd8022db3d79c02 --- /dev/null +++ b/assets/translations/en.json @@ -0,0 +1,12 @@ +{ + "app_name": "Snake", + + "settings_title": "Settings", + "settings_label_theme": "Theme mode", + + "about_title": "Informations", + "about_content": "Snake", + "about_version": "Version: {version}", + + "": "" +} diff --git a/assets/translations/fr.json b/assets/translations/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..a3385d5aa978a400a411837f75fb7c83f02607a7 --- /dev/null +++ b/assets/translations/fr.json @@ -0,0 +1,12 @@ +{ + "app_name": "Serpent", + + "settings_title": "Réglages", + "settings_label_theme": "Thème de couleurs", + + "about_title": "Informations", + "about_content": "Serpent.", + "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/game_fail.png b/assets/ui/game_fail.png similarity index 100% rename from assets/icons/game_fail.png rename to assets/ui/game_fail.png diff --git a/assets/icons/game_win.png b/assets/ui/game_win.png similarity index 100% rename from assets/icons/game_win.png rename to assets/ui/game_win.png diff --git a/assets/ui/placeholder.png b/assets/ui/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..814df31be6ddc4275ebe4490c79365578dbef1f0 Binary files /dev/null and b/assets/ui/placeholder.png differ 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_game_icons.sh b/icons/build_game_icons.sh deleted file mode 100755 index 03fb6aee8918ca7f193a04fe22c90197ec124f81..0000000000000000000000000000000000000000 --- a/icons/build_game_icons.sh +++ /dev/null @@ -1,109 +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}")" - -OPTIPNG_OPTIONS="-preserve -quiet -o7" -ICON_SIZE=192 - -####################################################### - -# Game images -AVAILABLE_GAME_IMAGES=" - button_back - button_start - game_fail - game_win - level_easy - level_normal - level_hard - level_nightmare -" - -# Skins -AVAILABLE_SKINS=" - retro - colors -" - -# Images per skin -SKIN_IMAGES=" - empty - head - body - fruit -" - -####################################################### - -# 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}" - fi - - optimize_svg "${SOURCE}" - - inkscape \ - --export-width=${ICON_SIZE} \ - --export-height=${ICON_SIZE} \ - --export-filename=${TARGET} \ - ${SOURCE} - - optipng ${OPTIPNG_OPTIONS} ${TARGET} -} - -function build_icon_for_skin() { - SKIN_CODE="$1" - - # skin main image - build_icon ${CURRENT_DIR}/skin_${SKIN_CODE}.svg ${BASE_DIR}/assets/icons/skin_${SKIN_CODE}.png - - # skin images - for SKIN_IMAGE in ${SKIN_IMAGES} - do - build_icon ${CURRENT_DIR}/skins/${SKIN_CODE}/${SKIN_IMAGE}.svg ${BASE_DIR}/assets/skins/${SKIN_CODE}_${SKIN_IMAGE}.png - done -} - -mkdir -p ${BASE_DIR}/assets/icons -mkdir -p ${BASE_DIR}/assets/skins - -# build game images -for GAME_IMAGE in ${AVAILABLE_GAME_IMAGES} -do - build_icon ${CURRENT_DIR}/${GAME_IMAGE}.svg ${BASE_DIR}/assets/icons/${GAME_IMAGE}.png -done - -# build skins images -for SKIN in ${AVAILABLE_SKINS} -do - build_icon_for_skin "${SKIN}" -done diff --git a/icons/level_hard.svg b/icons/level_hard.svg deleted file mode 100644 index 0ae4e0dd6f1256d902c2b8f685b9c9649f752d10..0000000000000000000000000000000000000000 --- a/icons/level_hard.svg +++ /dev/null @@ -1,2 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="100" height="100" ry="0" fill="#db8616" stroke="#000" stroke-width="2"/></svg> diff --git a/icons/level_nightmare.svg b/icons/level_nightmare.svg deleted file mode 100644 index b25a22b548856aa50f71eff63ee3da9380440339..0000000000000000000000000000000000000000 --- a/icons/level_nightmare.svg +++ /dev/null @@ -1,2 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="100" height="100" ry="0" fill="#db1616" stroke="#000" stroke-width="2"/></svg> diff --git a/icons/level_normal.svg b/icons/level_normal.svg deleted file mode 100644 index a9533abd6476a37a9910b070c2f4183869143692..0000000000000000000000000000000000000000 --- a/icons/level_normal.svg +++ /dev/null @@ -1,2 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="100" height="100" ry="0" fill="#6a78de" stroke="#000" stroke-width="2"/></svg> diff --git a/icons/skin_colors.svg b/icons/skin_colors.svg deleted file mode 100644 index 40f592d1c157429220d7abbf1a1b4cb8c07ffec1..0000000000000000000000000000000000000000 --- a/icons/skin_colors.svg +++ /dev/null @@ -1,2 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="100" height="100" ry="0" fill="#be6ade" stroke="#000" stroke-width="2"/><rect x="8.5767" y="8.6213" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="22.27" y="8.6213" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="36.557" y="8.6213" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="50.844" y="8.6213" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="50.844" y="22.864" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="50.844" y="37.105" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="50.844" y="51.285" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="65.064" y="51.285" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="79.284" y="51.285" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="79.284" y="65.266" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="79.284" y="79.24" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="65.064" y="79.24" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="50.844" y="79.24" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="36.557" y="79.24" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="22.27" y="79.24" width="14.139" height="14.139" fill="#353ab0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="22.27" y="65.266" width="14.139" height="14.139" fill="#2d00e0" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><circle cx="29.34" cy="44.175" r="5.7602" fill="#209239" stroke="#1c6e24" stroke-linecap="round" stroke-width="4.2294"/></svg> diff --git a/icons/skin_retro.svg b/icons/skin_retro.svg deleted file mode 100644 index 08938c86d4af30275ab58c62bb9e7e91b686b6a4..0000000000000000000000000000000000000000 --- a/icons/skin_retro.svg +++ /dev/null @@ -1,2 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="100" height="100" ry="0" fill="#be6ade" stroke="#000" stroke-width="2"/><rect x="8.5767" y="8.6213" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="22.27" y="8.6213" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="36.557" y="8.6213" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="50.844" y="8.6213" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="50.844" y="22.864" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="50.844" y="37.105" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="50.844" y="51.285" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="65.064" y="51.285" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="79.284" y="51.285" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="79.284" y="65.266" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="79.284" y="79.24" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="65.064" y="79.24" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="50.844" y="79.24" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="36.557" y="79.24" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="22.27" y="79.24" width="14.139" height="14.139" fill="#727272" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><rect x="22.27" y="65.266" width="14.139" height="14.139" fill="#6f6f71" stroke="#fff" stroke-linecap="round" stroke-width="3.6305"/><circle cx="29.34" cy="44.175" r="5.7602" fill="#595959" stroke="#454545" stroke-linecap="round" stroke-width="4.2294"/></svg> diff --git a/lib/config/default_game_settings.dart b/lib/config/default_game_settings.dart new file mode 100644 index 0000000000000000000000000000000000000000..576c3a0bc6e2be68a134b3831bca4af19d72ea76 --- /dev/null +++ b/lib/config/default_game_settings.dart @@ -0,0 +1,57 @@ +import 'package:snake/utils/tools.dart'; + +class DefaultGameSettings { + // available game parameters codes + static const String parameterCodeLevel = 'level'; + static const String parameterCodeSize = 'size'; + static const List<String> availableParameters = [ + parameterCodeLevel, + parameterCodeSize, + ]; + + // level: available values + static const String levelValueEasy = 'easy'; + static const String levelValueMedium = 'medium'; + static const String levelValueHard = 'hard'; + static const String levelValueNightmare = 'nightmare'; + static const List<String> allowedLevelValues = [ + levelValueEasy, + levelValueMedium, + levelValueHard, + levelValueNightmare, + ]; + // level: default value + static const String defaultLevelValue = levelValueMedium; + + // size: available values + static const String sizeValueSmall = '10x10'; + static const String sizeValueMedium = '15x15'; + static const String sizeValueLarge = '20x20'; + static const String sizeValueExtraLarge = '30x30'; + static const List<String> allowedSizeValues = [ + sizeValueSmall, + sizeValueMedium, + sizeValueLarge, + sizeValueExtraLarge, + ]; + // size: default value + static const String defaultSizeValue = sizeValueMedium; + + // available values from parameter code + static List<String> getAvailableValues(String parameterCode) { + switch (parameterCode) { + case parameterCodeLevel: + return DefaultGameSettings.allowedLevelValues; + case parameterCodeSize: + return DefaultGameSettings.allowedSizeValues; + } + + 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..22727f4b2f5df60a25a0d4e91585872e631469f0 --- /dev/null +++ b/lib/config/default_global_settings.dart @@ -0,0 +1,35 @@ +import 'package:snake/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 skinValueColors = 'colors'; + static const String skinValueRetro = 'retro'; + static const List<String> allowedSkinValues = [ + skinValueColors, + skinValueRetro, + ]; + // skin: default value + static const String defaultSkinValue = skinValueRetro; + + // 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..a3b4e0840632274832ebf3c8c058acbe196d79f1 --- /dev/null +++ b/lib/config/menu.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:unicons/unicons.dart'; + +import 'package:snake/ui/screens/page_about.dart'; +import 'package:snake/ui/screens/page_game.dart'; +import 'package:snake/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..958997768ad717054c2c0d93aaf79b0039212b11 --- /dev/null +++ b/lib/cubit/game_cubit.dart @@ -0,0 +1,88 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:snake/models/game/game.dart'; +import 'package:snake/models/settings/settings_game.dart'; +import 'package:snake/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 + board: state.currentGame.board, + ); + // 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); + 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(); + } + + @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..c2bf0d4e42f381253d003eb8071b575aaff013dc --- /dev/null +++ b/lib/cubit/nav_cubit.dart @@ -0,0 +1,37 @@ +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:snake/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..dc4998070533e8929609319c51b69e6da41aff4e --- /dev/null +++ b/lib/cubit/settings_game_cubit.dart @@ -0,0 +1,72 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:snake/config/default_game_settings.dart'; +import 'package:snake/models/settings/settings_game.dart'; + +part 'settings_game_state.dart'; + +class GameSettingsCubit extends HydratedCubit<GameSettingsState> { + GameSettingsCubit() : super(GameSettingsState(settings: GameSettings.createDefault())); + + void setValues({ + String? level, + String? size, + }) { + emit( + GameSettingsState( + settings: GameSettings( + level: level ?? state.settings.level, + size: size ?? state.settings.size, + ), + ), + ); + } + + String getParameterValue(String code) { + switch (code) { + case DefaultGameSettings.parameterCodeLevel: + return GameSettings.getLevelValueFromUnsafe(state.settings.level); + case DefaultGameSettings.parameterCodeSize: + return GameSettings.getSizeValueFromUnsafe(state.settings.size); + } + + return ''; + } + + void setParameterValue(String code, String value) { + final String level = code == DefaultGameSettings.parameterCodeLevel + ? value + : getParameterValue(DefaultGameSettings.parameterCodeLevel); + final String size = code == DefaultGameSettings.parameterCodeSize + ? value + : getParameterValue(DefaultGameSettings.parameterCodeSize); + + setValues( + level: level, + size: size, + ); + } + + @override + GameSettingsState? fromJson(Map<String, dynamic> json) { + final String level = json[DefaultGameSettings.parameterCodeLevel] as String; + final String size = json[DefaultGameSettings.parameterCodeSize] as String; + + return GameSettingsState( + settings: GameSettings( + level: level, + size: size, + ), + ); + } + + @override + Map<String, dynamic>? toJson(GameSettingsState state) { + return <String, dynamic>{ + DefaultGameSettings.parameterCodeLevel: state.settings.level, + DefaultGameSettings.parameterCodeSize: state.settings.size, + }; + } +} 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..44a4de5c83a7affa2b6be24d0a99644cde8d7ba1 --- /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:snake/config/default_global_settings.dart'; +import 'package:snake/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/layout/game.dart b/lib/layout/game.dart deleted file mode 100644 index 0abc56eaf1fbc040002ac4211b7a2111e8ec62b1..0000000000000000000000000000000000000000 --- a/lib/layout/game.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:snake/provider/data.dart'; -import 'package:snake/utils/game_utils.dart'; - -class Game { - static Widget buildGameWidget(Data myProvider) { - final bool gameIsFinished = myProvider.isGameFinished(); - - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Expanded( - child: Text('❇️'), - ), - const SizedBox(height: 2), - SizedBox( - height: 150, - width: double.maxFinite, - child: gameIsFinished ? Game.buildEndGameMessage(myProvider) : const Text('❇️'), - ), - ], - ); - } - - static TextButton buildRestartGameButton(Data myProvider) { - return TextButton( - child: const Image( - image: AssetImage('assets/icons/button_back.png'), - fit: BoxFit.fill, - ), - onPressed: () => GameUtils.resetGame(myProvider), - ); - } - - static Widget buildEndGameMessage(Data myProvider) { - String decorationImageAssetName = ''; - if (myProvider.gameWon) { - decorationImageAssetName = 'assets/icons/game_win.png'; - } else { - decorationImageAssetName = 'assets/icons/game_fail.png'; - } - - Image decorationImage = Image( - image: AssetImage(decorationImageAssetName), - fit: BoxFit.fill, - ); - - return Container( - margin: const EdgeInsets.all(2), - padding: const EdgeInsets.all(2), - child: Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - children: [ - TableRow( - children: [ - Column(children: [decorationImage]), - Column(children: [buildRestartGameButton(myProvider)]), - Column(children: [decorationImage]), - ], - ), - ], - ), - ); - } -} diff --git a/lib/layout/parameters.dart b/lib/layout/parameters.dart deleted file mode 100644 index 42dcd062130a7a40aafef0ba511e218bc77eeb5e..0000000000000000000000000000000000000000 --- a/lib/layout/parameters.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:snake/provider/data.dart'; -import 'package:snake/utils/game_utils.dart'; - -class Parameters { - static Container buildParametersSelector(Data myProvider) { - return Container( - padding: const EdgeInsets.all(2), - margin: const EdgeInsets.all(2), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Parameters.buildParameterSelector(myProvider, 'level'), - const SizedBox(height: 5), - Parameters.buildParameterSelector(myProvider, 'skin'), - const SizedBox(height: 5), - Parameters.buildStartGameButton(myProvider), - ], - ), - ); - } - - static Container buildStartGameButton(Data myProvider) { - Column decorationImage = const Column( - children: [ - Image( - image: AssetImage('assets/icons/game_win.png'), - fit: BoxFit.fill, - ), - ], - ); - - return Container( - margin: const EdgeInsets.all(2), - padding: const EdgeInsets.all(2), - child: Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - children: [ - TableRow( - children: [ - decorationImage, - Column( - children: [ - TextButton( - child: const Image( - image: AssetImage('assets/icons/button_start.png'), - fit: BoxFit.fill, - ), - onPressed: () => GameUtils.startGame(myProvider), - ), - ], - ), - decorationImage, - ], - ), - ], - ), - ); - } - - static Widget buildParameterSelector(Data myProvider, String parameterCode) { - List availableValues = myProvider.getParameterAvailableValues(parameterCode); - - if (availableValues.length == 1) { - return const SizedBox(height: 1); - } - - return Table( - defaultColumnWidth: const IntrinsicColumnWidth(), - children: [ - TableRow( - children: [ - for (var index = 0; index < availableValues.length; index++) - Column( - children: [ - _buildParameterButton(myProvider, parameterCode, availableValues[index]) - ], - ), - ], - ), - ], - ); - } - - static TextButton _buildParameterButton( - Data myProvider, String parameterCode, String parameterValue) { - String currentValue = myProvider.getParameterValue(parameterCode).toString(); - - bool isActive = (parameterValue == currentValue); - String imageAsset = 'assets/icons/${parameterCode}_$parameterValue.png'; - - return TextButton( - child: Container( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: isActive ? Colors.blue : Colors.white, - width: 10, - ), - ), - child: Image( - image: AssetImage(imageAsset), - fit: BoxFit.fill, - ), - ), - onPressed: () => myProvider.setParameterValue(parameterCode, parameterValue), - ); - } -} diff --git a/lib/main.dart b/lib/main.dart index d38f70afcab0c10d5b820f46eef24f2119de14d5..90804d73602e6237bd35cdbb228e2feffd8fdddc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,15 +1,43 @@ +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:snake/provider/data.dart'; -import 'package:snake/screens/home.dart'; +import 'package:snake/config/default_global_settings.dart'; +import 'package:snake/config/theme.dart'; +import 'package:snake/cubit/game_cubit.dart'; +import 'package:snake/cubit/nav_cubit.dart'; +import 'package:snake/cubit/settings_game_cubit.dart'; +import 'package:snake/cubit/settings_global_cubit.dart'; +import 'package:snake/cubit/theme_cubit.dart'; +import 'package:snake/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,25 +45,71 @@ 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( - debugShowCheckedModeBanner: false, - 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: 'Snake', + 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', + 'game_fail', + 'game_win', + 'placeholder', + ]; + + for (String image in gameImages) { + assets.add('assets/ui/$image.png'); + } + + final List<String> skinImages = [ + 'body', + 'empty', + 'fruit', + 'head', + ]; + + for (String skin in DefaultGlobalSettings.allowedSkinValues) { + for (String image in skinImages) { + assets.add('assets/skins/${skin}_$image.png'); + } + } + + return assets; + } } diff --git a/lib/models/game/board.dart b/lib/models/game/board.dart new file mode 100644 index 0000000000000000000000000000000000000000..f176eb059243081331100865ab3acfb412678837 --- /dev/null +++ b/lib/models/game/board.dart @@ -0,0 +1,207 @@ +import 'dart:math'; + +import 'package:snake/models/game/cell.dart'; +import 'package:snake/models/game/cell_location.dart'; +import 'package:snake/utils/tools.dart'; + +typedef BoardCells = List<List<Cell>>; + +class Board { + Board({ + required this.cells, + }); + + BoardCells cells = const []; + + factory Board.createEmpty() { + return Board( + cells: [], + ); + } + + factory Board.createNew({ + required BoardCells cells, + }) { + return Board( + cells: cells, + ); + } + + Cell get(CellLocation location) { + if (location.row < cells.length) { + if (location.col < cells[location.row].length) { + return cells[location.row][location.col]; + } + } + + return Cell.none; + } + + void set(CellLocation location, Cell cell) { + cells[location.row][location.col] = cell; + } + + BoardCells copyCells() { + final BoardCells copiedGrid = []; + for (int rowIndex = 0; rowIndex < cells.length; rowIndex++) { + final List<Cell> row = []; + for (int colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) { + row.add(Cell( + location: CellLocation.go(rowIndex, colIndex), + value: cells[rowIndex][colIndex].value, + isFixed: false, + )); + } + copiedGrid.add(row); + } + + return copiedGrid; + } + + BoardCells getSolvedGrid() { + final Board tmpBoard = Board(cells: copyCells()); + + do { + final List<List<int>> cellsWithUniqueAvailableValue = + tmpBoard.getEmptyCellsWithUniqueAvailableValue(); + if (cellsWithUniqueAvailableValue.isEmpty) { + break; + } + + for (int i = 0; i < cellsWithUniqueAvailableValue.length; i++) { + final int row = cellsWithUniqueAvailableValue[i][0]; + final int col = cellsWithUniqueAvailableValue[i][1]; + final int value = cellsWithUniqueAvailableValue[i][2]; + + tmpBoard.cells[row][col] = Cell( + location: CellLocation.go(row, col), + value: value, + isFixed: tmpBoard.cells[row][col].isFixed, + ); + } + } while (true); + + return tmpBoard.cells; + } + + List<List<int>> getEmptyCellsWithUniqueAvailableValue() { + List<List<int>> candidateCells = []; + + final int boardSize = cells.length; + + for (int row = 0; row < boardSize; row++) { + for (int col = 0; col < boardSize; col++) { + if (cells[row][col].value == 0) { + int allowedValuesCount = 0; + int candidateValue = 0; + for (int value = 1; value <= boardSize; value++) { + if (isValueAllowed(CellLocation.go(row, col), value)) { + candidateValue = value; + allowedValuesCount++; + } + } + if (allowedValuesCount == 1) { + candidateCells.add([row, col, candidateValue]); + } + } + } + } + + return candidateCells; + } + + bool isValueAllowed(CellLocation? candidateLocation, int candidateValue) { + if ((candidateLocation == null) || (candidateValue == 0)) { + return true; + } + + final int boardSize = cells.length; + + // check lines does not contains a value twice + for (int row = 0; row < boardSize; row++) { + final List<int> values = []; + for (int col = 0; col < boardSize; col++) { + int value = cells[row][col].value; + if (row == candidateLocation.row && col == candidateLocation.col) { + value = candidateValue; + } + if (value != 0) { + values.add(value); + } + } + final List<int> distinctValues = values.toSet().toList(); + if (values.length != distinctValues.length) { + return false; + } + } + + // check columns does not contains a value twice + for (int col = 0; col < boardSize; col++) { + final List<int> values = []; + for (int row = 0; row < boardSize; row++) { + int value = cells[row][col].value; + if (row == candidateLocation.row && col == candidateLocation.col) { + value = candidateValue; + } + if (value != 0) { + values.add(value); + } + } + final List<int> distinctValues = values.toSet().toList(); + if (values.length != distinctValues.length) { + return false; + } + } + + // check blocks does not contains a value twice + final int blockSizeVertical = sqrt(cells.length).toInt(); + final int blockSizeHorizontal = cells.length ~/ blockSizeVertical; + + final int horizontalBlocksCount = blockSizeVertical; + final int verticalBlocksCount = blockSizeHorizontal; + for (int blockRow = 0; blockRow < verticalBlocksCount; blockRow++) { + for (int blockCol = 0; blockCol < horizontalBlocksCount; blockCol++) { + final List<int> values = []; + + for (int rowInBlock = 0; rowInBlock < blockSizeVertical; rowInBlock++) { + for (int colInBlock = 0; colInBlock < blockSizeHorizontal; colInBlock++) { + final int row = (blockRow * blockSizeVertical) + rowInBlock; + final int col = (blockCol * blockSizeHorizontal) + colInBlock; + int value = cells[row][col].value; + if (row == candidateLocation.row && col == candidateLocation.col) { + value = candidateValue; + } + if (value != 0) { + values.add(value); + } + } + } + + final List<int> distinctValues = values.toSet().toList(); + if (values.length != distinctValues.length) { + return false; + } + } + } + + return true; + } + + void dump() { + printlog(''); + printlog('$Board:'); + printlog(' cells: $cells'); + printlog(''); + } + + @override + String toString() { + return '$Board(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + 'cells': cells, + }; + } +} diff --git a/lib/models/game/cell.dart b/lib/models/game/cell.dart new file mode 100644 index 0000000000000000000000000000000000000000..7d6518bf812e2eddf409fdbc10797d10aab3d019 --- /dev/null +++ b/lib/models/game/cell.dart @@ -0,0 +1,41 @@ +import 'package:snake/models/game/cell_location.dart'; +import 'package:snake/utils/tools.dart'; + +class Cell { + const Cell({ + required this.location, + required this.value, + required this.isFixed, + }); + + final CellLocation location; + final int value; + final bool isFixed; + + static Cell none = Cell( + location: CellLocation.go(0, 0), + value: 0, + isFixed: true, + ); + + void dump() { + printlog('$Cell:'); + printlog(' location: $location'); + printlog(' value: $value'); + printlog(' isFixed: $isFixed'); + printlog(''); + } + + @override + String toString() { + return '$Cell(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + 'location': location.toJson(), + 'value': value, + 'isFixed': isFixed, + }; + } +} diff --git a/lib/models/game/cell_location.dart b/lib/models/game/cell_location.dart new file mode 100644 index 0000000000000000000000000000000000000000..b9e0a27ebd6778985fe49afd1a61db2528261c38 --- /dev/null +++ b/lib/models/game/cell_location.dart @@ -0,0 +1,34 @@ +import 'package:snake/utils/tools.dart'; + +class CellLocation { + final int col; + final int row; + + CellLocation({ + required this.col, + required this.row, + }); + + factory CellLocation.go(int row, int col) { + return CellLocation(col: col, row: row); + } + + void dump() { + printlog('$CellLocation:'); + printlog(' row: $row'); + printlog(' col: $col'); + printlog(''); + } + + @override + String toString() { + return '$CellLocation(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + 'row': row, + 'col': col, + }; + } +} diff --git a/lib/models/game/game.dart b/lib/models/game/game.dart new file mode 100644 index 0000000000000000000000000000000000000000..6df44d9aa407b37b5e813c6ca617533ba16919d8 --- /dev/null +++ b/lib/models/game/game.dart @@ -0,0 +1,135 @@ +import 'package:snake/models/game/board.dart'; +import 'package:snake/models/settings/settings_game.dart'; +import 'package:snake/models/settings/settings_global.dart'; +import 'package:snake/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 + required this.board, + + // Game data + this.score = 0, + this.gameWon = false, + }); + + // Settings + final GameSettings gameSettings; + final GlobalSettings globalSettings; + + // State + bool isRunning; + bool isStarted; + bool isFinished; + bool animationInProgress; + + // Base data + final Board board; + + // Game data + int score; + bool gameWon; + + factory Game.createEmpty() { + return Game( + // Settings + gameSettings: GameSettings.createDefault(), + globalSettings: GlobalSettings.createDefault(), + // Base data + board: Board.createEmpty(), + ); + } + + factory Game.createNew({ + GameSettings? gameSettings, + GlobalSettings? globalSettings, + }) { + final GameSettings newGameSettings = gameSettings ?? GameSettings.createDefault(); + final GlobalSettings newGlobalSettings = globalSettings ?? GlobalSettings.createDefault(); + + final Board board = Board.createEmpty(); + + return Game( + // Settings + gameSettings: newGameSettings, + globalSettings: newGlobalSettings, + // State + isRunning: true, + // Base data + board: board, + ); + } + + 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('board:'); + board.dump(); + printGrid(); + printlog(' Game data'); + printlog(' score: $score'); + printlog(''); + } + + printGrid() { + final BoardCells cells = board.cells; + + const String stringValues = '0123456789ABCDEFG'; + printlog(''); + printlog('-------'); + for (int rowIndex = 0; rowIndex < cells.length; rowIndex++) { + String row = ''; + for (int colIndex = 0; colIndex < cells[rowIndex].length; colIndex++) { + row += stringValues[cells[rowIndex][colIndex].value]; + } + printlog(row); + } + printlog('-------'); + 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 + 'board': board.toJson(), + // Game data + 'score': score, + }; + } +} diff --git a/lib/models/settings/settings_game.dart b/lib/models/settings/settings_game.dart new file mode 100644 index 0000000000000000000000000000000000000000..52184416988898c1e71b7603f4c2c76d8b44c21c --- /dev/null +++ b/lib/models/settings/settings_game.dart @@ -0,0 +1,54 @@ +import 'package:snake/config/default_game_settings.dart'; +import 'package:snake/utils/tools.dart'; + +class GameSettings { + final String level; + final String size; + + GameSettings({ + required this.level, + required this.size, + }); + + static String getLevelValueFromUnsafe(String level) { + if (DefaultGameSettings.allowedLevelValues.contains(level)) { + return level; + } + + return DefaultGameSettings.defaultLevelValue; + } + + static String getSizeValueFromUnsafe(String size) { + if (DefaultGameSettings.allowedSizeValues.contains(size)) { + return size; + } + + return DefaultGameSettings.defaultSizeValue; + } + + factory GameSettings.createDefault() { + return GameSettings( + level: DefaultGameSettings.defaultLevelValue, + size: DefaultGameSettings.defaultSizeValue, + ); + } + + void dump() { + printlog('$GameSettings:'); + printlog(' ${DefaultGameSettings.parameterCodeLevel}: $level'); + printlog(' ${DefaultGameSettings.parameterCodeSize}: $size'); + printlog(''); + } + + @override + String toString() { + return '$GameSettings(${toJson()})'; + } + + Map<String, dynamic>? toJson() { + return <String, dynamic>{ + DefaultGameSettings.parameterCodeLevel: level, + DefaultGameSettings.parameterCodeSize: size, + }; + } +} diff --git a/lib/models/settings/settings_global.dart b/lib/models/settings/settings_global.dart new file mode 100644 index 0000000000000000000000000000000000000000..fd72e4b6a0ddce0d9fb4717ef56225dd2c1a110f --- /dev/null +++ b/lib/models/settings/settings_global.dart @@ -0,0 +1,41 @@ +import 'package:snake/config/default_global_settings.dart'; +import 'package:snake/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 4b89660e4326593b37da3a7d3eab7efad14ccd39..0000000000000000000000000000000000000000 --- a/lib/provider/data.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class Data extends ChangeNotifier { - // Configuration available values - final List<String> _availableLevelValues = ['easy', 'normal', 'hard', 'nightmare']; - final List<String> _availableSkinValues = ['colors', 'retro']; - - List<String> get availableLevelValues => _availableLevelValues; - List<String> get availableSkinValues => _availableSkinValues; - - // Application default configuration - String _level = ''; - final String _levelDefault = 'normal'; - String _skin = ''; - final String _skinDefault = 'colors'; - - // Game data - bool _gameIsRunning = false; - bool _gameWon = false; - - String get level => _level; - void updateLevel(String level) { - _level = level; - notifyListeners(); - } - - String get skin => _skin; - void updateSkin(String skin) { - _skin = skin; - notifyListeners(); - } - - getParameterValue(String parameterCode) { - switch (parameterCode) { - case 'level': - { - return _level; - } - case 'skin': - { - return _skin; - } - } - } - - List<String> getParameterAvailableValues(String parameterCode) { - switch (parameterCode) { - case 'level': - { - return _availableLevelValues; - } - case 'skin': - { - return _availableSkinValues; - } - } - return []; - } - - setParameterValue(String parameterCode, String parameterValue) async { - switch (parameterCode) { - case 'level': - { - updateLevel(parameterValue); - } - break; - case 'skin': - { - updateSkin(parameterValue); - } - break; - } - final prefs = await SharedPreferences.getInstance(); - prefs.setString(parameterCode, parameterValue); - } - - void initParametersValues() async { - final prefs = await SharedPreferences.getInstance(); - setParameterValue('level', prefs.getString('level') ?? _levelDefault); - setParameterValue('skin', prefs.getString('skin') ?? _skinDefault); - } - - bool get gameIsRunning => _gameIsRunning; - void updateGameIsRunning(bool gameIsRunning) { - _gameIsRunning = gameIsRunning; - notifyListeners(); - } - - bool isGameFinished() { - return false; - } - - bool get gameWon => _gameWon; - - void resetGame() { - _gameIsRunning = false; - _gameWon = false; - notifyListeners(); - } -} diff --git a/lib/screens/home.dart b/lib/screens/home.dart deleted file mode 100644 index 5379f48541d4e13229175352d15201f937efa636..0000000000000000000000000000000000000000 --- a/lib/screens/home.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:overlay_support/overlay_support.dart'; - -import 'package:snake/layout/game.dart'; -import 'package:snake/layout/parameters.dart'; -import 'package:snake/provider/data.dart'; -import 'package:snake/utils/game_utils.dart'; - -class Home extends StatefulWidget { - static const String id = 'home'; - - const Home({super.key}); - - @override - HomeState createState() => HomeState(); -} - -class HomeState extends State<Home> { - @override - void initState() { - super.initState(); - - Data myProvider = Provider.of<Data>(context, listen: false); - myProvider.initParametersValues(); - } - - @override - Widget build(BuildContext context) { - Data myProvider = Provider.of<Data>(context); - - List<Widget> menuActions = []; - - if (myProvider.gameIsRunning) { - menuActions = [ - TextButton( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - border: Border.all( - color: Colors.blue, - width: 4, - ), - ), - margin: const EdgeInsets.all(8), - child: const Image( - image: AssetImage('assets/icons/button_back.png'), - fit: BoxFit.fill, - ), - ), - onPressed: () => toast('Long press to quit game...'), - onLongPress: () => GameUtils.resetGame(myProvider), - ), - ]; - } - - return Scaffold( - appBar: AppBar( - actions: menuActions, - ), - body: SafeArea( - child: Center( - child: myProvider.gameIsRunning - ? Game.buildGameWidget(myProvider) - : Parameters.buildParametersSelector(myProvider), - ), - ), - ); - } -} diff --git a/lib/ui/game/game_bottom.dart b/lib/ui/game/game_bottom.dart new file mode 100644 index 0000000000000000000000000000000000000000..41b36cb67479bbf0a3afac9eded6d6735a9072ff --- /dev/null +++ b/lib/ui/game/game_bottom.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:snake/cubit/game_cubit.dart'; +import 'package:snake/models/game/game.dart'; + +class GameBottomWidget extends StatelessWidget { + const GameBottomWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + return Text(currentGame.score.toString()); + }, + ); + } +} diff --git a/lib/ui/game/game_end.dart b/lib/ui/game/game_end.dart new file mode 100644 index 0000000000000000000000000000000000000000..9eca613e845b9403259cb2eef44712d02787cec8 --- /dev/null +++ b/lib/ui/game/game_end.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:snake/cubit/game_cubit.dart'; +import 'package:snake/models/game/game.dart'; +import 'package:snake/ui/widgets/actions/button_game_quit.dart'; + +class GameEndWidget extends StatelessWidget { + const GameEndWidget({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + final Image decorationImage = Image( + image: AssetImage( + currentGame.gameWon ? 'assets/ui/game_win.png' : 'assets/ui/game_fail.png'), + fit: BoxFit.fill, + ); + + return Container( + margin: const EdgeInsets.all(2), + padding: const EdgeInsets.all(2), + child: Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + children: [ + TableRow( + children: [ + Column( + children: [decorationImage], + ), + Column( + children: [ + currentGame.animationInProgress == true + ? decorationImage + : const QuitGameButton() + ], + ), + Column( + children: [decorationImage], + ), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/game/game_top.dart b/lib/ui/game/game_top.dart new file mode 100644 index 0000000000000000000000000000000000000000..78f1b3374ca425770816a4a9be8e7703ff89b83a --- /dev/null +++ b/lib/ui/game/game_top.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +import 'package:snake/ui/widgets/indicators/indicator_score.dart'; + +class GameTopWidget extends StatelessWidget { + const GameTopWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const ScoreIndicator(); + } +} 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..fa64b90a133ba57f7bbb9d8b5335857df458af2f --- /dev/null +++ b/lib/ui/helpers/outlined_text_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:snake/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..4419b3749a028ce5c9cc26159a1c081043ffb2be --- /dev/null +++ b/lib/ui/layouts/game_layout.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:snake/cubit/game_cubit.dart'; +import 'package:snake/models/game/game.dart'; +import 'package:snake/ui/game/game_bottom.dart'; +import 'package:snake/ui/game/game_end.dart'; +import 'package:snake/ui/game/game_top.dart'; +import 'package:snake/ui/widgets/game/game_board.dart'; + +class GameLayout extends StatelessWidget { + const GameLayout({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + return Container( + alignment: AlignmentDirectional.topCenter, + padding: const EdgeInsets.all(4), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const GameTopWidget(), + const SizedBox(height: 8), + const GameBoardWidget(), + const SizedBox(height: 8), + const GameBottomWidget(), + const Expanded(child: SizedBox.shrink()), + currentGame.isFinished ? const GameEndWidget() : const SizedBox.shrink(), + ], + ), + ); + }, + ); + } +} diff --git a/lib/ui/layouts/parameters_layout.dart b/lib/ui/layouts/parameters_layout.dart new file mode 100644 index 0000000000000000000000000000000000000000..29a66dd936ddc8f721edc93b44ce99baf553c966 --- /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:snake/config/default_game_settings.dart'; +import 'package:snake/config/default_global_settings.dart'; +import 'package:snake/cubit/settings_game_cubit.dart'; +import 'package:snake/cubit/settings_global_cubit.dart'; +import 'package:snake/ui/parameters/parameter_image.dart'; +import 'package:snake/ui/parameters/parameter_painter.dart'; +import 'package:snake/ui/widgets/actions/button_delete_saved_game.dart'; +import 'package:snake/ui/widgets/actions/button_game_start_new.dart'; +import 'package:snake/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..d7fec53499adb774034189d6f5854447d1cd8afe --- /dev/null +++ b/lib/ui/parameters/parameter_painter.dart @@ -0,0 +1,215 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:snake/config/default_game_settings.dart'; +import 'package:snake/models/settings/settings_game.dart'; +import 'package:snake/models/settings/settings_global.dart'; +import 'package:snake/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) { + case DefaultGameSettings.parameterCodeLevel: + paintLevelParameterItem(value, canvas, canvasSize); + break; + case DefaultGameSettings.parameterCodeSize: + paintSizeParameterItem(value, canvas, canvasSize); + break; + 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, + ), + ); + } + + void paintLevelParameterItem( + final String value, + final Canvas canvas, + final double size, + ) { + const text = 'X'; + + Color backgroundColor = Colors.grey; + + switch (value) { + case DefaultGameSettings.levelValueEasy: + backgroundColor = Colors.green; + break; + case DefaultGameSettings.levelValueMedium: + backgroundColor = Colors.orange; + break; + case DefaultGameSettings.levelValueHard: + backgroundColor = Colors.red; + break; + case DefaultGameSettings.levelValueNightmare: + backgroundColor = Colors.grey; + break; + default: + printlog('Wrong value for level parameter value: $value'); + } + + final paint = Paint(); + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 3 / 100 * size; + + // Colored background + paint.color = backgroundColor; + paint.style = PaintingStyle.fill; + canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint); + + // centered text value + final textSpan = TextSpan( + text: text, + style: TextStyle( + color: Colors.black, + fontSize: size / 2.6, + 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, + ), + ); + } + + void paintSizeParameterItem( + final String value, + final Canvas canvas, + final double size, + ) { + const text = 'o'; + + Color backgroundColor = Colors.grey; + + switch (value) { + case DefaultGameSettings.sizeValueSmall: + backgroundColor = Colors.green; + break; + case DefaultGameSettings.sizeValueMedium: + backgroundColor = Colors.orange; + break; + case DefaultGameSettings.sizeValueLarge: + backgroundColor = Colors.red; + break; + case DefaultGameSettings.sizeValueExtraLarge: + backgroundColor = Colors.grey; + break; + default: + printlog('Wrong value for size parameter value: $value'); + } + + final paint = Paint(); + paint.strokeJoin = StrokeJoin.round; + paint.strokeWidth = 3; + + // Colored background + paint.color = backgroundColor; + paint.style = PaintingStyle.fill; + canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint); + + // centered text value + final textSpan = TextSpan( + text: text, + style: TextStyle( + color: Colors.black, + fontSize: size / 2.6, + 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..c1c0bb85151d3756132b31ebad2855dd9712e2ad --- /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:snake/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..a02eab6d4ff560cb55845b913e7f0c625ccb3c10 --- /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:snake/cubit/game_cubit.dart'; +import 'package:snake/models/game/game.dart'; +import 'package:snake/ui/layouts/game_layout.dart'; +import 'package:snake/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..ca42633568e1970359d6bc34c41f6ce94657adda --- /dev/null +++ b/lib/ui/screens/page_settings.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import 'package:snake/ui/helpers/app_titles.dart'; +import 'package:snake/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..5149ea7801efba1d91d358901b8febf9047b9c44 --- /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:snake/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..09aa7a4ff773f6920ddf8cc1418a76cb732cdcb9 --- /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:snake/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..e713d8d65141ac3ae0270acf84a37bb6ec898ee0 --- /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:snake/config/menu.dart'; +import 'package:snake/cubit/nav_cubit.dart'; +import 'package:snake/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..1b7087a3ba16765d84edce1885e90a9424cd3c0f --- /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:snake/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..8bdfdc9d83e5e69b241d82e9e866bca83dc6f096 --- /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:snake/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..208426d0facccc9a30088101472fba2e12a4596d --- /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:snake/cubit/game_cubit.dart'; +import 'package:snake/cubit/settings_game_cubit.dart'; +import 'package:snake/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..1a970effdd73aadf0ba0fc845af16d9f247155ec --- /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:snake/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/ui/widgets/game/game_board.dart b/lib/ui/widgets/game/game_board.dart new file mode 100644 index 0000000000000000000000000000000000000000..d79acad303685eb1db3161f2166e8322f52fba73 --- /dev/null +++ b/lib/ui/widgets/game/game_board.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:snake/cubit/game_cubit.dart'; +import 'package:snake/models/game/game.dart'; + +class GameBoardWidget extends StatelessWidget { + const GameBoardWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + final Game currentGame = gameState.currentGame; + + return Text(currentGame.toString()); + }, + ), + ); + } +} diff --git a/lib/ui/widgets/global_app_bar.dart b/lib/ui/widgets/global_app_bar.dart new file mode 100644 index 0000000000000000000000000000000000000000..ccd980f7076850be9804cac4c2af6e2ab0686049 --- /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:snake/config/menu.dart'; +import 'package:snake/cubit/game_cubit.dart'; +import 'package:snake/cubit/nav_cubit.dart'; +import 'package:snake/models/game/game.dart'; +import 'package:snake/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/ui/widgets/indicators/indicator_score.dart b/lib/ui/widgets/indicators/indicator_score.dart new file mode 100644 index 0000000000000000000000000000000000000000..26ed0840fea97a1d8ea433abd8554e915e9d9b8a --- /dev/null +++ b/lib/ui/widgets/indicators/indicator_score.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:snake/cubit/game_cubit.dart'; +import 'package:snake/ui/helpers/outlined_text_widget.dart'; +import 'package:snake/utils/color_extensions.dart'; + +class ScoreIndicator extends StatelessWidget { + const ScoreIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder<GameCubit, GameState>( + builder: (BuildContext context, GameState gameState) { + const Color baseColor = Color.fromARGB(255, 218, 218, 218); + final Color outlineColor = baseColor.darken(); + + return OutlinedText( + text: gameState.currentGame.score.toString(), + fontSize: 70, + textColor: baseColor, + outlineColor: outlineColor, + ); + }, + ); + } +} 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 53e6dbc349e9b78a1681f491b18d7b2193037436..0000000000000000000000000000000000000000 --- a/lib/utils/game_utils.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:snake/provider/data.dart'; -import 'package:snake/utils/tools.dart'; - -class GameUtils { - static Future<void> resetGame(Data myProvider) async { - myProvider.updateGameIsRunning(false); - } - - static Future<void> startGame(Data myProvider) async { - printlog('Starting game'); - printlog('- level: ${myProvider.level}'); - - myProvider.resetGame(); - - 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 b8f11d28bb26030dbe86ddaf315ff83366a0bf92..15a9c092fcd74ff82deac59f6205d944d4a66f90 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,22 +1,49 @@ name: snake description: snake 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/skins/ + - assets/ui/ + - assets/translations/ + + fonts: + - 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_application_icons.sh b/resources/app/build_application_resources.sh similarity index 98% rename from icons/build_application_icons.sh rename to resources/app/build_application_resources.sh index 27dbe2647fe4e6d562fbd99451716d1b7d448570..6d67b8f4f9eca701d1aed7331ef41dfb0bd44f20 100755 --- a/icons/build_application_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/resources/build_resources.sh b/resources/build_resources.sh new file mode 100755 index 0000000000000000000000000000000000000000..659697a1c043cfe1c7654635cfaec3e4a0ff8a1a --- /dev/null +++ b/resources/build_resources.sh @@ -0,0 +1,7 @@ +#! /bin/bash + +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + +${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/game_fail.svg b/resources/ui/images/game_fail.svg similarity index 100% rename from icons/game_fail.svg rename to resources/ui/images/game_fail.svg diff --git a/icons/game_win.svg b/resources/ui/images/game_win.svg similarity index 100% rename from icons/game_win.svg rename to resources/ui/images/game_win.svg diff --git a/icons/level_easy.svg b/resources/ui/images/placeholder.svg similarity index 60% rename from icons/level_easy.svg rename to resources/ui/images/placeholder.svg index b6505a4bc70481202c814a3e3bbe3c992bc9c33c..23ace81fbb82a8409cc0710c0f7bddd6381f7256 100644 --- a/icons/level_easy.svg +++ b/resources/ui/images/placeholder.svg @@ -1,2 +1,2 @@ <?xml version="1.0" encoding="UTF-8"?> -<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="100" height="100" ry="0" fill="#6ade98" stroke="#000" stroke-width="2"/></svg> +<svg enable-background="new 0 0 100 100" version="1.1" viewBox="0 0 102 102" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"/> diff --git a/icons/skins/colors/body.svg b/resources/ui/skins/colors/body.svg similarity index 100% rename from icons/skins/colors/body.svg rename to resources/ui/skins/colors/body.svg diff --git a/icons/skins/colors/empty.svg b/resources/ui/skins/colors/empty.svg similarity index 100% rename from icons/skins/colors/empty.svg rename to resources/ui/skins/colors/empty.svg diff --git a/icons/skins/colors/fruit.svg b/resources/ui/skins/colors/fruit.svg similarity index 100% rename from icons/skins/colors/fruit.svg rename to resources/ui/skins/colors/fruit.svg diff --git a/icons/skins/colors/head.svg b/resources/ui/skins/colors/head.svg similarity index 100% rename from icons/skins/colors/head.svg rename to resources/ui/skins/colors/head.svg diff --git a/icons/skins/retro/body.svg b/resources/ui/skins/retro/body.svg similarity index 100% rename from icons/skins/retro/body.svg rename to resources/ui/skins/retro/body.svg diff --git a/icons/skins/retro/empty.svg b/resources/ui/skins/retro/empty.svg similarity index 100% rename from icons/skins/retro/empty.svg rename to resources/ui/skins/retro/empty.svg diff --git a/icons/skins/retro/fruit.svg b/resources/ui/skins/retro/fruit.svg similarity index 100% rename from icons/skins/retro/fruit.svg rename to resources/ui/skins/retro/fruit.svg diff --git a/icons/skins/retro/head.svg b/resources/ui/skins/retro/head.svg similarity index 100% rename from icons/skins/retro/head.svg rename to resources/ui/skins/retro/head.svg