Skip to content

Commit ed2bc3c

Browse files
realistic minecraft movement system
1 parent 07ecee1 commit ed2bc3c

5 files changed

Lines changed: 396 additions & 76 deletions

File tree

src/block_manager.js

Lines changed: 152 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ const BLOCKS = {
1616

1717

1818
const CHUNK_SIZE = 16;
19-
const WORLD_HEIGHT = 128;
19+
const WORLD_HEIGHT = 96;
20+
const SEA_LEVEL = 28;
2021
const GROUND_LEVEL = 31;
2122
const CHUNK_GENERATION_HEIGHT = GROUND_LEVEL + 1;
2223

@@ -166,80 +167,136 @@ function smoothstep(edge0, edge1, x) {
166167
return x * x * (3 - 2 * x);
167168
}
168169

170+
function placeTree(chunkData, x, y, z, worldX, worldZ, terrainNoise) {
171+
const trunkHeight = 2 + Math.floor((terrainNoise(worldX * 0.2, worldZ * 0.2) + 1) * 1.5);
172+
173+
// --- Trunk ---
174+
for (let i = 0; i < trunkHeight; i++) {
175+
if (y + i >= WORLD_HEIGHT) continue;
176+
chunkData[x][y + i][z] = BLOCKS["log"];
177+
}
178+
179+
const top = y + trunkHeight;
180+
181+
// --- 3×3 Leaf Square ---
182+
for (let lx = -1; lx <= 1; lx++) {
183+
for (let lz = -1; lz <= 1; lz++) {
184+
const dx = x + lx;
185+
const dy = top;
186+
const dz = z + lz;
187+
188+
if (dx < 0 || dx >= CHUNK_SIZE ||
189+
dy < 0 || dy >= WORLD_HEIGHT ||
190+
dz < 0 || dz >= CHUNK_SIZE) continue;
191+
192+
if (chunkData[dx][dy][dz].id === 0) {
193+
chunkData[dx][dy][dz] = BLOCKS["leaf"];
194+
}
195+
}
196+
}
197+
198+
// --- Single Top Leaf ---
199+
const topLeafY = top + 1;
200+
if (topLeafY < WORLD_HEIGHT && chunkData[x][topLeafY][z].id === 0) {
201+
chunkData[x][topLeafY][z] = BLOCKS["leaf"];
202+
}
203+
}
204+
205+
class TerrainPointInfo {
206+
constructor(temperature, humidity, mountainFactor, desertFactor, forestFactor, plainsFactor, terrainHeight) {
207+
this.temperature = temperature;
208+
this.humidity = humidity;
209+
this.mountainFactor = mountainFactor;
210+
this.desertFactor = desertFactor;
211+
this.forestFactor = forestFactor;
212+
this.plainsFactor = plainsFactor;
213+
this.terrainHeight = terrainHeight;
214+
}
215+
}
216+
217+
function getTerrainPointInfo(worldX, worldZ, terrainNoise, tempNoise, humidityNoise) {
218+
// BIOME NOISE
219+
const temperature = tempNoise(worldX * 0.001, worldZ * 0.001);
220+
const humidity = humidityNoise(worldX * 0.001, worldZ * 0.001);
221+
222+
// BIOME BLENDING
223+
const mountainFactor = smoothstep(-0.2, 0.4, -temperature); // Mountains appear in colder regions
224+
const desertFactor = smoothstep(0.2, 0.7, temperature) * smoothstep(0.3, -0.3, humidity); // Deserts appear in hot + dry regions
225+
const forestFactor = smoothstep(0.1, 0.7, humidity); // Forests appear in humid regions
226+
let plainsFactor = 1.0 - Math.max(mountainFactor, desertFactor, forestFactor); // Plains are default
227+
228+
plainsFactor = Math.max(plainsFactor, 0);
229+
230+
// DOMAIN WARPING
231+
const warpX = terrainNoise(worldX * 0.002, worldZ * 0.002) * 30;
232+
const warpZ = terrainNoise((worldX + 1000) * 0.002, (worldZ + 1000) * 0.002) * 30;
233+
234+
const nx = worldX + warpX;
235+
const nz = worldZ + warpZ;
236+
237+
// BASE TERRAIN
238+
const baseTerrain = fbm(terrainNoise, nx * 0.003, nz * 0.003, 5, 0.5, 2.0);
239+
240+
// RIDGED MOUNTAINS
241+
const mountains = ridgedFBM(terrainNoise, nx * 0.008, nz * 0.008, 5);
242+
243+
// BIOME HEIGHTS
244+
const plainsHeight = baseTerrain * 8;
245+
const forestHeight = baseTerrain * 12;
246+
const desertHeight = baseTerrain * 5;
247+
const mountainHeight = baseTerrain * 15 + mountains * 45;
248+
249+
// FINAL BLENDED HEIGHT
250+
const terrainHeight = Math.floor(
251+
plainsHeight * plainsFactor +
252+
forestHeight * forestFactor +
253+
desertHeight * desertFactor +
254+
mountainHeight * mountainFactor
255+
) + SEA_LEVEL;
256+
257+
return new TerrainPointInfo(temperature, humidity, mountainFactor, desertFactor, forestFactor, plainsFactor, terrainHeight);
258+
}
259+
169260
function GenerateChunk(chunkX, chunkZ, terrainNoise, tempNoise, humidityNoise) {
170261

171262
if (chunkX % CHUNK_SIZE != 0 || chunkZ % CHUNK_SIZE != 0) {
172263
console.error(`Chunk coordinates: ${chunkX}, ${chunkZ} must be multiples of 16`);
173264
return null;
174265
}
175266

176-
const SEA_LEVEL = 28;
177-
const MAX_TREES_PER_CHUNK = 5;
178-
let treesInChunk = 0;
267+
const TREE_CELL_SIZE = 6;
179268

180269
let chunkData = new Array(CHUNK_SIZE);
181-
182270
for (let x = 0; x < CHUNK_SIZE; x++) {
183271

184272
chunkData[x] = new Array(WORLD_HEIGHT);
185273

186274
for (let y = 0; y < WORLD_HEIGHT; y++) {
187275
chunkData[x][y] = new Array(CHUNK_SIZE);
276+
for (let z = 0; z < CHUNK_SIZE; z++) {
277+
chunkData[x][y][z] = BLOCKS["air"];
278+
}
188279
}
280+
}
189281

282+
for (let x = 0; x < CHUNK_SIZE; x++) {
190283
for (let z = 0; z < CHUNK_SIZE; z++) {
284+
191285
const worldX = chunkX + x;
192286
const worldZ = chunkZ + z;
193287

194-
// BIOME NOISE
195-
const temperature = tempNoise(worldX * 0.001, worldZ * 0.001);
196-
const humidity = humidityNoise(worldX * 0.001, worldZ * 0.001);
197-
198-
// BIOME BLENDING
199-
const mountainFactor = smoothstep(-0.2, 0.4, -temperature); // Mountains appear in colder regions
200-
const desertFactor = smoothstep(0.2, 0.7, temperature) * smoothstep(0.3, -0.3, humidity); // Deserts appear in hot + dry regions
201-
const forestFactor = smoothstep(0.1, 0.7, humidity); // Forests appear in humid regions
202-
let plainsFactor = 1.0 - Math.max(mountainFactor, desertFactor, forestFactor); // Plains are default
203-
204-
plainsFactor = Math.max(plainsFactor, 0);
205-
206-
// DOMAIN WARPING
207-
const warpX = terrainNoise(worldX * 0.002, worldZ * 0.002) * 30;
208-
const warpZ = terrainNoise((worldX + 1000) * 0.002, (worldZ + 1000) * 0.002) * 30;
209-
210-
const nx = worldX + warpX;
211-
const nz = worldZ + warpZ;
212-
213-
// BASE TERRAIN
214-
const baseTerrain = fbm(terrainNoise, nx * 0.003, nz * 0.003, 5, 0.5, 2.0);
215-
216-
// RIDGED MOUNTAINS
217-
const mountains = ridgedFBM(terrainNoise, nx * 0.008, nz * 0.008, 5);
218-
219-
// BIOME HEIGHTS
220-
const plainsHeight = baseTerrain * 8;
221-
const forestHeight = baseTerrain * 12;
222-
const desertHeight = baseTerrain * 5;
223-
const mountainHeight = baseTerrain * 15 + mountains * 45;
224-
225-
// FINAL BLENDED HEIGHT
226-
const terrainHeight = Math.floor(
227-
plainsHeight * plainsFactor +
228-
forestHeight * forestFactor +
229-
desertHeight * desertFactor +
230-
mountainHeight * mountainFactor
231-
) + SEA_LEVEL;
288+
const tpi = getTerrainPointInfo(worldX, worldZ, terrainNoise, tempNoise, humidityNoise)
232289

233290
// SURFACE BLOCKS
234291
let surfaceBlock = "gras";
235292
let subsurfaceBlock = "dir";
236293

237-
if (desertFactor > 0.5) {
294+
if (tpi.desertFactor > 0.5) {
238295
surfaceBlock = "san";
239296
subsurfaceBlock = "san";
240297
}
241298

242-
if (mountainFactor > 0.6) {
299+
if (tpi.mountainFactor > 0.6) {
243300
surfaceBlock = "cob";
244301
subsurfaceBlock = "cob";
245302
}
@@ -254,34 +311,47 @@ function GenerateChunk(chunkX, chunkZ, terrainNoise, tempNoise, humidityNoise) {
254311
chunkData[x][y][z] = BLOCKS["bedroc"];
255312
}
256313

257-
else if (y < terrainHeight - 4) {
314+
else if (y < tpi.terrainHeight - 4) {
258315
chunkData[x][y][z] = BLOCKS["cob"];
259316
}
260317

261-
else if (y < terrainHeight) {
262-
if (mountainFactor > 0.6 && y < mountainCobHeight) {
318+
else if (y < tpi.terrainHeight) {
319+
if (tpi.mountainFactor > 0.6 && y < mountainCobHeight) {
263320
subsurfaceBlock = "dir"; // for mountains, transition from dir to cob
264321
}
265322
chunkData[x][y][z] = BLOCKS[subsurfaceBlock];
266323
}
267324

268-
else if (y === terrainHeight) {
269-
if (mountainFactor > 0.6 && y < mountainCobHeight) {
325+
else if (y === tpi.terrainHeight) {
326+
if (tpi.mountainFactor > 0.6 && y < mountainCobHeight) {
270327
surfaceBlock = "gras"; // for mountains, transition from gras to cob
271328
}
272329
chunkData[x][y][z] = BLOCKS[surfaceBlock];
273330
}
274331

275332
else {
276-
chunkData[x][y][z] = BLOCKS["air"];
333+
//chunkData[x][y][z] = BLOCKS["air"];
277334
}
278335
}
279336

280337
// TREES
281-
const treeNoise = terrainNoise(worldX * 0.1, worldZ * 0.1);
282-
if (forestFactor > 0.7 && treeNoise > 0.6 && MAX_TREES_PER_CHUNK > treesInChunk && terrainHeight < mountainCobHeight) {
283-
chunkData[x][terrainHeight + 1][z] = BLOCKS["log"];
284-
treesInChunk++;
338+
const forestDensity = terrainNoise(worldX * 0.01, worldZ * 0.01);
339+
340+
const cellX = Math.floor(worldX / TREE_CELL_SIZE);
341+
const cellZ = Math.floor(worldZ / TREE_CELL_SIZE);
342+
343+
const localX = Math.floor((terrainNoise(cellX * 17, cellZ * 17) + 1) * 0.5 * TREE_CELL_SIZE);
344+
const localZ = Math.floor((terrainNoise(cellX * 29, cellZ * 29) + 1) * 0.5 * TREE_CELL_SIZE);
345+
346+
const treeX = cellX * TREE_CELL_SIZE + localX;
347+
const treeZ = cellZ * TREE_CELL_SIZE + localZ;
348+
349+
if (worldX === treeX && worldZ === treeZ && tpi.forestFactor > 0.5 && forestDensity > 0.2 && tpi.terrainHeight < mountainCobHeight) {
350+
const isOnChunkEdge = x === 0 || x === CHUNK_SIZE - 1 || z === 0 || z === CHUNK_SIZE - 1;
351+
352+
if (!isOnChunkEdge) {
353+
placeTree(chunkData, x, tpi.terrainHeight + 1, z, worldX, worldZ, terrainNoise);
354+
}
285355
}
286356
}
287357
}
@@ -604,6 +674,24 @@ class ChunkManager {
604674
chunk.data = GenerateChunk(chunk.x * CHUNK_SIZE, chunk.z * CHUNK_SIZE, this.terrainNoise, this.tempNoise, this.humidityNoise);
605675
}
606676
}
677+
678+
getBlock(wx, wy, wz) {
679+
const cx = worldToChunkCoord(wx);
680+
const cz = worldToChunkCoord(wz);
681+
const lx = worldToLocal(wx);
682+
const lz = worldToLocal(wz);
683+
684+
const key = chunkKey(cx, cz);
685+
const chunk = this.chunks.get(key);
686+
if (!chunk) return BLOCKS["air"];
687+
688+
if (wy < 0 || wy >= WORLD_HEIGHT) {
689+
//console.warn(`Y coordinate ${wy} is out of bounds`);
690+
return BLOCKS["air"];
691+
}
692+
693+
return chunk.data[lx][wy][lz];
694+
}
607695
}
608696

609697
function getAffectedChunks(wx, wy, wz) {
@@ -623,6 +711,14 @@ function getAffectedChunks(wx, wy, wz) {
623711
return chunks;
624712
}
625713

714+
function TopPositionInWorld(xmin, xmax, zmin, zmax, chunkManager) {
715+
const x = Math.floor(Math.random() * (xmax - xmin) + xmin);
716+
const z = Math.floor(Math.random() * (zmax - zmin) + zmin);
717+
718+
const tpi = getTerrainPointInfo(x, z, chunkManager.terrainNoise, chunkManager.tempNoise, chunkManager.humidityNoise);
719+
const y = tpi.terrainHeight;
626720

721+
return new THREE.Vector3(x, y + 2, z);
722+
}
627723

628-
export const BLOCK_MANAGER = { Chunk, ChunkManager, GenerateChunk, RenderChunk, worldToChunkCoord, chunkKey, BLOCKS, CHUNK_SIZE, WORLD_HEIGHT, GROUND_LEVEL, CHUNK_GENERATION_HEIGHT, FACE_DEFINITIONS };
724+
export const BLOCK_MANAGER = { Chunk, ChunkManager, TerrainPointInfo, GenerateChunk, RenderChunk, worldToChunkCoord, chunkKey, TopPositionInWorld, BLOCKS, CHUNK_SIZE, WORLD_HEIGHT, GROUND_LEVEL, CHUNK_GENERATION_HEIGHT, FACE_DEFINITIONS };

src/game.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ BLOCK_OUTLINE.updateCrosshair();
1111

1212
const scene = new THREE.Scene();
1313

14-
const player = new PLAYER_MANAGER.Player("Player1");
15-
1614
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById("c") });
1715
renderer.setSize(SETTINGS.WIDTH, SETTINGS.HEIGHT);
1816
renderer.shadowMap.enabled = true;
@@ -25,9 +23,15 @@ const dayCycle = new ATMOSPHERE.DayCycle(scene);
2523

2624
const chunkManager = new BLOCK_MANAGER.ChunkManager(scene, SETTINGS.SEED);
2725
chunkManager.loadWorld();
28-
chunkManager.update(player.position);
29-
let lastChunkX, lastChunkZ = 0;
3026

27+
const spawnDist = 16;
28+
const spawnPosition = BLOCK_MANAGER.TopPositionInWorld(-spawnDist, spawnDist, -spawnDist, spawnDist, chunkManager);
29+
chunkManager.update(spawnPosition);
30+
31+
let player = new PLAYER_MANAGER.Player("Player1");
32+
player.position.copy(spawnPosition);
33+
34+
let lastChunkX, lastChunkZ = 0;
3135
let lastTime = 0;
3236

3337
let blockOutlineData = BLOCK_OUTLINE.createBlockOutline(scene);
@@ -54,6 +58,7 @@ function RenderFrame(time) {
5458

5559
if (currentChunkX !== lastChunkX || currentChunkZ !== lastChunkZ) {
5660
chunkManager.update(player.position);
61+
player.physicsEnabled = true;
5762
}
5863
chunkManager.updateDirtyChunks();
5964

src/input_manager.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,30 @@ let yaw = 0;
2727
let pitch = 0;
2828

2929
window.addEventListener("keydown", (e) => {
30-
if (e.key in keys) keys[e.key] = true;
31-
if (e.key === " ") keys.Space = true;
30+
let key = e.key;
31+
if (key.match(/^[A-Z]$/)) key = key.toLowerCase();
32+
if (key in keys) keys[key] = true;
33+
if (key === " ") keys.Space = true;
3234

33-
if (e.key === "Escape" && SETTINGS.LOCKED_IN) {
35+
if (key === "Escape" && SETTINGS.LOCKED_IN) {
3436
SETTINGS.LOCKED_IN = false;
3537
document.exitPointerLock();
3638
}
3739

3840
if (e.ctrlKey || e.metaKey) {
3941
const blockedKeys = ['s', 'p', 'w', 'r', 'a', 't'];
40-
if (blockedKeys.includes(e.key.toLowerCase())) {
42+
if (blockedKeys.includes(key.toLowerCase())) {
4143
e.stopPropagation();
4244
e.preventDefault(); // Stop default browser action
4345
}
4446
}
4547
});
4648

4749
window.addEventListener("keyup", (e) => {
48-
if (e.key in keys) keys[e.key] = false;
49-
if (e.key === " ") keys.Space = false;
50+
let key = e.key;
51+
if (key.match(/^[A-Z]$/)) key = key.toLowerCase();
52+
if (key in keys) keys[key] = false;
53+
if (key === " ") keys.Space = false;
5054
});
5155

5256
document.addEventListener("click", () => {

0 commit comments

Comments
 (0)