```python
import bpy
import math
import random
from mathutils import Vector, Euler

# ==============================================================================
# INITIALIZATION & CLEANUP
# ==============================================================================

# Switch to object mode if not already
if bpy.ops.object.mode_set.poll():
    bpy.ops.object.mode_set(mode='OBJECT')

# Clear existing objects in the scene
for obj in bpy.data.objects:
    bpy.data.objects.remove(obj, do_unlink=True)

# Clear unused block data
for block in bpy.data.meshes:
    bpy.data.meshes.remove(block)
for block in bpy.data.materials:
    bpy.data.materials.remove(block)
for block in bpy.data.lamps if hasattr(bpy.data, "lamps") else []:
    bpy.data.lamps.remove(block)
for block in bpy.data.cameras:
    bpy.data.cameras.remove(block)

# Set up Cycles Engine
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.samples = 64
bpy.context.scene.cycles.use_denoising = True

# Color Management Setup
bpy.context.scene.view_settings.view_transform = 'AgX'
bpy.context.scene.view_settings.look = 'Medium High Contrast'

# ==============================================================================
# SHADERS & MATERIALS
# ==============================================================================

def create_wood_material():
    mat = bpy.data.materials.new(name="WeatheredWood")
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    nodes.clear()

    output = nodes.new(type='ShaderNodeOutputMaterial')
    bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')

    # Texture coordinate mapping to stretch grain
    tex_coord = nodes.new(type='ShaderNodeTexCoord')
    mapping = nodes.new(type='ShaderNodeMapping')
    mapping.inputs['Scale'].default_value[0] = 12.0
    mapping.inputs['Scale'].default_value[1] = 1.0
    mapping.inputs['Scale'].default_value[2] = 1.0

    noise = nodes.new(type='ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 8.0
    noise.inputs['Detail'].default_value = 4.0

    ramp = nodes.new(type='ShaderNodeValToRGB')
    ramp.color_ramp.elements[0].position = 0.35
    ramp.color_ramp.elements[0].color = (0.24, 0.20, 0.17, 1.0) # Grey weathered wood
    ramp.color_ramp.elements[1].position = 0.65
    ramp.color_ramp.elements[1].color = (0.12, 0.10, 0.08, 1.0) # Rotten dark wood

    bump = nodes.new(type='ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.35

    links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector'])
    links.new(mapping.outputs['Vector'], noise.inputs['Vector'])
    links.new(noise.outputs['Color'], ramp.inputs['Fac'])
    links.new(noise.outputs['Factor'], bump.inputs['Height'])
    links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])
    links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])

    bsdf.inputs['Roughness'].default_value = 0.95

    links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])
    return mat

def create_rust_material():
    mat = bpy.data.materials.new(name="RustedMetal")
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    nodes.clear()

    output = nodes.new(type='ShaderNodeOutputMaterial')
    bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')

    noise = nodes.new(type='ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 14.0
    noise.inputs['Detail'].default_value = 6.0

    # ColorRamp for metal surface to corroded rust transition
    ramp_color = nodes.new(type='ShaderNodeValToRGB')
    ramp_color.color_ramp.elements[0].position = 0.42
    ramp_color.color_ramp.elements[0].color = (0.32, 0.33, 0.35, 1.0) # Clean oxidized metal
    ramp_color.color_ramp.elements[1].position = 0.58
    ramp_color.color_ramp.elements[1].color = (0.22, 0.08, 0.03, 1.0) # Deep rust orange-brown

    # ColorRamp for metallic intensity (rust is non-metallic)
    ramp_metal = nodes.new(type='ShaderNodeValToRGB')
    ramp_metal.color_ramp.elements[0].position = 0.44
    ramp_metal.color_ramp.elements[0].color = (0.8, 0.8, 0.8, 1.0) # Metallic metal
    ramp_metal.color_ramp.elements[1].position = 0.54
    ramp_metal.color_ramp.elements[1].color = (0.0, 0.0, 0.0, 1.0) # Non-metallic rust

    bump = nodes.new(type='ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.4

    links.new(noise.outputs['Color'], ramp_color.inputs['Fac'])
    links.new(noise.outputs['Color'], ramp_metal.inputs['Fac'])
    links.new(noise.outputs['Factor'], bump.inputs['Height'])

    links.new(ramp_color.outputs['Color'], bsdf.inputs['Base Color'])
    links.new(ramp_metal.outputs['Color'], bsdf.inputs['Metallic'])
    links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])

    bsdf.inputs['Roughness'].default_value = 0.85

    links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])
    return mat

def create_moss_material():
    mat = bpy.data.materials.new(name="MossFoliage")
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    nodes.clear()

    output = nodes.new(type='ShaderNodeOutputMaterial')
    bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')

    noise = nodes.new(type='ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 18.0
    noise.inputs['Detail'].default_value = 4.0

    ramp = nodes.new(type='ShaderNodeValToRGB')
    ramp.color_ramp.elements[0].position = 0.25
    ramp.color_ramp.elements[0].color = (0.05, 0.12, 0.03, 1.0) # Moody forest green
    ramp.color_ramp.elements[1].position = 0.75
    ramp.color_ramp.elements[1].color = (0.18, 0.28, 0.06, 1.0) # Bright moss highlight

    links.new(noise.outputs['Color'], ramp.inputs['Fac'])
    links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])
    bsdf.inputs['Roughness'].default_value = 0.98

    links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])
    return mat

def create_ground_material():
    mat = bpy.data.materials.new(name="GroundMaterial")
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    nodes.clear()

    output = nodes.new(type='ShaderNodeOutputMaterial')
    bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')

    noise = nodes.new(type='ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 6.0
    noise.inputs['Detail'].default_value = 4.0

    ramp = nodes.new(type='ShaderNodeValToRGB')
    ramp.color_ramp.elements[0].position = 0.38
    ramp.color_ramp.elements[0].color = (0.13, 0.09, 0.07, 1.0) # Wet soil
    ramp.color_ramp.elements[1].position = 0.52
    ramp.color_ramp.elements[1].color = (0.08, 0.14, 0.04, 1.0) # Forest/ground moss

    bump = nodes.new(type='ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.6

    links.new(noise.outputs['Color'], ramp.inputs['Fac'])
    links.new(noise.outputs['Factor'], bump.inputs['Height'])
    links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])
    links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])

    bsdf.inputs['Roughness'].default_value = 0.96

    links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])
    return mat

def create_stone_material():
    mat = bpy.data.materials.new(name="StoneMaterial")
    mat.use_nodes = True
    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    nodes.clear()

    output = nodes.new(type='ShaderNodeOutputMaterial')
    bsdf = nodes.new(type='ShaderNodeBsdfPrincipled')

    noise = nodes.new(type='ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 12.0
    noise.inputs['Detail'].default_value = 4.0

    ramp = nodes.new(type='ShaderNodeValToRGB')
    ramp.color_ramp.elements[0].position = 0.3
    ramp.color_ramp.elements[0].color = (0.2, 0.2, 0.2, 1.0)
    ramp.color_ramp.elements[1].position = 0.7
    ramp.color_ramp.elements[1].color = (0.35, 0.35, 0.35, 1.0)

    bump = nodes.new(type='ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.5

    links.new(noise.outputs['Color'], ramp.inputs['Fac'])
    links.new(noise.outputs['Factor'], bump.inputs['Height'])
    links.new(ramp.outputs['Color'], bsdf.inputs['Base Color'])
    links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])
    bsdf.inputs['Roughness'].default_value = 0.85

    links.new(bsdf.outputs['BSDF'], output.inputs['Surface'])
    return mat

# Instantiate Materials
wood_mat = create_wood_material()
rust_mat = create_rust_material()
moss_mat = create_moss_material()
ground_mat = create_ground_material()
stone_mat = create_stone_material()

# ==============================================================================
# TERRAIN GENERATION
# ==============================================================================

def get_terrain_height(x, y):
    # Generates a winding path/valley down the middle, with hills on the sides
    val_center = 2.0 * math.sin(y * 0.2)
    dist_to_path = abs(x - val_center)
    h = (dist_to_path ** 1.3) * 0.12 - 0.5
    noise_val = 0.15 * math.sin(x*1.5) * math.cos(y*1.5) + 0.05 * math.sin(x*4)
    return h + noise_val

def create_terrain(mat):
    bpy.ops.mesh.primitive_grid_add(x_subdivisions=60, y_subdivisions=60, size=28.0)
    terrain = bpy.context.active_object
    terrain.name = "Terrain"
    terrain.data.materials.append(mat)

    # Deform vertices based on terrain height model
    mesh = terrain.data
    for vert in mesh.vertices:
        x, y = vert.co.x, vert.co.y
        vert.co.z = get_terrain_height(x, y)

    bpy.ops.object.shade_smooth()
    return terrain

terrain_obj = create_terrain(ground_mat)

# ==============================================================================
# ENVIRONMENT MECHANICS (FOLIAGE / WEED SHARDS)
# ==============================================================================

def duplicate_object(original, name="Instance"):
    new_obj = original.copy()
    new_obj.data = original.data.copy()
    new_obj.name = name
    bpy.context.scene.collection.objects.link(new_obj)
    return new_obj

def create_grass_asset(mat):
    # Generates a single detailed 3D clump of grass blades
    mesh_data = bpy.data.meshes.new(name="GrassAssetMesh")
    obj = bpy.data.objects.new("GrassAsset", mesh_data)
    bpy.context.scene.collection.objects.link(obj)
    obj.data.materials.append(mat)

    all_verts = []
    all_faces = []

    for i in range(14):
        length = random.uniform(0.35, 0.65)
        width = random.uniform(0.012, 0.026)
        rot_y = random.uniform(0.12, 0.48)
        rot_z = random.uniform(0.0, math.pi * 2.0)

        verts = [
            (-width, 0, 0), (width, 0, 0),
            (-width*0.8, 0, length*0.4), (width*0.8, 0, length*0.4),
            (-width*0.4, -width*0.25, length*0.8), (width*0.4, -width*0.25, length*0.8),
            (0, -width*0.5, length)
        ]

        # Splay out from middle cluster randomly
        offset_r = random.uniform(0.0, 0.06)
        offset_ang = random.uniform(0.0, math.pi*2)
        offset_x = math.cos(offset_ang) * offset_r
        offset_y = math.sin(offset_ang) * offset_r

        cos_z, sin_z = math.cos(rot_z), math.sin(rot_z)
        cos_y, sin_y = math.cos(rot_y), math.sin(rot_y)

        v_start = len(all_verts)
        for v in verts:
            x, y, z = v[0], v[1], v[2]
            # Lean curve
            new_x = x * cos_y - z * sin_y
            new_z = x * sin_y + z * cos_y
            # Rotation
            final_x = new_x * cos_z - y * sin_z + offset_x
            final_y = new_x * sin_z + y * cos_z + offset_y
            all_verts.append((final_x, final_y, new_z))

        all_faces.append((v_start, v_start+1, v_start+3, v_start+2))
        all_faces.append((v_start+2, v_start+3, v_start+5, v_start+4))
        all_faces.append((v_start+4, v_start+5, v_start+6))

    mesh_data.from_pydata(all_verts, [], all_faces)
    mesh_data.update()

    # Hide baseline master object
    obj.location = (0, 0, -100)
    return obj

grass_asset = create_grass_asset(moss_mat)

# Scatter grass across the terrain, leaving path relatively navigable but muddy/grassy
random.seed(88)
for i in range(180):
    x = random.uniform(-11.5, 11.5)
    y = random.uniform(-10.0, 11.5)

    val_center = 2.0 * math.sin(y * 0.2)
    dist_to_path = abs(x - val_center)

    # Prevent thick grass directly on path centerline to keep composition clear
    if dist_to_path < 1.1 and random.random() < 0.8:
        continue

    z = get_terrain_height(x, y)
    g_inst = duplicate_object(grass_asset, f"Grass_G_{i}")
    g_inst.location = (x, y, z)
    g_inst.rotation_euler = (random.uniform(-0.06, 0.06), random.uniform(-0.06, 0.06), random.uniform(0, math.pi*2))
    g_inst.scale = Vector((1, 1, 1)) * random.uniform(0.65, 1.35)

# ==============================================================================
# PROCEDURAL WEATHERED BUILDING SYSTEMS
# ==============================================================================

def spawn_plank_mesh(pos, z_pos, w, h, dir_norm, mat, parent):
    bpy.ops.mesh.primitive_cube_add(size=1.0)
    plank = bpy.context.active_object
    plank.data.materials.append(mat)
    plank.scale = (w * 0.95 / 2.0, 0.024, h / 2.0)
    plank.location = (pos[0], pos[1], z_pos)

    # Adding realistic decay (warping, wood tilting)
    rot_y = (random.random() - 0.5) * 0.11
    rot_x = (random.random() - 0.5) * 0.05
    rot_z = math.atan2(dir_norm.y, dir_norm.x) + (random.random() - 0.5) * 0.06
    plank.rotation_euler = (rot_x, rot_y, rot_z)
    plank.parent = parent

def create_weathered_wall(parent, start, end, height, mat, name_prefix="Wall", has_door=False, has_window=False):
    p1 = Vector((start[0], start[1], 0))
    p2 = Vector((end[0], end[1], 0))
    direction = p2 - p1
    length = direction.length
    dir_normalized = direction.normalized()

    plank_w = 0.23
    num_planks = int(length / plank_w)

    for i in range(num_planks):
        t = (i + 0.5) * plank_w
        pos = p1 + dir_normalized * t
        norm_pos = t / length

        is_door_zone = has_door and (0.35 < norm_pos < 0.65)
        is_window_zone = has_window and (0.32 < norm_pos < 0.68)

        if is_door_zone:
            # Doorway opening, occasionally spawn dangling loose wood header
            if random.random() < 0.22:
                p_h = height * 0.25
                z_pos = height - p_h/2.0
                spawn_plank_mesh(pos, z_pos, plank_w, p_h, dir_normalized, mat, parent)
            continue

        if is_window_zone:
            # Broken window frame opening
            # Bottom section of decayed frame wall
            p_h_bot = height * 0.32
            z_pos_bot = p_h_bot / 2.0
            if random.random() > 0.15:
                spawn_plank_mesh(pos, z_pos_bot, plank_w, p_h_bot, dir_normalized, mat, parent)
            # Top frame
            p_h_top = height * 0.28
            z_pos_top = height - p_h_top / 2.0
            if random.random() > 0.15:
                spawn_plank_mesh(pos, z_pos_top, plank_w, p_h_top, dir_normalized, mat, parent)
            continue

        # Regular Wall Decays: dynamic missing planks
        if random.random() < 0.15:
            continue

        # Standard vertical weathered planks
        spawn_plank_mesh(pos, height / 2.0, plank_w, height, dir_normalized, mat, parent)

def create_corrugated_sheet(width, length, mat):
    # Generates beautifully realistic bent sheet metal panels
    bpy.ops.mesh.primitive_grid_add(x_subdivisions=28, y_subdivisions=2, size=1.0)
    sheet = bpy.context.active_object
    sheet.data.materials.append(mat)
    sheet.scale = (width, length, 1.0)
    bpy.ops.object.transform_apply(scale=True)

    # Corrugated waviness logic
    mesh = sheet.data
    for vert in mesh.vertices:
        vert.co.z = 0.045 * math.sin(vert.co.x * 24.0)

    # Shell depth/solidify modifier for 3D realism
    sol = sheet.modifiers.new(name="Solidify", type='SOLIDIFY')
    sol.thickness = 0.012
    bpy.ops.object.modifier_apply(modifier="Solidify")

    return sheet

def create_ivy_on_wall(parent, wall_start, wall_end, wall_height, mat, leaf_mat, seed_val):
    # Crawling procedural ivy climbing decaying ruins
    random.seed(seed_val)
    t_start = random.uniform(0.25, 0.75)
    p_start = Vector((wall_start[0] + (wall_end[0] - wall_start[0])*t_start,
                      wall_start[1] + (wall_end[1] - wall_start[1])*t_start,
                      0.0))

    curr_pos = p_start.copy()
    points = [curr_pos.copy()]
    steps = int(random.uniform(9, 16))

    for _ in range(steps):
        up_step = random.uniform(0.16, 0.32)
        side_step = random.uniform(-0.18, 0.18)
        dir_vec = (Vector((wall_end[0], wall_end[1], 0)) - Vector((wall_start[0], wall_start[0], 0))).normalized()
        curr_pos += dir_vec * side_step + Vector((0, 0, up_step))
        if curr_pos.z > wall_height * 0.95:
            break
        points.append(curr_pos.copy())

    # Build vine trunk
    mesh_data = bpy.data.meshes.new(name="VineTrunk")
    obj = bpy.data.objects.new("Vine", mesh_data)
    bpy.context.scene.collection.objects.link(obj)
    obj.parent = parent
    obj.data.materials.append(mat)

    verts = []
    faces = []
    width_thick = 0.016
    for i, p in enumerate(points):
        verts.extend([
            (p.x - width_thick, p.y, p.z),
            (p.x + width_thick, p.y, p.z),
            (p.x, p.y - width_thick, p.z),
            (p.x, p.y + width_thick, p.z)
        ])
        if i > 0:
            v_base = (i - 1) * 4
            v_curr = i * 4
            faces.extend([
                (v_base, v_base+1, v_curr+1, v_curr),
                (v_base+2, v_base+3, v_curr+3, v_curr+2)
            ])

    mesh_data.from_pydata(verts, [], faces)
    mesh_data.update()

    # Ivy Leaves
    leaf_mesh = bpy.data.meshes.new(name="IvyLeaf")
    leaf_obj = bpy.data.objects.new("Leaf", leaf_mesh)
    bpy.context.scene.collection.objects.link(leaf_obj)
    leaf_obj.parent = parent
    leaf_obj.data.materials.append(leaf_mat)
    l_verts = [(-0.065, 0, 0), (0.065, 0, 0), (0, -0.025, 0.11), (0, 0.015, -0.025)]
    l_faces = [(0, 1, 2), (0, 3, 1)]
    leaf_mesh.from_pydata(l_verts, [], l_faces)
    leaf_mesh.update()
    leaf_obj.location = (0, 0, -100) # shelter master node template

    for p in points[1:]:
        num_leaves = random.randint(2, 4)
        for _ in range(num_leaves):
            l_inst = duplicate_object(leaf_obj, "IvyLeaf")
            l_inst.parent = parent
            offset_variation = Vector((random.uniform(-0.06, 0.06), random.uniform(-0.02, 0.02), random.uniform(-0.06, 0.06)))
            l_inst.location = p + offset_variation
            l_inst.rotation_euler = (random.uniform(-0.4, 0.4), random.uniform(-0.4, 0.4), random.uniform(0, math.pi*2))
            l_inst.scale = Vector((1.0, 1.0, 1.0)) * random.uniform(0.65, 1.25)

def build_house(name, pos, rot_z, size_x=3.8, size_y=2.8, height=2.1, wood_mat=None, roof_mat=None):
    house_root = bpy.data.objects.new(name, None)
    bpy.context.scene.collection.objects.link(house_root)
    house_root.location = pos
    house_root.rotation_euler[2] = rot_z

    half_x = size_x / 2.0
    half_y = size_y / 2.0

    # Build vertical wall planks structure
    create_weathered_wall(house_root, (-half_x, -half_y), (half_x, -half_y), height, wood_mat, "Wall_Front", has_door=True)
    create_weathered_wall(house_root, (half_x, half_y), (-half_x, half_y), height, wood_mat, "Wall_Back", has_window=True)
    create_weathered_wall(house_root, (-half_x, half_y), (-half_x, -half_y), height, wood_mat, "Wall_Left", has_window=True)
    create_weathered_wall(house_root, (half_x, -half_y), (half_x, half_y), height, wood_mat, "Wall_Right", has_window=False)

    # Roof trusses / dilapidated slats
    angle = math.radians(24)
    ridge_h = height + math.tan(angle) * (half_y + 0.2)

    # Left sliding corrugated panel
    if random.random() > 0.08:
        sheet_l = create_corrugated_sheet(size_x + 0.45, half_y + 0.4, roof_mat)
        sheet_l.parent = house_root
        sheet_l.location = (0, -half_y/2.0 - 0.08, height + (ridge_h - height)/2.8)
        sheet_l.rotation_euler = (angle, 0, 0)

    # Right hanging sliding panel - looks heavily broken/slipping
    if random.random() > 0.08:
        sheet_r = create_corrugated_sheet(size_x + 0.45, half_y + 0.4, roof_mat)
        sheet_r.parent = house_root
        slip_y = random.uniform(0.08, 0.22)
        sheet_r.location = (random.uniform(-0.12, 0.12), half_y/2.0 + 0.08 - slip_y, height + (ridge_h - height)/2.8 - slip_y*0.22)
        sheet_r.rotation_euler = (-angle + random.uniform(-0.06, 0.06), random.uniform(-0.02, 0.02), random.uniform(-0.08, 0.08))

    return house_root

def place_foliage_on_house(house_obj, num_clumps, grass_asset, size_x, size_y, height):
    # Grow mossy weeds out of corrugated metal valleys
    angle = math.radians(24)
    half_y = size_y / 2.0

    for _ in range(num_clumps):
        side = 1 if random.random() > 0.5 else -1
        lx = random.uniform(-size_x/2.1, size_x/2.1)
        ly_slope = random.uniform(-0.1, half_y + 0.1)
        ly = side * ly_slope * math.cos(angle)
        lz = height + (half_y - (ly_slope if side==1 else -ly_slope)) * math.sin(angle) + 0.12

        world_pos = house_obj.matrix_world @ Vector((lx, ly, lz))

        g = duplicate_object(grass_asset, "RoofGrass")
        g.location = world_pos
        g.rotation_euler = house_obj.rotation_euler
        g.rotation_euler[0] += side * angle
        g.rotation_euler[2] += random.uniform(-0.6, 0.6)
        g.scale = Vector((1.0, 1.0, 1.0)) * random.uniform(0.42, 0.78)

# ==============================================================================
# POPULATING THE ABANDONED VILLAGE
# ==============================================================================

# House 1: Foreground, main focus of age and decay
h1_pos = (2.3, -2.8)
h1_height = get_terrain_height(h1_pos[0], h1_pos[1])
h1 = build_house("House_Primary", (h1_pos[0], h1_pos[1], h1_height), rot_z=math.radians(-18), size_x=3.7, size_y=2.7, height=2.1, wood_mat=wood_mat, roof_mat=rust_mat)

# Crawling Vines climbing up weathering House 1 walls
create_ivy_on_wall(h1, (-1.85, -1.35), (1.85, -1.35), 2.1, wood_mat, moss_mat, 15) # front
create_ivy_on_wall(h1, (-1.85, 1.35), (-1.85, -1.35), 2.1, wood_mat, moss_mat, 18)  # side
place_foliage_on_house(h1, 10, grass_asset, 3.7, 2.7, 2.1)

# House 2: Reclaimed framing structure in background
h2_pos = (-3.8, 3.8)
h2_height = get_terrain_height(h2_pos[0], h2_pos[1])
h2 = build_house("House_BG_Left", (h2_pos[0], h2_pos[1], h2_height), rot_z=math.radians(32), size_x=3.2, size_y=2.4, height=1.9, wood_mat=wood_mat, roof_mat=rust_mat)
create_ivy_on_wall(h2, (-1.6, -1.2), (1.6, -1.2), 1.9, wood_mat, moss_mat, 25)
place_foliage_on_house(h2, 7, grass_asset, 3.2, 2.4, 1.9)

# Scatter debris, broken collapsed boards on the soil
def scatter_ground_debris(count, center, radius, mat):
    for i in range(count):
        bpy.ops.mesh.primitive_cube_add(size=1.0)
        plank = bpy.context.active_object
        plank.name = f"DebrisPlank_{i}"
        plank.data.materials.append(mat)
        plank.scale = (random.uniform(0.12, 0.16), random.uniform(0.45, 0.95), 0.016)

        dx = random.uniform(-radius, radius)
        dy = random.uniform(-radius, radius)
        x = center[0] + dx
        y = center[1] + dy
        z = get_terrain_height(x, y) + 0.02

        plank.location = (x, y, z)
        plank.rotation_euler = (random.uniform(-0.15, 0.15), random.uniform(-0.15, 0.15), random.uniform(0, math.pi*2))

scatter_ground_debris(14, (2.3, -2.8), 2.8, wood_mat)
scatter_ground_debris(8, (-3.8, 3.8), 2.2, wood_mat)

# ==============================================================================
# PROCEDURAL FORESTRY / RECLAIMED NATURE CANOPY
# ==============================================================================

def build_weathered_tree(pos, bark_mat, foliage_mat):
    # Generates dense canopy trees framing the ghost town scene
    bpy.ops.mesh.primitive_cylinder_add(vertices=7, radius=0.28, depth=4.2)
    trunk = bpy.context.active_object
    trunk.name = "TreeTrunk"
    trunk.data.materials.append(bark_mat)
    trunk.location = (pos[0], pos[1], pos[2] + 2.0)

    # Warping trunk profile
    for vert in trunk.data.vertices:
        if vert.co.z > 0:
            vert.co.x += 0.38 * math.sin(vert.co.z * 1.4)
            vert.co.y += 0.24 * math.cos(vert.co.z * 1.4)

    # Master overgrown crowns
    bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=2, radius=1.6)
    canopy = bpy.context.active_object
    canopy.name = "TreeCanopy"
    canopy.data.materials.append(foliage_mat)
    canopy.location = (pos[0] + 0.5, pos[1] - 0.2, pos[2] + 4.3)
    canopy.parent = trunk

    bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=2, radius=1.1)
    canopy2 = bpy.context.active_object
    canopy2.name = "TreeCanopySec"
    canopy2.data.materials.append(foliage_mat)
    canopy2.location = (pos[0] - 0.5, pos[1] + 0.4, pos[2] + 3.6)
    canopy2.parent = trunk

build_weathered_tree((-3.6, -3.2, get_terrain_height(-3.6, -3.2)), wood_mat, moss_mat)
build_weathered_tree((4.8, -1.2, get_terrain_height(4.8, -1.2)), wood_mat, moss_mat)
build_weathered_tree((-6.2, 6.5, get_terrain_height((-6.2), 6.5)), wood_mat, moss_mat)
build_weathered_tree((2.2, 8.5, get_terrain_height(2.2, 8.5)), wood_mat, moss_mat)

# ==============================================================================
# DETAILED COBBLE PATH SCATTER
# ==============================================================================

for i in range(35):
    y = random.uniform(-8.5, 8.5)
    val_center = 2.0 * math.sin(y * 0.2)
    x = val_center + random.uniform(-0.75, 0.75)
    z = get_terrain_height(x, y) - 0.03

    bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=1, radius=random.uniform(0.09, 0.22))
    stone = bpy.context.active_object
    stone.name = f"StonePath_{i}"
    stone.data.materials.append(stone_mat)
    stone.location = (x, y, z)
    stone.scale = Vector((random.uniform(1.1, 1.6), random.uniform(1.1, 1.6), random.uniform(0.3, 0.55)))
    stone.rotation_euler = (random.uniform(-0.1, 0.1), random.uniform(-0.1, 0.1), random.uniform(0, math.pi*2))

# ==============================================================================
# CAMERAS, CINEMATOGRAPHY & WEATHERED GLOOM LIGHTING
# ==============================================================================

# Creative camera placement: wide angled setup focusing on House 1 in foreground & wilderness engulfing everything
cam_data = bpy.data.cameras.new("ContestantCam")
cam_obj = bpy.data.objects.new("Camera", cam_data)
bpy.context.scene.collection.objects.link(cam_obj)
cam_data.lens = 26.0 # wide lens to exaggerate natural reclaim scale
cam_obj.location = (0.55, -9.2, 1.7)
cam_obj.rotation_euler = (math.radians(85.5), 0.0, math.radians(14))
bpy.context.scene.camera = cam_obj

# Nishita Sky Atmosphere: low misty sun penetrating decay
bpy.context.scene.use_nodes = True
world = bpy.context.scene.world
world.use_nodes = True
nodes = world.node_tree.nodes
links = world.node_tree.links
nodes.clear()

world_output = nodes.new(type='ShaderNodeOutputWorld')
sky = nodes.new(type='ShaderNodeTexSky')
sky.sky_type = 'NISHITA'
sky.sun_elevation = math.radians(11.0)
sky.sun_rotation = math.radians(222.0)
sky.altitude = 12.0
sky.air_density = 1.6
sky.dust_density = 2.4
sky.ozone_density = 4.2

bg = nodes.new(type='ShaderNodeBackground')
bg.inputs['Strength'].default_value = 0.85

links.new(sky.outputs['Color'], bg.inputs['Color'])
links.new(bg.outputs['Background'], world_output.inputs['Surface'])

# Direct Warm Sun rays caught on rusted sheets
sun_data = bpy.data.lights.new(name="MistySunLight", type='SUN')
sun_obj = bpy.data.objects.new(name="Sun", object_data=sun_data)
bpy.context.scene.collection.objects.link(sun_obj)
sun_obj.location = (0, 0, 16)
sun_obj.rotation_euler = (math.radians(11), math.radians(8), math.radians(222))
sun_data.energy = 2.2
sun_data.color = (1.0, 0.94, 0.86)

# Soft moody bounce area light for dark vegetation shadows
area_data = bpy.data.lights.new(name="AmbientGloomBounce", type='AREA')
area_obj = bpy.data.objects.new(name="AmbientBounce", object_data=area_data)
bpy.context.scene.collection.objects.link(area_obj)
area_obj.location = (0, 0, 8)
area_obj.rotation_euler = (math.radians(180), 0, 0)
area_data.size = 15.0
area_data.energy = 280.0
area_data.color = (0.8, 0.92, 1.0) # Cold sky fill

print("Successfully established an abandoned village scene slowly reclaimed by nature.")
```