From dde8f2e4ca274687dbadddd5c837bdfae6c17b96 Mon Sep 17 00:00:00 2001 From: workmailan8n-hash Date: Thu, 9 Apr 2026 21:49:38 +0300 Subject: [PATCH 1/3] fix(panel): right panel was drawing over the office wall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs from asymmetric OX / OX_RIGHT split: 1. CW = OX + COLS*T + OX used left offset for both sides — canvas was 20px too narrow, so right panel anchored at CW-OX_RIGHT sat inside the right wall. 2. drawRightPanel used panelX = CW - OX + 3 (left offset) instead of CW - OX_RIGHT + 3 — panel drew 20px further left than intended. 3. Day-light shaft at ly used CW - OX*2; corrected to CW - OX - OX_RIGHT. Now right panel sits flush against the canvas edge and the wall is fully visible next to it. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/app.js b/src/app.js index 7682931..785c0a2 100644 --- a/src/app.js +++ b/src/app.js @@ -7121,7 +7121,7 @@ function buildBackground() { lg.addColorStop(0, "#fffff0"); lg.addColorStop(1, "transparent"); ctx.fillStyle = lg; - ctx.fillRect(OX, ly, CW - OX * 2, T * 8); + ctx.fillRect(OX, ly, CW - OX - OX_RIGHT, T * 8); ctx.restore(); } @@ -12396,8 +12396,8 @@ function drawLeftPanel(ctx, tick) { } function drawRightPanel(ctx, tick) { - const panelX = CW - OX + 3; - const W = OX - 6, + const panelX = CW - OX_RIGHT + 3; + const W = OX_RIGHT - 6, H = CH; // Background @@ -15386,7 +15386,7 @@ function applyWallPositions() { if (saved.wall_zone !== undefined) ACT_ZONE_Y = Math.round(saved.wall_zone); if (saved.wall_right !== undefined) { COLS = Math.round(saved.wall_right); - CW = OX + COLS * T + OX; + CW = OX + COLS * T + OX_RIGHT; const cv = document.getElementById("office"); if (cv) cv.width = CW; bgBuf.width = CW; @@ -15711,7 +15711,7 @@ canvas.addEventListener("mousemove", (e) => { // Live-resize canvas when dragging outer walls (no rebuild during drag) if (w.id === "wall_right") { COLS = Math.round(w.pos); - CW = OX + COLS * T + OX; + CW = OX + COLS * T + OX_RIGHT; canvas.width = CW; bgBuf.width = CW; } @@ -15783,7 +15783,7 @@ document.addEventListener("mousemove", (e) => { } if (w.id === "wall_right") { COLS = Math.round(w.pos); - CW = OX + COLS * T + OX; + CW = OX + COLS * T + OX_RIGHT; canvas.width = CW; bgBuf.width = CW; } @@ -15811,7 +15811,7 @@ document.addEventListener("mouseup", (e) => { if (w2.id === "wall_zone") ACT_ZONE_Y = Math.round(w2.pos); if (w2.id === "wall_right") { COLS = Math.round(w2.pos); - CW = OX + COLS * T + OX; + CW = OX + COLS * T + OX_RIGHT; canvas.width = CW; bgBuf.width = CW; } @@ -15846,7 +15846,7 @@ canvas.addEventListener("mouseup", (e) => { if (w.id === "wall_zone") ACT_ZONE_Y = Math.round(w.pos); if (w.id === "wall_right") { COLS = Math.round(w.pos); - CW = OX + COLS * T + OX; + CW = OX + COLS * T + OX_RIGHT; canvas.width = CW; bgBuf.width = CW; } @@ -17924,7 +17924,7 @@ document.getElementById("admin-start-pos").onclick = () => { window._adminPos = Object.assign({}, BUILTIN_POSITIONS); // Reset walls to default sizes COLS = 35; - CW = OX + COLS * T + OX; + CW = OX + COLS * T + OX_RIGHT; canvas.width = CW; bgBuf.width = CW; generateLayout(Math.max(12, Object.keys(agentsData).length)); From 153a2d95826f63f5681b41528459f122dad0886e Mon Sep 17 00:00:00 2001 From: workmailan8n-hash Date: Thu, 9 Apr 2026 21:51:08 +0300 Subject: [PATCH 2/3] fix: define missing OX_RIGHT constant (canvas was blank) --- src/app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app.js b/src/app.js index 785c0a2..e28dffb 100644 --- a/src/app.js +++ b/src/app.js @@ -40,7 +40,8 @@ import { launchJukeboxGame } from "./minigames/jukebox.js"; // ════════════════════════════════════════════════════════════════ let CW = 1400; const T = 32; // tile size px -const OX = 150; // canvas left margin +const OX = 180; // canvas left margin (left side panel width) +const OX_RIGHT = 160; // canvas right margin (right side panel width) const OY = 12; // canvas top margin let COLS = 35; // room width in tiles (mutable for wall editor) let ROWS = 14; From cc1809130c6e366440f9bb7208dee75d53bc297d Mon Sep 17 00:00:00 2001 From: workmailan8n-hash Date: Fri, 10 Apr 2026 12:05:40 +0300 Subject: [PATCH 3/3] feat: export PNG screenshot + foosball minigame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 📸 PNG button to header that captures office canvas as timestamped PNG download - Mark ROADMAP [x] Export office as PNG screenshot (Day 10) - New minigame: src/minigames/foosball.js — top-down foosball vs CPU • ↑↓ / W/S controls your row of 3 blue players • CPU red row auto-tracks ball • First to 5 goals wins, score saved to leaderboard • Pixel-art green field, glowing players, physics ball deflection - Wire foosball click handler in app.js (foosball object already in CLICK_OBJ_MAP) Co-Authored-By: Claude Sonnet 4.6 --- ROADMAP.md | 2 +- src/app.js | 7 + src/index.html | 1 + src/main.js | 42 +++++ src/minigames/foosball.js | 340 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 src/minigames/foosball.js diff --git a/ROADMAP.md b/ROADMAP.md index f97ace1..4e409fa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -81,7 +81,7 @@ Autonomous development — Claude works on it daily, deploying improvements. ## Day 10 — Premium Polish - [x] Loading screen with pixel art progress bar - [x] Settings panel (toggle sounds, particles, animations) -- [ ] Export office as PNG screenshot +- [x] Export office as PNG screenshot - [ ] Export as GIF animation (5 second loop) ## Day 11 — Social Features diff --git a/src/app.js b/src/app.js index e28dffb..8636a10 100644 --- a/src/app.js +++ b/src/app.js @@ -34,6 +34,7 @@ import { launchCoffeeGame } from "./minigames/coffee.js"; import { launchWhiteboardGame } from "./minigames/whiteboard.js"; import { launchPlantGame } from "./minigames/plant.js"; import { launchJukeboxGame } from "./minigames/jukebox.js"; +import { launchFoosballGame } from "./minigames/foosball.js"; // ════════════════════════════════════════════════════════════════ // CONSTANTS @@ -16364,6 +16365,12 @@ canvas.addEventListener("click", (e) => { setTimeout(() => blip(659, 0.08, "sine", 0.03), 200); return; } + if (hit.type === "foosball") { + launchFoosballGame(); + blip(440, 0.08, "square", 0.04); + setTimeout(() => blip(660, 0.06, "square", 0.03), 80); + return; + } if (clickAnims.some((a) => a.id === hit.id)) return; clickAnims.push({ id: hit.id, diff --git a/src/index.html b/src/index.html index 0ec02f8..e30dfe2 100644 --- a/src/index.html +++ b/src/index.html @@ -49,6 +49,7 @@ 0 agents + ⏳ tunnel... diff --git a/src/main.js b/src/main.js index de8776d..3e66c18 100644 --- a/src/main.js +++ b/src/main.js @@ -550,3 +550,45 @@ if (typeof window !== "undefined") { localStorage.setItem(key, JSON.stringify(lb)); }; })(); + +// ════════════════════════════════════════════════════════════════ +// SCREENSHOT EXPORT — capture office canvas as PNG +// ════════════════════════════════════════════════════════════════ +(function initScreenshot() { + const btn = document.getElementById("btn-screenshot"); + if (!btn) return; + + btn.addEventListener("click", () => { + const canvas = document.getElementById("office"); + if (!canvas) return; + + // Flash effect + btn.textContent = "⏳"; + btn.disabled = true; + + // Use timeout so the current frame finishes rendering + setTimeout(() => { + try { + const dataUrl = canvas.toDataURL("image/png"); + const link = document.createElement("a"); + const now = new Date(); + const stamp = + now.getFullYear() + + String(now.getMonth() + 1).padStart(2, "0") + + String(now.getDate()).padStart(2, "0") + + "_" + + String(now.getHours()).padStart(2, "0") + + String(now.getMinutes()).padStart(2, "0") + + String(now.getSeconds()).padStart(2, "0"); + link.download = "agent-office_" + stamp + ".png"; + link.href = dataUrl; + link.click(); + } catch (e) { + console.error("[screenshot] failed:", e); + } finally { + btn.textContent = "📸 PNG"; + btn.disabled = false; + } + }, 50); + }); +})(); diff --git a/src/minigames/foosball.js b/src/minigames/foosball.js new file mode 100644 index 0000000..f910e3f --- /dev/null +++ b/src/minigames/foosball.js @@ -0,0 +1,340 @@ +// ════════════════════════════════════════════════════════════════ +// FOOSBALL MINIGAME — top-down table football, no external deps +// Triggered when user clicks the foosball table object. +// ════════════════════════════════════════════════════════════════ + +const GW = 360, + GH = 240; +const GOAL_W = 24, + GOAL_H = 70; +const PLAYER_R = 8, + BALL_R = 6; +const WIN_SCORE = 5; +const CPU_SPEED = 2.8; + +export function launchFoosballGame() { + // ── Overlay ─────────────────────────────────────────────────── + const overlay = document.createElement("div"); + overlay.style.cssText = + "position:fixed;inset:0;background:rgba(5,5,15,0.95);display:flex;" + + "flex-direction:column;align-items:center;justify-content:center;" + + "z-index:1000;font-family:'Press Start 2P',monospace;"; + + const title = document.createElement("div"); + title.style.cssText = + "color:#e0af68;font-size:11px;margin-bottom:10px;" + + "text-shadow:0 0 14px #e0af68aa;letter-spacing:2px;"; + title.textContent = "⚽ FOOSBALL"; + overlay.appendChild(title); + + const gc = document.createElement("canvas"); + gc.width = GW; + gc.height = GH; + gc.style.cssText = + "border:2px solid #3a3860;border-radius:4px;display:block;" + + "image-rendering:pixelated;outline:none;"; + gc.setAttribute("tabindex", "0"); + overlay.appendChild(gc); + const ctx = gc.getContext("2d"); + + const instrEl = document.createElement("div"); + instrEl.style.cssText = + "color:#a9b1d640;font-size:5px;margin-top:8px;letter-spacing:1px;"; + instrEl.textContent = "↑↓ or W/S to move your players | ESC: exit"; + overlay.appendChild(instrEl); + + const closeBtn = document.createElement("button"); + closeBtn.textContent = "✕ EXIT"; + closeBtn.style.cssText = + "margin-top:8px;background:#2a2848;color:#f7768e;" + + "border:1px solid #f7768e50;padding:6px 14px;" + + "font-family:inherit;font-size:6px;cursor:pointer;border-radius:3px;"; + overlay.appendChild(closeBtn); + document.body.appendChild(overlay); + gc.focus(); + + // ── Game state ───────────────────────────────────────────────── + let playerScore = 0, + cpuScore = 0; + let running = true, + rafId = null; + let flashMsg = null, + flashTimer = 0, + gameOver = false; + let keys = {}; + + // Players: user row at x=90 (3 players), cpu row at x=270 (3 players) + const USER_X = 90, + CPU_X = 270; + const ROW_SPACING = 55; + let userY = GH / 2; // center of user's row (moves as unit) + let cpuY = GH / 2; + + function makeRow(cx, cy) { + return [-ROW_SPACING, 0, ROW_SPACING].map((dy) => ({ x: cx, y: cy + dy })); + } + + function resetBall(dir) { + const angle = Math.random() * 0.6 - 0.3; + return { + x: GW / 2, + y: GH / 2, + vx: dir * (3.2 + Math.random() * 1.2), + vy: Math.sin(angle) * 2.5, + }; + } + + let ball = resetBall(1); + + // ── Physics ──────────────────────────────────────────────────── + function deflectByRow(rowX, rowCY, col) { + const PR = PLAYER_R + BALL_R; + for (const dy of [-ROW_SPACING, 0, ROW_SPACING]) { + const py = rowCY + dy; + const dx = ball.x - rowX, + ddy = ball.y - py; + const dist = Math.sqrt(dx * dx + ddy * ddy); + if (dist < PR) { + // Push ball away + const nx = dx / dist, + ny = ddy / dist; + const speed = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); + const boost = col === "user" ? 1.1 : 1.0; + ball.vx = nx * speed * boost; + ball.vy = + ny * speed * boost + + (col === "user" + ? keys["ArrowUp"] + ? -1 + : keys["ArrowDown"] + ? 1 + : 0 + : 0); + // Separate + const overlap = PR - dist + 1; + ball.x += nx * overlap; + ball.y += ny * overlap; + // Clamp speed + const spd = Math.sqrt(ball.vx * ball.vx + ball.vy * ball.vy); + if (spd > 7) { + ball.vx = (ball.vx / spd) * 7; + ball.vy = (ball.vy / spd) * 7; + } + return true; + } + } + return false; + } + + function update() { + if (gameOver || flashTimer > 0) { + if (flashTimer > 0) flashTimer--; + if (flashTimer === 0 && flashMsg) { + flashMsg = null; + if (!gameOver) ball = resetBall(cpuScore > playerScore ? 1 : -1); + } + return; + } + + // User input + const MOVE = 4; + if (keys["ArrowUp"] || keys["w"] || keys["W"]) + userY = Math.max(GOAL_H / 2 + 10, userY - MOVE); + if (keys["ArrowDown"] || keys["s"] || keys["S"]) + userY = Math.min(GH - GOAL_H / 2 - 10, userY + MOVE); + + // CPU tracks ball + const cpuDy = ball.y - cpuY; + cpuY += Math.sign(cpuDy) * Math.min(CPU_SPEED, Math.abs(cpuDy)); + cpuY = Math.max(GOAL_H / 2 + 10, Math.min(GH - GOAL_H / 2 - 10, cpuY)); + + // Move ball + ball.x += ball.vx; + ball.y += ball.vy; + + // Top / bottom wall bounce + if (ball.y - BALL_R < 0) { + ball.y = BALL_R; + ball.vy *= -1; + } + if (ball.y + BALL_R > GH) { + ball.y = GH - BALL_R; + ball.vy *= -1; + } + + // Deflect by player rows + deflectByRow(USER_X, userY, "user"); + deflectByRow(CPU_X, cpuY, "cpu"); + + // Goals: left edge = CPU scores, right edge = user scores + const goalTop = (GH - GOAL_H) / 2, + goalBot = (GH + GOAL_H) / 2; + if (ball.x - BALL_R < 0) { + if (ball.y >= goalTop && ball.y <= goalBot) { + cpuScore++; + flashMsg = cpuScore >= WIN_SCORE ? "CPU WINS!" : "CPU GOAL!"; + if (cpuScore >= WIN_SCORE) gameOver = true; + flashTimer = 80; + ball.vx = 0; + ball.vy = 0; + } else { + ball.x = BALL_R; + ball.vx = Math.abs(ball.vx); + } + } + if (ball.x + BALL_R > GW) { + if (ball.y >= goalTop && ball.y <= goalBot) { + playerScore++; + if ( + playerScore >= WIN_SCORE && + typeof window.saveGameScore === "function" + ) + window.saveGameScore("foosball", playerScore); + flashMsg = playerScore >= WIN_SCORE ? "YOU WIN!" : "GOAL!"; + if (playerScore >= WIN_SCORE) gameOver = true; + flashTimer = 80; + ball.vx = 0; + ball.vy = 0; + } else { + ball.x = GW - BALL_R; + ball.vx = -Math.abs(ball.vx); + } + } + } + + // ── Render ───────────────────────────────────────────────────── + function render() { + if (!running) return; + update(); + + ctx.clearRect(0, 0, GW, GH); + + // Field + ctx.fillStyle = "#1a3a18"; + ctx.fillRect(0, 0, GW, GH); + + // Field lines + ctx.strokeStyle = "#2a5a28"; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(GW / 2, 0); + ctx.lineTo(GW / 2, GH); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(GW / 2, GH / 2, 30, 0, Math.PI * 2); + ctx.stroke(); + + // Goals (left = user, right = cpu) + const gTop = (GH - GOAL_H) / 2, + gBot = (GH + GOAL_H) / 2; + ctx.fillStyle = "#7aa2f720"; + ctx.fillRect(0, gTop, GOAL_W, GOAL_H); + ctx.fillStyle = "#f7768e20"; + ctx.fillRect(GW - GOAL_W, gTop, GOAL_W, GOAL_H); + ctx.strokeStyle = "#7aa2f7"; + ctx.strokeRect(0, gTop, GOAL_W, GOAL_H); + ctx.strokeStyle = "#f7768e"; + ctx.strokeRect(GW - GOAL_W, gTop, GOAL_W, GOAL_H); + + // Player rows + const drawRow = (cx, cy, col) => { + for (const dy of [-ROW_SPACING, 0, ROW_SPACING]) { + ctx.save(); + ctx.shadowColor = col; + ctx.shadowBlur = 8; + ctx.fillStyle = col; + ctx.beginPath(); + ctx.arc(cx, cy + dy, PLAYER_R, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + ctx.fillStyle = "#000a"; + ctx.beginPath(); + ctx.arc(cx, cy + dy, PLAYER_R - 3, 0, Math.PI * 2); + ctx.fill(); + } + }; + drawRow(USER_X, userY, "#7aa2f7"); + drawRow(CPU_X, cpuY, "#f7768e"); + + // Ball + ctx.save(); + ctx.shadowColor = "#ffffff80"; + ctx.shadowBlur = 10; + ctx.fillStyle = "#ffffff"; + ctx.beginPath(); + ctx.arc(ball.x, ball.y, BALL_R, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + ctx.fillStyle = "#cccccc"; + ctx.fillRect(ball.x - 2, ball.y - 2, 2, 2); + + // Scoreboard + ctx.font = "10px 'Press Start 2P',monospace"; + ctx.textAlign = "center"; + ctx.fillStyle = "#7aa2f7"; + ctx.fillText(playerScore, GW / 2 - 28, 20); + ctx.fillStyle = "#a9b1d6"; + ctx.fillText("-", GW / 2, 20); + ctx.fillStyle = "#f7768e"; + ctx.fillText(cpuScore, GW / 2 + 28, 20); + + // Flash message + if (flashMsg) { + ctx.save(); + ctx.globalAlpha = 0.92; + ctx.fillStyle = "#0a0a18"; + ctx.fillRect(GW / 2 - 70, GH / 2 - 18, 140, 32); + ctx.globalAlpha = 1; + ctx.font = "9px 'Press Start 2P',monospace"; + ctx.fillStyle = flashMsg.includes("WIN") + ? "#ffd700" + : flashMsg === "GOAL!" + ? "#9ece6a" + : "#f7768e"; + ctx.shadowColor = ctx.fillStyle; + ctx.shadowBlur = 14; + ctx.fillText(flashMsg, GW / 2, GH / 2 + 4); + ctx.restore(); + } + + if (gameOver) { + ctx.font = "6px 'Press Start 2P',monospace"; + ctx.textAlign = "center"; + ctx.fillStyle = "#a9b1d680"; + ctx.fillText("CLICK or ESC to exit", GW / 2, GH - 10); + } + + rafId = requestAnimationFrame(render); + } + + // ── Input ───────────────────────────────────────────────────── + function onKey(e) { + if (e.type === "keydown") { + keys[e.key] = true; + if (e.key === "Escape") { + close(); + return; + } + if ((e.key === " " || e.key === "Enter") && gameOver) close(); + if (["ArrowUp", "ArrowDown", " "].includes(e.key)) e.preventDefault(); + } else { + keys[e.key] = false; + } + } + gc.addEventListener("click", () => { + if (gameOver) close(); + }); + document.addEventListener("keydown", onKey); + document.addEventListener("keyup", onKey); + + function close() { + running = false; + cancelAnimationFrame(rafId); + overlay.remove(); + document.removeEventListener("keydown", onKey); + document.removeEventListener("keyup", onKey); + } + + closeBtn.addEventListener("click", close); + rafId = requestAnimationFrame(render); +}