From 6af2b5bf30924d0f68bc44651407801fd1559f22 Mon Sep 17 00:00:00 2001 From: John Bampton Date: Thu, 29 Jan 2026 20:54:55 +1000 Subject: [PATCH] Add space invaders --- src/assets/css/style.css | 9 +- src/assets/js/eggs.js | 353 ++++++++++++++++++++++++++++++--------- src/assets/js/script.js | 35 ---- 3 files changed, 279 insertions(+), 118 deletions(-) diff --git a/src/assets/css/style.css b/src/assets/css/style.css index a66e583..4381e01 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -918,6 +918,11 @@ a:hover { left: 0; width: 100vw; height: 100vh; - z-index: 9999; - pointer-events: none; /* Allow clicks to pass through to the site if needed */ + z-index: 9999; /* Ensures it sits ABOVE your website content */ + pointer-events: none; /* Let's start with none so it doesn't block the heart */ +} + +/* Ensure the canvas itself fills the container */ +#phaser-container canvas { + display: block; } diff --git a/src/assets/js/eggs.js b/src/assets/js/eggs.js index 63c3cbd..2ba15bb 100644 --- a/src/assets/js/eggs.js +++ b/src/assets/js/eggs.js @@ -1,113 +1,304 @@ +/** + * eggs.js - The Space Invaders Easter Egg + * Logic: 5 Heart Clicks -> Emoji Explosion -> 80s Space Invaders + */ + +// 1. GLOBAL CONSTANTS & STATE +const emojiBurst = [ + "๐ŸŽฎ", + "๐Ÿ•น๏ธ", + "๐Ÿ‘พ", + "๐Ÿš€", + "โœจ", + "โญ", + "๐Ÿ”ฅ", + "๐Ÿ’ฅ", + "๐ŸŒˆ", + "๐ŸŽ‰", + "๐Ÿ’–", + "๐Ÿ’Ž", + "๐Ÿค–", + "๐Ÿ‘ป", + "๐Ÿฆ„", + "๐Ÿ„", + "๐ŸŒ", + "โšก", + "๐Ÿ†", + "๐ŸŽฏ", + "๐Ÿ›ธ", + "๐Ÿ‘ฝ", + "๐Ÿ‘พ", + "๐Ÿ™", + "๐Ÿฆ–", + "๐Ÿช", + "๐ŸŒŒ", + "๐ŸŒ ", + "โ˜„๏ธ", + "๐ŸŒ™", +]; + +let heartClickCount = 0; +let phaserStarted = false; +let gameInstance; +let player; +let cursors; +let aliens; +let bullets; +let lastFired = 0; + +// 2. DOM TRIGGER LOGIC (Integrate this with your footer heart) +const heart = document.getElementById("footer-heart"); + +if (heart) { + heart.style.cursor = "pointer"; + heart.style.display = "inline-block"; // Necessary for transforms + + heart.addEventListener("click", () => { + if (phaserStarted) return; + + heartClickCount++; + + // Visual feedback: Heart grows + const scaleAmount = 1 + heartClickCount * 0.3; + heart.style.transition = + "transform 0.1s cubic-bezier(0.17, 0.67, 0.83, 0.67)"; + heart.style.transform = `scale(${scaleAmount})`; + + if (heartClickCount === 5) { + phaserStarted = true; + heart.innerHTML = "๐ŸŽฎ"; // Swap to gamer emoji + heart.style.transform = "scale(1.5)"; + + setTimeout(() => { + heart.style.opacity = "0"; // Fade out the heart + initPhaserGame(); + }, 300); + } + }); +} + +// 3. PHASER ENGINE INITIALIZATION function initPhaserGame() { + // Create dedicated Canvas + const canvas = document.createElement("canvas"); + canvas.id = "phaser-game-canvas"; + Object.assign(canvas.style, { + position: "fixed", + top: "0", + left: "0", + width: "100vw", + height: "100vh", + zIndex: "10000", + pointerEvents: "none", // Start as click-through + }); + document.body.appendChild(canvas); + const config = { - type: Phaser.AUTO, + type: Phaser.CANVAS, + canvas: canvas, width: window.innerWidth, height: window.innerHeight, - parent: "phaser-container", // Make sure this div exists in your HTML transparent: true, physics: { default: "arcade", - arcade: { gravity: { y: 300 } }, + arcade: { gravity: { y: 0 }, debug: false }, }, scene: { preload: preload, create: create, + update: update, }, }; - const game = new Phaser.Game(config); + gameInstance = new Phaser.Game(config); } +// 4. PHASER SCENE FUNCTIONS function preload() { - // No need to preload images if we are only using text/emojis! + // No assets to load - we use emojis! } function create() { - const emojis = [ - // Gaming & Tech - "๐ŸŽฎ", - "๐Ÿ•น๏ธ", - "๐Ÿ‘พ", - "๐Ÿš€", - "๐Ÿ’ป", - "๐Ÿ“ฑ", - "โŒจ๏ธ", - "๐Ÿ–ฑ๏ธ", - "๐Ÿ”‹", - "๐Ÿ”Œ", - // Magic & Space - "โœจ", - "โญ", - "๐ŸŒŸ", - "๐Ÿ”ฎ", - "๐ŸŒŒ", - "๐ŸŒ ", - "๐ŸŒ™", - "โ˜„๏ธ", - "๐Ÿ›ธ", - "๐Ÿ‘ฝ", - // Action & Fun - "๐Ÿ”ฅ", - "๐Ÿ’ฅ", - "๐Ÿงจ", - "โšก", - "๐ŸŒˆ", - "๐ŸŽ‰", - "๐ŸŽŠ", - "๐ŸŽˆ", - "๐ŸŽ", - "๐Ÿ’Ž", - // Hearts & Expressions - "๐Ÿ’–", - "๐ŸŽฏ", - "๐Ÿ†", - "๐Ÿฅ‡", - "๐Ÿงฟ", - "๐Ÿ€", - "๐Ÿ•", - "๐Ÿญ", - "๐Ÿฆ", - "๐Ÿฉ", - // Creatures & Icons - "๐Ÿค–", - "๐Ÿ‘ป", - "๐Ÿฒ", - "๐Ÿฆ„", - "๐ŸฆŠ", - "๐Ÿฑ", - "๐Ÿง", - "๐Ÿฆ–", - "๐Ÿ„", - "๐ŸŒ", - ]; + const particles = spawnExplosion(this); + + // After 5 seconds, clear explosion and start the real game + this.time.delayedCall(5000, () => { + this.tweens.add({ + targets: particles.getChildren(), + alpha: 0, + duration: 1000, + onComplete: () => { + particles.clear(true, true); + + // Make the game interactive + const canvas = document.getElementById("phaser-game-canvas"); + if (canvas) canvas.style.pointerEvents = "auto"; + + setupSpaceInvaders.call(this); + }, + }); + }); +} + +function update() { + if (!player || !player.body) return; + + // Movement + if (cursors.left.isDown) { + player.body.setVelocityX(-400); + } else if (cursors.right.isDown) { + player.body.setVelocityX(400); + } else { + player.body.setVelocityX(0); + } + + // Shooting + if (cursors.space.isDown) { + fireBullet(this); + } +} + +// 5. HELPER FUNCTIONS (The Mechanics) + +function spawnExplosion(scene) { const heartRect = document .getElementById("footer-heart") .getBoundingClientRect(); + const particles = scene.add.group(); - for (let i = 0; i < 75; i++) { - // 1. Pick a random emoji - const randomEmoji = emojis[Math.floor(Math.random() * emojis.length)]; - - // 2. Create the emoji at the heart's location - // We use this.add.text instead of this.physics.add.image - const particle = this.add.text(heartRect.left, heartRect.top, randomEmoji, { + for (let i = 0; i < 40; i++) { + const emoji = Phaser.Utils.Array.GetRandom(emojiBurst); + const p = scene.add.text(heartRect.left, heartRect.top, emoji, { fontSize: "32px", }); - // 3. Manually add physics to the text object - this.physics.add.existing(particle); - - // 4. Apply the "Explosion" physics - // Shoots them out in a cone shape upward - particle.body.setVelocity( - Phaser.Math.Between(-300, 300), - Phaser.Math.Between(-500, -1000), + scene.physics.add.existing(p); + p.body.setVelocity( + Phaser.Math.Between(-400, 400), + Phaser.Math.Between(-600, -1200), ); + p.body.setBounce(0.6); + p.body.setCollideWorldBounds(true); + p.body.setAngularVelocity(Phaser.Math.Between(-200, 200)); + + particles.add(p); + } + return particles; +} + +function setupSpaceInvaders() { + const scene = this; + + // Player Rocket + player = scene.add.text( + window.innerWidth / 2, + window.innerHeight - 80, + "๐Ÿš€", + { fontSize: "50px" }, + ); + scene.physics.add.existing(player); + player.body.setCollideWorldBounds(true); + + // Bullets + bullets = scene.physics.add.group(); + + // Aliens Grid - Adjusted for smaller size + aliens = scene.physics.add.group(); + const rows = 5; + const cols = 10; + const spacingX = 50; // Tighter horizontal spacing + const spacingY = 45; // Tighter vertical spacing + + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + let alienEmoji = ["๐Ÿ‘พ", "๐Ÿ‘ฝ", "๐Ÿ›ธ", "๐Ÿ™", "๐Ÿ‘พ"][y]; + // Shrink from 35px to 24px + let alien = scene.add.text( + x * spacingX + 80, + y * spacingY + 80, + alienEmoji, + { fontSize: "24px" }, + ); + + scene.physics.add.existing(alien); + alien.body.setAllowGravity(false); + // Shrink the collision box to match the smaller emoji + alien.body.setSize(24, 24); + + aliens.add(alien); + } + } + + // Alien Movement Timer + scene.alienDirection = 1; + scene.time.addEvent({ + delay: 800, + callback: moveAliens, + callbackScope: scene, + loop: true, + }); - particle.body.setCollideWorldBounds(true); - particle.body.setBounce(0.7); + // Collisions + scene.physics.add.overlap(bullets, aliens, (bullet, alien) => { + bullet.destroy(); + alien.destroy(); + if (aliens.countActive() === 0) { + alert("INVADERS REPELLED! YOU WIN!"); + window.location.reload(); + } + }); - // Optional: Add a little random rotation for flair - particle.setAngle(Phaser.Math.Between(0, 360)); + cursors = scene.input.keyboard.createCursorKeys(); +} + +function moveAliens() { + let hitEdge = false; + const padding = 60; + const children = aliens.getChildren(); + + children.forEach((alien) => { + if (this.alienDirection === 1 && alien.x > window.innerWidth - padding) + hitEdge = true; + if (this.alienDirection === -1 && alien.x < padding) hitEdge = true; + }); + + if (hitEdge) { + this.alienDirection *= -1; + children.forEach((alien) => { + alien.y += 40; + alien.x += this.alienDirection * 10; + }); + } else { + children.forEach((alien) => { + alien.x += 25 * this.alienDirection; + }); } } +function fireBullet(scene) { + const now = scene.time.now; + if (now - lastFired < 400) return; + + // 1. Create the bullet slightly above the player's center + const bullet = scene.add.text( + player.x + player.width / 2 - 10, + player.y - 20, + "๐Ÿ”ฅ", + { + fontSize: "20px", + }, + ); + + // 2. Add to physics and the group + scene.physics.add.existing(bullet); + bullets.add(bullet); + + // 3. FAIL-SAFES + bullet.body.setAllowGravity(false); // Ensure gravity isn't pulling it down + bullet.body.setImmovable(false); // Ensure it's allowed to move + bullet.body.setVelocityY(-600); // Set the upward speed + + // 4. Force a sync between the physics body and the Text object + bullet.body.isCircle = true; // Often helps with collision detection for small objects + + lastFired = now; +} diff --git a/src/assets/js/script.js b/src/assets/js/script.js index 031f7cf..a6d41a9 100644 --- a/src/assets/js/script.js +++ b/src/assets/js/script.js @@ -1337,41 +1337,6 @@ window.addEventListener("DOMContentLoaded", () => { } }); -let heartClickCount = 0; -let phaserStarted = false; - -const heart = document.getElementById("footer-heart"); - -heart.addEventListener("click", () => { - heartClickCount++; - - // 1. Grow the heart with each click - const scaleAmount = 1 + heartClickCount * 0.3; - heart.style.transform = `scale(${scaleAmount})`; - heart.style.display = "inline-block"; // Ensuring transform works - heart.style.transition = - "transform 0.1s cubic-bezier(0.17, 0.67, 0.83, 0.67)"; - - // 2. The Big Swap at 5 clicks - if (heartClickCount === 5 && !phaserStarted) { - phaserStarted = true; - - // Visual "Pop" effect - heart.innerHTML = "๐ŸŽฎ"; // Swap to gamer emoji - heart.style.transform = "scale(1.5)"; // Slight bounce - - // Give the user a split second to see the emoji before Phaser starts - setTimeout(() => { - // Optional: make the emoji float away or disappear - heart.classList.add("animate-ping"); // Uses Tailwind's built-in animation - - initPhaserGame(); - - // Optional: Hide the emoji after Phaser covers the screen - // setTimeout(() => { heart.style.opacity = '0'; }, 500); - }, 300); - } -}); /** * INITIALIZATION */