🎮ArcadeLab

MiniCraft 3D Pré-Alpha

by MegaFlare51
1628 lines25.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">
<title>MiniCraft 3D Pré-Alpha</title>

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

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

#game{
    position:fixed;
    inset:0;
}

#hud{
    position:fixed;
    top:10px;
    left:10px;
    z-index:10;
    color:white;
    background:#0009;
    padding:8px 12px;
    border-radius:8px;
    font-size:14px;
    pointer-events:none;
}

#crosshair{
    position:fixed;
    left:50%;
    top:50%;
    transform:translate(-50%,-50%);
    z-index:10;
    color:white;
    font-size:25px;
    text-shadow:0 0 5px #000;
    pointer-events:none;
}

#quality{
    position:fixed;
    right:10px;
    top:10px;
    z-index:20;
    background:#0009;
    color:white;
    padding:7px;
    border-radius:8px;
    font-size:12px;
}

#quality input{
    width:110px;
}

#message{
    position:fixed;
    left:50%;
    top:15%;
    transform:translateX(-50%);
    z-index:50;
    color:white;
    background:#000b;
    padding:10px 16px;
    border-radius:10px;
    display:none;
    font-weight:bold;
}

#movement{
    position:fixed;
    left:22px;
    bottom:22px;
    width:180px;
    height:180px;
    z-index:30;
}

.move{
    position:absolute;
    width:60px;
    height:60px;
    border:2px solid #fff8;
    border-radius:14px;
    background:#111c;
    color:white;
    font-size:30px;
    font-weight:bold;
    display:flex;
    align-items:center;
    justify-content:center;
    user-select:none;
    -webkit-user-select:none;
    box-shadow:0 3px 8px #0008;
    padding:0;
}

.move:active{
    background:#666;
}

#up{left:60px;top:0}
#left{left:0;top:60px}
#right{right:0;top:60px}
#down{left:60px;bottom:0}

#actions{
    position:fixed;
    right:22px;
    bottom:22px;
    z-index:30;
    display:grid;
    grid-template-columns:72px 72px;
    gap:10px;
}

.action{
    width:72px;
    height:60px;
    border:2px solid #fff8;
    border-radius:12px;
    background:#111c;
    color:white;
    font-size:12px;
    font-weight:bold;
}

.action:active{
    background:#666;
}

#inventory{
    position:fixed;
    left:50%;
    bottom:15px;
    transform:translateX(-50%);
    z-index:30;
    display:flex;
    gap:6px;
}

.slot{
    width:48px;
    height:48px;
    background:#222d;
    border:2px solid #777;
    border-radius:7px;
    display:flex;
    align-items:center;
    justify-content:center;
    font-size:25px;
}

.slot.selected{
    border:3px solid white;
}

@media (orientation:portrait){
    #movement{
        left:10px;
        bottom:10px;
        transform:scale(.85);
        transform-origin:bottom left;
    }

    #actions{
        right:10px;
        bottom:10px;
        transform:scale(.85);
        transform-origin:bottom right;
    }

    #inventory{
        bottom:8px;
    }
}
</style>
</head>

<body>

<div id="game"></div>

<div id="hud">
    MiniCraft 3D Pré-Alpha<br>
    Mundo: 2300 × 2300
</div>

<div id="quality">
    Renderização:
    <input id="renderRange" type="range" min="8" max="55" value="28">
</div>

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

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

<!-- MOVIMENTO -->
<div id="movement">

    <button class="move" id="up">⬆️</button>

    <button class="move" id="left">⬅️</button>

    <button class="move" id="right">➡️</button>

    <button class="move" id="down">⬇️</button>

</div>

<!-- AÇÕES -->
<div id="actions">

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

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

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

    <button class="action" id="config">
        CONFIG
    </button>

</div>

<!-- INVENTÁRIO -->
<div id="inventory">

    <div class="slot selected" data-type="grass">
        🟩
    </div>

    <div class="slot" data-type="stone">
        🪨
    </div>

    <div class="slot" data-type="wood">
        🪵
    </div>

    <div class="slot" data-type="leaf">
        🌿
    </div>

</div>

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

<script>

////////////////////////////////////////////////////////////
// CONFIGURAÇÃO
////////////////////////////////////////////////////////////

const WORLD_SIZE = 2300;

let RENDER_DISTANCE = 28;

const BLOCK_SIZE = 1;

////////////////////////////////////////////////////////////
// THREE.JS
////////////////////////////////////////////////////////////

const scene = new THREE.Scene();

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

scene.fog = new THREE.Fog(
    0x87ceeb,
    RENDER_DISTANCE * .65,
    RENDER_DISTANCE * 1.15
);

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

const renderer = new THREE.WebGLRenderer({
    antialias:false,
    powerPreference:"high-performance"
});

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

renderer.setSize(
    innerWidth,
    innerHeight
);

document
    .getElementById("game")
    .appendChild(renderer.domElement);

////////////////////////////////////////////////////////////
// LUZ
////////////////////////////////////////////////////////////

const hemi = new THREE.HemisphereLight(
    0xffffff,
    0x668866,
    1.8
);

scene.add(hemi);

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

sun.position.set(
    100,
    150,
    80
);

scene.add(sun);

////////////////////////////////////////////////////////////
// MATERIAIS
////////////////////////////////////////////////////////////

const materials = {

    grass:new THREE.MeshLambertMaterial({
        color:0x39a844
    }),

    stone:new THREE.MeshLambertMaterial({
        color:0x777777
    }),

    wood:new THREE.MeshLambertMaterial({
        color:0x8b5a2b
    }),

    leaf:new THREE.MeshLambertMaterial({
        color:0x218c36
    })

};

////////////////////////////////////////////////////////////
// GEOMETRIA
////////////////////////////////////////////////////////////

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

////////////////////////////////////////////////////////////
// JOGADOR
////////////////////////////////////////////////////////////

const player = {

    x:0,

    y:2,

    z:8,

    vy:0,

    onGround:false

};

const PLAYER_HEIGHT = 1.8;

const PLAYER_RADIUS = .3;

////////////////////////////////////////////////////////////
// CÂMERA
////////////////////////////////////////////////////////////

let yaw = 0;

let pitch = 0;

////////////////////////////////////////////////////////////
// BLOCOS VISUAIS
////////////////////////////////////////////////////////////

const blocks = new Map();

////////////////////////////////////////////////////////////
// MODIFICAÇÕES DO JOGADOR
////////////////////////////////////////////////////////////

const modifications = new Map();

function key(x,y,z){

    return `${x},${y},${z}`;

}

////////////////////////////////////////////////////////////
// ÁRVORES DETERMINÍSTICAS
////////////////////////////////////////////////////////////

function hash(x,z){

    let n =
        Math.sin(
            x * 127.1 +
            z * 311.7
        ) * 43758.5453;

    return n - Math.floor(n);

}

function treeAt(x,z){

    if(
        Math.abs(x % 8) > 0.01 ||
        Math.abs(z % 8) > 0.01
    ){

        return false;

    }

    return hash(x/8,z/8) < .12;

}

////////////////////////////////////////////////////////////
// BLOCO NATURAL
////////////////////////////////////////////////////////////

function naturalBlockAt(x,y,z){

    // SOLO
    if(y === 0){

        return "grass";

    }

    // ÁRVORE
    if(treeAt(x,z)){

        if(y >= 1 && y <= 4){

            return "wood";

        }

    }

    // FOLHAS
    for(let tx=-2;tx<=2;tx++){

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

            const treeX =
                x - tx;

            const treeZ =
                z - tz;

            if(treeAt(treeX,treeZ)){

                const distance =
                    Math.abs(tx)+Math.abs(tz);

                if(
                    y >= 3 &&
                    y <= 6 &&
                    distance <= 2
                ){

                    return "leaf";

                }

                if(
                    y === 7 &&
                    tx === 0 &&
                    tz === 0
                ){

                    return "leaf";

                }

            }

        }

    }

    return null;

}

////////////////////////////////////////////////////////////
// BLOCO DO MUNDO
////////////////////////////////////////////////////////////

function worldBlockAt(x,y,z){

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

    if(modifications.has(k)){

        return modifications.get(k);

    }

    return naturalBlockAt(x,y,z);

}

////////////////////////////////////////////////////////////
// ADICIONAR BLOCO VISUAL
////////////////////////////////////////////////////////////

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

    if(!type)return;

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

    if(blocks.has(k)){

        scene.remove(blocks.get(k));

        blocks.delete(k);

    }

    const mesh =
        new THREE.Mesh(
            cubeGeometry,
            materials[type]
        );

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

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

    scene.add(mesh);

    blocks.set(k,mesh);

}

////////////////////////////////////////////////////////////
// REMOVER BLOCO VISUAL
////////////////////////////////////////////////////////////

function removeVisual(x,y,z){

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

    const mesh = blocks.get(k);

    if(mesh){

        scene.remove(mesh);

        blocks.delete(k);

    }

}

////////////////////////////////////////////////////////////
// SALVAR MODIFICAÇÃO
////////////////////////////////////////////////////////////

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

    modifications.set(
        key(x,y,z),
        type
    );

}

////////////////////////////////////////////////////////////
// ÁREA CARREGADA
////////////////////////////////////////////////////////////

let lastCX = null;
let lastCZ = null;

function generateArea(){

    for(const mesh of blocks.values()){

        scene.remove(mesh);

    }

    blocks.clear();

    const radius =
        Math.ceil(RENDER_DISTANCE);

    const px =
        Math.floor(player.x);

    const pz =
        Math.floor(player.z);

    for(
        let x=px-radius;
        x<=px+radius;
        x++
    ){

        for(
            let z=pz-radius;
            z<=pz+radius;
            z++
        ){

            if(
                x < -WORLD_SIZE/2 ||
                x > WORLD_SIZE/2 ||
                z < -WORLD_SIZE/2 ||
                z > WORLD_SIZE/2
            ){

                continue;

            }

            const distance =
                Math.sqrt(
                    (x-player.x)*(x-player.x)+
                    (z-player.z)*(z-player.z)
                );

            if(distance > radius){

                continue;

            }

            // GRAMA
            const ground =
                worldBlockAt(x,0,z);

            if(ground){

                addBlock(
                    x,
                    0,
                    z,
                    ground
                );

            }

            // ALTURA EXTRA DAS ÁRVORES
            for(let y=1;y<=7;y++){

                const type =
                    worldBlockAt(x,y,z);

                if(type){

                    addBlock(
                        x,
                        y,
                        z,
                        type
                    );

                }

            }

        }

    }

    lastCX = Math.floor(
        player.x
    );

    lastCZ = Math.floor(
        player.z
    );

}

////////////////////////////////////////////////////////////
// ATUALIZAR ÁREA
////////////////////////////////////////////////////////////

function updateArea(){

    const cx =
        Math.floor(player.x);

    const cz =
        Math.floor(player.z);

    if(
        cx !== lastCX ||
        cz !== lastCZ
    ){

        generateArea();

    }

}

////////////////////////////////////////////////////////////
// COLISÃO
////////////////////////////////////////////////////////////

function collides(x,y,z){

    const minX =
        Math.floor(
            x - PLAYER_RADIUS
        );

    const maxX =
        Math.floor(
            x + PLAYER_RADIUS
        );

    const minY =
        Math.floor(y);

    const maxY =
        Math.floor(
            y + PLAYER_HEIGHT
        );

    const minZ =
        Math.floor(
            z - PLAYER_RADIUS
        );

    const maxZ =
        Math.floor(
            z + PLAYER_RADIUS
        );

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

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

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

                if(
                    worldBlockAt(
                        bx,
                        by,
                        bz
                    )
                ){

                    return true;

                }

            }

        }

    }

    return false;

}

////////////////////////////////////////////////////////////
// MOVIMENTO HORIZONTAL
////////////////////////////////////////////////////////////

function movePlayer(dx,dz){

    const nx =
        player.x + dx;

    const nz =
        player.z + dz;

    if(
        !collides(
            nx,
            player.y,
            player.z
        )
    ){

        player.x = nx;

    }

    if(
        !collides(
            player.x,
            player.y,
            nz
        )
    ){

        player.z = nz;

    }

}

////////////////////////////////////////////////////////////
// TECLAS DE MOVIMENTO
////////////////////////////////////////////////////////////

const keys = {

    up:false,
    down:false,
    left:false,
    right:false

};

function bindMove(id,name){

    const button =
        document.getElementById(id);

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

            e.preventDefault();

            keys[name]=true;

        }
    );

    button.addEventListener(
        "pointerup",
        e=>{

            e.preventDefault();

            keys[name]=false;

        }
    );

    button.addEventListener(
        "pointercancel",
        ()=>{
            keys[name]=false;
        }
    );

    button.addEventListener(
        "pointerleave",
        ()=>{
            keys[name]=false;
        }
    );

}

bindMove("up","up");
bindMove("down","down");
bindMove("left","left");
bindMove("right","right");

////////////////////////////////////////////////////////////
// MOVIMENTO RELATIVO À CÂMERA
////////////////////////////////////////////////////////////

function updateMovement(dt){

    let forward = 0;

    let strafe = 0;

    if(keys.up)
        forward += 1;

    if(keys.down)
        forward -= 1;

    if(keys.left)
        strafe -= 1;

    if(keys.right)
        strafe += 1;

    // Evita ficar mais rápido na diagonal
    const length =
        Math.sqrt(
            forward*forward +
            strafe*strafe
        );

    if(length > 1){

        forward /= length;
        strafe /= length;

    }

    /*
       MOVIMENTO RELATIVO À CÂMERA

       forward:
       direção para onde a câmera olha

       strafe:
       esquerda/direita da câmera
    */

    const forwardX =
        -Math.sin(yaw);

    const forwardZ =
        -Math.cos(yaw);

    const rightX =
        Math.cos(yaw);

    const rightZ =
        -Math.sin(yaw);

    const dx =
        forwardX * forward +
        rightX * strafe;

    const dz =
        forwardZ * forward +
        rightZ * strafe;

    const speed =
        5 * dt;

    movePlayer(
        dx * speed,
        dz * speed
    );

    ////////////////////////////////////////////////////////
    // GRAVIDADE
    ////////////////////////////////////////////////////////

    player.vy -=
        18 * dt;

    const newY =
        player.y +
        player.vy * dt;

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

        player.y = newY;

        player.onGround = false;

    }else{

        if(player.vy < 0){

            player.onGround = true;

        }

        player.vy = 0;

    }

    ////////////////////////////////////////////////////////
    // QUEDA
    ////////////////////////////////////////////////////////

    if(player.y < -20){

        player.x = 0;

        player.y = 2;

        player.z = 8;

        player.vy = 0;

    }

}

////////////////////////////////////////////////////////////
// CÂMERA
////////////////////////////////////////////////////////////

function updateCamera(){

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

    camera.rotation.order =
        "YXZ";

    camera.rotation.y =
        yaw;

    camera.rotation.x =
        pitch;

}

////////////////////////////////////////////////////////////
// CONTROLE DA CÂMERA PELO TOQUE
////////////////////////////////////////////////////////////

let cameraTouch = null;

renderer.domElement.addEventListener(
    "pointerdown",
    e=>{

        cameraTouch = {

            id:e.pointerId,

            x:e.clientX,

            y:e.clientY

        };

    }
);

renderer.domElement.addEventListener(
    "pointermove",
    e=>{

        if(
            !cameraTouch ||
            e.pointerId !==
            cameraTouch.id
        ){

            return;

        }

        const dx =
            e.clientX -
            cameraTouch.x;

        const dy =
            e.clientY -
            cameraTouch.y;

        yaw -=
            dx * .006;

        pitch -=
            dy * .006;

        pitch =
            Math.max(
                -Math.PI/2 + .1,
                Math.min(
                    Math.PI/2 - .1,
                    pitch
                )
            );

        cameraTouch.x =
            e.clientX;

        cameraTouch.y =
            e.clientY;

    }
);

renderer.domElement.addEventListener(
    "pointerup",
    e=>{

        if(
            cameraTouch &&
            e.pointerId ===
            cameraTouch.id
        ){

            cameraTouch=null;

        }

    }
);

renderer.domElement.addEventListener(
    "pointercancel",
    ()=>{

        cameraTouch=null;

    }
);

////////////////////////////////////////////////////////////
// RAYCAST
////////////////////////////////////////////////////////////

const raycaster =
    new THREE.Raycaster();

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

function getTargetBlock(){

    raycaster.setFromCamera(
        center,
        camera
    );

    const hits =
        raycaster.intersectObjects(
            Array.from(blocks.values())
        );

    if(!hits.length){

        return null;

    }

    const hit = hits[0];

    return {

        mesh:hit.object,

        normal:hit.face.normal.clone()

    };

}

////////////////////////////////////////////////////////////
// QUEBRAR
////////////////////////////////////////////////////////////

function breakBlock(){

    const target =
        getTargetBlock();

    if(!target){

        showMessage(
            "Nenhum bloco encontrado"
        );

        return;

    }

    const mesh =
        target.mesh;

    const x =
        mesh.userData.x;

    const y =
        mesh.userData.y;

    const z =
        mesh.userData.z;

    // O chão NÃO pode ser quebrado
    if(y === 0){

        showMessage(
            "O chão não pode ser quebrado!"
        );

        return;

    }

    saveModification(
        x,
        y,
        z,
        null
    );

    removeVisual(
        x,
        y,
        z
    );

}

////////////////////////////////////////////////////////////
// COLOCAR
////////////////////////////////////////////////////////////

let selectedType =
    "grass";

function placeBlock(){

    const target =
        getTargetBlock();

    if(!target){

        showMessage(
            "Mire em um bloco"
        );

        return;

    }

    const mesh =
        target.mesh;

    const x =
        Math.round(
            mesh.userData.x +
            target.normal.x
        );

    const y =
        Math.round(
            mesh.userData.y +
            target.normal.y
        );

    const z =
        Math.round(
            mesh.userData.z +
            target.normal.z
        );

    // Não colocar dentro do jogador
    if(
        collides(
            x,
            y,
            z
        )
    ){

        return;

    }

    saveModification(
        x,
        y,
        z,
        selectedType
    );

    addBlock(
        x,
        y,
        z,
        selectedType
    );

}

////////////////////////////////////////////////////////////
// PULAR
////////////////////////////////////////////////////////////

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

            e.preventDefault();

            if(player.onGround){

                player.vy =
                    7;

                player.onGround =
                    false;

            }

        }
    );

////////////////////////////////////////////////////////////
// BOTÕES
////////////////////////////////////////////////////////////

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

            e.preventDefault();

            breakBlock();

        }
    );

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

            e.preventDefault();

            placeBlock();

        }
    );

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

            e.preventDefault();

            showMessage(
                "Configurações: use a barra de renderização"
            );

        }
    );

////////////////////////////////////////////////////////////
// INVENTÁRIO
////////////////////////////////////////////////////////////

document
    .querySelectorAll(".slot")
    .forEach(slot=>{

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

                e.preventDefault();

                document
                    .querySelectorAll(".slot")
                    .forEach(s=>
                        s.classList.remove(
                            "selected"
                        )
                    );

                slot.classList.add(
                    "selected"
                );

                selectedType =
                    slot.dataset.type;

            }
        );

    });

////////////////////////////////////////////////////////////
// MENSAGEM
////////////////////////////////////////////////////////////

let messageTimer = null;

function showMessage(text){

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

    box.textContent =
        text;

    box.style.display =
        "block";

    clearTimeout(
        messageTimer
    );

    messageTimer =
        setTimeout(
            ()=>{
                box.style.display =
                    "none";
            },
            1800
        );

}

////////////////////////////////////////////////////////////
// TECLADO
////////////////////////////////////////////////////////////

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

        if(e.key==="w" ||
           e.key==="ArrowUp")
            keys.up=true;

        if(e.key==="s" ||
           e.key==="ArrowDown")
            keys.down=true;

        if(e.key==="a" ||
           e.key==="ArrowLeft")
            keys.left=true;

        if(e.key==="d" ||
           e.key==="ArrowRight")
            keys.right=true;

        if(e.code==="Space" &&
           player.onGround){

            player.vy=7;

        }

    }
);

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

        if(e.key==="w" ||
           e.key==="ArrowUp")
            keys.up=false;

        if(e.key==="s" ||
           e.key==="ArrowDown")
            keys.down=false;

        if(e.key==="a" ||
           e.key==="ArrowLeft")
            keys.left=false;

        if(e.key==="d" ||
           e.key==="ArrowRight")
            keys.right=false;

    }
);

////////////////////////////////////////////////////////////
// QUALIDADE / DISTÂNCIA
////////////////////////////////////////////////////////////

document
    .getElementById("renderRange")
    .addEventListener(
        "input",
        e=>{

            RENDER_DISTANCE =
                Number(e.target.value);

            scene.fog.near =
                RENDER_DISTANCE * .65;

            scene.fog.far =
                RENDER_DISTANCE * 1.15;

            generateArea();

        }
    );

////////////////////////////////////////////////////////////
// RESIZE
////////////////////////////////////////////////////////////

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

        camera.aspect =
            innerWidth /
            innerHeight;

        camera.updateProjectionMatrix();

        renderer.setSize(
            innerWidth,
            innerHeight
        );

    }
);

////////////////////////////////////////////////////////////
// LOOP
////////////////////////////////////////////////////////////

let previous =
    performance.now();

function loop(now){

    requestAnimationFrame(
        loop
    );

    let dt =
        (now - previous) / 1000;

    previous = now;

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

    updateMovement(dt);

    updateArea();

    updateCamera();

    renderer.render(
        scene,
        camera
    );

}

////////////////////////////////////////////////////////////
// INICIAR
////////////////////////////////////////////////////////////

generateArea();

updateCamera();

requestAnimationFrame(
    loop
);

</script>

</body>
</html>

Game Source: MiniCraft 3D Pré-Alpha

Creator: MegaFlare51

Libraries: three

Complexity: complex (1628 lines, 25.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-pr-alpha-megaflare51" to link back to the original. Then publish at arcadelab.ai/publish.