🎮ArcadeLab

MultiBlox

by DriftBear16
1764 lines25.1 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
      content="width=device-width,initial-scale=1.0,user-scalable=no">

<title>MultiBlox</title>

<style>

*{
    box-sizing:border-box;
}

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

canvas{
    display:block;
}

#ui{
    position:fixed;
    top:10px;
    left:10px;
    z-index:20;
    color:white;
    background:rgba(0,0,0,.65);
    border:1px solid rgba(255,255,255,.25);
    border-radius:12px;
    padding:12px;
    min-width:190px;
    pointer-events:none;
}

.logo{
    font-size:23px;
    font-weight:bold;
    margin-bottom:5px;
}

.stat{
    font-size:14px;
    margin:3px 0;
}

#message{
    color:#8cff8c;
    margin-top:6px;
}

#crosshair{
    position:fixed;
    left:50%;
    top:50%;
    transform:translate(-50%,-50%);
    z-index:15;
    color:white;
    font-size:28px;
    font-weight:bold;
    pointer-events:none;
    text-shadow:0 0 4px black;
}

#hotbar{
    position:fixed;
    bottom:20px;
    left:50%;
    transform:translateX(-50%);
    z-index:20;
    display:flex;
    gap:5px;
}

.slot{
    width:58px;
    height:58px;
    border:3px solid #555;
    background:rgba(0,0,0,.65);
    color:white;
    border-radius:7px;
    display:flex;
    align-items:center;
    justify-content:center;
    flex-direction:column;
    font-size:12px;
}

.slot.selected{
    border-color:#fff;
    box-shadow:0 0 10px white;
}

.blockIcon{
    width:25px;
    height:25px;
    margin-bottom:3px;
}

#help{
    position:fixed;
    right:10px;
    top:10px;
    z-index:20;
    color:white;
    background:rgba(0,0,0,.55);
    padding:10px;
    border-radius:10px;
    font-size:13px;
}

#mobile{
    display:none;
    position:fixed;
    bottom:90px;
    left:15px;
    z-index:30;
}

.mbtn{
    width:55px;
    height:55px;
    margin:3px;
    border:0;
    border-radius:12px;
    background:rgba(0,0,0,.55);
    color:white;
    font-size:22px;
}

@media(max-width:700px){

    #help{
        display:none;
    }

    #mobile{
        display:block;
    }

}

</style>
</head>

<body>

<div id="ui">

    <div class="logo">🌍 MultiBlox</div>

    <div class="stat">
        💰 MultiBux:
        <span id="bux">100</span>
    </div>

    <div class="stat">
        🪙 Coins:
        <span id="coins">0</span>
    </div>

    <div class="stat">
        ❤️ Health:
        <span id="health">100</span>
    </div>

    <div class="stat">
        🧱 Blocks:
        <span id="blocks">0</span>
    </div>

    <div id="message">
        World loading...
    </div>

</div>


<div id="help">
    <b>🎮 Controls</b><br>
    WASD — Move<br>
    Mouse — Look<br>
    Space — Jump<br>
    Shift — Sprint<br>
    Left Click — Break<br>
    Right Click — Place<br>
    1–5 — Select Block<br>
    P — Save
</div>


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


<div id="hotbar"></div>


<div id="mobile">

    <button class="mbtn" id="up">▲</button><br>

    <button class="mbtn" id="left">◀</button>

    <button class="mbtn" id="down">▼</button>

    <button class="mbtn" id="right">▶</button>

</div>


<script type="module">

import * as THREE from "three";



/* =====================================================
   MULTIBLOX ENGINE
   ===================================================== */

const scene =
    new THREE.Scene();

scene.background =
    new THREE.Color(0x87ceeb);



/* =====================================================
   CAMERA
   ===================================================== */

const camera =
    new THREE.PerspectiveCamera(
        75,
        innerWidth / innerHeight,
        0.1,
        1000
    );

camera.position.set(
    0,
    4,
    8
);



/* =====================================================
   RENDERER
   ===================================================== */

const renderer =
    new THREE.WebGLRenderer({
        antialias:true
    });

renderer.setPixelRatio(
    Math.min(devicePixelRatio,2)
);

renderer.setSize(
    innerWidth,
    innerHeight
);

document.body.appendChild(
    renderer.domElement
);



/* =====================================================
   LIGHTING
   ===================================================== */

const sun =
    new THREE.DirectionalLight(
        0xffffff,
        2
    );

sun.position.set(
    40,
    80,
    30
);

scene.add(sun);


scene.add(
    new THREE.HemisphereLight(
        0x87ceeb,
        0x446644,
        0.8
    )
);



/* =====================================================
   BLOCK DEFINITIONS
   ===================================================== */

const BLOCKS = {

    grass:{
        color:0x55aa33,
        name:"Grass"
    },

    dirt:{
        color:0x8b5a2b,
        name:"Dirt"
    },

    stone:{
        color:0x777777,
        name:"Stone"
    },

    wood:{
        color:0x8b4513,
        name:"Wood"
    },

    leaves:{
        color:0x228b22,
        name:"Leaves"
    }

};



/* =====================================================
   BLOCK MATERIALS
   ===================================================== */

const materials={};

for(const type in BLOCKS){

    materials[type]=
        new THREE.MeshStandardMaterial({
            color:BLOCKS[type].color
        });

}



/* =====================================================
   WORLD
   ===================================================== */

const world =
    new Map();

const blockGroup =
    new THREE.Group();

scene.add(blockGroup);


const SIZE=32;


function key(x,y,z){

    return x+","+y+","+z;

}


function addBlock(
    x,
    y,
    z,
    type
){

    const k=
        key(x,y,z);

    if(world.has(k))
        return;

    const mesh=
        new THREE.Mesh(

            new THREE.BoxGeometry(
                1,1,1
            ),

            materials[type]
        );

    mesh.position.set(
        x,
        y,
        z
    );

    mesh.userData.type=
        type;

    mesh.userData.block=
        true;

    blockGroup.add(mesh);

    world.set(k,mesh);

}



function removeBlock(
    x,
    y,
    z
){

    const k=
        key(x,y,z);

    const mesh=
        world.get(k);

    if(!mesh)
        return null;

    blockGroup.remove(mesh);

    world.delete(k);

    mesh.geometry.dispose();

    return mesh;

}



/* =====================================================
   TERRAIN GENERATION
   ===================================================== */

function terrainHeight(x,z){

    const a=
        Math.sin(x*0.25)*1.4;

    const b=
        Math.cos(z*0.22)*1.2;

    const c=
        Math.sin((x+z)*0.12)*1.5;

    return Math.max(
        1,
        Math.floor(
            3+a+b+c
        )
    );

}



for(
    let x=-SIZE;
    x<=SIZE;
    x++
){

    for(
        let z=-SIZE;
        z<=SIZE;
        z++
    ){

        const h=
            terrainHeight(x,z);


        for(
            let y=0;
            y<h;
            y++
        ){

            let type="dirt";

            if(y===h-1)
                type="grass";

            else if(y<1)
                type="stone";

            addBlock(
                x,
                y,
                z,
                type
            );

        }

    }

}



/* =====================================================
   TREES
   ===================================================== */

function createTree(x,z){

    const h=
        terrainHeight(x,z);

    for(
        let y=h;
        y<h+4;
        y++
    ){

        addBlock(
            x,
            y,
            z,
            "wood"
        );

    }


    for(
        let dx=-2;
        dx<=2;
        dx++
    ){

        for(
            let dz=-2;
            dz<=2;
            dz++
        ){

            for(
                let dy=2;
                dy<=4;
                dy++
            ){

                if(
                    Math.abs(dx)+
                    Math.abs(dz)+
                    Math.abs(dy-3)
                    <=4
                ){

                    addBlock(
                        x+dx,
                        h+dy,
                        z+dz,
                        "leaves"
                    );

                }

            }

        }

    }

}



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

    const x=
        Math.floor(
            Math.random()*
            (SIZE*2-4)
        )-SIZE+2;

    const z=
        Math.floor(
            Math.random()*
            (SIZE*2-4)
        )-SIZE+2;

    createTree(x,z);

}



/* =====================================================
   WATER
   ===================================================== */

const waterMaterial=
    new THREE.MeshStandardMaterial({

        color:0x168cff,

        transparent:true,

        opacity:.55,

        roughness:.1

    });


const water=
    new THREE.Mesh(

        new THREE.PlaneGeometry(
            SIZE*2,
            SIZE*2
        ),

        waterMaterial

    );


water.rotation.x=
    -Math.PI/2;

water.position.y=
    1.25;

scene.add(water);



/* =====================================================
   PLAYER
   ===================================================== */

const player=
    new THREE.Object3D();

player.position.set(
    0,
    terrainHeight(0,0)+1.8,
    5
);

scene.add(player);



const playerBody=
    new THREE.Mesh(

        new THREE.BoxGeometry(
            .8,
            1.7,
            .8
        ),

        new THREE.MeshStandardMaterial({
            color:0xff4444
        })

    );

player.add(playerBody);

playerBody.position.y=
    -0.8;



/* =====================================================
   PLAYER DATA
   ===================================================== */

let health=100;

let bux=100;

let coins=0;

let selected=0;

let inventory={
    grass:20,
    dirt:20,
    stone:10,
    wood:10,
    leaves:5
};



const blockTypes=
    Object.keys(BLOCKS);



/* =====================================================
   HOTBAR
   ===================================================== */

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


function updateHotbar(){

    hotbar.innerHTML="";


    blockTypes.forEach(
        (type,i)=>{

            const slot=
                document.createElement(
                    "div"
                );

            slot.className=
                "slot";

            if(i===selected)
                slot.classList.add(
                    "selected"
                );


            const icon=
                document.createElement(
                    "div"
                );

            icon.className=
                "blockIcon";

            icon.style.background=
                "#"+
                BLOCKS[type].color
                    .toString(16)
                    .padStart(6,"0");


            const text=
                document.createElement(
                    "div"
                );

            text.textContent=
                (i+1)+
                " "+
                inventory[type];


            slot.appendChild(icon);

            slot.appendChild(text);

            hotbar.appendChild(slot);

        }
    );

}


updateHotbar();



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

function updateUI(){

    document.getElementById(
        "bux"
    ).textContent=bux;

    document.getElementById(
        "coins"
    ).textContent=coins;

    document.getElementById(
        "health"
    ).textContent=health;

    let total=0;

    for(const type in inventory)
        total+=inventory[type];

    document.getElementById(
        "blocks"
    ).textContent=total;

    updateHotbar();

}



function message(text){

    document.getElementById(
        "message"
    ).textContent=text;

}



/* =====================================================
   KEYBOARD
   ===================================================== */

const keys={};


addEventListener(
    "keydown",
    e=>{

        keys[
            e.key.toLowerCase()
        ]=true;


        const n=
            Number(e.key);

        if(
            n>=1 &&
            n<=5
        ){

            selected=n-1;

            updateUI();

        }


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

            saveGame();

        }

    }
);


addEventListener(
    "keyup",
    e=>{

        keys[
            e.key.toLowerCase()
        ]=false;

    }
);



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

let yaw=0;

let pitch=0;

let mouseLocked=false;


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

        if(
            !mouseLocked
        ){

            renderer
                .requestPointerLock();

        }

    }
);


document.addEventListener(
    "pointerlockchange",
    ()=>{

        mouseLocked=
            document.pointerLockElement===
            renderer;

    }
);


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

        if(!mouseLocked)
            return;

        yaw-=e.movementX*.002;

        pitch-=e.movementY*.002;

        pitch=
            Math.max(
                -1.4,
                Math.min(
                    1.4,
                    pitch
                )
            );

    }
);



/* =====================================================
   RAYCASTER
   ===================================================== */

const raycaster=
    new THREE.Raycaster();


const center=
    new THREE.Vector2(0,0);


function targetedBlock(){

    raycaster.setFromCamera(
        center,
        camera
    );

    const hits=
        raycaster.intersectObjects(
            blockGroup.children
        );

    if(
        hits.length===0
    )
        return null;

    return hits[0];

}



/* =====================================================
   BREAK BLOCK
   ===================================================== */

function breakBlock(){

    const hit=
        targetedBlock();

    if(!hit)
        return;


    const block=
        hit.object;


    const p=
        block.position;


    const type=
        block.userData.type;


    /*
       Don't allow the player to
       destroy the world below
       the minimum layer.
    */

    if(p.y<=0)
        return;


    removeBlock(
        p.x,
        p.y,
        p.z
    );


    inventory[type] =
        (inventory[type]||0)+1;


    coins+=1;

    bux+=1;


    updateUI();

    message(
        "⛏️ Broke "+BLOCKS[type].name+
        "  +1 MultiBux"
    );

}



/* =====================================================
   PLACE BLOCK
   ===================================================== */

function placeBlock(){

    const hit=
        targetedBlock();

    if(!hit)
        return;


    const type=
        blockTypes[selected];


    if(
        inventory[type]<=0
    ){

        message(
            "❌ No "+BLOCKS[type].name+
            " blocks!"
        );

        return;

    }


    const normal=
        hit.face.normal;


    const p=
        hit.object.position;


    const x=
        Math.round(
            p.x+normal.x
        );

    const y=
        Math.round(
            p.y+normal.y
        );

    const z=
        Math.round(
            p.z+normal.z
        );


    const k=
        key(x,y,z);


    if(world.has(k))
        return;


    /*
       Prevent placing a block
       directly inside the player.
    */

    if(
        Math.abs(
            x-player.position.x
        )<1 &&
        Math.abs(
            z-player.position.z
        )<1 &&
        Math.abs(
            y-player.position.y
        )<2
    ){

        message(
            "❌ Can't place block here"
        );

        return;

    }


    addBlock(
        x,
        y,
        z,
        type
    );


    inventory[type]--;

    updateUI();

    message(
        "🧱 Placed "+
        BLOCKS[type].name
    );

}



/* =====================================================
   MOUSE BUTTONS
   ===================================================== */

addEventListener(
    "mousedown",
    e=>{

        if(e.button===0)
            breakBlock();

        if(e.button===2)
            placeBlock();

    }
);


addEventListener(
    "contextmenu",
    e=>{

        e.preventDefault();

    }
);



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

let velocityY=0;

let onGround=false;


function updatePlayer(){

    let speed=
        keys.shift ? .22 : .12;


    const forward=
        new THREE.Vector3(
            Math.sin(yaw),
            0,
            Math.cos(yaw)
        );


    const right=
        new THREE.Vector3(
            Math.cos(yaw),
            0,
            -Math.sin(yaw)
        );


    if(keys.w)
        player.position.addScaledVector(
            forward,
            -speed
        );


    if(keys.s)
        player.position.addScaledVector(
            forward,
            speed
        );


    if(keys.a)
        player.position.addScaledVector(
            right,
            -speed
        );


    if(keys.d)
        player.position.addScaledVector(
            right,
            speed
        );


    if(
        keys[" "] &&
        onGround
    ){

        velocityY=.25;

        onGround=false;

    }


    velocityY-=.012;

    player.position.y+=
        velocityY;


    const ground=
        terrainHeight(
            Math.round(
                player.position.x
            ),
            Math.round(
                player.position.z
            )
        )+1.8;


    if(
        player.position.y<
        ground
    ){

        player.position.y=
            ground;

        velocityY=0;

        onGround=true;

    }


    player.rotation.y=
        yaw;

}



/* =====================================================
   CAMERA
   ===================================================== */

function updateCamera(){

    const distance=7;

    const height=3;


    const target=
        new THREE.Vector3(
            player.position.x,
            player.position.y+1,
            player.position.z
        );


    camera.position.set(

        target.x+
        Math.sin(yaw)*
        Math.cos(pitch)*
        distance,

        target.y+
        Math.sin(pitch)*
        distance+
        height,

        target.z+
        Math.cos(yaw)*
        Math.cos(pitch)*
        distance

    );


    camera.lookAt(
        target
    );

}



/* =====================================================
   COINS
   ===================================================== */

const coinGroup=
    new THREE.Group();

scene.add(coinGroup);


function createCoin(x,y,z){

    const coin=
        new THREE.Mesh(

            new THREE.CylinderGeometry(
                .25,
                .25,
                .08,
                16
            ),

            new THREE.MeshStandardMaterial({
                color:0xffd700,
                metalness:.7,
                roughness:.2
            })

        );


    coin.rotation.x=
        Math.PI/2;

    coin.position.set(
        x,y,z
    );

    coin.userData.coin=true;

    coinGroup.add(
        coin
    );

}



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

    const x=
        Math.floor(
            Math.random()*50
        )-25;

    const z=
        Math.floor(
            Math.random()*50
        )-25;

    createCoin(
        x,
        terrainHeight(x,z)+1,
        z
    );

}



function updateCoins(){

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

        const coin=
            coinGroup.children[i];


        coin.rotation.z+=.04;


        const dx=
            coin.position.x-
            player.position.x;

        const dz=
            coin.position.z-
            player.position.z;


        if(
            dx*dx+dz*dz<
            1.5
        ){

            coinGroup.remove(
                coin
            );

            coins++;

            bux+=2;

            updateUI();

            message(
                "🪙 Coin collected! +2 MultiBux"
            );

        }

    }

}



/* =====================================================
   SAVE / LOAD
   ===================================================== */

function saveGame(){

    const data={

        bux:bux,

        coins:coins,

        health:health,

        inventory:inventory,

        position:{
            x:player.position.x,
            y:player.position.y,
            z:player.position.z
        }

    };


    localStorage.setItem(
        "multiblox_save",
        JSON.stringify(data)
    );


    message(
        "💾 MultiBlox saved!"
    );

}



function loadGame(){

    try{

        const raw=
            localStorage.getItem(
                "multiblox_save"
            );


        if(!raw)
            return;


        const data=
            JSON.parse(raw);


        bux=
            data.bux??100;

        coins=
            data.coins??0;

        health=
            data.health??100;

        inventory=
            data.inventory||
            inventory;


        if(data.position){

            player.position.set(
                data.position.x,
                data.position.y,
                data.position.z
            );

        }


    }catch(e){

        console.log(
            "Save could not be loaded."
        );

    }

}


loadGame();

updateUI();



/* =====================================================
   DAY / NIGHT
   ===================================================== */

let worldTime=0;


function updateDayNight(){

    worldTime+=.0007;


    const sunlight=
        Math.sin(worldTime)*
        .5+.5;


    sun.intensity=
        .25+
        sunlight*2;


    const sky=
        new THREE.Color();


    sky.setHSL(
        .55,
        .65,
        .45+
        sunlight*.15
    );


    scene.background=
        sky;

}



/* =====================================================
   WATER ANIMATION
   ===================================================== */

function updateWater(){

    water.position.y=
        1.25+
        Math.sin(
            performance.now()*.001
        )*.03;

}



/* =====================================================
   MOBILE BUTTONS
   ===================================================== */

function mobileKey(
    button,
    key
){

    const element=
        document.getElementById(
            button
        );


    element.addEventListener(
        "touchstart",
        e=>{

            e.preventDefault();

            keys[key]=true;

        }
    );


    element.addEventListener(
        "touchend",
        e=>{

            e.preventDefault();

            keys[key]=false;

        }
    );

}


mobileKey(
    "up",
    "w"
);

mobileKey(
    "down",
    "s"
);

mobileKey(
    "left",
    "a"
);

mobileKey(
    "right",
    "d"
);



/* =====================================================
   GAME LOOP
   ===================================================== */

function animate(){

    requestAnimationFrame(
        animate
    );


    updatePlayer();

    updateCamera();

    updateCoins();

    updateDayNight();

    updateWater();


    renderer.render(
        scene,
        camera
    );

}


animate();



/* =====================================================
   RESIZE
   ===================================================== */

addEventListener(
    "resize",
    ()=>{

        camera.aspect=
            innerWidth/
            innerHeight;

        camera.updateProjectionMatrix();

        renderer.setSize(
            innerWidth,
            innerHeight
        );

    }
);


message(
    "🟢 World ready! Break blocks with left click."
);

</script>

</body>
</html>

Game Source: MultiBlox

Creator: DriftBear16

Libraries: none

Complexity: complex (1764 lines, 25.1 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: multiblox-driftbear16" to link back to the original. Then publish at arcadelab.ai/publish.