/* global React, ReactDOM */ const { useState, useEffect, useMemo, useRef } = React; // ---------------- Shared bits ---------------- // `np` is mutated when fresh data arrives from data.js. Components close over // this binding and re-render via App's setState when the data updates. let np = window.NP_DATA; function fmt(n) { if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M'; if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K'; return String(n); } function CountryFlag({ code }) { return {code}; } function WeaponGlyph({ id, size = 28 }) { // Stylized SVG silhouettes - rust-flavored, monochrome const s = size; const stroke = '#bce505'; const paths = { // Rifles. Each has a distinctive silhouette: // AK = angled banana mag dropping down + wood stock + gas tube on top // LR-300 = AR-style straight mag + telescoping stock + picatinny rail // Bolt = bolt-action with prominent scope and short straight mag // M39 = AK-like body with long scope rail // L96 = long sniper with full-length scope // Semi = stubby semi-auto rifle, no mag protrusion ak47: , lr300: , bolt: , semi: , l96: , m39: , // SMG mp5: , thompson: , customsmg: , // LMG m249: , hmlmg: , // Pistols python: , revolver: , m92: , semipistol: , eoka: , nailgun: , // Shotguns spas12: , pumpshotgun: , doublebarrel: , waterpipe: , // Bows / xbow / speargun bow: , crossbow: , speargun: , spear: , // Knives / swords / clubs knife: , sword: , mace: , // Tools jackhammer: , chainsaw: , hatchet: , pickaxe: , hammer: , rock: , torch: , // Launchers / explosives rocketlauncher: , grenadelauncher: , snowballgun: , grenade: , beancan: , smoke: , c4: , satchel: , rocket: , // Default custom: , }; return ( {paths[id] || paths.custom} ); } function Sparkline({ data, color = '#bce505', width = 80, height = 22 }) { const max = Math.max(...data, 1); const pts = data.map((v, i) => `${(i / (data.length - 1)) * width},${height - (v / max) * height}`).join(' '); return ( ); } // ---------------- Top bar ---------------- function TopBar({ route, setRoute }) { return (
{ e.preventDefault(); setRoute({ name: 'leaderboard' }); }}> NP Noobs Paradise // LEADERBOARD
{(np.meta && np.meta.online) || 0} / {(np.meta && np.meta.max_players) || 100} ONLINE
); } // ---------------- LEADERBOARD ---------------- function Podium({ top3, onPick, killsKey = 'kills' }) { // Order: 2nd left, 1st center, 3rd right const slots = [top3[1], top3[0], top3[2]]; const heights = [180, 240, 150]; const ranks = [2, 1, 3]; return (
{slots.map((p, i) => p ? ( ) :
)}
); } // Compute the scoped kills value for a player given the active scope. // 'wipe' => current totals. '24h' => last day of sparkline. // '7d' => sum of last 7 days. '24h'/'7d' values are bounded by the sparkline, // so they reflect activity since the plugin started — partial during the // first 14 days post-wipe. 'all' uses lifetime fields via projectAllTime. function scopedKills(p, scope) { const sp = p.sparkline || []; if (scope === '24h') return sp.length ? sp[sp.length - 1] : 0; if (scope === '7d') return sp.slice(-7).reduce((s, v) => s + v, 0); return p.kills; } // Project lifetime fields onto the player so downstream sort/display reads // them naturally when scope='all'. Falls back to current-wipe values for // any field the backend hasn't started emitting yet. function projectAllTime(p) { const lt = p.lifetime; if (!lt) return p; return { ...p, kills: lt.kills ?? p.kills, deaths: lt.deaths ?? p.deaths, kd: lt.kd ?? p.kd, headshotPct: lt.headshotPct ?? p.headshotPct, damageDealt: lt.damageDealt ?? p.damageDealt, playtime: lt.playtime ?? p.playtime, resources: lt.resources ?? p.resources, structuresBuilt: lt.structuresBuilt ?? p.structuresBuilt, basesRaided: lt.basesRaided ?? p.basesRaided, longestKill: lt.longestKill ?? p.longestKill, streak: lt.bestStreak ?? p.streak }; } function Leaderboard({ setRoute }) { const [scope, setScope] = useState('wipe'); const [sort, setSort] = useState('kills'); const [query, setQuery] = useState(''); const [visible, setVisible] = useState(25); const scoped = useMemo(() => { return np.players.map(p => { const proj = scope === 'all' ? projectAllTime(p) : p; return { ...proj, _scopedKills: scopedKills(proj, scope) }; }); }, [scope]); const filtered = useMemo(() => { let list = scoped.slice(); if (query) list = list.filter(p => p.name.toLowerCase().includes(query.toLowerCase()) || (p.clan && p.clan.toLowerCase().includes(query.toLowerCase())) ); if (sort === 'kills') { list.sort((a, b) => b._scopedKills - a._scopedKills); } else { list.sort((a, b) => b[sort] - a[sort]); } return list; }, [scoped, sort, query]); const top3 = filtered.slice(0, 3); const rest = filtered.slice(3, visible); const showScopedKills = scope === '24h' || scope === '7d'; return (
{(() => { if (scope === 'all') return 'ALL TIME // ACROSS WIPES'; if (scope === '24h') return 'LAST 24 HOURS'; if (scope === '7d') return 'LAST 7 DAYS'; const ws = np.meta && np.meta.wipe_started; if (!ws) return 'CURRENT WIPE'; const days = Math.max(1, Math.floor((Date.now() - ws * 1000) / 86400000)); return `CURRENT WIPE // DAY ${days}`; })()}

