- 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>
194 lines
5.8 KiB
TypeScript
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;
|
|
}
|