Initial commit: fuesse.sexy — Where's the Feet
Wimmel-Suchspiel: finde 3 versteckte Fuesse in einer randvollen Szene. - Node/Express-Server generiert Szenen prozedural aus einer gemeinsamen Library (library/props = Deko, library/feet = Fuesse); Spezial-Level (emoji) unter scenes/ - Serverseitige Trefferpruefung (nicht per DOM austrickbar) - Frontend rendert die Szene als ein Bild -> fluessiges Zoom/Pan (panzoom) - Dark Mode, kein Start-Screen, Installationsskript + Deploy-Doku (Proxmox LXC) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
101
server/index.js
Normal file
101
server/index.js
Normal file
@@ -0,0 +1,101 @@
|
||||
// fuesse.sexy — "Where's the Feet"
|
||||
// Express-Server: liefert das Frontend aus, scannt die Szenen-Ordner beim Start,
|
||||
// generiert auf Anfrage eine zufaellige Szene und prueft Klick-Treffer serverseitig.
|
||||
|
||||
import express from 'express';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
import { scanLibrary, scanScenes } from './sceneScanner.js';
|
||||
import { generateScene } from './sceneGenerator.js';
|
||||
import { getScene } from './sceneStore.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = join(__dirname, '..');
|
||||
const SCENES_DIR = join(ROOT, 'scenes');
|
||||
const LIBRARY_DIR = join(ROOT, 'library');
|
||||
const PUBLIC_DIR = join(ROOT, 'public');
|
||||
const PANZOOM_DIST = join(ROOT, 'node_modules', 'panzoom', 'dist');
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Wie oft die normale Library-Szene im Vergleich zu einem Spezial-Level drankommt.
|
||||
const LIBRARY_WEIGHT = 3;
|
||||
|
||||
// Beim Start: Library (Normalfall) + Spezial-Level (Ausnahmen) einlesen und zu einem
|
||||
// gewichteten Zufalls-Pool zusammenbauen.
|
||||
const library = scanLibrary(LIBRARY_DIR);
|
||||
const specials = scanScenes(SCENES_DIR);
|
||||
|
||||
const scenePool = [];
|
||||
if (library) {
|
||||
const libCategory = {
|
||||
id: 'library',
|
||||
name: '',
|
||||
type: 'clipart',
|
||||
isLibrary: true,
|
||||
background: null, // pro Szene zufaellig
|
||||
minSize: 68,
|
||||
maxSize: 128,
|
||||
cellFactor: 0.82,
|
||||
props: library.props,
|
||||
targets: library.feet,
|
||||
};
|
||||
for (let i = 0; i < LIBRARY_WEIGHT; i++) scenePool.push(libCategory);
|
||||
}
|
||||
for (const s of specials) scenePool.push(s);
|
||||
|
||||
if (scenePool.length === 0) {
|
||||
console.warn('[server] Keine spielbaren Szenen — fuelle library/ oder scenes/.');
|
||||
} else {
|
||||
console.log(
|
||||
`[server] bereit: Library=${library ? 'ja' : 'nein'}, Spezial-Level: ${specials.map((c) => c.id).join(', ') || '(keine)'}`
|
||||
);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Statisches Frontend + Assets
|
||||
app.use(express.static(PUBLIC_DIR));
|
||||
app.use('/scenes', express.static(SCENES_DIR));
|
||||
app.use('/library', express.static(LIBRARY_DIR));
|
||||
if (existsSync(PANZOOM_DIST)) {
|
||||
app.use('/vendor', express.static(PANZOOM_DIST));
|
||||
}
|
||||
|
||||
// Eine zufaellige, frisch gemischte Szene liefern (meist Library, selten Spezial-Level).
|
||||
app.get('/api/scene', (req, res) => {
|
||||
if (scenePool.length === 0) {
|
||||
return res.status(503).json({ error: 'Noch keine Szenen vorhanden.' });
|
||||
}
|
||||
const cat = scenePool[Math.floor(Math.random() * scenePool.length)];
|
||||
const payload = generateScene(cat);
|
||||
res.json(payload);
|
||||
});
|
||||
|
||||
// Klick pruefen: liegt (x, y) in einer der Ziel-Trefferzonen dieser Szene?
|
||||
app.post('/api/scene/:sceneId/guess', (req, res) => {
|
||||
const entry = getScene(req.params.sceneId);
|
||||
if (!entry) {
|
||||
return res.status(404).json({ error: 'Szene unbekannt oder abgelaufen.' });
|
||||
}
|
||||
const { x, y } = req.body || {};
|
||||
if (typeof x !== 'number' || typeof y !== 'number') {
|
||||
return res.status(400).json({ error: 'x und y muessen Zahlen sein.' });
|
||||
}
|
||||
|
||||
let hit = null;
|
||||
for (const t of entry.targets) {
|
||||
if (Math.hypot(t.cx - x, t.cy - y) <= t.radius) {
|
||||
hit = t.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
res.json({ hit });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`[server] fuesse.sexy laeuft auf http://localhost:${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user