"""Modular terrain, team cores and neutral figures for the Saumwacht asset set. Blender coordinates: Z up, forward -Y. This module only constructs meshes; the caller owns collection cleanup, material creation, export and validation. """ from math import cos, pi, sin from common import box, ellipsoid, cylinder, cone, beam, ring, mesh_object, empty def _prism(name, outline, front_y, back_y, material): """Extrude an X/Z outline along Y, useful for readable masks and wedges.""" area = sum(outline[i][0] * outline[(i + 1) % len(outline)][1] - outline[(i + 1) % len(outline)][0] * outline[i][1] for i in range(len(outline))) if area < 0: outline = list(reversed(outline)) count = len(outline) vertices = [(x, front_y, z) for x, z in outline] vertices += [(x, back_y, z) for x, z in outline] faces = [tuple(range(count)), tuple(range(2 * count - 1, count - 1, -1))] for i in range(count): following = (i + 1) % count faces.append((i, i + count, following + count, following)) return mesh_object(name, vertices, faces, material) def _figure(spec, nominal_height): """Scaled primitive wrappers keep species proportions stable at any height.""" scale = float(spec.get("height", nominal_height)) / nominal_height def point(value): return tuple(component * scale for component in value) class Figure: def box(self, name, loc, size, material, rotation=None): return box(name, point(loc), point(size), material, rotation=rotation) def ellipsoid(self, name, loc, size, material, subdivisions=1): return ellipsoid(name, point(loc), point(size), material, subdivisions=subdivisions) def cylinder(self, name, loc, radius, depth, material, vertices=8, rotation=None): return cylinder(name, point(loc), radius * scale, depth * scale, material, vertices=vertices, rotation=rotation) def cone(self, name, loc, radius1, radius2, depth, material, vertices=8, rotation=None): return cone(name, point(loc), radius1 * scale, radius2 * scale, depth * scale, material, vertices=vertices, rotation=rotation) def beam(self, name, start, end, radius, material, vertices=6): return beam(name, point(start), point(end), radius * scale, material, vertices=vertices) def ring(self, name, loc, major_radius, minor_radius, material, rotation=None, segments=10): return ring(name, point(loc), major_radius * scale, minor_radius * scale, material, rotation=rotation, segments=segments) def prism(self, name, outline, front_y, back_y, material): return _prism(name, [(x * scale, z * scale) for x, z in outline], front_y * scale, back_y * scale, material) return Figure() def _boots(f, material, x=0.22, width=0.26, height=0.18, depth=0.42): for side, sign in (("L", -1), ("R", 1)): f.box("Leg" + side + "_Boot", (sign * x, -0.075, height / 2), (width, depth, height), material) def _eyes(f, material, z, y, separation=0.10): for side, sign in (("L", -1), ("R", 1)): f.box("Head_Eye" + side, (sign * separation, y, z), (0.065, 0.025, 0.034), material) def build_lane(spec, mat): """12 m traversable width, 4 m repeat length, deck at Z=0. Side decoration remains strictly outside X=[-6,+6], and nothing extends past Y=[-2,+2], so consecutive copies meet without a transverse obstacle. """ box("Body_LaneFoundation", (0, 0, -0.195), (12, 4, 0.31), mat["primary"]) for column in range(6): box("Body_LaneSlab_%02d" % column, (-5 + column * 2, 0, -0.02), (2, 4, 0.04), mat["primary"]) for side, sign in (("L", -1), ("R", 1)): box("Body_" + side + "RetainingLip", (sign * 6.15, 0, -0.10), (0.30, 4, 0.50), mat["secondary"]) for index, y in enumerate((-1.42, 0, 1.42)): ellipsoid("Body_" + side + "Basalt_%02d" % index, (sign * 6.46, y, -0.05), (0.26, 0.42, 0.26 if index != 1 else 0.30), mat["secondary"]) for index, y in enumerate((-1.15, 1.15)): box("Body_" + side + "BrassMarker_%02d" % index, (sign * 6.15, y, 0.165), (0.19, 0.20, 0.03), mat["accent"]) ellipsoid("Body_" + side + "Moss", (sign * 6.39, 0.65, 0.02), (0.17, 0.34, 0.13), mat["light"]) empty("Anchor_Deck", (0, 0, 0)) empty("Anchor_JoinNorth", (0, 2, 0)) empty("Anchor_JoinSouth", (0, -2, 0)) def build_core(spec, mat): """A grounded crystal cradle with a physical team circle or triangle.""" triangular = int(spec.get("team", 0)) == 1 sides = 3 if triangular else 12 cylinder("Body_CorePlinth", (0, 0, 0.13), 1.26, 0.26, mat["primary"], vertices=sides) cylinder("Body_BrassPlinthRim", (0, 0, 0.285), 1.18, 0.05, mat["secondary"], vertices=sides) cylinder("Body_CradleStep", (0, 0, 0.37), 0.85, 0.12, mat["primary"], vertices=8) if triangular: points = [(1.06 * sin(i * 2 * pi / 3), -1.06 * cos(i * 2 * pi / 3), 0.34) for i in range(3)] for i in range(3): beam("Team_Triangle_%d" % i, points[i], points[(i + 1) % 3], 0.045, mat["accent"], vertices=6) else: ring("Team_Circle", (0, 0, 0.345), 1.025, 0.045, mat["accent"], segments=20) # Multiple low-poly rings create a recognizable mineral, not a sphere. vertices = [(0, 0, 0.51)] for radius, z in ((0.37, 0.70), (0.58, 1.16), (0.36, 1.72)): vertices.extend((radius * cos(i * pi / 3), radius * sin(i * pi / 3), z) for i in range(6)) vertices.append((0, 0, 2.20)) faces = [(0, 1 + (i + 1) % 6, 1 + i) for i in range(6)] for row in range(2): start = 1 + 6 * row for i in range(6): following = (i + 1) % 6 faces.append((start + i, start + following, start + 6 + following, start + 6 + i)) faces.extend((13 + i, 13 + (i + 1) % 6, 19) for i in range(6)) mesh_object("Body_CoreCrystal", vertices, faces, mat["accent"]) for index in range(4): angle = pi / 4 + index * pi / 2 x, y = sin(angle), cos(angle) beam("Arm_Cradle_%d" % index, (x * 0.72, y * 0.72, 0.42), (x * 0.47, y * 0.47, 0.88), 0.075, mat["secondary"]) ellipsoid("Arm_CradleClasp_%d" % index, (x * 0.48, y * 0.48, 0.85), (0.12, 0.12, 0.09), mat["light"]) empty("Anchor_CoreEffect", (0, 0, 1.25)) def build_worker(spec, mat): """Small clay Funkenleser with a fork and a framed energy container.""" f = _figure(spec, 1.1) _boots(f, mat["primary"], x=0.18, width=0.23, height=0.14, depth=0.31) for side, sign in (("L", -1), ("R", 1)): f.beam("Leg" + side, (sign * 0.16, 0, 0.14), (sign * 0.15, 0, 0.36), 0.10, mat["primary"]) f.ellipsoid("Body_Clay", (0, 0, 0.49), (0.30, 0.21, 0.30), mat["primary"]) f.prism("Body_Apron", [(-0.20, 0.68), (-0.24, 0.30), (0.24, 0.30), (0.20, 0.68)], -0.23, -0.19, mat["secondary"]) f.box("Body_Buckle", (0, -0.248, 0.56), (0.10, 0.035, 0.07), mat["accent"]) f.ellipsoid("Head_Hood", (0, 0.005, 0.88), (0.26, 0.235, 0.22), mat["secondary"]) f.ellipsoid("Head_ClayFace", (0, -0.175, 0.88), (0.195, 0.10, 0.17), mat["primary"]) _eyes(f, mat["secondary"], 0.905, -0.274, separation=0.075) f.beam("ArmL", (-0.23, 0, 0.64), (-0.40, -0.19, 0.52), 0.085, mat["primary"]) f.beam("ArmR", (0.23, 0, 0.64), (0.39, -0.23, 0.54), 0.085, mat["primary"]) f.ellipsoid("ArmL_Hand", (-0.40, -0.19, 0.52), (0.105, 0.09, 0.09), mat["primary"]) f.ellipsoid("ArmR_Hand", (0.39, -0.23, 0.54), (0.105, 0.09, 0.09), mat["primary"]) f.beam("Weapon_SparkForkHandle", (-0.43, -0.20, 0.13), (-0.43, -0.20, 0.78), 0.032, mat["accent"]) for sign in (-1, 1): f.beam("Weapon_SparkForkProng_%d" % sign, (-0.43, -0.20, 0.78), (-0.43 + sign * 0.10, -0.20, 0.99), 0.026, mat["accent"]) f.cylinder("Weapon_ContainerBase", (0.44, -0.24, 0.32), 0.11, 0.045, mat["accent"]) f.cylinder("Weapon_ContainerLid", (0.44, -0.24, 0.61), 0.11, 0.045, mat["accent"]) # An open brass cage gives glass-like legibility without alpha sorting. for index in range(4): angle = index * pi / 2 f.beam("Weapon_ContainerRib_%d" % index, (0.44 + cos(angle) * 0.085, -0.24 + sin(angle) * 0.085, 0.33), (0.44 + cos(angle) * 0.085, -0.24 + sin(angle) * 0.085, 0.60), 0.016, mat["accent"], vertices=4) f.ellipsoid("Weapon_CapturedSpark", (0.44, -0.24, 0.47), (0.058, 0.058, 0.105), mat["light"]) def _scout(spec, mat): f = _figure(spec, 1.70) _boots(f, mat["primary"], x=0.18, width=0.23, height=0.18) for side, sign in (("L", -1), ("R", 1)): f.beam("Leg" + side, (sign * 0.18, 0, 0.17), (sign * 0.15, 0, 0.78), 0.087, mat["primary"]) f.box("Leg" + side + "_ShinPlate", (sign * 0.18, -0.095, 0.35), (0.13, 0.05, 0.24), mat["secondary"]) f.cone("Body_Armor", (0, 0, 0.97), 0.22, 0.28, 0.49, mat["primary"], vertices=6) f.box("Body_Belt", (0, 0, 0.77), (0.44, 0.30, 0.10), mat["secondary"]) f.ellipsoid("Head_Hood", (0, 0.015, 1.42), (0.23, 0.21, 0.25), mat["primary"]) f.cone("Head_HoodPoint", (0, 0.055, 1.60), 0.145, 0, 0.20, mat["primary"], vertices=5) f.prism("Head_Mask", [(-0.15, 1.54), (-0.13, 1.32), (0, 1.26), (0.13, 1.32), (0.15, 1.54)], -0.21, -0.165, mat["light"]) _eyes(f, mat["primary"], 1.46, -0.225, separation=0.075) f.beam("ArmL_Upper", (-0.24, 0, 1.12), (-0.34, -0.17, 0.96), 0.078, mat["primary"]) f.beam("ArmL_Fore", (-0.34, -0.17, 0.96), (-0.06, -0.37, 1.02), 0.071, mat["primary"]) f.beam("ArmR_Upper", (0.24, 0, 1.12), (0.35, -0.15, 0.97), 0.078, mat["primary"]) f.beam("ArmR_Fore", (0.35, -0.15, 0.97), (0.25, -0.43, 1.07), 0.071, mat["primary"]) f.box("Weapon_DartLauncherStock", (0.19, -0.34, 1.07), (0.19, 0.44, 0.15), mat["secondary"]) f.cylinder("Weapon_DartLauncherBarrel", (0.19, -0.66, 1.09), 0.065, 0.34, mat["light"], rotation=(pi / 2, 0, 0)) f.cylinder("Weapon_DartLauncherBore", (0.19, -0.836, 1.09), 0.04, 0.008, mat["primary"], rotation=(pi / 2, 0, 0)) f.box("Body_DartQuiver", (-0.14, 0.23, 1.02), (0.23, 0.17, 0.45), mat["secondary"], rotation=(0, -0.16, 0)) for index in range(3): x = -0.23 + index * 0.085 f.beam("Weapon_SpareDart_%d" % index, (x, 0.24, 1.10), (x - 0.06, 0.24, 1.47), 0.024, mat["light"], vertices=4) def _ram(spec, mat): f = _figure(spec, 1.48) _boots(f, mat["primary"], x=0.27, width=0.35, height=0.20, depth=0.45) for side, sign in (("L", -1), ("R", 1)): f.box("Leg" + side, (sign * 0.27, 0, 0.35), (0.25, 0.30, 0.35), mat["primary"]) f.ellipsoid("Arm" + side + "_Pauldron", (sign * 0.43, 0, 1.05), (0.27, 0.25, 0.25), mat["secondary"]) f.box("Body_RustArmor", (0, 0, 0.83), (0.74, 0.48, 0.67), mat["secondary"]) f.box("Body_UpperLamella", (0, -0.256, 1.01), (0.71, 0.06, 0.18), mat["primary"]) f.box("Body_LowerLamella", (0, -0.261, 0.80), (0.71, 0.06, 0.16), mat["primary"]) f.ellipsoid("Head_Helmet", (0, 0, 1.29), (0.27, 0.23, 0.19), mat["primary"]) f.box("Head_BrowGuard", (0, -0.23, 1.35), (0.45, 0.085, 0.10), mat["light"]) _eyes(f, mat["accent"], 1.275, -0.235) f.beam("ArmL", (-0.43, 0, 1.03), (-0.43, -0.40, 0.81), 0.14, mat["primary"]) f.beam("ArmR", (0.43, 0, 1.03), (0.61, -0.18, 0.72), 0.14, mat["primary"]) f.box("ArmR_HammerFist", (0.61, -0.19, 0.66), (0.30, 0.28, 0.29), mat["secondary"]) outline = [(-0.63, 1.09), (-0.68, 0.34), (-0.48, 0.21), (0.45, 0.21), (0.65, 0.34), (0.60, 1.09)] f.prism("Weapon_RamShield", outline, -0.54, -0.39, mat["secondary"]) f.box("Weapon_ShieldTopRim", (-0.015, -0.575, 1.05), (1.18, 0.07, 0.095), mat["light"]) f.prism("Weapon_CentralWedge", [(-0.13, 0.94), (-0.18, 0.37), (0, 0.28), (0.18, 0.37), (0.13, 0.94)], -0.74, -0.53, mat["primary"]) def _singer(spec, mat): f = _figure(spec, 1.82) _boots(f, mat["primary"], x=0.18, width=0.23, height=0.12, depth=0.32) f.cone("Body_BellRobe", (0, 0, 0.63), 0.43, 0.20, 1.10, mat["primary"], vertices=8) for index, x in enumerate((-0.22, 0, 0.22)): f.prism("Body_RobeFold_%d" % index, [(x - 0.055, 0.15), (x + 0.055, 0.15), (x * 0.45, 1.09)], -0.32, -0.24, mat["accent"]) f.ellipsoid("Body_Mantle", (0, 0, 1.24), (0.37, 0.27, 0.21), mat["accent"]) f.ellipsoid("Head_DeepHood", (0, 0, 1.57), (0.26, 0.25, 0.25), mat["primary"]) f.prism("Head_SingerMask", [(-0.13, 1.68), (-0.12, 1.47), (0, 1.41), (0.12, 1.47), (0.13, 1.68)], -0.246, -0.21, mat["light"]) _eyes(f, mat["primary"], 1.60, -0.257, separation=0.065) f.beam("ArmL", (-0.29, 0, 1.27), (-0.34, -0.37, 1.05), 0.085, mat["primary"]) f.beam("ArmR", (0.29, 0, 1.27), (0.36, -0.43, 1.10), 0.085, mat["primary"]) f.ellipsoid("ArmL_Hand", (-0.34, -0.37, 1.05), (0.095, 0.075, 0.075), mat["light"]) f.ellipsoid("ArmR_Hand", (0.36, -0.43, 1.10), (0.095, 0.075, 0.075), mat["light"]) f.beam("Weapon_HornPipe", (-0.30, -0.37, 1.08), (0.36, -0.43, 1.13), 0.075, mat["secondary"]) # A true open bell: two rings connected by a tapered shell, no filled mouth. vertices = [] for y, radius in ((-0.43, 0.075), (-0.84, 0.28), (-0.81, 0.235), (-0.47, 0.042)): for index in range(10): angle = index * pi / 5 vertices.append((0.36 + cos(angle) * radius, y, 1.13 + sin(angle) * radius)) faces = [] for row in range(3): for i in range(10): j = (i + 1) % 10 faces.append((row * 10 + i, row * 10 + j, (row + 1) * 10 + j, (row + 1) * 10 + i)) scale = float(spec.get("height", 1.82)) / 1.82 mesh_object("Weapon_ResonanceHorn", [tuple(v * scale for v in point) for point in vertices], faces, mat["secondary"]) f.ring("Weapon_HornRim", (0.36, -0.84, 1.13), 0.258, 0.023, mat["light"], rotation=(pi / 2, 0, 0)) def _breaker(spec, mat): f = _figure(spec, 1.82) _boots(f, mat["primary"], x=0.28, width=0.36, height=0.22, depth=0.49) for side, sign in (("L", -1), ("R", 1)): f.box("Leg" + side, (sign * 0.26, 0, 0.47), (0.28, 0.31, 0.54), mat["primary"]) f.box("Leg" + side + "_Knee", (sign * 0.27, -0.19, 0.51), (0.27, 0.13, 0.24), mat["secondary"]) f.box("Arm" + side + "_Pauldron", (sign * 0.48, 0, 1.29), (0.42, 0.52, 0.34), mat["primary"], rotation=(0, sign * 0.14, 0)) f.cone("Body_SteelTorso", (0, 0, 1.01), 0.35, 0.47, 0.70, mat["primary"], vertices=6) f.prism("Body_RustChestInset", [(-0.28, 1.29), (0, 0.95), (0.28, 1.29)], -0.397, -0.35, mat["secondary"]) f.box("Head_Helmet", (0, 0, 1.61), (0.43, 0.40, 0.42), mat["primary"]) f.prism("Head_Faceplate", [(-0.17, 1.76), (-0.17, 1.50), (0, 1.44), (0.17, 1.50), (0.17, 1.76)], -0.23, -0.19, mat["light"]) f.box("Head_Visor", (0, -0.246, 1.66), (0.25, 0.026, 0.045), mat["primary"]) f.beam("ArmL", (-0.47, 0, 1.25), (-0.31, -0.50, 0.92), 0.12, mat["primary"]) f.beam("ArmR", (0.47, 0, 1.25), (0.30, -0.50, 1.20), 0.12, mat["primary"]) f.ellipsoid("ArmL_Hand", (-0.31, -0.50, 0.92), (0.145, 0.12, 0.12), mat["secondary"]) f.ellipsoid("ArmR_Hand", (0.30, -0.50, 1.20), (0.145, 0.12, 0.12), mat["secondary"]) f.beam("Weapon_HammerShaft", (-0.65, -0.51, 0.77), (0.66, -0.51, 1.36), 0.055, mat["secondary"]) f.prism("Weapon_KeilhammerHead", [(0.40, 1.26), (0.43, 1.58), (0.82, 1.80), (1.02, 1.61), (0.90, 1.18)], -0.79, -0.24, mat["primary"]) # Offset along the sloped edge normal; coplanar cap faces would flicker. f.prism("Weapon_ChiselEdge", [(0.832, 1.814), (1.032, 1.624), (0.992, 1.354), (0.932, 1.644)], -0.80, -0.23, mat["light"]) def _rift(spec, mat): f = _figure(spec, 2.08) _boots(f, mat["primary"], x=0.22, width=0.21, height=0.12, depth=0.35) for side, sign in (("L", -1), ("R", 1)): f.beam("Leg" + side + "_Shin", (sign * 0.22, 0, 0.12), (sign * 0.30, 0.12, 0.45), 0.069, mat["primary"]) f.beam("Leg" + side + "_Thigh", (sign * 0.30, 0.12, 0.45), (sign * 0.17, 0, 0.81), 0.083, mat["primary"]) f.ellipsoid("Body_Pelvis", (0, 0, 0.83), (0.25, 0.16, 0.16), mat["primary"]) # Separate ribs bound real negative space; there is deliberately no chest fill. for side, sign in (("L", -1), ("R", 1)): f.beam("Body_" + side + "LowerRib", (sign * 0.15, 0, 0.89), (sign * 0.31, 0, 1.18), 0.095, mat["primary"]) f.beam("Body_" + side + "UpperRib", (sign * 0.31, 0, 1.18), (sign * 0.21, 0, 1.52), 0.10, mat["primary"]) f.beam("Body_" + side + "RiftSeam", (sign * 0.17, -0.080, 0.95), (sign * 0.245, -0.080, 1.18), 0.019, mat["accent"], vertices=4) f.beam("Body_" + side + "RiftUpperSeam", (sign * 0.245, -0.080, 1.18), (sign * 0.16, -0.080, 1.46), 0.019, mat["accent"], vertices=4) f.beam("Body_Collar", (-0.21, 0, 1.52), (0.21, 0, 1.52), 0.095, mat["primary"]) f.ellipsoid("Head_HollowShell", (0, 0, 1.73), (0.20, 0.155, 0.23), mat["primary"]) f.box("Head_RiftMouth", (0, -0.153, 1.69), (0.17, 0.025, 0.043), mat["accent"]) for index, (x, z) in enumerate(((-0.19, 1.90), (-0.10, 2.02), (0.10, 2.02), (0.19, 1.90))): f.beam("Head_BrokenCrown_%d" % index, (x, 0, z - 0.13), (x * 1.18, 0, z + 0.06), 0.038, mat["light"], vertices=5) f.beam("ArmL_Upper", (-0.30, 0, 1.47), (-0.48, 0.02, 1.13), 0.068, mat["primary"]) f.beam("ArmL_Fore", (-0.48, 0.02, 1.13), (-0.46, -0.18, 0.77), 0.056, mat["primary"]) for index in range(3): f.beam("ArmL_Claw_%d" % index, (-0.50 + index * 0.043, -0.18, 0.79), (-0.51 + index * 0.049, -0.25, 0.64), 0.020, mat["light"], vertices=4) f.beam("ArmR_Upper", (0.30, 0, 1.47), (0.49, 0, 1.10), 0.068, mat["primary"]) f.beam("ArmR_Fore", (0.49, 0, 1.10), (0.37, -0.33, 1.23), 0.056, mat["primary"]) f.ellipsoid("ArmR_CradlingHand", (0.37, -0.33, 1.23), (0.12, 0.09, 0.052), mat["primary"]) f.ellipsoid("Weapon_DrainOrb", (0.37, -0.34, 1.40), (0.13, 0.13, 0.13), mat["accent"]) f.ring("Weapon_DrainOrbit", (0.37, -0.34, 1.40), 0.175, 0.015, mat["light"], rotation=(0.35, 0.20, 0), segments=10) def build_mercenary(spec, mat): """Dispatch five neutral mercenaries, retaining recognizable role silhouettes.""" builders = {"s01": _scout, "s02": _ram, "s03": _singer, "s04": _breaker, "s05": _rift} identifier = str(spec["id"]) if identifier not in builders: raise ValueError("Unknown Saumwacht mercenary: " + identifier) builders[identifier](spec, mat)