import bpy, math, mathutils, random
from mathutils import Vector
from math import radians, sin, cos, atan2, sqrt

# ---------- clean scene ----------
if bpy.ops.object.mode_set.poll():
    try: bpy.ops.object.mode_set(mode='OBJECT')
    except: pass
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
for datablocks in (bpy.data.meshes, bpy.data.curves, bpy.data.materials, bpy.data.cameras, bpy.data.lights):
    # leave datablocks to Blender cleanup unless unused; object deletion is enough
    pass

scene = bpy.context.scene
scene.render.engine = 'BLENDER_EEVEE_NEXT'
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
try:
    scene.view_settings.look = 'AgX - Medium High Contrast'
except:
    pass
try:
    scene.view_settings.view_transform = 'AgX'
except:
    pass
scene.view_settings.exposure = 0.0
scene.view_settings.gamma = 1.0

# ---------- collections ----------
root = scene.collection
collections = {}
for nm in ['ARCHITECTURE','CHARACTER','GROUND','LIGHTING','DETAILS']:
    c = bpy.data.collections.new(nm)
    root.children.link(c)
    collections[nm] = c

def move_to(obj, coll):
    # unlink from all collections and link to target
    for c in list(obj.users_collection):
        c.objects.unlink(obj)
    collections[coll].objects.link(obj)
    return obj

# ---------- materials ----------
def make_mat(name, color, metallic=0.0, rough=0.5, alpha=1.0, transmission=0.0, emission=None, emission_strength=0.0):
    m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes = True
    bs = m.node_tree.nodes.get('Principled BSDF')
    if bs:
        if bs.inputs.get('Base Color'): bs.inputs['Base Color'].default_value = (*color, alpha)
        if bs.inputs.get('Metallic'): bs.inputs['Metallic'].default_value = metallic
        if bs.inputs.get('Roughness'): bs.inputs['Roughness'].default_value = rough
        if bs.inputs.get('Alpha'): bs.inputs['Alpha'].default_value = alpha
        if bs.inputs.get('Transmission Weight'): bs.inputs['Transmission Weight'].default_value = transmission
        elif bs.inputs.get('Transmission'): bs.inputs['Transmission'].default_value = transmission
        if emission is not None:
            if bs.inputs.get('Emission Color'): bs.inputs['Emission Color'].default_value = (*emission, 1)
            elif bs.inputs.get('Emission'): bs.inputs['Emission'].default_value = (*emission, 1)
            if bs.inputs.get('Emission Strength'): bs.inputs['Emission Strength'].default_value = emission_strength
    m.diffuse_color = (*color, alpha)
    if alpha < 0.999:
        try: m.surface_render_method = 'DITHERED'
        except: pass
        try: m.use_transparency_overlap = False
        except: pass
    return m

def make_noise_mat(name, c1, c2, scale=4.0, rough=0.7, metallic=0.0, bump=0.15):
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    nt=m.node_tree; nt.nodes.clear()
    out=nt.nodes.new('ShaderNodeOutputMaterial')
    bs=nt.nodes.new('ShaderNodeBsdfPrincipled')
    tex=nt.nodes.new('ShaderNodeTexNoise'); tex.inputs['Scale'].default_value=scale; tex.inputs['Detail'].default_value=3.0; tex.inputs['Roughness'].default_value=rough
    ramp=nt.nodes.new('ShaderNodeValToRGB')
    ramp.color_ramp.elements[0].color=(*c1,1); ramp.color_ramp.elements[1].color=(*c2,1)
    bumpn=nt.nodes.new('ShaderNodeBump'); bumpn.inputs['Strength'].default_value=bump; bumpn.inputs['Distance'].default_value=0.12
    nt.links.new(tex.outputs['Fac'],ramp.inputs['Fac']); nt.links.new(ramp.outputs['Color'],bs.inputs['Base Color']); nt.links.new(tex.outputs['Fac'],bumpn.inputs['Height']); nt.links.new(bumpn.outputs['Normal'],bs.inputs['Normal']); nt.links.new(bs.outputs['BSDF'],out.inputs['Surface'])
    bs.inputs['Roughness'].default_value=0.78; bs.inputs['Metallic'].default_value=metallic
    m.diffuse_color=(*c1,1)
    return m

ground_mat = make_noise_mat('Dust_ochre_grit', (0.095,0.046,0.018),(0.24,0.135,0.055),scale=3.2,rough=.82,bump=.28)
wall_mat = make_noise_mat('Weathered_colony_planks', (0.17,0.075,0.027),(0.38,0.20,0.07),scale=5.5,rough=.82,bump=.2)
wall_dark = make_mat('Board_shadow', (0.055,0.022,0.010), metallic=0, rough=.88)
wood = make_mat('Porch_wood', (0.20,0.075,0.025), rough=.72)
wood_light = make_mat('Worn_edge_wood', (0.34,0.14,0.045), rough=.68)
coat = make_noise_mat('Duster_brown_canvas', (0.075,0.018,0.008),(0.25,0.075,0.018),scale=7,rough=.78,bump=.16)
coat_dark = make_mat('Duster_fold_shadow', (0.045,0.010,0.004), rough=.88)
leather = make_noise_mat('Belt_and_holster_leather', (0.025,0.006,0.003),(0.14,0.028,0.008),scale=11,rough=.68,bump=.2)
leather_edge = make_mat('Leather_edges', (0.22,0.055,0.012), rough=.55)
suit = make_mat('Vac_suit_navy', (0.012,0.027,0.065), metallic=.15, rough=.34)
suit_high = make_mat('Vac_suit_seams', (0.035,0.095,0.14), metallic=.25, rough=.27)
polymer = make_mat('Hard_polymer', (0.018,0.032,0.044), metallic=.18, rough=.24)
polymer_light = make_mat('Polymer_highlights', (0.10,0.16,0.19), metallic=.35, rough=.22)
metal = make_mat('Brushed_steel', (0.20,0.25,0.28), metallic=.88, rough=.25)
metal_dark = make_mat('Oxidized_steel', (0.055,0.072,0.075), metallic=.78, rough=.42)
brass = make_mat('Aged_brass', (0.42,0.17,0.035), metallic=.78, rough=.28)
copper = make_mat('Copper_conduit', (0.32,0.07,0.018), metallic=.82, rough=.30)
red = make_mat('Warning_red', (0.30,0.018,0.008), metallic=.15, rough=.30)
cyan = make_mat('Energy_cyan', (0.015,0.18,0.32), metallic=.25, rough=.18, emission=(0.01,0.3,0.8), emission_strength=5.0)
orange = make_mat('Interior_amber', (0.42,0.075,0.012), rough=.34, emission=(1.0,0.12,0.015), emission_strength=4.0)
window_mat = make_mat('Dusty_window_glass', (0.045,0.09,0.085), metallic=.05, rough=.24, alpha=.44, transmission=.25)
glass = make_mat('Helmet_dome_glass', (0.05,0.19,0.27), metallic=.05, rough=.10, alpha=.24, transmission=.68)
skin = make_mat('Warm_skin', (0.46,0.20,0.105), rough=.48)
skin_light = make_mat('Face_highlight', (0.60,0.30,0.16), rough=.43)
hair = make_mat('Hair_and_beard', (0.022,0.009,0.004), rough=.63)
eye_white = make_mat('Eye_white', (0.72,0.70,0.60), rough=.3)
iris = make_mat('Iris', (0.015,0.06,0.075), metallic=.1, rough=.2)
glove = make_mat('Sealed_glove', (0.055,0.10,0.13), metallic=.25, rough=.30)
dust_dark = make_mat('Footprint_dust', (0.035,0.016,0.007), rough=.95)
sign_mat = make_mat('Sign_paint_cream', (0.78,0.49,0.16), metallic=.1, rough=.48)
sign_dark = make_mat('Sign_backing', (0.075,0.018,0.006), rough=.60)

# ---------- geometry helpers ----------
def assign_mat(obj, material):
    if material: obj.data.materials.append(material)

def bevel(obj, width=0.04, segments=2):
    if width <= 0: return obj
    mod=obj.modifiers.new('Softened manufactured edges','BEVEL'); mod.width=width; mod.segments=segments
    try: mod.limit_method='ANGLE'
    except: pass
    return obj

def cube(name, loc, dims, material, rot=(0,0,0), bev=0.04, coll='DETAILS'):
    bpy.ops.mesh.primitive_cube_add(location=loc, rotation=rot)
    o=bpy.context.object; o.name=name; o.dimensions=dims
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    assign_mat(o,material); bevel(o,bev,3 if bev>0.05 else 2); move_to(o,coll)
    return o

def uv(name, loc, scale, material, rot=(0,0,0), coll='DETAILS', seg=32, rings=20):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=seg, ring_count=rings, location=loc, rotation=rot)
    o=bpy.context.object; o.name=name; o.scale=scale
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    assign_mat(o,material); bpy.ops.object.shade_smooth(); move_to(o,coll)
    return o

def ico(name, loc, scale, material, coll='GROUND', subdivisions=2):
    bpy.ops.mesh.primitive_ico_sphere_add(subdivisions=subdivisions, radius=1, location=loc)
    o=bpy.context.object; o.name=name; o.scale=scale; bpy.ops.object.transform_apply(location=False,rotation=False,scale=True); assign_mat(o,material); bpy.ops.object.shade_smooth(); move_to(o,coll); return o

def cyl_between(name, p1, p2, radius, material, coll='DETAILS', verts=24, bev=0.015):
    p1,p2=Vector(p1),Vector(p2); d=p2-p1; L=d.length
    if L<1e-5: return None
    bpy.ops.mesh.primitive_cylinder_add(vertices=verts, radius=radius, depth=L, location=(p1+p2)/2)
    o=bpy.context.object; o.name=name; o.rotation_mode='QUATERNION'; o.rotation_quaternion=d.to_track_quat('Z','Y'); o.rotation_mode='XYZ'
    assign_mat(o,material); bevel(o,bev,2); move_to(o,coll); return o

def cone_between(name,p1,p2,r1,r2,material,coll='DETAILS',verts=24,bev=.012):
    p1,p2=Vector(p1),Vector(p2); d=p2-p1; L=d.length
    bpy.ops.mesh.primitive_cone_add(vertices=verts, radius1=r1, radius2=r2, depth=L, location=(p1+p2)/2)
    o=bpy.context.object; o.name=name; o.rotation_mode='QUATERNION'; o.rotation_quaternion=d.to_track_quat('Z','Y'); o.rotation_mode='XYZ'; assign_mat(o,material); bevel(o,bev,2); move_to(o,coll); return o

def torus(name, loc, major, minor, material, scale=(1,1,1), rot=(0,0,0), coll='DETAILS'):
    bpy.ops.mesh.primitive_torus_add(major_radius=major, minor_radius=minor, major_segments=48, minor_segments=12, location=loc, rotation=rot)
    o=bpy.context.object; o.name=name; o.scale=scale; bpy.ops.object.transform_apply(location=False,rotation=False,scale=True); assign_mat(o,material); bpy.ops.object.shade_smooth(); move_to(o,coll); return o

def curve_tube(name, points, radius, material, coll='DETAILS', resolution=2):
    cu=bpy.data.curves.new(name+'_Curve','CURVE'); cu.dimensions='3D'; cu.resolution_u=resolution; cu.bevel_depth=radius; cu.bevel_resolution=3; cu.resolution_u=12
    spl=cu.splines.new('BEZIER'); spl.bezier_points.add(len(points)-1)
    for bp,co in zip(spl.bezier_points,points): bp.co=co; bp.handle_left_type='AUTO'; bp.handle_right_type='AUTO'
    o=bpy.data.objects.new(name,cu); collections[coll].objects.link(o); assign_mat(o,material)
    # make it real mesh for GLB fidelity
    bpy.context.view_layer.objects.active=o; o.select_set(True)
    try: bpy.ops.object.convert(target='MESH')
    except: pass
    o=bpy.context.object; o.name=name
    for c in list(o.users_collection):
        if c!=collections[coll]: c.objects.unlink(o)
    if collections[coll] not in o.users_collection: collections[coll].objects.link(o)
    o.select_set(False)
    return o

def poly_extrude(name, xz_points, yfront, depth, material, coll='DETAILS', bev=.025):
    # polygon in x,z, front toward camera at lower y, solid extruded toward +y
    n=len(xz_points); verts=[]
    for x,z in xz_points: verts.append((x,yfront,z))
    for x,z in xz_points: verts.append((x,yfront+depth,z))
    faces=[]
    faces.append(tuple(range(n)))
    faces.append(tuple(range(2*n-1,n-1,-1)))
    for i in range(n):
        j=(i+1)%n; faces.append((i,j,n+j,n+i))
    me=bpy.data.meshes.new(name+'_Mesh'); me.from_pydata(verts,[],faces); me.update()
    o=bpy.data.objects.new(name,me); collections[coll].objects.link(o); assign_mat(o,material); bevel(o,bev,2); return o

def annular_ring(name, loc, outer, inner, z0, z1, material, coll='DETAILS', segments=48):
    # elliptical radii outer=(rx,ry), inner=(rx,ry)
    verts=[]
    for z in [z0,z1]:
        for rx,ry in [outer,inner]:
            for i in range(segments):
                a=2*math.pi*i/segments; verts.append((loc[0]+rx*cos(a),loc[1]+ry*sin(a),z))
    # index layer: bottom outer 0, bottom inner S, top outer 2S, top inner3S
    S=segments; faces=[]
    for i in range(S):
        j=(i+1)%S
        faces += [( (i),(j),(2*S+j),(2*S+i) ), ((S+i),(3*S+i),(3*S+j),(S+j)), ((2*S+i),(2*S+j),(3*S+j),(3*S+i)), ((i),(S+i),(S+j),(j))]
    me=bpy.data.meshes.new(name+'_Mesh'); me.from_pydata(verts,[],faces); me.update(); o=bpy.data.objects.new(name,me); collections[coll].objects.link(o); assign_mat(o,material); bevel(o,.018,2); return o

def text_obj(name, text, loc, size, material, extrude=.025, rot=(radians(90),0,0), coll='ARCHITECTURE'):
    cu=bpy.data.curves.new(name+'_TextCurve','FONT'); cu.body=text; cu.align_x='CENTER'; cu.align_y='CENTER'; cu.size=size; cu.extrude=extrude; cu.bevel_depth=.008; cu.bevel_resolution=2; cu.resolution_u=10
    o=bpy.data.objects.new(name,cu); collections[coll].objects.link(o); o.location=loc; o.rotation_euler=rot; assign_mat(o,material)
    bpy.context.view_layer.objects.active=o; o.select_set(True)
    try: bpy.ops.object.convert(target='MESH')
    except: pass
    o=bpy.context.object; o.name=name; o.select_set(False); return o

# ---------- GROUND ----------
cube('Dusty_terrain_base',(2.5,1.6,-.12),(18,15,.22),ground_mat,bev=.08,coll='GROUND')
# subtle ground strata patches
for i,(x,y,sx,sy,rot) in enumerate([(-2,0.8,2.5,0.8,.12),(4.5,-1.4,3.8,1.2,-.15),(7.8,3.8,2.8,1.4,.25),(-1.5,4.8,1.8,.8,-.2)]):
    o=cube('Dust_rutted_patch_%02d'%i,(x,y,.015),(sx,sy,.025),dust_dark,rot=(0,0,rot),bev=.04,coll='GROUND')
# stones and grit scatter deliberately placed, not noise-only
random.seed(12)
for i in range(42):
    x=random.uniform(-3.8,8.6); y=random.uniform(-1.8,5.3)
    # keep a little clear around main boots
    if -0.8<x<1.4 and .6<y<2.5: continue
    sc=random.uniform(.025,.09)
    ico('Grit_stone_%02d'%i,(x,y,.04+sc*.25),(sc*1.5,sc,sc*.7),metal_dark if i%7==0 else wall_dark,coll='GROUND',subdivisions=1)
# larger scrap chunks
for i,(loc,dim,rot) in enumerate([
    ((-1.9,.15,.14),(.75,.18,.18),(.2,.1,-.25)),((7.5,-.1,.18),(.52,.24,.25),(.1,-.3,.5)),
    ((-2.9,2.5,.12),(.45,.22,.16),(.3,.2,-.8)),((6.9,4.2,.22),(.8,.12,.28),(.1,.5,.2))]):
    cube('Colony_scrap_%02d'%i,loc,dim,metal_dark,rot=rot,bev=.04,coll='GROUND')
# cable tumbleweed: a loose secondary coil
for i in range(3):
    a=i*2.1
    pts=[(6.9+0.35*cos(a),1.0+0.35*sin(a),.12),(7.2+0.42*cos(a+.8),1.1+0.35*sin(a+.8),.26),(7.45+0.32*cos(a+1.7),1.0+0.32*sin(a+1.7),.13)]
    curve_tube('Loose_cable_coil_%d'%i,pts,.025,copper,'GROUND')
# footprints, toe and heel as sculpted dark shallow shapes
foot_specs=[(-.05,.78,0.0),(-.18,.22,.10),(-.30,-.28,.08),(-.42,-.78,.18)]
for i,(x,y,a) in enumerate(foot_specs):
    # oriented shoe tread silhouette with toe and heel
    cube('Boot_print_%02d_toe'%i,(x,y,.028),(.24,.38,.025),dust_dark,rot=(0,0,a),bev=.08,coll='GROUND')
    cube('Boot_print_%02d_heel'%i,(x,y-.23,.031),(.19,.18,.027),dust_dark,rot=(0,0,a),bev=.045,coll='GROUND')
# metal tread markings on ground
for i in range(5):
    cube('Tread_mark_%02d'%i,(-.10-.04*i,.30-.15*i,.045),(.12,.035,.012),metal_dark,rot=(0,0,.12),bev=.01,coll='GROUND')

# ---------- SALOON ARCHITECTURE ----------
# shells and side corner
cube('Saloon_front_shell',(4.62,2.55,2.82),(7.55,.30,5.64),wall_mat,bev=.035,coll='ARCHITECTURE')
cube('Saloon_side_shell',(.86,4.35,2.82),(.30,3.9,5.64),wall_mat,bev=.035,coll='ARCHITECTURE')
# alternating vertical plank seams on front and side
for i,x in enumerate([1.25,1.72,2.18,2.67,3.15,3.66,4.12,5.35,5.86,6.34,6.84,7.32,7.82,8.16]):
    cube('Front_board_seam_%02d'%i,(x,2.365,2.78),(.035,.055,5.38),wall_dark,bev=.008,coll='ARCHITECTURE')
for i,y in enumerate([2.85,3.35,3.88,4.43,4.96,5.48,5.95]):
    cube('Side_board_seam_%02d'%i,(.695,y,2.78),(.05,.035,5.36),wall_dark,bev=.008,coll='ARCHITECTURE')
# metal corner and structural rails
cube('Corner_riveted_upright',(.66,2.31,2.88),(.24,.24,5.8),metal_dark,bev=.035,coll='ARCHITECTURE')
cube('Facade_top_rail',(4.6,2.30,5.53),(7.65,.21,.22),metal_dark,bev=.035,coll='ARCHITECTURE')
cube('Side_top_rail',(.69,4.30,5.53),(.22,3.95,.22),metal_dark,bev=.035,coll='ARCHITECTURE')
cube('Facade_base_rail',(4.6,2.29,.38),(7.65,.20,.22),metal_dark,bev=.03,coll='ARCHITECTURE')
# porch decking and front edge
cube('Covered_porch_deck',(4.45,1.68,.14),(8.15,1.75,.28),wood,bev=.06,coll='ARCHITECTURE')
for i in range(16):
    x=.55+i*.51
    cube('Porch_plank_%02d'%i,(x,1.68,.30),(.035,1.55,.035),wood_light,bev=.008,coll='ARCHITECTURE')
# porch fascia and beams
cube('Porch_front_fascia',(4.45,.83,4.02),(8.2,.18,.36),wood_light,bev=.045,coll='ARCHITECTURE')
cube('Porch_back_beam',(4.45,2.22,4.32),(8.2,.18,.30),metal_dark,bev=.04,coll='ARCHITECTURE')
# individual sloped roof plates across width
for i in range(9):
    x=.85+i*.86
    cube('Corrugated_awning_panel_%02d'%i,(x,1.54,4.20),(.78,1.72,.12),metal_dark if i%3 else brass,rot=(radians(10),0,0),bev=.025,coll='ARCHITECTURE')
    # raised ridges on each panel
    for j in range(2):
        yy=1.0+j*.65
        cube('Awning_rib_%02d_%d'%(i,j),(x,yy,4.25),(.035,.57,.035),metal,rot=(radians(10),0,0),bev=.008,coll='ARCHITECTURE')
