JIT
Creating an 8-bit ninja fighting game with mouse controls
8 bit ninja fighting game that use the mouse as well
Updated files
v1
make waves and make the enemies actually try and chase the player.

Here's the updated version with chasing enemies and wave mechanics:

Updated files
v2
make it a knock back fight

Here's the updated version with knockback mechanics for both the player and enemies:

Updated files
v3
make it W a s d movement but make the player have a sword as a weapon that has knock back and a cool slash effect

Here's the updated version with WASD movement, a sword with knockback, and a slash effect:

<!DOCTYPE html>
<html>
<head>
    <title>8-bit Ninja with Sword</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #222;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
        }
        canvas {
            border: 4px solid #444;
            image-rendering: pixelated;
        }
    </style>
</head>
<body>
    <canvas id="gameCanvas"></canvas>
    <script>
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        canvas.width = 800;
        canvas.height = 600;

        // Game state
        const keys = {};
        const player = {
            x: 400,
            y: 300,
            width: 32,
            height: 32,
            speed: 3,
            health: 100,
            maxHealth: 100,
            direction: { x: 0, y: 1 },
            isAttacking: false,
            attackCooldown: 0,
            invincible: 0,
            lastDirection: { x: 0, y: 1 }
        };

        const sword = {
            width: 40,
            height: 10,
            damage: 15,
            knockback: 15,
            active: false,
            angle: 0,
            swingSpeed: 0.1,
            swingProgress: 0,
            swingDirection: 1
        };

        const enemies = [];
        let score = 0;
        let gameOver = false;
        let particles = [];

        // Input handling
        document.addEventListener('keydown', (e) => {
            keys[e.key.toLowerCase()] = true;
            if (e.key === ' ') {
                startAttack();
            }
        });

        document.addEventListener('keyup', (e) => {
            keys[e.key.toLowerCase()] = false;
        });

        function startAttack() {
            if (!player.isAttacking && player.attackCooldown <= 0) {
                player.isAttacking = true;
                sword.active = true;
                sword.swingProgress = 0;
                sword.swingDirection = Math.random() > 0.5 ? 1 : -1;
            }
        }

        function spawnEnemy() {
            const x = Math.random() * canvas.width;
            const y = Math.random() * canvas.height;
            enemies.push({
                x: x,
                y: y,
                width: 32,
                height: 32,
                speed: 1 + Math.random() * 1.5,
                health: 30,
                maxHealth: 30,
                knockback: { x: 0, y: 0, timer: 0 }
            });
        }

        function updatePlayer() {
            // Movement
            player.direction = { x: 0, y: 0 };
            if (keys['w']) player.direction.y = -1;
            if (keys['s']) player.direction.y = 1;
            if (keys['a']) player.direction.x = -1;
            if (keys['d']) player.direction.x = 1;

            // Normalize diagonal movement
            if (player.direction.x !== 0 || player.direction.y !== 0) {
                const length = Math.sqrt(player.direction.x * player.direction.x + player.direction.y * player.direction.y);
                player.direction.x /= length;
                player.direction.y /= length;
                player.lastDirection = { ...player.direction };
            }

            player.x += player.direction.x * player.speed;
            player.y += player.direction.y * player.speed;

            // Boundary check
            player.x = Math.max(0, Math.min(canvas.width - player.width, player.x));
            player.y = Math.max(0, Math.min(canvas.height - player.height, player.y));

            // Attack cooldown
            if (player.attackCooldown > 0) {
                player.attackCooldown--;
            }

            // Invincibility timer
            if (player.invincible > 0) {
                player.invincible--;
            }

            // Sword swing animation
            if (sword.active) {
                sword.swingProgress += sword.swingSpeed;
                if (sword.swingProgress >= Math.PI) {
                    sword.active = false;
                    player.isAttacking = false;
                    player.attackCooldown = 20;
                }
            }
        }

        function updateEnemies() {
            for (let i = enemies.length - 1; i >= 0; i--) {
                const enemy = enemies[i];

                // Apply knockback if active
                if (enemy.knockback.timer > 0) {
                    enemy.x += enemy.knockback.x;
                    enemy.y += enemy.knockback.y;
                    enemy.knockback.timer--;
                } else {
                    // Chase player
                    const dx = player.x - enemy.x;
                    const dy = player.y - enemy.y;
                    const distance = Math.sqrt(dx * dx + dy * dy);
                    if (distance > 0) {
                        enemy.x += (dx / distance) * enemy.speed;
                        enemy.y += (dy / distance) * enemy.speed;
                    }
                }

                // Check collision with player
                if (checkCollision(player, enemy) && player.invincible <= 0) {
                    player.health -= 10;
                    player.invincible = 30;
                    if (player.health <= 0) {
                        gameOver = true;
                    }
                }

                // Check sword hit
                if (sword.active && checkSwordHit(enemy)) {
                    // Calculate knockback direction
                    const dx = enemy.x - player.x;
                    const dy = enemy.y - player.y;
                    const distance = Math.sqrt(dx * dx + dy * dy);
                    const knockbackX = (dx / distance) * sword.knockback;
                    const knockbackY = (dy / distance) * sword.knockback;

                    enemy.health -= sword.damage;
                    enemy.knockback = { x: knockbackX, y: knockbackY, timer: 10 };
                    createSlashEffect(enemy.x, enemy.y);

                    if (enemy.health <= 0) {
                        enemies.splice(i, 1);
                        score += 10;
                        createParticles(enemy.x, enemy.y, 10);
                    }
                }
            }
        }

        function checkCollision(a, b) {
            return (
                a.x < b.x + b.width &&
                a.x + a.width > b.x &&
                a.y < b.y + b.height &&
                a.y + a.height > b.y
            );
        }

        function checkSwordHit(enemy) {
            const swordX = player.x + player.width / 2;
            const swordY = player.y + player.height / 2;
            const swordLength = 40;
            const swordWidth = 10;
            const angle = Math.atan2(player.lastDirection.y, player.lastDirection.x) + 
                         (swingDirection() * sword.swingProgress);

            // Calculate sword position
            const swordEndX = swordX + Math.cos(angle) * swordLength;
            const swordEndY = swordY + Math.sin(angle) * swordLength;

            // Check if enemy is within sword range
            const dx = enemy.x + enemy.width/2 - swordX;
            const dy = enemy.y + enemy.height/2 - swordY;
            const distance = Math.sqrt(dx * dx + dy * dy);
            const angleToEnemy = Math.atan2(dy, dx);
            const angleDiff = Math.abs(angle - angleToEnemy);

            return distance < swordLength && angleDiff < 0.5;
        }

        function swingDirection() {
            return sword.swingDirection;
        }

        function createSlashEffect(x, y) {
            for (let i = 0; i < 5; i++) {
                particles.push({
                    x: x + Math.random() * 20 - 10,
                    y: y + Math.random() * 20 - 10,
                    size: 2 + Math.random() * 3,
                    color: '#FFFF00',
                    speed: 1 + Math.random() * 2,
                    angle: Math.random() * Math.PI * 2,
                    life: 30 + Math.random() * 30
                });
            }
        }

        function createParticles(x, y, count) {
            for (let i = 0; i < count; i++) {
                particles.push({
                    x: x + Math.random() * 20 - 10,
                    y: y + Math.random() * 20 - 10,
                    size: 1 + Math.random() * 3,
                    color: '#FF0000',
                    speed: 0.5 + Math.random() * 2,
                    angle: Math.random() * Math.PI * 2,
                    life: 20 + Math.random() * 20
                });
            }
        }

        function updateParticles() {
            for (let i = particles.length - 1; i >= 0; i--) {
                const p = particles[i];
                p.x += Math.cos(p.angle) * p.speed;
                p.y += Math.sin(p.angle) * p.speed;
                p.life--;
                if (p.life <= 0) {
                    particles.splice(i, 1);
                }
            }
        }

        function drawPlayer() {
            // Draw player
            if (player.invincible <= 0 || Math.floor(Date.now() / 100) % 2 === 0) {
                ctx.fillStyle = '#00FF00';
                ctx.fillRect(player.x, player.y, player.width, player.height);
            }

            // Draw sword
            if (sword.active) {
                const swordX = player.x + player.width / 2;
                swordY = player.y + player.height / 2;
                const swordLength = 40;
                const angle = Math.atan2(player.lastDirection.y, player.lastDirection.x) + 
                             (swingDirection() * sword.swingProgress);

                ctx.save();
                ctx.translate(swordX, swordY);
                ctx.rotate(angle);
                ctx.fillStyle = '#FFFF00';
                ctx.fillRect(0, -sword.height/2, swordLength, sword.height);
                ctx.restore();
            }
        }

        function drawEnemies() {
            ctx.fillStyle = '#FF0000';
            for (const enemy of enemies) {
                if (enemy.knockback.timer <= 0 || Math.floor(Date.now() / 100) % 2 === 0) {
                    ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
                }
            }
        }

        function drawParticles() {
            for (const p of particles) {
                ctx.fillStyle = p.color;
                ctx.fillRect(p.x, p.y, p.size, p.size);
            }
        }

        function drawUI() {
            ctx.fillStyle = '#FFFFFF';
            ctx.font = '20px Arial';
            ctx.textAlign = 'left';
            ctx.fillText(`Score: ${score}`, 20, 30);
            ctx.fillText(`Health: ${player.health}/${player.maxHealth}`, 20, 60);
        }

        function gameLoop() {
            if (gameOver) {
                ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
                ctx.fillRect(0, 0, canvas.width, canvas.height);
                ctx.fillStyle = '#FFFFFF';
                ctx.font = '48px Arial';
                ctx.textAlign = 'center';
                ctx.fillText('GAME OVER', canvas.width/2, canvas.height/2);
                ctx.font = '24px Arial';
                ctx.fillText(`Score: ${score}`, canvas.width/2, canvas.height/2 + 50);
                return;
            }

            // Clear canvas
            ctx.fillStyle = '#222';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            // Spawn enemies
            if (Math.random() < 0.01 && enemies.length < 10) {
                spawnEnemy();
            }

            // Update game state
            updatePlayer();
            updateEnemies();
            updateParticles();

            // Draw game
            drawEnemies();
            drawPlayer();
            drawParticles();
            drawUI();

            requestAnimationFrame(gameLoop);
        }

        // Start game
        gameLoop();
    </script>
</body>
</html>

This version includes:

  1. WASD movement controls (W for up, A for left, S for down, D for right)
  2. Spacebar to attack with the sword
  3. A visible sword with a swinging animation
  4. Knockback effect when enemies are hit by the sword
  5. Slash effects when hitting enemies
  6. Particle effects for visual feedback
  7. Enemy health and knockback system
  8. Player invincibility frames when hit

The sword swings in an arc in front of the player and applies knockback to enemies when they're hit. The direction of the swing alternates between left and right for variety. Enemies will chase the player and can be knocked back when hit by the sword.*

make the sword have a cool flame effect when clicking space which gives the player the sword. Also make the sword more realistic. also make it so the cursor controls the sword when summoned. make the sword knockback

Here's the enhanced version with flame effects, cursor-controlled sword, and improved knockback mechanics:

<!DOCTYPE html>
<html>
<head>
    <title>Flame Sword Ninja</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #222;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            cursor: none;
        }
        canvas {
            border: 4px solid #444;
            image-rendering: pixelated;
        }
        #cursor {
            position: absolute;
            width: 16px;
            height: 16px;
            pointer-events: none;
            z-index: 100;
            display: none;
        }
    </style>
</head>
<body>
    <canvas id="gameCanvas"></canvas>
    <div id="cursor"></div>
    <script>
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const cursor = document.getElementById('cursor');
        canvas.width = 800;
        canvas.height = 600;

        // Game state
        const keys = {};
        const player = {
            x: 400,
            y: 300,
            width: 32,
            height: 32,
            speed: 3,
            health: 100,
            maxHealth: 100,
            direction: { x: 0, y: 1 },
            isAttacking: false,
            attackCooldown: 0,
            invincible: 0,
            hasSword: false
        };

        const sword = {
            width: 8,
            length: 60,
            damage: 20,
            knockback: 20,
            active: false,
            angle: 0,
            swingSpeed: 0.2,
            swingProgress: 0,
            flameIntensity: 0,
            x: 0,
            y: 0
        };

        const enemies = [];
        let score = 0;
        let gameOver = false;
        let particles = [];
        let mouseX = 0;
        let mouseY = 0;

        // Input handling
        document.addEventListener('keydown', (e) => {
            keys[e.key.toLowerCase()] = true;
            if (e.key === ' ' && !player.hasSword) {
                summonSword();
            }
        });

        document.addEventListener('keyup', (e) => {
            keys[e.key.toLowerCase()] = false;
        });

        document.addEventListener('mousemove', (e) => {
            const rect = canvas.getBoundingClientRect();
            mouseX = e.clientX - rect.left;
            mouseY = e.clientY - rect.top;
        });

        document.addEventListener('mousedown', (e) => {
            if (player.hasSword && !player.isAttacking) {
                swingSword();
            }
        });

        function summonSword() {
            player.hasSword = true;
            cursor.style.display = 'block';
            cursor.style.background = 'radial-gradient(circle, #ff6600, #ff3300)';
            cursor.style.borderRadius = '50%';
            cursor.style.boxShadow = '0 0 10px #ff6600';
            
            // Flame summon effect
            for (let i = 0; i < 50; i++) {
                particles.push({
                    x: player.x + player.width/2,
                    y: player.y + player.height/2,
                    size: 2 + Math.random() * 4,
                    color: `hsl(${20 + Math.random() * 20}, 100%, 50%)`,
                    speed: 1 + Math.random() * 3,
                    angle: Math.random() * Math.PI * 2,
                    life: 30 + Math.random() * 30
                });
            }
        }

        function swingSword() {
            if (player.attackCooldown <= 0) {
                player.isAttacking = true;
                sword.active = true;
                sword.swingProgress = 0;
                sword.flameIntensity = 1;
            }
        }

        function spawnEnemy() {
            const x = Math.random() * canvas.width;
            const y = Math.random() * canvas.height;
            enemies.push({
                x: x,
                y: y,
                width: 32,
                height: 32,
                speed: 1 + Math.random() * 1.5,
                health: 30,
                maxHealth: 30,
                knockback: { x: 0, y: 0, timer: 0 }
            });
        }

        function updatePlayer() {
            // Movement
            player.direction = { x: 0, y: 0 };
            if (keys['w']) player.direction.y = -1;
            if (keys['s']) player.direction.y = 1;
            if (keys['a']) player.direction.x = -1;
            if (keys['d']) player.direction.x = 1;

            // Normalize diagonal movement
            if (player.direction.x !== 0 || player.direction.y !== 0) {
                const length = Math.sqrt(player.direction.x * player.direction.x + player.direction.y * player.direction.y);
                player.direction.x /= length;
                player.direction.y /= length;
            }

            player.x += player.direction.x * player.speed;
            player.y += player.direction.y * player.speed;

            // Boundary check
            player.x = Math.max(0, Math.min(canvas.width - player.width, player.x));
            player.y = Math.max(0, Math.min(canvas.height - player.height, player.y));

            // Attack cooldown
            if (player.attackCooldown > 0) {
                player.attackCooldown--;
            }

            // Invincibility timer
            if (player.invincible > 0) {
                player.invincible--;
            }

            // Sword position and angle
            if (player.hasSword) {
                const dx = mouseX - (player.x + player.width/2);
                const dy = mouseY - (player.y + player.height/2);
                sword.angle = Math.atan2(dy, dx);
                
                // Update cursor position
                cursor.style.left = (mouseX - 8) + 'px';
                cursor.style.top = (mouseY - 8) + 'px';
            }

            // Sword swing animation
            if (sword.active) {
                sword.swingProgress += sword.swingSpeed;
                sword.flameIntensity = Math.max(0, sword.flameIntensity - 0.02);
                
                if (sword.swingProgress >= Math.PI/2) {
                    sword.active = false;
                    player.isAttacking = false;
                    player.attackCooldown = 15;
                }
                
                // Flame particles during swing
                if (Math.random() < 0.3) {
                    const baseX = player.x + player.width/2;
                    const baseY = player.y + player.height/2;
                    const swordTipX = baseX + Math.cos(sword.angle) * sword.length;
                    const swordTipY = baseY + Math.sin(sword.angle) * sword.length;
                    
                    particles.push({
                        x: swordTipX + Math.random() * 10 - 5,
                        y: swordTipY + Math.random() * 10 - 5,
                        size: 1 + Math.random() * 3,
                        color: `hsl(${20 + Math.random() * 20}, 100%, 50%)`,
                        speed: 0.5 + Math.random() * 1,
                        angle: sword.angle + Math.PI + (Math.random() * 0.4 - 0.2),
                        life: 20 + Math.random() * 20
                    });
                }
            }
        }

        function updateEnemies() {
            for (let i = enemies.length - 1; i >= 0; i--) {
                const enemy = enemies[i];

                // Apply knockback if active
                if (enemy.knockback.timer > 0) {
                    enemy.x += enemy.knockback.x;
                    enemy.y += enemy.knockback.y;
                    enemy.knockback.timer--;
                } else {
                    // Chase player
                    const dx = player.x - enemy.x;
                    const dy = player.y - enemy.y;
                    const distance = Math.sqrt(dx * dx + dy * dy);
                    if (distance > 0) {
                        enemy.x += (dx / distance) * enemy.speed;
                        enemy.y += (dy / distance) * enemy.speed;
                    }
                }

                // Check collision with player
                if (checkCollision(player, enemy) && player.invincible <= 0) {
                    player.health -= 10;
                    player.invincible = 30;
                    if (player.health <= 0) {
                        gameOver = true;
                    }
                }

                // Check sword hit
                if (sword.active && checkSwordHit(enemy)) {
                    // Calculate knockback direction
                    const dx = enemy.x - player.x;
                    const dy = enemy.y - player.y;
                    const distance = Math.sqrt(dx * dx + dy * dy);
                    const knockbackX = (dx / distance) * sword.knockback;
                    const knockbackY = (dy / distance) * sword.knockback;

                    enemy.health -= sword.damage;
                    enemy.knockback = { x: knockbackX, y: knockbackY, timer: 15 };
                    createSlashEffect(enemy.x, enemy.y);

                    if (enemy.health <= 0) {
                        enemies.splice(i, 1);
                        score += 10;
                        createParticles(enemy.x, enemy.y, 15);
                    }
                }
            }
        }

        function checkCollision(a, b) {
            return (
                a.x < b.x + b.width &&
                a.x + a.width > b.x &&
                a.y < b.y + b.height &&
                a.y + a.height > b.y
            );
        }

        function checkSwordHit(enemy) {
            const baseX = player.x + player.width/2;
            const baseY = player.y + player.height/2;
            const swordTipX = baseX + Math.cos(sword.angle) * sword.length;
            const swordTipY = baseY + Math.sin(sword.angle) * sword.length;
            
            // Check if enemy is within sword range
            const enemyCenterX = enemy.x + enemy.width/2;
            const enemyCenterY = enemy.y + enemy.height/2;
            
            // Distance from enemy to sword line
            const u = ((enemyCenterX - baseX) * (swordTipX - baseX) + 
                      (enemyCenterY - baseY) * (swordTipY - baseY)) / 
                     (Math.pow(swordTipX - baseX, 2) + Math.pow(swordTipY - baseY, 2));
            
            const closestX = baseX + u * (swordTipX - baseX);
            const closestY = baseY + u * (swordTipY - baseY);
            
            const distance = Math.sqrt(Math.pow(enemyCenterX - closestX, 2) + 
                                      Math.pow(enemyCenterY - closestY, 2));
            
            return distance < (enemy.width/2 + sword.width/2) && 
                   u >= 0 && u <= 1 && 
                   sword.swingProgress < Math.PI/4; // Only hit during first half of swing
        }

        function createSlashEffect(x, y) {
            for (let i = 0; i < 10; i++) {
                particles.push({
                    x: x + Math.random() * 20 - 10,
                    y: y + Math.random() * 20 - 10,
                    size: 2 + Math.random() * 3,
                    color: '#FFFF00',
                    speed: 1 + Math.random() * 2,
                    angle: Math.random() * Math.PI * 2,
                    life: 30 + Math.random() * 30
                });
            }
        }

        function createParticles(x, y, count) {
            for (let i = 0; i < count; i++) {
                particles.push({
                    x: x + Math.random() * 20 - 10,
                    y: y + Math.random() * 20 - 10,
                    size: 1 + Math.random() * 3,
                    color: '#FF0000',
                    speed: 0.5 + Math.random() * 2,
                    angle: Math.random() * Math.PI * 2,
                    life: 20 + Math.random() * 20
                });
            }
        }

        function updateParticles() {
            for (let i = particles.length - 1; i >= 0; i--) {
                const p = particles[i];
                p.x += Math.cos(p.angle) * p.speed;
                p.y += Math.sin(p.angle) * p.speed;
                p.life--;
                if (p.life <= 0) {
                    particles.splice(i, 1);
                }
            }
        }

        function drawPlayer() {
            // Draw player
            if (player.invincible <= 0 || Math.floor(Date.now() / 100) % 2 === 0) {
                ctx.fillStyle = '#00FF00';
                ctx.fillRect(player.x, player.y, player.width, player.height);
            }
        }

        function drawSword() {
            if (!player.hasSword) return;
            
            const baseX = player.x + player.width/2;
            const baseY = player.y + player.height/2;
            const swordTipX = baseX + Math.cos(sword.angle) * sword.length;
            const swordTipY = baseY + Math.sin(sword.angle) * sword.length;
            
            // Draw sword blade
            ctx.save();
            ctx.beginPath();
            ctx.moveTo(baseX, baseY);
            ctx.lineTo(swordTipX, swordTipY);
            ctx.lineWidth = sword.width;
            ctx.strokeStyle = '#999';
            ctx.stroke();
            
            // Draw sword glow when attacking
            if (sword.active) {
                ctx.lineWidth = sword.width + 4;
                ctx.strokeStyle = `rgba(255, 200, 0, ${sword.flameIntensity})`;
                ctx.stroke();
                
                ctx.lineWidth = sword.width + 8;
                ctx.strokeStyle = `rgba(255, 100, 0, ${sword.flameIntensity * 0.7})`;
                ctx.stroke();
            }
            
            // Draw sword hilt
            ctx.beginPath();
            ctx.arc(baseX, baseY, sword.width * 1.5, 0, Math.PI * 2);
            ctx.fillStyle = '#555';
            ctx.fill();
            ctx.restore();
        }

        function drawEnemies() {
            ctx.fillStyle = '#FF0000';
            for (const enemy of enemies) {
                if (enemy.knockback.timer <= 0 || Math.floor(Date.now() / 100) % 2 === 0) {
                    ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
                }
            }
        }

        function drawParticles() {
            for (const p of particles) {
                ctx.globalAlpha = p.life / 50;
                ctx.fillStyle = p.color;
                ctx.fillRect(p.x, p.y, p.size, p.size);
            }
            ctx.globalAlpha = 1;
        }

        function drawUI() {
            ctx.fillStyle = '#FFFFFF';
            ctx.font = '20px Arial';
            ctx.textAlign = 'left';
            ctx.fillText(`Score: ${score}`, 20, 30);
            ctx.fillText(`Health: ${player.health}/${player.maxHealth}`, 20, 60);
            
            if (!player.hasSword) {
                ctx.fillText('Press SPACE to summon flame sword', 20, 90);
            } else if (!player.isAttacking) {
                ctx.fillText('Click to swing sword', 20, 90);
            }
        }

        function gameLoop() {
            if (gameOver) {
                ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
                ctx.fillRect(0, 0, canvas.width, canvas.height);
                ctx.fillStyle = '#FFFFFF';
                ctx.font = '48px Arial';
                ctx.textAlign = 'center';
                ctx.fillText('GAME OVER', canvas.width/2, canvas.height/2);
                ctx.font = '24px Arial';
                ctx.fillText(`Score: ${score}`, canvas.width/2, canvas.height/2 + 50);
                return;
            }

            // Clear canvas
            ctx.fillStyle = '#222';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            // Spawn enemies
            if (Math.random() < 0.01 && enemies.length < 10) {
                spawnEnemy();
            }

            // Update game state
            updatePlayer();
            updateEnemies();
            updateParticles();

            // Draw game
            drawEnemies();
            drawPlayer();
            drawSword();
            drawParticles();
            drawUI();

            requestAnimationFrame(gameLoop);
        }

        // Start game
        gameLoop();
    </script>
