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

random.seed(7319)

# ---------------------------------------------------------------------
# Scene setup
# ---------------------------------------------------------------------

bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)

scene = bpy.context.scene
scene.render.engine = 'CYCLES'
scene.render.resolution_x = 768
scene.render.resolution_y = 768
scene.render.resolution_percentage = 100
scene.render.image_settings.file_format = 'PNG'
scene.render.film_transparent = False
scene.cycles.samples = 64
scene.cycles.use_denoising = True
scene.cycles.max_bounces = 8
scene.cycles.diffuse_bounces = 4
scene.cycles.glossy_bounces = 4
scene.cycles.transparent_max_bounces = 8
scene["description"] = "An abandoned village reclaimed by grass, moss, ivy, shrubs, and trees."

try:
    scene.view_settings.view_transform = 'AgX'
    scene.view_settings.look = 'AgX - Medium High Contrast'
    scene.view_settings.exposure = -0.15
except Exception:
    pass

# ---------------------------------------------------------------------
# Utility functions
# ---------------------------------------------------------------------

def set_socket(node, names, value):
    for name in names:
        socket = node.inputs.get(name)
        if socket is not None:
            socket.default_value = value
            return

def make_simple_material(name, color, roughness=0.7, metallic=0.0,
                         alpha=1.0, transmission=0.0):
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    mat.diffuse_color = color
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    set_socket(bsdf, ["Base Color"], color)
    set_socket(bsdf, ["Roughness"], roughness)
    set_socket(bsdf, ["Metallic"], metallic)
    set_socket(bsdf, ["Alpha"], alpha)
    set_socket(bsdf, ["Transmission Weight", "Transmission"], transmission)
    if alpha < 1.0:
        try:
            mat.surface_render_method = 'DITHERED'
        except Exception:
            pass
    return mat

def make_noise_material(name, c1, c2, roughness=0.75, metallic=0.0,
                        scale=4.0, detail=4.0, bump_strength=0.18,
                        bump_distance=0.12, mid=None):
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    avg = tuple((c1[i] + c2[i]) * 0.5 for i in range(3)) + (1.0,)
    mat.diffuse_color = avg

    nodes = mat.node_tree.nodes
    links = mat.node_tree.links
    nodes.clear()

    output = nodes.new("ShaderNodeOutputMaterial")
    bsdf = nodes.new("ShaderNodeBsdfPrincipled")
    texcoord = nodes.new("ShaderNodeTexCoord")
    noise = nodes.new("ShaderNodeTexNoise")
    ramp = nodes.new("ShaderNodeValToRGB")
    bump = nodes.new("ShaderNodeBump")

    noise.noise_dimensions = '3D'
    if noise.inputs.get("Scale"):
        noise.inputs["Scale"].default_value = scale
    if noise.inputs.get("Detail"):
        noise.inputs["Detail"].default_value = detail
    if noise.inputs.get("Roughness"):
        noise.inputs["Roughness"].default_value = 0.72

    ramp.color_ramp.elements[0].color = c1
    ramp.color_ramp.elements[0].position = 0.18
    ramp.color_ramp.elements[1].color = c2
    ramp.color_ramp.elements[1].position = 0.82
    if mid is not None:
        elem = ramp.color_ramp.elements.new(0.50)
        elem.color = mid

    set_socket(bsdf, ["Roughness"], roughness)
    set_socket(bsdf, ["Metallic"], metallic)
    bump.inputs["Strength"].default_value = bump_strength
    bump.inputs["Distance"].default_value = bump_distance

    links.new(texcoord.outputs["Generated"], noise.inputs["Vector"])
    links.new(noise.outputs["Fac"], ramp.inputs["Fac"])
    links.new(ramp.outputs["Color"], bsdf.inputs["Base Color"])
    links.new(noise.outputs["Fac"], bump.inputs["Height"])
    links.new(bump.outputs["Normal"], bsdf.inputs["Normal"])
    links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
    return mat

def assign_material(obj, mat):
    if mat is not None and hasattr(obj.data, "materials"):
        obj.data.materials.append(mat)

def make_empty(name, loc=(0, 0, 0), rot=(0, 0, 0), parent=None):
    obj = bpy.data.objects.new(name, None)
    scene.collection.objects.link(obj)
    obj.location = loc
    obj.rotation_euler = rot
    obj.empty_display_type = 'PLAIN_AXES'
    obj.empty_display_size = 0.35
    if parent is not None:
        obj.parent = parent
    return obj

def add_box(name, loc, dims, mat=None, rot=(0, 0, 0), parent=None,
            bevel=0.0):
    bpy.ops.mesh.primitive_cube_add(location=loc, rotation=rot)
    obj = bpy.context.object
    obj.name = name
    obj.scale = (dims[0] * 0.5, dims[1] * 0.5, dims[2] * 0.5)
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    if parent is not None:
        obj.parent = parent
    assign_material(obj, mat)
    if bevel > 0:
        mod = obj.modifiers.new("Soft weathered edges", 'BEVEL')
        mod.width = bevel
        mod.segments = 2
        mod.limit_method = 'ANGLE'
    return obj

def add_mesh_obj(name, verts, faces, mats=None, mat_indices=None,
                 parent=None, smooth=False):
    mesh = bpy.data.meshes.new(name + "_Mesh")
    mesh.from_pydata(verts, [], faces)
    mesh.update()
    obj = bpy.data.objects.new(name, mesh)
    scene.collection.objects.link(obj)
    if parent is not None:
        obj.parent = parent
    if mats:
        for mat in mats:
            mesh.materials.append(mat)
    if mat_indices:
        for poly, idx in zip(mesh.polygons, mat_indices):
            poly.material_index = idx
    if smooth:
        for poly in mesh.polygons:
            poly.use_smooth = True
    return obj

def add_ico(name, loc, scale, mat, parent=None, subdivisions=1,
            rot=(0, 0, 0), smooth=False):
    bpy.ops.mesh.primitive_ico_sphere_add(
        subdivisions=subdivisions, radius=1.0, location=loc
    )
    obj = bpy.context.object
    obj.name = name
    obj.scale = scale
    obj.rotation_euler = rot
    if parent is not None:
        obj.parent = parent
    assign_material(obj, mat)
    if smooth:
        for poly in obj.data.polygons:
            poly.use_smooth = True
    return obj

def add_cone_between(name, p1, p2, r1, r2, mat, parent=None,
                     vertices=8, smooth=False):
    p1 = Vector(p1)
    p2 = Vector(p2)
    vec = p2 - p1
    length = vec.length
    if length < 0.0001:
        return None
    midpoint = (p1 + p2) * 0.5
    bpy.ops.mesh.primitive_cone_add(
        vertices=vertices, radius1=r1, radius2=r2,
        depth=length, location=midpoint
    )
    obj = bpy.context.object
    obj.name = name
    obj.rotation_mode = 'QUATERNION'
    obj.rotation_quaternion = vec.to_track_quat('Z', 'Y')
    if parent is not None:
        obj.parent = parent
    assign_material(obj, mat)
    if smooth:
        for poly in obj.data.polygons:
            poly.use_smooth = True
    return obj

def add_beam_between(name, p1, p2, thickness, mat, parent=None,
                     bevel=0.015):
    p1 = Vector(p1)
    p2 = Vector(p2)
    vec = p2 - p1
    if vec.length < 0.0001:
        return None
    obj = add_box(
        name, (p1 + p2) * 0.5,
        (thickness, thickness, vec.length),
        mat, parent=parent, bevel=bevel
    )
    obj.rotation_mode = 'QUATERNION'
    obj.rotation_quaternion = vec.to_track_quat('Z', 'Y')
    return obj

def create_curve_mesh(name, points, bevel_depth, mat, parent=None,
                      bevel_resolution=1):
    if len(points) < 2:
        return None
    curve = bpy.data.curves.new(name + "_Curve", type='CURVE')
    curve.dimensions = '3D'
    curve.resolution_u = 1
    curve.bevel_depth = bevel_depth
    curve.bevel_resolution = bevel_resolution
    curve.fill_mode = 'FULL'
    spline = curve.splines.new('POLY')
    spline.points.add(len(points) - 1)
    for i, p in enumerate(points):
        spline.points[i].co = (p[0], p[1], p[2], 1.0)
    obj = bpy.data.objects.new(name, curve)
    scene.collection.objects.link(obj)
    if parent is not None:
        obj.parent = parent
    curve.materials.append(mat)

    bpy.ops.object.select_all(action='DESELECT')
    obj.select_set(True)
    bpy.context.view_layer.objects.active = obj
    try:
        bpy.ops.object.convert(target='MESH')
        obj = bpy.context.object
        for poly in obj.data.polygons:
            poly.use_smooth = True
    except Exception:
        pass
    return obj

def create_leaf_mesh(name, centers, plane, mats, parent=None,
                     size_range=(0.11, 0.22)):
    verts = []
    faces = []
    mat_indices = []
    for center in centers:
        c = Vector(center)
        length = random.uniform(*size_range)
        width = length * random.uniform(0.42, 0.64)
        ang = random.uniform(0, math.tau)

        if plane == 'front':
            a = Vector((math.cos(ang), 0, math.sin(ang)))
            b = Vector((-math.sin(ang), 0, math.cos(ang)))
            normal = Vector((0, -1, 0))
        elif plane == 'side':
            a = Vector((0, math.cos(ang), math.sin(ang)))
            b = Vector((0, -math.sin(ang), math.cos(ang)))
            normal = Vector((1, 0, 0))
        else:
            a = Vector((math.cos(ang), math.sin(ang), 0))
            b = Vector((-math.sin(ang), math.cos(ang), 0))
            normal = Vector((0, 0, 1))

        base = len(verts)
        verts.extend([
            tuple(c + a * (length * 0.5)),
            tuple(c + b * (width * 0.5)),
            tuple(c - a * (length * 0.5)),
            tuple(c - b * (width * 0.5)),
            tuple(c + normal * 0.022),
        ])
        faces.extend([
            (base, base + 1, base + 4),
            (base + 1, base + 2, base + 4),
            (base + 2, base + 3, base + 4),
            (base + 3, base, base + 4),
        ])
        idx = random.randrange(len(mats))
        mat_indices.extend([idx] * 4)

    if not faces:
        return None
    return add_mesh_obj(name, verts, faces, mats, mat_indices, parent)

def terrain_z(x, y):
    return (
        -0.16
        + 0.075 * math.sin(x * 0.31)
        + 0.055 * math.sin(y * 0.24 + 0.7)
        + 0.035 * math.sin(x * 0.73 + y * 0.39)
        + 0.018 * math.sin(x * 1.47 - y * 0.81)
    )

def path_x(y):
    return 0.55 * math.sin((y + 1.5) * 0.24) + 0.18 * math.sin(y * 0.72)

# ---------------------------------------------------------------------
# Materials
# ---------------------------------------------------------------------

GROUND = make_noise_material(
    "Uneven meadow earth",
    (0.025, 0.047, 0.018, 1),
    (0.18, 0.145, 0.055, 1),
    roughness=0.96, scale=5.0, detail=6.0,
    bump_strength=0.28, bump_distance=0.18,
    mid=(0.065, 0.09, 0.025, 1)
)
HILL_MAT = make_noise_material(
    "Distant muted hillside",
    (0.055, 0.075, 0.042, 1),
    (0.14, 0.16, 0.09, 1),
    roughness=1.0, scale=2.4, detail=3.0,
    bump_strength=0.1, bump_distance=0.2
)
PATH_MAT = make_noise_material(
    "Old muddy lane",
    (0.055, 0.028, 0.014, 1),
    (0.22, 0.13, 0.052, 1),
    roughness=0.94, scale=7.0, detail=5.0,
    bump_strength=0.24, bump_distance=0.12
)
DAMP = make_noise_material(
    "Damp wall stains",
    (0.018, 0.025, 0.012, 1),
    (0.10, 0.085, 0.035, 1),
    roughness=0.98, scale=3.0, detail=4.0,
    bump_strength=0.15, bump_distance=0.06
)

WALL1 = make_noise_material(
    "Faded lime plaster",
    (0.19, 0.18, 0.145, 1),
    (0.53, 0.49, 0.37, 1),
    roughness=0.95, scale=4.2, detail=6.0,
    bump_strength=0.32, bump_distance=0.09,
    mid=(0.34, 0.32, 0.25, 1)
)
WALL2 = make_noise_material(
    "Cold weathered plaster",
    (0.16, 0.17, 0.15, 1),
    (0.46, 0.43, 0.34, 1),
    roughness=0.96, scale=4.8, detail=5.0,
    bump_strength=0.3, bump_distance=0.09
)
WALL_GREEN = make_noise_material(
    "Faded sage plaster",
    (0.11, 0.14, 0.10, 1),
    (0.38, 0.40, 0.29, 1),
    roughness=0.97, scale=4.3, detail=5.0,
    bump_strength=0.28, bump_distance=0.1
)

WOOD = make_noise_material(
    "Rotten brown timber",
    (0.035, 0.016, 0.007, 1),
    (0.24, 0.10, 0.035, 1),
    roughness=0.93, scale=4.0, detail=5.0,
    bump_strength=0.3, bump_distance=0.07
)
WOOD_DARK = make_noise_material(
    "Blackened structural wood",
    (0.012, 0.008, 0.005, 1),
    (0.09, 0.045, 0.018, 1),
    roughness=0.96, scale=4.0, detail=5.0,
    bump_strength=0.25, bump_distance=0.06
)
WOOD_GRAY = make_noise_material(
    "Silvered fence wood",
    (0.055, 0.052, 0.045, 1),
    (0.25, 0.23, 0.19, 1),
    roughness=0.98, scale=5.0, detail=5.0,
    bump_strength=0.28, bump_distance=0.07
)

RUST1 = make_noise_material(
    "Orange corroded iron",
    (0.045, 0.012, 0.004, 1),
    (0.56, 0.145, 0.018, 1),
    roughness=0.78, metallic=0.46,
    scale=5.0, detail=7.0,
    bump_strength=0.38, bump_distance=0.07,
    mid=(0.25, 0.055, 0.009, 1)
)
RUST2 = make_noise_material(
    "Deep brown roof rust",
    (0.025, 0.009, 0.004, 1),
    (0.34, 0.075, 0.016, 1),
    roughness=0.84, metallic=0.38,
    scale=6.5, detail=6.0,
    bump_strength=0.42, bump_distance=0.065
)
RUST3 = make_noise_material(
    "Remnant oxidized steel",
    (0.065, 0.07, 0.065, 1),
    (0.31, 0.18, 0.075, 1),
    roughness=0.7, metallic=0.58,
    scale=5.5, detail=6.0,
    bump_strength=0.32, bump_distance=0.055
)
RUST_DARK = make_simple_material(
    "Dark rusted iron", (0.075, 0.025, 0.009, 1),
    roughness=0.86, metallic=0.44
)
ROOF_MATS = [RUST1, RUST2, RUST3, RUST1, RUST2]

MOSS = make_noise_material(
    "Deep roof moss",
    (0.018, 0.05, 0.009, 1),
    (0.19, 0.27, 0.045, 1),
    roughness=1.0, scale=4.0, detail=5.0,
    bump_strength=0.35, bump_distance=0.08
)
MOSS_LIGHT = make_noise_material(
    "Yellow green moss",
    (0.055, 0.085, 0.013, 1),
    (0.35, 0.39, 0.075, 1),
    roughness=1.0, scale=4.0, detail=5.0,
    bump_strength=0.3, bump_distance=0.07
)

VINE_STEM = make_simple_material(
    "Old ivy stems", (0.035, 0.047, 0.012, 1), roughness=0.95
)
VINE_BROWN = make_simple_material(
    "Woody vine", (0.065, 0.032, 0.009, 1), roughness=0.96
)
LEAF1 = make_simple_material(
    "Ivy leaf green", (0.035, 0.115, 0.018, 1), roughness=0.86
)
LEAF2 = make_simple_material(
    "Ivy leaf yellow green", (0.13, 0.22, 0.025, 1), roughness=0.88
)
LEAF_DARK = make_simple_material(
    "Deep foliage", (0.018, 0.065, 0.013, 1), roughness=0.9
)
LEAF_HAZE = make_simple_material(
    "Distant desaturated foliage", (0.105, 0.145, 0.085, 1), roughness=0.96
)
LEAF_DRY = make_simple_material(
    "Dead leaves", (0.27, 0.12, 0.018, 1), roughness=0.92
)

GRASS1 = make_simple_material(
    "Grass dark", (0.025, 0.08, 0.012, 1), roughness=0.95
)
GRASS2 = make_simple_material(
    "Grass green", (0.09, 0.18, 0.022, 1), roughness=0.93
)
GRASS_DRY = make_simple_material(
    "Dry straw grass", (0.30, 0.22, 0.055, 1), roughness=0.96
)
FERN_MAT = make_simple_material(
    "Fern fronds", (0.035, 0.145, 0.025, 1), roughness=0.9
)

STONE1 = make_noise_material(
    "Old foundation stone",
    (0.055, 0.052, 0.043, 1),
    (0.34, 0.32, 0.25, 1),
    roughness=0.98, scale=4.0, detail=5.0,
    bump_strength=0.3, bump_distance=0.08
)
STONE2 = make_simple_material(
    "Warm gray stone", (0.19, 0.17, 0.13, 1), roughness=0.98
)
STONE3 = make_simple_material(
    "Dark wet stone", (0.075, 0.075, 0.061, 1), roughness=0.96
)
BRICK = make_noise_material(
    "Old red brick",
    (0.12, 0.026, 0.011, 1),
    (0.43, 0.105, 0.036, 1),
    roughness=0.93, scale=4.0, detail=4.0,
    bump_strength=0.24, bump_distance=0.05
)
BRICK2 = make_simple_material(
    "Blackened brick", (0.16, 0.047, 0.018, 1), roughness=0.95
)
MORTAR = make_simple_material(
    "Exposed old mortar", (0.12, 0.105, 0.077, 1), roughness=1.0
)

DARK = make_simple_material(
    "Unlit abandoned interior", (0.0025, 0.003, 0.0025, 1), roughness=1.0
)
CRACK = make_simple_material(
    "Deep plaster cracks", (0.018, 0.012, 0.007, 1), roughness=1.0
)
GLASS = make_simple_material(
    "Dusty broken glass", (0.16, 0.28, 0.29, 0.33),
    roughness=0.18, alpha=0.33, transmission=0.35
)
WATER = make_simple_material(
    "Dark rain puddles", (0.025, 0.075, 0.09, 1),
    roughness=0.14, metallic=0.1
)
ROPE = make_simple_material(
    "Old well rope", (0.16, 0.08, 0.025, 1), roughness=0.98
)

# ---------------------------------------------------------------------
# Terrain, lane, puddles, stones, and ground vegetation
# ---------------------------------------------------------------------

x0, x1 = -19.0, 19.0
y0, y1 = -11.0, 34.0
nx, ny = 60, 70
verts = []
faces = []
for j in range(ny + 1):
    y = y0 + (y1 - y0) * j / ny
    for i in range(nx + 1):
        x = x0 + (x1 - x0) * i / nx
        verts.append((x, y, terrain_z(x, y)))
for j in range(ny):
    for i in range(nx):
        a = j * (nx + 1) + i
        b = a + 1
        d = (j + 1) * (nx + 1) + i
        c = d + 1
        faces.append((a, b, c, d))
ground = add_mesh_obj("Rolling overgrown village ground", verts, faces, [GROUND], smooth=True)

# Low rolling backdrop to hide the terrain edge naturally.
for idx, (loc, scale) in enumerate([
    ((-11, 30.5, -1.25), (10.5, 5.6, 2.7)),
    ((1.0, 31.7, -1.4), (12.5, 5.3, 2.9)),
    ((12.5, 30.5, -1.35), (9.0, 5.6, 2.6)),
]):
    add_ico("Distant rolling hill %02d" % idx, loc, scale, HILL_MAT,
            subdivisions=2, smooth=True)

# Winding mud lane.
path_verts = []
path_faces = []
samples = 58
for i in range(samples):
    y = -9.0 + 36.0 * i / (samples - 1)
    width = 1.18 + 0.22 * math.sin(y * 0.31 + 0.8)
    cx = path_x(y)
    path_verts.extend([
        (cx - width, y, terrain_z(cx - width, y) + 0.025),
        (cx + width, y, terrain_z(cx + width, y) + 0.025)
    ])
for i in range(samples - 1):
    a = i * 2
    path_faces.append((a, a + 1, a + 3, a + 2))
add_mesh_obj("Narrow muddy village lane", path_verts, path_faces, [PATH_MAT])

# Embedded remnants of old cobbles.
stone_verts = []
stone_faces = []
stone_mats = []
for s in range(118):
    y = random.uniform(-7.8, 23.0)
    cx = path_x(y)
    x = cx + random.uniform(-0.93, 0.93)
    if random.random() < 0.16:
        x += random.choice([-1, 1]) * random.uniform(0.25, 0.55)
    z = terrain_z(x, y) + 0.037
    rx = random.uniform(0.12, 0.30)
    ry = random.uniform(0.10, 0.24)
    h = random.uniform(0.045, 0.105)
    sides = 6
    base = len(stone_verts)
    for k in range(sides):
        ang = math.tau * k / sides
        jitter = random.uniform(0.86, 1.12)
        stone_verts.append((x + math.cos(ang) * rx * jitter,
                            y + math.sin(ang) * ry * jitter, z))
    for k in range(sides):
        ang = math.tau * k / sides
        jitter = random.uniform(0.86, 1.10)
        stone_verts.append((x + math.cos(ang) * rx * jitter,
                            y + math.sin(ang) * ry * jitter, z + h))
    stone_faces.append(tuple(base + sides + k for k in range(sides)))
    stone_mats.append(random.randrange(3))
    for k in range(sides):
        stone_faces.append((
            base + k,
            base + (k + 1) % sides,
            base + sides + (k + 1) % sides,
            base + sides + k
        ))
        stone_mats.append(random.randrange(3))
add_mesh_obj("Scattered sunken cobblestones", stone_verts, stone_faces,
             [STONE1, STONE2, STONE3], stone_mats)

# Puddles on the abandoned lane.
for pi, y in enumerate([-5.2, 0.2, 5.9, 12.1]):
    cx = path_x(y) + random.uniform(-0.35, 0.35)
    cy = y
    ring = []
    count = 13
    center_z = terrain_z(cx, cy) + 0.067
    ring.append((cx, cy, center_z))
    for k in range(count):
        ang = math.tau * k / count
        rr = random.uniform(0.78, 1.15)
        ring.append((
            cx + math.cos(ang) * random.uniform(0.42, 0.84) * rr,
            cy + math.sin(ang) * random.uniform(0.18, 0.38) * rr,
            center_z
        ))
    puddle_faces = []
    for k in range(count):
        puddle_faces.append((0, 1 + k, 1 + (k + 1) % count))
    add_mesh_obj("Rain puddle %02d" % pi, ring, puddle_faces, [WATER])

planned_houses = [
    (-3.7, 3.0, 3.15),
    (4.15, 5.45, 2.75),
    (-5.1, 11.7, 2.45),
    (5.0, 14.6, 2.8),
]

# Thousands of visible blades, combined into one GLB-friendly mesh.
grass_verts = []
grass_faces = []
grass_indices = []
accepted = 0
attempts = 0
while accepted < 1050 and attempts < 10000:
    attempts += 1
    x = random.uniform(-14.5, 14.5)
    y = random.uniform(-8.8, 24.5)
    blocked = False
    for hx, hy, hr in planned_houses:
        if (x - hx) ** 2 + (y - hy) ** 2 < (hr * 0.78) ** 2:
            blocked = True
            break
    if blocked:
        continue
    if (x - 0.1) ** 2 + (y - 9.0) ** 2 < 1.25 ** 2:
        continue
    if abs(x - path_x(y)) < 1.05 and random.random() < 0.72:
        continue

    z = terrain_z(x, y) + 0.025
    for blade in range(random.randint(3, 5)):
        bx = x + random.uniform(-0.11, 0.11)
        by = y + random.uniform(-0.11, 0.11)
        bz = terrain_z(bx, by) + 0.025
        ang = random.uniform(0, math.tau)
        width = random.uniform(0.012, 0.026)
        height = random.uniform(0.16, 0.58)
        if random.random() < 0.08:
            height *= 1.5
        right = Vector((math.cos(ang), math.sin(ang), 0)) * width
        bend = Vector((random.uniform(-0.08, 0.08),
                       random.uniform(-0.08, 0.08), height))
        basev = Vector((bx, by, bz))
        top = basev + bend
        index = len(grass_verts)
        grass_verts.extend([
            tuple(basev - right),
            tuple(basev + right),
            tuple(top + right * 0.15),
            tuple(top - right * 0.15)
        ])
        grass_faces.append((index, index + 1, index + 2, index + 3))
        r = random.random()
        grass_indices.append(0 if r < 0.42 else (1 if r < 0.83 else 2))
    accepted += 1
add_mesh_obj("Wild grass carpet", grass_verts, grass_faces,
             [GRASS1, GRASS2, GRASS_DRY], grass_indices)

# Fallen leaves scattered through the foreground and lane.
leaf_verts = []
leaf_faces = []
leaf_indices = []
for i in range(230):
    x = random.uniform(-10.5, 10.5)
    y = random.uniform(-7.5, 17.5)
    z = terrain_z(x, y) + 0.048
    ang = random.uniform(0, math.tau)
    length = random.uniform(0.055, 0.145)
    width = length * random.uniform(0.35, 0.6)
    a = Vector((math.cos(ang), math.sin(ang), 0)) * length
    b = Vector((-math.sin(ang), math.cos(ang), 0)) * width
    c = Vector((x, y, z))
    index = len(leaf_verts)
    leaf_verts.extend([tuple(c + a), tuple(c + b), tuple(c - a), tuple(c - b)])
    leaf_faces.append((index, index + 1, index + 2, index + 3))
    leaf_indices.append(random.randrange(3))
add_mesh_obj("Fallen leaf litter", leaf_verts, leaf_faces,
             [LEAF_DRY, GRASS_DRY, VINE_BROWN], leaf_indices)

# ---------------------------------------------------------------------
# Architectural detail helpers
# ---------------------------------------------------------------------

def add_front_window(root, d, x, z, width, height, name,
                     broken=True, shutter=False):
    y = -d * 0.5
    add_box(name + " black opening", (x, y - 0.025, z),
            (width + 0.08, 0.04, height + 0.08), DARK, parent=root)
    fy = y - 0.095
    frame = random.choice([WOOD, WOOD_DARK, WOOD_GRAY])
    add_box(name + " left jamb", (x - width * 0.5, fy, z),
            (0.105, 0.11, height + 0.17), frame, parent=root, bevel=0.015)
    add_box(name + " right jamb", (x + width * 0.5, fy, z),
            (0.105, 0.11, height + 0.17), frame, parent=root, bevel=0.015)
    add_box(name + " sill", (x, fy - 0.005, z - height * 0.5),
            (width + 0.21, 0.14, 0.105), frame, parent=root, bevel=0.015)
    add_box(name + " sagging lintel", (x, fy, z + height * 0.5),
            (width + 0.22, 0.12, 0.11), frame,
            rot=(0, 0, random.uniform(-0.035, 0.035)),
            parent=root, bevel=0.015)

    if broken:
        add_box(name + " broken lower mullion",
                (x + random.uniform(-0.06, 0.06), fy - 0.015,
                 z - height * 0.22),
                (0.07, 0.09, height * 0.48), frame,
                rot=(0, 0, random.uniform(-0.11, 0.11)),
                parent=root, bevel=0.01)
        add_box(name + " broken cross piece",
                (x - width * 0.24, fy - 0.02, z + 0.04),
                (width * 0.42, 0.085, 0.065), frame,
                rot=(0, 0, random.uniform(-0.05, 0.05)),
                parent=root, bevel=0.008)

        gy = fy - 0.075
        glass_verts = [
            (x - width * 0.43, gy, z + height * 0.40),
            (x - width * 0.03, gy, z + height * 0.40),
            (x - width * 0.35, gy, z + height * 0.06),
            (x + width * 0.45, gy, z - height * 0.40),
            (x + width * 0.08, gy, z - height * 0.36),
            (x + width * 0.38, gy, z - height * 0.05),
        ]
        add_mesh_obj(name + " surviving glass shards", glass_verts,
                     [(0, 1, 2), (3, 4, 5)], [GLASS], parent=root)
    else:
        add_box(name + " center mullion", (x, fy - 0.015, z),
                (0.065, 0.08, height), frame, parent=root, bevel=0.008)
        add_box(name + " center crossbar", (x, fy - 0.015, z),
                (width, 0.08, 0.065), frame, parent=root, bevel=0.008)

    add_ico(name + " mossy sill", (x - width * 0.18, fy - 0.045,
                                   z - height * 0.5 + 0.065),
            (width * 0.25, 0.055, 0.065),
            random.choice([MOSS, MOSS_LIGHT]), root, subdivisions=1)

    if shutter:
        hinge = make_empty(name + " shutter hinge",
                           (x - width * 0.5 - 0.03, fy - 0.03, z),
                           parent=root)
        hinge.rotation_euler[2] = math.radians(-58)
        sw = width * 0.52
        add_box(name + " hanging shutter", (sw * 0.5, 0, 0),
                (sw, 0.075, height * 0.96), WOOD_DARK,
                parent=hinge, bevel=0.018)
        add_box(name + " shutter brace 1", (sw * 0.5, -0.045, height * 0.26),
                (sw * 0.88, 0.035, 0.08), WOOD,
                parent=hinge, bevel=0.008)
        add_box(name + " shutter brace 2", (sw * 0.5, -0.045, -height * 0.23),
                (sw * 0.88, 0.035, 0.08), WOOD,
                parent=hinge, bevel=0.008)

def add_side_window(root, w_house, y, z, width, height, name, broken=True):
    x = w_house * 0.5
    add_box(name + " black opening", (x + 0.025, y, z),
            (0.04, width + 0.08, height + 0.08), DARK, parent=root)
    fx = x + 0.095
    frame = random.choice([WOOD, WOOD_DARK, WOOD_GRAY])
    add_box(name + " near jamb", (fx, y - width * 0.5, z),
            (0.11, 0.105, height + 0.16), frame, parent=root, bevel=0.014)
    add_box(name + " far jamb", (fx, y + width * 0.5, z),
            (0.11, 0.105, height + 0.16), frame, parent=root, bevel=0.014)
    add_box(name + " side sill", (fx, y, z - height * 0.5),
            (0.14, width + 0.2, 0.105), frame, parent=root, bevel=0.014)
    add_box(name + " side lintel", (fx, y, z + height * 0.5),
            (0.13, width + 0.2, 0.105), frame, parent=root, bevel=0.014)
    if broken:
        add_box(name + " crooked side mullion", (fx + 0.01, y, z - 0.12),
                (0.08, 0.07, height * 0.68), frame,
                rot=(random.uniform(-0.08, 0.08), 0, 0),
                parent=root, bevel=0.008)
    else:
        add_box(name + " side crossbar", (fx + 0.01, y, z),
                (0.08, width, 0.065), frame, parent=root, bevel=0.008)

def add_back_window(root, d, x, z, width, height, name):
    y = d * 0.5
    add_box(name + " dark recess", (x, y + 0.025, z),
            (width, 0.04, height), DARK, parent=root)
    fy = y + 0.09
    for xx in [x - width * 0.5, x + width * 0.5]:
        add_box(name + " back jamb", (xx, fy, z),
                (0.095, 0.11, height + 0.12), WOOD_GRAY,
                parent=root, bevel=0.012)
    for zz in [z - height * 0.5, z + height * 0.5]:
        add_box(name + " back rail", (x, fy, zz),
                (width + 0.17, 0.11, 0.095), WOOD_GRAY,
                parent=root, bevel=0.012)

