Add redwood biome and trail updates

This commit is contained in:
chelsea
2025-12-05 00:33:59 -06:00
parent 4f700e3457
commit 5bb1e05acf
3 changed files with 470 additions and 55 deletions

4
.gitignore vendored
View File

@@ -2,17 +2,15 @@
__pycache__/
*.pyc
# Build artifacts
# Build artifacts we don't want in repo
*.o
*.obj
*.bin
*.mca
*.zip
*.tar
*.tar.gz
# Project-specific output
worldgen-c/bin/
worldgen-c/test_out/
mc.zip
lakeland-update-*/

Binary file not shown.

View File

@@ -8,7 +8,7 @@
#include <float.h>
#include <limits.h>
#define TRAIL_NODE_SPACING 1200.0
#define TRAIL_NODE_SPACING 1600.0
#define TRAIL_CELL_SIZE 16.0
#define TRAIL_MARGIN 96.0
#define TRAIL_WIDTH 5
@@ -16,7 +16,8 @@
typedef enum {
BIOME_WEST_KY_COALFIELDS = 0,
BIOME_EAST_KY_RIDGEBREAKS = 1,
BIOME_OLD_GROWTH_PLAINS = 2
BIOME_OLD_GROWTH_PLAINS = 2,
BIOME_REDWOOD_FOREST = 3
} biome_id;
typedef struct {
@@ -127,21 +128,32 @@ static inline int clamp_int(int v, int min_v, int max_v) {
static int ground_slope(worldgen_ctx *ctx, int x, int z);
static uint16_t select_surface_block(worldgen_ctx *ctx, const column_data *data, int world_x, int world_z);
static void generate_chunk_trails(worldgen_ctx *ctx, int chunk_x, int chunk_z, column_data columns[CHUNK_SIZE][CHUNK_SIZE], chunk_data *out);
static void generate_chunk_redwood_floor(worldgen_ctx *ctx, int chunk_x, int chunk_z,
column_data columns[CHUNK_SIZE][CHUNK_SIZE], chunk_data *chunk);
static void generate_chunk_grass(worldgen_ctx *ctx, int chunk_x, int chunk_z, column_data columns[CHUNK_SIZE][CHUNK_SIZE], chunk_data *out);
static void generate_chunk_flowers(worldgen_ctx *ctx, int chunk_x, int chunk_z, column_data columns[CHUNK_SIZE][CHUNK_SIZE], chunk_data *out);
static void generate_chunk_cabins(worldgen_ctx *ctx, int chunk_x, int chunk_z, column_data columns[CHUNK_SIZE][CHUNK_SIZE], chunk_data *out);
static void ensure_trail_prepass(worldgen_ctx *ctx, int chunk_x, int chunk_z);
static void append_trail_segment(worldgen_ctx *ctx, int ax, int az, int bx, int bz, int *points, int count);
static int *smooth_trail_polyline(worldgen_ctx *ctx, int *points, int count, int *out_count);
static int choose_trail_width(worldgen_ctx *ctx, int x0, int z0, int x1, int z1);
static double point_segment_distance2(double px, double pz, double ax, double az, double bx, double bz);
static int segment_parallel_and_close(int ax, int az, int bx, int bz, int cx, int cz, int dx, int dz, double min_dist);
static double nearest_trail_distance2(worldgen_ctx *ctx, int x, int z, double fallback);
static int chunk_contains_trail(worldgen_ctx *ctx, int chunk_x, int chunk_z);
static void trail_node_position(worldgen_ctx *ctx, int node_x, int node_z, double spacing, double *out_x, double *out_z);
static void carve_trail_pad(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_data *out, column_data columns[CHUNK_SIZE][CHUNK_SIZE], int center_x, int center_z, int radius, int target_height);
static void carve_trail_span(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_data *out, column_data columns[CHUNK_SIZE][CHUNK_SIZE], int x0, int z0, int x1, int z1, int width);
static int build_trail_path(worldgen_ctx *ctx, double ax, double az, double bx, double bz, int **out_points, int *out_count);
static double old_growth_plains_mask(worldgen_ctx *ctx, int x, int z);
static double old_growth_grove_mask(worldgen_ctx *ctx, int x, int z);
static double rainfall_field(worldgen_ctx *ctx, int x, int z);
static double redwood_biome_mask(worldgen_ctx *ctx, int x, int z, int column_height, int local_slope);
static uint16_t generate_normal_ores(worldgen_ctx *ctx, int x, int y, int z, const column_data *col);
static void connect_cabin_to_trail(worldgen_ctx *ctx, int chunk_x, int chunk_z,
column_data columns[CHUNK_SIZE][CHUNK_SIZE], chunk_data *chunk,
int door_x, int door_z, int door_side, int path_width);
static int column_has_manmade_surface(chunk_data *chunk, int chunk_x, int chunk_z, int world_x, int world_z);
static column_data get_column_data(worldgen_ctx *ctx, int x, int z);
static int generate_coal(worldgen_ctx *ctx, int x, int y, int z, int column_height, biome_id biome);
@@ -172,6 +184,7 @@ static double land_value(worldgen_ctx *ctx, int x, int z) {
value += ore_bonus;
if (col.biome == BIOME_OLD_GROWTH_PLAINS) value += 0.2;
if (col.biome == BIOME_WEST_KY_COALFIELDS) value += 0.1;
if (col.biome == BIOME_REDWOOD_FOREST) value += 0.18;
if (col.has_water && col.height >= col.water_surface - 2 && col.height <= col.water_surface + 2) {
value += 0.12;
}
@@ -183,6 +196,26 @@ static double land_value(worldgen_ctx *ctx, int x, int z) {
}
static void append_trail_segment(worldgen_ctx *ctx, int ax, int az, int bx, int bz, int *points, int count) {
/* Drop near-parallel segments that hug existing roads to keep the network from bunching up. */
const double min_parallel_distance = 72.0;
for (size_t i = 0; i < ctx->trail_segment_count; ++i) {
trail_segment *existing = &ctx->trail_segments[i];
if (!existing) continue;
int ex_ax = existing->ax;
int ex_az = existing->az;
int ex_bx = existing->bx;
int ex_bz = existing->bz;
if (existing->points && existing->count >= 2) {
ex_ax = existing->points[0];
ex_az = existing->points[1];
ex_bx = existing->points[(existing->count - 1) * 2];
ex_bz = existing->points[(existing->count - 1) * 2 + 1];
}
if (segment_parallel_and_close(ax, az, bx, bz, ex_ax, ex_az, ex_bx, ex_bz, min_parallel_distance)) {
free(points);
return;
}
}
if (ctx->trail_segment_count >= ctx->trail_segment_cap) {
size_t new_cap = ctx->trail_segment_cap ? ctx->trail_segment_cap * 2 : 32;
trail_segment *resized = (trail_segment *)realloc(ctx->trail_segments, new_cap * sizeof(trail_segment));
@@ -217,9 +250,9 @@ void worldgen_prepass(worldgen_ctx *ctx, int min_x, int max_x, int min_z, int ma
ctx->trail_segment_count = 0;
ctx->trail_segment_cap = 0;
const int step = 64;
const int max_points = 96;
const double min_spacing = 96.0;
const int step = 96;
const int max_points = 64;
const double min_spacing = 144.0;
int cap = max_points;
int count = 0;
int *px = (int *)malloc((size_t)cap * sizeof(int));
@@ -232,7 +265,7 @@ void worldgen_prepass(worldgen_ctx *ctx, int min_x, int max_x, int min_z, int ma
for (int z = min_z; z <= max_z; z += step) {
for (int x = min_x; x <= max_x; x += step) {
double val = land_value(ctx, x, z);
if (val < -0.05) continue;
if (val < 0.02) continue;
int spaced = 1;
for (int i = 0; i < count; ++i) {
double dx = (double)(x - px[i]);
@@ -436,6 +469,39 @@ static double old_growth_plains_mask(worldgen_ctx *ctx, int x, int z) {
return clamp01(mask);
}
static double rainfall_field(worldgen_ctx *ctx, int x, int z) {
/* Bias rainfall along pseudo-coastal ridges to mimic PNW storm tracks. */
double pacific_front = worley_distance(x - 48000, z + 52000, 0.00042, 0xA138B651u);
double coast_push = clamp01(1.18 - pacific_front * 1.25);
double storm_bands = simplex_noise2(&ctx->noise, (x + 26000) * 0.00055, (z - 26000) * 0.00055) * 0.5 + 0.5;
double drizzle = simplex_noise2(&ctx->noise, x * 0.00018, z * 0.00018) * 0.5 + 0.5;
double rain = coast_push * 0.45 + storm_bands * 0.35 + drizzle * 0.2;
return clamp01(rain);
}
static double redwood_biome_mask(worldgen_ctx *ctx, int x, int z, int column_height, int local_slope) {
double rain = rainfall_field(ctx, x, z);
double elevation = clamp01((double)(column_height - (ctx->sea_level + 4)) / 48.0);
if (column_height < ctx->sea_level - 2) return 0.0;
double ridge_noise = worley_distance(x + 32000, z - 36000, 0.0005, 0x5A8F7E21u);
double ridge_bias = clamp01(1.15 - ridge_noise * 1.2);
double slope_factor = clamp01((double)local_slope / 14.0);
double basin_relief = clamp01((double)(column_height - ctx->sea_level - 20) / 65.0);
double mask = rain * 0.5 + elevation * 0.25 + ridge_bias * 0.25;
mask *= (0.65 + slope_factor * 0.35);
mask *= (0.5 + basin_relief * 0.5);
return clamp01(mask);
}
static double redwood_tree_presence(worldgen_ctx *ctx, int x, int z) {
double cell = worley_distance(x - 54000, z + 54000, 0.022, 0xC7E4B129u);
double cluster = clamp01(1.25 - cell * 1.45);
double detail = simplex_noise2(&ctx->noise, (x + 18000) * 0.005, (z - 18000) * 0.005) * 0.5 + 0.5;
double ridge = simplex_noise2(&ctx->noise, (x - 42000) * 0.0016, (z + 42000) * 0.0016) * 0.5 + 0.5;
double presence = cluster * (0.65 + detail * 0.25 + ridge * 0.1);
return clamp01(presence);
}
static double region_blend(worldgen_ctx *ctx, int x, int z) {
(void)ctx;
@@ -467,6 +533,10 @@ static biome_id classify_biome(worldgen_ctx *ctx, int x, int z, int column_heigh
}
double blend = region_blend(ctx, x, z);
int slope = local_relief(ctx, x, z, column_height);
double redwood = redwood_biome_mask(ctx, x, z, column_height, slope);
if (redwood > 0.62) {
return BIOME_REDWOOD_FOREST;
}
double slope_score = clamp01((double)slope / 10.0);
double relief = clamp01((double)(column_height - ctx->sea_level) / 45.0);
double jitter = simplex_noise2(&ctx->noise, (x - 17000) * 0.001, (z + 17000) * 0.001) * 0.1;
@@ -632,6 +702,10 @@ static coal_zone classify_coal_zone(worldgen_ctx *ctx, int column_height, biome_
if (relief > 24) return COAL_ZONE_FOOTHILL;
return COAL_ZONE_WEST;
}
if (biome == BIOME_REDWOOD_FOREST) {
if (relief > 28) return COAL_ZONE_EAST;
return COAL_ZONE_FOOTHILL;
}
if (relief > 36) return COAL_ZONE_EAST;
return COAL_ZONE_FOOTHILL;
}
@@ -806,6 +880,15 @@ static uint16_t select_surface_block(worldgen_ctx *ctx, const column_data *data,
double noise = simplex_noise2(&ctx->noise, world_x * 0.02, world_z * 0.02) * 0.5 + 0.5;
if (noise < t) return BLOCK_SNOW;
}
if (data->biome == BIOME_REDWOOD_FOREST) {
int slope = ground_slope(ctx, world_x, world_z);
if (slope >= 5) return BLOCK_STONE;
if (slope >= 3) return BLOCK_GRAVEL;
double rain = rainfall_field(ctx, world_x, world_z);
double litter = simplex_noise2(&ctx->noise, (world_x + 14000) * 0.015, (world_z - 14000) * 0.015) * 0.5 + 0.5;
double mulch = clamp01(rain * 0.6 + (1.0 - litter) * 0.4);
if (mulch > 0.35) return BLOCK_DIRT;
}
return BLOCK_GRASS;
}
@@ -1331,6 +1414,60 @@ static void build_maple_ancient(worldgen_ctx *ctx, int x, int y, int z, int heig
place_leaf_circle(x, canopy_top + 1, z, 3, arch->leaf_block, rng, 0.1, out);
}
static void build_redwood_titan(worldgen_ctx *ctx, int x, int y, int z, int height, rng_state *rng, block_list *out, const tree_archetype *arch) {
(void)ctx;
int extra = rng_range_inclusive(rng, 20, 32);
int core_height = height + extra;
if (core_height < 60) core_height = 60;
if (y + core_height + 16 >= CHUNK_HEIGHT) {
core_height = CHUNK_HEIGHT - y - 16;
if (core_height < 40) core_height = 40;
}
int taper_start = core_height - 12;
for (int dy = 0; dy < core_height; ++dy) {
int radius = 3;
if (dy > taper_start) radius = 2;
if (dy > taper_start + 6) radius = 1;
for (int dx = -radius; dx <= radius; ++dx) {
for (int dz = -radius; dz <= radius; ++dz) {
if (radius >= 3 && abs(dx) == radius && abs(dz) == radius && dy > 4) continue;
if (radius == 2 && abs(dx) == 2 && abs(dz) == 2 && dy > taper_start) continue;
block_list_push(out, x + dx, y + dy, z + dz, (uint16_t)arch->log_block);
}
}
}
int spire = 6 + rng_range_inclusive(rng, 0, 3);
place_log_column(x, y + core_height, z, spire, arch->log_block, out);
int canopy_base = y + core_height - 6;
for (int ring = 0; ring < 5; ++ring) {
int radius = 6 - ring;
if (radius < 2) radius = 2;
place_leaf_circle(x, canopy_base + ring, z, radius, arch->leaf_block, rng, 0.1, out);
place_leaf_circle(x, canopy_base + ring + 1, z, radius - 1, arch->leaf_block, rng, 0.18, out);
}
place_leaf_circle(x, canopy_base + 6, z, 2, arch->leaf_block, rng, 0.0, out);
place_leaf_circle(x, canopy_base + 7, z, 1, arch->leaf_block, rng, 0.0, out);
const int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int i = 0; i < 4; ++i) {
int buttress_height = 4 + rng_range_inclusive(rng, 0, 3);
for (int step = 0; step < 4; ++step) {
int px = x + dirs[i][0] * (2 + step);
int pz = z + dirs[i][1] * (2 + step);
int stack = buttress_height - step;
if (stack < 1) stack = 1;
for (int s = 0; s < stack; ++s) {
block_list_push(out, px, y + s, pz, (uint16_t)arch->log_block);
}
}
}
for (int i = 0; i < 4; ++i) {
int branch_y = canopy_base - 3 - rng_range_inclusive(rng, 0, 3);
int branch_len = 5 + rng_range_inclusive(rng, 0, 3);
place_branch_span(x, branch_y, z, dirs[i][0], dirs[i][1], branch_len, 3, arch->log_block, arch->leaf_block, rng, out);
}
place_leaf_blob(x, canopy_base - 2, z, 5, 4, arch->leaf_block, rng, out);
}
typedef enum {
TREE_OAK_ROUND = 0,
TREE_OAK_SPRAWL,
@@ -1352,6 +1489,7 @@ static void build_maple_ancient(worldgen_ctx *ctx, int x, int y, int z, int heig
TREE_OAK_ANCIENT,
TREE_SPRUCE_ANCIENT,
TREE_MAPLE_ANCIENT,
TREE_REDWOOD_TITAN,
TREE_COUNT
} tree_kind;
@@ -1372,10 +1510,11 @@ static void build_maple_ancient(worldgen_ctx *ctx, int x, int y, int z, int heig
[TREE_CYPRESS_COLUMN] = {"cypress_column", TREE_SPECIES_CYPRESS, BLOCK_BIRCH_LOG, BLOCK_BIRCH_LEAVES, 8, 12, 2, 6, build_cypress_column},
[TREE_CYPRESS_FAN] = {"cypress_fan", TREE_SPECIES_CYPRESS, BLOCK_BIRCH_LOG, BLOCK_BIRCH_LEAVES, 8, 12, 3, 6, build_cypress_fan},
[TREE_MAPLE_TANGLE] = {"maple_tangle", TREE_SPECIES_MAPLE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 7, 10, 4, 5, build_maple_tangle},
[TREE_MAPLE_SPREAD] = {"maple_spread", TREE_SPECIES_MAPLE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 7, 10, 4, 5, build_maple_spread},
[TREE_OAK_ANCIENT] = {"oak_ancient", TREE_SPECIES_OAK, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 12, 16, 6, 7, build_oak_ancient},
[TREE_SPRUCE_ANCIENT] = {"spruce_ancient", TREE_SPECIES_SPRUCE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 12, 16, 5, 7, build_spruce_ancient},
[TREE_MAPLE_ANCIENT] = {"maple_ancient", TREE_SPECIES_MAPLE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 11, 15, 6, 6, build_maple_ancient},
[TREE_MAPLE_SPREAD] = {"maple_spread", TREE_SPECIES_MAPLE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 7, 10, 4, 5, build_maple_spread},
[TREE_OAK_ANCIENT] = {"oak_ancient", TREE_SPECIES_OAK, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 12, 16, 6, 7, build_oak_ancient},
[TREE_SPRUCE_ANCIENT] = {"spruce_ancient", TREE_SPECIES_SPRUCE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 12, 16, 5, 7, build_spruce_ancient},
[TREE_MAPLE_ANCIENT] = {"maple_ancient", TREE_SPECIES_MAPLE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 11, 15, 6, 6, build_maple_ancient},
[TREE_REDWOOD_TITAN] = {"redwood_titan", TREE_SPECIES_PINE, BLOCK_OAK_LOG, BLOCK_OAK_LEAVES, 70, 86, 7, 24, build_redwood_titan},
};
static const int TREE_VARIANTS_OAK[] = {TREE_OAK_ROUND, TREE_OAK_SPRAWL, TREE_OAK_COLUMNAR, TREE_OAK_ANCIENT};
@@ -1440,7 +1579,7 @@ static const int TREE_POOL_OLD_GROWTH[] = {
TREE_SPRUCE_TIERS, TREE_PINE_CROWN
};
static const tree_archetype *choose_tree_archetype(worldgen_ctx *ctx, const column_data *data, rng_state *rng) {
static const tree_archetype *choose_tree_archetype(worldgen_ctx *ctx, const column_data *data, int world_x, int world_z, rng_state *rng) {
int altitude = data->height - ctx->sea_level;
const tree_archetype *fallback = &TREE_TYPES[TREE_OAK_ROUND];
int snow_line = ctx->snow_line;
@@ -1448,6 +1587,14 @@ static const tree_archetype *choose_tree_archetype(worldgen_ctx *ctx, const colu
const tree_archetype *snow_pick = choose_species_variant(TREE_SPECIES_SPRUCE, rng);
if (snow_pick && rng_next_f64(rng) < 0.9) return snow_pick;
}
if (data->biome == BIOME_REDWOOD_FOREST) {
double stand = redwood_tree_presence(ctx, world_x, world_z);
if (stand > 0.35 || rng_next_f64(rng) < 0.8) {
return &TREE_TYPES[TREE_REDWOOD_TITAN];
}
const tree_archetype *spruce = choose_species_variant(TREE_SPECIES_SPRUCE, rng);
if (spruce) return spruce;
}
if (data->biome == BIOME_OLD_GROWTH_PLAINS) {
size_t pool_size = sizeof(TREE_POOL_OLD_GROWTH) / sizeof(TREE_POOL_OLD_GROWTH[0]);
size_t idx = (size_t)rng_range_inclusive(rng, 0, (int)pool_size - 1);
@@ -1476,12 +1623,77 @@ static void generate_tree(worldgen_ctx *ctx, const tree_archetype *arch, int x,
arch->builder(ctx, x, y, z, height, rng, out, arch);
}
static int try_place_redwood_fallen_log(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_data *chunk,
column_data columns[CHUNK_SIZE][CHUNK_SIZE], int start_dx, int start_dz, rng_state *rng) {
(void)ctx;
(void)chunk_x;
(void)chunk_z;
const int dirs[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int dir_idx = rng_range_inclusive(rng, 0, 3);
int dir_x = dirs[dir_idx][0];
int dir_z = dirs[dir_idx][1];
int len = 3 + rng_range_inclusive(rng, 0, 3);
column_data origin = columns[start_dx][start_dz];
if (origin.biome != BIOME_REDWOOD_FOREST) return 0;
if (origin.height <= 0 || origin.height >= CHUNK_HEIGHT - 2) return 0;
int base_height = origin.height;
for (int step = 0; step < len; ++step) {
int lx = start_dx + dir_x * step;
int lz = start_dz + dir_z * step;
if (lx < 0 || lx >= CHUNK_SIZE || lz < 0 || lz >= CHUNK_SIZE) return 0;
column_data col = columns[lx][lz];
if (col.biome != BIOME_REDWOOD_FOREST) return 0;
if (col.height <= 0 || col.height >= CHUNK_HEIGHT - 2) return 0;
if (abs(col.height - base_height) > 1) return 0;
uint16_t above = chunk->blocks[col.height + 1][lx][lz];
if (above != BLOCK_AIR && above != BLOCK_TALL_GRASS) return 0;
}
uint16_t log_block = (dir_z != 0) ? BLOCK_OAK_LOG_Z : BLOCK_OAK_LOG_X;
for (int step = 0; step < len; ++step) {
int lx = start_dx + dir_x * step;
int lz = start_dz + dir_z * step;
column_data col = columns[lx][lz];
chunk->blocks[col.height + 1][lx][lz] = log_block;
}
return 1;
}
static void generate_chunk_redwood_floor(worldgen_ctx *ctx, int chunk_x, int chunk_z,
column_data columns[CHUNK_SIZE][CHUNK_SIZE], chunk_data *chunk) {
int world_chunk_seed = hash_coords(chunk_x, chunk_z, (uint32_t)(ctx->world_seed ^ 0x6D23F1A7u));
rng_state rng;
rng_seed(&rng, (uint64_t)world_chunk_seed << 1);
for (int dx = 0; dx < CHUNK_SIZE; ++dx) {
for (int dz = 0; dz < CHUNK_SIZE; ++dz) {
column_data cd = columns[dx][dz];
if (cd.biome != BIOME_REDWOOD_FOREST) continue;
if (cd.height <= 0 || cd.height >= CHUNK_HEIGHT - 2) continue;
int wx = chunk_x * CHUNK_SIZE + dx;
int wz = chunk_z * CHUNK_SIZE + dz;
uint16_t ground = chunk->blocks[cd.height][dx][dz];
if (ground == BLOCK_GRASS) {
chunk->blocks[cd.height][dx][dz] = BLOCK_DIRT;
}
double rain = rainfall_field(ctx, wx, wz);
double litter = simplex_noise2(&ctx->noise, (wx + 8000) * 0.03, (wz - 8000) * 0.03) * 0.5 + 0.5;
double fern_chance = clamp01(0.25 + rain * 0.5 + (1.0 - litter) * 0.25);
if (chunk->blocks[cd.height + 1][dx][dz] == BLOCK_AIR && rng_next_f64(&rng) < fern_chance) {
chunk->blocks[cd.height + 1][dx][dz] = BLOCK_TALL_GRASS;
}
if (rng_next_f64(&rng) < 0.0035) {
try_place_redwood_fallen_log(ctx, chunk_x, chunk_z, chunk, columns, dx, dz, &rng);
}
}
}
}
static void generate_chunk_grass(worldgen_ctx *ctx, int chunk_x, int chunk_z, column_data columns[CHUNK_SIZE][CHUNK_SIZE], chunk_data *out) {
for (int dx = 0; dx < CHUNK_SIZE; ++dx) {
for (int dz = 0; dz < CHUNK_SIZE; ++dz) {
column_data cd = columns[dx][dz];
if (cd.height <= 0 || cd.height >= CHUNK_HEIGHT - 1) continue;
if (cd.height >= ctx->snow_line - 1) continue;
if (cd.biome == BIOME_REDWOOD_FOREST) continue;
int world_x = chunk_x * CHUNK_SIZE + dx;
int world_z = chunk_z * CHUNK_SIZE + dz;
if (out->blocks[cd.height][dx][dz] != BLOCK_GRASS) continue;
@@ -1490,7 +1702,8 @@ static void generate_chunk_grass(worldgen_ctx *ctx, int chunk_x, int chunk_z, co
double density = tree_density_mask(ctx, world_x, world_z);
double meadow = simplex_noise2(&ctx->noise, (world_x + 1200) * 0.02, (world_z - 1200) * 0.02) * 0.5 + 0.5;
double humidity = simplex_noise2(&ctx->noise, (world_x - 9000) * 0.01, (world_z + 9000) * 0.01) * 0.5 + 0.5;
double chance = 0.15 + meadow * 0.25 + humidity * 0.2 + (1.0 - density) * 0.2;
double rain = rainfall_field(ctx, world_x, world_z);
double chance = 0.15 + meadow * 0.25 + humidity * 0.2 + (1.0 - density) * 0.2 + (rain - 0.5) * 0.2;
if (cd.height < ctx->sea_level) chance *= 0.7;
chance = clamp01(chance);
uint32_t h = hash_coords(world_x, world_z, (uint32_t)(ctx->world_seed ^ 0x5F3759DFu));
@@ -1519,6 +1732,7 @@ static void generate_chunk_flowers(worldgen_ctx *ctx, int chunk_x, int chunk_z,
for (int dz = 0; dz < CHUNK_SIZE; ++dz) {
column_data cd = columns[dx][dz];
if (cd.height <= 0 || cd.height >= CHUNK_HEIGHT - 2) continue;
if (cd.biome == BIOME_REDWOOD_FOREST) continue;
int world_x = chunk_x * CHUNK_SIZE + dx;
int world_z = chunk_z * CHUNK_SIZE + dz;
if (out->blocks[cd.height][dx][dz] != BLOCK_GRASS) continue;
@@ -2041,6 +2255,7 @@ static int try_place_cabin(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_da
int lz = wz - chunk_origin_z;
column_data col = columns[lx][lz];
if (col.has_water && col.height < col.water_surface) return 0;
if (column_has_manmade_surface(chunk, chunk_x, chunk_z, wx, wz)) return 0;
if (col.height < min_h) min_h = col.height;
if (col.height > max_h) max_h = col.height;
}
@@ -2102,6 +2317,9 @@ static void generate_chunk_cabins(worldgen_ctx *ctx, int chunk_x, int chunk_z, c
rng_seed(&rng, seed ^ 0xCAB1A5u);
double noise = simplex_noise2(&ctx->noise, (chunk_x * CHUNK_SIZE + 5000) * 0.0006, (chunk_z * CHUNK_SIZE - 5000) * 0.0006) * 0.5 + 0.5;
double spawn_chance = 0.015 + noise * 0.02;
if (ctx->enable_trails && !chunk_contains_trail(ctx, chunk_x, chunk_z)) {
return; /* only spawn cabins where a road crosses the chunk, so the spur can hook in */
}
if (rng_next_f64(&rng) > spawn_chance) return;
size_t blueprint_count = sizeof(CABIN_BLUEPRINTS) / sizeof(CABIN_BLUEPRINTS[0]);
unsigned char occupancy[CHUNK_SIZE][CHUNK_SIZE];
@@ -2125,10 +2343,34 @@ static void generate_chunk_cabins(worldgen_ctx *ctx, int chunk_x, int chunk_z, c
if (req_max_z < max_local_z) max_local_z = req_max_z;
}
if (min_local_x >= max_local_x - 1 || min_local_z >= max_local_z - 1) continue;
int local_cx = rng_range_inclusive(&rng, min_local_x, max_local_x);
int local_cz = rng_range_inclusive(&rng, min_local_z, max_local_z);
int world_cx = chunk_x * CHUNK_SIZE + local_cx;
int world_cz = chunk_z * CHUNK_SIZE + local_cz;
int best_local_x = min_local_x;
int best_local_z = min_local_z;
double best_score = -1e9;
const int samples = 12;
for (int s = 0; s < samples; ++s) {
int local_cx = rng_range_inclusive(&rng, min_local_x, max_local_x);
int local_cz = rng_range_inclusive(&rng, min_local_z, max_local_z);
int world_cx = chunk_x * CHUNK_SIZE + local_cx;
int world_cz = chunk_z * CHUNK_SIZE + local_cz;
double score = land_value(ctx, world_cx, world_cz);
if (ctx->enable_trails) {
double dist2 = nearest_trail_distance2(ctx, world_cx, world_cz, 1.0e12);
double bonus = 0.0;
double dist = sqrt(dist2);
if (dist < 640.0) {
double t = clamp01(1.0 - dist / 640.0);
bonus += 0.45 * t;
}
score += bonus;
}
if (score > best_score) {
best_score = score;
best_local_x = local_cx;
best_local_z = local_cz;
}
}
int world_cx = chunk_x * CHUNK_SIZE + best_local_x;
int world_cz = chunk_z * CHUNK_SIZE + best_local_z;
if (try_place_cabin(ctx, chunk_x, chunk_z, out, columns, occupancy, world_cx, world_cz, bp, &rng)) {
break;
}
@@ -2202,16 +2444,18 @@ static void connect_cabin_to_trail(worldgen_ctx *ctx, int chunk_x, int chunk_z,
if (path_width < 2) path_width = 2;
carve_cabin_path(ctx, chunk_x, chunk_z, chunk, columns, start_x, start_z, step_x, step_z, spur_len, path_width);
int target_x = 0, target_z = 0;
int found = 0;
if (ctx && ctx->enable_trails) {
found = find_nearest_trail_point_from_segments(ctx, start_x, start_z, &target_x, &target_z);
int found_seg = find_nearest_trail_point_from_segments(ctx, start_x, start_z, &target_x, &target_z);
int found = found_seg;
if (!found) {
found = find_nearest_trail_block(columns, chunk, chunk_x, chunk_z, start_x, start_z, 512, &target_x, &target_z);
}
if (!found && found_seg) {
/* We know a segment exists; just aim for it even if no gravel in this chunk. */
found = 1;
}
if (!found) {
found = find_nearest_trail_block(columns, chunk, chunk_x, chunk_z, start_x, start_z, 192, &target_x, &target_z);
}
if (!found) {
int fallback_x = start_x + step_x * 120;
int fallback_z = start_z + step_z * 120;
int fallback_x = start_x + step_x * 240;
int fallback_z = start_z + step_z * 240;
carve_trail_span(ctx, chunk_x, chunk_z, chunk, columns, start_x, start_z, fallback_x, fallback_z, path_width);
return;
}
@@ -2292,9 +2536,9 @@ static uint16_t generate_normal_ores(worldgen_ctx *ctx, int x, int y, int z, con
weight = ore_depth_weight(y, 68, -4);
if (weight > 0.0) {
cluster = ore_cluster_field(ctx, x, y, z, 0.018, 6000.0);
if (cluster > 0.58) {
double biome_bonus = (col->biome == BIOME_EAST_KY_RIDGEBREAKS) ? 0.05 : 0.0;
chance = 0.04 + weight * 0.25 + biome_bonus;
if (cluster > 0.60) {
double biome_bonus = (col->biome == BIOME_EAST_KY_RIDGEBREAKS) ? 0.01 : 0.0;
chance = 0.008 + weight * 0.08 + biome_bonus;
h = hash_coords3(x, y, z, seed ^ 0x1B0EFACEu);
if ((h & 0xFFFF) <= (uint32_t)(chance * 65535.0)) {
return BLOCK_IRON_ORE;
@@ -2377,13 +2621,21 @@ static void generate_chunk_trees(worldgen_ctx *ctx, int chunk_x, int chunk_z, ch
column_data data = get_column_data(ctx, candidate_x, candidate_z);
if (data.has_water && data.height < data.water_surface) continue;
if (data.height < min_tree_alt || data.height > tree_line) continue;
if (ground_slope(ctx, candidate_x, candidate_z) > max_slope) continue;
int slope = ground_slope(ctx, candidate_x, candidate_z);
int slope_limit = (data.biome == BIOME_REDWOOD_FOREST) ? 4 : max_slope;
if (slope > slope_limit) continue;
int is_old_growth = (data.biome == BIOME_OLD_GROWTH_PLAINS);
int is_redwood = (data.biome == BIOME_REDWOOD_FOREST);
double stand_density = is_redwood ? redwood_tree_presence(ctx, candidate_x, candidate_z) : 0.0;
double grove_mask = is_old_growth ? old_growth_grove_mask(ctx, candidate_x, candidate_z) : 0.0;
if (is_old_growth && grove_mask < 0.35) {
double skip_chance = 0.7 - grove_mask * 0.5;
if (rng_next_f64(&rng) < skip_chance) continue;
}
if (is_redwood && stand_density < 0.2) {
double thinning = clamp01(0.8 - stand_density * 0.6);
if (rng_next_f64(&rng) < thinning) continue;
}
double altitude_factor = 1.0;
if (data.height < low_fade_top) {
@@ -2397,21 +2649,30 @@ static void generate_chunk_trees(worldgen_ctx *ctx, int chunk_x, int chunk_z, ch
if (is_old_growth) {
altitude_factor *= 0.85 + 0.15 * grove_mask;
}
if (is_redwood) {
altitude_factor *= clamp01(0.7 + stand_density * 0.4);
}
if (altitude_factor <= 0.0) continue;
double density = tree_density_mask(ctx, candidate_x, candidate_z);
if (is_old_growth) {
density = clamp01(0.25 + grove_mask * 0.75);
}
double spawn_prob = 0.7 * (0.55 + 0.45 * density) * altitude_factor;
if (is_old_growth) {
spawn_prob = clamp01(0.25 + grove_mask * 0.7) * altitude_factor;
double spawn_prob;
if (is_redwood) {
double rain = rainfall_field(ctx, candidate_x, candidate_z);
spawn_prob = clamp01(0.25 + stand_density * 0.65 + (rain - 0.5) * 0.2) * altitude_factor;
} else {
double density = tree_density_mask(ctx, candidate_x, candidate_z);
if (is_old_growth) {
density = clamp01(0.25 + grove_mask * 0.75);
}
spawn_prob = 0.7 * (0.55 + 0.45 * density) * altitude_factor;
if (is_old_growth) {
spawn_prob = clamp01(0.25 + grove_mask * 0.7) * altitude_factor;
}
}
if (rng_next_f64(&rng) > spawn_prob) continue;
block_list tmp;
block_list_init(&tmp);
const tree_archetype *arch = choose_tree_archetype(ctx, &data, &rng);
const tree_archetype *arch = choose_tree_archetype(ctx, &data, candidate_x, candidate_z, &rng);
int base_y = data.height + 1;
uint16_t surface = select_surface_block(ctx, &data, candidate_x, candidate_z);
if (surface == BLOCK_SNOW) {
@@ -2461,11 +2722,7 @@ static const trail_neighbor_offset TRAIL_NEIGHBOR_OFFSETS[] = {
{ 1, 0 },
{ 0, 1 },
{ 1, 1 },
{ 1, -1 },
{ 2, 0 },
{ 0, 2 },
{ 2, 1 },
{ 1, 2 }
{ 1, -1 }
};
static uint32_t trail_segment_hash(int ax, int az, int bx, int bz, uint32_t seed) {
@@ -2516,13 +2773,13 @@ static int should_connect_trail_nodes(worldgen_ctx *ctx, int node_x0, int node_z
double dx = fabs((double)(node_x1 - node_x0));
double dz = fabs((double)(node_z1 - node_z0));
double grid_len = sqrt(dx * dx + dz * dz);
double base = 0.12;
double base = 0.08;
if (grid_len <= 1.05) {
base += 0.2;
base += 0.18;
} else if (grid_len <= 2.2) {
base += 0.12;
base += 0.1;
} else {
base += 0.05;
base += 0.02;
}
int axis_edge = (dx == 0.0 || dz == 0.0);
if (axis_edge) {
@@ -2549,6 +2806,141 @@ static int should_connect_trail_nodes(worldgen_ctx *ctx, int node_x0, int node_z
return r < base;
}
static int *smooth_trail_polyline(worldgen_ctx *ctx, int *points, int count, int *out_count) {
/* Keep endpoints fixed, gently relax interior vertices and add a tiny perpendicular wiggle so paths stop looking grid-aligned. */
if (count < 3 || !points) {
*out_count = count;
return points;
}
int *smoothed = (int *)malloc((size_t)count * 2 * sizeof(int));
if (!smoothed) {
*out_count = count;
return points;
}
/* Preserve endpoints exactly */
smoothed[0] = points[0];
smoothed[1] = points[1];
smoothed[(count - 1) * 2] = points[(count - 1) * 2];
smoothed[(count - 1) * 2 + 1] = points[(count - 1) * 2 + 1];
for (int i = 1; i < count - 1; ++i) {
double ax = (double)points[(i - 1) * 2];
double az = (double)points[(i - 1) * 2 + 1];
double bx = (double)points[i * 2];
double bz = (double)points[i * 2 + 1];
double cx = (double)points[(i + 1) * 2];
double cz = (double)points[(i + 1) * 2 + 1];
/* Weighted average pulls vertices off the stair-step grid */
double mx = (ax + 4.0 * bx + cx) / 6.0;
double mz = (az + 4.0 * bz + cz) / 6.0;
double dx = cx - ax;
double dz = cz - az;
double len = sqrt(dx * dx + dz * dz);
double px = 0.0, pz = 0.0;
if (len > 0.001) {
px = -dz / len;
pz = dx / len;
}
/* Tiny perpendicular wiggle breaks perfectly straight segments */
double wiggle = simplex_noise2(&ctx->noise, bx * 0.0025, bz * 0.0025) * 0.5; /* -0.5..0.5 */
double wiggle_mag = 2.0 + (simplex_noise2(&ctx->noise, (bx + 12000.0) * 0.0018, (bz - 12000.0) * 0.0018) * 0.5 + 0.5) * 2.5;
mx += px * wiggle * wiggle_mag;
mz += pz * wiggle * wiggle_mag;
smoothed[i * 2] = (int)llround(mx);
smoothed[i * 2 + 1] = (int)llround(mz);
}
free(points);
*out_count = count;
return smoothed;
}
static int choose_trail_width(worldgen_ctx *ctx, int x0, int z0, int x1, int z1) {
uint32_t h = hash_coords((x0 + x1) / 2, (z0 + z1) / 2, (uint32_t)ctx->world_seed ^ 0xC0FFEEu);
int delta = (int)(h % 3) - 1; /* -1, 0, or +1 */
int w = TRAIL_WIDTH + delta;
if (w < 3) w = 3;
if (w > 7) w = 7;
return w;
}
static double point_segment_distance2(double px, double pz, double ax, double az, double bx, double bz) {
double vx = bx - ax;
double vz = bz - az;
double len2 = vx * vx + vz * vz;
double t = 0.0;
if (len2 > 0.0001) {
t = ((px - ax) * vx + (pz - az) * vz) / len2;
if (t < 0.0) t = 0.0;
if (t > 1.0) t = 1.0;
}
double cx = ax + vx * t;
double cz = az + vz * t;
double dx = px - cx;
double dz = pz - cz;
return dx * dx + dz * dz;
}
static int segment_parallel_and_close(int ax, int az, int bx, int bz, int cx, int cz, int dx, int dz, double min_dist) {
double ux = (double)bx - (double)ax;
double uz = (double)bz - (double)az;
double vx = (double)dx - (double)cx;
double vz = (double)dz - (double)cz;
double ulen = sqrt(ux * ux + uz * uz);
double vlen = sqrt(vx * vx + vz * vz);
if (ulen < 0.0001 || vlen < 0.0001) return 0;
ux /= ulen; uz /= ulen;
vx /= vlen; vz /= vlen;
double alignment = fabs(ux * vx + uz * vz);
if (alignment < 0.92) return 0; /* not parallel enough */
double min_d2 = point_segment_distance2((double)ax, (double)az, (double)cx, (double)cz, (double)dx, (double)dz);
double d2 = point_segment_distance2((double)bx, (double)bz, (double)cx, (double)cz, (double)dx, (double)dz);
if (d2 < min_d2) min_d2 = d2;
d2 = point_segment_distance2((double)cx, (double)cz, (double)ax, (double)az, (double)bx, (double)bz);
if (d2 < min_d2) min_d2 = d2;
d2 = point_segment_distance2((double)dx, (double)dz, (double)ax, (double)az, (double)bx, (double)bz);
if (d2 < min_d2) min_d2 = d2;
return min_d2 < min_dist * min_dist;
}
static double nearest_trail_distance2(worldgen_ctx *ctx, int x, int z, double fallback) {
if (!ctx || ctx->trail_segment_count == 0) return fallback;
double best = fallback;
for (size_t i = 0; i < ctx->trail_segment_count; ++i) {
trail_segment *seg = &ctx->trail_segments[i];
if (!seg || seg->count < 1 || !seg->points) continue;
for (int p = 0; p < seg->count; ++p) {
int tx = seg->points[p * 2];
int tz = seg->points[p * 2 + 1];
double dx = (double)(tx - x);
double dz = (double)(tz - z);
double d2 = dx * dx + dz * dz;
if (d2 < best) best = d2;
}
}
return best;
}
static int chunk_contains_trail(worldgen_ctx *ctx, int chunk_x, int chunk_z) {
if (!ctx || ctx->trail_segment_count == 0) return 0;
int min_x = chunk_x * CHUNK_SIZE;
int max_x = min_x + CHUNK_SIZE - 1;
int min_z = chunk_z * CHUNK_SIZE;
int max_z = min_z + CHUNK_SIZE - 1;
for (size_t i = 0; i < ctx->trail_segment_count; ++i) {
trail_segment *seg = &ctx->trail_segments[i];
if (!seg || seg->count < 1 || !seg->points) continue;
for (int p = 0; p < seg->count; ++p) {
int tx = seg->points[p * 2];
int tz = seg->points[p * 2 + 1];
if (tx >= min_x && tx <= max_x && tz >= min_z && tz <= max_z) {
return 1;
}
}
}
return 0;
}
static int build_trail_path(worldgen_ctx *ctx, double ax, double az, double bx, double bz, int **out_points, int *out_count) {
double min_x = fmin(ax, bx) - TRAIL_MARGIN;
double max_x = fmax(ax, bx) + TRAIL_MARGIN;
@@ -2621,18 +3013,20 @@ static int build_trail_path(worldgen_ctx *ctx, double ax, double az, double bx,
double next_height = heights[ni];
double slope = fabs(next_height - current_height);
if (slope > 3.0) continue;
int water = 0;
int wx = (int)llround(min_x + nx * TRAIL_CELL_SIZE);
int wz = (int)llround(min_z + nz * TRAIL_CELL_SIZE);
column_data cd = get_column_data(ctx, wx, wz);
if (cd.has_water && next_height <= cd.water_surface) {
water = 1;
continue; /* trails should never be routed through water */
}
double tree_penalty = 0.0;
if (cd.biome == BIOME_REDWOOD_FOREST) {
double stand = redwood_tree_presence(ctx, wx, wz);
if (stand > 0.78) continue; /* treat massive trunks as hard obstacles */
tree_penalty = stand;
}
double base_cost = (neighbors[n][0] == 0 || neighbors[n][1] == 0) ? 1.0 : 1.41421356237;
double cost = base_cost * (1.0 + slope * 6.0);
if (water) {
cost += 10.0;
}
double cost = base_cost * (1.0 + slope * 6.0 + tree_penalty * 4.5) + tree_penalty * 6.5;
if (dist[current] + cost < dist[ni]) {
dist[ni] = dist[current] + cost;
prev[ni] = current;
@@ -2672,6 +3066,8 @@ static int build_trail_path(worldgen_ctx *ctx, double ax, double az, double bx,
break;
}
}
/* Post-process to soften grid artifacts */
points = smooth_trail_polyline(ctx, points, path_len, &path_len);
*out_points = points;
*out_count = path_len;
free(heights);
@@ -2712,6 +3108,25 @@ static trail_segment *get_trail_segment(worldgen_ctx *ctx, int node_x0, int node
double ax, az, bx, bz;
trail_node_position(ctx, node_x0, node_z0, TRAIL_NODE_SPACING, &ax, &az);
trail_node_position(ctx, node_x1, node_z1, TRAIL_NODE_SPACING, &bx, &bz);
const double min_parallel_distance = 72.0;
for (size_t i = 0; i < ctx->trail_segment_count; ++i) {
trail_segment *existing = &ctx->trail_segments[i];
if (!existing) continue;
int ex_ax = existing->ax;
int ex_az = existing->az;
int ex_bx = existing->bx;
int ex_bz = existing->bz;
if (existing->points && existing->count >= 2) {
ex_ax = existing->points[0];
ex_az = existing->points[1];
ex_bx = existing->points[(existing->count - 1) * 2];
ex_bz = existing->points[(existing->count - 1) * 2 + 1];
}
if (segment_parallel_and_close((int)llround(ax), (int)llround(az), (int)llround(bx), (int)llround(bz),
ex_ax, ex_az, ex_bx, ex_bz, min_parallel_distance)) {
return NULL;
}
}
int *points = NULL;
int count = 0;
if (!build_trail_path(ctx, ax, az, bx, bz, &points, &count)) {
@@ -2843,7 +3258,6 @@ static void generate_chunk_trails(worldgen_ctx *ctx, int chunk_x, int chunk_z, c
int chunk_max_x = chunk_min_x + CHUNK_SIZE - 1;
int chunk_min_z = chunk_z * CHUNK_SIZE;
int chunk_max_z = chunk_min_z + CHUNK_SIZE - 1;
int width = TRAIL_WIDTH;
if (ctx->trail_segment_count > 0) {
for (size_t s = 0; s < ctx->trail_segment_count; ++s) {
@@ -2860,6 +3274,7 @@ static void generate_chunk_trails(worldgen_ctx *ctx, int chunk_x, int chunk_z, c
int span_max_z = (z0 > z1) ? z0 : z1;
if (span_max_x < chunk_min_x - 4 || span_min_x > chunk_max_x + 4) continue;
if (span_max_z < chunk_min_z - 4 || span_min_z > chunk_max_z + 4) continue;
int width = choose_trail_width(ctx, x0, z0, x1, z1);
carve_trail_span(ctx, chunk_x, chunk_z, out, columns, x0, z0, x1, z1, width);
}
}
@@ -2891,6 +3306,7 @@ static void generate_chunk_trails(worldgen_ctx *ctx, int chunk_x, int chunk_z, c
int span_max_z = (z0 > z1) ? z0 : z1;
if (span_max_x < chunk_min_x - 4 || span_min_x > chunk_max_x + 4) continue;
if (span_max_z < chunk_min_z - 4 || span_min_z > chunk_max_z + 4) continue;
int width = choose_trail_width(ctx, x0, z0, x1, z1);
carve_trail_span(ctx, chunk_x, chunk_z, out, columns, x0, z0, x1, z1, width);
}
}
@@ -2968,6 +3384,7 @@ void worldgen_generate_chunk(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_
}
generate_chunk_cabins(ctx, chunk_x, chunk_z, columns, out);
generate_chunk_redwood_floor(ctx, chunk_x, chunk_z, columns, out);
generate_chunk_grass(ctx, chunk_x, chunk_z, columns, out);
generate_chunk_flowers(ctx, chunk_x, chunk_z, columns, out);