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>
570 lines
25 KiB
Python
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)
|