Top of the Trash Heap

Live leaderboard for the Noobs Paradise 3X TRIO server. Click any operator to drill into their kill feed, weapons, and play patterns.

TOTAL KILLS{fmt(np.players.reduce((s, p) => s + p.kills, 0))}
RESOURCES{fmt(np.players.reduce((s, p) => s + p.resources, 0))}
ACTIVE{np.players.filter(p => p.online).length}
SCOPE {[['wipe', 'Current Wipe'], ['24h', 'Last 24h'], ['7d', 'Last 7 Days'], ['all', 'All Time']].map(([k, l]) => ( ))}
SORT {[['kills', 'Kills'], ['resources', 'Resources'], ['kd', 'K/D'], ['playtime', 'Playtime'], ['damageDealt', 'Damage']].map(([k, l]) => ( ))}
$ setQuery(e.target.value)} />
setRoute({ name: 'player', id: p.id })} killsKey={showScopedKills ? '_scopedKills' : 'kills'} />
RANK OPERATOR K / D KILLS RESOURCES PLAYTIME HS% STREAK FAV STATUS
{rest.map((p, idx) => ( ))}
{visible < filtered.length && ( )}
); } // ---------------- PLAYER OVERVIEW ---------------- function Heatmap({ data }) { // Polar/radial 24x7 — 7 concentric rings (days), 24 wedges (hours) const size = 360; const cx = size / 2, cy = size / 2; const innerR = 50; const outerR = 168; const ringW = (outerR - innerR) / 7; const days = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']; const wedge = (d, h) => { const r0 = innerR + d * ringW; const r1 = r0 + ringW - 1.4; const a0 = (h / 24) * Math.PI * 2 - Math.PI / 2; const a1 = ((h + 1) / 24) * Math.PI * 2 - Math.PI / 2; const x0 = cx + r0 * Math.cos(a0), y0 = cy + r0 * Math.sin(a0); const x1 = cx + r1 * Math.cos(a0), y1 = cy + r1 * Math.sin(a0); const x2 = cx + r1 * Math.cos(a1), y2 = cy + r1 * Math.sin(a1); const x3 = cx + r0 * Math.cos(a1), y3 = cy + r0 * Math.sin(a1); return `M${x0},${y0} L${x1},${y1} A${r1},${r1} 0 0 1 ${x2},${y2} L${x3},${y3} A${r0},${r0} 0 0 0 ${x0},${y0} Z`; }; const [hover, setHover] = useState(null); const peak = useMemo(() => { let best = { v: 0 }; data.forEach((row, d) => row.forEach((v, h) => { if (v > best.v) best = { v, d, h }; })); return best; }, [data]); return (
{/* Hour ticks */} {Array.from({ length: 24 }, (_, h) => { const a = (h / 24) * Math.PI * 2 - Math.PI / 2; const x1 = cx + (outerR + 6) * Math.cos(a); const y1 = cy + (outerR + 6) * Math.sin(a); const x2 = cx + (outerR + 14) * Math.cos(a); const y2 = cy + (outerR + 14) * Math.sin(a); const labelX = cx + (outerR + 24) * Math.cos(a); const labelY = cy + (outerR + 24) * Math.sin(a); return ( {h % 3 === 0 && ( {String(h).padStart(2, '0')} )} ); })} {/* Wedges */} {data.map((row, d) => row.map((v, h) => { const opacity = Math.max(0.04, v / 100); const isHover = hover && hover.d === d && hover.h === h; const isPeak = peak.d === d && peak.h === h; return ( 0 ? '#bce505' : '#161a1e'} fillOpacity={opacity} stroke={isHover ? '#bce505' : (isPeak ? '#f4b74a' : '#0c0e11')} strokeWidth={isHover || isPeak ? 1.5 : 0.4} onMouseEnter={() => setHover({ d, h, v })} onMouseLeave={() => setHover(null)} /> ); }))} {/* Day labels along top wedge of each ring */} {days.map((d, i) => { const r = innerR + i * ringW + ringW / 2; return ( {d} ); })} {/* Center hub */} PEAK {days[peak.d]} {String(peak.h).padStart(2, '0')}:00 {peak.v}% ACTIVE
LESS
{[0.1, 0.3, 0.5, 0.75, 1].map((o, i) => ( ))}
MORE {hover && ( {days[hover.d]} {String(hover.h).padStart(2, '0')}:00 — {hover.v}% active )}
); } function fmtAgo(sec) { if (sec < 5) return 'NOW'; if (sec < 60) return sec + 's'; if (sec < 3600) return Math.floor(sec / 60) + 'm'; if (sec < 86400) return Math.floor(sec / 3600) + 'h'; return Math.floor(sec / 86400) + 'd'; } // `entries` is an array of killfeed entries from np.killfeed. // Optional `viewerSteamId` filters to only kills involving that player and // renders the verb relative to them ("killed" vs "killed by"). function KillFeed({ entries, viewerSteamId, title }) { const [paused, setPaused] = useState(false); const [tick, setTick] = useState(0); useEffect(() => { if (paused) return; const id = setInterval(() => setTick(t => t + 1), 1000); return () => clearInterval(id); }, [paused]); const list = useMemo(() => { let xs = (entries || []).slice(); if (viewerSteamId) { xs = xs.filter(e => e.killerId === viewerSteamId || e.victimId === viewerSteamId); } return xs.slice(0, 14); }, [entries, viewerSteamId]); const nowSec = Math.floor(Date.now() / 1000); return (

