Google Calendar Breakout Game

Play Breakout on your calendar - events become bricks that temporarily hide for 10 minutes when hit

Size

14.8 KB

Version

1.1.3

Created

Oct 22, 2025

Updated

1 day ago

1// ==UserScript==
2// @name		Google Calendar Breakout Game
3// @description		Play Breakout on your calendar - events become bricks that temporarily hide for 10 minutes when hit
4// @version		1.1.3
5// @match		https://*.calendar.google.com/*
6// ==/UserScript==
7(function() {
8    'use strict';
9
10    console.log('Google Calendar Breakout Game initialized');
11
12    // Game state
13    let gameActive = false;
14    let canvas, ctx;
15    let paddle, ball;
16    let hiddenEvents = new Map(); // Track hidden events with timestamps
17    let animationFrameId;
18
19    // Game constants
20    const PADDLE_WIDTH = 100;
21    const PADDLE_HEIGHT = 15;
22    const BALL_RADIUS = 8;
23    const BALL_SPEED = 6;
24    const HIDE_DURATION = 10 * 60 * 1000; // 10 minutes in milliseconds
25
26    // Confetti animation
27    class Confetti {
28        constructor(x, y) {
29            this.x = x;
30            this.y = y;
31            this.size = Math.random() * 8 + 4;
32            this.speedX = Math.random() * 6 - 3;
33            this.speedY = Math.random() * -8 - 2;
34            this.gravity = 0.3;
35            this.color = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff'][Math.floor(Math.random() * 6)];
36            this.rotation = Math.random() * 360;
37            this.rotationSpeed = Math.random() * 10 - 5;
38        }
39
40        update() {
41            this.speedY += this.gravity;
42            this.x += this.speedX;
43            this.y += this.speedY;
44            this.rotation += this.rotationSpeed;
45        }
46
47        draw(ctx) {
48            ctx.save();
49            ctx.translate(this.x, this.y);
50            ctx.rotate(this.rotation * Math.PI / 180);
51            ctx.fillStyle = this.color;
52            ctx.fillRect(-this.size / 2, -this.size / 2, this.size, this.size);
53            ctx.restore();
54        }
55
56        isOffScreen(canvasHeight) {
57            return this.y > canvasHeight;
58        }
59    }
60
61    let confettiParticles = [];
62    let gameOverDisplayed = false;
63
64    // Paddle object
65    class Paddle {
66        constructor(canvasWidth, canvasHeight) {
67            this.width = PADDLE_WIDTH;
68            this.height = PADDLE_HEIGHT;
69            this.x = canvasWidth / 2 - this.width / 2;
70            this.y = canvasHeight - 30;
71            this.speed = 8;
72            this.canvasWidth = canvasWidth;
73        }
74
75        draw(ctx) {
76            ctx.fillStyle = '#4285f4';
77            ctx.fillRect(this.x, this.y, this.width, this.height);
78        }
79
80        moveLeft() {
81            this.x = Math.max(0, this.x - this.speed);
82        }
83
84        moveRight() {
85            this.x = Math.min(this.canvasWidth - this.width, this.x + this.speed);
86        }
87    }
88
89    // Ball object
90    class Ball {
91        constructor(canvasWidth, canvasHeight) {
92            this.radius = BALL_RADIUS;
93            this.x = canvasWidth / 2;
94            this.y = canvasHeight - 50;
95            this.dx = BALL_SPEED * (Math.random() > 0.5 ? 1 : -1);
96            this.dy = -BALL_SPEED;
97            this.canvasWidth = canvasWidth;
98            this.canvasHeight = canvasHeight;
99        }
100
101        draw(ctx) {
102            ctx.beginPath();
103            ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
104            ctx.fillStyle = '#ea4335';
105            ctx.fill();
106            ctx.closePath();
107        }
108
109        update(paddle) {
110            this.x += this.dx;
111            this.y += this.dy;
112
113            // Wall collision
114            if (this.x + this.radius > this.canvasWidth || this.x - this.radius < 0) {
115                this.dx = -this.dx;
116            }
117            if (this.y - this.radius < 0) {
118                this.dy = -this.dy;
119            }
120
121            // Paddle collision
122            if (this.y + this.radius > paddle.y &&
123                this.x > paddle.x &&
124                this.x < paddle.x + paddle.width) {
125                this.dy = -Math.abs(this.dy);
126                // Add some angle variation based on where ball hits paddle
127                const hitPos = (this.x - paddle.x) / paddle.width;
128                this.dx = BALL_SPEED * (hitPos - 0.5) * 2;
129            }
130
131            // Ball falls off bottom - reset
132            if (this.y - this.radius > this.canvasHeight) {
133                this.reset();
134            }
135        }
136
137        reset() {
138            this.x = this.canvasWidth / 2;
139            this.y = this.canvasHeight - 50;
140            this.dx = BALL_SPEED * (Math.random() > 0.5 ? 1 : -1);
141            this.dy = -BALL_SPEED;
142        }
143    }
144
145    // Get all calendar event elements
146    function getCalendarEvents() {
147        return Array.from(document.querySelectorAll('div[data-eventchip][role="button"]'));
148    }
149
150    // Hide an event temporarily
151    function hideEvent(eventElement) {
152        if (!eventElement || hiddenEvents.has(eventElement)) return;
153
154        console.log('Hiding event temporarily');
155        eventElement.style.opacity = '0';
156        eventElement.style.pointerEvents = 'none';
157        
158        const hideTime = Date.now();
159        hiddenEvents.set(eventElement, hideTime);
160
161        // Schedule reappearance after 10 minutes
162        setTimeout(() => {
163            showEvent(eventElement);
164        }, HIDE_DURATION);
165    }
166
167    // Show an event again
168    function showEvent(eventElement) {
169        if (!eventElement) return;
170
171        console.log('Showing event again');
172        eventElement.style.opacity = '';
173        eventElement.style.pointerEvents = '';
174        hiddenEvents.delete(eventElement);
175    }
176
177    // Check collision between ball and events
178    function checkEventCollisions() {
179        const events = getCalendarEvents();
180        const canvasRect = canvas.getBoundingClientRect();
181
182        events.forEach(event => {
183            if (hiddenEvents.has(event)) return; // Skip already hidden events
184
185            const eventRect = event.getBoundingClientRect();
186            
187            // Convert event position to canvas coordinates
188            const eventX = eventRect.left - canvasRect.left;
189            const eventY = eventRect.top - canvasRect.top;
190            const eventWidth = eventRect.width;
191            const eventHeight = eventRect.height;
192
193            // Check if ball intersects with event
194            const closestX = Math.max(eventX, Math.min(ball.x, eventX + eventWidth));
195            const closestY = Math.max(eventY, Math.min(ball.y, eventY + eventHeight));
196
197            const distanceX = ball.x - closestX;
198            const distanceY = ball.y - closestY;
199            const distanceSquared = distanceX * distanceX + distanceY * distanceY;
200
201            if (distanceSquared < ball.radius * ball.radius) {
202                // Collision detected!
203                hideEvent(event);
204                
205                // Bounce ball
206                if (Math.abs(distanceX) > Math.abs(distanceY)) {
207                    ball.dx = -ball.dx;
208                } else {
209                    ball.dy = -ball.dy;
210                }
211                
212                // Check if all events are now hidden
213                checkGameOver();
214            }
215        });
216    }
217
218    // Check if all events are hidden (game over)
219    function checkGameOver() {
220        const allEvents = getCalendarEvents();
221        const allHidden = allEvents.every(event => hiddenEvents.has(event));
222        
223        if (allHidden && allEvents.length > 0 && !gameOverDisplayed) {
224            console.log('Game Over! All events cleared!');
225            gameOverDisplayed = true;
226            triggerConfetti();
227        }
228    }
229
230    // Trigger confetti celebration
231    function triggerConfetti() {
232        // Create confetti particles from multiple positions
233        for (let i = 0; i < 150; i++) {
234            const x = Math.random() * canvas.width;
235            const y = Math.random() * canvas.height * 0.3; // Start from top third of screen
236            confettiParticles.push(new Confetti(x, y));
237        }
238    }
239
240    // Update and draw confetti
241    function updateConfetti() {
242        confettiParticles = confettiParticles.filter(particle => {
243            particle.update();
244            particle.draw(ctx);
245            return !particle.isOffScreen(canvas.height);
246        });
247    }
248
249    // Draw game over message
250    function drawGameOver() {
251        if (!gameOverDisplayed) return;
252        
253        // Draw semi-transparent background
254        ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
255        ctx.fillRect(0, 0, canvas.width, canvas.height);
256        
257        // Draw "GAME OVER" text
258        ctx.fillStyle = '#FFD700';
259        ctx.font = 'bold 80px Arial';
260        ctx.textAlign = 'center';
261        ctx.textBaseline = 'middle';
262        ctx.fillText('GAME OVER!', canvas.width / 2, canvas.height / 2 - 50);
263        
264        // Draw subtitle
265        ctx.fillStyle = '#FFFFFF';
266        ctx.font = 'bold 40px Arial';
267        ctx.fillText('All Events Cleared! 🎉', canvas.width / 2, canvas.height / 2 + 50);
268    }
269
270    // Game loop
271    function gameLoop() {
272        if (!gameActive) return;
273
274        // Clear canvas
275        ctx.clearRect(0, 0, canvas.width, canvas.height);
276
277        // Update and draw
278        ball.update(paddle);
279        checkEventCollisions();
280        
281        paddle.draw(ctx);
282        ball.draw(ctx);
283
284        // Update and draw confetti if game over
285        if (gameOverDisplayed) {
286            updateConfetti();
287            drawGameOver();
288        }
289
290        animationFrameId = requestAnimationFrame(gameLoop);
291    }
292
293    // Keyboard controls
294    const keys = {};
295    function handleKeyDown(e) {
296        if (!gameActive) return;
297        keys[e.key] = true;
298
299        if (keys['ArrowLeft'] || keys['a'] || keys['A']) {
300            paddle.moveLeft();
301        }
302        if (keys['ArrowRight'] || keys['d'] || keys['D']) {
303            paddle.moveRight();
304        }
305    }
306
307    function handleKeyUp(e) {
308        keys[e.key] = false;
309    }
310
311    // Mouse controls
312    function handleMouseMove(e) {
313        if (!gameActive) return;
314        const rect = canvas.getBoundingClientRect();
315        const mouseX = e.clientX - rect.left;
316        paddle.x = Math.max(0, Math.min(canvas.width - paddle.width, mouseX - paddle.width / 2));
317    }
318
319    // Create game canvas overlay
320    function createGameCanvas() {
321        canvas = document.createElement('canvas');
322        canvas.id = 'breakout-game-canvas';
323        canvas.style.cssText = `
324            position: fixed;
325            top: 0;
326            left: 0;
327            width: 100%;
328            height: 100%;
329            z-index: 9998;
330            pointer-events: none;
331        `;
332        document.body.appendChild(canvas);
333
334        // Set canvas size
335        canvas.width = window.innerWidth;
336        canvas.height = window.innerHeight;
337        ctx = canvas.getContext('2d');
338
339        // Initialize game objects
340        paddle = new Paddle(canvas.width, canvas.height);
341        ball = new Ball(canvas.width, canvas.height);
342
343        // Handle window resize
344        window.addEventListener('resize', () => {
345            canvas.width = window.innerWidth;
346            canvas.height = window.innerHeight;
347            paddle.canvasWidth = canvas.width;
348            ball.canvasWidth = canvas.width;
349            ball.canvasHeight = canvas.height;
350        });
351
352        console.log('Game canvas created');
353    }
354
355    // Create toggle button
356    function createToggleButton() {
357        const button = document.createElement('button');
358        button.id = 'breakout-toggle-btn';
359        button.textContent = '🎮 Start Breakout';
360        button.style.cssText = `
361            position: fixed;
362            top: 20px;
363            right: 20px;
364            z-index: 9999;
365            padding: 12px 20px;
366            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
367            color: white;
368            border: none;
369            border-radius: 8px;
370            font-size: 16px;
371            font-weight: bold;
372            cursor: pointer;
373            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
374            transition: all 0.3s ease;
375            font-family: 'Google Sans', Roboto, Arial, sans-serif;
376        `;
377
378        button.addEventListener('mouseenter', () => {
379            button.style.transform = 'translateY(-2px)';
380            button.style.boxShadow = '0 6px 20px rgba(0, 0, 0, 0.3)';
381        });
382
383        button.addEventListener('mouseleave', () => {
384            button.style.transform = 'translateY(0)';
385            button.style.boxShadow = '0 4px 15px rgba(0, 0, 0, 0.2)';
386        });
387
388        button.addEventListener('click', toggleGame);
389        document.body.appendChild(button);
390
391        console.log('Toggle button created');
392    }
393
394    // Toggle game on/off
395    function toggleGame() {
396        gameActive = !gameActive;
397        const button = document.getElementById('breakout-toggle-btn');
398
399        if (gameActive) {
400            button.textContent = '⏸️ Stop Breakout';
401            button.style.background = 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)';
402            canvas.style.pointerEvents = 'auto';
403            
404            // Add event listeners
405            document.addEventListener('keydown', handleKeyDown);
406            document.addEventListener('keyup', handleKeyUp);
407            canvas.addEventListener('mousemove', handleMouseMove);
408            
409            // Reset ball position
410            ball.reset();
411            
412            // Start game loop
413            gameLoop();
414            
415            console.log('Game started');
416        } else {
417            button.textContent = '🎮 Start Breakout';
418            button.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
419            canvas.style.pointerEvents = 'none';
420            
421            // Remove event listeners
422            document.removeEventListener('keydown', handleKeyDown);
423            document.removeEventListener('keyup', handleKeyUp);
424            canvas.removeEventListener('mousemove', handleMouseMove);
425            
426            // Stop game loop
427            if (animationFrameId) {
428                cancelAnimationFrame(animationFrameId);
429            }
430            
431            // Clear canvas
432            ctx.clearRect(0, 0, canvas.width, canvas.height);
433            
434            console.log('Game stopped');
435        }
436    }
437
438    // Restore any hidden events on page unload
439    window.addEventListener('beforeunload', () => {
440        hiddenEvents.forEach((time, event) => {
441            showEvent(event);
442        });
443    });
444
445    // Initialize game when calendar is ready
446    function init() {
447        // Wait for calendar to load
448        const checkCalendar = setInterval(() => {
449            const events = getCalendarEvents();
450            if (events.length > 0) {
451                clearInterval(checkCalendar);
452                console.log(`Found ${events.length} calendar events`);
453                
454                createGameCanvas();
455                createToggleButton();
456                
457                console.log('Breakout game ready! Click the button to start playing.');
458                console.log('Controls: Arrow keys or A/D to move paddle, or use your mouse!');
459            }
460        }, 1000);
461    }
462
463    // Start initialization
464    if (document.readyState === 'loading') {
465        document.addEventListener('DOMContentLoaded', init);
466    } else {
467        init();
468    }
469})();
Google Calendar Breakout Game | Robomonkey