🎮ArcadeLab

Medieval Battle - Realistic Units

by SolarGalaxy43
843 lines21.7 KB
▶ Play
<!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.