···11+import type { KAPLAYCtx, GameObj } from "kaplay";
22+33+// Extend the KAPLAYCtx type to include our addConfetti method
44+declare module "kaplay" {
55+ interface KAPLAYCtx {
66+ addConfetti?: (pos: { x: number, y: number }) => GameObj[];
77+ }
88+}
99+1010+// Function to create a confetti effect at a position
1111+export function addConfetti(k: KAPLAYCtx, pos: { x: number, y: number }) {
1212+ // Number of confetti particles
1313+ const PARTICLE_COUNT = 50;
1414+1515+ // Confetti colors
1616+ const COLORS = [
1717+ [255, 0, 0], // Red
1818+ [0, 255, 0], // Green
1919+ [0, 0, 255], // Blue
2020+ [255, 255, 0], // Yellow
2121+ [255, 0, 255], // Magenta
2222+ [0, 255, 255], // Cyan
2323+ [255, 165, 0], // Orange
2424+ [128, 0, 128], // Purple
2525+ ];
2626+2727+ // Create particles
2828+ const particles: GameObj[] = [];
2929+3030+ for (let i = 0; i < PARTICLE_COUNT; i++) {
3131+ // Random color
3232+ const color = COLORS[Math.floor(Math.random() * COLORS.length)];
3333+3434+ // Random size
3535+ const size = Math.random() * 8 + 2; // 2-10 pixels
3636+3737+ // Random shape (circle or rect)
3838+ const isCircle = Math.random() > 0.5;
3939+4040+ // Random velocity
4141+ const angle = Math.random() * Math.PI * 2;
4242+ const speed = Math.random() * 400 + 100; // 100-500 pixels per second
4343+ const vx = Math.cos(angle) * speed;
4444+ const vy = Math.sin(angle) * speed - 200; // Initial upward boost
4545+4646+ // Random rotation speed
4747+ const rotSpeed = Math.random() * 10 - 5; // -5 to 5 radians per second
4848+4949+ // Create particle
5050+ const particle = k.add([
5151+ isCircle ? k.circle(size / 2) : k.rect(size, size / 2),
5252+ k.pos(pos.x, pos.y),
5353+ k.color(...color),
5454+ k.anchor("center"),
5555+ k.rotate(Math.random() * 360), // Random initial rotation
5656+ k.opacity(1),
5757+ k.z(100), // Above most game elements
5858+ {
5959+ // Custom properties for movement
6060+ vx,
6161+ vy,
6262+ rotSpeed,
6363+ gravity: 980, // Gravity effect
6464+ lifespan: Math.random() * 1 + 1, // 1-2 seconds
6565+ fadeStart: 0.7, // When to start fading (0.7 = 70% of lifespan)
6666+ },
6767+ ]);
6868+6969+ // Update function for the particle
7070+ particle.onUpdate(() => {
7171+ // Update position based on velocity
7272+ particle.pos.x += particle.vx * k.dt();
7373+ particle.pos.y += particle.vy * k.dt();
7474+7575+ // Apply gravity
7676+ particle.vy += particle.gravity * k.dt();
7777+7878+ // Apply rotation
7979+ particle.angle += particle.rotSpeed * k.dt() * 60;
8080+8181+ // Update lifespan
8282+ particle.lifespan -= k.dt();
8383+8484+ // Fade out
8585+ if (particle.lifespan < particle.fadeStart) {
8686+ particle.opacity = Math.max(0, particle.lifespan / particle.fadeStart);
8787+ }
8888+8989+ // Destroy when lifespan is over
9090+ if (particle.lifespan <= 0) {
9191+ particle.destroy();
9292+ }
9393+ });
9494+9595+ particles.push(particle);
9696+ }
9797+9898+ return particles;
9999+}
100100+101101+// Component to add confetti method to the game context
102102+export function confettiPlugin(k: KAPLAYCtx) {
103103+ return {
104104+ // Add the confetti function to the game context
105105+ addConfetti(pos: { x: number, y: number }) {
106106+ return addConfetti(k, pos);
107107+ }
108108+ };
109109+}
110110+111111+export default confettiPlugin;
+213
src/enemy.ts
···11+import type { KAPLAYCtx, Comp, GameObj } from "kaplay";
22+33+// Define enemy component type
44+export interface EnemyComp extends Comp {
55+ health: number;
66+ maxHealth: number;
77+ speed: number;
88+ damage(amount: number): void;
99+ update(): void;
1010+}
1111+1212+export function enemy(k: KAPLAYCtx, target: GameObj) {
1313+ // Use closed local variables for internal data
1414+ let maxHealth = 100;
1515+ let health = maxHealth;
1616+ let speed = 100;
1717+ let isHit = false;
1818+ let isDying = false;
1919+ let healthBar: GameObj | null = null;
2020+ let healthBarBg: GameObj | null = null;
2121+ let lastDamageTime = 0;
2222+ const DAMAGE_COOLDOWN = 0.5; // seconds
2323+2424+ return {
2525+ id: "enemy",
2626+ require: ["pos", "sprite", "area", "body"],
2727+2828+ // Exposed properties
2929+ health,
3030+ maxHealth,
3131+ speed,
3232+3333+ // Damage the enemy
3434+ damage(this: GameObj, amount: number) {
3535+ if (isDying) return;
3636+3737+ health -= amount;
3838+ console.log(`Enemy damaged: ${amount}, health: ${health}`);
3939+4040+ // Flash red when hit
4141+ isHit = true;
4242+ this.color = k.rgb(255, 0, 0);
4343+4444+ // Reset color after a short time
4545+ k.wait(0.1, () => {
4646+ this.color = k.rgb();
4747+ isHit = false;
4848+ });
4949+5050+ // Update health bar
5151+ if (healthBar) {
5252+ const healthPercent = Math.max(0, health / maxHealth);
5353+ healthBar.width = 40 * healthPercent;
5454+ }
5555+5656+ // Check if enemy is dead
5757+ if (health <= 0 && !isDying) {
5858+ isDying = true;
5959+ this.die();
6060+ }
6161+ },
6262+6363+ // Enemy death
6464+ die(this: GameObj) {
6565+ // Add confetti effect only (no kaboom)
6666+ if (k.addConfetti) {
6767+ k.addConfetti(this.pos);
6868+ }
6969+7070+ // Remove health bar
7171+ if (healthBarBg) healthBarBg.destroy();
7272+ if (healthBar) healthBar.destroy();
7373+7474+ // Scale down and fade out
7575+ k.tween(
7676+ this.scale.x,
7777+ 0,
7878+ 0.5,
7979+ (v) => {
8080+ this.scale.x = v;
8181+ this.scale.y = v;
8282+ },
8383+ k.easings.easeInQuad,
8484+ );
8585+8686+ k.tween(
8787+ 1,
8888+ 0,
8989+ 0.5,
9090+ (v) => {
9191+ this.opacity = v;
9292+ },
9393+ k.easings.easeInQuad,
9494+ );
9595+9696+ // Destroy after animation completes
9797+ k.wait(0.5, () => {
9898+ this.destroy();
9999+ });
100100+ },
101101+102102+ // Add method runs when the component is added to a game object
103103+ add(this: GameObj) {
104104+ // Create health bar background (gray)
105105+ healthBarBg = k.add([
106106+ k.rect(40, 5),
107107+ k.pos(this.pos.x - 20, this.pos.y - 30),
108108+ k.color(100, 100, 100),
109109+ k.z(0.9),
110110+ ]);
111111+112112+ // Create health bar (red)
113113+ healthBar = k.add([
114114+ k.rect(40, 5),
115115+ k.pos(this.pos.x - 20, this.pos.y - 30),
116116+ k.color(255, 0, 0),
117117+ k.z(1),
118118+ ]);
119119+120120+ // Handle collisions with sword
121121+ this.onCollide("sword", (sword) => {
122122+ if (sword.isAttacking && !isHit) {
123123+ // Sword does 35 damage (35% of enemy health)
124124+ this.damage(35);
125125+ }
126126+ });
127127+ },
128128+129129+ // Runs every frame
130130+ update(this: GameObj) {
131131+ if (isDying) return;
132132+133133+ // Move toward target
134134+ const dir = k.vec2(target.pos.x - this.pos.x, target.pos.y - this.pos.y);
135135+136136+ // Normalize direction vector
137137+ const dist = Math.sqrt(dir.x * dir.x + dir.y * dir.y);
138138+139139+ // Only move if not too close to target
140140+ if (dist > 50) {
141141+ const normalizedDir = {
142142+ x: dir.x / dist,
143143+ y: dir.y / dist,
144144+ };
145145+146146+ // Move toward target
147147+ this.move(normalizedDir.x * speed, normalizedDir.y * speed);
148148+149149+ // Flip sprite based on movement direction
150150+ if (normalizedDir.x !== 0) {
151151+ this.flipX = normalizedDir.x < 0;
152152+ }
153153+ }
154154+155155+ // Check for collision with player and apply damage if in contact
156156+ // Only apply damage if cooldown has passed
157157+ if (k.time() - lastDamageTime > DAMAGE_COOLDOWN) {
158158+ const playerObj = k.get("player")[0];
159159+ if (playerObj && this.isColliding(playerObj)) {
160160+ lastDamageTime = k.time();
161161+162162+ // Damage player
163163+ if (playerObj.damage) {
164164+ playerObj.damage(5);
165165+ }
166166+167167+ // Knockback effect
168168+ const knockback = 200;
169169+ const knockbackDir = k.vec2(
170170+ playerObj.pos.x - this.pos.x,
171171+ playerObj.pos.y - this.pos.y
172172+ ).unit();
173173+174174+ playerObj.move(knockbackDir.x * knockback, knockbackDir.y * knockback);
175175+ }
176176+ }
177177+178178+ // Update health bar position to follow enemy
179179+ if (healthBar && healthBarBg) {
180180+ healthBarBg.pos.x = this.pos.x - 20;
181181+ healthBarBg.pos.y = this.pos.y - 30;
182182+183183+ healthBar.pos.x = this.pos.x - 20;
184184+ healthBar.pos.y = this.pos.y - 30;
185185+ }
186186+ },
187187+188188+ // Cleanup when destroyed
189189+ destroy() {
190190+ if (healthBar) healthBar.destroy();
191191+ if (healthBarBg) healthBarBg.destroy();
192192+ },
193193+ };
194194+}
195195+196196+// Function to create an enemy
197197+export function makeEnemy(k: KAPLAYCtx, target: GameObj, x: number, y: number) {
198198+ // Create enemy
199199+ const newEnemy = k.add([
200200+ k.sprite("bean"),
201201+ k.pos(x, y),
202202+ k.scale(1),
203203+ k.anchor("center"),
204204+ k.area(),
205205+ k.body(),
206206+ enemy(k, target),
207207+ "enemy", // Add tag for collision detection
208208+ ]);
209209+210210+ return newEnemy;
211211+}
212212+213213+export default enemy;
-11
src/main.js
···11-import kaplay from "kaplay";
22-// import "kaplay/global"; // uncomment if you want to use without the k. prefix
33-44-const k = kaplay();
55-66-k.loadRoot("./"); // A good idea for Itch.io publishing later
77-k.loadSprite("bean", "sprites/bean.png");
88-99-k.add([k.pos(120, 80), k.sprite("bean")]);
1010-1111-k.onClick(() => k.addKaboom(k.mousePos()));
+156
src/main.ts
···11+import { crew } from "@kaplayjs/crew";
22+import kaplay from "kaplay";
33+44+import player from "./player";
55+import { makeEnemy, type EnemyComp } from "./enemy";
66+import confettiPlugin, { addConfetti } from "./confetti";
77+88+const k = kaplay({ plugins: [crew] });
99+k.loadRoot("./"); // A good idea for Itch.io publishing later
1010+k.loadCrew("sprite", "glady-o");
1111+k.loadCrew("sprite", "sword-o");
1212+k.loadCrew("sprite", "bean"); // Using bean sprite for enemies
1313+1414+// Add confetti plugin to the game context
1515+const confetti = confettiPlugin(k);
1616+k.addConfetti = confetti.addConfetti;
1717+1818+k.setGravity(1600);
1919+2020+// Create ground
2121+const ground = k.add([
2222+ k.rect(k.width(), 48),
2323+ k.pos(0, k.height() - 48),
2424+ k.outline(4),
2525+ k.area(),
2626+ k.body({ isStatic: true }),
2727+ k.color(127, 200, 255),
2828+]);
2929+3030+// Create player object with components
3131+const playerObj = k.add([
3232+ k.pos(120, 500),
3333+ k.sprite("glady-o"),
3434+ k.body(),
3535+ k.area(),
3636+ player(k),
3737+ "player", // Add tag for collision detection
3838+]);
3939+4040+// Enemy spawning variables
4141+let enemies: any[] = [];
4242+let initialMaxEnemies = 5;
4343+let maxEnemies = initialMaxEnemies;
4444+let initialSpawnInterval = 3; // seconds
4545+let spawnInterval = initialSpawnInterval;
4646+let gameTime = 0; // Track game time in seconds
4747+let difficultyLevel = 1;
4848+let spawnLoopId: any = null;
4949+5050+// Difficulty scaling
5151+function updateDifficulty() {
5252+ gameTime += 1; // Increment game time by 1 second
5353+5454+ // Every 30 seconds, increase difficulty
5555+ if (gameTime % 30 === 0) {
5656+ difficultyLevel += 1;
5757+5858+ // Increase max enemies (cap at 15)
5959+ maxEnemies = Math.min(initialMaxEnemies + difficultyLevel, 15);
6060+6161+ // Decrease spawn interval (minimum 0.5 seconds)
6262+ spawnInterval = Math.max(initialSpawnInterval - (difficultyLevel * 0.3), 0.5);
6363+6464+ console.log(`Difficulty increased to level ${difficultyLevel}. Max enemies: ${maxEnemies}, Spawn interval: ${spawnInterval}s`);
6565+6666+ // Cancel previous spawn loop and start a new one with updated interval
6767+ if (spawnLoopId !== null) {
6868+ k.cancel(spawnLoopId);
6969+ }
7070+ spawnLoopId = k.loop(spawnInterval, spawnEnemy);
7171+7272+ // Visual feedback for difficulty increase
7373+ const screenCenter = k.vec2(k.width() / 2, k.height() / 2);
7474+ if (k.addConfetti) {
7575+ k.addConfetti(screenCenter);
7676+ }
7777+7878+ // Add difficulty level text
7979+ const levelText = k.add([
8080+ k.text(`Difficulty Level ${difficultyLevel}!`, { size: 32 }),
8181+ k.pos(screenCenter),
8282+ k.anchor("center"),
8383+ k.color(255, 255, 255),
8484+ k.outline(2, k.rgb(0, 0, 0)),
8585+ k.z(100),
8686+ k.opacity(1),
8787+ ]);
8888+8989+ // Fade out and destroy the text
9090+ k.tween(
9191+ 1,
9292+ 0,
9393+ 2,
9494+ (v) => {
9595+ levelText.opacity = v;
9696+ },
9797+ k.easings.easeInQuad,
9898+ );
9999+100100+ k.wait(2, () => {
101101+ levelText.destroy();
102102+ });
103103+ }
104104+}
105105+106106+// Start difficulty scaling
107107+k.loop(1, updateDifficulty);
108108+109109+// Spawn an enemy at a random position
110110+function spawnEnemy() {
111111+ // Don't spawn if we already have max enemies
112112+ if (enemies.length >= maxEnemies) return;
113113+114114+ // Random position at the edges of the screen
115115+ const side = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left
116116+ let x, y;
117117+118118+ switch (side) {
119119+ case 0: // top
120120+ x = Math.random() * k.width();
121121+ y = -50;
122122+ break;
123123+ case 1: // right
124124+ x = k.width() + 50;
125125+ y = Math.random() * k.height();
126126+ break;
127127+ case 2: // bottom
128128+ x = Math.random() * k.width();
129129+ y = k.height() + 50;
130130+ break;
131131+ case 3: // left
132132+ x = -50;
133133+ y = Math.random() * k.height();
134134+ break;
135135+ }
136136+137137+ // Create enemy using the makeEnemy function
138138+ const newEnemy = makeEnemy(k, playerObj, x, y);
139139+ enemies.push(newEnemy);
140140+141141+ // Remove from array when destroyed
142142+ newEnemy.on("destroy", () => {
143143+ enemies = enemies.filter((e) => e !== newEnemy);
144144+ });
145145+}
146146+147147+// Start spawning enemies
148148+spawnLoopId = k.loop(spawnInterval, spawnEnemy);
149149+150150+// Game loop
151151+k.onUpdate(() => {
152152+ // Update enemy list (remove destroyed enemies)
153153+ enemies = enemies.filter((enemy) => enemy.exists());
154154+});
155155+156156+console.log(typeof k);
+469
src/player.ts
···11+import type { KAPLAYCtx, Comp, GameObj } from "kaplay";
22+import { Vec2 } from "kaplay";
33+44+// Define player component type
55+interface PlayerComp extends Comp {
66+ speed: number;
77+ health: number;
88+ maxHealth: number;
99+ damage(amount: number): void;
1010+ attack(): void;
1111+ update(): void;
1212+}
1313+1414+function player(k: KAPLAYCtx): PlayerComp {
1515+ // Use closed local variable for internal data
1616+ let speed = 500;
1717+ let jumpForce = 600;
1818+ let maxHealth = 100;
1919+ let health = maxHealth;
2020+ let isAttacking = false;
2121+ let isHit = false;
2222+ let sword: GameObj | null = null;
2323+ let arrowPoints: GameObj[] = [];
2424+ let healthBar: GameObj | null = null;
2525+ let healthBarBg: GameObj | null = null;
2626+ const ARROW_SEGMENTS = 8; // Number of segments in the arrow
2727+ const MAX_ATTACK_DISTANCE = 500; // Maximum distance for attacks and kaboom
2828+ let attackRangeCircle: GameObj | null = null; // Visual indicator of attack range
2929+3030+ // Helper function to convert radians to degrees
3131+ const radToDeg = (rad: number) => (rad * 180) / Math.PI;
3232+3333+ // Helper function to create a bezier curve point
3434+ const bezierPoint = (
3535+ t: number,
3636+ p0: number,
3737+ p1: number,
3838+ p2: number,
3939+ p3: number,
4040+ ) => {
4141+ const mt = 1 - t;
4242+ return (
4343+ mt * mt * mt * p0 +
4444+ 3 * mt * mt * t * p1 +
4545+ 3 * mt * t * t * p2 +
4646+ t * t * t * p3
4747+ );
4848+ };
4949+5050+ // Helper function to clamp a point to a circle
5151+ const clampToCircle = (
5252+ center: { x: number; y: number },
5353+ point: { x: number; y: number },
5454+ radius: number,
5555+ ): Vec2 => {
5656+ const dx = point.x - center.x;
5757+ const dy = point.y - center.y;
5858+ const distance = Math.sqrt(dx * dx + dy * dy);
5959+6060+ if (distance <= radius) {
6161+ return k.vec2(point.x, point.y); // Point is already inside circle
6262+ }
6363+6464+ // Calculate the point on the circle's edge
6565+ const ratio = radius / distance;
6666+ return k.vec2(center.x + dx * ratio, center.y + dy * ratio);
6767+ };
6868+6969+ return {
7070+ id: "player",
7171+ require: ["body", "area", "pos"],
7272+7373+ // Exposed properties
7474+ speed,
7575+ health,
7676+ maxHealth,
7777+7878+ // Take damage
7979+ damage(this: GameObj, amount: number) {
8080+ if (isHit) return; // Prevent taking damage too quickly
8181+8282+ health -= amount;
8383+8484+ // Flash red when hit
8585+ isHit = true;
8686+ this.color = k.rgb(255, 0, 0);
8787+8888+ // Reset color after a short time
8989+ k.wait(0.1, () => {
9090+ this.color = k.rgb();
9191+ isHit = false;
9292+ });
9393+9494+ // Update health bar
9595+ if (healthBar) {
9696+ const healthPercent = Math.max(0, health / maxHealth);
9797+ healthBar.width = 60 * healthPercent;
9898+ }
9999+100100+ // Check if player is dead
101101+ if (health <= 0) {
102102+ // Game over logic here
103103+ k.addKaboom(this.pos);
104104+ k.shake(20);
105105+ }
106106+ },
107107+108108+ // Runs when the obj is added to scene
109109+ add(this: GameObj) {
110110+ // Create health bar background (gray)
111111+ healthBarBg = k.add([
112112+ k.rect(60, 8),
113113+ k.pos(this.pos.x - 30, this.pos.y - 40),
114114+ k.color(100, 100, 100),
115115+ k.z(0.9),
116116+ ]);
117117+118118+ // Create health bar (red)
119119+ healthBar = k.add([
120120+ k.rect(60, 8),
121121+ k.pos(this.pos.x - 30, this.pos.y - 40),
122122+ k.color(255, 0, 0),
123123+ k.z(1),
124124+ ]);
125125+126126+ // Create sword attached to player
127127+ sword = k.add([
128128+ k.sprite("sword-o"),
129129+ k.pos(this.pos.x + 30, this.pos.y - 10),
130130+ k.rotate(45), // Hold at 45 degrees
131131+ k.anchor("center"),
132132+ k.scale(0.7),
133133+ k.area(), // Add area for collision detection
134134+ k.z(1), // Make sure sword is in front of player
135135+ "sword", // Add tag for collision detection
136136+ {
137137+ isAttacking: false, // Custom property to track attack state
138138+ },
139139+ ]);
140140+141141+ // Create attack range indicator (semi-transparent circle)
142142+ attackRangeCircle = k.add([
143143+ k.circle(MAX_ATTACK_DISTANCE),
144144+ k.pos(this.pos.x, this.pos.y),
145145+ k.color(255, 255, 255),
146146+ k.opacity(0.1), // Very subtle
147147+ k.z(0.1), // Behind everything
148148+ ]);
149149+150150+ // Create arrow segments
151151+ for (let i = 0; i < ARROW_SEGMENTS; i++) {
152152+ // Create segment with white outline
153153+ const segment = k.add([
154154+ k.circle(3), // Initial size, will be scaled based on distance
155155+ k.pos(this.pos.x, this.pos.y - 30), // Start from player's head
156156+ k.color(255, 0, 0), // Red fill
157157+ k.outline(2, k.rgb(255, 255, 255)), // White outline
158158+ k.z(0.5),
159159+ ]);
160160+ arrowPoints.push(segment);
161161+ }
162162+163163+ // Create arrow head (using a circle for now)
164164+ const arrowHead = k.add([
165165+ k.circle(6), // Larger circle for the arrow head
166166+ k.pos(this.pos.x, this.pos.y - 30),
167167+ k.color(255, 0, 0), // Red fill
168168+ k.outline(2, k.rgb(255, 255, 255)), // White outline
169169+ k.z(0.5),
170170+ ]);
171171+ arrowPoints.push(arrowHead);
172172+173173+ // Jump with space or up arrow
174174+ this.onKeyPress(["space", "up", "w"], () => {
175175+ if (this.isGrounded()) {
176176+ this.jump(jumpForce);
177177+ }
178178+ });
179179+180180+ // Attack with X key
181181+ this.onKeyPress("x", () => {
182182+ this.attack();
183183+ });
184184+185185+ // Attack, kaboom and shake on click
186186+ k.onClick(() => {
187187+ // Attack with sword
188188+ this.attack();
189189+190190+ // Get mouse position and clamp it to the attack range
191191+ const mousePos = k.mousePos();
192192+ const clampedPos = clampToCircle(
193193+ this.pos,
194194+ mousePos,
195195+ MAX_ATTACK_DISTANCE,
196196+ );
197197+198198+ console.log("Creating explosion at", clampedPos.x, clampedPos.y);
199199+200200+ // Create visual explosion effect
201201+ k.addKaboom(clampedPos);
202202+203203+ // Create explosion area for damage
204204+ const explosionRadius = 120;
205205+ const explosion = k.add([
206206+ k.circle(explosionRadius),
207207+ k.pos(clampedPos),
208208+ k.color(255, 0, 0), // Semi-transparent red
209209+ k.area(),
210210+ k.anchor("center"),
211211+ k.opacity(0.3), // Add opacity component
212212+ "explosion",
213213+ ]);
214214+215215+ // Destroy explosion after a short time
216216+ k.wait(0.1, () => {
217217+ explosion.destroy();
218218+ });
219219+220220+ // Manually check for enemies in range
221221+ const enemies = k.get("enemy");
222222+ enemies.forEach((enemy) => {
223223+ const dist = k.vec2(enemy.pos).dist(clampedPos);
224224+ if (dist < explosionRadius) {
225225+ // Calculate damage based on distance from center
226226+ // At center (dist = 0): 70 damage (70% of enemy health)
227227+ // At edge (dist = explosionRadius): 20 damage (20% of enemy health)
228228+ const damagePercent = 0.7 - (0.5 * dist) / explosionRadius;
229229+ const damage = Math.floor(100 * damagePercent); // 100 is enemy max health
230230+231231+ console.log(
232232+ `Direct damage to enemy: ${damage}, distance: ${dist}, percent: ${damagePercent}`,
233233+ );
234234+ // Add type assertion to tell TypeScript that enemy has a damage method
235235+ (enemy as any).damage(damage);
236236+ }
237237+ });
238238+239239+ // Shake the screen
240240+ k.shake(10);
241241+ });
242242+ },
243243+244244+ // Attack method
245245+ attack(this: GameObj) {
246246+ if (isAttacking) return;
247247+248248+ isAttacking = true;
249249+250250+ if (sword) {
251251+ // Set sword to attacking state for collision detection
252252+ sword.isAttacking = true;
253253+254254+ // Store original angle
255255+ const originalAngle = this.flipX ? -30 : 30;
256256+257257+ // Animate sword swing
258258+ const direction = this.flipX ? -1 : 1;
259259+ const endAngle = direction > 0 ? 90 : -90;
260260+261261+ // Tween the sword rotation
262262+ k.tween(
263263+ sword.angle,
264264+ endAngle,
265265+ 0.15,
266266+ (val) => (sword!.angle = val),
267267+ k.easings.easeInOutQuad,
268268+ );
269269+270270+ // Return sword to original position
271271+ k.wait(0.15, () => {
272272+ if (sword) {
273273+ k.tween(
274274+ sword.angle,
275275+ originalAngle,
276276+ 0.15,
277277+ (val) => (sword!.angle = val),
278278+ k.easings.easeOutQuad,
279279+ );
280280+ }
281281+ });
282282+283283+ // End attack state
284284+ k.wait(0.3, () => {
285285+ isAttacking = false;
286286+ if (sword) {
287287+ sword.isAttacking = false;
288288+ }
289289+ });
290290+ }
291291+ },
292292+293293+ // Runs every frame
294294+ update(this: GameObj) {
295295+ // Left movement (left arrow or A key)
296296+ if (k.isKeyDown(["left", "a"])) {
297297+ this.move(-speed, 0);
298298+ this.flipX = true;
299299+ }
300300+301301+ // Right movement (right arrow or D key)
302302+ if (k.isKeyDown(["right", "d"])) {
303303+ this.move(speed, 0);
304304+ this.flipX = false;
305305+ }
306306+307307+ // Update sword position to follow player
308308+ if (sword) {
309309+ const xOffset = this.flipX ? 10 : 60;
310310+ const yOffset = 60; // Slightly above center
311311+ sword.pos.x = this.pos.x + xOffset;
312312+ sword.pos.y = this.pos.y + yOffset;
313313+314314+ // Update sword angle and flip based on player direction (when not attacking)
315315+ if (!isAttacking) {
316316+ sword.flipX = this.flipX;
317317+ sword.angle = this.flipX ? -30 : 30; // Mirror angle when facing left
318318+ }
319319+ }
320320+321321+ // Update health bar position to follow player
322322+ if (healthBar && healthBarBg) {
323323+ healthBarBg.pos.x = this.pos.x + 5;
324324+ healthBarBg.pos.y = this.pos.y - 40;
325325+326326+ healthBar.pos.x = this.pos.x + 5;
327327+ healthBar.pos.y = this.pos.y - 40;
328328+ }
329329+330330+ // Update attack range circle to follow player
331331+ if (attackRangeCircle) {
332332+ attackRangeCircle.pos = this.pos;
333333+ }
334334+335335+ // Update arrow to create an arc from player to mouse
336336+ if (arrowPoints.length > 0) {
337337+ const mousePos = k.mousePos();
338338+ const startPos = { x: this.pos.x + 40, y: this.pos.y }; // Player's head
339339+340340+ // Clamp mouse position to maximum attack range
341341+ const clampedMousePos = clampToCircle(
342342+ this.pos,
343343+ mousePos,
344344+ MAX_ATTACK_DISTANCE,
345345+ );
346346+347347+ // Calculate horizontal distance from player to mouse
348348+ const horizontalDist = clampedMousePos.x - startPos.x;
349349+350350+ // Calculate total distance from player to mouse
351351+ const dist = Math.sqrt(
352352+ Math.pow(clampedMousePos.x - startPos.x, 2) +
353353+ Math.pow(clampedMousePos.y - startPos.y, 2),
354354+ );
355355+356356+ // Determine arc direction based on horizontal position
357357+ // Use a smooth transition near the center
358358+ const centerThreshold = 50; // Distance from center where arc is minimal
359359+ let arcDirection = 0;
360360+361361+ if (Math.abs(horizontalDist) < centerThreshold) {
362362+ // Smooth transition near center
363363+ arcDirection = -(horizontalDist / centerThreshold); // Will be between -1 and 1
364364+ } else {
365365+ // Full curve away from center
366366+ arcDirection = horizontalDist > 0 ? -1 : 1;
367367+ }
368368+369369+ // Calculate arc height based on distance and direction
370370+ // Reduce arc height when close to center
371371+ const maxArcHeight = 100;
372372+ const arcHeightFactor = Math.min(Math.abs(arcDirection), 1); // Between 0 and 1
373373+ const arcHeight = Math.min(dist * 0.5, maxArcHeight) * arcHeightFactor;
374374+375375+ // Calculate perpendicular direction for control points
376376+ const dirX = clampedMousePos.x - startPos.x;
377377+ const dirY = clampedMousePos.y - startPos.y;
378378+ const len = Math.sqrt(dirX * dirX + dirY * dirY);
379379+ const perpX = (-dirY / len) * arcDirection;
380380+ const perpY = (dirX / len) * arcDirection;
381381+382382+ // Control points for the bezier curve
383383+ const ctrl1 = {
384384+ x: startPos.x + dirX * 0.25 + perpX * arcHeight,
385385+ y: startPos.y + dirY * 0.25 + perpY * arcHeight,
386386+ };
387387+388388+ const ctrl2 = {
389389+ x: startPos.x + dirX * 0.75 + perpX * arcHeight,
390390+ y: startPos.y + dirY * 0.75 + perpY * arcHeight,
391391+ };
392392+393393+ // Position each segment along the bezier curve
394394+ for (let i = 0; i < ARROW_SEGMENTS; i++) {
395395+ const t = i / (ARROW_SEGMENTS - 1);
396396+ const x = bezierPoint(
397397+ t,
398398+ startPos.x,
399399+ ctrl1.x,
400400+ ctrl2.x,
401401+ clampedMousePos.x,
402402+ );
403403+ const y = bezierPoint(
404404+ t,
405405+ startPos.y,
406406+ ctrl1.y,
407407+ ctrl2.y,
408408+ clampedMousePos.y,
409409+ );
410410+411411+ // Calculate segment position along the curve
412412+ arrowPoints[i].pos.x = x;
413413+ arrowPoints[i].pos.y = y;
414414+415415+ // Scale circle size based on distance from start
416416+ // Segments get progressively larger toward the end
417417+ const segmentDist = i / (ARROW_SEGMENTS - 1); // 0 to 1
418418+ const minSize = 2;
419419+ const maxSize = 5;
420420+ const size = minSize + segmentDist * (maxSize - minSize);
421421+422422+ // Apply scale
423423+ if (arrowPoints[i].scale) {
424424+ arrowPoints[i].scale.x = size / 3; // Divide by default size (3)
425425+ arrowPoints[i].scale.y = size / 3;
426426+ }
427427+ }
428428+429429+ // Position arrow head at the end of the curve and make it larger
430430+ const arrowHead = arrowPoints[arrowPoints.length - 1];
431431+ arrowHead.pos.x = clampedMousePos.x;
432432+ arrowHead.pos.y = clampedMousePos.y;
433433+434434+ // Make arrow head larger
435435+ if (arrowHead.scale) {
436436+ arrowHead.scale.x = 3;
437437+ arrowHead.scale.y = 3;
438438+ }
439439+ }
440440+ },
441441+442442+ // Cleanup when destroyed
443443+ destroy() {
444444+ if (sword) {
445445+ sword.destroy();
446446+ }
447447+448448+ if (attackRangeCircle) {
449449+ attackRangeCircle.destroy();
450450+ }
451451+452452+ if (healthBar) {
453453+ healthBar.destroy();
454454+ }
455455+456456+ if (healthBarBg) {
457457+ healthBarBg.destroy();
458458+ }
459459+460460+ // Destroy all arrow segments
461461+ arrowPoints.forEach((segment) => {
462462+ segment.destroy();
463463+ });
464464+ arrowPoints = [];
465465+ },
466466+ };
467467+}
468468+469469+export default player;