added epic counters
This commit is contained in:
parent
10c51f6825
commit
09a6cd4946
4 changed files with 963 additions and 8 deletions
373
static/script.js
373
static/script.js
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue