MiniCraft 3D 800x800
by MegaFlare513640 lines49.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,
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.