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

# The lane has been empty for a long time.
random.seed(29)
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)

# --------------------------------------------------------------------
# Materials
# --------------------------------------------------------------------
def material(name, color, roughness=.8, metallic=0):
    m = bpy.data.materials.new(name)
    m.diffuse_color = (*color, 1)
    m.use_nodes = True
    p = m.node_tree.nodes.get('Principled BSDF')
    p.inputs['Base Color'].default_value = (*color, 1)
    p.inputs['Roughness'].default_value = roughness
    p.inputs['Metallic'].default_value = metallic
    return m

def weather(name, colors, scale=5, bump=.15, metallic=0):
    m = material(name, colors[len(colors)//2], .86, metallic)
    n, l = m.node_tree.nodes, m.node_tree.links
    p = n.get('Principled BSDF')
    tex = n.new('ShaderNodeTexNoise')
    tex.inputs['Scale'].default_value = scale
    tex.inputs['Detail'].default_value = 5
    tex.inputs['Roughness'].default_value = .78
    ramp = n.new('ShaderNodeValToRGB')
    ramp.color_ramp.elements.remove(ramp.color_ramp.elements[1])
    for i, c in enumerate(colors):
        e = ramp.color_ramp.elements[0] if i == 0 else ramp.color_ramp.elements.new(i/(len(colors)-1))
        e.position = i/(len(colors)-1)
        e.color = (*c, 1)
    l.new(tex.outputs['Fac'], ramp.inputs[0])
    l.new(ramp.outputs[0], p.inputs['Base Color'])
    fine = n.new('ShaderNodeTexNoise')
    fine.inputs['Scale'].default_value = 67
    fine.inputs['Detail'].default_value = 3
    bn = n.new('ShaderNodeBump')
    bn.inputs['Strength'].default_value = .5
    bn.inputs['Distance'].default_value = bump
    l.new(fine.outputs['Fac'], bn.inputs['Height'])
    l.new(bn.outputs[0], p.inputs['Normal'])
    return m

earth = weather('Damp earth, roots and old leaf mould',
                [( .045,.060,.031),(.14,.17,.071),(.25,.25,.13)], 7,.13)
edge = weather('Exposed earth along the land edge',
               [(.035,.027,.017),(.095,.074,.045),(.17,.14,.085)],9,.12)
dirt = weather('The last trace of the village lane',
               [(.12,.105,.073),(.24,.215,.145),(.33,.29,.205)],8,.08)
plasters = [
    weather('Limewash • warm ash',[(.16,.18,.13),(.47,.44,.31),(.72,.69,.52)],5,.1),
    weather('Limewash • faded ochre',[(.16,.17,.11),(.42,.35,.22),(.64,.56,.38)],6,.1),
    weather('Limewash • cold grey',[(.13,.19,.17),(.38,.44,.38),(.61,.63,.52)],5,.1)]
rusts = [
    weather('Corrugated iron • orange corrosion',[(.075,.044,.024),(.24,.092,.032),(.47,.19,.056),(.24,.28,.25)],5,.08,.48),
    weather('Corrugated iron • old red oxide',[(.045,.047,.035),(.15,.055,.025),(.34,.11,.034),(.43,.31,.17)],6,.085,.42),
    weather('Corrugated iron • remnant galvanizing',[(.075,.082,.068),(.22,.105,.043),(.34,.29,.18),(.43,.47,.40)],7,.07,.55)]
wood = weather('Splintered silver-brown timber',[(.048,.035,.022),(.18,.14,.085),(.36,.32,.23)],5,.085)
wooddark = weather('Wet structural timber',[(.025,.029,.017),(.085,.068,.035),(.19,.15,.071)],5,.08)
doorpaint = weather('Remains of green door paint',[(.06,.083,.057),(.16,.23,.17),(.33,.36,.25)],5,.05)
stone = weather('Old local limestone',[(.18,.20,.17),(.34,.36,.29),(.51,.50,.40)],6,.12)
brick = weather('Soft exposed masonry',[(.14,.105,.065),(.31,.23,.14),(.45,.36,.24)],6,.07)
moss = weather('Thick velvety roof moss',[(.046,.087,.022),(.14,.22,.044),(.29,.35,.095)],5,.07)
dark = material('Unlit recesses',(.022,.028,.021),1)
crackmat = material('Hairline cracks and missing mortar',(.065,.070,.050))
iron = weather('Black rust on fittings',[(.025,.031,.027),(.095,.074,.039),(.24,.12,.05)],7,.035,.6)
glass = material('Fragments of dirty bottle-green glass',(.12,.23,.19),.25,.2)
glass.node_tree.nodes.get('Principled BSDF').inputs['Transmission Weight'].default_value = .22
leafmats = [material('Leaves / '+str(i),c,.88) for i,c in enumerate([
    (.055,.115,.034),(.105,.19,.049),(.19,.285,.071),(.28,.34,.10),
    (.085,.17,.085),(.34,.35,.12)])]
grassmats = [material('Wild grasses / '+str(i),c,.95) for i,c in enumerate([
    (.13,.20,.052),(.24,.29,.078),(.34,.34,.13),(.42,.37,.20)])]
flower = material('Tiny faded ivory wildflowers',(.73,.70,.46))
fallen = material('Old copper-coloured leaves',(.25,.135,.045))
backdrop = material('Quiet warm grey background',(.115,.145,.128))

# --------------------------------------------------------------------
# Geometry helpers
# --------------------------------------------------------------------
def mesh(name, verts, faces, mat=None):
    me = bpy.data.meshes.new(name)
    me.from_pydata(verts, [], faces)
    me.update()
    ob = bpy.data.objects.new(name, me)
    bpy.context.collection.objects.link(ob)
    if mat: me.materials.append(mat)
    return ob

def box(name, loc, scale, mat, bevel=0, rot=0):
    bpy.ops.mesh.primitive_cube_add(size=1, location=loc)
    ob = bpy.context.object
    ob.name = name
    ob.dimensions = scale
    ob.rotation_euler[2] = rot
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    ob.data.materials.append(mat)
    if bevel:
        b = ob.modifiers.new('Soft crumbling edges','BEVEL')
        b.width = bevel
        b.segments = 2
        ob.modifiers.new('Weighted corner normals','WEIGHTED_NORMAL')
    return ob

def ico(name, loc, scale, mat, subdivisions=1):
    bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=subdivisions, radius=1, location=loc)
    ob = bpy.context.object
    ob.name = name
    ob.scale = scale
    ob.data.materials.append(mat)
    return ob

def tube(name, points, radii, mat, sides=7):
    points = [Vector(p) for p in points]
    if isinstance(radii,(float,int)): radii = [radii]*len(points)
    vs, fs = [], []
    for i,p in enumerate(points):
        tangent = points[min(i+1,len(points)-1)] - points[max(i-1,0)]
        tangent.normalize()
        u = tangent.cross(Vector((0,0,1)))
        if u.length < .01: u = tangent.cross(Vector((0,1,0)))
        u.normalize()
        v = tangent.cross(u).normalized()
        for j in range(sides):
            vs.append(p + radii[i]*(u*math.cos(j*math.tau/sides)+v*math.sin(j*math.tau/sides)))
    for i in range(len(points)-1):
        for j in range(sides):
            a=i*sides+j; b=i*sides+(j+1)%sides
            fs.append((a,b,b+sides,a+sides))
    fs.append(tuple(reversed(range(sides))))
    fs.append(tuple((len(points)-1)*sides+j for j in range(sides)))
    return mesh(name,vs,fs,mat)

def beam(name,a,b,width,depth,mat):
    mid=(Vector(a)+Vector(b))/2
    ob=box(name,mid,(width,depth,(Vector(b)-Vector(a)).length),mat,.015)
    ob.rotation_euler=(Vector(b)-Vector(a)).to_track_quat('Z','Y').to_euler()
    return ob

class FoliageBatch:
    def __init__(self,name,mats):
        self.name=name; self.mats=mats; self.v=[]; self.f=[]; self.mi=[]
    def poly(self,verts,faces,mi):
        n=len(self.v); self.v.extend(verts)
        self.f.extend([tuple(n+i for i in f) for f in faces])
        self.mi.extend([mi]*len(faces))
    def leaf(self,p,size,mi=None):
        p=Vector(p)
        normal=Vector((random.uniform(-1,1),random.uniform(-1,1),random.uniform(-.2,1))).normalized()
        u=normal.cross(Vector((0,0,1)))
        if u.length<.01: u=Vector((1,0,0))
        u.normalize()
        v=normal.cross(u).normalized()
        angle=random.uniform(0,math.tau)
        u,v=u*math.cos(angle)+v*math.sin(angle),-u*math.sin(angle)+v*math.cos(angle)
        verts=[p-v*size*.55,p-u*size*.37-v*size*.15,
               p-u*size*.42+v*size*.18,p+v*size*.72,
               p+u*size*.42+v*size*.18,p+u*size*.37-v*size*.15,
               p+normal*size*.12]
        self.poly(verts,[(6,j,(j+1)%6) for j in range(6)],
                  random.randrange(len(self.mats)) if mi is None else mi)
    def finish(self):
        ob=mesh(self.name,self.v,self.f)
        for m in self.mats: ob.data.materials.append(m)
        for p,i in zip(ob.data.polygons,self.mi): p.material_index=i
        return ob

leaves=FoliageBatch('Individual leaves • ivy, brambles and tree crowns',leafmats)
grasses=FoliageBatch('Thousands of bent grass blades',grassmats)
litter=FoliageBatch('Windblown leaves in the abandoned lane',[fallen])
flowers=FoliageBatch('Scattered meadow flowers',[flower])

def cluster(p,r,count=70,size=.20):
    for i in range(count):
        v=Vector((random.gauss(0,.46),random.gauss(0,.46),random.gauss(0,.4)))
        if v.length>1: v.normalize()
        leaves.leaf(Vector(p)+Vector((v.x*r,v.y*r,v.z*r*.8)),random.uniform(size*.65,size*1.3))

def ivy(points, density=13):
    tube('Woody climbing vine',points,[.035*(1-.7*i/max(1,len(points)-1)) for i in range(len(points))],wooddark,5)
    for a,b in zip(points[:-1],points[1:]):
        a,b=Vector(a),Vector(b)
        steps=max(3,int((b-a).length*density))
        for i in range(steps):
            p=a.lerp(b,i/steps)
            for j in range(2):
                leaves.leaf(p+Vector((random.uniform(-.20,.20),random.uniform(-.20,.20),random.uniform(-.12,.12))),
                            random.uniform(.16,.31))

def grass_tuft(x,y,z,height):
    for i in range(random.randint(5,9)):
        a=random.random()*math.tau
        base=Vector((x+random.uniform(-.13,.13),y+random.uniform(-.13,.13),z))
        h=height*random.uniform(.6,1.3)
        d=Vector((math.cos(a),math.sin(a),0))
        side=Vector((-d.y,d.x,0))*random.uniform(.016,.035)
        bend=d*h*random.uniform(.20,.55)
        mid=base+Vector((0,0,h*.58))+bend*.3
        tip=base+Vector((0,0,h))+bend
        grasses.poly([base-side,base+side,mid+side*.6,mid-side*.6,tip],
                     [(0,1,2,3),(3,2,4)],random.randrange(4))

# --------------------------------------------------------------------
# A real, softly uneven patch of terrain, with exposed earthen sides.
# --------------------------------------------------------------------
def groundz(x,y): return .025+ .055*math.sin(x*.8)*math.cos(y*.65)
N=112
rings=22
verts=[(0,0,groundz(0,0))]
boundary=[]
for j in range(N):
    a=j*math.tau/N
    r=1+.022*math.sin(7*a)+.012*math.sin(13*a)
    boundary.append((15.6*r*math.cos(a),14.8*r*math.sin(a)))
for k in range(1,rings+1):
    for x,y in boundary:
        x*=k/rings; y*=k/rings
        verts.append((x,y,groundz(x,y)))
faces=[]
for j in range(N): faces.append((0,1+j,1+(j+1)%N))
for k in range(rings-1):
    a=1+k*N; b=a+N
    for j in range(N): faces.append((a+j,b+j,b+(j+1)%N,a+(j+1)%N))
terrain=mesh('Uneven living ground',verts,faces,earth)
for p in terrain.data.polygons:p.use_smooth=True
sv=[]; sf=[]
for j,(x,y) in enumerate(boundary):
    sv.extend([(x,y,groundz(x,y)),(x*.98,y*.98,-.8)])
for j in range(N): sf.append((2*j,2*((j+1)%N),2*((j+1)%N)+1,2*j+1))
sf.append(tuple(2*j+1 for j in reversed(range(N))))
mesh('Dark soil beneath the village',sv,sf,edge)

def pathx(y): return .75*math.sin(y*.40)+.18
pv=[]; pf=[]
for i in range(100):
    y=-14.5+i*.282
    x=pathx(y)
    width=1.2+.15*math.sin(y*1.8)
    pv.extend([(x-width,y,groundz(x-width,y)+.018),(x+width,y,groundz(x+width,y)+.018)])
    if i:pf.append((2*i-2,2*i-1,2*i+1,2*i))
mesh('Winding, half-erased dirt lane',pv,pf,dirt)
for i in range(250):
    y=random.uniform(-14,12.5); x=pathx(y)+random.uniform(-1,1)
    if (x*x+(y+3.8)**2)<1.7: continue
    ob=ico('Worn and sunken lane cobble',(x,y,groundz(x,y)+.045),
           (random.uniform(.12,.29),random.uniform(.14,.32),random.uniform(.055,.11)),stone,1)
    ob.rotation_euler[2]=random.random()*math.tau

# --------------------------------------------------------------------
# Village buildings. Openings are built as openings, not painted on.
# --------------------------------------------------------------------
houses=[]
def house(name,cx,cy,w,d,h,rise,angle,wallmat,ruin=.13):
    ca,sa=math.cos(angle),math.sin(angle)
    base=.12
    def W(p):
        x,y,z=p
        return (cx+x*ca-y*sa,cy+x*sa+y*ca,z+base)
    def B(label,p,sz,mat=wood,bv=.015):
        return box(name+' / '+label,W(p),sz,mat,bv,angle)
    def T(label,pts,rad,mat=wooddark):
        return tube(name+' / '+label,[W(p) for p in pts],rad,mat)
    def E(label,a,b,ww=.12,dd=.12,mat=wood):
        return beam(name+' / '+label,W(a),W(b),ww,dd,mat)
    houses.append((cx,cy,w,d,angle))
    B('stone footing',(0,0,.20),(w+.24,d+.24,.40),stone,.06)
    B('interior floor',(0,0,.42),(w-.25,d-.25,.12),wooddark)
    # Straight walls subdivided around their rectangular openings.
    def wall(axis,const,length,openings):
        us=sorted(set([-length/2,length/2]+[v for op in openings for v in (op[0],op[1])]))
        zs=sorted(set([.40,h]+[v for op in openings for v in (op[2],op[3])]))
        for a,b in zip(us[:-1],us[1:]):
            for lo,hi in zip(zs[:-1],zs[1:]):
                u=(a+b)/2; z=(lo+hi)/2
                if any(o[0]<u<o[1] and o[2]<z<o[3] for o in openings):continue
                if axis=='x': B('cracked lime wall',(u,const,z),(b-a,.24,hi-lo),wallmat,.025)
                else:B('cracked lime wall',(const,u,z),(.24,b-a,hi-lo),wallmat,.025)
    front=[(-w*.37,-w*.13,1.45,2.66),(w*.09,w*.32,.42,2.65)]
    back=[(-w*.35,-w*.12,1.45,2.65),(w*.12,w*.35,1.45,2.65)]
    side=[(-d*.34,-d*.13,1.4,2.6),(d*.12,d*.34,1.4,2.6)]
    wall('x',-d/2,w,front);wall('x',d/2,w,back)
    wall('y',-w/2,d,side);wall('y',w/2,d,side)
    for yy in [-d/2,d/2]:
        # Thick triangular gables.
        vs=[W((-w/2,yy-.12,h)),W((w/2,yy-.12,h)),W((0,yy-.12,h+rise)),
            W((-w/2,yy+.12,h)),W((w/2,yy+.12,h)),W((0,yy+.12,h+rise))]
        mesh(name+' / weathered gable',vs,[(0,1,2),(5,4,3),(0,3,4,1),(1,4,5,2),(2,5,3,0)],wallmat)
        E('gable tie',(-w/2,yy-.14,h-.08),(w/2,yy-.14,h-.08),.14,.12,wooddark)
        E('gable king post',(0,yy-.14,h),(0,yy-.14,h+rise-.12),.11,.12)
    for xx in [-w/2,w/2]:
        for yy in [-d/2,d/2]:
            B('exposed corner post',(xx,yy,h/2+.17),(.17,.18,h-.2),wooddark)
    # Windows have sills, mullions, broken glass and crooked shutters.
    def opening(axis,const,o,isdoor=False):
        l,r,bot,top=o
        mid=(l+r)/2; width=r-l
        def P(u,z,offset=0):
            return (u,const+offset,z) if axis=='x' else (const+offset,u,z)
        outward=-1 if const<0 else 1
        off=outward*.16
        for u in [l-.035,r+.035]:
            E('rotting opening jamb',P(u,bot,off),P(u,top+.07,off),.095,.10,wood)
        E('lintel',P(l-.12,top+.05,off),P(r+.12,top+.05,off),.13,.14,wood)
        if isdoor:
            # Only part of the original door remains; darkness visible beside it.
            for j in range(4):
                u=l+.095+j*width*.13
                a=P(u,bot+.08,off*.35);b=P(u+random.uniform(-.025,.025),top-.08-random.uniform(0,.18),off*.35)
                E('peeling door plank',a,b,width*.12,.065,doorpaint)
            E('door cross rail',P(l,1.0,off*.8),P(mid,1.0,off*.8),.10,.10,wood)
            B('worn doorstep',P(mid,.45,outward*.32),
              (width+.4,.65,.16) if axis=='x' else (.65,width+.4,.16),stone,.04)
        else:
            E('deep stone sill',P(l-.12,bot-.04,off),P(r+.12,bot-.04,off),.17,.26,stone)
            E('surviving mullion',P(mid,bot,off),P(mid,top,off),.045,.045,wooddark)
            E('broken cross bar',P(l,(bot+top)/2,off),P(r-.15,(bot+top)/2,off),.05,.05,wooddark)
            # Jagged shards occupy only corners, leaving actual holes.
            for u,sgn in [(l,1),(r,-1)]:
                pts=[P(u,top-.05,off*.4),P(u+sgn*width*.35,top-.05,off*.4),
                     P(u+sgn*width*.15,top-.36,off*.4),P(u+sgn*width*.08,top-.21,off*.4)]
                mesh(name+' / jagged remaining glass',[W(p) for p in pts],[(0,1,2,3)],glass)
            if random.random()<.68:
                u=l-width*.34
                for k in range(3):
                    a=P(u+k*.105,bot+.05,off+outward*.045)
                    b=P(u+k*.105+.08,top-.09,off+outward*.11)
                    E('hanging shutter board',a,b,.095,.055,doorpaint)
                E('shutter brace',P(u-.03,bot+.25,off+outward*.15),
                  P(u+.34,top-.35,off+outward*.15),.07,.055,wood)
    for op in front:opening('x',-d/2,op,op==front[1])
    for op in back:opening('x',d/2,op)
    for xx in [-w/2,w/2]:
        for op in side:opening('y',xx,op)
    # Irregular exposed foundation stones and places where plaster has fallen away.
    for yy in [-d/2-.135,d/2+.135]:
        for row in range(3):
            for j in range(int(w/.46)):
                if row>0 and random.random()<.40:continue
                x=-w/2+.25+j*.46+(row%2)*.10
                B('exposed fieldstone',(x,yy,.53+row*.25),
                  (random.uniform(.29,.44),.075,random.uniform(.16,.24)),brick,.035)
    for xx in [-w/2-.135,w/2+.135]:
        for j in range(int(d/.45)):
            B('foundation rubble',(xx,-d/2+.22+j*.45,.52),
              (.08,random.uniform(.31,.43),random.uniform(.18,.28)),stone,.03)
    # Branching cracks in the front and side plaster.
    for sidewall in [False,True]:
        for k in range(5):
            u=random.choice([-1,1])*random.uniform(w*.34,w*.45) if not sidewall else random.uniform(-d*.43,d*.43)
            z=random.uniform(1.1,h-.2)
            pts=[]
            for j in range(5):
                pts.append((u+random.uniform(-.06,.06),-d/2-.128,z-j*.13) if not sidewall
                           else (w/2+.128,u+random.uniform(-.055,.055),z-j*.13))
            T('branching plaster fissure',pts,.009,crackmat)
    # Exposed rafters support the corrugated metal, even beneath missing sheets.
    roofrun=w/2+.35
    def rz(x):return h+rise-rise*abs(x)/(w/2)
    for j in range(int((d+.6)/.68)+1):
        y=-d/2-.27+j*.68
        for sign in [-1,1]:
            E('visible roof rafter',(0,y,h+rise-.12),(sign*roofrun,y,rz(roofrun)-.12),.105,.13,wooddark)
    for sign in [-1,1]:
        for t in [.20,.55,.90]:
            x=sign*roofrun*t
            E('roof purlin',(x,-d/2-.25,rz(x)-.035),(x,d/2+.25,rz(x)-.035),.10,.095,wood)
    n=int((d+.65)/.76)+1
    sw=(d+.65)/n
    for sign in [-1,1]:
        for j in range(n):
            # Missing sheets reveal a black interior and the decayed roof structure.
            if random.random()<ruin:continue
            y0=-d/2-.325+j*sw
            vx=[];fx=[]
            nu,nv=19,14
            damaged=random.random()<.46
            for a in range(nu+1):
                t=a/nu
                x=sign*roofrun*t
                for b in range(nv+1):
                    y=y0+sw*b/nv
                    corr=.033*math.sin((y-y0)/sw*math.tau*5)
                    bend=(max(0,t-.78)/.22)**2 * (b/nv)**5*random.uniform(.06,.17) if damaged else 0
                    vx.append(W((x,y,rz(x)+corr+.055+bend)))
            for a in range(nu):
                for b in range(nv):
                    if damaged and 8<a<14 and 4<b<9 and random.random()<.83:continue
                    q=a*(nv+1)+b
                    fx.append((q,q+1,q+nv+2,q+nv+1))
            ob=mesh(name+' / perforated corrugated roof sheet',vx,fx,random.choice(rusts))
            sol=ob.modifiers.new('Thin corroded sheet edge','SOLIDIFY');sol.thickness=.012
    # Folded ridge flashing, itself dark with corrosion.
    vv=[]
    for yy in [-d/2-.34,d/2+.34]:
        vv.extend([W((-.19,yy,h+rise-.025)),W((0,yy,h+rise+.10)),W((.19,yy,h+rise-.025))])
    mesh(name+' / rusted ridge cap',vv,[(0,3,4,1),(1,4,5,2)],rusts[1])
    # Roof moss is actual geometry, and vines follow the slopes.
    for k in range(28):
        x=random.uniform(-roofrun*.92,roofrun*.92)
        y=random.uniform(-d/2,d/2)
        if random.random()<.3: x*=.3
        ico(name+' / roof moss cushion',W((x,y,rz(x)+.11)),
            (random.uniform(.18,.44),random.uniform(.15,.40),random.uniform(.055,.12)),moss,2)
    for sign in [-1,1]:
        for k in range(2):
            y=random.uniform(-d*.42,d*.42)
            pts=[]
            for j in range(9):
                x=sign*(roofrun*(1-j/10))
                pts.append(W((x,y+.13*math.sin(j),rz(x)+.16)))
            ivy(pts,10)
    # Ivy colonizes corners and passes directly through the empty windows.
    for k in range(5):
        x=random.choice([-1,1])*w/2+random.uniform(-.18,.18)
        yy=-d/2-.20 if k<3 else d/2+.18
        pts=[W((x+.13*math.sin(j*1.3+k),yy+.06*math.cos(j),.45+j*(h+.3)/8)) for j in range(9)]
        ivy(pts,16)
    op=front[0]
    for k in range(3):
        x=(op[0]+op[1])/2+random.uniform(-.35,.35)
        ivy([W((x,-d/2-.35,.4)),W((x+.2,-d/2-.31,1.1)),
             W((x-.08,-d/2-.30,1.7)),W((x+.12,-d/2+.18,2.0)),
             W((x+.30,-d/2+.4,2.4))],14)
    for k in range(5):
        p=W((random.uniform(-w/2,w/2),-d/2-.30,random.uniform(.35,.6)))
        cluster(p,.4,35,.19)
    # Fallen splinters and masonry outside, rather than pristine house bases.
    for k in range(10):
        x=random.uniform(-w*.65,w*.65); y=-d/2-random.uniform(.3,1.1)
        if k<5:
            ob=B('fallen roof timber',(x,y,.15),(random.uniform(.4,1.3),.12,.10),wooddark)
            ob.rotation_euler[2]+=random.uniform(-.7,.7)
        else:
            ico(name+' / fallen masonry',W((x,y,.1)),(.18,.13,.12),brick)
    return W

house('01 • The ivy cottage',-6.2,-2.9,4.9,5.1,3.45,1.65,-.13,plasters[0],.12)
house('02 • Empty house at the bend',6.1,-.3,5.1,5.7,3.75,1.85,.10,plasters[1],.15)
house('03 • North cottage',-5.0,6.5,4.8,4.9,3.50,1.8,-.035,plasters[2],.17)
house('04 • Last house on the lane',4.25,7.7,4.3,4.4,3.15,1.6,.17,plasters[0],.10)
house('05 • Collapsing store shed',-8.9,-9.0,3.25,3.0,2.25,1.05,-.20,plasters[1],.40)

# --------------------------------------------------------------------
# The community well: dark, open, mossy, with a disused winding frame.
# --------------------------------------------------------------------
wx,wy=.05,-3.85
for row in range(4):
    count=15
    for j in range(count):
        a=(j+(row%2)*.5)*math.tau/count
        da=math.tau/count*.94
        z=.14+row*.235
        vs=[]
        for zz in [z,z+.22]:
            for rr,ang in [(1.05,a-da/2),(1.05,a+da/2),(.77,a+da/2),(.77,a-da/2)]:
                vs.append((wx+rr*math.cos(ang),wy+rr*math.sin(ang),zz+random.uniform(-.013,.013)))
        ob=mesh('Well / individual fitted stone',vs,[(0,3,2,1),(4,5,6,7),(0,1,5,4),(1,2,6,5),(2,3,7,6),(3,0,4,7)],stone)
        b=ob.modifiers.new('Eroded stone corners','BEVEL');b.width=.023;b.segments=2
for j in range(15):
    a=j*math.tau/15
    ico('Moss growing along well rim',(wx+math.cos(a)*.94,wy+math.sin(a)*.94,1.08),(.22,.18,.065),moss,2)
bpy.ops.mesh.primitive_cylinder_add(vertices=48,radius=.76,depth=.025,location=(wx,wy,.13))
bpy.context.object.name='Black water far below the well rim'
bpy.context.object.data.materials.append(dark)
for x in [-1.22,1.22]:
    beam('Well / leaning oak upright',(wx+x,wy,.1),(wx+x+.10,wy+.03,2.8),.16,.20,wooddark)
beam('Well / cracked overhead lintel',(wx-1.48,wy+.03,2.80),(wx+1.55,wy+.03,2.84),.20,.22,wood)
tube('Well / rusted winding axle',[(wx-1.4,wy,2.05),(wx+1.48,wy,2.05)],.065,iron,12)
tube('Well / hanging old rope',[(wx+.05,wy,2.05),(wx+.01,wy,1.70),(wx+.06,wy,1.21),(wx+.04,wy,.42)],.022,wood,6)
tube('Well / crank',[(wx+1.48,wy,2.05),(wx+1.48,wy,1.72),(wx+1.74,wy,1.72)],.045,iron)
ivy([(wx-.85,wy-.5,.10),(wx-.91,wy-.43,.60),(wx-.72,wy-.6,1.09),(wx-.25,wy-.93,1.10)],18)

# --------------------------------------------------------------------
# Forgotten everyday objects
# --------------------------------------------------------------------
def barrel(x,y,z=.12,scale=1):
    n=18
    vs=[]; fs=[]
    for zz,rr in [(0,.36),(.10,.40),(.49,.45),(.90,.40),(1.0,.35)]:
        for j in range(n):
            a=j*math.tau/n
            vs.append((x+rr*scale*math.cos(a),y+rr*scale*math.sin(a),z+zz*scale))
    for k in range(4):
        for j in range(n):
            a=k*n+j;b=k*n+(j+1)%n
            fs.append((a,b,b+n,a+n))
    mesh('Abandoned rusting barrel',vs,fs,rusts[1])
    for zz,rr in [(.14,.408),(.82,.418)]:
        pts=[(x+rr*scale*math.cos(j*math.tau/48),y+rr*scale*math.sin(j*math.tau/48),z+zz*scale) for j in range(49)]
        tube('Barrel / corroded rolled hoop',pts,.027*scale,iron,6)
    bpy.ops.mesh.primitive_cylinder_add(vertices=32,radius=.345*scale,depth=.018,location=(x,y,z+.88*scale))
    bpy.context.object.name='Barrel / dark open top';bpy.context.object.data.materials.append(dark)
barrel(8.7,-3.9)
barrel(-8.6,.1,scale=.8)
cluster((8.65,-3.9,1.1),.42,65,.21)

# Broken fence at the near edge, and fragments on the east side.
def fence_line(points):
    tops=[]
    for x,y,h in points:
        top=(x+random.uniform(-.16,.16),y+random.uniform(-.15,.15),h)
        beam('Split fence post',(x,y,.03),top,.13,.16,wood)
        tops.append(top)
    for i in range(len(points)-1):
        x,y,h=points[i];xx,yy,hh=points[i+1]
        if random.random()<.22:continue
        for z in [.55,1.0]:
            end=(xx,yy,z+random.uniform(-.16,.12))
            if random.random()<.3:end=(x+(xx-x)*.68,y+(yy-y)*.68,.20)
            beam('Sagging fence rail',(x,y,z),end,.075,.12,wood)
fence_line([(-13,-10,1.5),(-11.4,-11.4,1.35),(-9.4,-12.0,1.20),(-7.2,-12.5,1.4),(-5.2,-12.8,1.25)])
fence_line([(5.4,-11.7,1.3),(7.5,-11.5,1.45),(9.5,-10.5,1.30),(11.3,-9,1.1)])
fence_line([(12,-2,1.3),(12.5,.1,1.5),(12.6,2.2,1.25)])
for x,y in [(-11.3,-11.3),(-7.2,-12.5),(9.5,-10.5),(12.5,.1)]:
    ivy([(x,y,.1),(x+.1,y,.55),(x-.1,y,1.2)],15)

# A skeletal cart wheel against the large cottage.
center=Vector((8.98,-1.55,.84))
normal=Vector((1,-.1,.2)).normalized()
u=normal.cross(Vector((0,0,1))).normalized();v=normal.cross(u).normalized()
pts=[center+.72*(math.cos(j*math.tau/64)*u+math.sin(j*math.tau/64)*v) for j in range(65)]
tube('Old cart wheel / iron tyre',pts,.042,iron,7)
pts=[center+.665*(math.cos(j*math.tau/64)*u+math.sin(j*math.tau/64)*v) for j in range(65)]
tube('Old cart wheel / timber rim',pts,.055,wood,7)
for j in range(10):
    a=j*math.tau/10
    tube('Cart wheel / surviving spoke',[center,center+.65*(math.cos(a)*u+math.sin(a)*v)],.031,wood,5)
tube('Cart wheel / hub',[center-normal*.13,center+normal*.13],.11,wood,12)

# --------------------------------------------------------------------
# Wild trees with branching trunks and individually modeled leaves.
# --------------------------------------------------------------------
bark=weather('Old bark • grey, furrowed and lichen-stained',
             [(.045,.053,.029),(.15,.16,.09),(.31,.30,.19)],5,.13)
def tree(x,y,height,crown=1):
    lean=Vector((random.uniform(-.45,.45),random.uniform(-.4,.4),0))
    root=Vector((x,y,groundz(x,y)))
    trunk=[root,root+Vector((0,0,height*.28))+lean*.25,
           root+Vector((0,0,height*.62))+lean*.65,
           root+Vector((0,0,height*.92))+lean]
    tube('Tree / twisting old trunk',trunk,[.28,.22,.14,.035],bark,9)
    for j in range(5):
        a=random.random()*math.tau
        tube('Tree / exposed spreading root',[root+Vector((math.cos(a)*.85,math.sin(a)*.85,.01)),
              root+Vector((math.cos(a)*.30,math.sin(a)*.30,.14)),root+Vector((0,0,.5))],[.035,.105,.16],bark,7)
    for j in range(13):
        a=j*2.399+random.uniform(-.3,.3)
        level=random.uniform(.43,.84)
        start=root+lean*level+Vector((0,0,height*level))
        spread=random.uniform(1.15,2.25)*crown*(1.1-(level-.45)*.6)
        end=start+Vector((math.cos(a)*spread,math.sin(a)*spread,random.uniform(.65,1.65)))
        mid=start.lerp(end,.48)+Vector((0,0,.23))
        tube('Tree / ascending branch',[start,mid,end],[.11*(1-level*.5),.052,.013],bark,7)
        for k in range(2):
            ep=end+Vector((random.uniform(-.55,.55),random.uniform(-.55,.55),random.uniform(-.12,.5)))
            tube('Tree / fine fork',[mid,ep],[.034,.006],bark,5)
            cluster(ep,random.uniform(.67,.99)*crown,170,.26*crown)
    cluster(root+lean+Vector((0,0,height*.98)),1.0*crown,230,.26*crown)
    if random.random()<.8:
        ivy([root+Vector((.22*math.sin(j),-.21,.3+j*.36)) for j in range(10)],12)

for args in [(-12,5.4,6.8,1.0),(-10.3,10.5,7.6,1.05),(-1.6,12.1,7.3,1.05),
             (8.4,11.3,7.6,1.1),(12.1,5.1,6.7,.95),(-13,-2.7,5.7,.9),
             (-12.1,-8.1,5.6,.75),(12,-6.3,4.9,.72)]:
    tree(*args)

# Shrubs fill the spaces between buildings without concealing the architecture.
for x,y,r in [(-11,1,1),(-10,4,.9),(-8,10,1),(0,10.9,1.05),(9.8,6,.9),
              (10.5,-.7,.85),(-2.8,5,.65),(2.0,7,.7),(-4,-7,.7),
              (8,-6,.7),(5.6,-7.2,.7),(-12,-5,.8),(11,8,.8),(-4.3,11.2,.9)]:
    for k in range(5):
        p=(x+random.uniform(-r*.6,r*.6),y+random.uniform(-r*.6,r*.6),random.uniform(.35,.9)*r)
        tube('Bramble / woody stem',[(x,y,.06),p],[.032,.008],wooddark,5)
        cluster(p,r*.70,125,.23)

def inside_house(x,y,margin=.18):
    for cx,cy,w,d,a in houses:
        dx=x-cx;dy=y-cy
        lx=dx*math.cos(a)+dy*math.sin(a)
        ly=-dx*math.sin(a)+dy*math.cos(a)
        if abs(lx)<w/2+margin and abs(ly)<d/2+margin:return True
    return False

# Long grass occupies every undisturbed place; the path remains legible.
for i in range(6900):
    x=random.uniform(-15,15); y=random.uniform(-14.3,14.3)
    if (x/15.2)**2+(y/14.4)**2>1:continue
    if inside_house(x,y):continue
    if (x-wx)**2+(y-wy)**2<1.5:continue
    onlane=abs(x-pathx(y))<1.12
    if onlane and random.random()<.89:continue
    h=random.uniform(.15,.46) if not onlane else random.uniform(.10,.23)
    grass_tuft(x,y,groundz(x,y)+.018,h)
    if random.random()<.018 and not onlane:
        # A few tiny flowers among the long grass, not a planted garden.
        top=Vector((x+.05,y,h+.10))
        for j in range(5):
            a=j*math.tau/5
            flowers.leaf(top+Vector((.036*math.cos(a),.036*math.sin(a),0)),.065,0)

for i in range(500):
    x=random.uniform(-14,14);y=random.uniform(-13.5,13.5)
    if (x/15)**2+(y/14)**2>1 or inside_house(x,y):continue
    litter.leaf((x,y,groundz(x,y)+.035),random.uniform(.055,.12),0)

# Small mossy stones, and one fallen branch.
for i in range(85):
    x=random.uniform(-14,14);y=random.uniform(-13,13)
    if (x/15)**2+(y/14)**2>1 or inside_house(x,y) or abs(x-pathx(y))<1.3:continue
    s=random.uniform(.10,.32)
    ico('Scattered fieldstone',(x,y,groundz(x,y)+s*.28),(s,s*.72,s*.48),stone)
    if random.random()<.5:
        ico('Moss on fieldstone',(x-.03,y,groundz(x,y)+s*.65),(s*.8,s*.6,.04),moss)
tube('Fallen tree limb',[(3.1,-9.5,.16),(4.2,-9.0,.21),(5.1,-8.9,.12),(5.8,-8.2,.08)],
     [.14,.11,.08,.025],bark)
tube('Fallen limb fork',[(4.1,-9.0,.2),(4.5,-8.2,.14),(4.4,-7.6,.08)],[.08,.04,.008],bark)
leaves.finish()
grasses.finish()
litter.finish()
flowers.finish()

# --------------------------------------------------------------------
# Presentation: quiet late-afternoon light, readable roofs and interiors.
# --------------------------------------------------------------------
box('Matte ground below the landscape',(0,0,-.99),(200,200,.15),backdrop)
world=bpy.data.worlds.new('Soft open sky')
bpy.context.scene.world=world
world.use_nodes=True
world.node_tree.nodes['Background'].inputs[0].default_value=(.53,.64,.62,1)
world.node_tree.nodes['Background'].inputs[1].default_value=.40

def area(name,location,energy,color,size,target):
    data=bpy.data.lights.new(name,'AREA')
    data.energy=energy;data.color=color;data.shape='DISK';data.size=size
    ob=bpy.data.objects.new(name,data);bpy.context.collection.objects.link(ob)
    ob.location=location
    ob.rotation_euler=(Vector(target)-ob.location).to_track_quat('-Z','Y').to_euler()
    return ob
area('Large warm break in the clouds',(-13,-10,24),2400,(1.0,.83,.62),11,(0,0,0))
area('Cool sky fill',(10,-3,17),1300,(.68,.81,1.0),12,(0,2,1))
area('Gentle canopy rim',(-3,15,20),1900,(1.0,.90,.71),9,(0,3,2))
sun_data=bpy.data.lights.new('Low afternoon sun','SUN')
sun_data.energy=1.65;sun_data.angle=.20;sun_data.color=(1.0,.88,.70)
sun=bpy.data.objects.new('Low afternoon sun',sun_data)
bpy.context.collection.objects.link(sun)
sun.rotation_euler=(math.radians(27),math.radians(-24),math.radians(-32))

camera_data=bpy.data.cameras.new('Village presentation camera')
camera=bpy.data.objects.new('Village presentation camera',camera_data)
bpy.context.collection.objects.link(camera)
camera.location=(26,-37,29)
target=Vector((0,.4,2.1))
camera.rotation_euler=(target-camera.location).to_track_quat('-Z','Y').to_euler()
camera_data.type='ORTHO'
camera_data.ortho_scale=39.5
camera_data.lens=48
bpy.context.scene.camera=camera

scene=bpy.context.scene
scene.render.engine='CYCLES'
scene.cycles.samples=64
scene.cycles.use_denoising=True
scene.render.resolution_x=1536
scene.render.resolution_y=1536
scene.render.resolution_percentage=100
scene.render.image_settings.file_format='PNG'
scene.render.film_transparent=False
scene.view_settings.view_transform='AgX'
try:
    scene.view_settings.look='AgX - Medium High Contrast'
except Exception:
    pass
scene.view_settings.exposure=.25
scene.render.filepath='/mnt/data/village.png'
scene.camera.data.limits=None if False else scene.camera.data.limits if hasattr(scene.camera.data,'limits') else None
```