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>
274 lines
15 KiB
Python
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))
|