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

Normalize game architecture

parent 6c63633d
No related branches found
No related tags found
1 merge request!24Resolve "Normalize game architecture"
Pipeline #5685 passed
Showing
with 748 additions and 102 deletions
File moved
import 'package:twister/config/default_game_settings.dart';
import 'package:twister/utils/tools.dart';
class GameSettings {
final String timerValue;
GameSettings({
required this.timerValue,
});
static String getTimerValueFromUnsafe(String timerValue) {
if (DefaultGameSettings.allowedTimerValues.contains(timerValue)) {
return timerValue;
}
return DefaultGameSettings.defaultTimerValue;
}
factory GameSettings.createDefault() {
return GameSettings(
timerValue: DefaultGameSettings.defaultTimerValue,
);
}
void dump() {
printlog('$GameSettings:');
printlog(' ${DefaultGameSettings.parameterCodeTimerValue}: $timerValue');
printlog('');
}
@override
String toString() {
return '$GameSettings(${toJson()})';
}
Map<String, dynamic>? toJson() {
return <String, dynamic>{
DefaultGameSettings.parameterCodeTimerValue: timerValue,
};
}
}
import 'package:twister/config/default_global_settings.dart';
import 'package:twister/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,
};
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:twister/cubit/game_cubit.dart';
import 'package:twister/models/game/game.dart';
import 'package:twister/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;
const Image decorationImage = Image(
image: AssetImage('assets/ui/game_end.png'),
fit: BoxFit.fill,
);
return Container(
margin: const EdgeInsets.all(2),
padding: const EdgeInsets.all(2),
child: Table(
defaultColumnWidth: const IntrinsicColumnWidth(),
children: [
TableRow(
children: [
const Column(
children: [decorationImage],
),
Column(
children: [
currentGame.animationInProgress == true
? decorationImage
: const QuitGameButton()
],
),
const Column(
children: [decorationImage],
),
],
),
],
),
);
},
);
}
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class AppTitle extends StatelessWidget {
const AppTitle({super.key, required this.text});
class AppHeader extends StatelessWidget {
const AppHeader({super.key, required this.text});
final String text;
......@@ -11,37 +11,22 @@ class AppTitle extends StatelessWidget {
return Text(
tr(text),
textAlign: TextAlign.start,
style: Theme.of(context).textTheme.headlineLarge!.apply(fontWeightDelta: 2),
style: Theme.of(context).textTheme.headlineMedium!.apply(fontWeightDelta: 2),
);
}
}
class AppTitle1 extends StatelessWidget {
const AppTitle1({super.key, required this.text});
class AppTitle extends StatelessWidget {
const AppTitle({super.key, required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Text(
text,
tr(text),
textAlign: TextAlign.start,
style: Theme.of(context).textTheme.titleLarge!.apply(fontWeightDelta: 2),
);
}
}
class AppTitle2 extends StatelessWidget {
const AppTitle2({super.key, required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Text(
text,
textAlign: TextAlign.start,
style: Theme.of(context).textTheme.titleMedium!.apply(fontWeightDelta: 2),
);
}
}
import 'package:flutter/material.dart';
import 'package:twister/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(),
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:twister/cubit/game_cubit.dart';
import 'package:twister/models/game/game.dart';
import 'package:twister/ui/game/game_end.dart';
import 'package:twister/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 GameBoardWidget(),
const Expanded(child: SizedBox.shrink()),
currentGame.isFinished ? const GameEndWidget() : const SizedBox.shrink(),
],
),
);
},
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:twister/config/default_game_settings.dart';
import 'package:twister/config/default_global_settings.dart';
import 'package:twister/cubit/settings_game_cubit.dart';
import 'package:twister/cubit/settings_global_cubit.dart';
import 'package:twister/ui/parameters/parameter_image.dart';
import 'package:twister/ui/parameters/parameter_painter.dart';
import 'package:twister/ui/widgets/actions/button_delete_saved_game.dart';
import 'package:twister/ui/widgets/actions/button_game_start_new.dart';
import 'package:twister/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;
}
}
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,
),
);
}
}
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:twister/models/settings/settings_game.dart';
import 'package:twister/models/settings/settings_global.dart';
import 'package:twister/utils/tools.dart';
class ParameterPainter extends CustomPainter {
const ParameterPainter({
required this.code,
required this.value,
required this.isSelected,
required this.gameSettings,
required this.globalSettings,
});
final String code;
final String value;
final bool isSelected;
final GameSettings gameSettings;
final GlobalSettings globalSettings;
@override
void paint(Canvas canvas, Size size) {
// force square
final double canvasSize = min(size.width, size.height);
const Color borderColorEnabled = Colors.blue;
const Color borderColorDisabled = Colors.white;
// "enabled/disabled" border
final paint = Paint();
paint.style = PaintingStyle.stroke;
paint.color = isSelected ? borderColorEnabled : borderColorDisabled;
paint.strokeJoin = StrokeJoin.round;
paint.strokeWidth = 10;
canvas.drawRect(
Rect.fromPoints(const Offset(0, 0), Offset(canvasSize, canvasSize)), paint);
// content
switch (code) {
default:
printlog('Unknown parameter: $code/$value');
paintUnknownParameterItem(value, canvas, canvasSize);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
// "unknown" parameter -> simple block with text
void paintUnknownParameterItem(
final String value,
final Canvas canvas,
final double size,
) {
final paint = Paint();
paint.strokeJoin = StrokeJoin.round;
paint.strokeWidth = 3;
paint.color = Colors.grey;
paint.style = PaintingStyle.fill;
canvas.drawRect(Rect.fromPoints(const Offset(0, 0), Offset(size, size)), paint);
final textSpan = TextSpan(
text: '?\n$value',
style: const TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.bold,
),
);
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(
(size - textPainter.width) * 0.5,
(size - textPainter.height) * 0.5,
),
);
}
}
import 'package:flutter/material.dart';
import 'package:twister/ui/widgets/game.dart';
import 'package:unicons/unicons.dart';
class ScreenHome extends StatelessWidget {
const ScreenHome({super.key});
static Icon navBarIcon = const Icon(UniconsLine.home);
static String navBarText = 'bottom_nav_game';
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 4),
physics: const BouncingScrollPhysics(),
children: const <Widget>[
SizedBox(height: 8),
Game(),
SizedBox(height: 36),
],
),
);
}
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:twister/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();
}
},
),
],
),
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:twister/cubit/game_cubit.dart';
import 'package:twister/models/game/game.dart';
import 'package:twister/ui/layouts/game_layout.dart';
import 'package:twister/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);
},
);
}
}
import 'package:flutter/material.dart';
import 'package:twister/ui/helpers/app_titles.dart';
import 'package:twister/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(),
],
),
);
}
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:twister/ui/widgets/app_titles.dart';
import 'package:twister/ui/widgets/settings_form.dart';
import 'package:unicons/unicons.dart';
class ScreenSettings extends StatelessWidget {
const ScreenSettings({super.key});
static Icon navBarIcon = const Icon(UniconsLine.setting);
static String navBarText = 'bottom_nav_settings';
@override
Widget build(BuildContext context) {
return Material(
color: Theme.of(context).colorScheme.background,
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 4),
physics: const BouncingScrollPhysics(),
children: <Widget>[
const SizedBox(height: 8),
AppTitle1(text: tr('settings_title')),
const SettingsForm(),
],
),
);
}
}
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:unicons/unicons.dart';
import 'package:twister/config/default_settings.dart';
import 'package:twister/cubit/settings_cubit.dart';
import 'package:twister/ui/widgets/app_titles.dart';
import 'package:twister/ui/widgets/theme_card.dart';
import 'package:twister/ui/settings/theme_card.dart';
class SettingsForm extends StatefulWidget {
const SettingsForm({super.key});
......@@ -16,42 +12,23 @@ class SettingsForm extends StatefulWidget {
}
class _SettingsFormState extends State<SettingsForm> {
int timerValue = DefaultSettings.defaultTimerValue;
List<bool> _selectedTimerValue = [];
@override
void didChangeDependencies() {
SettingsCubit settings = BlocProvider.of<SettingsCubit>(context);
timerValue = settings.getTimerValue();
_selectedTimerValue =
DefaultSettings.allowedTimerValues.map((e) => (e == timerValue)).toList();
super.didChangeDependencies();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
void saveSettings() {
BlocProvider.of<SettingsCubit>(context).setValues(
timerValue: timerValue,
);
void didChangeDependencies() {
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: <Widget>[
const SizedBox(height: 8),
// Light/dark theme
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
......@@ -80,33 +57,6 @@ class _SettingsFormState extends State<SettingsForm> {
),
const SizedBox(height: 16),
AppTitle2(text: tr('settings_title_game')),
// Timer value
Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text('settings_label_game_timer_value').tr(),
ToggleButtons(
onPressed: (int index) {
setState(() {
timerValue = DefaultSettings.allowedTimerValues[index];
for (int i = 0; i < _selectedTimerValue.length; i++) {
_selectedTimerValue[i] = i == index;
}
});
saveSettings();
},
borderRadius: const BorderRadius.all(Radius.circular(8)),
constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0),
isSelected: _selectedTimerValue,
children:
DefaultSettings.allowedTimerValues.map((e) => Text(e.toString())).toList(),
),
],
),
],
);
}
......
......@@ -35,11 +35,13 @@ class ThemeCard extends StatelessWidget {
child: Icon(
icon,
size: 32,
color:
state.themeMode != mode ? Theme.of(context).colorScheme.primary : Colors.white,
color: state.themeMode != mode
? Theme.of(context).colorScheme.primary
: Colors.white,
),
),
);
});
},
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:twister/cubit/bottom_nav_cubit.dart';
import 'package:twister/ui/screens/home.dart';
import 'package:twister/ui/screens/settings.dart';
import 'package:twister/ui/widgets/app_bar.dart';
import 'package:twister/ui/widgets/bottom_nav_bar.dart';
import 'package:twister/config/menu.dart';
import 'package:twister/cubit/nav_cubit.dart';
import 'package:twister/ui/widgets/global_app_bar.dart';
class SkeletonScreen extends StatefulWidget {
class SkeletonScreen extends StatelessWidget {
const SkeletonScreen({super.key});
@override
State<SkeletonScreen> createState() => _SkeletonScreenState();
}
class _SkeletonScreenState extends State<SkeletonScreen> {
@override
Widget build(BuildContext context) {
List<Widget> pageNavigation = <Widget>[
const ScreenHome(),
const ScreenSettings(),
];
return Scaffold(
appBar: const StandardAppBar(),
appBar: const GlobalAppBar(),
extendBodyBehindAppBar: false,
body: BlocBuilder<BottomNavCubit, int>(
builder: (BuildContext context, int state) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: pageNavigation.elementAt(state),
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.background,
bottomNavigationBar: const BottomNavBar(),
),
backgroundColor: Theme.of(context).colorScheme.surface,
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:twister/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();
},
);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment