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.type == 'COLLISION_STARTED') {
|
||||||
if ( eventData.collidedAgainst.transformNode.id == 'ammo') {
|
if ( eventData.collidedAgainst.transformNode.id == 'ammo') {
|
||||||
log.debug('[RockFactory] ASTEROID HIT! Triggering explosion...');
|
log.debug('[RockFactory] ASTEROID HIT! Triggering explosion...');
|
||||||
score.notifyObservers({score: 1, remaining: -1, message: "Asteroid Destroyed"});
|
|
||||||
|
|
||||||
// Get the asteroid mesh before disposing
|
// Get the asteroid mesh before disposing
|
||||||
const asteroidMesh = eventData.collider.transformNode as AbstractMesh;
|
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:', {
|
log.debug('[RockFactory] Asteroid mesh to explode:', {
|
||||||
name: asteroidMesh.name,
|
name: asteroidMesh.name,
|
||||||
id: asteroidMesh.id,
|
id: asteroidMesh.id,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { getAnalytics } from "../analytics";
|
import { getAnalytics } from "../analytics";
|
||||||
import log from "../core/logger";
|
import log from "../core/logger";
|
||||||
import { calculateScore, ScoreCalculation } from "./scoreCalculator";
|
import { calculateAsteroidPoints, calculateFinalScore, ScoreCalculation } from "./scoreCalculator";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracks game statistics for display on status screen
|
* Tracks game statistics for display on status screen
|
||||||
@ -13,6 +13,8 @@ export class GameStats {
|
|||||||
private _shotsHit: number = 0;
|
private _shotsHit: number = 0;
|
||||||
private _fuelConsumed: number = 0;
|
private _fuelConsumed: number = 0;
|
||||||
private _performanceTimer: number | null = null;
|
private _performanceTimer: number | null = null;
|
||||||
|
private _runningScore: number = 0;
|
||||||
|
private _parTime: number = 120;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the game timer and performance tracking
|
* 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++;
|
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._shotsFired = 0;
|
||||||
this._shotsHit = 0;
|
this._shotsHit = 0;
|
||||||
this._fuelConsumed = 0;
|
this._fuelConsumed = 0;
|
||||||
|
this._runningScore = 0;
|
||||||
|
|
||||||
// Restart performance tracking
|
// Restart performance tracking
|
||||||
this.startPerformanceTracking();
|
this.startPerformanceTracking();
|
||||||
@ -188,22 +208,20 @@ export class GameStats {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate final score based on current statistics
|
* Calculate final score based on current statistics
|
||||||
*
|
* @param includeEndGameBonuses - Whether to include end-game bonuses (only at game end)
|
||||||
* @param parTime - Expected completion time in seconds (default: 120)
|
* @returns Complete score calculation with bonuses and star ratings
|
||||||
* @returns Complete score calculation with multipliers and star ratings
|
|
||||||
*/
|
*/
|
||||||
public calculateFinalScore(parTime: number = 120): ScoreCalculation {
|
public getFinalScore(includeEndGameBonuses: boolean = true): ScoreCalculation {
|
||||||
const gameTimeSeconds = this.getGameTime();
|
|
||||||
const accuracy = this.getAccuracy();
|
const accuracy = this.getAccuracy();
|
||||||
const fuelConsumed = this._fuelConsumed * 100; // Convert to percentage
|
const fuelConsumed = this._fuelConsumed * 100; // Convert to percentage
|
||||||
const hullDamage = this._hullDamageTaken * 100; // Convert to percentage
|
const hullDamage = this._hullDamageTaken * 100; // Convert to percentage
|
||||||
|
|
||||||
return calculateScore(
|
return calculateFinalScore(
|
||||||
gameTimeSeconds,
|
this._runningScore,
|
||||||
accuracy,
|
|
||||||
fuelConsumed,
|
|
||||||
hullDamage,
|
hullDamage,
|
||||||
parTime
|
fuelConsumed,
|
||||||
|
accuracy,
|
||||||
|
includeEndGameBonuses
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Score calculation system for space shooter game
|
* Score calculation system for space shooter game
|
||||||
* Uses linear-clamped multipliers that never go negative
|
* Additive scoring: starts at 0, builds through asteroid destruction and end-game bonuses
|
||||||
* Rewards speed, accuracy, fuel efficiency, and hull integrity
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// 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)
|
* Star rating levels (0-3 stars per category)
|
||||||
*/
|
*/
|
||||||
interface StarRatings {
|
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
|
accuracy: number; // 0-3 stars based on shot accuracy
|
||||||
fuel: number; // 0-3 stars based on fuel efficiency
|
fuel: number; // 0-3 stars based on fuel efficiency
|
||||||
hull: number; // 0-3 stars based on hull integrity
|
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 {
|
export interface EndGameBonuses {
|
||||||
rawFuelConsumed: number; // Actual fuel consumed (can be >100%)
|
hull: number;
|
||||||
rawHullDamage: number; // Actual hull damage (can be >100%)
|
fuel: number;
|
||||||
fuelEfficiency: number; // 0-100 display value (clamped)
|
accuracy: number;
|
||||||
hullIntegrity: number; // 0-100 display value (clamped)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete score calculation result
|
* Complete score calculation result
|
||||||
*/
|
*/
|
||||||
export interface ScoreCalculation {
|
export interface ScoreCalculation {
|
||||||
baseScore: number;
|
asteroidScore: number; // Points from destroying asteroids
|
||||||
timeMultiplier: number;
|
bonuses: EndGameBonuses; // End-game bonuses
|
||||||
accuracyMultiplier: number;
|
finalScore: number; // Total score
|
||||||
fuelMultiplier: number;
|
|
||||||
hullMultiplier: number;
|
|
||||||
finalScore: number;
|
|
||||||
stars: StarRatings;
|
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 {
|
export function calculateAsteroidPoints(
|
||||||
baseScore?: number; // Default: 10000
|
scale: number,
|
||||||
minMultiplier?: number; // Minimum multiplier floor (default: 0.5)
|
elapsedSeconds: number,
|
||||||
maxTimeMultiplier?: number; // Maximum time bonus (default: 3.0)
|
parTime: number
|
||||||
minTimeMultiplier?: number; // Minimum time multiplier (default: 0.1)
|
): 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
|
* Calculate end-game bonuses based on performance
|
||||||
*
|
* @param hullDamage - Total hull damage taken (0-300+%)
|
||||||
* @param gameTimeSeconds - Total game time in seconds
|
* @param fuelConsumed - Total fuel consumed (0-300+%)
|
||||||
* @param accuracy - Shot accuracy percentage (0-100)
|
* @param accuracy - Shot accuracy percentage (0-100%)
|
||||||
* @param fuelConsumed - Fuel consumed percentage (0-∞, can exceed 100% with refuels)
|
* @returns Bonus points for each category
|
||||||
* @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(
|
export function calculateEndGameBonuses(
|
||||||
gameTimeSeconds: number,
|
|
||||||
accuracy: number,
|
|
||||||
fuelConsumed: number,
|
|
||||||
hullDamage: number,
|
hullDamage: number,
|
||||||
parTime: number = 120,
|
fuelConsumed: number,
|
||||||
config: ScoreConfig = {}
|
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 {
|
): ScoreCalculation {
|
||||||
const {
|
const bonuses = includeEndGameBonuses
|
||||||
baseScore = 10000,
|
? calculateEndGameBonuses(hullDamage, fuelConsumed, accuracy)
|
||||||
minMultiplier = 0.5,
|
: { hull: 0, fuel: 0, accuracy: 0 };
|
||||||
maxTimeMultiplier = 3.0,
|
const finalScore = asteroidScore + bonuses.hull + bonuses.fuel + bonuses.accuracy;
|
||||||
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 = {
|
const stars: StarRatings = {
|
||||||
time: getTimeStars(gameTimeSeconds, parTime),
|
asteroids: getAsteroidStars(asteroidScore),
|
||||||
accuracy: getAccuracyStars(accuracy),
|
accuracy: getAccuracyStars(accuracy),
|
||||||
fuel: getFuelStars(fuelConsumed),
|
fuel: getFuelStars(fuelConsumed),
|
||||||
hull: getHullStars(hullDamage),
|
hull: getHullStars(hullDamage),
|
||||||
total: 0
|
total: 0
|
||||||
};
|
};
|
||||||
stars.total = stars.time + stars.accuracy + stars.fuel + stars.hull;
|
stars.total = stars.asteroids + 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 {
|
return {
|
||||||
baseScore,
|
asteroidScore,
|
||||||
timeMultiplier,
|
bonuses,
|
||||||
accuracyMultiplier,
|
|
||||||
fuelMultiplier,
|
|
||||||
hullMultiplier,
|
|
||||||
finalScore,
|
finalScore,
|
||||||
stars,
|
stars
|
||||||
debug
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate time stars based on completion time vs par
|
* Calculate asteroid stars based on score earned
|
||||||
*
|
* Note: This is a rough heuristic; actual thresholds may need tuning per level
|
||||||
* @param seconds - Completion time in seconds
|
|
||||||
* @param par - Par time in seconds
|
|
||||||
* @returns 0-3 stars
|
|
||||||
*/
|
*/
|
||||||
function getTimeStars(seconds: number, par: number): number {
|
function getAsteroidStars(asteroidScore: number): number {
|
||||||
const ratio = seconds / par;
|
// Assumes average ~20,000 pts for good performance
|
||||||
if (ratio <= 0.5) return 3; // Finished in half the par time
|
if (asteroidScore >= 25000) return 3;
|
||||||
if (ratio <= 1.0) return 2; // Finished at or under par
|
if (asteroidScore >= 15000) return 2;
|
||||||
if (ratio <= 1.5) return 1; // Finished within 150% of par
|
if (asteroidScore >= 8000) return 1;
|
||||||
return 0; // Over 150% of par
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate accuracy stars based on hit percentage
|
* Calculate accuracy stars based on hit percentage
|
||||||
*
|
|
||||||
* @param accuracy - Shot accuracy percentage (0-100)
|
* @param accuracy - Shot accuracy percentage (0-100)
|
||||||
* @returns 0-3 stars
|
* @returns 0-3 stars
|
||||||
*/
|
*/
|
||||||
function getAccuracyStars(accuracy: number): number {
|
function getAccuracyStars(accuracy: number): number {
|
||||||
if (accuracy >= 75) return 3; // Excellent accuracy
|
if (accuracy >= 80) return 3;
|
||||||
if (accuracy >= 50) return 2; // Good accuracy
|
if (accuracy >= 50) return 2;
|
||||||
if (accuracy >= 25) return 1; // Fair accuracy
|
if (accuracy >= 20) return 1;
|
||||||
return 0; // Poor accuracy
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate fuel efficiency stars
|
* Calculate fuel efficiency stars
|
||||||
*
|
* @param fuelConsumed - Fuel consumed percentage (0-300+%)
|
||||||
* @param fuelConsumed - Fuel consumed percentage (0-∞)
|
|
||||||
* @returns 0-3 stars
|
* @returns 0-3 stars
|
||||||
*/
|
*/
|
||||||
function getFuelStars(fuelConsumed: number): number {
|
function getFuelStars(fuelConsumed: number): number {
|
||||||
// Stars only consider first 100% of fuel
|
if (fuelConsumed <= 50) return 3;
|
||||||
// Refueling doesn't earn extra stars
|
if (fuelConsumed <= 150) return 2;
|
||||||
if (fuelConsumed <= 30) return 3; // Used ≤30% fuel
|
if (fuelConsumed <= 250) return 1;
|
||||||
if (fuelConsumed <= 60) return 2; // Used ≤60% fuel
|
return 0;
|
||||||
if (fuelConsumed <= 80) return 1; // Used ≤80% fuel
|
|
||||||
return 0; // Used >80% fuel (including refuels)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate hull integrity stars
|
* Calculate hull integrity stars
|
||||||
*
|
* @param hullDamage - Hull damage percentage (0-300+%)
|
||||||
* @param hullDamage - Hull damage percentage (0-∞)
|
|
||||||
* @returns 0-3 stars
|
* @returns 0-3 stars
|
||||||
*/
|
*/
|
||||||
function getHullStars(hullDamage: number): number {
|
function getHullStars(hullDamage: number): number {
|
||||||
// Stars only consider first 100% of damage
|
if (hullDamage <= 30) return 3;
|
||||||
// Dying and respawning = 0 stars
|
if (hullDamage <= 100) return 2;
|
||||||
if (hullDamage <= 10) return 3; // Took ≤10% damage
|
if (hullDamage <= 200) return 1;
|
||||||
if (hullDamage <= 30) return 2; // Took ≤30% damage
|
return 0;
|
||||||
if (hullDamage <= 60) return 1; // Took ≤60% damage
|
|
||||||
return 0; // Took >60% damage (including deaths)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get star rating color based on count
|
* Get star rating color based on count
|
||||||
*
|
|
||||||
* @param stars - Number of stars (0-3)
|
* @param stars - Number of stars (0-3)
|
||||||
* @returns Hex color code
|
* @returns Hex color code
|
||||||
*/
|
*/
|
||||||
@ -245,7 +182,6 @@ export function getStarColor(stars: number): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Format stars as Unicode string
|
* Format stars as Unicode string
|
||||||
*
|
|
||||||
* @param earned - Number of stars earned (0-3)
|
* @param earned - Number of stars earned (0-3)
|
||||||
* @param total - Total possible stars (default: 3)
|
* @param total - Total possible stars (default: 3)
|
||||||
* @returns Unicode star string (e.g., "★★☆")
|
* @returns Unicode star string (e.g., "★★☆")
|
||||||
|
|||||||
@ -154,7 +154,7 @@ export class GameResultsService {
|
|||||||
|
|
||||||
// Get stats
|
// Get stats
|
||||||
const stats = gameStats.getStats();
|
const stats = gameStats.getStats();
|
||||||
const scoreCalc = gameStats.calculateFinalScore(parTime);
|
const scoreCalc = gameStats.getFinalScore();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
|
|||||||
@ -418,17 +418,17 @@ export class Ship {
|
|||||||
this._scoreboard.initialize();
|
this._scoreboard.initialize();
|
||||||
|
|
||||||
// Subscribe to score events to track asteroids destroyed
|
// Subscribe to score events to track asteroids destroyed
|
||||||
this._scoreboard.onScoreObservable.add(() => {
|
this._scoreboard.onScoreObservable.add((event) => {
|
||||||
// Each score event represents an asteroid destroyed
|
// Each score event represents an asteroid destroyed, pass scale for point calc
|
||||||
this._gameStats.recordAsteroidDestroyed();
|
this._gameStats.recordAsteroidDestroyed(event.scale || 1);
|
||||||
|
|
||||||
// Track asteroid destruction in analytics
|
// Track asteroid destruction in analytics
|
||||||
try {
|
try {
|
||||||
const analytics = getAnalytics();
|
const analytics = getAnalytics();
|
||||||
analytics.track('asteroid_destroyed', {
|
analytics.track('asteroid_destroyed', {
|
||||||
weaponType: 'laser', // TODO: Get actual weapon type from event
|
weaponType: 'laser',
|
||||||
distance: 0, // TODO: Calculate distance if available
|
distance: 0,
|
||||||
asteroidSize: 0, // TODO: Get actual size if available
|
asteroidSize: event.scale || 0,
|
||||||
remainingCount: this._scoreboard.remaining
|
remainingCount: this._scoreboard.remaining
|
||||||
}, { sampleRate: 0.2 }); // Sample 20% of asteroid events to reduce data
|
}, { sampleRate: 0.2 }); // Sample 20% of asteroid events to reduce data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -198,12 +198,14 @@ export class WeaponSystem {
|
|||||||
if (isAsteroid) {
|
if (isAsteroid) {
|
||||||
log.debug('[WeaponSystem] Asteroid hit! Triggering destruction...');
|
log.debug('[WeaponSystem] Asteroid hit! Triggering destruction...');
|
||||||
|
|
||||||
// Update score
|
// Update score with asteroid scale for point calculation
|
||||||
if (this._scoreObservable) {
|
if (this._scoreObservable) {
|
||||||
|
const asteroidScale = hitMesh.scaling.x;
|
||||||
this._scoreObservable.notifyObservers({
|
this._scoreObservable.notifyObservers({
|
||||||
score: 1,
|
score: 1,
|
||||||
remaining: -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 log from '../../core/logger';
|
||||||
import { LevelConfig } from "../../levels/config/levelConfig";
|
import { LevelConfig } from "../../levels/config/levelConfig";
|
||||||
import { CloudLevelEntry } from "../../services/cloudLevelService";
|
import { CloudLevelEntry } from "../../services/cloudLevelService";
|
||||||
|
import { addButtonHoverEffect } from "../utils/buttonEffects";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mission brief display for VR
|
* Mission brief display for VR
|
||||||
@ -48,9 +49,9 @@ export class MissionBrief {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mesh.parent = ship;
|
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.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
|
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 parented to ship at position:', mesh.position);
|
||||||
log.info('[MissionBrief] Mesh absolute position:', mesh.getAbsolutePosition());
|
log.info('[MissionBrief] Mesh absolute position:', mesh.getAbsolutePosition());
|
||||||
@ -71,7 +72,7 @@ export class MissionBrief {
|
|||||||
this._container.height = "600px";
|
this._container.height = "600px";
|
||||||
this._container.thickness = 4;
|
this._container.thickness = 4;
|
||||||
this._container.color = "#00ff00";
|
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.cornerRadius = 20;
|
||||||
this._container.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
this._container.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||||
this._container.verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER;
|
this._container.verticalAlignment = Control.VERTICAL_ALIGNMENT_CENTER;
|
||||||
@ -202,7 +203,7 @@ export class MissionBrief {
|
|||||||
|
|
||||||
// Spacer before button
|
// Spacer before button
|
||||||
const spacer3 = new Rectangle("spacer3");
|
const spacer3 = new Rectangle("spacer3");
|
||||||
spacer3.height = "40px";
|
spacer3.height = "20px";
|
||||||
spacer3.thickness = 0;
|
spacer3.thickness = 0;
|
||||||
contentPanel.addControl(spacer3);
|
contentPanel.addControl(spacer3);
|
||||||
|
|
||||||
@ -217,6 +218,8 @@ export class MissionBrief {
|
|||||||
startButton.fontSize = "36px";
|
startButton.fontSize = "36px";
|
||||||
startButton.fontWeight = "bold";
|
startButton.fontWeight = "bold";
|
||||||
startButton.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
startButton.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||||
|
addButtonHoverEffect(startButton);
|
||||||
|
|
||||||
startButton.onPointerClickObservable.add(() => {
|
startButton.onPointerClickObservable.add(() => {
|
||||||
log.debug('[MissionBrief] START button clicked - dismissing mission brief');
|
log.debug('[MissionBrief] START button clicked - dismissing mission brief');
|
||||||
this.hide();
|
this.hide();
|
||||||
|
|||||||
@ -13,7 +13,8 @@ export type ScoreEvent = {
|
|||||||
score: number,
|
score: number,
|
||||||
message: string,
|
message: string,
|
||||||
remaining: number,
|
remaining: number,
|
||||||
timeRemaining? : number
|
scale?: number, // Asteroid scale for point calculation
|
||||||
|
timeRemaining?: number
|
||||||
}
|
}
|
||||||
export class Scoreboard {
|
export class Scoreboard {
|
||||||
private _score: number = 0;
|
private _score: number = 0;
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { GameStats } from "../../game/gameStats";
|
|||||||
import { DefaultScene } from "../../core/defaultScene";
|
import { DefaultScene } from "../../core/defaultScene";
|
||||||
import { ProgressionManager } from "../../game/progression";
|
import { ProgressionManager } from "../../game/progression";
|
||||||
import { AuthService } from "../../services/authService";
|
import { AuthService } from "../../services/authService";
|
||||||
|
import { addButtonHoverEffect } from "../utils/buttonEffects";
|
||||||
import { FacebookShare, ShareData } from "../../services/facebookShare";
|
import { FacebookShare, ShareData } from "../../services/facebookShare";
|
||||||
import { InputControlManager } from "../../ship/input/inputControlManager";
|
import { InputControlManager } from "../../ship/input/inputControlManager";
|
||||||
import { formatStars } from "../../game/scoreCalculator";
|
import { formatStars } from "../../game/scoreCalculator";
|
||||||
@ -46,9 +47,11 @@ export class StatusScreen {
|
|||||||
private _fuelConsumedText: TextBlock;
|
private _fuelConsumedText: TextBlock;
|
||||||
|
|
||||||
// Text blocks for score display
|
// Text blocks for score display
|
||||||
|
private _scoreTitleText: TextBlock;
|
||||||
private _finalScoreText: TextBlock;
|
private _finalScoreText: TextBlock;
|
||||||
private _scoreBreakdownText: TextBlock;
|
private _scoreBreakdownText: TextBlock;
|
||||||
private _starRatingText: TextBlock;
|
private _starsContainer: StackPanel;
|
||||||
|
private _totalStarsText: TextBlock;
|
||||||
|
|
||||||
// Buttons
|
// Buttons
|
||||||
private _replayButton: Button;
|
private _replayButton: Button;
|
||||||
@ -97,8 +100,8 @@ export class StatusScreen {
|
|||||||
|
|
||||||
// Parent to ship for fixed cockpit position
|
// Parent to ship for fixed cockpit position
|
||||||
this._screenMesh.parent = this._shipNode;
|
this._screenMesh.parent = this._shipNode;
|
||||||
this._screenMesh.position = new Vector3(0, 1, 2); // 2 meters forward in local space
|
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.renderingGroupId = 3; // Always render on top
|
||||||
this._screenMesh.metadata = { uiPickable: true }; // TAG: VR UI - allow pointer selection
|
this._screenMesh.metadata = { uiPickable: true }; // TAG: VR UI - allow pointer selection
|
||||||
|
|
||||||
// Create material
|
// Create material
|
||||||
@ -165,11 +168,11 @@ export class StatusScreen {
|
|||||||
const spacer2b = this.createSpacer(30);
|
const spacer2b = this.createSpacer(30);
|
||||||
mainPanel.addControl(spacer2b);
|
mainPanel.addControl(spacer2b);
|
||||||
|
|
||||||
// Final score display
|
// Score title (changes based on game state)
|
||||||
const scoreTitle = this.createTitleText("FINAL SCORE");
|
this._scoreTitleText = this.createTitleText("CURRENT SCORE");
|
||||||
scoreTitle.fontSize = "50px";
|
this._scoreTitleText.fontSize = "50px";
|
||||||
scoreTitle.height = "70px";
|
this._scoreTitleText.height = "70px";
|
||||||
mainPanel.addControl(scoreTitle);
|
mainPanel.addControl(this._scoreTitleText);
|
||||||
|
|
||||||
this._finalScoreText = new TextBlock();
|
this._finalScoreText = new TextBlock();
|
||||||
this._finalScoreText.text = "0";
|
this._finalScoreText.text = "0";
|
||||||
@ -186,22 +189,28 @@ export class StatusScreen {
|
|||||||
this._scoreBreakdownText.color = "#aaaaaa";
|
this._scoreBreakdownText.color = "#aaaaaa";
|
||||||
this._scoreBreakdownText.fontSize = "20px";
|
this._scoreBreakdownText.fontSize = "20px";
|
||||||
this._scoreBreakdownText.height = "120px";
|
this._scoreBreakdownText.height = "120px";
|
||||||
|
this._scoreBreakdownText.width = "100%";
|
||||||
this._scoreBreakdownText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
this._scoreBreakdownText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||||
this._scoreBreakdownText.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_TOP;
|
this._scoreBreakdownText.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_TOP;
|
||||||
this._scoreBreakdownText.textWrapping = true;
|
this._scoreBreakdownText.textWrapping = true;
|
||||||
mainPanel.addControl(this._scoreBreakdownText);
|
mainPanel.addControl(this._scoreBreakdownText);
|
||||||
|
|
||||||
// Star ratings
|
// Star ratings container (populated in updateStatistics)
|
||||||
this._starRatingText = new TextBlock();
|
this._starsContainer = new StackPanel("starsContainer");
|
||||||
this._starRatingText.text = "";
|
this._starsContainer.isVertical = false;
|
||||||
this._starRatingText.color = "#FFD700";
|
this._starsContainer.height = "100px";
|
||||||
this._starRatingText.fontSize = "40px";
|
this._starsContainer.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||||
this._starRatingText.height = "100px";
|
mainPanel.addControl(this._starsContainer);
|
||||||
this._starRatingText.fontWeight = "bold";
|
|
||||||
this._starRatingText.textHorizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
// Total stars display
|
||||||
this._starRatingText.textVerticalAlignment = Control.VERTICAL_ALIGNMENT_TOP;
|
this._totalStarsText = new TextBlock();
|
||||||
this._starRatingText.textWrapping = true;
|
this._totalStarsText.text = "";
|
||||||
mainPanel.addControl(this._starRatingText);
|
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
|
// Add spacing before buttons
|
||||||
const spacer3 = this.createSpacer(40);
|
const spacer3 = this.createSpacer(40);
|
||||||
@ -213,6 +222,9 @@ export class StatusScreen {
|
|||||||
buttonBar.height = "80px";
|
buttonBar.height = "80px";
|
||||||
buttonBar.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
buttonBar.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
|
||||||
buttonBar.spacing = 20;
|
buttonBar.spacing = 20;
|
||||||
|
buttonBar.paddingLeft = 20;
|
||||||
|
buttonBar.paddingRight = 20;
|
||||||
|
buttonBar.clipChildren = false;
|
||||||
|
|
||||||
// Create Resume button (only shown when game hasn't ended)
|
// Create Resume button (only shown when game hasn't ended)
|
||||||
this._resumeButton = Button.CreateSimpleButton("resumeButton", "RESUME GAME");
|
this._resumeButton = Button.CreateSimpleButton("resumeButton", "RESUME GAME");
|
||||||
@ -224,6 +236,7 @@ export class StatusScreen {
|
|||||||
this._resumeButton.thickness = 0;
|
this._resumeButton.thickness = 0;
|
||||||
this._resumeButton.fontSize = "30px";
|
this._resumeButton.fontSize = "30px";
|
||||||
this._resumeButton.fontWeight = "bold";
|
this._resumeButton.fontWeight = "bold";
|
||||||
|
addButtonHoverEffect(this._resumeButton);
|
||||||
this._resumeButton.onPointerClickObservable.add(() => {
|
this._resumeButton.onPointerClickObservable.add(() => {
|
||||||
if (this._onResumeCallback) {
|
if (this._onResumeCallback) {
|
||||||
this._onResumeCallback();
|
this._onResumeCallback();
|
||||||
@ -275,6 +288,7 @@ export class StatusScreen {
|
|||||||
this._exitButton.thickness = 0;
|
this._exitButton.thickness = 0;
|
||||||
this._exitButton.fontSize = "30px";
|
this._exitButton.fontSize = "30px";
|
||||||
this._exitButton.fontWeight = "bold";
|
this._exitButton.fontWeight = "bold";
|
||||||
|
addButtonHoverEffect(this._exitButton, "#cc3333", "#ff4444");
|
||||||
this._exitButton.onPointerClickObservable.add(() => {
|
this._exitButton.onPointerClickObservable.add(() => {
|
||||||
if (this._onExitCallback) {
|
if (this._onExitCallback) {
|
||||||
this._onExitCallback();
|
this._onExitCallback();
|
||||||
@ -356,6 +370,33 @@ export class StatusScreen {
|
|||||||
return spacer;
|
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
|
* Toggle visibility of status screen
|
||||||
*/
|
*/
|
||||||
@ -502,29 +543,35 @@ export class StatusScreen {
|
|||||||
this._accuracyText.text = `Accuracy: ${stats.accuracy}%`;
|
this._accuracyText.text = `Accuracy: ${stats.accuracy}%`;
|
||||||
this._fuelConsumedText.text = `Fuel Consumed: ${stats.fuelConsumed}%`;
|
this._fuelConsumedText.text = `Fuel Consumed: ${stats.fuelConsumed}%`;
|
||||||
|
|
||||||
// Calculate and display score
|
// Calculate score - only include end-game bonuses if game has ended
|
||||||
const scoreCalc = this._gameStats.calculateFinalScore(this._parTime);
|
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();
|
this._finalScoreText.text = scoreCalc.finalScore.toLocaleString();
|
||||||
|
|
||||||
// Update score breakdown
|
// Update score breakdown - show bonuses only at game end
|
||||||
this._scoreBreakdownText.text =
|
if (this._isGameEnded) {
|
||||||
`Time: ${scoreCalc.timeMultiplier.toFixed(2)}x | ` +
|
this._scoreBreakdownText.text =
|
||||||
`Accuracy: ${scoreCalc.accuracyMultiplier.toFixed(2)}x\n` +
|
`Asteroids: ${scoreCalc.asteroidScore.toLocaleString()} | ` +
|
||||||
`Fuel: ${scoreCalc.fuelMultiplier.toFixed(2)}x | ` +
|
`Acc: +${scoreCalc.bonuses.accuracy.toLocaleString()}\n` +
|
||||||
`Hull: ${scoreCalc.hullMultiplier.toFixed(2)}x`;
|
`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
|
// Rebuild star rating columns
|
||||||
const timeStars = formatStars(scoreCalc.stars.time);
|
this._starsContainer.clearControls();
|
||||||
const accStars = formatStars(scoreCalc.stars.accuracy);
|
this._starsContainer.addControl(this.createStarRatingColumn(scoreCalc.stars.asteroids, "Kills"));
|
||||||
const fuelStars = formatStars(scoreCalc.stars.fuel);
|
this._starsContainer.addControl(this.createStarRatingColumn(scoreCalc.stars.accuracy, "Acc"));
|
||||||
const hullStars = formatStars(scoreCalc.stars.hull);
|
this._starsContainer.addControl(this.createStarRatingColumn(scoreCalc.stars.fuel, "Fuel"));
|
||||||
|
this._starsContainer.addControl(this.createStarRatingColumn(scoreCalc.stars.hull, "Hull"));
|
||||||
|
|
||||||
this._starRatingText.text =
|
// Update total stars
|
||||||
`${timeStars} ${accStars} ${fuelStars} ${hullStars}\n` +
|
this._totalStarsText.text = `${scoreCalc.stars.total}/12 Stars`;
|
||||||
`Time Acc Fuel Hull\n` +
|
|
||||||
`${scoreCalc.stars.total}/12 Stars`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -55,7 +55,14 @@ export class Preloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private setupButtonHandler(): void {
|
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 {
|
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