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