diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java b/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java index 4e94d94..e308315 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/GameActivity.java @@ -28,7 +28,6 @@ import androidx.preference.PreferenceManager; import com.aldo.apps.ochecompanion.database.DatabaseHelper; import com.aldo.apps.ochecompanion.database.objects.Player; -import com.aldo.apps.ochecompanion.database.objects.Statistics; import com.aldo.apps.ochecompanion.game.GameManager; import com.aldo.apps.ochecompanion.ui.PlayerStatsView; import com.aldo.apps.ochecompanion.utils.CheckoutEngine; @@ -37,6 +36,7 @@ import com.aldo.apps.ochecompanion.utils.Log; import com.aldo.apps.ochecompanion.utils.SoundEngine; import com.aldo.apps.ochecompanion.utils.UIConstants; import com.google.android.material.button.MaterialButton; +import android.widget.ImageButton; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -72,6 +72,17 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC */ private boolean mAreStatsShown = false; + /** + * Index of the player currently shown in the stats view. + * Used for cycling through players with the navigation arrows. + */ + private int mStatsPlayerIndex = 0; + + /** + * Cached list of all player states for stats navigation. + */ + private List mMatchPlayerStates; + // ======================================================================================== // Game Manager (Singleton Business Logic Handler) // ======================================================================================== @@ -157,6 +168,18 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC */ private PlayerStatsView mStatsView; + /** + * Navigation bar for switching between player stats. + */ + private View mStatsNavBar; + + /** + * Previous/Next buttons and indicator for stats navigation. + */ + private ImageButton mBtnStatsPrev; + private ImageButton mBtnStatsNext; + private TextView mTvStatsPlayerIndicator; + /** * Array of three TextViews showing darts thrown in current turn. */ @@ -277,6 +300,7 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC private void handleBackPressed() { if (mAreStatsShown) { mStatsView.setVisibility(View.GONE); + mStatsNavBar.setVisibility(View.GONE); mShowStatsBtn.setVisibility(View.VISIBLE); mAreStatsShown = false; return; @@ -314,10 +338,34 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC mShowStatsBtn = findViewById(R.id.show_stats_btn); mStatsView = findViewById(R.id.player_stats_view); + + // Stats navigation bar + mStatsNavBar = findViewById(R.id.stats_nav_bar); + mBtnStatsPrev = findViewById(R.id.btn_stats_prev); + mBtnStatsNext = findViewById(R.id.btn_stats_next); + mTvStatsPlayerIndicator = findViewById(R.id.tv_stats_player_indicator); + mShowStatsBtn.setOnClickListener(v -> { mStatsView.setVisibility(View.VISIBLE); mShowStatsBtn.setVisibility(View.GONE); + mStatsNavBar.setVisibility(View.VISIBLE); mAreStatsShown = true; + showStatsForPlayer(mStatsPlayerIndex); + }); + + mBtnStatsPrev.setOnClickListener(v -> { + if (mMatchPlayerStates != null && !mMatchPlayerStates.isEmpty()) { + mStatsPlayerIndex = (mStatsPlayerIndex - 1 + mMatchPlayerStates.size()) + % mMatchPlayerStates.size(); + showStatsForPlayer(mStatsPlayerIndex); + } + }); + + mBtnStatsNext.setOnClickListener(v -> { + if (mMatchPlayerStates != null && !mMatchPlayerStates.isEmpty()) { + mStatsPlayerIndex = (mStatsPlayerIndex + 1) % mMatchPlayerStates.size(); + showStatsForPlayer(mStatsPlayerIndex); + } }); mSubmitTurnBtn.setOnClickListener(v -> submitTurn()); @@ -542,24 +590,47 @@ public class GameActivity extends BaseActivity implements GameManager.GameStateC } /** - * Retrieves the winning player's statistics from the database and binds them to - * the stats view. - * Runs database query on a background thread to avoid blocking the UI. + * Caches all player states from the completed match and shows stats for the + * winner (first player in the list, index 0, which is typically the active + * player at match end). The stats view will show match-specific stats. */ private void attachPlayerStats() { - new Thread(() -> { - try { - final GameManager.PlayerState activePlayer = mGameManager.getActivePlayer(); - if (activePlayer == null) - return; + mMatchPlayerStates = mGameManager.getPlayerStates(); + if (mMatchPlayerStates == null || mMatchPlayerStates.isEmpty()) { + Log.e(TAG, "attachPlayerStats: No player states available"); + return; + } - final Player player = activePlayer.player; - final Statistics statistics = mDatabaseHelper.getStatisticsForPlayer(player.id); - runOnUiThread(() -> mStatsView.bind(player, statistics)); - } catch (Exception e) { - Log.e(TAG, "attachPlayerStats: Failed to retrieve player statistics", e); + // Find the winner (remainingScore == 0) and start with them + mStatsPlayerIndex = 0; + for (int i = 0; i < mMatchPlayerStates.size(); i++) { + if (mMatchPlayerStates.get(i).remainingScore == 0) { + mStatsPlayerIndex = i; + break; } - }).start(); + } + + showStatsForPlayer(mStatsPlayerIndex); + } + + /** + * Binds match-specific stats for the player at the given index to the stats + * view. + * Updates the navigation indicator to show current position (e.g., "1 / 3"). + * + * @param index Index of the player in {@link #mMatchPlayerStates} + */ + private void showStatsForPlayer(final int index) { + if (mMatchPlayerStates == null || index < 0 || index >= mMatchPlayerStates.size()) { + return; + } + + final GameManager.PlayerState playerState = mMatchPlayerStates.get(index); + mStatsView.bindMatchStats(playerState); + + // Update navigation indicator + mTvStatsPlayerIndicator.setText( + String.format("%s (%d / %d)", playerState.name, index + 1, mMatchPlayerStates.size())); } /** diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/database/DatabaseHelper.java b/app/src/main/java/com/aldo/apps/ochecompanion/database/DatabaseHelper.java index e8c314c..2b34b09 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/database/DatabaseHelper.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/database/DatabaseHelper.java @@ -351,7 +351,8 @@ public class DatabaseHelper { player.id, player.username, startingScore, // Initial score equals starting score - 0 // No darts thrown yet + 0, // No darts thrown yet + 0, 0, 0, 0, 0, 0, 0, 0, null // Per-match stats start at zero )); } } else { @@ -360,7 +361,9 @@ public class DatabaseHelper { 0L, // Guest has ID 0 "GUEST", startingScore, - 0)); + 0, // No darts thrown yet + 0, 0, 0, 0, 0, 0, 0, 0, null // Per-match stats start at zero + )); } // Convert to JSON diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/game/GameManager.java b/app/src/main/java/com/aldo/apps/ochecompanion/game/GameManager.java index 208713f..9fe997f 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/game/GameManager.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/game/GameManager.java @@ -8,10 +8,13 @@ import com.aldo.apps.ochecompanion.database.objects.Player; import com.aldo.apps.ochecompanion.utils.DartsConstants; import com.aldo.apps.ochecompanion.utils.Log; import com.aldo.apps.ochecompanion.utils.MatchProgress; +import com.aldo.apps.ochecompanion.utils.converters.HitDistributionConverter; import com.aldo.apps.ochecompanion.utils.converters.MatchProgressConverter; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * GameManager: Singleton manager for handling all X01 game business logic. @@ -195,7 +198,7 @@ public class GameManager { /** * State holder for a single player's X01 game progress. - * Tracks current match state for an individual player. + * Tracks current match state and per-match statistics for an individual player. */ public static class PlayerState { /** @@ -228,6 +231,35 @@ public class GameManager { */ public int dartsThrown = 0; + // ---- Per-Match Statistics ---- + + /** Total points scored in this match (for match average calculation). */ + public int matchPointsScored = 0; + + /** Number of busted turns in this match. */ + public int matchBustCount = 0; + + /** Highest single-turn score in this match. */ + public int matchHighestTurnScore = 0; + + /** Checkout value if this player won, 0 otherwise. */ + public int matchCheckoutValue = 0; + + /** Count of turns scoring 60+ points in this match. */ + public int matchCount60Plus = 0; + + /** Count of turns scoring 100+ points in this match. */ + public int matchCount100Plus = 0; + + /** Count of turns scoring 140+ points in this match. */ + public int matchCount140Plus = 0; + + /** Count of 180s scored in this match. */ + public int matchCount180 = 0; + + /** Per-match dart hit distribution for heatmap rendering. */ + public Map matchHitDistribution = new HashMap<>(); + /** * Constructs a PlayerState for a player with the specified starting score. * @@ -240,6 +272,28 @@ public class GameManager { this.name = player.username; this.remainingScore = startScore; } + + /** + * Returns the match average (points per 3 darts) for this match. + * + * @return Match average, or 0.0 if no darts have been thrown + */ + public double getMatchAverage() { + if (dartsThrown == 0) + return 0.0; + return (double) matchPointsScored / dartsThrown * 3; + } + + /** + * Records a dart hit to the per-match hit distribution map. + * + * @param baseValue The dartboard number (1-20 or 25 for bull) + * @param multiplier The multiplier (1=single, 2=double, 3=triple) + */ + public void recordMatchDartHit(final int baseValue, final int multiplier) { + final String key = HitDistributionConverter.getSegmentKey(baseValue, multiplier); + matchHitDistribution = HitDistributionConverter.recordHit(matchHitDistribution, key); + } } /** @@ -407,12 +461,25 @@ public class GameManager { // Restore active player index mActivePlayerIndex = progress.activePlayerIndex; - // Restore player scores and darts thrown + // Restore player scores, darts, and per-match stats for (int i = 0; i < progress.players.size() && i < mPlayerStates.size(); i++) { MatchProgress.PlayerStateSnapshot snapshot = progress.players.get(i); PlayerState state = mPlayerStates.get(i); state.remainingScore = snapshot.remainingScore; state.dartsThrown = snapshot.dartsThrown; + + // Restore per-match stats + state.matchPointsScored = snapshot.matchPointsScored; + state.matchBustCount = snapshot.matchBustCount; + state.matchHighestTurnScore = snapshot.matchHighestTurnScore; + state.matchCheckoutValue = snapshot.matchCheckoutValue; + state.matchCount60Plus = snapshot.matchCount60Plus; + state.matchCount100Plus = snapshot.matchCount100Plus; + state.matchCount140Plus = snapshot.matchCount140Plus; + state.matchCount180 = snapshot.matchCount180; + state.matchHitDistribution = snapshot.matchHitDistribution != null + ? new HashMap<>(snapshot.matchHitDistribution) + : new HashMap<>(); } Log.d(TAG, "loadMatchProgress: Match progress loaded successfully"); @@ -503,12 +570,32 @@ public class GameManager { winner.remainingScore = 0; winner.dartsThrown += dartsThrown; - // Update statistics + // Accumulate per-match stats for the winning turn + winner.matchPointsScored += pointsMade; + winner.matchCheckoutValue = checkoutValue; + if (pointsMade > winner.matchHighestTurnScore) { + winner.matchHighestTurnScore = pointsMade; + } + if (pointsMade >= 60) + winner.matchCount60Plus++; + if (pointsMade >= 100) + winner.matchCount100Plus++; + if (pointsMade >= 140) + winner.matchCount140Plus++; + if (pointsMade == 180) + winner.matchCount180++; + + // Update career statistics updatePlayerStats(winner, dartsThrown, pointsMade, false, checkoutValue); - // Record dart hits + // Record dart hits to career stats (async DB) recordTurnHitsToStatistics(winner, new ArrayList<>(mCurrentTurnDartHits)); + // Record dart hits to per-match distribution (local) + for (final DartHit hit : mCurrentTurnDartHits) { + winner.recordMatchDartHit(hit.baseValue, hit.multiplier); + } + // Increment matches played for all players incrementMatchesPlayed(); @@ -557,13 +644,35 @@ public class GameManager { if (!isBust) { active.remainingScore = finalScore; active.dartsThrown += mCurrentTurnDarts.size(); + + // Accumulate per-match stats + active.matchPointsScored += turnTotal; + if (turnTotal > active.matchHighestTurnScore) { + active.matchHighestTurnScore = turnTotal; + } + if (turnTotal >= 60) + active.matchCount60Plus++; + if (turnTotal >= 100) + active.matchCount100Plus++; + if (turnTotal >= 140) + active.matchCount140Plus++; + if (turnTotal == 180) + active.matchCount180++; + } else { + // Track bust in per-match stats + active.matchBustCount++; } updatePlayerStats(active, mCurrentTurnDarts.size(), turnTotal, isBust); - // Record dart hits + // Record dart hits to career stats (async DB) recordTurnHitsToStatistics(active, new ArrayList<>(mCurrentTurnDartHits)); + // Record dart hits to per-match distribution (local, even on bust for tracking) + for (final DartHit hit : mCurrentTurnDartHits) { + active.recordMatchDartHit(hit.baseValue, hit.multiplier); + } + // Rotate to next player mActivePlayerIndex = (mActivePlayerIndex + 1) % mPlayerStates.size(); @@ -628,7 +737,16 @@ public class GameManager { state.playerId, state.name, state.remainingScore, - state.dartsThrown)); + state.dartsThrown, + state.matchPointsScored, + state.matchBustCount, + state.matchHighestTurnScore, + state.matchCheckoutValue, + state.matchCount60Plus, + state.matchCount100Plus, + state.matchCount140Plus, + state.matchCount180, + state.matchHitDistribution)); } String progressJson = MatchProgressConverter.fromProgress(progress); @@ -680,7 +798,16 @@ public class GameManager { state.playerId, state.name, state.remainingScore, - state.dartsThrown)); + state.dartsThrown, + state.matchPointsScored, + state.matchBustCount, + state.matchHighestTurnScore, + state.matchCheckoutValue, + state.matchCount60Plus, + state.matchCount100Plus, + state.matchCount140Plus, + state.matchCount180, + state.matchHitDistribution)); } match.participantData = MatchProgressConverter.fromProgress(finalProgress); diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/HeatmapView.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/HeatmapView.java index 421f6ce..e60e1d2 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/HeatmapView.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/HeatmapView.java @@ -22,9 +22,12 @@ import java.util.Map; * HeatmapView: A custom high-performance rendering component that draws a * dartboard and overlays player performance data as a color-coded heatmap. *

