🎮ArcadeLab

Voxel Realms - Character World

by ApexPirate86
1106 lines19.5 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
      content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">

<title>Voxel Realms - Character World</title>

<style>
html,body{
  margin:0;
  width:100%;
  height:100%;
  overflow:hidden;
  background:#07111b;
  font-family:Arial,sans-serif;
  touch-action:none;
  user-select:none;
}

canvas{
  display:block;
  width:100%;
  height:100%;
}

#loading{
  position:fixed;
  inset:0;
  display:flex;
  align-items:center;
  justify-content:center;
  background:#07111b;
  color:white;
  z-index:100;
  font-size:24px;
}

#hud{
  position:fixed;
  inset:0;
  pointer-events:none;
  color:white;
}

#title{
  position:absolute;
  top:12px;
  left:15px;
  font-size:20px;
  font-weight:bold;
  text-shadow:0 2px 4px #000;
}

#info{
  position:absolute;
  top:42px;
  left:15px;
  font-size:13px;
  text-shadow:0 2px 4px #000;
}

#health{
  position:absolute;
  top:12px;
  right:15px;
  background:#111b;
  padding:8px 12px;
  border-radius:12px;
}

#joystick{
  position:absolute;
  left:25px;
  bottom:28px;
  width:125px;
  height:125px;
  border-radius:50%;
  background:#ffffff22;
  border:2px solid #ffffff55;
  pointer-events:auto;
}

#stick{
  position:absolute;
  width:55px;
  height:55px;
  left:35px;
  top:35px;
  border-radius:50%;
  background:#ffffff66;
  border:2px solid #ffffffaa;
}

#lookZone{
  position:absolute;
  right:0;
  top:0;
  width:52%;
  height:72%;
  pointer-events:auto;
}

.action{
  position:absolute;
  pointer-events:auto;
  width:65px;
  height:65px;
  border-radius:50%;
  border:2px solid #ffffff66;
  background:#172333bb;
  color:white;
  font-size:25px;
  display:flex;
  justify-content:center;
  align-items:center;
  box-shadow:0 4px 10px #0008;
}

#jump{
  right:25px;
  bottom:32px;
}

#sprint{
  right:105px;
  bottom:105px;
  width:58px;
  height:58px;
  font-size:21px;
}

#cameraBtn{
  right:25px;
  top:85px;
  width:52px;
  height:52px;
  font-size:22px;
}

#start{
  position:fixed;
  inset:0;
  z-index:90;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  background:linear-gradient(#07111bee,#0d2435ee);
  color:white;
  text-align:center;
}

#start h1{
  font-size:42px;
  margin:0 0 8px;
}

#start p{
  opacity:.8;
  max-width:330px;
}

#start button{
  margin-top:20px;
  padding:15px 35px;
  border:0;
  border-radius:14px;
  background:#42c86b;
  color:white;
  font-size:18px;
  font-weight:bold;
}
</style>
</head>

<body>

<div id="loading">Loading Voxel Realms...</div>

<div id="start">
  <h1>🌍 Voxel Realms</h1>
  <p>Explore the world, meet characters and discover your home.</p>
  <p>Mobile controls included.</p>
  <button id="startBtn">PLAY</button>
</div>

<div id="hud">
  <div id="title">🌍 Voxel Realms</div>
  <div id="info">Explore • Find your home</div>
  <div id="health">❤️ 100</div>

  <div id="lookZone"></div>

  <div id="joystick">
    <div id="stick"></div>
  </div>

  <div class="action" id="jump">⬆️</div>
  <div class="action" id="sprint">🏃</div>
  <div class="action" id="cameraBtn">📷</div>
</div>

<script type="module">

import * as THREE from
"https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js";

/* =========================================================
   BASIC SETUP
========================================================= */

const scene = new THREE.Scene();

scene.background = new THREE.Color(0x82cfff);
scene.fog = new THREE.Fog(0x82cfff,45,130);

const camera = new THREE.PerspectiveCamera(
  65,
  innerWidth/innerHeight,
  .1,
  250
);

const renderer = new THREE.WebGLRenderer({
  antialias:true
});

renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));

renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;

renderer.outputColorSpace=THREE.SRGBColorSpace;
renderer.toneMapping=THREE.ACESFilmicToneMapping;

document.body.appendChild(renderer.domElement);

/* =========================================================
   LIGHT
========================================================= */

const hemi = new THREE.HemisphereLight(
  0xbfe8ff,
  0x536344,
  2.0
);

scene.add(hemi);

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

sun.position.set(-40,70,30);
sun.castShadow=true;

sun.shadow.mapSize.width=1024;
sun.shadow.mapSize.height=1024;

sun.shadow.camera.left=-70;
sun.shadow.camera.right=70;
sun.shadow.camera.top=70;
sun.shadow.camera.bottom=-70;

scene.add(sun);

/* =========================================================
   MATERIALS
========================================================= */

const MAT = {
  grass:new THREE.MeshStandardMaterial({color:0x55a83c}),
  dirt:new THREE.MeshStandardMaterial({color:0x80552f}),
  stone:new THREE.MeshStandardMaterial({color:0x858b91}),
  wood:new THREE.MeshStandardMaterial({color:0x8b542d}),
  darkWood:new THREE.MeshStandardMaterial({color:0x4d2c19}),
  roof:new THREE.MeshStandardMaterial({color:0x9b382f}),
  leaf:new THREE.MeshStandardMaterial({color:0x31843b}),
  glass:new THREE.MeshStandardMaterial({
    color:0x7bd9ff,
    transparent:true,
    opacity:.55
  }),
  white:new THREE.MeshStandardMaterial({color:0xffffff}),
  black:new THREE.MeshStandardMaterial({color:0x111111}),
  skin:new THREE.MeshStandardMaterial({color:0xd28c62}),
  shirt:new THREE.MeshStandardMaterial({color:0x2778d0}),
  pants:new THREE.MeshStandardMaterial({color:0x25304a}),
  shoe:new THREE.MeshStandardMaterial({color:0x202020}),
  hair:new THREE.MeshStandardMaterial({color:0x3a2117}),
  red:new THREE.MeshStandardMaterial({color:0xc93434}),
  yellow:new THREE.MeshStandardMaterial({color:0xe9bf32}),
  green:new THREE.MeshStandardMaterial({color:0x31a85b}),
  pink:new THREE.MeshStandardMaterial({color:0xe4779a})
};

/* =========================================================
   BLOCK FUNCTION
========================================================= */

function block(x,y,z,mat,s=1){

  const geo=new THREE.BoxGeometry(s,s,s);
  const mesh=new THREE.Mesh(geo,mat);

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

  mesh.castShadow=true;
  mesh.receiveShadow=true;

  scene.add(mesh);

  return mesh;
}

/* =========================================================
   WORLD
========================================================= */

const WORLD=48;

for(let x=-WORLD;x<=WORLD;x++){

  for(let z=-WORLD;z<=WORLD;z++){

    let h=1;

    const d=Math.sqrt(x*x+z*z);

    if(d>12){
      h=1+
        Math.floor(
          Math.sin(x*.18)*.7+
          Math.cos(z*.15)*.6+
          Math.sin((x+z)*.08)*.8
        );

      h=Math.max(1,Math.min(4,h));
    }

    for(let y=0;y<h;y++){

      let mat =
        y===h-1
        ? MAT.grass
        : MAT.dirt;

      block(x,y,z,mat);
    }
  }
}

/* =========================================================
   WATER
========================================================= */

const waterGeo=new THREE.PlaneGeometry(70,70);

const waterMat=new THREE.MeshStandardMaterial({
  color:0x238dd0,
  transparent:true,
  opacity:.55
});

const water=new THREE.Mesh(waterGeo,waterMat);

water.rotation.x=-Math.PI/2;
water.position.y=.55;
water.position.set(25,.55,-25);

