🎮ArcadeLab

Skeleton Boss Fight

by PrismScout28
1337 lines24.8 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Skeleton Boss Fight</title>

<style>
    * {
        box-sizing: border-box;
        user-select: none;
    }

    body {
        margin: 0;
        background: #050505;
        color: white;
        font-family: Arial, Helvetica, sans-serif;
        display: flex;
        justify-content: center;
        align-items: center;
        min-height: 100vh;
        overflow: hidden;
    }

    #game {
        position: relative;
        width: min(900px, 100vw);
        height: min(700px, 100vh);
        background: #000;
        border: 3px solid #fff;
        overflow: hidden;
    }

    canvas {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        image-rendering: pixelated;
    }

    #ui {
        position: absolute;
        inset: 0;
        pointer-events: none;
    }

    #dialogue {
        position: absolute;
        left: 8%;
        right: 8%;
        top: 8%;
        min-height: 80px;
        border: 3px solid white;
        background: #000;
        padding: 18px;
        font-size: 20px;
        line-height: 1.4;
    }

    #boss {
        position: absolute;
        top: 23%;
        width: 100%;
        text-align: center;
        font-size: 46px;
        font-weight: bold;
        text-shadow: 0 0 10px #fff;
    }

    #status {
        position: absolute;
        left: 8%;
        right: 8%;
        bottom: 19%;
        display: flex;
        justify-content: space-between;
        font-size: 20px;
    }

    #hpbar {
        position: absolute;
        left: 8%;
        bottom: 14%;
        width: 40%;
        height: 18px;
        border: 2px solid white;
    }

    #hpfill {
        height: 100%;
        width: 100%;
        background: #f5d547;
    }

    #buttons {
        position: absolute;
        left: 5%;
        right: 5%;
        bottom: 4%;
        display: flex;
        justify-content: space-between;
        gap: 10px;
        pointer-events: auto;
    }

    button {
        flex: 1;
        background: #000;
        color: #f5d547;
        border: 3px solid #f5d547;
        padding: 12px 5px;
        font-size: 18px;
        font-weight: bold;
        cursor: pointer;
    }

    button:hover {
        background: #f5d547;
        color: #000;
    }

    #help {
        position: absolute;
        right: 10px;
        bottom: 5px;
        color: #aaa;
        font-size: 12px;
    }

    .hidden {
        display: none !important;
    }

    @media(max-width: 600px) {
        #dialogue {
            font-size: 15px;
        }

        #boss {
            font-size: 32px;
        }

        button {
            font-size: 13px;
            padding: 10px 2px;
        }
    }
</style>
</head>

<body>

<div id="game">

    <canvas id="canvas" width="900" height="700"></canvas>

    <div id="ui">

        <div id="dialogue">
            heya. ready to have a bad time?
        </div>

        <div id="boss">THE SKELETON</div>

        <div id="status">
            <span>HP: <b id="playerHP">92</b>/92</span>
            <span>BOSS HP: <b id="bossHP">1000</b></span>
        </div>

        <div id="hpbar">
            <div id="hpfill"></div>
        </div>

        <div id="buttons">
            <button onclick="fight()">FIGHT</button>
            <button onclick="act()">ACT</button>
            <button onclick="item()">ITEM</button>
            <button onclick="mercy()">MERCY</button>
        </div>

        <div id="help">
            Move: Arrow Keys / WASD
        </div>

    </div>
</div>

<script>
/* ============================================================
   ORIGINAL BOSS FIGHT DEMO
   Everything is contained in this file.
   ============================================================ */

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

const W = canvas.width;
const H = canvas.height;

/* -----------------------------
   Game state
----------------------------- */

let gameState = "menu";

let player = {
    x: W / 2,
    y: 545,
    size: 9,
    speed: 4,
    hp: 92,
    maxHp: 92,
    invincible: 0
};

let boss = {
    hp: 1000,
    maxHp: 1000
};

let phase = 0;
let attackNumber = 0;

let attacks = [];
let particles = [];

let keys = {};

let attackTimer = 0;
let attackDuration = 0;

let dialogueIndex = 0;

let canAct = true;
let battleOver = false;

const arena = {
    x: 250,
    y: 360,
    width: 400,
    height: 230
};

/* -----------------------------
   Input
----------------------------- */

window.addEventListener("keydown", e => {
    keys[e.key.toLowerCase()] = true;

    if (e.key === "ArrowUp" ||
        e.key === "ArrowDown" ||
        e.key === "ArrowLeft" ||
        e.key === "ArrowRight") {
        e.preventDefault();
    }
});

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

/* -----------------------------
   Utility
----------------------------- */

function rand(min, max) {
    return Math.random() * (max - min) + min;
}

function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
}

function distance(a, b) {
    return Math.hypot(a.x - b.x, a.y - b.y);
}

function setDialogue(text) {
    document.getElementById("dialogue").textContent = text;
}

/* -----------------------------
   Player
----------------------------- */

function updatePlayer(dt) {

    if (battleOver) return;

    let dx = 0;
    let dy = 0;

    if (keys["arrowleft"] || keys["a"]) dx--;
    if (keys["arrowright"] || keys["d"]) dx++;
    if (keys["arrowup"] || keys["w"]) dy--;
    if (keys["arrowdown"] || keys["s"]) dy++;

    if (dx !== 0 || dy !== 0) {

        const length = Math.hypot(dx, dy);

        dx /= length;
        dy /= length;

        player.x += dx * player.speed;
        player.y += dy * player.speed;
    }

    player.x = clamp(
        player.x,
        arena.x + player.size,
        arena.x + arena.width - player.size
    );

    player.y = clamp(
        player.y,
        arena.y + player.size,
        arena.y + arena.height - player.size
    );

    if (player.invincible > 0) {
        player.invincible -= dt;
    }
}

/* -----------------------------
   Player drawing
----------------------------- */

function drawPlayer() {

    if (player.invincible > 0 &&
        Math.floor(player.invincible * 15) % 2 === 0) {
        return;
    }

    ctx.fillStyle = "#ff3030";

    ctx.beginPath();
    ctx.moveTo(player.x, player.y - 9);
    ctx.lineTo(player.x + 8, player.y);
    ctx.lineTo(player.x, player.y + 9);
    ctx.lineTo(player.x - 8, player.y);
    ctx.closePath();
    ctx.fill();
}

/* -----------------------------
   Boss drawing
----------------------------- */

function drawBoss() {

    // head
    ctx.fillStyle = "#fff";

    ctx.beginPath();
    ctx.arc(450, 250, 60, 0, Math.PI * 2);
    ctx.fill();

    // eyes
    ctx.fillStyle = "#000";

    ctx.beginPath();
    ctx.ellipse(430, 245, 10, 18, 0, 0, Math.PI * 2);
    ctx.fill();

    ctx.beginPath();
    ctx.ellipse(470, 245, 10, 18, 0, 0, Math.PI * 2);
    ctx.fill();

    // smile
    ctx.strokeStyle = "#000";
    ctx.lineWidth = 5;

    ctx.beginPath();
    ctx.arc(450, 255, 30, 0.2, Math.PI - 0.2);
    ctx.stroke();

    // body
    ctx.fillStyle = "#fff";
    ctx.fillRect(425, 300, 50, 55);

    // arms
    ctx.strokeStyle = "#fff";
    ctx.lineWidth = 12;

    ctx.beginPath();
    ctx.moveTo(430, 315);
    ctx.lineTo(390, 350);
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(470, 315);
    ctx.lineTo(510, 350);
    ctx.stroke();

    // eye glow during advanced phases
    if (phase >= 2) {

        ctx.shadowColor = "#35aaff";
        ctx.shadowBlur = 20;

        ctx.fillStyle = "#35aaff";

        ctx.fillRect(425, 240, 10, 20);

        ctx.shadowBlur = 0;
    }
}

/* ============================================================
   ATTACK SYSTEM
   ============================================================ */

function startAttack() {

    attackNumber++;

    attacks = [];

    attackTimer = 0;

    attackDuration =
        attackNumber < 4 ? 6 :
        attackNumber < 8 ? 7 :
        9;

    phase =
        attackNumber < 4 ? 0 :
        attackNumber < 8 ? 1 :
        2;

    setDialogue(
        attackNumber === 1
            ? "let's see how well you can dodge."
            : attackNumber === 4
                ? "okay. now things get interesting."
                : attackNumber === 8
                    ? "you've made it this far."
                    : "keep moving."
    );

    switch (attackNumber % 6) {

        case 0:
            boneRain();
            break;

        case 1:
            boneWalls();
            break;

        case 2:
            blasterAttack();
            break;

        case 3:
            spiralAttack();
            break;

        case 4:
            bouncingBones();
            break;

        case 5:
            mixedAttack();
            break;
    }
}

/* -----------------------------
   Bone
----------------------------- */

function createBone(x, y, vx, vy, width, height, color="#fff") {

    attacks.push({
        type: "bone",
        x,
        y,
        vx,
        vy,
        width,
        height,
        color,
        life: 20
    });
}

/* -----------------------------
   Bone rain
----------------------------- */

