Implement comprehensive scoring system with star ratings
All checks were successful
Build / build (push) Successful in 1m35s
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>
This commit is contained in:
parent
1422c5b926
commit
71ff46e4cf
Binary file not shown.
BIN
public/assets/themes/default/audio/thrust5.mp3
Normal file
BIN
public/assets/themes/default/audio/thrust5.mp3
Normal file
Binary file not shown.
@ -1,5 +1,6 @@
|
||||
import { getAnalytics } from "../analytics";
|
||||
import debugLog from "../core/debug";
|
||||
import { calculateScore, ScoreCalculation } from "./scoreCalculator";
|
||||
|
||||
/**
|
||||
* Tracks game statistics for display on status screen
|
||||
@ -185,6 +186,27 @@ export class GameStats {
|
||||
this.startPerformanceTracking();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate final score based on current statistics
|
||||
*
|
||||
* @param parTime - Expected completion time in seconds (default: 120)
|
||||
* @returns Complete score calculation with multipliers and star ratings
|
||||
*/
|
||||
public calculateFinalScore(parTime: number = 120): ScoreCalculation {
|
||||
const gameTimeSeconds = this.getGameTime();
|
||||
const accuracy = this.getAccuracy();
|
||||
const fuelConsumed = this._fuelConsumed * 100; // Convert to percentage
|
||||
const hullDamage = this._hullDamageTaken * 100; // Convert to percentage
|
||||
|
||||
return calculateScore(
|
||||
gameTimeSeconds,
|
||||
accuracy,
|
||||
fuelConsumed,
|
||||
hullDamage,
|
||||
parTime
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup when game ends
|
||||
*/
|
||||
|
||||
257
src/game/scoreCalculator.ts
Normal file
257
src/game/scoreCalculator.ts
Normal file
@ -0,0 +1,257 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@ -135,6 +135,7 @@ export interface LevelConfig {
|
||||
description?: string;
|
||||
babylonVersion?: string;
|
||||
captureTime?: number;
|
||||
parTime?: number; // Expected completion time in seconds for scoring
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ export class Level1 implements Level {
|
||||
private _audioEngine: AudioEngineV2;
|
||||
private _deserializer: LevelDeserializer;
|
||||
private _backgroundStars: BackgroundStars;
|
||||
private _physicsRecorder: PhysicsRecorder;
|
||||
private _physicsRecorder: PhysicsRecorder | null = null;
|
||||
private _isReplayMode: boolean;
|
||||
private _backgroundMusic: StaticSound;
|
||||
private _missionBrief: MissionBrief;
|
||||
@ -331,17 +331,10 @@ export class Level1 implements Level {
|
||||
// Only create recorder in game mode, not replay mode
|
||||
if (!this._isReplayMode) {
|
||||
setLoadingMessage("Initializing physics recorder...");
|
||||
this._physicsRecorder = new PhysicsRecorder(DefaultScene.MainScene, this._levelConfig);
|
||||
//this._physicsRecorder = new PhysicsRecorder(DefaultScene.MainScene, this._levelConfig);
|
||||
debugLog('Physics recorder initialized (will start on XR pose)');
|
||||
}
|
||||
|
||||
// Wire up recording keyboard shortcuts (only in game mode)
|
||||
if (!this._isReplayMode) {
|
||||
this._ship.keyboardInput.onRecordingActionObservable.add((action) => {
|
||||
this.handleRecordingAction(action);
|
||||
});
|
||||
}
|
||||
|
||||
// Load background music before marking as ready
|
||||
if (this._audioEngine) {
|
||||
setLoadingMessage("Loading background music...");
|
||||
@ -364,47 +357,39 @@ export class Level1 implements Level {
|
||||
|
||||
this._initialized = true;
|
||||
|
||||
// Set par time for score calculation based on difficulty
|
||||
const parTime = this.getParTimeForDifficulty(this._levelConfig.difficulty);
|
||||
const statusScreen = (this._ship as any)._statusScreen; // Access private status screen
|
||||
if (statusScreen) {
|
||||
statusScreen.setParTime(parTime);
|
||||
debugLog(`Set par time to ${parTime}s for difficulty: ${this._levelConfig.difficulty}`);
|
||||
}
|
||||
|
||||
// Notify that initialization is complete
|
||||
this._onReadyObservable.notifyObservers(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle recording keyboard shortcuts
|
||||
* Get par time based on difficulty level
|
||||
* Can be overridden by level config metadata
|
||||
*/
|
||||
private handleRecordingAction(action: string): void {
|
||||
switch (action) {
|
||||
case "exportRingBuffer":
|
||||
// R key: Export last 30 seconds from ring buffer
|
||||
const ringRecording = this._physicsRecorder.exportRingBuffer(30);
|
||||
this._physicsRecorder.downloadRecording(ringRecording, "ring-buffer-30s");
|
||||
debugLog("Exported ring buffer (last 30 seconds)");
|
||||
break;
|
||||
|
||||
case "toggleLongRecording":
|
||||
// Ctrl+R: Toggle long recording
|
||||
const stats = this._physicsRecorder.getStats();
|
||||
if (stats.isLongRecording) {
|
||||
this._physicsRecorder.stopLongRecording();
|
||||
debugLog("Long recording stopped");
|
||||
} else {
|
||||
this._physicsRecorder.startLongRecording();
|
||||
debugLog("Long recording started");
|
||||
}
|
||||
break;
|
||||
|
||||
case "exportLongRecording":
|
||||
// Shift+R: Export long recording
|
||||
const longRecording = this._physicsRecorder.exportLongRecording();
|
||||
if (longRecording.snapshots.length > 0) {
|
||||
this._physicsRecorder.downloadRecording(longRecording, "long-recording");
|
||||
debugLog("Exported long recording");
|
||||
} else {
|
||||
debugLog("No long recording data to export");
|
||||
}
|
||||
break;
|
||||
}
|
||||
private getParTimeForDifficulty(difficulty: string): number {
|
||||
// Check if level config has explicit par time
|
||||
if (this._levelConfig.metadata?.parTime) {
|
||||
return this._levelConfig.metadata.parTime;
|
||||
}
|
||||
|
||||
// Default par times by difficulty
|
||||
const difficultyMap: { [key: string]: number } = {
|
||||
'recruit': 300, // 5 minutes
|
||||
'pilot': 180, // 3 minutes
|
||||
'captain': 120, // 2 minutes
|
||||
'commander': 90, // 1.5 minutes
|
||||
'test': 60 // 1 minute
|
||||
};
|
||||
|
||||
return difficultyMap[difficulty.toLowerCase()] || 120; // Default to 2 minutes
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the physics recorder instance
|
||||
|
||||
@ -54,7 +54,7 @@ export class ShipAudio {
|
||||
"/assets/themes/default/audio/collision.mp3",
|
||||
{
|
||||
loop: false,
|
||||
volume: 0.35,
|
||||
volume: 0.25,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ export interface ForceApplicationResult {
|
||||
export class ShipPhysics {
|
||||
private _shipStatus: ShipStatus | null = null;
|
||||
private _gameStats: GameStats | null = null;
|
||||
|
||||
private _config = GameConfig.getInstance().shipPhysics;
|
||||
/**
|
||||
* Set the ship status instance for fuel consumption tracking
|
||||
*/
|
||||
@ -49,11 +49,10 @@ export class ShipPhysics {
|
||||
if (!physicsBody) {
|
||||
return { linearMagnitude: 0, angularMagnitude: 0 };
|
||||
}
|
||||
|
||||
const { leftStick, rightStick } = inputState;
|
||||
|
||||
// Get physics config
|
||||
const config = GameConfig.getInstance().shipPhysics;
|
||||
|
||||
|
||||
// Get current velocities for velocity cap checks
|
||||
const currentLinearVelocity = physicsBody.getLinearVelocity();
|
||||
@ -70,7 +69,7 @@ export class ShipPhysics {
|
||||
// Check if we have fuel before applying force
|
||||
if (this._shipStatus && this._shipStatus.fuel > 0) {
|
||||
// Only apply force if we haven't reached max velocity
|
||||
if (currentSpeed < config.maxLinearVelocity) {
|
||||
if (currentSpeed < this._config.maxLinearVelocity) {
|
||||
// Get local direction (Z-axis for forward/backward thrust)
|
||||
const localDirection = new Vector3(0, 0, -leftStick.y);
|
||||
// Transform to world space
|
||||
@ -78,7 +77,7 @@ export class ShipPhysics {
|
||||
localDirection,
|
||||
transformNode.getWorldMatrix()
|
||||
);
|
||||
const force = worldDirection.scale(config.linearForceMultiplier);
|
||||
const force = worldDirection.scale(this._config.linearForceMultiplier);
|
||||
|
||||
// Calculate thrust point: center of mass + offset (0, 1, 0) in world space
|
||||
const thrustPoint = Vector3.TransformCoordinates(
|
||||
@ -113,14 +112,14 @@ export class ShipPhysics {
|
||||
const currentAngularSpeed = currentAngularVelocity.length();
|
||||
|
||||
// Only apply torque if we haven't reached max angular velocity
|
||||
if (currentAngularSpeed < config.maxAngularVelocity) {
|
||||
if (currentAngularSpeed < this._config.maxAngularVelocity) {
|
||||
const yaw = -leftStick.x;
|
||||
const pitch = rightStick.y;
|
||||
const roll = rightStick.x;
|
||||
|
||||
// Create torque in local space, then transform to world space
|
||||
const localTorque = new Vector3(pitch, yaw, roll).scale(
|
||||
config.angularForceMultiplier
|
||||
this._config.angularForceMultiplier
|
||||
);
|
||||
const worldTorque = Vector3.TransformNormal(
|
||||
localTorque,
|
||||
|
||||
@ -20,6 +20,7 @@ import { ProgressionManager } from "../../game/progression";
|
||||
import { AuthService } from "../../services/authService";
|
||||
import { FacebookShare, ShareData } from "../../services/facebookShare";
|
||||
import { InputControlManager } from "../../ship/input/inputControlManager";
|
||||
import { formatStars, getStarColor } from "../../game/scoreCalculator";
|
||||
|
||||
/**
|
||||
* Status screen that displays game statistics
|
||||
@ -32,6 +33,7 @@ export class StatusScreen {
|
||||
private _texture: AdvancedDynamicTexture | null = null;
|
||||
private _isVisible: boolean = false;
|
||||
private _camera: Camera | null = null;
|
||||
private _parTime: number = 120; // Default par time in seconds
|
||||
|
||||
// Text blocks for statistics
|
||||
private _gameTimeText: TextBlock;
|
||||
@ -41,6 +43,11 @@ export class StatusScreen {
|
||||
private _accuracyText: TextBlock;
|
||||
private _fuelConsumedText: TextBlock;
|
||||
|
||||
// Text blocks for score display
|
||||
private _finalScoreText: TextBlock;
|
||||
private _scoreBreakdownText: TextBlock;
|
||||
private _starRatingText: TextBlock;
|
||||
|
||||
// Buttons
|
||||
private _replayButton: Button;
|
||||
private _exitButton: Button;
|
||||
@ -78,7 +85,7 @@ export class StatusScreen {
|
||||
// Create a plane mesh for the status screen
|
||||
this._screenMesh = MeshBuilder.CreatePlane(
|
||||
"statusScreen",
|
||||
{ width: 1.5, height: 1.0 },
|
||||
{ width: 1.5, height: 2.25 },
|
||||
this._scene
|
||||
);
|
||||
|
||||
@ -96,7 +103,7 @@ export class StatusScreen {
|
||||
this._texture = AdvancedDynamicTexture.CreateForMesh(
|
||||
this._screenMesh,
|
||||
1024,
|
||||
768
|
||||
1536
|
||||
);
|
||||
this._texture.background = "#1a1a2e";
|
||||
|
||||
@ -137,10 +144,63 @@ export class StatusScreen {
|
||||
this._fuelConsumedText = this.createStatText("Fuel Consumed: 0%");
|
||||
mainPanel.addControl(this._fuelConsumedText);
|
||||
|
||||
// Add spacing before buttons
|
||||
const spacer2 = this.createSpacer(50);
|
||||
// Add spacing before score section
|
||||
const spacer2 = this.createSpacer(40);
|
||||
mainPanel.addControl(spacer2);
|
||||
|
||||
// Score section divider
|
||||
const scoreDivider = new Rectangle("scoreDivider");
|
||||
scoreDivider.height = "2px";
|
||||
scoreDivider.width = "700px";
|
||||
scoreDivider.background = "#00ff88";
|
||||
scoreDivider.thickness = 0;
|
||||
mainPanel.addControl(scoreDivider);
|
||||
|
||||
const spacer2b = this.createSpacer(30);
|
||||
mainPanel.addControl(spacer2b);
|
||||
|
||||
// Final score display
|
||||
const scoreTitle = this.createTitleText("FINAL SCORE");
|
||||
scoreTitle.fontSize = "50px";
|
||||
scoreTitle.height = "70px";
|
||||
mainPanel.addControl(scoreTitle);
|
||||
|
||||
this._finalScoreText = new TextBlock();
|
||||
this._finalScoreText.text = "0";
|
||||
this._finalScoreText.color = "#FFD700"; // Gold color
|
||||
this._finalScoreText.fontSize = "80px";
|
||||
this._finalScoreText.height = "100px";
|
||||
this._finalScoreText.fontWeight = "bold";
|
||||
this._finalScoreText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||
mainPanel.addControl(this._finalScoreText);
|
||||
|
||||
// Score breakdown
|
||||
this._scoreBreakdownText = new TextBlock();
|
||||
this._scoreBreakdownText.text = "";
|
||||
this._scoreBreakdownText.color = "#aaaaaa";
|
||||
this._scoreBreakdownText.fontSize = "20px";
|
||||
this._scoreBreakdownText.height = "120px";
|
||||
this._scoreBreakdownText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||
this._scoreBreakdownText.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_TOP;
|
||||
this._scoreBreakdownText.textWrapping = true;
|
||||
mainPanel.addControl(this._scoreBreakdownText);
|
||||
|
||||
// Star ratings
|
||||
this._starRatingText = new TextBlock();
|
||||
this._starRatingText.text = "";
|
||||
this._starRatingText.color = "#FFD700";
|
||||
this._starRatingText.fontSize = "40px";
|
||||
this._starRatingText.height = "100px";
|
||||
this._starRatingText.fontWeight = "bold";
|
||||
this._starRatingText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||
this._starRatingText.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_TOP;
|
||||
this._starRatingText.textWrapping = true;
|
||||
mainPanel.addControl(this._starRatingText);
|
||||
|
||||
// Add spacing before buttons
|
||||
const spacer3 = this.createSpacer(40);
|
||||
mainPanel.addControl(spacer3);
|
||||
|
||||
// Create button bar
|
||||
const buttonBar = new StackPanel("buttonBar");
|
||||
buttonBar.isVertical = false;
|
||||
@ -309,6 +369,14 @@ export class StatusScreen {
|
||||
this._currentLevelName = levelName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the par time for score calculation
|
||||
* @param parTime - Expected completion time in seconds
|
||||
*/
|
||||
public setParTime(parTime: number): void {
|
||||
this._parTime = parTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the status screen
|
||||
* @param isGameEnded - true if game has ended (death/stranded/victory), false if manually paused
|
||||
@ -417,6 +485,30 @@ export class StatusScreen {
|
||||
this._shotsFiredText.text = `Shots Fired: ${stats.shotsFired}`;
|
||||
this._accuracyText.text = `Accuracy: ${stats.accuracy}%`;
|
||||
this._fuelConsumedText.text = `Fuel Consumed: ${stats.fuelConsumed}%`;
|
||||
|
||||
// Calculate and display score
|
||||
const scoreCalc = this._gameStats.calculateFinalScore(this._parTime);
|
||||
|
||||
// Update final score
|
||||
this._finalScoreText.text = scoreCalc.finalScore.toLocaleString();
|
||||
|
||||
// Update score breakdown
|
||||
this._scoreBreakdownText.text =
|
||||
`Time: ${scoreCalc.timeMultiplier.toFixed(2)}x | ` +
|
||||
`Accuracy: ${scoreCalc.accuracyMultiplier.toFixed(2)}x\n` +
|
||||
`Fuel: ${scoreCalc.fuelMultiplier.toFixed(2)}x | ` +
|
||||
`Hull: ${scoreCalc.hullMultiplier.toFixed(2)}x`;
|
||||
|
||||
// Update star ratings with Unicode stars and colors
|
||||
const timeStars = formatStars(scoreCalc.stars.time);
|
||||
const accStars = formatStars(scoreCalc.stars.accuracy);
|
||||
const fuelStars = formatStars(scoreCalc.stars.fuel);
|
||||
const hullStars = formatStars(scoreCalc.stars.hull);
|
||||
|
||||
this._starRatingText.text =
|
||||
`${timeStars} ${accStars} ${fuelStars} ${hullStars}\n` +
|
||||
`Time Acc Fuel Hull\n` +
|
||||
`${scoreCalc.stars.total}/12 Stars`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user