# posts
for i,(x,y,z,h) in enumerate([(1.00,.96,2.10,3.75),(7.95,.96,2.10,3.75),(3.42,.96,2.02,3.55),(5.95,.96,2.02,3.55)]):
    cube('Porch_support_post_%02d'%i,(x,y,z),(.28,.30,h),wood_light,bev=.045,coll='ARCHITECTURE')
    cube('Post_metal_boot_%02d'%i,(x,y,.38),(.38,.38,.18),metal_dark,bev=.025,coll='ARCHITECTURE')
    # post bolts facing camera
    for zz in [.8,2.0,3.25]:
        cyl_between('Post_bolt_%02d_%s'%(i,str(zz)),(x-.16,y-.17,zz),(x-.16,y-.205,zz),.035,brass,'ARCHITECTURE',verts=16,bev=.005)
# entrance opening and warm interior
cube('Airlock_interior_void',(4.62,2.345,2.02),(1.72,.08,3.10),wall_dark,bev=.01,coll='ARCHITECTURE')
cube('Airlock_warm_backlight',(4.62,2.26,2.05),(1.36,.025,2.74),orange,bev=.015,coll='ARCHITECTURE')
# dark interior offset to show doors
cube('Airlock_inner_depth',(4.62,2.20,2.05),(1.26,.03,2.5),polymer,bev=.01,coll='ARCHITECTURE')
# door frame structural pieces
cube('Airlock_left_frame',(3.72,2.20,2.08),(.22,.30,3.18),metal,bev=.035,coll='ARCHITECTURE')
cube('Airlock_right_frame',(5.52,2.20,2.08),(.22,.30,3.18),metal,bev=.035,coll='ARCHITECTURE')
cube('Airlock_top_frame',(4.62,2.20,3.60),(1.95,.30,.22),metal,bev=.035,coll='ARCHITECTURE')
# swinging segmented doors, canted open
leftdoor=cube('Swinging_airlock_door_L',(4.18,2.04,1.92),(.82,.13,2.45),metal_dark,rot=(0,0,radians(-16)),bev=.045,coll='ARCHITECTURE')
rightdoor=cube('Swinging_airlock_door_R',(5.04,2.04,1.92),(.82,.13,2.45),metal_dark,rot=(0,0,radians(16)),bev=.045,coll='ARCHITECTURE')
# door panel inserts and portholes
for side,x in [('L',4.18),('R',5.04)]:
    cube('Door_panel_insert_'+side,(x,1.965,1.92),(.55,.025,1.92),polymer_light,bev=.02,coll='ARCHITECTURE')
    # porthole disk and rim on camera-facing side
    cyl_between('Door_porthole_'+side,(x,1.91,2.48),(x,1.865,2.48),.18,window_mat,'ARCHITECTURE',verts=32,bev=.008)
    torus('Door_porthole_rim_'+side,(x,1.85,2.48),.18,.025,brass,rot=(radians(90),0,0),coll='ARCHITECTURE')
    for j in range(4):
        a=j*math.pi/2
        cyl_between('Door_rivet_%s_%d'%(side,j),(x+.18*cos(a),1.855,2.48+.18*sin(a)),(x+.18*cos(a),1.82,2.48+.18*sin(a)),.025,brass,'ARCHITECTURE',verts=12,bev=.004)
# windows with dusty panes and mullions
for wi,x in enumerate([2.30,6.82]):
    cube('Dusty_window_%d'%wi,(x,2.325,3.03),(1.24,.06,1.55),window_mat,bev=.02,coll='ARCHITECTURE')
    # warm glow behind pane
    cube('Window_amber_depth_%d'%wi,(x,2.30,3.03),(1.10,.018,1.38),orange,bev=.01,coll='ARCHITECTURE')
    for xx in [-.62,.62]: cube('Window_frame_v_%d_%s'%(wi,str(xx)),(x+xx,2.24,3.03),(.10,.16,1.78),metal_dark,bev=.015,coll='ARCHITECTURE')
    for zz in [2.25,3.81]: cube('Window_frame_h_%d_%s'%(wi,str(zz)),(x,2.24,zz),(1.35,.16,.10),metal_dark,bev=.015,coll='ARCHITECTURE')
    cube('Window_mullion_v_%d'%wi,(x,2.205,3.03),(.055,.035,1.38),brass,bev=.008,coll='ARCHITECTURE')
    cube('Window_mullion_h_%d'%wi,(x,2.205,3.03),(1.12,.035,.055),brass,bev=.008,coll='ARCHITECTURE')
    # dusty streaks over panes
    for s in range(4):
        xx=x-.44+s*.27
        cube('Dust_streak_%d_%d'%(wi,s),(xx,2.185,3.14-.05*s),(.035,.018,1.10-.12*s),wall_dark,bev=.01,coll='ARCHITECTURE')
# diagonal riveted braces
brace_specs=[((1.10,2.16,.68),(2.15,2.16,4.92)),((7.95,2.16,.65),(7.10,2.16,4.85)),((1.35,2.12,4.95),(2.35,2.12,4.05))]
for bi,(p1,p2) in enumerate(brace_specs):
    cyl_between('Diagonal_reinforcement_%d'%bi,p1,p2,.075,metal_dark,'ARCHITECTURE',verts=20,bev=.012)
    for t in [.12,.32,.52,.72,.9]:
        p=Vector(p1).lerp(Vector(p2),t)
        cyl_between('Brace_rivet_%d_%02d'%(bi,int(t*100)),p,(p[0],p[1]-.065,p[2]),.045,brass,'ARCHITECTURE',verts=16,bev=.006)
# conduits and utility box
curve_tube('Facade_exposed_conduit',[(7.62,2.10,.64),(7.62,2.08,1.65),(7.80,2.08,2.30),(7.80,2.08,4.65),(8.10,2.08,4.82)],.055,copper,'ARCHITECTURE')
curve_tube('Conduit_branch',[(7.62,2.10,1.45),(7.20,2.06,1.62),(6.90,2.06,1.62)],.035,metal,'ARCHITECTURE')
cube('Utility_control_box',(7.62,2.10,1.02),(.62,.17,.78),polymer,bev=.05,coll='ARCHITECTURE')
cube('Utility_box_face',(7.62,1.99,1.08),(.42,.025,.47),metal_dark,bev=.02,coll='ARCHITECTURE')
for j in range(3):
    cyl_between('Utility_box_bolt_%d'%j,(7.44+j*.18,1.96,1.25),(7.44+j*.18,1.93,1.25),.025,brass,'ARCHITECTURE',verts=12,bev=.004)
# corner rivets visible
for z in [1.0,1.65,2.3,2.95,3.6,4.25,4.9]:
    cyl_between('Corner_rivet_%s'%str(z),(.51,2.15,z),(.48,2.11,z),.043,brass,'ARCHITECTURE',verts=16,bev=.006)
# sign backing and border above entrance
cube('SALOON_sign_back',(4.62,2.16,5.12),(4.18,.20,.88),sign_dark,bev=.05,coll='ARCHITECTURE')
cube('SALOON_sign_top',(4.62,2.02,5.55),(4.22,.12,.10),brass,bev=.02,coll='ARCHITECTURE')
cube('SALOON_sign_bottom',(4.62,2.02,4.69),(4.22,.12,.10),brass,bev=.02,coll='ARCHITECTURE')
cube('SALOON_sign_left',(2.58,2.02,5.12),(.10,.12,.88),brass,bev=.02,coll='ARCHITECTURE')
cube('SALOON_sign_right',(6.66,2.02,5.12),(.10,.12,.88),brass,bev=.02,coll='ARCHITECTURE')
text_obj('SALOON_weathered_sign','SALOON',(4.62,2.00,5.13),.64,sign_mat,.035,coll='ARCHITECTURE')
# sign scratches and small bolts
for i in range(6):
    cube('Sign_weathering_%d'%i,(3.0+i*.55,1.985,4.92+(i%2)*.33),(.18,.012,.018),wall_dark,rot=(0,0,(-.3+i*.11)),bev=.005,coll='ARCHITECTURE')
for x in [2.75,6.50]:
    for z in [4.82,5.43]: cyl_between('Sign_bolt_%s_%s'%(x,z),(x,1.96,z),(x,1.92,z),.035,metal,'ARCHITECTURE',verts=16,bev=.005)
# porch hanging lamp
cyl_between('Lamp_cord',(4.62,1.55,4.32),(4.62,1.55,3.86),.025,metal_dark,'ARCHITECTURE',verts=12,bev=.004)
uv('Warm_porch_lamp',(4.62,1.55,3.72),(.16,.16,.16),orange,coll='ARCHITECTURE')
torus('Lamp_cage',(4.62,1.55,3.72),.18,.018,metal,coll='ARCHITECTURE')

# ---------- CHARACTER: leaning space cowboy ----------
# posed torso and hips
pelvis=(.40,1.83,2.10); chest=(.64,1.84,3.23); neckbase=(.70,1.82,3.78)
uv('Cowboy_pelvis',pelvis,(.48,.31,.38),suit,rot=(0,radians(-7),radians(-2)),coll='CHARACTER')
uv('Cowboy_torso',(.56,1.83,2.73),(.52,.32,.89),suit,rot=(0,radians(-9),radians(-2)),coll='CHARACTER')
# abdomen segmented suit ribs
for i,z in enumerate([2.52,2.68,2.84,3.00]):
    torus('Suit_abdomen_rib_%d'%i,(.57,1.82,z),.36,.025,suit_high,scale=(1.0,.68,1),coll='CHARACTER')
# shoulders / collar
uv('Shoulder_L',(.20,1.82,3.37),(.24,.28,.26),suit,coll='CHARACTER')
uv('Shoulder_R',(1.03,1.86,3.35),(.25,.28,.26),suit,coll='CHARACTER')
# collar ring and pressure collar
cyl_between('Neck_skin',(.70,1.82,3.68),(.70,1.82,3.99),.16,skin,'CHARACTER',verts=24,bev=.012)
torus('Pressure_collar_ring',(.70,1.82,3.82),.245,.075,metal_dark,scale=(1,.88,1),coll='CHARACTER')
torus('Collar_blue_seal',(.70,1.82,3.86),.205,.025,cyan,scale=(1,.88,1),coll='CHARACTER')
# legs: planted left and wall-braced right
hipL=(.24,1.78,2.10); kneeL=(.06,1.51,1.18); ankleL=(-.02,1.34,.45)
hipR=(.66,1.86,2.08); kneeR=(.92,1.98,1.34); ankleR=(.74,1.78,.92)
cone_between('Left_trouser',hipL,kneeL,.23,.18,suit,'CHARACTER',verts=28,bev=.018)
cone_between('Left_lower_suit',kneeL,ankleL,.17,.13,suit,'CHARACTER',verts=28,bev=.018)
uv('Left_knee_pad',kneeL,(.20,.17,.14),polymer_light,rot=(0,radians(15),0),coll='CHARACTER')
cone_between('Right_trouser',hipR,kneeR,.23,.18,suit,'CHARACTER',verts=28,bev=.018)
cone_between('Right_lower_suit',kneeR,ankleR,.17,.13,suit,'CHARACTER',verts=28,bev=.018)
uv('Right_knee_pad',kneeR,(.20,.17,.14),polymer_light,rot=(0,radians(-20),0),coll='CHARACTER')
# boot helper rounded volumes
leftboot=cube('Mag_boot_ground',(-.08,1.17,.28),(.62,.58,.46),polymer,rot=(0,radians(-2),radians(-4)),bev=.12,coll='CHARACTER')
cube('Ground_boot_toe_cap',(-.10,.91,.33),(.48,.22,.28),metal_dark,rot=(0,radians(-2),radians(-4)),bev=.09,coll='CHARACTER')
cube('Ground_boot_sole',(-.08,1.15,.075),(.68,.62,.12),metal_dark,rot=(0,radians(-2),radians(-4)),bev=.035,coll='CHARACTER')
cube('Ground_boot_mag_strip',(-.08,1.16,.02),(.42,.34,.035),cyan,rot=(0,radians(-2),radians(-4)),bev=.01,coll='CHARACTER')
# braced boot pointing at corner wall x=.66
cube('Mag_boot_wall_braced',(.69,1.77,.87),(.62,.52,.46),polymer,rot=(0,radians(3),radians(2)),bev=.12,coll='CHARACTER')
cube('Braced_boot_toe_plate',(.97,1.77,.90),(.12,.44,.32),metal_dark,rot=(0,radians(3),radians(2)),bev=.05,coll='CHARACTER')
cube('Braced_boot_sole',(.69,1.77,.63),(.65,.55,.10),metal_dark,rot=(0,radians(3),radians(2)),bev=.035,coll='CHARACTER')
# magnet discs on braced boot, against wall side
for j,y in enumerate([1.62,1.80,1.98]):
    cyl_between('Wall_boot_magnet_%d'%j,(.995,y,.88),(.965,y,.88),.055,cyan,'CHARACTER',verts=20,bev=.008)
# coat back and split tails
poly_extrude('Duster_left_tail',[(-.03,3.53),(.34,3.62),(.34,2.42),(.24,1.18),(-.12,1.08),(-.35,1.30),(-.27,2.52)],1.91,.18,coat,'CHARACTER',bev=.035)
poly_extrude('Duster_right_tail',[(.38,3.61),(.87,3.49),(1.03,2.62),(.93,1.15),(.57,1.08),(.42,2.35)],1.88,.18,coat,'CHARACTER',bev=.035)
# coat shoulder yoke and side panels
poly_extrude('Duster_left_yoke',[(-.10,3.65),(.42,3.72),(.40,3.32),(.10,3.17),(-.22,3.30)],1.70,.16,coat,'CHARACTER',bev=.035)
poly_extrude('Duster_right_yoke',[(.42,3.72),(.98,3.60),(1.16,3.32),(.78,3.18),(.40,3.32)],1.70,.16,coat,'CHARACTER',bev=.035)
# lapels, front split, coat folds
poly_extrude('Duster_left_lapel',[(.12,3.56),(.40,3.42),(.47,2.65),(.22,2.42),(.06,3.06)],1.52,.08,coat_dark,'CHARACTER',bev=.018)
poly_extrude('Duster_right_lapel',[(.42,3.43),(.76,3.53),(.88,3.06),(.69,2.55),(.47,2.65)],1.51,.08,coat_dark,'CHARACTER',bev=.018)
# coat seam/fold tubes
for i,pts in enumerate([
    [(.02,1.49,3.25),(.06,1.47,2.65),(.02,1.48,1.35)],
    [(.27,1.43,3.10),(.22,1.42,2.52),(.18,1.44,1.25)],
    [(.73,1.43,3.13),(.78,1.42,2.45),(.73,1.43,1.25)],
    [(.93,1.57,3.22),(.95,1.57,2.48),(.86,1.58,1.34)]]):
    curve_tube('Duster_fold_%d'%i,pts,.022,coat_dark,'CHARACTER')
# coat hem piping
curve_tube('Duster_left_hem',[(-.11,1.45,1.10),(.05,1.45,1.03),(.25,1.45,1.15)],.032,leather_edge,'CHARACTER')
curve_tube('Duster_right_hem',[(.57,1.48,1.08),(.75,1.48,1.02),(.94,1.52,1.16)],.032,leather_edge,'CHARACTER')
# coat buttons
for i,z in enumerate([3.30,3.08,2.86,2.64]):
    uv('Duster_button_%d'%i,(.44,1.46,z),(.045,.025,.045),brass,coll='CHARACTER',seg=20,rings=12)
# outer sleeves and arms, relaxed left and gun-side right
shoulderL=(.20,1.80,3.38); elbowL=(-.04,1.48,2.83); wristL=(.12,1.31,2.52)
shoulderR=(1.04,1.84,3.37); elbowR=(1.24,1.56,2.88); wristR=(1.10,1.43,2.64)
cone_between('Left_duster_sleeve',shoulderL,elbowL,.22,.17,coat,'CHARACTER',verts=28,bev=.02)
cone_between('Left_suit_forearm',elbowL,wristL,.16,.12,suit,'CHARACTER',verts=28,bev=.016)
cone_between('Right_duster_sleeve',shoulderR,elbowR,.22,.17,coat,'CHARACTER',verts=28,bev=.02)
cone_between('Right_suit_forearm',elbowR,wristR,.16,.12,suit,'CHARACTER',verts=28,bev=.016)
# sleeve cuff rings
torus('Left_sealed_cuff',wristL,.145,.04,metal_dark,scale=(1,.75,1),coll='CHARACTER')
torus('Right_sealed_cuff',wristR,.145,.04,metal_dark,scale=(1,.75,1),coll='CHARACTER')
# glove hands with individually segmented fingers
def gloved_hand(prefix, center, wrist, mirror=1, relaxed=False):
    center=Vector(center); wrist=Vector(wrist)
    uv(prefix+'_palm',center,(.16,.115,.17),glove,coll='CHARACTER')
    cyl_between(prefix+'_wrist_seal',wrist,center,.11,glove,'CHARACTER',verts=24,bev=.014)
    # fingers spread visibly, each has two articulated segments
    offs=[-.12,-.06,0,.06,.12]
    for i,off in enumerate(offs):
        root=center+Vector((off, -.055, .08))
        splay=mirror*(.025 + abs(off)*.12)
        p2=root+Vector((splay,-.045,-.115-abs(off)*.03))
        p3=p2+Vector((splay*.55,-.035,-.105))
        cone_between(prefix+'_finger_%d_phalanxA'%i,root,p2,.041,.034,glove,'CHARACTER',verts=16,bev=.008)
        cone_between(prefix+'_finger_%d_phalanxB'%i,p2,p3,.036,.026,glove,'CHARACTER',verts=16,bev=.008)
        uv(prefix+'_finger_%d_knuckle'%i,p2,(.043,.038,.043),glove,coll='CHARACTER',seg=16,rings=10)
        # tiny colored knuckle patch makes fingers legible
        if i in [1,2,3]: uv(prefix+'_finger_%d_plate'%i,p2+Vector((0,-.025,.01)),(.025,.012,.016),polymer_light,coll='CHARACTER',seg=12,rings=8)
    # thumb in a different direction
    tr=center+Vector((mirror*.13,-.02,.02)); tm=tr+Vector((mirror*.13,-.08,-.08)); te=tm+Vector((mirror*.055,-.04,-.10))
    cone_between(prefix+'_thumb_A',tr,tm,.045,.034,glove,'CHARACTER',verts=16,bev=.008)
    cone_between(prefix+'_thumb_B',tm,te,.036,.025,glove,'CHARACTER',verts=16,bev=.008)
    uv(prefix+'_thumb_joint',tm,(.045,.038,.045),glove,coll='CHARACTER',seg=16,rings=10)
    return center
left_hand=gloved_hand('Relaxed_left_hand',(.12,1.30,2.46),wristL,mirror=-1)
right_hand=gloved_hand('Open_gun_hand',(1.10,1.40,2.48),wristR,mirror=1)
# cuff status bands
for name,p in [('Left',wristL),('Right',wristR)]:
    cube(name+'_cuff_indicator',(p[0],p[1]-.10,p[2]),(.09,.025,.045),cyan,bev=.01,coll='CHARACTER')
# chest life support plate and hardware, directly on front of suit
cube('Chest_life_support_plate',(.61,1.47,3.12),(.72,.13,.72),polymer,rot=(0,radians(-8),0),bev=.08,coll='CHARACTER')
cube('Chest_plate_inset',(.61,1.385,3.12),(.47,.035,.46),metal_dark,rot=(0,radians(-8),0),bev=.035,coll='CHARACTER')
# central regulator and gauges
cyl_between('Chest_regulator',(.61,1.31,3.13),(.61,1.22,3.13),.13,metal,'CHARACTER',verts=32,bev=.012)
torus('Regulator_outer_ring',(.61,1.205,3.13),.13,.022,brass,rot=(radians(90),0,0),coll='CHARACTER')
cyl_between('Regulator_glow',(.61,1.18,3.13),(.61,1.15,3.13),.068,cyan,'CHARACTER',verts=24,bev=.006)
for i,(x,z) in enumerate([(.42,3.31),(.80,3.31),(.42,2.93),(.80,2.93)]):
    cyl_between('Life_support_gauge_%d'%i,(x,1.34,z),(x,1.27,z),.055,brass,'CHARACTER',verts=20,bev=.006)
    cyl_between('Gauge_face_%d'%i,(x,1.255,z),(x,1.23,z),.034,cyan if i%2==0 else red,'CHARACTER',verts=16,bev=.004)
# chest warning stripes
for i in range(3):
    cube('Chest_warning_stripe_%d'%i,(.47+i*.09,1.23,2.78),(.055,.015,.12),red,rot=(0,0,radians(-15)),bev=.008,coll='CHARACTER')
# backpack unit behind torso
cube('Backpack_main_unit',(.68,2.10,3.05),(.78,.34,1.12),polymer,rot=(0,radians(-6),0),bev=.09,coll='CHARACTER')
cube('Backpack_backplate',(.68,2.29,3.07),(.56,.08,.88),metal_dark,rot=(0,radians(-6),0),bev=.04,coll='CHARACTER')
for i,x in enumerate([.42,.91]):
    cyl_between('Backpack_tank_%d'%(i),(x,2.26,2.66),(x,2.26,3.53),.14,metal_dark,'CHARACTER',verts=24,bev=.025)
    torus('Backpack_tank_ring_%d'%i,(x,2.26,3.13),.14,.018,brass,coll='CHARACTER')