scene.add(water);

/* =========================================================
   TREE
========================================================= */

function tree(x,z){

  const trunk=block(x,2,z,MAT.wood,1);
  trunk.scale.y=3;

  for(let dx=-1;dx<=1;dx++){
    for(let dz=-1;dz<=1;dz++){

      const leaf=block(
        x+dx,
        4.3,
        z+dz,
        MAT.leaf,
        1.3
      );

      leaf.scale.y=.8;
    }
  }

  block(x,5.1,z,MAT.leaf,1.3);
}

for(let i=0;i<30;i++){

  const x=Math.floor(Math.random()*75)-37;
  const z=Math.floor(Math.random()*75)-37;

  if(Math.abs(x)<12 && Math.abs(z)<12) continue;

  tree(x,z);
}

/* =========================================================
   HOUSE
========================================================= */

function createHouse(){

  const g=new THREE.Group();

  /* floor */

  const floor=new THREE.Mesh(
    new THREE.BoxGeometry(12,.5,10),
    MAT.wood
  );

  floor.position.y=1.25;
  floor.receiveShadow=true;
  g.add(floor);

  /* walls */

  function wall(x,y,z,sx,sy,sz){

    const m=new THREE.Mesh(
      new THREE.BoxGeometry(sx,sy,sz),
      MAT.wood
    );

    m.position.set(x,y,z);
    m.castShadow=true;
    m.receiveShadow=true;

    g.add(m);
  }

  wall(-6,4,0,.6,5,10);
  wall(6,4,0,.6,5,10);
  wall(0,4,-5,12,5,.6);

  /* front wall pieces, leaving door */

  wall(-4,4,5,.6,5,2.5);
  wall(4,4,5,.6,5,2.5);
  wall(0,6,5,7,1.5,.6);

  /* roof */

  const roof1=new THREE.Mesh(
    new THREE.BoxGeometry(7,1,12),
    MAT.roof
  );

  roof1.position.set(-3,7,0);
  roof1.rotation.z=-.35;
  roof1.castShadow=true;
  g.add(roof1);

  const roof2=roof1.clone();
  roof2.position.x=3;
  roof2.rotation.z=.35;
  g.add(roof2);

  /* windows */

  function window(x,z){

    const w=new THREE.Mesh(
      new THREE.BoxGeometry(2,.05,2),
      MAT.glass
    );

    w.position.set(x,4,z);
    w.rotation.x=Math.PI/2;

    g.add(w);
  }

  window(-6.3,0);
  window(6.3,0);

  /* door */

  const door=new THREE.Mesh(
    new THREE.BoxGeometry(2.2,.25,4),
    MAT.darkWood
  );

  door.position.set(0,3,5.25);
  g.add(door);

  /* bed */

  const bed=new THREE.Mesh(
    new THREE.BoxGeometry(3,0.7,2),
    MAT.red
  );

  bed.position.set(-3,2,-2.5);
  g.add(bed);

  const pillow=new THREE.Mesh(
    new THREE.BoxGeometry(1.5,.4,1.7),
    MAT.white
  );

  pillow.position.set(-3,2.5,-3);
  g.add(pillow);

  /* table */

  const table=new THREE.Mesh(
    new THREE.BoxGeometry(2,.4,1.5),
    MAT.darkWood
  );

  table.position.set(3,3,-1);
  g.add(table);

  g.position.set(0,0,0);

  scene.add(g);
}

createHouse();

/* =========================================================
   CHARACTER BUILDER
========================================================= */

