diff --git a/.gitignore b/.gitignore index 1a90dd6..d7b740f 100644 --- a/.gitignore +++ b/.gitignore @@ -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-*/ diff --git a/worldgen-c/bin/worldgen b/worldgen-c/bin/worldgen index 0c78795..4730b1d 100755 Binary files a/worldgen-c/bin/worldgen and b/worldgen-c/bin/worldgen differ diff --git a/worldgen-c/src/worldgen.c b/worldgen-c/src/worldgen.c index 5192402..206fbe0 100644 --- a/worldgen-c/src/worldgen.c +++ b/worldgen-c/src/worldgen.c @@ -8,7 +8,7 @@ #include #include -#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);