# backpack fins and vent lights
cube('Backpack_left_fin',(.23,2.10,3.18),(.12,.22,.62),metal,rot=(0,radians(-8),0),bev=.03,coll='CHARACTER')
cube('Backpack_right_fin',(1.13,2.10,3.18),(.12,.22,.62),metal,rot=(0,radians(-8),0),bev=.03,coll='CHARACTER')
for i,z in enumerate([2.78,3.03,3.28]):
    cube('Backpack_vent_%d'%i,(.68,1.91,z),(.30,.025,.045),cyan if i==1 else metal,bev=.008,coll='CHARACTER')
# hoses from chest/collar to pack
curve_tube('Life_support_hose_A',[(.72,1.37,3.24),(.92,1.50,3.52),(1.07,1.82,3.50),(.98,2.02,3.42)],.045,copper,'CHARACTER')
curve_tube('Life_support_hose_B',[(.48,1.38,3.02),(.36,1.58,3.38),(.35,1.98,3.32),(.42,2.12,3.18)],.038,metal,'CHARACTER')
curve_tube('Collar_hose',[(.82,1.75,3.83),(.98,1.88,3.92),(1.05,2.05,3.70)],.032,cyan,'CHARACTER')
# belt, buckle, loops and cells
# torus belt encircling waist
torus('Wide_gun_belt',(.49,1.79,2.25),.48,.085,leather,scale=(1.0,.70,1),coll='CHARACTER')
cube('Gun_belt_front',(.49,1.43,2.25),(.95,.13,.16),leather,rot=(0,radians(-7),0),bev=.04,coll='CHARACTER')
cube('Gun_belt_buckle',(.49,1.34,2.26),(.27,.06,.27),brass,rot=(radians(90),0,0),bev=.025,coll='CHARACTER')
cube('Buckle_inner',(.49,1.305,2.26),(.12,.025,.12),leather,rot=(radians(90),0,0),bev=.01,coll='CHARACTER')
for i,x in enumerate([.02,.18,.80,1.00]):
    torus('Belt_loop_%d'%i,(x,1.42,2.27),.065,.018,leather_edge,scale=(1,.55,1),rot=(radians(90),0,0),coll='CHARACTER')
    cube('Power_cell_%d'%i,(x,1.33,2.42),(.10,.10,.28),polymer,bev=.025,coll='CHARACTER')
    cube('Power_cell_glow_%d'%i,(x,1.27,2.46),(.045,.018,.15),cyan,bev=.012,coll='CHARACTER')
# open holster custom shell and straps
poly_extrude('Open_leather_holster',[(.73,2.17),(1.24,2.13),(1.19,1.60),(.96,1.47),(.76,1.62)],1.28,.16,leather,'CHARACTER',bev=.045)
cube('Holster_inner_lining',(1.00,1.245,1.88),(.25,.025,.40),polymer,rot=(0,radians(-10),0),bev=.025,coll='CHARACTER')
cube('Holster_top_rim',(.99,1.20,2.13),(.52,.12,.10),leather_edge,rot=(0,radians(-8),0),bev=.025,coll='CHARACTER')
cube('Holster_leg_strap',(.96,1.25,1.57),(.45,.10,.10),leather,rot=(0,radians(-5),0),bev=.025,coll='CHARACTER')
# energy revolver: dark grip, silver frame, glowing cylinder, emitter barrel
cone_between('Energy_revolver_grip',(1.05,1.20,2.16),(1.13,1.20,1.78),.10,.075,polymer,'CHARACTER',verts=24,bev=.018)
for i in range(3):
    torus('Revolver_grip_wrap_%d'%i,(1.07+i*.018,1.20,1.86+i*.10),.08,.012,leather_edge,scale=(1,.8,1),rot=(0,radians(12),0),coll='CHARACTER')
cube('Energy_revolver_frame',(.91,1.20,2.17),(.38,.17,.20),metal,rot=(0,radians(-5),0),bev=.05,coll='CHARACTER')
cyl_between('Revolver_energy_cylinder',(.80,1.10,2.17),(.80,1.28,2.17),.12,cyan,'CHARACTER',verts=32,bev=.012)
torus('Revolver_cylinder_rim',(.80,1.10,2.17),.12,.018,brass,rot=(radians(90),0,0),coll='CHARACTER')
cyl_between('Revolver_emitter_barrel',(.72,1.20,2.19),(.30,1.20,2.24),.055,metal_dark,'CHARACTER',verts=24,bev=.012)
cyl_between('Revolver_emitter_glow',(.31,1.20,2.24),(.18,1.20,2.25),.038,cyan,'CHARACTER',verts=24,bev=.008)
torus('Revolver_muzzle_ring',(.18,1.20,2.25),.06,.018,brass,rot=(0,radians(90),0),coll='CHARACTER')
# hammer and trigger guard
cube('Revolver_hammer',(1.02,1.20,2.31),(.10,.13,.08),metal_dark,rot=(0,0,radians(-12)),bev=.025,coll='CHARACTER')
torus('Revolver_trigger_guard',(.99,1.10,2.06),.10,.018,metal_dark,scale=(1,.75,1),rot=(radians(90),0,0),coll='CHARACTER')
# helmeted head, turned toward opponent/camera
head=(.70,1.67,4.25); yaw=radians(-30); f=Vector((sin(yaw),-cos(yaw),0)); r=Vector((cos(yaw),sin(yaw),0))
uv('Cowboy_head',head,(.34,.29,.43),skin,rot=(0,radians(-3),yaw),coll='CHARACTER')
# ears
uv('Ear_left',Vector(head)+r*.31,(.085,.055,.13),skin,coll='CHARACTER')
uv('Ear_right',Vector(head)-r*.31,(.085,.055,.13),skin,coll='CHARACTER')
# face planes / eyes
for side,sgn in [('L',1),('R',-1)]:
    ec=Vector(head)+f*.285+r*(.105*sgn)+Vector((0,0,.065))
    uv('Eye_white_'+side,ec,(.070,.043,.052),eye_white,coll='CHARACTER',seg=24,rings=14)
    uv('Iris_'+side,ec+f*.050,(.034,.018,.035),iris,coll='CHARACTER',seg=20,rings=12)
    uv('Pupil_'+side,ec+f*.068,(.014,.010,.018),hair,coll='CHARACTER',seg=16,rings=10)
    # eyebrow bars angled with head
    p1=Vector(head)+f*.295+r*(.17*sgn)+Vector((0,0,.17)); p2=Vector(head)+f*.30+r*(.04*sgn)+Vector((0,0,.18))
    curve_tube('Brow_'+side,[p1,p2],.026,hair,'CHARACTER')
