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>
This commit is contained in:
parent
64331b4566
commit
749cc18211
@ -175,10 +175,11 @@ export class RockFactory {
|
||||
if (eventData.type == 'COLLISION_STARTED') {
|
||||
if ( eventData.collidedAgainst.transformNode.id == 'ammo') {
|
||||
log.debug('[RockFactory] ASTEROID HIT! Triggering explosion...');
|
||||
score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed"});
|
||||
|
||||
// Get the asteroid mesh before disposing
|
||||
const asteroidMesh = eventData.collider.transformNode as AbstractMesh;
|
||||
const asteroidScale = asteroidMesh.scaling.x;
|
||||
score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed", scale: asteroidScale});
|
||||
log.debug('[RockFactory] Asteroid mesh to explode:', {
|
||||
name: asteroidMesh.name,
|
||||
id: asteroidMesh.id,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { getAnalytics } from "../analytics";
|
||||
import log from "../core/logger";
|
||||
import { calculateScore, ScoreCalculation } from "./scoreCalculator";
|
||||
import { calculateAsteroidPoints, calculateFinalScore, ScoreCalculation } from "./scoreCalculator";
|
||||
|
||||
/**
|
||||
* Tracks game statistics for display on status screen
|
||||
@ -13,6 +13,8 @@ export class GameStats {
|
||||
private _shotsHit: number = 0;
|
||||
private _fuelConsumed: number = 0;
|
||||
private _performanceTimer: number | null = null;
|
||||
private _runningScore: number = 0;
|
||||
private _parTime: number = 120;
|
||||
|
||||
/**
|
||||
* Start the game timer and performance tracking
|
||||
@ -89,10 +91,27 @@ export class GameStats {
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment asteroids destroyed count
|
||||
* Set the par time for score calculation
|
||||
*/
|
||||
public recordAsteroidDestroyed(): void {
|
||||
public setParTime(parTime: number): void {
|
||||
this._parTime = parTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record asteroid destroyed and calculate points
|
||||
* @param scale - Asteroid scale (size)
|
||||
*/
|
||||
public recordAsteroidDestroyed(scale: number = 1): void {
|
||||
this._asteroidsDestroyed++;
|
||||
const points = calculateAsteroidPoints(scale, this.getGameTime(), this._parTime);
|
||||
this._runningScore += points;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the running score from asteroid destruction
|
||||
*/
|
||||
public getRunningScore(): number {
|
||||
return this._runningScore;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -181,6 +200,7 @@ export class GameStats {
|
||||
this._shotsFired = 0;
|
||||
this._shotsHit = 0;
|
||||
this._fuelConsumed = 0;
|
||||
this._runningScore = 0;
|
||||
|
||||
// Restart performance tracking
|
||||
this.startPerformanceTracking();
|
||||
@ -188,22 +208,20 @@ export class GameStats {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param includeEndGameBonuses - Whether to include end-game bonuses (only at game end)
|
||||
* @returns Complete score calculation with bonuses and star ratings
|
||||
*/
|
||||
public calculateFinalScore(parTime: number = 120): ScoreCalculation {
|
||||
const gameTimeSeconds = this.getGameTime();
|
||||
public getFinalScore(includeEndGameBonuses: boolean = true): ScoreCalculation {
|
||||
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,
|
||||
return calculateFinalScore(
|
||||
this._runningScore,
|
||||
hullDamage,
|
||||
parTime
|
||||
fuelConsumed,
|
||||
accuracy,
|
||||
includeEndGameBonuses
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
/**
|
||||
* Score calculation system for space shooter game
|
||||
* Uses linear-clamped multipliers that never go negative
|
||||
* Rewards speed, accuracy, fuel efficiency, and hull integrity
|
||||
* 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 {
|
||||
time: number; // 0-3 stars based on completion time
|
||||
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
|
||||
@ -16,221 +20,154 @@ interface StarRatings {
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug information for score calculation
|
||||
* End-game bonus breakdown
|
||||
*/
|
||||
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)
|
||||
export interface EndGameBonuses {
|
||||
hull: number;
|
||||
fuel: number;
|
||||
accuracy: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete score calculation result
|
||||
*/
|
||||
export interface ScoreCalculation {
|
||||
baseScore: number;
|
||||
timeMultiplier: number;
|
||||
accuracyMultiplier: number;
|
||||
fuelMultiplier: number;
|
||||
hullMultiplier: number;
|
||||
finalScore: number;
|
||||
asteroidScore: number; // Points from destroying asteroids
|
||||
bonuses: EndGameBonuses; // End-game bonuses
|
||||
finalScore: number; // Total score
|
||||
stars: StarRatings;
|
||||
debug: ScoreDebugInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for score calculation
|
||||
* 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
|
||||
*/
|
||||
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)
|
||||
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 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
|
||||
* 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 calculateScore(
|
||||
gameTimeSeconds: number,
|
||||
accuracy: number,
|
||||
fuelConsumed: number,
|
||||
export function calculateEndGameBonuses(
|
||||
hullDamage: number,
|
||||
parTime: number = 120,
|
||||
config: ScoreConfig = {}
|
||||
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 {
|
||||
baseScore = 10000,
|
||||
minMultiplier = 0.5,
|
||||
maxTimeMultiplier = 3.0,
|
||||
minTimeMultiplier = 0.1
|
||||
} = config;
|
||||
const bonuses = includeEndGameBonuses
|
||||
? calculateEndGameBonuses(hullDamage, fuelConsumed, accuracy)
|
||||
: { hull: 0, fuel: 0, accuracy: 0 };
|
||||
const finalScore = asteroidScore + bonuses.hull + bonuses.fuel + bonuses.accuracy;
|
||||
|
||||
// ============================================
|
||||
// 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),
|
||||
asteroids: getAsteroidStars(asteroidScore),
|
||||
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))
|
||||
};
|
||||
stars.total = stars.asteroids + stars.accuracy + stars.fuel + stars.hull;
|
||||
|
||||
return {
|
||||
baseScore,
|
||||
timeMultiplier,
|
||||
accuracyMultiplier,
|
||||
fuelMultiplier,
|
||||
hullMultiplier,
|
||||
asteroidScore,
|
||||
bonuses,
|
||||
finalScore,
|
||||
stars,
|
||||
debug
|
||||
stars
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Calculate asteroid stars based on score earned
|
||||
* Note: This is a rough heuristic; actual thresholds may need tuning per level
|
||||
*/
|
||||
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
|
||||
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 >= 75) return 3; // Excellent accuracy
|
||||
if (accuracy >= 50) return 2; // Good accuracy
|
||||
if (accuracy >= 25) return 1; // Fair accuracy
|
||||
return 0; // Poor accuracy
|
||||
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-∞)
|
||||
* @param fuelConsumed - Fuel consumed percentage (0-300+%)
|
||||
* @returns 0-3 stars
|
||||
*/
|
||||
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)
|
||||
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-∞)
|
||||
* @param hullDamage - Hull damage percentage (0-300+%)
|
||||
* @returns 0-3 stars
|
||||
*/
|
||||
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)
|
||||
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
|
||||
*/
|
||||
@ -245,7 +182,6 @@ export function getStarColor(stars: number): string {
|
||||
|
||||
/**
|
||||
* 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., "★★☆")
|
||||
|
||||
@ -154,7 +154,7 @@ export class GameResultsService {
|
||||
|
||||
// Get stats
|
||||
const stats = gameStats.getStats();
|
||||
const scoreCalc = gameStats.calculateFinalScore(parTime);
|
||||
const scoreCalc = gameStats.getFinalScore();
|
||||
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
|
||||
@ -418,17 +418,17 @@ export class Ship {
|
||||
this._scoreboard.initialize();
|
||||
|
||||
// Subscribe to score events to track asteroids destroyed
|
||||
this._scoreboard.onScoreObservable.add(() => {
|
||||
// Each score event represents an asteroid destroyed
|
||||
this._gameStats.recordAsteroidDestroyed();
|
||||
this._scoreboard.onScoreObservable.add((event) => {
|
||||
// Each score event represents an asteroid destroyed, pass scale for point calc
|
||||
this._gameStats.recordAsteroidDestroyed(event.scale || 1);
|
||||
|
||||
// Track asteroid destruction in analytics
|
||||
try {
|
||||
const analytics = getAnalytics();
|
||||
analytics.track('asteroid_destroyed', {
|
||||
weaponType: 'laser', // TODO: Get actual weapon type from event
|
||||
distance: 0, // TODO: Calculate distance if available
|
||||
asteroidSize: 0, // TODO: Get actual size if available
|
||||
weaponType: 'laser',
|
||||
distance: 0,
|
||||
asteroidSize: event.scale || 0,
|
||||
remainingCount: this._scoreboard.remaining
|
||||
}, { sampleRate: 0.2 }); // Sample 20% of asteroid events to reduce data
|
||||
} catch (error) {
|
||||
|
||||
@ -198,12 +198,14 @@ export class WeaponSystem {
|
||||
if (isAsteroid) {
|
||||
log.debug('[WeaponSystem] Asteroid hit! Triggering destruction...');
|
||||
|
||||
// Update score
|
||||
// Update score with asteroid scale for point calculation
|
||||
if (this._scoreObservable) {
|
||||
const asteroidScale = hitMesh.scaling.x;
|
||||
this._scoreObservable.notifyObservers({
|
||||
score: 1,
|
||||
remaining: -1,
|
||||
message: "Asteroid Destroyed"
|
||||
message: "Asteroid Destroyed",
|
||||
scale: asteroidScale
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import type { AudioEngineV2 } from "@babylonjs/core";
|
||||
import log from '../../core/logger';
|
||||
import { LevelConfig } from "../../levels/config/levelConfig";
|
||||
import { CloudLevelEntry } from "../../services/cloudLevelService";
|
||||
import { addButtonHoverEffect } from "../utils/buttonEffects";
|
||||
|
||||
/**
|
||||
* Mission brief display for VR
|
||||
@ -48,9 +49,9 @@ export class MissionBrief {
|
||||
}
|
||||
|
||||
mesh.parent = ship;
|
||||
mesh.position = new Vector3(0,1,2.8);
|
||||
mesh.position = new Vector3(0,1.2,2);
|
||||
mesh.rotation = new Vector3(0, 0, 0);
|
||||
mesh.renderingGroupId = 3; // Same as status screen for consistent rendering
|
||||
//mesh.renderingGroupId = 3; // Same as status screen for consistent rendering
|
||||
mesh.metadata = { uiPickable: true }; // TAG: VR UI - allow pointer selection
|
||||
log.info('[MissionBrief] Mesh parented to ship at position:', mesh.position);
|
||||
log.info('[MissionBrief] Mesh absolute position:', mesh.getAbsolutePosition());
|
||||
@ -71,7 +72,7 @@ export class MissionBrief {
|
||||
this._container.height = "600px";
|
||||
this._container.thickness = 4;
|
||||
this._container.color = "#00ff00";
|
||||
this._container.background = "rgba(0, 0, 0, 0.95)";
|
||||
this._container.background = "rgba(0, 0, 0, 0.99)";
|
||||
this._container.cornerRadius = 20;
|
||||
this._container.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||
this._container.verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER;
|
||||
@ -202,7 +203,7 @@ export class MissionBrief {
|
||||
|
||||
// Spacer before button
|
||||
const spacer3 = new Rectangle("spacer3");
|
||||
spacer3.height = "40px";
|
||||
spacer3.height = "20px";
|
||||
spacer3.thickness = 0;
|
||||
contentPanel.addControl(spacer3);
|
||||
|
||||
@ -217,6 +218,8 @@ export class MissionBrief {
|
||||
startButton.fontSize = "36px";
|
||||
startButton.fontWeight = "bold";
|
||||
startButton.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||
addButtonHoverEffect(startButton);
|
||||
|
||||
startButton.onPointerClickObservable.add(() => {
|
||||
log.debug('[MissionBrief] START button clicked - dismissing mission brief');
|
||||
this.hide();
|
||||
|
||||
@ -13,7 +13,8 @@ export type ScoreEvent = {
|
||||
score: number,
|
||||
message: string,
|
||||
remaining: number,
|
||||
timeRemaining? : number
|
||||
scale?: number, // Asteroid scale for point calculation
|
||||
timeRemaining?: number
|
||||
}
|
||||
export class Scoreboard {
|
||||
private _score: number = 0;
|
||||
|
||||
@ -18,6 +18,7 @@ import { GameStats } from "../../game/gameStats";
|
||||
import { DefaultScene } from "../../core/defaultScene";
|
||||
import { ProgressionManager } from "../../game/progression";
|
||||
import { AuthService } from "../../services/authService";
|
||||
import { addButtonHoverEffect } from "../utils/buttonEffects";
|
||||
import { FacebookShare, ShareData } from "../../services/facebookShare";
|
||||
import { InputControlManager } from "../../ship/input/inputControlManager";
|
||||
import { formatStars } from "../../game/scoreCalculator";
|
||||
@ -46,9 +47,11 @@ export class StatusScreen {
|
||||
private _fuelConsumedText: TextBlock;
|
||||
|
||||
// Text blocks for score display
|
||||
private _scoreTitleText: TextBlock;
|
||||
private _finalScoreText: TextBlock;
|
||||
private _scoreBreakdownText: TextBlock;
|
||||
private _starRatingText: TextBlock;
|
||||
private _starsContainer: StackPanel;
|
||||
private _totalStarsText: TextBlock;
|
||||
|
||||
// Buttons
|
||||
private _replayButton: Button;
|
||||
@ -97,8 +100,8 @@ export class StatusScreen {
|
||||
|
||||
// Parent to ship for fixed cockpit position
|
||||
this._screenMesh.parent = this._shipNode;
|
||||
this._screenMesh.position = new Vector3(0, 1, 2); // 2 meters forward in local space
|
||||
this._screenMesh.renderingGroupId = 3; // Always render on top
|
||||
this._screenMesh.position = new Vector3(0, 1.1, 2); // 2 meters forward in local space
|
||||
//this._screenMesh.renderingGroupId = 3; // Always render on top
|
||||
this._screenMesh.metadata = { uiPickable: true }; // TAG: VR UI - allow pointer selection
|
||||
|
||||
// Create material
|
||||
@ -165,11 +168,11 @@ export class StatusScreen {
|
||||
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);
|
||||
// Score title (changes based on game state)
|
||||
this._scoreTitleText = this.createTitleText("CURRENT SCORE");
|
||||
this._scoreTitleText.fontSize = "50px";
|
||||
this._scoreTitleText.height = "70px";
|
||||
mainPanel.addControl(this._scoreTitleText);
|
||||
|
||||
this._finalScoreText = new TextBlock();
|
||||
this._finalScoreText.text = "0";
|
||||
@ -186,22 +189,28 @@ export class StatusScreen {
|
||||
this._scoreBreakdownText.color = "#aaaaaa";
|
||||
this._scoreBreakdownText.fontSize = "20px";
|
||||
this._scoreBreakdownText.height = "120px";
|
||||
this._scoreBreakdownText.width = "100%";
|
||||
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);
|
||||
// Star ratings container (populated in updateStatistics)
|
||||
this._starsContainer = new StackPanel("starsContainer");
|
||||
this._starsContainer.isVertical = false;
|
||||
this._starsContainer.height = "100px";
|
||||
this._starsContainer.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||
mainPanel.addControl(this._starsContainer);
|
||||
|
||||
// Total stars display
|
||||
this._totalStarsText = new TextBlock();
|
||||
this._totalStarsText.text = "";
|
||||
this._totalStarsText.color = "#FFD700";
|
||||
this._totalStarsText.fontSize = "32px";
|
||||
this._totalStarsText.height = "50px";
|
||||
this._totalStarsText.fontWeight = "bold";
|
||||
this._totalStarsText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||
mainPanel.addControl(this._totalStarsText);
|
||||
|
||||
// Add spacing before buttons
|
||||
const spacer3 = this.createSpacer(40);
|
||||
@ -213,6 +222,9 @@ export class StatusScreen {
|
||||
buttonBar.height = "80px";
|
||||
buttonBar.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||
buttonBar.spacing = 20;
|
||||
buttonBar.paddingLeft = 20;
|
||||
buttonBar.paddingRight = 20;
|
||||
buttonBar.clipChildren = false;
|
||||
|
||||
// Create Resume button (only shown when game hasn't ended)
|
||||
this._resumeButton = Button.CreateSimpleButton("resumeButton", "RESUME GAME");
|
||||
@ -224,6 +236,7 @@ export class StatusScreen {
|
||||
this._resumeButton.thickness = 0;
|
||||
this._resumeButton.fontSize = "30px";
|
||||
this._resumeButton.fontWeight = "bold";
|
||||
addButtonHoverEffect(this._resumeButton);
|
||||
this._resumeButton.onPointerClickObservable.add(() => {
|
||||
if (this._onResumeCallback) {
|
||||
this._onResumeCallback();
|
||||
@ -275,6 +288,7 @@ export class StatusScreen {
|
||||
this._exitButton.thickness = 0;
|
||||
this._exitButton.fontSize = "30px";
|
||||
this._exitButton.fontWeight = "bold";
|
||||
addButtonHoverEffect(this._exitButton, "#cc3333", "#ff4444");
|
||||
this._exitButton.onPointerClickObservable.add(() => {
|
||||
if (this._onExitCallback) {
|
||||
this._onExitCallback();
|
||||
@ -356,6 +370,33 @@ export class StatusScreen {
|
||||
return spacer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a star rating column with stars on top and label below
|
||||
*/
|
||||
private createStarRatingColumn(stars: number, label: string): StackPanel {
|
||||
const column = new StackPanel();
|
||||
column.isVertical = true;
|
||||
column.width = "150px";
|
||||
|
||||
const starsText = new TextBlock();
|
||||
starsText.text = formatStars(stars);
|
||||
starsText.color = "#FFD700";
|
||||
starsText.fontSize = "36px";
|
||||
starsText.height = "50px";
|
||||
starsText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||
column.addControl(starsText);
|
||||
|
||||
const labelText = new TextBlock();
|
||||
labelText.text = label;
|
||||
labelText.color = "#aaaaaa";
|
||||
labelText.fontSize = "24px";
|
||||
labelText.height = "35px";
|
||||
labelText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||
column.addControl(labelText);
|
||||
|
||||
return column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle visibility of status screen
|
||||
*/
|
||||
@ -502,29 +543,35 @@ export class StatusScreen {
|
||||
this._accuracyText.text = `Accuracy: ${stats.accuracy}%`;
|
||||
this._fuelConsumedText.text = `Fuel Consumed: ${stats.fuelConsumed}%`;
|
||||
|
||||
// Calculate and display score
|
||||
const scoreCalc = this._gameStats.calculateFinalScore(this._parTime);
|
||||
// Calculate score - only include end-game bonuses if game has ended
|
||||
const scoreCalc = this._gameStats.getFinalScore(this._isGameEnded);
|
||||
|
||||
// Update final score
|
||||
// Update score title based on game state
|
||||
this._scoreTitleText.text = this._isGameEnded ? "FINAL SCORE" : "CURRENT SCORE";
|
||||
|
||||
// Update score value
|
||||
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 score breakdown - show bonuses only at game end
|
||||
if (this._isGameEnded) {
|
||||
this._scoreBreakdownText.text =
|
||||
`Asteroids: ${scoreCalc.asteroidScore.toLocaleString()} | ` +
|
||||
`Acc: +${scoreCalc.bonuses.accuracy.toLocaleString()}\n` +
|
||||
`Fuel: +${scoreCalc.bonuses.fuel.toLocaleString()} | ` +
|
||||
`Hull: +${scoreCalc.bonuses.hull.toLocaleString()}`;
|
||||
} else {
|
||||
this._scoreBreakdownText.text = `Points from asteroid destruction`;
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Rebuild star rating columns
|
||||
this._starsContainer.clearControls();
|
||||
this._starsContainer.addControl(this.createStarRatingColumn(scoreCalc.stars.asteroids, "Kills"));
|
||||
this._starsContainer.addControl(this.createStarRatingColumn(scoreCalc.stars.accuracy, "Acc"));
|
||||
this._starsContainer.addControl(this.createStarRatingColumn(scoreCalc.stars.fuel, "Fuel"));
|
||||
this._starsContainer.addControl(this.createStarRatingColumn(scoreCalc.stars.hull, "Hull"));
|
||||
|
||||
this._starRatingText.text =
|
||||
`${timeStars} ${accStars} ${fuelStars} ${hullStars}\n` +
|
||||
`Time Acc Fuel Hull\n` +
|
||||
`${scoreCalc.stars.total}/12 Stars`;
|
||||
// Update total stars
|
||||
this._totalStarsText.text = `${scoreCalc.stars.total}/12 Stars`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -55,7 +55,14 @@ export class Preloader {
|
||||
}
|
||||
|
||||
private setupButtonHandler(): void {
|
||||
this.startButton?.addEventListener('click', () => this.onStartCallback?.());
|
||||
this.startButton?.addEventListener('click', () => {
|
||||
if (this.startButton) {
|
||||
(this.startButton as HTMLButtonElement).disabled = true;
|
||||
this.startButton.style.opacity = '0.6';
|
||||
this.startButton.textContent = 'ENTERING XR...';
|
||||
}
|
||||
this.onStartCallback?.();
|
||||
});
|
||||
}
|
||||
|
||||
public setLevelInfo(name: string, difficulty: string, missionBrief: string[]): void {
|
||||
|
||||
29
src/ui/utils/buttonEffects.ts
Normal file
29
src/ui/utils/buttonEffects.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Button } from "@babylonjs/gui";
|
||||
|
||||
/**
|
||||
* Adds a prominent hover effect to a BabylonJS GUI button
|
||||
* @param button - The button to add hover effects to
|
||||
* @param baseBackground - The base background color (default: #00ff88)
|
||||
* @param hoverBackground - The hover background color (default: #00ffaa)
|
||||
*/
|
||||
export function addButtonHoverEffect(
|
||||
button: Button,
|
||||
baseBackground: string = "#00ff88",
|
||||
hoverBackground: string = "#00ffaa"
|
||||
): void {
|
||||
button.onPointerEnterObservable.add(() => {
|
||||
button.background = hoverBackground;
|
||||
button.scaleX = 1.05;
|
||||
button.scaleY = 1.05;
|
||||
button.thickness = 3;
|
||||
button.color = "#ffffff";
|
||||
});
|
||||
|
||||
button.onPointerOutObservable.add(() => {
|
||||
button.background = baseBackground;
|
||||
button.scaleX = 1;
|
||||
button.scaleY = 1;
|
||||
button.thickness = 0;
|
||||
button.color = "white";
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user