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:
Guido
2026-08-04 17:29:25 +02:00
commit 1a9710ec88
42 changed files with 2555 additions and 0 deletions

88
public/js/app.js Normal file
View File

@@ -0,0 +1,88 @@
// App-Flow: beim Laden sofort eine zufaellige Szene holen (kein Start-Screen, keine
// Auswahl). Alle 3 Fuesse finden -> Gewinn-Overlay -> "Weiter" laedt die naechste.
import { Scene } from './scene.js';
import { Legend } from './legend.js';
const svg = document.getElementById('scene-svg');
const layer = document.getElementById('scene-layer');
const stage = document.getElementById('stage');
const loading = document.getElementById('loading');
const winOverlay = document.getElementById('win-overlay');
const toast = document.getElementById('toast');
const scene = new Scene(svg, layer, stage);
const legend = new Legend(
document.getElementById('legend-items'),
document.getElementById('legend-counter')
);
let current = null; // aktuelles Szenen-Payload
let busy = false;
function showLoading(on) {
loading.classList.toggle('hidden', !on);
}
function showToast(message) {
toast.textContent = message;
toast.classList.add('show');
clearTimeout(showToast._t);
showToast._t = setTimeout(() => toast.classList.remove('show'), 900);
}
async function loadScene() {
showLoading(true);
winOverlay.classList.add('hidden');
try {
const res = await fetch('/api/scene');
if (!res.ok) throw new Error('Szene konnte nicht geladen werden.');
current = await res.json();
await scene.render(current);
scene.resetView();
legend.render(current.targets);
document.getElementById('category-name').textContent = current.category || '';
} catch (err) {
console.error(err);
showToast('Ups — Szene konnte nicht geladen werden.');
} finally {
showLoading(false);
}
}
async function handleTap({ x, y }) {
if (busy || !current) return;
busy = true;
try {
const res = await fetch(`/api/scene/${current.sceneId}/guess`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ x, y }),
});
const data = await res.json();
if (data.hit) {
const isNew = legend.markFound(data.hit);
scene.pingAt(x, y, true);
if (isNew && legend.allFound()) {
setTimeout(() => winOverlay.classList.remove('hidden'), 400);
}
} else {
scene.pingAt(x, y, false);
}
} catch (err) {
console.error(err);
} finally {
busy = false;
}
}
scene.onTap(handleTap);
// Steuerung
document.getElementById('zoom-in').addEventListener('click', () => scene.zoomIn());
document.getElementById('zoom-out').addEventListener('click', () => scene.zoomOut());
document.getElementById('reset-view').addEventListener('click', () => scene.resetView());
document.getElementById('next-scene').addEventListener('click', loadScene);
// Los geht's — sofort.
loadScene();