</body>
</html>

Key Features:

  1. Flame Sword Summoning: Press SPACE to summon a flaming sword with a dramatic particle effect
  2. Cursor-Controlled Sword: The sword follows your mouse cursor when summoned
  3. Realistic Sword Physics:
    • Proper sword angle calculation
    • Smooth swing animation
    • Accurate hit detection
  4. Enhanced Knockback:
    • Strong directional knockback based on swing angle
    • Visual feedback when enemies are hit
  5. Flame Effects:
    • Glowing sword trail during swings
    • Flame particles that emit from the sword
    • Custom cursor that looks like a flame
  6. Improved Visuals:
    • Proper sword rendering with hilt and blade
    • Dynamic glow effects when attacking
    • Particle effects for hits and enemy deaths

Controls:

  • WASD to move
  • SPACE to summon the sword
  • Mouse click to swing the sword (when summoned)
  • Mouse movement controls sword direction

The sword now has proper physics and looks much more realistic with:

  • A metal blade that glows when swinging
  • A visible hilt
  • Flame particles that trail behind it
  • Strong knockback effects on enemies
  • Accurate hit detection based on sword angle*
make a flame slash ability that launches a flaming slash that knocks back as well. Also make it so the health slowly goes back up when if being damaged

Here's the enhanced version with a flame slash ability, health regeneration, and improved combat mechanics:

