Admin-Panel (/admin): Auth, Spieler-Tracking, Live-Tuning, Bild-Verwaltung
- Passwortgeschütztes Panel unter /admin (Auto-Passwort 20 Zeichen, per CLI abrufbar: `node server/admin.js password` / `password reset`), In-Memory-Sessions + Cookie, Login-Rate-Limit. - Spieler-Tracking: pro Score IP (CF/XFF/req.ip), Gerät/Browser (ua-parser-js), grober Ort (geoip-lite, offline) -> data/players.json; Aggregation pro Name. - Live-Tuning: configStore (data/config.json) für Timer + Schwierigkeitskurve; Server (makeRound) und Client (/api/config) lesen daraus -> Änderungen ohne Neustart. - Bild-Verwaltung: Upload/Löschen von Füßen & Deko (multer) + rescanLibrary(). - Panel-Aktionen: Spieler bannen/entbannen, Rangliste heute/alles leeren. - Fix: .item object-fit:contain -> nicht-quadratische Bilder werden nicht mehr verzerrt. - trust proxy für korrekte Client-IP hinter Reverse-Proxy/Cloudflare. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,11 +4,21 @@
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
// ---- Konstanten (leicht justierbar) ----
|
||||
const START_TIME = 15; // Sekunden zu Beginn
|
||||
const MAX_TIME = 30; // Deckel (Timerleiste = timeLeft / MAX_TIME)
|
||||
const BONUS = 1.2; // Zeit pro Treffer
|
||||
const PENALTY = 2.0; // Zeitabzug pro Fehlklick
|
||||
// ---- Konfiguration: kommt LIVE vom Server (/api/config); hier nur Fallback-Defaults ----
|
||||
const DEFAULT_CFG = {
|
||||
startTime: 15, maxTime: 30, bonus: 1.2, penalty: 2.0,
|
||||
countBase: 10, countPerScore: 1.5, stepStart: 25, stepEvery: 3, stepAdd: 2, countCap: 100,
|
||||
stageScatter: 3, stageGroups: 6, stageIndividual: 11,
|
||||
speedGroupsBase: 45, speedGroupsPer: 10, speedGroupsMax: 150,
|
||||
speedIndivBase: 70, speedIndivPer: 12, speedIndivMax: 320,
|
||||
};
|
||||
let CFG = { ...DEFAULT_CFG };
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const r = await fetch('/api/config');
|
||||
if (r.ok) CFG = { ...DEFAULT_CFG, ...(await r.json()) };
|
||||
} catch {}
|
||||
}
|
||||
const NAME_KEY = 'fuesse_name';
|
||||
|
||||
// ---- Zustand ----
|
||||
@@ -32,28 +42,24 @@ function shuffle(a) {
|
||||
return a;
|
||||
}
|
||||
|
||||
// Objekt-Anzahl: erst linear, ab STEP_START zusaetzlich SCHRITTWEISE mehr (mit Deckel).
|
||||
// Alle Werte hier leicht justierbar.
|
||||
const COUNT_BASE = 10; // Startanzahl
|
||||
const COUNT_PER_SCORE = 1.5; // frühes lineares Wachstum
|
||||
const STEP_START = 25; // ab diesem Score kommen Stufen dazu
|
||||
const STEP_EVERY = 3; // alle N Punkte eine Stufe
|
||||
const STEP_ADD = 2; // +M Objekte pro Stufe
|
||||
const COUNT_CAP = 100; // harte Obergrenze (Performance/Spielbarkeit)
|
||||
|
||||
// Objekt-Anzahl aus der Config (identisch zur Server-Formel)
|
||||
function objectCount(score) {
|
||||
let count = COUNT_BASE + Math.floor(score * COUNT_PER_SCORE);
|
||||
if (score >= STEP_START) count += Math.floor((score - STEP_START) / STEP_EVERY) * STEP_ADD;
|
||||
return Math.min(count, COUNT_CAP);
|
||||
let count = CFG.countBase + Math.floor(score * CFG.countPerScore);
|
||||
if (score >= CFG.stepStart) count += Math.floor((score - CFG.stepStart) / CFG.stepEvery) * CFG.stepAdd;
|
||||
return Math.min(count, CFG.countCap);
|
||||
}
|
||||
|
||||
// Schwierigkeit aus dem Score ableiten
|
||||
// Schwierigkeit aus dem Score ableiten (Grenzen/Tempo aus der Config)
|
||||
function difficulty(score) {
|
||||
const count = objectCount(score);
|
||||
if (score < 3) return { count, stage: 'grid', groups: 0, speed: 0 };
|
||||
if (score < 6) return { count, stage: 'scatter', groups: 0, speed: 0 };
|
||||
if (score < 11) return { count, stage: 'groups', groups: score < 9 ? 2 : 3, speed: Math.min(45 + (score - 6) * 10, 150) };
|
||||
return { count, stage: 'individual', groups: 0, speed: Math.min(70 + (score - 11) * 12, 320) };
|
||||
if (score < CFG.stageScatter) return { count, stage: 'grid', groups: 0, speed: 0 };
|
||||
if (score < CFG.stageGroups) return { count, stage: 'scatter', groups: 0, speed: 0 };
|
||||
if (score < CFG.stageIndividual) {
|
||||
const speed = Math.min(CFG.speedGroupsBase + (score - CFG.stageGroups) * CFG.speedGroupsPer, CFG.speedGroupsMax);
|
||||
return { count, stage: 'groups', groups: score < CFG.stageGroups + 3 ? 2 : 3, speed };
|
||||
}
|
||||
const speed = Math.min(CFG.speedIndivBase + (score - CFG.stageIndividual) * CFG.speedIndivPer, CFG.speedIndivMax);
|
||||
return { count, stage: 'individual', groups: 0, speed };
|
||||
}
|
||||
|
||||
// ---- Rangliste (zwei Boards: Allzeit-Top-5 + Heute-Top-10) ----
|
||||
@@ -105,7 +111,8 @@ async function startGame() {
|
||||
$('gameover').classList.add('hidden');
|
||||
$('game').classList.remove('hidden');
|
||||
|
||||
state = { score: 0, timeLeft: START_TIME, items: [], groups: [], stage: 'grid', running: true, sessionId: null, nonce: null, awaiting: false };
|
||||
await loadConfig(); // aktuelle Werte aus dem Admin-Panel holen
|
||||
state = { score: 0, timeLeft: CFG.startTime, items: [], groups: [], stage: 'grid', running: true, sessionId: null, nonce: null, awaiting: false };
|
||||
$('score').textContent = '0';
|
||||
|
||||
const start = await startSession();
|
||||
@@ -342,7 +349,7 @@ function loop(now) {
|
||||
}
|
||||
|
||||
function updateTimerBar() {
|
||||
const pct = clamp(state.timeLeft / MAX_TIME, 0, 1) * 100;
|
||||
const pct = clamp(state.timeLeft / CFG.maxTime, 0, 1) * 100;
|
||||
const bar = $('timer-bar');
|
||||
bar.style.width = pct + '%';
|
||||
bar.classList.toggle('low', state.timeLeft <= 6);
|
||||
@@ -393,7 +400,7 @@ async function hitTarget(target) {
|
||||
});
|
||||
const data = res.ok ? await res.json() : null;
|
||||
if (data && data.correct && data.round) {
|
||||
state.timeLeft = Math.min(state.timeLeft + BONUS, MAX_TIME);
|
||||
state.timeLeft = Math.min(state.timeLeft + CFG.bonus, CFG.maxTime);
|
||||
$('score').parentElement.classList.remove('bump');
|
||||
void $('score').parentElement.offsetWidth;
|
||||
$('score').parentElement.classList.add('bump');
|
||||
@@ -408,7 +415,7 @@ async function hitTarget(target) {
|
||||
}
|
||||
|
||||
function missDecoy(el) {
|
||||
state.timeLeft = Math.max(state.timeLeft - PENALTY, 0);
|
||||
state.timeLeft = Math.max(state.timeLeft - CFG.penalty, 0);
|
||||
if (el) {
|
||||
el.classList.remove('shake');
|
||||
void el.offsetWidth;
|
||||
|
||||
Reference in New Issue
Block a user