🎮ArcadeLab

Claude's Warehouse

by PrismWave59
1684 lines25.3 KB
▶ Play
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Claude's Warehouse</title>

<style>
*{box-sizing:border-box}

html,body{
    margin:0;
    width:100%;
    height:100%;
    overflow:hidden;
    background:#050505;
    font-family:Arial,sans-serif;
    color:white;
}

canvas{
    width:100%;
    height:100%;
    display:block;
    background:#111;
    cursor:crosshair;
}

#menu{
    position:fixed;
    inset:0;
    z-index:10;
    display:flex;
    align-items:center;
    justify-content:center;
    flex-direction:column;
    text-align:center;
    background:
        radial-gradient(circle at center,#302b21,#090909 70%);
}

#menu h1{
    font-family:Georgia,serif;
    font-size:clamp(40px,8vw,85px);
    letter-spacing:6px;
    margin:0 0 15px;
    text-shadow:0 5px 0 #111;
}

#menu p{
    color:#aaa;
    max-width:600px;
    line-height:1.6;
}

button{
    margin-top:20px;
    padding:15px 35px;
    background:#d3ad45;
    border:0;
    font-size:17px;
    font-weight:bold;
    cursor:pointer;
}

#hud{
    position:fixed;
    inset:0;
    pointer-events:none;
    z-index:5;
    display:none;
}

#objective{
    position:absolute;
    top:18px;
    left:18px;
    background:rgba(0,0,0,.75);
    padding:12px 16px;
    border-left:4px solid #d3ad45;
    font-size:14px;
}

#inventory{
    position:absolute;
    top:18px;
    right:18px;
    background:rgba(0,0,0,.75);
    padding:12px 16px;
    min-width:150px;
    font-size:13px;
}

#crosshair{
    position:absolute;
    left:50%;
    top:50%;
    transform:translate(-50%,-50%);
    font-size:20px;
}

#prompt{
    position:absolute;
    left:50%;
    bottom:18%;
    transform:translateX(-50%);
    background:rgba(0,0,0,.85);
    border:1px solid #777;
    padding:10px 18px;
    display:none;
}

#message{
    position:absolute;
    left:50%;
    bottom:35px;
    transform:translateX(-50%);
    background:rgba(0,0,0,.9);
    padding:12px 20px;
    border:1px solid #555;
    opacity:0;
    transition:.25s;
}

#message.show{
    opacity:1;
}

#cutscene{
    position:fixed;
    inset:0;
    z-index:20;
    background:#050505;
    display:none;
    align-items:center;
    justify-content:center;
    flex-direction:column;
    text-align:center;
    padding:30px;
}

#cutsceneText{
    max-width:800px;
    font-size:21px;
    line-height:1.7;
}

#cutsceneHint{
    position:absolute;
    bottom:25px;
    color:#666;
}

#ending{
    position:fixed;
    inset:0;
    z-index:30;
    display:none;
    align-items:center;
    justify-content:center;
    flex-direction:column;
    background:#000;
    text-align:center;
}

#ending h1{
    font-family:Georgia,serif;
    font-size:clamp(38px,8vw,90px);
    letter-spacing:8px;
    animation:flicker 2s infinite;
}

#ending p{
    color:#555;
    letter-spacing:5px;
}

@keyframes flicker{
    0%,20%,23%,60%,100%{opacity:1}
    21%,22%,61%{opacity:.1}
}
</style>
</head>

<body>

<canvas id="game"></canvas>

<div id="menu">
    <h1>CLAUDE'S<br>WAREHOUSE</h1>

    <p>
        The warehouse was abandoned after its mascots
        supposedly attacked the workers.
    </p>

    <p>
        WASD — Move<br>
        Mouse — Look<br>
        E — Interact
    </p>

    <button onclick="startGame()">START GAME</button>
</div>

<div id="cutscene">
    <div id="cutsceneText"></div>
    <div id="cutsceneHint">
        CLICK or press SPACE
    </div>
</div>

<div id="hud">

    <div id="objective">
        OBJECTIVE: Explore the warehouse.
    </div>

    <div id="inventory">
        INVENTORY
        <hr>
        <span id="items">Empty</span>
    </div>

    <div id="crosshair">+</div>

    <div id="prompt"></div>

    <div id="message"></div>

</div>

<div id="ending">
    <h1>TO BE CONTINUED</h1>
    <p>CLAUDE'S WAREHOUSE</p>
</div>

<script>

/* =====================================================
   CLAUDE'S WAREHOUSE
   100% STANDALONE
   NO EXTERNAL LIBRARIES
===================================================== */

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

let W=0;
let H=0;

function resize(){
    W=canvas.width=innerWidth;
    H=canvas.height=innerHeight;
}

resize();

window.addEventListener("resize",resize);


/* =====================================================
   GAME STATE
===================================================== */

let playing=false;
let state="intro";

let x=0;
let y=27;

let angle=0;

const keys={};

let inventory=[];

let objective="Explore the warehouse.";

let interactText="";

let messageTimer=0;

let introIndex=0;


/* =====================================================
   INTRO
===================================================== */

const intro=[
    "A few years ago, you worked at a warehouse.",
    "The warehouse was strange, but it was your job.",
    "Then the mascots supposedly started attacking people.",
    "Workers disappeared.",
    "The warehouse was shut down.",
    "Years later, a note arrives from the factory.",
    "\"COME BACK TO THE WAREHOUSE.\"",
    "You don't know why they want you back.",
    "But you return.",
    "You drive toward the abandoned building.",
    "You park outside.",
    "You still have your old employee keys.",
    "You walk toward the entrance..."
];

function startGame(){

    document.getElementById("menu").style.display="none";

    document.getElementById("cutscene").style.display="flex";

    introIndex=0;

    document.getElementById("cutsceneText")
        .textContent=intro[0];

}

function advanceIntro(){

    introIndex++;

    if(introIndex>=intro.length){

        document.getElementById("cutscene")
            .style.display="none";

        document.getElementById("hud")
            .style.display="block";

        playing=true;

        state="warehouse";

        objective=
            "Explore the warehouse.";

        showMessage(
            "You unlock the old warehouse door.",
            3500
        );

        return;
    }

    document.getElementById("cutsceneText")
        .textContent=intro[introIndex];
}

document.getElementById("cutscene")
    .addEventListener("click",advanceIntro);


/* =====================================================
   INPUT
===================================================== */

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

    keys[e.code]=true;

    if(
        document.getElementById("cutscene")
        .style.display==="flex" &&
        e.code==="Space"
    ){
        advanceIntro();
        return;
    }

    if(
        playing &&
        e.code==="KeyE"
    ){
        interact();
    }

});

document.addEventListener("keyup",e=>{
    keys[e.code]=false;
});


/* =====================================================
   MOUSE LOOK
===================================================== */

canvas.addEventListener("click",()=>{

    if(playing){

        canvas.requestPointerLock();
    }

});

document.addEventListener("mousemove",e=>{

    if(document.pointerLockElement===canvas){

        /*
          This is the actual player rotation.
        */

        angle+=e.movementX*0.0025;
    }

});


/* =====================================================
   WAREHOUSE MAP
===================================================== */

/*
   The warehouse is a grid.

   # = wall
   . = floor
*/

const mapW=70;
const mapH=70;

const walls=[];


/* aisle walls */

for(let row=0;row<8;row++){

    const z=8+row*7;

    for(let col=0;col<6;col++){

        const xx=-27+col*10;

        walls.push({
            x:xx-2.2,
            y:z,
            w:4.5,
            h:5
        });

        walls.push({
            x:xx+2.2,
            y:z,
            w:4.5,
            h:5
        });
    }
}


/* =====================================================
   AISLE DATA
===================================================== */

const aisles=[];

const labels=[
    "TOOLS",
    "ELECTRICAL",
    "PLUMBING",
    "HARDWARE",
    "PAINT",
    "LUMBER",
    "FASTENERS",
    "STORAGE",
    "BUILDING",
    "LIGHTING"
];

const blank=[
    7,13,18,25,31,38,44
];

const ripped=[
    9,22,34,41
];

const scratched=[
    5,14,27,36,47
];

let aisleNumber=1;

