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