|
|
@@ -8,176 +8,224 @@ import java.util.*;
|
|
|
|
|
|
public class WorldGenerator {
|
|
|
private static int worldGenNumber = 0;
|
|
|
+ private static final int TILE_GRASS = 0;
|
|
|
+ private static final int TILE_PATH = 5;
|
|
|
+ private static final int TILE_WATER = 2;
|
|
|
+ private static final int TILE_TREE = 4;
|
|
|
+ private static final int TILE_EARTH = 3;
|
|
|
+
|
|
|
+ public static int getWorldGenNumber(){
|
|
|
+ return worldGenNumber;
|
|
|
+ }
|
|
|
|
|
|
- public static String generateNewWorld(int worldWidth, int worldHeight, int seed) {
|
|
|
+ public static String generateNewWorld(int width, int height, long seed) {
|
|
|
Random rand = new Random(seed);
|
|
|
- int[][] world = new int[worldHeight][worldWidth];
|
|
|
- initializeWorld(world);
|
|
|
- createMainPath(world, worldWidth, worldHeight, rand);
|
|
|
- addSecondaryPaths(world, worldWidth, worldHeight, rand);
|
|
|
- addWater(world, worldWidth, worldHeight, rand);
|
|
|
- addTrees(world, worldWidth, worldHeight, rand);
|
|
|
- addEarthPatches(world, worldWidth, worldHeight, rand);
|
|
|
- return writeWorldToFile(world, worldWidth, worldHeight);
|
|
|
+ int[][] world = new int[height][width];
|
|
|
+ fill(world, TILE_GRASS);
|
|
|
+
|
|
|
+ // 1) Roads: random walks for realistic winding one-tile paths
|
|
|
+ generateTrails(world, width, height, rand);
|
|
|
+
|
|
|
+ // 2) Earth clusters (fewer patches)
|
|
|
+ generateEarthClusters(world, width, height, rand, 10, 4);
|
|
|
+
|
|
|
+ // 3) Tree clusters (denser forests)
|
|
|
+ addTreeClusters(world, width, height, rand, 15);
|
|
|
+
|
|
|
+ // 4) Coastline & lakes
|
|
|
+ generateCoastline(world, width, height, rand);
|
|
|
+ addInteriorLakes(world, width, height, rand, 3);
|
|
|
+
|
|
|
+ return writeToFile(world, width, height);
|
|
|
}
|
|
|
|
|
|
- private static void initializeWorld(int[][] world) {
|
|
|
- for (int[] row : world) {
|
|
|
- Arrays.fill(row, 0); // Fill with grass
|
|
|
- }
|
|
|
+ public static String generateNewWorld(int width, int height) {
|
|
|
+ return generateNewWorld(width, height, System.currentTimeMillis());
|
|
|
}
|
|
|
|
|
|
- private static void createMainPath(int[][] world, int width, int height, Random rand) {
|
|
|
- int startX = width / 2;
|
|
|
- int startY = height / 2;
|
|
|
- // Create a winding main path with multiple segments
|
|
|
- drunkardsWalk(world, startX, startY, 300, width, height, rand, true);
|
|
|
+ private static void fill(int[][] world, int tile) {
|
|
|
+ for (int[] row : world) Arrays.fill(row, tile);
|
|
|
}
|
|
|
|
|
|
- private static void addSecondaryPaths(int[][] world, int width, int height, Random rand) {
|
|
|
- int numBranches = 10;
|
|
|
- for (int i = 0; i < numBranches; i++) {
|
|
|
- // Find a random point on the existing path
|
|
|
- int startX, startY;
|
|
|
+ // ===== TRAILS =====
|
|
|
+ private static void generateTrails(int[][] world, int width, int height, Random rand) {
|
|
|
+ int numTrails = 6; // increased number of trails
|
|
|
+ for (int i = 0; i < numTrails; i++) {
|
|
|
+ int sx = rand.nextInt(width);
|
|
|
+ int sy = rand.nextInt(height);
|
|
|
+ // end point another random but ensure sufficient distance
|
|
|
+ int tx, ty;
|
|
|
do {
|
|
|
- startX = rand.nextInt(width);
|
|
|
- startY = rand.nextInt(height);
|
|
|
- } while (world[startY][startX] != 5);
|
|
|
+ tx = rand.nextInt(width);
|
|
|
+ ty = rand.nextInt(height);
|
|
|
+ } while (Math.abs(sx - tx) + Math.abs(sy - ty) < Math.max(width, height) / 5);
|
|
|
+ carveTrail(world, sx, sy, tx, ty, rand);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- drunkardsWalk(world, startX, startY, 50 + rand.nextInt(50), width, height, rand, false);
|
|
|
+ private static void carveTrail(int[][] world, int sx, int sy, int tx, int ty, Random rand) {
|
|
|
+ int cx = sx, cy = sy;
|
|
|
+ int maxSteps = (Math.abs(sx - tx) + Math.abs(sy - ty)) * 2;
|
|
|
+ for (int i = 0; i < maxSteps; i++) {
|
|
|
+ world[cy][cx] = TILE_PATH;
|
|
|
+ if (Math.abs(cx - tx) + Math.abs(cy - ty) < 2) break;
|
|
|
+ // direction toward target
|
|
|
+ int dx = Integer.compare(tx, cx);
|
|
|
+ int dy = Integer.compare(ty, cy);
|
|
|
+ // random offset more frequently
|
|
|
+ if (rand.nextDouble() < 0.4) {
|
|
|
+ if (rand.nextBoolean()) dx = 0;
|
|
|
+ else dy = 0;
|
|
|
+ }
|
|
|
+ // apply move
|
|
|
+ cx = Math.max(0, Math.min(world[0].length - 1, cx + dx));
|
|
|
+ cy = Math.max(0, Math.min(world.length - 1, cy + dy));
|
|
|
+ // increased chance for side branches
|
|
|
+ if (rand.nextDouble() < 0.1) carveShortBranch(world, cx, cy, rand, 15 + rand.nextInt(15));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static void drunkardsWalk(int[][] world, int startX, int startY, int steps,
|
|
|
- int width, int height, Random rand, boolean isMain) {
|
|
|
- int x = startX;
|
|
|
- int y = startY;
|
|
|
- for (int i = 0; i < steps; i++) {
|
|
|
- int dir = rand.nextInt(4);
|
|
|
- switch (dir) {
|
|
|
- case 0 -> x = Math.min(x + 1, width - 1);
|
|
|
- case 1 -> x = Math.max(x - 1, 0);
|
|
|
- case 2 -> y = Math.min(y + 1, height - 1);
|
|
|
- case 3 -> y = Math.max(y - 1, 0);
|
|
|
+ private static void carveShortBranch(int[][] world, int cx, int cy, Random rand, int steps) {
|
|
|
+ int bx = cx, by = cy;
|
|
|
+ for (int b = 0; b < steps; b++) {
|
|
|
+ world[by][bx] = TILE_PATH;
|
|
|
+ int d = rand.nextInt(4);
|
|
|
+ switch (d) {
|
|
|
+ case 0 -> bx = Math.min(bx + 1, world[0].length - 1);
|
|
|
+ case 1 -> bx = Math.max(bx - 1, 0);
|
|
|
+ case 2 -> by = Math.min(by + 1, world.length - 1);
|
|
|
+ case 3 -> by = Math.max(by - 1, 0);
|
|
|
}
|
|
|
- world[y][x] = 5; // Set to path
|
|
|
-
|
|
|
- // Randomly widen path
|
|
|
- if (rand.nextDouble() < (isMain ? 0.4 : 0.3)) {
|
|
|
- int widen = rand.nextInt(2) + 1;
|
|
|
- for (int dx = -widen; dx <= widen; dx++) {
|
|
|
- for (int dy = -widen; dy <= widen; dy++) {
|
|
|
- if (x + dx >= 0 && x + dx < width && y + dy >= 0 && y + dy < height) {
|
|
|
- if (world[y + dy][x + dx] == 0) {
|
|
|
- world[y + dy][x + dx] = 5;
|
|
|
- }
|
|
|
- }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== EARTH CLUSTERS =====
|
|
|
+ private static void generateEarthClusters(int[][] world, int width, int height,
|
|
|
+ Random rand, int clusters, int maxSize) {
|
|
|
+ for (int i = 0; i < clusters; i++) {
|
|
|
+ int cx = rand.nextInt(width);
|
|
|
+ int cy = rand.nextInt(height);
|
|
|
+ int size = 1 + rand.nextInt(maxSize);
|
|
|
+ for (int dx = -size; dx <= size; dx++) {
|
|
|
+ for (int dy = -size; dy <= size; dy++) {
|
|
|
+ if (cx + dx < 0 || cx + dx >= width || cy + dy < 0 || cy + dy >= height) continue;
|
|
|
+ if (Math.hypot(dx, dy) < size && rand.nextDouble() < 0.4) {
|
|
|
+ if (world[cy + dy][cx + dx] == TILE_GRASS) world[cy + dy][cx + dx] = TILE_EARTH;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static void addWater(int[][] world, int width, int height, Random rand) {
|
|
|
- for (int i = 0; i < 5; i++) {
|
|
|
- int x = rand.nextInt(width);
|
|
|
- int y = rand.nextInt(height);
|
|
|
- if (world[y][x] == 0) {
|
|
|
- floodFill(world, x, y, 2, 50 + rand.nextInt(50), width, height);
|
|
|
+ // ===== TREE CLUSTERS =====
|
|
|
+ private static void addTreeClusters(int[][] world, int width, int height,
|
|
|
+ Random rand, int clusters) {
|
|
|
+ for (int i = 0; i < clusters; i++) {
|
|
|
+ int tx = rand.nextInt(width);
|
|
|
+ int ty = rand.nextInt(height);
|
|
|
+ int size = 3 + rand.nextInt(3);
|
|
|
+ for (int dx = -size; dx <= size; dx++) {
|
|
|
+ for (int dy = -size; dy <= size; dy++) {
|
|
|
+ if (tx + dx < 0 || tx + dx >= width || ty + dy < 0 || ty + dy >= height) continue;
|
|
|
+ if (world[ty + dy][tx + dx] == TILE_GRASS && rand.nextDouble() < 0.75) world[ty + dy][tx + dx] = TILE_TREE;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static void floodFill(int[][] world, int x, int y, int tile, int maxSize, int width, int height) {
|
|
|
- Queue<Point> queue = new LinkedList<>();
|
|
|
- Set<Point> visited = new HashSet<>();
|
|
|
- queue.add(new Point(x, y));
|
|
|
- int filled = 0;
|
|
|
-
|
|
|
- while (!queue.isEmpty() && filled < maxSize) {
|
|
|
- Point p = queue.poll();
|
|
|
- if (visited.contains(p)) continue;
|
|
|
- visited.add(p);
|
|
|
-
|
|
|
- if (world[p.y][p.x] == 0) {
|
|
|
- world[p.y][p.x] = tile;
|
|
|
- filled++;
|
|
|
- // Add neighbors
|
|
|
- if (p.x > 0) queue.add(new Point(p.x - 1, p.y));
|
|
|
- if (p.x < width - 1) queue.add(new Point(p.x + 1, p.y));
|
|
|
- if (p.y > 0) queue.add(new Point(p.x, p.y - 1));
|
|
|
- if (p.y < height - 1) queue.add(new Point(p.x, p.y + 1));
|
|
|
+ // ===== COASTLINE =====
|
|
|
+ private static void generateCoastline(int[][] world, int width, int height, Random rand) {
|
|
|
+ int borderMax = Math.min(width, height) / 40;
|
|
|
+ double centerX = width / 2.0, centerY = height / 2.0;
|
|
|
+ double baseRadius = Math.min(width, height) / 2.0;
|
|
|
+ double maxRadius = baseRadius - borderMax * 0.75;
|
|
|
+ double[] radiusMap = generateRadiusMap(720, borderMax, rand);
|
|
|
+ for (int y = 0; y < height; y++) {
|
|
|
+ for (int x = 0; x < width; x++) {
|
|
|
+ double dx = x - centerX, dy = y - centerY;
|
|
|
+ double dist = Math.hypot(dx, dy);
|
|
|
+ int angle360 = (int) (Math.toDegrees(Math.atan2(dy, dx)) + 360) % 360;
|
|
|
+ int idx = (int) (angle360 * (radiusMap.length / 360.0));
|
|
|
+ double jitter = radiusMap[idx] * 0.8;
|
|
|
+ double noise = (rand.nextDouble() - 0.5) * borderMax * 1.5;
|
|
|
+ double threshold = maxRadius - jitter + noise;
|
|
|
+ if (dist > threshold) world[y][x] = TILE_WATER;
|
|
|
+ else if (rand.nextDouble() < 0.015 && dist > threshold * 0.85) world[y][x] = TILE_WATER;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static void addTrees(int[][] world, int width, int height, Random rand) {
|
|
|
- for (int i = 0; i < 10; i++) {
|
|
|
- int x = rand.nextInt(width);
|
|
|
- int y = rand.nextInt(height);
|
|
|
- if (world[y][x] == 0) {
|
|
|
- generateTreeCluster(world, x, y, 4 + rand.nextInt(4), width, height, rand);
|
|
|
- }
|
|
|
+ private static double[] generateRadiusMap(int resolution, int maxOffset, Random rand) {
|
|
|
+ double[] map = new double[resolution];
|
|
|
+ map[0] = map[resolution/2] = maxOffset;
|
|
|
+ subdivide(map, 0, resolution/2, maxOffset, rand);
|
|
|
+ subdivide(map, resolution/2, resolution, maxOffset, rand);
|
|
|
+ for (int i = 0; i < resolution; i += resolution/8) {
|
|
|
+ int spike = rand.nextInt(resolution);
|
|
|
+ map[spike] += rand.nextDouble() * maxOffset * 1.2;
|
|
|
}
|
|
|
+ return map;
|
|
|
}
|
|
|
|
|
|
- private static void generateTreeCluster(int[][] world, int x, int y, int size,
|
|
|
- int width, int height, Random rand) {
|
|
|
- for (int dx = -size; dx <= size; dx++) {
|
|
|
- for (int dy = -size; dy <= size; dy++) {
|
|
|
- if (x + dx >= 0 && x + dx < width && y + dy >= 0 && y + dy < height) {
|
|
|
- double dist = Math.sqrt(dx*dx + dy*dy);
|
|
|
- if (dist <= size && rand.nextDouble() < 0.7 - (dist / size) * 0.4) {
|
|
|
- if (world[y + dy][x + dx] == 0) {
|
|
|
- world[y + dy][x + dx] = 4;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ private static void subdivide(double[] arr, int left, int right, double offset, Random rand) {
|
|
|
+ if (right - left < 2) return;
|
|
|
+ int mid = (left + right) / 2;
|
|
|
+ double a = arr[left];
|
|
|
+ double b = arr[right % arr.length];
|
|
|
+ arr[mid] = (a + b) / 2 + (rand.nextDouble() * 2 - 1) * offset;
|
|
|
+ double newOffset = offset * 0.7;
|
|
|
+ subdivide(arr, left, mid, newOffset, rand);
|
|
|
+ subdivide(arr, mid, right, newOffset, rand);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== INTERIOR LAKES =====
|
|
|
+ private static void addInteriorLakes(int[][] world, int width, int height, Random rand, int lakes) {
|
|
|
+ for (int i = 0; i < lakes; i++) {
|
|
|
+ int lx = rand.nextInt(width);
|
|
|
+ int ly = rand.nextInt(height);
|
|
|
+ int size = 5 + rand.nextInt(10);
|
|
|
+ floodFill(world, lx, ly, TILE_WATER, size);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static void addEarthPatches(int[][] world, int width, int height, Random rand) {
|
|
|
- for (int i = 0; i < width * height / 20; i++) {
|
|
|
- int x = rand.nextInt(width);
|
|
|
- int y = rand.nextInt(height);
|
|
|
- if (world[y][x] == 0) {
|
|
|
- world[y][x] = 3; // Earth
|
|
|
+ private static void floodFill(int[][] world, int x, int y, int tile, int maxSize) {
|
|
|
+ Queue<int[]> q = new LinkedList<>();
|
|
|
+ Set<Long> visited = new HashSet<>();
|
|
|
+ q.add(new int[]{x, y});
|
|
|
+ int filled = 0;
|
|
|
+ int w = world[0].length, h = world.length;
|
|
|
+ while (!q.isEmpty() && filled < maxSize) {
|
|
|
+ int[] p = q.poll();
|
|
|
+ int px = p[0], py = p[1];
|
|
|
+ long key = (((long) px) << 32) | py;
|
|
|
+ if (visited.contains(key)) continue;
|
|
|
+ visited.add(key);
|
|
|
+ if (px < 0 || px >= w || py < 0 || py >= h) continue;
|
|
|
+ if (world[py][px] == TILE_GRASS) {
|
|
|
+ world[py][px] = tile;
|
|
|
+ filled++;
|
|
|
+ q.add(new int[]{px-1, py}); q.add(new int[]{px+1, py});
|
|
|
+ q.add(new int[]{px, py-1}); q.add(new int[]{px, py+1});
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static String writeWorldToFile(int[][] world, int width, int height) {
|
|
|
- String fileName = "gamefiles/worlds/world_" + worldGenNumber++ + ".txt";
|
|
|
+ // ===== OUTPUT =====
|
|
|
+ private static String writeToFile(int[][] world, int width, int height) {
|
|
|
+ String dir = "gamefiles/worlds";
|
|
|
+ new File(dir).mkdirs();
|
|
|
+ String fileName = dir + "/world.txt";
|
|
|
try (PrintWriter writer = new PrintWriter(new File(fileName))) {
|
|
|
for (int[] row : world) {
|
|
|
- StringBuilder sb = new StringBuilder();
|
|
|
- for (int i = 0; i < row.length; i++) {
|
|
|
- sb.append(i > 0 ? " " : "").append(row[i]);
|
|
|
+ for (int i = 0; i < width; i++) {
|
|
|
+ writer.print(row[i] + (i < width-1 ? " " : ""));
|
|
|
}
|
|
|
- writer.println(sb);
|
|
|
+ writer.println();
|
|
|
}
|
|
|
} catch (IOException e) {
|
|
|
e.printStackTrace();
|
|
|
}
|
|
|
return fileName;
|
|
|
}
|
|
|
-
|
|
|
- private static class Point {
|
|
|
- int x, y;
|
|
|
- Point(int x, int y) { this.x = x; this.y = y; }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean equals(Object o) {
|
|
|
- if (this == o) return true;
|
|
|
- if (o == null || getClass() != o.getClass()) return false;
|
|
|
- Point point = (Point) o;
|
|
|
- return x == point.x && y == point.y;
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public int hashCode() {
|
|
|
- return Objects.hash(x, y);
|
|
|
- }
|
|
|
- }
|
|
|
}
|
|
|
+
|