"""Glutpakt rebuilt from the ten actual Phase-3 icons, not role archetypes. Shape controls live in the small part functions. G06 is a walking ballista; G07/08 have massive mortar bodies; G09/10 are headless living hearths. """ from math import pi, sin, cos import bpy import hashlib from common import box, ellipsoid, cone, cylinder, beam, ring from reference_geometry import r_plate, r_block, r_rock, r_sweep, r_lathe, r_band, r_disc, r_inset, r_sun def g_legs(m, spread=.28, height=.40, lean=0, slim=False): """Bent stone legs, joint rings and two separate forward toes.""" for s,side in [(-1,'L'),(1,'R')]: ankle=(s*spread,-.015,.14) knee=(s*spread*1.1,.03+lean,height*.61) hip=(s*spread*.72,.06,height) radius=.075 if slim else .155 r_band('Leg'+side+'_Thigh',hip,knee,radius,m['primary'],6) r_band('Leg'+side+'_Shin',knee,ankle,radius*.9,m['primary'],6) r_rock('Leg'+side+'_Knee',knee,(radius*1.2,radius,radius*1.1),m['primary'],s*.2) r_disc('Leg'+side+'_Joint',(knee[0],knee[1]-.085,knee[2]),radius*.60,.06,m['accent']) r_band('Leg'+side+'_AnkleBand',(ankle[0],ankle[1],.10),(ankle[0],ankle[1],.17),radius*1.1,m['accent']) for j in range(2): x=ankle[0]+(j-.5)*(.12 if slim else .15) r_block('Leg'+side+'_Toe'+str(j),(x,-.11,.065),(.13 if slim else .18,.34,.13),m['primary']) def g_arm(name, shoulder, elbow, hand, m, radius=.16, gold=True): r_rock(name+'_Shoulder',shoulder,(radius*1.40,radius*1.24,radius*1.36),m['primary'],.23) r_band(name+'_Upper',shoulder,elbow,radius,m['primary'],7) r_rock(name+'_Elbow',elbow,(radius*1.12,)*3,m['primary'],.7) r_band(name+'_Lower',elbow,hand,radius*1.1,m['primary'],7) if gold: a=tuple(elbow[i]*.35+hand[i]*.65 for i in range(3)) b=tuple(elbow[i]*.15+hand[i]*.85 for i in range(3)) r_band(name+'_Cuff',a,b,radius*1.24,m['accent'],8) r_rock(name+'_Hand',hand,(radius*1.35,radius*1.2,radius*1.25),m['primary'],-.4) def g_coal(up,m): """Wide jaw-dominated rock creature, short legs, hanging claws and a crest.""" g_legs(m,.26,.40) r_rock('Body_MoltenCore',(0,.02,.73),(.43,.30,.50),m['secondary'],.16) r_rock('Body_BackRock',(0,.15,.79),(.46,.27,.46),m['primary'],.37) # A real wide mouth is framed by stone teeth, rather than a separate white face. jaw=[(-.36,.10),(-.28,.20),(-.16,.16),(0,.23),(.17,.14),(.30,.18),(.38,.06), (.29,-.18),(.16,-.09),(.02,-.23),(-.13,-.10),(-.29,-.19)] r_plate('Head_GlowingJaw',(0,-.298,.75),jaw,.045,m['secondary']) for s,side in [(-1,'L'),(1,'R')]: brow=[(-.20,-.04),(-.04,.02),(.19,-.12),(.20,.17),(-.04,.22),(-.19,.11)] r_plate('Head_Brow'+side,(s*.19,-.335,.97),[(s*x,z) for x,z in brow],.16,m['primary'],.06) r_sweep('Head_Eye'+side,[(s*.29,-.431,.92),(s*.11,-.434,.95)],[.015,.024],m['secondary'],4) r_rock('Head_Cheek'+side,(s*.34,-.19,.73),(.19,.18,.27),m['primary'],s*.28) for j in range(3): x=s*(.07+j*.095) r_sweep('Head_UpperFang'+side+str(j),[(x,-.39,.95-j*.022),(x+s*.018,-.408,.79-j*.036)], [.069,0],m['primary'],4) r_sweep('Head_LowerFang'+side+str(j),[(x,-.315,.49+j*.025),(x,-.395,.65+j*.035)], [.075,0],m['primary'],4) sh=(s*.45,.01,.91) elbow=(s*.58,-.045,.66) hand=(s*(.65 if up else .59),-.18,.46) g_arm('Arm'+side,sh,elbow,hand,m,.145,up) r_rock('Arm'+side+'_GoldPlate',(s*.47,-.08,1.035),(.105,.08,.13),m['accent']) for j in range(2): x=hand[0]+(j-.5)*.18 if up: points=[(x,-.245,.57),(x+s*.04,-.42,.46),(x+s*.03,-.52,.27),(x-s*.065,-.48,.15)] radii=[.12,.12,.065,0] else: points=[(x,-.245,.51),(x+s*.025,-.36,.43),(x+s*.01,-.42,.30),(x-s*.025,-.40,.24)] radii=[.075,.07,.038,0] r_sweep('Weapon'+side+'_Claw'+str(j),points,radii,m['accent'],6) if up: r_sweep('Body_ShoulderHorn'+side,[(s*.40,.03,1.03),(s*.53,.03,1.24),(s*.46,.02,1.33)], [.15,.09,0],m['primary'],5) crest=[(0,.15,1.14),(0,.13,1.34 if up else 1.29),(0,.08,1.47 if up else 1.34)] r_sweep('Head_CragCrown',crest,[.22,.12,0],m['primary'],5) r_rock('Head_CrownGold',(0,-.055,1.19),(.075,.055,.115),m['accent']) if up: for s in [-1,1]: r_sweep('Body_DorsalSpire'+str(s),[(s*.20,.21,1.11),(s*.26,.21,1.42)], [.13,0],m['primary'],5) def g_tower(name,loc,size,m,slots=2): """Solid shoulder fortress with true separated crenellations.""" x,y,z=loc w,d,h=size r_block(name+'_Keep',loc,size,m['primary']) r_block(name+'_GoldBelt',(x,y,z-h*.28),(w*1.04,d*1.04,.065),m['accent'],.08) for j in range(slots): bx=x+(j-(slots-1)/2)*w*.42 box(name+'_Window'+str(j),(bx,y-d*.5-.041,z),(.037,.017,h*.32),m['secondary']) for j,(dx,dy) in enumerate([(-1,-1),(1,-1),(-1,1),(1,1)]): r_block(name+'_Merlon'+str(j),(x+dx*w*.34,y+dy*d*.31,z+h*.56), (w*.26,d*.38,h*.34),m['primary'],.04) def g_tank(up,m): g_legs(m,.29,.42) r_block('Body_StoneMass',(0,.04,.85),(.84,.54,.94),m['primary']) shield=[(-.38,.47),(.38,.47),(.39,-.26),(0,-.51),(-.39,-.26)] r_plate('Body_TowerShield_Rim',(0,-.29,.82),shield,.15,m['accent']) r_plate('Body_TowerShield_Face',(0,-.375,.82),[(x*.90,z*.94) for x,z in shield],.045,m['primary'],.065) # Vertical furnace slit is the focal mark of both icons. sigil=[(-.035,.30),(.035,.30),(.035,-.08),(0,-.13),(-.035,-.08)] r_plate('Head_VerticalEmber',(0,-.466,1.00),sigil,.025,m['secondary']) for s,side in [(-1,'L'),(1,'R')]: r_band('Body_ShieldLine'+side,(s*.065,-.457,.69),(s*.065,-.457,1.22),.021,m['accent'],4) g_arm('Arm'+side,(s*.48,.035,1.03),(s*.59,.03,.73),(s*.63,-.13,.47),m,.19) if up: g_tower('Arm'+side+'_Castle',(s*.48,.035,1.12),(.42,.47,.37),m) g_tower('Arm'+side+'_Bastion',(s*.61,-.07,.65),(.30,.39,.30),m,1) else: r_block('Arm'+side+'_ShoulderGold',(s*.48,-.04,1.13),(.35,.37,.12),m['accent']) if up: g_tower('Head_CentralKeep',(0,.06,1.37),(.30,.32,.37),m,1) r_block('Body_ShieldRidge',(0,-.45,.85),(.08,.045,.95),m['accent'],.05) r_plate('Body_ShieldEmber',(0,-.482,1.01),[(-.022,.17),(.022,.17),(.022,-.14),(0,-.18),(-.022,-.14)],.02,m['secondary']) def g_lancer(m): g_legs(m,.235,.69,.12,True) r_plate('Body_Cuirass',(0,.02,.98),[(-.24,.19),(.24,.19),(.15,-.13),(0,-.29),(-.15,-.13)],.26,m['primary'],.06) r_inset('Body_ChestCleft',(0,-.145,.98),[(-.10,.17),(.08,.17),(.035,-.20),(-.01,-.25)],.025,m['accent'],m['secondary']) r_band('Head_Neck',(0,.02,1.13),(0,-.04,1.38),.078,m['primary'],5) r_plate('Head_SpearHelm',(0,-.045,1.42),[(-.16,.08),(0,.02),(.16,.10),(.065,-.04),(0,-.19),(-.065,-.04)],.16,m['accent'],.035) r_plate('Head_EmberVisor',(0,-.148,1.33),[(-.04,.05),(.055,.05),(0,-.15)],.03,m['secondary']) r_sweep('Head_HelmCrest',[(0,.00,1.43),(-.035,.10,1.66)],[.085,0],m['accent'],4) for s,side in [(-1,'L'),(1,'R')]: sh=(s*.245,0,1.14) hand=(s*.28,-.25,.91) r_band('Arm'+side+'_Upper',sh,(s*.34,-.04,.95),.067,m['primary'],5) r_band('Arm'+side+'_Forearm',(s*.34,-.04,.95),hand,.067,m['primary'],5) r_rock('Arm'+side+'_Hand',hand,(.075,.065,.075),m['primary']) r_plate('Arm'+side+'_Pauldron',(s*.27,0,1.17),[(s*-.095,-.07),(s*.17,-.04),(s*.075,.10)],.16,m['accent']) # Long reinforced lance runs across both hands, held at waist height. start=(-.66,-.23,.94) end=(.60,-.30,.94) r_band('Weapon_LanceCore',start,end,.065,m['primary'],8) for j in range(5): t=.10+j*.19 p=tuple(start[i]*(1-t)+end[i]*t for i in range(3)) q=(p[0]+.038,p[1]-.002,p[2]) r_band('Weapon_LanceCollar'+str(j),p,q,.086,m['accent'],8) r_sweep('Weapon_GlassSpearhead',[end,(.73,-.307,.94),(.98,-.32,.94)],[.10,.13,0],m['secondary'],4) def g_ballista(m): """Headless two-legged siege crossbow, matching the G06 image silhouette.""" g_legs(m,.32,.57,.12) r_block('Body_BallistaChassis',(0,.05,.64),(.48,.48,.34),m['primary']) r_block('Weapon_Rail',(0,-.05,.90),(.21,1.05,.20),m['primary']) r_block('Weapon_RearHousing',(0,.44,.98),(.32,.32,.27),m['primary']) box('Weapon_EmberRail',(0,-.10,1.01),(.07,1.08,.023),m['secondary']) for i,y in enumerate([-.54,-.17,.33]): r_block('Weapon_RailCollar'+str(i),(0,y,.92),(.26,.065,.245),m['accent'],.08) for s,side in [(-1,'L'),(1,'R')]: bow=[(s*.12,-.40,.94),(s*.39,-.43,.92),(s*.70,-.24,.96),(s*.81,.04,1.03)] r_sweep('Weapon_GlassLimb'+side,bow,[.115,.14,.105,.065],m['secondary'],5) r_band('Weapon_BowBinding'+side,(s*.16,-.42,.92),(s*.22,-.42,.92),.15,m['accent'],6) r_band('Weapon_Drawstring'+side,bow[-1],(0,.45,1.03),.014,m['primary'],4) r_disc('Body_Crank'+side,(s*.20,-.13,.69),.13,.07,m['accent']) r_disc('Body_CrankInset'+side,(s*.20,-.175,.69),.09,.025,m['primary']) r_sweep('Weapon_CrystalBolt',[(0,-.54,.99),(0,-.68,.99),(0,-.96,.99)],[.06,.10,0],m['secondary'],4) def g_mortar(up,m): """Huge bowl torso on squat stone legs; upgrade widens the magma crater.""" g_legs(m,.29,.41) r_rock('Body_Foundation',(0,.035,.55),(.41,.29,.29),m['primary']) radius=.52 if up else .45 axis=(0,-.24,.97) r_lathe('Body_Mortar',(0,.08,.54),[(0,0),(.25,.02),(radius*.90,.29),(radius,.65), (radius*.84,.65),(radius*.73,.28),(0,.13)],m['primary'],axis,12) # Wide gold lip, open centre and visible lower ember bed. r_lathe('Body_MortarRim',(0,.08,.54),[(radius*1.018,.57),(radius*1.035,.663), (radius*.815,.663),(radius*.81,.60)],m['accent'],axis,12) r_lathe('Weapon_LavaPool',(0,-.015,.99 if up else .91),[(0,0),(.36 if up else .23,0), (.36 if up else .23,.035),(0,.035)],m['secondary'],axis,12) for s,side in [(-1,'L'),(1,'R')]: g_arm('Arm'+side,(s*.45,.05,.95),(s*.59,-.03,.70),(s*.46,-.27,.53),m,.17) r_block('Body_RimClasp'+side,(s*radius*.71,-.26,1.16),(.11,.16,.16),m['accent']) if up: r_inset('Body_FrontSigil',(0,-.44,.96),[(-.14,.19),(.14,.19),(.06,-.15),(0,-.20),(-.06,-.15)],.065,m['accent'],m['secondary']) for k in range(4): a=k*pi/2+.3 r_rock('Weapon_LavaRock'+str(k),(.23*cos(a),-.015+.20*sin(a),1.025),(.09,.085,.04),m['primary']) else: r_rock('Weapon_EmberBoulder',(0,-.02,.91),(.19,.16,.18),m['secondary'],.2) def g_hearth(up,m): """Broad oven body with cream hood; no human head. Upgrade has a sun core.""" g_legs(m,.28,.42) r_rock('Body_StoneBack',(0,.10,.85),(.45,.30,.53),m['primary']) # Arched opening is a thick half-ring and two pillars, not a forward horn. if not up: pts=[(.34*cos(pi*k/10),-.11,1.03+.34*sin(pi*k/10)) for k in range(11)] r_sweep('Body_CeramicArch',pts,[.105]*len(pts),m['light'],6) for s in [-1,1]: r_block('Body_CeramicPillar'+str(s),(s*.34,-.12,.89),(.19,.25,.36),m['light']) r_block('Head_Chimney',(0,.075,1.39),(.37,.35,.22),m['light']) r_block('Head_ChimneyShadow',(0,-.109,1.37),(.23,.025,.11),m['primary']) else: r_rock('Body_SunCore',(0,-.22,1.13),(.23,.09,.25),m['secondary']) ring('Body_SunHalo',(0,-.19,1.17),.35,.045,m['accent'],rotation=(pi/2,0,0),segments=12) for k in range(7): a=pi*k/6 r_sweep('Body_HaloRay'+str(k),[(.34*cos(a),-.15,1.17+.34*sin(a)), (.49*cos(a),-.14,1.17+.49*sin(a))],[.065,0],m['accent'],4) for s,side in [(-1,'L'),(1,'R')]: r_plate('Body_CeramicMantle'+side,(s*.42,.06,.90),[(s*-.06,.38),(s*.14,.30),(s*.20,-.24),(s*.11,-.40),(s*-.08,-.21)],.27,m['light']) r_plate('Body_CeramicTabard'+side,(s*.49,-.06,.39),[(s*-.08,.23),(s*.08,.25),(s*.14,-.21),(s*.04,-.29),(s*-.10,-.20)],.04,m['light']) bowl_z=.64 r_lathe('Weapon_HearthBowl',(0,-.39,bowl_z-.16),[(0,0),(.17,.02),(.37 if up else .30,.17), (.39 if up else .32,.23),(.33 if up else .26,.23),(.23,.10),(0,.08)],m['accent'],segments=12) cylinder('Weapon_EmberBed',(0,-.39,bowl_z+.015),.30 if up else .23,.028,m['secondary'],vertices=12) for s,side in [(-1,'L'),(1,'R')]: g_arm('Arm'+side,(s*.41,.015,1.03),(s*.54,-.13,.76),(s*.36,-.40,.61),m,.17) r_disc('Arm'+side+'_Sigil',(s*.46,-.16,1.01),.12,.045,m['accent']) for k,(x,z) in enumerate([(-.12,.26),(.06,.40),(.18,.20)]): r_sweep('Weapon_Flame'+str(k),[(x,-.39,bowl_z),(x-.045,-.37,bowl_z+z*.42), (x+.025,-.35,bowl_z+z*.78),(x-.04,-.36,bowl_z+z)], [.07,.09,.045,0],m['secondary'],5) if up: r_sweep('Body_SolarStream',[(0,-.29,1.00),(.03,-.38,.85),(0,-.39,.70)], [.06,.05,.055],m['secondary'],5) r_sun('Weapon_BowlSun',(0,-.69,.66),.095,m) def build_glutpakt(spec,m): role=spec['role'] up=spec['upgraded'] if role=='melee': g_coal(up,m) elif role=='tank': g_tank(up,m) elif role=='ranged': g_ballista(m) if up else g_lancer(m) elif role=='artillery': g_mortar(up,m) elif role=='support': g_hearth(up,m) else: raise ValueError('Unknown Glutpakt role: '+role) finish_glutpakt_surfaces(m) def finish_glutpakt_surfaces(m): """Stable basalt/gold facet values, matching the chipped stone in the icons. Material changes only: retain the original solid geometry and triangle cap. Each editable face keeps a material slot; no image projection or randomness. """ for key, factors in [('primary', (.78, 1.0, 1.32, 1.65)), ('accent', (.76, 1.0, 1.25))]: original = m[key] palette = [] for index, factor in enumerate(factors): mat = original.copy() mat.name = original['material_name'] + '_Facet' + str(index) mat['material_name'] = mat.name base = list(original.diffuse_color) base[:3] = [min(1.0, v * factor) for v in base[:3]] mat.diffuse_color = base mat.node_tree.nodes.get('Principled BSDF').inputs['Base Color'].default_value = base palette.append(mat) for obj in bpy.context.scene.objects: if obj.type != 'MESH' or not obj.data.materials or obj.data.materials[0] != original: continue obj.data.materials.clear() for mat in palette: obj.data.materials.append(mat) seed = int(hashlib.sha256(obj['part_name'].encode()).hexdigest()[:8], 16) for face in obj.data.polygons: # Slightly brighter upward facets, varied side facets. face.material_index = min(len(palette)-1, (seed + face.index * 7) % len(palette))