Add world generator sources and binary
This commit is contained in:
709
mc.py
Normal file
709
mc.py
Normal file
@@ -0,0 +1,709 @@
|
||||
import math
|
||||
import random
|
||||
from opensimplex import OpenSimplex # OpenSimplex noise for smooth, terrain-like variation
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BASIC WORLD CONSTANTS
|
||||
# ---------------------------------------------------------------------------
|
||||
# Approximate water surface level (everything below may become ocean/lake)
|
||||
sea_level = 70
|
||||
# World seed: change this to get a completely different world layout,
|
||||
# but the same seed always produces the same terrain.
|
||||
world_seed = 123456
|
||||
# Create a global OpenSimplex noise generator.
|
||||
# This object gives us deterministic noise values from coordinates + seed.
|
||||
noise = OpenSimplex(world_seed)
|
||||
# Cache column heights so repeated lookups (like water detection) stay cheap.
|
||||
_height_cache = {}
|
||||
# Cache derived column data (height + water surface) so generateBlock doesn't
|
||||
# recompute expensive basin detection for every y in the same column.
|
||||
_column_data_cache = {}
|
||||
# Base biome temperatures (you already had this)
|
||||
biome_base_temps = {
|
||||
"desert": 110,
|
||||
"plains": 80,
|
||||
"plains_river": 80,
|
||||
"rolling_hills": 75,
|
||||
}
|
||||
# Map block "names" to numeric IDs.
|
||||
# These IDs are what your engine or save format actually uses internally.
|
||||
# You can change these numbers to match your own block registry.
|
||||
block_ids = {
|
||||
"bedrock": 0,
|
||||
"stone": 1,
|
||||
"dirt": 2,
|
||||
"grass": 3,
|
||||
"water": 4,
|
||||
"air": 5,
|
||||
"oak_log": 6,
|
||||
"oak_leaves": 7,
|
||||
"birch_log": 8,
|
||||
"birch_leaves": 9,
|
||||
"coal": 10,
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ORE GENERATION
|
||||
# ---------------------------------------------------------------------------
|
||||
class OreGeneration:
|
||||
"""Namespace for all ore generation systems"""
|
||||
|
||||
class CoalSeams:
|
||||
"""Coal seam generation - each seam type is independent and combinable"""
|
||||
|
||||
# --- Atomic Helper Functions ---
|
||||
|
||||
@staticmethod
|
||||
def _check_point_in_seam(y, seam_center, half_thickness):
|
||||
"""Atomic: Check if y coordinate is within seam vertical bounds"""
|
||||
return abs(y - seam_center) <= half_thickness
|
||||
|
||||
@staticmethod
|
||||
def _calculate_seam_offset(x, y, z, scale=0.02, amplitude=2.0):
|
||||
"""Atomic: Calculate vertical offset for seam undulation using 3D noise"""
|
||||
return noise.noise3(x * scale, y * scale, z * scale) * amplitude
|
||||
|
||||
@staticmethod
|
||||
def _calculate_thickness_variation(x, z, scale=0.03, amplitude=1.0):
|
||||
"""Atomic: Calculate thickness variation across horizontal plane using 2D noise"""
|
||||
return noise.noise2(x * scale, z * scale) * amplitude
|
||||
|
||||
@staticmethod
|
||||
def _check_seam_continuity(x, y, z, threshold=-0.7):
|
||||
"""Atomic: Determine if seam is solid or pinched out at this point"""
|
||||
continuity_noise = noise.noise3(x * 0.05, y * 0.05, z * 0.05)
|
||||
return continuity_noise > threshold
|
||||
|
||||
@staticmethod
|
||||
def _check_seam(x, y, z, center, thickness, undulation_scale=0.02, undulation_amp=2.0,
|
||||
thickness_scale=0.03, thickness_amp=1.0, continuity_threshold=-0.7):
|
||||
"""
|
||||
Generic seam checker - all seam types use this.
|
||||
|
||||
Args:
|
||||
center: Base y-level for seam center
|
||||
thickness: Base thickness in blocks
|
||||
undulation_scale/amp: Control vertical wave pattern
|
||||
thickness_scale/amp: Control horizontal thickness variation
|
||||
continuity_threshold: Controls gap frequency (higher = fewer gaps)
|
||||
"""
|
||||
seam_offset = OreGeneration.CoalSeams._calculate_seam_offset(x, y, z, undulation_scale, undulation_amp)
|
||||
adjusted_center = center + seam_offset
|
||||
|
||||
thickness_variation = OreGeneration.CoalSeams._calculate_thickness_variation(x, z, thickness_scale, thickness_amp)
|
||||
adjusted_thickness = thickness + thickness_variation
|
||||
half_thickness = adjusted_thickness / 2.0
|
||||
|
||||
if OreGeneration.CoalSeams._check_point_in_seam(y, adjusted_center, half_thickness):
|
||||
if OreGeneration.CoalSeams._check_seam_continuity(x, y, z, continuity_threshold):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _check_flat_ceiling_seam(x, y, z, column_height, ceiling_depth, thickness,
|
||||
thickness_scale=0.03, thickness_amp=1.0, continuity_threshold=-0.7):
|
||||
"""
|
||||
Check for seam with flat ceiling and flat floor (strip mining incentive).
|
||||
|
||||
Geometry:
|
||||
- Ceiling (top): flat at column_height - ceiling_depth
|
||||
- Floor (bottom): flat at column_height - ceiling_depth - thickness
|
||||
- Ore exposure: wavy top surface via continuity check
|
||||
|
||||
Args:
|
||||
column_height: Terrain surface height at this x,z
|
||||
ceiling_depth: Fixed depth below surface for seam ceiling
|
||||
thickness: Fixed thickness in blocks (no variation)
|
||||
thickness_scale/amp: Unused (kept for compatibility)
|
||||
continuity_threshold: Controls gap frequency (higher = fewer gaps)
|
||||
"""
|
||||
seam_ceiling = column_height - ceiling_depth
|
||||
seam_floor = seam_ceiling - thickness
|
||||
|
||||
if seam_floor <= y <= seam_ceiling:
|
||||
if OreGeneration.CoalSeams._check_seam_continuity(x, y, z, continuity_threshold):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _check_terrain_relative_seam(x, y, z, column_height, depth_below_surface, thickness,
|
||||
thickness_scale=0.03, thickness_amp=1.0, continuity_threshold=-0.7):
|
||||
"""
|
||||
Check for seam at fixed depth below terrain surface (perfect for strip mining).
|
||||
|
||||
Args:
|
||||
column_height: The terrain surface height at this x,z
|
||||
depth_below_surface: How many blocks below surface the seam center is
|
||||
thickness: Base thickness in blocks
|
||||
thickness_scale/amp: Control horizontal thickness variation
|
||||
continuity_threshold: Controls gap frequency (higher = fewer gaps)
|
||||
"""
|
||||
# Seam follows terrain - always at same depth below surface
|
||||
seam_center = column_height - depth_below_surface
|
||||
|
||||
thickness_variation = OreGeneration.CoalSeams._calculate_thickness_variation(x, z, thickness_scale, thickness_amp)
|
||||
adjusted_thickness = thickness + thickness_variation
|
||||
half_thickness = adjusted_thickness / 2.0
|
||||
|
||||
if OreGeneration.CoalSeams._check_point_in_seam(y, seam_center, half_thickness):
|
||||
if OreGeneration.CoalSeams._check_seam_continuity(x, y, z, continuity_threshold):
|
||||
return True
|
||||
return False
|
||||
|
||||
# --- Individual Seam Types (Combinable) ---
|
||||
|
||||
@staticmethod
|
||||
def deep_seam(x, y, z):
|
||||
"""Deep seam (y=10-15): 5 blocks thick, for room-and-pillar underground mining"""
|
||||
return OreGeneration.CoalSeams._check_seam(x, y, z, center=12, thickness=5)
|
||||
|
||||
@staticmethod
|
||||
def medium_seam(x, y, z):
|
||||
"""Medium seam (y=30-35): 4 blocks thick, standard underground mining depth"""
|
||||
return OreGeneration.CoalSeams._check_seam(x, y, z, center=32, thickness=4)
|
||||
|
||||
@staticmethod
|
||||
def shallow_mountain_seam(x, y, z):
|
||||
"""Shallow seam (y=80-90): 3 blocks thick, exposed on mountains for strip/MTR mining"""
|
||||
return OreGeneration.CoalSeams._check_seam(x, y, z, center=85, thickness=3)
|
||||
|
||||
@staticmethod
|
||||
def shallow_plains_seam(x, y, z):
|
||||
"""Shallow plains seam (y=45): 2 blocks thick, less undulation"""
|
||||
return OreGeneration.CoalSeams._check_seam(
|
||||
x, y, z, center=45, thickness=2,
|
||||
undulation_scale=0.015, undulation_amp=1.5,
|
||||
thickness_scale=0.04, thickness_amp=0.5,
|
||||
continuity_threshold=-0.6 # More continuous
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def medium_plains_seam(x, y, z):
|
||||
"""Medium plains seam (y=25): 3 blocks thick, less undulation"""
|
||||
return OreGeneration.CoalSeams._check_seam(
|
||||
x, y, z, center=25, thickness=3,
|
||||
undulation_scale=0.015, undulation_amp=1.5,
|
||||
thickness_scale=0.04, thickness_amp=0.5,
|
||||
continuity_threshold=-0.6 # More continuous
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def surface_strip_seam(x, y, z, column_height):
|
||||
"""
|
||||
Strip mining seam with flat ceiling at column_height - 5.
|
||||
West Kentucky accurate: 1 block thick, highly continuous.
|
||||
Incentivizes leveling terrain to access the ore layer.
|
||||
"""
|
||||
return OreGeneration.CoalSeams._check_flat_ceiling_seam(
|
||||
x, y, z, column_height,
|
||||
ceiling_depth=5,
|
||||
thickness=1,
|
||||
thickness_scale=0.02,
|
||||
thickness_amp=0.3,
|
||||
continuity_threshold=-0.3 # Very high continuity, fewer gaps
|
||||
)
|
||||
|
||||
# --- Biome Configurations (which seams are active) ---
|
||||
|
||||
BIOME_SEAMS = {
|
||||
"appalachian": ["deep_seam", "medium_seam", "shallow_mountain_seam", "surface_strip_seam"],
|
||||
"plains": ["medium_plains_seam", "shallow_plains_seam", "surface_strip_seam"],
|
||||
"desert": ["deep_seam"], # Only deep seams in desert
|
||||
"mixed": ["deep_seam", "medium_seam", "medium_plains_seam", "surface_strip_seam"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def generate_coal(x, y, z, column_height=None, biome="appalachian"):
|
||||
"""
|
||||
Main entry point for coal generation.
|
||||
Checks all seam types active for the given biome.
|
||||
|
||||
Args:
|
||||
x, y, z: Block coordinates
|
||||
column_height: Terrain surface height (needed for terrain-relative seams)
|
||||
biome: Biome name (e.g., "appalachian", "plains", "desert")
|
||||
|
||||
Returns:
|
||||
True if coal should be placed at this location, False otherwise
|
||||
"""
|
||||
# Get seam types for this biome (default to appalachian)
|
||||
seam_types = OreGeneration.CoalSeams.BIOME_SEAMS.get(biome, ["deep_seam", "medium_seam", "shallow_mountain_seam", "surface_strip_seam"])
|
||||
|
||||
# Check each active seam type
|
||||
for seam_name in seam_types:
|
||||
seam_func = getattr(OreGeneration.CoalSeams, seam_name, None)
|
||||
if seam_func:
|
||||
# Handle terrain-relative seams that need column_height
|
||||
if seam_name == "surface_strip_seam":
|
||||
if column_height is not None and seam_func(x, y, z, column_height):
|
||||
return True
|
||||
else:
|
||||
# Standard fixed-depth seams
|
||||
if seam_func(x, y, z):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WORLD GENERATION
|
||||
# ---------------------------------------------------------------------------
|
||||
class worldGen:
|
||||
# minimum bowl depth needed before we consider it a lake candidate
|
||||
BASIN_MIN_DEPTH = 4
|
||||
# how far around a column we scan to estimate its bowl rim
|
||||
BASIN_RADIUS = 4
|
||||
# Tree placement tuning
|
||||
TREE_GRID_SPACING = 6 # coarse grid to enforce spacing
|
||||
TREE_MARGIN = 3 # how far outside the chunk we look for trunks (so crowns don't cut off)
|
||||
MIN_TREE_ALT = sea_level - 2
|
||||
LOW_FADE_TOP = sea_level + 6
|
||||
TREE_LINE = sea_level + 60
|
||||
TREE_LINE_FADE = 20
|
||||
MAX_SLOPE = 2
|
||||
|
||||
@staticmethod
|
||||
def getBlockID(block_name: str) -> int:
|
||||
"""
|
||||
Convert a human-readable block name (e.g. "grass") into a numeric ID.
|
||||
If the name isn't found, we default to "air" so we don't crash.
|
||||
In a real engine, you'd probably want to raise an error instead.
|
||||
"""
|
||||
return block_ids.get(block_name, block_ids["air"])
|
||||
|
||||
@staticmethod
|
||||
def _get_column_height(x: int, z: int) -> int:
|
||||
key = (x, z)
|
||||
if key in _height_cache:
|
||||
return _height_cache[key]
|
||||
|
||||
# DOMAIN WARPING - Keep this the same
|
||||
warp1_x = x + noise.noise2(x * 0.001, z * 0.001) * 50
|
||||
warp1_z = z + noise.noise2((x+1000) * 0.001, (z+1000) * 0.001) * 50
|
||||
|
||||
warp2_x = warp1_x + noise.noise2(warp1_x * 0.01, warp1_z * 0.01) * 10
|
||||
warp2_z = warp1_z + noise.noise2((warp1_x+500) * 0.01, (warp1_z+500) * 0.01) * 10
|
||||
base_height = sea_level - 5
|
||||
|
||||
height = base_height
|
||||
|
||||
# KEEP THESE - Large scale features (mountains, valleys)
|
||||
height += noise.noise2(warp2_x * 0.0005, warp2_z * 0.0005) * 80
|
||||
height += noise.noise2(warp2_x * 0.002, warp2_z * 0.002) * 40
|
||||
height += noise.noise2(warp2_x * 0.005, warp2_z * 0.005) * 25
|
||||
height += noise.noise2(warp2_x * 0.01, warp2_z * 0.01) * 15
|
||||
|
||||
# REDUCE OR REMOVE THESE - Small scale bumps and roughness
|
||||
height += noise.noise2(warp2_x * 0.02, warp2_z * 0.02) * 5 # Was 10
|
||||
height += noise.noise2(warp2_x * 0.05, warp2_z * 0.05) * 2 # Was 6
|
||||
height += noise.noise2(warp2_x * 0.1, warp2_z * 0.1) * 1 # Was 3
|
||||
# height += noise.noise2(warp2_x * 0.2, warp2_z * 0.2) * 1 # REMOVE - too rough
|
||||
|
||||
_height_cache[key] = int(height)
|
||||
return int(height)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _estimate_basin_water_surface(x: int, z: int, column_height: int):
|
||||
"""Look for local bowls and return a rim height if one exists."""
|
||||
samples = []
|
||||
for dx in range(-worldGen.BASIN_RADIUS, worldGen.BASIN_RADIUS + 1):
|
||||
for dz in range(-worldGen.BASIN_RADIUS, worldGen.BASIN_RADIUS + 1):
|
||||
if dx == 0 and dz == 0:
|
||||
continue
|
||||
samples.append(worldGen._get_column_height(x + dx, z + dz))
|
||||
if not samples:
|
||||
return None
|
||||
rim_height = min(samples)
|
||||
if rim_height - column_height < worldGen.BASIN_MIN_DEPTH:
|
||||
return None
|
||||
return rim_height
|
||||
|
||||
@staticmethod
|
||||
def _get_column_data(x: int, z: int):
|
||||
key = (x, z)
|
||||
cached = _column_data_cache.get(key)
|
||||
if cached:
|
||||
return cached
|
||||
column_height = worldGen._get_column_height(x, z)
|
||||
basin_surface = worldGen._estimate_basin_water_surface(x, z, column_height)
|
||||
if basin_surface is not None:
|
||||
water_surface_y = basin_surface
|
||||
elif column_height < sea_level:
|
||||
water_surface_y = sea_level
|
||||
else:
|
||||
water_surface_y = None
|
||||
data = (column_height, water_surface_y)
|
||||
_column_data_cache[key] = data
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def generateBlock(x: int, y: int, z: int) -> int:
|
||||
"""
|
||||
Determine which block ID should exist at world coordinates (x, y, z).
|
||||
This uses:
|
||||
- A noise-based heightmap (via OpenSimplex) to decide terrain surface.
|
||||
- Simple layering rules for stone/dirt/grass.
|
||||
- sea_level to decide where water goes.
|
||||
"""
|
||||
# -------------------------------------------------------------------
|
||||
# 1. BEDROCK LAYER
|
||||
# -------------------------------------------------------------------
|
||||
# We force a solid bedrock floor at the bottom of the world.
|
||||
# You can change "0" to something else if you want a thicker bedrock band.
|
||||
if y == 0:
|
||||
return worldGen.getBlockID("bedrock")
|
||||
# -------------------------------------------------------------------
|
||||
# 2. TERRAIN HEIGHT FOR THIS (x, z) COLUMN
|
||||
# -------------------------------------------------------------------
|
||||
column_height, water_surface_y = worldGen._get_column_data(x, z)
|
||||
# -------------------------------------------------------------------
|
||||
# 3. WATER LEVEL LOGIC
|
||||
# -------------------------------------------------------------------
|
||||
# First, try finding a local bowl and fill it to the rim. If no bowl
|
||||
# exists, fall back to the global sea_level rule for oceans.
|
||||
# (handled inside _get_column_data now)
|
||||
# -------------------------------------------------------------------
|
||||
# 4. BLOCK SELECTION BY VERTICAL POSITION
|
||||
# -------------------------------------------------------------------
|
||||
# We now choose the block type based on how y compares to:
|
||||
# - column_height (terrain surface)
|
||||
# - water_surface_y (ocean/lake surface)
|
||||
#
|
||||
# Basic layering:
|
||||
# - Bedrock at y <= 0 (we already handled this above)
|
||||
# - From y=1 up to terrain surface - 4: stone
|
||||
# - Next 3 layers under the surface: dirt
|
||||
# - Surface: grass (if above water), or sand/etc. if you add biomes later
|
||||
# - Between terrain surface and water surface: water
|
||||
# - Above water surface: air
|
||||
# 4a. Terrain below the ground surface => underground blocks
|
||||
if y < column_height - 3:
|
||||
# Check for coal seams first (pass column_height for terrain-relative seams)
|
||||
if OreGeneration.generate_coal(x, y, z, column_height=column_height, biome="appalachian"):
|
||||
return worldGen.getBlockID("coal")
|
||||
# Deep underground: solid stone
|
||||
return worldGen.getBlockID("stone")
|
||||
if y < column_height:
|
||||
# Just under the surface: a few layers of dirt
|
||||
return worldGen.getBlockID("dirt")
|
||||
# 4b. Exactly at the terrain surface
|
||||
if y == column_height:
|
||||
# If this position is also *below* sea level, we might prefer sand or
|
||||
# some other block later depending on biome and proximity to water.
|
||||
# For now, always grass as your basic "land" surface.
|
||||
return worldGen.getBlockID("grass")
|
||||
# 4c. Above the terrain surface but below water surface => water column
|
||||
if water_surface_y is not None and y <= water_surface_y and y > column_height:
|
||||
# This is underwater. A simple ocean/lake fill.
|
||||
return worldGen.getBlockID("water")
|
||||
# 4d. Everything else is air
|
||||
return worldGen.getBlockID("air")
|
||||
|
||||
@staticmethod
|
||||
def _can_place_tree(x: int, y: int, z: int, height: int) -> bool:
|
||||
"""
|
||||
Check if there's enough space to place a tree.
|
||||
- Must be on grass block
|
||||
- Must have air above for tree height + crown
|
||||
"""
|
||||
ground_block = worldGen.generateBlock(x, y - 1, z)
|
||||
if ground_block != worldGen.getBlockID("grass"):
|
||||
return False
|
||||
for check_y in range(y, y + height + 3):
|
||||
if worldGen.generateBlock(x, check_y, z) != worldGen.getBlockID("air"):
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _tree_density_mask(x: int, z: int) -> float:
|
||||
"""Blend two low-frequency noises to drive forest clumping."""
|
||||
forest = noise.noise2((x + 3000) * 0.01, (z - 3000) * 0.01)
|
||||
moisture = noise.noise2((x - 5000) * 0.02, (z + 5000) * 0.02)
|
||||
return (forest * 0.6 + moisture * 0.4 + 1.0) * 0.5 # normalize to ~0..1
|
||||
|
||||
@staticmethod
|
||||
def _ground_slope(x: int, z: int) -> int:
|
||||
center = worldGen._get_column_height(x, z)
|
||||
max_delta = 0
|
||||
for dx, dz in ((1, 0), (-1, 0), (0, 1), (0, -1)):
|
||||
neighbor = worldGen._get_column_height(x + dx, z + dz)
|
||||
max_delta = max(max_delta, abs(neighbor - center))
|
||||
return max_delta
|
||||
|
||||
@staticmethod
|
||||
def _generate_oak_tree(x: int, y: int, z: int, variation: int = 0, rng: random.Random = None):
|
||||
"""
|
||||
Generate an oak tree structure.
|
||||
Returns a list of (x, y, z, block_id) tuples.
|
||||
|
||||
variation: 0-2 for small/medium/large trees
|
||||
"""
|
||||
if rng is None:
|
||||
rng = random
|
||||
blocks = []
|
||||
log_id = worldGen.getBlockID("oak_log")
|
||||
leaf_id = worldGen.getBlockID("oak_leaves")
|
||||
trunk_height = 4 + variation + rng.randint(0, 2)
|
||||
# Build trunk
|
||||
for dy in range(trunk_height):
|
||||
blocks.append((x, y + dy, z, log_id))
|
||||
# Crown starting height
|
||||
crown_start = y + trunk_height - 2
|
||||
crown_top = y + trunk_height + 2
|
||||
# Build leaf crown (roughly spherical)
|
||||
for cy in range(crown_start, crown_top):
|
||||
distance_from_center = abs(cy - (crown_start + 2))
|
||||
if distance_from_center == 0:
|
||||
radius = 2
|
||||
elif distance_from_center == 1:
|
||||
radius = 2
|
||||
elif distance_from_center == 2:
|
||||
radius = 1
|
||||
else:
|
||||
radius = 1
|
||||
# Place leaves in a circle
|
||||
for dx in range(-radius, radius + 1):
|
||||
for dz in range(-radius, radius + 1):
|
||||
if abs(dx) == radius and abs(dz) == radius:
|
||||
if rng.random() > 0.3:
|
||||
continue
|
||||
if dx == 0 and dz == 0 and cy < y + trunk_height:
|
||||
continue
|
||||
blocks.append((x + dx, cy, z + dz, leaf_id))
|
||||
return blocks
|
||||
|
||||
@staticmethod
|
||||
def _generate_birch_tree(x: int, y: int, z: int, rng: random.Random = None):
|
||||
"""
|
||||
Generate a birch tree structure (taller, thinner than oak).
|
||||
Returns a list of (x, y, z, block_id) tuples.
|
||||
"""
|
||||
if rng is None:
|
||||
rng = random
|
||||
blocks = []
|
||||
log_id = worldGen.getBlockID("birch_log")
|
||||
leaf_id = worldGen.getBlockID("birch_leaves")
|
||||
trunk_height = 5 + rng.randint(0, 3)
|
||||
# Build trunk
|
||||
for dy in range(trunk_height):
|
||||
blocks.append((x, y + dy, z, log_id))
|
||||
# Smaller, higher crown
|
||||
crown_start = y + trunk_height - 1
|
||||
crown_top = y + trunk_height + 2
|
||||
for cy in range(crown_start, crown_top):
|
||||
distance_from_top = crown_top - cy
|
||||
if distance_from_top <= 1:
|
||||
radius = 1
|
||||
else:
|
||||
radius = 2
|
||||
for dx in range(-radius, radius + 1):
|
||||
for dz in range(-radius, radius + 1):
|
||||
if abs(dx) + abs(dz) > radius + 1:
|
||||
continue
|
||||
if dx == 0 and dz == 0 and cy < y + trunk_height:
|
||||
continue
|
||||
blocks.append((x + dx, cy, z + dz, leaf_id))
|
||||
return blocks
|
||||
|
||||
@staticmethod
|
||||
def generateTree(x: int, y: int, z: int, tree_type: str = "oak", variation: int = None, rng: random.Random = None):
|
||||
"""
|
||||
Main tree generation function.
|
||||
|
||||
Args:
|
||||
x, y, z: Base coordinates for tree trunk
|
||||
tree_type: "oak" or "birch"
|
||||
variation: For oak trees, 0-2 for size variation
|
||||
rng: optional random.Random for deterministic per-tree variation
|
||||
|
||||
Returns:
|
||||
List of (x, y, z, block_id) tuples, or None if can't place
|
||||
"""
|
||||
if rng is None:
|
||||
rng = random
|
||||
if tree_type == "oak":
|
||||
if variation is None:
|
||||
variation = rng.randint(0, 2)
|
||||
if not worldGen._can_place_tree(x, y, z, 4 + variation):
|
||||
return None
|
||||
return worldGen._generate_oak_tree(x, y, z, variation, rng)
|
||||
elif tree_type == "birch":
|
||||
if not worldGen._can_place_tree(x, y, z, 6):
|
||||
return None
|
||||
return worldGen._generate_birch_tree(x, y, z, rng)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def generate_chunk_trees(chunk_x: int, chunk_z: int):
|
||||
"""
|
||||
Populate trees for a chunk (including a small margin so crowns don't cut off).
|
||||
Returns a dict keyed by (x, y, z) with block IDs to overlay on terrain.
|
||||
"""
|
||||
tree_blocks = {}
|
||||
grid = worldGen.TREE_GRID_SPACING
|
||||
margin = worldGen.TREE_MARGIN
|
||||
chunk_min_x = chunk_x * 16 - margin
|
||||
chunk_max_x = chunk_min_x + 16 + 2 * margin - 1
|
||||
chunk_min_z = chunk_z * 16 - margin
|
||||
chunk_max_z = chunk_min_z + 16 + 2 * margin - 1
|
||||
|
||||
grid_min_x = math.floor(chunk_min_x / grid)
|
||||
grid_max_x = math.floor(chunk_max_x / grid)
|
||||
grid_min_z = math.floor(chunk_min_z / grid)
|
||||
grid_max_z = math.floor(chunk_max_z / grid)
|
||||
|
||||
for gx in range(grid_min_x, grid_max_x + 1):
|
||||
for gz in range(grid_min_z, grid_max_z + 1):
|
||||
rng = random.Random(world_seed + gx * 341873128712 + gz * 132897987541)
|
||||
candidate_x = gx * grid + rng.randint(0, grid - 1)
|
||||
candidate_z = gz * grid + rng.randint(0, grid - 1)
|
||||
# Only consider trunks that could affect this chunk (with margin)
|
||||
if not (chunk_min_x <= candidate_x <= chunk_max_x and chunk_min_z <= candidate_z <= chunk_max_z):
|
||||
continue
|
||||
|
||||
column_height, water_surface_y = worldGen._get_column_data(candidate_x, candidate_z)
|
||||
# Avoid underwater or flooded tiles
|
||||
if water_surface_y is not None and column_height < water_surface_y:
|
||||
continue
|
||||
# Elevation gates
|
||||
if column_height < worldGen.MIN_TREE_ALT or column_height > worldGen.TREE_LINE:
|
||||
continue
|
||||
# Slope gate to keep cliffs and sharp ridges clear
|
||||
if worldGen._ground_slope(candidate_x, candidate_z) > worldGen.MAX_SLOPE:
|
||||
continue
|
||||
|
||||
altitude_factor = 1.0
|
||||
# Fade near shore/marsh
|
||||
if column_height < worldGen.LOW_FADE_TOP:
|
||||
fade_span = max(1, worldGen.LOW_FADE_TOP - worldGen.MIN_TREE_ALT)
|
||||
altitude_factor *= max(0.0, (column_height - worldGen.MIN_TREE_ALT) / fade_span)
|
||||
# Fade near tree line
|
||||
if column_height > worldGen.TREE_LINE - worldGen.TREE_LINE_FADE:
|
||||
altitude_factor *= max(0.0, (worldGen.TREE_LINE - column_height) / worldGen.TREE_LINE_FADE)
|
||||
if altitude_factor <= 0:
|
||||
continue
|
||||
|
||||
density = worldGen._tree_density_mask(candidate_x, candidate_z)
|
||||
base_prob = 0.5
|
||||
spawn_prob = base_prob * (0.6 + 0.4 * density) * altitude_factor
|
||||
if rng.random() > spawn_prob:
|
||||
continue
|
||||
|
||||
# Cooler, higher ground skews to birch
|
||||
if column_height > sea_level + 35:
|
||||
tree_type = "birch" if rng.random() < 0.7 else "oak"
|
||||
elif column_height > sea_level + 15:
|
||||
tree_type = "oak" if rng.random() < 0.7 else "birch"
|
||||
else:
|
||||
tree_type = "oak"
|
||||
|
||||
tree = worldGen.generateTree(
|
||||
candidate_x,
|
||||
column_height + 1,
|
||||
candidate_z,
|
||||
tree_type=tree_type,
|
||||
variation=None,
|
||||
rng=rng,
|
||||
)
|
||||
if not tree:
|
||||
continue
|
||||
|
||||
for bx, by, bz, block_id in tree:
|
||||
# Only apply blocks that fall inside this chunk
|
||||
if chunk_x * 16 <= bx <= chunk_x * 16 + 15 and chunk_z * 16 <= bz <= chunk_z * 16 + 15:
|
||||
if 0 <= by < 256:
|
||||
tree_blocks[(bx, by, bz)] = block_id
|
||||
return tree_blocks
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BIOME / TEMPERATURE FUNCTIONS (your original, with a small bug fix)
|
||||
# ---------------------------------------------------------------------------
|
||||
class biomeFuncs:
|
||||
@staticmethod
|
||||
def get_current_block_tmep(x, y, z):
|
||||
# TODO: You can later use height, biome, time, etc. to get the exact
|
||||
# temperature at this block. For now it's just a stub.
|
||||
#remember to account for things getting cooler/hotter as we dig.
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_current_biome_temp(biome, time, season):
|
||||
"""
|
||||
Compute the current temperature for a given biome at a given time/season.
|
||||
NOTE: I fixed a small bug:
|
||||
- You were looping "for biome in biome_base_temps" which overwrote
|
||||
the biome parameter and always ended with the last one.
|
||||
- Now we just look up the base temp directly from the biome argument.
|
||||
"""
|
||||
# Get base temperature for the biome (e.g. plains: 80)
|
||||
base_temp = biome_base_temps.get(biome, 70) # default 70 if biome unknown
|
||||
# Calculate weather adjustments based on biome and season
|
||||
standardWeatherAdjustment = 0
|
||||
weatherVariabilityFactor = 0
|
||||
if biome == "desert":
|
||||
if season == "winter":
|
||||
standardWeatherAdjustment = -30
|
||||
elif season == "fall":
|
||||
standardWeatherAdjustment = -10
|
||||
elif season == "summer":
|
||||
standardWeatherAdjustment = +10
|
||||
elif season == "spring":
|
||||
standardWeatherAdjustment = 0
|
||||
weatherVariabilityFactor = random.randint(-20, 20)
|
||||
elif biome == "plains":
|
||||
if season == "winter":
|
||||
standardWeatherAdjustment = -25
|
||||
elif season == "fall":
|
||||
standardWeatherAdjustment = -5
|
||||
elif season == "summer":
|
||||
standardWeatherAdjustment = +5
|
||||
elif season == "spring":
|
||||
standardWeatherAdjustment = 0
|
||||
weatherVariabilityFactor = random.randint(-15, 15)
|
||||
elif biome == "plains_river":
|
||||
if season == "winter":
|
||||
standardWeatherAdjustment = -20
|
||||
elif season == "fall":
|
||||
standardWeatherAdjustment = -3
|
||||
elif season == "summer":
|
||||
standardWeatherAdjustment = +3
|
||||
elif season == "spring":
|
||||
standardWeatherAdjustment = 0
|
||||
weatherVariabilityFactor = random.randint(-10, 10)
|
||||
elif biome == "rolling_hills":
|
||||
if season == "winter":
|
||||
standardWeatherAdjustment = -28
|
||||
elif season == "fall":
|
||||
standardWeatherAdjustment = -7
|
||||
elif season == "summer":
|
||||
standardWeatherAdjustment = +7
|
||||
elif season == "spring":
|
||||
standardWeatherAdjustment = 0
|
||||
weatherVariabilityFactor = random.randint(-18, 18)
|
||||
# -------------------------------------------------------------------
|
||||
# TIME-OF-DAY TEMPERATURE ADJUSTMENT (your original logic)
|
||||
# -------------------------------------------------------------------
|
||||
time_adjustment = 0
|
||||
# Convert time (0–24000 ticks) into a "Minecraft hour" in [0, 24).
|
||||
# 0 ticks = 6am, 6000 = 12pm, 12000 = 6pm, 18000 = 12am
|
||||
minecraft_hour = (time / 1000 + 6) % 24
|
||||
if 6 <= minecraft_hour < 14: # Morning to peak heat
|
||||
time_progress = (minecraft_hour - 6) / 8 # 0..1
|
||||
time_adjustment = time_progress * 15 # 0..+15
|
||||
elif 14 <= minecraft_hour < 20: # Afternoon to evening
|
||||
time_progress = (minecraft_hour - 14) / 6 # 0..1
|
||||
time_adjustment = 15 - (time_progress * 12) # +15..+3
|
||||
elif 20 <= minecraft_hour < 24: # Evening to midnight
|
||||
time_progress = (minecraft_hour - 20) / 4 # 0..1
|
||||
time_adjustment = 3 - (time_progress * 8) # +3..-5
|
||||
else: # Midnight to morning (0-6)
|
||||
time_progress = minecraft_hour / 6 # 0..1
|
||||
time_adjustment = -5 + (time_progress * 5) # -5..0
|
||||
# Apply biome-specific time modifiers
|
||||
if biome == "desert":
|
||||
time_adjustment *= 1.5 # Deserts have more extreme swings
|
||||
elif biome == "plains_river":
|
||||
time_adjustment *= 0.7 # Rivers moderate temperature changes
|
||||
# Final temperature
|
||||
temp = base_temp + standardWeatherAdjustment + weatherVariabilityFactor + time_adjustment
|
||||
return temp
|
||||
Reference in New Issue
Block a user