import bpy, math, random, os
from mathutils import Vector
from math import sin, cos, pi

# -------------------- scene reset --------------------
random.seed(17)
for obj in list(bpy.data.objects):
    bpy.data.objects.remove(obj, do_unlink=True)
for datablocks in (bpy.data.materials, bpy.data.curves, bpy.data.meshes, bpy.data.cameras, bpy.data.lights):
    pass
scene = bpy.context.scene
scene.world.color = (0.008, 0.015, 0.04)

# collections
root = scene.collection
collections = {}
for name in ['ENVIRONMENT', 'VENDING_MACHINE', 'SHRINE_DETAILS', 'WEATHERING', 'LIGHTS']:
    c = bpy.data.collections.new(name)
    root.children.link(c)
    collections[name] = c

# -------------------- materials --------------------
def make_mat(name, color, rough=0.45, metallic=0.0, emission=None, estrength=0.0, noise=False):
    m = bpy.data.materials.new(name)
    m.diffuse_color = (*color, 1)
    m.use_nodes = True
    nt = m.node_tree
    nt.nodes.clear()
    out = nt.nodes.new('ShaderNodeOutputMaterial')
    bs = nt.nodes.new('ShaderNodeBsdfPrincipled')
    bs.inputs['Base Color'].default_value = (*color, 1)
    bs.inputs['Roughness'].default_value = rough
    bs.inputs['Metallic'].default_value = metallic
    if 'Specular IOR Level' in bs.inputs:
        bs.inputs['Specular IOR Level'].default_value = 0.45
    if emission is not None:
        if 'Emission Color' in bs.inputs:
            bs.inputs['Emission Color'].default_value = (*emission, 1)
        elif 'Emission' in bs.inputs:
            bs.inputs['Emission'].default_value = (*emission, 1)
        if 'Emission Strength' in bs.inputs:
            bs.inputs['Emission Strength'].default_value = estrength
    nt.links.new(bs.outputs['BSDF'], out.inputs['Surface'])
    if noise:
        tex = nt.nodes.new('ShaderNodeTexNoise')
        tex.inputs['Scale'].default_value = 5.0
        tex.inputs['Detail'].default_value = 2.2
        tex.inputs['Roughness'].default_value = 0.7
        ramp = nt.nodes.new('ShaderNodeValToRGB')
        ramp.color_ramp.elements[0].position = 0.22
        ramp.color_ramp.elements[0].color = (max(0.08, rough-0.12),)*3 + (1,)
        ramp.color_ramp.elements[1].position = 0.78
        ramp.color_ramp.elements[1].color = (min(0.92, rough+0.18),)*3 + (1,)
        nt.links.new(tex.outputs['Fac'], ramp.inputs['Fac'])
        nt.links.new(ramp.outputs['Color'], bs.inputs['Roughness'])
    return m

def alpha_mat(name, color, alpha=0.3, rough=0.35):
    m=make_mat(name,color,rough=rough)
    bs=m.node_tree.nodes.get('Principled BSDF')
    if bs:
        bs.inputs['Alpha'].default_value=alpha
        if 'Transmission Weight' in bs.inputs: bs.inputs['Transmission Weight'].default_value=0.08
    try: m.surface_render_method='DITHERED'
    except: pass
    return m

