added epic counters

This commit is contained in:
erik 2025-06-11 08:20:57 +00:00
parent 10c51f6825
commit 09a6cd4946
4 changed files with 963 additions and 8 deletions

View file

@ -17,12 +17,33 @@
<!-- Container for sort and filter controls -->
<div id="sortButtons" class="sort-buttons"></div>
<h2>Active Players</h2>
<h2 id="activePlayersHeader">Active Mosswart Enjoyers</h2>
<!-- Total rares counter -->
<div id="totalRaresCounter" class="total-rares-counter">
🔥 Total Rares: <span id="totalRaresCount">Loading...</span>
</div>
<!-- Server KPH counter -->
<div id="serverKphCounter" class="server-kph-counter">
⚡ Server KPH: <span id="serverKphCount">Loading...</span>
</div>
<!-- Total kills counter -->
<div id="totalKillsCounter" class="total-kills-counter">
⚔️ Total Kills: <span id="totalKillsCount">Loading...</span>
</div>
<!-- Text input to filter active players by name -->
<input type="text" id="playerFilter" class="player-filter" placeholder="Filter players..." />
<ul id="playerList"></ul>
</aside>
<!-- Epic rare notifications container -->
<div id="rareNotifications" class="rare-notifications"></div>
<!-- Fireworks container -->
<div id="fireworksContainer" class="fireworks-container"></div>
<!-- Main map container showing terrain and player data -->
<div id="mapContainer">
<div id="mapGroup">

View file

