Add world generator sources and binary

This commit is contained in:
chelsea
2025-12-02 18:38:45 -06:00
parent 3f92f5add7
commit 2482740b89
10 changed files with 4163 additions and 0 deletions

709
mc.py Normal file
View 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 (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