feat: Caves and ores

This commit is contained in:
Jakub Žitník 2025-03-09 15:28:01 +01:00
parent a736795bd7
commit b04e73dee3
Signed by: jzitnik
GPG Key ID: C577A802A6AF4EF3
4 changed files with 260 additions and 114 deletions

View File

@ -7,7 +7,6 @@ import cz.jzitnik.game.entities.items.ItemBlockSupplier;
import cz.jzitnik.game.sprites.Steve; import cz.jzitnik.game.sprites.Steve;
import java.util.List; import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
public class Generation { public class Generation {
@ -23,19 +22,17 @@ public class Generation {
steveBlock2.setGhost(true); steveBlock2.setGhost(true);
steveBlock2.setMob(true); steveBlock2.setMob(true);
int[] terrainHeight = generateTerrain(); int[] terrainHeight = PopulateWorld.generateTerrain();
game.getPlayer().setPlayerBlock1(steveBlock); game.getPlayer().setPlayerBlock1(steveBlock);
game.getPlayer().setPlayerBlock2(steveBlock2); game.getPlayer().setPlayerBlock2(steveBlock2);
populateWorld(world, terrainHeight); PopulateWorld.populateWorld(world, terrainHeight);
plantTrees(world, terrainHeight); Trees.plantTrees(world, terrainHeight);
// Spawn player at a valid starting point // Spawn player at a valid starting point
world[terrainHeight[256] - 1][256].add(steveBlock2); world[terrainHeight[256] - 1][256].add(steveBlock2);
world[terrainHeight[256] - 2][256].add(steveBlock); world[terrainHeight[256] - 2][256].add(steveBlock);
game.getInventory().addItem(ItemBlockSupplier.getItem("shears"));
} }
private static void initializeWorld(List<Block>[][] world) { private static void initializeWorld(List<Block>[][] 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<Block>[][] 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<Block>[] lists : world) {
for (List<Block> list : lists) {
list.addFirst(new Block("air", SpriteLoader.SPRITES.AIR, true, false));
}
}
}
private static boolean isTreeNearby(List<Block>[][] 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<Block>[][] 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<Block>[][] 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"));
}
}
}
}
} }

View File

@ -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<Block>[][] 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<Block>[] column : world) {
for (List<Block> list : column) {
list.addFirst(new Block("air", SpriteLoader.SPRITES.AIR, true, false));
}
}
}
private static void generateOres(List<Block>[][] 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<Block>[][] 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<Block> 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;
}
}

View File

@ -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<Block>[][] 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<Block>[][] 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<Block>[][] 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"));
}
}
}
}
}

View File

@ -5,7 +5,7 @@ import cz.jzitnik.game.annotations.BlockRegistry;
import cz.jzitnik.game.annotations.CustomLogic; import cz.jzitnik.game.annotations.CustomLogic;
import cz.jzitnik.game.annotations.Sapling; import cz.jzitnik.game.annotations.Sapling;
import cz.jzitnik.game.entities.Block; 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 cz.jzitnik.game.logic.CustomLogicInterface;
import org.reflections.Reflections; import org.reflections.Reflections;
@ -85,7 +85,7 @@ public class SaplingLogic implements CustomLogicInterface {
if (data.getGrowWait() == 1) { if (data.getGrowWait() == 1) {
// Grow // Grow
world[y][x].remove(sapling); world[y][x].remove(sapling);
Generation.tree(world, x, y + 1); Trees.tree(world, x, y + 1);
return; return;
} }