premier app version beta

This commit is contained in:
2026-01-18 13:38:09 +01:00
commit 031d4a4e17
164 changed files with 13698 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
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);
}
}
}