def add_front_door(root, d, x, name, open_angle=-28):
    y = -d * 0.5
    width = 1.12
    height = 2.24
    z = 0.25 + height * 0.5
    add_box(name + " pitch black doorway", (x, y - 0.025, z),
            (width + 0.12, 0.045, height + 0.1), DARK, parent=root)
    fy = y - 0.10
    add_box(name + " door left post", (x - width * 0.5, fy, z),
            (0.14, 0.15, height + 0.2), WOOD_DARK,
            parent=root, bevel=0.02)
    add_box(name + " door right post", (x + width * 0.5, fy, z),
            (0.14, 0.15, height + 0.2), WOOD_DARK,
            parent=root, bevel=0.02)
    add_box(name + " bowed door lintel", (x, fy, z + height * 0.5),
            (width + 0.25, 0.16, 0.15), WOOD_DARK,
            rot=(0, 0, math.radians(random.uniform(-2.0, 2.0))),
            parent=root, bevel=0.02)

    hinge = make_empty(name + " open door hinge",
                       (x - width * 0.5, y - 0.14, z), parent=root)
    hinge.rotation_euler[2] = math.radians(open_angle)
    add_box(name + " warped open door", (width * 0.5, 0, 0),
            (width, 0.105, height), WOOD, parent=hinge, bevel=0.022)
    for p in range(1, 5):
        px = width * p / 5
        add_box(name + " plank gap %02d" % p, (px, -0.06, 0),
                (0.024, 0.012, height * 0.94), WOOD_DARK,
                parent=hinge)
    add_box(name + " diagonal door brace", (width * 0.5, -0.07, 0),
            (width * 0.92, 0.045, 0.09), WOOD_DARK,
            rot=(0, 0, math.radians(28)), parent=hinge, bevel=0.008)
    add_ico(name + " rusty handle", (width * 0.86, -0.105, 0.02),
            (0.055, 0.035, 0.055), RUST_DARK, hinge, subdivisions=1)

def add_barn_doors(root, d, name):
    y = -d * 0.5
    z = 1.46
    add_box(name + " barn darkness", (0, y - 0.025, z),
            (2.65, 0.045, 2.52), DARK, parent=root)
    fy = y - 0.105
    add_box(name + " barn opening left post", (-1.35, fy, z),
            (0.18, 0.16, 2.72), WOOD_DARK, parent=root, bevel=0.022)
    add_box(name + " barn opening right post", (1.35, fy, z),
            (0.18, 0.16, 2.72), WOOD_DARK, parent=root, bevel=0.022)
    add_box(name + " barn lintel", (0, fy, 2.76),
            (2.88, 0.18, 0.18), WOOD_DARK,
            rot=(0, 0, math.radians(-1.7)), parent=root, bevel=0.022)

    left = add_box(name + " left barn door", (-0.70, fy - 0.06, z),
                   (1.25, 0.10, 2.40), WOOD, parent=root, bevel=0.018)
    left.rotation_euler[2] = math.radians(-4)
    right = add_box(name + " hanging barn door", (1.06, fy - 0.13, 1.30),
                    (0.92, 0.10, 2.20), WOOD_DARK,
                    rot=(0, 0, math.radians(11)), parent=root, bevel=0.018)
    for xx in [-1.05, -0.7, -0.35]:
        add_box(name + " barn plank seam", (xx, fy - 0.12, z),
                (0.025, 0.018, 2.25), WOOD_DARK, parent=root)

def add_brick_patch_front(root, d, cx, cz, sx, sz, name):
    y = -d * 0.5 - 0.05
    outline = [
        (cx - sx * 0.48, y, cz - sz * 0.16),
        (cx - sx * 0.36, y, cz - sz * 0.48),
        (cx + sx * 0.08, y, cz - sz * 0.52),
        (cx + sx * 0.50, y, cz - sz * 0.20),
        (cx + sx * 0.40, y, cz + sz * 0.34),
        (cx + sx * 0.05, y, cz + sz * 0.52),
        (cx - sx * 0.45, y, cz + sz * 0.27),
    ]
    add_mesh_obj(name + " missing plaster", outline,
                 [tuple(range(len(outline)))], [MORTAR], parent=root)
    bw = sx / 4.0
    bh = sz / 6.0
    for row in range(5):
        for col in range(3):
            bx = cx + (col - 1) * bw + (0.5 * bw if row % 2 else 0)
            bz = cz + (row - 2) * bh
            if ((bx - cx) / (sx * 0.52)) ** 2 + ((bz - cz) / (sz * 0.55)) ** 2 < 0.9:
                add_box(name + " exposed brick", (bx, y - 0.035, bz),
                        (bw * 0.82, 0.055, bh * 0.58),
                        random.choice([BRICK, BRICK2]), parent=root,
                        bevel=0.01)

def add_brick_patch_side(root, w, cy, cz, sy, sz, name):
    x = w * 0.5 + 0.05
    outline = [
        (x, cy - sy * 0.48, cz - sz * 0.20),
        (x, cy - sy * 0.36, cz - sz * 0.50),
        (x, cy + sy * 0.18, cz - sz * 0.48),
        (x, cy + sy * 0.50, cz - sz * 0.12),
        (x, cy + sy * 0.37, cz + sz * 0.43),
        (x, cy - sy * 0.10, cz + sz * 0.51),
        (x, cy - sy * 0.46, cz + sz * 0.22),
    ]
    add_mesh_obj(name + " missing side plaster", outline,
                 [tuple(range(len(outline)))], [MORTAR], parent=root)
    bw = sy / 4.0
    bh = sz / 6.0
    for row in range(5):
        for col in range(3):
            by = cy + (col - 1) * bw + (0.5 * bw if row % 2 else 0)
            bz = cz + (row - 2) * bh
            if ((by - cy) / (sy * 0.54)) ** 2 + ((bz - cz) / (sz * 0.55)) ** 2 < 0.92:
                add_box(name + " exposed side brick", (x + 0.035, by, bz),
                        (0.055, bw * 0.82, bh * 0.58),
                        random.choice([BRICK, BRICK2]), parent=root,
                        bevel=0.01)

def add_window_sprout(root, d, x, z, name):
    y = -d * 0.5 - 0.10
    points = [
        (x, y, z - 0.16),
        (x + 0.12, y - 0.35, z + 0.02),
        (x - 0.05, y - 0.68, z + 0.28),
        (x + 0.28, y - 0.92, z + 0.53),
    ]
    create_curve_mesh(name + " branch through window", points, 0.025,
                      VINE_BROWN, root)
    branch = [
        points[2],
        (x - 0.35, y - 0.77, z + 0.42),
        (x - 0.52, y - 0.86, z + 0.56)
    ]
    create_curve_mesh(name + " window branch fork", branch, 0.015,
                      VINE_STEM, root)
    centers = [
        points[1], points[2], points[3],
        branch[1], branch[2],
        (x + 0.10, y - 0.72, z + 0.37),
        (x + 0.36, y - 0.86, z + 0.48),
    ]
    create_leaf_mesh(name + " leaves through window", centers, 'front',
                     [LEAF1, LEAF2], root, (0.13, 0.24))

def add_ivy_front(root, w, d, eave, roof_h, x0, name, density=1.0):
    y = -d * 0.5 - 0.135
    n = 11
    max_z = eave + roof_h * 0.56
    main = []
    for i in range(n):
        t = i / (n - 1)
        z = 0.08 + t * (max_z - 0.08)
        x = x0 * (1.0 - 0.50 * t) + 0.19 * math.sin(t * 11.5 + x0)
        main.append((x, y, z))
    create_curve_mesh(name + " main ivy trunk", main, 0.027,
                      VINE_BROWN, root)

    leaves = []
    for i in range(1, n):
        p = Vector(main[i])
        leaves.append(tuple(p + Vector((random.uniform(-0.12, 0.12),
                                        -0.02,
                                        random.uniform(-0.10, 0.12)))))
        if random.random() < 0.75 * density:
            leaves.append(tuple(p + Vector((random.uniform(-0.18, 0.18),
                                            -0.025,
                                            random.uniform(-0.08, 0.15)))))

    branch_ids = [2, 4, 6, 8]
    for bi, idx in enumerate(branch_ids):
        start = Vector(main[idx])
        sign = -1 if bi % 2 == 0 else 1
        reach = random.uniform(0.45, 0.82) * density
        branch = [
            tuple(start),
            tuple(start + Vector((sign * reach * 0.55, -0.01, 0.18))),
            tuple(start + Vector((sign * reach, -0.015, 0.34)))
        ]
        create_curve_mesh(name + " ivy branch %02d" % bi, branch, 0.014,
                          VINE_STEM, root)
        leaves.extend([
            branch[1], branch[2],
            tuple(Vector(branch[2]) + Vector((sign * 0.12, -0.02, -0.08)))
        ])
    create_leaf_mesh(name + " ivy leaves", leaves, 'front',
                     [LEAF_DARK, LEAF1, LEAF2], root,
                     (0.11, 0.22))

def add_ivy_side(root, w, d, eave, y0, name):
    x = w * 0.5 + 0.135
    main = []
    for i in range(9):
        t = i / 8
        y = y0 * (1.0 - 0.42 * t) + 0.16 * math.sin(t * 10.0)
        z = 0.07 + t * (eave - 0.15)
        main.append((x, y, z))
    create_curve_mesh(name + " side ivy trunk", main, 0.025,
                      VINE_BROWN, root)
    leaves = []
    for i in range(1, 9):
        leaves.extend([
            main[i],
            (x + 0.025, main[i][1] + random.uniform(-0.20, 0.20),
             main[i][2] + random.uniform(-0.08, 0.12))
        ])
    for bi, idx in enumerate([2, 4, 6]):
        start = Vector(main[idx])
        sign = -1 if bi % 2 == 0 else 1
        branch = [
            tuple(start),
            tuple(start + Vector((0.01, sign * 0.38, 0.16))),
            tuple(start + Vector((0.015, sign * 0.69, 0.29)))
        ]
        create_curve_mesh(name + " side ivy fork %02d" % bi, branch,
                          0.013, VINE_STEM, root)
        leaves.extend([branch[1], branch[2]])
    create_leaf_mesh(name + " side ivy leaves", leaves, 'side',
                     [LEAF_DARK, LEAF1, LEAF2], root,
                     (0.11, 0.21))

def create_loose_sheet(name, parent, loc, size, rot):
    sx, sy = size
    nu, nv = 11, 18
    verts = []
    faces = []
    for j in range(nv + 1):
        v = j / nv
        yy = -sy * 0.5 + sy * v
        corr = 0.025 * math.sin(v * math.tau * 8)
        for i in range(nu + 1):
            u = i / nu
            xx = -sx * 0.5 + sx * u
            verts.append((xx, yy, corr))
    for j in range(nv):
        for i in range(nu):
            a = j * (nu + 1) + i
            faces.append((a, a + 1, a + nu + 2, a + nu + 1))
    obj = add_mesh_obj(name, verts, faces, [random.choice(ROOF_MATS)],
                       parent=parent, smooth=True)
    obj.location = loc
    obj.rotation_euler = rot
    solid = obj.modifiers.new("Ragged sheet thickness", 'SOLIDIFY')
    solid.thickness = 0.025
    return obj

def roof_holes(side, variant):
    if variant == 0:
        return ([(0.48, 0.32, 0.15, 0.085),
                 (0.73, 0.68, 0.09, 0.06)]
                if side == 1 else [(0.68, 0.56, 0.08, 0.05)])
    if variant == 1:
        return ([(0.39, 0.42, 0.21, 0.12),
                 (0.80, 0.17, 0.16, 0.10)]
                if side == 1 else [(0.60, 0.64, 0.12, 0.07)])
    if variant == 2:
        return [(0.73, 0.42, 0.10, 0.06)] if side == 1 else []
    return [(0.64, 0.32, 0.14, 0.08)] if side == 1 else []

def create_corrugated_roof(root, w, d, eave, rh, variant, name):
    span = w * 0.5 + 0.30
    ymin = -d * 0.5 - 0.32
    ymax = d * 0.5 + 0.32
    length = ymax - ymin
    nu = 17
    nv = max(48, int(length / 0.07))
    waves = max(13, int(length / 0.235))

    for side in [-1, 1]:
        holes = roof_holes(side, variant)
        verts = []
        faces = []
        indices = []
        panel_count = max(4, int(round(length / 0.78)))
        panel_assign = [random.randrange(len(ROOF_MATS))
                        for _ in range(panel_count)]
        for j in range(nv + 1):
            v = j / nv
            y = ymin + v * length
            corr = 0.034 * math.sin(v * math.tau * waves)
            for i in range(nu + 1):
                u = i / nu
                x = side * span * u
                z = eave + rh * (1.0 - u) + corr
                verts.append((x, y, z))
        for j in range(nv):
            vc = (j + 0.5) / nv
            for i in range(nu):
                uc = (i + 0.5) / nu
                missing = False
                for hu, hv, hru, hrv in holes:
                    if ((uc - hu) / hru) ** 2 + ((vc - hv) / hrv) ** 2 < 1.0:
                        missing = True
                        break
                if missing:
                    continue
                a = j * (nu + 1) + i
                b = a + 1
                d0 = (j + 1) * (nu + 1) + i
                c = d0 + 1
                if side == 1:
                    faces.append((a, b, c, d0))
                else:
                    faces.append((a, d0, c, b))
                panel = min(panel_count - 1, int(vc * panel_count))
                indices.append(panel_assign[panel])

        roof = add_mesh_obj(
            name + (" right corroded roof" if side == 1 else " left corroded roof"),
            verts, faces, ROOF_MATS, indices, root, smooth=True
        )
        solid = roof.modifiers.new("Thin torn metal edges", 'SOLIDIFY')
        solid.thickness = 0.025
        solid.offset = 0.0

        # Rafters visible beneath holes and at the open front edge.
        rafter_ys = [ymin + 0.06]
        for hole in holes:
            rafter_ys.extend([
                ymin + (hole[1] - hole[3] * 0.35) * length,
                ymin + (hole[1] + hole[3] * 0.35) * length
            ])
        for ri, ry in enumerate(rafter_ys):
            add_beam_between(
                name + " exposed rafter %d %d" % (side, ri),
                (side * 0.035, ry, eave + rh - 0.12),
                (side * (span - 0.07), ry, eave - 0.10),
                0.095, WOOD_DARK, root, bevel=0.01
            )

    add_cone_between(name + " rusted ridge cap",
                     (0, ymin, eave + rh + 0.045),
                     (0, ymax, eave + rh + 0.045),
                     0.095, 0.095, RUST_DARK, root,
                     vertices=12, smooth=True)

    # Broken guttering and downpipe.
    add_cone_between(name + " broken right gutter front",
                     (span + 0.02, ymin, eave - 0.03),
                     (span + 0.02, -0.12, eave - 0.03),
                     0.065, 0.065, RUST_DARK, root,
                     vertices=10, smooth=True)
    add_cone_between(name + " broken right gutter rear",
                     (span + 0.02, 0.26, eave - 0.05),
                     (span + 0.02, ymax, eave - 0.05),
                     0.065, 0.055, RUST_DARK, root,
                     vertices=10, smooth=True)
    add_cone_between(name + " left gutter",
                     (-span - 0.02, ymin, eave - 0.04),
                     (-span - 0.02, ymax * 0.65, eave - 0.04),
                     0.055, 0.055, RUST_DARK, root,
                     vertices=10, smooth=True)
    downpipe = [
        (span + 0.02, ymin + 0.04, eave - 0.02),
        (span + 0.06, ymin + 0.02, eave - 1.05),
        (span - 0.01, ymin - 0.02, eave - 2.00),
        (span + 0.19, ymin - 0.17, eave - 2.48),
    ]
    create_curve_mesh(name + " bent downpipe", downpipe, 0.052,
                      RUST_DARK, root)

    # Moss spread across both roof slopes.
    moss_count = 15 if variant in (0, 3) else 11
    for mi in range(moss_count):
        side = 1 if random.random() < 0.66 else -1
        u = random.uniform(0.28, 0.97)
        y = random.uniform(ymin + 0.12, ymax - 0.12)
        x = side * span * u
        z = eave + rh * (1.0 - u) + 0.065
        slope_angle = side * math.atan(rh / span)
        add_ico(
            name + " rooftop moss %02d" % mi,
            (x, y, z),
            (random.uniform(0.22, 0.63),
             random.uniform(0.16, 0.48),
             random.uniform(0.035, 0.075)),
            random.choice([MOSS, MOSS, MOSS_LIGHT]),
            root, subdivisions=2,
            rot=(0, slope_angle, random.uniform(-0.35, 0.35))
        )

    # Grass rooted in seams and blocked gutters.
    weed_verts = []
    weed_faces = []
    weed_indices = []
    for wi in range(15 if variant == 0 else 10):
        side = 1 if random.random() < 0.75 else -1
        u = random.uniform(0.72, 0.97)
        y = random.uniform(ymin + 0.12, ymax - 0.12)
        x = side * span * u
        z = eave + rh * (1.0 - u) + 0.07
        for blade in range(2):
            ang = random.uniform(0, math.tau)
            right = Vector((math.cos(ang), math.sin(ang), 0)) * 0.014
            base = Vector((x + random.uniform(-0.06, 0.06),
                           y + random.uniform(-0.06, 0.06), z))
            top = base + Vector((random.uniform(-0.05, 0.05),
                                 random.uniform(-0.05, 0.05),
                                 random.uniform(0.20, 0.48)))
            idx = len(weed_verts)
            weed_verts.extend([
                tuple(base - right), tuple(base + right),
                tuple(top + right * 0.12), tuple(top - right * 0.12)
            ])
            weed_faces.append((idx, idx + 1, idx + 2, idx + 3))
            weed_indices.append(random.choice([0, 0, 1]))
    add_mesh_obj(name + " plants growing from roof",
                 weed_verts, weed_faces,
                 [GRASS2, GRASS_DRY], weed_indices, root)

def add_brick_chimney(root, w, d, eave, rh, name):
    span = w * 0.5 + 0.30
    x = 0.72
    y = d * 0.15
    u = abs(x) / span
    roof_z = eave + rh * (1.0 - u)
    add_box(name + " crumbling brick chimney",
            (x, y, roof_z + 0.64),
            (0.58, 0.62, 1.34), BRICK, parent=root, bevel=0.025)
    for i in range(4):
        add_box(name + " chimney mortar band %02d" % i,
                (x, y, roof_z + 0.18 + i * 0.29),
                (0.605, 0.645, 0.035), MORTAR, parent=root)
    add_box(name + " soot-filled chimney mouth",
            (x, y, roof_z + 1.325),
            (0.43, 0.46, 0.025), DARK, parent=root)
    add_box(name + " broken chimney brick",
            (x + 0.17, y - 0.13, roof_z + 1.42),
            (0.24, 0.30, 0.20), BRICK2,
            rot=(0.05, 0.03, 0.09), parent=root, bevel=0.015)
    add_ico(name + " chimney base moss", (x, y, roof_z + 0.04),
            (0.38, 0.36, 0.07), MOSS, root, subdivisions=2)

def add_metal_stovepipe(root, w, d, eave, rh, name):
    span = w * 0.5 + 0.30
    x = -0.62
    y = 0.55
    u = abs(x) / span
    roof_z = eave + rh * (1.0 - u)
    points = [
        (x, y, roof_z - 0.04),
        (x, y, roof_z + 1.05),
        (x + 0.18, y, roof_z + 1.18),
    ]
    create_curve_mesh(name + " bent rusted stovepipe", points, 0.105,
                      RUST_DARK, root, bevel_resolution=2)
    add_cone_between(name + " stovepipe cap",
                     (x + 0.02, y, roof_z + 1.25),
                     (x + 0.33, y, roof_z + 1.25),
                     0.17, 0.11, RUST1, root, vertices=10, smooth=True)

# ---------------------------------------------------------------------
# House construction
# ---------------------------------------------------------------------

def create_house(name, loc, rot_z, w, d, h, rh, variant):
    root = make_empty(name + " ROOT",
                      (loc[0], loc[1], terrain_z(loc[0], loc[1]) + 0.025),
                      (0, 0, rot_z))
    base = 0.24
    eave = base + h
    wall_mat = [WALL1, WALL2, WALL_GREEN, WALL2][variant]

    add_box(name + " solid weathered shell",
            (0, 0, base + h * 0.5), (w, d, h),
            wall_mat, parent=root, bevel=0.045)

    # Triangular gable walls.
    gable_verts = [
        (-w * 0.5, -d * 0.5, eave),
        (w * 0.5, -d * 0.5, eave),
        (0, -d * 0.5, eave + rh),
        (w * 0.5, d * 0.5, eave),
        (-w * 0.5, d * 0.5, eave),
        (0, d * 0.5, eave + rh),
    ]
    add_mesh_obj(name + " plaster gables", gable_verts,
                 [(0, 1, 2), (3, 4, 5)], [wall_mat], parent=root)

    # Dark ceiling makes the roof holes visibly hollow.
    add_box(name + " dark exposed attic",
            (0, 0, eave + 0.02),
            (w - 0.22, d - 0.22, 0.07), DARK, parent=root)

    # Rotten front gable timbers.
    front_y = -d * 0.5 - 0.075
    add_beam_between(name + " left gable fascia",
                     (-w * 0.5, front_y, eave),
                     (0, front_y, eave + rh),
                     0.105, WOOD_DARK, root)
    add_beam_between(name + " right gable fascia",
                     (0, front_y, eave + rh),
                     (w * 0.5, front_y, eave),
                     0.105, WOOD_DARK, root)
    add_beam_between(name + " warped eave beam",
                     (-w * 0.5, front_y, eave),
                     (w * 0.5, front_y, eave),
                     0.12, WOOD_DARK, root)

    create_corrugated_roof(root, w, d, eave, rh, variant, name)

    # Irregular stone foundation on all sides.
    nfront = max(6, int(w / 0.62))
    nside = max(5, int(d / 0.62))
    for side_y in [-1, 1]:
        for i in range(nfront):
            if random.random() < 0.08:
                continue
            x = -w * 0.5 + (i + 0.5) * w / nfront
            y = side_y * (d * 0.5 + 0.035)
            add_ico(
                name + " foundation stone F",
                (x + random.uniform(-0.035, 0.035),
                 y + random.uniform(-0.025, 0.025),
                 0.17 + random.uniform(-0.02, 0.02)),
                (random.uniform(0.25, 0.34),
                 random.uniform(0.17, 0.23),
                 random.uniform(0.13, 0.19)),
                random.choice([STONE1, STONE1, STONE2, STONE3]),
                root, subdivisions=1,
                rot=(random.uniform(-0.15, 0.15),
                     random.uniform(-0.15, 0.15),
                     random.uniform(0, math.tau))
            )
    for side_x in [-1, 1]:
        for i in range(nside):
            if random.random() < 0.09:
                continue
            y = -d * 0.5 + (i + 0.5) * d / nside
            x = side_x * (w * 0.5 + 0.035)
            add_ico(
                name + " foundation stone S",
                (x + random.uniform(-0.025, 0.025),
                 y + random.uniform(-0.035, 0.035),
                 0.17 + random.uniform(-0.02, 0.02)),
                (random.uniform(0.17, 0.23),
                 random.uniform(0.25, 0.34),
                 random.uniform(0.13, 0.19)),
                random.choice([STONE1, STONE1, STONE2, STONE3]),
                root, subdivisions=1,
                rot=(random.uniform(-0.15, 0.15),
                     random.uniform(-0.15, 0.15),
                     random.uniform(0, math.tau))
            )

    # Dampness and moss spreading up the wall bases.
    for i in range(7):
        x = random.uniform(-w * 0.43, w * 0.43)
        z = random.uniform(0.30, 0.83)
        add_ico(name + " front wall moss %02d" % i,
                (x, -d * 0.5 - 0.035, z),
                (random.uniform(0.22, 0.60), 0.035,
                 random.uniform(0.11, 0.32)),
                random.choice([DAMP, MOSS, MOSS_LIGHT]),
                root, subdivisions=1)
    for i in range(5):
        y = random.uniform(-d * 0.42, d * 0.42)
        z = random.uniform(0.35, 1.05)
        add_ico(name + " side wall moss %02d" % i,
                (w * 0.5 + 0.035, y, z),
                (0.035, random.uniform(0.22, 0.58),
                 random.uniform(0.12, 0.36)),
                random.choice([DAMP, MOSS]),
                root, subdivisions=1)
    for i in range(3):
        x = random.uniform(-w * 0.35, w * 0.35)
        add_ico(name + " dark rain streak %02d" % i,
                (x, -d * 0.5 - 0.025, random.uniform(1.9, eave - 0.3)),
                (random.uniform(0.08, 0.15), 0.022,
                 random.uniform(0.32, 0.60)),
                DAMP, root, subdivisions=1)

    # Ground moss at the corners.
    for i in range(5):
        ang = random.uniform(0, math.tau)
        xx = math.cos(ang) * random.uniform(w * 0.45, w * 0.68)
        yy = math.sin(ang) * random.uniform(d * 0.45, d * 0.68)
        add_ico(name + " foundation ground moss %02d" % i,
                (xx, yy, 0.035),
                (random.uniform(0.32, 0.80),
                 random.uniform(0.26, 0.65),
                 random.uniform(0.025, 0.055)),
                random.choice([MOSS, MOSS_LIGHT]), root,
                subdivisions=2)

    # Cracks in the plaster.
    crack_x = random.uniform(-w * 0.3, w * 0.3)
    crack_points = [
        (crack_x, -d * 0.5 - 0.074, eave - 0.22),
        (crack_x - 0.10, -d * 0.5 - 0.076, eave - 0.73),
        (crack_x + 0.04, -d * 0.5 - 0.077, eave - 1.16),
        (crack_x - 0.16, -d * 0.5 - 0.078, eave - 1.65),
    ]
    create_curve_mesh(name + " main plaster crack", crack_points,
                      0.012, CRACK, root)
    fork = [
        crack_points[2],
        (crack_x + 0.34, -d * 0.5 - 0.078, eave - 1.35),
        (crack_x + 0.48, -d * 0.5 - 0.078, eave - 1.62)
    ]
    create_curve_mesh(name + " plaster crack fork", fork,
                      0.009, CRACK, root)

    if variant == 0:
        add_front_door(root, d, 1.22, name + " front")
        add_front_window(root, d, -1.20, 1.76, 1.30, 1.37,
                         name + " broken front window",
                         broken=True, shutter=True)
        add_front_window(root, d, 0.0, eave + rh * 0.38, 0.76, 0.68,
                         name + " shattered attic window",
                         broken=True, shutter=False)
        add_side_window(root, w, -0.42, 1.74, 1.20, 1.28,
                        name + " side window", True)
        add_back_window(root, d, -0.65, 1.63, 1.05, 1.12,
                        name + " rear window")
        add_window_sprout(root, d, -1.20, 1.76,
                          name + " living branch")
        add_brick_patch_side(root, w, 0.94, 1.48, 1.28, 1.22,
                             name + " exposed side bricks")
        add_ivy_front(root, w, d, eave, rh, -2.05,
                      name + " facade", 1.15)
        add_ivy_side(root, w, d, eave, 1.20,
                     name + " right wall")
        create_loose_sheet(name + " fallen roof sheet", root,
                           (w * 0.5 + 0.58, 1.17, 0.64),
                           (1.15, 1.52),
                           (0.10, math.radians(67), 0.14))
        add_brick_chimney(root, w, d, eave, rh, name)

    elif variant == 1:
        add_front_door(root, d, -1.05, name + " front", open_angle=-20)
        add_front_window(root, d, 0.86, 1.64, 1.18, 1.26,
                         name + " broken front window",
                         broken=True, shutter=False)
        add_front_window(root, d, 0.0, eave + rh * 0.36, 0.65, 0.58,
                         name + " loft hole", broken=True)
        add_side_window(root, w, 0.36, 1.64, 1.12, 1.22,
                        name + " broken side window", True)
        add_back_window(root, d, 0.55, 1.55, 0.92, 1.05,
                        name + " rear window")
        add_window_sprout(root, d, 0.86, 1.64,
                          name + " shrub through window")
        add_brick_patch_front(root, d, 1.72, 0.98, 1.02, 0.95,
                              name + " front exposed bricks")
        add_ivy_front(root, w, d, eave, rh, 1.82,
                      name + " facade", 1.0)
        add_ivy_side(root, w, d, eave, -1.12,
                     name + " side")
        create_loose_sheet(name + " hanging collapsed roof panel", root,
                           (0.70, -d * 0.5 - 0.48, eave - 0.32),
                           (1.10, 1.25),
                           (math.radians(48), 0.05, -0.10))
        add_metal_stovepipe(root, w, d, eave, rh, name)

    elif variant == 2:
        add_front_door(root, d, 0.72, name + " front", open_angle=-18)
        add_front_window(root, d, -0.98, 1.49, 1.03, 1.13,
                         name + " front window", True, False)
        add_side_window(root, w, -0.20, 1.48, 0.92, 1.06,
                        name + " side window", True)
        add_back_window(root, d, 0.45, 1.40, 0.88, 0.98,
                        name + " back window")
        add_brick_patch_front(root, d, -1.62, 0.86, 0.74, 0.72,
                              name + " crumbled plaster")
        add_ivy_front(root, w, d, eave, rh, -1.50,
                      name + " facade", 0.86)
        add_brick_chimney(root, w, d, eave, rh, name)

    else:
        add_barn_doors(root, d, name)
        add_front_window(root, d, 0.0, eave + rh * 0.37, 0.88, 0.66,
                         name + " empty hayloft", True, False)
        add_side_window(root, w, -0.35, 1.62, 1.12, 1.24,
                        name + " barn side window", True)
        add_back_window(root, d, -0.75, 1.56, 1.02, 1.10,
                        name + " barn rear window")
        add_brick_patch_side(root, w, 0.86, 1.05, 1.12, 0.98,
                             name + " barn side brick patch")
        add_ivy_front(root, w, d, eave, rh, 2.02,
                      name + " barn facade", 1.04)
        add_ivy_side(root, w, d, eave, 1.12,
                     name + " barn side")
    return root

hero_house = create_house(
    "Forsaken Cottage", (-3.7, 3.0), math.radians(5),
    5.25, 4.20, 3.60, 2.12, 0
)
right_house = create_house(
    "Collapsed Lane Cottage", (4.15, 5.45), math.radians(18),
    4.55, 3.78, 3.18, 1.72, 1
)
back_house = create_house(
    "Distant Sage Cottage", (-5.10, 11.70), math.radians(-6),
    4.05, 3.22, 2.84, 1.52, 2
)
barn_house = create_house(
    "Overgrown Village Barn", (5.0, 14.60), math.radians(11),
    5.05, 3.72, 3.12, 1.48, 3
)

# ---------------------------------------------------------------------
# Nature: ferns, shrubs, trees, and seed heads
# ---------------------------------------------------------------------

def create_fern(name, loc, scale=1.0):
    root = make_empty(name + " ROOT",
                      (loc[0], loc[1], terrain_z(loc[0], loc[1]) + 0.02))
    leaf_verts = []
    leaf_faces = []
    for frond in range(6):
        ang = math.tau * frond / 6 + random.uniform(-0.18, 0.18)
        radius = random.uniform(0.42, 0.68) * scale
        points = [
            (0, 0, 0.03),
            (math.cos(ang) * radius * 0.38,
             math.sin(ang) * radius * 0.38, 0.28 * scale),
            (math.cos(ang) * radius * 0.72,
             math.sin(ang) * radius * 0.72, 0.39 * scale),
            (math.cos(ang) * radius,
             math.sin(ang) * radius, 0.30 * scale),
        ]
        create_curve_mesh(name + " frond stem %02d" % frond,
                          points, 0.009 * scale, VINE_STEM, root)
        direction = Vector((math.cos(ang), math.sin(ang), 0))
        lateral = Vector((-math.sin(ang), math.cos(ang), 0))
        for j in range(2, 8):
            t = j / 8.0
            center = direction * radius * t
            center.z = (0.12 + 0.30 * math.sin(t * math.pi)) * scale
            leaflet_len = (0.15 * (1.0 - t * 0.55) + 0.035) * scale
            base_width = 0.035 * scale
            for sign in [-1, 1]:
                idx = len(leaf_verts)
                tip = center + lateral * sign * leaflet_len + direction * 0.025
                leaf_verts.extend([
                    tuple(center - direction * base_width),
                    tuple(tip),
                    tuple(center + direction * base_width),
                ])
                leaf_faces.append((idx, idx + 1, idx + 2))
    add_mesh_obj(name + " fern leaflets", leaf_verts, leaf_faces,
                 [FERN_MAT], parent=root)
    return root

def create_shrub(name, loc, size=1.0):
    root = make_empty(name + " ROOT",
                      (loc[0], loc[1], terrain_z(loc[0], loc[1]) + 0.01))
    tips = []
    for i in range(7):
        ang = math.tau * i / 7 + random.uniform(-0.32, 0.32)
        r = random.uniform(0.45, 0.78) * size
        tip = (math.cos(ang) * r, math.sin(ang) * r,
               random.uniform(0.72, 1.25) * size)
        mid = (math.cos(ang) * r * 0.45,
               math.sin(ang) * r * 0.45,
               tip[2] * 0.53)
        add_cone_between(name + " shrub branch A", (0, 0, 0.04),
                         mid, 0.035 * size, 0.022 * size,
                         VINE_BROWN, root, vertices=7)
        add_cone_between(name + " shrub branch B", mid, tip,
                         0.025 * size, 0.010 * size,
                         VINE_BROWN, root, vertices=7)
        tips.append(Vector(tip))
    for i, tip in enumerate(tips):
        for j in range(random.randint(1, 2)):
            offset = Vector((random.uniform(-0.22, 0.22),
                             random.uniform(-0.22, 0.22),
                             random.uniform(-0.12, 0.16))) * size
            add_ico(name + " shrub foliage %02d" % (i * 2 + j),
                    tuple(tip + offset),
                    (random.uniform(0.30, 0.48) * size,
                     random.uniform(0.27, 0.45) * size,
                     random.uniform(0.27, 0.48) * size),
                    random.choice([LEAF_DARK, LEAF1, LEAF2]),
                    root, subdivisions=1,
                    rot=(random.random(), random.random(), random.random()),
                    smooth=False)
    return root

