Horror Adventure
by AtomicHawk35390 lines13.0 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Horror Adventure</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;}
body{
overflow:hidden;
background:#111;
font-family:'Courier New',monospace;
}
canvas{display:block;}
#hud{
position:fixed;top:15px;left:15px;color:#fff;font-size:16px;z-index:10;
text-shadow:0 0 6px rgba(255,0,0,.6);pointer-events:none;line-height:1.8;}
#gameover{
display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);
z-index:20;display:flex;align-items:center;justify-content:center;flex-direction:column;}
#gameover h1{font-size:52px;color:#f22;text-shadow:0 0 30px red;margin-bottom:10px;}
#gameover p{color:#ccc;font-size:18px;margin:4px 0;}
#restartBtn{
margin-top:24px;padding:12px 36px;background:#a00;color:#fff;border:none;cursor:pointer;
font-size:18px;font-family:inherit;border-radius:6px;}
#restartBtn:hover{background:#c00;}
</style>
</head>
<body>
<div id="hud">
<div>⏱ Survived: <span id="timer">0</span>s</div>
<div>👻 Fear Level: <span id="level">1</span></div>
<div>❤️ Lives: <span id="lives">3</span></div>
</div>
<canvas id="arena"></canvas>
<!-- Game Over overlay -->
<div id="gameover" style="display:none;">
<h1>GAMЕ OVER</h1>
<p>You survived <strong id="finalTime">0</strong> seconds.</p>
<button id="restartBtn">Play Again</button>
</div>
<script>
// ─── Setup Canvas ──────────────────────────────────────
const canvas = document.getElementById('arena');
const ctx = canvas.getContext('2d');
function resize(){ canvas.width = window.innerWidth; canvas.height = window.innerHeight; }
window.addEventListener('resize', resize);
resize();
// ─── Config ────────────────────────────────────────────
const PLAYER_SIZE = 20;
const MONSTER_BASE_SPEED = 2;
const STEPS_BEFORE_TELEPORT = 40; // teleport chance increases over time
// ─── Walls (percentage of screen size → pixel coords) ──
let W, H;
function pixelsFor(pctW, pctH, px, py){
const w = parseFloat(pctW)*wRatio || px;
const h = parseFloat(pctH)*hRatio || py;
return { left:parseFloat(px)*(canvas.width/900), top:parseFloat(py)*(canvas.height/700), width:w, height:h };
}
// Wall definitions stored as percentages then converted
const WALL_DEFS = [
{ l:.22, t:.14, w:.33, h:.04 }, // horizontal bar
{ l:.55, t:.28, w:.04, h:.43 }, // vertical bar
{ l:.11, t:.64, w:.44, h:.04 }, // bottom-left horiz
{ l:.7, t:.1, w:.04,h:.25 }, // right tall wall
{ l:.35, t:.5, w:.2,w:.04 }, // center cross piece
];
function getWalls(){
return WALL_DEFS.map(w => ({
left: w.l * canvas.width,
top: w.t * canvas.height,
width: Math.max(w.w * canvas.width, w.h === undefined ? w.w : w.w*canvas.width)/4, // normalize
height: Math.max(w.h * canvas.height, 30)
}));
}
// Actually simpler approach – define walls in pixels relative to ~900×600 base, scaled:
const BASE_W = 900, BASE_H = 600;
const WALL_DEFS_RAW = [
{ x:198, y:84, w:297, h:24 }, // horiz upper
{ x:495,y:168, w:24, h:258 }, // vert middle-right
{ x:99, y:384, w:396, h:24 }, // horiz lower-left
{ x:630,y:60, w:24, h:165 }, // vert far-right
{ x:315,y:300, w:180, h:24 }, // horiz mid-low
];
function getScaledWall(def){
const sx = canvas.width / BASE_W;
const sy = canvas.height / BASE_H;
return {
x: def.x * sx, y: def.y * sy,
w: def.w * sx, h: def.h * sy
};
}
// ─── Game State ────────────────────────────────────────
let playerX = 80, playerY = 80;
let lives = 3;
let startTime = Date.now();
let monsters = [];
let particles = [];
let keysPressed = {};
let shakeAmount = 0;
let flashlightAngle = 0;
let flashlightOn = true;
// Spawn initial monster
monsters.push({ x: 700, y: 400, speed: MONSTER_BASE_SPEED });
// ─── Input ─────────────────────────────────────────────
document.addEventListener('keydown', e => {
keysPressed[e.key] = true;
if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault();
});
document.addEventListener('keyup', e => { keysPressed[e.key] = false; });
document.getElementById('restartBtn').addEventListener('click', () => resetGame());
// ─── Helpers ───────────────────────────────────────────
function rectOverlap(ax,ay,aw,ah,bx,by,bw,bh){
return ax < bx+bw && ax+aw > bx && ay < by+bh && ay+ah > by;
}
function resolveCollisions(newX, newY, radius){
const hw = PLAYER_SIZE/2;
const halfSize = hw;
const walls = WALL_DEFS_RAW.map(getScaledWall);
for(const w of walls){
// Check collision with each axis independently for sliding
// Try X movement first
if(rectOverlap(newX-halfSize, playerY-halfSize, PLAYER_SIZE, PLAYER_SIZE, w.x, w.y, w.w, w.h)){
newX = playerX; // revert X
break;
}
}
// Now try Y
let testX = newX;
for(const w of walls){
if(rectOverlap(testX-halfSize, playerY-halfSize, PLAYER_SIZE, PLAYER_SIZE, w.x, w.y, w.w, w.h)){
testX = playerX;
break;
}
}
for(const w of walls){
if(rectOverlap(testX-halfSize, newY-halfSize, PLAYER_SIZE, PLAYER_SIZE, w.x, w.y, w.w, w.h)){
newY = playerY;
break;
}
}
return { x: testX, y: newY };
}
function dist(x1,y1,x2,y2){ return Math.hypot(x2-x1, y2-y1); }
function spawnParticles(x,y,color,count){
for(let i=0;i<count;i++){
particles.push({
x, y, vx:(Math.random()-0.5)*6, vy:(Math.random()-0.5)*6,
life:1, decay:0.02+Math.random()*0.03, color, size:2+Math.random()*3
});
}
}
function respawnMonster(m,idx){
// Teleport monster to random corner area
const corners = [
{x: canvas.width-150, y: canvas.height-150},
{x: canvas.width-100, y: 80},
{x: 80, y: canvas.height-100},
];
const c = corners[Math.floor(Math.random()*corners.length)];
m.x = c.x; m.y = c.y;
m.speed += 0.3; // gets faster each respawn
spawnParticles(m.x,m.y,'#ff0000',15);
}
function resetGame(){
playerX=80; playerY=80; lives=3; startTime=Date.now();
monsters=[{x:700,y:400,speed:MONSTER_BASE_SPEED}];
particles=[]; shakeAmount=0;
document.getElementById('gameover').style.display='none';
loop();
}
// ─── Update ────────────────────────────────────────────
function update(dt){
const SPEED = 4;
let nx = playerX, ny = playerY;
if(keysPressed['ArrowUp']||keysPressed['w']) ny -= SPEED;
if(keysPressed['ArrowDown']||keysPressed['s']) ny += SPEED;
if(keysPressed['ArrowLeft']||keysPressed['a']) nx -= SPEED;
if(keysPressed['ArrowRight']||keysPressed['d']) nx += SPEED;
// Clamp to bounds
nx = Math.max(hw,nx); ny = Math.max(hw,ny);
nx = Math.min(canvas.width-hw,nx); ny = Math.min(canvas.height-hw,ny);
// Resolve wall collisions
const resolved = resolveCollisions(nx, ny, PLAYER_SIZE/2);
playerX = resolved.x;
playerY = resolved.y;
// Flashlight flicker
flashlightAngle += dt*2;
// Monsters
const elapsed = (Date.now()-startTime)/1000;
document.getElementById('timer').textContent = Math.floor(elapsed);
document.getElementById('level').textContent = Math.ceil(elapsed/30)+1;
for(let mi=monsters.length-1;mi>=0;mi--){
const m = monsters[mi];
const dx = playerX-m.x, dy = playerY-m.y;
const d = Math.hypot(dx,dy);
if(d>0){
const spd = m.speed*(1+elapsed/120); // slowly increase overall speed
let mv_x = (dx/d)*spd;
let mv_y = (dy/d)*spd;
// Simple AI: try moving toward player, stop at walls
let newMX = m.x+mv_x, newMY = m.y;
let blocked=false;
for(const w of WALL_DEFS_RAW.map(getScaledWall)){
if(dist(newMX,newMY+w.size/2)>PLAYER_SIZE&&rectOverlap(newMX-playerX*.5,newMY+m.size*.5,...)){blocked=true;break;}
}
if(!blocked)m.x=newMX else m.x+=mv_x*0.3;
newMX=m.x; newMY=m.y+mv_y;
blocked=false;
for(const w of WALL_DEFS_RAW.map(getScaledWall)){
if(rectOverlap(newMX-10,newMY-10,20,20,w.x,w.y,w.w,w.h)){blocked=true;break;}
}
if(!blocked)m.y=newMY else m.y+=mv_y*0.3;
}
// Hit detection
if(dist(playerX,playerY,m.x,m.y)<PLAYER_SIZE+10){
lives--;
shakeAmount=15;
spawnParticles(playerX,playerY,'#ffff00',20);
if(lives<=0){
document.getElementById('finalTime').textContent=Math.floor(elapsed);
document.getElementById('gameover').style.display='flex';
cancelAnimationFrame(frameId);
return;
}else{
// Knockback player
const angle=Math.atan2(playerY-m.y,playerX-m.x);
playerX+=Math.cos(angle)*80;
playerY+=Math.sin(angle)*80;
playerX=Math.max(hw,Math.min(canvas.width-hw,playerX));
playerY=Math.max(hw,Math.min(canvas.height-hw,playerY));
// Respawn monster elsewhere
respawnMonster(m,mi);
}
}
}
// Particles
particles.forEach(p=>{p.x+=p.vx;p.y+=p.vy;p.life-=p.decay;});
particles=particles.filter(p=>p.life>0);
// Shake decay
if(shakeAmount>0)shakeAmount*=0.85;
}
let frameId=null;
// ─── Draw ──────────────────────────────────────────────
function draw(){
ctx.save();
// Screen shake offset
if(shakeAmount>0.5){
ctx.translate((Math.random()-.5)*shakeAmount,(Math.random()-.5)*shakeAmount);
}
// Background
ctx.fillStyle='#181818';
ctx.fillRect(-50,-50,canvas.width+100,canvas.height+100);
// Grid floor
ctx.strokeStyle='#222';ctx.lineWidth=1;
for(let gx=0;gx<canvas.width;gx+=40){ctx.beginPath();ctx.moveTo(gx,0);ctx.lineTo(gx,canvas.height);ctx.stroke();}
for(let gy=0;gy<canvas.height;gy+=40){ctx.beginPath();ctx.moveTo(0,gy);ctx.lineTo(canvas.width,gy);ctx.stroke();}
// Walls
const walls=WALL_DEFS_RAW.map(getScaledWall);
for(const w of walls){
ctx.fillStyle='#444';
ctx.shadowColor='#000';ctx.shadowBlur=15;
ctx.fillRect(w.x,w.y,w.w,w.h);
ctx.shadowBlur=0;
// Brick-like border
ctx.strokeStyle='#555';ctx.lineWidth=2;
ctx.strokeRect(w.x,w.y,w.w,w.h);
}
// Exit door indicator (far right side – survival goal!)
ctx.fillStyle=`rgba(0,${Math.floor(200+55*Math.sin(Date.now()/300))},0,0.3)`;
ctx.font='bold 24px Courier New';ctx.textAlign='center';
ctx.fillText('EXIT ▶',canvas.width-60,canvas.height/2);
// Player glow
const grd=ctx.createRadialGradient(playerX,playerY,0,playerX,playerY,60);
grd.addColorStop(0,'rgba(0,255,255,0.15)');grd.addColorStop(1,'rgba(0,255,255,0)');
ctx.fillStyle=grd;ctx.fillRect(playerX-60,playerY-60,120,120);
// Player
ctx.fillStyle=cyan;
ctx.shadowColor='#0ff';ctx.shadowBlur=15;
ctx.fillRect(playerX-PLAYER_SIZE/2,playerY-PLAYER_SIZE/2,PLAYER_SIZE,PLAYER_SIZE);
ctx.shadowBlur=0;
// Eyes
ctx.fillStyle='#000';
ctx.fillRect(playerX-5,playerY-5,3,3);ctx.fillRect(playerX+2,playerY-5,3,3);
// Monsters
for(const m of monsters){
const pulse=.9+.1*Math.sin(Date.now()/200);
// Glow
const mg=ctx.createRadialGradient(m.x,m.y,0,m.x,m.y,50*pulse);
mg.addColorStop(0,'rgba(255,0,0,0.2)');mg.addColorStop(1,'rgba(255,0,0,0)');
ctx.fillStyle=mg;ctx.fillRect(m.x-50,m.y-50,100,100);
// Body
ctx.fillStyle='#cc0000';
ctx.shadowColor='#f00';ctx.shadowBlur=20;
ctx.beginPath();ctx.arc(m.x,m.y,20*pulse,0,Math.PI*2);ctx.fill();
ctx.shadowBlur=0;
// Angry eyes
ctx.fillStyle='#ff0';
ctx.beginPath();ctx.arc(m.x-6,m.y-4,3,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(m.x+6,m.y-4,3,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#000';
ctx.beginPath();ctx.arc(m.x-6,m.y-4,1.5,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(m.x+6,m.y-4,1.5,0,Math.PI*2);ctx.fill();
// Mouth
ctx.strokeStyle='#800';ctx.lineWidth=2;
ctx.beginPath();ctx.arc(m.x,m.y+6,8,0,Math.PI);ctx.stroke();
}
// Particles
particles.forEach(p=>{
ctx.globalAlpha=p.life;
ctx.fillStyle=p.color;
ctx.fillRect(p.x-p.size/2,p.y-p.size/2,p.size,p.size);
});
ctx.globalAlpha=1;
// Darkness / Vignette effect
const vgrd=ctx.createRadialGradient(playerX,playerY,100,playerX,playerY,canvas.width*.6);
vgrd.addColorStop(0,'rgba(0,0,0,0)');vgrd.addColorStop(.6,'rgba(0,0,0,0.5)');vgrd.addColorStop(1,'rgba(0,0,0,0.85)');
ctx.fillStyle=vgrd;ctx.fillRect(-50,-50,canvas.width+100,canvas.height+100);
// Scanlines
ctx.fillStyle='rgba(0,0,0,0.04)';
for(let sl=0;sl<canvas.height;sl+=3){ctx.fillRect(-50,sl,canvas.width+100,1);}
ctx.restore();
}
// ─── Main Loop ─────────────────────────────────────────
let prevTs=null;
function loop(ts=0){
const dt=(ts-prevTs)/1000||0.016;prevTs=ts;
update(dt);draw();
frameId=requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>
Game Source: Horror Adventure
Creator: AtomicHawk35
Libraries: none
Complexity: complex (390 lines, 13.0 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: horror-adventure-atomichawk35" to link back to the original. Then publish at arcadelab.ai/publish.