Compare commits

..

2 Commits

Author SHA1 Message Date
MC-Worldgen Dev
0a06f34d99 Tune tree density/spacing and fix trail distance to use segments
- nearest_trail_distance2 now measures point-to-segment distance to each
  trail edge instead of only to stored points, with a single-point fallback.
- Tree height scaling keeps canopy proportions; maturity adds subtle
  variation instead of shrinking young trees to bare poles.
- Raise forest spawn probability and tighten spacing for denser, more
  coherent stands; redwoods cluster a bit closer.
- Cabins near trails favor a wooded setback off the centerline; the spur
  still connects.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-11 20:33:21 +00:00
root
e2436f6e24 Improve roads, cabins, forests, and woodland communities 2026-07-11 20:00:19 +00:00
6 changed files with 474 additions and 69 deletions

View File

@@ -74,11 +74,30 @@ Features
- Redwoods are enabled automatically. The biome classifier marks high-rainfall, - Redwoods are enabled automatically. The biome classifier marks high-rainfall,
higher-relief areas as redwood forest, then places titan trees, dirt/mulch higher-relief areas as redwood forest, then places titan trees, dirt/mulch
floor cover, tall grass, and occasional fallen logs. floor cover, tall grass, and occasional fallen logs.
- General forests grow as broad, coherent oak, maple, birch, pine, spruce,
walnut, and cypress stands with occasional companion species. Seeded
clearings, sparse field trees, regenerating edges, mature interiors, variable
tree height, and irregular crown spacing keep the distribution from resolving
into a uniform grid.
- Cabins are enabled automatically. With `--trails`, cabins only try to spawn on - Cabins are enabled automatically. With `--trails`, cabins only try to spawn on
chunks crossed by the trail network so their spur paths can connect to roads. chunks crossed by the trail network so their spur paths can connect to roads.
Without `--trails`, cabins can still spawn, but they are rarer and unconnected. Without `--trails`, cabins can still spawn, but they are rarer and unconnected.
- Cabin generation includes compact homesteads, longhouses, two-story houses,
L-shaped cabins, cross-gabled lodges, mixed-height tower lodges, and rambling
multi-wing cabins. Roof direction, wing height, material palette, windows,
porches, chimneys, decay, and path width vary by design.
- Seeded woodland-community regions raise the local cabin frequency and give
their roads permission to bridge narrowly bounded water at exact sea level.
Eligible crossings must have nearby dry banks on both sides; deep water,
broad lakes, and open ocean remain impassable. Bridges use raised spruce
decks, timber supports, and intermittent edge posts rather than gravel fill.
- Trails are optional because they need a prepass over the target area. Use - Trails are optional because they need a prepass over the target area. Use
`--trails` when you want roads and better-connected cabins. `--trails` when you want roads and better-connected cabins.
- Long roads search a broad terrain corridor and prefer valley floors, gentle
contour-following grades, low ridge crossings, and stable side slopes. Deep
water remains impassable; shallow water is crossed only when the detour cost
is greater, keeping fords and causeways short. Short cabin spurs use looser
engineering constraints so they can still reach the main network.
Minecraft Version Minecraft Version
----------------- -----------------

Binary file not shown.

View File

