Medieval Battle - Realistic Units
by SolarGalaxy43843 lines21.7 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 - Realistic Units</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
touch-action: none;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #0d1117;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#game {
position: fixed;
inset: 0;
background: #3b5e2b;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
/* HUD */
#hud {
position: fixed;
top: 15px;
left: 15px;
right: 15px;
display: flex;
justify-content: space-between;
align-items: flex-start;
pointer-events: none;
z-index: 10;
}
.panel {
background: rgba(15, 23, 42, 0.85);
color: white;
padding: 10px 15px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
backdrop-filter: blur(5px);
}
#hpBar {
width: 160px;
height: 12px;
background: #311;
border-radius: 6px;
overflow: hidden;
margin-top: 6px;
border: 1px solid rgba(255,255,255,0.1);
}
#hpFill {
height: 100%;
width: 100%;
background: linear-gradient(90deg, #ef4444, #22c55e);
transition: width 0.1s linear;
}
/* Controls */
#controls {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 20;
}
#joystick {
position: absolute;
left: 30px;
bottom: 30px;
width: 130px;
height: 130px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.3);
border: 2px solid rgba(255, 255, 255, 0.25);
pointer-events: auto;
}
#stick {
position: absolute;
width: 55px;
height: 55px;
left: 37.5px;
top: 37.5px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
border: 2px solid white;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
}
#attack {
position: absolute;
right: 30px;
bottom: 40px;
width: 100px;
height: 100px;
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, 0.7);
background: radial-gradient(circle, #dc2626, #7f1d1d);
color: white;
font-size: 38px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
#attack:active {
transform: scale(0.92);
}
/* Message */
#message {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
background: rgba(15, 23, 42, 0.95);
border: 1px solid rgba(255,255,255,0.2);
padding: 30px 40px;
border-radius: 16px;
text-align: center;
display: none;
z-index: 50;
box-shadow: 0 10px 30px rgba(0,0,0,0.7);
}
#message h1 {
margin-bottom: 12px;
font-size: 28px;
}
#message p {
color: #cbd5e1;
margin-bottom: 20px;
}
#restart {
padding: 12px 30px;
border: 0;
border-radius: 8px;
background: #d97706;
color: white;
font-weight: bold;
font-size: 16px;
cursor: pointer;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
#restart:hover {
background: #b45309;
}
</style>
</head>
<body>
<div id="game">
<canvas id="canvas"></canvas>
</div>
<div id="hud">
<div class="panel">
<b style="color: #60a5fa;">⚔️ Royal Knight (Player)</b>
<div id="hpBar">
<div id="hpFill"></div>
</div>
</div>
<div class="panel">
🛡️ Allies: <span id="allyCount">0</span> | ☠️ Bandits: <span id="enemyCount">0</span>
</div>
</div>
<div id="controls">
<div id="joystick">
<div id="stick"></div>
</div>
<div id="attack">⚔</div>
</div>
<div id="message">
<h1 id="messageTitle">Game Over</h1>
<p id="messageText"></p>
<button id="restart">Play Again</button>
</div>
<script>
// --- AUDIO SYSTEM ---
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(320, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(70, audioCtx.currentTime + 0.12);
gain.gain.setValueAtTime(0.25, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.01, audioCtx.currentTime + 0.12);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.12);
}
function playSoundHit() {
initAudio();
if (!audioCtx) return;
let osc = audioCtx.createOscillator();
let gain = audioCtx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(140, audioCtx.currentTime);
osc.frequency.linearRampToValueAtTime(35, audioCtx.currentTime + 0.15);
gain.gain.setValueAtTime(0.25, 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 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(110, audioCtx.currentTime);
osc.frequency.linearRampToValueAtTime(40, audioCtx.currentTime + 0.4);
}
gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0.01, audioCtx.currentTime + (win ? 0.6 : 0.4));
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + (win ? 0.6 : 0.4));
}
// --- GAME CORE SETUP ---
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let W, H;
function resize() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * devicePixelRatio;
canvas.height = H * devicePixelRatio;
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
}
window.addEventListener("resize", resize);
resize();
const world = { width: 5000, height: 5000 };
let camera = { x: 0, y: 0 };
const player = {
x: 2500,
y: 2500,
radius: 20,
speed: 3.6,
hp: 100,
maxHp: 100,
attackCooldown: 0,
direction: 0,
animFrame: 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;
// Generate environment (Trees & Rocks)
for (let i = 0; i < 350; i++) {
let x = random(100, world.width - 100);
let y = random(100, world.height - 100);
if (Math.hypot(x - player.x, y - player.y) < 500) continue;
decorations.push({ type: "tree", x, y, size: random(0.7, 1.4) });
}
for (let i = 0; i < 180; i++) {
decorations.push({
type: "rock",
x: random(50, world.width - 50),
y: random(50, world.height - 50),
size: random(0.5, 1.2)
});
}
// Initial Enemies
for (let i = 0; i < 22; i++) {
spawnEnemy();
}
}
function spawnEnemy() {
let angle = random(0, Math.PI * 2);
let distance = random(900, 1900);
enemies.push({
x: player.x + Math.cos(angle) * distance,
y: player.y + Math.sin(angle) * distance,
radius: 20,
speed: random(1.2, 1.8),
hp: 50,
maxHp: 50,
attackCooldown: random(0, 60),
hitFlash: 0,
direction: 0
});
}
function spawnAlly(x, y) {
allies.push({
x: x,
y: y,
radius: 20,
speed: 1.9,
hp: 45,
maxHp: 45,
attackCooldown: random(0, 50),
direction: 0
});
}
createWorld();
const castle = { x: 2500, y: 1200, width: 500, height: 400 };
function drawWorld() {
ctx.fillStyle = "#477233";
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = "rgba(25, 60, 15, 0.15)";
ctx.lineWidth = 1.5;
let grid = 120;
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 < -150 || sx > W + 150 || sy < -150 || sy > H + 150) continue;
if (d.type === "tree") drawTree(sx, sy, d.size);
else drawRock(sx, sy, d.size);
}
}
function drawTree(x, y, s) {
ctx.fillStyle = "#2c1d0c";
ctx.fillRect(x - 7 * s, y, 14 * s, 40 * s);
// Detail dedaunan realistis bertingkat
ctx.fillStyle = "#1b3814";
ctx.beginPath(); ctx.arc(x, y - 10 * s, 32 * s, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = "#234a1a";
ctx.beginPath();
ctx.arc(x - 14 * s, y + 2 * s, 22 * s, 0, Math.PI * 2);
ctx.arc(x + 14 * s, y + 2 * s, 22 * s, 0, Math.PI * 2);
ctx.fill();
}
function drawRock(x, y, s) {
ctx.fillStyle = "#4b5563";
ctx.beginPath(); ctx.ellipse(x, y, 22 * s, 14 * s, 0, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = "#6b7280";
ctx.beginPath(); ctx.ellipse(x - 6 * s, y - 4 * s, 9 * s, 5 * s, 0, 0, Math.PI * 2); ctx.fill();
}
function drawCastle() {
const x = castle.x - camera.x;
const y = castle.y - camera.y;
ctx.fillStyle = "#4b5563";
ctx.fillRect(x - castle.width / 2, y - castle.height / 2, castle.width, castle.height);
const tw = 90, th = 160;
for (const tx of [x - castle.width / 2, x + castle.width / 2 - tw]) {
ctx.fillStyle = "#374151";
ctx.fillRect(tx, y - castle.height / 2 - th / 2, tw, th);
ctx.fillStyle = "#1f2937";
ctx.beginPath();
ctx.moveTo(tx - 10, y - castle.height / 2 - th / 2);
ctx.lineTo(tx + tw / 2, y - castle.height / 2 - th / 2 - 50);
ctx.lineTo(tx + tw + 10, y - castle.height / 2 - th / 2);
ctx.closePath();
ctx.fill();
}
ctx.fillStyle = "#111827";
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);
}
// --- REALISTIS CHARACTER RENDERER ---
function drawHumanoid(x, y, dir, armorColor, clothColor, hasHelmet, weaponType, isAttacking) {
// Shadow
ctx.fillStyle = "rgba(0, 0, 0, 0.35)";
ctx.beginPath(); ctx.ellipse(x, y + 16, 16, 7, 0, 0, Math.PI * 2); ctx.fill();
ctx.save();
ctx.translate(x, y);
ctx.rotate(dir);
// Kaki / Sepatu kulit gelap
ctx.fillStyle = "#1e1b18";
ctx.fillRect(-8, 6, 5, 12);
ctx.fillRect(3, 6, 5, 12);
// Tubuh / Torso (Armor/Cloth)
ctx.fillStyle = armorColor;
ctx.beginPath();
ctx.roundRect(-11, -12, 22, 22, [4]);
ctx.fill();
// Jubah dada tengah
ctx.fillStyle = clothColor;
ctx.fillRect(-5, -12, 10, 22);
// Bahu / Pauldrons logam
ctx.fillStyle = "#9ca3af";
ctx.beginPath();
ctx.arc(-11, -8, 5, 0, Math.PI * 2);
ctx.arc(11, -8, 5, 0, Math.PI * 2);
ctx.fill();
// Kepala / Wajah
ctx.fillStyle = "#d1a37a"; // Kulit
ctx.beginPath(); ctx.arc(0, -18, 10, 0, Math.PI * 2); ctx.fill();
if (hasHelmet) {
// Helm Ksatria Besi/Salad
ctx.fillStyle = "#6b7280";
ctx.beginPath();
ctx.arc(0, -19, 11, Math.PI, 0);
ctx.fill();
ctx.fillRect(-11, -23, 22, 5);
// Visor slit (celah mata)
ctx.fillStyle = "#111";
ctx.fillRect(-5, -20, 10, 2);
} else {
// Rambut / Topi Bandit
ctx.fillStyle = "#78350f";
ctx.beginPath(); ctx.arc(0, -21, 10, Math.PI, 0); ctx.fill();
// Kain penutup kepala bandit
ctx.fillStyle = "#7f1d1d";
ctx.fillRect(-10, -19, 20, 6);
}
// Senjata Dinamis
let weaponOffset = isAttacking ? 18 : 0;
if (weaponType === "sword") {
// Tangan & Pedang Panjang Ksatria
ctx.fillStyle = "#9ca3af";
ctx.fillRect(10 + weaponOffset, -14, 4, 8); // Tangan
ctx.fillStyle = "#374151"; // Gagang
ctx.fillRect(12 + weaponOffset, -12, 2, 14);
ctx.fillStyle = "#b45309"; // Hilt
ctx.beginPath(); ctx.arc(13 + weaponOffset, -5, 4, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = "#e5e7eb"; // Bilah Pedang Baja
ctx.beginPath();
ctx.moveTo(11 + weaponOffset, -16);
ctx.lineTo(15 + weaponOffset, -16);
ctx.lineTo(14 + weaponOffset, -38);
ctx.lineTo(12 + weaponOffset, -38);
ctx.closePath();
ctx.fill();
} else if (weaponType === "spear") {
// Tombak Sekutu
ctx.fillStyle = "#9ca3af";
ctx.fillRect(10 + weaponOffset, -12, 4, 8);
ctx.fillStyle = "#78350f"; // Kayu tombak
ctx.fillRect(12 + weaponOffset, -35, 3, 50);
ctx.fillStyle = "#cbd5e1"; // Mata tombak besi
ctx.beginPath();
ctx.moveTo(11 + weaponOffset, -35);
ctx.lineTo(15 + weaponOffset, -35);
ctx.lineTo(13.5 + weaponOffset, -52);
ctx.closePath();
ctx.fill();
} else if (weaponType === "dagger") {
// Belati Bandit
ctx.fillStyle = "#9ca3af";
ctx.fillRect(10 + weaponOffset, -10, 3, 6);
ctx.fillStyle = "#9ca3af"; // Bilah belati bergerigi
ctx.beginPath();
ctx.moveTo(10 + weaponOffset, -12);
ctx.lineTo(14 + weaponOffset, -12);
ctx.lineTo(12 + weaponOffset, -24);
ctx.closePath();
ctx.fill();
}
ctx.restore();
}
function drawPlayer() {
const x = player.x - camera.x;
const y = player.y - camera.y;
drawHumanoid(x, y, player.direction, "#1e3a8a", "#3b82f6", true, "sword", player.attackCooldown > 12);
}
function drawAlly(a) {
const x = a.x - camera.x;
const y = a.y - camera.y;
drawHumanoid(x, y, a.direction, "#14532d", "#22c55e", true, "spear", a.attackCooldown > 30);
// HP Mini Bar di atas kepala ally
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(x - 16, y - 36, 32, 4);
ctx.fillStyle = "#22c55e";
ctx.fillRect(x - 16, y - 36, 32 * (a.hp / a.maxHp), 4);
}
function drawEnemy(e) {
const x = e.x - camera.x;
const y = e.y - camera.y;
let armorColor = e.hitFlash > 0 ? "#ffffff" : "#450a0a";
drawHumanoid(x, y, e.direction, armorColor, "#b91c1c", false, "dagger", e.attackCooldown > 40);
// HP Mini Bar di atas kepala enemy
ctx.fillStyle = "rgba(0,0,0,0.6)";
ctx.fillRect(x - 16, y - 36, 32, 4);
ctx.fillStyle = "#ef4444";
ctx.fillRect(x - 16, y - 36, 32 * (e.hp / e.maxHp), 4);
}
// --- CONTROLS & LOGIC ---
function attack() {
if (player.attackCooldown > 0) return;
player.attackCooldown = 22;
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 < 95 && 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) * 30;
closest.y += (dy / len) * 30;
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(0px, 0px)";
}
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) > 0.1 || Math.abs(joyY) > 0.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 wave reinforcement timer
enemySpawnTimer++;
if (enemySpawnTimer >= 50 * 60) {
enemySpawnTimer = 0;
if (enemyExtraAdded < MAX_EXTRA_ENEMIES) {
spawnEnemy();
enemyExtraAdded++;
}
}
// Update Allies AI
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);
a.direction = Math.atan2(dy, dx);
if (dist > 50) {
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 {
const dx = player.x - a.x;
const dy = player.y - a.y;
const dist = Math.hypot(dx, dy);
if (dist > 110) {
a.direction = Math.atan2(dy, dx);
a.x += (dx / dist) * a.speed;
a.y += (dy / dist) * a.speed;
}
}
}
// Update Enemies AI
for (const e of enemies) {
if (e.attackCooldown > 0) e.attackCooldown--;
if (e.hitFlash > 0) e.hitFlash--;
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);
e.direction = Math.atan2(dy, dx);
if (dist < 850 && dist > 45) {
e.x += (dx / dist) * e.speed;
e.y += (dy / dist) * e.speed;
}
if (dist < 55 && e.attackCooldown <= 0) {
target.hp -= 7;
e.attackCooldown = 55;
playSoundHit();
if (target === player && player.hp <= 0) {
player.hp = 0;
gameOver(false);
}
}
}
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 = Math.max(0, 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 bandits have been crushed!" : "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 - Realistic Units
Creator: SolarGalaxy43
Libraries: none
Complexity: complex (843 lines, 21.7 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-realistic-units-solargalaxy43" to link back to the original. Then publish at arcadelab.ai/publish.