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

570 lines
25 KiB
Python

"""Saumwacht backline: 24 faceted ranged, artillery and support variants.
Coordinates are metres, Z up, face toward -Y. The exporter normalizes the
roughly 1.4 m figures to each catalog height. Every component stays separate
for object animation. No scene reset, export or material ownership lives here.
"""
from math import cos, sin, pi, sqrt
from common import box, ellipsoid, cylinder, cone, beam, ring, mesh_object, empty
def _add(a, b):
return tuple(a[i] + b[i] for i in range(3))
def _mul(a, scale):
return tuple(v * scale for v in a)
def _cross(a, b):
return (a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0])
def _unit(a):
length = sqrt(sum(v * v for v in a))
return tuple(v / length for v in a)
def _frame(axis):
w = _unit(axis)
reference = (0, 0, 1) if abs(w[2]) < .9 else (0, 1, 0)
u = _unit(_cross(reference, w))
return u, _cross(w, u), w
def _lathe(name, loc, profile, mat, axis=(0, 0, 1), segments=10):
"""Closed profile, including real inner walls; zero radii become poles.
Profile travels from the back/bottom up the outside, down the inside.
Independent pole vertices avoid degenerate zero-area triangles.
"""
u, v, w = _frame(axis)
verts, loops, faces = [], [], []
for radius, depth in profile:
if radius == 0:
loops.append([len(verts)])
verts.append(_add(loc, _mul(w, depth)))
continue
loop = []
for k in range(segments):
angle = 2 * pi * k / segments
offset = _add(_mul(w, depth),
_add(_mul(u, radius * cos(angle)),
_mul(v, radius * sin(angle))))
loop.append(len(verts))
verts.append(_add(loc, offset))
loops.append(loop)
for j in range(len(loops)):
low, high = loops[j], loops[(j + 1) % len(loops)]
if len(low) == 1 and len(high) == 1:
continue
for k in range(segments):
nxt = (k + 1) % segments
if len(low) == 1:
faces.append((low[0], high[nxt], high[k]))
elif len(high) == 1:
faces.append((low[k], low[nxt], high[0]))
else:
faces.append((low[k], low[nxt], high[nxt], high[k]))
mesh_object(name, verts, faces, mat)
def _cup(name, loc, radius, depth, mat, axis=(0, 0, 1), segments=10):
# The rim is 13% thick; the inset bottom is visible from above/front.
_lathe(name, loc, [(0, 0), (radius * .5, .04),
(radius * .73, depth * .35), (radius, depth),
(radius * .87, depth), (radius * .59, depth * .35),
(0, depth * .20)], mat, axis, segments)
def _horn(name, loc, radius, depth, mat, axis=(0, -1, .18)):
_lathe(name, loc, [(radius * .16, 0), (radius * .25, depth * .42),
(radius * .58, depth * .76), (radius, depth),
(radius * .84, depth), (radius * .43, depth * .72),
(radius * .1, depth * .35), (0, depth * .29),
(0, 0)], mat, axis, 10)
def _spike(name, start, end, radius, mat, sides=5):
axis = tuple(end[i] - start[i] for i in range(3))
u, v, _ = _frame(axis)
verts = [_add(start, _add(_mul(u, radius * cos(2 * pi * k / sides)),
_mul(v, radius * sin(2 * pi * k / sides))))
for k in range(sides)]
verts.append(end)
faces = [tuple(reversed(range(sides)))]
faces.extend((k, (k + 1) % sides, sides) for k in range(sides))
mesh_object(name, verts, faces, mat)
def _leaf(name, loc, width, height, mat, lean=0):
# Thick diamond leaf: large central ridge, only eight triangles.
x, y, z = loc
verts = [(x, y, z), (x - width / 2, y, z + height * .42),
(x + lean, y + .05, z + height),
(x + width / 2, y, z + height * .42),
(x + lean * .3, y - .09, z + height * .45),
(x + lean * .3, y + .055, z + height * .45)]
faces = [(0, 4, 1), (1, 4, 2), (2, 4, 3), (3, 4, 0),
(1, 5, 0), (2, 5, 1), (3, 5, 2), (0, 5, 3)]
mesh_object(name, verts, faces, mat)
def _eyes(loc, mat, spread=.075, scale=.038):
for side, suffix in ((-1, "L"), (1, "R")):
p = (loc[0] + side * spread, loc[1], loc[2])
ellipsoid("Head_Eye" + suffix, p, (scale, .023, scale * .75), mat)
def _mask(name, loc, scale, mat, eyes_mat):
ellipsoid(name, loc, (.15 * scale, .075 * scale, .22 * scale), mat)
for sign, suffix in ((-1, "L"), (1, "R")):
ellipsoid(name + "_Eye" + suffix,
(loc[0] + sign * .052 * scale, loc[1] - .071 * scale,
loc[2] + .023 * scale),
(.028 * scale, .012 * scale, .046 * scale), eyes_mat)
def _basalt_legs(mat, wide=False):
spread = .32 if wide else .22
for side, suffix in ((-1, "L"), (1, "R")):
ellipsoid("Leg" + suffix + "_Foot", (side * spread, -.075, .12),
(.20 if wide else .15, .24, .12), mat)
beam("Leg" + suffix + "_Shin", (side * spread, 0, .15),
(side * spread * .7, .02, .49), .095, mat, vertices=6)
def _mechanical_legs(mat, wide=False):
spread = .27 if wide else .20
for side, suffix in ((-1, "L"), (1, "R")):
box("Leg" + suffix + "_Foot", (side * spread, -.07, .07),
(.22, .33, .14), mat["accent"])
beam("Leg" + suffix + "_Shin", (side * spread, 0, .14),
(side * spread, .08, .35), .065, mat["accent"], vertices=6)
ellipsoid("Leg" + suffix + "_Knee", (side * spread, .08, .35),
(.10, .10, .10), mat["secondary"])
beam("Leg" + suffix + "_Thigh", (side * spread, .08, .35),
(side * spread * .66, 0, .57), .09, mat["primary"], vertices=6)
def _root_feet(mat, count=4, spread=.43, top=.60):
for k in range(count):
a = 2 * pi * k / count + pi / 4
base = (spread * cos(a), spread * sin(a), .055)
elbow = (spread * .6 * cos(a), spread * .6 * sin(a), .24)
suffix = str(k + 1)
beam("Leg_Root" + suffix, base, elbow, .09, mat, vertices=5)
beam("Leg_RootUpper" + suffix, elbow, (0, .02, top), .12,
mat, vertices=5)
ellipsoid("Leg_RootToe" + suffix, base, (.16, .10, .055), mat)
def _mist_robe(mat, upgraded=False):
# Alternating long cloth points touch the floor; the upper body floats.
cone("Body_Robe", (0, .035, .49), .31 if upgraded else .25,
.13, .74, mat["primary"], vertices=7)
for k in range(5):
a = 2 * pi * k / 5
_spike("Body_ClothPoint" + str(k),
(.16 * cos(a), .035 + .16 * sin(a), .35),
(.31 * cos(a), .035 + .27 * sin(a), .015), .11,
mat["secondary"] if k % 2 else mat["primary"])
ellipsoid("Body_Chest", (0, 0, .90), (.23, .16, .23), mat["secondary"])
def _glut_ranged(up, m):
_basalt_legs(m["primary"], up)
ellipsoid("Body_Core", (0, .035, .69), (.30 if up else .21, .22, .27),
m["primary"])
ellipsoid("Head_Helm", (0, -.015, 1.12), (.19, .16, .20), m["light"])
_eyes((0, -.17, 1.13), m["secondary"])
box("Body_Ember", (0, -.197, .71), (.11, .045, .19), m["secondary"])
if up:
# Broad glass bow arms distinguish the upgrade from the slender lance.
box("Weapon_Rail", (0, -.20, .96), (.10, .98, .09), m["accent"])
for s, suffix in ((-1, "L"), (1, "R")):
beam("Arm" + suffix, (s * .2, 0, .88), (s * .30, -.15, .91),
.075, m["primary"], vertices=6)
_spike("Weapon_GlassBow" + suffix, (s * .06, -.25, .99),
(s * .62, -.04, 1.25), .16, m["secondary"])
beam("Weapon_String" + suffix, (s * .62, -.04, 1.25),
(0, .22, 1.03), .012, m["light"], vertices=4)
beam("Weapon_Bolt", (0, .20, 1.02), (0, -.64, 1.02), .025, m["light"])
_spike("Weapon_BoltHead", (0, -.57, 1.02), (0, -.80, 1.02),
.08, m["secondary"])
else:
beam("ArmL", (-.15, 0, .89), (-.25, -.07, .70), .07, m["primary"])
beam("ArmR", (.15, 0, .90), (.30, -.19, .91), .075, m["primary"])
beam("Weapon_Lance", (.29, .34, .81), (.29, -.60, 1.05), .038,
m["accent"])
_spike("Weapon_GlassTip", (.29, -.53, 1.03), (.29, -.81, 1.11),
.105, m["secondary"])
def _nieten_ranged(up, m):
_mechanical_legs(m)
cone("Body_Cuirass", (0, .02, .78), .21, .27, .43, m["primary"], vertices=6)
box("Body_Plate", (0, -.18, .80), (.27, .09, .22), m["secondary"])
beam("Head_Neck", (0, 0, 1.0), (0, 0, 1.10), .075, m["accent"])
box("Head_Housing", (0, -.015, 1.22), (.29, .25, .25), m["light"])
box("Head_Visor", (0, -.146, 1.23), (.18, .028, .05), m["primary"])
weapon_x = 0 if up else .29
beam("ArmL", (-.21, 0, .92), (-.28, -.13, .77), .065, m["accent"])
beam("ArmR", (.21, 0, .92), (weapon_x + .07, -.24, .91), .065,
m["accent"])
for s, suffix in ((-1, "L"), (1, "R")):
box("Weapon_Rail" + suffix, (weapon_x + s * .055, -.30, .97),
(.055, .82 if up else .61, .07), m["accent"])
tip = (weapon_x + s * (.44 if up else .23), -.27, .97)
beam("Weapon_Bow" + suffix, (weapon_x, -.43, .97), tip, .055,
m["secondary"], vertices=6)
beam("Weapon_String" + suffix, tip, (weapon_x, .02, .97), .011,
m["light"], vertices=4)
_spike("Weapon_Bolt", (weapon_x, -.44, .98),
(weapon_x, -.78 if up else -.66, .98), .052, m["light"])
if up:
box("Weapon_Sight", (0, -.13, 1.08), (.08, .25, .09), m["primary"])
_spike("Head_Crest", (0, .03, 1.34), (0, .10, 1.58), .075,
m["secondary"])
for s, suffix in ((-1, "L"), (1, "R")):
box("Arm" + suffix + "_Pauldron", (s * .27, .01, .96),
(.16, .25, .13), m["secondary"])
def _nebel_ranged(up, m):
_mist_robe(m, up)
_mask("Head_Mask", (0, -.105, 1.18), 1.0, m["light"], m["primary"])
_leaf("Head_Hood", (0, .055, 1.16), .35, .37, m["primary"])
for s, suffix in ((-1, "L"), (1, "R")):
beam("Arm" + suffix, (s * .18, 0, .94), (s * .31, -.18, .77),
.055, m["secondary"], vertices=5)
horn_sides = (-1, 1) if up else (1,)
for s in horn_sides:
suffix = "L" if s < 0 else "R"
root = (s * .25, -.12, .79)
elbow = (s * .46, .07, 1.08 if up else .97)
mouth = (s * (.48 if up else .31), -.03, 1.27 if up else 1.02)
beam("Weapon_HornStem" + suffix, root, elbow, .055, m["secondary"])
beam("Weapon_HornCurve" + suffix, elbow, mouth, .072, m["light"])
_horn("Weapon_HornBell" + suffix, mouth, .19 if up else .16,
.40 if up else .49, m["light"])
axis = _unit((0, -1, .18))
lip = _add(mouth, _mul(axis, .39 if up else .48))
_lathe("Weapon_MintLip" + suffix, lip,
[(.19 if up else .16, 0), (.19 if up else .16, .025),
(.16 if up else .132, .025), (.16 if up else .132, 0)],
m["accent"], axis)
def _wurzel_ranged(up, m):
_root_feet(m["primary"], 3, .34, .56)
ellipsoid("Body_Trunk", (0, .025, .76), (.19, .16, .29), m["primary"])
ellipsoid("Head_Seed", (0, -.025, 1.13), (.17, .15, .20), m["secondary"])
_eyes((0, -.17, 1.13), m["accent"], spread=.057, scale=.03)
_leaf("Body_LeafMantle", (0, -.14, .59), .43, .40, m["accent"])
for s, suffix in ((-1, "L"), (1, "R")):
shoulder = (s * .17, 0, .91)
hand = (s * .29, -.21, .85)
beam("Arm" + suffix, shoulder, hand, .07, m["primary"], vertices=5)
_leaf("Arm" + suffix + "_Leaf", (s * .26, -.13, .81), .21, .26,
m["secondary"], lean=s * .1)
beam("Weapon_VineLower", (.28, -.17, .73), (.44, -.08, .93), .046,
m["primary"], vertices=5)
beam("Weapon_VineUpper", (.44, -.08, .93), (.40, -.17, 1.17), .04,
m["primary"], vertices=5)
_cup("Weapon_SeedCapsule", (.24, -.19, .88), .19 if up else .15,
.20, m["accent"], axis=(0, -.7, .7), segments=8)
ellipsoid("Weapon_Seed", (.24, -.30, 1.02), (.075, .075, .10), m["light"])
if up:
# Overlapping tiers make a pinecone crown, not a mushroom cap.
for layer in range(3):
z = 1.24 + layer * .10
radius = .23 - layer * .035
cone("Head_ConeTier" + str(layer), (0, .025, z), radius,
radius * .47, .16, m["primary"], vertices=7)
for k in range(5):
a = 2 * pi * k / 5 + layer * .3
_leaf("Head_ConeScale" + str(layer) + "_" + str(k),
(radius * .75 * cos(a), .025 + radius * .75 * sin(a), z - .05),
.11, .16, m["accent"])
else:
_leaf("Head_LeafCrest", (0, .04, 1.22), .22, .25, m["accent"], .08)
def _glut_artillery(up, m):
_basalt_legs(m["primary"], True)
ellipsoid("Body_Foundation", (0, .025, .48), (.39, .31, .22), m["primary"])
_cup("Weapon_Crater", (0, .02, .52), .55 if up else .43,
.63 if up else .50, m["primary"], segments=12)
_cup("Weapon_EmberLining", (0, .02, .66), .37 if up else .29,
.24 if up else .20, m["secondary"], segments=10)
for s, suffix in ((-1, "L"), (1, "R")):
ellipsoid("Arm" + suffix + "_Slab", (s * .39, .0, .65),
(.13, .19, .25), m["primary"])
ellipsoid("Head_Eye" + suffix, (s * .13, -.28, .53),
(.055, .025, .035), m["secondary"])
for k in range(3 if up else 2):
ellipsoid("Weapon_Coal" + str(k),
((k - (1 if up else .5)) * .12, .015, .80 if up else .77),
(.105, .10, .10), m["accent"])
if up:
for k in range(5):
a = .2 + pi * k / 4
_spike("Body_CraterFin" + str(k),
(.40 * cos(a), .03 + .39 * sin(a), .87),
(.46 * cos(a), .03 + .43 * sin(a), 1.37), .095,
m["primary"])
def _nieten_artillery(up, m):
_mechanical_legs(m, True)
box("Body_Chassis", (0, .035, .67), (.56 if up else .45, .37, .34),
m["primary"])
for s, suffix in ((-1, "L"), (1, "R")):
beam("Arm" + suffix + "_Piston", (s * .25, .1, .60),
(s * .34, .17, .96), .065, m["accent"])
cylinder("Arm" + suffix + "_Collar", (s * .31, .15, .84), .095,
.15, m["secondary"], vertices=8)
bell_positions = (-.29, .29) if up else (0,)
for i, x in enumerate(bell_positions):
base = (x, .08, .92)
_horn("Weapon_Resonator" + str(i), base, .27 if up else .34,
.48, m["secondary"], axis=(0, -1, .3))
# A dark interior cavity and a slim central piston remain visible.
beam("Weapon_Clapper" + str(i), (x, .045, .94), (x, -.26, 1.035),
.038, m["accent"])
ellipsoid("Weapon_ClapperHead" + str(i), (x, -.27, 1.04),
(.09, .07, .09), m["light"])
box("Head_Visor", (0, -.164, .68), (.20, .03, .075), m["light"])
for x, suffix in ((-.24, "L"), (.24, "R")):
cylinder("Body_Exhaust" + suffix, (x, .25, 1.03), .075,
.50 if up else .34, m["accent"], vertices=8)
if up:
box("Body_Brow", (0, -.10, 1.34), (.66, .14, .085), m["primary"])
cone("Head_Metronome", (0, -.03, 1.47), .10, 0, .20,
m["light"], vertices=4)
def _nebel_artillery(up, m):
# Face forward/slightly up: the cavity remains visible to an elevated camera.
_mist_robe(m, up)
bell_base = (0, -.16, 1.05 if up else .85)
radius, depth = (.45, .60) if up else (.38, .50)
_horn("Body_CathedralBell", bell_base, radius, depth,
m["light"], axis=(0, -1, .12))
beam("Weapon_ClapperStem", (0, -.27, 1.09 if up else .90),
(0, -.70 if up else -.60, 1.0 if up else .80), .045, m["accent"])
ellipsoid("Weapon_Clapper", (0, -.73 if up else -.63, 1.0 if up else .80),
(.13, .10, .13), m["accent"])
for s, suffix in ((-1, "L"), (1, "R")):
beam("Arm" + suffix, (s * .18, .02, .96),
(s * .44, -.08, .63), .07, m["secondary"], vertices=5)
_spike("Arm" + suffix + "_Sickle", (s * .44, -.08, .63),
(s * .49, -.19, .94), .10, m["primary"])
_mask("Head_Mask", (0, .035, 1.49 if up else 1.38), .65,
m["secondary"], m["accent"])
if up:
for i, x in enumerate((-.27, 0, .27)):
beam("Head_SpireBase" + str(i), (x * .65, .08, 1.16),
(x, .09, 1.42), .075, m["primary"], vertices=5)
_spike("Head_Spire" + str(i), (x, .09, 1.40),
(x * .65, -.01, 1.71 if i == 1 else 1.59),
.075, m["light"])
def _mushroom(name, loc, radius, height, m):
x, y, z = loc
cone(name + "_Stem", (x, y, z + height * .45), .095, .07, height * .9,
m["primary"], vertices=7)
# An angular overhanging cap and light gill ring form a readable roof.
cone(name + "_Gills", (x, y, z + height * .86), radius * .91,
radius * .5, .10, m["accent"], vertices=10)
cone(name + "_Cap", (x, y, z + height), radius, radius * .15,
.22, m["light"], vertices=10)
def _wurzel_artillery(up, m):
_root_feet(m["primary"], 5, .50 if up else .43, .56)
ellipsoid("Body_Moss", (0, .02, .48), (.32, .26, .18), m["secondary"])
_cup("Weapon_SporeChalice", (0, -.10, .50), .40 if up else .43,
.43, m["light"], segments=12)
_cup("Weapon_SporeInterior", (0, -.10, .60), .28, .18,
m["secondary"], segments=10)
for i, x in enumerate((-.105, .105)):
ellipsoid("Weapon_Spore" + str(i), (x, -.08, .81),
(.06, .06, .075), m["accent"])
for s, suffix in ((-1, "L"), (1, "R")):
_leaf("Arm" + suffix + "_Leaf", (s * .30, -.05, .46), .20, .36,
m["accent"], lean=s * .14)
if up:
_mushroom("Body_RoofL", (-.34, .12, .47), .30, .67, m)
_mushroom("Body_RoofR", (.34, .18, .47), .29, .81, m)
_mushroom("Body_RoofCrown", (0, .33, .54), .39, .98, m)
for x, suffix in ((-.12, "L"), (.12, "R")):
beam("Body_Buttress" + suffix, (x * 3, .33, .12),
(x, .31, 1.21), .075, m["primary"], vertices=5)
def _glut_support(up, m):
_basalt_legs(m["primary"], up)
ellipsoid("Body_FurnaceBack", (0, .11, .79),
(.34 if up else .28, .20, .37), m["primary"])
_cup("Body_OpenHearth", (0, .015, .85), .28 if up else .23,
.33, m["accent"], axis=(0, -1, 0), segments=8)
ellipsoid("Body_Ember", (0, -.10, .85), (.12, .085, .15), m["secondary"])
ellipsoid("Head_Capstone", (0, .01, 1.17), (.21, .17, .15), m["light"])
_eyes((0, -.15, 1.16), m["secondary"], spread=.075)
for s, suffix in ((-1, "L"), (1, "R")):
beam("Arm" + suffix, (s * .26, .01, .92),
(s * .33, -.33, .63), .09, m["primary"], vertices=6)
_cup("Weapon_WarmingBowl", (0, -.38, .52), .36 if up else .29,
.22, m["accent"], segments=10)
ellipsoid("Weapon_WarmCoal", (0, -.38, .61), (.15, .12, .07),
m["secondary"])
if up:
# Upper-half sun rays stay behind the face, safely within the footprint.
for k in range(7):
a = pi * k / 6
start = (.30 * cos(a), .09, 1.12 + .30 * sin(a))
end = (.48 * cos(a), .11, 1.12 + .48 * sin(a))
_spike("Head_SunRay" + str(k), start, end, .065, m["accent"])
cone("Head_Crown", (0, .0, 1.34), .16, .23, .12,
m["accent"], vertices=8)
def _nieten_support(up, m):
_mechanical_legs(m, up)
cone("Body_Coat", (0, .02, .73), .37 if up else .24,
.20, .51, m["primary"], vertices=6)
box("Body_PendulumCase", (0, -.18, .79), (.18, .10, .30), m["secondary"])
beam("Body_Pendulum", (0, -.24, .89), (.045, -.24, .64), .018, m["accent"])
ellipsoid("Body_PendulumWeight", (.045, -.24, .65), (.07, .035, .07),
m["light"])
ellipsoid("Head_Casing", (0, .015, 1.14), (.17, .14, .17), m["accent"])
ellipsoid("Head_Dial", (0, -.119, 1.14), (.135, .025, .135), m["light"])
beam("Head_DialHand", (0, -.15, 1.14), (.055, -.15, 1.22), .015,
m["primary"], vertices=4)
for s, suffix in ((-1, "L"), (1, "R")):
elbow = (s * .35, .0, .91)
hand = (s * .45, -.13, 1.10 if s > 0 else .89)
beam("Arm" + suffix + "_Upper", (s * .2, .02, .95), elbow, .06,
m["accent"])
beam("Arm" + suffix + "_Fore", elbow, hand, .047, m["secondary"])
ellipsoid("Arm" + suffix + "_Hand", hand, (.065, .065, .065), m["light"])
beam("Weapon_Baton", (.45, -.13, 1.06), (.61, -.22, 1.45), .019,
m["light"], vertices=5)
if up:
for k, x in enumerate((-.18, 0, .18)):
_spike("Head_CrownTine" + str(k), (x, .015, 1.28),
(x, .015, 1.56 if k == 1 else 1.47), .06,
m["secondary"])
box("Head_MetronomeBridge", (0, .015, 1.36), (.45, .12, .07),
m["secondary"])
for s in (-1, 1):
_leaf("Body_CoatTail" + str(s), (s * .20, .20, .33), .24, .59,
m["secondary"], lean=s * .16)
else:
_spike("Head_Spindle", (0, .015, 1.28), (0, .015, 1.45), .055,
m["secondary"])
def _nebel_support(up, m):
_mist_robe(m, up)
_mask("Head_MainMask", (0, -.13, 1.17), 1.0, m["light"], m["primary"])
for s, suffix in ((-1, "L"), (1, "R")):
beam("Arm" + suffix + "_Lower", (s * .17, 0, .88),
(s * .37, -.04, .82), .062, m["secondary"], vertices=5)
_spike("Arm" + suffix + "_Sickle", (s * .37, -.04, .82),
(s * .49, -.04, 1.21), .095, m["primary"])
_leaf("Body_SickleCollar" + suffix, (s * .13, .02, .88), .29,
.43 if up else .29, m["secondary"], lean=s * .29)
if up:
positions = ((-.51, -.035, 1.46), (0, .11, 1.65), (.51, -.035, 1.46))
for i, p in enumerate(positions):
_mask("Head_CompanionMask" + str(i), p, .67,
m["light"], m["primary"])
for i, x in enumerate((-.12, 0, .12)):
_spike("Head_Crown" + str(i), (x, -.035, 1.31),
(x * 1.2, -.035, 1.53 if i == 1 else 1.45), .045,
m["accent"])
else:
_mask("Head_CompanionMask0", (.49, -.035, 1.44), .70,
m["light"], m["primary"])
ellipsoid("Body_ChoirGem", (0, -.19, .83), (.055, .035, .09), m["accent"])
def _flower(name, loc, scale, m):
for k in range(5):
a = 2 * pi * k / 5
ellipsoid(name + "_Petal" + str(k),
(loc[0] + cos(a) * .09 * scale,
loc[1] + sin(a) * .09 * scale, loc[2]),
(.085 * scale, .07 * scale, .035 * scale), m["light"])
ellipsoid(name + "_Heart", (loc[0], loc[1], loc[2] + .025 * scale),
(.052 * scale, .052 * scale, .035 * scale), m["accent"])
def _wurzel_support(up, m):
_root_feet(m["primary"], 5 if up else 3, .45 if up else .31, .55)
# Two counter-leaning stems create an open woven body in the upgrade.
if up:
for s, suffix in ((-1, "L"), (1, "R")):
beam("Body_WovenRoot" + suffix, (s * .20, .02, .38),
(-s * .17, .015, .83), .105, m["primary"], vertices=5)
beam("Body_WovenRootTop" + suffix, (-s * .17, .015, .83),
(s * .11, .025, 1.16), .085, m["primary"], vertices=5)
else:
ellipsoid("Body_Trunk", (0, .025, .72), (.19, .16, .29), m["primary"])
ellipsoid("Head_Seed", (0, -.03, 1.12), (.15, .14, .18), m["secondary"])
_eyes((0, -.17, 1.12), m["accent"], spread=.05, scale=.026)
for s, suffix in ((-1, "L"), (1, "R")):
beam("Arm" + suffix, (s * .15, 0, .94),
(s * .28, -.27, .69), .065, m["primary"], vertices=5)
_leaf("Arm" + suffix + "_Leaf", (s * .23, -.21, .68), .21, .28,
m["secondary"], lean=s * .12)
_cup("Weapon_LeafBasin", (0, -.31, .62), .30 if up else .23,
.20, m["accent"], segments=8)
# Water uses the optional shared mint accent, with a leaf-colour fallback.
# Opaque faceted geometry avoids sorting issues until the VFX integration.
water = m.get("water", m["accent"])
cylinder("Weapon_Pool", (0, -.31, .739), .21 if up else .145,
.025, water, vertices=10)
ellipsoid("Weapon_Dew", (0, -.31, .86), (.085, .075, .13), water)
_spike("Weapon_DewTip", (0, -.31, .93), (0, -.31, 1.035), .059, water)
if up:
for s, suffix in ((-1, "L"), (1, "R")):
beam("Body_CanopyStem" + suffix, (s * .17, .11, .88),
(s * .36, .14, 1.28), .055, m["primary"], vertices=5)
_leaf("Head_Canopy" + suffix, (s * .12, .10, 1.17), .56, .36,
m["secondary"], lean=s * .28)
_flower("Head_Flower" + suffix, (s * .31, .12, 1.43), 1.0, m)
_flower("Head_FlowerCrown", (0, .07, 1.53), .9, m)
else:
_leaf("Head_LeafCap", (0, .02, 1.23), .33, .25, m["accent"], -.06)
_RANGED = {"glutpakt": _glut_ranged, "nietenkonvent": _nieten_ranged,
"nebelchor": _nebel_ranged, "wurzelbund": _wurzel_ranged}
_ARTILLERY = {"glutpakt": _glut_artillery, "nietenkonvent": _nieten_artillery,
"nebelchor": _nebel_artillery, "wurzelbund": _wurzel_artillery}
_SUPPORT = {"glutpakt": _glut_support, "nietenkonvent": _nieten_support,
"nebelchor": _nebel_support, "wurzelbund": _wurzel_support}
def build_ranged(spec, mat):
"""Build IDs *05 (base) and *06 (upgraded)."""
_RANGED[spec["faction"]](spec["upgraded"], mat)
def build_artillery(spec, mat):
"""Build IDs *07 (base) and *08 (upgraded)."""
_ARTILLERY[spec["faction"]](spec["upgraded"], mat)
def build_support(spec, mat):
"""Build IDs *09 (base) and *10 (upgraded)."""
_SUPPORT[spec["faction"]](spec["upgraded"], mat)