// Baut aus einer Kategorie ein frisch gemischtes Szenen-Layout: Deko wird zufaellig // gestreut, dann werden 3 Ziele darueber versteckt. Weil wir hier platzieren, kennen // wir die Ziel-Trefferzonen exakt -> die Klickpruefung kann serverseitig laufen. import { saveScene } from './sceneStore.js'; const CANVAS_WIDTH = 2000; const CANVAS_HEIGHT = 1400; const TARGET_COUNT = 3; const EDGE_MARGIN = 60; const MIN_TARGET_DISTANCE = 220; // Ziele nicht zu dicht beieinander // Zufaellige, angenehme Hintergrundfarben fuer normale (nicht-thematische) Szenen. const PALETTE = [ '#fbe7b6', '#d7ecff', '#ffe0ec', '#e5f7e0', '#efe6ff', '#fff2cc', '#dfeff2', '#f4e2d0', ]; function rand(min, max) { return min + Math.random() * (max - min); } function randInt(min, max) { return Math.floor(rand(min, max + 1)); } function pick(array) { return array[randInt(0, array.length - 1)]; } function makeSceneId() { return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; } // 🦶 -> "1f9b6" ; entfernt die Variation-Selector-Bytes (FE0F), joined mit "-". // Damit passt es zu gaengigen Emoji-Bildsets (Datei je Codepoint-Sequenz). function toCodepoint(glyph) { return [...glyph] .map((ch) => ch.codePointAt(0)) .filter((cp) => cp !== 0xfe0f) .map((cp) => cp.toString(16)) .join('-'); } function randomPosition() { return { cx: rand(EDGE_MARGIN, CANVAS_WIDTH - EDGE_MARGIN), cy: rand(EDGE_MARGIN, CANVAS_HEIGHT - EDGE_MARGIN), }; } // Jittered Grid: fuellt die ganze Flaeche lueckenlos, wirkt durch Zufalls-Versatz, // -Groesse und -Drehung aber organisch/chaotisch (echtes "Wimmel"-Gefuehl). // cellFactor < 1 => Zellen kleiner als die Items => sie ueberlappen => randvoll. function gridPositions(cellFactor, avgSize) { const cell = Math.max(16, avgSize * cellFactor); const cols = Math.ceil(CANVAS_WIDTH / cell); const rows = Math.ceil(CANVAS_HEIGHT / cell); const jitter = cell * 0.45; const out = []; for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { out.push({ cx: c * cell + cell / 2 + rand(-jitter, jitter), cy: r * cell + cell / 2 + rand(-jitter, jitter), }); } } return out; } // Reihenfolge mischen, damit gleiche Emojis/Props nicht sichtbar "in Reihe" liegen. function shuffle(arr) { for (let i = arr.length - 1; i > 0; i--) { const j = randInt(0, i); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; } // Platz fuer ein Ziel finden, das genug Abstand zu bereits gesetzten Zielen haelt. function findTargetPosition(placedTargets) { for (let attempt = 0; attempt < 40; attempt++) { const pos = randomPosition(); const clear = placedTargets.every((t) => { const dx = t.cx - pos.cx; const dy = t.cy - pos.cy; return Math.hypot(dx, dy) >= MIN_TARGET_DISTANCE; }); if (clear) return pos; } return randomPosition(); // Notfall: nimm irgendeine Position } function buildImageItem(cat, pos) { return { kind: 'image', assetUrl: pick(cat.props), cx: pos.cx, cy: pos.cy, size: rand(cat.minSize, cat.maxSize), rot: rand(-14, 14), // aufrecht mit leichter Neigung -> wirkt platziert, nicht hingekippt }; } function buildEmojiItem(cat, pos) { const glyph = pick(cat.propGlyphs); return { kind: 'emoji', glyph, codepoint: toCodepoint(glyph), cx: pos.cx, cy: pos.cy, size: rand(cat.minSize, cat.maxSize), rot: rand(-18, 18), }; } // Erzeugt eine Szene fuer die gegebene Kategorie und liefert: // { payload } -> geht ans Frontend (ohne Kennzeichnung, welche Items Ziele sind) // und speichert die Trefferzonen serverseitig unter der sceneId. export function generateScene(cat) { const sceneId = makeSceneId(); const isEmoji = cat.type === 'emoji'; // 1) Deko lueckenlos ueber ein jittered Grid streuen -> randvolles Wimmelbild const items = []; const avgSize = (cat.minSize + cat.maxSize) / 2; const cellFactor = cat.cellFactor || (isEmoji ? 0.8 : 0.82); const positions = shuffle(gridPositions(cellFactor, avgSize)); for (const pos of positions) { items.push(isEmoji ? buildEmojiItem(cat, pos) : buildImageItem(cat, pos)); } // 2) Ziele auswaehlen const chosenTargets = []; if (isEmoji) { const glyphPool = shuffle([...cat.targetGlyphs]); for (let i = 0; i < TARGET_COUNT; i++) { const glyph = glyphPool.length > 0 ? glyphPool.pop() : pick(cat.targetGlyphs); chosenTargets.push({ glyph, codepoint: toCodepoint(glyph), name: 'Fuss-Emoji' }); } } else { // 3 moeglichst unterschiedliche Ziel-Bilder (Duplikate nur, wenn zu wenige da sind) const pool = shuffle([...cat.targets]); for (let i = 0; i < TARGET_COUNT; i++) { const picked = pool.length > 0 ? pool.pop() : pick(cat.targets); chosenTargets.push({ url: picked.url, name: picked.name }); } } // 2b) Ablenker-Fuesse streuen: die NICHT gewaehlten Fuesse (nur normale/Clipart-Level). // So muss man die 3 richtigen aus der Legende finden, nicht irgendeinen Fuss. if (!isEmoji && Array.isArray(cat.targets) && cat.targets.length > TARGET_COUNT) { const chosenUrls = new Set(chosenTargets.map((t) => t.url)); const decoyPool = cat.targets.filter((t) => !chosenUrls.has(t.url)); if (decoyPool.length > 0) { const decoyCount = Math.min(12, decoyPool.length * 2 + 2); for (let i = 0; i < decoyCount; i++) { const d = pick(decoyPool); const pos = randomPosition(); items.push({ kind: 'image', assetUrl: d.url, cx: pos.cx, cy: pos.cy, size: rand((cat.minSize + cat.maxSize) / 2, cat.maxSize), rot: rand(-16, 16), }); } } } // 3) Ziele platzieren (kommen ans Ende von items -> werden oben gerendert, also sichtbar) const placedTargets = []; const legend = []; chosenTargets.forEach((t, i) => { const pos = findTargetPosition(placedTargets); const size = isEmoji ? rand(cat.minSize, cat.maxSize) : rand((cat.minSize + cat.maxSize) / 2, cat.maxSize); const id = `t${i}`; placedTargets.push({ id, cx: pos.cx, cy: pos.cy, radius: size * 0.55 }); const item = isEmoji ? { kind: 'emoji', glyph: t.glyph, codepoint: t.codepoint, cx: pos.cx, cy: pos.cy, size, rot: rand(-12, 12) } : { kind: 'image', assetUrl: t.url, cx: pos.cx, cy: pos.cy, size, rot: rand(-14, 14) }; items.push(item); legend.push( isEmoji ? { id, name: t.name, glyph: t.glyph, codepoint: t.codepoint } : { id, name: t.name, assetUrl: t.url } ); }); // Trefferzonen nur serverseitig ablegen saveScene(sceneId, placedTargets); const payload = { sceneId, type: cat.type, category: cat.name, width: CANVAS_WIDTH, height: CANVAS_HEIGHT, background: cat.background || pick(PALETTE), targets: legend, items, }; return payload; }