Merge pull request #122 from flixcoo/enhance/104-rework-kamikaze-features

Rework kamikaze feature
This commit is contained in:
2025-07-19 18:06:53 +02:00
committed by GitHub
9 changed files with 155 additions and 139 deletions

View File

@@ -4,7 +4,9 @@ class CustomTheme {
static Color white = CupertinoColors.white; static Color white = CupertinoColors.white;
static Color primaryColor = CupertinoColors.systemGreen; static Color primaryColor = CupertinoColors.systemGreen;
static Color backgroundColor = const Color(0xFF101010); static Color backgroundColor = const Color(0xFF101010);
static Color backgroundTintColor = CupertinoColors.darkBackgroundGray; static Color mainElementBackgroundColor = CupertinoColors.darkBackgroundGray;
static Color playerTileColor = CupertinoColors.secondaryLabel;
static Color buttonBackgroundColor = const Color(0xFF202020);
// Line Colors for GraphView // Line Colors for GraphView
static const Color graphColor1 = Color(0xFFF44336); static const Color graphColor1 = Color(0xFFF44336);

View File

@@ -71,6 +71,7 @@
"results": "Ergebnisse", "results": "Ergebnisse",
"who_said_cabo": "Wer hat CABO gesagt?", "who_said_cabo": "Wer hat CABO gesagt?",
"kamikaze": "Kamikaze", "kamikaze": "Kamikaze",
"who_has_kamikaze": "Wer hat Kamikaze?",
"done": "Fertig", "done": "Fertig",
"next_round": "Nächste Runde", "next_round": "Nächste Runde",
"bonus_points_title": "Bonus-Punkte!", "bonus_points_title": "Bonus-Punkte!",

View File

@@ -71,6 +71,7 @@
"results": "Results", "results": "Results",
"who_said_cabo": "Who called Cabo?", "who_said_cabo": "Who called Cabo?",
"kamikaze": "Kamikaze", "kamikaze": "Kamikaze",
"who_has_kamikaze": "Who has Kamikaze?",
"done": "Done", "done": "Done",
"next_round": "Next Round", "next_round": "Next Round",
"bonus_points_title": "Bonus-Points!", "bonus_points_title": "Bonus-Points!",

View File

@@ -404,6 +404,12 @@ abstract class AppLocalizations {
/// **'Kamikaze'** /// **'Kamikaze'**
String get kamikaze; String get kamikaze;
/// No description provided for @who_has_kamikaze.
///
/// In de, this message translates to:
/// **'Wer hat Kamikaze?'**
String get who_has_kamikaze;
/// No description provided for @done. /// No description provided for @done.
/// ///
/// In de, this message translates to: /// In de, this message translates to:

View File

@@ -172,6 +172,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get kamikaze => 'Kamikaze'; String get kamikaze => 'Kamikaze';
@override
String get who_has_kamikaze => 'Wer hat Kamikaze?';
@override @override
String get done => 'Fertig'; String get done => 'Fertig';

View File

@@ -170,6 +170,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get kamikaze => 'Kamikaze'; String get kamikaze => 'Kamikaze';
@override
String get who_has_kamikaze => 'Who has Kamikaze?';
@override @override
String get done => 'Done'; String get done => 'Done';

View File

@@ -77,18 +77,19 @@ class _RoundViewState extends State<RoundView> {
transitionBetweenRoutes: true, transitionBetweenRoutes: true,
leading: CupertinoButton( leading: CupertinoButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onPressed: () => onPressed: () => {
{LocalStorageService.saveGameSessions(), Navigator.pop(context)}, LocalStorageService.saveGameSessions(),
Navigator.pop(context)
},
child: Text(AppLocalizations.of(context).cancel), child: Text(AppLocalizations.of(context).cancel),
), ),
middle: Text(AppLocalizations.of(context).results), middle: Text(AppLocalizations.of(context).results),
trailing: widget.gameSession.isGameFinished trailing: Visibility(
? const Icon( visible: widget.gameSession.isGameFinished,
child: const Icon(
CupertinoIcons.lock, CupertinoIcons.lock,
size: 25, size: 25,
) ))),
: null,
),
child: Stack( child: Stack(
children: [ children: [
Positioned.fill( Positioned.fill(
@@ -114,9 +115,10 @@ class _RoundViewState extends State<RoundView> {
vertical: 10, vertical: 10,
), ),
child: SizedBox( child: SizedBox(
height: 40, height: 60,
child: CupertinoSegmentedControl<int>( child: CupertinoSegmentedControl<int>(
unselectedColor: CustomTheme.backgroundTintColor, unselectedColor:
CustomTheme.mainElementBackgroundColor,
selectedColor: CustomTheme.primaryColor, selectedColor: CustomTheme.primaryColor,
groupValue: _caboPlayerIndex, groupValue: _caboPlayerIndex,
children: Map.fromEntries(widget.gameSession.players children: Map.fromEntries(widget.gameSession.players
@@ -130,7 +132,7 @@ class _RoundViewState extends State<RoundView> {
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 6, horizontal: 6,
vertical: 6, vertical: 8,
), ),
child: FittedBox( child: FittedBox(
fit: BoxFit.scaleDown, fit: BoxFit.scaleDown,
@@ -154,27 +156,6 @@ class _RoundViewState extends State<RoundView> {
), ),
), ),
), ),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: CupertinoListTile(
title: Text(AppLocalizations.of(context).player),
trailing: Row(
children: [
SizedBox(
width: 100,
child: Center(
child: Text(
AppLocalizations.of(context).points))),
const SizedBox(width: 20),
SizedBox(
width: 80,
child: Center(
child: Text(AppLocalizations.of(context)
.kamikaze))),
],
),
),
),
ListView.builder( ListView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
@@ -190,7 +171,7 @@ class _RoundViewState extends State<RoundView> {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: CupertinoListTile( child: CupertinoListTile(
backgroundColor: CupertinoColors.secondaryLabel, backgroundColor: CustomTheme.playerTileColor,
title: Row(children: [ title: Row(children: [
Expanded( Expanded(
child: Row(children: [ child: Row(children: [
@@ -204,16 +185,14 @@ class _RoundViewState extends State<RoundView> {
), ),
Visibility( Visibility(
visible: shouldShowMedal, visible: shouldShowMedal,
child: const Icon(FontAwesomeIcons.medal, child: const Icon(FontAwesomeIcons.crown,
size: 15)) size: 15))
])) ]))
]), ]),
subtitle: Text( subtitle: Text(
'${widget.gameSession.playerScores[originalIndex]}' '${widget.gameSession.playerScores[originalIndex]}'
' ${AppLocalizations.of(context).points}'), ' ${AppLocalizations.of(context).points}'),
trailing: Row( trailing: SizedBox(
children: [
SizedBox(
width: 100, width: 100,
child: CupertinoTextField( child: CupertinoTextField(
maxLength: 3, maxLength: 3,
@@ -227,9 +206,7 @@ class _RoundViewState extends State<RoundView> {
FilteringTextInputFormatter.digitsOnly, FilteringTextInputFormatter.digitsOnly,
], ],
textInputAction: index == textInputAction: index ==
widget.gameSession.players widget.gameSession.players.length - 1
.length -
1
? TextInputAction.done ? TextInputAction.done
: TextInputAction.next, : TextInputAction.next,
controller: controller:
@@ -242,52 +219,34 @@ class _RoundViewState extends State<RoundView> {
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
), ),
), ),
const SizedBox(width: 50),
GestureDetector(
onTap: () {
setState(() {
_kamikazePlayerIndex =
(_kamikazePlayerIndex ==
originalIndex)
? null
: originalIndex;
});
},
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _kamikazePlayerIndex ==
originalIndex
? CupertinoColors.systemRed
: CupertinoColors
.tertiarySystemFill,
border: Border.all(
color: _kamikazePlayerIndex ==
originalIndex
? CupertinoColors.systemRed
: CupertinoColors.systemGrey,
),
),
child: _kamikazePlayerIndex ==
originalIndex
? const Icon(
CupertinoIcons.exclamationmark,
size: 16,
color: CupertinoColors.white,
)
: null,
),
),
const SizedBox(width: 22),
],
),
), ),
), ),
); );
}, },
), ),
Padding(
padding: const EdgeInsets.fromLTRB(0, 10, 0, 0),
child: Center(
heightFactor: 1,
child: CupertinoButton(
sizeStyle: CupertinoButtonSize.medium,
borderRadius: BorderRadius.circular(12),
color: CustomTheme.buttonBackgroundColor,
onPressed: () async {
if (await _showKamikazeSheet(context)) {
if (!context.mounted) return;
_endOfRoundNavigation(context, true);
}
},
child: Text(
AppLocalizations.of(context).kamikaze,
style: const TextStyle(
color: CupertinoColors.destructiveRed,
),
),
),
),
),
], ],
), ),
), ),
@@ -302,21 +261,14 @@ class _RoundViewState extends State<RoundView> {
return Container( return Container(
height: 80, height: 80,
padding: const EdgeInsets.only(bottom: 20), padding: const EdgeInsets.only(bottom: 20),
color: CustomTheme.backgroundTintColor, color: CustomTheme.mainElementBackgroundColor,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
CupertinoButton( CupertinoButton(
onPressed: _areRoundInputsValid() onPressed: _areRoundInputsValid()
? () async { ? () {
List<int> bonusPlayersIndices = _finishRound(); _endOfRoundNavigation(context, false);
if (bonusPlayersIndices.isNotEmpty) {
await _showBonusPopup(
context, bonusPlayersIndices);
}
LocalStorageService.saveGameSessions();
if (!context.mounted) return;
Navigator.pop(context);
} }
: null, : null,
child: Text(AppLocalizations.of(context).done), child: Text(AppLocalizations.of(context).done),
@@ -324,21 +276,8 @@ class _RoundViewState extends State<RoundView> {
if (!widget.gameSession.isGameFinished) if (!widget.gameSession.isGameFinished)
CupertinoButton( CupertinoButton(
onPressed: _areRoundInputsValid() onPressed: _areRoundInputsValid()
? () async { ? () {
List<int> bonusPlayersIndices = _endOfRoundNavigation(context, true);
_finishRound();
if (bonusPlayersIndices.isNotEmpty) {
await _showBonusPopup(
context, bonusPlayersIndices);
}
LocalStorageService.saveGameSessions();
if (widget.gameSession.isGameFinished &&
context.mounted) {
Navigator.pop(context);
} else if (context.mounted) {
Navigator.pop(
context, widget.roundNumber + 1);
}
} }
: null, : null,
child: Text(AppLocalizations.of(context).next_round), child: Text(AppLocalizations.of(context).next_round),
@@ -401,6 +340,37 @@ class _RoundViewState extends State<RoundView> {
]; ];
} }
/// Shows a Cupertino action sheet to select the player who has Kamikaze.
/// It returns true if a player was selected, false if the action was cancelled.
Future<bool> _showKamikazeSheet(BuildContext context) async {
return await showCupertinoModalPopup<bool?>(
context: context,
builder: (BuildContext context) {
return CupertinoActionSheet(
title: Text(AppLocalizations.of(context).kamikaze),
message: Text(AppLocalizations.of(context).who_has_kamikaze),
actions: widget.gameSession.players.asMap().entries.map((entry) {
final index = entry.key;
final name = entry.value;
return CupertinoActionSheetAction(
onPressed: () {
_kamikazePlayerIndex = index;
Navigator.pop(context, true);
},
child: Text(name),
);
}).toList(),
cancelButton: CupertinoActionSheetAction(
onPressed: () => Navigator.pop(context, false),
isDestructiveAction: true,
child: Text(AppLocalizations.of(context).cancel),
),
);
},
) ??
false;
}
/// Focuses the next text field in the list of text fields. /// Focuses the next text field in the list of text fields.
/// [index] is the index of the current text field. /// [index] is the index of the current text field.
void _focusNextTextfield(int index) { void _focusNextTextfield(int index) {
@@ -471,10 +441,9 @@ class _RoundViewState extends State<RoundView> {
return bonusPlayers; return bonusPlayers;
} }
/// Shows a popup dialog with the bonus information. /// Shows a popup dialog with the information which player received the bonus points.
Future<void> _showBonusPopup( Future<void> _showBonusPopup(
BuildContext context, List<int> bonusPlayers) async { BuildContext context, List<int> bonusPlayers) async {
print('Bonus Popup wird angezeigt');
int pointLimit = widget.gameSession.pointLimit; int pointLimit = widget.gameSession.pointLimit;
int bonusPoints = (pointLimit / 2).round(); int bonusPoints = (pointLimit / 2).round();
@@ -521,6 +490,37 @@ class _RoundViewState extends State<RoundView> {
return resultText; return resultText;
} }
/// Handles the navigation for the end of the round.
/// It checks for bonus players and shows a popup, saves the game session,
/// and navigates to the next round or back to the previous screen.
/// It takes the BuildContext [context] and a boolean [navigateToNextRound] to determine
/// if it should navigate to the next round or not.
Future<void> _endOfRoundNavigation(
BuildContext context, bool navigateToNextRound) async {
List<int> bonusPlayersIndices = _finishRound();
if (bonusPlayersIndices.isNotEmpty) {
await _showBonusPopup(context, bonusPlayersIndices);
}
LocalStorageService.saveGameSessions();
if (context.mounted) {
// If the game is finished, pop the context and return to the previous screen.
if (widget.gameSession.isGameFinished) {
Navigator.pop(context);
return;
}
// If navigateToNextRound is false, pop the context and return to the previous screen.
if (!navigateToNextRound) {
Navigator.pop(context);
return;
}
// If navigateToNextRound is true and the game isn't finished yet,
// pop the context and navigate to the next round.
Navigator.pop(context, widget.roundNumber + 1);
}
}
@override @override
void dispose() { void dispose() {
for (final controller in _scoreControllerList) { for (final controller in _scoreControllerList) {

View File

@@ -17,7 +17,7 @@ class _TabViewState extends State<TabView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CupertinoTabScaffold( return CupertinoTabScaffold(
tabBar: CupertinoTabBar( tabBar: CupertinoTabBar(
backgroundColor: CustomTheme.backgroundTintColor, backgroundColor: CustomTheme.mainElementBackgroundColor,
iconSize: 27, iconSize: 27,
height: 55, height: 55,
items: <BottomNavigationBarItem>[ items: <BottomNavigationBarItem>[

View File

@@ -2,7 +2,7 @@ name: cabo_counter
description: "Mobile app for the card game Cabo" description: "Mobile app for the card game Cabo"
publish_to: 'none' publish_to: 'none'
version: 0.4.8+525 version: 0.4.9+533
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4