@@ -71,7 +71,9 @@
BLOCK_CLAY = 58, BLOCK_CLAY = 58,
BLOCK_SEAGRASS = 59, BLOCK_SEAGRASS = 59,
BLOCK_IRON_BARS_NS = 60, BLOCK_IRON_BARS_NS = 60,
BLOCK_IRON_BARS_EW = 61 BLOCK_IRON_BARS_EW = 61,
BLOCK_SPRUCE_STAIRS_N = 62,
BLOCK_SPRUCE_STAIRS_S = 63
}; };
struct trail_segment; struct trail_segment;
@@ -112,5 +114,7 @@ void worldgen_prepass(worldgen_ctx *ctx, int min_x, int max_x, int min_z, int ma
void worldgen_free_trails(worldgen_ctx *ctx); void worldgen_free_trails(worldgen_ctx *ctx);
int worldgen_debug_biome(worldgen_ctx *ctx, int x, int z); int worldgen_debug_biome(worldgen_ctx *ctx, int x, int z);
double worldgen_debug_redwood_mask(worldgen_ctx *ctx, int x, int z); double worldgen_debug_redwood_mask(worldgen_ctx *ctx, int x, int z);
double worldgen_debug_community_strength(worldgen_ctx *ctx, int x, int z);
int worldgen_debug_bridge_site(worldgen_ctx *ctx, int x, int z, int dir_x, int dir_z);
#endif #endif

View File

@@ -166,7 +166,9 @@ static const block_state BLOCK_STATE_TABLE[] = {
[BLOCK_CLAY] = {"minecraft:clay", NULL, 0}, [BLOCK_CLAY] = {"minecraft:clay", NULL, 0},
[BLOCK_SEAGRASS] = {"minecraft:seagrass", NULL, 0}, [BLOCK_SEAGRASS] = {"minecraft:seagrass", NULL, 0},
[BLOCK_IRON_BARS_NS] = {"minecraft:iron_bars", PROPS_IRON_BARS_NS, 5}, [BLOCK_IRON_BARS_NS] = {"minecraft:iron_bars", PROPS_IRON_BARS_NS, 5},
[BLOCK_IRON_BARS_EW] = {"minecraft:iron_bars", PROPS_IRON_BARS_EW, 5} [BLOCK_IRON_BARS_EW] = {"minecraft:iron_bars", PROPS_IRON_BARS_EW, 5},
[BLOCK_SPRUCE_STAIRS_N] = {"minecraft:spruce_stairs", PROPS_STAIRS_N, 4},
[BLOCK_SPRUCE_STAIRS_S] = {"minecraft:spruce_stairs", PROPS_STAIRS_S, 4}
}; };
static const block_state *get_block_state(uint16_t id) { static const block_state *get_block_state(uint16_t id) {

View File

@@ -10,7 +10,8 @@
#define TRAIL_NODE_SPACING 1600.0 #define TRAIL_NODE_SPACING 1600.0
#define TRAIL_CELL_SIZE 16.0 #define TRAIL_CELL_SIZE 16.0
#define TRAIL_MARGIN 96.0 #define TRAIL_MARGIN 256.0
#define TRAIL_LANDFORM_RADIUS 64
#define TRAIL_WIDTH 5 #define TRAIL_WIDTH 5
typedef enum { typedef enum {
@@ -50,6 +51,7 @@ typedef struct trail_segment {
static int column_height(worldgen_ctx *ctx, int x, int z); static int column_height(worldgen_ctx *ctx, int x, int z);
static double worley_distance(int x, int z, double scale, uint32_t seed);
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// RNG (deterministic, cheap) // RNG (deterministic, cheap)
@@ -215,6 +217,46 @@ static double land_value(worldgen_ctx *ctx, int x, int z) {
return value; return value;
} }
/* Broad, seed-stable pockets where cabins form recognizable communities.
* Roads and cabin spawning share this field, so bridge permissions occur in
* the same woods where the extra local traffic is actually generated. */
static double woodland_community_strength(worldgen_ctx *ctx, int x, int z) {
double cell = worley_distance(x + 18000, z - 26000, 0.00105,
0xC011AB1Eu ^ (uint32_t)ctx->world_seed);
double cluster = smoothstep01((0.62 - cell) / 0.32);
double texture = simplex_noise2(&ctx->noise, (x - 31000) * 0.0018,
(z + 22000) * 0.0018) * 0.5 + 0.5;
return clamp01(cluster * 0.72 + texture * 0.28);
}
static int is_dry_sea_level_bank(worldgen_ctx *ctx, int x, int z) {
column_data bank = get_column_data(ctx, x, z);
if (bank.has_water && bank.height < bank.water_surface) return 0;
return bank.height >= ctx->sea_level - 1 && bank.height <= ctx->sea_level + 6;
}
static int small_community_water_crossing(worldgen_ctx *ctx, int x, int z, int dir_x, int dir_z) {
column_data water = get_column_data(ctx, x, z);
if (!water.has_water || water.water_surface != ctx->sea_level || water.height >= water.water_surface) return 0;
if (water.water_surface - water.height > 6) return 0;
if (woodland_community_strength(ctx, x, z) < 0.56) return 0;
if (dir_x == 0 && dir_z == 0) return 0;
double len = sqrt((double)dir_x * dir_x + (double)dir_z * dir_z);
double ux = (double)dir_x / len;
double uz = (double)dir_z / len;
int bank_forward = 0, bank_backward = 0;
for (int distance = 4; distance <= 48; distance += 4) {
int fx = x + (int)llround(ux * distance);
int fz = z + (int)llround(uz * distance);
int bx = x - (int)llround(ux * distance);
int bz = z - (int)llround(uz * distance);
if (!bank_forward && is_dry_sea_level_bank(ctx, fx, fz)) bank_forward = 1;
if (!bank_backward && is_dry_sea_level_bank(ctx, bx, bz)) bank_backward = 1;
if (bank_forward && bank_backward) return 1;
}
return 0;
}
static void append_trail_segment(worldgen_ctx *ctx, int ax, int az, int bx, int bz, int *points, int count) { 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. */ /* Drop near-parallel segments that hug existing roads to keep the network from bunching up. */
const double min_parallel_distance = 72.0; const double min_parallel_distance = 72.0;
@@ -597,6 +639,14 @@ double worldgen_debug_redwood_mask(worldgen_ctx *ctx, int x, int z) {
return redwood_biome_mask(ctx, x, z, height, local_relief(ctx, x, z, height)); return redwood_biome_mask(ctx, x, z, height, local_relief(ctx, x, z, height));
} }
double worldgen_debug_community_strength(worldgen_ctx *ctx, int x, int z) {
return woodland_community_strength(ctx, x, z);
}
int worldgen_debug_bridge_site(worldgen_ctx *ctx, int x, int z, int dir_x, int dir_z) {
return small_community_water_crossing(ctx, x, z, dir_x, dir_z);
}
static void block_list_init(block_list *list) { static void block_list_init(block_list *list) {
list->items = NULL; list->items = NULL;
list->count = 0; list->count = 0;
@@ -1067,6 +1117,20 @@ static double old_growth_grove_mask(worldgen_ctx *ctx, int x, int z) {
return clamp01(cluster * 0.7 + noise * 0.3); return clamp01(cluster * 0.7 + noise * 0.3);
} }
static double forest_clearing_mask(worldgen_ctx *ctx, int x, int z) {
double cell = worley_distance(x - 23000, z + 17000, 0.0032,
0xC1EA4123u ^ (uint32_t)ctx->world_seed);
double opening = smoothstep01((0.28 - cell) / 0.16);
return opening;
}
static double forest_maturity_mask(worldgen_ctx *ctx, int x, int z, double cover) {
double age = simplex_noise2(&ctx->noise, (x + 44000) * 0.0011, (z - 36000) * 0.0011) * 0.5 + 0.5;
double disturbance = simplex_noise2(&ctx->noise, (x - 15000) * 0.0045, (z + 26000) * 0.0045) * 0.5 + 0.5;
double interior = smoothstep01((cover - 0.48) / 0.32);
return clamp01(age * 0.5 + interior * 0.38 + disturbance * 0.12);
}
static int can_place_tree(worldgen_ctx *ctx, int x, int y, int z, int trunk_height, int radius, int crown_extra) { static int can_place_tree(worldgen_ctx *ctx, int x, int y, int z, int trunk_height, int radius, int crown_extra) {
int ground_block = sample_block(ctx, x, y - 1, z); int ground_block = sample_block(ctx, x, y - 1, z);
if (ground_block != BLOCK_GRASS && ground_block != BLOCK_SNOW && ground_block != BLOCK_DIRT) return 0; if (ground_block != BLOCK_GRASS && ground_block != BLOCK_SNOW && ground_block != BLOCK_DIRT) return 0;
@@ -1857,6 +1921,39 @@ static const int TREE_POOL_OLD_GROWTH[] = {
TREE_SPRUCE_TIERS, TREE_PINE_CROWN TREE_SPRUCE_TIERS, TREE_PINE_CROWN
}; };
static double tree_species_stand(worldgen_ctx *ctx, int x, int z, double ox, double oz) {
double broad = simplex_noise2(&ctx->noise, (x + ox) * 0.0022, (z + oz) * 0.0022) * 0.5 + 0.5;
double detail = simplex_noise2(&ctx->noise, (x - oz) * 0.006, (z + ox) * 0.006) * 0.5 + 0.5;
return broad * 0.78 + detail * 0.22;
}
/* Nearby trees usually share a dominant species. A small companion-tree
* chance below preserves mixed forests without producing species confetti. */
static tree_species dominant_tree_species(worldgen_ctx *ctx, const column_data *data, int x, int z) {
double rain = rainfall_field(ctx, x, z);
int altitude = data->height - ctx->sea_level;
tree_species best = TREE_SPECIES_OAK;
double best_score = -1.0;
#define CONSIDER_SPECIES(species_id, score_value) do { \
double candidate_score = (score_value); \
if (candidate_score > best_score) { best_score = candidate_score; best = (species_id); } \
} while (0)
if (data->biome == BIOME_WEST_KY_COALFIELDS) {
CONSIDER_SPECIES(TREE_SPECIES_OAK, tree_species_stand(ctx, x, z, 11000, -7000) + 0.12);
CONSIDER_SPECIES(TREE_SPECIES_WALNUT, tree_species_stand(ctx, x, z, -19000, 13000) + rain * 0.08);
CONSIDER_SPECIES(TREE_SPECIES_MAPLE, tree_species_stand(ctx, x, z, 27000, 21000) + rain * 0.16);
CONSIDER_SPECIES(TREE_SPECIES_CYPRESS, tree_species_stand(ctx, x, z, -33000, -17000) + rain * 0.28 - 0.12);
} else {
CONSIDER_SPECIES(TREE_SPECIES_OAK, tree_species_stand(ctx, x, z, 11000, -7000) - altitude * 0.004);
CONSIDER_SPECIES(TREE_SPECIES_MAPLE, tree_species_stand(ctx, x, z, 27000, 21000) + rain * 0.12);
CONSIDER_SPECIES(TREE_SPECIES_BIRCH, tree_species_stand(ctx, x, z, -8000, 31000) + altitude * 0.006);
CONSIDER_SPECIES(TREE_SPECIES_PINE, tree_species_stand(ctx, x, z, 39000, -29000) + altitude * 0.008);
CONSIDER_SPECIES(TREE_SPECIES_SPRUCE, tree_species_stand(ctx, x, z, -41000, 37000) + altitude * 0.011 + rain * 0.08);
}
#undef CONSIDER_SPECIES
return best;
}
static const tree_archetype *choose_tree_archetype(worldgen_ctx *ctx, const column_data *data, int world_x, int world_z, 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; int altitude = data->height - ctx->sea_level;
const tree_archetype *fallback = &TREE_TYPES[TREE_OAK_ROUND]; const tree_archetype *fallback = &TREE_TYPES[TREE_OAK_ROUND];
@@ -1879,10 +1976,18 @@ static const tree_archetype *choose_tree_archetype(worldgen_ctx *ctx, const colu
return &TREE_TYPES[TREE_POOL_OLD_GROWTH[idx]]; return &TREE_TYPES[TREE_POOL_OLD_GROWTH[idx]];
} }
if (data->biome == BIOME_WEST_KY_COALFIELDS) { if (data->biome == BIOME_WEST_KY_COALFIELDS) {
if (rng_next_f64(rng) < 0.82) {
const tree_archetype *stand_tree = choose_species_variant(dominant_tree_species(ctx, data, world_x, world_z), rng);
if (stand_tree) return stand_tree;
}
size_t pool_size = sizeof(TREE_POOL_WEST) / sizeof(TREE_POOL_WEST[0]); size_t pool_size = sizeof(TREE_POOL_WEST) / sizeof(TREE_POOL_WEST[0]);
size_t idx = (size_t)rng_range_inclusive(rng, 0, (int)pool_size - 1); size_t idx = (size_t)rng_range_inclusive(rng, 0, (int)pool_size - 1);
return &TREE_TYPES[TREE_POOL_WEST[idx]]; return &TREE_TYPES[TREE_POOL_WEST[idx]];
} }
if (rng_next_f64(rng) < 0.82) {
const tree_archetype *stand_tree = choose_species_variant(dominant_tree_species(ctx, data, world_x, world_z), rng);
if (stand_tree) return stand_tree;
}
const int *pool = (altitude > 30) ? TREE_POOL_EAST_HIGH : TREE_POOL_EAST_LOW; const int *pool = (altitude > 30) ? TREE_POOL_EAST_HIGH : TREE_POOL_EAST_LOW;
size_t pool_size = (altitude > 30) ? sizeof(TREE_POOL_EAST_HIGH) / sizeof(TREE_POOL_EAST_HIGH[0]) size_t pool_size = (altitude > 30) ? sizeof(TREE_POOL_EAST_HIGH) / sizeof(TREE_POOL_EAST_HIGH[0])
: sizeof(TREE_POOL_EAST_LOW) / sizeof(TREE_POOL_EAST_LOW[0]); : sizeof(TREE_POOL_EAST_LOW) / sizeof(TREE_POOL_EAST_LOW[0]);
@@ -1891,9 +1996,15 @@ static const tree_archetype *choose_tree_archetype(worldgen_ctx *ctx, const colu
return choice ? choice : fallback; return choice ? choice : fallback;
} }
static void generate_tree(worldgen_ctx *ctx, const tree_archetype *arch, int x, int y, int z, rng_state *rng, block_list *out) { static void generate_tree(worldgen_ctx *ctx, const tree_archetype *arch, int x, int y, int z,
double maturity, rng_state *rng, block_list *out) {
if (!arch) return; if (!arch) return;
int height = rng_range_inclusive(rng, arch->min_height, arch->max_height); int height = rng_range_inclusive(rng, arch->min_height, arch->max_height);
/* Preserve the proportions expected by each canopy builder; maturity now
* adds subtle height variation without reducing young trees to bare poles. */
double height_scale = 0.86 + clamp01(maturity) * 0.14;
height = (int)llround((double)height * height_scale);
if (height < 4) height = 4;
if (!can_place_tree(ctx, x, y, z, height, arch->space_radius, arch->canopy_extra)) { if (!can_place_tree(ctx, x, y, z, height, arch->space_radius, arch->canopy_extra)) {
if (!can_place_tree(ctx, x, y, z, height, arch->space_radius - 1 >= 1 ? arch->space_radius - 1 : 1, arch->canopy_extra)) return; if (!can_place_tree(ctx, x, y, z, height, arch->space_radius - 1 >= 1 ? arch->space_radius - 1 : 1, arch->canopy_extra)) return;
} }
@@ -2114,6 +2225,8 @@ typedef struct {
int offset_z; int offset_z;
int half_w; int half_w;
int half_l; int half_l;
int roof_axis;
int stories;
} cabin_rect; } cabin_rect;
#define CABIN_MAX_RECTS 4 #define CABIN_MAX_RECTS 4
@@ -2139,18 +2252,36 @@ typedef struct {
int door_rect_index; int door_rect_index;
int has_ladder; int has_ladder;
int path_width; int path_width;
int porch_depth;
int has_chimney;
} cabin_blueprint; } cabin_blueprint;
static const cabin_rect CABIN_RECT_SMALL[] = { static const cabin_rect CABIN_RECT_SMALL[] = {
{0, 0, 2, 3} {0, 0, 2, 3, 0, 0}
}; };
static const cabin_rect CABIN_RECT_LONG[] = { static const cabin_rect CABIN_RECT_LONG[] = {
{0, 0, 3, 4} {0, 0, 3, 4, 0, 0}
}; };
static const cabin_rect CABIN_RECT_TWO_STORY[] = { static const cabin_rect CABIN_RECT_TWO_STORY[] = {
{0, 0, 3, 3} {0, 0, 3, 3, 1, 0}
};
static const cabin_rect CABIN_RECT_L_SHAPED[] = {
{-1, 0, 2, 3, 0, 1}, {2, 1, 2, 1, 1, 1}
};
static const cabin_rect CABIN_RECT_CROSS_GABLE[] = {
{0, 0, 2, 4, 0, 1}, {0, 0, 3, 2, 1, 1}
};
static const cabin_rect CABIN_RECT_TOWER_LODGE[] = {
{-1, 0, 2, 3, 0, 1}, {2, 1, 1, 1, 1, 2}
};
static const cabin_rect CABIN_RECT_RAMBLING[] = {
{0, 0, 2, 3, 0, 1}, {-2, -2, 1, 2, 1, 1}, {2, 2, 1, 2, 1, 1}
}; };
static const cabin_blueprint CABIN_BLUEPRINTS[] = { static const cabin_blueprint CABIN_BLUEPRINTS[] = {
@@ -2174,7 +2305,9 @@ static const cabin_blueprint CABIN_BLUEPRINTS[] = {
0.08, 0.08,
0, 0,
0, 0,
2 2,
1,
1
}, },
{ {
CABIN_RECT_LONG, CABIN_RECT_LONG,
@@ -2196,7 +2329,9 @@ static const cabin_blueprint CABIN_BLUEPRINTS[] = {
0.1, 0.1,
0, 0,
0, 0,
3 3,
2,
1
}, },
{ {
CABIN_RECT_TWO_STORY, CABIN_RECT_TWO_STORY,
@@ -2218,7 +2353,53 @@ static const cabin_blueprint CABIN_BLUEPRINTS[] = {
0.08, 0.08,
0, 0,
1, 1,
2 2,
1,
1
},
{
.rects = CABIN_RECT_L_SHAPED,
.rect_count = sizeof(CABIN_RECT_L_SHAPED) / sizeof(CABIN_RECT_L_SHAPED[0]),
.wall_height = 4, .stories = 1,
.floor_block = BLOCK_OAK_PLANKS, .roof_block = BLOCK_SPRUCE_PLANKS,
.log_x_block = BLOCK_SPRUCE_LOG_X, .log_z_block = BLOCK_SPRUCE_LOG_Z, .corner_log = BLOCK_OAK_LOG,
.stair_e_block = BLOCK_SPRUCE_STAIRS_E, .stair_w_block = BLOCK_SPRUCE_STAIRS_W,
.window_block = BLOCK_GLASS_PANE, .foundation_block = BLOCK_STONE,
.log_decay = 0.025, .plank_decay = 0.12, .window_chance = 0.62, .door_decay = 0.04,
.door_rect_index = 0, .has_ladder = 0, .path_width = 3, .porch_depth = 2, .has_chimney = 1
},
{
.rects = CABIN_RECT_CROSS_GABLE,
.rect_count = sizeof(CABIN_RECT_CROSS_GABLE) / sizeof(CABIN_RECT_CROSS_GABLE[0]),
.wall_height = 4, .stories = 1,
.floor_block = BLOCK_SPRUCE_PLANKS, .roof_block = BLOCK_OAK_PLANKS,
.log_x_block = BLOCK_OAK_LOG_X, .log_z_block = BLOCK_OAK_LOG_Z, .corner_log = BLOCK_OAK_LOG,
.stair_e_block = BLOCK_SPRUCE_STAIRS_E, .stair_w_block = BLOCK_SPRUCE_STAIRS_W,
.window_block = BLOCK_GLASS, .foundation_block = BLOCK_STONE_BRICKS,
.log_decay = 0.015, .plank_decay = 0.08, .window_chance = 0.7, .door_decay = 0.02,
.door_rect_index = 1, .has_ladder = 0, .path_width = 3, .porch_depth = 2, .has_chimney = 1
},
{
.rects = CABIN_RECT_TOWER_LODGE,
.rect_count = sizeof(CABIN_RECT_TOWER_LODGE) / sizeof(CABIN_RECT_TOWER_LODGE[0]),
.wall_height = 4, .stories = 1,
.floor_block = BLOCK_SPRUCE_PLANKS, .roof_block = BLOCK_SPRUCE_PLANKS,
.log_x_block = BLOCK_OAK_LOG_X, .log_z_block = BLOCK_OAK_LOG_Z, .corner_log = BLOCK_OAK_LOG,
.stair_e_block = BLOCK_SPRUCE_STAIRS_E, .stair_w_block = BLOCK_SPRUCE_STAIRS_W,
.window_block = BLOCK_GLASS_PANE, .foundation_block = BLOCK_STONE,
.log_decay = 0.02, .plank_decay = 0.1, .window_chance = 0.72, .door_decay = 0.03,
.door_rect_index = 0, .has_ladder = 1, .path_width = 2, .porch_depth = 1, .has_chimney = 1
},
{
.rects = CABIN_RECT_RAMBLING,
.rect_count = sizeof(CABIN_RECT_RAMBLING) / sizeof(CABIN_RECT_RAMBLING[0]),
.wall_height = 3, .stories = 1,
.floor_block = BLOCK_OAK_PLANKS, .roof_block = BLOCK_SPRUCE_PLANKS,
.log_x_block = BLOCK_SPRUCE_LOG_X, .log_z_block = BLOCK_SPRUCE_LOG_Z, .corner_log = BLOCK_OAK_LOG,
.stair_e_block = BLOCK_SPRUCE_STAIRS_E, .stair_w_block = BLOCK_SPRUCE_STAIRS_W,
.window_block = BLOCK_GLASS_PANE, .foundation_block = BLOCK_STONE,
.log_decay = 0.035, .plank_decay = 0.16, .window_chance = 0.58, .door_decay = 0.06,
.door_rect_index = 0, .has_ladder = 0, .path_width = 2, .porch_depth = 2, .has_chimney = 1
} }
}; };
@@ -2297,7 +2478,7 @@ static int cabin_site_height_range(const cabin_blueprint *bp, int chunk_origin_x
static void build_cabin_roof(const cabin_blueprint *bp, int chunk_x, int chunk_z, chunk_data *chunk, static void build_cabin_roof(const cabin_blueprint *bp, int chunk_x, int chunk_z, chunk_data *chunk,
int rect_center_x, int rect_center_z, int half_w, int half_l, int rect_center_x, int rect_center_z, int half_w, int half_l,
int roof_base_y, rng_state *rng) { int roof_axis, int roof_base_y, rng_state *rng) {
int chunk_origin_x = chunk_x * CHUNK_SIZE; int chunk_origin_x = chunk_x * CHUNK_SIZE;
int chunk_origin_z = chunk_z * CHUNK_SIZE; int chunk_origin_z = chunk_z * CHUNK_SIZE;
int overhang = 1; int overhang = 1;
@@ -2305,14 +2486,18 @@ static void build_cabin_roof(const cabin_blueprint *bp, int chunk_x, int chunk_z
int x1 = rect_center_x + half_w; int x1 = rect_center_x + half_w;
int z0 = rect_center_z - half_l; int z0 = rect_center_z - half_l;
int z1 = rect_center_z + half_l; int z1 = rect_center_z + half_l;
int layers = half_w + 3; int layers = (roof_axis ? half_l : half_w) + 3;
for (int layer = 0; layer < layers; ++layer) { for (int layer = 0; layer < layers; ++layer) {
int start_x = (x0 - overhang) + layer; int start_cross = (roof_axis ? z0 : x0) - overhang + layer;
int end_x = (x1 + overhang) - layer; int end_cross = (roof_axis ? z1 : x1) + overhang - layer;
if (start_x > end_x) break; if (start_cross > end_cross) break;
int y = roof_base_y + layer + 1; int y = roof_base_y + layer + 1;
for (int wz = z0 - overhang; wz <= z1 + overhang; ++wz) { int run0 = (roof_axis ? x0 : z0) - overhang;
for (int wx = start_x; wx <= end_x; ++wx) { int run1 = (roof_axis ? x1 : z1) + overhang;
for (int run = run0; run <= run1; ++run) {
for (int cross = start_cross; cross <= end_cross; ++cross) {
int wx = roof_axis ? run : cross;
int wz = roof_axis ? cross : run;
if (rng && bp->plank_decay > 0.0 && rng_next_f64(rng) < bp->plank_decay * 0.2) continue; if (rng && bp->plank_decay > 0.0 && rng_next_f64(rng) < bp->plank_decay * 0.2) continue;
int lx = wx - chunk_origin_x; int lx = wx - chunk_origin_x;
int lz = wz - chunk_origin_z; int lz = wz - chunk_origin_z;
@@ -2320,18 +2505,19 @@ static void build_cabin_roof(const cabin_blueprint *bp, int chunk_x, int chunk_z
set_block_with_height(chunk, lx, lz, y, bp->roof_block); set_block_with_height(chunk, lx, lz, y, bp->roof_block);
} }
} }
for (int wz = z0 - overhang; wz <= z1 + overhang; ++wz) { for (int run = run0; run <= run1; ++run) {
int east_x = end_x + 1; int wx0 = roof_axis ? run : start_cross - 1;
int west_x = start_x - 1; int wz0 = roof_axis ? start_cross - 1 : run;
int lx_e = east_x - chunk_origin_x; int wx1 = roof_axis ? run : end_cross + 1;
int lx_w = west_x - chunk_origin_x; int wz1 = roof_axis ? end_cross + 1 : run;
int lz = wz - chunk_origin_z; int lx0 = wx0 - chunk_origin_x, lz0 = wz0 - chunk_origin_z;
if (lx_e >= 0 && lx_e < CHUNK_SIZE && lz >= 0 && lz < CHUNK_SIZE) { int lx1 = wx1 - chunk_origin_x, lz1 = wz1 - chunk_origin_z;
set_block_with_height(chunk, lx_e, lz, y, bp->stair_w_block); uint16_t low_stair = roof_axis ? BLOCK_SPRUCE_STAIRS_S : bp->stair_e_block;
} uint16_t high_stair = roof_axis ? BLOCK_SPRUCE_STAIRS_N : bp->stair_w_block;
if (lx_w >= 0 && lx_w < CHUNK_SIZE && lz >= 0 && lz < CHUNK_SIZE) { if (lx0 >= 0 && lx0 < CHUNK_SIZE && lz0 >= 0 && lz0 < CHUNK_SIZE)
set_block_with_height(chunk, lx_w, lz, y, bp->stair_e_block); set_block_with_height(chunk, lx0, lz0, y, low_stair);
} if (lx1 >= 0 && lx1 < CHUNK_SIZE && lz1 >= 0 && lz1 < CHUNK_SIZE)
set_block_with_height(chunk, lx1, lz1, y, high_stair);
} }
} }
} }
@@ -2375,18 +2561,7 @@ static void build_cabin_rect(worldgen_ctx *ctx, const cabin_blueprint *bp, const
int x1 = rect_center_x + rect->half_w; int x1 = rect_center_x + rect->half_w;
int z0 = rect_center_z - rect->half_l; int z0 = rect_center_z - rect->half_l;
int z1 = rect_center_z + rect->half_l; int z1 = rect_center_z + rect->half_l;
int clear_top = base_floor_y + bp->wall_height * bp->stories + 6; int rect_stories = rect->stories > 0 ? rect->stories : bp->stories;
for (int wz = z0 - 2; wz <= z1 + 2; ++wz) {
for (int wx = x0 - 2; wx <= x1 + 2; ++wx) {
int lx = wx - chunk_origin_x;
int lz = wz - chunk_origin_z;
if (lx < 0 || lx >= CHUNK_SIZE || lz < 0 || lz >= CHUNK_SIZE) continue;
for (int y = base_floor_y; y <= clear_top && y < CHUNK_HEIGHT; ++y) {
set_block_with_height(chunk, lx, lz, y, BLOCK_AIR);
}
}
}
uint16_t foundation_block = bp->foundation_block ? bp->foundation_block : BLOCK_STONE; uint16_t foundation_block = bp->foundation_block ? bp->foundation_block : BLOCK_STONE;
for (int wz = z0; wz <= z1; ++wz) { for (int wz = z0; wz <= z1; ++wz) {
@@ -2414,7 +2589,7 @@ static void build_cabin_rect(worldgen_ctx *ctx, const cabin_blueprint *bp, const
int ladder_hole_x = INT_MIN; int ladder_hole_x = INT_MIN;
int ladder_hole_z = INT_MIN; int ladder_hole_z = INT_MIN;
for (int story = 0; story < bp->stories; ++story) { for (int story = 0; story < rect_stories; ++story) {
int story_floor = base_floor_y + story * bp->wall_height; int story_floor = base_floor_y + story * bp->wall_height;
if (story_floor >= CHUNK_HEIGHT - 4) break; if (story_floor >= CHUNK_HEIGHT - 4) break;
for (int wz = z0 + 1; wz <= z1 - 1; ++wz) { for (int wz = z0 + 1; wz <= z1 - 1; ++wz) {
@@ -2521,7 +2696,7 @@ static void build_cabin_rect(worldgen_ctx *ctx, const cabin_blueprint *bp, const
} }
} }
if (bp->has_ladder && story < bp->stories - 1) { if (bp->has_ladder && story < rect_stories - 1) {
int ladder_x = rect_center_x; int ladder_x = rect_center_x;
int ladder_z = rect_center_z; int ladder_z = rect_center_z;
uint16_t ladder_block = BLOCK_LADDER_S; uint16_t ladder_block = BLOCK_LADDER_S;
@@ -2549,8 +2724,9 @@ static void build_cabin_rect(worldgen_ctx *ctx, const cabin_blueprint *bp, const
} }
} }
int roof_base = base_floor_y + bp->wall_height * bp->stories; int roof_base = base_floor_y + bp->wall_height * rect_stories;
build_cabin_roof(bp, chunk_x, chunk_z, chunk, rect_center_x, rect_center_z, rect->half_w, rect->half_l, roof_base, rng); build_cabin_roof(bp, chunk_x, chunk_z, chunk, rect_center_x, rect_center_z,
rect->half_w, rect->half_l, rect->roof_axis, roof_base, rng);
int attic_y = roof_base; int attic_y = roof_base;
for (int wz = z0 + 1; wz <= z1 - 1; ++wz) { for (int wz = z0 + 1; wz <= z1 - 1; ++wz) {
@@ -2578,6 +2754,41 @@ static void build_cabin_rect(worldgen_ctx *ctx, const cabin_blueprint *bp, const
update_columns_for_rect(columns, chunk_origin_x, chunk_origin_z, x0, x1, z0, z1, roof_base + 2); update_columns_for_rect(columns, chunk_origin_x, chunk_origin_z, x0, x1, z0, z1, roof_base + 2);
} }
static void build_cabin_porch_and_chimney(const cabin_blueprint *bp, const cabin_rect *door_rect,
int chunk_x, int chunk_z, chunk_data *chunk, int rect_center_x, int rect_center_z,
int base_floor_y, int door_x, int door_z, int door_side) {
int origin_x = chunk_x * CHUNK_SIZE;
int origin_z = chunk_z * CHUNK_SIZE;
int out_x = door_side == 2 ? -1 : door_side == 3 ? 1 : 0;
int out_z = door_side == 0 ? -1 : door_side == 1 ? 1 : 0;
int side_x = out_z;
int side_z = out_x;
for (int depth = 1; depth <= bp->porch_depth; ++depth) {
for (int side = -1; side <= 1; ++side) {
int wx = door_x + out_x * depth + side_x * side;
int wz = door_z + out_z * depth + side_z * side;
int lx = wx - origin_x, lz = wz - origin_z;
if (lx < 0 || lx >= CHUNK_SIZE || lz < 0 || lz >= CHUNK_SIZE) continue;
set_block_with_height(chunk, lx, lz, base_floor_y, bp->floor_block);
set_block_with_height(chunk, lx, lz, base_floor_y + 4, bp->roof_block);
if (depth == bp->porch_depth && (side == -1 || side == 1)) {
for (int y = base_floor_y + 1; y <= base_floor_y + 3; ++y)
set_block_with_height(chunk, lx, lz, y, bp->corner_log);
}
}
}
if (!bp->has_chimney) return;
int chimney_x = rect_center_x + (door_side == 2 ? door_rect->half_w - 1 : -door_rect->half_w + 1);
int chimney_z = rect_center_z + (door_side == 0 ? door_rect->half_l - 1 : -door_rect->half_l + 1);
int lx = chimney_x - origin_x, lz = chimney_z - origin_z;
if (lx < 0 || lx >= CHUNK_SIZE || lz < 0 || lz >= CHUNK_SIZE) return;
int rect_stories = door_rect->stories > 0 ? door_rect->stories : bp->stories;
int roof_top = base_floor_y + bp->wall_height * rect_stories +
(door_rect->roof_axis ? door_rect->half_l : door_rect->half_w) + 4;
for (int y = base_floor_y + 1; y <= roof_top && y < CHUNK_HEIGHT; ++y)
set_block_with_height(chunk, lx, lz, y, BLOCK_STONE_BRICKS);
}
static int try_place_cabin(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_data *chunk, column_data columns[CHUNK_SIZE][CHUNK_SIZE], static int try_place_cabin(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_data *chunk, column_data columns[CHUNK_SIZE][CHUNK_SIZE],
unsigned char occupancy[CHUNK_SIZE][CHUNK_SIZE], unsigned char occupancy[CHUNK_SIZE][CHUNK_SIZE],
int center_x, int center_z, const cabin_blueprint *bp, rng_state *rng) { int center_x, int center_z, const cabin_blueprint *bp, rng_state *rng) {
@@ -2592,6 +2803,7 @@ static int try_place_cabin(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_da
int rect_z1[CABIN_MAX_RECTS]; int rect_z1[CABIN_MAX_RECTS];
int min_h = INT_MAX; int min_h = INT_MAX;
int max_h = INT_MIN; int max_h = INT_MIN;
int max_structure_height = 0;
for (size_t i = 0; i < bp->rect_count; ++i) { for (size_t i = 0; i < bp->rect_count; ++i) {
const cabin_rect *rect = &bp->rects[i]; const cabin_rect *rect = &bp->rects[i];
int rcx = center_x + rect->offset_x; int rcx = center_x + rect->offset_x;
@@ -2608,6 +2820,10 @@ static int try_place_cabin(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_da
rect_x1[i] = x1; rect_x1[i] = x1;
rect_z0[i] = z0; rect_z0[i] = z0;
rect_z1[i] = z1; rect_z1[i] = z1;
int rect_stories = rect->stories > 0 ? rect->stories : bp->stories;
int roof_rise = (rect->roof_axis ? rect->half_l : rect->half_w) + 4;
int structure_height = bp->wall_height * rect_stories + roof_rise;
if (structure_height > max_structure_height) max_structure_height = structure_height;
if (rect_overlaps_mask(occupancy, chunk_origin_x, chunk_origin_z, x0, x1, z0, z1, 3)) { if (rect_overlaps_mask(occupancy, chunk_origin_x, chunk_origin_z, x0, x1, z0, z1, 3)) {
return 0; return 0;
} }
@@ -2636,7 +2852,7 @@ static int try_place_cabin(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_da
if (!cabin_site_height_range(bp, chunk_origin_x, chunk_origin_z, columns, center_x, center_z, 2, &pad_min_h, &pad_max_h)) return 0; if (!cabin_site_height_range(bp, chunk_origin_x, chunk_origin_z, columns, center_x, center_z, 2, &pad_min_h, &pad_max_h)) return 0;
if (pad_max_h - pad_min_h > 2) return 0; if (pad_max_h - pad_min_h > 2) return 0;
int base_floor_y = min_h + 1; int base_floor_y = min_h + 1;
if (base_floor_y + bp->wall_height * bp->stories + 6 >= CHUNK_HEIGHT) return 0; if (base_floor_y + max_structure_height >= CHUNK_HEIGHT) return 0;
int door_rect_index = clamp_int(bp->door_rect_index, 0, (int)bp->rect_count - 1); int door_rect_index = clamp_int(bp->door_rect_index, 0, (int)bp->rect_count - 1);
int door_side = rng_range_inclusive(rng, 0, 3); int door_side = rng_range_inclusive(rng, 0, 3);
@@ -2670,6 +2886,19 @@ static int try_place_cabin(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_da
door_z = clamp_int(door_z + door_offset, min_door_z, max_door_z); door_z = clamp_int(door_z + door_offset, min_door_z, max_door_z);
} }
/* Clear the complete compound footprint before building any wing. Clearing
* per rectangle would erase walls and roofs where wings overlap. */
for (size_t i = 0; i < bp->rect_count; ++i) {
for (int wz = rect_z0[i] - 2; wz <= rect_z1[i] + 2; ++wz) {
for (int wx = rect_x0[i] - 2; wx <= rect_x1[i] + 2; ++wx) {
int lx = wx - chunk_origin_x, lz = wz - chunk_origin_z;
if (lx < 0 || lx >= CHUNK_SIZE || lz < 0 || lz >= CHUNK_SIZE) continue;
for (int y = base_floor_y; y <= base_floor_y + max_structure_height && y < CHUNK_HEIGHT; ++y)
set_block_with_height(chunk, lx, lz, y, BLOCK_AIR);
}
}
}
for (size_t i = 0; i < bp->rect_count; ++i) { for (size_t i = 0; i < bp->rect_count; ++i) {
int has_door = (int)i == door_rect_index; int has_door = (int)i == door_rect_index;
build_cabin_rect(ctx, bp, &bp->rects[i], chunk_x, chunk_z, chunk, columns, build_cabin_rect(ctx, bp, &bp->rects[i], chunk_x, chunk_z, chunk, columns,
@@ -2677,6 +2906,9 @@ static int try_place_cabin(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_da
door_x, door_z, door_side, has_door, rng); door_x, door_z, door_side, has_door, rng);
mark_rect_occupancy(occupancy, chunk_origin_x, chunk_origin_z, rect_x0[i], rect_x1[i], rect_z0[i], rect_z1[i], 3); mark_rect_occupancy(occupancy, chunk_origin_x, chunk_origin_z, rect_x0[i], rect_x1[i], rect_z0[i], rect_z1[i], 3);
} }
build_cabin_porch_and_chimney(bp, &bp->rects[door_rect_index], chunk_x, chunk_z, chunk,
rect_center_x[door_rect_index], rect_center_z[door_rect_index],
base_floor_y, door_x, door_z, door_side);
connect_cabin_to_trail(ctx, chunk_x, chunk_z, columns, chunk, door_x, door_z, door_side, bp->path_width); connect_cabin_to_trail(ctx, chunk_x, chunk_z, columns, chunk, door_x, door_z, door_side, bp->path_width);
@@ -2688,7 +2920,10 @@ static void generate_chunk_cabins(worldgen_ctx *ctx, int chunk_x, int chunk_z, c
rng_state rng; rng_state rng;
rng_seed(&rng, seed ^ 0xCAB1A5u); 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 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; int chunk_center_x = chunk_x * CHUNK_SIZE + CHUNK_SIZE / 2;
int chunk_center_z = chunk_z * CHUNK_SIZE + CHUNK_SIZE / 2;
double community = woodland_community_strength(ctx, chunk_center_x, chunk_center_z);
double spawn_chance = 0.008 + noise * 0.015 + smoothstep01((community - 0.48) / 0.34) * 0.085;
if (ctx->enable_trails && !chunk_contains_trail(ctx, chunk_x, chunk_z)) { 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 */ return; /* only spawn cabins where a road crosses the chunk, so the spur can hook in */
} }
@@ -2725,6 +2960,7 @@ static void generate_chunk_cabins(worldgen_ctx *ctx, int chunk_x, int chunk_z, c
int world_cx = chunk_x * CHUNK_SIZE + local_cx; int world_cx = chunk_x * CHUNK_SIZE + local_cx;
int world_cz = chunk_z * CHUNK_SIZE + local_cz; int world_cz = chunk_z * CHUNK_SIZE + local_cz;
double score = land_value(ctx, world_cx, world_cz); double score = land_value(ctx, world_cx, world_cz);
score += woodland_community_strength(ctx, world_cx, world_cz) * 0.35;
int flat_min_h = INT_MAX; int flat_min_h = INT_MAX;
int flat_max_h = INT_MIN; int flat_max_h = INT_MIN;
if (!cabin_site_height_range(bp, chunk_x * CHUNK_SIZE, chunk_z * CHUNK_SIZE, columns, if (!cabin_site_height_range(bp, chunk_x * CHUNK_SIZE, chunk_z * CHUNK_SIZE, columns,
@@ -2738,10 +2974,10 @@ static void generate_chunk_cabins(worldgen_ctx *ctx, int chunk_x, int chunk_z, c
double dist2 = nearest_trail_distance2(ctx, world_cx, world_cz, 1.0e12); double dist2 = nearest_trail_distance2(ctx, world_cx, world_cz, 1.0e12);
double bonus = 0.0; double bonus = 0.0;
double dist = sqrt(dist2); double dist = sqrt(dist2);
if (dist < 640.0) { /* Favor a wooded setback instead of putting the building footprint
double t = clamp01(1.0 - dist / 640.0); * directly on the centerline. The spur still handles the connection. */
bonus += 0.45 * t; if (dist < 8.0) bonus -= (8.0 - dist) * 0.9;
} else if (dist < 96.0) bonus += 0.45 * clamp01(1.0 - fabs(dist - 20.0) / 76.0);
score += bonus; score += bonus;
} }
if (score > best_score) { if (score > best_score) {
@@ -2956,6 +3192,35 @@ static int column_has_manmade_surface(chunk_data *chunk, int chunk_x, int chunk_
return 0; return 0;
} }
static void tree_grid_candidate(worldgen_ctx *ctx, int gx, int gz, int grid, int *out_x, int *out_z) {
uint64_t seed = (uint64_t)ctx->world_seed + (uint64_t)gx * 341873128712ULL + (uint64_t)gz * 132897987541ULL;
rng_state rng;
rng_seed(&rng, seed);
*out_x = gx * grid + rng_range_inclusive(&rng, 0, grid - 1);
*out_z = gz * grid + rng_range_inclusive(&rng, 0, grid - 1);
}
/* Matérn-style priority thinning gives deterministic, irregular spacing and
* prevents adjacent grid cells from producing colliding crowns. */
static int tree_wins_spacing(worldgen_ctx *ctx, int gx, int gz, int grid, int x, int z, double spacing) {
int reach = (int)ceil(spacing / (double)grid) + 1;
uint32_t priority = hash_coords(gx, gz, (uint32_t)ctx->world_seed ^ 0x71EE5A9Du);
double spacing2 = spacing * spacing;
for (int ox = -reach; ox <= reach; ++ox) {
for (int oz = -reach; oz <= reach; ++oz) {
if (ox == 0 && oz == 0) continue;
int ngx = gx + ox, ngz = gz + oz;
int nx, nz;
tree_grid_candidate(ctx, ngx, ngz, grid, &nx, &nz);
double dx = (double)(nx - x), dz = (double)(nz - z);
if (dx * dx + dz * dz >= spacing2) continue;
uint32_t other = hash_coords(ngx, ngz, (uint32_t)ctx->world_seed ^ 0x71EE5A9Du);
if (other < priority || (other == priority && (ngx < gx || (ngx == gx && ngz < gz)))) return 0;
}
}
return 1;
}
static void generate_chunk_trees(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_data *chunk, block_list *out) { static void generate_chunk_trees(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_data *chunk, block_list *out) {
const int grid = 5; const int grid = 5;
const int margin = 4; const int margin = 4;
@@ -2981,8 +3246,8 @@ static void generate_chunk_trees(worldgen_ctx *ctx, int chunk_x, int chunk_z, ch
rng_state rng; rng_state rng;
rng_seed(&rng, seed); rng_seed(&rng, seed);
int candidate_x = gx * grid + rng_range_inclusive(&rng, 0, grid - 1); int candidate_x, candidate_z;
int candidate_z = gz * grid + rng_range_inclusive(&rng, 0, grid - 1); tree_grid_candidate(ctx, gx, gz, grid, &candidate_x, &candidate_z);
if (candidate_x < chunk_min_x || candidate_x > chunk_max_x || candidate_z < chunk_min_z || candidate_z > chunk_max_z) { if (candidate_x < chunk_min_x || candidate_x > chunk_max_x || candidate_z < chunk_min_z || candidate_z > chunk_max_z) {
continue; continue;
@@ -3024,21 +3289,32 @@ static void generate_chunk_trees(worldgen_ctx *ctx, int chunk_x, int chunk_z, ch
} }
if (altitude_factor <= 0.0) continue; if (altitude_factor <= 0.0) continue;
double base_cover = tree_density_mask(ctx, candidate_x, candidate_z);
double clearing = forest_clearing_mask(ctx, candidate_x, candidate_z);
double cover = clamp01(base_cover * (1.0 - clearing * 0.92));
double maturity = forest_maturity_mask(ctx, candidate_x, candidate_z, cover);
double spawn_prob; double spawn_prob;
if (is_redwood) { if (is_redwood) {
double rain = rainfall_field(ctx, candidate_x, candidate_z); 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; spawn_prob = clamp01(0.25 + stand_density * 0.65 + (rain - 0.5) * 0.2) * altitude_factor;
maturity = clamp01(0.55 + stand_density * 0.45);
} else { } else {
double density = tree_density_mask(ctx, candidate_x, candidate_z); double density = cover;
if (is_old_growth) { if (is_old_growth) {
density = clamp01(0.25 + grove_mask * 0.75); density = clamp01(0.25 + grove_mask * 0.75);
maturity = clamp01(0.72 + grove_mask * 0.28);
} }
spawn_prob = 0.7 * (0.55 + 0.45 * density) * altitude_factor; /* Openings retain a few isolated field trees, while forest interiors
* become substantially denser than their regenerating edges. */
spawn_prob = (0.08 + density * 0.9) * altitude_factor;
if (is_old_growth) { if (is_old_growth) {
spawn_prob = clamp01(0.25 + grove_mask * 0.7) * altitude_factor; spawn_prob = clamp01(0.25 + grove_mask * 0.7) * altitude_factor;
} }
} }
if (rng_next_f64(&rng) > spawn_prob) continue; if (rng_next_f64(&rng) > spawn_prob) continue;
double spacing = is_redwood ? (8.0 + (1.0 - stand_density) * 3.0)
: (3.5 + (1.0 - cover) * 2.5 + maturity * 0.75);
if (!tree_wins_spacing(ctx, gx, gz, grid, candidate_x, candidate_z, spacing)) continue;
block_list tmp; block_list tmp;
block_list_init(&tmp); block_list_init(&tmp);
@@ -3052,7 +3328,7 @@ static void generate_chunk_trees(worldgen_ctx *ctx, int chunk_x, int chunk_z, ch
block_list_free(&tmp); block_list_free(&tmp);
continue; continue;
} }
generate_tree(ctx, arch, candidate_x, base_y, candidate_z, &rng, &tmp); generate_tree(ctx, arch, candidate_x, base_y, candidate_z, maturity, &rng, &tmp);
if (tmp.count == 0) { if (tmp.count == 0) {
block_list_free(&tmp); block_list_free(&tmp);
continue; continue;
@@ -3109,6 +3385,34 @@ static uint32_t trail_segment_hash(int ax, int az, int bx, int bz, uint32_t seed
return hash_coords(mix_x, mix_z, seed); return hash_coords(mix_x, mix_z, seed);
} }
typedef struct {
double valley_depth;
double ridge_exposure;
double gradient_x;
double gradient_z;
} trail_landform;
/* Read the broad landform rather than reacting to individual blocks. This
* distinguishes sheltered valley floors from ridges and steep side slopes. */
static trail_landform sample_trail_landform(worldgen_ctx *ctx, int x, int z, double height) {
const int r = TRAIL_LANDFORM_RADIUS;
double east = (double)column_height(ctx, x + r, z);
double west = (double)column_height(ctx, x - r, z);
double south = (double)column_height(ctx, x, z + r);
double north = (double)column_height(ctx, x, z - r);
double se = (double)column_height(ctx, x + r, z + r);
double nw = (double)column_height(ctx, x - r, z - r);
double ne = (double)column_height(ctx, x + r, z - r);
double sw = (double)column_height(ctx, x - r, z + r);
double mean = (east + west + south + north + se + nw + ne + sw) / 8.0;
trail_landform land;
land.valley_depth = mean - height;
land.ridge_exposure = fmax(0.0, height - mean);
land.gradient_x = (east - west) / (2.0 * r);
land.gradient_z = (south - north) / (2.0 * r);
return land;
}
static int should_connect_trail_nodes(worldgen_ctx *ctx, int node_x0, int node_z0, int node_x1, int node_z1) { static int should_connect_trail_nodes(worldgen_ctx *ctx, int node_x0, int node_z0, int node_x1, int node_z1) {
if (node_x0 == node_x1 && node_z0 == node_z1) return 0; if (node_x0 == node_x1 && node_z0 == node_z1) return 0;
uint32_t seg_hash = trail_segment_hash(node_x0, node_z0, node_x1, node_z1, (uint32_t)ctx->world_seed ^ 0xB37D4A91u); uint32_t seg_hash = trail_segment_hash(node_x0, node_z0, node_x1, node_z1, (uint32_t)ctx->world_seed ^ 0xB37D4A91u);
@@ -3120,9 +3424,10 @@ static int should_connect_trail_nodes(worldgen_ctx *ctx, int node_x0, int node_z
int mid_ix = (int)llround(mid_x); int mid_ix = (int)llround(mid_x);
int mid_iz = (int)llround(mid_z); int mid_iz = (int)llround(mid_z);
column_data mid_column = get_column_data(ctx, mid_ix, mid_iz); column_data mid_column = get_column_data(ctx, mid_ix, mid_iz);
double ridge_bias = clamp01((double)(mid_column.height - ctx->sea_level) / 40.0); trail_landform midpoint_land = sample_trail_landform(ctx, mid_ix, mid_iz, (double)mid_column.height);
double valley_bias = clamp01((midpoint_land.valley_depth + 4.0) / 16.0);
if (mid_column.has_water && mid_column.height <= mid_column.water_surface) { if (mid_column.has_water && mid_column.height <= mid_column.water_surface) {
ridge_bias -= 0.35; valley_bias -= 0.35;
} }
double dir_x = bx - ax; double dir_x = bx - ax;
double dir_z = bz - az; double dir_z = bz - az;
@@ -3167,7 +3472,7 @@ static int should_connect_trail_nodes(worldgen_ctx *ctx, int node_x0, int node_z
base += 0.07; base += 0.07;
} }
base += (alignment - 0.5) * 0.2; base += (alignment - 0.5) * 0.2;
base += (ridge_bias - 0.5) * 0.15; base += (valley_bias - 0.5) * 0.15;
double texture = simplex_noise2(&ctx->noise, mid_x * 0.0012, mid_z * 0.0012) * 0.5 + 0.5; double texture = simplex_noise2(&ctx->noise, mid_x * 0.0012, mid_z * 0.0012) * 0.5 + 0.5;
base += (texture - 0.5) * 0.15; base += (texture - 0.5) * 0.15;
if (base < 0.02) base = 0.02; if (base < 0.02) base = 0.02;
@@ -3279,11 +3584,15 @@ static double nearest_trail_distance2(worldgen_ctx *ctx, int x, int z, double fa
for (size_t i = 0; i < ctx->trail_segment_count; ++i) { for (size_t i = 0; i < ctx->trail_segment_count; ++i) {
trail_segment *seg = &ctx->trail_segments[i]; trail_segment *seg = &ctx->trail_segments[i];
if (!seg || seg->count < 1 || !seg->points) continue; if (!seg || seg->count < 1 || !seg->points) continue;
for (int p = 0; p < seg->count; ++p) { for (int p = 0; p < seg->count - 1; ++p) {
int tx = seg->points[p * 2]; double d2 = point_segment_distance2((double)x, (double)z,
int tz = seg->points[p * 2 + 1]; (double)seg->points[p * 2], (double)seg->points[p * 2 + 1],
double dx = (double)(tx - x); (double)seg->points[(p + 1) * 2], (double)seg->points[(p + 1) * 2 + 1]);
double dz = (double)(tz - z); if (d2 < best) best = d2;
}
if (seg->count == 1) {
double dx = (double)(seg->points[0] - x);
double dz = (double)(seg->points[1] - z);
double d2 = dx * dx + dz * dz; double d2 = dx * dx + dz * dz;
if (d2 < best) best = d2; if (d2 < best) best = d2;
} }
@@ -3321,6 +3630,12 @@ static int build_trail_path(worldgen_ctx *ctx, double ax, double az, double bx,
if (grid_w < 2) grid_w = 2; if (grid_w < 2) grid_w = 2;
if (grid_h < 2) grid_h = 2; if (grid_h < 2) grid_h = 2;
int total = grid_w * grid_h; int total = grid_w * grid_h;
double route_dx = bx - ax;
double route_dz = bz - az;
double route_length = sqrt(route_dx * route_dx + route_dz * route_dz);
/* Long inter-settlement roads are engineered more conservatively than
* short cabin spurs, which may accept steeper and more direct approaches. */
double road_scale = clamp01((route_length - 240.0) / 900.0);
double *heights = (double *)malloc((size_t)total * sizeof(double)); double *heights = (double *)malloc((size_t)total * sizeof(double));
if (!heights) return 0; if (!heights) return 0;
for (int gz = 0; gz < grid_h; ++gz) { for (int gz = 0; gz < grid_h; ++gz) {
@@ -3381,14 +3696,23 @@ static int build_trail_path(worldgen_ctx *ctx, double ax, double az, double bx,
int ni = nz * grid_w + nx; int ni = nz * grid_w + nx;
if (used[ni]) continue; if (used[ni]) continue;
double next_height = heights[ni]; double next_height = heights[ni];
double slope = fabs(next_height - current_height);
if (slope > 3.0) continue;
int wx = (int)llround(min_x + nx * TRAIL_CELL_SIZE); int wx = (int)llround(min_x + nx * TRAIL_CELL_SIZE);
int wz = (int)llround(min_z + nz * TRAIL_CELL_SIZE); int wz = (int)llround(min_z + nz * TRAIL_CELL_SIZE);
column_data cd = get_column_data(ctx, wx, wz); column_data cd = get_column_data(ctx, wx, wz);
double water_penalty = 0.0;
if (cd.has_water && next_height <= cd.water_surface) { if (cd.has_water && next_height <= cd.water_surface) {
continue; /* trails should never be routed through water */ if (!small_community_water_crossing(ctx, wx, wz, neighbors[n][0], neighbors[n][1])) continue;
next_height = (double)cd.water_surface + 1.0;
water_penalty = 8.0; /* worthwhile only when it materially shortens a community road */
} }
int current_wx = (int)llround(min_x + cx * TRAIL_CELL_SIZE);
int current_wz = (int)llround(min_z + cz * TRAIL_CELL_SIZE);
column_data current_cd = get_column_data(ctx, current_wx, current_wz);
if (current_cd.has_water && current_cd.height < current_cd.water_surface)
current_height = (double)current_cd.water_surface + 1.0;
double slope = fabs(next_height - current_height);
double max_step = 4.0 - road_scale;
if (slope > max_step) continue;
double tree_penalty = 0.0; double tree_penalty = 0.0;
if (cd.biome == BIOME_REDWOOD_FOREST) { if (cd.biome == BIOME_REDWOOD_FOREST) {
double stand = redwood_tree_presence(ctx, wx, wz); double stand = redwood_tree_presence(ctx, wx, wz);
@@ -3396,7 +3720,20 @@ static int build_trail_path(worldgen_ctx *ctx, double ax, double az, double bx,
tree_penalty = stand; tree_penalty = stand;
} }
double base_cost = (neighbors[n][0] == 0 || neighbors[n][1] == 0) ? 1.0 : 1.41421356237; double base_cost = (neighbors[n][0] == 0 || neighbors[n][1] == 0) ? 1.0 : 1.41421356237;
double cost = base_cost * (1.0 + slope * 6.0 + tree_penalty * 4.5) + tree_penalty * 6.5; trail_landform land = sample_trail_landform(ctx, wx, wz, next_height);
double valley_bonus = clamp01(land.valley_depth / 18.0) * (0.4 + road_scale * 0.2);
double ridge_penalty = clamp01(land.ridge_exposure / 22.0) * (1.4 + road_scale * 1.8);
double move_x = (double)neighbors[n][0] / base_cost;
double move_z = (double)neighbors[n][1] / base_cost;
double contour_climb = fabs(move_x * land.gradient_x + move_z * land.gradient_z);
double cross_relief = fabs(-move_z * land.gradient_x + move_x * land.gradient_z) *
(2.0 * TRAIL_LANDFORM_RADIUS);
double contour_penalty = contour_climb * (5.0 + road_scale * 5.0);
double earthwork_penalty = clamp01((cross_relief - 10.0) / 28.0) * (0.8 + road_scale * 1.2);
double grade_penalty = slope * (5.5 + road_scale * 3.5);
double terrain_cost = 1.0 + grade_penalty + ridge_penalty + contour_penalty +
earthwork_penalty + tree_penalty * 4.5 + water_penalty;
double cost = base_cost * terrain_cost * (1.0 - valley_bonus) + tree_penalty * 6.5;
if (dist[current] + cost < dist[ni]) { if (dist[current] + cost < dist[ni]) {
dist[ni] = dist[current] + cost; dist[ni] = dist[current] + cost;
prev[ni] = current; prev[ni] = current;
@@ -3556,6 +3893,26 @@ static void place_trail_column(chunk_data *out, column_data columns[CHUNK_SIZE][
columns[local_x][local_z].height = target_height; columns[local_x][local_z].height = target_height;
} }
static void place_trail_bridge_column(chunk_data *out, column_data columns[CHUNK_SIZE][CHUNK_SIZE],
int chunk_x, int chunk_z, int world_x, int world_z, int deck_y, int railing) {
int lx = world_x - chunk_x * CHUNK_SIZE;
int lz = world_z - chunk_z * CHUNK_SIZE;
if (lx < 0 || lx >= CHUNK_SIZE || lz < 0 || lz >= CHUNK_SIZE) return;
if (deck_y < 2 || deck_y >= CHUNK_HEIGHT - 4) return;
for (int y = 1; y < CHUNK_HEIGHT; ++y)
if (is_cabin_structure_block(out->blocks[y][lx][lz])) return;
out->blocks[deck_y][lx][lz] = BLOCK_SPRUCE_PLANKS;
for (int y = deck_y + 1; y <= deck_y + 3; ++y) out->blocks[y][lx][lz] = BLOCK_AIR;
uint32_t support_hash = hash_coords(world_x, world_z, 0xB21D6E5u);
if ((support_hash % 5u) == 0u) {
int bed = columns[lx][lz].height;
for (int y = bed + 1; y < deck_y; ++y) out->blocks[y][lx][lz] = BLOCK_OAK_LOG;
}
if (railing && (support_hash % 3u) == 0u) out->blocks[deck_y + 1][lx][lz] = BLOCK_OAK_LOG;
columns[lx][lz].height = deck_y;
out->heightmap[lx][lz] = (uint16_t)(deck_y + (railing ? 1 : 0));
}
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_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) {
if (radius < 1) radius = 1; if (radius < 1) radius = 1;
(void)ctx; (void)ctx;
@@ -3565,6 +3922,8 @@ static void carve_trail_pad(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_d
if (dx * dx + dz * dz > radius_sq) continue; if (dx * dx + dz * dz > radius_sq) continue;
int wx = center_x + dx; int wx = center_x + dx;
int wz = center_z + dz; int wz = center_z + dz;
column_data data = get_column_data(ctx, wx, wz);
if (data.has_water && data.height < data.water_surface) continue;
place_trail_column(out, columns, chunk_x, chunk_z, wx, wz, target_height); place_trail_column(out, columns, chunk_x, chunk_z, wx, wz, target_height);
} }
} }
@@ -3596,7 +3955,8 @@ static void carve_trail_span(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_
int wx = (int)llround(px); int wx = (int)llround(px);
int wz = (int)llround(pz); int wz = (int)llround(pz);
column_data data = get_column_data(ctx, wx, wz); column_data data = get_column_data(ctx, wx, wz);
int target = data.height; int is_bridge = small_community_water_crossing(ctx, wx, wz, x1 - x0, z1 - z0);
int target = is_bridge ? data.water_surface + 1 : data.height;
if (last_height != INT32_MIN) { if (last_height != INT32_MIN) {
double blended = carry_weight * (double)last_height + (1.0 - carry_weight) * (double)target; double blended = carry_weight * (double)last_height + (1.0 - carry_weight) * (double)target;
target = (int)llround(blended); target = (int)llround(blended);
@@ -3614,6 +3974,15 @@ static void carve_trail_span(worldgen_ctx *ctx, int chunk_x, int chunk_z, chunk_
double oz = pz + nz * w; double oz = pz + nz * w;
int fx = (int)llround(ox); int fx = (int)llround(ox);
int fz = (int)llround(oz); int fz = (int)llround(oz);
column_data cross_data = get_column_data(ctx, fx, fz);
if (cross_data.has_water && cross_data.height < cross_data.water_surface) {
if (small_community_water_crossing(ctx, fx, fz, x1 - x0, z1 - z0)) {
int edge = fabs(w) >= half_width - 0.55;
place_trail_bridge_column(out, columns, chunk_x, chunk_z, fx, fz,
cross_data.water_surface + 1, edge);
}
continue;
}
place_trail_column(out, columns, chunk_x, chunk_z, fx, fz, target); place_trail_column(out, columns, chunk_x, chunk_z, fx, fz, target);
} }
} }

View File

@@ -10,6 +10,7 @@ static int is_cabin_block(uint16_t block) {
block == BLOCK_OAK_LOG_X || block == BLOCK_OAK_LOG_Z || block == BLOCK_OAK_LOG_X || block == BLOCK_OAK_LOG_Z ||
block == BLOCK_SPRUCE_LOG_X || block == BLOCK_SPRUCE_LOG_Z || block == BLOCK_SPRUCE_LOG_X || block == BLOCK_SPRUCE_LOG_Z ||
block == BLOCK_GLASS_PANE || block == BLOCK_GLASS || block == BLOCK_SPRUCE_STAIRS_E || block == BLOCK_SPRUCE_STAIRS_W || block == BLOCK_GLASS_PANE || block == BLOCK_GLASS || block == BLOCK_SPRUCE_STAIRS_E || block == BLOCK_SPRUCE_STAIRS_W ||
block == BLOCK_SPRUCE_STAIRS_N || block == BLOCK_SPRUCE_STAIRS_S ||
block == BLOCK_LADDER_N || block == BLOCK_LADDER_S || block == BLOCK_LADDER_E || block == BLOCK_LADDER_W || block == BLOCK_LADDER_N || block == BLOCK_LADDER_S || block == BLOCK_LADDER_E || block == BLOCK_LADDER_W ||
block == BLOCK_SPRUCE_DOOR_N_LOWER || block == BLOCK_SPRUCE_DOOR_N_UPPER || block == BLOCK_SPRUCE_DOOR_N_LOWER || block == BLOCK_SPRUCE_DOOR_N_UPPER ||
block == BLOCK_SPRUCE_DOOR_S_LOWER || block == BLOCK_SPRUCE_DOOR_S_UPPER || block == BLOCK_SPRUCE_DOOR_S_LOWER || block == BLOCK_SPRUCE_DOOR_S_UPPER ||
@@ -78,6 +79,8 @@ int main(int argc, char **argv) {
long cabin_columns = 0; long cabin_columns = 0;
long gravel_columns = 0; long gravel_columns = 0;
long cabin_gravel_columns = 0; long cabin_gravel_columns = 0;
long bridge_deck_columns = 0;
long eligible_bridge_columns = 0;
long tall_log_columns = 0; long tall_log_columns = 0;
long max_oak_log_run = 0; long max_oak_log_run = 0;
long biome_columns[4] = {0, 0, 0, 0}; long biome_columns[4] = {0, 0, 0, 0};
@@ -96,8 +99,11 @@ int main(int argc, char **argv) {
if (biome >= 0 && biome < 4) ++biome_columns[biome]; if (biome >= 0 && biome < 4) ++biome_columns[biome];
double redwood_mask = worldgen_debug_redwood_mask(&ctx, wx, wz); double redwood_mask = worldgen_debug_redwood_mask(&ctx, wx, wz);
if (redwood_mask > max_redwood_mask) max_redwood_mask = redwood_mask; if (redwood_mask > max_redwood_mask) max_redwood_mask = redwood_mask;
if (worldgen_debug_bridge_site(&ctx, wx, wz, 1, 0) ||
worldgen_debug_bridge_site(&ctx, wx, wz, 0, 1)) ++eligible_bridge_columns;
int has_cabin = 0; int has_cabin = 0;
int has_gravel = 0; int has_gravel = 0;
int has_bridge_deck = 0;
int current_run = 0; int current_run = 0;
int best_run = 0; int best_run = 0;
for (int y = 1; y < CHUNK_HEIGHT; ++y) { for (int y = 1; y < CHUNK_HEIGHT; ++y) {
@@ -109,6 +115,8 @@ int main(int argc, char **argv) {
if (block == BLOCK_GRAVEL) { if (block == BLOCK_GRAVEL) {
has_gravel = 1; has_gravel = 1;
} }
if (block == BLOCK_SPRUCE_PLANKS && y > 1 && chunk.blocks[y - 1][lx][lz] == BLOCK_WATER)
has_bridge_deck = 1;
if (block == BLOCK_OAK_LOG) { if (block == BLOCK_OAK_LOG) {
++current_run; ++current_run;
if (current_run > best_run) best_run = current_run; if (current_run > best_run) best_run = current_run;
@@ -120,6 +128,7 @@ int main(int argc, char **argv) {
if (best_run >= 40) ++tall_log_columns; if (best_run >= 40) ++tall_log_columns;
if (has_cabin) ++cabin_columns; if (has_cabin) ++cabin_columns;
if (has_gravel) ++gravel_columns; if (has_gravel) ++gravel_columns;
if (has_bridge_deck) ++bridge_deck_columns;
if (has_cabin && has_gravel) { if (has_cabin && has_gravel) {
++cabin_gravel_columns; ++cabin_gravel_columns;
} }
@@ -133,6 +142,8 @@ int main(int argc, char **argv) {
printf("cabin_columns=%ld\n", cabin_columns); printf("cabin_columns=%ld\n", cabin_columns);
printf("gravel_columns=%ld\n", gravel_columns); printf("gravel_columns=%ld\n", gravel_columns);
printf("cabin_gravel_columns=%ld\n", cabin_gravel_columns); printf("cabin_gravel_columns=%ld\n", cabin_gravel_columns);
printf("bridge_deck_columns=%ld\n", bridge_deck_columns);
printf("eligible_bridge_columns=%ld\n", eligible_bridge_columns);
printf("tall_oak_log_columns=%ld\n", tall_log_columns); printf("tall_oak_log_columns=%ld\n", tall_log_columns);
printf("max_oak_log_run=%ld\n", max_oak_log_run); printf("max_oak_log_run=%ld\n", max_oak_log_run);
printf("biome_west_ky=%ld\n", biome_columns[0]); printf("biome_west_ky=%ld\n", biome_columns[0]);