function boneRain() {

    for (let i = 0; i < 28; i++) {

        createBone(
            rand(arena.x, arena.x + arena.width),
            arena.y - rand(20, 500),
            0,
            rand(100, 230),
            14,
            rand(35, 75)
        );
    }
}

/* -----------------------------
   Walls
----------------------------- */

function boneWalls() {

    for (let i = 0; i < 12; i++) {

        const y = arena.y + i * 19;

        createBone(
            arena.x - 20,
            y,
            rand(80, 160),
            0,
            55,
            12
        );

        createBone(
            arena.x + arena.width + 20,
            y,
            -rand(80, 160),
            0,
            55,
            12
        );
    }
}

/* -----------------------------
   Blasters
----------------------------- */

function createBlaster(x, y, angle, delay=0) {

    attacks.push({
        type: "blaster",
        x,
        y,
        angle,
        delay,
        life: 3.2,
        charge: 1.1,
        fired: false,
        beam: 0
    });
}

function blasterAttack() {

    createBlaster(
        arena.x - 70,
        player.y,
        0
    );

    createBlaster(
        arena.x + arena.width + 70,
        player.y,
        Math.PI
    );

    createBlaster(
        player.x,
        arena.y - 70,
        Math.PI / 2
    );

    createBlaster(
        player.x,
        arena.y + arena.height + 70,
        -Math.PI / 2
    );
}

/* -----------------------------
   Spiral
----------------------------- */

function spiralAttack() {

    for (let i = 0; i < 35; i++) {

        const angle = i * 0.5;

        const radius = 50 + i * 5;

        const x =
            arena.x +
            arena.width / 2 +
            Math.cos(angle) * radius;

        const y =
            arena.y +
            arena.height / 2 +
            Math.sin(angle) * radius;

        const vx = Math.cos(angle) * -50;
        const vy = Math.sin(angle) * -50;

        createBone(
            x,
            y,
            vx,
            vy,
            10,
            42
        );
    }
}

/* -----------------------------
   Bouncing bones
----------------------------- */

function bouncingBones() {

    for (let i = 0; i < 10; i++) {

        createBone(
            rand(arena.x, arena.x + arena.width),
            rand(arena.y, arena.y + arena.height),
            rand(-100, 100),
            rand(-100, 100),
            15,
            45
        );
    }
}

/* -----------------------------
   Mixed attack
----------------------------- */

function mixedAttack() {

    boneRain();

    setTimeout(() => {

        if (!battleOver) {
            createBlaster(
                arena.x - 80,
                arena.y + arena.height / 2,
                0
            );
        }

    }, 1200);

    setTimeout(() => {

        if (!battleOver) {
            createBlaster(
                arena.x + arena.width + 80,
                arena.y + arena.height / 2,
                Math.PI
            );
        }

    }, 2500);
}

/* ============================================================
   Attack update
   ============================================================ */

function updateAttacks(dt) {

    for (let i = attacks.length - 1; i >= 0; i--) {

        const a = attacks[i];

        if (a.type === "bone") {

            a.x += a.vx * dt;
            a.y += a.vy * dt;

            a.life -= dt;

            if (a.life <= 0) {
                attacks.splice(i, 1);
                continue;
            }

            checkBoneCollision(a);
        }

        if (a.type === "blaster") {

            a.life -= dt;

            if (a.delay > 0) {
                a.delay -= dt;
                continue;
            }

            if (a.charge > 0) {

                a.charge -= dt;

            } else {

                a.fired = true;
                a.beam += dt * 7;

                checkBlasterCollision(a);

                if (a.beam > 1.3) {
                    attacks.splice(i, 1);
                    continue;
                }
            }
        }
    }
}

/* -----------------------------
   Collision
----------------------------- */

function checkBoneCollision(a) {

    if (player.invincible > 0) return;

    const closestX =
        clamp(player.x, a.x - a.width / 2, a.x + a.width / 2);

    const closestY =
        clamp(player.y, a.y - a.height / 2, a.y + a.height / 2);

    const dx = player.x - closestX;
    const dy = player.y - closestY;

    if (dx * dx + dy * dy <
        player.size * player.size) {

        damagePlayer(4);
    }
}

function checkBlasterCollision(a) {

    if (!a.fired || player.invincible > 0) return;

    const beamWidth = 25;

    const dx = Math.cos(a.angle);
    const dy = Math.sin(a.angle);

    const px = player.x - a.x;
    const py = player.y - a.y;

    const projection =
        px * dx + py * dy;

    if (projection < 0 || projection > 1000) {
        return;
    }

    const perpendicular =
        Math.abs(px * dy - py * dx);

    if (perpendicular < beamWidth) {
        damagePlayer(6);
    }
}

