at main 5.9 kB view raw
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;