# nose and cheek
uv('Nose',Vector(head)+f*.34+Vector((0,0,-.015)),(.075,.095,.105),skin_light,rot=(0,0,yaw),coll='CHARACTER')
uv('Nose_tip',Vector(head)+f*.405+Vector((0,0,-.065)),(.060,.055,.055),skin_light,rot=(0,0,yaw),coll='CHARACTER')
# moustache and beard patch
uv('Moustache_L',Vector(head)+f*.315+r*.075+Vector((0,0,-.10)),(.11,.045,.045),hair,rot=(0,0,yaw),coll='CHARACTER')
uv('Moustache_R',Vector(head)+f*.315-r*.075+Vector((0,0,-.10)),(.11,.045,.045),hair,rot=(0,0,yaw),coll='CHARACTER')
uv('Beard_chin',Vector(head)+f*.29+Vector((0,0,-.22)),(.22,.10,.17),hair,rot=(0,0,yaw),coll='CHARACTER')
curve_tube('Mouth_line',[Vector(head)+f*.375+r*.075+Vector((0,0,-.14)),Vector(head)+f*.38-r*.075+Vector((0,0,-.14))],.012,red,'CHARACTER')
# helmet transparent dome, rim, visor seam
uv('Transparent_helmet_dome',(.70,1.68,4.36),(.49,.43,.58),glass,coll='CHARACTER',seg=40,rings=24)
torus('Helmet_dome_lower_rim',(.70,1.68,4.02),.37,.032,metal,scale=(1,.88,1),coll='CHARACTER')
torus('Helmet_dome_upper_rim',(.70,1.68,4.72),.43,.028,metal_dark,scale=(1,.88,1),coll='CHARACTER')
curve_tube('Helmet_visor_seam',[(.34,1.49,4.48),(.70,1.36,4.58),(1.07,1.49,4.48)],.016,cyan,'CHARACTER')
# hat brim and open crown surrounding dome
bpy.ops.mesh.primitive_cylinder_add(vertices=64, radius=1, depth=.12, location=(.70,1.68,4.78))
hatbrim=bpy.context.object; hatbrim.name='Wide_frontier_hat_brim'; hatbrim.scale=(.72,.60,1); bpy.ops.object.transform_apply(location=False,rotation=False,scale=True); assign_mat(hatbrim,leather); bevel(hatbrim,.035,3); move_to(hatbrim,'CHARACTER')
annular_ring('Hat_open_crown',(.70,1.68),(.45,.37),(.31,.27),4.72,5.02,leather,'CHARACTER',segments=48)
torus('Hatband',(.70,1.68,4.78),.36,.045,leather_edge,scale=(1,.83,1),coll='CHARACTER')
cube('Hatband_badge',(.70,1.34,4.80),(.16,.035,.20),brass,rot=(radians(90),0,0),bev=.018,coll='CHARACTER')
# hat crown side creases
for a in [0.3,1.9,3.5,5.1]:
    p1=(.70+.35*cos(a),1.68+.29*sin(a),4.80); p2=(.70+.40*cos(a),1.68+.33*sin(a),4.98)
    curve_tube('Hat_crown_crease_%s'%str(a),[p1,p2],.012,coat_dark,'CHARACTER')

# ---------- lighting ----------
world=scene.world or bpy.data.worlds.new('World'); scene.world=world; world.use_nodes=True
world.node_tree.nodes['Background'].inputs['Color'].default_value=(0.008,0.015,0.035,1)
world.node_tree.nodes['Background'].inputs['Strength'].default_value=.22

def area_light(name, loc, energy, color, size, target):
    data=bpy.data.lights.new(name,'AREA'); data.energy=energy; data.color=color; data.shape='DISK'; data.size=size
    o=bpy.data.objects.new(name,data); collections['LIGHTING'].objects.link(o); o.location=loc; o.rotation_euler=(Vector(target)-Vector(loc)).to_track_quat('-Z','Y').to_euler(); return o

def point_light(name,loc,energy,color, radius=.2):
    data=bpy.data.lights.new(name,'POINT'); data.energy=energy; data.color=color; data.shadow_soft_size=radius
    o=bpy.data.objects.new(name,data); collections['LIGHTING'].objects.link(o); o.location=loc; return o
# low, hard warm key from camera-left creates wall/ground shadow
area_light('Warm_low_key',(-5.5,-6.5,6.0),1100,(1.0,.26,.08),1.2,(1.2,1.8,2.4))
# cool fill from high camera right
area_light('Cool_sky_fill',(5.0,-4.0,7.5),760,(.10,.28,1.0),5.0,(2.0,2.1,2.5))
# subtle rim
area_light('Cool_rim',(2.8,5.0,5.4),1000,(.08,.22,1.0),2.5,(.6,1.8,3.0))
point_light('Saloon_interior_glow',(4.62,1.75,2.4),180,(1.0,.10,.02),.35)
point_light('Lamp_warm_pool',(4.62,1.50,3.72),120,(1.0,.20,.04),.2)
# camera
cam_data=bpy.data.cameras.new('Presentation_Camera'); cam=bpy.data.objects.new('Presentation_Camera',cam_data); collections['LIGHTING'].objects.link(cam)
scene.camera=cam; cam.location=(-7.9,-11.8,3.35); target=(2.55,2.00,2.72); cam.rotation_euler=(Vector(target)-cam.location).to_track_quat('-Z','Y').to_euler(); cam.data.lens=52; cam.data.sensor_width=36
cam.data.dof.use_dof=False
# add a tiny camera framing marker as custom props
cam['composition']='Chest-height three-quarter; full cowboy and SALOON entrance'; cam['subject']='Space cowboy leaning on corner'

# ---------- scene metadata / render ----------
scene['benchmark_task']='Space cowboy duel at weathered frontier saloon'
scene['requirements']='Full modeled human; individually posed fingers; contact boot/wall/ground; saloon facade and airlock doors'
scene['character_name']='The Last Light Gunslinger'
# use cycles for saved native-fidelity scene
scene.render.engine='BLENDER_EEVEE_NEXT'
scene.render.filepath='/tmp/space_cowboy_saloon_preview.png'
# compositor slight glare only for emissive readability, no reliance on it
scene.use_nodes=True
nt=scene.node_tree; nt.nodes.clear(); rl=nt.nodes.new('CompositorNodeRLayers'); glare=nt.nodes.new('CompositorNodeGlare'); glare.glare_type='FOG_GLOW'; glare.quality='HIGH'; glare.threshold=2.0; glare.size=6; comp=nt.nodes.new('CompositorNodeComposite'); nt.links.new(rl.outputs['Image'],glare.inputs['Image']); nt.links.new(glare.outputs['Image'],comp.inputs['Image'])
# select camera for nice viewport
bpy.context.view_layer.objects.active=cam; cam.select_set(True)
# save
bpy.ops.wm.save_as_mainfile(filepath='/tmp/space_cowboy_saloon.blend')
result={'objects':len(bpy.data.objects),'meshes':len(bpy.data.meshes),'materials':len(bpy.data.materials),'camera':cam.name,'file':'/tmp/space_cowboy_saloon.blend'}


Traceback (most recent call last):
  File "<repo>\.runner\blender-mcp-v1.0.0\addon\mcp_to_blender_server.py", line 230, in _execute_code
    exec(code, namespace)
    ~~~~^^^^^^^^^^^^^^^^^
  File "<string>", line 16, in <module>
TypeError: bpy_struct: item.attr = val: enum "BLENDER_EEVEE_NEXT" not found in ('BLENDER_EEVEE', 'BLENDER_WORKBENCH', 'CYCLES')