space-game/src/game/scoreCalculator.ts
Michael Mainguy 749cc18211 Refactor scoring system to additive model starting at 0
- Replace multiplier-based scoring with additive system
- Score builds from asteroid destruction based on size and timing
- Small asteroids (<10 scale): 1000 pts, Medium (10-20): 500 pts, Large (>20): 250 pts
- Timing multiplier: 3x in first 1/3 of par time, 2x in middle, 1x in last third
- End-game bonuses only applied at game end (hull, fuel, accuracy)
- Add scale property to ScoreEvent for point calculation
- Update status screen to show "CURRENT SCORE" during play, "FINAL SCORE" at end
- Refactor star ratings display into individual columns
- Fix button clipping on hover with clipChildren = false
- Add reusable button hover effects utility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 16:39:27 -06:00

194 lines
5.8 KiB
TypeScript

/**
* Score calculation system for space shooter game
* Additive scoring: starts at 0, builds through asteroid destruction and end-game bonuses
*/
// Bonus constants
const MAX_HULL_BONUS = 5000;
const MAX_FUEL_BONUS = 5000;
const MAX_ACCURACY_BONUS = 10000;
/**
* Star rating levels (0-3 stars per category)
*/
interface StarRatings {
asteroids: number; // 0-3 stars based on asteroid destruction timing
accuracy: number; // 0-3 stars based on shot accuracy
fuel: number; // 0-3 stars based on fuel efficiency
hull: number; // 0-3 stars based on hull integrity
total: number; // Sum of all star ratings (0-12)
}
/**
* End-game bonus breakdown
*/
export interface EndGameBonuses {
hull: number;
fuel: number;
accuracy: number;
}
/**
* Complete score calculation result
*/
export interface ScoreCalculation {
asteroidScore: number; // Points from destroying asteroids
bonuses: EndGameBonuses; // End-game bonuses
finalScore: number; // Total score
stars: StarRatings;
}
/**
* Calculate points for destroying an asteroid
* @param scale - Asteroid scale (size)
* @param elapsedSeconds - Time elapsed since game start
* @param parTime - Expected level completion time
* @returns Points earned for this asteroid
*/
export function calculateAsteroidPoints(
scale: number,
elapsedSeconds: number,
parTime: number
): number {
// Size points: smaller scale = more points
// Small (<10): 1000 pts, Medium (10-20): 500 pts, Large (>20): 250 pts
const sizePoints = scale < 10 ? 1000 : scale <= 20 ? 500 : 250;
// Timing multiplier based on par time progress
const progress = elapsedSeconds / parTime;
const timingMultiplier = progress <= 0.333 ? 3 : progress <= 0.666 ? 2 : 1;
return sizePoints * timingMultiplier;
}
/**
* Calculate end-game bonuses based on performance
* @param hullDamage - Total hull damage taken (0-300+%)
* @param fuelConsumed - Total fuel consumed (0-300+%)
* @param accuracy - Shot accuracy percentage (0-100%)
* @returns Bonus points for each category
*/
export function calculateEndGameBonuses(
hullDamage: number,
fuelConsumed: number,
accuracy: number
): EndGameBonuses {
return {
hull: Math.floor(MAX_HULL_BONUS * Math.max(0, 1 - hullDamage / 300)),
fuel: Math.floor(MAX_FUEL_BONUS * Math.max(0, 1 - fuelConsumed / 300)),
accuracy: Math.floor(MAX_ACCURACY_BONUS * Math.max(0, (accuracy - 1) / 99))
};
}
/**
* Calculate final score with all bonuses
* @param asteroidScore - Running score from asteroid destruction
* @param hullDamage - Hull damage percentage (0-300+%)
* @param fuelConsumed - Fuel consumed percentage (0-300+%)
* @param accuracy - Shot accuracy percentage (0-100%)
* @param includeEndGameBonuses - Whether to include end-game bonuses (only at game end)
* @returns Complete score calculation
*/
export function calculateFinalScore(
asteroidScore: number,
hullDamage: number,
fuelConsumed: number,
accuracy: number,
includeEndGameBonuses: boolean = true
): ScoreCalculation {
const bonuses = includeEndGameBonuses
? calculateEndGameBonuses(hullDamage, fuelConsumed, accuracy)
: { hull: 0, fuel: 0, accuracy: 0 };
const finalScore = asteroidScore + bonuses.hull + bonuses.fuel + bonuses.accuracy;
const stars: StarRatings = {
asteroids: getAsteroidStars(asteroidScore),
accuracy: getAccuracyStars(accuracy),
fuel: getFuelStars(fuelConsumed),
hull: getHullStars(hullDamage),
total: 0
};
stars.total = stars.asteroids + stars.accuracy + stars.fuel + stars.hull;
return {
asteroidScore,
bonuses,
finalScore,
stars
};
}
/**
* Calculate asteroid stars based on score earned
* Note: This is a rough heuristic; actual thresholds may need tuning per level
*/
function getAsteroidStars(asteroidScore: number): number {
// Assumes average ~20,000 pts for good performance
if (asteroidScore >= 25000) return 3;
if (asteroidScore >= 15000) return 2;
if (asteroidScore >= 8000) return 1;
return 0;
}
/**
* Calculate accuracy stars based on hit percentage
* @param accuracy - Shot accuracy percentage (0-100)
* @returns 0-3 stars
*/
function getAccuracyStars(accuracy: number): number {
if (accuracy >= 80) return 3;
if (accuracy >= 50) return 2;
if (accuracy >= 20) return 1;
return 0;
}
/**
* Calculate fuel efficiency stars
* @param fuelConsumed - Fuel consumed percentage (0-300+%)
* @returns 0-3 stars
*/
function getFuelStars(fuelConsumed: number): number {
if (fuelConsumed <= 50) return 3;
if (fuelConsumed <= 150) return 2;
if (fuelConsumed <= 250) return 1;
return 0;
}
/**
* Calculate hull integrity stars
* @param hullDamage - Hull damage percentage (0-300+%)
* @returns 0-3 stars
*/
function getHullStars(hullDamage: number): number {
if (hullDamage <= 30) return 3;
if (hullDamage <= 100) return 2;
if (hullDamage <= 200) return 1;
return 0;
}
/**
* Get star rating color based on count
* @param stars - Number of stars (0-3)
* @returns Hex color code
*/
export function getStarColor(stars: number): string {
switch (stars) {
case 3: return '#FFD700'; // Gold
case 2: return '#C0C0C0'; // Silver
case 1: return '#CD7F32'; // Bronze
default: return '#808080'; // Gray
}
}
/**
* Format stars as Unicode string
* @param earned - Number of stars earned (0-3)
* @param total - Total possible stars (default: 3)
* @returns Unicode star string (e.g., "★★☆")
*/
export function formatStars(earned: number, total: number = 3): string {
const filled = '★'.repeat(Math.min(earned, total));
const empty = '☆'.repeat(Math.max(0, total - earned));
return filled + empty;
}