From 3ab47115be327f292b90b6c7c4234d743517fa13 Mon Sep 17 00:00:00 2001 From: Jonathan Espinosa <65873243+JonathanEspinosaLong@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:27:28 -0600 Subject: [PATCH 1/3] Split arena quick-info chips: commander damage top-left, rest top-right Parameterize PlayerCellQuickInfo with a QuickInfoGroup so commander-damage chips can be pinned to the top-left of the cell and counters, statuses and casts to the top-right, instead of all chips sharing one side. --- .../components/player_cell_basic_body.dart | 18 +- .../components/player_cell_quick_info.dart | 180 ++++++++++-------- 2 files changed, 111 insertions(+), 87 deletions(-) diff --git a/lib/widgets/arena/player_cell/components/player_cell_basic_body.dart b/lib/widgets/arena/player_cell/components/player_cell_basic_body.dart index 1494b0c5..205c2522 100644 --- a/lib/widgets/arena/player_cell/components/player_cell_basic_body.dart +++ b/lib/widgets/arena/player_cell/components/player_cell_basic_body.dart @@ -29,14 +29,26 @@ class PlayerCellBasicBody extends StatelessWidget { return PlayerCellSideTaps( child: Stack( children: [ + // Commander damage chips, pinned to the top-left. Positioned( top: 0, bottom: PlayerCellBottom.iconButtonsSize, - left: inverted ? 0 : null, - right: !inverted ? 0 : null, + left: 0, child: PlayerCellQuickInfo( playerIndex: playerIndex, - axisAlignment: inverted ? 1 : -1, + axisAlignment: 1, + group: QuickInfoGroup.commanderDamage, + ), + ), + // Counters, statuses and casts, pinned to the top-right. + Positioned( + top: 0, + bottom: PlayerCellBottom.iconButtonsSize, + right: 0, + child: PlayerCellQuickInfo( + playerIndex: playerIndex, + axisAlignment: -1, + group: QuickInfoGroup.other, ), ), Positioned.fill( diff --git a/lib/widgets/arena/player_cell/components/player_cell_quick_info.dart b/lib/widgets/arena/player_cell/components/player_cell_quick_info.dart index ac682a12..2000d54b 100644 --- a/lib/widgets/arena/player_cell/components/player_cell_quick_info.dart +++ b/lib/widgets/arena/player_cell/components/player_cell_quick_info.dart @@ -16,15 +16,27 @@ extension on String { String get initial => isEmpty ? this : this[0]; } +/// Which subset of quick-info chips a [PlayerCellQuickInfo] renders, so the two +/// groups can be pinned to opposite sides of the cell. +enum QuickInfoGroup { + /// Commander damage taken (and dealt, when [PlayerCellQuickInfo.showDamageDealt]). + commanderDamage, + + /// Counters, statuses and commander casts. + other, +} + class PlayerCellQuickInfo extends StatelessWidget { const PlayerCellQuickInfo({ super.key, required this.playerIndex, required this.axisAlignment, + required this.group, this.showDamageDealt = false, }); final int playerIndex; final double axisAlignment; + final QuickInfoGroup group; final bool showDamageDealt; @override @@ -38,6 +50,7 @@ class PlayerCellQuickInfo extends StatelessWidget { playerIndex: playerIndex, playerSettings: game.settings.playerSettings, playerStates: game.currentState.playerStates, + group: group, showDamageDealt: showDamageDealt, ), ), @@ -60,19 +73,20 @@ class _PlayerCellQuickInfo extends StatelessWidget { required this.playerIndex, required this.playerSettings, required this.playerStates, + required this.group, required this.showDamageDealt, }); final int playerIndex; final List playerStates; final List playerSettings; + final QuickInfoGroup group; final bool showDamageDealt; @override Widget build(BuildContext context) { final PlayerSettings thisPlayerSettings = playerSettings[playerIndex]; final PlayerState thisPlayerState = playerStates[playerIndex]; - final int playerCount = playerStates.length; final List damageDealt = [ for (final playerStateDelta in playerStates) @@ -81,15 +95,87 @@ class _PlayerCellQuickInfo extends StatelessWidget { final theme = context.theme; final layout = theme.layout; - final isEmpty = - thisPlayerState.copyWith(lifePoints: 0) == - PlayerState.start(startingLifeTotal: 0, playerCount: playerCount) && - (!showDamageDealt || - damageDealt.every( - (element) => element == (fromPartnerA: 0, fromPartnerB: 0), - )); - if (isEmpty) return const SizedBox.shrink(); + final List chips = switch (group) { + QuickInfoGroup.commanderDamage => [ + for ( + int i = 0; + i < thisPlayerState.commanderDamageTaken.length; + i++ + ) ...[ + if (thisPlayerState.commanderDamageTaken[i].fromPartnerA + case int damage) + if (damage != 0) + DeltaChip.result( + icon: CounterSpellIcons.defense_filled, + result: damage, + note: switch (playerSettings[i].runsTwoPartners) { + true => 'by ${playerSettings[i].name.initial} (A)', + false => 'by ${playerSettings[i].name.initial}', + }, + ), + if (thisPlayerState.commanderDamageTaken[i].fromPartnerB + case int damage) + if (damage != 0) + DeltaChip.result( + icon: CounterSpellIcons.defense_filled, + note: 'by ${playerSettings[i].name.initial} (B)', + result: damage, + ), + ], + if (showDamageDealt) + for (int i = 0; i < damageDealt.length; i++) ...[ + if (damageDealt[i].fromPartnerA case int damage) + if (damage != 0) + DeltaChip.result( + icon: CounterSpellIcons.attack, + result: damage, + note: switch (thisPlayerSettings.runsTwoPartners) { + true => 'to ${playerSettings[i].name.initial} (A)', + false => 'to ${playerSettings[i].name.initial}', + }, + ), + if (damageDealt[i].fromPartnerB case int damage) + if (damage != 0) + DeltaChip.result( + icon: CounterSpellIcons.attack, + note: 'to ${playerSettings[i].name.initial} (B)', + result: damage, + ), + ], + ], + QuickInfoGroup.other => [ + if (thisPlayerState.counters.isNotEmpty) + for (final counter in Counter.values) + if (thisPlayerState.counters[counter] case int amount) + if (amount != 0) + DeltaChip.result( + icon: counter.filledIcon, + result: amount, + boolean: counter.isBoolean, + ), + if (thisPlayerState.commanderCasts.partnerA case int castsA) + if (castsA != 0) + DeltaChip.result( + icon: InteractionMode.cast.filledIcon, + note: switch (thisPlayerSettings.runsTwoPartners) { + true => 'A', + false => null, + }, + result: castsA, + ), + if (thisPlayerState.commanderCasts.partnerB case int castsB) + if (castsB != 0) + DeltaChip.result( + icon: InteractionMode.cast.filledIcon, + note: 'B', + result: castsB, + ), + // TODO: add is dead chip + ], + }; + + if (chips.isEmpty) return const SizedBox.shrink(); return SingleChildScrollView( physics: CallbackScrollPhysics( @@ -103,81 +189,7 @@ class _PlayerCellQuickInfo extends StatelessWidget { vertical: layout.padding.smaller, child: Column( mainAxisSize: MainAxisSize.min, - children: [ - if (thisPlayerState.counters.isNotEmpty) - for (final counter in Counter.values) - if (thisPlayerState.counters[counter] case int amount) - if (amount != 0) - DeltaChip.result( - icon: counter.filledIcon, - result: amount, - boolean: counter.isBoolean, - ), - for ( - int i = 0; - i < thisPlayerState.commanderDamageTaken.length; - i++ - ) ...[ - if (thisPlayerState.commanderDamageTaken[i].fromPartnerA - case int damage) - if (damage != 0) - DeltaChip.result( - icon: CounterSpellIcons.defense_filled, - result: damage, - note: switch (playerSettings[i].runsTwoPartners) { - true => 'by ${playerSettings[i].name.initial} (A)', - false => 'by ${playerSettings[i].name.initial}', - }, - ), - if (thisPlayerState.commanderDamageTaken[i].fromPartnerB - case int damage) - if (damage != 0) - DeltaChip.result( - icon: CounterSpellIcons.defense_filled, - note: 'by ${playerSettings[i].name.initial} (B)', - result: damage, - ), - ], - if (showDamageDealt) - for (int i = 0; i < damageDealt.length; i++) ...[ - if (damageDealt[i].fromPartnerA case int damage) - if (damage != 0) - DeltaChip.result( - icon: CounterSpellIcons.attack, - result: damage, - note: switch (thisPlayerSettings.runsTwoPartners) { - true => 'to ${playerSettings[i].name.initial} (A)', - false => 'to ${playerSettings[i].name.initial}', - }, - ), - if (damageDealt[i].fromPartnerB case int damage) - if (damage != 0) - DeltaChip.result( - icon: CounterSpellIcons.attack, - note: 'to ${playerSettings[i].name.initial} (B)', - result: damage, - ), - ], - - if (thisPlayerState.commanderCasts.partnerA case int castsA) - if (castsA != 0) - DeltaChip.result( - icon: InteractionMode.cast.filledIcon, - note: switch (thisPlayerSettings.runsTwoPartners) { - true => 'A', - false => null, - }, - result: castsA, - ), - if (thisPlayerState.commanderCasts.partnerB case int castsB) - if (castsB != 0) - DeltaChip.result( - icon: InteractionMode.cast.filledIcon, - note: 'B', - result: castsB, - ), - // TODO: add is dead chip - ].separateWith(Space.vertical(layout.spacing.tiny)), + children: chips.separateWith(Space.vertical(layout.spacing.tiny)), ), ), ); From 1f998ae8fb635c925a3bc3f5e20815b67aa0ab3b Mon Sep 17 00:00:00 2001 From: Jonathan Espinosa <65873243+JonathanEspinosaLong@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:41:46 -0600 Subject: [PATCH 2/3] Tint commander-damage chips by the attacking commander's theme Route commander-damage chips through a builder that resolves the attacking commander's theme and wraps each pill in it, so the chip takes that commander's primaryContainer with a legible onPrimaryContainer foreground. --- .../components/player_cell_quick_info.dart | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/lib/widgets/arena/player_cell/components/player_cell_quick_info.dart b/lib/widgets/arena/player_cell/components/player_cell_quick_info.dart index 2000d54b..5ec504df 100644 --- a/lib/widgets/arena/player_cell/components/player_cell_quick_info.dart +++ b/lib/widgets/arena/player_cell/components/player_cell_quick_info.dart @@ -8,6 +8,7 @@ import 'package:counter_spell/models/interaction/interaction_mode.dart'; import 'package:counter_spell/widgets/arena/player_cell/builders/cell_mode_builder.dart'; import 'package:counter_spell/widgets/body/players_list_view/player_tile/components/split_theme.dart'; import 'package:counter_spell/widgets/components/builders/cell_mode_and_increment_builder.dart'; +import 'package:counter_spell/widgets/components/builders/player_cards_and_themes_builder.dart'; import 'package:counter_spell/widgets/components/project/delta_chip.dart'; import 'package:flutter/material.dart'; import 'package:sid_base/sid_base.dart'; @@ -106,7 +107,9 @@ class _PlayerCellQuickInfo extends StatelessWidget { if (thisPlayerState.commanderDamageTaken[i].fromPartnerA case int damage) if (damage != 0) - DeltaChip.result( + _CommanderDamageChip( + attackerIndex: i, + fromPartnerA: true, icon: CounterSpellIcons.defense_filled, result: damage, note: switch (playerSettings[i].runsTwoPartners) { @@ -117,17 +120,21 @@ class _PlayerCellQuickInfo extends StatelessWidget { if (thisPlayerState.commanderDamageTaken[i].fromPartnerB case int damage) if (damage != 0) - DeltaChip.result( + _CommanderDamageChip( + attackerIndex: i, + fromPartnerA: false, icon: CounterSpellIcons.defense_filled, - note: 'by ${playerSettings[i].name.initial} (B)', result: damage, + note: 'by ${playerSettings[i].name.initial} (B)', ), ], if (showDamageDealt) for (int i = 0; i < damageDealt.length; i++) ...[ if (damageDealt[i].fromPartnerA case int damage) if (damage != 0) - DeltaChip.result( + _CommanderDamageChip( + attackerIndex: playerIndex, + fromPartnerA: true, icon: CounterSpellIcons.attack, result: damage, note: switch (thisPlayerSettings.runsTwoPartners) { @@ -137,10 +144,12 @@ class _PlayerCellQuickInfo extends StatelessWidget { ), if (damageDealt[i].fromPartnerB case int damage) if (damage != 0) - DeltaChip.result( + _CommanderDamageChip( + attackerIndex: playerIndex, + fromPartnerA: false, icon: CounterSpellIcons.attack, - note: 'to ${playerSettings[i].name.initial} (B)', result: damage, + note: 'to ${playerSettings[i].name.initial} (B)', ), ], ], @@ -195,3 +204,35 @@ class _PlayerCellQuickInfo extends StatelessWidget { ); } } + +/// A commander-damage [DeltaChip] themed by the commander that applied the +/// damage — [attackerIndex]'s partner A or B theme — so the pill takes that +/// commander's container color with a legible on-container foreground. +class _CommanderDamageChip extends StatelessWidget { + const _CommanderDamageChip({ + required this.attackerIndex, + required this.fromPartnerA, + required this.icon, + required this.result, + this.note, + }); + + final int attackerIndex; + final bool fromPartnerA; + final IconData icon; + final int result; + final String? note; + + @override + Widget build(BuildContext context) { + return PlayerCardsAndThemesBuilder( + playerIndex: attackerIndex, + builder: (context, cardA, cardB, themeA, themeB, child) { + return Theme( + data: fromPartnerA ? themeA : themeB, + child: DeltaChip.result(icon: icon, result: result, note: note), + ); + }, + ); + } +} From 6eafae42ccb82ed1a68ccad280bf62efa92452db Mon Sep 17 00:00:00 2001 From: Jonathan Espinosa <65873243+JonathanEspinosaLong@users.noreply.github.com> Date: Tue, 7 Jul 2026 13:31:42 -0600 Subject: [PATCH 3/3] Reset commander color to its seed immediately when the card changes On a commander card id change, fall back to the new card's color-identity seed right away instead of keeping the previous commander's color until the artwork-derived scheme finishes computing asynchronously. Keeps commander- tinted UI (info pills, cell backgrounds, partner borders) in sync when a commander is changed. --- .../components/builders/card_theme_builder.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/widgets/components/builders/card_theme_builder.dart b/lib/widgets/components/builders/card_theme_builder.dart index e3d7077a..badd6209 100644 --- a/lib/widgets/components/builders/card_theme_builder.dart +++ b/lib/widgets/components/builders/card_theme_builder.dart @@ -121,10 +121,17 @@ class _CardThemeBuilderState extends State<_CardThemeBuilder> { @override void didUpdateWidget(covariant _CardThemeBuilder oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.variant != widget.variant || + final bool cardChanged = oldWidget.card.id != widget.card.id; + if (cardChanged) { + // Fall back to the new card's color-identity seed right away so the + // previous commander's color doesn't linger while the artwork-derived + // scheme is computed asynchronously. + colorScheme = widget.card.colorSchemes(widget.appTheme).first; + } + if (cardChanged || + oldWidget.variant != widget.variant || oldWidget.contrast != widget.contrast || - widget.appTheme.brightness != oldWidget.appTheme.brightness || - (oldWidget.card.id != widget.card.id)) { + widget.appTheme.brightness != oldWidget.appTheme.brightness) { _updateColorScheme(); } }