/* -----------------------------
   Damage player
----------------------------- */

function damagePlayer(amount) {

    if (player.invincible > 0 || battleOver) return;

    player.hp -= amount;

    player.invincible = 0.8;

    createHitParticles();

    updateUI();

    if (player.hp <= 0) {

        player.hp = 0;

        gameOver();
    }
}

/* ============================================================
   Drawing attacks
   ============================================================ */

function drawAttacks() {

    for (const a of attacks) {

        if (a.type === "bone") {

            ctx.save();

            ctx.translate(a.x, a.y);

            if (Math.abs(a.vy) < Math.abs(a.vx)) {
                ctx.rotate(Math.PI / 2);
            }

            ctx.fillStyle = a.color;

            ctx.fillRect(
                -a.width / 2,
                -a.height / 2,
                a.width,
                a.height
            );

            // bone cap
            ctx.beginPath();
            ctx.arc(
                0,
                -a.height / 2,
                a.width,
                0,
                Math.PI * 2
            );
            ctx.fill();

            ctx.restore();
        }

        if (a.type === "blaster") {

            drawBlaster(a);
        }
    }
}

/* -----------------------------
   Blaster drawing
----------------------------- */

function drawBlaster(a) {

    ctx.save();

    ctx.translate(a.x, a.y);
    ctx.rotate(a.angle);

    // head
    ctx.fillStyle = "#fff";

    ctx.beginPath();
    ctx.arc(0, 0, 35, 0, Math.PI * 2);
    ctx.fill();

    // mouth
    ctx.fillStyle = "#000";

    ctx.fillRect(
        0,
        -18,
        45,
        36
    );

    // eye
    ctx.fillStyle = "#35aaff";

    ctx.fillRect(
        0,
        -9,
        12,
        18
    );

    // charging glow
    if (!a.fired) {

        ctx.shadowColor = "#35aaff";
        ctx.shadowBlur = 25;

        ctx.fillStyle = "#35aaff";

        ctx.beginPath();
        ctx.arc(15, 0, 8, 0, Math.PI * 2);
        ctx.fill();

        ctx.shadowBlur = 0;
    }

    // beam
    if (a.fired) {

        const beamLength = 1000;

        const gradient =
            ctx.createLinearGradient(
                40, 0,
                beamLength, 0
            );

        gradient.addColorStop(0, "#fff");
        gradient.addColorStop(0.5, "#55ccff");
        gradient.addColorStop(1, "rgba(40,130,255,0)");

        ctx.shadowColor = "#35aaff";
        ctx.shadowBlur = 30;

        ctx.fillStyle = gradient;

        ctx.fillRect(
            40,
            -15,
            beamLength,
            30
        );

        ctx.shadowBlur = 0;
    }

    ctx.restore();
}

/* ============================================================
   Particles
   ============================================================ */

function createHitParticles() {

    for (let i = 0; i < 12; i++) {

        particles.push({
            x: player.x,
            y: player.y,
            vx: rand(-100, 100),
            vy: rand(-100, 100),
            life: 0.5
        });
    }
}

function updateParticles(dt) {

    for (let i = particles.length - 1; i >= 0; i--) {

        const p = particles[i];

        p.x += p.vx * dt;
        p.y += p.vy * dt;

        p.life -= dt;

        if (p.life <= 0) {
            particles.splice(i, 1);
        }
    }
}

function drawParticles() {

    ctx.fillStyle = "#ff3030";

    for (const p of particles) {

        ctx.globalAlpha =
            Math.max(0, p.life * 2);

        ctx.fillRect(
            p.x,
            p.y,
            4,
            4
        );
    }

    ctx.globalAlpha = 1;
}

/* ============================================================
   Battle commands
   ============================================================ */

function fight() {

    if (!canAct || battleOver) return;

    canAct = false;

    const damage =
        Math.floor(rand(30, 70));

    boss.hp -= damage;

    setDialogue(
        "you attacked. " +
        damage +
        " damage."
    );

    updateUI();

    if (boss.hp <= 0) {

        boss.hp = 0;

        setTimeout(victory, 900);

        return;
    }

    setTimeout(() => {

        startAttack();

        canAct = true;

    }, 1000);
}

function act() {

    if (!canAct || battleOver) return;

    canAct = false;

    const messages = [
        "you studied the boss carefully.",
        "you told a terrible joke.",
        "you stared directly at the boss.",
        "you tried to look intimidating."
    ];

    setDialogue(
        messages[
            Math.floor(
                Math.random() * messages.length
            )
        ]
    );

    setTimeout(() => {

        startAttack();

        canAct = true;

    }, 1000);
}

