$analysis['game_title'] ?? 'MicroGame', 'concept' => $analysis['game_concept'] ?? '', 'mechanic' => $analysis['game_mechanic'] ?? '', 'genre' => $gameGenre, 'entities' => $analysis['game_entities'] ?? ['target'], 'powerups' => $analysis['game_powerups'] ?? [], 'win' => $analysis['game_win'] ?? 'Reach target score', 'lose' => $analysis['game_lose'] ?? 'Miss too many', 'scoring' => $analysis['game_scoring'] ?? 'Score increases on hit', 'difficulty' => $analysis['game_difficulty'] ?? 'Speed increases over time', 'inspiration' => $analysis['inspiration'] ?? '', 'understanding' => $analysis['understanding'] ?? '', 'colors' => [ 'bg' => $bgColor, 'primary' => $primaryColor, 'accent' => $accentColor, 'text' => $textColor ] ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); // ─── HTML HEAD + BODY (PHP heredoc with interpolation) ─── $html = << $gameTitle
SCORE: 0
TIME: 0s
LIVES: 3

$gameTitle

$gameConcept

$gameMechanic

Win: $gameWin · Lose: $gameLose

Inspired by video evidence · $gameGenre · source video exposed: false
HTML; // ─── GAME ENGINE JS (PHP nowdoc — NO interpolation) ─── $js = <<<'JSEOF' (function(){ const cfg = JSON.parse(document.getElementById('gameConfig').textContent); const canvas = document.getElementById('game'); const ctx = canvas.getContext('2d'); const overlay = document.getElementById('overlay'); const scoreEl = document.getElementById('score'); const livesEl = document.getElementById('lives'); const timerEl = document.getElementById('timer'); const startBtn = document.getElementById('startBtn'); const C = cfg.colors; let W = canvas.width, H = canvas.height; let state = 'menu'; let score = 0, lives = 3, time = 0, startTime = 0; let entities = [], particles = [], powerupActive = null; let difficulty = 1; let lastTime = 0; let mouse = {x: W/2, y: H/2, down: false}; function resize() { const container = document.getElementById('gameContainer'); const rect = container.getBoundingClientRect(); canvas.width = Math.min(800, rect.width); canvas.height = Math.min(600, rect.height); W = canvas.width; H = canvas.height; } window.addEventListener('resize', resize); resize(); canvas.addEventListener('mousemove', e => { const rect = canvas.getBoundingClientRect(); mouse.x = (e.clientX - rect.left) * (canvas.width / rect.width); mouse.y = (e.clientY - rect.top) * (canvas.height / rect.height); }); canvas.addEventListener('mousedown', () => { mouse.down = true; }); canvas.addEventListener('mouseup', () => { mouse.down = false; }); canvas.addEventListener('click', e => { if (state === 'playing') handleClick(mouse.x, mouse.y); }); function makeEntity(type, x, y) { const names = cfg.entities.length ? cfg.entities : ['target']; const name = names[Math.floor(Math.random() * names.length)] || 'target'; return { type: type || 'target', name: name, x: x !== undefined ? x : Math.random() * (W - 60) + 30, y: y !== undefined ? y : Math.random() * (H - 60) + 30, vx: (Math.random() - 0.5) * (2 + difficulty), vy: (Math.random() - 0.5) * (2 + difficulty), r: 20 + Math.random() * 15, hp: type === 'enemy' ? 2 : 1, maxHp: type === 'enemy' ? 2 : 1, color: type === 'enemy' ? '#ff4444' : C.primary, alive: true, age: 0, pulse: Math.random() * Math.PI * 2, points: type === 'enemy' ? 50 : 10 }; } function makeParticle(x, y, color) { return { x, y, vx: (Math.random() - 0.5) * 6, vy: (Math.random() - 0.5) * 6, life: 1, color: color || C.primary, size: 2 + Math.random() * 3 }; } function spawnEntities() { const count = 3 + Math.floor(difficulty); for (let i = 0; i < count; i++) { const isEnemy = Math.random() < 0.25 + difficulty * 0.03; entities.push(makeEntity(isEnemy ? 'enemy' : 'target')); } } function handleClick(x, y) { let hit = false; for (let e of entities) { if (!e.alive) continue; const dx = x - e.x, dy = y - e.y; if (dx*dx + dy*dy < e.r*e.r) { e.hp--; hit = true; if (e.hp <= 0) { e.alive = false; score += e.points; scoreEl.textContent = 'SCORE: ' + score; for (let i = 0; i < 12; i++) { particles.push(makeParticle(e.x, e.y, e.color)); } if (cfg.powerups.length > 0 && Math.random() < 0.15) { const pu = cfg.powerups[Math.floor(Math.random() * cfg.powerups.length)]; powerupActive = {name: pu, time: 5}; } } else { for (let i = 0; i < 5; i++) { particles.push(makeParticle(e.x, e.y, e.color)); } } } } if (!hit && (cfg.genre === 'arcade' || cfg.genre === 'action')) { score = Math.max(0, score - 5); scoreEl.textContent = 'SCORE: ' + score; } } function updateGame(dt) { time = (Date.now() - startTime) / 1000; timerEl.textContent = 'TIME: ' + Math.floor(time) + 's'; difficulty = 1 + time * 0.05; for (let e of entities) { if (!e.alive) continue; e.age += dt; e.pulse += dt * 3; if (cfg.genre === 'action' || cfg.genre === 'arcade') { e.x += e.vx * difficulty; e.y += e.vy * difficulty; if (e.x < e.r) { e.x = e.r; e.vx *= -1; } if (e.x > W - e.r) { e.x = W - e.r; e.vx *= -1; } if (e.y < e.r) { e.y = e.r; e.vy *= -1; } if (e.y > H - e.r) { e.y = H - e.r; e.vy *= -1; } } else if (cfg.genre === 'puzzle') { e.x += e.vx * 0.3; e.y += e.vy * 0.3; if (e.x < e.r || e.x > W - e.r) e.vx *= -1; if (e.y < e.r || e.y > H - e.r) e.vy *= -1; } else { e.y += Math.sin(e.pulse) * 0.5; e.x += Math.cos(e.pulse * 0.7) * 0.3; } if (e.type === 'enemy' && cfg.genre === 'action') { const dx = mouse.x - e.x, dy = mouse.y - e.y; if (dx*dx + dy*dy < (e.r + 15) * (e.r + 15)) { lives--; livesEl.textContent = 'LIVES: ' + lives; e.alive = false; for (let i = 0; i < 15; i++) { particles.push(makeParticle(e.x, e.y, '#ff4444')); } if (lives <= 0) { gameOver(); return; } } } } entities = entities.filter(e => e.alive); if (entities.length < 3 + Math.floor(difficulty)) { const isEnemy = Math.random() < 0.2 + difficulty * 0.03; entities.push(makeEntity(isEnemy ? 'enemy' : 'target')); } for (let p of particles) { p.x += p.vx; p.y += p.vy; p.vy += 0.15; p.life -= dt * 1.5; } particles = particles.filter(p => p.life > 0); if (powerupActive) { powerupActive.time -= dt; if (powerupActive.time <= 0) powerupActive = null; } if (score >= 500) gameWin(); } function drawGame() { ctx.fillStyle = C.bg; ctx.fillRect(0, 0, W, H); ctx.strokeStyle = 'rgba(255,255,255,.03)'; ctx.lineWidth = 1; for (let x = 0; x < W; x += 40) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke(); } for (let y = 0; y < H; y += 40) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke(); } for (let e of entities) { if (!e.alive) continue; const pulseR = e.r + Math.sin(e.pulse) * 3; const grad = ctx.createRadialGradient(e.x, e.y, 0, e.x, e.y, pulseR * 2); grad.addColorStop(0, e.color + '88'); grad.addColorStop(1, e.color + '00'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(e.x, e.y, pulseR * 2, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = e.color; ctx.beginPath(); ctx.arc(e.x, e.y, pulseR, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = 'rgba(255,255,255,.3)'; ctx.beginPath(); ctx.arc(e.x - pulseR * 0.3, e.y - pulseR * 0.3, pulseR * 0.3, 0, Math.PI * 2); ctx.fill(); if (e.type === 'enemy' && e.hp < e.maxHp) { ctx.fillStyle = 'rgba(255,255,255,.2)'; ctx.fillRect(e.x - e.r, e.y - e.r - 8, e.r * 2, 4); ctx.fillStyle = '#ff4444'; ctx.fillRect(e.x - e.r, e.y - e.r - 8, (e.r * 2) * (e.hp / e.maxHp), 4); } ctx.fillStyle = 'rgba(255,255,255,.4)'; ctx.font = '9px monospace'; ctx.textAlign = 'center'; ctx.fillText(e.name.substring(0, 12), e.x, e.y + e.r + 14); } for (let p of particles) { ctx.globalAlpha = p.life; ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; if (state === 'playing') { ctx.strokeStyle = C.accent; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(mouse.x, mouse.y, 12, 0, Math.PI * 2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(mouse.x - 6, mouse.y); ctx.lineTo(mouse.x + 6, mouse.y); ctx.moveTo(mouse.x, mouse.y - 6); ctx.lineTo(mouse.x, mouse.y + 6); ctx.stroke(); } if (powerupActive) { ctx.fillStyle = C.accent; ctx.font = 'bold 14px monospace'; ctx.textAlign = 'left'; ctx.fillText('PWR: ' + powerupActive.name + ' (' + powerupActive.time.toFixed(1) + 's)', 12, H - 12); } } function startGame() { state = 'playing'; score = 0; lives = 3; time = 0; startTime = Date.now(); difficulty = 1; entities = []; particles = []; powerupActive = null; scoreEl.textContent = 'SCORE: 0'; livesEl.textContent = 'LIVES: 3'; timerEl.textContent = 'TIME: 0s'; overlay.classList.add('hidden'); spawnEntities(); } function gameWin() { state = 'won'; overlay.classList.remove('hidden'); overlay.innerHTML = '

