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