GEOMETRICKWORD Pré-Alpha 0.2
by MegaFlare513777 lines51.2 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.0,
maximum-scale=1.0,user-scalable=no,
viewport-fit=cover">
<title>GEOMETRICKWORD Pré-Alpha 0.2</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<style>
*{
box-sizing:border-box;
-webkit-tap-highlight-color:transparent;
}
html,body{
margin:0;
width:100%;
height:100%;
overflow:hidden;
background:#000;
font-family:Arial,sans-serif;
touch-action:none;
}
button,input{
font:inherit;
}
button{
cursor:pointer;
user-select:none;
-webkit-user-select:none;
}
#game{
position:fixed;
inset:0;
width:100%;
height:100%;
display:block;
}
/* =====================================================
MENU
===================================================== */
#startScreen{
position:fixed;
inset:0;
z-index:500;
background:#000;
}
#menu3d{
position:absolute;
inset:0;
width:100%;
height:100%;
}
#menuShade{
position:absolute;
inset:0;
background:
linear-gradient(
to bottom,
rgba(0,0,0,.12),
rgba(0,0,0,.50)
);
pointer-events:none;
}
#menuTitle{
position:absolute;
top:9%;
left:50%;
transform:translateX(-50%);
color:#fff;
text-align:center;
font-size:clamp(26px,6vw,58px);
font-weight:900;
text-shadow:4px 4px #000;
white-space:nowrap;
}
#menuVersion{
font-size:14px;
margin-top:5px;
letter-spacing:2px;
}
#playButton{
position:absolute;
left:50%;
top:62%;
transform:translate(-50%,-50%);
width:min(330px,70vw);
height:68px;
border:4px solid #111;
border-radius:5px;
background:#555;
color:#fff;
font-size:25px;
font-weight:900;
box-shadow:6px 6px rgba(0,0,0,.55);
}
#playButton:active{
background:#888;
transform:translate(-50%,-48%);
}
/* =====================================================
SELEÇÃO DE MUNDO
===================================================== */
#worldSelect{
position:fixed;
inset:0;
z-index:510;
display:none;
justify-content:center;
align-items:center;
background:#111;
color:#fff;
}
#worldBox{
width:min(430px,86vw);
max-height:90vh;
background:#333;
border:4px solid #111;
box-shadow:7px 7px #000;
padding:20px;
text-align:center;
}
#worldBox h1{
margin:0 0 18px;
font-size:27px;
}
.worldButton{
width:100%;
height:62px;
margin:7px 0;
border:3px solid #111;
border-radius:4px;
background:#666;
color:#fff;
font-size:20px;
font-weight:900;
}
.worldButton:active{
background:#999;
}
/* =====================================================
GERAÇÃO
===================================================== */
#generating{
position:fixed;
inset:0;
z-index:600;
display:none;
justify-content:center;
align-items:center;
background:#000;
color:#fff;
text-align:center;
}
#generating h1{
margin:0;
font-size:27px;
}
#loadingDots{
margin-top:15px;
font-size:20px;
}
#loadingInfo{
margin-top:9px;
color:#aaa;
font-size:12px;
}
/* =====================================================
HUD
===================================================== */
#hud{
position:fixed;
top:7px;
left:8px;
z-index:50;
color:#fff;
font-size:12px;
line-height:17px;
text-shadow:2px 2px #000;
pointer-events:none;
}
#crosshair{
position:fixed;
left:50%;
top:50%;
transform:translate(-50%,-50%);
z-index:40;
color:#fff;
font-size:25px;
font-weight:bold;
text-shadow:2px 2px #000;
pointer-events:none;
}
/* =====================================================
TOPO DIREITO
===================================================== */
#topRight{
position:fixed;
right:8px;
top:7px;
z-index:100;
display:flex;
align-items:flex-end;
gap:7px;
pointer-events:none;
}
#config{
width:46px;
height:46px;
border:2px solid #151515;
border-radius:4px;
background:rgba(65,65,65,.90);
color:#fff;
font-size:21px;
pointer-events:auto;
}
#renderWrap{
height:42px;
padding:5px 8px;
display:flex;
align-items:center;
gap:5px;
background:rgba(0,0,0,.70);
border:2px solid #222;
color:#fff;
font-size:10px;
pointer-events:auto;
}
#renderDistance{
width:72px;
}
/* =====================================================
VIDA
===================================================== */
#hearts{
position:fixed;
left:50%;
bottom:76px;
transform:translateX(-50%);
z-index:110;
display:none;
color:#ff3030;
font-size:22px;
letter-spacing:1px;
white-space:nowrap;
text-shadow:2px 2px #000;
pointer-events:none;
}
/* =====================================================
CONTROLES ESQUERDOS
===================================================== */
#movePad{
position:fixed;
left:12px;
bottom:12px;
width:154px;
height:154px;
z-index:120;
pointer-events:none;
}
.btn{
border:2px solid #191919;
border-radius:5px;
background:rgba(70,70,70,.88);
color:#fff;
font-weight:900;
box-shadow:2px 2px rgba(0,0,0,.5);
pointer-events:auto;
touch-action:none;
}
.btn:active{
background:#999;
}
.moveBtn{
position:absolute;
width:48px;
height:48px;
font-size:19px;
}
#up{
left:53px;
top:0;
}
#left{
left:0;
top:53px;
}
#down{
left:53px;
top:53px;
}
#right{
left:106px;
top:53px;
}
/* =====================================================
CONTROLES DIREITOS
===================================================== */
#actionPad{
position:fixed;
right:12px;
bottom:12px;
width:190px;
height:154px;
z-index:120;
pointer-events:none;
}
.actionBtn{
position:absolute;
width:72px;
height:48px;
font-size:10px;
}
#jump{
left:0;
bottom:0;
}
#break{
left:78px;
top:0;
}
#place{
right:0;
bottom:0;
}
/* =====================================================
HOTBAR
===================================================== */
#inventory{
position:fixed;
left:50%;
bottom:10px;
transform:translateX(-50%);
display:flex;
gap:3px;
z-index:130;
max-width:55vw;
pointer-events:auto;
}
.slot{
width:42px;
height:42px;
flex:0 0 42px;
position:relative;
display:flex;
justify-content:center;
align-items:center;
background:#4d4d4d;
border:2px solid #202020;
}
.slot.sel{
border:3px solid #fff;
background:#888;
}
.slot canvas{
width:30px;
height:30px;
image-rendering:pixelated;
}
.count{
position:absolute;
right:2px;
bottom:0;
color:#fff;
font-size:11px;
font-weight:bold;
text-shadow:2px 2px #000;
}
/* =====================================================
BOTÃO INVENTÁRIO
===================================================== */
#bagButton{
position:fixed;
left:calc(50% + 145px);
bottom:9px;
z-index:140;
width:44px;
height:44px;
border:3px solid #171717;
border-radius:4px;
background:#555;
color:#fff;
font-size:20px;
display:none;
}
#bagButton:active{
background:#888;
}
/* =====================================================
INVENTÁRIO ABERTO
===================================================== */
#craftPanel{
position:fixed;
inset:0;
z-index:300;
display:none;
justify-content:center;
align-items:center;
padding:10px;
background:rgba(0,0,0,.78);
}
#craftBox{
width:min(400px,88vw);
max-height:92vh;
padding:15px;
background:#333;
color:#fff;
border:4px solid #111;
box-shadow:6px 6px #000;
}
#craftBox h2{
margin:0;
font-size:21px;
}
#closeCraft{
float:right;
width:34px;
height:34px;
border:2px solid #111;
background:#777;
color:#fff;
font-weight:bold;
}
#craftTop{
display:flex;
align-items:center;
justify-content:center;
gap:12px;
margin:18px 0;
}
.craftSlot{
width:56px;
height:56px;
display:flex;
align-items:center;
justify-content:center;
background:#555;
border:3px solid #111;
}
.craftSlot canvas{
width:42px;
height:42px;
image-rendering:pixelated;
}
#craftArrow{
font-size:27px;
}
#craftInventory{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:5px;
}
#craftInventory .slot{
width:auto;
height:50px;
flex:auto;
}
/* =====================================================
MÃO
===================================================== */
#hand{
position:fixed;
right:24%;
bottom:-3px;
width:42px;
height:73px;
background:#bd8051;
border:3px solid #6c4027;
z-index:35;
transform:rotate(-9deg);
transform-origin:bottom center;
pointer-events:none;
display:none;
}
#handTop{
position:absolute;
left:-3px;
top:-13px;
width:42px;
height:19px;
background:#c98b5a;
border:3px solid #6c4027;
}
/* =====================================================
MORTE
===================================================== */
#deathScreen{
position:fixed;
inset:0;
z-index:450;
display:none;
justify-content:center;
align-items:center;
background:rgba(0,0,0,.84);
text-align:center;
color:#fff;
}
#deathBox h1{
margin:0 0 20px;
font-size:37px;
text-shadow:3px 3px #000;
}
#respawn{
width:220px;
height:58px;
border:3px solid #111;
background:#666;
color:#fff;
font-size:21px;
font-weight:bold;
}
/* =====================================================
CONFIGURAÇÕES
===================================================== */
#settings{
position:fixed;
inset:0;
z-index:200;
display:none;
justify-content:center;
align-items:center;
padding:10px;
background:rgba(0,0,0,.78);
}
#settingsBox{
width:min(330px,86vw);
background:#333;
color:#fff;
border:3px solid #111;
padding:16px;
box-shadow:5px 5px #111;
}
#settingsBox h2{
margin:0 0 15px;
}
#closeSettings{
float:right;
width:32px;
height:32px;
background:#777;
color:#fff;
border:2px solid #111;
font-weight:bold;
}
.setting{
margin:15px 0;
}
.setting input{
width:100%;
}
#qualityInfo{
display:block;
margin-top:5px;
color:#ddd;
font-size:11px;
}
/* =====================================================
TELAS MUITO PEQUENAS
===================================================== */
@media(max-height:430px){
#hud{
font-size:10px;
line-height:14px;
}
#movePad{
transform:scale(.88);
transform-origin:left bottom;
left:8px;
bottom:7px;
}
#actionPad{
transform:scale(.88);
transform-origin:right bottom;
right:8px;
bottom:7px;
}
#inventory{
bottom:7px;
gap:2px;
}
.slot{
width:36px;
height:36px;
flex-basis:36px;
}
.slot canvas{
width:26px;
height:26px;
}
#hearts{
bottom:60px;
font-size:18px;
}
#bagButton{
left:calc(50% + 125px);
bottom:6px;
width:38px;
height:38px;
}
#topRight{
top:5px;
right:5px;
}
#renderWrap{
display:none;
}
#config{
width:40px;
height:40px;
}
}
</style>
</head>
<body>
<canvas id="game"></canvas>
<!-- MENU -->
<div id="startScreen">
<canvas id="menu3d"></canvas>
<div id="menuShade"></div>
<div id="menuTitle">
GEOMETRICKWORD
<div id="menuVersion">PRÉ-ALPHA 0.2</div>
</div>
<button id="playButton">JOGAR</button>
</div>
<!-- SELEÇÃO -->
<div id="worldSelect">
<div id="worldBox">
<h1>SELECIONE MUNDO</h1>
<button class="worldButton" id="freeButton">
🟩 LIVRE
</button>
<button class="worldButton" id="survivalButton">
🟫 SOBREVIVA
</button>
</div>
</div>
<!-- GERAÇÃO -->
<div id="generating">
<div>
<h1>GERANDO MUNDO...</h1>
<div id="loadingDots">●</div>
<div id="loadingInfo">
Preparando terreno...
</div>
</div>
</div>
<!-- HUD -->
<div id="hud">
<b>GEOMETRICKWORD</b><br>
<b>PRÉ-ALPHA 0.2</b><br>
Mundo: 2300 × 2300<br>
Modo: <span id="modeText">---</span><br>
FPS: <span id="fps">0</span>
</div>
<div id="crosshair">+</div>
<!-- VIDA -->
<div id="hearts"></div>
<!-- TOPO -->
<div id="topRight">
<div id="renderWrap">
RENDER
<input
id="renderDistance"
type="range"
min="8"
max="55"
value="28">
<span id="renderValue">28</span>
</div>
<button id="config">⚙</button>
</div>
<!-- MOVIMENTO -->
<div id="movePad">
<button class="btn moveBtn" id="up">▲</button>
<button class="btn moveBtn" id="left">◀</button>
<button class="btn moveBtn" id="down">▼</button>
<button class="btn moveBtn" id="right">▶</button>
</div>
<!-- AÇÕES -->
<div id="actionPad">
<button class="btn actionBtn" id="jump">
PULAR
</button>
<button class="btn actionBtn" id="break">
QUEBRAR
</button>
<button class="btn actionBtn" id="place">
COLOCAR
</button>
</div>
<!-- HOTBAR -->
<div id="inventory"></div>
<button id="bagButton">🎒</button>
<!-- MÃO -->
<div id="hand">
<div id="handTop"></div>
</div>
<!-- INVENTÁRIO -->
<div id="craftPanel">
<div id="craftBox">
<button id="closeCraft">X</button>
<h2>INVENTÁRIO</h2>
<div id="craftTop">
<div class="craftSlot" id="craftInput"></div>
<div id="craftArrow">➡️</div>
<div class="craftSlot" id="craftOutput"></div>
</div>
<div id="craftInventory"></div>
</div>
</div>
<!-- MORTE -->
<div id="deathScreen">
<div id="deathBox">
<h1>VOCÊ MORREU</h1>
<button id="respawn">
RENASCER
</button>
</div>
</div>
<!-- CONFIGURAÇÕES -->
<div id="settings">
<div id="settingsBox">
<button id="closeSettings">X</button>
<h2>Configurações</h2>
<div class="setting">
Sensibilidade
<input
id="sens"
type="range"
min=".1"
max="3"
step=".1"
value="1">
</div>
<div class="setting">
Qualidade
<input
id="quality"
type="range"
min="50"
max="150"
step="1"
value="100">
<span id="qualityInfo">
100% — Texturas 8×8 ATIVADAS
</span>
</div>
<div class="setting">
Renderização
<input
id="render2"
type="range"
min="8"
max="55"
value="28">
</div>
</div>
</div>
<script>
/* =====================================================
CONFIGURAÇÃO
===================================================== */
const WORLD_SIZE=2300;
const CHUNK_SIZE=16;
let RENDER_DISTANCE=28;
let quality=100;
let gameStarted=false;
let gameMode="free";
let dead=false;
const spawnPoint={
x:0,
y:1.05,
z:8
};
/* =====================================================
CENA
===================================================== */
const scene=new THREE.Scene();
scene.background=
new THREE.Color(0x87ceeb);
scene.fog=new THREE.Fog(
0x87ceeb,
14,
RENDER_DISTANCE
);
const camera=
new THREE.PerspectiveCamera(
75,
innerWidth/innerHeight,
.1,
1000
);
const renderer=
new THREE.WebGLRenderer({
canvas:document.getElementById("game"),
antialias:false,
alpha:false,
powerPreference:"high-performance"
});
renderer.setPixelRatio(
Math.min(devicePixelRatio,1)
);
renderer.setSize(
innerWidth,
innerHeight
);
renderer.outputColorSpace=
THREE.SRGBColorSpace;
/* =====================================================
ILUMINAÇÃO
===================================================== */
scene.add(
new THREE.HemisphereLight(
0xffffff,
0x555555,
1.35
)
);
const sun=
new THREE.DirectionalLight(
0xffffff,
1.15
);
sun.position.set(
40,
70,
30
);
scene.add(sun);
/* =====================================================
MENU 3D
===================================================== */
const menuCanvas=
document.getElementById("menu3d");
const menuScene=
new THREE.Scene();
menuScene.background=
new THREE.Color(0x87ceeb);
const menuCamera=
new THREE.PerspectiveCamera(
60,
innerWidth/innerHeight,
.1,
500
);
const menuRenderer=
new THREE.WebGLRenderer({
canvas:menuCanvas,
antialias:false
});
menuRenderer.setPixelRatio(
Math.min(devicePixelRatio,1)
);
menuRenderer.setSize(
innerWidth,
innerHeight
);
menuRenderer.outputColorSpace=
THREE.SRGBColorSpace;
menuScene.add(
new THREE.HemisphereLight(
0xffffff,
0x555555,
1.4
)
);
const menuSun=
new THREE.DirectionalLight(
0xffffff,
1.1
);
menuSun.position.set(
30,50,20
);
menuScene.add(menuSun);
/* =====================================================
TEXTURAS
===================================================== */
function pixelTexture(colors,pattern){
const c=
document.createElement("canvas");
c.width=8;
c.height=8;
const ctx=
c.getContext("2d");
ctx.imageSmoothingEnabled=false;
for(let y=0;y<8;y++){
for(let x=0;x<8;x++){
ctx.fillStyle=
colors[pattern(x,y)];
ctx.fillRect(
x,y,1,1
);
}
}
const texture=
new THREE.CanvasTexture(c);
texture.magFilter=
THREE.NearestFilter;
texture.minFilter=
THREE.NearestFilter;
texture.generateMipmaps=false;
texture.colorSpace=
THREE.SRGBColorSpace;
texture.needsUpdate=true;
return{
texture,
canvas:c
};
}
/* GRAMA */
const grassTexture=
pixelTexture(
[
"#49a83b",
"#3c9633",
"#5bb84b",
"#2f822c"
],
(x,y)=>{
const n=
(x*17+y*31+x*y*7)%23;
if(n===0||n===7)return 2;
if(n===4||n===15)return 3;
if(n%5===0)return 1;
return 0;
}
);
/* PEDRA */
const stoneTexture=
pixelTexture(
[
"#777777",
"#696969",
"#898989",
"#595959"
],
(x,y)=>{
const n=
(x*13+y*19+x*y*3)%19;
if(n===0||n===8)return 2;
if(n===5||n===14)return 3;
if(n%4===0)return 1;
return 0;
}
);
/* MADEIRA */
const woodTexture=
pixelTexture(
[
"#8b5a2b",
"#70451f",
"#a96d36",
"#5b3619"
],
(x,y)=>{
if(x===1||x===5)return 1;
if(x===3&&y===3)return 3;
if(x===6&&y===6)return 2;
return 0;
}
);
/* FOLHA */
const leafTexture=
pixelTexture(
[
"#2f8f2f",
"#267a27",
"#43a83d",
"#1d651f"
],
(x,y)=>{
const n=
(x*11+y*23+x*y*5)%17;
if(n===0||n===6)return 2;
if(n===4||n===12)return 3;
if(n%4===0)return 1;
return 0;
}
);
/* ARBUSTO ESCURO */
const darkBushTexture=
pixelTexture(
[
"#174d20",
"#123d19",
"#226b2b",
"#0c2c12"
],
(x,y)=>{
const n=
(x*19+y*13+x*y)%17;
if(n===0||n===5)return 2;
if(n===7||n===12)return 3;
if(n%3===0)return 1;
return 0;
}
);
/* ARBUSTO CLARO */
const lightBushTexture=
pixelTexture(
[
"#4da83e",
"#3e9633",
"#6abd4d",
"#2f7d29"
],
(x,y)=>{
const n=
(x*7+y*17+x*y*3)%19;
if(n===0||n===8)return 2;
if(n===5||n===14)return 3;
if(n%4===0)return 1;
return 0;
}
);
/* =====================================================
MATERIAIS
===================================================== */
/*
MeshBasicMaterial foi usado aqui de propósito:
evita que texturas fiquem pretas quando o
dispositivo tem algum problema de iluminação.
*/
function makeMaterial(tex){
return new THREE.MeshBasicMaterial({
map:tex.texture,
color:0xffffff
});
}
const texturedMaterials={
grass:makeMaterial(grassTexture),
stone:makeMaterial(stoneTexture),
wood:makeMaterial(woodTexture),
leaf:makeMaterial(leafTexture),
darkBush:makeMaterial(darkBushTexture),
lightBush:makeMaterial(lightBushTexture)
};
let materials=texturedMaterials;
/* =====================================================
GEOMETRIA
===================================================== */
const cubeGeometry=
new THREE.BoxGeometry(
1,1,1
);
/* =====================================================
PLAYER
===================================================== */
const player={
x:spawnPoint.x,
y:spawnPoint.y,
z:spawnPoint.z,
vy:0,
onGround:false
};
const playerHeight=1.8;
const playerRadius=.3;
let yaw=0;
let pitch=0;
let sensitivity=1;
/* =====================================================
VIDA
===================================================== */
let hearts=8;
let fallStartY=null;
function updateHearts(){
const el=
document.getElementById("hearts");
if(gameMode!=="survival"){
el.style.display="none";
return;
}
el.style.display="block";
let text="";
for(let i=0;i<8;i++){
text+=
i<hearts
?"♥"
:"♡";
}
el.textContent=text;
}
function getFallDamage(blocks){
if(blocks<2)return 0;
if(blocks<3)return 1;
if(blocks<4)return 2;
if(blocks<5)return 3;
if(blocks<6)return 4;
if(blocks<7)return 5;
return 8;
}
function damage(amount){
if(
gameMode!=="survival"||
dead
)
return;
hearts=
Math.max(
0,
hearts-amount
);
updateHearts();
if(hearts<=0)
die();
}
/* =====================================================
MORTE
===================================================== */
function die(){
dead=true;
moveKeys.up=false;
moveKeys.down=false;
moveKeys.left=false;
moveKeys.right=false;
document
.getElementById("deathScreen")
.style.display="flex";
}
function respawn(){
dead=false;
hearts=8;
player.x=spawnPoint.x;
player.y=spawnPoint.y;
player.z=spawnPoint.z;
player.vy=0;
player.onGround=false;
fallStartY=null;
document
.getElementById("deathScreen")
.style.display="none";
updateHearts();
camera.position.set(
player.x,
player.y+1.6,
player.z
);
updateChunks();
}
/* =====================================================
MUNDO
===================================================== */
const modifications=
new Map();
const loadedChunks=
new Map();
function key(x,y,z){
return `${x},${y},${z}`;
}
function treeHash(x,z){
const n=
Math.sin(
x*12.9898+
z*78.233
)*43758.5453;
return n-
Math.floor(n);
}
function isTree(x,z){
return(
x%8===0&&
z%8===0&&
treeHash(x,z)<.12
);
}
function bushHash(x,z){
const n=
Math.sin(
x*34.123+
z*91.77
)*15731.73;
return n-
Math.floor(n);
}
function isDarkBush(x,z){
return(
x%5===0&&
z%7===0&&
bushHash(x,z)<.20
);
}
function isLightBush(x,z){
return(
x%7===0&&
z%5===0&&
bushHash(x,z)>.70
);
}
function treeBlockAt(x,y,z){
if(!isTree(x,z))
return null;
if(y>=1&&y<=4)
return "wood";
if(y>=4&&y<=7)
return "leaf";
return null;
}
function naturalBlockAt(x,y,z){
if(
Math.abs(x)>WORLD_SIZE/2||
Math.abs(z)>WORLD_SIZE/2
)
return null;
if(y===0)
return "grass";
const tree=
treeBlockAt(x,y,z);
if(tree)
return tree;
for(let tx=-1;tx<=1;tx++){
for(let tz=-1;tz<=1;tz++){
if(!isTree(x-tx,z-tz))
continue;
if(
y>=4&&
y<=7&&
Math.abs(tx)<=1&&
Math.abs(tz)<=1
){
if(
Math.abs(tx)===1&&
Math.abs(tz)===1&&
(x+z+y)%2!==0
)
continue;
return "leaf";
}
}
}
if(y===1){
if(isDarkBush(x,z))
return "darkBush";
if(isLightBush(x,z))
return "lightBush";
}
return null;
}
function worldBlockAt(x,y,z){
const k=
key(x,y,z);
if(modifications.has(k))
return modifications.get(k);
return naturalBlockAt(x,y,z);
}
/* =====================================================
CHUNKS
===================================================== */
function chunkKey(cx,cz){
return `${cx},${cz}`;
}
function removeChunk(cx,cz){
const ck=
chunkKey(cx,cz);
const group=
loadedChunks.get(ck);
if(!group)
return;
scene.remove(group);
loadedChunks.delete(ck);
}
function blockVisible(x,y,z){
return(
!worldBlockAt(x+1,y,z)||
!worldBlockAt(x-1,y,z)||
!worldBlockAt(x,y+1,z)||
!worldBlockAt(x,y-1,z)||
!worldBlockAt(x,y,z+1)||
!worldBlockAt(x,y,z-1)
);
}
function generateChunk(cx,cz){
const ck=
chunkKey(cx,cz);
if(loadedChunks.has(ck))
removeChunk(cx,cz);
const group=
new THREE.Group();
const sx=
cx*CHUNK_SIZE;
const sz=
cz*CHUNK_SIZE;
for(
let x=sx;
x<sx+CHUNK_SIZE;
x++
){
for(
let z=sz;
z<sz+CHUNK_SIZE;
z++
){
for(
let y=0;
y<=20;
y++
){
const type=
worldBlockAt(x,y,z);
if(!type)
continue;
if(!blockVisible(x,y,z))
continue;
const mesh=
new THREE.Mesh(
cubeGeometry,
materials[type]
);
mesh.position.set(
x+.5,
y+.5,
z+.5
);
mesh.userData.block=true;
mesh.userData.x=x;
mesh.userData.y=y;
mesh.userData.z=z;
mesh.userData.type=type;
group.add(mesh);
}
}
}
scene.add(group);
loadedChunks.set(
ck,
group
);
}
function rebuildLoadedChunks(){
const list=
[...loadedChunks.keys()]
.map(ck=>{
const p=
ck.split(",");
return[
Number(p[0]),
Number(p[1])
];
});
list.forEach(
([cx,cz])=>{
generateChunk(cx,cz);
}
);
}
function updateChunks(){
if(!gameStarted)
return;
const pcx=
Math.floor(
player.x/CHUNK_SIZE
);
const pcz=
Math.floor(
player.z/CHUNK_SIZE
);
const radius=
Math.ceil(
RENDER_DISTANCE/
CHUNK_SIZE
);
const needed=
new Set();
for(
let cx=pcx-radius;
cx<=pcx+radius;
cx++
){
for(
let cz=pcz-radius;
cz<=pcz+radius;
cz++
){
const dx=
cx*CHUNK_SIZE+
CHUNK_SIZE/2-
player.x;
const dz=
cz*CHUNK_SIZE+
CHUNK_SIZE/2-
player.z;
if(
Math.hypot(dx,dz)>
RENDER_DISTANCE+
CHUNK_SIZE
)
continue;
const ck=
chunkKey(cx,cz);
needed.add(ck);
if(!loadedChunks.has(ck))
generateChunk(cx,cz);
}
}
for(
const ck of
[...loadedChunks.keys()]
){
if(!needed.has(ck)){
const p=
ck.split(",");
removeChunk(
Number(p[0]),
Number(p[1])
);
}
}
}
/* =====================================================
COLISÃO
===================================================== */
function collides(nx,ny,nz){
const minX=
Math.floor(
nx-playerRadius
);
const maxX=
Math.floor(
nx+playerRadius
);
const minY=
Math.floor(ny);
const maxY=
Math.floor(
ny+playerHeight
);
const minZ=
Math.floor(
nz-playerRadius
);
const maxZ=
Math.floor(
nz+playerRadius
);
for(
let x=minX;
x<=maxX;
x++
){
for(
let y=minY;
y<=maxY;
y++
){
for(
let z=minZ;
z<=maxZ;
z++
){
if(
worldBlockAt(x,y,z)
)
return true;
}
}
}
return false;
}
/* =====================================================
MOVIMENTO
===================================================== */
const moveKeys={
up:false,
down:false,
left:false,
right:false
};
function updatePlayer(dt){
if(
!gameStarted||
dead
)
return;
const fx=
-Math.sin(yaw);
const fz=
-Math.cos(yaw);
const rx=
Math.cos(yaw);
const rz=
-Math.sin(yaw);
let dx=0;
let dz=0;
if(moveKeys.up){
dx+=fx;
dz+=fz;
}
if(moveKeys.down){
dx-=fx;
dz-=fz;
}
if(moveKeys.left){
dx-=rx;
dz-=rz;
}
if(moveKeys.right){
dx+=rx;
dz+=rz;
}
const len=
Math.hypot(dx,dz);
if(len){
dx/=len;
dz/=len;
}
const speed=
5*dt;
if(
!collides(
player.x+dx*speed,
player.y,
player.z
)
){
player.x+=
dx*speed;
}
if(
!collides(
player.x,
player.y,
player.z+dz*speed
)
){
player.z+=
dz*speed;
}
const wasGround=
player.onGround;
player.vy-=
18*dt;
const nextY=
player.y+
player.vy*dt;
if(
collides(
player.x,
nextY,
player.z
)
){
if(player.vy<0){
player.onGround=true;
if(
fallStartY!==null
){
const fallDistance=
Math.max(
0,
fallStartY-
player.y
);
if(
gameMode==="survival"&&
fallDistance>=2
){
damage(
getFallDamage(
fallDistance
)
);
}
fallStartY=null;
}
}
player.vy=0;
}else{
if(
wasGround&&
player.vy<=0
){
fallStartY=
player.y;
}
player.onGround=false;
player.y=nextY;
}
camera.position.set(
player.x,
player.y+1.6,
player.z
);
camera.rotation.order=
"YXZ";
camera.rotation.y=
yaw;
camera.rotation.x=
pitch;
}
/* =====================================================
CÂMERA
===================================================== */
let touchId=null;
let lastX=0;
let lastY=0;
const canvas=
document.getElementById("game");
canvas.addEventListener(
"pointerdown",
e=>{
if(
!gameStarted||
dead||
e.pointerType!=="touch"
)
return;
touchId=e.pointerId;
lastX=e.clientX;
lastY=e.clientY;
}
);
canvas.addEventListener(
"pointermove",
e=>{
if(
touchId===null||
e.pointerId!==touchId
)
return;
const dx=
e.clientX-lastX;
const dy=
e.clientY-lastY;
yaw-=
dx*.004*
sensitivity;
pitch-=
dy*.004*
sensitivity;
pitch=
Math.max(
-1.45,
Math.min(
1.45,
pitch
)
);
lastX=e.clientX;
lastY=e.clientY;
}
);
canvas.addEventListener(
"pointerup",
e=>{
if(
e.pointerId===touchId
)
touchId=null;
}
);
canvas.addEventListener(
"pointercancel",
e=>{
if(
e.pointerId===touchId
)
touchId=null;
}
);
/* =====================================================
BOTÕES DE MOVIMENTO
===================================================== */
function holdButton(id,keyName){
const b=
document.getElementById(id);
b.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
if(!gameStarted||dead)
return;
moveKeys[keyName]=true;
try{
b.setPointerCapture(
e.pointerId
);
}catch(_){}
}
);
b.addEventListener(
"pointerup",
e=>{
e.preventDefault();
moveKeys[keyName]=false;
}
);
b.addEventListener(
"pointercancel",
()=>{
moveKeys[keyName]=false;
}
);
b.addEventListener(
"pointerleave",
()=>{
if(e.pointerType==="mouse")
moveKeys[keyName]=false;
}
);
}
holdButton("up","up");
holdButton("down","down");
holdButton("left","left");
holdButton("right","right");
/* =====================================================
PULAR
===================================================== */
document
.getElementById("jump")
.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
if(
gameStarted&&
!dead&&
player.onGround
){
player.vy=7;
player.onGround=false;
}
}
);
/* =====================================================
INVENTÁRIO
===================================================== */
const itemTypes=[
"grass",
"stone",
"wood",
"leaf",
"darkBush",
"lightBush"
];
const itemNames={
grass:"Grama",
stone:"Pedra",
wood:"Madeira",
leaf:"Folha",
darkBush:"Arbusto escuro",
lightBush:"Arbusto claro"
};
const itemTextures={
grass:grassTexture.canvas,
stone:stoneTexture.canvas,
wood:woodTexture.canvas,
leaf:leafTexture.canvas,
darkBush:darkBushTexture.canvas,
lightBush:lightBushTexture.canvas
};
let inventory=[];
for(let i=0;i<15;i++){
inventory.push({
type:null,
count:0
});
}
let selectedSlot=0;
let selected="grass";
/* =====================================================
SLOT
===================================================== */
function createSlot(item,index){
const slot=
document.createElement("div");
slot.className="slot";
if(index===selectedSlot)
slot.classList.add("sel");
const c=
document.createElement("canvas");
c.width=8;
c.height=8;
slot.appendChild(c);
const count=
document.createElement("span");
count.className="count";
slot.appendChild(count);
if(item.type){
const ctx=
c.getContext("2d");
ctx.imageSmoothingEnabled=false;
ctx.drawImage(
itemTextures[item.type],
0,0,8,8
);
if(item.count>1)
count.textContent=
item.count;
}
slot.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
if(!item.type)
return;
selectedSlot=index;
selected=item.type;
renderInventory();
updateCraft();
}
);
return slot;
}
/*
Hotbar mostra somente os 6 primeiros
slots para não bagunçar a tela.
*/
function renderInventory(){
const inv=
document.getElementById(
"inventory"
);
inv.innerHTML="";
const hotbarCount=
Math.min(6,inventory.length);
for(
let i=0;
i<hotbarCount;
i++
){
inv.appendChild(
createSlot(
inventory[i],
i
)
);
}
const craftInv=
document.getElementById(
"craftInventory"
);
craftInv.innerHTML="";
inventory.forEach(
(item,index)=>{
craftInv.appendChild(
createSlot(
item,
index
)
);
}
);
}
/* =====================================================
DAR ITEM
===================================================== */
function giveItem(
type,
amount=1
){
if(gameMode!=="survival")
return true;
for(
const item of inventory
){
if(
item.type===type&&
item.count<64
){
const add=
Math.min(
amount,
64-item.count
);
item.count+=add;
amount-=add;
if(amount<=0){
renderInventory();
return true;
}
}
}
for(
const item of inventory
){
if(!item.type){
item.type=type;
item.count=amount;
renderInventory();
return true;
}
}
return false;
}
/* =====================================================
REMOVER ITEM
===================================================== */
function removeSelected(
amount=1
){
if(gameMode!=="survival")
return true;
const item=
inventory[selectedSlot];
if(
!item||
!item.type||
item.count<amount
)
return false;
item.count-=amount;
if(item.count<=0){
item.type=null;
item.count=0;
const next=
inventory.findIndex(
x=>x.type===selected
);
if(next>=0)
selectedSlot=next;
}
renderInventory();
return true;
}
/* =====================================================
CRIATIVO
===================================================== */
function setupCreativeInventory(){
inventory=[
{type:"grass",count:64},
{type:"stone",count:64},
{type:"wood",count:64},
{type:"leaf",count:64},
{type:"darkBush",count:64},
{type:"lightBush",count:64}
];
while(
inventory.length<15
){
inventory.push({
type:null,
count:0
});
}
selectedSlot=0;
selected="grass";
renderInventory();
}
/* =====================================================
SOBREVIVÊNCIA
===================================================== */
function setupSurvivalInventory(){
inventory=[];
for(let i=0;i<15;i++){
inventory.push({
type:null,
count:0
});
}
selectedSlot=0;
selected="grass";
renderInventory();
}
/* =====================================================
RAYCAST
===================================================== */
const raycaster=
new THREE.Raycaster();
function getBlockMeshes(){
const result=[];
for(
const group of
loadedChunks.values()
){
group.traverse(
obj=>{
if(
obj.isMesh&&
obj.userData.block
)
result.push(obj);
}
);
}
return result;
}
/* =====================================================
REBUILD
===================================================== */
function rebuildAround(x,z){
const cx=
Math.floor(
x/CHUNK_SIZE
);
const cz=
Math.floor(
z/CHUNK_SIZE
);
for(let dx=-1;dx<=1;dx++){
for(let dz=-1;dz<=1;dz++){
const nx=cx+dx;
const nz=cz+dz;
if(
loadedChunks.has(
chunkKey(nx,nz)
)
){
generateChunk(
nx,nz
);
}
}
}
}
/* =====================================================
QUEBRAR
===================================================== */
document
.getElementById("break")
.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
if(
!gameStarted||
dead
)
return;
raycaster.setFromCamera(
{x:0,y:0},
camera
);
const hit=
raycaster
.intersectObjects(
getBlockMeshes(),
false
)[0];
if(!hit)
return;
const b=
hit.object.userData;
if(b.y===0)
return;
if(
gameMode==="survival"
){
if(
!giveItem(
b.type,
1
)
)
return;
}
modifications.set(
key(
b.x,
b.y,
b.z
),
null
);
rebuildAround(
b.x,
b.z
);
}
);
/* =====================================================
COLOCAR
===================================================== */
document
.getElementById("place")
.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
if(
!gameStarted||
dead
)
return;
if(
gameMode==="survival"&&
(
!inventory[selectedSlot]||
!inventory[selectedSlot].type
)
)
return;
raycaster.setFromCamera(
{x:0,y:0},
camera
);
const hit=
raycaster
.intersectObjects(
getBlockMeshes(),
false
)[0];
if(!hit)
return;
const b=
hit.object.userData;
const n=
hit.face.normal;
const x=
b.x+
Math.round(n.x);
const y=
b.y+
Math.round(n.y);
const z=
b.z+
Math.round(n.z);
if(
y<0||
y>20
)
return;
if(worldBlockAt(x,y,z))
return;
const overlap=
x<player.x+playerRadius&&
x+1>player.x-playerRadius&&
y<player.y+playerHeight&&
y+1>player.y&&
z<player.z+playerRadius&&
z+1>player.z-playerRadius;
if(overlap)
return;
if(
gameMode==="survival"
){
if(!removeSelected(1))
return;
}
modifications.set(
key(x,y,z),
selected
);
rebuildAround(
x,z
);
}
);
/* =====================================================
INVENTÁRIO ABERTO
===================================================== */
function drawCraftItem(
element,
type
){
element.innerHTML="";
if(!type)
return;
const c=
document.createElement("canvas");
c.width=8;
c.height=8;
const ctx=
c.getContext("2d");
ctx.imageSmoothingEnabled=false;
ctx.drawImage(
itemTextures[type],
0,0,8,8
);
element.appendChild(c);
}
function updateCraft(){
const input=
document.getElementById(
"craftInput"
);
const output=
document.getElementById(
"craftOutput"
);
drawCraftItem(
input,
inventory[selectedSlot]?.type
);
output.innerHTML="";
}
document
.getElementById("bagButton")
.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
document
.getElementById(
"craftPanel"
)
.style.display="flex";
renderInventory();
updateCraft();
}
);
document
.getElementById("closeCraft")
.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
document
.getElementById(
"craftPanel"
)
.style.display="none";
}
);
/* =====================================================
TECLADO
===================================================== */
window.addEventListener(
"keydown",
e=>{
if(
!gameStarted||
dead
)
return;
if(
e.key==="w"||
e.key==="ArrowUp"
)
moveKeys.up=true;
if(
e.key==="s"||
e.key==="ArrowDown"
)
moveKeys.down=true;
if(
e.key==="a"||
e.key==="ArrowLeft"
)
moveKeys.left=true;
if(
e.key==="d"||
e.key==="ArrowRight"
)
moveKeys.right=true;
if(
e.key===" "&&
player.onGround
){
player.vy=7;
player.onGround=false;
}
const number=
Number(e.key);
if(
number>=1&&
number<=6
){
const index=
number-1;
if(
inventory[index]&&
inventory[index].type
){
selectedSlot=index;
selected=
inventory[index].type;
renderInventory();
}
}
}
);
window.addEventListener(
"keyup",
e=>{
if(
e.key==="w"||
e.key==="ArrowUp"
)
moveKeys.up=false;
if(
e.key==="s"||
e.key==="ArrowDown"
)
moveKeys.down=false;
if(
e.key==="a"||
e.key==="ArrowLeft"
)
moveKeys.left=false;
if(
e.key==="d"||
e.key==="ArrowRight"
)
moveKeys.right=false;
}
);
/* =====================================================
RENDERIZAÇÃO
===================================================== */
const renderSlider=
document.getElementById(
"renderDistance"
);
const renderValue=
document.getElementById(
"renderValue"
);
function setRender(value){
RENDER_DISTANCE=
Number(value);
renderValue.textContent=
RENDER_DISTANCE;
scene.fog.near=
RENDER_DISTANCE*.5;
scene.fog.far=
RENDER_DISTANCE;
if(gameStarted)
updateChunks();
}
renderSlider.addEventListener(
"input",
()=>{
setRender(
renderSlider.value
);
}
);
/* =====================================================
CONFIGURAÇÕES
===================================================== */
const settings=
document.getElementById(
"settings"
);
document
.getElementById("config")
.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
settings.style.display=
"flex";
}
);
document
.getElementById("closeSettings")
.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
settings.style.display=
"none";
}
);
document
.getElementById("sens")
.addEventListener(
"input",
e=>{
sensitivity=
Number(e.target.value);
}
);
const qualitySlider=
document.getElementById(
"quality"
);
const qualityInfo=
document.getElementById(
"qualityInfo"
);
qualitySlider.addEventListener(
"input",
()=>{
quality=
Number(
qualitySlider.value
);
renderer.setPixelRatio(
Math.min(
devicePixelRatio,
quality/100
)
);
if(quality>90){
qualityInfo.textContent=
quality+
"% — Texturas 8×8 ATIVADAS";
}else{
qualityInfo.textContent=
quality+
"% — modo econômico";
}
}
);
document
.getElementById("render2")
.addEventListener(
"input",
e=>{
renderSlider.value=
e.target.value;
setRender(
e.target.value
);
}
);
/* =====================================================
MENU 3D
===================================================== */
function createMenuBlock(
type,x,y,z
){
const mesh=
new THREE.Mesh(
cubeGeometry,
texturedMaterials[type]
);
mesh.position.set(
x+.5,
y+.5,
z+.5
);
menuScene.add(mesh);
}
function createMenuWorld(){
for(
let x=-14;
x<=14;
x++
){
for(
let z=-12;
z<=12;
z++
){
createMenuBlock(
"grass",
x,0,z
);
}
}
const trees=[
[-8,-5],
[7,-4],
[-4,5],
[10,7],
[-11,7]
];
trees.forEach(
([x,z])=>{
for(
let y=1;
y<=4;
y++
){
createMenuBlock(
"wood",
x,y,z
);
}
for(
let x2=-1;
x2<=1;
x2++
){
for(
let z2=-1;
z2<=1;
z2++
){
for(
let y=4;
y<=6;
y++
){
if(
Math.abs(x2)===1&&
Math.abs(z2)===1&&
y===6
)
continue;
createMenuBlock(
"leaf",
x+x2,
y,
z+z2
);
}
}
}
}
);
for(
let x=-3;
x<=3;
x++
){
createMenuBlock(
"stone",
x,
1,
-1
);
}
}
createMenuWorld();
menuCamera.position.set(
17,
9,
19
);
menuCamera.lookAt(
0,2,0
);
/* =====================================================
ELEMENTOS DE JOGO
===================================================== */
function showGameUI(){
document
.getElementById("hud")
.style.display="block";
document
.getElementById("crosshair")
.style.display="block";
document
.getElementById("topRight")
.style.display="flex";
document
.getElementById("movePad")
.style.display="block";
document
.getElementById("actionPad")
.style.display="block";
document
.getElementById("hand")
.style.display="block";
}
/* =====================================================
INICIAR MUNDO
===================================================== */
function startWorld(mode){
gameMode=mode;
document
.getElementById("worldSelect")
.style.display="none";
document
.getElementById("generating")
.style.display="flex";
const min=
mode==="free"
?4000
:8000;
const max=
mode==="free"
?6000
:12000;
const delay=
Math.floor(
min+
Math.random()*
(max-min+1)
);
document
.getElementById("loadingInfo")
.textContent=
mode==="free"
?"Preparando mundo livre..."
:"Preparando mundo de sobrevivência...";
let dots=0;
const dotTimer=
setInterval(
()=>{
dots=
(dots+1)%4;
document
.getElementById(
"loadingDots"
)
.textContent=
"●".repeat(
dots||1
);
},
350
);
setTimeout(
()=>{
clearInterval(dotTimer);
gameStarted=true;
dead=false;
hearts=8;
document
.getElementById(
"generating"
)
.style.display="none";
document
.getElementById(
"startScreen"
)
.style.display="none";
document
.getElementById(
"modeText"
)
.textContent=
mode==="free"
?"Livre / Criativo"
:"Sobrevivência";
if(mode==="free")
setupCreativeInventory();
else
setupSurvivalInventory();
document
.getElementById(
"bagButton"
)
.style.display="block";
showGameUI();
updateHearts();
player.x=
spawnPoint.x;
player.y=
spawnPoint.y;
player.z=
spawnPoint.z;
player.vy=0;
camera.position.set(
player.x,
player.y+1.6,
player.z
);
updateChunks();
},
delay
);
}
/* =====================================================
BOTÕES MENU
===================================================== */
document
.getElementById("playButton")
.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
document
.getElementById(
"worldSelect"
)
.style.display="flex";
}
);
document
.getElementById("freeButton")
.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
startWorld("free");
}
);
document
.getElementById("survivalButton")
.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
startWorld("survival");
}
);
/* =====================================================
RENASCER
===================================================== */
document
.getElementById("respawn")
.addEventListener(
"pointerdown",
e=>{
e.preventDefault();
respawn();
}
);
/* =====================================================
LOOP
===================================================== */
let fpsFrames=0;
let fpsTime=
performance.now();
let lastChunkX=null;
let lastChunkZ=null;
function animate(time){
requestAnimationFrame(
animate
);
const dt=
Math.min(
.05,
(
time-
(animate.last||time)
)/1000
);
animate.last=time;
/* MENU */
if(!gameStarted){
const t=
time*.00015;
menuCamera.position.x=
Math.sin(t)*19;
menuCamera.position.z=
Math.cos(t)*19;
menuCamera.position.y=
8+
Math.sin(t*2)*1.2;
menuCamera.lookAt(
0,2,0
);
menuRenderer.render(
menuScene,
menuCamera
);
}
updatePlayer(dt);
if(gameStarted){
const cx=
Math.floor(
player.x/
CHUNK_SIZE
);
const cz=
Math.floor(
player.z/
CHUNK_SIZE
);
if(
cx!==lastChunkX||
cz!==lastChunkZ
){
lastChunkX=cx;
lastChunkZ=cz;
updateChunks();
}
renderer.render(
scene,
camera
);
}
fpsFrames++;
if(
time-fpsTime>=500
){
document
.getElementById("fps")
.textContent=
Math.round(
fpsFrames*1000/
(time-fpsTime)
);
fpsFrames=0;
fpsTime=time;
}
}
animate(0);
/* =====================================================
RESIZE
===================================================== */
window.addEventListener(
"resize",
()=>{
camera.aspect=
innerWidth/
innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(
innerWidth,
innerHeight
);
menuCamera.aspect=
innerWidth/
innerHeight;
menuCamera.updateProjectionMatrix();
menuRenderer.setSize(
innerWidth,
innerHeight
);
}
);
/* =====================================================
ESTADO INICIAL
===================================================== */
document
.getElementById("hud")
.style.display="none";
document
.getElementById("crosshair")
.style.display="none";
document
.getElementById("topRight")
.style.display="none";
document
.getElementById("movePad")
.style.display="none";
document
.getElementById("actionPad")
.style.display="none";
document
.getElementById("hand")
.style.display="none";
</script>
</body>
</html>Game Source: GEOMETRICKWORD Pré-Alpha 0.2
Creator: MegaFlare51
Libraries: three
Complexity: complex (3777 lines, 51.2 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.