for(let row=0;row<8;row++){

    for(let col=0;col<6;col++){

        if(aisleNumber>47)break;

        aisles.push({

            number:aisleNumber,

            x:-27+col*10,

            y:8+row*7,

            label:
                blank.includes(aisleNumber)
                ? ""
                : ripped.includes(aisleNumber)
                ? "RIPPED"
                : scratched.includes(aisleNumber)
                ? "SCRATCHED"
                : labels[
                    aisleNumber%labels.length
                ]
        });

        aisleNumber++;
    }
}


/* =====================================================
   ITEMS
===================================================== */

const items=[

    {
        type:"crowbar",
        x:-27,
        y:8,
        taken:false
    },

    {
        type:"srkey",
        x:-25.5,
        y:8,
        taken:false
    }

];


/* =====================================================
   DRAWING
===================================================== */

function clear(){

    ctx.fillStyle="#08090b";
    ctx.fillRect(0,0,W,H);

}


/* pseudo 3D raycasting */

const FOV=Math.PI/3;

function raycast(){

    const horizon=H*.52;

    /*
       ceiling
    */

    const ceiling=
        ctx.createLinearGradient(
            0,0,0,horizon
        );

    ceiling.addColorStop(
        0,
        "#08090c"
    );

    ceiling.addColorStop(
        1,
        "#25262a"
    );

    ctx.fillStyle=ceiling;

    ctx.fillRect(
        0,0,W,horizon
    );


    /*
       floor
    */

    const floor=
        ctx.createLinearGradient(
            0,horizon,0,H
        );

    floor.addColorStop(
        0,
        "#343434"
    );

    floor.addColorStop(
        1,
        "#080808"
    );

    ctx.fillStyle=floor;

    ctx.fillRect(
        0,horizon,W,H-horizon
    );


    /*
       floor lines
    */

    ctx.strokeStyle=
        "rgba(255,255,255,.06)";

    for(
        let i=1;
        i<15;
        i++
    ){

        const yy=
            horizon+
            Math.pow(i/15,1.7)*
            (H-horizon);

        ctx.beginPath();

        ctx.moveTo(0,yy);
        ctx.lineTo(W,yy);

        ctx.stroke();
    }


    /*
       fake depth objects
    */

    const visible=[];

    for(const a of aisles){

        const dx=a.x-x;
        const dy=a.y-y;

        const dist=
            Math.sqrt(dx*dx+dy*dy);

        let rel=
            Math.atan2(
                dy,
                dx
            )-angle;

        while(rel>Math.PI)
            rel-=Math.PI*2;

        while(rel<-Math.PI)
            rel+=Math.PI*2;

        if(
            Math.abs(rel)<FOV*.9 &&
            dist<45
        ){

            visible.push({
                a,
                dist,
                rel
            });
        }
    }


    visible.sort(
        (a,b)=>b.dist-a.dist
    );


    for(const obj of visible){

        drawAisle(
            obj.a,
            obj.dist,
            obj.rel
        );
    }


    /*
       security room
    */

    drawObject(
        28,
        -30,
        5,
        "#22252a",
        "SECURITY ROOM"
    );


    /*
       entrance
    */

    drawObject(
        0,
        -33,
        8,
        "#151515",
        "EXIT"
    );


    /*
       items
    */

    for(const item of items){

        if(item.taken)
            continue;

        drawItem(item);
    }


    /*
       cameras
    */

    drawCamera(
        -31,-31
    );

    drawCamera(
        31,-31
    );

    drawCamera(
        -31,31
    );

    drawCamera(
        31,31
    );

}


/* =====================================================
   DRAW AISLE
===================================================== */

function drawAisle(a,dist,rel){

    const screenX=
        W/2+
        (rel/FOV)*W;

    if(
        screenX<-200 ||
        screenX>W+200
    )return;

    const size=
        700/dist;

    const top=
        H*.52-size*.7;

    const color=
        a.label==="SCRATCHED"
        ? "#252525"
        : a.label==="RIPPED"
        ? "#333333"
        : "#42474a";


    /*
       shelf
    */

    ctx.fillStyle=color;

    ctx.fillRect(
        screenX-size*.35,
        top,
        size*.7,
        size*1.4
    );


    /*
       colored boxes
    */

    const boxColors=[
        "#8d302e",
        "#315985",
        "#bd9c35"
    ];

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

        ctx.fillStyle=
            boxColors[
                (a.number+i)%3
            ];

        ctx.fillRect(
            screenX-size*.25,
            top+size*(.2+i*.25),
            size*.5,
            size*.13
        );
    }


    /*
       sign
    */

    ctx.fillStyle=
        a.label==="SCRATCHED"
        ? "#151515"
        : "#c3aa5a";

    ctx.fillRect(
        screenX-size*.35,
        top-size*.2,
        size*.7,
        size*.22
    );


    ctx.fillStyle="#111";

    ctx.font=
        Math.max(
            8,
            size*.12
        )+
        "px Arial";

    ctx.textAlign="center";

    ctx.fillText(
        "AISLE "+a.number,
        screenX,
        top-size*.04
    );


    if(a.label){

        ctx.font=
            Math.max(
                6,
                size*.08
            )+
            "px Arial";

        ctx.fillText(
            a.label,
            screenX,
            top+size*.11
        );
    }


    /*
       Claude scratches
    */

    if(a.label==="SCRATCHED"){

        ctx.strokeStyle="#070707";

        ctx.lineWidth=
            Math.max(
                2,
                size*.035
            );

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

            ctx.beginPath();

            ctx.moveTo(
                screenX-size*.25+i*size*.12,
                top+size*.35
            );

            ctx.lineTo(
                screenX-size*.05+i*size*.12,
                top+size*.9
            );

            ctx.stroke();
        }
    }
}


/* =====================================================
   DRAW GENERIC OBJECT
===================================================== */

function drawObject(
    ox,
    oy,
    objectSize,
    color,
    text
){

    const dx=ox-x;
    const dy=oy-y;

    const dist=
        Math.sqrt(dx*dx+dy*dy);

    let rel=
        Math.atan2(
            dy,
            dx
        )-angle;

    while(rel>Math.PI)
        rel-=Math.PI*2;

    while(rel<-Math.PI)
        rel+=Math.PI*2;

    if(
        Math.abs(rel)>FOV
    )return;

    const sx=
        W/2+
        rel/FOV*W;

    const size=
        objectSize*600/dist;

    ctx.fillStyle=color;

    ctx.fillRect(
        sx-size/2,
        H*.5-size,
        size,
        size*1.5
    );

    ctx.fillStyle="#ddd";

    ctx.font=
        Math.max(
            8,
            size*.09
        )+
        "px Arial";

    ctx.textAlign="center";

    ctx.fillText(
        text,
        sx,
        H*.5-size-10
    );
}


/* =====================================================
   ITEMS
===================================================== */

function drawItem(item){

    const dx=item.x-x;
    const dy=item.y-y;

    const dist=
        Math.sqrt(dx*dx+dy*dy);

    let rel=
        Math.atan2(
            dy,
            dx
        )-angle;

    while(rel>Math.PI)
        rel-=Math.PI*2;

    while(rel<-Math.PI)
        rel+=Math.PI*2;

    if(
        Math.abs(rel)>FOV
    )return;

    const sx=
        W/2+
        rel/FOV*W;

    const size=
        400/dist;

    if(item.type==="crowbar"){

        ctx.strokeStyle="#aaa";

        ctx.lineWidth=
            Math.max(
                3,
                size*.07
            );

        ctx.beginPath();

        ctx.moveTo(
            sx-size/2,
            H*.54
        );

        ctx.lineTo(
            sx+size/2,
            H*.42
        );

        ctx.stroke();
    }

    if(item.type==="srkey"){

        ctx.fillStyle="#e0b83c";

        ctx.beginPath();

        ctx.arc(
            sx,
            H*.53,
            Math.max(4,size*.1),
            0,
            Math.PI*2
        );

        ctx.fill();
    }
}


/* =====================================================
   CAMERAS
===================================================== */

function drawCamera(ox,oy){

    const dx=ox-x;
    const dy=oy-y;

    const dist=
        Math.sqrt(dx*dx+dy*dy);

    let rel=
        Math.atan2(
            dy,
            dx
        )-angle;

    while(rel>Math.PI)
        rel-=Math.PI*2;

    while(rel<-Math.PI)
        rel+=Math.PI*2;

    if(
        Math.abs(rel)>FOV
    )return;

    const sx=
        W/2+
        rel/FOV*W;

    const size=
        300/dist;

    ctx.fillStyle="#111";

    ctx.fillRect(
        sx-size/2,
        H*.3,
        size,
        size*.5
    );

    ctx.fillStyle="#e32e2e";

    ctx.beginPath();

    ctx.arc(
        sx,
        H*.34,
        Math.max(2,size*.08),
        0,
        Math.PI*2
    );

    ctx.fill();
}


/* =====================================================
   MOVEMENT
===================================================== */

function move(){

    if(!playing)return;

    let dx=0;
    let dy=0;

    if(keys.KeyW){
        dx+=Math.cos(angle);
        dy+=Math.sin(angle);
    }

    if(keys.KeyS){
        dx-=Math.cos(angle);
        dy-=Math.sin(angle);
    }

    if(keys.KeyA){
        dx+=Math.cos(angle-Math.PI/2);
        dy+=Math.sin(angle-Math.PI/2);
    }

    if(keys.KeyD){
        dx+=Math.cos(angle+Math.PI/2);
        dy+=Math.sin(angle+Math.PI/2);
    }

    const len=
        Math.sqrt(
            dx*dx+dy*dy
        );

    if(len>0){

        dx/=len;
        dy/=len;

        const speed=.12;

        const nx=x+dx*speed;
        const ny=y+dy*speed;

        if(nx>-32 && nx<32)
            x=nx;

        if(ny>-32 && ny<32)
            y=ny;
    }
}


/* =====================================================
   INTERACTION
===================================================== */

function interact(){

    /*
       Aisle 19 is approximately:
       x=-27, y=8
    */

    const d19=
        Math.hypot(
            x-(-27),
            y-8
        );

    if(
        d19<5 &&
        !items[0].taken
    ){

        items[0].taken=true;

        inventory.push(
            "Crowbar"
        );

        updateInventory();

        objective=
            "Find the key marked SR.";

        showMessage(
            "You picked up the crowbar.",
            2500
        );

        return;
    }


    const keyDistance=
        Math.hypot(
            x-(-25.5),
            y-8
        );

    if(
        keyDistance<4 &&
        !items[1].taken
    ){

        items[1].taken=true;

        inventory.push(
            "SR Key"
        );

        updateInventory();

        objective=
            "Go to the Security Room.";

        showMessage(
            "You found the SR key.",
            2500
        );

        return;
    }


    /*
       Security room
    */

    const securityDistance=
        Math.hypot(
            x-28,
            y-(-30)
        );

    if(securityDistance<6){

        if(
            inventory.includes(
                "SR Key"
            )
        ){

            startClaude();

        }else{

            showMessage(
                "Locked. You need the SR key.",
                2500
            );
        }
    }
}


/* =====================================================
   CLAUDE EVENT
===================================================== */

function startClaude(){

    if(state!=="warehouse")
        return;

    state="claude";

    showMessage(
        "The security room door opens...",
        2500
    );

    setTimeout(()=>{

        showMessage(
            "You hear breathing behind you.",
            2500
        );

    },2500);

    setTimeout(()=>{

        showMessage(
            "CLAUDE GRABS YOU.",
            1800
        );

        setTimeout(
            crateRoom,
            2200
        );

    },5000);
}


/* =====================================================
   CRATE ROOM
===================================================== */

function crateRoom(){

    state="crate";

    x=0;
    y=-10;
    angle=-Math.PI/2;

    objective=
        "Find a way out of the crate room.";

    showMessage(
        "You wake up in a room filled with crates.",
        4000
    );

    setTimeout(()=>{

        objective=
            "Use the crates to reach the narrow window.";

    },3500);
}


/* =====================================================
   CRATE ROOM INTERACTION
===================================================== */

function crateInteraction(){

    if(state!=="crate")
        return;

    const distance=
        Math.hypot(
            x-0,
            y-(-19)
        );

    if(distance<7){

        dominicScene();
    }
}


/* =====================================================
   DOMINIC
===================================================== */