{title || 'LIVE KILL FEED'}

{list.length === 0 ? (

{viewerSteamId ? 'No kills tracked for this player yet.' : 'No kills recorded yet.'}

) : ( )}
); } function SessionsChart({ data }) { const sessions = data || []; const maxMin = Math.max(...sessions.map(s => s.minutes || 0), 1); const totalMin = sessions.reduce((a, b) => a + (b.minutes || 0), 0); const totalKills = sessions.reduce((a, b) => a + (b.kills || 0), 0); const totalDeaths = sessions.reduce((a, b) => a + (b.deaths || 0), 0); return (
14-DAY MIN{totalMin}
14-DAY KILLS{totalKills}
14-DAY DEATHS{totalDeaths}
{sessions.map((s, i) => { const h = Math.max(2, Math.round((s.minutes / maxMin) * 64)); const k = s.kills || 0; const d = s.deaths || 0; const isToday = i === sessions.length - 1; const dayLabel = i === sessions.length - 1 ? 'TODAY' : (sessions.length - 1 - i) + 'd'; return (
{k || ''} {d || ''}
{dayLabel}
); })}
); } function WeaponBreakdown({ weapons }) { const max = Math.max(...weapons.map(w => w.kills)); return (
    {weapons.map(w => (
  • {w.name} {w.kills} kills
    ACC {w.accuracy}% HS {w.headshotPct}% DMG {fmt(w.dmg)}
  • ))}
); } function ResourceTotals({ resources }) { const items = [ { k: 'stone', label: 'Stone', color: '#9da69e' }, { k: 'wood', label: 'Wood', color: '#a07a4a' }, { k: 'metal', label: 'Metal Frags', color: '#7c8a92' }, { k: 'sulfur', label: 'Sulfur', color: '#f4b74a' }, { k: 'hqm', label: 'HQ Metal', color: '#e8eef0' }, { k: 'cloth', label: 'Cloth', color: '#d6c5a8' }, { k: 'scrap', label: 'Scrap', color: '#bce505' }, { k: 'lowGrade', label: 'Low Grade', color: '#39c8b7' }, ]; const total = Object.values(resources).reduce((s, v) => s + v, 0); return (
TOTAL GATHERED {fmt(total)} UNITS
{items.map(i => (
{i.label} {fmt(resources[i.k])}
))}
); } function AchievementWall({ achievements }) { const earnedCount = achievements.filter(a => a.earned).length; return (
UNLOCKED {earnedCount} / {achievements.length}
{achievements.map(a => (
{a.earned ? : ?}
{a.name}
{a.desc}
{a.rarity.toUpperCase()}
))}
); } function PlayerOverview({ playerId, setRoute }) { const player = np.players.find(p => p.id === playerId) || np.players[0]; if (!player) { return (

Player not found.

); } const weapons = player.weapons || []; const resources = player.resourceBreakdown || { stone: 0, wood: 0, metal: 0, sulfur: 0, hqm: 0, cloth: 0, scrap: 0, lowGrade: 0 }; const achievements = player.achievements || []; const heatmap = player.heatmap || Array.from({ length: 7 }, () => new Array(24).fill(0)); const lastSeenStr = player.lastSeen || (player.online ? 'ONLINE NOW' : '—'); const totalWeaponKills = weapons.reduce((s, w) => s + (w.kills || 0), 0); return (
{player.name.slice(0, 2).toUpperCase()}
RANK #{player.rank} · {player.clan ? `[${player.clan}]` : 'NO CLAN'}{player.country ? ` · ${player.country}` : ''}

{player.name}

{player.bio &&

{player.bio}

}
STEAM64{player.steamId} JOINED{player.joined} LAST SEEN{lastSeenStr}
KILLS{fmt(player.kills)}
DEATHS{fmt(player.deaths)}
K/D{player.kd}
PLAYTIME{player.playtime}h
HEADSHOT{player.headshotPct}%
LONGEST{player.longestKill}m
RAIDS{player.basesRaided}
STREAK= 5 ? 'live' : ''}>🔥 {player.streak}

ACTIVE PLAYTIME RADAR

24×7 RADIAL · UTC

WEAPON BREAKDOWN

{totalWeaponKills} KILLS RECORDED
{weapons.length > 0 ? :

No weapon kills tracked yet.

}

RESOURCES GATHERED

CURRENT WIPE

BADGE WALL

SERVER ACHIEVEMENTS

SESSION HISTORY

14-DAY · MINUTES PLAYED
); } // ---------------- App shell ---------------- function LoadingScreen({ message, error }) { return (

