From b04e73dee3da516de832ffcea363d7c1eff007ae Mon Sep 17 00:00:00 2001 From: jzitnik-dev Date: Sun, 9 Mar 2025 15:28:01 +0100 Subject: [PATCH] feat: Caves and ores --- .../jzitnik/game/generation/Generation.java | 115 +---------- .../game/generation/PopulateWorld.java | 191 ++++++++++++++++++ .../cz/jzitnik/game/generation/Trees.java | 64 ++++++ .../logic/services/saplings/SaplingLogic.java | 4 +- 4 files changed, 260 insertions(+), 114 deletions(-) create mode 100644 src/main/java/cz/jzitnik/game/generation/PopulateWorld.java create mode 100644 src/main/java/cz/jzitnik/game/generation/Trees.java diff --git a/src/main/java/cz/jzitnik/game/generation/Generation.java b/src/main/java/cz/jzitnik/game/generation/Generation.java index f909409..764cfc6 100644 --- a/src/main/java/cz/jzitnik/game/generation/Generation.java +++ b/src/main/java/cz/jzitnik/game/generation/Generation.java @@ -7,7 +7,6 @@ import cz.jzitnik.game.entities.items.ItemBlockSupplier; import cz.jzitnik.game.sprites.Steve; import java.util.List; -import java.util.Random; import java.util.concurrent.CopyOnWriteArrayList; public class Generation { @@ -23,19 +22,17 @@ public class Generation { steveBlock2.setGhost(true); steveBlock2.setMob(true); - int[] terrainHeight = generateTerrain(); + int[] terrainHeight = PopulateWorld.generateTerrain(); game.getPlayer().setPlayerBlock1(steveBlock); game.getPlayer().setPlayerBlock2(steveBlock2); - populateWorld(world, terrainHeight); - plantTrees(world, terrainHeight); + PopulateWorld.populateWorld(world, terrainHeight); + Trees.plantTrees(world, terrainHeight); // Spawn player at a valid starting point world[terrainHeight[256] - 1][256].add(steveBlock2); world[terrainHeight[256] - 2][256].add(steveBlock); - - game.getInventory().addItem(ItemBlockSupplier.getItem("shears")); } private static void initializeWorld(List[][] world) { @@ -45,110 +42,4 @@ public class Generation { } } } - - private static int[] generateTerrain() { - Random random = new Random(); - int baseHeight = 120; - int[] terrainHeight = new int[512]; - - terrainHeight[0] = baseHeight; - for (int i = 1; i < 512; i++) { - int heightChange = random.nextInt(3) - 1; - terrainHeight[i] = Math.max(100, Math.min(140, terrainHeight[i - 1] + heightChange)); - } - - for (int i = 2; i < 510; i++) { - terrainHeight[i] = (terrainHeight[i - 1] + terrainHeight[i] + terrainHeight[i + 1]) / 3; - } - - return terrainHeight; - } - - private static void populateWorld(List[][] world, int[] terrainHeight) { - Random random = new Random(); - - for (int i = 0; i < 512; i++) { - int hillHeight = terrainHeight[i]; - - world[hillHeight][i].add(ItemBlockSupplier.getBlock("grass")); - - if (random.nextDouble() < 0.1 && !isTreeNearby(world, i, hillHeight)) { - world[hillHeight - 1][i].add(ItemBlockSupplier.getBlock("grass_bush")); - } - - for (int j = 1; j <= 4; j++) { - if (hillHeight + j < 256) { - world[hillHeight + j][i].add(ItemBlockSupplier.getBlock("dirt")); - } - } - - for (int j = hillHeight + 5; j < 250; j++) { - world[j][i].add(ItemBlockSupplier.getBlock("stone")); - } - - world[255][i].add(new Block("bedrock", SpriteLoader.SPRITES.BEDROCK)); - } - - // Fill air blocks - for (List[] lists : world) { - for (List list : lists) { - list.addFirst(new Block("air", SpriteLoader.SPRITES.AIR, true, false)); - } - } - } - - private static boolean isTreeNearby(List[][] world, int x, int y) { - int radius = 3; // Check within a 3-block radius for trees - for (int dx = -radius; dx <= radius; dx++) { - for (int dy = -radius; dy <= radius; dy++) { - int nx = x + dx; - int ny = y + dy; - if (nx >= 0 && nx < 512 && ny >= 0 && ny < 256) { - for (Block block : world[ny][nx]) { - if (block.getBlockId().equals("oak_log") || block.getBlockId().equals("oak_leaves")) { - return true; - } - } - } - } - } - return false; - } - - - private static void plantTrees(List[][] world, int[] terrainHeight) { - Random random = new Random(); - for (int i = 10; i < 502; i += random.nextInt(20) + 20) { - int treeBase = terrainHeight[i]; - - if (treeBase - 3 < 0) - continue; - - tree(world, i, treeBase); - } - } - - public static void tree(List[][] world, int i, int treeBase) { - for (int j = 0; j < 3; j++) { - if (treeBase - j >= 0) { - world[treeBase - j - 1][i].add(ItemBlockSupplier.getBlock("oak_log")); - } - } - - int leafY = treeBase - 4; - - for (int layer = 0; layer < 3; layer++) { - int size = 5 - (layer * 2); - int offsetY = leafY - layer; - - for (int dx = -size / 2; dx <= size / 2; dx++) { - int x = i + dx; - int y = offsetY; - - if (x >= 0 && x < world[0].length && y >= 0) { - world[y][x].add(ItemBlockSupplier.getBlock("oak_leaves")); - } - } - } - } } diff --git a/src/main/java/cz/jzitnik/game/generation/PopulateWorld.java b/src/main/java/cz/jzitnik/game/generation/PopulateWorld.java new file mode 100644 index 0000000..f98773a --- /dev/null +++ b/src/main/java/cz/jzitnik/game/generation/PopulateWorld.java @@ -0,0 +1,191 @@ +package cz.jzitnik.game.generation; + +import cz.jzitnik.game.entities.Block; +import cz.jzitnik.game.SpriteLoader; +import cz.jzitnik.game.entities.items.ItemBlockSupplier; + +import java.util.List; +import java.util.Random; + +public class PopulateWorld { + private static final int WORLD_WIDTH = 512; + private static final int WORLD_HEIGHT = 256; + + public static int[] generateTerrain() { + Random random = new Random(); + int baseHeight = 120; + int[] terrainHeight = new int[WORLD_WIDTH]; + + terrainHeight[0] = baseHeight; + for (int i = 1; i < WORLD_WIDTH; i++) { + int heightChange = random.nextInt(3) - 1; + terrainHeight[i] = Math.max(100, Math.min(140, terrainHeight[i - 1] + heightChange)); + } + + for (int i = 2; i < WORLD_WIDTH - 2; i++) { + terrainHeight[i] = (terrainHeight[i - 1] + terrainHeight[i] + terrainHeight[i + 1]) / 3; + } + + return terrainHeight; + } + + public static void populateWorld(List[][] world, int[] terrainHeight) { + boolean[][] isCave = generateCavesWithCellularAutomata(terrainHeight); + Random random = new Random(); + + for (int x = 0; x < WORLD_WIDTH; x++) { + int hillHeight = terrainHeight[x]; + + // Surface block + world[hillHeight][x].add(ItemBlockSupplier.getBlock("grass")); + + // Grass bush + if (random.nextDouble() < 0.1 && !Trees.isTreeNearby(world, x, hillHeight)) { + world[hillHeight - 1][x].add(ItemBlockSupplier.getBlock("grass_bush")); + } + + // Dirt layer + for (int y = hillHeight + 1; y <= hillHeight + 4; y++) { + if (!isCave[y][x]) { + world[y][x].add(ItemBlockSupplier.getBlock("dirt")); + } + } + + // Stone layer + for (int y = hillHeight + 5; y < WORLD_HEIGHT - 1; y++) { + if (!isCave[y][x]) { + world[y][x].add(ItemBlockSupplier.getBlock("stone")); + } + } + + // Bedrock + world[WORLD_HEIGHT - 1][x].add(new Block("bedrock", SpriteLoader.SPRITES.BEDROCK)); + } + + // Generate ores + generateOres(world, isCave); + + // Fill air blocks + for (List[] column : world) { + for (List list : column) { + list.addFirst(new Block("air", SpriteLoader.SPRITES.AIR, true, false)); + } + } + } + + private static void generateOres(List[][] world, boolean[][] isCave) { + Random random = new Random(); + + // Ore configs: name, minY, maxY, minVeinSize, maxVeinSize, frequency + String[][] ores = { + {"coal_ore", "100", "220", "6", "15", "100"}, // More veins, bigger sizes + {"iron_ore", "110", "220", "4", "10", "80"}, + {"gold_ore", "120", "230", "3", "7", "50"}, + {"diamond_ore", "130", "240", "2", "5", "30"}, + }; + + for (String[] ore : ores) { + String oreName = ore[0]; + int minY = Integer.parseInt(ore[1]); + int maxY = Integer.parseInt(ore[2]); + int minVeinSize = Integer.parseInt(ore[3]); + int maxVeinSize = Integer.parseInt(ore[4]); + int frequency = Integer.parseInt(ore[5]); + + for (int i = 0; i < frequency; i++) { + int x = random.nextInt(WORLD_WIDTH); + int y = minY + random.nextInt(maxY - minY + 1); + int veinSize = minVeinSize + random.nextInt(maxVeinSize - minVeinSize + 1); + + generateOreVein(world, isCave, x, y, oreName, veinSize); + } + } + } + + + + private static void generateOreVein(List[][] world, boolean[][] isCave, int startX, int startY, String oreName, int veinSize) { + Random random = new Random(); + for (int i = 0; i < veinSize; i++) { + int dx = startX + random.nextInt(3) - 1; + int dy = startY + random.nextInt(3) - 1; + + if (dx >= 0 && dx < WORLD_WIDTH && dy >= 0 && dy < WORLD_HEIGHT - 1) { + System.out.println("Placed " + oreName + " at (" + dx + ", " + dy + ")"); + // Only place ore if it's inside stone and not cave + List blockList = world[dy][dx]; + for (int j = 0; j < blockList.size(); j++) { + Block b = blockList.get(j); + if (b.getBlockId().equals("stone") && !isCave[dy][dx]) { + blockList.set(j, ItemBlockSupplier.getBlock(oreName)); + break; + } + } + } + } + } + + + private static boolean[][] generateCavesWithCellularAutomata(int[] terrainHeight) { + Random random = new Random(); + boolean[][] caveMap = new boolean[WORLD_HEIGHT][WORLD_WIDTH]; + + // Dynamically define caveStartY per column to not overlap dirt layer + for (int x = 0; x < WORLD_WIDTH; x++) { + int dirtEndY = terrainHeight[x] + 4; + int caveStartY = dirtEndY + 1; + int caveEndY = Math.min(caveStartY + 40, WORLD_HEIGHT - 2); // limit depth to ~40 blocks + + // Initial noise map for this column + for (int y = caveStartY; y < caveEndY; y++) { + caveMap[y][x] = random.nextDouble() < 0.45; + } + } + + // Smooth cave map with CA rules + int iterations = 5; + for (int iter = 0; iter < iterations; iter++) { + boolean[][] newMap = new boolean[WORLD_HEIGHT][WORLD_WIDTH]; + + for (int x = 0; x < WORLD_WIDTH; x++) { + int dirtEndY = terrainHeight[x] + 4; + int caveStartY = dirtEndY + 1; + int caveEndY = Math.min(caveStartY + 40, WORLD_HEIGHT - 2); + + for (int y = caveStartY; y < caveEndY; y++) { + int walls = countSurroundingWalls(caveMap, x, y); + + if (walls > 4) newMap[y][x] = true; // wall + else if (walls < 4) newMap[y][x] = false; // cave + else newMap[y][x] = caveMap[y][x]; // keep + } + } + + caveMap = newMap; + } + + return caveMap; + } + + + private static int countSurroundingWalls(boolean[][] map, int x, int y) { + int count = 0; + + for (int dy = -1; dy <= 1; dy++) { + for (int dx = -1; dx <= 1; dx++) { + if (dx == 0 && dy == 0) continue; + + int nx = x + dx; + int ny = y + dy; + + if (nx < 0 || nx >= WORLD_WIDTH || ny < 0 || ny >= WORLD_HEIGHT) { + count++; // out-of-bounds counts as wall + } else if (map[ny][nx]) { + count++; + } + } + } + + return count; + } +} diff --git a/src/main/java/cz/jzitnik/game/generation/Trees.java b/src/main/java/cz/jzitnik/game/generation/Trees.java new file mode 100644 index 0000000..9e9b66e --- /dev/null +++ b/src/main/java/cz/jzitnik/game/generation/Trees.java @@ -0,0 +1,64 @@ +package cz.jzitnik.game.generation; + +import cz.jzitnik.game.entities.Block; +import cz.jzitnik.game.entities.items.ItemBlockSupplier; + +import java.util.List; +import java.util.Random; + +public class Trees { + public static boolean isTreeNearby(List[][] world, int x, int y) { + int radius = 3; // Check within a 3-block radius for trees + for (int dx = -radius; dx <= radius; dx++) { + for (int dy = -radius; dy <= radius; dy++) { + int nx = x + dx; + int ny = y + dy; + if (nx >= 0 && nx < 512 && ny >= 0 && ny < 256) { + for (Block block : world[ny][nx]) { + if (block.getBlockId().equals("oak_log") || block.getBlockId().equals("oak_leaves")) { + return true; + } + } + } + } + } + return false; + } + + + public static void plantTrees(List[][] world, int[] terrainHeight) { + Random random = new Random(); + for (int i = 10; i < 502; i += random.nextInt(20) + 20) { + int treeBase = terrainHeight[i]; + + if (treeBase - 3 < 0) + continue; + + tree(world, i, treeBase); + } + } + + public static void tree(List[][] world, int i, int treeBase) { + for (int j = 0; j < 3; j++) { + if (treeBase - j >= 0) { + world[treeBase - j - 1][i].add(ItemBlockSupplier.getBlock("oak_log")); + } + } + + int leafY = treeBase - 4; + + for (int layer = 0; layer < 3; layer++) { + int size = 5 - (layer * 2); + int offsetY = leafY - layer; + + for (int dx = -size / 2; dx <= size / 2; dx++) { + int x = i + dx; + int y = offsetY; + + if (x >= 0 && x < world[0].length && y >= 0) { + world[y][x].add(ItemBlockSupplier.getBlock("oak_leaves")); + } + } + } + } +} diff --git a/src/main/java/cz/jzitnik/game/logic/services/saplings/SaplingLogic.java b/src/main/java/cz/jzitnik/game/logic/services/saplings/SaplingLogic.java index 3a237f4..7353762 100644 --- a/src/main/java/cz/jzitnik/game/logic/services/saplings/SaplingLogic.java +++ b/src/main/java/cz/jzitnik/game/logic/services/saplings/SaplingLogic.java @@ -5,7 +5,7 @@ import cz.jzitnik.game.annotations.BlockRegistry; import cz.jzitnik.game.annotations.CustomLogic; import cz.jzitnik.game.annotations.Sapling; import cz.jzitnik.game.entities.Block; -import cz.jzitnik.game.generation.Generation; +import cz.jzitnik.game.generation.Trees; import cz.jzitnik.game.logic.CustomLogicInterface; import org.reflections.Reflections; @@ -85,7 +85,7 @@ public class SaplingLogic implements CustomLogicInterface { if (data.getGrowWait() == 1) { // Grow world[y][x].remove(sapling); - Generation.tree(world, x, y + 1); + Trees.tree(world, x, y + 1); return; }