# palette
mat_asphalt = make_mat('Road asphalt - blue hour', (0.035,0.045,0.060), rough=.90, noise=True)
mat_asphalt2 = make_mat('Fresh patch asphalt', (0.055,0.065,0.075), rough=.94, noise=True)
mat_crack = make_mat('Crack shadow', (0.008,0.010,0.013), rough=.98)
mat_concrete = make_mat('Curb concrete', (0.27,0.29,0.30), rough=.86, noise=True)
mat_concrete_edge = make_mat('Curb edge wear', (0.43,0.43,0.39), rough=.78, noise=True)
mat_red = make_mat('Vermillion cabinet paint', (0.42,0.018,0.012), rough=.38, noise=True)
mat_red_dark = make_mat('Deep red trim', (0.16,0.009,0.008), rough=.43, noise=True)
mat_red_faded = make_mat('Sun faded vermillion', (0.57,0.105,0.055), rough=.52, noise=True)
mat_red_wet = make_mat('Rain darkened red', (0.22,0.018,0.016), rough=.25)
mat_cream = make_mat('Warm ivory enamel', (0.82,0.72,0.52), rough=.43, noise=True)
mat_cream_clean = make_mat('Fresh shrine ivory', (0.96,0.86,0.68), rough=.30)
mat_cream_shadow = make_mat('Panel shadow', (0.30,0.23,0.18), rough=.64)
mat_glass = make_mat('Smoky tinted display glass', (0.025,0.085,0.105), rough=.18, metallic=.03)
mat_glass_highlight = alpha_mat('Glass reflection highlight', (0.36,0.69,0.72), alpha=.22, rough=.12)
mat_black = make_mat('Rubber and slot black', (0.008,0.010,0.012), rough=.38)
mat_metal = make_mat('Brushed aluminum', (0.42,0.47,0.48), rough=.28, metallic=.78, noise=True)
mat_metal_dark = make_mat('Aged steel', (0.12,0.15,0.16), rough=.43, metallic=.70, noise=True)
mat_metal_bright = make_mat('Door highlight metal', (0.62,0.67,0.65), rough=.22, metallic=.84)
mat_brass = make_mat('Bell brass', (0.70,0.34,0.055), rough=.24, metallic=.82, noise=True)
mat_gold = make_mat('Lettering gold', (0.82,0.47,0.08), rough=.27, metallic=.62)
mat_rust = make_mat('Rust orange bloom', (0.42,0.085,0.018), rough=.72, noise=True)
mat_rust_dark = make_mat('Rust dark edge', (0.18,0.028,0.012), rough=.79, noise=True)
mat_rust_light = make_mat('Rust fresh orange', (0.72,0.22,0.035), rough=.64)
mat_wood = make_mat('Clean shrine wood', (0.27,0.105,0.035), rough=.46, noise=True)
mat_wood_light = make_mat('Wood edge', (0.55,0.25,0.065), rough=.38, noise=True)
mat_torii = make_mat('Torii vermillion fresh', (0.64,0.035,0.018), rough=.34, noise=True)
mat_torii_dark = make_mat('Torii shaded red', (0.27,0.012,0.009), rough=.42)
mat_rope = make_mat('Bright rice straw rope', (0.88,0.62,0.17), rough=.72, noise=True)
mat_rope_light = make_mat('Rope fresh fibers', (0.98,0.79,0.32), rough=.64)
mat_paper = make_mat('Crisp shide paper', (0.96,0.91,0.76), rough=.62)
mat_paper_shadow = make_mat('Paper fold shadow', (0.69,0.54,0.37), rough=.68)
mat_paper_red = make_mat('Fortune red ink', (0.62,0.025,0.018), rough=.52)
mat_fox = make_mat('Glazed kitsune ceramic', (0.92,0.86,0.69), rough=.22)
mat_fox_orange = make_mat('Kitsune ear and tail', (0.78,0.22,0.035), rough=.28)
mat_fox_inner = make_mat('Ear inner glaze', (0.96,0.45,0.12), rough=.26)
mat_grass = make_mat('Dusk grass', (0.055,0.16,0.055), rough=.84)
mat_grass_light = make_mat('Grass tips', (0.14,0.30,0.08), rough=.72)
mat_pole = make_mat('Nobori pole', (0.16,0.075,0.028), rough=.50)
mat_banner_red = make_mat('Nobori vermillion cloth', (0.70,0.035,0.022), rough=.70, noise=True)
mat_banner = make_mat('Nobori cream cloth', (0.92,0.78,0.55), rough=.72, noise=True)
mat_banner_dark = make_mat('Nobori ink', (0.10,0.025,0.017), rough=.68)
mat_faded = alpha_mat('Ghost sticker residue', (0.62,0.31,0.16), alpha=.45, rough=.70)
mat_wet = alpha_mat('Rain streak sheen', (0.12,0.20,0.22), alpha=.45, rough=.18)
mat_button_amber = make_mat('Button warm glow', (0.68,0.20,0.025), rough=.25, emission=(1.0,0.16,0.015), estrength=3.5)
mat_button_cyan = make_mat('Button cool glow', (0.02,0.35,0.32), rough=.23, emission=(0.08,0.9,0.72), estrength=3.2)
mat_button_mint = make_mat('Button mint glow', (0.10,0.48,0.32), rough=.23, emission=(0.22,1.0,0.48), estrength=3.0)
mat_warm = make_mat('Interior fluorescent', (0.95,0.29,0.045), rough=.28, emission=(1.0,0.16,0.015), estrength=8.0)
mat_white_emit = make_mat('Tiny label glow', (0.78,0.62,0.22), rough=.30, emission=(1.0,0.55,0.10), estrength=2.0)

# -------------------- geometry helpers --------------------
def put(obj, coll):
    for c in list(obj.users_collection):
        c.objects.unlink(obj)
    coll.objects.link(obj)
    return obj

def cube(name, loc, dims, mat, bevel=0.0, coll=None, rot=(0,0,0)):
    bpy.ops.mesh.primitive_cube_add(location=loc)
    o=bpy.context.object; o.name=name
    o.dimensions=dims
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    o.rotation_euler=rot
    if mat: o.data.materials.append(mat)
    if bevel>0:
        mod=o.modifiers.new('Softened sheet-metal edges','BEVEL'); mod.width=bevel; mod.segments=3
    if coll: put(o,coll)
    return o

def cyl(name, loc, radius, depth, mat, vertices=20, rot=(0,0,0), bevel=0.0, coll=None):
    bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius, depth=depth, location=loc, rotation=rot)
    o=bpy.context.object; o.name=name
    if mat: o.data.materials.append(mat)
    for p in o.data.polygons: p.use_smooth=True
    if bevel>0:
        mod=o.modifiers.new('Rounded rim','BEVEL'); mod.width=bevel; mod.segments=2
    if coll: put(o,coll)
    return o

def uv(name, loc, scale, mat, coll=None, segments=16, rings=10):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=segments, ring_count=rings, location=loc)
    o=bpy.context.object; o.name=name; o.scale=scale
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    if mat: o.data.materials.append(mat)
    for p in o.data.polygons: p.use_smooth=True
    if coll: put(o,coll)
    return o

def cone(name, loc, r1, r2, depth, mat, coll=None, vertices=12, rot=(0,0,0)):
    bpy.ops.mesh.primitive_cone_add(vertices=vertices, radius1=r1, radius2=r2, depth=depth, location=loc, rotation=rot)
    o=bpy.context.object; o.name=name
    if mat: o.data.materials.append(mat)
    for p in o.data.polygons: p.use_smooth=True
    if coll: put(o,coll)
    return o

def torus(name, loc, major, minor, mat, coll=None, rot=(0,0,0)):
    bpy.ops.mesh.primitive_torus_add(major_radius=major, minor_radius=minor, major_segments=20, minor_segments=8, location=loc, rotation=rot)
    o=bpy.context.object; o.name=name
    if mat: o.data.materials.append(mat)
    for p in o.data.polygons: p.use_smooth=True
    if coll: put(o,coll)
    return o

def curve_poly(name, pts, bevel, mat, coll=None, cyclic=False):
    cu=bpy.data.curves.new(name,'CURVE'); cu.dimensions='3D'; cu.resolution_u=2; cu.bevel_depth=bevel; cu.bevel_resolution=2; cu.fill_mode='FULL'
    sp=cu.splines.new('POLY'); sp.points.add(len(pts)-1)
    for p,co in zip(sp.points,pts): p.co=(*co,1)
    sp.use_cyclic_u=cyclic
    o=bpy.data.objects.new(name,cu)
    (coll or root).objects.link(o)
    if mat: cu.materials.append(mat)
    return o

def cyl_between(name, a, b, radius, mat, coll=None, vertices=12):
    a,b=Vector(a),Vector(b); v=b-a
    o=cyl(name,(a+b)/2,radius,v.length,mat,vertices=vertices,coll=coll)
    o.rotation_mode='QUATERNION'; o.rotation_quaternion=v.to_track_quat('Z','Y')
    return o

def box_between(name,a,b,width,depth,mat,coll=None,bevel=0.0):
    a,b=Vector(a),Vector(b); v=b-a
    o=cube(name,(a+b)/2,(width,depth,v.length),mat,bevel=bevel,coll=coll)
    o.rotation_mode='QUATERNION'; o.rotation_quaternion=v.to_track_quat('Z','Y')
    return o

def poly_obj(name, verts, faces, mat, coll=None):
    me=bpy.data.meshes.new(name+'Mesh'); me.from_pydata(verts,[],faces); me.update()
    o=bpy.data.objects.new(name,me); (coll or root).objects.link(o)
    if mat: me.materials.append(mat)
    return o

def front_patch(name, cx, cz, rx, rz, mat, y=-0.84, n=11, seed=1):
    rr=random.Random(seed)
    verts=[(cx,y,cz)]
    for i in range(n):
        a=2*pi*i/n; r=1+rr.uniform(-.25,.18)
        verts.append((cx+cos(a)*rx*r,y+rr.uniform(-.002,.002),cz+sin(a)*rz*r))
    faces=[tuple([0]+list(range(1,n+1)))]
    return poly_obj(name,verts,faces,mat,collections['WEATHERING'])

def side_patch(name, cy, cz, ry, rz, mat, x=-1.061, n=10, seed=2):
    rr=random.Random(seed); verts=[(x,cy,cz)]
    for i in range(n):
        a=2*pi*i/n; r=1+rr.uniform(-.22,.2)
        verts.append((x+rr.uniform(-.002,.002),cy+cos(a)*ry*r,cz+sin(a)*rz*r))
    faces=[tuple([0]+list(range(1,n+1)))]
    return poly_obj(name,verts,faces,mat,collections['WEATHERING'])

def text_obj(name, body, loc, size, mat, coll=None, rot=(pi/2,0,0), extrude=.004, align='CENTER'):
    cu=bpy.data.curves.new(name+'Curve','FONT'); cu.body=body; cu.align_x=align; cu.align_y='CENTER'; cu.size=size; cu.extrude=extrude; cu.bevel_depth=.0015; cu.bevel_resolution=2
    o=bpy.data.objects.new(name,cu); (coll or root).objects.link(o); o.location=loc; o.rotation_euler=rot
    if mat: cu.materials.append(mat)
    return o

def mesh_grid_banner(name, x0, x1, z0, z1, ybase, mat, cols=5, rows=6, amp=.035):
    verts=[]
    for j in range(rows+1):
        z=z0+(z1-z0)*j/rows
        for i in range(cols+1):
            x=x0+(x1-x0)*i/cols
            y=ybase + amp*sin((i/cols)*pi*2 + j*.45)
            verts.append((x,y,z))
    faces=[]
    for j in range(rows):
        for i in range(cols):
            a=j*(cols+1)+i; faces.append((a,a+1,a+cols+2,a+cols+1))
    return poly_obj(name,verts,faces,mat,collections['ENVIRONMENT'])

# -------------------- environment --------------------
env=collections['ENVIRONMENT']; mach=collections['VENDING_MACHINE']; shrine=collections['SHRINE_DETAILS']; weather=collections['WEATHERING']
road=cube('Rural mountain road',(0,1.3,-.10),(14,12,.16),mat_asphalt,bevel=.03,coll=env)
# irregular patched shoulder under machine
patch_pts=[(-3.4,-2.55),(2.85,-2.52),(3.38,-.95),(2.72,1.9),(1.25,2.75),(-1.65,2.62),(-3.25,1.28),(-3.65,-.4)]
poly_obj('Irregular cracked asphalt patch',[(x,y,.0) for x,y in patch_pts],[tuple(range(len(patch_pts)))],mat_asphalt2,env)
# curb and top worn strip
cube('Concrete roadside curb',(0,-2.32,.10),(8.6,.34,.24),mat_concrete,bevel=.035,coll=env)
cube('Curb sunlit top',(0,-2.46,.225),(8.6,.08,.055),mat_concrete_edge,bevel=.015,coll=env)
for i,x in enumerate([-3.4,-2.0,-.6,.8,2.2,3.6]):
    cube('Curb joint %02d'%i,(x,-2.30,.11),(.025,.36,.20),mat_crack,bevel=.003,coll=env)
# cracks
cracks=[ [(-2.2,-1.5,.012),(-1.7,-1.65,.014),(-1.48,-1.92,.015)],
         [(1.0,-1.3,.014),(1.32,-1.48,.015),(1.66,-1.43,.015),(1.92,-1.68,.015)],
         [(-.75,.8,.013),(-.45,.63,.014),(-.52,.30,.014)],
         [(2.1,.2,.014),(1.8,.04,.014),(1.72,-.25,.014)] ]
