1import { crew } from "@kaplayjs/crew"; 2import kaplay from "kaplay"; 3 4import player from "./player"; 5import { makeEnemy } from "./enemy"; 6import confettiPlugin from "./confetti"; 7 8const k = kaplay({ plugins: [crew] }); 9k.loadRoot("./"); // A good idea for Itch.io publishing later 10k.loadCrew("sprite", "glady-o"); 11k.loadCrew("sprite", "sword-o"); 12k.loadCrew("sprite", "bean"); // Using bean sprite for enemies 13 14// Add confetti plugin to the game context 15const confetti = confettiPlugin(k); 16k.addConfetti = confetti.addConfetti; 17 18// Game state 19let gameActive = true; 20let finalScore = 0; 21 22// Define game scenes 23k.scene("main", () => { 24 // Reset game state 25 gameActive = true; 26 27 k.setGravity(1600); 28 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 ]); 38 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 ]); 50 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 ]); 61 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 ]); 72 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 // Add a hidden score tracker that can be accessed by other components 96 const scoreTracker = k.add([ 97 k.pos(0, 0), 98 "game-score-tracker", 99 { 100 score: score, 101 difficultyLevel: difficultyLevel, 102 updateScore(newScore: number) { 103 this.score = newScore; 104 score = newScore; // Update the main score variable 105 }, 106 updateDifficulty(newLevel: number) { 107 this.difficultyLevel = newLevel; 108 }, 109 }, 110 ]); 111 112 // Difficulty scaling 113 function updateDifficulty() { 114 if (!gameActive) return; 115 116 gameTime += 1; // Increment game time by 1 second 117 118 // Check if it's time to increase difficulty based on score 119 // Use a formula that scales with difficulty level 120 const scoreThreshold = 50 * difficultyLevel; 121 122 if (score >= scoreThreshold && score % scoreThreshold < 10) { 123 // Only trigger once when crossing the threshold 124 if (!k.get("level-up-text").length) { 125 difficultyLevel += 1; 126 127 // Update difficulty in tracker 128 const tracker = k.get("game-score-tracker")[0]; 129 if (tracker) { 130 tracker.updateDifficulty(difficultyLevel); 131 } 132 133 // Increase max enemies (cap at 15) 134 maxEnemies = Math.min(initialMaxEnemies + difficultyLevel, 15); 135 136 // Decrease spawn interval (minimum 0.5 seconds) 137 spawnInterval = Math.max( 138 initialSpawnInterval - difficultyLevel * 0.2, 139 0.5, 140 ); 141 142 console.log( 143 `Difficulty increased to level ${difficultyLevel}. Max enemies: ${maxEnemies}, Spawn interval: ${spawnInterval}s`, 144 ); 145 146 // Cancel previous spawn loop and start a new one with updated interval 147 k.loop(spawnInterval, spawnEnemy); 148 149 // Visual feedback for difficulty increase 150 const screenCenter = k.vec2(k.width() / 2, k.height() / 2); 151 if (k.addConfetti) { 152 k.addConfetti(screenCenter); 153 } 154 155 // Add difficulty level text 156 const levelText = k.add([ 157 k.text(`Difficulty Level ${difficultyLevel}!`, { size: 32 }), 158 k.pos(screenCenter), 159 k.anchor("center"), 160 k.color(255, 255, 255), 161 k.outline(2, k.rgb(0, 0, 0)), 162 k.z(100), 163 k.opacity(1), 164 "level-up-text", 165 ]); 166 167 // Fade out and destroy the text 168 k.tween( 169 1, 170 0, 171 2, 172 (v) => { 173 if (levelText.exists()) { 174 levelText.opacity = v; 175 } 176 }, 177 k.easings.easeInQuad, 178 ); 179 180 k.wait(2, () => { 181 if (levelText.exists()) levelText.destroy(); 182 }); 183 } 184 } 185 } 186 187 // Start difficulty scaling 188 k.loop(1, updateDifficulty); 189 190 // Spawn an enemy at a random position 191 function spawnEnemy() { 192 if (!gameActive) return; 193 194 // Don't spawn if we already have max enemies 195 if (enemies.length >= maxEnemies) return; 196 197 // Random position at the edges of the screen 198 // As difficulty increases, add chance to spawn in center 199 let spawnSide; 200 201 // Calculate center spawn chance based on difficulty level 202 // 0% at level 1-2, increasing to 30% at level 10+ 203 const centerSpawnChance = 204 difficultyLevel <= 2 ? 0 : Math.min((difficultyLevel - 2) * 0.04, 0.3); 205 206 // Determine spawn location 207 if (Math.random() < centerSpawnChance) { 208 // Center spawn 209 spawnSide = 2; // Center 210 } else { 211 // Side spawn (left or right) 212 spawnSide = Math.floor(Math.random() * 2); // 0: left, 1: right 213 } 214 215 let x = 0, 216 y = 0; 217 218 switch (spawnSide) { 219 case 0: // left 220 x = 10; // Just inside the left wall 221 y = Math.random() * (k.height() - 48 - 20) + 20; // Avoid spawning behind top wall or inside ground 222 break; 223 case 1: // right 224 x = k.width() - 10; // Just inside the right wall 225 y = Math.random() * (k.height() - 48 - 20) + 20; // Avoid spawning behind top wall or inside ground 226 break; 227 case 2: // center (mid-air) 228 // Random position in the middle area of the screen 229 x = k.width() * (0.3 + Math.random() * 0.4); // 30-70% of screen width 230 y = k.height() * (0.2 + Math.random() * 0.5); // 20-70% of screen height 231 break; 232 } 233 234 // Create enemy using the makeEnemy function 235 const newEnemy = makeEnemy(k, playerObj, x, y); 236 enemies.push(newEnemy); 237 238 // Remove from array when destroyed 239 newEnemy.on("destroy", () => { 240 enemies = enemies.filter((e) => e !== newEnemy); 241 242 // Increase score when enemy is destroyed 243 const pointsEarned = Math.round(10 + Math.pow(difficultyLevel, 0.75)); 244 score += pointsEarned; 245 246 // Update score display 247 scoreText.text = `Score: ${score}`; 248 249 // Update score tracker 250 const tracker = k.get("game-score-tracker")[0]; 251 if (tracker) { 252 tracker.score = score; 253 } 254 255 // Heal player when killing an enemy 256 const player = k.get("player")[0]; 257 if (player && player.heal) { 258 // Heal amount is 5 health points 259 const healAmount = 5; 260 player.heal(healAmount); 261 262 // Show healing effect 263 const healText = k.add([ 264 k.text(`+${healAmount} HP`, { size: 16 }), 265 k.pos(player.pos.x, player.pos.y - 60), 266 k.anchor("center"), 267 k.color(0, 255, 0), 268 k.z(100), 269 k.opacity(1), 270 ]); 271 272 // Float upward and fade out 273 k.tween( 274 1, 275 0, 276 0.8, 277 (v) => { 278 if (healText.exists()) { 279 healText.opacity = v; 280 healText.pos.y -= 0.5; 281 } 282 }, 283 k.easings.easeOutQuad, 284 ); 285 286 k.wait(0.8, () => { 287 if (healText.exists()) healText.destroy(); 288 }); 289 } 290 291 if (Math.random() < 0.2 * Math.pow(difficultyLevel, 0.75)) spawnEnemy(); 292 }); 293 } 294 295 // Start spawning enemies 296 k.loop(spawnInterval, spawnEnemy); 297 298 // Game loop 299 k.onUpdate(() => { 300 // Update enemy list (remove destroyed enemies) 301 enemies = enemies.filter((enemy) => enemy.exists()); 302 }); 303 304 // Listen for game over event 305 playerObj.on("death", () => { 306 gameActive = false; 307 308 // Get final score from tracker 309 const tracker = k.get("game-score-tracker")[0]; 310 if (tracker) { 311 finalScore = tracker.score; 312 } else { 313 finalScore = score; 314 } 315 316 // Wait a moment before showing game over screen 317 k.wait(2, () => { 318 k.go("gameOver", finalScore); 319 }); 320 }); 321}); 322 323// Game over scene 324k.scene("gameOver", (score: number) => { 325 // Background 326 k.add([k.rect(k.width(), k.height()), k.color(0, 0, 0), k.opacity(0.7)]); 327 328 // Game over text 329 k.add([ 330 k.text("GAME OVER", { size: 64 }), 331 k.pos(k.width() / 2, k.height() / 3), 332 k.anchor("center"), 333 k.color(255, 50, 50), 334 ]); 335 336 // Score display 337 k.add([ 338 k.text(`Final Score: ${score}`, { size: 36 }), 339 k.pos(k.width() / 2, k.height() / 2), 340 k.anchor("center"), 341 k.color(255, 255, 255), 342 ]); 343 344 // Restart button 345 const restartBtn = k.add([ 346 k.rect(200, 60), 347 k.pos(k.width() / 2, (k.height() * 2) / 3), 348 k.anchor("center"), 349 k.color(50, 150, 50), 350 k.area(), 351 "restart-btn", 352 ]); 353 354 // Restart text 355 k.add([ 356 k.text("RESTART", { size: 24 }), 357 k.pos(k.width() / 2, (k.height() * 2) / 3), 358 k.anchor("center"), 359 k.color(255, 255, 255), 360 ]); 361 362 // Restart on button click 363 restartBtn.onClick(() => { 364 k.go("main"); 365 }); 366 367 // Restart on key press 368 k.onKeyPress("r", () => { 369 k.go("main"); 370 }); 371 372 // Restart on enter key 373 k.onKeyPress("enter", () => { 374 k.go("main"); 375 }); 376}); 377 378// Start the game 379k.go("main");