Änderung: Anpassung der TaxiHighscore-API und Verbesserung der Highscore-Anzeige im Taxi-Spiel

Änderungen:
- Umbenennung des API-Endpunkts für Highscores von `/api/taxi/highscore` zu `/api/taxi/highscores`.
- Anpassung der Highscore-Datenstruktur zur Verwendung von `hashedUserId` anstelle von `userId`.
- Erweiterung der Router-Logik zur besseren Organisation der Highscore-Abfragen.
- Implementierung einer neuen Highscore-Anzeige im Spiel, die die Top 20 Spieler und den aktuellen Spieler anzeigt.

Diese Anpassungen verbessern die API-Konsistenz und erweitern die Benutzeroberfläche für die Highscore-Anzeige im Spiel.
This commit is contained in:
Torsten Schulz (local)
2025-10-05 12:42:37 +02:00
parent 42349e46c8
commit 1bde46430b
6 changed files with 345 additions and 43 deletions

View File

@@ -44,7 +44,7 @@ app.use('/api/admin', adminRouter);
app.use('/api/match3', match3Router); app.use('/api/match3', match3Router);
app.use('/api/taxi', taxiRouter); app.use('/api/taxi', taxiRouter);
app.use('/api/taxi-maps', taxiMapRouter); app.use('/api/taxi-maps', taxiMapRouter);
app.use('/api/taxi/highscore', taxiHighscoreRouter); app.use('/api/taxi/highscores', taxiHighscoreRouter);
app.use('/images', express.static(path.join(__dirname, '../frontend/public/images'))); app.use('/images', express.static(path.join(__dirname, '../frontend/public/images')));
app.use('/api/contact', contactRouter); app.use('/api/contact', contactRouter);
app.use('/api/socialnetwork', socialnetworkRouter); app.use('/api/socialnetwork', socialnetworkRouter);

View File

