- 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>
98 lines
2.8 KiB
JavaScript
98 lines
2.8 KiB
JavaScript
// Spiel-Parameter, live per Admin-Panel einstellbar, persistent in data/config.json.
|
|
// Wird pro Request frisch gelesen -> Aenderungen wirken SOFORT (Server + Client).
|
|
|
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const DATA_DIR = join(__dirname, '..', 'data');
|
|
const CONFIG_FILE = join(DATA_DIR, 'config.json');
|
|
|
|
// Defaults = die bisherigen Konstanten aus public/js/app.js + server/index.js
|
|
export const DEFAULT_CONFIG = {
|
|
// Timer
|
|
startTime: 15,
|
|
maxTime: 30,
|
|
bonus: 1.2,
|
|
penalty: 2.0,
|
|
// Objekt-Anzahl-Kurve
|
|
countBase: 10,
|
|
countPerScore: 1.5,
|
|
stepStart: 25,
|
|
stepEvery: 3,
|
|
stepAdd: 2,
|
|
countCap: 100,
|
|
// Schwierigkeits-Stufen (Score-Grenzen) + Tempo
|
|
stageScatter: 3, // ab hier "scatter"
|
|
stageGroups: 6, // ab hier "groups"
|
|
stageIndividual: 11, // ab hier "individual"
|
|
speedGroupsBase: 45,
|
|
speedGroupsPer: 10,
|
|
speedGroupsMax: 150,
|
|
speedIndivBase: 70,
|
|
speedIndivPer: 12,
|
|
speedIndivMax: 320,
|
|
};
|
|
|
|
// Nur bekannte Keys uebernehmen, Werte auf sinnvolle Zahlen begrenzen
|
|
function sanitize(input) {
|
|
const out = { ...DEFAULT_CONFIG };
|
|
if (input && typeof input === 'object') {
|
|
for (const key of Object.keys(DEFAULT_CONFIG)) {
|
|
const v = Number(input[key]);
|
|
if (Number.isFinite(v)) out[key] = clampField(key, v);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function clampField(key, v) {
|
|
const bounds = {
|
|
startTime: [3, 120],
|
|
maxTime: [5, 300],
|
|
bonus: [0, 30],
|
|
penalty: [0, 30],
|
|
countBase: [1, 200],
|
|
countPerScore: [0, 20],
|
|
stepStart: [0, 500],
|
|
stepEvery: [1, 100],
|
|
stepAdd: [0, 50],
|
|
countCap: [5, 400],
|
|
stageScatter: [0, 1000],
|
|
stageGroups: [0, 1000],
|
|
stageIndividual: [0, 1000],
|
|
speedGroupsBase: [0, 2000],
|
|
speedGroupsPer: [0, 500],
|
|
speedGroupsMax: [0, 5000],
|
|
speedIndivBase: [0, 2000],
|
|
speedIndivPer: [0, 500],
|
|
speedIndivMax: [0, 5000],
|
|
};
|
|
const [min, max] = bounds[key] || [-1e9, 1e9];
|
|
return Math.max(min, Math.min(max, v));
|
|
}
|
|
|
|
export function getConfig() {
|
|
try {
|
|
if (!existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
return sanitize(JSON.parse(readFileSync(CONFIG_FILE, 'utf8')));
|
|
} catch {
|
|
return { ...DEFAULT_CONFIG };
|
|
}
|
|
}
|
|
|
|
export function setConfig(input) {
|
|
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
|
const next = sanitize(input);
|
|
writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2));
|
|
return next;
|
|
}
|
|
|
|
// Objekt-Anzahl aus Config + Score (identisch fuer Server & Client)
|
|
export function objectCount(cfg, score) {
|
|
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);
|
|
}
|