<!DOCTYPE html>
<html>
<head>
    <title>Flame Sword Ninja</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
            background-color: #222;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            cursor: none;
        }
        canvas {
            border: 4px solid #444;
            image-rendering: pixelated;
        }
        #cursor {
            position: absolute;
            width: 16px;
            height: 16px;
            pointer-events: none;
            z-index: 100;
            display: none;
        }
    </style>
</head>
<body>
    <canvas id="gameCanvas"></canvas>
    <div id="cursor"></div>
    <script>
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const cursor = document.getElementById('cursor');
        canvas.width = 800;
        canvas.height = 600;

        // Game state
        const keys = {};
        const player = {
            x: 400,
            y: 300,
            width: 32,
            height: 32,
            speed: 3,
            health: 100,
            maxHealth: 100,
            direction: { x: 0, y: 1 },
            isAttacking: false,
            attackCooldown: 0,
            invincible: 0,
            hasSword: false,
            regenTimer: 0,
            lastHitTime: 0
        };

        const sword = {
            width: 8,
            length: 60,
            damage: 20,
            knockback: 20,
            active: false,
            angle: 0,
            swingSpeed: 0.2,
            swingProgress: 0,
            flameIntensity: 0,
            x: 0,
            y: 0,
            specialCooldown: 0
        };

        const flameSlash = {
            active: false,
            x: 0,
            y: 0,
            angle: 0,
            speed: 8,
            distance: 0,
            maxDistance: 300,
            width: 40,
            damage: 30,
            knockback: 30,
            particles: []
        };

        const enemies = [];
        let score = 0;
        let gameOver = false;
        let particles = [];
        let mouseX = 0;
        let mouseY = 0;
        let gameTime = 0;

        // Input handling
        document.addEventListener('keydown', (e) => {
            keys[e.key.toLowerCase()] = true;
            if (e.key === ' ' && !player.hasSword) {
                summonSword();
            }
            if (e.key === 'e' && player.hasSword && sword.specialCooldown <= 0) {
                launchFlameSlash();
            }
        });

        document.addEventListener('keyup', (e) => {
            keys[e.key.toLowerCase()] = false;
        });

        document.addEventListener('mousemove', (e) => {
            const rect = canvas.getBoundingClientRect();
            mouseX = e.clientX - rect.left;
            mouseY = e.clientY - rect.top;
        });

        document.addEventListener('mousedown', (e) => {
            if (player.hasSword && !player.isAttacking) {
                swingSword();
            }
        });

        function summonSword() {
            player.hasSword = true;
            cursor.style.display = 'block';
            cursor.style.background = 'radial-gradient(circle, #ff6600, #ff3300)';
            cursor.style.borderRadius = '50%';
            cursor.style.boxShadow = '0 0 10px #ff6600';
            
            // Flame summon effect
            for (let i = 0; i < 50; i++) {
                particles.push({
                    x: player.x + player.width/2,
                    y: player.y + player.height/2,
                    size: 2 + Math.random() * 4,
                    color: `hsl(${20 + Math.random() * 20}, 100%, 50%)`,
                    speed: 1 + Math.random() * 3,
                    angle: Math.random() * Math.PI * 2,
                    life: 30 + Math.random() * 30
                });
            }
        }

        function launchFlameSlash() {
            if (sword.specialCooldown <= 0) {
                flameSlash.active = true;
                flameSlash.x = player.x + player.width/2;
                flameSlash.y = player.y + player.height/2;
                flameSlash.angle = sword.angle;
                flameSlash.distance = 0;
                sword.specialCooldown = 120; // 2 second cooldown
                
                // Initial flame effect
                for (let i = 0; i < 30; i++) {
                    flameSlash.particles.push({
                        x: flameSlash.x,
                        y: flameSlash.y,
                        size: 2 + Math.random() * 4,
                        color: `hsl(${20 + Math.random() * 20}, 100%, 50%)`,
                        speed: 0.5 + Math.random() * 2,
                        angle: flameSlash.angle + (Math.random() * 0.4 - 0.2),
                        life: 20 + Math.random() * 20
                    });
                }
            }
        }

        function swingSword() {
            if (player.attackCooldown <= 0) {
                player.isAttacking = true;
                sword.active = true;
                sword.swingProgress = 0;
                sword.flameIntensity = 1;
            }
        }

        function spawnEnemy() {
            const x = Math.random() * canvas.width;
            const y = Math.random() * canvas.height;
            enemies.push({
                x: x,
                y: y,
                width: 32,
                height: 32,
                speed: 1 + Math.random() * 1.5,
                health: 30,
                maxHealth: 30,
                knockback: { x: 0, y: 0, timer: 0 }
            });
        }

        function updatePlayer() {
            // Movement
            player.direction = { x: 0, y: 0 };
            if (keys['w']) player.direction.y = -1;
            if (keys['s']) player.direction.y = 1;
            if (keys['a']) player.direction.x = -1;
            if (keys['d']) player.direction.x = 1;

            // Normalize diagonal movement
            if (player.direction.x !== 0 || player.direction.y !== 0) {
                const length = Math.sqrt(player.direction.x * player.direction.x + player.direction.y * player.direction.y);
                player.direction.x /= length;
                player.direction.y /= length;
            }

            player.x += player.direction.x * player.speed;
            player.y += player.direction.y * player.speed;

            // Boundary check
            player.x = Math.max(0, Math.min(canvas.width - player.width, player.x));
            player.y = Math.max(0, Math.min(canvas.height - player.height, player.y));

            // Attack cooldown
            if (player.attackCooldown > 0) {
                player.attackCooldown--;
            }

            // Invincibility timer
            if (player.invincible > 0) {
                player.invincible--;
            }

            // Special cooldown
            if (sword.specialCooldown > 0) {
                sword.specialCooldown--;
            }

            // Health regeneration (slower when recently hit)
            player.regenTimer++;
            const timeSinceHit = gameTime - player.lastHitTime;
            if (player.regenTimer % 10 === 0 && timeSinceHit > 60) {
                const regenRate = timeSinceHit > 180 ? 2 : 1; // Faster regen after 3 seconds
                player.health = Math.min(player.maxHealth, player.health + regenRate);
            }

            // Sword position and angle
            if (player.hasSword) {
                const dx = mouseX - (player.x + player.width/2);
                const dy = mouseY - (player.y + player.height/2);
                sword.angle = Math.atan2(dy, dx);
                
                // Update cursor position
                cursor.style.left = (mouseX - 8) + 'px';
                cursor.style.top = (mouseY - 8) + 'px';
            }

            // Sword swing animation
            if (sword.active) {
                sword.swingProgress += sword.swingSpeed;
                sword.flameIntensity = Math.max(0, sword.flameIntensity - 0.02);
                
                if (sword.swingProgress >= Math.PI/2) {
                    sword.active = false;
                    player.isAttacking = false;
                    player.attackCooldown = 15;
                }
                
                // Flame particles during swing
                if (Math.random() < 0.3) {
                    const baseX = player.x + player.width/2;
                    const baseY = player.y + player.height/2;
                    const swordTipX = baseX + Math.cos(sword.angle) * sword.length;
                    const swordTipY = baseY + Math.sin(sword.angle) * sword.length;
                    
                    particles.push({
                        x: swordTipX + Math.random() * 10 - 5,
                        y: swordTipY + Math.random() * 10 - 5,
                        size: 1 + Math.random() * 3,
                        color: `hsl(${20 + Math.random() * 20}, 100%, 50%)`,
                        speed: 0.5 + Math.random() * 1,
                        angle: sword.angle + Math.PI + (Math.random() * 0.4 - 0.2),
                        life: 20 + Math.random() * 20
                    });
                }
            }
        }

        function updateFlameSlash() {
            if (!flameSlash.active) return;

            // Move flame slash
            flameSlash.x += Math.cos(flameSlash.angle) * flameSlash.speed;
            flameSlash.y += Math.sin(flameSlash.angle) * flameSlash.speed;
            flameSlash.distance += flameSlash.speed;

            // Add trail particles
            if (Math.random() < 0.5) {
                flameSlash.particles.push({
                    x: flameSlash.x,
                    y: flameSlash.y,
                    size: 2 + Math.random() * 3,
                    color: `hsl(${15 + Math.random() * 30}, 100%, 50%)`,
                    speed: 0.2 + Math.random() * 0.5,
                    angle: flameSlash.angle + Math.PI + (Math.random() * 0.6 - 0.3),
                    life: 15 + Math.random() * 15
                });
            }

            // Check for enemy hits
            for (let i = enemies.length - 1; i >= 0; i--) {
                const enemy = enemies[i];
                const dx = enemy.x + enemy.width/2 - flameSlash.x;
                const dy = enemy.y + enemy.height/2 - flameSlash.y;
                const distance = Math.sqrt(dx * dx + dy * dy);

                if (distance < (enemy.width/2 + flameSlash.width/2)) {
                    // Calculate knockback direction
                    const knockbackX = (dx / distance) * flameSlash.knockback;
                    const knockbackY = (dy / distance) * flameSlash.knockback;

                    enemy.health -= flameSlash.damage;
                    enemy.knockback = { x: knockbackX, y: knockbackY, timer: 20 };
                    createExplosionEffect(enemy.x, enemy.y);

                    if (enemy.health <= 0) {
                        enemies.splice(i, 1);
                        score += 15; // Bonus points for special attack kill
                        createParticles(enemy.x, enemy.y, 20);
                    }
                }
            }

            // Check if flame slash has traveled max distance or is out of bounds
            if (flameSlash.distance >= flameSlash.maxDistance ||
                flameSlash.x < -50 || flameSlash.x > canvas.width + 50 ||
                flameSlash.y < -50 || flameSlash.y > canvas.height + 50) {
                flameSlash.active = false;
                createExplosionEffect(flameSlash.x, flameSlash.y);
            }

            // Update flame slash particles
            for (let i = flameSlash.particles.length - 1; i >= 0; i--) {
                const p = flameSlash.particles[i];
                p.x += Math.cos(p.angle) * p.speed;
                p.y += Math.sin(p.angle) * p.speed;
                p.life--;
                if (p.life <= 0) {
                    flameSlash.particles.splice(i, 1);
                }
            }
        }

        function updateEnemies() {
            for (let i = enemies.length - 1; i >= 0; i--) {
                const enemy = enemies[i];

                // Apply knockback if active
                if (enemy.knockback.timer > 0) {
                    enemy.x += enemy.knockback.x;
                    enemy.y += enemy.knockback.y;
                    enemy.knockback.timer--;
                } else {
                    // Chase player
                    const dx = player.x - enemy.x;
                    const dy = player.y - enemy.y;
                    const distance = Math.sqrt(dx * dx + dy * dy);
                    if (distance > 0) {
                        enemy.x += (dx / distance) * enemy.speed;
                        enemy.y += (dy / distance) * enemy.speed;
                    }
                }

                // Check collision with player
                if (checkCollision(player, enemy) && player.invincible <= 0) {
                    player.health -= 10;
                    player.invincible = 30;
                    player.lastHitTime = gameTime;
                    if (player.health <= 0) {
                        gameOver = true;
                    }
                }

                // Check sword hit
                if (sword.active && checkSwordHit(enemy)) {
                    // Calculate knockback direction
                    const dx = enemy.x - player.x;
                    const dy = enemy.y - player.y;
                    const distance = Math.sqrt(dx * dx + dy * dy);
                    const knockbackX = (dx / distance) * sword.knockback;
                    const knockbackY = (dy / distance) * sword.knockback;

                    enemy.health -= sword.damage;
                    enemy.knockback = { x: knockbackX, y: knockbackY, timer: 15 };
                    createSlashEffect(enemy.x, enemy.y);

                    if (enemy.health <= 0) {
                        enemies.splice(i, 1);
                        score += 10;
                        createParticles(enemy.x, enemy.y, 15);
                    }
                }
            }
        }

        function checkCollision(a, b) {
            return (
                a.x < b.x + b.width &&
                a.x + a.width > b.x &&
                a.y < b.y + b.height &&
                a.y + a.height > b.y
            );
        }

        function checkSwordHit(enemy) {
            const baseX = player.x + player.width/2;
            const baseY = player.y + player.height/2;
            const swordTipX = baseX + Math.cos(sword.angle) * sword.length;
            const swordTipY = baseY + Math.sin(sword.angle) * sword.length;
            
            // Check if enemy is within sword range
            const enemyCenterX = enemy.x + enemy.width/2;
            const enemyCenterY = enemy.y + enemy.height/2;
            
            // Distance from enemy to sword line
            const u = ((enemyCenterX - baseX) * (swordTipX - baseX) + 
                      (enemyCenterY - baseY) * (swordTipY - baseY)) / 
                     (Math.pow(swordTipX - baseX, 2) + Math.pow(swordTipY - baseY, 2));
            
            const closestX = baseX + u * (swordTipX - baseX);
            const closestY = baseY + u * (swordTipY - baseY);
            
            const distance = Math.sqrt(Math.pow(enemyCenterX - closestX, 2) + 
                                      Math.pow(enemyCenterY - closestY, 2));
            
            return distance < (enemy.width/2 + sword.width/2) && 
                   u >= 0 && u <= 1 && 
                   sword.swingProgress < Math.PI/4;
        }

        function createSlashEffect(x, y) {
            for (let i = 0; i < 10; i++) {
                particles.push({
                    x: x + Math.random() * 20 - 10,
                    y: y + Math.random() * 20 - 10,
                    size: 2 + Math.random() * 3,
                    color: '#FFFF00',
                    speed: 1 + Math.random() * 2,
                    angle: Math.random() * Math.PI * 2,
                    life: 30 + Math.random() * 30
                });
            }
        }

        function createExplosionEffect(x, y) {
            for (let i = 0; i < 30; i++) {
                particles.push({
                    x: x + Math.random() * 30 - 15,
                    y: y + Math.random() * 30 - 15,
                    size: 2 + Math.random() * 4,
                    color: `hsl(${15 + Math.random() * 30}, 100%, 50%)`,
                    speed: 0.5 + Math.random() * 3,
                    angle: Math.random() * Math.PI * 2,
                    life: 20 + Math.random() * 30
                });
            }
        }

        function createParticles(x, y, count) {
            for (let i = 0; i < count; i++) {
                particles.push({
                    x: x + Math.random() * 20 - 10,
                    y: y + Math.random() * 20 - 10,
                    size: 1 + Math.random() * 3,
                    color: '#FF0000',
                    speed: 0.5 + Math.random() * 2,
                    angle: Math.random() * Math.PI * 2,
                    life: 20 + Math.random() * 20
                });
            }
        }

        function updateParticles() {
            for (let i = particles.length - 1; i >= 0; i--) {
                const p = particles[i];
                p.x += Math.cos(p.angle) * p.speed;
                p.y += Math.sin(p.angle) * p.speed;
                p.life--;
                if (p.life <= 0) {
                    particles.splice(i, 1);
                }
            }
        }

        function drawPlayer() {
            // Draw player
            if (player.invincible <= 0 || Math.floor(Date.now() / 100) % 2 === 0) {
                ctx.fillStyle = '#00FF00';
                ctx.fillRect(player.x, player.y, player.width, player.height);
            }
        }

        function drawSword() {
            if (!player.hasSword) return;
            
            const baseX = player.x + player.width/2;
            const baseY = player.y + player.height/2;
            const swordTipX = baseX + Math.cos(sword.angle) * sword.length;
            const swordTipY = baseY + Math.sin(sword.angle) * sword.length;
            
            // Draw sword blade
            ctx.save();
            ctx.beginPath();
            ctx.moveTo(baseX, baseY);
            ctx.lineTo(swordTipX, swordTipY);
            ctx.lineWidth = sword.width;
            ctx.strokeStyle = '#999';
            ctx.stroke();
            
            // Draw sword glow when attacking
            if (sword.active) {
                ctx.lineWidth = sword.width + 4;
                ctx.strokeStyle = `rgba(255, 200, 0, ${sword.flameIntensity})`;
                ctx.stroke();
                
                ctx.lineWidth = sword.width + 8;
                ctx.strokeStyle = `rgba(255, 100, 0, ${sword.flameIntensity * 0.7})`;
                ctx.stroke();
            }
            
            // Draw sword hilt
            ctx.beginPath();
            ctx.arc(baseX, baseY, sword.width * 1.5, 0, Math.PI * 2);
            ctx.fillStyle = '#555';
            ctx.fill();
            ctx.restore();
        }

        function drawFlameSlash() {
            if (!flameSlash.active) return;

            // Draw flame slash
            ctx.save();
            ctx.translate(flameSlash.x, flameSlash.y);
            ctx.rotate(flameSlash.angle);
            
            // Main slash
            ctx.fillStyle = `rgba(255, 100, 0, 0.7)`;
            ctx.fillRect(0, -flameSlash.width/2, 40, flameSlash.width);
            
            // Glow effect
            ctx.fillStyle = `rgba(255, 200, 0, 0.4)`;
            ctx.fillRect(0, -flameSlash.width/2 - 5, 30, flameSlash.width + 10);
            
            ctx.restore();

            // Draw particles
            for (const p of flameSlash.particles) {
                ctx.globalAlpha = p.life / 30;
                ctx.fillStyle = p.color;
                ctx.fillRect(p.x, p.y, p.size, p.size);
            }
            ctx.globalAlpha = 1;
        }

        function drawEnemies() {
            ctx.fillStyle = '#FF0000';
            for (const enemy of enemies) {
                if (enemy.knockback.timer <= 0 || Math.floor(Date.now() / 100) % 2 === 0) {
                    ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
                }
            }
        }

        function drawParticles() {
            for (const p of particles) {
                ctx.globalAlpha = p.life / 50;
                ctx.fillStyle = p.color;
                ctx.fillRect(p.x, p.y, p.size, p.size);
            }
            ctx.globalAlpha = 1;
        }

        function drawUI() {
            ctx.fillStyle = '#FFFFFF';
            ctx.font = '20px Arial';
            ctx.textAlign = 'left';
            ctx.fillText(`Score: ${score}`, 20, 30);
            
            // Health bar
            ctx.fillStyle = '#FF0000';
            ctx.fillRect(20, 60, 200, 20);
            ctx.fillStyle = '#00FF00';
            ctx.fillRect(20, 60, 200 * (player.health / player.maxHealth), 20);
            ctx.strokeStyle = '#FFFFFF';
            ctx.strokeRect(20, 60, 200, 20);
            ctx.fillStyle = '#FFFFFF';
            ctx.fillText(`Health: ${Math.floor(player.health)}/${player.maxHealth}`, 25, 77);
            
            // Special cooldown
            if (player.hasSword) {
                ctx.fillStyle = '#333333';
                ctx.fillRect(20, 90, 200, 10);
                ctx.fillStyle = '#FF9900';
                ctx.fillRect(20, 90, 200 * (1 - sword.specialCooldown/120), 10);
                ctx.fillStyle = '#FFFFFF';
                ctx.fillText('Flame Slash (E)', 25, 100);
            }
            
            if (!player.hasSword) {
                ctx.fillText('Press SPACE to summon flame sword', 20, 130);
            } else if (!player.isAttacking) {
                ctx.fillText('Click to swing sword', 20, 130);
            }
        }

        function gameLoop() {
            gameTime++;
            
            if (gameOver) {
                ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
                ctx.fillRect(0, 0, canvas.width, canvas.height);
                ctx.fillStyle = '#FFFFFF';
                ctx.font = '48px Arial';
                ctx.textAlign = 'center';
                ctx.fillText('GAME OVER', canvas.width/2, canvas.height/2);
                ctx.font = '24px Arial';
                ctx.fillText(`Score: ${score}`, canvas.width/2, canvas.height/2 + 50);
                ctx.fillText('Click to restart', canvas.width/2, canvas.height/2 + 100);
                
                canvas.addEventListener('click', restartGame, { once: true });
                return;
            }

            // Clear canvas
            ctx.fillStyle = '#222';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            // Spawn enemies
            if (Math.random() < 0.01 && enemies.length < 5 + wave) {
                spawnEnemy();
            }

            // Update game state
            updatePlayer();
            updateFlameSlash();
            updateEnemies();
            updateParticles();

            // Draw game
            drawEnemies();
            drawPlayer();
            drawSword();
            drawFlameSlash();
            drawParticles();
            drawUI();

            requestAnimationFrame(gameLoop);
        }

        function restartGame() {
            // Reset game state
            player.x = 400;
            player.y = 300;
            player.health = 100;
            player.hasSword = false;
            player.invincible = 0;
            player.lastHitTime = 0;
            
            enemies.length = 0;
            particles.length = 0;
            flameSlash.active = false;
            flameSlash.particles = [];
            
            score = 0;
            gameOver = false;
            gameTime = 0;
            
            cursor.style.display = 'none';
            
            // Start new game
            gameLoop();
        }

        // Start game
        gameLoop();
    </script>
</body>
</html>

Key New Features:

  1. Flame Slash Ability:

    • Press 'E' to launch a powerful flame projectile
    • Travels in a straight line with flame trail
    • Deals heavy damage and knockback to enemies
    • Has a cooldown period (shown in UI)
  2. Health Regeneration System:

    • Health slowly regenerates when not taking damage
    • Faster regeneration after 3 seconds without being hit
    • Visual health bar shows current health status
  3. Enhanced Combat:

    • More satisfying knockback effects
    • Improved particle effects for all attacks
    • Better visual feedback when hitting enemies
    • Special attack rewards bonus points
  4. Improved UI:

    • Health bar with numerical display
    • Cooldown indicator for special attack
    • Clear instructions for controls
    • Better game over screen with restart option

Controls:

  • WASD: Movement
  • SPACE: Summon sword
  • Mouse: Aim sword
  • Left Click: Swing sword
  • E: Flame Slash (when sword is summoned)

The game now has more strategic depth with the special attack and health management, while maintaining the fast-paced action of the original.*