import 'dart:math' as math; import '../data/models/shot.dart'; import '../data/models/target_type.dart'; import '../core/constants/app_constants.dart'; class ScoreResult { final int totalScore; final int maxPossibleScore; final double percentage; final Map scoreDistribution; // score -> count final int shotCount; ScoreResult({ required this.totalScore, required this.maxPossibleScore, required this.percentage, required this.scoreDistribution, required this.shotCount, }); } class ScoreCalculatorService { /// Calculate score for a single shot on a concentric target /// ringCount determines the number of scoring zones (default 10) /// Center is always 10 (bullseye), each ring decrements by 1 /// Example: 5 rings = 10, 9, 8, 7, 6 /// imageAspectRatio is width/height to account for non-square images int calculateConcentricScore({ required double shotX, required double shotY, required double targetCenterX, required double targetCenterY, required double targetRadius, int ringCount = 10, double imageAspectRatio = 1.0, List? ringRadii, // Optional individual ring radii multipliers }) { final dx = shotX - targetCenterX; final dy = shotY - targetCenterY; // Account for aspect ratio to match visual representation // The visual uses min(width, height) for the radius double normalizedDistance; if (imageAspectRatio >= 1.0) { // Landscape or square: scale x by aspectRatio normalizedDistance = math.sqrt(dx * dx * imageAspectRatio * imageAspectRatio + dy * dy) / targetRadius; } else { // Portrait: scale y by 1/aspectRatio normalizedDistance = math.sqrt(dx * dx + dy * dy / (imageAspectRatio * imageAspectRatio)) / targetRadius; } // Use custom ring radii if provided, otherwise use equal spacing if (ringRadii != null && ringRadii.length == ringCount) { for (int i = 0; i < ringCount; i++) { if (normalizedDistance <= ringRadii[i]) { // Center = 10, decrement by 1 for each ring return 10 - i; } } } else { // Generate dynamic zone radii based on ring count // Each zone has equal width for (int i = 0; i < ringCount; i++) { final zoneRadius = (i + 1) / ringCount; if (normalizedDistance <= zoneRadius) { // Center = 10, decrement by 1 for each ring return 10 - i; } } } return 0; // Outside target } /// Calculate score for a single shot on a silhouette target int calculateSilhouetteScore({ required double shotX, required double shotY, required double targetCenterX, required double targetCenterY, required double targetWidth, required double targetHeight, }) { // Check if shot is within silhouette bounds final relativeX = (shotX - targetCenterX).abs() / (targetWidth / 2); final relativeY = (shotY - (targetCenterY - targetHeight / 2)) / targetHeight; // Outside horizontal bounds if (relativeX > 1.0) return 0; // Check vertical zones (from top of silhouette) if (relativeY < 0) return 0; // Above silhouette if (relativeY <= AppConstants.silhouetteZones['head']!) { // Head zone - 5 points, but narrower if (relativeX <= 0.4) return 5; } else if (relativeY <= AppConstants.silhouetteZones['center']!) { // Center mass - 5 points if (relativeX <= 0.6) return 5; } else if (relativeY <= AppConstants.silhouetteZones['body']!) { // Body - 4 points if (relativeX <= 0.7) return 4; } else if (relativeY <= AppConstants.silhouetteZones['lower']!) { // Lower body - 3 points if (relativeX <= 0.5) return 3; } return 0; // Outside target } /// Calculate scores for all shots ScoreResult calculateScores({ required List shots, required TargetType targetType, required double targetCenterX, required double targetCenterY, double? targetRadius, // For concentric double? targetWidth, // For silhouette double? targetHeight, // For silhouette int ringCount = 10, // For concentric double imageAspectRatio = 1.0, // For concentric List? ringRadii, // For concentric with individual ring radii }) { final scoreDistribution = {}; int totalScore = 0; for (final shot in shots) { int score; if (targetType == TargetType.concentric) { score = calculateConcentricScore( shotX: shot.x, shotY: shot.y, targetCenterX: targetCenterX, targetCenterY: targetCenterY, targetRadius: targetRadius ?? 0.4, ringCount: ringCount, imageAspectRatio: imageAspectRatio, ringRadii: ringRadii, ); } else { score = calculateSilhouetteScore( shotX: shot.x, shotY: shot.y, targetCenterX: targetCenterX, targetCenterY: targetCenterY, targetWidth: targetWidth ?? 0.3, targetHeight: targetHeight ?? 0.7, ); } totalScore += score; scoreDistribution[score] = (scoreDistribution[score] ?? 0) + 1; } final maxScore = targetType == TargetType.concentric ? 10 : 5; final maxPossibleScore = shots.length * maxScore; final percentage = maxPossibleScore > 0 ? (totalScore / maxPossibleScore) * 100 : 0.0; return ScoreResult( totalScore: totalScore, maxPossibleScore: maxPossibleScore, percentage: percentage, scoreDistribution: scoreDistribution, shotCount: shots.length, ); } /// Recalculate a shot's score Shot recalculateShot({ required Shot shot, required TargetType targetType, required double targetCenterX, required double targetCenterY, double? targetRadius, double? targetWidth, double? targetHeight, int ringCount = 10, double imageAspectRatio = 1.0, List? ringRadii, }) { int newScore; if (targetType == TargetType.concentric) { newScore = calculateConcentricScore( shotX: shot.x, shotY: shot.y, targetCenterX: targetCenterX, targetCenterY: targetCenterY, targetRadius: targetRadius ?? 0.4, ringCount: ringCount, imageAspectRatio: imageAspectRatio, ringRadii: ringRadii, ); } else { newScore = calculateSilhouetteScore( shotX: shot.x, shotY: shot.y, targetCenterX: targetCenterX, targetCenterY: targetCenterY, targetWidth: targetWidth ?? 0.3, targetHeight: targetHeight ?? 0.7, ); } return shot.copyWith(score: newScore); } /// Get score color index (0-9 for zone colors) int getScoreColorIndex(int score, TargetType targetType) { if (targetType == TargetType.concentric) { return (10 - score).clamp(0, 9); } else { // Map silhouette scores (0-5) to color indices return ((5 - score) * 2).clamp(0, 9); } } }