the game
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");