function dominicScene(){

    state="dominic";

    showMessage(
        "You squeeze through the narrow window...",
        2500
    );

    setTimeout(()=>{

        showMessage(
            "CLAUDE IS RIGHT BEHIND YOU!",
            2200
        );

    },2500);

    setTimeout(()=>{

        showMessage(
            "DOMINIC TACKLES CLAUDE!",
            3000
        );

    },4700);

    setTimeout(
        elevatorScene,
        7800
    );
}


/* =====================================================
   ELEVATOR
===================================================== */

function elevatorScene(){

    state="elevator";

    x=0;
    y=0;
    angle=Math.PI/2;

    objective=
        "Escape the warehouse.";

    showMessage(
        "Dominic drags you into an elevator.",
        3500
    );

    setTimeout(()=>{

        showMessage(
            "The elevator begins descending...",
            3500
        );

    },3500);

    setTimeout(()=>{

        showMessage(
            "LEVEL -1",
            1800
        );

    },7000);

    setTimeout(()=>{

        showMessage(
            "LEVEL -2",
            2500
        );

    },9000);

    setTimeout(()=>{

        showMessage(
            "A huge underground cement facility...",
            3000
        );

    },11200);

    setTimeout(()=>{

        showMessage(
            "THUD.",
            1000
        );

    },14500);

    setTimeout(()=>{

        showMessage(
            "CLAUDE CRASHES ONTO THE ELEVATOR!",
            2200
        );

    },15300);

    setTimeout(
        endGame,
        17700
    );
}


/* =====================================================
   ENDING
===================================================== */

function endGame(){

    playing=false;

    document.exitPointerLock?.();

    document.getElementById("hud")
        .style.display="none";

    document.getElementById("ending")
        .style.display="flex";
}


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

function updateInventory(){

    document.getElementById("items")
        .innerHTML=
            inventory.length
            ? inventory.join("<br>")
            : "Empty";
}

function updateUI(){

    document.getElementById("objective")
        .textContent=
            "OBJECTIVE: "+objective;

    const prompt=
        document.getElementById("prompt");

    prompt.style.display="none";

    if(!playing)return;

    if(state==="warehouse"){

        const d19=
            Math.hypot(
                x+27,
                y-8
            );

        if(
            d19<5 &&
            !items[0].taken
        ){

            prompt.textContent=
                "E — Pick up Crowbar";

            prompt.style.display="block";

            return;
        }

        const kd=
            Math.hypot(
                x+25.5,
                y-8
            );

        if(
            kd<4 &&
            !items[1].taken
        ){

            prompt.textContent=
                "E — Pick up SR Key";

            prompt.style.display="block";

            return;
        }

        const sd=
            Math.hypot(
                x-28,
                y+30
            );

        if(sd<6){

            prompt.textContent=
                "E — Open Security Room";

            prompt.style.display="block";
        }
    }

    if(state==="crate"){

        const d=
            Math.hypot(
                x,
                y+19
            );

        if(d<7){

            prompt.textContent=
                "E — Squeeze through window";

            prompt.style.display="block";
        }
    }
}

function showMessage(text,time){

    const el=
        document.getElementById("message");

    el.textContent=text;

    el.classList.add("show");

    clearTimeout(messageTimer);

    messageTimer=
        setTimeout(()=>{
            el.classList.remove("show");
        },time);
}


/* =====================================================
   MAIN LOOP
===================================================== */

function loop(){

    move();

    if(state==="crate"){
        crateInteraction();
    }

    clear();

    raycast();

    updateUI();

    /*
       Dark vignette for horror atmosphere.
    */

    const vignette=
        ctx.createRadialGradient(
            W/2,
            H/2,
            H*.15,
            W/2,
            H/2,
            H*.75
        );

    vignette.addColorStop(
        0,
        "rgba(0,0,0,0)"
    );

    vignette.addColorStop(
        1,
        "rgba(0,0,0,.8)"
    );

    ctx.fillStyle=vignette;

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

    requestAnimationFrame(loop);
}

loop();

</script>

</body>
</html>

Game Source: Claude's Warehouse

Creator: PrismWave59

Libraries: none

Complexity: complex (1684 lines, 25.3 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: claude-s-warehouse-prismwave59" to link back to the original. Then publish at arcadelab.ai/publish.