- * This custom View renders a professional dartboard visualization with color-coded - * performance data overlaid on each segment (singles, doubles, triples, and bulls). - * The heatmap uses a gradient from "cold" (low frequency) to "hot" (high frequency) + * This custom View renders a professional dartboard visualization with + * color-coded + * performance data overlaid on each segment (singles, doubles, triples, and + * bulls). + * The heatmap uses a gradient from "cold" (low frequency) to "hot" (high + * frequency) * based on how often a player hits each segment. *

* Optimized Palette: @@ -46,7 +49,7 @@ public class HeatmapView extends View { * Reused across all drawing operations for efficiency. */ private final Paint mBasePaint = new Paint(Paint.ANTI_ALIAS_FLAG); - + /** * Color evaluator for interpolating between cold (green) and hot (red) colors. * Evaluates intermediate colors based on normalized hit frequency (0.0 to 1.0). @@ -58,28 +61,29 @@ public class HeatmapView extends View { * Calculated from view width in onSizeChanged. */ private float mCenterX; - + /** * Y-coordinate of the dartboard center in pixels. * Calculated from view height in onSizeChanged. */ private float mCenterY; - + /** * Radius of the dartboard in pixels. * Calculated as 95% of the minimum dimension (width or height) to leave margin. * All segment boundaries are defined as factors of this radius. */ private float mRadius; - + /** * Cache of pre-calculated Path objects for all dartboard segments. * Key format: "d" + number (doubles), "t" + number (triples), - * "s" + number + "_inner/outer" (singles), "sb" (single bull), "db" (double bull). + * "s" + number + "_inner/outer" (singles), "sb" (single bull), "db" (double + * bull). * Calculated once in calculatePaths() and reused for efficient rendering. */ private final Map mSegmentPaths = new HashMap<>(); - + /** * Player statistics object containing hit distribution data. * Used to determine heatmap colors for each segment. @@ -87,6 +91,12 @@ public class HeatmapView extends View { */ private Statistics mStats; + /** + * Raw hit distribution map. Populated by either {@link #setStats(Statistics)} + * or {@link #setHitDistribution(Map)} for per-match rendering. + */ + private Map mHitDistribution; + /** * Standard dartboard segment order (clockwise starting from 20 at the top). * This array defines the physical layout of numbers around the dartboard @@ -100,19 +110,21 @@ public class HeatmapView extends View { * Constructs a new HeatmapView programmatically. * Used when creating the view from code rather than XML inflation. * - * @param context The Context the view is running in, through which it can access resources + * @param context The Context the view is running in, through which it can + * access resources */ public HeatmapView(final Context context) { super(context); init(); } - + /** * Constructs a new HeatmapView from XML. * Used when inflating the view from an XML layout. * - * @param context The Context the view is running in, through which it can access resources - * @param attrs The attributes of the XML tag that is inflating the view + * @param context The Context the view is running in, through which it can + * access resources + * @param attrs The attributes of the XML tag that is inflating the view */ public HeatmapView(final Context context, @Nullable final AttributeSet attrs) { super(context, attrs); @@ -132,6 +144,19 @@ public class HeatmapView extends View { */ public void setStats(final Statistics stats) { this.mStats = stats; + this.mHitDistribution = stats != null ? stats.getHitDistribution() : null; + invalidate(); + } + + /** + * Binds a raw hit distribution map to the view and triggers a redraw. + * Used for per-match heatmap rendering where no full Statistics object exists. + * + * @param hitDistribution Map of segment keys (e.g. "t20", "s5") to hit counts + */ + public void setHitDistribution(final Map hitDistribution) { + this.mStats = null; + this.mHitDistribution = hitDistribution; invalidate(); } @@ -140,8 +165,8 @@ public class HeatmapView extends View { * Recalculates the dartboard geometry (center point and radius) and regenerates * all segment paths based on the new dimensions. * - * @param w Current width of this view - * @param h Current height of this view + * @param w Current width of this view + * @param h Current height of this view * @param oldw Old width of this view (before size change) * @param oldh Old height of this view (before size change) */ @@ -157,7 +182,8 @@ public class HeatmapView extends View { } /** - * Calculates the Path objects for every segment on the dartboard based on current view size. + * Calculates the Path objects for every segment on the dartboard based on + * current view size. *

* Clears the existing path cache and regenerates all paths for: * - 20 double ring segments (90-100% radius) @@ -197,33 +223,39 @@ public class HeatmapView extends View { } /** - * Creates a closed Path representing a wedge-shaped arc segment of the dartboard. + * Creates a closed Path representing a wedge-shaped arc segment of the + * dartboard. *

* Generates a path for a single dartboard segment (e.g., the triple 20 region) - * by drawing an arc along the outer boundary, then an arc along the inner boundary + * by drawing an arc along the outer boundary, then an arc along the inner + * boundary * in reverse, creating a closed wedge shape. *

* The path is constructed by: * 1. Drawing an outer arc from startAngle to startAngle + sweep - * 2. Drawing an inner arc from startAngle + sweep back to startAngle (negative sweep) + * 2. Drawing an inner arc from startAngle + sweep back to startAngle (negative + * sweep) * 3. Closing the path to connect the endpoints * - * @param innerFactor Ratio of inner radius to base radius (0.0 to 1.0). E.g., 0.50 = 50% of radius - * @param outerFactor Ratio of outer radius to base radius (0.0 to 1.0). E.g., 1.00 = 100% of radius - * @param startAngle Starting angle in degrees (0° = right, increasing clockwise) - * @param sweep Angular sweep in degrees (typically 18° for standard dartboard segments) + * @param innerFactor Ratio of inner radius to base radius (0.0 to 1.0). E.g., + * 0.50 = 50% of radius + * @param outerFactor Ratio of outer radius to base radius (0.0 to 1.0). E.g., + * 1.00 = 100% of radius + * @param startAngle Starting angle in degrees (0° = right, increasing + * clockwise) + * @param sweep Angular sweep in degrees (typically 18° for standard + * dartboard segments) * @return A closed Path object representing the arc segment */ - private Path createArcPath(final float innerFactor, final float outerFactor, final float startAngle, final float sweep) { + private Path createArcPath(final float innerFactor, final float outerFactor, final float startAngle, + final float sweep) { final Path path = new Path(); final RectF outerRect = new RectF( mCenterX - mRadius * outerFactor, mCenterY - mRadius * outerFactor, - mCenterX + mRadius * outerFactor, mCenterY + mRadius * outerFactor - ); + mCenterX + mRadius * outerFactor, mCenterY + mRadius * outerFactor); final RectF innerRect = new RectF( mCenterX - mRadius * innerFactor, mCenterY - mRadius * innerFactor, - mCenterX + mRadius * innerFactor, mCenterY + mRadius * innerFactor - ); + mCenterX + mRadius * innerFactor, mCenterY + mRadius * innerFactor); path.arcTo(outerRect, startAngle, sweep); path.arcTo(innerRect, startAngle + sweep, -sweep); @@ -238,10 +270,11 @@ public class HeatmapView extends View { * 1. Early exit if no statistics are loaded * 2. Resolve color palette from resources (cold/hot) and hardcoded (empty) * 3. For each dartboard segment: - * - Determine hit count from statistics - * - If zero hits: use subtle ghost color - * - If hits exist: interpolate between cold and hot based on normalized frequency - * - Draw the filled path with the calculated color + * - Determine hit count from statistics + * - If zero hits: use subtle ghost color + * - If hits exist: interpolate between cold and hot based on normalized + * frequency + * - Draw the filled path with the calculated color * 4. Draw wireframe overlay on all segments for visual definition *

* Color mapping: @@ -255,7 +288,15 @@ public class HeatmapView extends View { @Override protected void onDraw(@NonNull final Canvas canvas) { super.onDraw(canvas); - if (mStats == null) return; + if (mHitDistribution == null) + return; + + // Calculate max hits for normalization + int maxHits = 0; + for (final Integer count : mHitDistribution.values()) { + if (count != null && count > maxHits) + maxHits = count; + } // Resolve branding colors from resources final int coldColor = ContextCompat.getColor(getContext(), R.color.volt_green); @@ -268,14 +309,14 @@ public class HeatmapView extends View { final String statsKey = key.contains("_") ? key.substring(0, key.indexOf("_")) : key; // Check if there are any hits recorded for this segment - final Integer hitCount = mStats.getHitDistribution().get(statsKey); + final Integer hitCount = mHitDistribution.get(statsKey); final int color; if (hitCount == null || hitCount == 0) { color = emptyColor; } else { - // Fetch the normalized heat (0.0 to 1.0) and evaluate against Green -> Red gradient - final float weight = mStats.getNormalizedWeight(statsKey); + // Normalize hit count against max and interpolate Green -> Red + final float weight = maxHits > 0 ? (float) hitCount / maxHits : 0f; color = (int) mColorEvaluator.evaluate(weight, coldColor, hotColor); } diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerItemView.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerItemView.java index 646d06d..32d1182 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerItemView.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerItemView.java @@ -124,17 +124,15 @@ public class PlayerItemView extends MaterialCardView { * @param player The Player object containing data to display * @param gameAverage The game average to display (points per 3 darts) * @param position The player's position (1 = 1st place, 2 = 2nd place, etc.) - * @param isWinner Whether this player won (remaining score = 0) */ public void bindWithGameAverageAndPosition(@NonNull final Player player, final double gameAverage, - final int position, final boolean isWinner) { + final int position) { Log.d(TAG, "bindWithGameAverageAndPosition() called with: player = [" + player - + "], gameAverage = [" + gameAverage + "], position = [" + position + "], isWinner = [" + isWinner - + "]"); + + "], gameAverage = [" + gameAverage + "], position = [" + position + "]"); // Build username with position indicator final String positionSuffix = getPositionSuffix(position); - final String displayName = isWinner + final String displayName = position == 1 ? "🏆 " + player.username + " (" + positionSuffix + ")" : player.username + " (" + positionSuffix + ")"; diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerStatsView.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerStatsView.java index 422a582..a67b03a 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerStatsView.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/PlayerStatsView.java @@ -4,6 +4,7 @@ import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; +import android.view.View; import android.widget.ScrollView; import android.widget.TextView; import androidx.annotation.NonNull; @@ -11,6 +12,7 @@ import androidx.annotation.Nullable; import com.aldo.apps.ochecompanion.R; import com.aldo.apps.ochecompanion.database.objects.Player; import com.aldo.apps.ochecompanion.database.objects.Statistics; +import com.aldo.apps.ochecompanion.game.GameManager; import com.bumptech.glide.Glide; import com.google.android.material.imageview.ShapeableImageView; @@ -18,17 +20,20 @@ import com.google.android.material.imageview.ShapeableImageView; * PlayerStatsView: A complete dashboard component that visualizes a player's * career performance, including a heatmap and detailed metrics. *

- * This custom {@link ScrollView} component provides a comprehensive player statistics + * This custom {@link ScrollView} component provides a comprehensive player + * statistics * display that includes: *

*

* The view is inflated from a layout resource and automatically initializes all - * UI components. Data binding is performed via the {@link #bind(Player, Statistics)} + * UI components. Data binding is performed via the + * {@link #bind(Player, Statistics)} * method, which populates all fields with formatted player statistics. * * @see HeatmapView @@ -49,6 +54,11 @@ public class PlayerStatsView extends ScrollView { */ private HeatmapView mHeatmap; + /** + * The label above the heatmap, hidden when showing match stats. + */ + private TextView mTvHeatmapLabel; + /** * The circular profile image view that displays the player's avatar. * Shows either a custom profile picture or a default user icon. @@ -85,23 +95,29 @@ public class PlayerStatsView extends ScrollView { */ private TextView mTvBestFinish; - /** - * The {@link TextView} displaying the count of turns scoring 60 points or more. - */ + // Dynamic labels for metric cards (can switch between career/match mode) + private TextView mTvCareerAvgLabel; + private TextView mTvFirst9Label; + private TextView mTvCheckoutPctLabel; + private TextView mTvBestFinishLabel; + private TextView mTvCount60; /** - * The {@link TextView} displaying the count of turns scoring 100 points or more (century). + * The {@link TextView} displaying the count of turns scoring 100 points or more + * (century). */ private TextView mTvCount100; /** - * The {@link TextView} displaying the count of turns scoring 140 points or more. + * The {@link TextView} displaying the count of turns scoring 140 points or + * more. */ private TextView mTvCount140; /** - * The {@link TextView} displaying the count of perfect 180-point turns (maximum score). + * The {@link TextView} displaying the count of perfect 180-point turns (maximum + * score). */ private TextView mTvCount180; @@ -110,14 +126,17 @@ public class PlayerStatsView extends ScrollView { } /** - * Constructs a new {@link PlayerStatsView} with the specified context and attributes. + * Constructs a new {@link PlayerStatsView} with the specified context and + * attributes. *

- * This constructor inflates the player stats layout and initializes all UI components. + * This constructor inflates the player stats layout and initializes all UI + * components. * It performs the following operations: *

    - *
  1. Calls the superclass constructor with context and attributes
  2. - *
  3. Inflates the {@code R.layout.player_stats_layout} resource into this view
  4. - *
  5. Initializes all child views via {@link #initViews()}
  6. + *
  7. Calls the superclass constructor with context and attributes
  8. + *
  9. Inflates the {@code R.layout.player_stats_layout} resource into this + * view
  10. + *
  11. Initializes all child views via {@link #initViews()}
  12. *
*

* This constructor is typically invoked when the view is inflated from XML. @@ -134,15 +153,19 @@ public class PlayerStatsView extends ScrollView { } /** - * Initializes all UI component references by finding views in the inflated layout. + * Initializes all UI component references by finding views in the inflated + * layout. *

* This method binds the following UI components: *

*

* This method is called once during construction and establishes references @@ -150,6 +173,7 @@ public class PlayerStatsView extends ScrollView { */ private void initViews() { mHeatmap = findViewById(R.id.statsHeatmap); + mTvHeatmapLabel = findViewById(R.id.tvHeatmapLabel); mIvAvatar = findViewById(R.id.ivPlayerAvatar); mTvUsername = findViewById(R.id.tvUsername); mTvCareerAvg = findViewById(R.id.tvCareerAvgValue); @@ -157,6 +181,12 @@ public class PlayerStatsView extends ScrollView { mTvCheckoutPct = findViewById(R.id.tvCheckoutPctValue); mTvBestFinish = findViewById(R.id.tvBestFinishValue); + // Dynamic labels + mTvCareerAvgLabel = findViewById(R.id.tvCareerAvgLabel); + mTvFirst9Label = findViewById(R.id.tvFirst9Label); + mTvCheckoutPctLabel = findViewById(R.id.tvCheckoutPctLabel); + mTvBestFinishLabel = findViewById(R.id.tvBestFinishLabel); + // Threshold counters mTvCount60 = findViewById(R.id.tvCount60); mTvCount100 = findViewById(R.id.tvCount100); @@ -167,29 +197,45 @@ public class PlayerStatsView extends ScrollView { /** * Binds both the player identity and their accumulated stats to the UI. *

- * This method populates all UI components with the provided player and statistics data. + * This method populates all UI components with the provided player and + * statistics data. * The binding process is organized into four logical sections: *

    - *
  1. Identity: Displays the player's username in uppercase and loads their - * profile picture using Glide (or shows a default icon if no picture is set)
  2. - *
  3. High-Level Metrics: Formats and displays career average, first-9 average, - * checkout percentage (with % symbol), and highest checkout value
  4. - *
  5. Threshold Totals: Displays counts for 60+, 100+, 140+, and 180 point turns
  6. - *
  7. Heatmap Rendering: Passes statistics to the {@link HeatmapView} for - * visual dartboard representation
  8. + *
  9. Identity: Displays the player's username in uppercase and loads + * their + * profile picture using Glide (or shows a default icon if no picture is + * set)
  10. + *
  11. High-Level Metrics: Formats and displays career average, first-9 + * average, + * checkout percentage (with % symbol), and highest checkout value
  12. + *
  13. Threshold Totals: Displays counts for 60+, 100+, 140+, and 180 + * point turns
  14. + *
  15. Heatmap Rendering: Passes statistics to the {@link HeatmapView} + * for + * visual dartboard representation
  16. *
*

- * If either parameter is {@code null}, the method logs an error and returns early + * If either parameter is {@code null}, the method logs an error and returns + * early * without modifying the UI. * - * @param player The {@link Player} object containing identity information (username, profile picture) - * @param stats The {@link Statistics} object containing all accumulated performance metrics + * @param player The {@link Player} object containing identity information + * (username, profile picture) + * @param stats The {@link Statistics} object containing all accumulated + * performance metrics */ public void bind(@NonNull final Player player, @NonNull final Statistics stats) { if (player == null || stats == null) { Log.e(TAG, "bind: Cannot bind, return"); return; } + + // Restore labels to career mode + mTvCareerAvgLabel.setText("Career Avg"); + mTvFirst9Label.setText("First 9 Avg"); + mTvCheckoutPctLabel.setText("Checkout %"); + mTvBestFinishLabel.setText("High Finish"); + // 1. Identity mTvUsername.setText(player.username.toUpperCase()); if (player.profilePictureUri != null) { @@ -211,6 +257,66 @@ public class PlayerStatsView extends ScrollView { mTvCount180.setText(String.valueOf(stats.getCount180())); // 4. Heatmap Rendering + mTvHeatmapLabel.setText("Impact Heatmap"); + mTvHeatmapLabel.setVisibility(View.VISIBLE); + mHeatmap.setVisibility(View.VISIBLE); mHeatmap.setStats(stats); } + + /** + * Binds match-specific statistics from a {@link GameManager.PlayerState} to the + * UI. + *

+ * Unlike {@link #bind(Player, Statistics)} which shows career stats, this + * method + * shows stats from the just-completed match only. Labels are updated to reflect + * match context (e.g., "Match Avg" instead of "Career Avg"). + * The heatmap is hidden since per-match dart distribution is not tracked. + * + * @param playerState The player's match state containing per-match statistics + */ + public void bindMatchStats(@NonNull final GameManager.PlayerState playerState) { + if (playerState == null) { + Log.e(TAG, "bindMatchStats: Cannot bind null playerState"); + return; + } + + final Player player = playerState.player; + + // 1. Identity + mTvUsername.setText(player.username.toUpperCase()); + if (player.profilePictureUri != null) { + Glide.with(getContext()).load(player.profilePictureUri) + .placeholder(R.drawable.ic_users) + .into(mIvAvatar); + } else { + mIvAvatar.setImageResource(R.drawable.ic_users); + } + + // 2. Switch labels to match mode + mTvCareerAvgLabel.setText("Match Avg"); + mTvFirst9Label.setText("Darts Thrown"); + mTvCheckoutPctLabel.setText("Checkout"); + mTvBestFinishLabel.setText("High Turn"); + + // 3. Match-specific metrics + mTvCareerAvg.setText(String.format("%.1f", playerState.getMatchAverage())); + mTvFirst9.setText(String.valueOf(playerState.dartsThrown)); + mTvCheckoutPct.setText(playerState.matchCheckoutValue > 0 + ? String.valueOf(playerState.matchCheckoutValue) + : "–"); + mTvBestFinish.setText(String.valueOf(playerState.matchHighestTurnScore)); + + // 4. Threshold Totals (match-specific) + mTvCount60.setText(String.valueOf(playerState.matchCount60Plus)); + mTvCount100.setText(String.valueOf(playerState.matchCount100Plus)); + mTvCount140.setText(String.valueOf(playerState.matchCount140Plus)); + mTvCount180.setText(String.valueOf(playerState.matchCount180)); + + // 5. Heatmap — render per-match dart hit distribution + mTvHeatmapLabel.setVisibility(View.VISIBLE); + mTvHeatmapLabel.setText("Match Heatmap"); + mHeatmap.setVisibility(View.VISIBLE); + mHeatmap.setHitDistribution(playerState.matchHitDistribution); + } } \ No newline at end of file diff --git a/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuGroupMatchAdapter.java b/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuGroupMatchAdapter.java index 8605fda..81b949c 100644 --- a/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuGroupMatchAdapter.java +++ b/app/src/main/java/com/aldo/apps/ochecompanion/ui/adapter/MainMenuGroupMatchAdapter.java @@ -176,7 +176,7 @@ public class MainMenuGroupMatchAdapter extends RecyclerView.Adapter matchHitDistribution; + /** Default constructor required for serialization/deserialization. */ - public PlayerStateSnapshot() {} + public PlayerStateSnapshot() { + } /** - * Constructs a new PlayerStateSnapshot with the specified values. - * - * @param playerId The unique ID of the player (0 for guests) - * @param name The display name of the player - * @param remainingScore The player's current remaining score in the match - * @param dartsThrown The total number of darts thrown by the player so far + * Constructs a new PlayerStateSnapshot from a full set of match state values. */ - public PlayerStateSnapshot(final long playerId, final String name, final int remainingScore, final int dartsThrown) { + public PlayerStateSnapshot(final long playerId, final String name, + final int remainingScore, final int dartsThrown, + final int matchPointsScored, final int matchBustCount, + final int matchHighestTurnScore, final int matchCheckoutValue, + final int matchCount60Plus, final int matchCount100Plus, + final int matchCount140Plus, final int matchCount180, + final Map matchHitDistribution) { this.playerId = playerId; this.name = name; this.remainingScore = remainingScore; this.dartsThrown = dartsThrown; + this.matchPointsScored = matchPointsScored; + this.matchBustCount = matchBustCount; + this.matchHighestTurnScore = matchHighestTurnScore; + this.matchCheckoutValue = matchCheckoutValue; + this.matchCount60Plus = matchCount60Plus; + this.matchCount100Plus = matchCount100Plus; + this.matchCount140Plus = matchCount140Plus; + this.matchCount180 = matchCount180; + this.matchHitDistribution = matchHitDistribution != null + ? new HashMap<>(matchHitDistribution) + : new HashMap<>(); } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_chevron_left.xml b/app/src/main/res/drawable/ic_chevron_left.xml new file mode 100644 index 0000000..eed000d --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_left.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/layout/activity_game.xml b/app/src/main/res/layout/activity_game.xml index 9c06492..b6232b6 100644 --- a/app/src/main/res/layout/activity_game.xml +++ b/app/src/main/res/layout/activity_game.xml @@ -210,4 +210,47 @@ android:layout_height="match_parent" android:visibility="gone"/> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_stats_layout.xml b/app/src/main/res/layout/player_stats_layout.xml index 2331e60..56bac5e 100644 --- a/app/src/main/res/layout/player_stats_layout.xml +++ b/app/src/main/res/layout/player_stats_layout.xml @@ -53,6 +53,7 @@ - + - + @@ -93,12 +94,12 @@ - + - +