saumwacht-godot/assets-source/phase4/blender/reference_glutpakt.py
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern
unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore
unversioniert; letztere sind unter ../backups gesichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:00:36 +02:00

274 lines
15 KiB
Python

"""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))