def create_main_tree(name, loc, scale=1.0):
    root = make_empty(name + " ROOT",
                      (loc[0], loc[1], terrain_z(loc[0], loc[1]) + 0.01))
    S = lambda p: tuple(v * scale for v in p)
    trunk_points = [
        S((0, 0, 0)),
        S((0.18, 0.02, 1.8)),
        S((-0.10, 0.08, 3.55)),
        S((0.30, 0.02, 5.25)),
        S((0.12, 0.18, 7.10)),
        S((-0.18, 0.24, 8.35)),
    ]
    radii = [0.60, 0.52, 0.43, 0.34, 0.25]
    for i in range(len(trunk_points) - 1):
        add_cone_between(name + " gnarled trunk %02d" % i,
                         trunk_points[i], trunk_points[i + 1],
                         radii[i] * scale, radii[i + 1] * scale,
                         WOOD_DARK, root, vertices=10)

    branches = [
        ((-0.06, 0.05, 3.45), (-1.30, 0.10, 4.85), (-3.05, 0.35, 6.20), True),
        ((0.17, 0.02, 4.15), (1.42, -0.35, 5.35), (3.18, -0.62, 6.35), False),
        ((0.24, 0.10, 5.22), (1.55, 0.66, 6.72), (2.52, 1.25, 8.05), True),
        ((0.12, 0.16, 6.02), (-1.25, 0.70, 7.34), (-2.35, 1.50, 8.35), True),
        ((0.04, 0.20, 6.90), (1.10, -0.08, 8.10), (1.55, -0.55, 9.10), True),
        ((-0.03, 0.20, 7.28), (-0.70, -0.55, 8.45), (-1.25, -1.05, 9.05), False),
    ]
    foliage_points = []
    for i, (p0, p1, p2, leafy) in enumerate(branches):
        p0s, p1s, p2s = S(p0), S(p1), S(p2)
        add_cone_between(name + " heavy branch A %02d" % i,
                         p0s, p1s, 0.22 * scale, 0.12 * scale,
                         WOOD_DARK, root, vertices=8)
        add_cone_between(name + " heavy branch B %02d" % i,
                         p1s, p2s, 0.13 * scale, 0.045 * scale,
                         WOOD_DARK, root, vertices=8)
        if leafy:
            foliage_points.extend([Vector(p1s), Vector(p2s)])

    # Dead twigs over the cottage.
    twig_specs = [
        ((3.18, -0.62, 6.35), (3.75, -0.92, 7.05)),
        ((3.18, -0.62, 6.35), (3.50, -0.15, 7.28)),
        ((-1.25, -1.05, 9.05), (-1.75, -1.45, 9.62)),
    ]
    for i, (a, b) in enumerate(twig_specs):
        add_cone_between(name + " bare twig %02d" % i, S(a), S(b),
                         0.045 * scale, 0.012 * scale,
                         WOOD_DARK, root, vertices=7)

    foliage_points.extend([
        Vector(S((-1.55, 0.62, 7.30))),
        Vector(S((0.75, 0.72, 8.25))),
        Vector(S((-0.35, 0.25, 9.00))),
        Vector(S((1.45, 0.10, 8.85))),
    ])
    for i, center in enumerate(foliage_points):
        for j in range(2):
            offset = Vector((random.uniform(-0.42, 0.42),
                             random.uniform(-0.42, 0.42),
                             random.uniform(-0.28, 0.32))) * scale
            add_ico(name + " sparse crown %02d" % (i * 2 + j),
                    tuple(center + offset),
                    (random.uniform(0.62, 1.05) * scale,
                     random.uniform(0.52, 0.92) * scale,
                     random.uniform(0.55, 1.00) * scale),
                    random.choice([LEAF_DARK, LEAF1, LEAF2]),
                    root, subdivisions=2, smooth=True)

    # Exposed roots.
    for i, ang in enumerate([-2.7, -1.65, -0.55, 0.45, 1.6, 2.55]):
        end = (math.cos(ang) * random.uniform(1.2, 2.2) * scale,
               math.sin(ang) * random.uniform(1.0, 1.8) * scale,
               0.03)
        add_cone_between(name + " exposed root %02d" % i,
                         (0, 0, 0.22 * scale), end,
                         0.17 * scale, 0.025 * scale,
                         WOOD_DARK, root, vertices=8)
    add_ico(name + " moss on trunk", (0.36 * scale, -0.18 * scale, 0.62 * scale),
            (0.34 * scale, 0.25 * scale, 0.46 * scale),
            MOSS, root, subdivisions=2)

    ivy_points = [
        S((0.42, -0.18, 0.12)),
        S((0.50, -0.25, 1.35)),
        S((0.20, -0.31, 2.65)),
        S((0.38, -0.26, 4.10)),
        S((0.05, -0.18, 5.22)),
    ]
    create_curve_mesh(name + " trunk ivy", ivy_points,
                      0.035 * scale, VINE_BROWN, root)
    create_leaf_mesh(name + " trunk ivy leaves",
                     [ivy_points[1], ivy_points[2], ivy_points[3],
                      ivy_points[4],
                      S((0.60, -0.27, 2.0)),
                      S((-0.05, -0.25, 3.45))],
                     'front', [LEAF_DARK, LEAF1], root,
                     (0.14 * scale, 0.25 * scale))
    return root

def create_background_tree(name, loc, height, haze=False):
    root = make_empty(name + " ROOT",
                      (loc[0], loc[1], terrain_z(loc[0], loc[1]) + 0.01))
    foliage_mat = LEAF_HAZE if haze else LEAF_DARK
    p0 = (0, 0, 0)
    p1 = (random.uniform(-0.18, 0.18), 0, height * 0.52)
    p2 = (random.uniform(-0.28, 0.28), random.uniform(-0.12, 0.12), height)
    add_cone_between(name + " trunk lower", p0, p1,
                     height * 0.055, height * 0.033,
                     WOOD_DARK, root, vertices=8)
    add_cone_between(name + " trunk upper", p1, p2,
                     height * 0.034, height * 0.015,
                     WOOD_DARK, root, vertices=8)
    for i in range(6):
        ang = math.tau * i / 6 + random.uniform(-0.4, 0.4)
        center = (
            p2[0] + math.cos(ang) * random.uniform(0.45, 1.25),
            p2[1] + math.sin(ang) * random.uniform(0.30, 1.0),
            height * random.uniform(0.72, 1.04)
        )
        add_ico(name + " distant crown %02d" % i, center,
                (random.uniform(0.75, 1.35),
                 random.uniform(0.65, 1.2),
                 random.uniform(0.75, 1.4)),
                foliage_mat if random.random() < 0.7 else LEAF_DARK,
                root, subdivisions=1, smooth=True)
    return root

create_main_tree("Ancient Reclaiming Tree", (-8.25, 7.8), 1.18)

for i, (x, y, h) in enumerate([
    (-13.2, 23.5, 7.6),
    (-9.1, 27.0, 8.3),
    (-1.5, 26.6, 7.1),
    (8.8, 25.8, 8.8),
    (13.2, 22.7, 7.4),
]):
    create_background_tree("Background tree %02d" % i, (x, y), h, haze=True)

for i, (x, y, size) in enumerate([
    (-6.55, 5.55, 1.10),
    (-0.72, 4.85, 0.82),
    (6.52, 7.25, 1.08),
    (2.55, 12.0, 0.90),
    (-7.1, 13.2, 0.96),
    (7.55, 16.0, 1.10),
    (-1.15, 16.8, 0.78),
]):
    create_shrub("Wild shrub %02d" % i, (x, y), size)

for i, (x, y, size) in enumerate([
    (-5.45, 0.72, 1.0),
    (-0.92, 3.65, 1.08),
    (2.35, 3.55, 0.92),
    (-0.78, 8.52, 1.06),
    (2.75, -2.85, 0.95),
    (5.95, 4.15, 0.90),
]):
    create_fern("Wild fern %02d" % i, (x, y), size)

# Moss mats spreading across the ground around each building.
for hi, (hx, hy, hr) in enumerate(planned_houses):
    for j in range(4):
        ang = random.uniform(0, math.tau)
        rr = random.uniform(hr * 0.70, hr * 1.05)
        x = hx + math.cos(ang) * rr
        y = hy + math.sin(ang) * rr
        add_ico("Ground moss patch %02d %02d" % (hi, j),
                (x, y, terrain_z(x, y) + 0.035),
                (random.uniform(0.45, 1.10),
                 random.uniform(0.32, 0.80),
                 random.uniform(0.025, 0.055)),
                random.choice([MOSS, MOSS_LIGHT]),
                subdivisions=2)

# Tall dry seed heads.
for i in range(25):
    for attempt in range(20):
        x = random.uniform(-10.0, 10.0)
        y = random.uniform(-6.5, 17.0)
        if abs(x - path_x(y)) > 0.45 or random.random() < 0.35:
            break
    z = terrain_z(x, y) + 0.03
    height = random.uniform(0.42, 0.92)
    lean = Vector((random.uniform(-0.08, 0.08),
                   random.uniform(-0.08, 0.08), height))
    add_cone_between("Dry weed stalk %02d" % i,
                     (x, y, z), Vector((x, y, z)) + lean,
                     0.013, 0.006, GRASS_DRY, vertices=6)
    tip = Vector((x, y, z)) + lean
    add_ico("Dry seed head %02d" % i, tip,
            (0.045, 0.045, 0.075), GRASS_DRY,
            subdivisions=1)

# ---------------------------------------------------------------------
# Abandoned stone well
# ---------------------------------------------------------------------

