$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"
]);