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;