for i,pts in enumerate(cracks): curve_poly('Asphalt crack %02d'%i,pts,.012,mat_crack,env)
# distant layered mountain silhouettes
mount1=[(-10,0),(-10,1.6),(-8.5,2.2),(-7.0,1.75),(-5.7,2.55),(-4.2,1.55),(-2.8,2.1),(-1.2,1.45),(0.2,2.5),(1.5,1.72),(3.0,2.35),(4.4,1.5),(6.1,2.18),(7.8,1.55),(9.5,2.4),(10,1.7),(10,0)]
poly_obj('Far mountain silhouette',[(x,6,z) for x,z in mount1],[tuple(range(len(mount1)))],make_mat('Far mountain blue',(0.025,0.055,0.12),rough=.98),env)
mount2=[(-10,0),(-10,1.0),(-8.6,1.6),(-7.2,1.2),(-5.8,1.9),(-4.0,1.15),(-2.0,1.75),(-.6,1.15),(1.0,1.82),(2.5,1.15),(4.0,1.7),(5.7,1.15),(7.2,1.65),(8.8,1.05),(10,1.5),(10,0)]
poly_obj('Near mountain silhouette',[(x,5.3,z) for x,z in mount2],[tuple(range(len(mount2)))],make_mat('Near mountain blue',(0.018,0.035,0.075),rough=.98),env)
# grass tufts around base
for i in range(34):
    a=random.uniform(0,2*pi); r=random.uniform(.9,1.55)
    x=math.cos(a)*r; y=random.uniform(-.45,.7)
    if abs(x)<1.04 and y<.5: continue
    h=random.uniform(.13,.32); lean=random.uniform(-.25,.25)
    blade=cone('Grass blade %02d'%i,(x,y,.03+h/2),.022,.004,h,mat_grass_light if i%4==0 else mat_grass,env,vertices=5,rot=(lean,random.uniform(-.18,.18),random.uniform(-.35,.35)))

# -------------------- vending cabinet --------------------
# feet, plinth, main sheet-metal shell
cube('Left leveling foot',(-.72,.18,.23),(.26,.32,.16),mat_metal_dark,bevel=.035,coll=mach)
cube('Right leveling foot',(.72,.18,.23),(.26,.32,.16),mat_metal_dark,bevel=.035,coll=mach)
cube('Cabinet lower plinth',(0,0,.38),(2.12,1.45,.24),mat_red_dark,bevel=.065,coll=mach)
cube('Main vermillion vending cabinet',(0,0,2.03),(2.08,1.40,3.40),mat_red,bevel=.11,coll=mach)
# faded side layer and roof pieces
cube('Sun-faded left side paint',(-1.052,-.04,2.54),(.018,1.08,1.52),mat_red_faded,bevel=.018,coll=weather)
cube('Chamfered roof cap',(0,0,3.78),(2.16,1.48,.18),mat_red,bevel=.095,coll=mach)
cube('Roof front rain lip',(0,-.765,3.69),(2.14,.13,.13),mat_red_dark,bevel=.025,coll=mach)
cube('Roof top worn highlight',(0,-.02,3.875),(1.75,1.10,.025),mat_red_faded,bevel=.012,coll=weather)
# recessed cream front and strong border
cube('Recessed warm cream front',(0,-.735,2.12),(1.82,.09,2.88),mat_cream,bevel=.035,coll=mach)
cube('Left red front rail',(-.955,-.802,2.12),(.13,.11,2.91),mat_red_dark,bevel=.025,coll=mach)
cube('Right red front rail',(.955,-.802,2.12),(.13,.11,2.91),mat_red_dark,bevel=.025,coll=mach)
cube('Bottom red front rail',(0,-.802,.67),(1.94,.11,.14),mat_red_dark,bevel=.025,coll=mach)
# display window recess and frame
cube('Display deep recess',(0,-.792,2.88),(1.68,.055,1.19),mat_cream_shadow,bevel=.025,coll=mach)
cube('Tinted display glass',(0,-.83,2.88),(1.54,.028,1.06),mat_glass,bevel=.018,coll=mach)
# subtle glass highlights, glints
cube('Glass vertical reflection',(-.57,-.852,2.89),(.025,.008,.91),mat_glass_highlight,bevel=.008,coll=weather,rot=(0,math.radians(-7),math.radians(-2)))
cube('Glass soft reflection',(.35,-.852,3.13),(.018,.008,.38),mat_glass_highlight,bevel=.006,coll=weather,rot=(0,math.radians(-10),math.radians(2)))
# frame rails
cube('Window top cream frame',(0,-.875,3.465),(1.68,.10,.075),mat_cream_clean,bevel=.018,coll=mach)
cube('Window bottom cream frame',(0,-.875,2.29),(1.68,.10,.075),mat_cream_clean,bevel=.018,coll=mach)
cube('Window left cream frame',(-.805,-.875,2.88),(.075,.10,1.19),mat_cream_clean,bevel=.018,coll=mach)
cube('Window right cream frame',(.805,-.875,2.88),(.075,.10,1.19),mat_cream_clean,bevel=.018,coll=mach)
# header fascia and title
cube('Brand header fascia',(0,-.852,3.575),(1.68,.07,.16),mat_red_dark,bevel=.018,coll=mach)
text_obj('Brand lettering','KITSUNE  DRINKS',(0,-.902,3.575),.073,mat_cream_clean,shrine,extrude=.003)
text_obj('Tiny hot cold legend','HOT  /  COLD',(.59,-.905,3.575),.030,mat_gold,shrine,extrude=.002)
# shelves, angled slightly toward viewer
for j,z in enumerate([2.46,2.98]):
    cube('Angled product shelf %d'%j,(0,-.91,z),(1.48,.36,.045),mat_metal_dark,bevel=.012,coll=mach,rot=(math.radians(7),0,0))
    cube('Shelf bright front lip %d'%j,(0,-1.095,z-.018),(1.48,.045,.055),mat_metal,bevel=.012,coll=mach)