@ -930,10 +930,32 @@ async function pollLive() {
}
}
async function pollTotalRares() {
try {
const response = await fetch(`${API_BASE}/total-rares/`);
const data = await response.json();
updateTotalRaresDisplay(data);
} catch (e) {
console.error('Total rares fetch failed:', e);
}
}
function updateTotalRaresDisplay(data) {
const countElement = document.getElementById('totalRaresCount');
if (countElement && data.all_time !== undefined && data.today !== undefined) {
const allTimeFormatted = data.all_time.toLocaleString();
const todayFormatted = data.today.toLocaleString();
countElement.textContent = `${allTimeFormatted} (Today: ${todayFormatted})`;
}
}
function startPolling() {
if (pollID !== null) return;
pollLive();
pollTotalRares(); // Initial fetch
pollID = setInterval(pollLive, POLL_MS);
// Poll total rares every 5 minutes (300,000 ms)
setInterval(pollTotalRares, 300000);
}
img.onload = () => {
@ -968,6 +990,43 @@ function render(players) {
dots.innerHTML = '';
list.innerHTML = '';
// Update header with active player count
const header = document.getElementById('activePlayersHeader');
if (header) {
header.textContent = `Active Mosswart Enjoyers (${players.length})`;
}
// Calculate and update server KPH
const totalKPH = players.reduce((sum, p) => sum + (p.kills_per_hour || 0), 0);
const kphElement = document.getElementById('serverKphCount');
if (kphElement) {
// Format with commas and one decimal place for EPIC display
const formattedKPH = totalKPH.toLocaleString('en-US', {
minimumFractionDigits: 1,
maximumFractionDigits: 1
});
kphElement.textContent = formattedKPH;
// Add extra epic effect for high KPH
const container = document.getElementById('serverKphCounter');
if (container) {
if (totalKPH > 5000) {
container.classList.add('ultra-epic');
} else {
container.classList.remove('ultra-epic');
}
}
}
// Calculate and update total kills
const totalKills = players.reduce((sum, p) => sum + (p.total_kills || 0), 0);
const killsElement = document.getElementById('totalKillsCount');
if (killsElement) {
// Format with commas for readability
const formattedKills = totalKills.toLocaleString();
killsElement.textContent = formattedKills;
}
players.forEach(p => {
const { x, y } = worldToPx(p.ew, p.ns);
@ -1002,7 +1061,8 @@ function render(players) {
const kpr = totalRares > 0 ? Math.round(totalKills / totalRares) : '∞';
li.innerHTML = `
<span class="player-name">${p.character_name} <span class="coordinates-inline">${loc(p.ns, p.ew)}</span></span>
<span class="player-name">${p.character_name}${createVitaeIndicator(p.character_name)} <span class="coordinates-inline">${loc(p.ns, p.ew)}</span></span>
${createVitalsHTML(p.character_name)}
<span class="stat kills">${p.kills}</span>
<span class="stat total-kills">${p.total_kills || 0}</span>
<span class="stat kph">${p.kills_per_hour}</span>
@ -1110,6 +1170,10 @@ function initWebSocket() {
try { msg = JSON.parse(evt.data); } catch { return; }
if (msg.type === 'chat') {
appendChatMessage(msg);
} else if (msg.type === 'vitals') {
updateVitalsDisplay(msg);
} else if (msg.type === 'rare') {
triggerEpicRareNotification(msg.character_name, msg.name);
}
});
socket.addEventListener('close', () => setTimeout(initWebSocket, 2000));
@ -1276,3 +1340,310 @@ wrap.addEventListener('mousemove', e => {
wrap.addEventListener('mouseleave', () => {
coordinates.style.display = 'none';
});
/* ---------- vitals display functions ----------------------------- */
// Store vitals data per character
const characterVitals = {};
function updateVitalsDisplay(vitalsMsg) {
// Store the vitals data for this character
characterVitals[vitalsMsg.character_name] = {
health_percentage: vitalsMsg.health_percentage,
stamina_percentage: vitalsMsg.stamina_percentage,
mana_percentage: vitalsMsg.mana_percentage,
vitae: vitalsMsg.vitae
};
// Re-render the player list to update vitals in the UI
renderList();
}
function createVitalsHTML(characterName) {
const vitals = characterVitals[characterName];
if (!vitals) {
return ''; // No vitals data available
}
return `
<div class="player-vitals">
<div class="vital-bar-inline ${getVitalClass(vitals.health_percentage)}">
<div class="vital-fill health" style="width: ${vitals.health_percentage}%"></div>
</div>
<div class="vital-bar-inline ${getVitalClass(vitals.stamina_percentage)}">
<div class="vital-fill stamina" style="width: ${vitals.stamina_percentage}%"></div>
</div>
<div class="vital-bar-inline ${getVitalClass(vitals.mana_percentage)}">
<div class="vital-fill mana" style="width: ${vitals.mana_percentage}%"></div>
</div>
</div>
`;
}
function createVitaeIndicator(characterName) {
const vitals = characterVitals[characterName];
if (!vitals || !vitals.vitae || vitals.vitae >= 100) {
return ''; // No vitae penalty
}
return `<span class="vitae-indicator">⚰️ ${vitals.vitae}%</span>`;
}
function getVitalClass(percentage) {
if (percentage <= 25) {
return 'critical-vital';
} else if (percentage <= 50) {
return 'low-vital';
}
return '';
}
/* ---------- epic rare notification system ------------------------ */
// Track previous rare count to detect increases
let lastRareCount = 0;
let notificationQueue = [];
let isShowingNotification = false;
function triggerEpicRareNotification(characterName, rareName) {
// Add to queue
notificationQueue.push({ characterName, rareName });
// Process queue if not already showing a notification
if (!isShowingNotification) {
processNotificationQueue();
}
// Trigger fireworks immediately
createFireworks();
// Highlight the player in the list
highlightRareFinder(characterName);
}
function processNotificationQueue() {
if (notificationQueue.length === 0) {
isShowingNotification = false;
return;
}
isShowingNotification = true;
const notification = notificationQueue.shift();
// Create notification element
const container = document.getElementById('rareNotifications');
const notifEl = document.createElement('div');
notifEl.className = 'rare-notification';
notifEl.innerHTML = `
<div class="rare-notification-title">🎆 LEGENDARY RARE! 🎆</div>
<div class="rare-notification-mob">${notification.rareName}</div>
<div class="rare-notification-finder">found by</div>
<div class="rare-notification-character"> ${notification.characterName} </div>
`;
container.appendChild(notifEl);
// Remove notification after 6 seconds and process next
setTimeout(() => {
notifEl.style.animation = 'notification-slide-out 0.5s ease-in forwards';
setTimeout(() => {
notifEl.remove();
processNotificationQueue();
}, 500);
}, 6000);
}
// Add slide out animation
const style = document.createElement('style');
style.textContent = `
@keyframes notification-slide-out {
to {
transform: translateY(-100px);
opacity: 0;
}
}
`;
document.head.appendChild(style);
function createFireworks() {
const container = document.getElementById('fireworksContainer');
const rareCounter = document.getElementById('totalRaresCounter');
const rect = rareCounter.getBoundingClientRect();
// Create 30 particles
const particleCount = 30;
const colors = ['particle-gold', 'particle-red', 'particle-orange', 'particle-purple', 'particle-blue'];
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement('div');
particle.className = `firework-particle ${colors[Math.floor(Math.random() * colors.length)]}`;
// Start position at rare counter
particle.style.left = `${rect.left + rect.width / 2}px`;
particle.style.top = `${rect.top + rect.height / 2}px`;
// Random explosion direction
const angle = (Math.PI * 2 * i) / particleCount + (Math.random() - 0.5) * 0.5;
const velocity = 100 + Math.random() * 200;
const dx = Math.cos(angle) * velocity;
const dy = Math.sin(angle) * velocity - 50; // Slight upward bias
// Create custom animation for this particle
const animName = `particle-${Date.now()}-${i}`;
const keyframes = `
@keyframes ${animName} {
0% {
transform: translate(0, 0) scale(1);
opacity: 1;
}
100% {
transform: translate(${dx}px, ${dy + 200}px) scale(0);
opacity: 0;
}
}
`;
const styleEl = document.createElement('style');
styleEl.textContent = keyframes;
document.head.appendChild(styleEl);
particle.style.animation = `${animName} 2s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards`;
container.appendChild(particle);
// Clean up particle and animation after completion
setTimeout(() => {
particle.remove();
styleEl.remove();
}, 2000);
}
}
function highlightRareFinder(characterName) {
// Find the player in the list
const playerItems = document.querySelectorAll('#playerList li');
playerItems.forEach(item => {
const nameSpan = item.querySelector('.player-name');
if (nameSpan && nameSpan.textContent.includes(characterName)) {
item.classList.add('rare-finder-glow');
// Remove glow after 5 seconds
setTimeout(() => {
item.classList.remove('rare-finder-glow');
}, 5000);
}
});
}
// Update total rares display to trigger fireworks on increase
const originalUpdateTotalRaresDisplay = updateTotalRaresDisplay;
updateTotalRaresDisplay = function(data) {
originalUpdateTotalRaresDisplay(data);
// Check if total increased
const newTotal = data.all_time || 0;
if (newTotal > lastRareCount && lastRareCount > 0) {
// Don't trigger on initial load
createFireworks();
// Check for milestones when count increases
if (newTotal > 0 && newTotal % 100 === 0) {
triggerMilestoneCelebration(newTotal);
}
}
lastRareCount = newTotal;
}
function triggerMilestoneCelebration(rareNumber) {
console.log(`🏆 MILESTONE: Rare #${rareNumber}! 🏆`);
// Create full-screen milestone overlay
const overlay = document.createElement('div');
overlay.className = 'milestone-overlay';
overlay.innerHTML = `
<div class="milestone-content">
<div class="milestone-number">#${rareNumber}</div>
<div class="milestone-text">🏆 EPIC MILESTONE! 🏆</div>
<div class="milestone-subtitle">Server Achievement Unlocked</div>
</div>
`;
document.body.appendChild(overlay);
// Add screen shake effect
document.body.classList.add('screen-shake');
// Create massive firework explosion
createMilestoneFireworks();
// Remove milestone overlay after 5 seconds
setTimeout(() => {
overlay.style.animation = 'milestone-fade-in 0.5s ease-out reverse';
document.body.classList.remove('screen-shake');
setTimeout(() => {
overlay.remove();
}, 500);
}, 5000);
}
function createMilestoneFireworks() {
const container = document.getElementById('fireworksContainer');
// Create multiple bursts across the screen
const burstCount = 5;
const particlesPerBurst = 50;
const colors = ['#ffd700', '#ff6600', '#ff0044', '#cc00ff', '#00ccff', '#00ff00'];
for (let burst = 0; burst < burstCount; burst++) {
setTimeout(() => {
// Random position for each burst
const x = Math.random() * window.innerWidth;
const y = Math.random() * (window.innerHeight * 0.7) + (window.innerHeight * 0.15);
for (let i = 0; i < particlesPerBurst; i++) {
const particle = document.createElement('div');
particle.className = 'milestone-particle';
particle.style.background = colors[Math.floor(Math.random() * colors.length)];
particle.style.boxShadow = `0 0 12px ${particle.style.background}`;
// Start position
particle.style.left = `${x}px`;
particle.style.top = `${y}px`;
// Random explosion direction
const angle = (Math.PI * 2 * i) / particlesPerBurst + (Math.random() - 0.5) * 0.8;
const velocity = 200 + Math.random() * 300;
const dx = Math.cos(angle) * velocity;
const dy = Math.sin(angle) * velocity - 100; // Upward bias
// Create custom animation
const animName = `milestone-particle-${Date.now()}-${burst}-${i}`;
const keyframes = `
@keyframes ${animName} {
0% {
transform: translate(0, 0) scale(1);
opacity: 1;
}
100% {
transform: translate(${dx}px, ${dy + 400}px) scale(0);
opacity: 0;
}
}
`;
const styleEl = document.createElement('style');
styleEl.textContent = keyframes;
document.head.appendChild(styleEl);
particle.style.animation = `${animName} 3s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards`;
container.appendChild(particle);
// Clean up
setTimeout(() => {
particle.remove();
styleEl.remove();
}, 3000);
}
}, burst * 200); // Stagger bursts
}
}

View file

@ -122,6 +122,104 @@ body {
font-size: 1.25rem;
color: var(--accent);
}
.total-rares-counter {
margin: 0 0 12px 0;
padding: 8px 12px;
background: linear-gradient(135deg, #2a2a2a, #1a1a1a);
border: 1px solid #444;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 600;
color: #ffd700;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.total-rares-counter #totalRaresCount {
color: #fff;
margin-left: 4px;
}
.server-kph-counter {
margin: 0 0 12px 0;
padding: 9px 12px;
background: linear-gradient(135deg, #2a2a44, #1a1a33);
border: 2px solid #4466aa;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
color: #aaccff;
text-align: center;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4);
position: relative;
animation: kph-border-glow 4s ease-in-out infinite;
}
@keyframes kph-border-glow {
0%, 100% { border-color: #4466aa; box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4); }
50% { border-color: #6688cc; box-shadow: 0 3px 12px rgba(102, 136, 204, 0.3); }
}
.server-kph-counter #serverKphCount {
color: #fff;
margin-left: 4px;
font-size: 1.1rem;
font-weight: 700;
text-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
animation: kph-pulse 3s ease-in-out infinite;
}
@keyframes kph-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
/* ULTRA MODE for KPH > 5000 */
.server-kph-counter.ultra-epic {
background: linear-gradient(135deg, #6644ff, #4422cc, #6644ff);
background-size: 200% 200%;
animation: kph-border-glow 4s ease-in-out infinite, ultra-background 3s ease-in-out infinite;
border-color: #8866ff;
color: #eeeeff;
box-shadow: 0 4px 12px rgba(102, 68, 255, 0.5);
}
@keyframes ultra-background {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.server-kph-counter.ultra-epic #serverKphCount {
font-size: 1.3rem;
color: #ffffff;
text-shadow: 0 0 12px rgba(255, 255, 255, 0.7);
animation: kph-pulse 3s ease-in-out infinite, ultra-glow 2s ease-in-out infinite alternate;
}
@keyframes ultra-glow {
from { text-shadow: 0 0 12px rgba(255, 255, 255, 0.7); }
to { text-shadow: 0 0 18px rgba(255, 255, 255, 0.9), 0 0 25px rgba(136, 102, 255, 0.5); }
}
.total-kills-counter {
margin: 0 0 12px 0;
padding: 8px 12px;
background: linear-gradient(135deg, #2a2a2a, #1a1a1a);
border: 1px solid #555;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 600;
color: #ff6666;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.total-kills-counter #totalKillsCount {
color: #fff;
margin-left: 4px;
}
#playerList {
list-style: none;
margin: 0;
@ -243,9 +341,10 @@ body {
#playerList li {
display: grid;
grid-template-columns: 1fr auto auto auto auto auto;
grid-template-rows: auto auto auto auto;
grid-template-rows: auto auto auto auto auto;
grid-template-areas:
"name name name name name name"
"vitals vitals vitals vitals vitals vitals"
"kills totalkills kph kph kph kph"
"rares kpr meta meta meta meta"
"onlinetime deaths tapers tapers tapers tapers";
@ -254,7 +353,7 @@ body {
padding: 8px 10px;
background: var(--card);
border-left: 4px solid transparent;
transition: background 0.15s;
transition: none;
font-size: 0.85rem;
}
@ -272,6 +371,8 @@ body {
.stat.deaths { grid-area: deaths; }
.stat.tapers { grid-area: tapers; }
.player-vitals { grid-area: vitals; }
/* pill styling */
#playerList li .stat {
background: rgba(255,255,255,0.1);
@ -719,3 +820,359 @@ body.noselect, body.noselect * {
margin-top: 4px;
text-align: center;
}
/* ---------- inline vitals bars ---------------------------------- */
.player-vitals {
grid-column: 1 / -1;
margin: 2px 0 4px 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.vital-bar-inline {
height: 5px;
background: #222;
border-radius: 3px;
overflow: hidden;
position: relative;
}
.vitae-indicator {
font-size: 0.75rem;
color: #ff6666;
margin-left: 8px;
font-weight: 500;
}
.vital-fill {
height: 100%;
transition: width 0.3s ease-out;
border-radius: 2px;
}
.vital-fill.health {
background: linear-gradient(90deg, #ff4444, #ff6666);
}
.vital-fill.stamina {
background: linear-gradient(90deg, #ffaa00, #ffcc44);
}
.vital-fill.mana {
background: linear-gradient(90deg, #4488ff, #66aaff);
}
/* Pulsing effects for low vitals */
.vital-bar-inline.low-vital {
animation: pulse-bar-low 2s ease-in-out infinite;
}
.vital-bar-inline.critical-vital {
animation: pulse-bar-critical 1s ease-in-out infinite;
}
@keyframes pulse-bar-low {
0%, 100% { background: #222; }
50% { background: #332200; }
}
@keyframes pulse-bar-critical {
0%, 100% { background: #222; }
50% { background: #440000; }
}
/* ---------- epic rare notifications ------------------------------ */
.rare-notifications {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 10001;
pointer-events: none;
}
.rare-notification {
background: linear-gradient(135deg, #ffd700, #ffed4e, #ffd700);
border: 3px solid #ff6600;
border-radius: 12px;
padding: 20px 30px;
margin-bottom: 10px;
text-align: center;
box-shadow: 0 8px 32px rgba(255, 215, 0, 0.5);
animation: notification-slide-in 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55),
epic-glow 2s ease-in-out infinite;
position: relative;
overflow: hidden;
}
@keyframes notification-slide-in {
from {
transform: translateY(-100px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes epic-glow {
0%, 100% {
box-shadow: 0 8px 32px rgba(255, 215, 0, 0.5);
}
50% {
box-shadow: 0 8px 48px rgba(255, 215, 0, 0.8);
}
}
.rare-notification-title {
font-size: 1.2rem;
font-weight: 800;
color: #ff0044;
text-transform: uppercase;
margin-bottom: 8px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
animation: epic-text-pulse 1s ease-in-out infinite;
}
@keyframes epic-text-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.rare-notification-mob {
font-size: 1.5rem;
font-weight: 700;
color: #1a0033;
margin-bottom: 4px;
text-shadow: 2px 2px 4px rgba(255, 255, 255, 0.5);
}
.rare-notification-finder {
font-size: 1rem;
color: #333;
font-style: italic;
margin-bottom: 4px;
}
.rare-notification-character {
font-size: 1.3rem;
font-weight: 700;
color: #ff0044;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
/* Shine effect overlay */
.rare-notification::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg,
transparent 30%,
rgba(255, 255, 255, 0.5) 50%,
transparent 70%
);
transform: rotate(45deg);
animation: notification-shine 3s infinite;
}
@keyframes notification-shine {
0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
}
/* ---------- fireworks particles ---------------------------------- */
.fireworks-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9999;
}
.firework-particle {
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
pointer-events: none;
animation: firework-fly 2s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
}
@keyframes firework-fly {
0% {
transform: translate(0, 0) scale(1);
opacity: 1;
}
100% {
opacity: 0;
}
}
/* Different particle colors */
.particle-gold { background: #ffd700; box-shadow: 0 0 6px #ffd700; }
.particle-red { background: #ff4444; box-shadow: 0 0 6px #ff4444; }
.particle-orange { background: #ff8800; box-shadow: 0 0 6px #ff8800; }
.particle-purple { background: #cc00ff; box-shadow: 0 0 6px #cc00ff; }
.particle-blue { background: #00ccff; box-shadow: 0 0 6px #00ccff; }
/* Character glow effect in player list */
.player-item.rare-finder-glow {
animation: rare-finder-highlight 5s ease-in-out;
border-left-color: #ffd700 !important;
border-left-width: 6px !important;
}
@keyframes rare-finder-highlight {
0%, 100% {
background: var(--card);
box-shadow: none;
}
50% {
background: rgba(255, 215, 0, 0.2);
box-shadow: 0 0 20px rgba(255, 215, 0, 0.5);
}
}
/* ---------- milestone celebration overlay ------------------------ */
.milestone-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(ellipse at center, rgba(255, 215, 0, 0.3), rgba(0, 0, 0, 0.8));
z-index: 20000;
display: flex;
align-items: center;
justify-content: center;
animation: milestone-fade-in 0.5s ease-out;
}
@keyframes milestone-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.milestone-content {
text-align: center;
animation: milestone-zoom 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
@keyframes milestone-zoom {
from {
transform: scale(0);
}
to {
transform: scale(1);
}
}
.milestone-number {
font-size: 8rem;
font-weight: 900;
color: #ffd700;
text-shadow:
0 0 30px #ffd700,
0 0 60px #ff6600,
0 0 90px #ff0044,
0 0 120px #ff0044;
margin-bottom: 20px;
animation: milestone-pulse 1s ease-in-out infinite alternate;
}
@keyframes milestone-pulse {
from {
transform: scale(1);
text-shadow:
0 0 30px #ffd700,
0 0 60px #ff6600,
0 0 90px #ff0044,
0 0 120px #ff0044;
}
to {
transform: scale(1.1);
text-shadow:
0 0 40px #ffd700,
0 0 80px #ff6600,
0 0 120px #ff0044,
0 0 160px #ff0044;
}
}
.milestone-text {
font-size: 3rem;
font-weight: 700;
color: #fff;
text-transform: uppercase;
letter-spacing: 0.2em;
text-shadow: 0 0 20px rgba(255, 255, 255, 0.8);
animation: milestone-text-glow 2s ease-in-out infinite;
}
@keyframes milestone-text-glow {
0%, 100% {
opacity: 0.8;
}
50% {
opacity: 1;
}
}
.milestone-subtitle {
font-size: 1.5rem;
color: #ffcc00;
margin-top: 20px;
font-style: italic;
animation: milestone-subtitle-slide 1s ease-out;
}
@keyframes milestone-subtitle-slide {
from {
transform: translateY(50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* Milestone firework burst - larger particles */
.milestone-particle {
position: absolute;
width: 12px;
height: 12px;
border-radius: 50%;
pointer-events: none;
background: #ffd700;
box-shadow: 0 0 12px #ffd700;
}
/* Screen shake effect */
@keyframes screen-shake {
0%, 100% { transform: translate(0, 0); }
10% { transform: translate(-5px, -5px); }
20% { transform: translate(5px, -5px); }
30% { transform: translate(-5px, 5px); }
40% { transform: translate(5px, 5px); }
50% { transform: translate(-3px, -3px); }
60% { transform: translate(3px, -3px); }
70% { transform: translate(-3px, 3px); }
80% { transform: translate(3px, 3px); }
90% { transform: translate(-1px, -1px); }
}
.screen-shake {
animation: screen-shake 0.5s ease-in-out;
}