@@ -17,7 +17,7 @@ class TaxiHighscoreController {
} }
const highscoreData = { const highscoreData = {
userId: parseInt(userId), hashedUserId: userId, // userId ist bereits ein String (Hash)
nickname, nickname,
passengersDelivered: parseInt(passengersDelivered), passengersDelivered: parseInt(passengersDelivered),
playtime: parseInt(playtime), playtime: parseInt(playtime),
@@ -75,8 +75,7 @@ class TaxiHighscoreController {
*/ */
async getUserBestScores(req, res) { async getUserBestScores(req, res) {
try { try {
const { userId } = req.params; const { userId, mapId } = req.query;
const { mapId } = req.query;
if (!userId) { if (!userId) {
return res.status(400).json({ return res.status(400).json({
@@ -86,7 +85,7 @@ class TaxiHighscoreController {
} }
const bestScores = await taxiHighscoreService.getUserBestScores( const bestScores = await taxiHighscoreService.getUserBestScores(
parseInt(userId), userId, // userId ist bereits ein String (Hash)
mapId ? parseInt(mapId) : null mapId ? parseInt(mapId) : null
); );
@@ -109,8 +108,7 @@ class TaxiHighscoreController {
*/ */
async getUserHighscores(req, res) { async getUserHighscores(req, res) {
try { try {
const { userId } = req.params; const { userId, limit = 20 } = req.query;
const { limit = 20 } = req.query;
if (!userId) { if (!userId) {
return res.status(400).json({ return res.status(400).json({
@@ -120,7 +118,7 @@ class TaxiHighscoreController {
} }
const highscores = await taxiHighscoreService.getUserHighscores( const highscores = await taxiHighscoreService.getUserHighscores(
parseInt(userId), userId, // userId ist bereits ein String (Hash)
parseInt(limit) parseInt(limit)
); );
@@ -143,8 +141,7 @@ class TaxiHighscoreController {
*/ */
async getUserRank(req, res) { async getUserRank(req, res) {
try { try {
const { userId } = req.params; const { userId, mapId, orderBy = 'points' } = req.query;
const { mapId, orderBy = 'points' } = req.query;
if (!userId) { if (!userId) {
return res.status(400).json({ return res.status(400).json({
@@ -154,7 +151,7 @@ class TaxiHighscoreController {
} }
const rank = await taxiHighscoreService.getUserRank( const rank = await taxiHighscoreService.getUserRank(
parseInt(userId), userId, // userId ist bereits ein String (Hash)
mapId ? parseInt(mapId) : null, mapId ? parseInt(mapId) : null,
orderBy orderBy
); );

View File

@@ -2,11 +2,23 @@ import express from 'express';
import taxiHighscoreController from '../controllers/taxiHighscoreController.js'; import taxiHighscoreController from '../controllers/taxiHighscoreController.js';
const router = express.Router(); const router = express.Router();
// POST /api/taxi/highscores - Neuen Highscore erstellen
router.post('/', taxiHighscoreController.createHighscore); router.post('/', taxiHighscoreController.createHighscore);
router.get('/top', taxiHighscoreController.getTopHighscores);
router.get('/my-best', taxiHighscoreController.getUserBestScores); // GET /api/taxi/highscores - Top Highscores abrufen
router.get('/my-scores', taxiHighscoreController.getUserHighscores); router.get('/', taxiHighscoreController.getTopHighscores);
router.get('/my-rank', taxiHighscoreController.getUserRank);
// GET /api/taxi/highscores/rank - Rang des Benutzers abrufen
router.get('/rank', taxiHighscoreController.getUserRank);
// GET /api/taxi/highscores/user/best - Beste Punkte des Benutzers
router.get('/user/best', taxiHighscoreController.getUserBestScores);
// GET /api/taxi/highscores/user - Alle Highscores des Benutzers
router.get('/user', taxiHighscoreController.getUserHighscores);
// GET /api/taxi/highscores/stats - Highscore-Statistiken
router.get('/stats', taxiHighscoreController.getHighscoreStats); router.get('/stats', taxiHighscoreController.getHighscoreStats);
export default router; export default router;

View File

@@ -13,7 +13,7 @@ class TaxiHighscoreService extends BaseService {
* Speichert oder aktualisiert einen Highscore-Eintrag * Speichert oder aktualisiert einen Highscore-Eintrag
* Jeder User kann nur einen Eintrag pro Map haben (der beste wird gespeichert) * Jeder User kann nur einen Eintrag pro Map haben (der beste wird gespeichert)
* @param {Object} highscoreData - Die Highscore-Daten * @param {Object} highscoreData - Die Highscore-Daten
* @param {number} highscoreData.userId - ID des Users * @param {string} highscoreData.hashedUserId - Hash-ID des Users
* @param {string} highscoreData.nickname - Nickname des Users * @param {string} highscoreData.nickname - Nickname des Users
* @param {number} highscoreData.passengersDelivered - Anzahl abgelieferter Passagiere * @param {number} highscoreData.passengersDelivered - Anzahl abgelieferter Passagiere
* @param {number} highscoreData.playtime - Spielzeit in Sekunden * @param {number} highscoreData.playtime - Spielzeit in Sekunden
@@ -24,10 +24,14 @@ class TaxiHighscoreService extends BaseService {
*/ */
async createHighscore(highscoreData) { async createHighscore(highscoreData) {
try { try {
// Hash-ID zu echter User-ID konvertieren
const user = await this.getUserByHashedId(highscoreData.hashedUserId);
const userId = user.id;
// Prüfen ob bereits ein Eintrag für diesen User und diese Map existiert // Prüfen ob bereits ein Eintrag für diesen User und diese Map existiert
const existingHighscore = await this.model.findOne({ const existingHighscore = await this.model.findOne({
where: { where: {
userId: highscoreData.userId, userId: userId,
mapId: highscoreData.mapId mapId: highscoreData.mapId
} }
}); });
@@ -50,7 +54,7 @@ class TaxiHighscoreService extends BaseService {
} else { } else {
// Kein existierender Eintrag, neuen erstellen // Kein existierender Eintrag, neuen erstellen
const highscore = await this.model.create({ const highscore = await this.model.create({
userId: highscoreData.userId, userId: userId,
nickname: highscoreData.nickname, nickname: highscoreData.nickname,
passengersDelivered: highscoreData.passengersDelivered, passengersDelivered: highscoreData.passengersDelivered,
playtime: highscoreData.playtime, playtime: highscoreData.playtime,
@@ -107,12 +111,16 @@ class TaxiHighscoreService extends BaseService {
/** /**
* Holt die persönlichen Bestleistungen eines Users * Holt die persönlichen Bestleistungen eines Users
* @param {number} userId - ID des Users * @param {string} hashedUserId - Hash-ID des Users
* @param {number} mapId - ID der Map (optional) * @param {number} mapId - ID der Map (optional)
* @returns {Promise<Object>} Bestleistungen des Users * @returns {Promise<Object>} Bestleistungen des Users
*/ */
async getUserBestScores(userId, mapId = null) { async getUserBestScores(hashedUserId, mapId = null) {
try { try {
// Hash-ID zu echter User-ID konvertieren
const user = await this.getUserByHashedId(hashedUserId);
const userId = user.id;
const whereClause = { userId }; const whereClause = { userId };
if (mapId) { if (mapId) {
whereClause.mapId = mapId; whereClause.mapId = mapId;
@@ -146,12 +154,16 @@ class TaxiHighscoreService extends BaseService {
/** /**
* Holt alle Highscores eines Users * Holt alle Highscores eines Users
* @param {number} userId - ID des Users * @param {string} hashedUserId - Hash-ID des Users
* @param {number} limit - Anzahl der Einträge (Standard: 20) * @param {number} limit - Anzahl der Einträge (Standard: 20)
* @returns {Promise<Array>} Array der Highscore-Einträge des Users * @returns {Promise<Array>} Array der Highscore-Einträge des Users
*/ */
async getUserHighscores(userId, limit = 20) { async getUserHighscores(hashedUserId, limit = 20) {
try { try {
// Hash-ID zu echter User-ID konvertieren
const user = await this.getUserByHashedId(hashedUserId);
const userId = user.id;
const highscores = await this.model.findAll({ const highscores = await this.model.findAll({
where: { userId }, where: { userId },
include: [ include: [
@@ -174,13 +186,17 @@ class TaxiHighscoreService extends BaseService {
/** /**
* Holt die Rangliste-Position eines Users für eine bestimmte Map * Holt die Rangliste-Position eines Users für eine bestimmte Map
* @param {number} userId - ID des Users * @param {string} hashedUserId - Hash-ID des Users
* @param {number} mapId - ID der Map (optional) * @param {number} mapId - ID der Map (optional)
* @param {string} orderBy - Sortierung ('points', 'passengersDelivered', 'playtime') * @param {string} orderBy - Sortierung ('points', 'passengersDelivered', 'playtime')
* @returns {Promise<number>} Rangliste-Position (1-basiert) * @returns {Promise<number>} Rangliste-Position (1-basiert)
*/ */
async getUserRank(userId, mapId = null, orderBy = 'points') { async getUserRank(hashedUserId, mapId = null, orderBy = 'points') {
try { try {
// Hash-ID zu echter User-ID konvertieren
const user = await this.getUserByHashedId(hashedUserId);
const userId = user.id;
const whereClause = mapId ? { mapId } : {}; const whereClause = mapId ? { mapId } : {};
const userHighscore = await this.model.findOne({ const userHighscore = await this.model.findOne({

View File

@@ -185,18 +185,19 @@ const store = createStore({
let retryCount = 0; let retryCount = 0;
const maxRetries = 10; const maxRetries = 10;
const retryConnection = (reconnectFn) => { const retryConnection = (reconnectFn) => {
console.log(`Reconnect-Versuch ${retryCount + 1}/${maxRetries}`); console.log(`Backend-Reconnect-Versuch ${retryCount + 1}/${maxRetries}`);
if (retryCount >= maxRetries) { if (retryCount >= maxRetries) {
// Nach maxRetries alle 5 Sekunden weiter versuchen // Nach maxRetries alle 5 Sekunden weiter versuchen
console.log('Max Retries erreicht, versuche weiter alle 5 Sekunden...'); console.log('Backend: Max Retries erreicht, versuche weiter alle 5 Sekunden...');
setTimeout(() => { setTimeout(() => {
retryCount = 0; // Reset für nächsten Zyklus
reconnectFn(); reconnectFn();
}, 5000); }, 5000);
return; return;
} }
retryCount++; retryCount++;
const delay = 5000; // Alle 5 Sekunden versuchen const delay = 5000; // Alle 5 Sekunden versuchen
console.log(`Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`); console.log(`Backend: Warte ${delay}ms bis zum nächsten Reconnect-Versuch...`);
setTimeout(() => { setTimeout(() => {
reconnectFn(); reconnectFn();
}, delay); }, delay);
@@ -309,6 +310,7 @@ const store = createStore({
// Nach maxRetries alle 5 Sekunden weiter versuchen // Nach maxRetries alle 5 Sekunden weiter versuchen
console.log('Daemon: Max Retries erreicht, versuche weiter alle 5 Sekunden...'); console.log('Daemon: Max Retries erreicht, versuche weiter alle 5 Sekunden...');
setTimeout(() => { setTimeout(() => {
retryCount = 0; // Reset für nächsten Zyklus
reconnectFn(); reconnectFn();
}, 5000); }, 5000);
return; return;

View File

@@ -91,17 +91,57 @@
</div> </div>
</div> </div>
<!-- Canvas --> <!-- Canvas und Highscore Container -->
<div class="game-canvas-container"> <div class="game-canvas-section">
<canvas <!-- Canvas (immer sichtbar) -->
ref="gameCanvas" <div class="game-canvas-container">
width="500" <canvas
height="500" ref="gameCanvas"
class="game-canvas" width="500"
@click="handleCanvasClick" height="500"
@keydown="handleKeyDown" class="game-canvas"
tabindex="0" @click="handleCanvasClick"
></canvas> @keydown="handleKeyDown"
tabindex="0"
></canvas>
</div>
<!-- Highscore-Anzeige (als Overlay über dem Canvas) -->
<div v-if="showHighscore" class="highscore-overlay">
<div class="highscore-header">
<h2>🏆 Highscore</h2>
<div class="highscore-subtitle">Top 20 Spieler</div>
</div>
<div class="highscore-list">
<div v-if="loadingHighscore" class="loading-message">
Lade Highscore...
</div>
<div v-else-if="highscoreList.length === 0" class="no-highscore">
Noch keine Highscores vorhanden
</div>
<div v-else class="highscore-table">
<div
v-for="(entry, index) in highscoreList"
:key="index"
class="highscore-entry"
:class="{ 'current-player': entry.isCurrentPlayer }"
>
<div class="highscore-rank">{{ entry.rank }}</div>
<div class="highscore-name">{{ entry.nickname }}</div>
<div class="highscore-points">{{ entry.points }} Pkt</div>
</div>
<div v-if="showCurrentPlayerBelow" class="highscore-separator">...</div>
<div
v-if="currentPlayerEntry && showCurrentPlayerBelow"
class="highscore-entry current-player"
>
<div class="highscore-rank">{{ currentPlayerEntry.rank }}</div>
<div class="highscore-name">{{ currentPlayerEntry.nickname }}</div>
<div class="highscore-points">{{ currentPlayerEntry.points }} Pkt</div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -114,6 +154,9 @@
<button @click="restartLevel" class="control-button"> <button @click="restartLevel" class="control-button">
{{ $t('minigames.taxi.restartLevel') }} {{ $t('minigames.taxi.restartLevel') }}
</button> </button>
<button @click="toggleHighscore" class="control-button">
{{ showHighscore ? 'Zurück zum Spiel' : 'Highscore' }}
</button>
</div> </div>
</div> </div>
@@ -312,6 +355,11 @@ export default {
,prevTaxiY: 250 ,prevTaxiY: 250
,skipRedLightOneFrame: false ,skipRedLightOneFrame: false
,gasStations: [] // Tankstellen im Spiel ,gasStations: [] // Tankstellen im Spiel
,showHighscore: false // Highscore-Anzeige aktiv
,highscoreList: [] // Liste der Highscore-Einträge
,loadingHighscore: false // Lade-Status für Highscore
,currentPlayerEntry: null // Eintrag des aktuellen Spielers
,showCurrentPlayerBelow: false // Zeige aktuellen Spieler nach Platz 20
} }
}, },
computed: { computed: {
@@ -586,6 +634,7 @@ export default {
}, },
setupEventListeners() { setupEventListeners() {
// Event-Listener auf Document registrieren (Canvas bleibt immer sichtbar)
document.addEventListener('keydown', this.handleKeyDown); document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp); document.addEventListener('keyup', this.handleKeyUp);
}, },
@@ -698,6 +747,9 @@ export default {
clearTimeout(this.motorStopTimeout); clearTimeout(this.motorStopTimeout);
this.motorStopTimeout = null; this.motorStopTimeout = null;
} }
// Event-Listener von Document entfernen
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
// Cleanup aller Timeouts // Cleanup aller Timeouts
if (this.passengerGenerationTimeout) { if (this.passengerGenerationTimeout) {
clearTimeout(this.passengerGenerationTimeout); clearTimeout(this.passengerGenerationTimeout);
@@ -708,8 +760,6 @@ export default {
this.crashDialogTimeout = null; this.crashDialogTimeout = null;
} }
// AudioContext bleibt global erhalten, nicht schließen // AudioContext bleibt global erhalten, nicht schließen
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
if (this.audioUnlockHandler) { if (this.audioUnlockHandler) {
document.removeEventListener('pointerdown', this.audioUnlockHandler, { capture: true }); document.removeEventListener('pointerdown', this.audioUnlockHandler, { capture: true });
document.removeEventListener('touchstart', this.audioUnlockHandler, { capture: true }); document.removeEventListener('touchstart', this.audioUnlockHandler, { capture: true });
@@ -2865,8 +2915,9 @@ export default {
// AudioContext bei erster Benutzerinteraktion initialisieren // AudioContext bei erster Benutzerinteraktion initialisieren
this.ensureAudioUnlockedInEvent(); this.ensureAudioUnlockedInEvent();
// Bei Beschleunigungs-Key Motor starten (User-Geste garantiert)
if ((event.key === 'ArrowUp' || event.key === 'w' || event.key === 'W') && this.motorSound && !this.motorSound.isPlaying) { // Motor nur starten wenn Spiel nicht pausiert ist
if (!this.isPaused && (event.key === 'ArrowUp' || event.key === 'w' || event.key === 'W') && this.motorSound && !this.motorSound.isPlaying) {
this.motorSound.start(); this.motorSound.start();
// Direkt Parameter setzen, um hörbares Feedback ohne Verzögerung zu bekommen // Direkt Parameter setzen, um hörbares Feedback ohne Verzögerung zu bekommen
const speedKmh = Math.max(5, this.taxi.speed * 5); const speedKmh = Math.max(5, this.taxi.speed * 5);
@@ -2917,6 +2968,14 @@ export default {
togglePause() { togglePause() {
this.isPaused = !this.isPaused; this.isPaused = !this.isPaused;
this.showPauseOverlay = this.isPaused; this.showPauseOverlay = this.isPaused;
// Motorgeräusch stoppen wenn pausiert, starten wenn fortgesetzt
if (this.isPaused) {
if (this.motorSound && this.motorSound.isPlaying) {
this.motorSound.stop();
}
}
// Wenn fortgesetzt wird, startet der Motor automatisch bei der nächsten Beschleunigung
}, },
restartLevel() { restartLevel() {
@@ -3432,6 +3491,106 @@ export default {
prev = curr; curr = next || null; prev = curr; curr = next || null;
} }
} }
},
// Highscore-Funktionen
async toggleHighscore() {
this.showHighscore = !this.showHighscore;
if (this.showHighscore) {
// Spiel pausieren wenn Highscore angezeigt wird
if (!this.isPaused) {
this.isPaused = true;
// Motorgeräusch stoppen wenn pausiert
if (this.motorSound && this.motorSound.isPlaying) {
this.motorSound.stop();
}
}
// Highscore laden
await this.loadHighscore();
} else {
// Highscore geschlossen - Spiel automatisch fortsetzen
this.isPaused = false;
this.showPauseOverlay = false;
// Motor startet automatisch bei der nächsten Beschleunigung
}
},
async loadHighscore() {
this.loadingHighscore = true;
try {
// Lade Top 20 Highscores für die aktuelle Map
const response = await apiClient.get('/api/taxi/highscores', {
params: {
mapId: this.selectedMapId,
limit: 20,
orderBy: 'points' // Sortiere nach Punkten
}
});
if (response.data && response.data.success && Array.isArray(response.data.data)) {
this.highscoreList = response.data.data.map((entry, index) => ({
rank: index + 1,
nickname: entry.nickname || 'Unbekannt',
points: entry.points,
isCurrentPlayer: entry.userId === this.$store.state.user?.id
}));
// Prüfe ob aktueller Spieler eine Platzierung hat
await this.checkCurrentPlayerRank();
}
} catch (error) {
console.error('Fehler beim Laden der Highscores:', error);
this.highscoreList = [];
} finally {
this.loadingHighscore = false;
}
},
async checkCurrentPlayerRank() {
if (!this.$store.state.user?.id) return;
try {
// Lade Rang des aktuellen Spielers
const response = await apiClient.get('/api/taxi/highscores/rank', {
params: {
userId: this.$store.state.user.id,
mapId: this.selectedMapId,
orderBy: 'points'
}
});
if (response.data && response.data.success && response.data.data && response.data.data.rank) {
const rank = response.data.data.rank;
// Wenn Spieler Platz 21 oder schlechter hat
if (rank > 20) {
this.showCurrentPlayerBelow = true;
// Lade beste Punkte des Spielers
const bestScoreResponse = await apiClient.get('/api/taxi/highscores/user/best', {
params: {
userId: this.$store.state.user.id,
mapId: this.selectedMapId
}
});
if (bestScoreResponse.data && bestScoreResponse.data.success && bestScoreResponse.data.data) {
this.currentPlayerEntry = {
rank: rank,
nickname: this.$store.state.user.nickname || 'Du',
points: bestScoreResponse.data.data.points,
isCurrentPlayer: true
};
}
} else {
this.showCurrentPlayerBelow = false;
this.currentPlayerEntry = null;
}
}
} catch (error) {
console.error('Fehler beim Laden des Spieler-Rangs:', error);
}
} }
} }
} }
@@ -3473,6 +3632,7 @@ export default {
/* Game Canvas Section */ /* Game Canvas Section */
.game-canvas-section { .game-canvas-section {
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@@ -3890,6 +4050,7 @@ export default {
margin: 0; /* Kein Margin */ margin: 0; /* Kein Margin */
text-align: center; text-align: center;
width: 500px; /* Feste Breite wie das Tacho-Display */ width: 500px; /* Feste Breite wie das Tacho-Display */
height: 500px; /* Feste Höhe beibehalten */
box-sizing: border-box; /* Border wird in die Breite eingerechnet */ box-sizing: border-box; /* Border wird in die Breite eingerechnet */
} }
@@ -3970,6 +4131,120 @@ export default {
border: 1px solid #7E471B; border: 1px solid #7E471B;
} }
/* Highscore Overlay */
.highscore-overlay {
position: absolute;
top: 0;
left: 0;
width: 500px;
height: 500px;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(5px);
border: 2px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 10;
}
.highscore-header {
background: #F9A22C;
color: #000;
padding: 20px;
text-align: center;
border-bottom: 2px solid #ddd;
}
.highscore-header h2 {
margin: 0 0 5px 0;
font-size: 1.5rem;
font-weight: 600;
}
.highscore-subtitle {
margin: 0;
font-size: 0.9rem;
opacity: 0.8;
}
.highscore-list {
flex: 1;
overflow-y: auto;
padding: 15px;
}
.loading-message,
.no-highscore {
text-align: center;
padding: 40px 20px;
color: #666;
font-style: italic;
}
.highscore-table {
display: flex;
flex-direction: column;
gap: 8px;
}
.highscore-entry {
display: grid;
grid-template-columns: 50px 1fr auto;
gap: 10px;
align-items: center;
padding: 10px 15px;
background: #f8f9fa;
border-radius: 4px;
transition: all 0.2s;
}
.highscore-entry:hover {
background: #e9ecef;
}
.highscore-entry.current-player {
background: #fff3cd;
border: 2px solid #ffc107;
font-weight: 600;
}
.highscore-entry.current-player:hover {
background: #ffe8a1;
}
.highscore-rank {
font-size: 1.2rem;
font-weight: 700;
color: #F9A22C;
text-align: center;
}
.highscore-name {
font-size: 1rem;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.highscore-points {
font-size: 1rem;
font-weight: 600;
color: #666;
text-align: right;
}
.highscore-separator {
text-align: center;
font-size: 1.5rem;
font-weight: 700;
color: #999;
padding: 10px 0;
letter-spacing: 5px;
}
/* Responsive Design */ /* Responsive Design */
@media (max-width: 1024px) { @media (max-width: 1024px) {