Skeleton Boss Fight
by PrismScout281337 lines24.8 KB
<!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.