YOU WIN!

' + '
' + '
FINAL SCORE
' + '
' + score + '
' + '
TIME
' + '
' + Math.floor(time) + 's
' + '
' + '

' + cfg.inspiration + '

' + ''; document.getElementById('startBtn').onclick = startGame; } function gameOver() { state = 'lost'; overlay.classList.remove('hidden'); overlay.innerHTML = '

GAME OVER

' + '
' + '
FINAL SCORE
' + '
' + score + '
' + '
TIME SURVIVED
' + '
' + Math.floor(time) + 's
' + '
' + '

' + cfg.inspiration + '

' + ''; document.getElementById('startBtn').onclick = startGame; } function loop(ts) { const dt = Math.min(0.05, (ts - lastTime) / 1000) || 0; lastTime = ts; if (state === 'playing') updateGame(dt); drawGame(); requestAnimationFrame(loop); } startBtn.onclick = startGame; requestAnimationFrame(loop); })(); JSEOF; file_put_contents("$appDir/index.html", $html); file_put_contents("$appDir/game.js", $js); write_json_file("$appDir/app_manifest.json", [ 'id' => $id, 'game_title' => $analysis['game_title'] ?? 'MicroGame', 'game_genre' => $gameGenre, 'game_concept' => $analysis['game_concept'] ?? '', 'inspiration' => $analysis['inspiration'] ?? '', 'understanding' => $analysis['understanding'] ?? '', 'created_at' => time(), 'source_video_exposed' => false ]); // Save to gallery $galleryFile = RECEIPT_DIR . '/gallery.json'; $gallery = file_exists($galleryFile) ? (read_json_file($galleryFile) ?: []) : []; $entry = [ 'videoId' => $id, 'videoName' => $receipt['videoName'] ?? 'unknown', 'gameTitle' => $analysis['game_title'] ?? 'MicroGame', 'gameGenre' => $gameGenre, 'gameConcept' => $analysis['game_concept'] ?? '', 'createdAt' => date('c'), 'duration' => $receipt['duration'] ?? 0 ]; $gallery = array_filter($gallery, fn($e) => ($e['videoId'] ?? '') !== $id); array_unshift($gallery, $entry); $gallery = array_slice($gallery, 0, 50); write_json_file($galleryFile, $gallery); json_response([ 'ok' => true, 'id' => $id, 'game_title' => $analysis['game_title'] ?? 'MicroGame', 'game_genre' => $gameGenre, 'game_concept' => $analysis['game_concept'] ?? '', 'preview_url' => "/generated/$id/index.html" ]);