🎮ArcadeLab

Horror Adventure

by AtomicHawk35
390 lines13.0 KB
▶ Play
<!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.