function item() {

    if (!canAct || battleOver) return;

    canAct = false;

    const heal = 18;

    player.hp =
        Math.min(
            player.maxHp,
            player.hp + heal
        );

    setDialogue(
        "you recovered " +
        heal +
        " HP."
    );

    updateUI();

    setTimeout(() => {

        startAttack();

        canAct = true;

    }, 1000);
}

function mercy() {

    if (!canAct || battleOver) return;

    canAct = false;

    if (boss.hp <= 150) {

        setDialogue(
            "the boss pauses... then lowers his guard."
        );

        setTimeout(victory, 1200);

    } else {

        setDialogue(
            "you tried to spare the boss. not yet."
        );

        setTimeout(() => {

            startAttack();

            canAct = true;

        }, 1000);
    }
}

/* ============================================================
   UI
   ============================================================ */

function updateUI() {

    document.getElementById("playerHP").textContent =
        Math.ceil(player.hp);

    document.getElementById("bossHP").textContent =
        Math.ceil(boss.hp);

    document.getElementById("hpfill").style.width =
        (player.hp / player.maxHp * 100) + "%";
}

/* ============================================================
   Game states
   ============================================================ */

function gameOver() {

    battleOver = true;

    attacks = [];

    setDialogue(
        "you couldn't dodge forever..."
    );

    setTimeout(() => {

        setDialogue(
            "GAME OVER — press R to restart."
        );

    }, 1500);
}

function victory() {

    battleOver = true;

    attacks = [];

    setDialogue(
        "you won. somehow."
    );

    document.getElementById("boss").textContent =
        "VICTORY";

    document.getElementById("boss").style.color =
        "#55ff55";
}

function restart() {

    player.hp = 92;
    player.x = W / 2;
    player.y = 545;

    boss.hp = 1000;

    attacks = [];
    particles = [];

    attackNumber = 0;
    phase = 0;

    battleOver = false;
    canAct = true;

    document.getElementById("boss").textContent =
        "THE SKELETON";

    document.getElementById("boss").style.color =
        "white";

    setDialogue(
        "heya. ready to have a bad time?"
    );

    updateUI();
}

/* keyboard restart */

window.addEventListener("keydown", e => {

    if (e.key.toLowerCase() === "r") {

        if (battleOver) {
            restart();
        }
    }
});

/* ============================================================
   Arena
   ============================================================ */

function drawArena() {

    ctx.strokeStyle = "#fff";
    ctx.lineWidth = 4;

    ctx.strokeRect(
        arena.x,
        arena.y,
        arena.width,
        arena.height
    );

    // subtle center grid
    ctx.strokeStyle = "rgba(255,255,255,0.04)";
    ctx.lineWidth = 1;

    for (let x = arena.x; x < arena.x + arena.width; x += 25) {

        ctx.beginPath();
        ctx.moveTo(x, arena.y);
        ctx.lineTo(x, arena.y + arena.height);
        ctx.stroke();
    }

    for (let y = arena.y; y < arena.y + arena.height; y += 25) {

        ctx.beginPath();
        ctx.moveTo(arena.x, y);
        ctx.lineTo(arena.x + arena.width, y);
        ctx.stroke();
    }
}

/* ============================================================
   Main loop
   ============================================================ */

let previousTime = performance.now();

function loop(now) {

    const dt =
        Math.min(
            (now - previousTime) / 1000,
            0.033
        );

    previousTime = now;

    updatePlayer(dt);
    updateAttacks(dt);
    updateParticles(dt);

    draw();

    requestAnimationFrame(loop);
}

/* ============================================================
   Draw everything
   ============================================================ */

function draw() {

    ctx.clearRect(0, 0, W, H);

    // background
    ctx.fillStyle = "#000";
    ctx.fillRect(0, 0, W, H);

    // stars / particles in background
    ctx.fillStyle = "#222";

    for (let i = 0; i < 40; i++) {

        const x = (i * 137) % W;
        const y = (i * 71) % H;

        ctx.fillRect(x, y, 2, 2);
    }

    drawBoss();

    drawArena();

    drawAttacks();

    drawParticles();

    drawPlayer();
}

/* ============================================================
   Start
   ============================================================ */

updateUI();

setTimeout(() => {
    startAttack();
}, 1500);

requestAnimationFrame(loop);

</script>

</body>
</html>

Game Source: Skeleton Boss Fight

Creator: PrismScout28

Libraries: none

Complexity: complex (1337 lines, 24.8 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: skeleton-boss-fight-prismscout28" to link back to the original. Then publish at arcadelab.ai/publish.