# cans
can_colors=[(0.75,.04,.025),(0.04,.37,.42),(.88,.46,.035),(.30,.08,.42),(.18,.48,.18),(.82,.76,.25),(.11,.25,.48),(.75,.18,.08),(.55,.03,.20),(.04,.45,.34),(.75,.33,.04),(.95,.83,.55)]
can_mats=[]
for i,c in enumerate(can_colors): can_mats.append(make_mat('Can label color %02d'%i,c,rough=.30,metallic=.22,noise=True))
xs=[-.63,-.38,-.13,.12,.37,.62]
for row,z in enumerate([2.67,3.17]):
    for col,x in enumerate(xs):
        idx=row*6+col
        body=cyl('Drink can %02d'%idx,(x,-.968,z),.112,.285,can_mats[idx],vertices=20,rot=(pi/2,0,0),bevel=.006,coll=mach)
        cyl('Can silver top %02d'%idx,(x,-1.118,z),.101,.014,mat_metal_bright,vertices=20,rot=(pi/2,0,0),coll=mach)
        cyl('Can base rim %02d'%idx,(x,-.818,z),.102,.012,mat_metal_dark,vertices=20,rot=(pi/2,0,0),coll=mach)
        torus('Can front ring %02d'%idx,(x,-1.128,z),.073,.008,mat_cream_clean if col%2 else mat_cream_shadow,coll=mach,rot=(pi/2,0,0))
        cyl('Can logo disc %02d'%idx,(x,-1.132,z),.034,.006,mat_cream_clean if idx%3 else mat_gold,vertices=16,rot=(pi/2,0,0),coll=mach)
        # small colored stripe across visible can face
        cube('Can face stripe %02d'%idx,(x,-1.137,z-.062),(.055,.006,.012),mat_gold if idx%2 else mat_cream_clean,bevel=.002,coll=mach)
# control band
cube('Dark control band',(0,-.792,1.78),(1.66,.06,.78),mat_red_dark,bevel=.025,coll=mach)
cube('Control band upper trim',(0,-.84,2.19),(1.64,.07,.055),mat_cream_clean,bevel=.012,coll=mach)
cube('Control band lower trim',(0,-.84,1.37),(1.64,.07,.055),mat_cream,bevel=.012,coll=mach)
# coin and bill modules
cube('Coin mechanism housing',(-.60,-.855,1.97),(.32,.09,.35),mat_metal_dark,bevel=.025,coll=mach)
cube('Coin face plate',(-.60,-.913,1.98),(.25,.018,.27),mat_metal,bevel=.012,coll=mach)
cube('Coin slot',(-.60,-.928,2.07),(.145,.012,.028),mat_black,bevel=.006,coll=mach)
cyl('Coin return button',(-.60,-.928,1.88),.040,.014,mat_brass,vertices=16,rot=(pi/2,0,0),coll=mach)
text_obj('Coin yen mark','Y',(-.60,-.942,2.00),.060,mat_black,shrine,extrude=.002)
cube('Bill reader housing',(-.60,-.855,1.56),(.32,.09,.20),mat_metal_dark,bevel=.022,coll=mach)
cube('Bill reader slot',(-.60,-.922,1.58),(.20,.014,.034),mat_button_cyan,bevel=.006,coll=mach)
cube('Bill reader indicator',(-.60,-.925,1.51),(.105,.009,.016),mat_cream_clean,bevel=.003,coll=mach)
# selection grid 11 illuminated buttons with tags
button_mats=[mat_button_amber,mat_button_cyan,mat_button_mint]
button_xs=[-.19,.10,.39,.68]; button_zs=[2.05,1.80,1.55]
button_index=0
for r,z in enumerate(button_zs):
    for c,x in enumerate(button_xs):
        if r==2 and c==3: continue
        bm=button_mats[button_index%3]
        cube('Selection button housing %02d'%button_index,(x,-.86,z),(.215,.065,.145),mat_metal_dark,bevel=.018,coll=mach)
        cube('Illuminated selection face %02d'%button_index,(x,-.903,z),(.168,.012,.092),bm,bevel=.010,coll=mach)
        cube('Price tag %02d'%button_index,(x,-.906,z-.105),(.17,.018,.043),mat_cream_clean,bevel=.006,coll=mach)
        text_obj('Price %02d'%button_index,'120',(x,-.922,z-.105),.027,mat_red_dark,shrine,extrude=.0015)
        button_index+=1
# tiny side label on controls
text_obj('Control hint','SELECT',(-.60,-.925,1.72),.033,mat_gold,shrine,extrude=.0015)
# bell replacing last button
bx,bz=.68,1.55
cyl('Bell mounting neck',(bx,-.875,1.63),.034,.10,mat_metal,vertices=12,coll=mach)
cone('Tiny brass bell',(bx,-.923,bz),.092,.055,.13,mat_brass,coll=shrine,vertices=20)
torus('Bell flared rim',(bx,-.923,bz-.06),.078,.012,mat_gold,coll=shrine)
uv('Bell clapper',(bx,-.923,bz-.105),(.025,.025,.025),mat_metal_dark,shrine,segments=12,rings=6)
# rope segmented red-white pull
for i,(z0,z1,mm) in enumerate([(1.42,1.34,mat_paper),(1.34,1.26,mat_paper_red),(1.26,1.18,mat_paper)]):
    cyl_between('Bell pull rope %d'%i,(bx,-.927,z0),(bx,-.927,z1),.014,mm,shrine,vertices=10)
uv('Bell pull handle',(bx,-.927,1.15),(.055,.035,.026),mat_paper_red,shrine,segments=12,rings=6)
# lower dispensing hatch
cube('Dispense recess',(.32,-.845,1.00),(1.00,.06,.42),mat_black,bevel=.025,coll=mach)
cube('Dispensing metal push door',(.32,-.905,1.02),(.84,.075,.29),mat_metal_bright,bevel=.035,coll=mach,rot=(math.radians(-5),0,0))
cube('Door inset shadow',(.32,-.948,1.03),(.69,.012,.16),mat_metal_dark,bevel=.015,coll=mach)
cube('Door push handle',(.32,-.987,1.035),(.28,.025,.035),mat_metal,bevel=.012,coll=mach)
for i,x in enumerate([-.08,.72]):
    cyl('Hatch hinge %d'%i,(x,-.94,1.19),.027,.16,mat_metal_dark,vertices=12,rot=(0,pi/2,0),coll=mach)
# offering box beside hatch
cube('Saisen bako body',(-.62,-.965,.98),(.43,.30,.30),mat_wood,bevel=.025,coll=shrine)
cube('Saisen bako front panel',(-.62,-1.125,1.00),(.37,.018,.20),mat_wood_light,bevel=.010,coll=shrine)
cube('Offering box top rim',(-.62,-.965,1.145),(.46,.32,.045),mat_wood_light,bevel=.012,coll=shrine)
for i,x in enumerate([-.78,-.70,-.62,-.54,-.46]):
    cube('Offering box top slat %d'%i,(x,-.965,1.18),(.045,.27,.045),mat_wood_light,bevel=.008,coll=shrine)
# offering box metal bolts
for x in [-.79,-.45]:
    cyl('Saisen bolt', (x,-1.145,.90),.018,.012,mat_metal_dark,vertices=12,rot=(pi/2,0,0),coll=shrine)
# front bolts and rust halos
for i,(x,z) in enumerate([(-.89,.72),(.89,.72),(-.89,3.48),(.89,3.48),(-.89,1.34),(.89,1.34)]):
    torus('Rust halo bolt %02d'%i,(x,-.87,z),.052,.012,mat_rust_dark,coll=weather,rot=(pi/2,0,0))
    cyl('Front bolt %02d'%i,(x,-.892,z),.033,.026,mat_metal_dark,vertices=12,rot=(pi/2,0,0),coll=mach)
# weathering rust patches / blooms
front_patch('Rust bloom lower left',-.82,.55,.22,.12,mat_rust,y=-.872,seed=7)
front_patch('Rust bloom lower center',.48,.51,.28,.10,mat_rust_dark,y=-.872,seed=8)
front_patch('Rust orange fleck lower right',.80,.72,.10,.08,mat_rust_light,y=-.877,seed=9)
front_patch('Rust drip near hatch',-.04,.74,.08,.19,mat_rust,y=-.877,seed=10)
front_patch('Rust halo top corner',-.82,3.47,.09,.07,mat_rust_light,y=-.878,seed=11)
# small rust drip trails
for i,(x,z0,z1) in enumerate([(-.82,.65,.43),(.48,.58,.40),(.02,.82,.60),(.79,.78,.67)]):
    curve_poly('Rust drip %02d'%i,[(x,-.882,z0),(x+random.uniform(-.018,.018),-.883,(z0+z1)/2),(x+random.uniform(-.02,.02),-.884,z1)],.010,mat_rust_dark,weather)
# ghost rectangular sticker outline on left side panel
# side plane is x=-1.065, spanning y/z
cube('Ghost sticker faded field',(-1.068,-.10,2.28),(.018,.66,.58),mat_faded,bevel=.008,coll=weather)
# mask center slightly back? outline pieces in brighter faded color
for nm,loc,dims in [
 ('Ghost sticker top',(-1.081,-.10,2.59),(.018,.72,.025)),
 ('Ghost sticker bottom',(-1.081,-.10,1.97),(.018,.72,.025)),
 ('Ghost sticker front',(-1.081,-.45,2.28),(.018,.025,.60)),
 ('Ghost sticker back',(-1.081,.25,2.28),(.018,.025,.60))]:
    cube(nm,loc,dims,mat_red_faded,bevel=.004,coll=weather)
# streaks under roof lip on front frame and left side
for i,(x,zend) in enumerate([(-.91,2.54),(-.84,2.72),(.86,2.76),(.92,2.45),(-.75,3.05),(.75,3.12)]):
    curve_poly('Rain streak front %02d'%i,[(x,-.884,3.62),(x+random.uniform(-.008,.008),-.887,3.38),(x+random.uniform(-.012,.012),-.888,zend)],.009 if i<4 else .006,mat_wet,weather)
for i,y in enumerate([-.58,-.34,-.08,.20,.42]):
    curve_poly('Rain streak side %02d'%i,[(-1.078,y,3.64),(-1.082,y+random.uniform(-.015,.015),3.35),(-1.084,y+random.uniform(-.025,.025),2.9-random.uniform(0,.3))],.010,mat_red_wet,weather)

# -------------------- shimenawa and shide across display --------------------
# two visible strands and fringe knots
for j,off in enumerate([-.025,.025]):
    pts=[]
    for i in range(13):
        t=i/12; x=-.78+1.56*t; z=3.57+off+.018*sin(t*pi*2); y=-1.02-.006*cos(t*pi)
        pts.append((x,y,z))
    curve_poly('Shimenawa twisted strand %d'%j,pts,.026 if j==0 else .020,mat_rope if j==0 else mat_rope_light,shrine)
for i,x in enumerate([-.70,-.36,0,.36,.70]):
    torus('Rope knot %d'%i,(x,-1.025,3.56),.045,.012,mat_rope_light,shrine,rot=(pi/2,0,0))
    # hanging small straw tassel
    curve_poly('Straw tassel %d'%i,[(x,-1.03,3.53),(x,-1.03,3.46)],.009,mat_rope,shrine)
# shide zigzag streamers, each made of folded rectangular segments
for si,cx in enumerate([-.52,0,.52]):
    p=[(cx,-1.055,3.54),(cx+.10,-1.062,3.43),(cx-.055,-1.064,3.31),(cx+.08,-1.066,3.18),(cx-.03,-1.067,3.08)]
    for k in range(len(p)-1):
        box_between('Shide %d fold %d'%(si,k),p[k],p[k+1],.070,.014,mat_paper,shrine,bevel=.006)
    # folded hanging tab
    cube('Shide paper fold %d'%si,(cx+.025,-1.075,3.19),(.10,.018,.055),mat_paper_shadow,bevel=.008,coll=shrine,rot=(0,math.radians(12*(si-1)),math.radians(8*(si-1))))

# -------------------- roof offering and torii --------------------
cube('Roof shrine offering platform',(0,.02,3.91),(1.48,.72,.12),mat_wood,bevel=.035,coll=shrine)
cube('Platform bright front edge',(0,-.35,3.96),(1.36,.045,.06),mat_wood_light,bevel=.012,coll=shrine)
# small stone feet under torii
for x in [-.58,.58]:
    cyl('Torii stone foot',(x,.07,4.00),.105,.09,mat_concrete_edge,vertices=16,coll=shrine)
# torii posts and crossbeams
for x in [-.60,.60]:
    cube('Torii upright',(x,.07,4.36),(.14,.16,.78),mat_torii,bevel=.028,coll=shrine)
    cube('Torii upright dark base',(x,.07,4.02),(.17,.19,.10),mat_torii_dark,bevel=.018,coll=shrine)
cube('Torii lower crossbar',(0,.07,4.48),(1.42,.17,.13),mat_torii_dark,bevel=.022,coll=shrine)
cube('Torii upper crossbar',(0,.07,4.68),(1.68,.20,.14),mat_torii,bevel=.035,coll=shrine)
# slightly raised caps at ends suggest kasagi
cube('Torii left cap',(-.82,.07,4.735),(.28,.21,.10),mat_torii,bevel=.035,coll=shrine,rot=(0,math.radians(-3),math.radians(-2)))
cube('Torii right cap',(.82,.07,4.735),(.28,.21,.10),mat_torii,bevel=.035,coll=shrine,rot=(0,math.radians(3),math.radians(2)))
# torii small gold fasteners
for x in [-.60,.60]:
    cyl('Torii gold peg',(x,-.025,4.49),.025,.018,mat_gold,vertices=12,rot=(pi/2,0,0),coll=shrine)
# fox figurines facing one another
def make_fox(label, d):
    bx=.27*d; by=-.22
    uv(label+' ceramic body',(bx,by,4.14),(.16,.14,.23),mat_fox,shrine,segments=20,rings=12)
    uv(label+' haunch',(bx-.035*d,by+.02,4.08),(.13,.14,.17),mat_fox,shrine,segments=16,rings=10)
    hx=bx+.13*d
    uv(label+' fox head',(hx,by,4.37),(.14,.13,.145),mat_fox,shrine,segments=20,rings=12)
    uv(label+' muzzle',(hx+.095*d,by-.012,4.335),(.075,.085,.055),mat_fox,shrine,segments=16,rings=10)
    uv(label+' nose',(hx+.155*d,by-.015,4.34),(.022,.028,.023),mat_black,shrine,segments=12,rings=8)
    # ears
    for ei,ey in enumerate([by-.065,by+.065]):
        cone(label+' ear %d'%ei,(hx+.015*d,ey,4.535),.060,.012,.19,mat_fox_orange,shrine,vertices=4)
        cone(label+' inner ear %d'%ei,(hx+.018*d,ey-.003,4.55),.028,.006,.095,mat_fox_inner,shrine,vertices=4)
    # collar and bib
    torus(label+' red collar',(bx+.10*d,by,4.285),.105,.018,mat_torii,shrine)
    verts=[(hx+.04*d,by-.135,4.30),(hx+.15*d,by-.135,4.30),(hx+.095*d,by-.14,4.14)]
    poly_obj(label+' red bib',verts,[(0,1,2)],mat_torii,shrine)
    # eye visible toward camera, plus far eye for silhouette
    for ey in [by-.12,by+.11]:
        uv(label+' eye',(hx+.055*d,ey,4.395),(.018,.014,.018),mat_black,shrine,segments=10,rings=6)
    # curled tail as a clean orange curve
    back=bx-.16*d
    pts=[(back,by+.03,4.12),(back-.12*d,by+.05,4.22),(back-.22*d,by+.06,4.33),(back-.30*d,by+.04,4.25),(back-.27*d,by+.02,4.15)]
    curve_poly(label+' curled tail',pts,.052,mat_fox_orange,shrine)
    uv(label+' tail tip',pts[-1],(.065,.055,.065),mat_fox_orange,shrine,segments=12,rings=8)
make_fox('Left kitsune',1)
make_fox('Right kitsune',-1)
# little offering bowl between foxes
cyl('Small offering bowl',(0,-.23,4.04),.12,.08,mat_cream_clean,vertices=20,coll=shrine)
torus('Offering bowl rim',(0,-.23,4.085),.105,.014,mat_gold,shrine)

# -------------------- omikuji rack on machine left side --------------------
# rack brackets and wire frame
for y in [-.54,.26]:
    cyl_between('Rack bracket %s'%y,(-1.07,y,2.06),(-1.22,y,2.06),.022,mat_metal_dark,weather,vertices=10)
for y in [-.58,.30]:
    cyl_between('Rack vertical wire %s'%y,(-1.23,y,1.27),(-1.23,y,2.18),.018,mat_metal,weather,vertices=10)
cyl_between('Rack upper horizontal wire',(-1.23,-.61,2.12),(-1.23,.33,2.12),.020,mat_metal,weather,vertices=12)
cyl_between('Rack lower horizontal wire',(-1.23,-.61,1.42),(-1.23,.33,1.42),.015,mat_metal_dark,weather,vertices=10)
# fortune strips in side yz plane, varied angles / lengths
fortune_specs=[(-.54,1.78,.34,-12,.02,mat_paper),(-.38,1.72,.43,9,-.08,mat_paper_shadow),(-.20,1.82,.30,-18,.05,mat_paper),(-.02,1.69,.48,6,.09,mat_paper),(.16,1.78,.37,22,-.04,mat_paper_shadow),(.28,1.72,.29,-8,.11,mat_paper)]
for i,(y,zc,h,ang,fold,pm) in enumerate(fortune_specs):
    # hanging main paper, narrow in Y and thin in X
    strip=cube('Omikuji fortune strip %02d'%i,(-1.27,y,zc),(.022,.115,h),pm,bevel=.008,coll=shrine,rot=(math.radians(ang),0,math.radians(fold)))
    # red tied knot at upper wire, little cord down the front
    uv('Omikuji knot %02d'%i,(-1.265,y,2.105),(.035,.035,.028),mat_paper_red,shrine,segments=10,rings=6)
    cyl_between('Omikuji tie cord %02d'%i,(-1.27,y,2.10),(-1.27,y,2.04),.009,mat_paper_red,shrine,vertices=8)
    # two ink bands on each paper, offset slightly outward on -X face
    for j,zz in enumerate([zc+h*.23,zc-h*.15]):
        cube('Omikuji ink mark %02d %d'%(i,j),(-1.287,y,zz),(.008,.075,.012),mat_paper_red if j==0 else mat_banner_dark,bevel=.002,coll=shrine,rot=(math.radians(ang),0,math.radians(fold)))
