All checks were successful
Build / build (push) Successful in 1m35s
Add linear-clamped scoring system that rewards speed, accuracy, fuel efficiency, and hull integrity. Scores are always positive with a 0.5x multiplier floor for refueling/repairs. Scoring Components: - Create scoreCalculator module with configurable scoring logic - Time multiplier: Exponential decay from par time (0.1x to 3.0x) - Accuracy multiplier: Linear 1.0x to 2.0x based on hit percentage - Fuel efficiency: Linear with 0.5x floor (handles refueling >100%) - Hull integrity: Linear with 0.5x floor (handles deaths/repairs >100%) - Star rating system: 0-3 stars per category (12 stars max) Integration: - Add calculateFinalScore() to GameStats - Support parTime in level config metadata - Auto-calculate par time from difficulty level in Level1 - Recruit: 300s, Pilot: 180s, Captain: 120s, Commander: 90s, Test: 60s - Display comprehensive score breakdown on status screen Status Screen Updates: - Increase mesh size from 1.5x1.0m to 1.5x2.25m (portrait orientation) - Increase texture from 1024x768 to 1024x1536 (fit all content) - Add score display section with: - Final score in gold with thousand separators - Score multiplier breakdown for each category - Unicode star ratings (★★★) per category - Total stars earned (X/12) Formula: finalScore = 10,000 × time × accuracy × fuel × hull All multipliers ≥ 0.5, ensuring scores are never negative even with multiple refuels/deaths. System rewards balanced excellence across all performance metrics. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
258 lines
8.2 KiB
TypeScript
258 lines
8.2 KiB
TypeScript
/**
|
|
* Score calculation system for space shooter game
|
|
* Uses linear-clamped multipliers that never go negative
|
|
* Rewards speed, accuracy, fuel efficiency, and hull integrity
|
|
*/
|
|
|
|
/**
|
|
* Star rating levels (0-3 stars per category)
|
|
*/
|
|
export interface StarRatings {
|
|
time: number; // 0-3 stars based on completion time
|
|
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)
|
|
}
|
|
|
|
/**
|
|
* Debug information for score calculation
|
|
*/
|
|
export interface ScoreDebugInfo {
|
|
rawFuelConsumed: number; // Actual fuel consumed (can be >100%)
|
|
rawHullDamage: number; // Actual hull damage (can be >100%)
|
|
fuelEfficiency: number; // 0-100 display value (clamped)
|
|
hullIntegrity: number; // 0-100 display value (clamped)
|
|
}
|
|
|
|
/**
|
|
* Complete score calculation result
|
|
*/
|
|
export interface ScoreCalculation {
|
|
baseScore: number;
|
|
timeMultiplier: number;
|
|
accuracyMultiplier: number;
|
|
fuelMultiplier: number;
|
|
hullMultiplier: number;
|
|
finalScore: number;
|
|
stars: StarRatings;
|
|
debug: ScoreDebugInfo;
|
|
}
|
|
|
|
/**
|
|
* Configuration for score calculation
|
|
*/
|
|
export interface ScoreConfig {
|
|
baseScore?: number; // Default: 10000
|
|
minMultiplier?: number; // Minimum multiplier floor (default: 0.5)
|
|
maxTimeMultiplier?: number; // Maximum time bonus (default: 3.0)
|
|
minTimeMultiplier?: number; // Minimum time multiplier (default: 0.1)
|
|
}
|
|
|
|
/**
|
|
* Calculate final score based on performance metrics
|
|
*
|
|
* @param gameTimeSeconds - Total game time in seconds
|
|
* @param accuracy - Shot accuracy percentage (0-100)
|
|
* @param fuelConsumed - Fuel consumed percentage (0-∞, can exceed 100% with refuels)
|
|
* @param hullDamage - Hull damage percentage (0-∞, can exceed 100% with deaths/repairs)
|
|
* @param parTime - Expected completion time in seconds (default: 120)
|
|
* @param config - Optional scoring configuration
|
|
* @returns Complete score calculation with multipliers and star ratings
|
|
*/
|
|
export function calculateScore(
|
|
gameTimeSeconds: number,
|
|
accuracy: number,
|
|
fuelConsumed: number,
|
|
hullDamage: number,
|
|
parTime: number = 120,
|
|
config: ScoreConfig = {}
|
|
): ScoreCalculation {
|
|
const {
|
|
baseScore = 10000,
|
|
minMultiplier = 0.5,
|
|
maxTimeMultiplier = 3.0,
|
|
minTimeMultiplier = 0.1
|
|
} = config;
|
|
|
|
// ============================================
|
|
// TIME MULTIPLIER
|
|
// ============================================
|
|
// Exponential decay from par time
|
|
// Faster than par = >1.0x, slower = <1.0x
|
|
// Clamped between minTimeMultiplier and maxTimeMultiplier
|
|
const timeRatio = gameTimeSeconds / parTime;
|
|
const timeMultiplier = Math.max(
|
|
minTimeMultiplier,
|
|
Math.min(
|
|
maxTimeMultiplier,
|
|
Math.exp(-timeRatio + 1) * 2
|
|
)
|
|
);
|
|
|
|
// ============================================
|
|
// ACCURACY MULTIPLIER
|
|
// ============================================
|
|
// Linear scaling: 0% = 1.0x, 100% = 2.0x
|
|
// Accuracy is always 0-100%, so no clamping needed
|
|
const accuracyMultiplier = 1.0 + (accuracy / 100);
|
|
|
|
// ============================================
|
|
// FUEL EFFICIENCY MULTIPLIER
|
|
// ============================================
|
|
// Linear with floor for refueling scenarios
|
|
// 0% consumed = 2.0x (perfect)
|
|
// 50% consumed = 1.5x
|
|
// 100% consumed = 1.0x
|
|
// >100% consumed = minMultiplier floor (e.g., 0.5x)
|
|
const fuelEfficiencyScore = Math.max(0, 100 - fuelConsumed);
|
|
const fuelMultiplier = Math.max(
|
|
minMultiplier,
|
|
1.0 + (fuelEfficiencyScore / 100)
|
|
);
|
|
|
|
// ============================================
|
|
// HULL INTEGRITY MULTIPLIER
|
|
// ============================================
|
|
// Linear with floor for death/repair scenarios
|
|
// 0% damage = 2.0x (perfect)
|
|
// 50% damage = 1.5x
|
|
// 100% damage = 1.0x
|
|
// >100% damage = minMultiplier floor (e.g., 0.5x)
|
|
const hullIntegrityScore = Math.max(0, 100 - hullDamage);
|
|
const hullMultiplier = Math.max(
|
|
minMultiplier,
|
|
1.0 + (hullIntegrityScore / 100)
|
|
);
|
|
|
|
// ============================================
|
|
// FINAL SCORE CALCULATION
|
|
// ============================================
|
|
const finalScore = Math.floor(
|
|
baseScore *
|
|
timeMultiplier *
|
|
accuracyMultiplier *
|
|
fuelMultiplier *
|
|
hullMultiplier
|
|
);
|
|
|
|
// ============================================
|
|
// STAR RATINGS
|
|
// ============================================
|
|
const stars: StarRatings = {
|
|
time: getTimeStars(gameTimeSeconds, parTime),
|
|
accuracy: getAccuracyStars(accuracy),
|
|
fuel: getFuelStars(fuelConsumed),
|
|
hull: getHullStars(hullDamage),
|
|
total: 0
|
|
};
|
|
stars.total = stars.time + stars.accuracy + stars.fuel + stars.hull;
|
|
|
|
// ============================================
|
|
// DEBUG INFO
|
|
// ============================================
|
|
const debug: ScoreDebugInfo = {
|
|
rawFuelConsumed: fuelConsumed,
|
|
rawHullDamage: hullDamage,
|
|
fuelEfficiency: Math.max(0, Math.min(100, 100 - fuelConsumed)),
|
|
hullIntegrity: Math.max(0, Math.min(100, 100 - hullDamage))
|
|
};
|
|
|
|
return {
|
|
baseScore,
|
|
timeMultiplier,
|
|
accuracyMultiplier,
|
|
fuelMultiplier,
|
|
hullMultiplier,
|
|
finalScore,
|
|
stars,
|
|
debug
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate time stars based on completion time vs par
|
|
*
|
|
* @param seconds - Completion time in seconds
|
|
* @param par - Par time in seconds
|
|
* @returns 0-3 stars
|
|
*/
|
|
export function getTimeStars(seconds: number, par: number): number {
|
|
const ratio = seconds / par;
|
|
if (ratio <= 0.5) return 3; // Finished in half the par time
|
|
if (ratio <= 1.0) return 2; // Finished at or under par
|
|
if (ratio <= 1.5) return 1; // Finished within 150% of par
|
|
return 0; // Over 150% of par
|
|
}
|
|
|
|
/**
|
|
* Calculate accuracy stars based on hit percentage
|
|
*
|
|
* @param accuracy - Shot accuracy percentage (0-100)
|
|
* @returns 0-3 stars
|
|
*/
|
|
export function getAccuracyStars(accuracy: number): number {
|
|
if (accuracy >= 75) return 3; // Excellent accuracy
|
|
if (accuracy >= 50) return 2; // Good accuracy
|
|
if (accuracy >= 25) return 1; // Fair accuracy
|
|
return 0; // Poor accuracy
|
|
}
|
|
|
|
/**
|
|
* Calculate fuel efficiency stars
|
|
*
|
|
* @param fuelConsumed - Fuel consumed percentage (0-∞)
|
|
* @returns 0-3 stars
|
|
*/
|
|
export function getFuelStars(fuelConsumed: number): number {
|
|
// Stars only consider first 100% of fuel
|
|
// Refueling doesn't earn extra stars
|
|
if (fuelConsumed <= 30) return 3; // Used ≤30% fuel
|
|
if (fuelConsumed <= 60) return 2; // Used ≤60% fuel
|
|
if (fuelConsumed <= 80) return 1; // Used ≤80% fuel
|
|
return 0; // Used >80% fuel (including refuels)
|
|
}
|
|
|
|
/**
|
|
* Calculate hull integrity stars
|
|
*
|
|
* @param hullDamage - Hull damage percentage (0-∞)
|
|
* @returns 0-3 stars
|
|
*/
|
|
export function getHullStars(hullDamage: number): number {
|
|
// Stars only consider first 100% of damage
|
|
// Dying and respawning = 0 stars
|
|
if (hullDamage <= 10) return 3; // Took ≤10% damage
|
|
if (hullDamage <= 30) return 2; // Took ≤30% damage
|
|
if (hullDamage <= 60) return 1; // Took ≤60% damage
|
|
return 0; // Took >60% damage (including deaths)
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|