saumwacht-godot/assets-source/phase4/blender/frontline.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

557 lines
25 KiB
Python

"""Original Saumwacht melee and tank silhouettes, built from named low-poly parts.
Coordinates are metres, Z up, facing -Y. The shared exporter owns materials,
normalisation, parenting, animation, scene lifetime and export. Every builder is
deterministic and deliberately preserves separate limb / weapon meshes.
"""
from math import cos, sin, pi
from common import box, ellipsoid, cylinder, cone, beam, ring, mesh_object, empty
def _profile(name, points, y, depth, mat):
"""Extrude a silhouette with outward normals and a centred editable origin."""
signed_area = sum(points[i][0] * points[(i+1) % len(points)][1]
- points[(i+1) % len(points)][0] * points[i][1]
for i in range(len(points)))
if signed_area < 0:
points = list(reversed(points))
center_x = (min(p[0] for p in points) + max(p[0] for p in points)) / 2
center_z = (min(p[1] for p in points) + max(p[1] for p in points)) / 2
n = len(points)
vertices = [(x-center_x, -depth/2, z-center_z) for x, z in points]
vertices += [(x-center_x, depth/2, z-center_z) for x, z in points]
faces = [tuple(range(n)), tuple(range(2 * n - 1, n - 1, -1))]
faces += [(i, i + n, (i + 1) % n + n, (i + 1) % n) for i in range(n)]
obj = mesh_object(name, vertices, faces, mat)
obj.location = (center_x, y, center_z)
return obj
def _slab(name, loc, size, mat, bevel=0.08):
"""Eight-sided shield / armour plate with real clipped corners."""
x, y, z = loc
w, d, h = size
a, b = w / 2, h / 2
c = min(bevel, a * 0.42, b * 0.42)
points = [(x-a+c, z-b), (x+a-c, z-b), (x+a, z-b+c),
(x+a, z+b-c), (x+a-c, z+b), (x-a+c, z+b),
(x-a, z+b-c), (x-a, z-b+c)]
return _profile(name, points, y, d, mat)
def _blade(name, origin, length, width, angle, mat, depth=0.055, hook=0.12):
"""A broad bent blade, with an unmistakable edge rather than a thin rod."""
x, y, z = origin
local = [(-width*.38, 0), (width*.48, 0),
(width*.54, length*.52), (hook, length),
(-width*.30, length*.65)]
points = [(x + u*cos(angle) + v*sin(angle),
z - u*sin(angle) + v*cos(angle)) for u, v in local]
return _profile(name, points, y, depth, mat)
def _crescent(name, origin, radius, thickness, start, end, mat,
depth=0.09, segments=10):
"""An open polygonal crescent plate in the front plane, angles in radians."""
x, y, z = origin
angles = [start + (end-start)*i/segments for i in range(segments+1)]
points = [(x + radius*cos(a), z + radius*sin(a)) for a in angles]
points += [(x + (radius-thickness)*cos(a),
z + (radius-thickness)*sin(a)) for a in reversed(angles)]
return _profile(name, points, y, depth, mat)
def _eyes(prefix, x, y, z, spacing, mat, width=0.085):
for side, s in (("L", -1), ("R", 1)):
box(prefix + "Eye" + side, (x+s*spacing, y, z),
(width, 0.023, 0.028), mat)
def _stone_legs(mat, upgraded=False):
for side, s in (("L", -1), ("R", 1)):
x = s * (0.27 if upgraded else 0.24)
_slab("Leg" + side + "Shin", (x, 0.02, 0.26),
(0.30, 0.31, 0.36), mat["primary"])
_slab("Leg" + side + "Foot", (x, -0.10, 0.095),
(0.37, 0.46, 0.19), mat["primary"], 0.07)
if upgraded:
box("Leg" + side + "Cuff", (x, -0.167, 0.27),
(0.23, 0.035, 0.07), mat["accent"])
def _coal_body(mat, upgraded):
ellipsoid("BodyCoal", (0, 0.01, 0.77), (0.44, 0.35, 0.48),
mat["primary"], subdivisions=2)
_slab("BodyForehead", (0, -0.16, 1.115), (0.62, 0.36, 0.25),
mat["primary"])
mouth = [(-.31,.83), (-.23,.77), (-.12,.80), (0,.735),
(.13,.785), (.24,.76), (.31,.83), (.25,.94),
(.1,.92), (0,.97), (-.14,.93), (-.27,.955)]
_profile("HeadEmberMouth", mouth, -.352, .045, mat["secondary"])
_eyes("Head", 0, -.350, 1.05, .155, mat["secondary"], .065)
for i, x in enumerate((-.20, 0, .20)):
_profile("HeadTooth%02d" % i,
[(x-.035,.94), (x,.865), (x+.035,.94)],
-.384, .025, mat["light"])
for side, s in (("L", -1), ("R", 1)):
if upgraded:
_slab("BodyShoulder" + side, (s*.49, .0, 1.08),
(.39, .47, .32), mat["primary"])
_blade("BodyShoulderCrest" + side, (s*.42, .035, 1.15),
.29, .16, s*.50, mat["primary"], .17, s*.04)
box("BodyShoulderSeam" + side, (s*.50, -.243, 1.05),
(.22, .02, .055), mat["secondary"])
else:
ellipsoid("BodyShoulder" + side, (s*.41, .02, .87),
(.19, .25, .25), mat["primary"])
def _coal_claws(mat, upgraded):
for side, s in (("L", -1), ("R", 1)):
beam("Arm" + side + "Upper", (s*.37, .0, .86),
(s*.55, -.075, .65), .13, mat["primary"])
ellipsoid("Arm" + side + "Fist", (s*.57, -.16, .56),
(.19, .24, .19), mat["primary"])
for i, dx in enumerate((-.105, .105)):
x = s*.57 + dx
# Curved front claws are separate wedges; upgrade adds long tips.
points = [(x-.045,.60), (x+.045,.60), (x+.065,.42),
(x+.015,.30 if upgraded else .37),
(x-.035,.43)]
_profile("Weapon" + side + "Claw%02d" % i, points,
-.39 if upgraded else -.34, .115,
mat["accent"] if upgraded else mat["light"])
if upgraded:
_slab("Arm" + side + "WristGuard", (s*.57, -.18, .70),
(.33, .40, .13), mat["accent"], .045)
def _basalt_body(mat, upgraded):
_slab("BodyBastion", (0, .035, .83),
(1.02 if upgraded else .87, .57, .95), mat["primary"])
_slab("BodyFrontShield", (0, -.30, .80), (.74, .18, .71),
mat["primary"], .14)
_slab("HeadFurnaceSlot", (0, -.401, 1.07), (.48, .035, .085),
mat["secondary"], .025)
for s in (-1, 1):
beam("BodyBrace" + ("L" if s < 0 else "R"),
(s*.29, -.398, .50), (s*.29, -.398, .95),
.033, mat["accent"], vertices=4)
if upgraded:
for i, (x, z, width) in enumerate(((-.48, 1.20, .31),
(0, 1.30, .43),
(.48, 1.20, .31))):
_slab("BodyBattlement%02d" % i, (x, .035, z),
(width, .54, .35), mat["primary"], .05)
box("BodyBattlementCap%02d" % i, (x, .035, z+.145),
(width+.025, .565, .055), mat["accent"])
_slab("BodyShieldBoss", (0, -.41, .73), (.38, .075, .35),
mat["accent"], .095)
_slab("BodyShieldCore", (0, -.456, .73), (.23, .025, .22),
mat["secondary"], .065)
def _basalt_arms(mat, upgraded):
for side, s in (("L", -1), ("R", 1)):
x = s * (.635 if upgraded else .54)
_slab("Arm" + side + "Pillar", (x, .01, .72),
(.28, .37, .60), mat["primary"])
_slab("Arm" + side + "Fist", (x, -.075, .48),
(.32, .42, .27), mat["primary"])
box("Arm" + side + "Band", (x, -.287, .52),
(.23, .028, .07), mat["accent"])
def _machine_legs(mat, upgraded=False, tall=False):
for side, s in (("L", -1), ("R", 1)):
x = s * (.25 if tall else .24)
top = .72 if tall else .46
knee = (x*1.15, .03, .38 if tall else .25)
ankle = (x, -.065, .13)
beam("Leg" + side + "PistonUpper", (x*.8, .035, top), knee,
.07 if tall else .13, mat["accent"])
beam("Leg" + side + "PistonLower", knee, ankle,
.055 if tall else .09, mat["accent"])
cylinder("Leg" + side + "Knee", knee, .085 if tall else .13,
.16, mat["secondary"], vertices=8, rotation=(pi/2, 0, 0))
_slab("Leg" + side + "Shoe", (x, -.105, .065),
(.23 if tall else .33, .36, .13), mat["primary"], .04)
if upgraded:
_slab("Leg" + side + "Greave", (x, -.115, .25),
(.17 if tall else .27, .12, .23), mat["primary"], .04)
def _machine_mask(mat, z=1.22):
_profile("HeadCeramicMask", [(-.14,z-.11), (.14,z-.11),
(.17,z+.06), (0,z+.18), (-.17,z+.06)],
-.205, .12, mat["light"])
_eyes("Head", 0, -.272, z+.025, .067, mat["primary"], .060)
def _shear_body(mat, upgraded):
ellipsoid("BodySpindle", (0, .02, .91),
(.285 if upgraded else .21, .235, .32), mat["primary"])
cylinder("BodyOcherHub", (0, -.224, .87), .14, .07,
mat["secondary"], vertices=10, rotation=(pi/2, 0, 0))
cylinder("BodyHubPin", (0, -.267, .87), .057, .025,
mat["accent"], vertices=8, rotation=(pi/2, 0, 0))
_machine_mask(mat, 1.235)
if upgraded:
_slab("BodyHubArmor", (0, .15, .98), (.57, .21, .37),
mat["primary"])
cone("HeadFin", (0, .055, 1.385), .065, 0, .21,
mat["secondary"], vertices=4)
def _shear_weapons(mat, upgraded):
for side, s in (("L", -1), ("R", 1)):
if upgraded:
for i, (z, angle) in enumerate(((1.10, .62), (.89, 1.32),
(.66, 2.07))):
pivot = (s*.47, -.105, z)
beam("Arm" + side + "Link%02d" % i,
(s*.21, .02, .93), pivot, .049, mat["secondary"])
cylinder("Arm" + side + "Hub%02d" % i, pivot, .076,
.10, mat["secondary"], vertices=8,
rotation=(pi/2, 0, 0))
_blade("Weapon" + side + "Blade%02d" % i,
(pivot[0], -.17, z), .39, .105, s*angle,
mat["accent"], .055, s*.07)
else:
pivot = (s*.43, -.12, .95)
beam("Arm" + side + "Upper", (s*.16, .025, 1.0),
(s*.34, .04, .82), .065, mat["secondary"])
beam("Arm" + side + "Forearm", (s*.34, .04, .82),
pivot, .060, mat["primary"])
for i, angle in enumerate((.39, 1.07)):
_blade("Weapon" + side + "Shear%02d" % i,
(pivot[0], -.19-i*.045, pivot[2]), .42, .10,
s*angle, mat["accent"], .055, s*.035)
cylinder("Weapon" + side + "ShearPivot", (s*.43, -.264, .95),
.092, .055, mat["secondary"], vertices=10,
rotation=(pi/2, 0, 0))
def _boiler_body(mat, upgraded):
cylinder("BodyBoiler", (0, .01, .77), .385, .66,
mat["primary"], vertices=12)
cone("BodyBoilerLid", (0, .01, 1.115), .39, .27, .14,
mat["primary"], vertices=12)
ring("BodyLowerRim", (0, .01, .475), .38, .035,
mat["secondary"], segments=12)
_slab("HeadCeramicFace", (0, -.355, .98), (.32, .105, .19),
mat["light"], .04)
_eyes("Head", 0, -.414, 1.005, .074, mat["primary"], .060)
cylinder("BodyGaugeRim", (0, -.402, .695), .117, .07,
mat["secondary"], vertices=12, rotation=(pi/2, 0, 0))
cylinder("BodyGaugeFace", (0, -.444, .695), .085, .016,
mat["light"], vertices=12, rotation=(pi/2, 0, 0))
beam("BodyGaugeNeedle", (0, -.458, .695), (.047, -.458, .747),
.012, mat["primary"], vertices=4)
if upgraded:
_slab("BodyChestArmor", (0, -.10, .82), (.81, .55, .61),
mat["primary"], .12)
# Face and gauge sit ahead of the armour, never buried inside it.
for side, s in (("L", -1), ("R", 1)):
cylinder("BodyPressureCylinder" + side, (s*.48, .06, 1.075),
.145, .54, mat["primary"], vertices=10)
cylinder("BodyPressureCap" + side, (s*.48, .06, 1.36),
.16, .07, mat["secondary"], vertices=10)
beam("BodyPressurePipe" + side, (s*.48, .13, .84),
(s*.30, .25, .66), .048, mat["accent"])
box("HeadBrow", (0, -.37, 1.12), (.46, .17, .09), mat["light"])
else:
cylinder("BodyChimney", (.235, .10, 1.245), .08, .23,
mat["accent"], vertices=8)
cylinder("BodyChimneyLip", (.235, .10, 1.365), .10, .045,
mat["secondary"], vertices=8)
def _boiler_arms(mat, upgraded):
for side, s in (("L", -1), ("R", 1)):
beam("Arm" + side + "Piston", (s*.33, .0, .88),
(s*.52, -.02, .63), .09, mat["accent"])
_slab("Arm" + side + "Fist", (s*.53, -.11, .53),
(.29, .34, .28), mat["accent"], .045)
if upgraded and side == "L":
_slab("WeaponLPressureShield", (-.57, -.245, .75),
(.37, .20, .69), mat["primary"], .08)
_slab("WeaponLShieldStripe", (-.57, -.356, .75),
(.085, .025, .50), mat["secondary"], .025)
elif upgraded:
cylinder("WeaponRPistonCap", (.55, -.302, .59), .165, .08,
mat["secondary"], vertices=8, rotation=(pi/2, 0, 0))
def _mist_legs(mat, tall=False, upgraded=False):
for side, s in (("L", -1), ("R", 1)):
hip = (s*.12, .045, .71 if tall else .48)
knee = (s*.25, .06 if side == "L" else -.04, .32)
ankle = (s*.20, -.12 if side == "L" else .02, .095)
beam("Leg" + side + "FoldUpper", hip, knee,
.10 if tall else .13, mat["primary"], vertices=5)
beam("Leg" + side + "FoldLower", knee, ankle, .075,
mat["secondary"], vertices=5)
_profile("Leg" + side + "TendrilFoot",
[(s*.20-.09,.035), (s*.20+.11,.035),
(s*.20+.07,.14), (s*.20-.045,.20)],
ankle[1]-.045, .20, mat["primary"])
def _mist_mask(mat, z, large=False):
sx, sz = (.235, .29) if large else (.158, .218)
ellipsoid("HeadMistMask", (0, -.17, z), (sx, .105, sz),
mat["light"], subdivisions=2)
_eyes("Head", 0, -.273, z+.035, sx*.43, mat["accent"], sx*.42)
_profile("HeadMaskChin", [(-sx*.50,z-.10), (0,z-sz*.92),
(sx*.50,z-.10)], -.252, .022, mat["light"])
def _dancer_body(mat, upgraded):
_profile("BodyFoldedRibbon", [(-.16,.56), (.10,.60), (.205,.97),
(.105,1.16), (-.06,1.13), (-.195,.86)],
.025, .27, mat["primary"])
_profile("BodyVioletFold", [(-.145,.64), (-.04,.67), (.15,1.075),
(.05,1.12)], -.13, .038, mat["secondary"])
_mist_mask(mat, 1.23)
if upgraded:
_crescent("HeadCrescentCrest", (0, .005, 1.37), .20, .065,
.08*pi, .92*pi, mat["secondary"], .075, 8)
_blade("BodyBackRibbonL", (-.12,.15,.94), .40, .13, -.9,
mat["secondary"], .06, -.05)
_blade("BodyBackRibbonR", (.12,.15,.94), .40, .13, .9,
mat["secondary"], .06, .05)
def _dancer_weapons(mat, upgraded):
if upgraded:
# One large open scissor: two distinct sweeping blades at one mint hub.
pivot = (.19, -.32, .79)
beam("ArmLUpper", (-.14,.015,1.05), (-.33,-.11,.85),
.065, mat["secondary"], vertices=5)
beam("ArmLForearm", (-.33,-.11,.85), pivot,
.061, mat["primary"], vertices=5)
beam("ArmRUpper", (.14,.015,1.05), (.36,-.12,.96),
.065, mat["primary"], vertices=5)
beam("ArmRForearm", (.36,-.12,.96), pivot,
.061, mat["secondary"], vertices=5)
_blade("WeaponScissorUpper", pivot, .69, .18, .57,
mat["light"], .075, .09)
_blade("WeaponScissorLower", (.19,-.405,.79), .66, .18, 1.42,
mat["light"], .075, .04)
ring("WeaponScissorHandleL", (.04,-.37,.655), .09, .025,
mat["secondary"], rotation=(pi/2,0,0), segments=8)
ring("WeaponScissorHandleR", (-.005,-.37,.79), .09, .025,
mat["secondary"], rotation=(pi/2,0,0), segments=8)
cylinder("WeaponScissorMintPivot", (.19,-.463,.79), .087, .028,
mat["accent"], vertices=12, rotation=(pi/2,0,0))
else:
for side, s, high in (("L", -1, False), ("R", 1, True)):
elbow = (s*.31, -.04, .91 if high else .80)
wrist = (s*.43, -.12, 1.07 if high else .66)
beam("Arm" + side + "Upper", (s*.14,.01,1.02), elbow,
.065, mat["primary"], vertices=5)
beam("Arm" + side + "Forearm", elbow, wrist,
.052, mat["secondary"], vertices=5)
_blade("Weapon" + side + "ThreadBlade", wrist,
.40, .065, s*(.50 if high else 2.12),
mat["light"], .045, s*.045)
cylinder("Weapon" + side + "MintGrip", wrist, .060, .09,
mat["accent"], vertices=8, rotation=(pi/2,0,0))
def _mist_tank_body(mat, upgraded):
ellipsoid("BodyFoldedMist", (0,.035,.73), (.37,.25,.44),
mat["primary"], subdivisions=2)
_profile("BodyVioletCenterFold", [(-.22,.43), (.035,.39),
(.25,.90), (.04,1.09)],
-.205, .060, mat["secondary"])
_mist_mask(mat, 1.145, large=True)
_crescent("BodyFrontCrescent", (0,-.32,.89), .44, .145,
.98*pi, 2.02*pi, mat["secondary"], .15, 12)
if upgraded:
_crescent("BodyOuterShield", (0,-.245,.99), .655, .17,
.89*pi, 2.11*pi, mat["light"], .15, 12)
_crescent("BodyRearShield", (0,.11,1.025), .60, .16,
.05*pi, .95*pi, mat["secondary"], .17, 12)
for side, s in (("L",-1),("R",1)):
_blade("BodyShieldWing" + side, (s*.44,.005,.72),
.49,.23,s*.39,mat["secondary"],.16,s*.10)
_blade("BodyShieldMintEdge" + side, (s*.49,-.09,.79),
.31,.047,s*.39,mat["accent"],.025,s*.065)
def _mist_tank_arms(mat, upgraded):
for side, s in (("L",-1),("R",1)):
beam("Arm" + side + "Fold", (s*.31,.005,.92),
(s*.43,-.045,.67), .080, mat["primary"], vertices=5)
_crescent("Arm" + side + "Crescent", (s*.42,-.14,.72),
.24,.075, -.4*pi if s < 0 else .6*pi,
.6*pi if s < 0 else 1.6*pi,
mat["secondary"],.095,8)
def _root_legs(mat, upgraded=False, four=False, spread=.32):
positions = [("L", -1, -.035), ("R", 1, -.035)]
if four:
positions = [("LFront",-1,-.27),("RFront",1,-.27),
("LRear",-1,.28),("RRear",1,.28)]
for label, s, y in positions:
beam("Leg" + label + "Root", (s*spread*.65,y,.48),
(s*spread,y-.035,.13), .11 if four else .13,
mat["primary"], vertices=6)
_slab("Leg" + label + "Foot", (s*spread,y-.10,.075),
(.28,.36,.15), mat["primary"], .065)
# One broad toe root is stronger at gameplay scale than many twigs.
beam("Leg" + label + "Toe", (s*spread,y-.12,.095),
(s*spread+s*.06,y-.31,.055), .045,
mat["accent"], vertices=5)
def _leaf(name, origin, length, width, angle, mat, depth=.055):
x,y,z = origin
local = [(0,0),(width*.55,length*.44),(0,length),
(-width*.55,length*.44)]
points = [(x+u*cos(angle)+v*sin(angle),
z-u*sin(angle)+v*cos(angle)) for u,v in local]
return _profile(name,points,y,depth,mat)
def _thorn_body(mat, upgraded):
if upgraded:
ellipsoid("BodyCoiledRoot", (0,.08,.63), (.48,.34,.39),
mat["primary"],subdivisions=2)
ring("BodyRootCoil", (0,.065,.61), .365,.085,
mat["primary"],segments=10)
ellipsoid("HeadUpperJaw", (0,-.16,1.07), (.55,.34,.26),
mat["primary"],subdivisions=2)
_slab("HeadLowerJaw", (0,-.265,.66), (.87,.44,.22),
mat["primary"],.085)
_slab("HeadMouthDepth", (0,-.26,.84), (.78,.20,.28),
mat["secondary"],.08)
for i,x in enumerate((-.30,-.10,.10,.30)):
_profile("WeaponUpperFang%02d" % i,
[(x-.065,1.04),(x,.835),(x+.065,1.04)],
-.485,.055,mat["accent"])
for i,x in enumerate((-.23,0,.23)):
_profile("WeaponLowerFang%02d" % i,
[(x-.07,.74),(x+.07,.74),(x,.91)],
-.497,.055,mat["accent"])
for side,s in (("L",-1),("R",1)):
_leaf("HeadCheekLeaf" + side,(s*.40,-.29,.82),.39,.29,
s*.59,mat["secondary"],.11)
_leaf("HeadCrownLeaf" + side,(s*.15,.01,1.21),.23,.15,
s*.67,mat["accent"])
_eyes("Head",0,-.425,1.155,.24,mat["accent"],.09)
else:
ellipsoid("BodyThornPod", (0,.03,.76), (.43,.34,.43),
mat["primary"],subdivisions=2)
_slab("HeadSlit",(0,-.308,.87),(.29,.045,.075),mat["secondary"],.025)
_eyes("Head",0,-.337,.96,.105,mat["accent"],.055)
for side,s in (("L",-1),("R",1)):
_leaf("HeadSprout" + side,(s*.10,.025,1.09),.31,.19,
s*.55,mat["secondary"],.07)
_blade("WeaponSideThorn" + side,(s*.34,.02,.76),
.24,.13,s*1.36,mat["secondary"],.13,s*.015)
for i,(x,y,z) in enumerate(((-.22,-.08,1.07),(.22,-.08,1.07),
(0,.285,.90))):
cone("BodyThorn%02d" % i,(x,y,z),.09,0,.25,
mat["accent"],vertices=5)
def _bark_head(mat, upgraded):
ellipsoid("HeadLowRoot", (0,-.36,.50), (.24,.23,.18),
mat["primary"])
_eyes("Head",0,-.559,.54,.097,mat["accent"],.077)
if upgraded:
_slab("HeadBrowBark",(0,-.43,.68),(.55,.23,.14),
mat["primary"],.045)
def _bark_shell(mat, upgraded):
ellipsoid("BodyCorkShell", (0,.07,.80), (.57,.49,.49),
mat["primary"],subdivisions=2)
if upgraded:
# Three actual layered trunk cross sections, readable from above/front.
for index,(x,y,z,r) in enumerate(((0,-.345,.91,.42),
(-.42,.06,.99,.32),
(.42,.06,.99,.32))):
cylinder("BodyGrowthShield%02d" % index,(x,y,z),r,.17,
mat["primary"],vertices=12,rotation=(pi/2,0,0))
for band,ratio in enumerate((.80,.50)):
ring("BodyGrowthRing%02d_%02d" % (index,band),
(x,y-.097,z),r*ratio,.022,
mat["accent"],rotation=(pi/2,0,0),segments=12)
cylinder("BodyGrowthHeart%02d" % index,(x,y-.099,z),r*.18,.02,
mat["secondary"],vertices=8,rotation=(pi/2,0,0))
_leaf("BodyCrownLeafL",(-.09,.16,1.19),.25,.19,-.67,
mat["secondary"],.055)
_leaf("BodyCrownLeafR",(.09,.16,1.19),.25,.19,.67,
mat["accent"],.055)
else:
# Asymmetric overlapping cork plates make a turtle-like living shield.
for i,(x,y,z,w,h) in enumerate(((-.26,-.015,1.04,.39,.36),
(.18,-.07,1.11,.42,.31),
(0,.245,.93,.53,.39))):
_slab("BodyCorkPlate%02d" % i,(x,y,z),(w,.29,h),
mat["primary"],.08)
_leaf("BodyMossPlateL",(-.39,-.065,.87),.24,.20,-.49,
mat["secondary"],.08)
_leaf("BodyMossPlateR",(.31,.04,1.06),.20,.19,.57,
mat["secondary"],.06)
def _faction(spec):
faction = str(spec["faction"]).lower()
aliases = {"g":"glutpakt", "m":"nietenkonvent", "n":"nebelchor",
"w":"wurzelbund"}
return aliases.get(faction, faction)
def build_melee(spec, mat):
"""Build a faction-specific melee unit or its visibly evolved upgrade."""
faction = _faction(spec)
upgraded = bool(spec.get("upgraded", False))
if faction == "glutpakt":
_stone_legs(mat, upgraded)
_coal_body(mat, upgraded)
_coal_claws(mat, upgraded)
elif faction == "nietenkonvent":
_machine_legs(mat, upgraded, tall=True)
_shear_body(mat, upgraded)
_shear_weapons(mat, upgraded)
elif faction == "nebelchor":
_mist_legs(mat, tall=True, upgraded=upgraded)
_dancer_body(mat, upgraded)
_dancer_weapons(mat, upgraded)
elif faction == "wurzelbund":
_root_legs(mat, upgraded, four=upgraded, spread=.34 if upgraded else .27)
_thorn_body(mat, upgraded)
else:
raise ValueError("Unknown Saumwacht melee faction: " + faction)
def build_tank(spec, mat):
"""Build a tank; upgrades add fortress tiers, pressure shields or growth rings."""
faction = _faction(spec)
upgraded = bool(spec.get("upgraded", False))
if faction == "glutpakt":
_stone_legs(mat, upgraded)
_basalt_body(mat, upgraded)
_basalt_arms(mat, upgraded)
elif faction == "nietenkonvent":
_machine_legs(mat, upgraded)
_boiler_body(mat, upgraded)
_boiler_arms(mat, upgraded)
elif faction == "nebelchor":
_mist_legs(mat, upgraded=upgraded)
_mist_tank_body(mat, upgraded)
_mist_tank_arms(mat, upgraded)
elif faction == "wurzelbund":
_root_legs(mat, upgraded, four=True, spread=.38 if upgraded else .33)
_bark_shell(mat, upgraded)
_bark_head(mat, upgraded)
else:
raise ValueError("Unknown Saumwacht tank faction: " + faction)