the game
1import type { KAPLAYCtx, Comp, GameObj } from "kaplay";
2
3// Define enemy component type
4export interface EnemyComp extends Comp {
5 health: number;
6 maxHealth: number;
7 speed: number;
8 damage(amount: number): void;
9 update(): void;
10}
11
12export function enemy(k: KAPLAYCtx, target: GameObj) {
13 // Use closed local variables for internal data
14 let maxHealth = 100;
15 let health = maxHealth;
16 let speed = 100;
17 let isHit = false;
18 let isDying = false;
19 let healthBar: GameObj | null = null;
20 let healthBarBg: GameObj | null = null;
21 let lastDamageTime = 0;
22 const DAMAGE_COOLDOWN = 0.5; // seconds
23
24 return {
25 id: "enemy",
26 require: ["pos", "sprite", "area", "body"],
27
28 // Exposed properties
29 health,
30 maxHealth,
31 speed,
32
33 // Damage the enemy
34 damage(this: GameObj, amount: number) {
35 if (isDying) return;
36
37 health -= amount;
38 console.log(`Enemy damaged: ${amount}, health: ${health}`);
39
40 // Play hit sound
41 if (typeof (window as any).gameSound !== 'undefined') {
42 (window as any).gameSound.playSfx('hit', { volume: 0.3, detune: 200 });
43 }
44
45 // Flash red when hit
46 isHit = true;
47 this.color = k.rgb(255, 0, 0);
48
49 // Reset color after a short time
50 k.wait(0.1, () => {
51 this.color = k.rgb();
52 isHit = false;
53 });
54
55 // Update health bar
56 if (healthBar) {
57 const healthPercent = Math.max(0, health / maxHealth);
58 healthBar.width = 40 * healthPercent;
59 }
60
61 // Check if enemy is dead
62 if (health <= 0 && !isDying) {
63 isDying = true;
64 this.die();
65 }
66 },
67
68 // Enemy death
69 die(this: GameObj) {
70 // Play death sound
71 if (typeof (window as any).gameSound !== 'undefined') {
72 (window as any).gameSound.playSfx('death', { volume: 0.4, detune: -100 });
73 }
74
75 // Add confetti effect only (no kaboom)
76 if (k.addConfetti) {
77 k.addConfetti(this.pos);
78 }
79
80 // Remove health bar
81 if (healthBarBg) healthBarBg.destroy();
82 if (healthBar) healthBar.destroy();
83
84 // Scale down and fade out
85 k.tween(
86 this.scale.x,
87 0,
88 0.5,
89 (v) => {
90 this.scale.x = v;
91 this.scale.y = v;
92 },
93 k.easings.easeInQuad,
94 );
95
96 k.tween(
97 1,
98 0,
99 0.5,
100 (v) => {
101 this.opacity = v;
102 },
103 k.easings.easeInQuad,
104 );
105
106 // Destroy after animation completes
107 k.wait(0.5, () => {
108 this.destroy();
109 });
110 },
111
112 // Add method runs when the component is added to a game object
113 add(this: GameObj) {
114 // Create health bar background (gray)
115 healthBarBg = k.add([
116 k.rect(40, 5),
117 k.pos(this.pos.x - 20, this.pos.y - 30),
118 k.color(100, 100, 100),
119 k.z(0.9),
120 ]);
121
122 // Create health bar (red)
123 healthBar = k.add([
124 k.rect(40, 5),
125 k.pos(this.pos.x - 20, this.pos.y - 30),
126 k.color(255, 0, 0),
127 k.z(1),
128 ]);
129
130 // Handle collisions with sword
131 this.onCollide("sword", (sword) => {
132 if (sword.isAttacking && !isHit) {
133 // Sword does 35 damage (35% of enemy health)
134 this.damage(35);
135 }
136 });
137 },
138
139 // Runs every frame
140 update(this: GameObj) {
141 if (isDying) return;
142
143 // Move toward target
144 const dir = k.vec2(target.pos.x - this.pos.x, target.pos.y - this.pos.y);
145
146 // Normalize direction vector
147 const dist = Math.sqrt(dir.x * dir.x + dir.y * dir.y);
148
149 // Only move if not too close to target
150 if (dist > 50) {
151 const normalizedDir = {
152 x: dir.x / dist,
153 y: dir.y / dist,
154 };
155
156 // Move toward target
157 this.move(normalizedDir.x * speed, normalizedDir.y * speed);
158
159 // Flip sprite based on movement direction
160 if (normalizedDir.x !== 0) {
161 this.flipX = normalizedDir.x < 0;
162 }
163 }
164
165 // Check for collision with player and apply damage if in contact
166 // Only apply damage if cooldown has passed
167 if (k.time() - lastDamageTime > DAMAGE_COOLDOWN) {
168 const playerObj = k.get("player")[0];
169 if (playerObj && this.isColliding(playerObj)) {
170 lastDamageTime = k.time();
171
172 // Damage player
173 if (playerObj.damage) {
174 // Get current difficulty level to scale damage
175 const difficultyLevel = k.get("game-score-tracker")[0]?.difficultyLevel || 1;
176
177 // Base damage is 5, increases with difficulty
178 const baseDamage = 5;
179 const scaledDamage = Math.round(baseDamage + (difficultyLevel - 1) * 2);
180
181 playerObj.damage(scaledDamage);
182 }
183
184 // Knockback effect
185 const knockback = 200;
186 const knockbackDir = k
187 .vec2(playerObj.pos.x - this.pos.x, playerObj.pos.y - this.pos.y)
188 .unit();
189
190 playerObj.move(
191 knockbackDir.x * knockback,
192 knockbackDir.y * knockback,
193 );
194 }
195 }
196
197 // Update health bar position to follow enemy
198 if (healthBar && healthBarBg) {
199 healthBarBg.pos.x = this.pos.x - 20;
200 healthBarBg.pos.y = this.pos.y - 30;
201
202 healthBar.pos.x = this.pos.x - 20;
203 healthBar.pos.y = this.pos.y - 30;
204 }
205 },
206
207 // Cleanup when destroyed
208 destroy() {
209 if (healthBar) healthBar.destroy();
210 if (healthBarBg) healthBarBg.destroy();
211 },
212 };
213}
214
215// Function to create an enemy
216export function makeEnemy(k: KAPLAYCtx, target: GameObj, x: number, y: number) {
217 // Create enemy
218 const newEnemy = k.add([
219 k.sprite("bean"),
220 k.pos(x, y),
221 k.scale(1),
222 k.anchor("center"),
223 k.area(),
224 k.body(),
225 enemy(k, target),
226 "enemy", // Add tag for collision detection
227 ]);
228
229 return newEnemy;
230}
231
232export default enemy;