🎮ArcadeLab

GEOMETRICKWORD Pré-Alpha 0.2

by MegaFlare51
3777 lines51.2 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport"
      content="width=device-width,initial-scale=1.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.