# side charm plaque
cube('Side prayer plaque',(-1.085,.45,1.15),(.025,.22,.28),mat_wood_light,bevel=.012,coll=shrine)
text_obj('Side plaque mark','FUKU',(-1.106,.45,1.16),.040,mat_torii,shrine,rot=(pi/2,0,pi/2),extrude=.002)

# -------------------- nobori banner --------------------
# pole and cap
cyl('Nobori banner pole',(1.48,-.22,1.72),.032,3.18,mat_pole,vertices=16,coll=env)
uv('Nobori pole finial',(1.48,-.22,3.34),(.065,.065,.065),mat_brass,env,segments=16,rings=8)
# outer red cloth and inner cream field
mesh_grid_banner('Nobori red outer cloth',1.51,1.94,1.02,2.98,-.28,mat_banner_red,cols=5,rows=8,amp=.045)
mesh_grid_banner('Nobori cream inner cloth',1.555,1.885,1.15,2.84,-.315,mat_banner,cols=5,rows=8,amp=.035)
# banner emblem, circle and lettering on camera side
cyl('Nobori sun emblem',(1.72,-.365,2.42),.115,.018,mat_banner_red,vertices=24,rot=(pi/2,0,0),coll=env)
text_obj('Nobori lettering','INARI',(1.72,-.382,1.78),.072,mat_banner_dark,env,extrude=.002)
text_obj('Nobori top mark','福',(1.72,-.382,2.65),.105,mat_banner_dark,env,extrude=.002)
# banner tie ropes
curve_poly('Banner top tie',[(1.48,-.25,2.98),(1.72,-.30,3.08)],.009,mat_rope,env)
curve_poly('Banner lower tie',[(1.48,-.25,1.18),(1.72,-.30,1.09)],.009,mat_rope,env)

# -------------------- lighting / camera --------------------
# world nodes
world=bpy.data.worlds.new('Blue hour world') if not bpy.data.worlds else bpy.data.worlds[0]
scene.world=world; world.use_nodes=True
wn=world.node_tree.nodes; wb=wn.get('Background'); wb.inputs['Color'].default_value=(0.006,0.015,0.055,1); wb.inputs['Strength'].default_value=.23

def look_at(o, target):
    o.rotation_euler=(Vector(target)-o.location).to_track_quat('-Z','Y').to_euler()

def point_light(name,loc,color,energy,radius):
    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['LIGHTS'].objects.link(o); o.location=loc; return o

def area_light(name,loc,color,energy,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['LIGHTS'].objects.link(o); o.location=loc; look_at(o,target); return o
# cool moon/sky fill
area_light('Cool blue hour fill',(-4.5,-4.5,7.5),(0.12,0.28,1.0),700,5.5,(0,0,1.8))
area_light('Soft cool rim',(4.0,2.5,5.5),(0.08,0.22,0.75),900,4.0,(0,0,2.2))
# dominant warm display and spill
point_light('Warm fluorescent inside window',(0,-1.18,2.78),(1.0,.13,.025),520,.32)
point_light('Warm ground spill',(0,-1.52,1.55),(1.0,.09,.015),240,.65)
point_light('Button halo',(.18,-1.12,1.78),(.06,1.0,.60),55,.55)
# soft highlight on shrine roof
area_light('Shrine warm top light',(0,-2.2,5.6),(1.0,.28,.08),180,2.5,(0,0,3.9))
# camera
cam_data=bpy.data.cameras.new('Presentation Camera'); cam=bpy.data.objects.new('Presentation Camera',cam_data); scene.collection.objects.link(cam)
cam.location=(-6.0,-10.5,4.75); target=(-.05,-.05,2.25); look_at(cam,target); cam_data.lens=48; cam_data.sensor_width=36
scene.camera=cam
# render setup
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
scene.render.image_settings.color_mode='RGBA'
try:
    scene.view_settings.look='AgX - Medium High Contrast'
except: pass
# ambient occlusion-like contact via world / render settings
# compositor glow
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=.75; glare.size=6; glare.mix=-.88
comp=nt.nodes.new('CompositorNodeComposite'); nt.links.new(rl.outputs['Image'],glare.inputs['Image']); nt.links.new(glare.outputs['Image'],comp.inputs['Image'])
# organize naming metadata
scene['design_notes']='Stylized late-90s Japanese drink vending machine lovingly converted to Inari shrine; all major forms procedural.'
scene['required_elements']='torii, two kitsune, shimenawa with three shide, saisen-bako, bell pull rope, omikuji rack, cans, controls, hatch, weathering, dusk road'

# save initial scene
bpy.ops.wm.save_as_mainfile(filepath='/home/oai/share/vending_shrine.blend')
# preview render
scene.render.filepath='/home/oai/share/vending_shrine_preview.png'
bpy.ops.render.render(write_still=True)
result={'objects':len(bpy.data.objects),'materials':len(bpy.data.materials),'filepath':scene.render.filepath,'camera':cam.name}


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 537, in <module>
TypeError: bpy_struct: item.attr = val: enum "BLENDER_EEVEE_NEXT" not found in ('BLENDER_EEVEE', 'BLENDER_WORKBENCH', 'CYCLES')