function makeCharacter(options={}){

  const c=new THREE.Group();

  const skin=options.skin || MAT.skin;
  const shirt=options.shirt || MAT.shirt;
  const pants=options.pants || MAT.pants;
  const hair=options.hair || MAT.hair;

  /* body */

  const torso=new THREE.Mesh(
    new THREE.BoxGeometry(1.5,2,0.8),
    shirt
  );

  torso.position.y=3.5;
  torso.castShadow=true;

  c.add(torso);

  /* neck */

  const neck=new THREE.Mesh(
    new THREE.BoxGeometry(.45,.45,.45),
    skin
  );

  neck.position.y=4.7;
  neck.castShadow=true;

  c.add(neck);

  /* head */

  const head=new THREE.Mesh(
    new THREE.BoxGeometry(1.65,1.55,1.35),
    skin
  );

  head.position.y=5.65;
  head.castShadow=true;

  c.add(head);

  /* hair */

  const hairTop=new THREE.Mesh(
    new THREE.BoxGeometry(1.72,.55,1.42),
    hair
  );

  hairTop.position.set(0,6.35,0);
  hairTop.castShadow=true;

  c.add(hairTop);

  /* =====================================================
     FACE
  ===================================================== */

  function facePart(geo,mat,x,y,z){

    const m=new THREE.Mesh(geo,mat);
    m.position.set(x,y,z);
    m.castShadow=true;
    c.add(m);
    return m;
  }

  /* eyes */

  const eyeGeo=new THREE.SphereGeometry(.16,12,8);

  const leftEye=facePart(
    eyeGeo,
    MAT.white,
    -.38,
    5.78,
    -.68
  );

  const rightEye=facePart(
    eyeGeo,
    MAT.white,
    .38,
    5.78,
    -.68
  );

  /* pupils */

  const pupilGeo=new THREE.SphereGeometry(.075,10,6);

  facePart(
    pupilGeo,
    MAT.black,
    -.38,
    5.78,
    -.82
  );

  facePart(
    pupilGeo,
    MAT.black,
    .38,
    5.78,
    -.82
  );

  /* nose */

  const nose=new THREE.Mesh(
    new THREE.BoxGeometry(.16,.22,.18),
    skin
  );

  nose.position.set(0,5.5,-.76);
  nose.castShadow=true;

  c.add(nose);

  /* mouth */

  const mouth=new THREE.Mesh(
    new THREE.BoxGeometry(.45,.11,.08),
    MAT.red
  );

  mouth.position.set(0,5.22,-.72);
  mouth.castShadow=true;

  c.add(mouth);

  /* ears */

  const earGeo=new THREE.SphereGeometry(.25,10,8);

  facePart(
    earGeo,
    skin,
    -.86,
    5.62,
    0
  );

  facePart(
    earGeo,
    skin,
    .86,
    5.62,
    0
  );

  /* =====================================================
     ARMS
  ===================================================== */

  const leftArm=new THREE.Mesh(
    new THREE.BoxGeometry(.48,1.9,.5),
    shirt
  );

  leftArm.position.set(-1.05,3.5,0);
  leftArm.castShadow=true;
  c.add(leftArm);

  const rightArm=leftArm.clone();
  rightArm.position.x=1.05;
  c.add(rightArm);

  /* hands */

  const handGeo=new THREE.BoxGeometry(.5,.45,.5);

  const leftHand=new THREE.Mesh(handGeo,skin);
  leftHand.position.set(-1.05,2.4,0);
  leftHand.castShadow=true;
  c.add(leftHand);

  const rightHand=leftHand.clone();
  rightHand.position.x=1.05;
  c.add(rightHand);

  /* =====================================================
     LEGS
  ===================================================== */

  const leftLeg=new THREE.Mesh(
    new THREE.BoxGeometry(.62,1.8,.65),
    pants
  );

  leftLeg.position.set(-.43,1.75,0);
  leftLeg.castShadow=true;
  c.add(leftLeg);

  const rightLeg=leftLeg.clone();
  rightLeg.position.x=.43;
  c.add(rightLeg);

  /* feet */

  const leftFoot=new THREE.Mesh(
    new THREE.BoxGeometry(.7,.4,1),
    MAT.shoe
  );

  leftFoot.position.set(-.43,.75,-.18);
  leftFoot.castShadow=true;
  c.add(leftFoot);

  const rightFoot=leftFoot.clone();
  rightFoot.position.x=.43;
  c.add(rightFoot);

  c.userData.parts={
    leftArm,
    rightArm,
    leftLeg,
    rightLeg
  };

  return c;
}

/* =========================================================
   PLAYER
========================================================= */

const player=makeCharacter({
  skin:MAT.skin,
  shirt:MAT.shirt,
  pants:MAT.pants,
  hair:MAT.hair
});

player.position.set(3,0,10);

scene.add(player);

/* =========================================================
   NPCs
========================================================= */

const npc1=makeCharacter({
  skin:new THREE.MeshStandardMaterial({color:0x9c603f}),
  shirt:MAT.green,
  pants:MAT.pants,
  hair:new THREE.MeshStandardMaterial({color:0x17110d})
});

npc1.position.set(-8,0,4);
npc1.rotation.y=Math.PI;

scene.add(npc1);

const npc2=makeCharacter({
  skin:new THREE.MeshStandardMaterial({color:0xf0b48b}),
  shirt:MAT.yellow,
  pants:new THREE.MeshStandardMaterial({color:0x68408e}),
  hair:new THREE.MeshStandardMaterial({color:0x8a4a20})
});

npc2.position.set(9,0,-5);

scene.add(npc2);

const npc3=makeCharacter({
  skin:new THREE.MeshStandardMaterial({color:0x70462f}),
  shirt:MAT.red,
  pants:new THREE.MeshStandardMaterial({color:0x253c59}),
  hair:new THREE.MeshStandardMaterial({color:0x090909})
});

npc3.position.set(-10,0,-9);

scene.add(npc3);

/* =========================================================
   ANIMALS
========================================================= */

function cow(x,z){

  const g=new THREE.Group();

  const body=new THREE.Mesh(
    new THREE.BoxGeometry(2.4,1.4,1.3),
    MAT.white
  );

  body.position.y=1.9;
  body.castShadow=true;
  g.add(body);

  const head=new THREE.Mesh(
    new THREE.BoxGeometry(1.1,1.1,1),
    MAT.white
  );

  head.position.set(0,2.6,-1.3);
  head.castShadow=true;
  g.add(head);

  /* eyes */

  const eyeGeo=new THREE.SphereGeometry(.1,8,6);

  const e1=new THREE.Mesh(eyeGeo,MAT.black);
  e1.position.set(-.28,2.75,-1.78);
  g.add(e1);

  const e2=e1.clone();
  e2.position.x=.28;
  g.add(e2);

  /* ears */

  const earGeo=new THREE.BoxGeometry(.35,.25,.35);

  const ear1=new THREE.Mesh(earGeo,MAT.white);
  ear1.position.set(-.55,3,-1.2);
  g.add(ear1);

  const ear2=ear1.clone();
  ear2.position.x=.55;
  g.add(ear2);

  /* legs */

  for(const x2 of [-.75,.75]){
    for(const z2 of [-.35,.35]){

      const leg=new THREE.Mesh(
        new THREE.BoxGeometry(.4,1.5,.4),
        MAT.white
      );

      leg.position.set(x2,1,z2);
      leg.castShadow=true;

      g.add(leg);
    }
  }

  g.position.set(x,0,z);

  scene.add(g);

  return g;
}

cow(14,7);
cow(17,4);

/* =========================================================
   CONTROLS
========================================================= */

const keys={};

addEventListener("keydown",e=>{
  keys[e.key.toLowerCase()]=true;

  if(e.code==="Space"){
    jumpRequest=true;
  }
});

addEventListener("keyup",e=>{
  keys[e.key.toLowerCase()]=false;
});

/* joystick */

const joystick=document.getElementById("joystick");
const stick=document.getElementById("stick");

let joyX=0;
let joyY=0;
let joyPointer=null;

