Files
MC-Worldgen/mc.py
2025-12-02 18:38:45 -06:00

710 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (024000 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