Medieval Battle
by FlashKoala39778 lines18.9 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Medieval Battle</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
touch-action: none;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #111;
font-family: Arial, sans-serif;
}
#game {
position: fixed;
inset: 0;
background: #4d8a3a;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
/* HUD */
#hud {
position: fixed;
top: 12px;
left: 12px;
right: 12px;
display: flex;
justify-content: space-between;
align-items: flex-start;
pointer-events: none;
z-index: 10;
}
.panel {
background: rgba(20,20,20,.78);
color: white;
padding: 9px 12px;
border-radius: 12px;
border: 1px solid rgba(255,255,255,.2);
box-shadow: 0 3px 10px rgba(0,0,0,.3);
}
#hpBar {
width: 150px;
height: 15px;
background: #401010;
border-radius: 10px;
overflow: hidden;
margin-top: 5px;
}
#hpFill {
height: 100%;
width: 100%;
background: linear-gradient(#55e75a,#159b28);
}
/* Controls */
#controls {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 20;
}
#joystick {
position: absolute;
left: 25px;
bottom: 25px;
width: 125px;
height: 125px;
border-radius: 50%;
background: rgba(0,0,0,.25);
border: 2px solid rgba(255,255,255,.3);
pointer-events: auto;
}
#stick {
position: absolute;
width: 55px;
height: 55px;
left: 33px;
top: 33px;
border-radius: 50%;
background: rgba(255,255,255,.45);
border: 2px solid white;
}
#attack {
position: absolute;
right: 25px;
bottom: 35px;
width: 95px;
height: 95px;
border-radius: 50%;
border: 4px solid rgba(255,255,255,.6);
background: radial-gradient(circle,#ff6363,#a90000);
color: white;
font-size: 34px;
box-shadow: 0 5px 15px rgba(0,0,0,.4);
pointer-events: auto;
}
#attack:active {
transform: scale(.9);
}
/* Message */
#message {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
color: white;
background: rgba(0,0,0,.85);
padding: 25px 30px;
border-radius: 18px;
text-align: center;
display: none;
z-index: 50;
}
#message h1 {
margin-bottom: 10px;
}
#restart {
margin-top: 15px;
padding: 12px 25px;
border: 0;
border-radius: 10px;
background: #d89b32;
color: white;
font-weight: bold;
font-size: 16px;
}
</style>
</head>
<body>
<div id="game">
<canvas id="canvas"></canvas>
</div>
<div id="hud">
<div class="panel">
<b>⚔️ Knight</b>
<div id="hpBar">
<div id="hpFill"></div>
</div>
</div>
<div class="panel">
🛡️ Allies: <span id="allyCount">0</span> | ☠️ Enemies: <span id="enemyCount">0</span>
</div>
</div>
<div id="controls">
<div id="joystick">
<div id="stick"></div>
</div>
<button id="attack">⚔</button>
</div>
<div id="message">
<h1 id="messageTitle">Game Over</h1>
<p id="messageText"></p>
<button id="restart">Play Again</button>
</div>
<script>
// --- AUDIO SYSTEM (Web Audio API) ---
let audioCtx = null;
function initAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playSoundAttack() {
initAudio();
if (!audioCtx) return;
let osc = audioCtx.createOscillator();
let gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(300, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(80, audioCtx.currentTime + 0.15);
gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.01, audioCtx.currentTime + 0.15);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.15);
}
function playSoundHit() {
initAudio();
if (!audioCtx) return;
let osc = audioCtx.createOscillator();
let gain = audioCtx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(150, audioCtx.currentTime);
osc.frequency.linearRampToValueAtTime(40, audioCtx.currentTime + 0.2);
gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.2);
}
function playSoundEnd(win) {
initAudio();
if (!audioCtx) return;
let osc = audioCtx.createOscillator();
let gain = audioCtx.createGain();
osc.type = win ? 'sine' : 'sawtooth';
if (win) {
osc.frequency.setValueAtTime(261.63, audioCtx.currentTime);
osc.frequency.setValueAtTime(329.63, audioCtx.currentTime + 0.15);
osc.frequency.setValueAtTime(392.00, audioCtx.currentTime + 0.3);
} else {
osc.frequency.setValueAtTime(120, audioCtx.currentTime);
osc.frequency.linearRampToValueAtTime(50, audioCtx.currentTime + 0.5);
}
gain.gain.setValueAtTime(0.4, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.01, audioCtx.currentTime + (win ? 0.6 : 0.5));
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + (win ? 0.6 : 0.5));
}
// --- GAME LOGIC ---
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let W, H;
function resize() {
W = canvas.width = window.innerWidth * devicePixelRatio;
H = canvas.height = window.innerHeight * devicePixelRatio;
canvas.style.width = window.innerWidth + "px";
canvas.style.height = window.innerHeight + "px";
ctx.setTransform(devicePixelRatio,0,0,devicePixelRatio,0,0);
W = window.innerWidth;
H = window.innerHeight;
}
window.addEventListener("resize", resize);
resize();
const world = { width: 5000, height: 5000 };
let camera = { x: 0, y: 0 };
const player = {
x: 2500,
y: 2500,
radius: 25,
speed: 3.5,
hp: 100,
maxHp: 100,
attackCooldown: 0,
direction: 0
};
let enemies = [];
let allies = [];
let decorations = [];
let enemySpawnTimer = 0;
let enemyExtraAdded = 0;
const MAX_EXTRA_ENEMIES = 25;
function random(min,max) {
return Math.random() * (max-min) + min;
}
function createWorld() {
enemies = [];
allies = [];
decorations = [];
enemySpawnTimer = 0;
enemyExtraAdded = 0;
// Trees
for(let i=0; i<300; i++) {
let x = random(100, world.width-100);
let y = random(100, world.height-100);
if(Math.hypot(x-player.x, y-player.y) < 400) continue;
decorations.push({ type: "tree", x, y, size: random(.8, 1.4) });
}
// Rocks
for(let i=0; i<150; i++) {
decorations.push({
type: "rock",
x: random(50, world.width-50),
y: random(50, world.height-50),
size: random(.6, 1.3)
});
}
// Initial Enemies
for(let i=0; i<20; i++) {
spawnEnemy();
}
}
function spawnEnemy() {
let angle = random(0, Math.PI*2);
let distance = random(800, 1800);
enemies.push({
x: player.x + Math.cos(angle)*distance,
y: player.y + Math.sin(angle)*distance,
radius: 23,
speed: random(1.1, 1.7),
hp: 50,
maxHp: 50,
attackCooldown: random(0, 60),
hitFlash: 0
});
}
function spawnAlly(x, y) {
allies.push({
x: x,
y: y,
radius: 23,
speed: 1.8,
hp: 40,
maxHp: 40,
attackCooldown: random(0, 50)
});
}
createWorld();
const castle = { x: 2500, y: 1200, width: 500, height: 400 };
function drawWorld() {
ctx.fillStyle = "#5c963e";
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = "rgba(30,70,20,.12)";
ctx.lineWidth = 1;
let grid = 100;
let startX = -camera.x % grid;
let startY = -camera.y % grid;
for(let x=startX; x<W; x+=grid) {
ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke();
}
for(let y=startY; y<H; y+=grid) {
ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke();
}
drawCastle();
for(const d of decorations) {
const sx = d.x - camera.x;
const sy = d.y - camera.y;
if(sx < -100 || sx > W+100 || sy < -100 || sy > H+100) continue;
if(d.type === "tree") drawTree(sx, sy, d.size);
else drawRock(sx, sy, d.size);
}
}
function drawTree(x,y,s) {
ctx.fillStyle = "#654321";
ctx.fillRect(x-8*s, y+5*s, 16*s, 35*s);
ctx.fillStyle = "#164f20";
ctx.beginPath(); ctx.arc(x, y-5*s, 35*s, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = "#287a2d";
ctx.beginPath();
ctx.arc(x-20*s, y+2*s, 25*s, 0, Math.PI*2);
ctx.arc(x+20*s, y+2*s, 25*s, 0, Math.PI*2);
ctx.fill();
}
function drawRock(x,y,s) {
ctx.fillStyle = "#666";
ctx.beginPath(); ctx.ellipse(x, y, 25*s, 16*s, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = "#888";
ctx.beginPath(); ctx.ellipse(x-7*s, y-5*s, 10*s, 6*s, 0, 0, Math.PI*2); ctx.fill();
}
function drawCastle() {
const x = castle.x - camera.x;
const y = castle.y - camera.y;
ctx.fillStyle = "#777";
ctx.fillRect(x - castle.width/2, y - castle.height/2, castle.width, castle.height);
const tw = 90, th = 150;
for(const tx of [x - castle.width/2, x + castle.width/2 - tw]) {
ctx.fillStyle = "#666";
ctx.fillRect(tx, y - castle.height/2 - th/2, tw, th);
ctx.fillStyle = "#333";
ctx.beginPath();
ctx.moveTo(tx-10, y - castle.height/2 - th/2);
ctx.lineTo(tx+tw/2, y - castle.height/2 - th/2 - 45);
ctx.lineTo(tx+tw+10, y - castle.height/2 - th/2);
ctx.closePath();
ctx.fill();
}
ctx.fillStyle = "#2b1a10";
ctx.beginPath(); ctx.arc(x, y + castle.height/2, 55, Math.PI, 0); ctx.fill();
ctx.fillRect(x-55, y + castle.height/2, 110, 70);
ctx.strokeStyle = "#222"; ctx.lineWidth = 5;
ctx.beginPath(); ctx.moveTo(x, y - castle.height/2 - 50); ctx.lineTo(x, y - castle.height/2 - 120); ctx.stroke();
ctx.fillStyle = "#b21f1f";
ctx.beginPath();
ctx.moveTo(x, y - castle.height/2 - 120);
ctx.lineTo(x+70, y - castle.height/2 - 100);
ctx.lineTo(x, y - castle.height/2 - 80);
ctx.closePath(); ctx.fill();
}
function drawPlayer() {
const x = player.x - camera.x;
const y = player.y - camera.y;
ctx.fillStyle = "rgba(0,0,0,.25)";
ctx.beginPath(); ctx.ellipse(x, y+25, 25, 10, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = "#292929";
ctx.fillRect(x-13, y+10, 10, 22);
ctx.fillRect(x+3, y+10, 10, 22);
ctx.fillStyle = "#3c5c8c";
ctx.fillRect(x-18, y-12, 36, 30);
ctx.fillStyle = "#d49b72";
ctx.beginPath(); ctx.arc(x, y-30, 17, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = "#aaa";
ctx.beginPath(); ctx.arc(x, y-34, 19, Math.PI, 0); ctx.fill();
ctx.save();
ctx.translate(x, y);
ctx.rotate(player.direction);
ctx.fillStyle = "#ddd"; ctx.fillRect(18, -5, 45, 7);
ctx.fillStyle = "#75451f"; ctx.fillRect(10, -7, 12, 11);
ctx.restore();
}
function drawAlly(a) {
const x = a.x - camera.x;
const y = a.y - camera.y;
ctx.fillStyle = "rgba(0,0,0,.25)";
ctx.beginPath(); ctx.ellipse(x, y+24, 25, 9, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = "#255a8e";
ctx.fillRect(x-18, y-10, 36, 32);
ctx.fillStyle = "#c18a65";
ctx.beginPath(); ctx.arc(x, y-28, 16, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = "#225";
ctx.beginPath(); ctx.arc(x, y-32, 18, Math.PI, 0); ctx.fill();
ctx.fillStyle = "#030"; ctx.fillRect(x-20, y-50, 40, 5);
ctx.fillStyle = "#3e3"; ctx.fillRect(x-20, y-50, 40 * (a.hp/a.maxHp), 5);
}
function drawEnemy(e) {
const x = e.x - camera.x;
const y = e.y - camera.y;
ctx.fillStyle = "rgba(0,0,0,.25)";
ctx.beginPath(); ctx.ellipse(x, y+24, 25, 9, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = e.hitFlash > 0 ? "#fff" : "#8e2525";
ctx.fillRect(x-18, y-10, 36, 32);
ctx.fillStyle = "#c18a65";
ctx.beginPath(); ctx.arc(x, y-28, 16, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = "#444";
ctx.beginPath(); ctx.arc(x, y-32, 18, Math.PI, 0); ctx.fill();
ctx.fillStyle = "#300"; ctx.fillRect(x-25, y-55, 50, 6);
ctx.fillStyle = "#e33"; ctx.fillRect(x-25, y-55, 50 * (e.hp/e.maxHp), 6);
}
function attack() {
if(player.attackCooldown > 0) return;
player.attackCooldown = 25;
playSoundAttack();
let closest = null;
let distance = Infinity;
for(const e of enemies) {
const d = Math.hypot(e.x - player.x, e.y - player.y);
if(d < 100 && d < distance) {
distance = d;
closest = e;
}
}
if(closest) {
closest.hp -= 25;
closest.hitFlash = 8;
playSoundHit();
const dx = closest.x - player.x;
const dy = closest.y - player.y;
const len = Math.hypot(dx, dy) || 1;
closest.x += dx/len * 35;
closest.y += dy/len * 35;
if(closest.hp <= 0) {
spawnAlly(closest.x, closest.y);
enemies = enemies.filter(e => e !== closest);
}
}
}
let joyActive = false;
let joyX = 0;
let joyY = 0;
const joystick = document.getElementById("joystick");
const stick = document.getElementById("stick");
function updateJoystick(clientX, clientY) {
const rect = joystick.getBoundingClientRect();
const cx = rect.left + rect.width/2;
const cy = rect.top + rect.height/2;
let dx = clientX - cx;
let dy = clientY - cy;
const max = 45;
const len = Math.hypot(dx, dy);
if(len > max) {
dx = dx/len * max;
dy = dy/len * max;
}
joyX = dx / max;
joyY = dy / max;
stick.style.transform = `translate(${dx}px,${dy}px)`;
}
joystick.addEventListener("pointerdown", e => {
joyActive = true;
joystick.setPointerCapture(e.pointerId);
updateJoystick(e.clientX, e.clientY);
});
joystick.addEventListener("pointermove", e => {
if(joyActive) updateJoystick(e.clientX, e.clientY);
});
joystick.addEventListener("pointerup", resetJoystick);
joystick.addEventListener("pointercancel", resetJoystick);
function resetJoystick() {
joyActive = false;
joyX = 0;
joyY = 0;
stick.style.transform = "translate(0,0)";
}
document.getElementById("attack").addEventListener("pointerdown", e => {
e.preventDefault();
attack();
});
function update() {
player.x += joyX * player.speed;
player.y += joyY * player.speed;
if(Math.abs(joyX) > .1 || Math.abs(joyY) > .1) {
player.direction = Math.atan2(joyY, joyX);
}
player.x = Math.max(40, Math.min(world.width-40, player.x));
player.y = Math.max(40, Math.min(world.height-40, player.y));
if(player.attackCooldown > 0) player.attackCooldown--;
// Enemy Spawn Timer (Every 50 Seconds, Max +25)
enemySpawnTimer++;
if (enemySpawnTimer >= 50 * 60) { // 60 frames per second * 50 seconds
enemySpawnTimer = 0;
if (enemyExtraAdded < MAX_EXTRA_ENEMIES) {
spawnEnemy();
enemyExtraAdded++;
}
}
// Update Allies
for(const a of allies) {
if(a.attackCooldown > 0) a.attackCooldown--;
let closestEnemy = null;
let minDist = Infinity;
for(const e of enemies) {
const dist = Math.hypot(e.x - a.x, e.y - a.y);
if(dist < minDist) {
minDist = dist;
closestEnemy = e;
}
}
if(closestEnemy) {
const dx = closestEnemy.x - a.x;
const dy = closestEnemy.y - a.y;
const dist = Math.hypot(dx, dy);
if(dist > 45) {
a.x += dx/dist * a.speed;
a.y += dy/dist * a.speed;
} else if(a.attackCooldown <= 0) {
closestEnemy.hp -= 15;
closestEnemy.hitFlash = 5;
a.attackCooldown = 45;
if(closestEnemy.hp <= 0) {
spawnAlly(closestEnemy.x, closestEnemy.y);
enemies = enemies.filter(item => item !== closestEnemy);
}
}
} else {
// Follow the player if no enemies are around
const dx = player.x - a.x;
const dy = player.y - a.y;
const dist = Math.hypot(dx, dy);
if(dist > 120) {
a.x += dx/dist * a.speed;
a.y += dy/dist * a.speed;
}
}
}
// Update Enemies
for(const e of enemies) {
if(e.attackCooldown > 0) e.attackCooldown--;
if(e.hitFlash > 0) e.hitFlash--;
// Find the closest target between Player or Allies
let target = player;
let minDist = Math.hypot(player.x - e.x, player.y - e.y);
for(const a of allies) {
const dist = Math.hypot(a.x - e.x, a.y - e.y);
if(dist < minDist) {
minDist = dist;
target = a;
}
}
const dx = target.x - e.x;
const dy = target.y - e.y;
const dist = Math.hypot(dx, dy);
if(dist < 800 && dist > 50) {
e.x += dx/dist * e.speed;
e.y += dy/dist * e.speed;
}
if(dist < 60 && e.attackCooldown <= 0) {
target.hp -= 8;
e.attackCooldown = 60;
playSoundHit();
if(target === player && player.hp <= 0) {
player.hp = 0;
gameOver(false);
}
}
}
// Remove defeated allies
allies = allies.filter(a => a.hp > 0);
camera.x = player.x - W/2;
camera.y = player.y - H/2;
camera.x = Math.max(0, Math.min(world.width - W, camera.x));
camera.y = Math.max(0, Math.min(world.height - H, camera.y));
updateHUD();
}
function updateHUD() {
document.getElementById("hpFill").style.width = player.hp + "%";
document.getElementById("enemyCount").textContent = enemies.length;
document.getElementById("allyCount").textContent = allies.length;
}
let ended = false;
function gameOver(win) {
if(ended) return;
ended = true;
playSoundEnd(win);
document.getElementById("message").style.display = "block";
document.getElementById("messageTitle").textContent = win ? "🏆 Victory!" : "💀 Defeat!";
document.getElementById("messageText").textContent = win ? "All enemies have been defeated." : "Your knight has fallen on the battlefield.";
}
document.getElementById("restart").addEventListener("click", () => {
player.x = 2500;
player.y = 2500;
player.hp = 100;
player.attackCooldown = 0;
ended = false;
document.getElementById("message").style.display = "none";
createWorld();
});
function loop() {
if(!ended) update();
drawWorld();
for(const a of allies) drawAlly(a);
for(const e of enemies) drawEnemy(e);
drawPlayer();
requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>Game Source: Medieval Battle
Creator: FlashKoala39
Libraries: none
Complexity: complex (778 lines, 18.9 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: medieval-battle-flashkoala39" to link back to the original. Then publish at arcadelab.ai/publish.