function updateJoystick(e){

  const r=joystick.getBoundingClientRect();

  let x=e.clientX-(r.left+r.width/2);
  let y=e.clientY-(r.top+r.height/2);

  const max=45;

  const len=Math.hypot(x,y);

  if(len>max){
    x=x/len*max;
    y=y/len*max;
  }

  joyX=x/max;
  joyY=y/max;

  stick.style.transform=
    `translate(${x}px,${y}px)`;
}

joystick.addEventListener("pointerdown",e=>{
  joyPointer=e.pointerId;
  joystick.setPointerCapture(e.pointerId);
  updateJoystick(e);
});

joystick.addEventListener("pointermove",e=>{
  if(e.pointerId===joyPointer)
    updateJoystick(e);
});

function resetJoystick(){
  joyX=0;
  joyY=0;
  stick.style.transform="translate(0,0)";
  joyPointer=null;
}

joystick.addEventListener("pointerup",resetJoystick);
joystick.addEventListener("pointercancel",resetJoystick);

/* =========================================================
   CAMERA LOOK
========================================================= */

const lookZone=document.getElementById("lookZone");

let lookPointer=null;
let lastLookX=0;
let lastLookY=0;

let camYaw=0;
let camPitch=.25;

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

  lookPointer=e.pointerId;

  lastLookX=e.clientX;
  lastLookY=e.clientY;

  lookZone.setPointerCapture(e.pointerId);
});

lookZone.addEventListener("pointermove",e=>{

  if(e.pointerId!==lookPointer)return;

  const dx=e.clientX-lastLookX;
  const dy=e.clientY-lastLookY;

  camYaw-=dx*.008;
  camPitch-=dy*.006;

  camPitch=Math.max(-.15,Math.min(.9,camPitch));

  lastLookX=e.clientX;
  lastLookY=e.clientY;
});

lookZone.addEventListener("pointerup",()=>{
  lookPointer=null;
});

lookZone.addEventListener("pointercancel",()=>{
  lookPointer=null;
});

/* =========================================================
   JUMP + SPRINT
========================================================= */

let jumpRequest=false;
let verticalVelocity=0;
let onGround=true;
let sprint=false;

const jumpBtn=document.getElementById("jump");
const sprintBtn=document.getElementById("sprint");

jumpBtn.addEventListener("pointerdown",()=>{
  jumpRequest=true;
});

sprintBtn.addEventListener("pointerdown",()=>{
  sprint=true;
});

sprintBtn.addEventListener("pointerup",()=>{
  sprint=false;
});

sprintBtn.addEventListener("pointercancel",()=>{
  sprint=false;
});

/* =========================================================
   CAMERA ANGLES
========================================================= */

let cameraMode=0;

document.getElementById("cameraBtn").addEventListener("click",()=>{

  cameraMode++;

  if(cameraMode>2)cameraMode=0;

  if(cameraMode===0){
    camYaw=0;
  }

  if(cameraMode===1){
    camYaw=Math.PI/2;
  }

  if(cameraMode===2){
    camYaw=Math.PI;
  }
});

/* =========================================================
   DESKTOP MOUSE CAMERA
========================================================= */

let mouseDown=false;
let mouseX=0;
let mouseY=0;

renderer.domElement.addEventListener("mousedown",e=>{
  mouseDown=true;
  mouseX=e.clientX;
  mouseY=e.clientY;
});

addEventListener("mouseup",()=>{
  mouseDown=false;
});

addEventListener("mousemove",e=>{

  if(!mouseDown)return;

  const dx=e.clientX-mouseX;
  const dy=e.clientY-mouseY;

  camYaw-=dx*.006;
  camPitch-=dy*.005;

  camPitch=Math.max(-.15,Math.min(.9,camPitch));

  mouseX=e.clientX;
  mouseY=e.clientY;
});

/* =========================================================
   ZOOM
==========

Game Source: Voxel Realms - Character World

Creator: ApexPirate86

Libraries: three

Complexity: complex (1106 lines, 19.5 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: voxel-realms-character-world-apexpirate86" to link back to the original. Then publish at arcadelab.ai/publish.