def create_well(loc):
    root = make_empty("Overgrown Village Well ROOT",
                      (loc[0], loc[1], terrain_z(loc[0], loc[1]) + 0.015),
                      (0, 0, math.radians(4)))
    stones = 15
    for layer in range(2):
        for i in range(stones):
            ang = math.tau * (i + layer * 0.5) / stones
            radius = 0.84
            x = math.cos(ang) * radius
            y = math.sin(ang) * radius
            z = 0.17 + layer * 0.27
            add_box("Well ring stone", (x, y, z),
                    (0.48, 0.34, 0.255),
                    random.choice([STONE1, STONE1, STONE2, STONE3]),
                    rot=(random.uniform(-0.08, 0.08),
                         random.uniform(-0.08, 0.08),
                         ang + math.pi * 0.5 + random.uniform(-0.06, 0.06)),
                    parent=root, bevel=0.055)
            if layer == 1 and i % 4 == 0:
                add_ico("Moss on well stone", (x, y, z + 0.145),
                        (0.25, 0.18, 0.045),
                        random.choice([MOSS, MOSS_LIGHT]),
                        root, subdivisions=2)
    bpy.ops.mesh.primitive_cylinder_add(vertices=32, radius=0.68,
                                       depth=0.035,
                                       location=(0, 0, 0.60))
    opening = bpy.context.object
    opening.name = "Black water deep in well"
    opening.parent = root
    assign_material(opening, DARK)

    left_post = add_box("Well left rotten post", (-1.10, 0, 1.56),
                        (0.21, 0.24, 2.45), WOOD_DARK,
                        rot=(0.02, -0.05, -0.025),
                        parent=root, bevel=0.025)
    right_post = add_box("Well right leaning post", (1.10, 0, 1.50),
                         (0.21, 0.24, 2.34), WOOD_DARK,
                         rot=(0.04, 0.09, 0.04),
                         parent=root, bevel=0.025)
    add_beam_between("Broken well crossbeam",
                     (-1.18, 0, 2.76), (1.17, 0, 2.70),
                     0.19, WOOD_DARK, root, bevel=0.025)
    add_cone_between("Well spindle",
                     (-0.72, -0.02, 2.05), (0.72, -0.02, 2.05),
                     0.10, 0.10, WOOD, root, vertices=10)
    create_curve_mesh("Well rope",
                      [(0.08, -0.03, 2.05),
                       (0.10, -0.05, 1.42),
                       (0.04, -0.03, 0.62)],
                      0.024, ROPE, root)
    add_ivy_side(root, 2.17, 0.45, 2.55, 0.05, "Well post ivy")
    return root

create_well((0.15, 9.0))

# ---------------------------------------------------------------------
# Broken wheelbarrow in the foreground
# ---------------------------------------------------------------------

def create_wheelbarrow(loc):
    root = make_empty("Abandoned Rusty Wheelbarrow ROOT",
                      (loc[0], loc[1], terrain_z(loc[0], loc[1]) + 0.02),
                      (0, 0, math.radians(-18)))

    verts = [
        (-0.42, -0.27, 0.22),
        (0.42, -0.27, 0.22),
        (0.38, 0.28, 0.22),
        (-0.38, 0.28, 0.22),
        (-0.74, -0.50, 0.72),
        (0.74, -0.50, 0.72),
        (0.64, 0.51, 0.72),
        (-0.64, 0.51, 0.72),
    ]
    faces = [
        (0, 3, 2, 1),
        (0, 1, 5, 4),
        (1, 2, 6, 5),
        (2, 3, 7, 6),
        (3, 0, 4, 7),
    ]
    bowl = add_mesh_obj("Corroded wheelbarrow basin", verts, faces,
                        [RUST1], parent=root)
    bowl.rotation_euler[0] = math.radians(-5)
    solid = bowl.modifiers.new("Pitted basin thickness", 'SOLIDIFY')
    solid.thickness = 0.025

    add_box("Soil collected in wheelbarrow", (0, -0.02, 0.57),
            (0.86, 0.56, 0.05), GROUND, parent=root, bevel=0.03)

    bpy.ops.mesh.primitive_torus_add(
        major_radius=0.34, minor_radius=0.055,
        major_segments=24, minor_segments=7,
        location=(0, 0.82, 0.34),
        rotation=(0, math.pi * 0.5, 0)
    )
    wheel = bpy.context.object
    wheel.name = "Wheelbarrow rusted wheel"
    wheel.parent = root
    assign_material(wheel, RUST_DARK)
    for poly in wheel.data.polygons:
        poly.use_smooth = True
    add_cone_between("Wheelbarrow axle",
                     (-0.43, 0.82, 0.34), (0.43, 0.82, 0.34),
                     0.04, 0.04, RUST_DARK, root, vertices=10)

    add_cone_between("Wheelbarrow left handle",
                     (-0.35, -0.26, 0.42), (-0.50, -1.42, 0.29),
                     0.045, 0.035, WOOD_DARK, root, vertices=8)
    add_cone_between("Wheelbarrow right handle",
                     (0.35, -0.26, 0.42), (0.50, -1.42, 0.29),
                     0.045, 0.035, WOOD_DARK, root, vertices=8)
    add_cone_between("Wheelbarrow broken leg",
                     (-0.29, -0.34, 0.34), (-0.36, -0.58, 0.03),
                     0.035, 0.025, RUST_DARK, root, vertices=7)

    # Weeds growing from the soil in the basin.
    for i in range(7):
        bx = random.uniform(-0.34, 0.34)
        by = random.uniform(-0.18, 0.20)
        height = random.uniform(0.25, 0.55)
        add_cone_between("Wheelbarrow weed %02d" % i,
                         (bx, by, 0.59),
                         (bx + random.uniform(-0.06, 0.06),
                          by + random.uniform(-0.06, 0.06),
                          0.59 + height),
                         0.012, 0.005,
                         random.choice([GRASS1, GRASS2, GRASS_DRY]),
                         root, vertices=6)
    return root

create_wheelbarrow((2.05, -2.25))

# ---------------------------------------------------------------------
# Broken fences with vines
# ---------------------------------------------------------------------

def create_fence(name, points):
    post_tops = []
    for i, (x, y) in enumerate(points):
        z = terrain_z(x, y)
        height = random.uniform(1.35, 1.75)
        post = add_box(name + " leaning post %02d" % i,
                       (x, y, z + height * 0.5),
                       (0.18, 0.18, height),
                       WOOD_GRAY,
                       rot=(random.uniform(-0.10, 0.10),
                            random.uniform(-0.10, 0.10),
                            random.uniform(-0.08, 0.08)),
                       bevel=0.025)
        post_tops.append((x, y, z, height))

    for i in range(len(points) - 1):
        if i == 1:
            # Deliberately missing upper rail.
            levels = [0.72]
        else:
            levels = [0.72, 1.25]
        for level in levels:
            x1, y1, z1, h1 = post_tops[i]
            x2, y2, z2, h2 = post_tops[i + 1]
            p1 = (x1, y1, z1 + min(level, h1 - 0.12))
            p2 = (x2, y2, z2 + min(level + random.uniform(-0.08, 0.08),
                                   h2 - 0.12))
            add_beam_between(name + " rotten rail", p1, p2,
                             0.12, WOOD_GRAY, bevel=0.018)

    # Ivy climbing the middle post.
    mid = len(points) // 2
    x, y = points[mid]
    z = terrain_z(x, y)
    vine = [
        (x - 0.09, y - 0.08, z + 0.02),
        (x + 0.08, y - 0.10, z + 0.48),
        (x - 0.06, y - 0.12, z + 0.94),
        (x + 0.07, y - 0.12, z + 1.43),
    ]
    create_curve_mesh(name + " fence ivy", vine, 0.022, VINE_BROWN)
    create_leaf_mesh(name + " fence ivy leaves",
                     [vine[1], vine[2], vine[3],
                      (x + 0.20, y - 0.12, z + 0.73),
                      (x - 0.22, y - 0.12, z + 1.16)],
                     'front', [LEAF_DARK, LEAF1, LEAF2],
                     size_range=(0.12, 0.23))

create_fence("Broken foreground fence",
             [(-8.2, -4.6), (-7.9, -2.6), (-7.55, -0.6), (-7.15, 1.15)])
create_fence("Distant broken fence",
             [(7.8, 8.2), (8.15, 10.2), (8.65, 12.0)])

# ---------------------------------------------------------------------
# Lighting, world, and presentation camera
# ---------------------------------------------------------------------

world = bpy.data.worlds.new("Clouded late-afternoon world")
world.use_nodes = True
scene.world = world
wnodes = world.node_tree.nodes
wlinks = world.node_tree.links
wnodes.clear()
wout = wnodes.new("ShaderNodeOutputWorld")
wbg = wnodes.new("ShaderNodeBackground")
sky = wnodes.new("ShaderNodeTexSky")
sky.sky_type = 'NISHITA'
sky.sun_elevation = math.radians(16)
sky.sun_rotation = math.radians(218)
try:
    sky.altitude = 0.15
    sky.air_density = 1.2
    sky.dust_density = 3.0
    sky.ozone_density = 1.15
    sky.sun_disc = True
    sky.sun_size = math.radians(2.5)
except Exception:
    pass
wbg.inputs["Strength"].default_value = 0.29
wlinks.new(sky.outputs["Color"], wbg.inputs["Color"])
wlinks.new(wbg.outputs["Background"], wout.inputs["Surface"])

def add_area_light(name, loc, target, energy, size, color):
    data = bpy.data.lights.new(name + " Data", type='AREA')
    data.energy = energy
    data.shape = 'DISK'
    data.size = size
    data.color = color
    obj = bpy.data.objects.new(name, data)
    scene.collection.objects.link(obj)
    obj.location = loc
    direction = Vector(target) - Vector(loc)
    obj.rotation_mode = 'QUATERNION'
    obj.rotation_quaternion = direction.to_track_quat('-Z', 'Y')
    return obj

def add_sun(name, loc, target, energy, angle, color):
    data = bpy.data.lights.new(name + " Data", type='SUN')
    data.energy = energy
    data.angle = angle
    data.color = color
    obj = bpy.data.objects.new(name, data)
    scene.collection.objects.link(obj)
    obj.location = loc
    direction = Vector(target) - Vector(loc)
    obj.rotation_mode = 'QUATERNION'
    obj.rotation_quaternion = direction.to_track_quat('-Z', 'Y')
    return obj

add_area_light(
    "Warm cloud-broken key",
    (-10.5, -9.0, 16.5), (-1.0, 6.0, 2.5),
    1550.0, 8.5, (1.0, 0.70, 0.45)
)
add_area_light(
    "Cool soft fill",
    (12.0, 1.5, 11.5), (0.0, 6.5, 2.5),
    520.0, 10.0, (0.46, 0.62, 0.82)
)
add_sun(
    "Low amber sun",
    (-12.0, -13.0, 20.0), (0.0, 7.0, 0.0),
    1.35, math.radians(10), (1.0, 0.67, 0.39)
)

camera_data = bpy.data.cameras.new("Presentation Camera Data")
camera = bpy.data.objects.new("Presentation Camera", camera_data)
scene.collection.objects.link(camera)
scene.camera = camera
camera.location = (13.8, -18.2, 9.25)
target = Vector((0.0, 6.2, 3.05))
direction = target - camera.location
camera.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler()
camera_data.lens = 50.0
camera_data.sensor_width = 36.0
camera_data.clip_start = 0.1
camera_data.clip_end = 180.0

focus = make_empty("Camera focus", (-0.7, 5.5, 2.55))
camera_data.dof.use_dof = True
camera_data.dof.focus_object = focus
camera_data.dof.aperture_fstop = 8.5
camera_data.dof.aperture_blades = 7

bpy.context.view_layer.update()
```