{error ? 'CONNECTION FAULT' : 'LINKING TO SERVER'}

{error || (message || 'Polling stats endpoint…')}

); } function EmptyServer({ setRoute, route }) { return (

SERVER QUIET

No tracked players yet. Stats will appear here once players start fighting.

); } function App() { const [, forceTick] = useState(0); const [ready, setReady] = useState(window.NP_DATA_READY === true && !!window.NP_DATA); const [error, setError] = useState(window.NP_DATA_ERROR); const [route, setRoute] = useState({ name: 'leaderboard' }); useEffect(() => { function onReady(e) { np = e.detail || window.NP_DATA; setError(null); setReady(true); forceTick(t => t + 1); } function onError(e) { setError(e.detail || 'Failed to load stats'); } if (window.NP_DATA_READY && window.NP_DATA) { np = window.NP_DATA; setReady(true); } window.addEventListener('np-data-ready', onReady); window.addEventListener('np-data-error', onError); return () => { window.removeEventListener('np-data-ready', onReady); window.removeEventListener('np-data-error', onError); }; }, []); if (!ready || !np) return ; const playerCount = (np.players && np.players.length) || 0; if (playerCount === 0) return ; const meta = np.meta || {}; const wipeStarted = meta.wipe_started ? new Date(meta.wipe_started * 1000) : null; const wipeDays = wipeStarted ? Math.max(1, Math.floor((Date.now() - wipeStarted.getTime()) / 86400000)) : null; return (
{route.name === 'leaderboard' ? : }
NP_LEADERBOARD{wipeDays ? ` · DAY ${wipeDays} OF WIPE` : ''} · POLLED EVERY 30s {meta.server_name || 'Noobs Paradise Network'} // 147.135.7.27:28015
); } ReactDOM.createRoot(document.getElementById('root')).render();