Skip to content
Snippets Groups Projects
Commit cc298e0b authored by Benoît Harrault's avatar Benoît Harrault
Browse files

Improve game architecture/conception

parent cb7131a6
No related branches found
No related tags found
1 merge request!40Resolve "Improve game architecture"
Pipeline #5634 passed
Showing
with 322 additions and 179 deletions
......@@ -37,7 +37,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 33
compileSdkVersion 34
namespace "org.benoitharrault.petitbac"
defaultConfig {
......
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
app.versionName=1.2.30
app.versionCode=36
app.versionName=1.2.31
app.versionCode=37
{
"categories": [
"Pays",
"Prénom fille",
"Prénom garçon",
"Animal",
"Métier",
"Villes",
"Dessin animé",
"Film",
"Auteur de littérature",
"Acteur ou actrice",
"Chanteur ou chanteuse",
"Chose ou objet",
"Fruit ou légume",
"Couleur",
"Marque",
"Moyen de transport",
"Outil",
"Capitale",
"Instrument de musique",
"Boisson",
"Fleur",
"Plat",
"Personnage historique",
"Vêtement",
"Minéral ou pierre précieuse",
"Étoile, planète ou constellation",
"Fleuve, cours d'eau ou océan",
"Partie du corps humain",
"Oiseau",
"Poisson",
"Qualité ou défaut",
"Arbre",
"Bande dessinée",
"Département français",
"Insecte",
"Dessert",
"Mammifère",
"Épice",
"Héros de mythologie",
"Héros fictif",
"Fromage",
"Jeu",
"Élément de véhicules",
"Site internet",
"Mot de plus de 8 lettres",
"Cadeau de Noël",
"Mot en anglais",
"Mot en espagnol",
"Métier dont rêvent les enfants",
"Chose qui se trouve dans une voiture",
"Chose qui se trouve dans un camping",
"Chose qui se trouve dans un cartable",
"Chose qui se trouve dans une maison",
"Chose qui se trouve dans une forêt",
"Chose qui se trouve dans la mer",
"Ville française",
"Qui sent mauvais",
"Qui fait plaisir",
"Mauvais pour la santé",
"Mauvais pour l'environement"
]
}
assets/icons/button_back.png

3.68 KiB

assets/icons/button_start.png

3.91 KiB

assets/icons/placeholder.png

170 B

Improve game architecture.
Amélioration globale de la conception du jeu.
......@@ -4,15 +4,29 @@
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; }
command -v convert >/dev/null 2>&1 || { echo >&2 "I require convert (imagemagick) but it's not installed. Aborting."; exit 1; }
CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
BASE_DIR="$(dirname "${CURRENT_DIR}")"
ASSETS_DIR="${BASE_DIR}/assets"
SOURCE="${CURRENT_DIR}/icon.svg"
OPTIPNG_OPTIONS="-preserve -quiet -o7"
ICON_SIZE=192
#######################################################
# Game images
AVAILABLE_GAME_IMAGES="
button_back
button_start
placeholder
"
#######################################################
# optimize svg
function optimize_svg() {
SOURCE="$1"
cp ${SOURCE} ${SOURCE}.tmp
scour \
--remove-descriptive-elements \
......@@ -20,29 +34,45 @@ scour \
--enable-viewboxing \
--enable-comment-stripping \
--nindent=4 \
--quiet \
-i ${SOURCE}.tmp \
-o ${SOURCE}
rm ${SOURCE}.tmp
}
# build icons
function build_icon() {
ICON_SIZE="$1"
SOURCE="$1"
TARGET="$2"
TARGET_PNG="${TARGET}.png"
echo "Building ${TARGET}"
if [ ! -f "${SOURCE}" ]; then
echo "Missing file: ${SOURCE}"
exit 1
fi
optimize_svg "${SOURCE}"
inkscape \
--export-width=${ICON_SIZE} \
--export-height=${ICON_SIZE} \
--export-filename=${TARGET_PNG} \
--export-filename=${TARGET} \
${SOURCE}
optipng ${OPTIPNG_OPTIONS} ${TARGET_PNG}
optipng ${OPTIPNG_OPTIONS} ${TARGET}
}
#######################################################
# Create output folders
mkdir -p ${ASSETS_DIR}/icons
# Delete existing generated images
find ${ASSETS_DIR}/icons -type f -name "*.png" -delete
build_icon 72 ${BASE_DIR}/android/app/src/main/res/mipmap-hdpi/ic_launcher
build_icon 48 ${BASE_DIR}/android/app/src/main/res/mipmap-mdpi/ic_launcher
build_icon 96 ${BASE_DIR}/android/app/src/main/res/mipmap-xhdpi/ic_launcher
build_icon 144 ${BASE_DIR}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher
build_icon 192 ${BASE_DIR}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher
# build game images
for GAME_IMAGE in ${AVAILABLE_GAME_IMAGES}
do
build_icon ${CURRENT_DIR}/${GAME_IMAGE}.svg ${ASSETS_DIR}/icons/${GAME_IMAGE}.png
done
<?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="#e41578" stroke="#fff" stroke-width=".238"/><path d="m59.387 71.362c1.1248 1.1302 4.0012 1.1302 4.0012 0v-45.921c0-1.1316-2.8832-1.1316-4.0121 0l-37.693 20.918c-1.1289 1.1248-1.1479 2.9551-0.02171 4.084z" fill="#fefeff" stroke="#930e4e" stroke-linecap="round" stroke-linejoin="round" stroke-width="8.257"/><path d="m57.857 68.048c0.96243 0.96706 3.4236 0.96706 3.4236 0v-39.292c0-0.96825-2.467-0.96825-3.4329 0l-32.252 17.898c-0.96594 0.96243-0.9822 2.5285-0.01858 3.4945z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.314"/></svg>
<?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="m34.852 25.44c-1.1248-1.1302-4.0012-1.1302-4.0012 0v45.921c0 1.1316 2.8832 1.1316 4.0121 0l37.693-20.918c1.1289-1.1248 1.1479-2.9551 0.02171-4.084z" fill="#fefeff" stroke="#105ca1" stroke-linecap="round" stroke-linejoin="round" stroke-width="8.257"/><path d="m36.382 28.754c-0.96243-0.96706-3.4236-0.96706-3.4236 0v39.292c0 0.96825 2.467 0.96825 3.4329 0l32.252-17.898c0.96594-0.96243 0.9822-2.5285 0.01858-3.4945z" fill="#fefeff" stroke="#feffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4.314"/></svg>
<?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"/>
import 'package:petitbac/utils/tools.dart';
class DefaultGameSettings {
// available game parameters codes
static const String parameterCodeItemsCount = 'itemsCount';
static const String parameterCodeTimerValue = 'timerValue';
static const List<String> availableParameters = [
parameterCodeItemsCount,
parameterCodeTimerValue,
];
// items count: available values
static const int itemsCountValueNoLimit = 0;
static const int itemsCountValueShort = 5;
static const int itemsCountValueMedium = 10;
static const int itemsCountValueLong = 20;
static const List<int> allowedItemsCountValues = [
itemsCountValueNoLimit,
itemsCountValueShort,
itemsCountValueMedium,
itemsCountValueLong,
];
// items count: default value
static const int defaultItemsCountValue = itemsCountValueMedium;
// timer value: available values
static const int timerValueNoTimer = 0;
static const int timerValueLow = 10;
static const int timerValueMedium = 30;
static const int timerValueHigh = 60;
static const List<int> allowedTimerValues = [
timerValueNoTimer,
timerValueLow,
timerValueMedium,
timerValueHigh,
];
// timer value: default value
static const int defaultTimerValue = timerValueMedium;
static List<int> getAvailableValues(String parameterCode) {
switch (parameterCode) {
case 'itemsCount':
return DefaultGameSettings.allowedItemsCountValues;
case 'timerValue':
return DefaultGameSettings.allowedTimerValues;
}
printlog('Did not find any available value for game parameter "$parameterCode".');
return [];
}
}
import 'package:petitbac/utils/tools.dart';
class DefaultGlobalSettings {
static const List<String> availableParameters = [];
static List<int> getAvailableValues(String parameterCode) {
printlog('Did not find any available value for global parameter "$parameterCode".');
return [];
}
}
class DefaultSettings {
static const List<int> allowedTimerValues = [
5,
10,
20,
30,
60,
90,
];
static const int defaultTimerValue = 10;
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:unicons/unicons.dart';
import 'package:petitbac/ui/screens/about_page.dart';
import 'package:petitbac/ui/screens/game_page.dart';
import 'package:petitbac/ui/screens/settings_page.dart';
import 'package:petitbac/ui/screens/page_about.dart';
import 'package:petitbac/ui/screens/page_game.dart';
import 'package:petitbac/ui/screens/page_settings.dart';
class MenuItem {
final String code;
......@@ -19,35 +18,39 @@ class MenuItem {
}
class Menu {
static List<MenuItem> items = [
const MenuItem(
code: 'bottom_nav_home',
static const indexGame = 0;
static const menuItemGame = MenuItem(
code: 'bottom_nav_game',
icon: Icon(UniconsLine.home),
page: GamePage(),
),
const MenuItem(
page: PageGame(),
);
static const indexSettings = 1;
static const menuItemSettings = MenuItem(
code: 'bottom_nav_settings',
icon: Icon(UniconsLine.setting),
page: SettingsPage(),
),
const MenuItem(
page: PageSettings(),
);
static const indexAbout = 2;
static const menuItemAbout = MenuItem(
code: 'bottom_nav_about',
icon: Icon(UniconsLine.info_circle),
page: AboutPage(),
),
];
page: PageAbout(),
);
static Widget getPageWidget(int pageIndex) {
return Menu.items.elementAt(pageIndex).page;
static Map<int, MenuItem> items = {
indexGame: menuItemGame,
indexSettings: menuItemSettings,
indexAbout: menuItemAbout,
};
static bool isIndexAllowed(int pageIndex) {
return items.keys.contains(pageIndex);
}
static List<BottomNavigationBarItem> getMenuItems() {
return Menu.items
.map((MenuItem item) => BottomNavigationBarItem(
icon: item.icon,
label: tr(item.code),
))
.toList();
static Widget getPageWidget(int pageIndex) {
return items[pageIndex]?.page ?? menuItemGame.page;
}
static int itemsCount = Menu.items.length;
......
import 'dart:async';
import 'dart:math';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:petitbac/models/game/game.dart';
import 'package:petitbac/data/fetch_data_helper.dart';
import 'package:petitbac/models/data/game_item.dart';
import 'package:petitbac/models/game.dart';
import 'package:petitbac/models/settings_game.dart';
import 'package:petitbac/models/settings_global.dart';
part 'game_state.dart';
class GameCubit extends HydratedCubit<GameState> {
GameCubit() : super(const GameState());
GameCubit()
: super(GameState(
currentGame: Game.createNull(),
));
void updateState(Game game) {
emit(GameState(
currentGame: game,
));
}
void refresh() {
final Game game = Game(
gameSettings: state.currentGame.gameSettings,
globalSettings: state.currentGame.globalSettings,
items: state.currentGame.items,
isRunning: state.currentGame.isRunning,
isFinished: state.currentGame.isFinished,
countdown: state.currentGame.countdown,
position: state.currentGame.position,
);
// game.dump();
updateState(game);
}
void startNewGame({
required GameSettings gameSettings,
required GlobalSettings globalSettings,
}) {
Game newGame = Game.createNew(
gameSettings: gameSettings,
globalSettings: globalSettings,
);
newGame.dump();
updateState(newGame);
startTimer();
}
void quitGame() {
state.currentGame.isRunning = false;
refresh();
}
void next() {
if (state.currentGame.position < (state.currentGame.gameSettings.itemsCount - 1)) {
state.currentGame.position++;
startTimer();
} else {
state.currentGame.isFinished = true;
}
refresh();
}
void startTimer() {
int timerValue = state.currentGame.gameSettings.timerValue;
void getData(GameState gameState) {
emit(gameState);
if (timerValue != 0) {
state.currentGame.countdown = timerValue;
const Duration interval = Duration(seconds: 1);
Timer.periodic(
interval,
(Timer timer) {
if (state.currentGame.countdown != 0) {
state.currentGame.countdown = max(state.currentGame.countdown - 1, 0);
refresh();
}
if (state.currentGame.countdown == 0) {
timer.cancel();
if (state.currentGame.position ==
(state.currentGame.gameSettings.itemsCount - 1)) {
state.currentGame.isFinished = true;
}
}
refresh();
},
);
} else {
if (state.currentGame.position == (state.currentGame.gameSettings.itemsCount - 1)) {
state.currentGame.isFinished = true;
}
}
refresh();
}
void pickNewItem() {
state.currentGame.items[state.currentGame.position] = FetchDataHelper().getRandomItem();
refresh();
}
void updateGameState(Game gameData) {
emit(GameState(game: gameData));
void pickNewCategory() {
GameItem newItem = GameItem(
letter: state.currentGame.letter,
category: FetchDataHelper().getRandomItem().category,
);
state.currentGame.items[state.currentGame.position] = newItem;
refresh();
}
void pickNewLetter() {
GameItem newItem = GameItem(
letter: FetchDataHelper().getRandomItem().letter,
category: state.currentGame.category,
);
state.currentGame.items[state.currentGame.position] = newItem;
refresh();
}
@override
GameState? fromJson(Map<String, dynamic> json) {
Game game = json['game'] as Game;
Game currentGame = json['currentGame'] as Game;
return GameState(
game: game,
currentGame: currentGame,
);
}
@override
Map<String, dynamic>? toJson(GameState state) {
return <String, dynamic>{
'game': state.game?.toJson(),
'currentGame': state.currentGame.toJson(),
};
}
}
......@@ -3,13 +3,13 @@ part of 'game_cubit.dart';
@immutable
class GameState extends Equatable {
const GameState({
this.game,
required this.currentGame,
});
final Game? game;
final Game currentGame;
@override
List<Object?> get props => <Object?>[
game,
currentGame,
];
}
......@@ -2,26 +2,32 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:petitbac/config/menu.dart';
class BottomNavCubit extends HydratedCubit<int> {
BottomNavCubit() : super(0);
class NavCubit extends HydratedCubit<int> {
NavCubit() : super(0);
void updateIndex(int index) {
if (isIndexAllowed(index)) {
if (Menu.isIndexAllowed(index)) {
emit(index);
} else {
goToHomePage();
goToGamePage();
}
}
bool isIndexAllowed(int index) {
return (index >= 0) && (index < Menu.itemsCount);
void goToGamePage() {
emit(Menu.indexGame);
}
void goToHomePage() => emit(0);
void goToSettingsPage() {
emit(Menu.indexSettings);
}
void goToAboutPage() {
emit(Menu.indexAbout);
}
@override
int fromJson(Map<String, dynamic> json) {
return 0;
return Menu.indexGame;
}
@override
......
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:petitbac/config/default_settings.dart';
part 'settings_state.dart';
class SettingsCubit extends HydratedCubit<SettingsState> {
SettingsCubit() : super(const SettingsState());
Object getSetting(String key, [String? defaultValue]) {
if (state.values.keys.contains(key)) {
return state.values[key] ?? defaultValue ?? '';
}
return defaultValue ?? '';
}
int getTimerValue() {
return state.timerValue ?? DefaultSettings.defaultTimerValue;
}
void setValues({
int? timerValue,
}) {
emit(SettingsState(
timerValue: timerValue ?? state.timerValue,
));
}
@override
SettingsState? fromJson(Map<String, dynamic> json) {
int timerValue = json['timerValue'] as int;
return SettingsState(
timerValue: timerValue,
);
}
@override
Map<String, dynamic>? toJson(SettingsState state) {
return <String, dynamic>{
'timerValue': state.timerValue ?? DefaultSettings.defaultTimerValue,
};
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment