MiniCraft 3D Pré-Alpha
by MegaFlare511628 lines25.8 KB🛠️ Three.js (3D graphics)
<!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.