at main 12 kB view raw
1import { crew } from "@kaplayjs/crew"; 2import kaplay from "kaplay"; 3 4import player from "./player"; 5import { makeEnemy } from "./enemy"; 6import confettiPlugin from "./confetti"; 7import setupSoundSystem from "./sound"; 8 9const k = kaplay({ plugins: [crew] }); 10k.loadRoot("./"); // A good idea for Itch.io publishing later 11k.loadCrew("sprite", "glady-o"); 12k.loadCrew("sprite", "sword-o"); 13k.loadCrew("sprite", "bean"); // Using bean sprite for enemies 14 15// Add confetti plugin to the game context 16const confetti = confettiPlugin(k); 17k.addConfetti = confetti.addConfetti; 18 19// Setup sound system 20const sound = setupSoundSystem(k); 21sound.preloadSounds(); 22 23// Make sound system globally available 24(window as any).gameSound = sound; 25 26// Game state 27let gameActive = true; 28let finalScore = 0; 29 30// Define game scenes 31k.scene("main", () => { 32 // Reset game state 33 gameActive = true; 34 35 // Start background music 36 sound.playRandomMusic(); 37 38 k.setGravity(1600); 39 40 // Create ground 41 let ground = k.add([ 42 k.rect(k.width(), 48), 43 k.pos(0, k.height() - 48), 44 k.outline(4), 45 k.area(), 46 k.body({ isStatic: true }), 47 k.color(127, 200, 255), 48 ]); 49 50 // Create walls around the edge of the map 51 // Left wall 52 let leftWall = k.add([ 53 k.rect(20, k.height()), 54 k.pos(-20, 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 ]); 61 62 // Right wall 63 let rightWall = k.add([ 64 k.rect(20, k.height()), 65 k.pos(k.width(), 0), 66 k.outline(4), 67 k.area(), 68 k.body({ isStatic: true }), 69 k.color(127, 200, 255), 70 k.opacity(0.5), 71 ]); 72 73 // Top wall 74 let topWall = k.add([ 75 k.rect(k.width(), 20), 76 k.pos(0, -20), 77 k.outline(4), 78 k.area(), 79 k.body({ isStatic: true }), 80 k.color(127, 200, 255), 81 k.opacity(0.5), 82 ]); 83 84 // Handle window resize 85 k.onResize(() => { 86 // Update ground 87 if (ground.exists()) { 88 ground.width = k.width(); 89 ground.pos.y = k.height() - 48; 90 } 91 92 // Update left wall 93 if (leftWall.exists()) { 94 leftWall.height = k.height(); 95 } 96 97 // Update right wall 98 if (rightWall.exists()) { 99 rightWall.height = k.height(); 100 rightWall.pos.x = k.width(); 101 } 102 103 // Update top wall 104 if (topWall.exists()) { 105 topWall.width = k.width(); 106 } 107 }); 108 109 // Create player object with components 110 const playerObj = k.add([ 111 k.pos(120, 500), 112 k.sprite("glady-o"), 113 k.body(), 114 k.area(), 115 player(k), 116 "player", // Add tag for collision detection 117 ]); 118 119 // Enemy spawning variables 120 let enemies: any[] = []; 121 let initialMaxEnemies = 5; 122 let maxEnemies = initialMaxEnemies; 123 let initialSpawnInterval = 3; // seconds 124 let spawnInterval = initialSpawnInterval; 125 let gameTime = 0; // Track game time in seconds 126 let difficultyLevel = 1; 127 let score = 0; 128 129 const scoreText = k.add([k.text(`Score: ${score}`), k.pos(16, 16), "score"]); 130 131 // Add a hidden score tracker that can be accessed by other components 132 const scoreTracker = k.add([ 133 k.pos(0, 0), 134 "game-score-tracker", 135 { 136 score: score, 137 difficultyLevel: difficultyLevel, 138 updateScore(newScore: number) { 139 this.score = newScore; 140 score = newScore; // Update the main score variable 141 }, 142 updateDifficulty(newLevel: number) { 143 this.difficultyLevel = newLevel; 144 }, 145 }, 146 ]); 147 148 // Difficulty scaling 149 function updateDifficulty() { 150 if (!gameActive) return; 151 152 gameTime += 1; // Increment game time by 1 second 153 154 // Check if it's time to increase difficulty based on score 155 // Use a formula that scales with difficulty level 156 const scoreThreshold = 50 * difficultyLevel; 157 158 if (score >= scoreThreshold && score % scoreThreshold < 10) { 159 // Only trigger once when crossing the threshold 160 if (!k.get("level-up-text").length) { 161 difficultyLevel += 1; 162 163 // Play level up sound effect 164 sound.playSfx("levelUp"); 165 166 // Update difficulty in tracker 167 const tracker = k.get("game-score-tracker")[0]; 168 if (tracker) { 169 tracker.updateDifficulty(difficultyLevel); 170 } 171 172 // Increase max enemies (cap at 15) 173 maxEnemies = Math.min(initialMaxEnemies + difficultyLevel, 15); 174 175 // Decrease spawn interval (minimum 0.5 seconds) 176 spawnInterval = Math.max( 177 initialSpawnInterval - difficultyLevel * 0.2, 178 0.5, 179 ); 180 181 console.log( 182 `Difficulty increased to level ${difficultyLevel}. Max enemies: ${maxEnemies}, Spawn interval: ${spawnInterval}s`, 183 ); 184 185 // Cancel previous spawn loop and start a new one with updated interval 186 k.loop(spawnInterval, spawnEnemy); 187 188 // Visual feedback for difficulty increase 189 const screenCenter = k.vec2(k.width() / 2, k.height() / 2); 190 if (k.addConfetti) { 191 k.addConfetti(screenCenter); 192 } 193 194 // Add difficulty level text 195 const levelText = k.add([ 196 k.text(`Difficulty Level ${difficultyLevel}!`, { size: 32 }), 197 k.pos(screenCenter), 198 k.anchor("center"), 199 k.color(255, 255, 255), 200 k.outline(2, k.rgb(0, 0, 0)), 201 k.z(100), 202 k.opacity(1), 203 "level-up-text", 204 ]); 205 206 // Fade out and destroy the text 207 k.tween( 208 1, 209 0, 210 2, 211 (v) => { 212 if (levelText.exists()) { 213 levelText.opacity = v; 214 } 215 }, 216 k.easings.easeInQuad, 217 ); 218 219 k.wait(2, () => { 220 if (levelText.exists()) levelText.destroy(); 221 }); 222 } 223 } 224 } 225 226 // Start difficulty scaling 227 k.loop(1, updateDifficulty); 228 229 // Spawn an enemy at a random position 230 function spawnEnemy() { 231 if (!gameActive) return; 232 233 // Don't spawn if we already have max enemies 234 if (enemies.length >= maxEnemies) return; 235 236 // Random position at the edges of the screen 237 // As difficulty increases, add chance to spawn in center 238 let spawnSide; 239 240 // Calculate center spawn chance based on difficulty level 241 // 0% at level 1-2, increasing to 30% at level 10+ 242 const centerSpawnChance = 243 difficultyLevel <= 2 ? 0 : Math.min((difficultyLevel - 2) * 0.04, 0.3); 244 245 // Determine spawn location 246 if (Math.random() < centerSpawnChance) { 247 // Center spawn 248 spawnSide = 2; // Center 249 } else { 250 // Side spawn (left or right) 251 spawnSide = Math.floor(Math.random() * 2); // 0: left, 1: right 252 } 253 254 let x = 0, 255 y = 0; 256 257 switch (spawnSide) { 258 case 0: // left 259 x = 10; // Just inside the left wall 260 y = Math.random() * (k.height() - 48 - 20) + 20; // Avoid spawning behind top wall or inside ground 261 break; 262 case 1: // right 263 x = k.width() - 10; // Just inside the right wall 264 y = Math.random() * (k.height() - 48 - 20) + 20; // Avoid spawning behind top wall or inside ground 265 break; 266 case 2: // center (mid-air) 267 // Random position in the middle area of the screen 268 x = k.width() * (0.3 + Math.random() * 0.4); // 30-70% of screen width 269 y = k.height() * (0.2 + Math.random() * 0.5); // 20-70% of screen height 270 break; 271 } 272 273 // Create enemy using the makeEnemy function 274 const newEnemy = makeEnemy(k, playerObj, x, y); 275 enemies.push(newEnemy); 276 277 // Remove from array when destroyed 278 newEnemy.on("destroy", () => { 279 enemies = enemies.filter((e) => e !== newEnemy); 280 281 // Increase score when enemy is destroyed 282 const pointsEarned = Math.round(10 + Math.pow(difficultyLevel, 0.75)); 283 score += pointsEarned; 284 285 // Update score display 286 scoreText.text = `Score: ${score}`; 287 288 // Update score tracker 289 const tracker = k.get("game-score-tracker")[0]; 290 if (tracker) { 291 tracker.score = score; 292 } 293 294 // Heal player when killing an enemy 295 const player = k.get("player")[0]; 296 if (player && player.heal) { 297 // Heal amount is 5 health points 298 const healAmount = 5; 299 player.heal(healAmount); 300 301 // Show healing effect 302 const healText = k.add([ 303 k.text(`+${healAmount} HP`, { size: 16 }), 304 k.pos(player.pos.x, player.pos.y - 60), 305 k.anchor("center"), 306 k.color(0, 255, 0), 307 k.z(100), 308 k.opacity(1), 309 ]); 310 311 // Float upward and fade out 312 k.tween( 313 1, 314 0, 315 0.8, 316 (v) => { 317 if (healText.exists()) { 318 healText.opacity = v; 319 healText.pos.y -= 0.5; 320 } 321 }, 322 k.easings.easeOutQuad, 323 ); 324 325 k.wait(0.8, () => { 326 if (healText.exists()) healText.destroy(); 327 }); 328 } 329 330 if (Math.random() < 0.2 * Math.pow(difficultyLevel, 0.75)) spawnEnemy(); 331 }); 332 } 333 334 // Start spawning enemies 335 k.loop(spawnInterval, spawnEnemy); 336 337 // Game loop 338 k.onUpdate(() => { 339 // Update enemy list (remove destroyed enemies) 340 enemies = enemies.filter((enemy) => enemy.exists()); 341 }); 342 343 // Listen for game over event 344 playerObj.on("death", () => { 345 gameActive = false; 346 347 // Get final score from tracker 348 const tracker = k.get("game-score-tracker")[0]; 349 if (tracker) { 350 finalScore = tracker.score; 351 } else { 352 finalScore = score; 353 } 354 355 // Wait a moment before showing game over screen 356 k.wait(2, () => { 357 k.go("gameOver", finalScore); 358 }); 359 }); 360}); 361 362// Game over scene 363k.scene("gameOver", (score: number) => { 364 // Background 365 let background = k.add([k.rect(k.width(), k.height()), k.color(0, 0, 0), k.opacity(0.7)]); 366 367 // Game over text 368 let gameOverText = k.add([ 369 k.text("GAME OVER", { size: 64 }), 370 k.pos(k.width() / 2, k.height() / 3), 371 k.anchor("center"), 372 k.color(255, 50, 50), 373 ]); 374 375 // Score display 376 let scoreDisplay = k.add([ 377 k.text(`Final Score: ${score}`, { size: 36 }), 378 k.pos(k.width() / 2, k.height() / 2), 379 k.anchor("center"), 380 k.color(255, 255, 255), 381 ]); 382 383 // Restart button 384 const restartBtn = k.add([ 385 k.rect(200, 60), 386 k.pos(k.width() / 2, (k.height() * 2) / 3), 387 k.anchor("center"), 388 k.color(50, 150, 50), 389 k.area(), 390 "restart-btn", 391 ]); 392 393 // Restart text 394 let restartText = k.add([ 395 k.text("RESTART", { size: 24 }), 396 k.pos(k.width() / 2, (k.height() * 2) / 3), 397 k.anchor("center"), 398 k.color(255, 255, 255), 399 ]); 400 401 // Handle window resize 402 k.onResize(() => { 403 // Update background 404 if (background.exists()) { 405 background.width = k.width(); 406 background.height = k.height(); 407 } 408 409 // Update game over text position 410 if (gameOverText.exists()) { 411 gameOverText.pos.x = k.width() / 2; 412 gameOverText.pos.y = k.height() / 3; 413 } 414 415 // Update score display position 416 if (scoreDisplay.exists()) { 417 scoreDisplay.pos.x = k.width() / 2; 418 scoreDisplay.pos.y = k.height() / 2; 419 } 420 421 // Update restart button position 422 if (restartBtn.exists()) { 423 restartBtn.pos.x = k.width() / 2; 424 restartBtn.pos.y = (k.height() * 2) / 3; 425 } 426 427 // Update restart text position 428 if (restartText.exists()) { 429 restartText.pos.x = k.width() / 2; 430 restartText.pos.y = (k.height() * 2) / 3; 431 } 432 }); 433 434 // Restart on button click 435 restartBtn.onClick(() => { 436 k.go("main"); 437 }); 438 439 // Restart on key press 440 k.onKeyPress("r", () => { 441 k.go("main"); 442 }); 443 444 // Restart on enter key 445 k.onKeyPress("enter", () => { 446 k.go("main"); 447 }); 448}); 449 450// Start the game 451k.go("main");