From d846613ca90165cf08ff8b2db543f1739523ab62 Mon Sep 17 00:00:00 2001 From: LabyStudio Date: Wed, 4 May 2022 23:35:53 +0200 Subject: [PATCH] implement new terrain generator, cave generator, tree generator, implement big tree generator, improve light update performance --- .../minecraft/client/entity/PlayerEntity.js | 5 +- .../minecraft/client/render/BlockRenderer.js | 5 +- .../minecraft/client/render/Tessellator.js | 10 +- .../minecraft/client/render/WorldRenderer.js | 14 +- src/js/net/minecraft/client/world/Chunk.js | 7 +- .../minecraft/client/world/ChunkSection.js | 2 +- src/js/net/minecraft/client/world/World.js | 50 +- .../net/minecraft/client/world/block/Block.js | 6 +- .../client/world/block/BlockRegistry.js | 18 +- .../client/world/block/type/BlockWater.js | 4 + .../client/world/generator/Generator.js | 18 + .../client/world/generator/Primer.js | 18 + .../client/world/generator/WorldGenerator.js | 487 +++++++++++++----- .../generator/noise/NoiseGeneratorOctaves.js | 24 + .../generator/noise/NoiseGeneratorPerlin.js | 160 +++++- .../generator/structure/BigTreeGenerator.js | 391 ++++++++++++++ .../generator/structure/CaveGenerator.js | 252 +++++++++ .../generator/structure/TreeGenerator.js | 88 ++++ .../net/minecraft/util/MetadataChunkBlock.js | 6 + src/js/net/minecraft/util/Random.js | 11 +- src/resources/terrain/terrain.png | Bin 7566 -> 7535 bytes 21 files changed, 1380 insertions(+), 196 deletions(-) create mode 100644 src/js/net/minecraft/client/world/generator/Generator.js create mode 100644 src/js/net/minecraft/client/world/generator/Primer.js create mode 100644 src/js/net/minecraft/client/world/generator/structure/BigTreeGenerator.js create mode 100644 src/js/net/minecraft/client/world/generator/structure/CaveGenerator.js create mode 100644 src/js/net/minecraft/client/world/generator/structure/TreeGenerator.js diff --git a/src/js/net/minecraft/client/entity/PlayerEntity.js b/src/js/net/minecraft/client/entity/PlayerEntity.js index 045ba11..644f39c 100644 --- a/src/js/net/minecraft/client/entity/PlayerEntity.js +++ b/src/js/net/minecraft/client/entity/PlayerEntity.js @@ -5,6 +5,7 @@ import Block from "../world/block/Block.js"; import MathHelper from "../../util/MathHelper.js"; import Keyboard from "../../util/Keyboard.js"; import Vector3 from "../../util/Vector3.js"; +import {BlockRegistry} from "../world/block/BlockRegistry.js"; export default class PlayerEntity extends EntityLiving { @@ -161,7 +162,7 @@ export default class PlayerEntity extends EntityLiving { } isInWater() { - return this.world.getBlockAt(this.getBlockPosX(), this.getBlockPosY(), this.getBlockPosZ()) === Block.WATER.getId(); + return this.world.getBlockAt(this.getBlockPosX(), this.getBlockPosY(), this.getBlockPosZ()) === BlockRegistry.WATER.getId(); } isHeadInWater() { @@ -170,7 +171,7 @@ export default class PlayerEntity extends EntityLiving { Math.floor(cameraPosition.x), Math.floor(cameraPosition.y + 0.12), Math.floor(cameraPosition.z) - ) === Block.WATER.getId() + ) === BlockRegistry.WATER.getId() } jump() { diff --git a/src/js/net/minecraft/client/render/BlockRenderer.js b/src/js/net/minecraft/client/render/BlockRenderer.js index f343c08..c3edea5 100644 --- a/src/js/net/minecraft/client/render/BlockRenderer.js +++ b/src/js/net/minecraft/client/render/BlockRenderer.js @@ -68,6 +68,9 @@ export default class BlockRenderer { this.tessellator.setColor(color, color, color); } + // Set opacity of block + this.tessellator.setAlpha(1 - block.getTransparency()); + // Add face to tessellator this.addFace(world, face, ambientOcclusion, minX, minY, minZ, maxX, maxY, maxZ, minU, minV, maxU, maxV); } @@ -129,7 +132,7 @@ export default class BlockRenderer { let color = brightness * face.getShading(); // Set color with shading - this.tessellator.setColor(color, color, color); + this.tessellator.setColorRGB(color, color, color); } getAverageLightLevelAt(world, x, y, z) { diff --git a/src/js/net/minecraft/client/render/Tessellator.js b/src/js/net/minecraft/client/render/Tessellator.js index 39724a1..f2b1997 100644 --- a/src/js/net/minecraft/client/render/Tessellator.js +++ b/src/js/net/minecraft/client/render/Tessellator.js @@ -25,10 +25,18 @@ export default class Tessellator { this.colors = []; } - setColor(red, green, blue, alpha = 1) { + setColorRGB(red, green, blue) { this.red = red; this.green = green; this.blue = blue; + } + + setColor(red, green, blue, alpha = 1) { + this.setColorRGB(red, green, blue); + this.setAlpha(alpha); + } + + setAlpha(alpha) { this.alpha = alpha; } diff --git a/src/js/net/minecraft/client/render/WorldRenderer.js b/src/js/net/minecraft/client/render/WorldRenderer.js index 638c2ea..f090c14 100644 --- a/src/js/net/minecraft/client/render/WorldRenderer.js +++ b/src/js/net/minecraft/client/render/WorldRenderer.js @@ -540,8 +540,8 @@ export default class WorldRenderer { let renderDistance = WorldRenderer.RENDER_DISTANCE; // Load chunks - for (let x = -renderDistance; x <= renderDistance; x++) { - for (let z = -renderDistance; z <= renderDistance; z++) { + for (let x = -renderDistance + 1; x < renderDistance; x++) { + for (let z = -renderDistance + 1; z < renderDistance; z++) { world.getChunkAt(cameraChunkX + x, cameraChunkZ + z); } } @@ -580,6 +580,16 @@ export default class WorldRenderer { } else { // Hide chunk chunk.group.visible = false; + + // Unload chunk + if (chunk.loaded) { + chunk.unload(); + + // TODO Implement chunk unloading + //let index = chunk.x + (chunk.z << 16); + //world.chunks.delete(index); + //world.group.add(chunk.group); + } } } diff --git a/src/js/net/minecraft/client/world/Chunk.js b/src/js/net/minecraft/client/world/Chunk.js index c22be53..4efd1c0 100644 --- a/src/js/net/minecraft/client/world/Chunk.js +++ b/src/js/net/minecraft/client/world/Chunk.js @@ -16,6 +16,7 @@ export default class Chunk { this.group.chunkZ = z; this.loaded = false; + this.isTerrainPopulated = false; // Initialize sections this.sections = []; @@ -75,7 +76,7 @@ export default class Chunk { let typeId = section.getBlockAt(x, y & 15, z); let block = Block.getById(typeId); - let opacity = block.getOpacity(); + let opacity = typeId === 0 ? 0 : block.getOpacity(); let blockLight = typeId === 0 ? 0 : block.getLightValue(); if (opacity === 0) { @@ -322,6 +323,10 @@ export default class Chunk { return this.loaded; } + unload() { + this.loaded = false; + } + setModifiedAllSections() { for (let y = 0; y < this.sections.length; y++) { this.sections[y].isModified = true; diff --git a/src/js/net/minecraft/client/world/ChunkSection.js b/src/js/net/minecraft/client/world/ChunkSection.js index cca9b12..13b40c0 100644 --- a/src/js/net/minecraft/client/world/ChunkSection.js +++ b/src/js/net/minecraft/client/world/ChunkSection.js @@ -38,7 +38,7 @@ export default class ChunkSection { this.blocks[index] = 0; this.blocksData[index] = 0; this.blockLight[index] = 0; - this.skyLight[index] = 0; + this.skyLight[index] = 14; } } } diff --git a/src/js/net/minecraft/client/world/World.js b/src/js/net/minecraft/client/world/World.js index f9ea4c6..4222e13 100644 --- a/src/js/net/minecraft/client/world/World.js +++ b/src/js/net/minecraft/client/world/World.js @@ -1,14 +1,13 @@ import ChunkSection from "./ChunkSection.js"; import WorldGenerator from "./generator/WorldGenerator.js"; -import Chunk from "./Chunk.js"; import MathHelper from "../../util/MathHelper.js"; import BoundingBox from "../../util/BoundingBox.js"; -import MetadataChunkBlock from "../../util/MetadataChunkBlock.js"; import EnumSkyBlock from "../../util/EnumSkyBlock.js"; import Block from "./block/Block.js"; import EnumBlockFace from "../../util/EnumBlockFace.js"; import Vector3 from "../../util/Vector3.js"; import Vector4 from "../../util/Vector4.js"; +import MetadataChunkBlock from "../../util/MetadataChunkBlock.js"; export default class World { @@ -28,7 +27,8 @@ export default class World { this.time = 0; // Load world - this.generator = new WorldGenerator(this, Date.now() % 100000); + this.seed = Date.now() % 100000; + this.generator = new WorldGenerator(this, this.seed); // Update lights async let scope = this; @@ -70,24 +70,41 @@ export default class World { let index = x + (z << 16); let chunk = this.chunks.get(index); if (typeof chunk === 'undefined') { - chunk = new Chunk(this, x, z); - // Generate new chunk - this.generator.generateChunk(chunk); + chunk = this.generator.newChunk(this, x, z); - // Init - chunk.generateSkylightMap(); - chunk.generateBlockLightMap(); - - // Register + // Register and mark as loaded chunk.loaded = true; this.chunks.set(index, chunk); + + // Populate the chunk + if (!chunk.isTerrainPopulated && this.chunkExists(x + 1, z + 1) && this.chunkExists(x, z + 1) && this.chunkExists(x + 1, z)) { + this.populate(x, z); + } + if (this.chunkExists(x - 1, z) && !this.getChunkAt(x - 1, z).isTerrainPopulated && this.chunkExists(x - 1, z + 1) && this.chunkExists(x, z + 1) && this.chunkExists(x - 1, z)) { + this.populate(x - 1, z); + } + if (this.chunkExists(x, z - 1) && !this.getChunkAt(x, z - 1).isTerrainPopulated && this.chunkExists(x + 1, z - 1) && this.chunkExists(x, z - 1) && this.chunkExists(x + 1, z)) { + this.populate(x, z - 1); + } + if (this.chunkExists(x - 1, z - 1) && !this.getChunkAt(x - 1, z - 1).isTerrainPopulated && this.chunkExists(x - 1, z - 1) && this.chunkExists(x, z - 1) && this.chunkExists(x - 1, z)) { + this.populate(x - 1, z - 1); + } + + // Register in three.js this.group.add(chunk.group); + } + return chunk; + } + + populate(x, z) { + let chunk = this.getChunkAt(x, z); + if (!chunk.isTerrainPopulated) { + chunk.isTerrainPopulated = true; // Populate chunk this.generator.populateChunk(chunk.x, chunk.z); } - return chunk; } getChunkAtBlock(x, y, z) { @@ -161,13 +178,18 @@ export default class World { } } + let centerChunk = this.getChunkAt(centerX >> 4, centerZ >> 4); + if (!centerChunk.loaded) { + return; + } + // Add light update region to queue - if (this.lightUpdateQueue.length < 10000) { + if (this.lightUpdateQueue.length < 9999) { this.lightUpdateQueue.push(new MetadataChunkBlock(sourceType, x1, y1, z1, x2, y2, z2)); } // Max light updates in queue - if (this.lightUpdateQueue.length > 100000) { + if (this.lightUpdateQueue.length > 10000) { this.lightUpdateQueue = []; } } diff --git a/src/js/net/minecraft/client/world/block/Block.js b/src/js/net/minecraft/client/world/block/Block.js index f0bc39c..4205206 100644 --- a/src/js/net/minecraft/client/world/block/Block.js +++ b/src/js/net/minecraft/client/world/block/Block.js @@ -35,8 +35,12 @@ export default class Block { return this.textureSlotId; } + getTransparency() { + return 0.0; + } + isTransparent() { - return this.getOpacity() < 1.0; + return this.getTransparency() > 0.0; } shouldRenderFace(world, x, y, z, face) { diff --git a/src/js/net/minecraft/client/world/block/BlockRegistry.js b/src/js/net/minecraft/client/world/block/BlockRegistry.js index 6cd4089..7617768 100644 --- a/src/js/net/minecraft/client/world/block/BlockRegistry.js +++ b/src/js/net/minecraft/client/world/block/BlockRegistry.js @@ -22,14 +22,14 @@ export class BlockRegistry { Block.sounds.sand = new Sound("sand", 1.0); // Blocks - Block.STONE = new BlockStone(1, 0); - Block.GRASS = new BlockGrass(2, 1); - Block.DIRT = new BlockDirt(3, 2); - Block.WOOD = new BlockWood(5, 10); - Block.LOG = new BlockLog(17, 4); - Block.LEAVE = new BlockLeave(18, 6); - Block.WATER = new BlockWater(9, 7); - Block.SAND = new BlockSand(12, 8) - Block.TORCH = new BlockTorch(50, 9) + BlockRegistry.STONE = new BlockStone(1, 0); + BlockRegistry.GRASS = new BlockGrass(2, 1); + BlockRegistry.DIRT = new BlockDirt(3, 2); + BlockRegistry.WOOD = new BlockWood(5, 10); + BlockRegistry.LOG = new BlockLog(17, 4); + BlockRegistry.LEAVE = new BlockLeave(18, 6); + BlockRegistry.WATER = new BlockWater(9, 7); + BlockRegistry.SAND = new BlockSand(12, 8) + BlockRegistry.TORCH = new BlockTorch(50, 9) } } \ No newline at end of file diff --git a/src/js/net/minecraft/client/world/block/type/BlockWater.js b/src/js/net/minecraft/client/world/block/type/BlockWater.js index a7f27b9..57dabf9 100644 --- a/src/js/net/minecraft/client/world/block/type/BlockWater.js +++ b/src/js/net/minecraft/client/world/block/type/BlockWater.js @@ -10,6 +10,10 @@ export default class BlockWater extends Block { return 0.01; } + getTransparency() { + return 0.2; + } + isSolid() { return false; } diff --git a/src/js/net/minecraft/client/world/generator/Generator.js b/src/js/net/minecraft/client/world/generator/Generator.js new file mode 100644 index 0000000..305646c --- /dev/null +++ b/src/js/net/minecraft/client/world/generator/Generator.js @@ -0,0 +1,18 @@ +import Random from "../../../util/Random.js"; + +export default class Generator { + + constructor(world, seed) { + this.world = world; + this.seed = seed; + this.random = new Random(seed); + } + + generateInChunk(chunkX, chunkZ, primer) { + + } + + generateAtBlock(x, y, z, primer) { + + } +} \ No newline at end of file diff --git a/src/js/net/minecraft/client/world/generator/Primer.js b/src/js/net/minecraft/client/world/generator/Primer.js new file mode 100644 index 0000000..266c6e2 --- /dev/null +++ b/src/js/net/minecraft/client/world/generator/Primer.js @@ -0,0 +1,18 @@ +export default class Primer { + + constructor(chunk) { + this.chunk = chunk; + } + + get(x, y, z) { + return this.chunk.getBlockAt(x, y, z); + } + + set(x, y, z, typeId) { + this.chunk.setBlockAt(x, y, z, typeId); + } + + setByIndex(index, typeId) { + this.set(index >> 12 & 15, index >> 8 & 15, index & 15, typeId); + } +} \ No newline at end of file diff --git a/src/js/net/minecraft/client/world/generator/WorldGenerator.js b/src/js/net/minecraft/client/world/generator/WorldGenerator.js index e6891af..252263a 100644 --- a/src/js/net/minecraft/client/world/generator/WorldGenerator.js +++ b/src/js/net/minecraft/client/world/generator/WorldGenerator.js @@ -1,176 +1,389 @@ -import Random from "../../../util/Random.js"; -import NoiseGeneratorCombined from "./noise/NoiseGeneratorCombined.js"; import NoiseGeneratorOctaves from "./noise/NoiseGeneratorOctaves.js"; +import Chunk from "../Chunk.js"; +import Primer from "./Primer.js"; +import CaveGenerator from "./structure/CaveGenerator.js"; +import {BlockRegistry} from "../block/BlockRegistry.js"; +import TreeGenerator from "./structure/TreeGenerator.js"; +import BigTreeGenerator from "./structure/BigTreeGenerator.js"; +import Generator from "./Generator.js"; import ChunkSection from "../ChunkSection.js"; -import Block from "../block/Block.js"; -export default class WorldGenerator { +export default class WorldGenerator extends Generator { constructor(world, seed) { - this.world = world; - this.random = new Random(seed); + super(world, seed); - this.waterLevel = 64; + this.seaLevel = 64; - // Create noise for the ground height - this.groundHeightNoise = new NoiseGeneratorOctaves(this.random, 8); - this.hillNoise = new NoiseGeneratorCombined(new NoiseGeneratorOctaves(this.random, 4), - new NoiseGeneratorCombined(new NoiseGeneratorOctaves(this.random, 4), - new NoiseGeneratorOctaves(this.random, 4))); + this.caveGenerator = new CaveGenerator(world, seed); - // Water noise - this.sandInWaterNoise = new NoiseGeneratorOctaves(this.random, 8); + this.terrainGenerator4 = new NoiseGeneratorOctaves(this.random, 16); + this.terrainGenerator5 = new NoiseGeneratorOctaves(this.random, 16); + this.terrainGenerator3 = new NoiseGeneratorOctaves(this.random, 8); + this.terrainGenerator1 = new NoiseGeneratorOctaves(this.random, 10); + this.terrainGenerator2 = new NoiseGeneratorOctaves(this.random, 16); - // Hole in hills and islands - this.holeNoise = new NoiseGeneratorOctaves(this.random, 3); - this.islandNoise = new NoiseGeneratorOctaves(this.random, 3); + this.natureGenerator1 = new NoiseGeneratorOctaves(this.random, 4); + this.natureGenerator2 = new NoiseGeneratorOctaves(this.random, 4); - // Caves - this.caveNoise = new NoiseGeneratorOctaves(this.random, 8); - - // Population - this.forestNoise = new NoiseGeneratorOctaves(this.random, 8); + this.populationNoiseGenerator = new NoiseGeneratorOctaves(this.random, 8); } - generateChunk(chunk) { - // For each block in the chunk - for (let relX = 0; relX < ChunkSection.SIZE; relX++) { - for (let relZ = 0; relZ < ChunkSection.SIZE; relZ++) { + newChunk(world, chunkX, chunkZ) { + this.random.setSeed(chunkX * 0x4f9939f508 + chunkZ * 0x1ef1565bd5); - // Absolute position of the block - let x = chunk.x * ChunkSection.SIZE + relX + 10000; // TODO fix this 10000 offset - let z = chunk.z * ChunkSection.SIZE + relZ + 10000; + let chunk = new Chunk(world, chunkX, chunkZ); + let primer = new Primer(chunk); - // Extract height value of the noise - let heightValue = this.groundHeightNoise.perlin(x, z); - let hillValue = Math.max(0, this.hillNoise.perlin(x / 18, z / 18) * 6); + this.generateInChunk(chunkX, chunkZ, primer); - // Calculate final height for this position - let groundHeightY = Math.floor(heightValue / 10 + this.waterLevel + hillValue); + // Init skylight + chunk.generateSkylightMap(); + chunk.generateBlockLightMap(); - if (groundHeightY < this.waterLevel) { - // Generate water - for (let y = 0; y <= this.waterLevel; y++) { - // Use noise to place sand in water - let sandInWater = this.sandInWaterNoise.perlin(x, z) < 0; - let block = y > groundHeightY ? Block.WATER : groundHeightY - y < 3 && sandInWater ? Block.SAND : Block.STONE; + return chunk; + } - // Send water, sand and stone - chunk.setBlockAt(x & 15, y, z & 15, block.getId()); - } - } else { - // Generate height, the highest block is grass - for (let y = 0; y <= groundHeightY; y++) { - // Use the height map to determine the start of the water by shifting it - let isBeach = heightValue < 5 && y < this.waterLevel + 2; - let block = y === groundHeightY ? isBeach ? Block.SAND : Block.GRASS : groundHeightY - y < 3 ? Block.DIRT : Block.STONE; + generateInChunk(chunkX, chunkZ, primer) { + this.generateTerrain(chunkX, chunkZ, primer); + this.naturalize(chunkX, chunkZ, primer); - // Set sand, grass, dirt and stone - chunk.setBlockAt(x & 15, y, z & 15, block.getId()); - } - } - - /* - int holeY = (int) (this.holeNouse.perlin(-x / 20F, -z / 20F) * 3F + this.waterLevel + 10); - int holeHeight = (int) this.holeNouse.perlin(x / 4F, -z / 4F); - if (holeHeight > 0) { - for (int y = holeY - holeHeight; y <= holeY + holeHeight; y++) { - chunk.setBlockAt(x & 15, y, z & 15, 1); - } - } - */ - - // Random holes in hills - let holePositionY = Math.floor(this.holeNoise.perlin(-x / 20, -z / 20) * 3 + this.waterLevel + 10); - let holeHeight = Math.floor(this.holeNoise.perlin(x / 4, -z / 4)); - - if (holeHeight > 0) { - for (let y = holePositionY - holeHeight; y <= holePositionY + holeHeight; y++) { - if (y > this.waterLevel) { - chunk.setBlockAt(x & 15, y, z & 15, 0); - } - } - } - - // Floating islands - let islandPositionY = Math.floor(this.islandNoise.perlin(-x / 10, -z / 10) * 3 + this.waterLevel + 10); - let islandHeight = Math.floor(this.islandNoise.perlin(x / 4, -z / 4) * 4); - let islandRarity = Math.floor(this.islandNoise.perlin(x / 40, z / 40) * 4) - 10; - - if (islandHeight > 0 && islandRarity > 0) { - for (let y = islandPositionY - islandHeight; y <= islandPositionY + islandHeight; y++) { - let block = y === islandPositionY + islandHeight ? Block.GRASS : (islandPositionY + islandHeight) - y < 2 ? Block.DIRT : Block.STONE; - chunk.setBlockAt(x & 15, y, z & 15, block.getId()); - } - } - - // Caves - } - } + this.caveGenerator.generateInChunk(chunkX, chunkZ, primer); } populateChunk(chunkX, chunkZ) { - for (let index = 0; index < 10; index++) { - let x = this.random.nextInt(ChunkSection.SIZE); - let z = this.random.nextInt(ChunkSection.SIZE); + // Reset seed + this.random.setSeed(this.seed); - // Absolute position of the block - let absoluteX = chunkX * ChunkSection.SIZE + x; - let absoluteZ = chunkZ * ChunkSection.SIZE + z; + // Set seed for chunk + let seedX = (this.random.nextInt() / 2) * 2 + 1; + let seedZ = (this.random.nextInt() / 2) * 2 + 1; + this.random.setSeed(chunkX * seedX + chunkZ * seedZ ^ this.seed); - // Use noise for a forest pattern - let perlin = this.forestNoise.perlin(absoluteX * 10, absoluteZ * 10); - if (perlin > 0 && this.random.nextInt(2) === 0) { + // Access noise data for population + let absoluteX = chunkX * 16; + let absoluteY = chunkZ * 16; + let amount = Math.floor((this.populationNoiseGenerator.perlin(absoluteX * 0.5, absoluteY * 0.5) / 8 + this.random.nextFloat() * 4 + 4) / 3); + if (amount < 0) { + amount = 0; + } + if (this.random.nextInt(10) === 0) { + amount++; + } - // Get the highest block at this position - let highestY = this.world.getHighestBlockAt(absoluteX, absoluteZ); + // Tree generator + let bigTree = this.random.nextInt(10) === 0; + let treeSeed = this.random.seed; + let treeGenerator = bigTree ? new BigTreeGenerator(this.world, treeSeed) : new TreeGenerator(this.world, treeSeed); - // Don't place a tree if there is no grass - if (this.world.getBlockAt(absoluteX, highestY, absoluteZ) === Block.GRASS.getId() - && this.world.getBlockAt(absoluteX, highestY + 1, absoluteZ) === 0) { - let treeHeight = this.random.nextInt(2) + 5; + // Plant the trees in the chunk + for (let i = 0; i < amount; i++) { + let totalX = absoluteX + this.random.nextInt(16) + 8; + let totalZ = absoluteY + this.random.nextInt(16) + 8; + let totalY = this.world.getHeightAt(totalX, totalZ); - // Create tree log - for (let i = 0; i < treeHeight; i++) { - this.world.setBlockAt(absoluteX, highestY + i + 1, absoluteZ, Block.LOG.getId()); - } + // Generate tree at position + treeGenerator.generateAtBlock(totalX, totalY, totalZ); + } + } - // Create big leave ring - for (let tx = -2; tx <= 2; tx++) { - for (let ty = 0; ty < 2; ty++) { - for (let tz = -2; tz <= 2; tz++) { - let isCorner = Math.abs(tx) === 2 && Math.abs(tz) === 2; - if (isCorner && this.random.nextBoolean()) { - continue; + + generateTerrain(chunkX, chunkZ, primer) { + let range = 4; + let sizeX = range + 1; + let sizeZ = 17; + let factor = 1 / 4; + + // Generate terrain noise + let noise = this.generateTerrainNoise(chunkX * range, 0, chunkZ * range, sizeX, sizeZ, sizeX); + + let isSnowBiome = false; + + for (let indexX = 0; indexX < range; indexX++) { + for (let indexZ = 0; indexZ < range; indexZ++) { + for (let indexY = 0; indexY < 16; indexY++) { + let sec = 1 / 8; + + // Terrain base noise values + let noise1 = noise[(indexX * sizeX + indexZ) * sizeZ + indexY]; + let noise2 = noise[(indexX * sizeX + (indexZ + 1)) * sizeZ + indexY]; + + let noise3 = noise[((indexX + 1) * sizeX + indexZ) * sizeZ + indexY]; + let noise4 = noise[((indexX + 1) * sizeX + (indexZ + 1)) * sizeZ + indexY]; + + // Mutation noise values + let mut1 = (noise[(indexX * sizeX + indexZ) * sizeZ + (indexY + 1)] - noise1) * sec; + let mut2 = (noise[(indexX * sizeX + (indexZ + 1)) * sizeZ + (indexY + 1)] - noise2) * sec; + let mut3 = (noise[((indexX + 1) * sizeX + indexZ) * sizeZ + (indexY + 1)] - noise3) * sec; + let mut4 = (noise[((indexX + 1) * sizeX + (indexZ + 1)) * sizeZ + (indexY + 1)] - noise4) * sec; + + // For each y level of the section + for (let y = 0; y < 8; y++) { + // Take two noise values for the stone to rise + let stoneNoiseAtY1 = noise1; + let stoneNoiseAtY2 = noise2; + + // Calculate difference of the selected noise values and two other noise values + let diffNoiseY1 = (noise3 - noise1) * factor; + let diffNoiseY2 = (noise4 - noise2) * factor; + + // For each x and z coordinate of the section + for (let x = 0; x < 4; x++) { + let stoneNoise = stoneNoiseAtY1; + let diffNoiseX = (stoneNoiseAtY2 - stoneNoiseAtY1) * factor; + + for (let z = 0; z < 4; z++) { + let typeId = 0; + + // Set water if y level is below sea level + if (indexY * 8 + y < this.seaLevel) { + if (isSnowBiome && indexY * 8 + y >= this.seaLevel - 1) { + typeId = BlockRegistry.WATER.getId(); // TODO add ice block + } else { + typeId = BlockRegistry.WATER.getId(); + } } - // Place leave if there is no block yet - if (!this.world.isSolidBlockAt(absoluteX + tx, highestY + treeHeight + ty - 2, absoluteZ + tz)) { - this.world.setBlockAt(absoluteX + tx, highestY + treeHeight + ty - 2, absoluteZ + tz, Block.LEAVE.getId()); + // Let the terrain rise out of the water + if (stoneNoise > 0.0) { + typeId = BlockRegistry.STONE.getId(); } + + //Set target type id + primer.set(indexX * 4 + x, indexY * 8 + y, indexZ * 4 + z, typeId); + + // Increase noise by noise x difference + stoneNoise += diffNoiseX; } - } - } - // Create small leave ring on top - for (let tx = -1; tx <= 1; tx++) { - for (let ty = 0; ty < 2; ty++) { - for (let tz = -1; tz <= 1; tz++) { - let isCorner = Math.abs(tx) === 1 && Math.abs(tz) === 1; - if (isCorner && (ty === 1 || this.random.nextBoolean())) { - continue; - } - - // Place leave if there is no block yet - if (!this.world.isSolidBlockAt(absoluteX + tx, highestY + treeHeight + ty, absoluteZ + tz)) { - this.world.setBlockAt(absoluteX + tx, highestY + treeHeight + ty, absoluteZ + tz, Block.LEAVE.getId()); - } - } + // Increase noise by noise y differences + stoneNoiseAtY1 += diffNoiseY1; + stoneNoiseAtY2 += diffNoiseY2; } + + // Mutate noise values + noise1 += mut1; + noise2 += mut2; + noise3 += mut3; + noise4 += mut4; } } } } - } + + naturalize(chunkX, chunkZ, primer) { + let strength = 1 / 32; + let chunkSize = ChunkSection.SIZE; + + // Generate noise for nature painting + let natureNoise1 = this.natureGenerator1.generateNoiseOctaves( + chunkX * chunkSize, chunkZ * chunkSize, + 0.0, chunkSize, chunkSize, 1, + strength, strength, 1.0 + ); + let natureNoise2 = this.natureGenerator1.generateNoiseOctaves( + chunkZ * chunkSize, 109.0134, chunkX * chunkSize, + chunkSize, 1, chunkSize, + strength, 1.0, strength); + let natureNoise3 = this.natureGenerator2.generateNoiseOctaves( + chunkX * chunkSize, chunkZ * chunkSize, 0.0, + chunkSize, chunkSize, 1, + strength * 2, strength * 2, strength * 2 + ); + + // Paint entire chunk with nature blocks + for (let x = 0; x < 16; x++) { + for (let z = 0; z < 16; z++) { + // Pull noise values for patches + let sandPatchNoise = natureNoise1[x + z * 16] + this.random.nextFloat() * 0.2 > 0; + let gravelPatchNoise = natureNoise2[x + z * 16] + this.random.nextFloat() * 0.2 > 3; + let stonePatchNoise = (natureNoise3[x + z * 16] / 3 + 3 + this.random.nextFloat() * 0.25); + + let prevStonePatchNoise = -1; + + // Default layer type ids + let topLayerTypeId = BlockRegistry.GRASS.getId(); + let innerLayerTypeId = BlockRegistry.DIRT.getId(); + + // For the entire height of the chunk + for (let y = 127; y >= 0; y--) { + // Set bedrock on floor level + if (y <= (this.random.nextInt(6)) - 1) { + primer.set(x, y, z, BlockRegistry.STONE.getId()); // TODO add bedrock block + continue; + } + + // Get block type at current position + let typeIdAt = primer.get(x, y, z); + + // Ignore air block + if (typeIdAt === 0) { + prevStonePatchNoise = -1; + continue; + } + + // Check if it's a stone block + if (typeIdAt !== BlockRegistry.STONE.getId()) { + continue; + } + + // Check if previous iteration was an air block + if (prevStonePatchNoise === -1) { + if (stonePatchNoise <= 0) { + // Keep the stone + topLayerTypeId = 0; + innerLayerTypeId = BlockRegistry.STONE.getId(); + } else if (y >= this.seaLevel - 4 && y <= this.seaLevel + 1) { + // Fallback is grass and dirt + topLayerTypeId = BlockRegistry.GRASS.getId(); + innerLayerTypeId = BlockRegistry.DIRT.getId(); + + // Add gravel patches + if (gravelPatchNoise) { + topLayerTypeId = 0; + innerLayerTypeId = BlockRegistry.STONE.getId(); // TODO add gravel block + } + + // Add sand patches + if (sandPatchNoise) { + topLayerTypeId = BlockRegistry.SAND.getId(); + innerLayerTypeId = BlockRegistry.SAND.getId(); + } + } + + // Set water if it's below the sea level + if (y < this.seaLevel && topLayerTypeId === 0) { + topLayerTypeId = BlockRegistry.WATER.getId(); // TODO add water moving block + } + + // Set flag that we hit a block + prevStonePatchNoise = stonePatchNoise; + + // Set grass or dirt type depending on sea level height + if (y >= this.seaLevel - 1) { + primer.set(x, y, z, topLayerTypeId); + } else { + primer.set(x, y, z, innerLayerTypeId); + } + continue; + } + + // Set further inner layer blocks + if (prevStonePatchNoise > 0) { + prevStonePatchNoise--; + primer.set(x, y, z, innerLayerTypeId); + } + } + } + } + } + + generateTerrainNoise(noiseX, noiseY, noiseZ, width, height, depth) { + let strength = 684.412; + + // Generate terrain noise + let terrainNoise1 = this.terrainGenerator1.generateNoiseOctaves(noiseX, noiseY, noiseZ, width, 1, depth, 1.0, 0.0, 1.0); + let terrainNoise2 = this.terrainGenerator2.generateNoiseOctaves(noiseX, noiseY, noiseZ, width, 1, depth, 100, 0.0, 100); + let terrainNoise3 = this.terrainGenerator3.generateNoiseOctaves(noiseX, noiseY, noiseZ, width, height, depth, strength / 80, strength / 160, strength / 80); + let terrainNoise4 = this.terrainGenerator4.generateNoiseOctaves(noiseX, noiseY, noiseZ, width, height, depth, strength, strength, strength); + let terrainNoise5 = this.terrainGenerator5.generateNoiseOctaves(noiseX, noiseY, noiseZ, width, height, depth, strength, strength, strength); + + // Output noise + let output = []; + + let index = 0; + let id = 0; + + // For each x, z coordinate + for (let x = 0; x < width; x++) { + for (let z = 0; z < depth; z++) { + let out1 = (terrainNoise1[id] + 256) / 512; + if (out1 > 1.0) { + out1 = 1.0; + } + + let maxY = 0.0; + let out2 = terrainNoise2[id] / 8000; + + if (out2 < 0.0) { + out2 = -out2; + } + + out2 = out2 * 3 - 3; + + if (out2 < 0.0) { + out2 /= 2; + + if (out2 < -1) { + out2 = -1; + } + + out2 /= 1.4; + out2 /= 2; + out1 = 0.0; + } else { + if (out2 > 1.0) { + out2 = 1.0; + } + out2 /= 6; + } + + out1 += 0.5; + out2 = (out2 * height) / 16; + id++; + + let h = height / 2 + out2 * 4; + + // Y loop + for (let y = 0; y < height; y++) { + let noise = 0; + let value = ((y - h) * 12) / out1; + + if (value < 0.0) { + value *= 4; + } + + let out4 = terrainNoise4[index] / 512; + let out5 = terrainNoise5[index] / 512; + let out3 = (terrainNoise3[index] / 10 + 1.0) / 2; + + if (out3 < 0.0) { + noise = out4; + } else if (out3 > 1.0) { + noise = out5; + } else { + noise = out4 + (out5 - out4) * out3; + } + + noise -= value; + + if (y > height - 4) { + let diff = (y - (height - 4)) / 3; + noise = noise * (1.0 - diff) + -10 * diff; + } + + if (y < maxY) { + let diff = (maxY - y) / 4; + + if (diff < 0.0) { + diff = 0.0; + } + + if (diff > 1.0) { + diff = 1.0; + } + noise = noise * (1.0 - diff) + -10 * diff; + } + + // Add noise to array and increase index + output[index] = noise; + index++; + } + } + } + + return output; + } + } \ No newline at end of file diff --git a/src/js/net/minecraft/client/world/generator/noise/NoiseGeneratorOctaves.js b/src/js/net/minecraft/client/world/generator/noise/NoiseGeneratorOctaves.js index 0948459..5299590 100644 --- a/src/js/net/minecraft/client/world/generator/noise/NoiseGeneratorOctaves.js +++ b/src/js/net/minecraft/client/world/generator/noise/NoiseGeneratorOctaves.js @@ -24,4 +24,28 @@ export default class NoiseGeneratorOctaves extends NoiseGenerator { return total; } + generateNoiseOctaves(x1, y1, z1, x2, y2, z2, strengthX, strengthY, strengthZ) { + let length = x2 * y2 * z2; + let noise = []; + for (let i = 0; i < length; i++) { + noise[i] = 0; + } + + let frequency = 1.0; + for (let i = 0; i < this.octaves; i++) { + this.generatorCollection[i].combined( + noise, + x1, y1, z1, + x2, y2, z2, + strengthX * frequency, + strengthY * frequency, + strengthZ * frequency, + frequency + ); + frequency /= 2; + } + + return noise; + } + } \ No newline at end of file diff --git a/src/js/net/minecraft/client/world/generator/noise/NoiseGeneratorPerlin.js b/src/js/net/minecraft/client/world/generator/noise/NoiseGeneratorPerlin.js index c7b6043..2b728b2 100644 --- a/src/js/net/minecraft/client/world/generator/noise/NoiseGeneratorPerlin.js +++ b/src/js/net/minecraft/client/world/generator/noise/NoiseGeneratorPerlin.js @@ -5,6 +5,10 @@ export default class NoiseGeneratorPerlin extends NoiseGenerator { constructor(random) { super(); + this.offsetX = random.nextFloat() * 256; + this.offsetY = random.nextFloat() * 256; + this.offsetZ = random.nextFloat() * 256; + this.permutations = []; for (let i = 0; i < 256; i++) { this.permutations[i] = i; @@ -46,42 +50,150 @@ export default class NoiseGeneratorPerlin extends NoiseGenerator { } perlin(x, z) { - let y; + return this.perlinXYZ(x, z, 0); + } - let xi = Math.floor(x) & 0xFF; - let zi = Math.floor(z) & 0xFF; - let yi = Math.floor(0.0) & 0xFF; + perlinXYZ(x, y, z) { + let shiftX = x + this.offsetX; + let shiftY = y + this.offsetY; + let shiftZ = z + this.offsetZ; - x -= Math.floor(x); - z -= Math.floor(z); - y = 0.0 - Math.floor(0.0); + let floorX = Math.floor(shiftX); + let floorY = Math.floor(shiftY); + let floorZ = Math.floor(shiftZ); - let u = this.fade(x); - let w = this.fade(z); - let v = this.fade(y); + if (shiftX < floorX) { + floorX--; + } + if (shiftY < floorY) { + floorY--; + } + if (shiftZ < floorZ) { + floorZ--; + } - let xzi = this.permutations[xi] + zi; - let xzyi = this.permutations[xzi] + yi; + let x1 = floorX & 0xff; + let y1 = floorY & 0xff; + let z1 = floorZ & 0xff; - xzi = this.permutations[xzi + 1] + yi; - xi = this.permutations[xi + 1] + zi; - zi = this.permutations[xi] + yi; - xi = this.permutations[xi + 1] + yi; + shiftX -= floorX; + shiftY -= floorY; + shiftZ -= floorZ; + + let u = this.fade(shiftX); + let w = this.fade(shiftY); + let v = this.fade(shiftZ); + + let xy = this.permutations[x1] + y1; + let xyz = this.permutations[xy] + z1; + + let xy1z = this.permutations[xy + 1] + z1; + let xi = this.permutations[x1 + 1] + y1; + let yi = this.permutations[xi] + z1; + let zi = this.permutations[xi + 1] + z1; return this.lerp(v, this.lerp(w, this.lerp(u, - this.grad(this.permutations[xzyi], x, z, y), - this.grad(this.permutations[zi], x - 1.0, z, y)), + this.grad(this.permutations[xyz], shiftX, shiftY, shiftZ), + this.grad(this.permutations[yi], shiftX - 1.0, shiftY, shiftZ)), this.lerp(u, - this.grad(this.permutations[xzi], x, z - 1.0, y), - this.grad(this.permutations[xi], x - 1.0, z - 1.0, y))), + this.grad(this.permutations[xy1z], shiftX, shiftY - 1.0, shiftZ), + this.grad(this.permutations[zi], shiftX - 1.0, shiftY - 1.0, shiftZ))), this.lerp(w, this.lerp(u, - this.grad(this.permutations[xzyi + 1], x, z, y - 1.0), - this.grad(this.permutations[zi + 1], x - 1.0, z, y - 1.0)), + this.grad(this.permutations[xyz + 1], shiftX, shiftY, shiftZ - 1.0), + this.grad(this.permutations[yi + 1], shiftX - 1.0, shiftY, shiftZ - 1.0)), this.lerp(u, - this.grad(this.permutations[xzi + 1], x, z - 1.0, y - 1.0), - this.grad(this.permutations[xi + 1], x - 1.0, z - 1.0, y - 1.0)))); + this.grad(this.permutations[xy1z + 1], shiftX, shiftY - 1.0, shiftZ - 1.0), + this.grad(this.permutations[zi + 1], shiftX - 1.0, shiftY - 1.0, shiftZ - 1.0)))); + } + + combined(noise, x1, y1, z1, x2, y2, z2, strengthX, strengthY, strengthZ, frequency) { + let index = 0; + let invertFrequency = 1.0 / frequency; + let prevY3 = -1; + + // Output values + let output1 = 0; + let output2 = 0; + let output3 = 0; + let output4 = 0; + + // X loop + for (let x = 0; x < x2; x++) { + let shiftX = (x1 + x) * strengthX + this.offsetX; + let floorX = Math.floor(shiftX); + + if (shiftX < floorX) { + floorX--; + } + + let x3 = floorX & 0xff; + shiftX -= floorX; + + // Z loop + let u = this.fade(shiftX); + for (let z = 0; z < z2; z++) { + let shiftZ = (z1 + z) * strengthZ + this.offsetZ; + let floorZ = Math.floor(shiftZ); + + if (shiftZ < floorZ) { + floorZ--; + } + + let z3 = floorZ & 0xff; + shiftZ -= floorZ; + + // Y loop + let w = this.fade(shiftZ); + for (let y = 0; y < y2; y++) { + let shiftY = (y1 + y) * strengthY + this.offsetY; + let floorY = Math.floor(shiftY); + + if (shiftY < floorY) { + floorY--; + } + + let y3 = floorY & 0xff; + shiftY -= floorY; + + let v = this.fade(shiftY); + + // Check if y changed + if (y === 0 || y3 !== prevY3) { + prevY3 = y3; + + let xy = this.permutations[x3] + y3; + let xyz = this.permutations[xy] + z3; + + let xy1z = this.permutations[xy + 1] + z3; + let xi = this.permutations[x3 + 1] + y3; + let yi = this.permutations[xi] + z3; + let zi = this.permutations[xi + 1] + z3; + + output1 = this.lerp(u, + this.grad(this.permutations[xyz], shiftX, shiftY, shiftZ), + this.grad(this.permutations[yi], shiftX - 1.0, shiftY, shiftZ)); + output2 = this.lerp(u, + this.grad(this.permutations[xy1z], shiftX, shiftY - 1.0, shiftZ), + this.grad(this.permutations[zi], shiftX - 1.0, shiftY - 1.0, shiftZ)); + output3 = this.lerp(u, + this.grad(this.permutations[xyz + 1], shiftX, shiftY, shiftZ - 1.0), + this.grad(this.permutations[yi + 1], shiftX - 1.0, shiftY, shiftZ - 1.0)); + output4 = this.lerp(u, + this.grad(this.permutations[xy1z + 1], shiftX, shiftY - 1.0, shiftZ - 1.0), + this.grad(this.permutations[zi + 1], shiftX - 1.0, shiftY - 1.0, shiftZ - 1.0)); + } + + let output5 = this.lerp(v, output1, output2); + let output6 = this.lerp(v, output3, output4); + + // Add final output to noise array + let output = this.lerp(w, output5, output6); + noise[index++] += output * invertFrequency; + } + } + } } } \ No newline at end of file diff --git a/src/js/net/minecraft/client/world/generator/structure/BigTreeGenerator.js b/src/js/net/minecraft/client/world/generator/structure/BigTreeGenerator.js new file mode 100644 index 0000000..76efd12 --- /dev/null +++ b/src/js/net/minecraft/client/world/generator/structure/BigTreeGenerator.js @@ -0,0 +1,391 @@ +import Generator from "../Generator.js"; +import {BlockRegistry} from "../../block/BlockRegistry.js"; + +export default class BigTreeGenerator extends Generator { + + constructor(world, seed) { + super(world, seed); + + this.heightLimit = 0; + this.heightAttenuation = 0.617; + this.branchSlope = 0.381; + this.scaleWidth = 1.0; + this.leafDensity = 1.0; + this.trunkSize = 1; + this.heightLimitLimit = 12; + this.offsetY = 4; + + this.coords = [] + this.types = [2, 0, 0, 1, 2, 1] + this.nodes = []; + } + + generateAtBlock(x, y, z) { + let seed = this.random.nextInt(); + this.random.setSeed(seed); + + this.coords[0] = x; + this.coords[1] = y; + this.coords[2] = z; + + if (this.heightLimit === 0) { + this.heightLimit = 5 + this.random.nextInt(this.heightLimitLimit); + } + + if (!this.validTreeLocation()) { + return false; + } else { + this.generateLeafNodeList(); + this.generateLeafNodes(); + this.generateTrunk(); + this.generateLeafNodeBases(); + return true; + } + } + + validTreeLocation() { + let minCoords = [this.coords[0], this.coords[1], this.coords[2]]; + let maxCoords = [this.coords[0], (this.coords[1] + this.heightLimit) - 1, this.coords[2]]; + + let typeId = this.world.getBlockAt(this.coords[0], this.coords[1] - 1, this.coords[2]); + if (typeId !== 2 && typeId !== 3) { + return false; + } + + let blockLine = this.checkBlockLine(minCoords, maxCoords); + if (blockLine === -1) { + return true; + } + + if (blockLine < 6) { + return false; + } else { + this.heightLimit = blockLine; + return true; + } + } + + checkBlockLine(minCoords, maxCoords) { + let dimension = [0, 0, 0] + let index = 0; + + // Find target index for dimension + let targetIndex = 0; + for (; index < 3; index++) { + dimension[index] = maxCoords[index] - minCoords[index]; + if (Math.abs(dimension[index]) > Math.abs(dimension[targetIndex])) { + targetIndex = index; + } + } + + // Invalid dimension + if (dimension[targetIndex] === 0) { + return -1; + } + + // Get dimension index by target index + let widthIndex = this.types[targetIndex]; + let heightIndex = this.types[targetIndex + 3]; + + // Determine offset + let offset; + if (dimension[targetIndex] > 0) { + offset = 1; + } else { + offset = -1; + } + + // Get width and height + let width = dimension[widthIndex] / dimension[targetIndex]; + let height = dimension[heightIndex] / dimension[targetIndex]; + + let indexBlock = 0; + let breakIndex = dimension[targetIndex] + offset; + + do { + if (indexBlock === breakIndex) { + break; + } + + let coords = [0, 0, 0] + coords[targetIndex] = minCoords[targetIndex] + indexBlock; + coords[widthIndex] = Math.floor(minCoords[widthIndex] + indexBlock * width); + coords[heightIndex] = Math.floor(minCoords[heightIndex] + indexBlock * height); + + // Check for collision with objects except leaves + let typeId = this.world.getBlockAt(coords[0], coords[1], coords[2]); + if (typeId !== 0 && typeId !== BlockRegistry.LEAVE.getId()) { + break; + } + + indexBlock += offset; + } while (true); + + if (indexBlock === breakIndex) { + return -1; + } else { + return Math.abs(indexBlock); + } + } + + generateLeafNodeList() { + this.height = Math.floor(this.heightLimit * this.heightAttenuation); + if (this.height >= this.heightLimit) { + this.height = this.heightLimit - 1; + } + + let spheres = Math.floor(1.38 + Math.pow((this.leafDensity * this.heightLimit) / 13, 2)); + if (spheres < 1) { + spheres = 1; + } + + // Create 2D array with max possible nodes + let array = []; + for (let i = 0; i < spheres * this.heightLimit; i++) { + array[i] = []; + } + + let minY = (this.coords[1] + this.heightLimit) - this.offsetY; + let maxY = this.coords[1] + this.height; + + // Start point + let y = minY - this.coords[1]; + + // Fill array + array[0][0] = this.coords[0]; // x + array[0][1] = minY; + array[0][2] = this.coords[2]; // z + array[0][3] = maxY; + + // Shift + minY--; + + let nodeIndex = 1; + while (y >= 0) { + let sphereIndex = 0; + let size = this.layerSize(y); + + if (size < 0.0) { + minY--; + y--; + } else { + let offsetXZ = 0.5; + + // Create multiple spheres (nodes) + for (; sphereIndex < spheres; sphereIndex++) { + let finalSize = this.scaleWidth * (size * (this.random.nextFloat() + 0.328)); + let rotation = this.random.nextFloat() * 2 * Math.PI; + + let x = Math.floor(finalSize * Math.sin(rotation) + this.coords[0] + offsetXZ); + let z = Math.floor(finalSize * Math.cos(rotation) + this.coords[2] + offsetXZ); + + let from = [x, minY, z]; + let to = [x, minY + this.offsetY, z]; + + // Check if node is in a valid spot + if (this.checkBlockLine(from, to) !== -1) { + continue; + } + + let coords = [this.coords[0], this.coords[1], this.coords[2]]; + let factor = Math.sqrt(Math.pow(Math.abs(this.coords[0] - from[0]), 2) + Math.pow(Math.abs(this.coords[2] - from[2]), 2)); + let slope = factor * this.branchSlope; + + if (from[1] - slope > maxY) { + coords[1] = maxY; + } else { + coords[1] = Math.floor(from[1] - slope); + } + + // Fill nodes + if (this.checkBlockLine(coords, from) === -1) { + array[nodeIndex][0] = x; + array[nodeIndex][1] = minY; + array[nodeIndex][2] = z; + array[nodeIndex][3] = coords[1]; + nodeIndex++; + } + } + + minY--; + y--; + } + } + + // Store nodes + this.nodes = []; + for (let i = 0; i < nodeIndex; i++) { + this.nodes[i] = array[i]; + } + } + + layerSize(y) { + if (y < this.heightLimit * 0.29) { + return -1.618; + } + + let halfY = this.heightLimit / 2.0; + let distance = this.heightLimit / 2.0 - y; + + let size; + if (distance === 0.0) { + size = halfY; + } else if (Math.abs(distance) >= halfY) { + size = 0.0; + } else { + size = Math.sqrt(Math.pow(Math.abs(halfY), 2) - Math.pow(Math.abs(distance), 2)); + } + + size *= 0.5; + return size; + } + + + generateLeafNode(x, y, z) { + for (let startY = y; startY < y + this.offsetY; startY++) { + let size = this.leafSize(startY - y); + this.generateNodeByType(x, startY, z, size, 1, BlockRegistry.LEAVE.getId()); + } + } + + leafSize(y) { + if (y < 0 || y >= this.offsetY) { + return -1; + } + return y !== 0 && y !== this.offsetY - 1 ? 3 : 2.0; + } + + generateNodeByType(x, y, z, size, type, typeId) { + let actualSize = Math.floor(size + 0.618); + let typeIndex1 = this.types[type]; + let typeIndex2 = this.types[type + 3]; + + let origin = [x, y, z]; + let coords = [0, 0, 0]; + coords[type] = origin[type]; + + for (let i = -actualSize; i <= actualSize; i++) { + coords[typeIndex1] = origin[typeIndex1] + i; + + for (let radius = -actualSize; radius <= actualSize;) { + let distance = Math.sqrt(Math.pow(Math.abs(i) + 0.5, 2) + Math.pow(Math.abs(radius) + 0.5, 2)); + + if (distance > size) { + radius++; + } else { + coords[typeIndex2] = origin[typeIndex2] + radius; + + let typeAt = this.world.getBlockAt(coords[0], coords[1], coords[2]); + if (typeAt !== 0 && typeAt !== BlockRegistry.LEAVE.getId()) { + radius++; + } else { + this.world.setBlockAt(coords[0], coords[1], coords[2], typeId); + radius++; + } + } + } + } + } + + generateLeafNodes() { + for (let i = 0; i < this.nodes.length; i++) { + let x = this.nodes[i][0]; + let y = this.nodes[i][1]; + let z = this.nodes[i][2]; + this.generateLeafNode(x, y, z); + } + } + + generateTrunk() { + let x = this.coords[0]; + let minY = this.coords[1]; + let maxY = this.coords[1] + this.height; + let z = this.coords[2]; + + let coordsMin = [x, minY, z]; + let coordsMax = [x, maxY, z]; + + // Place normal trunk blocks + let logTypeId = BlockRegistry.LOG.getId(); + this.setBlocks(coordsMin, coordsMax, logTypeId); + + // Increase trunk size + if (this.trunkSize === 2) { + coordsMin[0]++; + coordsMax[0]++; + this.setBlocks(coordsMin, coordsMax, logTypeId); + coordsMin[2]++; + coordsMax[2]++; + this.setBlocks(coordsMin, coordsMax, logTypeId); + coordsMin[0]--; + coordsMax[0]--; + this.setBlocks(coordsMin, coordsMax, logTypeId); + } + } + + setBlocks(minCoords, maxCoords, typeId) { + let coords = [0, 0, 0]; + let index = 0; + + // Find target index for dimension + let targetIndex = 0; + for (; index < 3; index++) { + coords[index] = maxCoords[index] - minCoords[index]; + if (Math.abs(coords[index]) > Math.abs(coords[targetIndex])) { + targetIndex = index; + } + } + + // Invalid dimension + if (coords[targetIndex] === 0) { + return; + } + + // Get dimension index by target index + let widthIndex = this.types[targetIndex]; + let heightIndex = this.types[targetIndex + 3]; + + let offset; + if (coords[targetIndex] > 0) { + offset = 1; + } else { + offset = -1; + } + + let width = coords[widthIndex] / coords[targetIndex]; + let height = coords[heightIndex] / coords[targetIndex]; + + // Fill area with blocks + for (let indexBlock = 0; indexBlock !== coords[targetIndex] + offset; indexBlock += offset) { + let coords2 = [0, 0, 0]; + coords2[targetIndex] = Math.floor((minCoords[targetIndex] + indexBlock) + 0.5); + coords2[widthIndex] = Math.floor(minCoords[widthIndex] + indexBlock * width + 0.5); + coords2[heightIndex] = Math.floor(minCoords[heightIndex] + indexBlock * height + 0.5); + this.world.setBlockAt(coords2[0], coords2[1], coords2[2], typeId); + } + } + + generateLeafNodeBases() { + let length = this.nodes.length; + let minCoords = [this.coords[0], this.coords[1], this.coords[2]]; + + for (let i = 0; i < length; i++) { + let node = this.nodes[i]; + let maxCoords = [node[0], node[1], node[2]]; + + // Override y coordinate + minCoords[1] = node[3]; + + // Set log in middle of sphere + let y = minCoords[1] - this.coords[1]; + if (this.leafNodeNeedsBase(y)) { + this.setBlocks(minCoords, maxCoords, BlockRegistry.LOG.getId()); + } + } + } + + leafNodeNeedsBase(y) { + return y >= this.heightLimit * 0.2; + } + +} \ No newline at end of file diff --git a/src/js/net/minecraft/client/world/generator/structure/CaveGenerator.js b/src/js/net/minecraft/client/world/generator/structure/CaveGenerator.js new file mode 100644 index 0000000..b20bba7 --- /dev/null +++ b/src/js/net/minecraft/client/world/generator/structure/CaveGenerator.js @@ -0,0 +1,252 @@ +import Random from "../../../../util/Random.js"; +import {BlockRegistry} from "../../block/BlockRegistry.js"; +import Generator from "../Generator.js"; + +export default class CaveGenerator extends Generator { + + constructor(world, seed) { + super(world, seed); + + this.chunkRange = 8; + } + + generateInChunk(originChunkX, originChunkZ, primer) { + // Reset seed + this.random.setSeed(this.seed); + + let offset = this.chunkRange; + let seedX = (this.random.nextInt() / 2) * 2 + 1; + let seedZ = (this.random.nextInt() / 2) * 2 + 1; + + // Generate entire cave over 16x16 chunk area + for (let chunkX = originChunkX - offset; chunkX <= originChunkX + offset; chunkX++) { + for (let chunkZ = originChunkZ - offset; chunkZ <= originChunkZ + offset; chunkZ++) { + // Set seed for position + this.random.setSeed(chunkX * seedX + chunkZ * seedZ ^ this.seed); + + // Generate entire cave + this.generateCave(chunkX, chunkZ, originChunkX, originChunkZ, primer); + } + } + } + + generateCave(chunkX, chunkZ, originChunkX, originChunkZ, primer) { + let num = this.random.nextInt(this.random.nextInt(this.random.nextInt(40) + 1) + 1); + if (this.random.nextInt(15) !== 0) { + num = 0; + } + + // Generate multiple caves in the given area + for (let i = 0; i < num; i++) { + let x = chunkX * 16 + this.random.nextInt(16); + let y = this.random.nextInt(this.random.nextInt(120) + 8); + let z = chunkZ * 16 + this.random.nextInt(16); + + let amount = 1; + + if (this.random.nextInt(4) === 0) { + this.generateBasicCaveAtBlock(originChunkX, originChunkZ, primer, x, y, z); + amount += this.random.nextInt(4); + } + + for (let j = 0; j < amount; j++) { + let num1 = this.random.nextFloat() * Math.PI * 2.0; + let num2 = ((this.random.nextFloat() - 0.5) * 2.0) / 8; + let num3 = this.random.nextFloat() * 2.0 + this.random.nextFloat(); + this.generateCaveAtBlock(originChunkX, originChunkZ, primer, x, y, z, num3, num1, num2, 0, 0, 1.0); + } + } + } + + generateBasicCaveAtBlock(originChunkX, originChunkZ, primer, x, y, z) { + this.generateCaveAtBlock(originChunkX, originChunkZ, primer, + x, y, z, + 1.0 + this.random.nextFloat() * 6, + 0.0, 0.0, + -1, -1, 0.5 + ); + } + + generateCaveAtBlock(originChunkX, originChunkZ, primer, absoluteX, absoluteY, absoluteZ, amplitude, rotation2, rotation1, progress, distance, strength) { + let centerX = originChunkX * 16 + 8; + let centerZ = originChunkZ * 16 + 8; + + let motion2 = 0; + let motion1 = 0; + + let random = new Random(this.random.nextInt()); + if (distance <= 0) { + let i1 = this.chunkRange * 16 - 16; + distance = i1 - random.nextInt(i1 / 4); + } + + let isBeginning = false; + if (progress === -1) { + progress = distance / 2; + isBeginning = true; + } + + let maxProgress = random.nextInt(distance / 2) + distance / 4; + let isStrong = random.nextInt(6) === 0; + + for (; progress < distance; progress++) { + let value = 1.5 + (Math.sin((progress * Math.PI) / distance) * amplitude); + let valueWithStrength = value * strength; + + let cos = Math.cos(rotation1); + let sin = Math.sin(rotation1); + + absoluteX += Math.cos(rotation2) * cos; + absoluteY += sin; + absoluteZ += Math.sin(rotation2) * cos; + + if (isStrong) { + rotation1 *= 0.92; + } else { + rotation1 *= 0.7; + } + + rotation1 += motion1 * 0.1; + rotation2 += motion2 * 0.1; + + motion1 *= 0.9; + motion2 *= 0.75; + + motion1 += (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0; + motion2 += (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4; + + if (!isBeginning && progress === maxProgress && amplitude > 1.0) { + this.generateCaveAtBlock( + originChunkX, originChunkZ, primer, + absoluteX, absoluteY, absoluteZ, + random.nextFloat() * 0.5 + 0.5, rotation2 - 1.570796, rotation1 / 3, progress, distance, 1.0 + ); + this.generateCaveAtBlock( + originChunkX, originChunkZ, primer, + absoluteX, absoluteY, absoluteZ, + random.nextFloat() * 0.5 + 0.5, rotation2 + 1.570796, rotation1 / 3, progress, distance, 1.0 + ); + return; + } + + if (!isBeginning && random.nextInt(4) === 0) { + continue; + } + + let distanceToCenterX = absoluteX - centerX; + let distanceToCenterY = absoluteZ - centerZ; + + let progressInvert = distance - progress; + let shiftedAmplitude = amplitude + 2.0 + 16; + + if ((distanceToCenterX * distanceToCenterX + distanceToCenterY * distanceToCenterY) + - progressInvert * progressInvert > shiftedAmplitude * shiftedAmplitude) { + return; + } + + if (absoluteX < centerX - 16 - value * 2 + || absoluteZ < centerZ - 16 - value * 2 + || absoluteX > centerX + 16 + value * 2 + || absoluteZ > centerZ + 16 + value * 2) { + continue; + } + + distanceToCenterX = Math.floor(absoluteX - value) - originChunkX * 16 - 1; + let layerX = (Math.floor(absoluteX + value) - originChunkX * 16) + 1; + + distanceToCenterY = Math.floor(absoluteY - valueWithStrength) - 1; + let layerY = Math.floor(absoluteY + valueWithStrength) + 1; + + progressInvert = Math.floor(absoluteZ - value) - originChunkZ * 16 - 1; + let layerZ = (Math.floor(absoluteZ + value) - originChunkZ * 16) + 1; + + distanceToCenterX = Math.max(distanceToCenterX, 0); + layerX = Math.min(layerX, 16); + distanceToCenterY = Math.max(distanceToCenterY, 1); + layerY = Math.min(layerY, 120); + progressInvert = Math.max(progressInvert, 0); + layerZ = Math.min(layerZ, 16); + + let isWaterCave = false; + + // Check for water + for (let x = Math.floor(distanceToCenterX); !isWaterCave && x < layerX; x++) { + for (let z = Math.floor(progressInvert); !isWaterCave && z < layerZ; z++) { + for (let y = layerY + 1; !isWaterCave && y >= distanceToCenterY - 1; y--) { + if (y < 0 || y >= 128) { + continue; + } + + // Get current block + let typeId = primer.get(x, y, z) + + // Check if we have water in here + if (typeId === BlockRegistry.WATER.getId() || typeId === BlockRegistry.WATER.getId()) { // TODO one of them WATER MOVING + isWaterCave = true; + } + + if (y !== distanceToCenterY - 1 && x !== distanceToCenterX && x !== layerX - 1 && z !== progressInvert && z !== layerZ - 1) { + y = Math.floor(distanceToCenterY); + } + } + } + } + if (isWaterCave) { + continue; + } + + // For each block on layer x + for (let x = Math.floor(distanceToCenterX); x < layerX; x++) { + let offsetX = (((x + originChunkX * 16) + 0.5) - absoluteX) / value; + + // For each block on layer z + for (let z = Math.floor(progressInvert); z < layerZ; z++) { + let offsetZ = (((z + originChunkZ * 16) + 0.5) - absoluteZ) / value; + + let totalY = layerY; + let hasGrass = false; + + // For each block on layer y + for (let y = layerY - 1; y >= distanceToCenterY; y--) { + let offsetY = ((y + 0.5) - absoluteY) / valueWithStrength; + + // Check if the offset vector length is below 1 + if (offsetY > -0.7 && offsetX * offsetX + offsetY * offsetY + offsetZ * offsetZ < 1.0) { + let typeId = primer.get(x, totalY, z) + + // We found grass + if (typeId === BlockRegistry.GRASS.getId()) { + hasGrass = true; + } + + // Check if we can create a cave here + if (typeId === BlockRegistry.STONE.getId() || typeId === BlockRegistry.DIRT.getId() || typeId === BlockRegistry.GRASS.getId()) { + + if (y < 10) { + // Lava cave + primer.set(x, totalY, z, BlockRegistry.WATER.getId()); // TODO LAVA STILL + } else { + // Normal cave + primer.set(x, totalY, z, 0); + + // Grow grass if we have dirt in here + if (hasGrass && primer.get(x, totalY - 1, z) === BlockRegistry.DIRT.getId()) { + primer.set(x, totalY - 1, z, BlockRegistry.GRASS.getId()); + } + } + } + } + + // Go further down + totalY--; + } + } + } + + if (isBeginning) { + break; + } + } + } + +} \ No newline at end of file diff --git a/src/js/net/minecraft/client/world/generator/structure/TreeGenerator.js b/src/js/net/minecraft/client/world/generator/structure/TreeGenerator.js new file mode 100644 index 0000000..20600a5 --- /dev/null +++ b/src/js/net/minecraft/client/world/generator/structure/TreeGenerator.js @@ -0,0 +1,88 @@ +import {BlockRegistry} from "../../block/BlockRegistry.js"; +import Generator from "../Generator.js"; + +export default class TreeGenerator extends Generator { + + constructor(world, seed) { + super(world, seed); + + this.size = 8; + } + + generateAtBlock(x, y, z) { + // Generate random height + let height = this.random.nextInt(3) + 4; + if (y < 1 || y + height + 1 > 128) { + return false; + } + + // Check if we have enough space for the tree to grow + for (let totalY = y; totalY <= y + 1 + height; totalY++) { + let radius = 1; + + // Define radius of the tree depending on the height + if (totalY === y) { + radius = 0; + } + if (totalY >= (y + 1 + height) - 2) { + radius = 2; + } + + // Scan radius + for (let totalX = x - radius; totalX <= x + radius; totalX++) { + for (let totalZ = z - radius; totalZ <= z + radius; totalZ++) { + if (totalY >= 0 && totalY < 128) { + let typeId = this.world.getBlockAt(totalX, totalY, totalZ); + + // Check if we have just air or leaves in the way + if (typeId !== 0 && typeId !== BlockRegistry.LEAVE.getId()) { + return; + } + } else { + return; + } + } + } + } + + // Check if tree can grow here + let typeIdBelowTree = this.world.getBlockAt(x, y - 1, z); + if (typeIdBelowTree !== BlockRegistry.GRASS.getId() && typeIdBelowTree !== BlockRegistry.DIRT.getId() || y >= 128 - height - 1) { + return false; + } + + // Set dirt below the tree + this.world.setBlockAt(x, y - 1, z, BlockRegistry.DIRT.getId()); + + // Create leaves + for (let totalY = (y - 3) + height; totalY <= y + height; totalY++) { + let offsetY = totalY - (y + height); + + // Increase leave radius depending on height + let radius = Math.floor(1 - offsetY / 2); + + // Create leave ring + for (let totalX = x - radius; totalX <= x + radius; totalX++) { + let offsetX = totalX - x; + for (let totalZ = z - radius; totalZ <= z + radius; totalZ++) { + let offsetZ = totalZ - z; + + // Skip corners + if (Math.abs(offsetX) !== radius || Math.abs(offsetZ) !== radius || this.random.nextInt(2) !== 0 && offsetY !== 0) { + this.world.setBlockAt(totalX, totalY, totalZ, BlockRegistry.LEAVE.getId()); + } + } + } + } + + // Create tree trunk + for (let i = 0; i < height; i++) { + let typeId = this.world.getBlockAt(x, y + i, z); + if (typeId === 0 || typeId === BlockRegistry.LEAVE.getId()) { + this.world.setBlockAt(x, y + i, z, BlockRegistry.LOG.getId()); + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/js/net/minecraft/util/MetadataChunkBlock.js b/src/js/net/minecraft/util/MetadataChunkBlock.js index 4ef3a92..5b6ed7f 100644 --- a/src/js/net/minecraft/util/MetadataChunkBlock.js +++ b/src/js/net/minecraft/util/MetadataChunkBlock.js @@ -22,12 +22,18 @@ export default class MetadataChunkBlock { if (index > 32768) { return; } + for (let x = this.x1; x <= this.x2; x++) { for (let z = this.z1; z <= this.z2; z++) { if (!world.blockExists(x, 0, z)) { continue; } + let centerChunk = world.getChunkAt(x >> 4, z >> 4); + if (!centerChunk.loaded) { + return; + } + for (let y = this.y1; y <= this.y2; y++) { if (y < 0 || y >= World.TOTAL_HEIGHT) { continue; diff --git a/src/js/net/minecraft/util/Random.js b/src/js/net/minecraft/util/Random.js index 6714957..72df661 100644 --- a/src/js/net/minecraft/util/Random.js +++ b/src/js/net/minecraft/util/Random.js @@ -4,15 +4,14 @@ export default class Random { constructor(seed = Date.now() % 1000000000 ^ Random.instances++ * 1000) { this.mask = 0xffffffff; - this.m_w = (123456789 + seed) & this.mask; - this.m_z = (987654321 - seed) & this.mask; + this.setSeed(seed); } nextBoolean() { return this.nextFloat() > 0.5; } - nextInt(max) { + nextInt(max = 0x7fffffff) { return Math.floor(this.nextFloat() * (max + 1)); } @@ -24,4 +23,10 @@ export default class Random { result /= 4294967296; return result; } + + setSeed(seed) { + this.seed = seed; + this.m_w = (123456789 + seed) & this.mask; + this.m_z = (987654321 - seed) & this.mask; + } } \ No newline at end of file diff --git a/src/resources/terrain/terrain.png b/src/resources/terrain/terrain.png index f2aea4dcd466722bae1da31822de4308f76f2b68..7b770f47501f9c1342c1e459c270a27e41c9eb85 100644 GIT binary patch delta 6333 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 delta 6394 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 IM6N<$f>$r!Jpcdz