🎮ArcadeLab

MiniCraft 3D 800x800

by MegaFlare51
3640 lines49.8 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">

<meta name="viewport"
content="width=device-width,
initial-scale=1,
maximum-scale=1,
user-scalable=no,
viewport-fit=cover">

<title>MiniCraft 3D 800x800</title>

<style>

*{
    box-sizing:border-box;
    margin:0;
    padding:0;
    user-select:none;
    -webkit-user-select:none;
    touch-action:none;
}

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

canvas{
    display:block;
    width:100%;
    height:100%;
}


/* =========================
   HUD
========================= */

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

#fps{
    position:absolute;
    left:10px;
    top:10px;
    color:white;
    font-size:14px;
    font-weight:bold;
    text-shadow:1px 1px 3px #000;
}

#coords{
    position:absolute;
    left:10px;
    top:30px;
    color:white;
    font-size:12px;
    text-shadow:1px 1px 3px #000;
}

#blocks{
    position:absolute;
    left:50%;
    bottom:20px;
    transform:translateX(-50%);
    color:white;
    font-size:17px;
    font-weight:bold;
    text-shadow:1px 1px 3px #000;
}


/* =========================
   MIRA PRECISA
========================= */

#crosshair{
    position:absolute;
    left:50%;
    top:50%;
    width:16px;
    height:16px;
    transform:translate(-50%,-50%);
}

#crosshair::before,
#crosshair::after{
    content:"";
    position:absolute;
    background:white;
    box-shadow:0 0 3px #000;
}

#crosshair::before{
    width:2px;
    height:16px;
    left:7px;
    top:0;
}

#crosshair::after{
    width:16px;
    height:2px;
    left:0;
    top:7px;
}


/* =========================
   BOTÕES
========================= */

.action{
    position:fixed;
    z-index:250;
    pointer-events:auto;
    border:2px solid rgba(255,255,255,.5);
    color:white;
    background:rgba(0,0,0,.55);
    border-radius:14px;
    font-weight:bold;
    box-shadow:0 3px 12px rgba(0,0,0,.35);
}

.action:active{
    transform:scale(.94);
}

#breakBtn{
    width:100px;
    height:58px;
    right:150px;
    bottom:30px;
}

#placeBtn{
    width:100px;
    height:58px;
    right:35px;
    bottom:30px;
}

#jumpBtn{
    width:100px;
    height:50px;
    right:35px;
    bottom:100px;
}

#pauseBtn{
    width:65px;
    height:45px;
    right:15px;
    top:15px;
    font-size:18px;
}

#settingsBtn{
    width:65px;
    height:45px;
    right:90px;
    top:15px;
    font-size:18px;
}


/* =========================
   JOYSTICK
========================= */

#joystick{
    position:fixed;
    left:25px;
    bottom:25px;
    width:125px;
    height:125px;
    border-radius:50%;
    background:rgba(255,255,255,.13);
    border:2px solid rgba(255,255,255,.35);
    z-index:220;
    pointer-events:auto;
}

#stick{
    position:absolute;
    width:58px;
    height:58px;
    left:50%;
    top:50%;
    transform:translate(-50%,-50%);
    border-radius:50%;
    background:rgba(255,255,255,.4);
    border:2px solid rgba(255,255,255,.7);
}


/* =========================
   OLHAR
========================= */

#lookArea{
    position:fixed;
    right:0;
    top:0;
    width:65%;
    height:100%;
    z-index:90;
    pointer-events:auto;
}


/* =========================
   PAUSA
========================= */

#pauseScreen{
    display:none;
    position:fixed;
    inset:0;
    z-index:400;
    background:rgba(0,0,0,.6);
    color:white;
    align-items:center;
    justify-content:center;
    flex-direction:column;
}

#pauseScreen h1{
    font-size:40px;
    margin-bottom:20px;
}

#resumeBtn{
    padding:14px 30px;
    border:0;
    border-radius:12px;
    font-weight:bold;
}


/* =========================
   CONFIGURAÇÕES
========================= */

#settings{
    display:none;
    position:fixed;
    inset:0;
    z-index:500;
    background:rgba(0,0,0,.75);
    align-items:center;
    justify-content:center;
}

.panel{
    width:min(440px,90vw);
    max-height:90vh;
    overflow:auto;
    padding:22px;
    border-radius:18px;
    background:#222;
    color:white;
    box-shadow:0 10px 40px #000;
}

.panel h2{
    text-align:center;
    margin-bottom:20px;
}

.setting{
    margin:16px 0;
}

.setting label{
    display:block;
    margin-bottom:7px;
    font-weight:bold;
}

.setting input{
    width:100%;
}

.value{
    float:right;
}

.panel button{
    width:100%;
    padding:13px;
    margin-top:10px;
    border:0;
    border-radius:10px;
    font-weight:bold;
}


/* =========================
   ORIENTAÇÃO
========================= */

#orientation{
    display:none;
    position:fixed;
    inset:0;
    z-index:1000;
    background:#111;
    color:white;
    align-items:center;
    justify-content:center;
    text-align:center;
    padding:30px;
    font-size:22px;
}

@media (orientation:portrait){
    #orientation{
        display:flex;
    }
}

</style>
</head>

<body>

<div id="orientation">
    📱<br><br>
    Vire o celular para o modo deitado.
</div>


<div id="hud">

    <div id="fps">
        FPS: --
    </div>

    <div id="coords">
        X: 0 Y: 1 Z: 0
    </div>

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

    <div id="blocks">
        Blocos: 64
    </div>

</div>


<div id="joystick">
    <div id="stick"></div>
</div>


<div id="lookArea"></div>


<button id="breakBtn" class="action">
    QUEBRAR
</button>

<button id="placeBtn" class="action">
    COLOCAR
</button>

<button id="jumpBtn" class="action">
    PULAR
</button>

<button id="settingsBtn" class="action">
    ⚙️
</button>

<button id="pauseBtn" class="action">
    Ⅱ
</button>


<div id="pauseScreen">

    <h1>PAUSADO</h1>

    <button id="resumeBtn">
        CONTINUAR
    </button>

</div>


<div id="settings">

    <div class="panel">

        <h2>
            ⚙️ CONFIGURAÇÕES
        </h2>


        <div class="setting">

            <label>
                Qualidade

                <span
                class="value"
                id="qualityValue">
                    100%
                </span>
            </label>

            <input
            id="quality"
            type="range"
            min="0"
            max="100"
            value="100">

        </div>


        <div class="setting">

            <label>
                Renderização

                <span
                class="value"
                id="renderValue">
                    12
                </span>
            </label>

            <input
            id="renderDistance"
            type="range"
            min="1"
            max="70"
            value="12">

        </div>


        <div class="setting">

            <label>
                Sensibilidade

                <span
                class="value"
                id="sensValue">
                    100%
                </span>
            </label>

            <input
            id="sensitivity"
            type="range"
            min="25"
            max="200"
            value="100">

        </div>


        <button id="saveSettings">
            SALVAR
        </button>


        <button id="resetWorld">
            RESETAR MUNDO
        </button>

    </div>

</div>


<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>


<script>

/* =========================================================
   CONFIGURAÇÃO
========================================================= */

const WORLD_SIZE = 800;

const HALF_WORLD =
    WORLD_SIZE / 2;

const CHUNK_SIZE = 16;

const MIN_Y = -3;

const GROUND_TOP = 0;

const MAX_BUILD_Y = 70;

const MAX_REACH = 6;

const PLAYER_WIDTH = .55;

const PLAYER_HEIGHT = 1.8;

const EYE_HEIGHT = 1.62;

const GRAVITY = 24;

const JUMP_SPEED = 8.5;

const WALK_SPEED = 4.8;


/* =========================================================
   ESTADO
========================================================= */

let renderDistance = 12;

let quality = 100;

let sensitivity = 1;

let paused = false;

let blockCount = 64;


/* =========================================================
   THREE
========================================================= */

const scene =
    new THREE.Scene();

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


const camera =
    new THREE.PerspectiveCamera(
        75,
        innerWidth / innerHeight,
        .05,
        120
    );


const renderer =
    new THREE.WebGLRenderer({

        antialias:true,

        powerPreference:
        "high-performance"
    });


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

renderer.setSize(
    innerWidth,
    innerHeight,
    false
);

renderer.outputColorSpace =
    THREE.SRGBColorSpace;

document.body.appendChild(
    renderer.domElement
);


/* =========================================================
   LUZ
========================================================= */

const hemi =
    new THREE.HemisphereLight(
        0xffffff,
        0x666666,
        1.5
    );

scene.add(hemi);


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

sun.position.set(
    100,
    200,
    100
);

scene.add(sun);


/* =========================================================
   MAPA LÓGICO
========================================================= */

const blocks =
    new Map();


function blockKey(x,y,z){

    return (
        x + "," +
        y + "," +
        z
    );
}


function isInsideWorld(x,z){

    return (
        x >= -HALF_WORLD &&
        x < HALF_WORLD &&
        z >= -HALF_WORLD &&
        z < HALF_WORLD
    );
}


function isOriginalGround(y){

    return (
        y >= MIN_Y &&
        y <= GROUND_TOP
    );
}


function hasBlock(x,y,z){

    if(
        !isInsideWorld(x,z)
    ){
        return false;
    }


    if(
        isOriginalGround(y)
    ){
        return true;
    }


    return blocks.has(
        blockKey(x,y,z)
    );
}


/* =========================================================
   CHUNKS
========================================================= */

const chunks =
    new Map();


function chunkKey(cx,cz){

    return (
        cx + "," + cz
    );
}


function worldToChunk(x,z){

    return {

        cx:
        Math.floor(
            (x + HALF_WORLD) /
            CHUNK_SIZE
        ),

        cz:
        Math.floor(
            (z + HALF_WORLD) /
            CHUNK_SIZE
        )
    };
}


function getChunk(cx,cz){

    const k =
        chunkKey(cx,cz);

    let chunk =
        chunks.get(k);


    if(!chunk){

        chunk = {

            cx:cx,

            cz:cz,

            group:
            new THREE.Group(),

            blocks:
            new Set(),

            visible:true
        };


        chunk.group.userData.chunk =
            chunk;


        scene.add(
            chunk.group
        );


        chunks.set(
            k,
            chunk
        );
    }


    return chunk;
}


/* =========================================================
   TEXTURA
========================================================= */

function createStoneTexture(){

    const canvas =
        document.createElement(
            "canvas"
        );

    canvas.width = 64;
    canvas.height = 64;


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


    ctx.fillStyle =
        "#858585";

    ctx.fillRect(
        0,
        0,
        64,
        64
    );


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

        const x =
            Math.random()*64;

        const y =
            Math.random()*64;

        const size =
            1 +
            Math.random()*3;

        const value =
            65 +
            Math.floor(
                Math.random()*65
            );


        ctx.fillStyle =
            `rgb(${value},${value},${value})`;


        ctx.fillRect(
            x,
            y,
            size,
            size
        );
    }


    const texture =
        new THREE.CanvasTexture(
            canvas
        );


    texture.magFilter =
        THREE.NearestFilter;

    texture.minFilter =
        THREE.NearestFilter;


    return texture;
}


const stoneTexture =
    createStoneTexture();


const blockGeometry =
    new THREE.BoxGeometry(
        1,
        1,
        1
    );


const blockMaterial =
    new THREE.MeshLambertMaterial({
        map:stoneTexture
    });


/* =========================================================
   CHÃO VISUAL
========================================================= */

const groundGeometry =
    new THREE.BoxGeometry(
        WORLD_SIZE,
        1,
        WORLD_SIZE
    );


const groundMaterial =
    new THREE.MeshLambertMaterial({
        color:0x4caf50
    });


const groundMesh =
    new THREE.Mesh(
        groundGeometry,
        groundMaterial
    );


groundMesh.position.set(
    0,
    -.5,
    0
);


groundMesh.userData.isGround =
    true;


scene.add(
    groundMesh
);


/* =========================================================
   CRIAR BLOCO
========================================================= */

function createBlockMesh(
    x,
    y,
    z
){

    const c =
        worldToChunk(x,z);


    const chunk =
        getChunk(
            c.cx,
            c.cz
        );


    const mesh =
        new THREE.Mesh(
            blockGeometry,
            blockMaterial
        );


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


    mesh.userData.isBlock =
        true;

    mesh.userData.x = x;
    mesh.userData.y = y;
    mesh.userData.z = z;


    chunk.group.add(
        mesh
    );


    chunk.blocks.add(
        blockKey(x,y,z)
    );


    return mesh;
}


/* =========================================================
   REMOVER BLOCO
========================================================= */

function removeBlockMesh(
    x,
    y,
    z
){

    const c =
        worldToChunk(x,z);


    const chunk =
        chunks.get(
            chunkKey(
                c.cx,
                c.cz
            )
        );


    if(!chunk)
        return;


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


    for(
        let i =
        chunk.group.children.length - 1;

        i >= 0;

        i--
    ){

        const mesh =
            chunk.group.children[i];


        if(
            mesh.userData.x === x &&
            mesh.userData.y === y &&
            mesh.userData.z === z
        ){

            chunk.group.remove(
                mesh
            );

            break;
        }
    }


    chunk.blocks.delete(k);


    if(
        chunk.group.children.length === 0
    ){

        scene.remove(
            chunk.group
        );

        chunks.delete(
            chunkKey(
                c.cx,
                c.cz
            )
        );
    }
}


/* =========================================================
   RENDERIZAÇÃO
========================================================= */

function updateChunkRendering(){

    const pc =
        worldToChunk(
            player.x,
            player.z
        );


    for(
        const chunk
        of chunks.values()
    ){

        const dx =
            chunk.cx - pc.cx;

        const dz =
            chunk.cz - pc.cz;


        const distance =
            Math.max(
                Math.abs(dx),
                Math.abs(dz)
            );


        const visible =
            distance <=
            renderDistance;


        chunk.visible =
            visible;

        chunk.group.visible =
            visible;
    }
}


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

const player = {

    x:0,

    y:1.01,

    z:0,

    vy:0,

    yaw:0,

    pitch:0,

    grounded:false
};


camera.position.set(
    player.x,
    player.y + EYE_HEIGHT,
    player.z
);


/* =========================================================
   TECLADO
========================================================= */

const keys = {};


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

        keys[e.code] = true;


        if(
            e.code === "Space"
        ){

            jump();
        }
    }
);


window.addEventListener(
    "keyup",
    e=>{

        keys[e.code] = false;
    }
);


/* =========================================================
   MOVIMENTO
========================================================= */

let joystickX = 0;
let joystickY = 0;

let keyX = 0;
let keyZ = 0;


function updateKeyboard(){

    let x = 0;
    let z = 0;


    if(
        keys["KeyA"] ||
        keys["ArrowLeft"]
    ){

        x -= 1;
    }


    if(
        keys["KeyD"] ||
        keys["ArrowRight"]
    ){

        x += 1;
    }


    /*
       CIMA = FRENTE
       BAIXO = TRÁS
    */

    if(
        keys["KeyW"] ||
        keys["ArrowUp"]
    ){

        z += 1;
    }


    if(
        keys["KeyS"] ||
        keys["ArrowDown"]
    ){

        z -= 1;
    }


    keyX = x;
    keyZ = z;
}


function getMovement(){

    updateKeyboard();


    let x =
        joystickX +
        keyX;

    let z =
        joystickY +
        keyZ;


    const length =
        Math.hypot(x,z);


    if(
        length > 1
    ){

        x /= length;
        z /= length;
    }


    return {
        x:x,
        z:z
    };
}


/* =========================================================
   JOYSTICK
========================================================= */

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


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


let joystickPointer = null;


function updateJoystick(e){

    const r =
        joystick.getBoundingClientRect();


    const cx =
        r.left +
        r.width/2;


    const cy =
        r.top +
        r.height/2;


    let dx =
        e.clientX -
        cx;


    let dy =
        e.clientY -
        cy;


    const max = 43;


    const distance =
        Math.hypot(dx,dy);


    if(
        distance > max
    ){

        dx =
            dx / distance * max;

        dy =
            dy / distance * max;
    }


    stick.style.transform =
        `translate(
            calc(-50% + ${dx}px),
            calc(-50% + ${dy}px)
        )`;


    joystickX =
        dx / max;


    joystickY =
        -dy / max;
}


joystick.addEventListener(
    "pointerdown",
    e=>{

        joystickPointer =
            e.pointerId;


        joystick.setPointerCapture(
            e.pointerId
        );


        updateJoystick(e);
    }
);


joystick.addEventListener(
    "pointermove",
    e=>{

        if(
            e.pointerId !==
            joystickPointer
        ){

            return;
        }


        updateJoystick(e);
    }
);


function resetJoystick(){

    joystickPointer = null;

    joystickX = 0;
    joystickY = 0;


    stick.style.transform =
        "translate(-50%,-50%)";
}


joystick.addEventListener(
    "pointerup",
    resetJoystick
);


joystick.addEventListener(
    "pointercancel",
    resetJoystick
);


/* =========================================================
   OLHAR
========================================================= */

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


let lookPointer = null;

let lastLookX = 0;
let lastLookY = 0;


lookArea.addEventListener(
    "pointerdown",
    e=>{

        if(paused)
            return;


        lookPointer =
            e.pointerId;


        lookArea.setPointerCapture(
            e.pointerId
        );


        lastLookX =
            e.clientX;

        lastLookY =
            e.clientY;
    }
);


lookArea.addEventListener(
    "pointermove",
    e=>{

        if(
            e.pointerId !==
            lookPointer
        ){

            return;
        }


        if(paused)
            return;


        const dx =
            e.clientX -
            lastLookX;


        const dy =
            e.clientY -
            lastLookY;


        lastLookX =
            e.clientX;

        lastLookY =
            e.clientY;


        player.yaw -=
            dx *
            .006 *
            sensitivity;


        player.pitch -=
            dy *
            .006 *
            sensitivity;


        const limit =
            Math.PI/2 -
            .05;


        player.pitch =
            Math.max(
                -limit,
                Math.min(
                    limit,
                    player.pitch
                )
            );
    }
);


lookArea.addEventListener(
    "pointerup",
    ()=>{
        lookPointer = null;
    }
);


lookArea.addEventListener(
    "pointercancel",
    ()=>{
        lookPointer = null;
    }
);


/* =========================================================
   COLISÃO
========================================================= */

function playerBoxAt(
    x,
    y,
    z
){

    return {

        minX:
            x -
            PLAYER_WIDTH/2,

        maxX:
            x +
            PLAYER_WIDTH/2,

        minY:y,

        maxY:
            y +
            PLAYER_HEIGHT,

        minZ:
            z -
            PLAYER_WIDTH/2,

        maxZ:
            z +
            PLAYER_WIDTH/2
    };
}


function blockBox(
    x,
    y,
    z
){

    return {

        minX:x,
        maxX:x+1,

        minY:y,
        maxY:y+1,

        minZ:z,
        maxZ:z+1
    };
}


function overlap(a,b){

    return (

        a.minX < b.maxX &&
        a.maxX > b.minX &&

        a.minY < b.maxY &&
        a.maxY > b.minY &&

        a.minZ < b.maxZ &&
        a.maxZ > b.minZ
    );
}


function collidesAt(
    x,
    y,
    z
){

    const p =
        playerBoxAt(
            x,
            y,
            z
        );


    const minX =
        Math.floor(
            p.minX
        ) - 1;

    const maxX =
        Math.floor(
            p.maxX
        ) + 1;

    const minY =
        Math.floor(
            p.minY
        ) - 1;

    const maxY =
        Math.floor(
            p.maxY
        ) + 1;

    const minZ =
        Math.floor(
            p.minZ
        ) - 1;

    const maxZ =
        Math.floor(
            p.maxZ
        ) + 1;


    for(
        let bx=minX;
        bx<=maxX;
        bx++
    ){

        for(
            let by=minY;
            by<=maxY;
            by++
        ){

            for(
                let bz=minZ;
                bz<=maxZ;
                bz++
            ){

                if(
                    !hasBlock(
                        bx,
                        by,
                        bz
                    )
                ){

                    continue;
                }


                if(
                    overlap(
                        p,
                        blockBox(
                            bx,
                            by,
                            bz
                        )
                    )
                ){

                    return true;
                }
            }
        }
    }


    return false;
}


/* =========================================================
   MOVIMENTO FÍSICO CORRIGIDO
========================================================= */

function movePlayer(
    dx,
    dy,
    dz
){

    /* X */

    if(
        !collidesAt(
            player.x + dx,
            player.y,
            player.z
        )
    ){

        player.x += dx;
    }


    /* Z */

    if(
        !collidesAt(
            player.x,
            player.y,
            player.z + dz
        )
    ){

        player.z += dz;
    }


    /* Y */

    const oldY =
        player.y;

    const newY =
        player.y + dy;


    if(
        !collidesAt(
            player.x,
            newY,
            player.z
        )
    ){

        player.y =
            newY;

        player.grounded =
            false;
    }

    else{

        /*
           SUBINDO:
           encontrou teto.
        */

        if(
            dy > 0
        ){

            let safeY =
                oldY;

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

                safeY += .01;

                if(
                    !collidesAt(
                        player.x,
                        safeY,
                        player.z
                    )
                ){

                    break;
                }
            }

            player.y =
                safeY;

            player.vy = 0;

            player.grounded = false;
        }

        /*
           DESCENDO:
           encontra o topo mais alto
           abaixo dos pés.
        */

        else{

            const feet =
                oldY;

            let landingY =
                -Infinity;


            const minX =
                Math.floor(
                    player.x -
                    PLAYER_WIDTH/2
                ) - 1;

            const maxX =
                Math.floor(
                    player.x +
                    PLAYER_WIDTH/2
                ) + 1;

            const minZ =
                Math.floor(
                    player.z -
                    PLAYER_WIDTH/2
                ) - 1;

            const maxZ =
                Math.floor(
                    player.z +
                    PLAYER_WIDTH/2
                ) + 1;


            for(
                let bx=minX;
                bx<=maxX;
                bx++
            ){

                for(
                    let bz=minZ;
                    bz<=maxZ;
                    bz++
                ){

                    for(
                        let by=
                        Math.floor(feet)-2;

                        by<=
                        Math.floor(feet)+1;

                        by++
                    ){

                        if(
                            !hasBlock(
                                bx,
                                by,
                                bz
                            )
                        ){

                            continue;
                        }


                        const box =
                            blockBox(
                                bx,
                                by,
                                bz
                            );


                        const horizontal =

                            player.x +
                            PLAYER_WIDTH/2 >
                            box.minX &&

                            player.x -
                            PLAYER_WIDTH/2 <
                            box.maxX &&

                            player.z +
                            PLAYER_WIDTH/2 >
                            box.minZ &&

                            player.z -
                            PLAYER_WIDTH/2 <
                            box.maxZ;


                        if(
                            horizontal &&
                            box.maxY <=
                            feet + .08 &&
                            box.maxY >=
                            newY - .08
                        ){

                            landingY =
                                Math.max(
                                    landingY,
                                    box.maxY
                                );
                        }
                    }
                }
            }


            if(
                landingY !==
                -Infinity
            ){

                player.y =
                    landingY;

                player.vy = 0;

                player.grounded = true;
            }

            else{

                /*
                   Última segurança.
                */

                player.vy = 0;

                player.grounded = true;
            }
        }
    }


    const limit =
        HALF_WORLD -
        PLAYER_WIDTH/2;


    player.x =
        Math.max(
            -limit,
            Math.min(
                limit,
                player.x
            )
        );


    player.z =
        Math.max(
            -limit,
            Math.min(
                limit,
                player.z
            )
        );
}


/* =========================================================
   PULO
========================================================= */

function jump(){

    if(paused)
        return;


    if(
        player.grounded
    ){

        player.vy =
            JUMP_SPEED;

        player.grounded =
            false;
    }
}


document
.getElementById("jumpBtn")
.addEventListener(
    "pointerdown",
    jump
);


/* =========================================================
   RAYCAST — MIRA PRECISA
========================================================= */

const raycaster =
    new THREE.Raycaster();


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


function getTarget(){

    camera.updateMatrixWorld(
        true
    );


    /*
       0,0 é exatamente o centro
       da tela.
    */

    raycaster.setFromCamera(
        rayCenter,
        camera
    );


    raycaster.near =
        .01;

    raycaster.far =
        MAX_REACH;


    let closest = null;


    /* CHÃO */

    const groundHits =
        raycaster.intersectObject(
            groundMesh,
            false
        );


    if(
        groundHits.length
    ){

        closest =
            groundHits[0];
    }


    /* BLOCOS */

    const pc =
        worldToChunk(
            player.x,
            player.z
        );


    const chunkRange =
        Math.ceil(
            MAX_REACH /
            CHUNK_SIZE
        ) + 1;


    const objects = [];


    for(
        let cx =
        pc.cx - chunkRange;

        cx <=
        pc.cx + chunkRange;

        cx++
    ){

        for(
            let cz =
            pc.cz - chunkRange;

            cz <=
            pc.cz + chunkRange;

            cz++
        ){

            const chunk =
                chunks.get(
                    chunkKey(
                        cx,
                        cz
                    )
                );


            if(!chunk)
                continue;


            if(
                !chunk.group.visible
            )
                continue;


            for(
                const mesh
                of chunk.group.children
            ){

                if(
                    mesh.userData.isBlock
                ){

                    objects.push(
                        mesh
                    );
                }
            }
        }
    }


    if(
        objects.length
    ){

        const hits =
            raycaster.intersectObjects(
                objects,
                false
            );


        if(
            hits.length
        ){

            const hit =
                hits[0];


            if(
                !closest ||
                hit.distance <
                closest.distance
            ){

                closest =
                    hit;
            }
        }
    }


    if(!closest)
        return null;


    /* CHÃO */

    if(
        closest.object.userData
        .isGround
    ){

        return {

            type:"ground",

            point:
                closest.point.clone(),

            distance:
                closest.distance
        };
    }


    /* BLOCO */

    const object =
        closest.object;


    if(
        !object.userData.isBlock
    ){

        return null;
    }


    /*
       Face real atingida.
    */

    const normal =
        closest.face.normal.clone();


    normal.transformDirection(
        object.matrixWorld
    );


    let nx = 0;
    let ny = 0;
    let nz = 0;


    const ax =
        Math.abs(normal.x);

    const ay =
        Math.abs(normal.y);

    const az =
        Math.abs(normal.z);


    if(
        ax >= ay &&
        ax >= az
    ){

        nx =
            normal.x >= 0
            ? 1
            : -1;
    }

    else if(
        ay >= ax &&
        ay >= az
    ){

        ny =
            normal.y >= 0
            ? 1
            : -1;
    }

    else{

        nz =
            normal.z >= 0
            ? 1
            : -1;
    }


    return {

        type:"block",

        x:
            object.userData.x,

        y:
            object.userData.y,

        z:
            object.userData.z,

        nx:nx,

        ny:ny,

        nz:nz,

        point:
            closest.point.clone(),

        distance:
            closest.distance
    };
}


/* =========================================================
   COLOCAR BLOCO
========================================================= */

function placeBlock(){

    if(paused)
        return;


    if(
        blockCount <= 0
    ){

        return;
    }


    const target =
        getTarget();


    if(!target)
        return;


    let x;
    let y;
    let z;


    if(
        target.type ===
        "ground"
    ){

        x =
            Math.floor(
                target.point.x
            );

        y = 1;

        z =
            Math.floor(
                target.point.z
            );
    }

    else{

        x =
            target.x +
            target.nx;

        y =
            target.y +
            target.ny;

        z =
            target.z +
            target.nz;
    }


    if(
        !Number.isInteger(x) ||
        !Number.isInteger(y) ||
        !Number.isInteger(z)
    ){

        return;
    }


    if(
        !isInsideWorld(x,z)
    ){

        return;
    }


    if(
        y < 1 ||
        y > MAX_BUILD_Y
    ){

        return;
    }


    if(
        hasBlock(x,y,z)
    ){

        return;
    }


    const pBox =
        playerBoxAt(
            player.x,
            player.y,
            player.z
        );


    const bBox =
        blockBox(
            x,
            y,
            z
        );


    if(
        overlap(
            pBox,
            bBox
        )
    ){

        return;
    }


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


    blocks.set(
        k,
        true
    );


    createBlockMesh(
        x,
        y,
        z
    );


    blockCount--;


    updateHUD();

    updateChunkRendering();

    saveWorld();
}


/* =========================================================
   PARTÍCULAS
========================================================= */

const particles = [];


const particleGeometry =
    new THREE.BoxGeometry(
        .08,
        .08,
        .08
    );


const particleMaterial =
    new THREE.MeshBasicMaterial({

        color:0x777777,

        transparent:true,

        opacity:.9
    });


function createBreakEffect(
    x,
    y,
    z
){

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

        const p =
            new THREE.Mesh(
                particleGeometry,
                particleMaterial.clone()
            );


        p.position.set(
            x+.5,
            y+.5,
            z+.5
        );


        p.userData.life =
            .45;

        p.userData.vx =
            (Math.random()-.5)*4;

        p.userData.vy =
            Math.random()*4;

        p.userData.vz =
            (Math.random()-.5)*4;


        scene.add(p);

        particles.push(p);
    }
}


function updateParticles(dt){

    for(
        let i =
        particles.length - 1;

        i >= 0;

        i--
    ){

        const p =
            particles[i];


        p.userData.life -=
            dt;


        p.position.x +=
            p.userData.vx * dt;

        p.position.y +=
            p.userData.vy * dt;

        p.position.z +=
            p.userData.vz * dt;


        p.userData.vy -=
            12 * dt;


        p.material.opacity =
            Math.max(
                0,
                p.userData.life / .45
            );


        if(
            p.userData.life <= 0
        ){

            scene.remove(p);

            p.material.dispose();

            particles.splice(
                i,
                1
            );
        }
    }
}


/* =========================================================
   QUEBRAR
========================================================= */

function breakBlock(){

    if(paused)
        return;


    const target =
        getTarget();


    if(!target)
        return;


    /*
       O chão original não pode
       ser destruído.
    */

    if(
        target.type !==
        "block"
    ){

        return;
    }


    const x =
        target.x;

    const y =
        target.y;

    const z =
        target.z;


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


    if(
        !blocks.has(k)
    ){

        return;
    }


    blocks.delete(k);


    removeBlockMesh(
        x,
        y,
        z
    );


    blockCount++;


    createBreakEffect(
        x,
        y,
        z
    );


    updateHUD();

    updateChunkRendering();

    saveWorld();
}


/* =========================================================
   BOTÕES
========================================================= */

document
.getElementById("placeBtn")
.addEventListener(
    "pointerdown",
    placeBlock
);


document
.getElementById("breakBtn")
.addEventListener(
    "pointerdown",
    breakBlock
);


/* =========================================================
   HUD
========================================================= */

function updateHUD(){

    document
    .getElementById("blocks")
    .textContent =
        "Blocos: " +
        blockCount;


    document
    .getElementById("coords")
    .textContent =

        "X: " +
        player.x.toFixed(1) +

        " Y: " +
        player.y.toFixed(1) +

        " Z: " +
        player.z.toFixed(1);
}


/* =========================================================
   CONFIGURAÇÕES
========================================================= */

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


const qualitySlider =
    document.getElementById(
        "quality"
    );


const renderSlider =
    document.getElementById(
        "renderDistance"
    );


const sensSlider =
    document.getElementById(
        "sensitivity"
    );


document
.getElementById("settingsBtn")
.addEventListener(
    "pointerdown",
    ()=>{

        paused = true;

        settings.style.display =
            "flex";
    }
);


/*
   QUALIDADE:
   aplica imediatamente.
*/

qualitySlider.addEventListener(
    "input",
    ()=>{

        quality =
            Number(
                qualitySlider.value
            );


        document
        .getElementById(
            "qualityValue"
        )
        .textContent =
            quality + "%";


        applyQuality();
    }
);


/*
   RENDERIZAÇÃO:
   aplica imediatamente ao mundo.
*/

renderSlider.addEventListener(
    "input",
    ()=>{

        renderDistance =
            Number(
                renderSlider.value
            );


        document
        .getElementById(
            "renderValue"
        )
        .textContent =
            renderDistance;


        updateChunkRendering();
    }
);


/*
   SENSIBILIDADE:
   aplica imediatamente.
*/

sensSlider.addEventListener(
    "input",
    ()=>{

        sensitivity =
            Number(
                sensSlider.value
            ) / 100;


        document
        .getElementById(
            "sensValue"
        )
        .textContent =
            sensSlider.value +
            "%";
    }
);


/*
   SALVAR:
   grava os valores atuais.
*/

document
.getElementById("saveSettings")
.addEventListener(
    "pointerdown",
    ()=>{

        quality =
            Number(
                qualitySlider.value
            );


        renderDistance =
            Number(
                renderSlider.value
            );


        sensitivity =
            Number(
                sensSlider.value
            ) / 100;


        applyQuality();

        updateChunkRendering();

        saveSettings();


        settings.style.display =
            "none";


        paused = false;
    }
);


/* =========================================================
   QUALIDADE
========================================================= */

function applyQuality(){

    let ratio;


    if(
        quality >= 90
    ){

        ratio =
            Math.min(
                devicePixelRatio,
                1.5
            );
    }

    else if(
        quality >= 70
    ){

        ratio =
            Math.min(
                devicePixelRatio,
                1.25
            );
    }

    else if(
        quality >= 40
    ){

        ratio = 1;
    }

    else if(
        quality >= 20
    ){

        ratio = .8;
    }

    else{

        ratio = .6;
    }


    renderer.setPixelRatio(
        ratio
    );


    renderer.setSize(
        innerWidth,
        innerHeight,
        false
    );
}


/* =========================================================
   SAVE CONFIGURAÇÕES
========================================================= */

const SETTINGS_KEY =
    "minicraft_settings_v4";


function saveSettings(){

    localStorage.setItem(
        SETTINGS_KEY,
        JSON.stringify({

            quality:
                quality,

            renderDistance:
                renderDistance,

            sensitivity:
                sensitivity
        })
    );
}


function loadSettings(){

    const raw =
        localStorage.getItem(
            SETTINGS_KEY
        );


    if(!raw)
        return;


    try{

        const data =
            JSON.parse(raw);


        if(
            typeof data.quality ===
            "number"
        ){

            quality =
                Math.max(
                    0,
                    Math.min(
                        100,
                        data.quality
                    )
                );
        }


        if(
            typeof data.renderDistance ===
            "number"
        ){

            renderDistance =
                Math.max(
                    1,
                    Math.min(
                        70,
                        data.renderDistance
                    )
                );
        }


        if(
            typeof data.sensitivity ===
            "number"
        ){

            sensitivity =
                Math.max(
                    .25,
                    Math.min(
                        2,
                        data.sensitivity
                    )
                );
        }


    }catch(e){

        console.warn(
            "Configuração inválida."
        );
    }


    qualitySlider.value =
        quality;

    renderSlider.value =
        renderDistance;

    sensSlider.value =
        sensitivity * 100;


    document
    .getElementById(
        "qualityValue"
    )
    .textContent =
        quality + "%";


    document
    .getElementById(
        "renderValue"
    )
    .textContent =
        renderDistance;


    document
    .getElementById(
        "sensValue"
    )
    .textContent =
        Math.round(
            sensitivity * 100
        ) + "%";
}


/* =========================================================
   SAVE DO MUNDO
========================================================= */

const SAVE_KEY =
    "minicraft_world_800_v4";


function saveWorld(){

    const data = [];


    for(
        const k
        of blocks.keys()
    ){

        data.push(k);
    }


    try{

        localStorage.setItem(
            SAVE_KEY,
            JSON.stringify({

                blocks:data,

                blockCount:
                    blockCount
            })
        );

    }catch(e){

        console.warn(
            "Não foi possível salvar.",
            e
        );
    }
}


function loadWorld(){

    const raw =
        localStorage.getItem(
            SAVE_KEY
        );


    if(!raw)
        return;


    try{

        const data =
            JSON.parse(raw);


        if(
            Array.isArray(
                data.blocks
            )
        ){

            for(
                const k
                of data.blocks
            ){

                const parts =
                    k.split(",")
                    .map(Number);


                if(
                    parts.length !== 3
                ){

                    continue;
                }


                const x =
                    parts[0];

                const y =
                    parts[1];

                const z =
                    parts[2];


                if(
                    !Number.isInteger(x) ||
                    !Number.isInteger(y) ||
                    !Number.isInteger(z)
                ){

                    continue;
                }


                if(
                    !isInsideWorld(x,z)
                ){

                    continue;
                }


                if(
                    y < 1 ||
                    y > MAX_BUILD_Y
                ){

                    continue;
                }


                if(
                    hasBlock(x,y,z)
                ){

                    continue;
                }


                blocks.set(
                    k,
                    true
                );


                createBlockMesh(
                    x,
                    y,
                    z
                );
            }
        }


        if(
            typeof data.blockCount ===
            "number"
        ){

            blockCount =
                Math.max(
                    0,
                    Math.floor(
                        data.blockCount
                    )
                );
        }


    }catch(e){

        console.warn(
            "Save corrompido.",
            e
        );
    }
}


/* =========================================================
   RESET
========================================================= */

document
.getElementById("resetWorld")
.addEventListener(
    "pointerdown",
    ()=>{

        const ok =
            confirm(
                "Resetar o mundo?"
            );


        if(!ok)
            return;


        blocks.clear();


        for(
            const chunk
            of chunks.values()
        ){

            scene.remove(
                chunk.group
            );
        }


        chunks.clear();


        blockCount = 64;


        localStorage.removeItem(
            SAVE_KEY
        );


        /*
           Reinicia também o jogador.
        */

        player.x = 0;
        player.y = 1.01;
        player.z = 0;
        player.vy = 0;
        player.yaw = 0;
        player.pitch = 0;
        player.grounded = false;


        updateHUD();

        updateChunkRendering();


        settings.style.display =
            "none";


        paused = false;
    }
);


/* =========================================================
   PAUSA
========================================================= */

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


document
.getElementById("pauseBtn")
.addEventListener(
    "pointerdown",
    ()=>{

        if(
            settings.style.display ===
            "flex"
        ){

            return;
        }


        paused = true;


        pauseScreen.style.display =
            "flex";
    }
);


document
.getElementById("resumeBtn")
.addEventListener(
    "pointerdown",
    ()=>{

        paused = false;

        pauseScreen.style.display =
            "none";
    }
);


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

window.addEventListener(
    "resize",
    ()=>{

        camera.aspect =
            innerWidth /
            innerHeight;


        camera.updateProjectionMatrix();


        renderer.setSize(
            innerWidth,
            innerHeight,
            false
        );


        /*
           Mantém a qualidade escolhida
           depois de girar/redimensionar.
        */

        applyQuality();
    }
);


/* =========================================================
   GAME UPDATE
========================================================= */

function updateGame(dt){

    const movement =
        getMovement();


    const mx =
        movement.x;

    const mz =
        movement.z;


    const sin =
        Math.sin(
            player.yaw
        );


    const cos =
        Math.cos(
            player.yaw
        );


    const worldX =
        mx * cos +
        mz * sin;


    const worldZ =
        mx * sin -
        mz * cos;


    /*
       GRAVIDADE ANTES DO MOVIMENTO
       VERTICAL.
    */

    player.vy -=
        GRAVITY * dt;


    movePlayer(

        worldX *
        WALK_SPEED *
        dt,

        player.vy *
        dt,

        worldZ *
        WALK_SPEED *
        dt
    );


    /*
       Segurança contra queda.
    */

    if(
        player.y < -20
    ){

        player.x = 0;

        player.y = 1.01;

        player.z = 0;

        player.vy = 0;

        player.grounded = false;
    }


    camera.position.set(

        player.x,

        player.y +
        EYE_HEIGHT,

        player.z
    );


    camera.rotation.order =
        "YXZ";


    camera.rotation.y =
        player.yaw;


    camera.rotation.x =
        player.pitch;
}


/* =========================================================
   FPS
========================================================= */

let fpsFrames = 0;

let fpsTimer = 0;

let fps = 0;


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

const clock =
    new THREE.Clock();


let chunkTimer = 0;

let autosaveTimer = 0;


function animate(){

    requestAnimationFrame(
        animate
    );


    const raw =
        clock.getDelta();


    const dt =
        Math.min(
            raw,
            .05
        );


    if(!paused){

        updateGame(dt);

        updateParticles(dt);


        chunkTimer += dt;

        autosaveTimer += dt;


        if(
            chunkTimer >= .16
        ){

            chunkTimer = 0;

            updateChunkRendering();
        }


        if(
            autosaveTimer >= 5
        ){

            autosaveTimer = 0;

            saveWorld();
        }
    }


    fpsFrames++;

    fpsTimer += dt;


    if(
        fpsTimer >= 1
    ){

        fps =
            fpsFrames;

        fpsFrames = 0;

        fpsTimer = 0;


        document
        .getElementById(
            "fps"
        )
        .textContent =
            "FPS: " + fps;
    }


    updateHUD();


    renderer.render(
        scene,
        camera
    );
}


/* =========================================================
   INICIALIZAÇÃO
========================================================= */

loadSettings();

applyQuality();

loadWorld();

updateChunkRendering();

updateHUD();

animate();

</script>

</body>
</html>

Game Source: MiniCraft 3D 800x800

Creator: MegaFlare51

Libraries: three

Complexity: complex (3640 lines, 49.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: minicraft-3d-800x800-megaflare51" to link back to the original. Then publish at arcadelab.ai/publish.