|
| 1 | +/** |
| 2 | + * eggs.js - The Space Invaders Easter Egg |
| 3 | + * Logic: 5 Heart Clicks -> Emoji Explosion -> 80s Space Invaders |
| 4 | + */ |
| 5 | + |
| 6 | +// 1. GLOBAL CONSTANTS & STATE |
| 7 | +const emojiBurst = [ |
| 8 | + "🎮", |
| 9 | + "🕹️", |
| 10 | + "👾", |
| 11 | + "🚀", |
| 12 | + "✨", |
| 13 | + "⭐", |
| 14 | + "🔥", |
| 15 | + "💥", |
| 16 | + "🌈", |
| 17 | + "🎉", |
| 18 | + "💖", |
| 19 | + "💎", |
| 20 | + "🤖", |
| 21 | + "👻", |
| 22 | + "🦄", |
| 23 | + "🍄", |
| 24 | + "🌍", |
| 25 | + "⚡", |
| 26 | + "🏆", |
| 27 | + "🎯", |
| 28 | + "🛸", |
| 29 | + "👽", |
| 30 | + "👾", |
| 31 | + "🐙", |
| 32 | + "🦖", |
| 33 | + "🪐", |
| 34 | + "🌌", |
| 35 | + "🌠", |
| 36 | + "☄️", |
| 37 | + "🌙", |
| 38 | +]; |
| 39 | + |
| 40 | +let heartClickCount = 0; |
| 41 | +let phaserStarted = false; |
| 42 | +let gameInstance; |
| 43 | +let player; |
| 44 | +let cursors; |
| 45 | +let aliens; |
| 46 | +let bullets; |
| 47 | +let lastFired = 0; |
| 48 | + |
| 49 | +// 2. DOM TRIGGER LOGIC (Integrate this with your footer heart) |
| 50 | +const heart = document.getElementById("footer-heart"); |
| 51 | + |
| 52 | +if (heart) { |
| 53 | + heart.style.cursor = "pointer"; |
| 54 | + heart.style.display = "inline-block"; // Necessary for transforms |
| 55 | + |
| 56 | + heart.addEventListener("click", () => { |
| 57 | + if (phaserStarted) return; |
| 58 | + |
| 59 | + heartClickCount++; |
| 60 | + |
| 61 | + // Visual feedback: Heart grows |
| 62 | + const scaleAmount = 1 + heartClickCount * 0.3; |
| 63 | + heart.style.transition = |
| 64 | + "transform 0.1s cubic-bezier(0.17, 0.67, 0.83, 0.67)"; |
| 65 | + heart.style.transform = `scale(${scaleAmount})`; |
| 66 | + |
| 67 | + if (heartClickCount === 5) { |
| 68 | + phaserStarted = true; |
| 69 | + heart.innerHTML = "🎮"; // Swap to gamer emoji |
| 70 | + heart.style.transform = "scale(1.5)"; |
| 71 | + |
| 72 | + setTimeout(() => { |
| 73 | + heart.style.opacity = "0"; // Fade out the heart |
| 74 | + initPhaserGame(); |
| 75 | + }, 300); |
| 76 | + } |
| 77 | + }); |
| 78 | +} |
| 79 | + |
| 80 | +// 3. PHASER ENGINE INITIALIZATION |
1 | 81 | function initPhaserGame() { |
| 82 | + // Create dedicated Canvas |
| 83 | + const canvas = document.createElement("canvas"); |
| 84 | + canvas.id = "phaser-game-canvas"; |
| 85 | + Object.assign(canvas.style, { |
| 86 | + position: "fixed", |
| 87 | + top: "0", |
| 88 | + left: "0", |
| 89 | + width: "100vw", |
| 90 | + height: "100vh", |
| 91 | + zIndex: "10000", |
| 92 | + pointerEvents: "none", // Start as click-through |
| 93 | + }); |
| 94 | + document.body.appendChild(canvas); |
| 95 | + |
2 | 96 | const config = { |
3 | | - type: Phaser.AUTO, |
| 97 | + type: Phaser.CANVAS, |
| 98 | + canvas: canvas, |
4 | 99 | width: window.innerWidth, |
5 | 100 | height: window.innerHeight, |
6 | | - parent: "phaser-container", // Make sure this div exists in your HTML |
7 | 101 | transparent: true, |
8 | 102 | physics: { |
9 | 103 | default: "arcade", |
10 | | - arcade: { gravity: { y: 300 } }, |
| 104 | + arcade: { gravity: { y: 0 }, debug: false }, |
11 | 105 | }, |
12 | 106 | scene: { |
13 | 107 | preload: preload, |
14 | 108 | create: create, |
| 109 | + update: update, |
15 | 110 | }, |
16 | 111 | }; |
17 | 112 |
|
18 | | - const game = new Phaser.Game(config); |
| 113 | + gameInstance = new Phaser.Game(config); |
19 | 114 | } |
20 | 115 |
|
| 116 | +// 4. PHASER SCENE FUNCTIONS |
21 | 117 | function preload() { |
22 | | - // No need to preload images if we are only using text/emojis! |
| 118 | + // No assets to load - we use emojis! |
23 | 119 | } |
24 | 120 |
|
25 | 121 | function create() { |
26 | | - const emojis = [ |
27 | | - // Gaming & Tech |
28 | | - "🎮", |
29 | | - "🕹️", |
30 | | - "👾", |
31 | | - "🚀", |
32 | | - "💻", |
33 | | - "📱", |
34 | | - "⌨️", |
35 | | - "🖱️", |
36 | | - "🔋", |
37 | | - "🔌", |
38 | | - // Magic & Space |
39 | | - "✨", |
40 | | - "⭐", |
41 | | - "🌟", |
42 | | - "🔮", |
43 | | - "🌌", |
44 | | - "🌠", |
45 | | - "🌙", |
46 | | - "☄️", |
47 | | - "🛸", |
48 | | - "👽", |
49 | | - // Action & Fun |
50 | | - "🔥", |
51 | | - "💥", |
52 | | - "🧨", |
53 | | - "⚡", |
54 | | - "🌈", |
55 | | - "🎉", |
56 | | - "🎊", |
57 | | - "🎈", |
58 | | - "🎁", |
59 | | - "💎", |
60 | | - // Hearts & Expressions |
61 | | - "💖", |
62 | | - "🎯", |
63 | | - "🏆", |
64 | | - "🥇", |
65 | | - "🧿", |
66 | | - "🍀", |
67 | | - "🍕", |
68 | | - "🍭", |
69 | | - "🍦", |
70 | | - "🍩", |
71 | | - // Creatures & Icons |
72 | | - "🤖", |
73 | | - "👻", |
74 | | - "🐲", |
75 | | - "🦄", |
76 | | - "🦊", |
77 | | - "🐱", |
78 | | - "🐧", |
79 | | - "🦖", |
80 | | - "🍄", |
81 | | - "🌍", |
82 | | - ]; |
| 122 | + const particles = spawnExplosion(this); |
| 123 | + |
| 124 | + // After 5 seconds, clear explosion and start the real game |
| 125 | + this.time.delayedCall(5000, () => { |
| 126 | + this.tweens.add({ |
| 127 | + targets: particles.getChildren(), |
| 128 | + alpha: 0, |
| 129 | + duration: 1000, |
| 130 | + onComplete: () => { |
| 131 | + particles.clear(true, true); |
| 132 | + |
| 133 | + // Make the game interactive |
| 134 | + const canvas = document.getElementById("phaser-game-canvas"); |
| 135 | + if (canvas) canvas.style.pointerEvents = "auto"; |
| 136 | + |
| 137 | + setupSpaceInvaders.call(this); |
| 138 | + }, |
| 139 | + }); |
| 140 | + }); |
| 141 | +} |
| 142 | + |
| 143 | +function update() { |
| 144 | + if (!player || !player.body) return; |
| 145 | + |
| 146 | + // Movement |
| 147 | + if (cursors.left.isDown) { |
| 148 | + player.body.setVelocityX(-400); |
| 149 | + } else if (cursors.right.isDown) { |
| 150 | + player.body.setVelocityX(400); |
| 151 | + } else { |
| 152 | + player.body.setVelocityX(0); |
| 153 | + } |
| 154 | + |
| 155 | + // Shooting |
| 156 | + if (cursors.space.isDown) { |
| 157 | + fireBullet(this); |
| 158 | + } |
| 159 | +} |
| 160 | + |
| 161 | +// 5. HELPER FUNCTIONS (The Mechanics) |
| 162 | + |
| 163 | +function spawnExplosion(scene) { |
83 | 164 | const heartRect = document |
84 | 165 | .getElementById("footer-heart") |
85 | 166 | .getBoundingClientRect(); |
| 167 | + const particles = scene.add.group(); |
86 | 168 |
|
87 | | - for (let i = 0; i < 75; i++) { |
88 | | - // 1. Pick a random emoji |
89 | | - const randomEmoji = emojis[Math.floor(Math.random() * emojis.length)]; |
90 | | - |
91 | | - // 2. Create the emoji at the heart's location |
92 | | - // We use this.add.text instead of this.physics.add.image |
93 | | - const particle = this.add.text(heartRect.left, heartRect.top, randomEmoji, { |
| 169 | + for (let i = 0; i < 40; i++) { |
| 170 | + const emoji = Phaser.Utils.Array.GetRandom(emojiBurst); |
| 171 | + const p = scene.add.text(heartRect.left, heartRect.top, emoji, { |
94 | 172 | fontSize: "32px", |
95 | 173 | }); |
96 | 174 |
|
97 | | - // 3. Manually add physics to the text object |
98 | | - this.physics.add.existing(particle); |
99 | | - |
100 | | - // 4. Apply the "Explosion" physics |
101 | | - // Shoots them out in a cone shape upward |
102 | | - particle.body.setVelocity( |
103 | | - Phaser.Math.Between(-300, 300), |
104 | | - Phaser.Math.Between(-500, -1000), |
| 175 | + scene.physics.add.existing(p); |
| 176 | + p.body.setVelocity( |
| 177 | + Phaser.Math.Between(-400, 400), |
| 178 | + Phaser.Math.Between(-600, -1200), |
105 | 179 | ); |
| 180 | + p.body.setBounce(0.6); |
| 181 | + p.body.setCollideWorldBounds(true); |
| 182 | + p.body.setAngularVelocity(Phaser.Math.Between(-200, 200)); |
| 183 | + |
| 184 | + particles.add(p); |
| 185 | + } |
| 186 | + return particles; |
| 187 | +} |
| 188 | + |
| 189 | +function setupSpaceInvaders() { |
| 190 | + const scene = this; |
| 191 | + |
| 192 | + // Player Rocket |
| 193 | + player = scene.add.text( |
| 194 | + window.innerWidth / 2, |
| 195 | + window.innerHeight - 80, |
| 196 | + "🚀", |
| 197 | + { fontSize: "50px" }, |
| 198 | + ); |
| 199 | + scene.physics.add.existing(player); |
| 200 | + player.body.setCollideWorldBounds(true); |
| 201 | + |
| 202 | + // Bullets |
| 203 | + bullets = scene.physics.add.group(); |
| 204 | + |
| 205 | + // Aliens Grid - Adjusted for smaller size |
| 206 | + aliens = scene.physics.add.group(); |
| 207 | + const rows = 5; |
| 208 | + const cols = 10; |
| 209 | + const spacingX = 50; // Tighter horizontal spacing |
| 210 | + const spacingY = 45; // Tighter vertical spacing |
| 211 | + |
| 212 | + for (let y = 0; y < rows; y++) { |
| 213 | + for (let x = 0; x < cols; x++) { |
| 214 | + let alienEmoji = ["👾", "👽", "🛸", "🐙", "👾"][y]; |
| 215 | + // Shrink from 35px to 24px |
| 216 | + let alien = scene.add.text( |
| 217 | + x * spacingX + 80, |
| 218 | + y * spacingY + 80, |
| 219 | + alienEmoji, |
| 220 | + { fontSize: "24px" }, |
| 221 | + ); |
| 222 | + |
| 223 | + scene.physics.add.existing(alien); |
| 224 | + alien.body.setAllowGravity(false); |
| 225 | + // Shrink the collision box to match the smaller emoji |
| 226 | + alien.body.setSize(24, 24); |
| 227 | + |
| 228 | + aliens.add(alien); |
| 229 | + } |
| 230 | + } |
| 231 | + |
| 232 | + // Alien Movement Timer |
| 233 | + scene.alienDirection = 1; |
| 234 | + scene.time.addEvent({ |
| 235 | + delay: 800, |
| 236 | + callback: moveAliens, |
| 237 | + callbackScope: scene, |
| 238 | + loop: true, |
| 239 | + }); |
106 | 240 |
|
107 | | - particle.body.setCollideWorldBounds(true); |
108 | | - particle.body.setBounce(0.7); |
| 241 | + // Collisions |
| 242 | + scene.physics.add.overlap(bullets, aliens, (bullet, alien) => { |
| 243 | + bullet.destroy(); |
| 244 | + alien.destroy(); |
| 245 | + if (aliens.countActive() === 0) { |
| 246 | + alert("INVADERS REPELLED! YOU WIN!"); |
| 247 | + window.location.reload(); |
| 248 | + } |
| 249 | + }); |
109 | 250 |
|
110 | | - // Optional: Add a little random rotation for flair |
111 | | - particle.setAngle(Phaser.Math.Between(0, 360)); |
| 251 | + cursors = scene.input.keyboard.createCursorKeys(); |
| 252 | +} |
| 253 | + |
| 254 | +function moveAliens() { |
| 255 | + let hitEdge = false; |
| 256 | + const padding = 60; |
| 257 | + const children = aliens.getChildren(); |
| 258 | + |
| 259 | + children.forEach((alien) => { |
| 260 | + if (this.alienDirection === 1 && alien.x > window.innerWidth - padding) |
| 261 | + hitEdge = true; |
| 262 | + if (this.alienDirection === -1 && alien.x < padding) hitEdge = true; |
| 263 | + }); |
| 264 | + |
| 265 | + if (hitEdge) { |
| 266 | + this.alienDirection *= -1; |
| 267 | + children.forEach((alien) => { |
| 268 | + alien.y += 40; |
| 269 | + alien.x += this.alienDirection * 10; |
| 270 | + }); |
| 271 | + } else { |
| 272 | + children.forEach((alien) => { |
| 273 | + alien.x += 25 * this.alienDirection; |
| 274 | + }); |
112 | 275 | } |
113 | 276 | } |
| 277 | +function fireBullet(scene) { |
| 278 | + const now = scene.time.now; |
| 279 | + if (now - lastFired < 400) return; |
| 280 | + |
| 281 | + // 1. Create the bullet slightly above the player's center |
| 282 | + const bullet = scene.add.text( |
| 283 | + player.x + player.width / 2 - 10, |
| 284 | + player.y - 20, |
| 285 | + "🔥", |
| 286 | + { |
| 287 | + fontSize: "20px", |
| 288 | + }, |
| 289 | + ); |
| 290 | + |
| 291 | + // 2. Add to physics and the group |
| 292 | + scene.physics.add.existing(bullet); |
| 293 | + bullets.add(bullet); |
| 294 | + |
| 295 | + // 3. FAIL-SAFES |
| 296 | + bullet.body.setAllowGravity(false); // Ensure gravity isn't pulling it down |
| 297 | + bullet.body.setImmovable(false); // Ensure it's allowed to move |
| 298 | + bullet.body.setVelocityY(-600); // Set the upward speed |
| 299 | + |
| 300 | + // 4. Force a sync between the physics body and the Text object |
| 301 | + bullet.body.isCircle = true; // Often helps with collision detection for small objects |
| 302 | + |
| 303 | + lastFired = now; |
| 304 | +} |
0 commit comments