221 lines
6.9 KiB
Dart
221 lines
6.9 KiB
Dart
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<int, int> 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<double>? 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<Shot> 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<double>? ringRadii, // For concentric with individual ring radii
|
|
}) {
|
|
final scoreDistribution = <int, int>{};
|
|
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<double>? 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);
|
|
}
|
|
}
|
|
}
|