Skip to content

Instantly share code, notes, and snippets.

@Siedrix
Last active November 26, 2025 02:57
Show Gist options
  • Select an option

  • Save Siedrix/c04d86af0941d6c37dbb593b6a76e76d to your computer and use it in GitHub Desktop.

Select an option

Save Siedrix/c04d86af0941d6c37dbb593b6a76e76d to your computer and use it in GitHub Desktop.
Use a vanilla javascript project with Three.js on a index.html file.
Create a 3D Minecraft clone.
## Core Features
- Procedural tree generation approach
- Smooth terrain generation algorithms
- Basic game play mechanics (movement, interaction, terrain manipulation)
## Project Structure
- Single HTML with full-page canvas layout
- Basic Ul controls
- Styles for full-screen 3D experience
## Textures
- Texture generation functions with Canvas API
- Draw geometric patterns (lines, circles, angles)
- Unique block textures:
- Grass texture generation
- Dirt texture generation
- Stone texture generation
- Wood texture generation
- Leaf texture generation
## Terrain Generation
- Multi-octave Simplex noise generator
- Elevation algorithms for:
- Rolling hills
- Plateaus
- Valleys
- Block placement logic for:
- Grass layers
- Dirt layers
- Stone layers
## Procedural Trees
- Tree generation system
- Trunk block placement
- Canopy leaf distribution
- Custom wood/leaf textures
## Player Controls
- WASD movement system
- Mouse look with pointer lock
- Jump mechanics
- Fly
- Gravity simulation
- Build collision detection system
## Block Interactions
- Left-click block breaking
- Right-click block placement
- Terrain modification system
## Atmosphere
- Gradient skybox system
- Daylight cycle
- Atmospheric depth effects
## Controls
Control scheme:
- WASD: Movement
- Mouse: Look around
- Space: Jump
- Shift: Crouch
- Left Click: Break blocks
- Right Click: Place blocks
## Technology Stack
- JavaScript
- Three.js: Use ES Module (ESM) loaded via importmap from jsdelivr
- CSS
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Minecraft Clone</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #000;
}
#game-canvas {
display: block;
width: 100vw;
height: 100vh;
}
#crosshair {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
pointer-events: none;
z-index: 100;
}
#crosshair::before,
#crosshair::after {
content: '';
position: absolute;
background: white;
mix-blend-mode: difference;
}
#crosshair::before {
width: 2px;
height: 20px;
left: 50%;
transform: translateX(-50%);
}
#crosshair::after {
width: 20px;
height: 2px;
top: 50%;
transform: translateY(-50%);
}
#ui-container {
position: fixed;
top: 10px;
left: 10px;
z-index: 100;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
}
#controls-panel {
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 8px;
font-size: 12px;
max-width: 250px;
backdrop-filter: blur(5px);
}
#controls-panel h3 {
margin-bottom: 10px;
color: #7fff7f;
font-size: 14px;
}
#controls-panel p {
margin: 4px 0;
color: #ddd;
}
#controls-panel .key {
display: inline-block;
background: rgba(255,255,255,0.2);
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
margin-right: 5px;
}
#stats-panel {
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 10px 15px;
border-radius: 8px;
color: white;
font-size: 12px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
backdrop-filter: blur(5px);
}
#stats-panel div {
margin: 3px 0;
}
#hotbar {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 4px;
z-index: 100;
}
.hotbar-slot {
width: 50px;
height: 50px;
background: rgba(0, 0, 0, 0.6);
border: 3px solid rgba(100, 100, 100, 0.8);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.hotbar-slot.selected {
border-color: #fff;
box-shadow: 0 0 10px rgba(255,255,255,0.5);
}
.hotbar-slot canvas {
width: 40px;
height: 40px;
image-rendering: pixelated;
}
.hotbar-slot .slot-number {
position: absolute;
top: 2px;
left: 4px;
font-size: 10px;
color: white;
text-shadow: 1px 1px 2px black;
}
#start-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
color: white;
}
#start-screen h1 {
font-size: 48px;
margin-bottom: 20px;
text-shadow: 4px 4px 8px rgba(0,0,0,0.5);
color: #7fff7f;
}
#start-screen p {
font-size: 18px;
margin-bottom: 30px;
color: #ccc;
}
#start-btn {
padding: 15px 40px;
font-size: 20px;
background: linear-gradient(135deg, #4a7c59 0%, #3d6b4f 100%);
border: none;
color: white;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
#start-btn:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
}
#loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
z-index: 999;
display: none;
}
#time-display {
position: fixed;
top: 10px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.5);
padding: 5px 15px;
border-radius: 15px;
color: white;
font-size: 14px;
z-index: 100;
}
</style>
</head>
<body>
<div id="start-screen">
<h1>Minecraft Clone</h1>
<p>A 3D voxel world built with Three.js</p>
<button id="start-btn">Click to Play</button>
</div>
<div id="loading">Generating World...</div>
<canvas id="game-canvas"></canvas>
<div id="crosshair"></div>
<div id="ui-container">
<div id="controls-panel">
<h3>Controls</h3>
<p><span class="key">W A S D</span> Move</p>
<p><span class="key">Mouse</span> Look around</p>
<p><span class="key">Space</span> Jump / Fly up</p>
<p><span class="key">Shift</span> Crouch / Fly down</p>
<p><span class="key">F</span> Toggle fly mode</p>
<p><span class="key">Left Click</span> Break block</p>
<p><span class="key">Right Click</span> Place block</p>
<p><span class="key">1-5</span> Select block</p>
<p><span class="key">Scroll</span> Change block</p>
<p><span class="key">ESC</span> Release mouse</p>
</div>
</div>
<div id="stats-panel">
<div>FPS: <span id="fps">0</span></div>
<div>Position: <span id="position">0, 0, 0</span></div>
<div>Blocks: <span id="block-count">0</span></div>
<div>Mode: <span id="fly-mode">Walking</span></div>
</div>
<div id="time-display">
<span id="time-icon">☀️</span> <span id="time-text">12:00</span>
</div>
<div id="hotbar"></div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// ============================================
// TEXTURE GENERATION SYSTEM
// ============================================
class TextureGenerator {
constructor() {
this.textureSize = 16;
this.textures = {};
}
createCanvas() {
const canvas = document.createElement('canvas');
canvas.width = this.textureSize;
canvas.height = this.textureSize;
return canvas;
}
// Seeded random for consistent textures
seededRandom(seed) {
const x = Math.sin(seed) * 10000;
return x - Math.floor(x);
}
generateGrassTop() {
const canvas = this.createCanvas();
const ctx = canvas.getContext('2d');
// Base green
ctx.fillStyle = '#5d9b3a';
ctx.fillRect(0, 0, 16, 16);
// Add variation
for (let i = 0; i < 60; i++) {
const x = Math.floor(this.seededRandom(i * 3) * 16);
const y = Math.floor(this.seededRandom(i * 5) * 16);
const shade = Math.floor(this.seededRandom(i * 7) * 40) - 20;
const g = Math.min(255, Math.max(0, 155 + shade));
ctx.fillStyle = `rgb(${93 + shade/2}, ${g}, ${58 + shade/3})`;
ctx.fillRect(x, y, 1, 1);
}
// Add grass blades
for (let i = 0; i < 20; i++) {
const x = Math.floor(this.seededRandom(i * 11) * 16);
const y = Math.floor(this.seededRandom(i * 13) * 16);
ctx.fillStyle = '#4a8030';
ctx.fillRect(x, y, 1, 2);
}
return canvas;
}
generateGrassSide() {
const canvas = this.createCanvas();
const ctx = canvas.getContext('2d');
// Dirt base
ctx.fillStyle = '#8b6b4a';
ctx.fillRect(0, 0, 16, 16);
// Dirt texture
for (let i = 0; i < 40; i++) {
const x = Math.floor(this.seededRandom(i * 17) * 16);
const y = Math.floor(this.seededRandom(i * 19) * 16);
const shade = Math.floor(this.seededRandom(i * 23) * 30) - 15;
ctx.fillStyle = `rgb(${139 + shade}, ${107 + shade}, ${74 + shade})`;
ctx.fillRect(x, y, 2, 2);
}
// Grass top strip
ctx.fillStyle = '#5d9b3a';
ctx.fillRect(0, 0, 16, 3);
// Grass overhang
for (let x = 0; x < 16; x++) {
const hang = Math.floor(this.seededRandom(x * 29) * 3);
ctx.fillStyle = '#5d9b3a';
ctx.fillRect(x, 3, 1, hang);
}
return canvas;
}
generateDirt() {
const canvas = this.createCanvas();
const ctx = canvas.getContext('2d');
// Base dirt color
ctx.fillStyle = '#8b6b4a';
ctx.fillRect(0, 0, 16, 16);
// Add dirt variation
for (let i = 0; i < 80; i++) {
const x = Math.floor(this.seededRandom(i * 31) * 16);
const y = Math.floor(this.seededRandom(i * 37) * 16);
const shade = Math.floor(this.seededRandom(i * 41) * 40) - 20;
ctx.fillStyle = `rgb(${139 + shade}, ${107 + shade}, ${74 + shade})`;
ctx.fillRect(x, y, 2, 2);
}
// Add darker spots
for (let i = 0; i < 15; i++) {
const x = Math.floor(this.seededRandom(i * 43) * 16);
const y = Math.floor(this.seededRandom(i * 47) * 16);
ctx.fillStyle = '#6b4b2a';
ctx.fillRect(x, y, 2, 2);
}
return canvas;
}
generateStone() {
const canvas = this.createCanvas();
const ctx = canvas.getContext('2d');
// Base stone color
ctx.fillStyle = '#8a8a8a';
ctx.fillRect(0, 0, 16, 16);
// Add stone variation
for (let i = 0; i < 100; i++) {
const x = Math.floor(this.seededRandom(i * 53) * 16);
const y = Math.floor(this.seededRandom(i * 59) * 16);
const shade = Math.floor(this.seededRandom(i * 61) * 50) - 25;
const gray = Math.min(255, Math.max(0, 138 + shade));
ctx.fillStyle = `rgb(${gray}, ${gray}, ${gray})`;
ctx.fillRect(x, y, 1, 1);
}
// Add crack lines
for (let i = 0; i < 5; i++) {
const x1 = Math.floor(this.seededRandom(i * 67) * 16);
const y1 = Math.floor(this.seededRandom(i * 71) * 16);
ctx.strokeStyle = '#5a5a5a';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x1 + this.seededRandom(i * 73) * 6, y1 + this.seededRandom(i * 79) * 6);
ctx.stroke();
}
return canvas;
}
generateWood() {
const canvas = this.createCanvas();
const ctx = canvas.getContext('2d');
// Base wood color
ctx.fillStyle = '#6b5030';
ctx.fillRect(0, 0, 16, 16);
// Add wood grain lines
for (let y = 0; y < 16; y += 2) {
const shade = Math.floor(this.seededRandom(y * 83) * 20) - 10;
ctx.fillStyle = `rgb(${107 + shade}, ${80 + shade}, ${48 + shade})`;
ctx.fillRect(0, y, 16, 1);
}
// Add bark texture
for (let i = 0; i < 30; i++) {
const x = Math.floor(this.seededRandom(i * 89) * 16);
const y = Math.floor(this.seededRandom(i * 97) * 16);
ctx.fillStyle = '#4a3520';
ctx.fillRect(x, y, 1, 2);
}
return canvas;
}
generateWoodTop() {
const canvas = this.createCanvas();
const ctx = canvas.getContext('2d');
// Base wood color
ctx.fillStyle = '#9b7050';
ctx.fillRect(0, 0, 16, 16);
// Draw rings
const cx = 8, cy = 8;
for (let r = 2; r < 8; r += 2) {
ctx.strokeStyle = r % 4 === 0 ? '#6b5030' : '#8b6040';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.stroke();
}
// Center
ctx.fillStyle = '#5a4020';
ctx.fillRect(7, 7, 2, 2);
return canvas;
}
generateLeaves() {
const canvas = this.createCanvas();
const ctx = canvas.getContext('2d');
// Transparent base
ctx.clearRect(0, 0, 16, 16);
// Add leaf clusters
for (let i = 0; i < 40; i++) {
const x = Math.floor(this.seededRandom(i * 101) * 16);
const y = Math.floor(this.seededRandom(i * 103) * 16);
const shade = Math.floor(this.seededRandom(i * 107) * 40);
ctx.fillStyle = `rgb(${50 + shade}, ${130 + shade/2}, ${50 + shade/2})`;
ctx.fillRect(x, y, 2, 2);
}
// Add darker areas
for (let i = 0; i < 20; i++) {
const x = Math.floor(this.seededRandom(i * 109) * 16);
const y = Math.floor(this.seededRandom(i * 113) * 16);
ctx.fillStyle = '#2a6a2a';
ctx.fillRect(x, y, 2, 2);
}
// Fill remaining with base green
const imageData = ctx.getImageData(0, 0, 16, 16);
for (let i = 0; i < imageData.data.length; i += 4) {
if (imageData.data[i + 3] === 0) {
imageData.data[i] = 60;
imageData.data[i + 1] = 140;
imageData.data[i + 2] = 60;
imageData.data[i + 3] = 255;
}
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
generateSand() {
const canvas = this.createCanvas();
const ctx = canvas.getContext('2d');
// Base sand color
ctx.fillStyle = '#e6d5a8';
ctx.fillRect(0, 0, 16, 16);
// Add sand grain variation
for (let i = 0; i < 100; i++) {
const x = Math.floor(this.seededRandom(i * 127) * 16);
const y = Math.floor(this.seededRandom(i * 131) * 16);
const shade = Math.floor(this.seededRandom(i * 137) * 30) - 15;
ctx.fillStyle = `rgb(${230 + shade}, ${213 + shade}, ${168 + shade})`;
ctx.fillRect(x, y, 1, 1);
}
return canvas;
}
generateWater() {
const canvas = this.createCanvas();
const ctx = canvas.getContext('2d');
// Base water color with transparency
ctx.fillStyle = 'rgba(30, 100, 200, 0.7)';
ctx.fillRect(0, 0, 16, 16);
// Add wave highlights
for (let i = 0; i < 20; i++) {
const x = Math.floor(this.seededRandom(i * 139) * 16);
const y = Math.floor(this.seededRandom(i * 149) * 16);
ctx.fillStyle = 'rgba(100, 180, 255, 0.5)';
ctx.fillRect(x, y, 3, 1);
}
return canvas;
}
createTexture(canvas) {
const texture = new THREE.CanvasTexture(canvas);
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
texture.colorSpace = THREE.SRGBColorSpace;
return texture;
}
generateAllTextures() {
this.textures = {
grassTop: this.createTexture(this.generateGrassTop()),
grassSide: this.createTexture(this.generateGrassSide()),
dirt: this.createTexture(this.generateDirt()),
stone: this.createTexture(this.generateStone()),
wood: this.createTexture(this.generateWood()),
woodTop: this.createTexture(this.generateWoodTop()),
leaves: this.createTexture(this.generateLeaves()),
sand: this.createTexture(this.generateSand()),
water: this.createTexture(this.generateWater())
};
return this.textures;
}
getHotbarCanvas(blockType) {
switch(blockType) {
case 'grass': return this.generateGrassTop();
case 'dirt': return this.generateDirt();
case 'stone': return this.generateStone();
case 'wood': return this.generateWood();
case 'leaves': return this.generateLeaves();
default: return this.generateGrassTop();
}
}
}
// ============================================
// SIMPLEX NOISE GENERATOR
// ============================================
class SimplexNoise {
constructor(seed = Math.random()) {
this.seed = seed;
this.p = this.buildPermutationTable();
this.perm = new Uint8Array(512);
this.permMod12 = new Uint8Array(512);
for (let i = 0; i < 512; i++) {
this.perm[i] = this.p[i & 255];
this.permMod12[i] = this.perm[i] % 12;
}
this.grad3 = [
[1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0],
[1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1],
[0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1]
];
}
buildPermutationTable() {
const p = [];
for (let i = 0; i < 256; i++) p[i] = i;
let n = 256;
let seed = this.seed * 2147483647;
while (n > 1) {
seed = (seed * 16807) % 2147483647;
const k = ((seed - 1) % n) + 1;
n--;
[p[n], p[k]] = [p[k], p[n]];
}
return p;
}
dot(g, x, y) {
return g[0] * x + g[1] * y;
}
noise2D(xin, yin) {
const F2 = 0.5 * (Math.sqrt(3) - 1);
const G2 = (3 - Math.sqrt(3)) / 6;
let n0, n1, n2;
const s = (xin + yin) * F2;
const i = Math.floor(xin + s);
const j = Math.floor(yin + s);
const t = (i + j) * G2;
const X0 = i - t;
const Y0 = j - t;
const x0 = xin - X0;
const y0 = yin - Y0;
let i1, j1;
if (x0 > y0) { i1 = 1; j1 = 0; }
else { i1 = 0; j1 = 1; }
const x1 = x0 - i1 + G2;
const y1 = y0 - j1 + G2;
const x2 = x0 - 1 + 2 * G2;
const y2 = y0 - 1 + 2 * G2;
const ii = i & 255;
const jj = j & 255;
const gi0 = this.permMod12[ii + this.perm[jj]];
const gi1 = this.permMod12[ii + i1 + this.perm[jj + j1]];
const gi2 = this.permMod12[ii + 1 + this.perm[jj + 1]];
let t0 = 0.5 - x0 * x0 - y0 * y0;
if (t0 < 0) n0 = 0;
else {
t0 *= t0;
n0 = t0 * t0 * this.dot(this.grad3[gi0], x0, y0);
}
let t1 = 0.5 - x1 * x1 - y1 * y1;
if (t1 < 0) n1 = 0;
else {
t1 *= t1;
n1 = t1 * t1 * this.dot(this.grad3[gi1], x1, y1);
}
let t2 = 0.5 - x2 * x2 - y2 * y2;
if (t2 < 0) n2 = 0;
else {
t2 *= t2;
n2 = t2 * t2 * this.dot(this.grad3[gi2], x2, y2);
}
return 70 * (n0 + n1 + n2);
}
// Multi-octave noise for terrain
octaveNoise(x, y, octaves, persistence, lacunarity, scale) {
let total = 0;
let frequency = 1;
let amplitude = 1;
let maxValue = 0;
for (let i = 0; i < octaves; i++) {
total += this.noise2D(x * frequency / scale, y * frequency / scale) * amplitude;
maxValue += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
return total / maxValue;
}
}
// ============================================
// TERRAIN GENERATOR
// ============================================
class TerrainGenerator {
constructor(seed = 12345) {
this.noise = new SimplexNoise(seed);
this.treeNoise = new SimplexNoise(seed + 1);
// Terrain parameters
this.baseHeight = 20;
this.heightVariation = 15;
this.waterLevel = 15;
}
getHeight(x, z) {
// Multi-octave noise for varied terrain
const elevation = this.noise.octaveNoise(x, z, 4, 0.5, 2, 60);
// Hills and valleys
const hills = this.noise.octaveNoise(x + 1000, z + 1000, 2, 0.5, 2, 30) * 0.5;
// Plateaus (using absolute value creates flat areas)
const plateaus = Math.abs(this.noise.octaveNoise(x + 2000, z + 2000, 1, 0.5, 2, 100)) * 0.3;
// Combine all terrain features
const combined = elevation * 0.6 + hills * 0.3 + plateaus * 0.1;
return Math.floor(this.baseHeight + combined * this.heightVariation);
}
getBlockType(x, y, z, surfaceHeight) {
if (y > surfaceHeight) {
if (y <= this.waterLevel) return 'water';
return 'air';
}
if (y === surfaceHeight) {
if (y <= this.waterLevel) return 'sand';
return 'grass';
}
if (y >= surfaceHeight - 3) {
return 'dirt';
}
return 'stone';
}
shouldPlaceTree(x, z, surfaceHeight) {
if (surfaceHeight <= this.waterLevel) return false;
const treeValue = this.treeNoise.noise2D(x * 0.3, z * 0.3);
return treeValue > 0.7;
}
}
// ============================================
// BLOCK MANAGER
// ============================================
class BlockManager {
constructor(textures) {
this.textures = textures;
this.blocks = new Map();
this.materials = this.createMaterials();
this.blockGeometry = new THREE.BoxGeometry(1, 1, 1);
}
createMaterials() {
return {
grass: [
new THREE.MeshLambertMaterial({ map: this.textures.grassSide }),
new THREE.MeshLambertMaterial({ map: this.textures.grassSide }),
new THREE.MeshLambertMaterial({ map: this.textures.grassTop }),
new THREE.MeshLambertMaterial({ map: this.textures.dirt }),
new THREE.MeshLambertMaterial({ map: this.textures.grassSide }),
new THREE.MeshLambertMaterial({ map: this.textures.grassSide })
],
dirt: new THREE.MeshLambertMaterial({ map: this.textures.dirt }),
stone: new THREE.MeshLambertMaterial({ map: this.textures.stone }),
wood: [
new THREE.MeshLambertMaterial({ map: this.textures.wood }),
new THREE.MeshLambertMaterial({ map: this.textures.wood }),
new THREE.MeshLambertMaterial({ map: this.textures.woodTop }),
new THREE.MeshLambertMaterial({ map: this.textures.woodTop }),
new THREE.MeshLambertMaterial({ map: this.textures.wood }),
new THREE.MeshLambertMaterial({ map: this.textures.wood })
],
leaves: new THREE.MeshLambertMaterial({
map: this.textures.leaves,
transparent: false,
side: THREE.DoubleSide
}),
sand: new THREE.MeshLambertMaterial({ map: this.textures.sand }),
water: new THREE.MeshLambertMaterial({
map: this.textures.water,
transparent: true,
opacity: 0.7
})
};
}
getKey(x, y, z) {
return `${x},${y},${z}`;
}
parseKey(key) {
const parts = key.split(',');
return {
x: parseInt(parts[0]),
y: parseInt(parts[1]),
z: parseInt(parts[2])
};
}
addBlock(x, y, z, type, scene) {
const key = this.getKey(x, y, z);
if (this.blocks.has(key)) return null;
if (type === 'air') return null;
const material = this.materials[type];
const mesh = new THREE.Mesh(this.blockGeometry, material);
mesh.position.set(x, y, z);
mesh.userData.blockType = type;
mesh.userData.blockKey = key;
scene.add(mesh);
this.blocks.set(key, { mesh, type });
return mesh;
}
removeBlock(x, y, z, scene) {
const key = this.getKey(x, y, z);
const block = this.blocks.get(key);
if (block) {
scene.remove(block.mesh);
this.blocks.delete(key);
return true;
}
return false;
}
getBlock(x, y, z) {
return this.blocks.get(this.getKey(x, y, z));
}
hasBlock(x, y, z) {
return this.blocks.has(this.getKey(x, y, z));
}
getBlockCount() {
return this.blocks.size;
}
}
// ============================================
// TREE GENERATOR
// ============================================
class TreeGenerator {
constructor(blockManager, scene) {
this.blockManager = blockManager;
this.scene = scene;
}
generateTree(x, baseY, z) {
const trunkHeight = 4 + Math.floor(Math.random() * 3);
// Generate trunk
for (let y = 0; y < trunkHeight; y++) {
this.blockManager.addBlock(x, baseY + y + 1, z, 'wood', this.scene);
}
// Generate canopy
const canopyBase = baseY + trunkHeight - 1;
const canopyHeight = 3;
for (let dy = 0; dy <= canopyHeight; dy++) {
let radius;
if (dy === 0) radius = 2;
else if (dy === 1) radius = 2;
else if (dy === 2) radius = 1;
else radius = 0;
for (let dx = -radius; dx <= radius; dx++) {
for (let dz = -radius; dz <= radius; dz++) {
// Skip corners for rounder canopy
if (Math.abs(dx) === radius && Math.abs(dz) === radius && radius > 1) {
if (Math.random() > 0.5) continue;
}
// Don't place leaves where trunk is (except top)
if (dx === 0 && dz === 0 && dy < canopyHeight - 1) continue;
const lx = x + dx;
const ly = canopyBase + dy;
const lz = z + dz;
if (!this.blockManager.hasBlock(lx, ly, lz)) {
this.blockManager.addBlock(lx, ly, lz, 'leaves', this.scene);
}
}
}
}
}
}
// ============================================
// WORLD GENERATOR
// ============================================
class WorldGenerator {
constructor(scene, textures, seed = 12345) {
this.scene = scene;
this.blockManager = new BlockManager(textures);
this.terrainGen = new TerrainGenerator(seed);
this.treeGen = new TreeGenerator(this.blockManager, scene);
this.chunkSize = 16;
this.renderDistance = 4;
this.generatedChunks = new Set();
}
generateChunk(chunkX, chunkZ) {
const chunkKey = `${chunkX},${chunkZ}`;
if (this.generatedChunks.has(chunkKey)) return;
this.generatedChunks.add(chunkKey);
const startX = chunkX * this.chunkSize;
const startZ = chunkZ * this.chunkSize;
const treePositions = [];
// First pass: generate terrain
for (let x = 0; x < this.chunkSize; x++) {
for (let z = 0; z < this.chunkSize; z++) {
const worldX = startX + x;
const worldZ = startZ + z;
const surfaceHeight = this.terrainGen.getHeight(worldX, worldZ);
// Generate blocks from bedrock to surface
for (let y = 0; y <= surfaceHeight; y++) {
const blockType = this.terrainGen.getBlockType(worldX, y, worldZ, surfaceHeight);
if (blockType !== 'air' && blockType !== 'water') {
this.blockManager.addBlock(worldX, y, worldZ, blockType, this.scene);
}
}
// Check for tree placement
if (this.terrainGen.shouldPlaceTree(worldX, worldZ, surfaceHeight)) {
treePositions.push({ x: worldX, y: surfaceHeight, z: worldZ });
}
}
}
// Second pass: generate trees
for (const pos of treePositions) {
this.treeGen.generateTree(pos.x, pos.y, pos.z);
}
}
generateAroundPlayer(playerX, playerZ) {
const playerChunkX = Math.floor(playerX / this.chunkSize);
const playerChunkZ = Math.floor(playerZ / this.chunkSize);
for (let dx = -this.renderDistance; dx <= this.renderDistance; dx++) {
for (let dz = -this.renderDistance; dz <= this.renderDistance; dz++) {
this.generateChunk(playerChunkX + dx, playerChunkZ + dz);
}
}
}
getSpawnHeight(x, z) {
return this.terrainGen.getHeight(x, z) + 2;
}
}
// ============================================
// PLAYER CONTROLLER
// ============================================
class PlayerController {
constructor(camera, blockManager, canvas) {
this.camera = camera;
this.blockManager = blockManager;
this.canvas = canvas;
// Player state
this.position = new THREE.Vector3(0, 30, 0);
this.velocity = new THREE.Vector3(0, 0, 0);
this.rotation = { x: 0, y: 0 };
// Movement settings
this.moveSpeed = 5;
this.flySpeed = 10;
this.jumpForce = 8;
this.gravity = 20;
this.friction = 0.9;
// Player dimensions
this.height = 1.7;
this.width = 0.6;
this.eyeHeight = 1.6;
// State flags
this.isFlying = false;
this.isGrounded = false;
this.isCrouching = false;
// Input state
this.keys = {};
this.mouseLocked = false;
this.setupControls();
}
setupControls() {
// Keyboard
document.addEventListener('keydown', (e) => {
this.keys[e.code] = true;
if (e.code === 'KeyF') {
this.isFlying = !this.isFlying;
this.velocity.y = 0;
}
});
document.addEventListener('keyup', (e) => {
this.keys[e.code] = false;
});
// Mouse movement
document.addEventListener('mousemove', (e) => {
if (!this.mouseLocked) return;
const sensitivity = 0.002;
this.rotation.y -= e.movementX * sensitivity;
this.rotation.x -= e.movementY * sensitivity;
// Clamp vertical rotation
this.rotation.x = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.rotation.x));
});
// Pointer lock
this.canvas.addEventListener('click', () => {
if (!this.mouseLocked) {
this.canvas.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
this.mouseLocked = document.pointerLockElement === this.canvas;
});
}
checkCollision(x, y, z) {
const margin = this.width / 2;
const positions = [
[x - margin, y, z - margin],
[x + margin, y, z - margin],
[x - margin, y, z + margin],
[x + margin, y, z + margin],
[x - margin, y + this.height / 2, z - margin],
[x + margin, y + this.height / 2, z - margin],
[x - margin, y + this.height / 2, z + margin],
[x + margin, y + this.height / 2, z + margin],
[x - margin, y + this.height, z - margin],
[x + margin, y + this.height, z - margin],
[x - margin, y + this.height, z + margin],
[x + margin, y + this.height, z + margin]
];
for (const [px, py, pz] of positions) {
const block = this.blockManager.getBlock(
Math.floor(px),
Math.floor(py),
Math.floor(pz)
);
if (block && block.type !== 'water') {
return true;
}
}
return false;
}
update(deltaTime) {
// Get movement direction
const forward = new THREE.Vector3(0, 0, -1);
const right = new THREE.Vector3(1, 0, 0);
forward.applyAxisAngle(new THREE.Vector3(0, 1, 0), this.rotation.y);
right.applyAxisAngle(new THREE.Vector3(0, 1, 0), this.rotation.y);
if (!this.isFlying) {
forward.y = 0;
right.y = 0;
forward.normalize();
right.normalize();
}
// Calculate movement
const moveDir = new THREE.Vector3(0, 0, 0);
const speed = this.isFlying ? this.flySpeed : this.moveSpeed;
if (this.keys['KeyW']) moveDir.add(forward);
if (this.keys['KeyS']) moveDir.sub(forward);
if (this.keys['KeyA']) moveDir.sub(right);
if (this.keys['KeyD']) moveDir.add(right);
if (moveDir.length() > 0) {
moveDir.normalize().multiplyScalar(speed);
}
// Apply movement
this.velocity.x = moveDir.x;
this.velocity.z = moveDir.z;
// Vertical movement
if (this.isFlying) {
this.velocity.y = 0;
if (this.keys['Space']) this.velocity.y = this.flySpeed;
if (this.keys['ShiftLeft'] || this.keys['ShiftRight']) this.velocity.y = -this.flySpeed;
} else {
// Gravity
this.velocity.y -= this.gravity * deltaTime;
// Jump
if (this.keys['Space'] && this.isGrounded) {
this.velocity.y = this.jumpForce;
this.isGrounded = false;
}
// Crouch
this.isCrouching = this.keys['ShiftLeft'] || this.keys['ShiftRight'];
}
// Apply velocity with collision
const newX = this.position.x + this.velocity.x * deltaTime;
const newY = this.position.y + this.velocity.y * deltaTime;
const newZ = this.position.z + this.velocity.z * deltaTime;
// X collision
if (!this.checkCollision(newX, this.position.y, this.position.z)) {
this.position.x = newX;
} else {
this.velocity.x = 0;
}
// Z collision
if (!this.checkCollision(this.position.x, this.position.y, newZ)) {
this.position.z = newZ;
} else {
this.velocity.z = 0;
}
// Y collision
if (!this.checkCollision(this.position.x, newY, this.position.z)) {
this.position.y = newY;
this.isGrounded = false;
} else {
if (this.velocity.y < 0) {
this.isGrounded = true;
}
this.velocity.y = 0;
}
// Prevent falling through world
if (this.position.y < -10) {
this.position.y = 50;
this.velocity.y = 0;
}
// Update camera
this.camera.position.copy(this.position);
this.camera.position.y += this.eyeHeight;
this.camera.rotation.order = 'YXZ';
this.camera.rotation.x = this.rotation.x;
this.camera.rotation.y = this.rotation.y;
}
}
// ============================================
// BLOCK INTERACTION
// ============================================
class BlockInteraction {
constructor(camera, blockManager, scene) {
this.camera = camera;
this.blockManager = blockManager;
this.scene = scene;
this.raycaster = new THREE.Raycaster();
this.maxDistance = 5;
this.selectedBlockType = 'grass';
this.blockTypes = ['grass', 'dirt', 'stone', 'wood', 'leaves'];
this.selectedIndex = 0;
this.setupControls();
}
setupControls() {
document.addEventListener('mousedown', (e) => {
if (document.pointerLockElement) {
if (e.button === 0) this.breakBlock();
if (e.button === 2) this.placeBlock();
}
});
document.addEventListener('contextmenu', (e) => e.preventDefault());
// Number keys for block selection
document.addEventListener('keydown', (e) => {
const num = parseInt(e.key);
if (num >= 1 && num <= 5) {
this.selectedIndex = num - 1;
this.selectedBlockType = this.blockTypes[this.selectedIndex];
this.updateHotbar();
}
});
// Scroll wheel for block selection
document.addEventListener('wheel', (e) => {
if (document.pointerLockElement) {
if (e.deltaY > 0) {
this.selectedIndex = (this.selectedIndex + 1) % this.blockTypes.length;
} else {
this.selectedIndex = (this.selectedIndex - 1 + this.blockTypes.length) % this.blockTypes.length;
}
this.selectedBlockType = this.blockTypes[this.selectedIndex];
this.updateHotbar();
}
});
}
updateHotbar() {
const slots = document.querySelectorAll('.hotbar-slot');
slots.forEach((slot, i) => {
slot.classList.toggle('selected', i === this.selectedIndex);
});
}
getTargetBlock() {
this.raycaster.setFromCamera(new THREE.Vector2(0, 0), this.camera);
const meshes = [];
this.blockManager.blocks.forEach(block => {
if (block.type !== 'water') {
meshes.push(block.mesh);
}
});
const intersects = this.raycaster.intersectObjects(meshes);
if (intersects.length > 0 && intersects[0].distance <= this.maxDistance) {
return intersects[0];
}
return null;
}
breakBlock() {
const target = this.getTargetBlock();
if (target) {
const pos = target.object.position;
this.blockManager.removeBlock(pos.x, pos.y, pos.z, this.scene);
}
}
placeBlock() {
const target = this.getTargetBlock();
if (target) {
const normal = target.face.normal;
const pos = target.object.position;
const newX = pos.x + normal.x;
const newY = pos.y + normal.y;
const newZ = pos.z + normal.z;
// Don't place block inside player
const playerPos = this.camera.position;
const dx = Math.abs(newX - playerPos.x);
const dy = Math.abs(newY - playerPos.y + 0.9);
const dz = Math.abs(newZ - playerPos.z);
if (dx < 0.8 && dy < 1.8 && dz < 0.8) return;
this.blockManager.addBlock(newX, newY, newZ, this.selectedBlockType, this.scene);
}
}
}
// ============================================
// ATMOSPHERE SYSTEM
// ============================================
class AtmosphereSystem {
constructor(scene) {
this.scene = scene;
this.dayDuration = 300; // seconds for full day cycle
this.timeOfDay = 0.25; // Start at noon (0 = midnight, 0.5 = noon)
this.sunLight = null;
this.ambientLight = null;
this.setupLighting();
this.setupSkybox();
}
setupLighting() {
// Ambient light
this.ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
this.scene.add(this.ambientLight);
// Sun light
this.sunLight = new THREE.DirectionalLight(0xffffff, 1);
this.sunLight.position.set(50, 100, 50);
this.sunLight.castShadow = false;
this.scene.add(this.sunLight);
// Hemisphere light for better ambient
const hemiLight = new THREE.HemisphereLight(0x87ceeb, 0x8b7355, 0.3);
this.scene.add(hemiLight);
}
setupSkybox() {
// Create gradient skybox
const vertexShader = `
varying vec3 vWorldPosition;
void main() {
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform float offset;
uniform float exponent;
varying vec3 vWorldPosition;
void main() {
float h = normalize(vWorldPosition + offset).y;
gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
}
`;
this.skyUniforms = {
topColor: { value: new THREE.Color(0x0077ff) },
bottomColor: { value: new THREE.Color(0xaaddff) },
offset: { value: 33 },
exponent: { value: 0.6 }
};
const skyGeo = new THREE.SphereGeometry(500, 32, 15);
const skyMat = new THREE.ShaderMaterial({
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: this.skyUniforms,
side: THREE.BackSide
});
const sky = new THREE.Mesh(skyGeo, skyMat);
this.scene.add(sky);
// Fog for depth
this.scene.fog = new THREE.Fog(0xaaddff, 50, 150);
}
update(deltaTime) {
// Update time
this.timeOfDay += deltaTime / this.dayDuration;
if (this.timeOfDay >= 1) this.timeOfDay -= 1;
// Calculate sun position
const sunAngle = this.timeOfDay * Math.PI * 2 - Math.PI / 2;
const sunHeight = Math.sin(sunAngle);
const sunDistance = 100;
this.sunLight.position.set(
Math.cos(sunAngle) * sunDistance,
sunHeight * sunDistance,
sunDistance * 0.5
);
// Day/night colors
const isDaytime = sunHeight > -0.1;
const dayProgress = Math.max(0, Math.min(1, (sunHeight + 0.1) / 1.1));
// Sky colors
const dayTopColor = new THREE.Color(0x0077ff);
const dayBottomColor = new THREE.Color(0xaaddff);
const nightTopColor = new THREE.Color(0x000022);
const nightBottomColor = new THREE.Color(0x001133);
const sunsetTopColor = new THREE.Color(0xff6600);
const sunsetBottomColor = new THREE.Color(0xff9933);
let topColor, bottomColor;
if (dayProgress > 0.3) {
// Day
topColor = dayTopColor;
bottomColor = dayBottomColor;
} else if (dayProgress > 0.1) {
// Sunset/sunrise
const t = (dayProgress - 0.1) / 0.2;
topColor = sunsetTopColor.clone().lerp(dayTopColor, t);
bottomColor = sunsetBottomColor.clone().lerp(dayBottomColor, t);
} else {
// Night to sunset
const t = dayProgress / 0.1;
topColor = nightTopColor.clone().lerp(sunsetTopColor, t);
bottomColor = nightBottomColor.clone().lerp(sunsetBottomColor, t);
}
this.skyUniforms.topColor.value.copy(topColor);
this.skyUniforms.bottomColor.value.copy(bottomColor);
// Lighting intensity
const lightIntensity = Math.max(0.1, dayProgress);
this.sunLight.intensity = lightIntensity;
this.ambientLight.intensity = 0.2 + lightIntensity * 0.4;
// Sun color
if (dayProgress < 0.3) {
this.sunLight.color.setHex(0xffaa66);
} else {
this.sunLight.color.setHex(0xffffff);
}
// Fog color
this.scene.fog.color.copy(bottomColor);
// Update UI
this.updateTimeDisplay();
}
updateTimeDisplay() {
const hours = Math.floor(this.timeOfDay * 24);
const minutes = Math.floor((this.timeOfDay * 24 - hours) * 60);
const timeStr = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
document.getElementById('time-text').textContent = timeStr;
// Update icon
const icon = document.getElementById('time-icon');
if (hours >= 6 && hours < 18) {
icon.textContent = '☀️';
} else {
icon.textContent = '🌙';
}
}
}
// ============================================
// MAIN GAME CLASS
// ============================================
class MinecraftGame {
constructor() {
this.scene = null;
this.camera = null;
this.renderer = null;
this.clock = new THREE.Clock();
this.textureGen = new TextureGenerator();
this.worldGen = null;
this.player = null;
this.interaction = null;
this.atmosphere = null;
this.lastFrameTime = performance.now();
this.frameCount = 0;
this.fps = 0;
}
async init() {
// Setup Three.js
this.setupRenderer();
this.setupCamera();
this.setupScene();
// Generate textures
const textures = this.textureGen.generateAllTextures();
// Setup world
this.worldGen = new WorldGenerator(this.scene, textures);
// Generate initial world
this.worldGen.generateAroundPlayer(0, 0);
// Setup player
const spawnY = this.worldGen.getSpawnHeight(0, 0);
this.player = new PlayerController(
this.camera,
this.worldGen.blockManager,
this.renderer.domElement
);
this.player.position.set(0, spawnY, 0);
// Setup interaction
this.interaction = new BlockInteraction(
this.camera,
this.worldGen.blockManager,
this.scene
);
// Setup atmosphere
this.atmosphere = new AtmosphereSystem(this.scene);
// Setup hotbar
this.setupHotbar();
// Start game loop
this.animate();
}
setupRenderer() {
const canvas = document.getElementById('game-canvas');
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
});
}
setupCamera() {
this.camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
500
);
}
setupScene() {
this.scene = new THREE.Scene();
}
setupHotbar() {
const hotbar = document.getElementById('hotbar');
const blockTypes = ['grass', 'dirt', 'stone', 'wood', 'leaves'];
blockTypes.forEach((type, i) => {
const slot = document.createElement('div');
slot.className = 'hotbar-slot' + (i === 0 ? ' selected' : '');
const canvas = this.textureGen.getHotbarCanvas(type);
slot.appendChild(canvas);
const num = document.createElement('span');
num.className = 'slot-number';
num.textContent = i + 1;
slot.appendChild(num);
hotbar.appendChild(slot);
});
}
updateStats() {
// FPS calculation
this.frameCount++;
const currentTime = performance.now();
if (currentTime - this.lastFrameTime >= 1000) {
this.fps = this.frameCount;
this.frameCount = 0;
this.lastFrameTime = currentTime;
document.getElementById('fps').textContent = this.fps;
}
// Position
const pos = this.player.position;
document.getElementById('position').textContent =
`${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)}`;
// Block count
document.getElementById('block-count').textContent =
this.worldGen.blockManager.getBlockCount();
// Fly mode
document.getElementById('fly-mode').textContent =
this.player.isFlying ? 'Flying' : 'Walking';
}
animate() {
requestAnimationFrame(() => this.animate());
const deltaTime = Math.min(this.clock.getDelta(), 0.1);
// Update systems
this.player.update(deltaTime);
this.atmosphere.update(deltaTime);
// Generate world around player
this.worldGen.generateAroundPlayer(
this.player.position.x,
this.player.position.z
);
// Update stats
this.updateStats();
// Render
this.renderer.render(this.scene, this.camera);
}
}
// ============================================
// GAME INITIALIZATION
// ============================================
const game = new MinecraftGame();
document.getElementById('start-btn').addEventListener('click', async () => {
document.getElementById('start-screen').style.display = 'none';
document.getElementById('loading').style.display = 'block';
// Small delay to show loading
await new Promise(resolve => setTimeout(resolve, 100));
await game.init();
document.getElementById('loading').style.display = 'none';
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Minecraft Clone</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #000;
}
#game-canvas {
display: block;
width: 100vw;
height: 100vh;
}
#crosshair {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
pointer-events: none;
z-index: 100;
}
#crosshair::before,
#crosshair::after {
content: '';
position: absolute;
background: white;
mix-blend-mode: difference;
}
#crosshair::before {
width: 2px;
height: 20px;
left: 50%;
transform: translateX(-50%);
}
#crosshair::after {
width: 20px;
height: 2px;
top: 50%;
transform: translateY(-50%);
}
#ui-container {
position: fixed;
top: 10px;
left: 10px;
z-index: 100;
color: white;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
}
#controls-panel {
background: rgba(0, 0, 0, 0.7);
padding: 15px;
border-radius: 8px;
font-size: 12px;
max-width: 250px;
backdrop-filter: blur(5px);
}
#controls-panel h3 {
margin-bottom: 10px;
color: #7fff7f;
font-size: 14px;
}
#controls-panel p {
margin: 4px 0;
color: #ddd;
}
#controls-panel .key {
display: inline-block;
background: rgba(255,255,255,0.2);
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
margin-right: 5px;
}
#stats-panel {
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
padding: 10px 15px;
border-radius: 8px;
color: white;
font-size: 12px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
backdrop-filter: blur(5px);
}
#stats-panel div {
margin: 3px 0;
}
#hotbar {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 4px;
z-index: 100;
}
.hotbar-slot {
width: 50px;
height: 50px;
background: rgba(0, 0, 0, 0.6);
border: 3px solid rgba(100, 100, 100, 0.8);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.hotbar-slot.selected {
border-color: #fff;
box-shadow: 0 0 10px rgba(255,255,255,0.5);
}
.hotbar-slot canvas {
width: 40px;
height: 40px;
image-rendering: pixelated;
}
.hotbar-slot .slot-number {
position: absolute;
top: 2px;
left: 4px;
font-size: 10px;
color: white;
text-shadow: 1px 1px 2px black;
}
#start-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1000;
color: white;
}
#start-screen h1 {
font-size: 48px;
margin-bottom: 20px;
text-shadow: 4px 4px 8px rgba(0,0,0,0.5);
color: #7fff7f;
}
#start-screen p {
font-size: 18px;
margin-bottom: 30px;
color: #ccc;
}
#start-btn {
padding: 15px 40px;
font-size: 20px;
background: linear-gradient(135deg, #4a7c59 0%, #3d6b4f 100%);
border: none;
color: white;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
#start-btn:hover {
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(0,0,0,0.4);
}
#loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 24px;
z-index: 999;
display: none;
}
#time-display {
position: fixed;
top: 10px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.5);
padding: 5px 15px;
border-radius: 15px;
color: white;
font-size: 14px;
z-index: 100;
}
</style>
</head>
<body>
<div id="start-screen">
<h1>Minecraft Clone</h1>
<p>A 3D voxel world built with Three.js</p>
<button id="start-btn">Click to Play</button>
</div>
<div id="loading">Generating World...</div>
<canvas id="game-canvas"></canvas>
<div id="crosshair"></div>
<div id="ui-container">
<div id="controls-panel">
<h3>Controls</h3>
<p><span class="key">W A S D</span> Move</p>
<p><span class="key">Mouse</span> Look around</p>
<p><span class="key">Space</span> Jump / Fly up</p>
<p><span class="key">Shift</span> Crouch / Fly down</p>
<p><span class="key">F</span> Toggle fly mode</p>
<p><span class="key">Left Click</span> Break block</p>
<p><span class="key">Right Click</span> Place block</p>
<p><span class="key">1-5</span> Select block</p>
<p><span class="key">Scroll</span> Change block</p>
<p><span class="key">ESC</span> Release mouse</p>
</div>
</div>
<div id="stats-panel">
<div>FPS: <span id="fps">0</span></div>
<div>Position: <span id="position">0, 0, 0</span></div>
<div>Chunks: <span id="chunk-count">0</span></div>
<div>Mode: <span id="fly-mode">Walking</span></div>
</div>
<div id="time-display">
<span id="time-icon">☀️</span> <span id="time-text">12:00</span>
</div>
<div id="hotbar"></div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// ============================================
// CONSTANTS
// ============================================
const CHUNK_SIZE = 16;
const CHUNK_HEIGHT = 64;
const RENDER_DISTANCE = 5;
const WATER_LEVEL = 15;
const BLOCK_TYPES = {
AIR: 0,
GRASS: 1,
DIRT: 2,
STONE: 3,
WOOD: 4,
LEAVES: 5,
SAND: 6,
WATER: 7
};
const BLOCK_NAMES = ['air', 'grass', 'dirt', 'stone', 'wood', 'leaves', 'sand', 'water'];
// ============================================
// TEXTURE ATLAS GENERATOR
// ============================================
class TextureAtlas {
constructor() {
this.tileSize = 16;
this.tilesPerRow = 8;
this.canvas = document.createElement('canvas');
this.canvas.width = this.tileSize * this.tilesPerRow;
this.canvas.height = this.tileSize * this.tilesPerRow;
this.ctx = this.canvas.getContext('2d');
this.texture = null;
// UV mapping: [top, bottom, sides] for each block type
// Index in atlas: row * tilesPerRow + col
this.uvMap = {};
}
seededRandom(seed) {
const x = Math.sin(seed) * 10000;
return x - Math.floor(x);
}
drawTile(col, row, drawFunc) {
const x = col * this.tileSize;
const y = row * this.tileSize;
this.ctx.save();
this.ctx.translate(x, y);
drawFunc(this.ctx, this.tileSize);
this.ctx.restore();
return row * this.tilesPerRow + col;
}
generateGrassTop(ctx, size) {
ctx.fillStyle = '#5d9b3a';
ctx.fillRect(0, 0, size, size);
for (let i = 0; i < 60; i++) {
const x = Math.floor(this.seededRandom(i * 3) * size);
const y = Math.floor(this.seededRandom(i * 5) * size);
const shade = Math.floor(this.seededRandom(i * 7) * 40) - 20;
ctx.fillStyle = `rgb(${93 + shade/2}, ${155 + shade}, ${58 + shade/3})`;
ctx.fillRect(x, y, 1, 1);
}
}
generateGrassSide(ctx, size) {
ctx.fillStyle = '#8b6b4a';
ctx.fillRect(0, 0, size, size);
for (let i = 0; i < 40; i++) {
const x = Math.floor(this.seededRandom(i * 17) * size);
const y = Math.floor(this.seededRandom(i * 19) * size);
const shade = Math.floor(this.seededRandom(i * 23) * 30) - 15;
ctx.fillStyle = `rgb(${139 + shade}, ${107 + shade}, ${74 + shade})`;
ctx.fillRect(x, y, 2, 2);
}
ctx.fillStyle = '#5d9b3a';
ctx.fillRect(0, 0, size, 3);
for (let x = 0; x < size; x++) {
const hang = Math.floor(this.seededRandom(x * 29) * 3);
ctx.fillRect(x, 3, 1, hang);
}
}
generateDirt(ctx, size) {
ctx.fillStyle = '#8b6b4a';
ctx.fillRect(0, 0, size, size);
for (let i = 0; i < 80; i++) {
const x = Math.floor(this.seededRandom(i * 31) * size);
const y = Math.floor(this.seededRandom(i * 37) * size);
const shade = Math.floor(this.seededRandom(i * 41) * 40) - 20;
ctx.fillStyle = `rgb(${139 + shade}, ${107 + shade}, ${74 + shade})`;
ctx.fillRect(x, y, 2, 2);
}
}
generateStone(ctx, size) {
ctx.fillStyle = '#8a8a8a';
ctx.fillRect(0, 0, size, size);
for (let i = 0; i < 100; i++) {
const x = Math.floor(this.seededRandom(i * 53) * size);
const y = Math.floor(this.seededRandom(i * 59) * size);
const shade = Math.floor(this.seededRandom(i * 61) * 50) - 25;
const gray = Math.min(255, Math.max(0, 138 + shade));
ctx.fillStyle = `rgb(${gray}, ${gray}, ${gray})`;
ctx.fillRect(x, y, 1, 1);
}
}
generateWood(ctx, size) {
ctx.fillStyle = '#6b5030';
ctx.fillRect(0, 0, size, size);
for (let y = 0; y < size; y += 2) {
const shade = Math.floor(this.seededRandom(y * 83) * 20) - 10;
ctx.fillStyle = `rgb(${107 + shade}, ${80 + shade}, ${48 + shade})`;
ctx.fillRect(0, y, size, 1);
}
}
generateWoodTop(ctx, size) {
ctx.fillStyle = '#9b7050';
ctx.fillRect(0, 0, size, size);
const cx = size/2, cy = size/2;
for (let r = 2; r < size/2; r += 2) {
ctx.strokeStyle = r % 4 === 0 ? '#6b5030' : '#8b6040';
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.stroke();
}
}
generateLeaves(ctx, size) {
ctx.fillStyle = '#3c8c3c';
ctx.fillRect(0, 0, size, size);
for (let i = 0; i < 40; i++) {
const x = Math.floor(this.seededRandom(i * 101) * size);
const y = Math.floor(this.seededRandom(i * 103) * size);
const shade = Math.floor(this.seededRandom(i * 107) * 40);
ctx.fillStyle = `rgb(${50 + shade}, ${130 + shade/2}, ${50 + shade/2})`;
ctx.fillRect(x, y, 2, 2);
}
}
generateSand(ctx, size) {
ctx.fillStyle = '#e6d5a8';
ctx.fillRect(0, 0, size, size);
for (let i = 0; i < 100; i++) {
const x = Math.floor(this.seededRandom(i * 127) * size);
const y = Math.floor(this.seededRandom(i * 131) * size);
const shade = Math.floor(this.seededRandom(i * 137) * 30) - 15;
ctx.fillStyle = `rgb(${230 + shade}, ${213 + shade}, ${168 + shade})`;
ctx.fillRect(x, y, 1, 1);
}
}
generateWater(ctx, size) {
ctx.fillStyle = '#3070c0';
ctx.fillRect(0, 0, size, size);
for (let i = 0; i < 20; i++) {
const x = Math.floor(this.seededRandom(i * 139) * size);
const y = Math.floor(this.seededRandom(i * 149) * size);
ctx.fillStyle = '#5090e0';
ctx.fillRect(x, y, 3, 1);
}
}
generate() {
// Row 0: Grass top, grass side, dirt, stone
const grassTop = this.drawTile(0, 0, (ctx, s) => this.generateGrassTop(ctx, s));
const grassSide = this.drawTile(1, 0, (ctx, s) => this.generateGrassSide(ctx, s));
const dirt = this.drawTile(2, 0, (ctx, s) => this.generateDirt(ctx, s));
const stone = this.drawTile(3, 0, (ctx, s) => this.generateStone(ctx, s));
// Row 1: Wood side, wood top, leaves, sand
const woodSide = this.drawTile(0, 1, (ctx, s) => this.generateWood(ctx, s));
const woodTop = this.drawTile(1, 1, (ctx, s) => this.generateWoodTop(ctx, s));
const leaves = this.drawTile(2, 1, (ctx, s) => this.generateLeaves(ctx, s));
const sand = this.drawTile(3, 1, (ctx, s) => this.generateSand(ctx, s));
// Row 2: Water
const water = this.drawTile(0, 2, (ctx, s) => this.generateWater(ctx, s));
// UV map: [+X, -X, +Y (top), -Y (bottom), +Z, -Z]
this.uvMap = {
[BLOCK_TYPES.GRASS]: [grassSide, grassSide, grassTop, dirt, grassSide, grassSide],
[BLOCK_TYPES.DIRT]: [dirt, dirt, dirt, dirt, dirt, dirt],
[BLOCK_TYPES.STONE]: [stone, stone, stone, stone, stone, stone],
[BLOCK_TYPES.WOOD]: [woodSide, woodSide, woodTop, woodTop, woodSide, woodSide],
[BLOCK_TYPES.LEAVES]: [leaves, leaves, leaves, leaves, leaves, leaves],
[BLOCK_TYPES.SAND]: [sand, sand, sand, sand, sand, sand],
[BLOCK_TYPES.WATER]: [water, water, water, water, water, water]
};
this.texture = new THREE.CanvasTexture(this.canvas);
this.texture.magFilter = THREE.NearestFilter;
this.texture.minFilter = THREE.NearestFilter;
this.texture.colorSpace = THREE.SRGBColorSpace;
return this.texture;
}
getUV(tileIndex) {
const col = tileIndex % this.tilesPerRow;
const row = Math.floor(tileIndex / this.tilesPerRow);
const size = 1 / this.tilesPerRow;
return {
u: col * size,
v: 1 - (row + 1) * size,
size: size
};
}
getHotbarCanvas(blockType) {
const canvas = document.createElement('canvas');
canvas.width = 16;
canvas.height = 16;
const ctx = canvas.getContext('2d');
switch(blockType) {
case 'grass': this.generateGrassTop(ctx, 16); break;
case 'dirt': this.generateDirt(ctx, 16); break;
case 'stone': this.generateStone(ctx, 16); break;
case 'wood': this.generateWood(ctx, 16); break;
case 'leaves': this.generateLeaves(ctx, 16); break;
}
return canvas;
}
}
// ============================================
// SIMPLEX NOISE (Optimized)
// ============================================
class SimplexNoise {
constructor(seed = Math.random()) {
this.p = new Uint8Array(512);
this.perm = new Uint8Array(512);
const p = new Uint8Array(256);
for (let i = 0; i < 256; i++) p[i] = i;
// Fisher-Yates shuffle with seed
let s = seed * 2147483647;
for (let i = 255; i > 0; i--) {
s = (s * 16807) % 2147483647;
const j = s % (i + 1);
[p[i], p[j]] = [p[j], p[i]];
}
for (let i = 0; i < 512; i++) {
this.perm[i] = p[i & 255];
}
this.F2 = 0.5 * (Math.sqrt(3) - 1);
this.G2 = (3 - Math.sqrt(3)) / 6;
this.grad3 = new Float32Array([
1,1,0, -1,1,0, 1,-1,0, -1,-1,0,
1,0,1, -1,0,1, 1,0,-1, -1,0,-1,
0,1,1, 0,-1,1, 0,1,-1, 0,-1,-1
]);
}
noise2D(xin, yin) {
const F2 = this.F2, G2 = this.G2;
const perm = this.perm, grad3 = this.grad3;
const s = (xin + yin) * F2;
const i = Math.floor(xin + s);
const j = Math.floor(yin + s);
const t = (i + j) * G2;
const x0 = xin - i + t;
const y0 = yin - j + t;
const i1 = x0 > y0 ? 1 : 0;
const j1 = x0 > y0 ? 0 : 1;
const x1 = x0 - i1 + G2;
const y1 = y0 - j1 + G2;
const x2 = x0 - 1 + 2 * G2;
const y2 = y0 - 1 + 2 * G2;
const ii = i & 255;
const jj = j & 255;
let n0 = 0, n1 = 0, n2 = 0;
let t0 = 0.5 - x0*x0 - y0*y0;
if (t0 > 0) {
const gi0 = (perm[ii + perm[jj]] % 12) * 3;
t0 *= t0;
n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0+1] * y0);
}
let t1 = 0.5 - x1*x1 - y1*y1;
if (t1 > 0) {
const gi1 = (perm[ii + i1 + perm[jj + j1]] % 12) * 3;
t1 *= t1;
n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1+1] * y1);
}
let t2 = 0.5 - x2*x2 - y2*y2;
if (t2 > 0) {
const gi2 = (perm[ii + 1 + perm[jj + 1]] % 12) * 3;
t2 *= t2;
n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2+1] * y2);
}
return 70 * (n0 + n1 + n2);
}
octaveNoise(x, y, octaves, persistence, lacunarity, scale) {
let total = 0, frequency = 1, amplitude = 1, maxValue = 0;
for (let i = 0; i < octaves; i++) {
total += this.noise2D(x * frequency / scale, y * frequency / scale) * amplitude;
maxValue += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
return total / maxValue;
}
}
// ============================================
// GREEDY MESHING CHUNK
// ============================================
class Chunk {
constructor(chunkX, chunkZ, noise, treeNoise, textureAtlas) {
this.chunkX = chunkX;
this.chunkZ = chunkZ;
this.worldX = chunkX * CHUNK_SIZE;
this.worldZ = chunkZ * CHUNK_SIZE;
this.noise = noise;
this.treeNoise = treeNoise;
this.textureAtlas = textureAtlas;
// Block data stored in flat array for cache efficiency
this.blocks = new Uint8Array(CHUNK_SIZE * CHUNK_HEIGHT * CHUNK_SIZE);
this.heightMap = new Uint8Array(CHUNK_SIZE * CHUNK_SIZE);
this.mesh = null;
this.waterMesh = null;
this.isDirty = true;
this.isGenerated = false;
}
getBlockIndex(x, y, z) {
return y * CHUNK_SIZE * CHUNK_SIZE + z * CHUNK_SIZE + x;
}
getBlock(x, y, z) {
if (x < 0 || x >= CHUNK_SIZE || y < 0 || y >= CHUNK_HEIGHT || z < 0 || z >= CHUNK_SIZE) {
return BLOCK_TYPES.AIR;
}
return this.blocks[this.getBlockIndex(x, y, z)];
}
setBlock(x, y, z, type) {
if (x < 0 || x >= CHUNK_SIZE || y < 0 || y >= CHUNK_HEIGHT || z < 0 || z >= CHUNK_SIZE) {
return false;
}
this.blocks[this.getBlockIndex(x, y, z)] = type;
this.isDirty = true;
return true;
}
getHeight(localX, localZ) {
const worldX = this.worldX + localX;
const worldZ = this.worldZ + localZ;
const elevation = this.noise.octaveNoise(worldX, worldZ, 4, 0.5, 2, 80);
const hills = this.noise.octaveNoise(worldX + 1000, worldZ + 1000, 2, 0.5, 2, 40) * 0.5;
const combined = elevation * 0.7 + hills * 0.3;
return Math.floor(20 + combined * 15);
}
generate() {
if (this.isGenerated) return;
const treePositions = [];
// Generate terrain
for (let x = 0; x < CHUNK_SIZE; x++) {
for (let z = 0; z < CHUNK_SIZE; z++) {
const height = this.getHeight(x, z);
this.heightMap[z * CHUNK_SIZE + x] = height;
for (let y = 0; y <= height; y++) {
let blockType;
if (y === height) {
blockType = height <= WATER_LEVEL ? BLOCK_TYPES.SAND : BLOCK_TYPES.GRASS;
} else if (y >= height - 3) {
blockType = BLOCK_TYPES.DIRT;
} else {
blockType = BLOCK_TYPES.STONE;
}
this.blocks[this.getBlockIndex(x, y, z)] = blockType;
}
// Check for tree
if (height > WATER_LEVEL) {
const worldX = this.worldX + x;
const worldZ = this.worldZ + z;
const treeVal = this.treeNoise.noise2D(worldX * 0.3, worldZ * 0.3);
if (treeVal > 0.7 && x > 2 && x < CHUNK_SIZE - 3 && z > 2 && z < CHUNK_SIZE - 3) {
treePositions.push({ x, y: height, z });
}
}
}
}
// Generate trees
for (const pos of treePositions) {
this.generateTree(pos.x, pos.y, pos.z);
}
this.isGenerated = true;
this.isDirty = true;
}
generateTree(x, baseY, z) {
const trunkHeight = 4 + Math.floor(Math.random() * 3);
// Trunk
for (let y = 1; y <= trunkHeight; y++) {
if (baseY + y < CHUNK_HEIGHT) {
this.blocks[this.getBlockIndex(x, baseY + y, z)] = BLOCK_TYPES.WOOD;
}
}
// Canopy
const canopyBase = baseY + trunkHeight - 1;
for (let dy = 0; dy <= 3; dy++) {
const radius = dy < 2 ? 2 : (dy === 2 ? 1 : 0);
for (let dx = -radius; dx <= radius; dx++) {
for (let dz = -radius; dz <= radius; dz++) {
if (Math.abs(dx) === radius && Math.abs(dz) === radius && radius > 1) {
if (Math.random() > 0.5) continue;
}
if (dx === 0 && dz === 0 && dy < 2) continue;
const lx = x + dx, ly = canopyBase + dy, lz = z + dz;
if (lx >= 0 && lx < CHUNK_SIZE && ly < CHUNK_HEIGHT && lz >= 0 && lz < CHUNK_SIZE) {
if (this.blocks[this.getBlockIndex(lx, ly, lz)] === BLOCK_TYPES.AIR) {
this.blocks[this.getBlockIndex(lx, ly, lz)] = BLOCK_TYPES.LEAVES;
}
}
}
}
}
}
buildMesh(scene, material, waterMaterial) {
if (!this.isDirty) return;
// Remove old meshes
if (this.mesh) {
scene.remove(this.mesh);
this.mesh.geometry.dispose();
}
if (this.waterMesh) {
scene.remove(this.waterMesh);
this.waterMesh.geometry.dispose();
}
const positions = [];
const normals = [];
const uvs = [];
const indices = [];
const waterPositions = [];
const waterNormals = [];
const waterUvs = [];
const waterIndices = [];
let vertexCount = 0;
let waterVertexCount = 0;
// Face data: [dx, dy, dz, nx, ny, nz, vertices...]
const faces = [
{ dir: [1, 0, 0], corners: [[1,0,0], [1,1,0], [1,1,1], [1,0,1]] }, // +X
{ dir: [-1, 0, 0], corners: [[0,0,1], [0,1,1], [0,1,0], [0,0,0]] }, // -X
{ dir: [0, 1, 0], corners: [[0,1,1], [1,1,1], [1,1,0], [0,1,0]] }, // +Y
{ dir: [0, -1, 0], corners: [[0,0,0], [1,0,0], [1,0,1], [0,0,1]] }, // -Y
{ dir: [0, 0, 1], corners: [[0,0,1], [1,0,1], [1,1,1], [0,1,1]] }, // +Z
{ dir: [0, 0, -1], corners: [[1,0,0], [0,0,0], [0,1,0], [1,1,0]] } // -Z
];
for (let y = 0; y < CHUNK_HEIGHT; y++) {
for (let z = 0; z < CHUNK_SIZE; z++) {
for (let x = 0; x < CHUNK_SIZE; x++) {
const block = this.getBlock(x, y, z);
if (block === BLOCK_TYPES.AIR) continue;
const isWater = block === BLOCK_TYPES.WATER;
const targetPositions = isWater ? waterPositions : positions;
const targetNormals = isWater ? waterNormals : normals;
const targetUvs = isWater ? waterUvs : uvs;
const targetIndices = isWater ? waterIndices : indices;
let targetVertexCount = isWater ? waterVertexCount : vertexCount;
for (let faceIdx = 0; faceIdx < 6; faceIdx++) {
const face = faces[faceIdx];
const nx = x + face.dir[0];
const ny = y + face.dir[1];
const nz = z + face.dir[2];
const neighbor = this.getBlock(nx, ny, nz);
// Skip face if neighbor is solid (or same type for water)
if (neighbor !== BLOCK_TYPES.AIR) {
if (isWater && neighbor === BLOCK_TYPES.WATER) continue;
if (!isWater && neighbor !== BLOCK_TYPES.WATER) continue;
}
// Get UV for this face
const tileIndex = this.textureAtlas.uvMap[block][faceIdx];
const uv = this.textureAtlas.getUV(tileIndex);
// Add vertices
for (const corner of face.corners) {
targetPositions.push(
this.worldX + x + corner[0],
y + corner[1],
this.worldZ + z + corner[2]
);
targetNormals.push(face.dir[0], face.dir[1], face.dir[2]);
}
// Add UVs
targetUvs.push(
uv.u, uv.v + uv.size,
uv.u + uv.size, uv.v + uv.size,
uv.u + uv.size, uv.v,
uv.u, uv.v
);
// Add indices
targetIndices.push(
targetVertexCount, targetVertexCount + 1, targetVertexCount + 2,
targetVertexCount, targetVertexCount + 2, targetVertexCount + 3
);
targetVertexCount += 4;
if (isWater) {
waterVertexCount = targetVertexCount;
} else {
vertexCount = targetVertexCount;
}
}
}
}
}
// Create solid mesh
if (positions.length > 0) {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
geometry.setIndex(indices);
this.mesh = new THREE.Mesh(geometry, material);
this.mesh.frustumCulled = true;
scene.add(this.mesh);
}
// Create water mesh
if (waterPositions.length > 0) {
const waterGeometry = new THREE.BufferGeometry();
waterGeometry.setAttribute('position', new THREE.Float32BufferAttribute(waterPositions, 3));
waterGeometry.setAttribute('normal', new THREE.Float32BufferAttribute(waterNormals, 3));
waterGeometry.setAttribute('uv', new THREE.Float32BufferAttribute(waterUvs, 2));
waterGeometry.setIndex(waterIndices);
this.waterMesh = new THREE.Mesh(waterGeometry, waterMaterial);
this.waterMesh.frustumCulled = true;
scene.add(this.waterMesh);
}
this.isDirty = false;
}
dispose(scene) {
if (this.mesh) {
scene.remove(this.mesh);
this.mesh.geometry.dispose();
this.mesh = null;
}
if (this.waterMesh) {
scene.remove(this.waterMesh);
this.waterMesh.geometry.dispose();
this.waterMesh = null;
}
}
}
// ============================================
// WORLD MANAGER
// ============================================
class World {
constructor(scene, textureAtlas, seed = 12345) {
this.scene = scene;
this.textureAtlas = textureAtlas;
this.noise = new SimplexNoise(seed);
this.treeNoise = new SimplexNoise(seed + 1);
this.chunks = new Map();
// Create materials
this.material = new THREE.MeshLambertMaterial({
map: textureAtlas.texture,
side: THREE.FrontSide
});
this.waterMaterial = new THREE.MeshLambertMaterial({
map: textureAtlas.texture,
transparent: true,
opacity: 0.7,
side: THREE.DoubleSide
});
this.lastPlayerChunkX = null;
this.lastPlayerChunkZ = null;
}
getChunkKey(cx, cz) {
return `${cx},${cz}`;
}
getChunk(cx, cz) {
return this.chunks.get(this.getChunkKey(cx, cz));
}
getOrCreateChunk(cx, cz) {
const key = this.getChunkKey(cx, cz);
let chunk = this.chunks.get(key);
if (!chunk) {
chunk = new Chunk(cx, cz, this.noise, this.treeNoise, this.textureAtlas);
this.chunks.set(key, chunk);
}
return chunk;
}
getBlock(x, y, z) {
const cx = Math.floor(x / CHUNK_SIZE);
const cz = Math.floor(z / CHUNK_SIZE);
const chunk = this.getChunk(cx, cz);
if (!chunk || !chunk.isGenerated) return BLOCK_TYPES.AIR;
const localX = ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const localZ = ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
return chunk.getBlock(localX, y, localZ);
}
setBlock(x, y, z, type) {
const cx = Math.floor(x / CHUNK_SIZE);
const cz = Math.floor(z / CHUNK_SIZE);
const chunk = this.getChunk(cx, cz);
if (!chunk) return false;
const localX = ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const localZ = ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const result = chunk.setBlock(localX, y, localZ, type);
// Rebuild mesh
if (result) {
chunk.buildMesh(this.scene, this.material, this.waterMaterial);
// Check if we need to update neighboring chunks
if (localX === 0) this.rebuildChunk(cx - 1, cz);
if (localX === CHUNK_SIZE - 1) this.rebuildChunk(cx + 1, cz);
if (localZ === 0) this.rebuildChunk(cx, cz - 1);
if (localZ === CHUNK_SIZE - 1) this.rebuildChunk(cx, cz + 1);
}
return result;
}
rebuildChunk(cx, cz) {
const chunk = this.getChunk(cx, cz);
if (chunk && chunk.isGenerated) {
chunk.isDirty = true;
chunk.buildMesh(this.scene, this.material, this.waterMaterial);
}
}
// Generate all chunks around a position immediately (for initial load)
generateInitialChunks(playerX, playerZ) {
const playerChunkX = Math.floor(playerX / CHUNK_SIZE);
const playerChunkZ = Math.floor(playerZ / CHUNK_SIZE);
this.lastPlayerChunkX = playerChunkX;
this.lastPlayerChunkZ = playerChunkZ;
// Generate ALL chunks around spawn synchronously
for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) {
for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) {
const cx = playerChunkX + dx;
const cz = playerChunkZ + dz;
const chunk = this.getOrCreateChunk(cx, cz);
chunk.generate();
chunk.buildMesh(this.scene, this.material, this.waterMaterial);
}
}
}
update(playerX, playerZ) {
const playerChunkX = Math.floor(playerX / CHUNK_SIZE);
const playerChunkZ = Math.floor(playerZ / CHUNK_SIZE);
// Only update if player moved to new chunk
if (playerChunkX === this.lastPlayerChunkX && playerChunkZ === this.lastPlayerChunkZ) {
return;
}
this.lastPlayerChunkX = playerChunkX;
this.lastPlayerChunkZ = playerChunkZ;
// Generate chunks around player
const chunksToGenerate = [];
for (let dx = -RENDER_DISTANCE; dx <= RENDER_DISTANCE; dx++) {
for (let dz = -RENDER_DISTANCE; dz <= RENDER_DISTANCE; dz++) {
const cx = playerChunkX + dx;
const cz = playerChunkZ + dz;
const chunk = this.getOrCreateChunk(cx, cz);
if (!chunk.isGenerated) {
chunksToGenerate.push(chunk);
} else if (chunk.isDirty) {
chunk.buildMesh(this.scene, this.material, this.waterMaterial);
}
}
}
// Generate new chunks (limit per frame for smoothness)
const maxChunksPerFrame = 4;
for (let i = 0; i < Math.min(chunksToGenerate.length, maxChunksPerFrame); i++) {
const chunk = chunksToGenerate[i];
chunk.generate();
chunk.buildMesh(this.scene, this.material, this.waterMaterial);
}
// Unload far chunks
const unloadDistance = RENDER_DISTANCE + 2;
for (const [key, chunk] of this.chunks) {
const dx = Math.abs(chunk.chunkX - playerChunkX);
const dz = Math.abs(chunk.chunkZ - playerChunkZ);
if (dx > unloadDistance || dz > unloadDistance) {
chunk.dispose(this.scene);
this.chunks.delete(key);
}
}
}
findSpawnPoint(startX, startZ) {
// Search in a spiral pattern for a clear spawn point
const searchRadius = 15;
for (let r = 0; r <= searchRadius; r++) {
for (let dx = -r; dx <= r; dx++) {
for (let dz = -r; dz <= r; dz++) {
if (r > 0 && Math.abs(dx) !== r && Math.abs(dz) !== r) continue; // Only check perimeter
const x = startX + dx;
const z = startZ + dz;
// Find the actual top of all blocks at this position (including trees)
const baseY = this.getTerrainHeight(x, z);
let topY = baseY;
// Scan upward to find the highest non-air block
for (let y = baseY; y < baseY + 20; y++) {
const block = this.getBlock(x, y, z);
if (block !== BLOCK_TYPES.AIR && block !== BLOCK_TYPES.WATER) {
topY = y;
}
}
// Check if there's clear space above (2 blocks for player height)
const blockAtFeet = this.getBlock(x, topY + 1, z);
const blockAtHead = this.getBlock(x, topY + 2, z);
const blockAtTop = this.getBlock(x, topY + 3, z);
// Make sure we're not spawning on water and have 3 clear blocks above
const groundBlock = this.getBlock(x, topY, z);
const isValidGround = groundBlock === BLOCK_TYPES.GRASS ||
groundBlock === BLOCK_TYPES.DIRT ||
groundBlock === BLOCK_TYPES.STONE ||
groundBlock === BLOCK_TYPES.SAND;
if (isValidGround &&
blockAtFeet === BLOCK_TYPES.AIR &&
blockAtHead === BLOCK_TYPES.AIR &&
blockAtTop === BLOCK_TYPES.AIR) {
return { x, y: topY + 1, z };
}
}
}
}
// Fallback: return position high above any obstacles
const y = this.getTerrainHeight(startX, startZ);
return { x: startX, y: y + 10, z: startZ };
}
getTerrainHeight(x, z) {
const cx = Math.floor(x / CHUNK_SIZE);
const cz = Math.floor(z / CHUNK_SIZE);
const chunk = this.getOrCreateChunk(cx, cz);
if (!chunk.isGenerated) {
chunk.generate();
chunk.buildMesh(this.scene, this.material, this.waterMaterial);
}
const localX = ((x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
const localZ = ((z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE;
return chunk.heightMap[localZ * CHUNK_SIZE + localX];
}
getChunkCount() {
return this.chunks.size;
}
}
// ============================================
// PLAYER CONTROLLER
// ============================================
class Player {
constructor(camera, world, canvas) {
this.camera = camera;
this.world = world;
this.canvas = canvas;
this.position = new THREE.Vector3(0, 30, 0);
this.velocity = new THREE.Vector3();
this.rotation = { x: 0, y: 0 };
this.moveSpeed = 5;
this.flySpeed = 15;
this.jumpForce = 8;
this.gravity = 25;
this.height = 1.7;
this.width = 0.6;
this.eyeHeight = 1.6;
this.isFlying = false;
this.isGrounded = false;
this.keys = {};
this.mouseLocked = false;
this.setupControls();
}
setupControls() {
document.addEventListener('keydown', (e) => {
this.keys[e.code] = true;
if (e.code === 'KeyF') {
this.isFlying = !this.isFlying;
this.velocity.y = 0;
}
});
document.addEventListener('keyup', (e) => {
this.keys[e.code] = false;
});
document.addEventListener('mousemove', (e) => {
if (!this.mouseLocked) return;
const sensitivity = 0.002;
this.rotation.y -= e.movementX * sensitivity;
this.rotation.x -= e.movementY * sensitivity;
this.rotation.x = Math.max(-Math.PI/2 + 0.01, Math.min(Math.PI/2 - 0.01, this.rotation.x));
});
this.canvas.addEventListener('click', () => {
if (!this.mouseLocked) {
this.canvas.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
this.mouseLocked = document.pointerLockElement === this.canvas;
});
}
checkCollision(x, y, z) {
const margin = this.width / 2 - 0.01; // Slight inset to prevent edge sticking
// Check at feet, mid-body, and head levels
const yLevels = [y + 0.01, y + 0.9, y + this.height - 0.01];
for (const py of yLevels) {
// Check all four corners at this height
const corners = [
[x - margin, py, z - margin],
[x + margin, py, z - margin],
[x - margin, py, z + margin],
[x + margin, py, z + margin]
];
for (const [px, checkY, pz] of corners) {
const block = this.world.getBlock(Math.floor(px), Math.floor(checkY), Math.floor(pz));
if (block !== BLOCK_TYPES.AIR && block !== BLOCK_TYPES.WATER) {
return true;
}
}
}
return false;
}
update(deltaTime) {
const forward = new THREE.Vector3(0, 0, -1);
const right = new THREE.Vector3(1, 0, 0);
forward.applyAxisAngle(new THREE.Vector3(0, 1, 0), this.rotation.y);
right.applyAxisAngle(new THREE.Vector3(0, 1, 0), this.rotation.y);
if (!this.isFlying) {
forward.y = 0;
right.y = 0;
forward.normalize();
right.normalize();
}
const moveDir = new THREE.Vector3();
const speed = this.isFlying ? this.flySpeed : this.moveSpeed;
if (this.keys['KeyW']) moveDir.add(forward);
if (this.keys['KeyS']) moveDir.sub(forward);
if (this.keys['KeyA']) moveDir.sub(right);
if (this.keys['KeyD']) moveDir.add(right);
if (moveDir.length() > 0) {
moveDir.normalize().multiplyScalar(speed);
}
this.velocity.x = moveDir.x;
this.velocity.z = moveDir.z;
if (this.isFlying) {
this.velocity.y = 0;
if (this.keys['Space']) this.velocity.y = this.flySpeed;
if (this.keys['ShiftLeft'] || this.keys['ShiftRight']) this.velocity.y = -this.flySpeed;
} else {
this.velocity.y -= this.gravity * deltaTime;
if (this.keys['Space'] && this.isGrounded) {
this.velocity.y = this.jumpForce;
this.isGrounded = false;
}
}
// Apply velocity with collision
const newX = this.position.x + this.velocity.x * deltaTime;
const newY = this.position.y + this.velocity.y * deltaTime;
const newZ = this.position.z + this.velocity.z * deltaTime;
if (!this.checkCollision(newX, this.position.y, this.position.z)) {
this.position.x = newX;
}
if (!this.checkCollision(this.position.x, this.position.y, newZ)) {
this.position.z = newZ;
}
if (!this.checkCollision(this.position.x, newY, this.position.z)) {
this.position.y = newY;
this.isGrounded = false;
} else {
if (this.velocity.y < 0) this.isGrounded = true;
this.velocity.y = 0;
}
if (this.position.y < -10) {
this.position.y = 50;
this.velocity.y = 0;
}
this.camera.position.copy(this.position);
this.camera.position.y += this.eyeHeight;
this.camera.rotation.order = 'YXZ';
this.camera.rotation.x = this.rotation.x;
this.camera.rotation.y = this.rotation.y;
}
}
// ============================================
// BLOCK INTERACTION
// ============================================
class BlockInteraction {
constructor(camera, world) {
this.camera = camera;
this.world = world;
this.raycaster = new THREE.Raycaster();
this.raycaster.far = 6;
this.selectedBlockType = BLOCK_TYPES.GRASS;
this.blockTypes = [BLOCK_TYPES.GRASS, BLOCK_TYPES.DIRT, BLOCK_TYPES.STONE, BLOCK_TYPES.WOOD, BLOCK_TYPES.LEAVES];
this.selectedIndex = 0;
this.setupControls();
}
setupControls() {
document.addEventListener('mousedown', (e) => {
if (document.pointerLockElement) {
if (e.button === 0) this.breakBlock();
if (e.button === 2) this.placeBlock();
}
});
document.addEventListener('contextmenu', (e) => e.preventDefault());
document.addEventListener('keydown', (e) => {
const num = parseInt(e.key);
if (num >= 1 && num <= 5) {
this.selectedIndex = num - 1;
this.selectedBlockType = this.blockTypes[this.selectedIndex];
this.updateHotbar();
}
});
document.addEventListener('wheel', (e) => {
if (document.pointerLockElement) {
if (e.deltaY > 0) {
this.selectedIndex = (this.selectedIndex + 1) % this.blockTypes.length;
} else {
this.selectedIndex = (this.selectedIndex - 1 + this.blockTypes.length) % this.blockTypes.length;
}
this.selectedBlockType = this.blockTypes[this.selectedIndex];
this.updateHotbar();
}
});
}
updateHotbar() {
const slots = document.querySelectorAll('.hotbar-slot');
slots.forEach((slot, i) => {
slot.classList.toggle('selected', i === this.selectedIndex);
});
}
raycast() {
this.raycaster.setFromCamera(new THREE.Vector2(0, 0), this.camera);
const origin = this.raycaster.ray.origin;
const direction = this.raycaster.ray.direction;
// Simple voxel raycast
let x = Math.floor(origin.x);
let y = Math.floor(origin.y);
let z = Math.floor(origin.z);
const stepX = direction.x >= 0 ? 1 : -1;
const stepY = direction.y >= 0 ? 1 : -1;
const stepZ = direction.z >= 0 ? 1 : -1;
const tDeltaX = Math.abs(1 / direction.x);
const tDeltaY = Math.abs(1 / direction.y);
const tDeltaZ = Math.abs(1 / direction.z);
let tMaxX = direction.x !== 0 ? (stepX > 0 ? (x + 1 - origin.x) : (origin.x - x)) * tDeltaX : Infinity;
let tMaxY = direction.y !== 0 ? (stepY > 0 ? (y + 1 - origin.y) : (origin.y - y)) * tDeltaY : Infinity;
let tMaxZ = direction.z !== 0 ? (stepZ > 0 ? (z + 1 - origin.z) : (origin.z - z)) * tDeltaZ : Infinity;
let lastX = x, lastY = y, lastZ = z;
for (let i = 0; i < 60; i++) {
const block = this.world.getBlock(x, y, z);
if (block !== BLOCK_TYPES.AIR && block !== BLOCK_TYPES.WATER) {
return {
hit: { x, y, z },
normal: { x: lastX - x, y: lastY - y, z: lastZ - z },
blockType: block
};
}
lastX = x; lastY = y; lastZ = z;
if (tMaxX < tMaxY) {
if (tMaxX < tMaxZ) {
x += stepX;
tMaxX += tDeltaX;
} else {
z += stepZ;
tMaxZ += tDeltaZ;
}
} else {
if (tMaxY < tMaxZ) {
y += stepY;
tMaxY += tDeltaY;
} else {
z += stepZ;
tMaxZ += tDeltaZ;
}
}
// Check distance
const dx = x - origin.x, dy = y - origin.y, dz = z - origin.z;
if (dx*dx + dy*dy + dz*dz > 36) break;
}
return null;
}
breakBlock() {
const result = this.raycast();
if (result) {
this.world.setBlock(result.hit.x, result.hit.y, result.hit.z, BLOCK_TYPES.AIR);
}
}
placeBlock() {
const result = this.raycast();
if (result) {
const newX = result.hit.x + result.normal.x;
const newY = result.hit.y + result.normal.y;
const newZ = result.hit.z + result.normal.z;
// Check player collision
const playerPos = this.camera.position;
const dx = Math.abs(newX + 0.5 - playerPos.x);
const dy = Math.abs(newY + 0.5 - playerPos.y + 0.9);
const dz = Math.abs(newZ + 0.5 - playerPos.z);
if (dx < 0.8 && dy < 1.8 && dz < 0.8) return;
this.world.setBlock(newX, newY, newZ, this.selectedBlockType);
}
}
}
// ============================================
// ATMOSPHERE
// ============================================
class Atmosphere {
constructor(scene) {
this.scene = scene;
this.dayDuration = 300;
this.timeOfDay = 0.25;
this.setupLighting();
this.setupSky();
}
setupLighting() {
this.ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
this.scene.add(this.ambientLight);
this.sunLight = new THREE.DirectionalLight(0xffffff, 1);
this.sunLight.position.set(50, 100, 50);
this.scene.add(this.sunLight);
const hemiLight = new THREE.HemisphereLight(0x87ceeb, 0x8b7355, 0.3);
this.scene.add(hemiLight);
}
setupSky() {
const vertexShader = `
varying vec3 vWorldPosition;
void main() {
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform float offset;
uniform float exponent;
varying vec3 vWorldPosition;
void main() {
float h = normalize(vWorldPosition + offset).y;
gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
}
`;
this.skyUniforms = {
topColor: { value: new THREE.Color(0x0077ff) },
bottomColor: { value: new THREE.Color(0xaaddff) },
offset: { value: 33 },
exponent: { value: 0.6 }
};
const skyGeo = new THREE.SphereGeometry(400, 32, 15);
const skyMat = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: this.skyUniforms,
side: THREE.BackSide
});
this.scene.add(new THREE.Mesh(skyGeo, skyMat));
this.scene.fog = new THREE.Fog(0xaaddff, 60, 180);
}
update(deltaTime) {
this.timeOfDay += deltaTime / this.dayDuration;
if (this.timeOfDay >= 1) this.timeOfDay -= 1;
const sunAngle = this.timeOfDay * Math.PI * 2 - Math.PI / 2;
const sunHeight = Math.sin(sunAngle);
this.sunLight.position.set(
Math.cos(sunAngle) * 100,
sunHeight * 100,
50
);
const dayProgress = Math.max(0, Math.min(1, (sunHeight + 0.1) / 1.1));
const dayTop = new THREE.Color(0x0077ff);
const dayBottom = new THREE.Color(0xaaddff);
const nightTop = new THREE.Color(0x000022);
const nightBottom = new THREE.Color(0x001133);
const sunsetTop = new THREE.Color(0xff6600);
const sunsetBottom = new THREE.Color(0xff9933);
let topColor, bottomColor;
if (dayProgress > 0.3) {
topColor = dayTop;
bottomColor = dayBottom;
} else if (dayProgress > 0.1) {
const t = (dayProgress - 0.1) / 0.2;
topColor = sunsetTop.clone().lerp(dayTop, t);
bottomColor = sunsetBottom.clone().lerp(dayBottom, t);
} else {
const t = dayProgress / 0.1;
topColor = nightTop.clone().lerp(sunsetTop, t);
bottomColor = nightBottom.clone().lerp(sunsetBottom, t);
}
this.skyUniforms.topColor.value.copy(topColor);
this.skyUniforms.bottomColor.value.copy(bottomColor);
this.sunLight.intensity = Math.max(0.1, dayProgress);
this.ambientLight.intensity = 0.2 + dayProgress * 0.4;
this.sunLight.color.setHex(dayProgress < 0.3 ? 0xffaa66 : 0xffffff);
this.scene.fog.color.copy(bottomColor);
// Update UI
const hours = Math.floor(this.timeOfDay * 24);
const minutes = Math.floor((this.timeOfDay * 24 - hours) * 60);
document.getElementById('time-text').textContent =
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
document.getElementById('time-icon').textContent = (hours >= 6 && hours < 18) ? '☀️' : '🌙';
}
}
// ============================================
// MAIN GAME
// ============================================
class Game {
constructor() {
this.scene = null;
this.camera = null;
this.renderer = null;
this.clock = new THREE.Clock();
this.textureAtlas = new TextureAtlas();
this.world = null;
this.player = null;
this.interaction = null;
this.atmosphere = null;
this.frameCount = 0;
this.lastFpsUpdate = 0;
this.fps = 0;
}
init() {
// Renderer
const canvas = document.getElementById('game-canvas');
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: false });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// Camera
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 500);
// Scene
this.scene = new THREE.Scene();
// Generate texture atlas
this.textureAtlas.generate();
// World
this.world = new World(this.scene, this.textureAtlas);
// Spawn position (center of world)
const spawnX = 8;
const spawnZ = 8;
// Generate ALL initial chunks synchronously before player spawns
this.world.generateInitialChunks(spawnX, spawnZ);
// Find a clear spawn point (not inside trees)
const spawn = this.world.findSpawnPoint(spawnX, spawnZ);
// Player
this.player = new Player(this.camera, this.world, this.renderer.domElement);
this.player.position.set(spawn.x, spawn.y, spawn.z);
// Interaction
this.interaction = new BlockInteraction(this.camera, this.world);
// Atmosphere
this.atmosphere = new Atmosphere(this.scene);
// Hotbar
this.setupHotbar();
// Resize handler
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
});
// Start loop
this.animate();
}
setupHotbar() {
const hotbar = document.getElementById('hotbar');
const blockNames = ['grass', 'dirt', 'stone', 'wood', 'leaves'];
blockNames.forEach((name, i) => {
const slot = document.createElement('div');
slot.className = 'hotbar-slot' + (i === 0 ? ' selected' : '');
const canvas = this.textureAtlas.getHotbarCanvas(name);
slot.appendChild(canvas);
const num = document.createElement('span');
num.className = 'slot-number';
num.textContent = i + 1;
slot.appendChild(num);
hotbar.appendChild(slot);
});
}
updateStats() {
this.frameCount++;
const now = performance.now();
if (now - this.lastFpsUpdate >= 1000) {
this.fps = this.frameCount;
this.frameCount = 0;
this.lastFpsUpdate = now;
document.getElementById('fps').textContent = this.fps;
}
const pos = this.player.position;
document.getElementById('position').textContent =
`${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)}`;
document.getElementById('chunk-count').textContent = this.world.getChunkCount();
document.getElementById('fly-mode').textContent = this.player.isFlying ? 'Flying' : 'Walking';
}
animate() {
requestAnimationFrame(() => this.animate());
const deltaTime = Math.min(this.clock.getDelta(), 0.1);
this.player.update(deltaTime);
this.world.update(this.player.position.x, this.player.position.z);
this.atmosphere.update(deltaTime);
this.updateStats();
this.renderer.render(this.scene, this.camera);
}
}
// ============================================
// INIT
// ============================================
const game = new Game();
document.getElementById('start-btn').addEventListener('click', async () => {
document.getElementById('start-screen').style.display = 'none';
document.getElementById('loading').style.display = 'block';
await new Promise(resolve => setTimeout(resolve, 50));
game.init();
document.getElementById('loading').style.display = 'none';
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment