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