Here's the updated version with chasing enemies and wave mechanics:
Here's the updated version with knockback mechanics for both the player and enemies:
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>
<!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:
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.*
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>
<!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:
Controls:
The sword now has proper physics and looks much more realistic with:
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>
<!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:
Flame Slash Ability:
Health Regeneration System:
Enhanced Combat:
Improved UI:
Controls:
The game now has more strategic depth with the special attack and health management, while maintaining the fast-paced action of the original.*