the game

feat: add ultimate ability and game over

dunkirk.sh 2cca584c 72a98fab

verified
+388 -158
+237 -150
src/main.ts
··· 15 15 const confetti = confettiPlugin(k); 16 16 k.addConfetti = confetti.addConfetti; 17 17 18 - k.setGravity(1600); 18 + // Game state 19 + let gameActive = true; 20 + let finalScore = 0; 19 21 20 - // Create ground 21 - const ground = k.add([ 22 - k.rect(k.width(), 48), 23 - k.pos(0, k.height() - 48), 24 - k.outline(4), 25 - k.area(), 26 - k.body({ isStatic: true }), 27 - k.color(127, 200, 255), 28 - ]); 22 + // Define game scenes 23 + k.scene("main", () => { 24 + // Reset game state 25 + gameActive = true; 29 26 30 - // Create walls around the edge of the map 31 - // Left wall 32 - const leftWall = k.add([ 33 - k.rect(20, k.height()), 34 - k.pos(-20, 0), 35 - k.outline(4), 36 - k.area(), 37 - k.body({ isStatic: true }), 38 - k.color(127, 200, 255), 39 - k.opacity(0.5), 40 - ]); 27 + k.setGravity(1600); 41 28 42 - // Right wall 43 - const rightWall = k.add([ 44 - k.rect(20, k.height()), 45 - k.pos(k.width(), 0), 46 - k.outline(4), 47 - k.area(), 48 - k.body({ isStatic: true }), 49 - k.color(127, 200, 255), 50 - k.opacity(0.5), 51 - ]); 29 + // Create ground 30 + const ground = k.add([ 31 + k.rect(k.width(), 48), 32 + k.pos(0, k.height() - 48), 33 + k.outline(4), 34 + k.area(), 35 + k.body({ isStatic: true }), 36 + k.color(127, 200, 255), 37 + ]); 52 38 53 - // Top wall 54 - const topWall = k.add([ 55 - k.rect(k.width(), 20), 56 - k.pos(0, -20), 57 - k.outline(4), 58 - k.area(), 59 - k.body({ isStatic: true }), 60 - k.color(127, 200, 255), 61 - k.opacity(0.5), 62 - ]); 39 + // Create walls around the edge of the map 40 + // Left wall 41 + const leftWall = k.add([ 42 + k.rect(20, k.height()), 43 + k.pos(-20, 0), 44 + k.outline(4), 45 + k.area(), 46 + k.body({ isStatic: true }), 47 + k.color(127, 200, 255), 48 + k.opacity(0.5), 49 + ]); 63 50 64 - // Create player object with components 65 - const playerObj = k.add([ 66 - k.pos(120, 500), 67 - k.sprite("glady-o"), 68 - k.body(), 69 - k.area(), 70 - player(k), 71 - "player", // Add tag for collision detection 72 - ]); 51 + // Right wall 52 + const rightWall = k.add([ 53 + k.rect(20, k.height()), 54 + k.pos(k.width(), 0), 55 + k.outline(4), 56 + k.area(), 57 + k.body({ isStatic: true }), 58 + k.color(127, 200, 255), 59 + k.opacity(0.5), 60 + ]); 73 61 74 - // Enemy spawning variables 75 - let enemies: any[] = []; 76 - let initialMaxEnemies = 5; 77 - let maxEnemies = initialMaxEnemies; 78 - let initialSpawnInterval = 3; // seconds 79 - let spawnInterval = initialSpawnInterval; 80 - let gameTime = 0; // Track game time in seconds 81 - let difficultyLevel = 1; 82 - let score = 0; 62 + // Top wall 63 + const topWall = k.add([ 64 + k.rect(k.width(), 20), 65 + k.pos(0, -20), 66 + k.outline(4), 67 + k.area(), 68 + k.body({ isStatic: true }), 69 + k.color(127, 200, 255), 70 + k.opacity(0.5), 71 + ]); 83 72 84 - const scoreText = k.add([k.text(`Score: ${score}`), k.pos(16, 16)]); 73 + // Create player object with components 74 + const playerObj = k.add([ 75 + k.pos(120, 500), 76 + k.sprite("glady-o"), 77 + k.body(), 78 + k.area(), 79 + player(k), 80 + "player", // Add tag for collision detection 81 + ]); 82 + 83 + // Enemy spawning variables 84 + let enemies: any[] = []; 85 + let initialMaxEnemies = 5; 86 + let maxEnemies = initialMaxEnemies; 87 + let initialSpawnInterval = 3; // seconds 88 + let spawnInterval = initialSpawnInterval; 89 + let gameTime = 0; // Track game time in seconds 90 + let difficultyLevel = 1; 91 + let score = 0; 92 + 93 + const scoreText = k.add([k.text(`Score: ${score}`), k.pos(16, 16), "score"]); 94 + 95 + // Difficulty scaling 96 + function updateDifficulty() { 97 + if (!gameActive) return; 98 + 99 + gameTime += 1; // Increment game time by 1 second 100 + 101 + // Every 30 seconds, increase difficulty 102 + if (score != 0 && score % (50 + 5 * difficultyLevel) === 0) { 103 + difficultyLevel += 1; 104 + 105 + // Increase max enemies (cap at 15) 106 + maxEnemies = Math.min(initialMaxEnemies + difficultyLevel * 3, 15); 107 + 108 + // Decrease spawn interval (minimum 0.5 seconds) 109 + spawnInterval = Math.max( 110 + initialSpawnInterval - difficultyLevel * 0.3, 111 + 0.5, 112 + ); 85 113 86 - // Difficulty scaling 87 - function updateDifficulty() { 88 - gameTime += 1; // Increment game time by 1 second 114 + console.log( 115 + `Difficulty increased to level ${difficultyLevel}. Max enemies: ${maxEnemies}, Spawn interval: ${spawnInterval}s`, 116 + ); 89 117 90 - // Every 30 seconds, increase difficulty 91 - if (score != 0 && score % (50 + 5 * difficultyLevel) === 0) { 92 - difficultyLevel += 1; 118 + // Cancel previous spawn loop and start a new one with updated interval 119 + k.cancel(); 120 + k.loop(spawnInterval, spawnEnemy); 121 + 122 + // Visual feedback for difficulty increase 123 + const screenCenter = k.vec2(k.width() / 2, k.height() / 2); 124 + if (k.addConfetti) { 125 + k.addConfetti(screenCenter); 126 + } 127 + 128 + // Add difficulty level text 129 + const levelText = k.add([ 130 + k.text(`Difficulty Level ${difficultyLevel}!`, { size: 32 }), 131 + k.pos(screenCenter), 132 + k.anchor("center"), 133 + k.color(255, 255, 255), 134 + k.outline(2, k.rgb(0, 0, 0)), 135 + k.z(100), 136 + k.opacity(1), 137 + ]); 138 + 139 + // Fade out and destroy the text 140 + k.tween( 141 + 1, 142 + 0, 143 + 2, 144 + (v) => { 145 + levelText.opacity = v; 146 + }, 147 + k.easings.easeInQuad, 148 + ); 149 + 150 + k.wait(2, () => { 151 + levelText.destroy(); 152 + }); 153 + } 154 + } 93 155 94 - // Increase max enemies (cap at 15) 95 - maxEnemies = Math.min(initialMaxEnemies + difficultyLevel * 3, 15); 156 + // Start difficulty scaling 157 + k.loop(1, updateDifficulty); 96 158 97 - // Decrease spawn interval (minimum 0.5 seconds) 98 - spawnInterval = Math.max(initialSpawnInterval - difficultyLevel * 0.3, 0.5); 159 + // Spawn an enemy at a random position 160 + function spawnEnemy() { 161 + if (!gameActive) return; 99 162 100 - console.log( 101 - `Difficulty increased to level ${difficultyLevel}. Max enemies: ${maxEnemies}, Spawn interval: ${spawnInterval}s`, 102 - ); 163 + // Don't spawn if we already have max enemies 164 + if (enemies.length >= maxEnemies) return; 103 165 104 - // Cancel previous spawn loop and start a new one with updated interval 105 - k.cancel(); 106 - k.loop(spawnInterval, spawnEnemy); 166 + // Random position at the edges of the screen 167 + const side = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left 168 + let x = 0, 169 + y = 0; 107 170 108 - // Visual feedback for difficulty increase 109 - const screenCenter = k.vec2(k.width() / 2, k.height() / 2); 110 - if (k.addConfetti) { 111 - k.addConfetti(screenCenter); 171 + switch (side) { 172 + case 0: // top 173 + x = Math.random() * (k.width() - 40) + 20; // Avoid spawning behind side walls 174 + y = 10; // Just inside the top wall 175 + break; 176 + case 1: // right 177 + x = k.width() - 10; // Just inside the right wall 178 + y = Math.random() * (k.height() - 48 - 20) + 20; // Avoid spawning behind top wall or inside ground 179 + break; 180 + case 2: // bottom 181 + x = Math.random() * (k.width() - 40) + 20; // Avoid spawning behind side walls 182 + y = k.height() - 58; // Just above the ground (ground is at height-48 with height 48) 183 + break; 184 + case 3: // left 185 + x = 10; // Just inside the left wall 186 + y = Math.random() * (k.height() - 48 - 20) + 20; // Avoid spawning behind top wall or inside ground 187 + break; 112 188 } 113 189 114 - // Add difficulty level text 115 - const levelText = k.add([ 116 - k.text(`Difficulty Level ${difficultyLevel}!`, { size: 32 }), 117 - k.pos(screenCenter), 118 - k.anchor("center"), 119 - k.color(255, 255, 255), 120 - k.outline(2, k.rgb(0, 0, 0)), 121 - k.z(100), 122 - k.opacity(1), 123 - ]); 190 + // Create enemy using the makeEnemy function 191 + const newEnemy = makeEnemy(k, playerObj, x, y); 192 + enemies.push(newEnemy); 193 + 194 + // Remove from array when destroyed 195 + newEnemy.on("destroy", () => { 196 + enemies = enemies.filter((e) => e !== newEnemy); 124 197 125 - // Fade out and destroy the text 126 - k.tween( 127 - 1, 128 - 0, 129 - 2, 130 - (v) => { 131 - levelText.opacity = v; 132 - }, 133 - k.easings.easeInQuad, 134 - ); 198 + // Increase score when enemy is destroyed 199 + score += Math.round(10 + Math.pow(difficultyLevel, 0.75)); 135 200 136 - k.wait(2, () => { 137 - levelText.destroy(); 201 + // Update score display 202 + scoreText.text = `Score: ${score}`; 203 + 204 + if (Math.random() < 0.5) spawnEnemy(); 138 205 }); 139 206 } 140 - } 141 207 142 - // Start difficulty scaling 143 - k.loop(1, updateDifficulty); 208 + // Start spawning enemies 209 + k.loop(spawnInterval, spawnEnemy); 144 210 145 - // Spawn an enemy at a random position 146 - function spawnEnemy() { 147 - // Don't spawn if we already have max enemies 148 - if (enemies.length >= maxEnemies) return; 211 + // Game loop 212 + k.onUpdate(() => { 213 + // Update enemy list (remove destroyed enemies) 214 + enemies = enemies.filter((enemy) => enemy.exists()); 215 + }); 216 + 217 + // Listen for game over event 218 + playerObj.on("death", () => { 219 + gameActive = false; 220 + finalScore = score; 221 + 222 + // Stop enemy spawning 223 + k.cancel("spawnEnemy"); 149 224 150 - // Random position at the edges of the screen 151 - const side = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left 152 - let x = 0, 153 - y = 0; 225 + // Wait a moment before showing game over screen 226 + k.wait(2, () => { 227 + k.go("gameOver", finalScore); 228 + }); 229 + }); 230 + }); 154 231 155 - switch (side) { 156 - case 0: // top 157 - x = Math.random() * (k.width() - 40) + 20; // Avoid spawning behind side walls 158 - y = 10; // Just inside the top wall 159 - break; 160 - case 1: // right 161 - x = k.width() - 10; // Just inside the right wall 162 - y = Math.random() * (k.height() - 48 - 20) + 20; // Avoid spawning behind top wall or inside ground 163 - break; 164 - case 2: // bottom 165 - x = Math.random() * (k.width() - 40) + 20; // Avoid spawning behind side walls 166 - y = k.height() - 58; // Just above the ground (ground is at height-48 with height 48) 167 - break; 168 - case 3: // left 169 - x = 10; // Just inside the left wall 170 - y = Math.random() * (k.height() - 48 - 20) + 20; // Avoid spawning behind top wall or inside ground 171 - break; 172 - } 232 + // Game over scene 233 + k.scene("gameOver", (score: number) => { 234 + // Background 235 + k.add([k.rect(k.width(), k.height()), k.color(0, 0, 0), k.opacity(0.7)]); 173 236 174 - // Create enemy using the makeEnemy function 175 - const newEnemy = makeEnemy(k, playerObj, x, y); 176 - enemies.push(newEnemy); 237 + // Game over text 238 + k.add([ 239 + k.text("GAME OVER", { size: 64 }), 240 + k.pos(k.width() / 2, k.height() / 3), 241 + k.anchor("center"), 242 + k.color(255, 50, 50), 243 + ]); 177 244 178 - // Remove from array when destroyed 179 - newEnemy.on("destroy", () => { 180 - enemies = enemies.filter((e) => e !== newEnemy); 245 + // Score display 246 + k.add([ 247 + k.text(`Final Score: ${score}`, { size: 36 }), 248 + k.pos(k.width() / 2, k.height() / 2), 249 + k.anchor("center"), 250 + k.color(255, 255, 255), 251 + ]); 181 252 182 - // Increase score when enemy is destroyed 183 - score += Math.round(10 + Math.pow(difficultyLevel, 0.75)); 253 + // Restart button 254 + const restartBtn = k.add([ 255 + k.rect(200, 60), 256 + k.pos(k.width() / 2, (k.height() * 2) / 3), 257 + k.anchor("center"), 258 + k.color(50, 150, 50), 259 + k.area(), 260 + "restart-btn", 261 + ]); 184 262 185 - // Update score display 186 - scoreText.text = `Score: ${score}`; 263 + // Restart text 264 + k.add([ 265 + k.text("RESTART", { size: 24 }), 266 + k.pos(k.width() / 2, (k.height() * 2) / 3), 267 + k.anchor("center"), 268 + k.color(255, 255, 255), 269 + ]); 187 270 188 - if (Math.random() < 0.5) spawnEnemy(); 271 + // Restart on button click 272 + restartBtn.onClick(() => { 273 + k.go("main"); 189 274 }); 190 - } 191 275 192 - // Start spawning enemies 193 - k.loop(spawnInterval, spawnEnemy); 276 + // Restart on key press 277 + k.onKeyPress("r", () => { 278 + k.go("main"); 279 + }); 194 280 195 - // Game loop 196 - k.onUpdate(() => { 197 - // Update enemy list (remove destroyed enemies) 198 - enemies = enemies.filter((enemy) => enemy.exists()); 281 + // Restart on enter key 282 + k.onKeyPress("enter", () => { 283 + k.go("main"); 284 + }); 199 285 }); 200 286 201 - console.log(typeof k); 287 + // Start the game 288 + k.go("main");
+151 -8
src/player.ts
··· 99 99 100 100 // Check if player is dead 101 101 if (health <= 0) { 102 - // Game over logic here 103 - k.addKaboom(this.pos); 102 + // Game over logic 103 + health = 0; // Ensure health doesn't go negative 104 + 105 + // Create dramatic death effect 106 + k.addKaboom(this.pos, { scale: 2 }); 104 107 k.shake(20); 108 + 109 + // Emit death event for game over handling 110 + this.trigger("death"); 105 111 } 106 112 }, 107 113 ··· 177 183 } 178 184 }); 179 185 180 - // Attack with X key 186 + // Attack with X key - now ultimate move 181 187 this.onKeyPress("x", () => { 182 - this.attack(); 188 + // Create visual effects for charging up 189 + const chargeEffect = k.add([ 190 + k.circle(50), 191 + k.pos(this.pos), 192 + k.color(255, 0, 0), 193 + k.opacity(0.5), 194 + k.anchor("center"), 195 + k.z(0.8), 196 + ]); 197 + 198 + // Grow the charge effect 199 + k.tween( 200 + 50, 201 + 200, 202 + 1.5, 203 + (v) => { 204 + if (chargeEffect.exists()) { 205 + chargeEffect.radius = v; 206 + chargeEffect.opacity = 0.5 + Math.sin(k.time() * 10) * 0.2; 207 + } 208 + }, 209 + k.easings.easeInQuad, 210 + ); 211 + 212 + // Add warning text 213 + const warningText = k.add([ 214 + k.text("ULTIMATE CHARGING...", { size: 24 }), 215 + k.pos(k.width() / 2, 100), 216 + k.color(255, 50, 50), 217 + k.anchor("center"), 218 + k.z(100), 219 + ]); 220 + 221 + // Flash the warning text 222 + k.loop(0.2, () => { 223 + if (warningText.exists()) { 224 + warningText.color = 225 + warningText.color === k.rgb(255, 50, 50) 226 + ? k.rgb(255, 100, 100) 227 + : k.rgb(255, 50, 50); 228 + } 229 + }); 230 + 231 + // After delay, trigger the ultimate explosion 232 + k.wait(2, () => { 233 + if (chargeEffect.exists()) chargeEffect.destroy(); 234 + if (warningText.exists()) warningText.destroy(); 235 + 236 + // Create massive explosion 237 + const explosionRadius = 500; // Much larger than normal explosions 238 + 239 + // Animate sword for dramatic effect 240 + if (sword) { 241 + // Make sword glow red 242 + sword.color = k.rgb(255, 0, 0); 243 + 244 + // Dramatic sword spin 245 + k.tween( 246 + 0, 247 + 720, 248 + 1, 249 + (v) => { 250 + if (sword) { 251 + sword.angle = v; 252 + sword.scaleTo(1 + Math.sin(k.time() * 10) * 0.3); 253 + } 254 + }, 255 + k.easings.easeInOutQuad, 256 + ); 257 + } 258 + 259 + // Visual effects 260 + k.addKaboom(this.pos, { 261 + scale: 5, 262 + }); 263 + 264 + // Add multiple explosion effects for dramatic impact 265 + for (let i = 0; i < 8; i++) { 266 + const angle = Math.PI * 2 * (i / 8); 267 + const offset = k.vec2(Math.cos(angle) * 100, Math.sin(angle) * 100); 268 + 269 + k.wait(i * 0.1, () => { 270 + k.addKaboom(k.vec2(this.pos).add(offset), { 271 + scale: 2 + Math.random() * 2, 272 + }); 273 + }); 274 + } 275 + 276 + // Heavy screen shake 277 + k.shake(40); 278 + 279 + // Create explosion area for damage 280 + const explosion = k.add([ 281 + k.circle(explosionRadius), 282 + k.pos(this.pos), 283 + k.color(255, 50, 50), 284 + k.area(), 285 + k.anchor("center"), 286 + k.opacity(0.6), 287 + "ultimate-explosion", 288 + ]); 289 + 290 + // Fade out explosion 291 + k.tween( 292 + 0.6, 293 + 0, 294 + 1.5, 295 + (v) => { 296 + if (explosion.exists()) { 297 + explosion.opacity = v; 298 + } 299 + }, 300 + k.easings.easeOutQuad, 301 + ); 302 + 303 + // Destroy explosion after animation 304 + k.wait(1.5, () => { 305 + if (explosion.exists()) explosion.destroy(); 306 + }); 307 + 308 + // Damage all enemies with high damage 309 + const enemies = k.get("enemy"); 310 + enemies.forEach((enemy) => { 311 + const dist = k.vec2(enemy.pos).dist(this.pos); 312 + if (dist < explosionRadius) { 313 + // Instant kill any enemy within the explosion radius 314 + (enemy as any).damage(1000); // Extremely high damage to ensure death 315 + 316 + // Add additional explosion effect at enemy position 317 + k.wait(Math.random() * 0.3, () => { 318 + k.addKaboom(enemy.pos, { 319 + scale: 1 + Math.random(), 320 + }); 321 + }); 322 + } 323 + }); 324 + 325 + // Kill the player (sacrifice) 326 + this.damage(health); // Use current health to ensure death 327 + }); 183 328 }); 184 329 185 330 // Attack, kaboom and shake on click ··· 427 572 428 573 // Apply scale 429 574 if (arrowPoints[i].scale) { 430 - arrowPoints[i].scale.x = size / 3; // Divide by default size (3) 431 - arrowPoints[i].scale.y = size / 3; 575 + arrowPoints[i].scaleTo(size / 3); // Divide by default size (3) 432 576 } 433 577 } 434 578 ··· 439 583 440 584 // Make arrow head larger 441 585 if (arrowHead.scale) { 442 - arrowHead.scale.x = 3; 443 - arrowHead.scale.y = 3; 586 + arrowHead.scaleTo(3); 444 587 } 445 588 } 446 589 },