saumwacht-godot/assets-source/luneraria/g2/build_towers.py
Martin Stickelbroeck 9f62e30377 G2: Ancients, Türme, Bossgrotte und Superboss; macOS- und Windows-ARM64-Pakete
Mondherz-Ancients (Saumkern-Kristall als Herz, Sichelklingen, Ringe, Krone,
Teamwappen, Energieadern) mit den Stufen stabil/instabil/kritisch, kurzem
Zusammenbruch und bleibender Ruine; der Sieg wartet nie auf die Grafik.
Turmfamilie aus einem Blender-Generator: Grenzwacht (Tower 1), Bastion
(Tower 2), Kernwacht (Basisturm in der Formensprache des Ancients), mit
Schadensstufen, Einsturz, Ruine und sichtbarem Angriff (Zielring, Aufladen,
Bolzen, Einschlag) - der Schaden bleibt vollständig in der Simulation.
Bossgrotte Mondschlund mit Tor, Treppe in die Tiefe, Altar, Säulen, Felsen
und fünf Zuständen; der Generator hält Abstand zur Top-Lane, die Höhle
braucht eine kleine Geländesenke (nur Darstellung). Superboss Schlundkrone
mit 24 Knochen und 11 Clips (inkl. Brüllen und Sieg), Adern in Teamfarbe,
dasselbe Modell für Grotte und Lane. Gemeinsame LN-Materialfamilie (Stein,
Metall, Kristall, Rune, Stoff, Mondwasser, Kreaturenhaut), Occlusion-Fade
ohne dithernde Schatten. Keine Regeländerung.

Geprüft: verify_g2 245/245, verify_g1 443/443, Regeln 91/91 und 73/73,
Stress 17/17, Balance 40 Partien identisch mit G1, verify_m1 38/38,
verify_m2 14/14, Netz 43/43 und Loopback 12/12, Legacy 209 und 463.
21 Ansichten vorher/nachher (1920x1080), Videos Turmkampf und Bossbegegnung,
Leistung G1/G2 auf demselben Mac (+0,5 ms in vollen Kampfansichten).
Pakete 0.17.0-g2 für macOS (Universal, ad-hoc signiert), Windows x64/ARM64
und Linux; macOS nativ und unter Rosetta, beide Windows-Pakete in Windows 11
ARM (Parallels, OpenGL-Fallback) mit --lune-smoke bestanden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:12:49 +02:00

444 lines
24 KiB
Python

"""G2 tower family of Champions of Luneraria (one generator, three GLBs).
Grenzwacht (Tower 1, outer lane tower): slender octagonal watchtower, open gallery, two
crescent horns cradling a medium crystal, few metal bands.
Bastion (Tower 2, inner lane tower): broad battered bastion with rear buttresses, shield
plates on the lane side, machicolated parapet, three horns, armillary ring and a large
crystal with satellite shards.
Kernwacht (base tower): no round shaft at all - a stout rune drum carrying two crescent
blades that meet in a lancet arch around the crystal, the same blade language as the
Ancient, so the base towers read as part of the Ancient's sanctuary.
All three share the octagonal plan, pale moonstone ashlar on a dark basalt plinth, team
metal (tinted at runtime), rune friezes and the crescent crystal cradle. Local frame: +z
faces the lane (attackers), -z is the outer side (buttresses, door, pennant pole).
Nodes per GLB (pivot at the ground centre unless noted):
Lower plinth and lower shaft (stays standing until the ruin replaces it)
Upper everything above the split height; pivot at (0, split, 0) so it can tip
Upper_Damaged heavy-damage variant of Upper (missing merlons, bent horn, loose plate)
Debris rubble at the foot, shown from heavy damage on
Crystal the energy crystal, pivot at its centre
Ring / Shard_n rotating armillary ring and orbiting shards (Bastion, Kernwacht)
Pennant / Banner_L / Banner_R cloth (team silhouette and emblem come from the shader)
Ruin the destroyed tower (stump, rubble, broken crystal), pivot at the ground
Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/g2/build_towers.py
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import lune_bpy as L
from lune_bpy import V, pi, sin, cos
from mathutils import noise, Matrix
OCT = pi / 8
def octa(name, profile, mat, **kw):
return L.lathe(name, profile, 8, mat, rotation=OCT, **kw)
def rivets(prefix, radius, y, count, mat, size=.07, rotation=OCT):
out = []
for k in range(count):
a = 2 * pi * k / count + rotation
out.append(L.box('%s_%d' % (prefix, k), (cos(a) * radius, y, sin(a) * radius), (size, size, size * .6), mat,
yaw=-a + pi / 2, bev=.012))
return out
def merlons(prefix, radius, y, count, size, mat, skip=(), jitter=0.0, seed=0):
out = []
for k in range(count):
if k in skip:
continue
a = 2 * pi * (k + .5) / count
tilt = jitter * noise.noise(V((k * 1.3, seed, 0)))
out.append(L.box('%s_%d' % (prefix, k), (cos(a) * radius, y + size[1] / 2, sin(a) * radius), size, mat,
yaw=-a + pi / 2, roll=tilt, bev=.03, segments=2))
return out
def horn(name, angle, r0, y0, r_mid, y_top, r_top, width, thick, mat, bent=0.0):
"""Crescent horn in the vertical plane at `angle`: rises from (r0, y0), bows out to r_mid
and curls back in to (r_top, y_top). Flat face shows sideways (tangent direction)."""
d = V((cos(angle), 0, sin(angle)))
t = V((-sin(angle), 0, cos(angle)))
h = y_top - y0
local = [(r0, y0), (r_mid, y0 + h * .38), (r_mid * .86 + r_top * .14, y0 + h * .72), (r_top, y_top)]
if bent:
local[-1] = (r_top + bent, y_top - bent * .6)
points = [d * r + V((0, y, 0)) for r, y in local]
return L.sweep(name, points, [width, width * .9, width * .6, width * .12], [thick, thick * .9, thick * .7, thick * .2], mat,
up_hint=tuple(t), steps=20, lens=.35)
def slits(prefix, radius, ys, angles, mat, size=(.13, .44)):
out = []
for j, y in enumerate(ys):
for k, a in enumerate(angles):
out.append(L.box('%s_%d_%d' % (prefix, j, k), (cos(a) * radius, y, sin(a) * radius), (size[0], size[1], .08), mat,
yaw=-a + pi / 2, bev=.0))
return out
def door(prefix, radius, mat_frame, mat_leaf, angle=-pi / 2, height=1.5, width=.76):
"""Arched door on a flat octagon face; `radius` is the face (apothem) distance."""
d = V((cos(angle), 0, sin(angle)))
t = V((-sin(angle), 0, cos(angle)))
c = d * radius
yaw = -angle + pi / 2
parts = [L.box(prefix + '_Leaf', (c.x, height / 2 + .55, c.z), (width, height, .1), mat_leaf, yaw=yaw, bev=.01)]
arch = []
for k in range(13):
a = pi * k / 12
arch.append(tuple(c + d * .06 + t * cos(a) * width * .56 + V((0, .55 + height - .12 + sin(a) * width * .42, 0))))
parts.append(L.sweep(prefix + '_Arch', arch, [.13] * 3, [.14] * 3, mat_frame, up_hint=tuple(d), steps=14, smooth=False))
for sx in (-1, 1):
post = c + d * .06 + t * sx * width * .56
parts.append(L.box('%s_Jamb_%d' % (prefix, sx), (post.x, .55 + (height - .12) / 2, post.z), (.14, height - .12, .14), mat_frame,
yaw=yaw, bev=.015))
return parts
def jagged_stump(name, radius_bottom, radius_top, y0, y_top, mat, seed, sides=8, bite=.55):
"""Octagonal shaft whose top ring is broken irregularly (ruin stump)."""
verts, faces = [], []
levels = 5
for j in range(levels + 1):
t = j / levels
for k in range(sides):
a = 2 * pi * k / sides + OCT
r = radius_bottom * (1 - t) + radius_top * t
y = y0 + (y_top - y0) * t
if j == levels:
y -= bite * (.5 + .5 * noise.noise(V((k * .9, seed, 1.7)))) + .25 * (1 if k % 3 == 0 else 0)
r *= .9 + .12 * noise.noise(V((k, seed, 3)))
elif j == levels - 1:
y -= bite * .25 * abs(noise.noise(V((k * .9, seed, 2.3))))
verts.append((cos(a) * r, y, sin(a) * r))
for j in range(levels):
for k in range(sides):
a0, b0 = j * sides + k, j * sides + (k + 1) % sides
faces.append((a0, b0, b0 + sides, a0 + sides))
faces.append(tuple(reversed(range(sides))))
faces.append(tuple(range(levels * sides, levels * sides + sides)))
ob = L.make(name, verts, faces, mat)
L._outward(ob)
return ob
def rubble(prefix, center, spread, count, size, mat, seed, y=0.0, outward_bias=None):
"""Scattered broken masonry chunks (ruin and damage debris), biased to one side."""
out = []
for k in range(count):
a = 2 * pi * ((k * .618 + seed * .13) % 1.0)
if outward_bias is not None:
a = outward_bias + (a - pi) * .55
r = spread * (.35 + .65 * ((k * .377 + seed * .21) % 1.0))
s = size * (.55 + .6 * ((k * .733 + seed) % 1.0))
cx = center[0] + cos(a) * r
cz = center[2] + sin(a) * r
out.append(L.chunk('%s_%d' % (prefix, k), (cx, y + s * .22, cz), (s * .75, s * .42, s * .58), mat, seed=seed * 3.1 + k,
yaw=a * 1.7 + seed, tilt=.3 * noise.noise(V((k, seed, 0)))))
return out
def shards(prefix, center, count, radius, mat, seed, spread=.9):
out = []
for k in range(count):
a = 2 * pi * ((k * .618 + seed * .07) % 1.0)
r = spread * (.3 + .7 * ((k * .41 + seed) % 1.0))
c = (center[0] + cos(a) * r, center[1] + radius * .3, center[2] + sin(a) * r)
out.append(L.crystal('%s_%d' % (prefix, k), c, radius * (.5 + .5 * ((k * .29) % 1.0)), radius * 1.6, radius * .5, mat,
sides=5, tilt=(1.1 + .4 * noise.noise(V((k, seed, 0))), a), seed=k + seed))
return out
# ================================================================ Grenzwacht (Tower 1)
def grenzwacht():
L.reset()
split = 2.2
lower = [
octa('Plinth', [(1.48, 0), (1.48, .26), (1.36, .32), (1.36, .46), (1.22, .52)], 'LN_Stone_Dark', jitter=.015, seed=1),
octa('ShaftLow', [(1.02, .5), (.99, 1.2), (.955, split + .02)], 'LN_Stone_Pale'),
octa('Course', [(1.07, .5), (1.07, .66), (1.02, .7)], 'LN_Stone_Dark', cap_bottom=False),
]
lower += slits('SlitLow', .905, [1.55], [0.0, pi], 'LN_Stone_Dark')
lower += door('Door', .9, 'LN_Stone_Dark', 'LN_Wood')
upper = [
octa('ShaftHigh', [(.955, split - .02), (.9, 2.9), (.85, 3.7), (.87, 3.74)], 'LN_Stone_Pale'),
octa('Band', [(.975, 2.34), (.975, 2.5)], 'LN_Metal_Team', cap_top=False, cap_bottom=False),
octa('Runes', [(.905, 3.02), (.89, 3.3)], 'LN_Rune_Team', cap_top=False, cap_bottom=False, uv_band=.34),
octa('Corbel', [(.86, 3.7), (1.18, 3.96), (1.23, 4.2), (1.15, 4.26)], 'LN_Stone_Pale'),
octa('Floor', [(1.0, 4.18), (1.0, 4.24)], 'LN_Stone_Dark'),
]
upper += rivets('Rivet', .99, 2.42, 8, 'LN_Metal_Iron')
upper += slits('SlitHigh', .84, [2.75], [0, pi / 2, pi, 3 * pi / 2], 'LN_Stone_Dark')
upper += merlons('Merlon', 1.07, 4.22, 8, (.36, .34, .24), 'LN_Stone_Pale')
horns = [horn('Horn_%d' % i, a, .6, 4.2, .82, 6.05, .24, .24, .15, 'LN_Metal_Team') for i, a in enumerate((0.0, pi))]
collar = L.ring('Collar', (0, 4.62, 0), .38, .07, .08, 'LN_Metal_Team', sides=24)
pole = L.box('PennantPole', (0, 5.0, -1.02), (.07, 1.7, .07), 'LN_Wood', bev=.01)
upper += horns + [collar, pole]
damaged = [
octa('D_ShaftHigh', [(.955, split - .02), (.9, 2.9), (.85, 3.7), (.87, 3.74)], 'LN_Stone_Pale'),
octa('D_Band', [(.975, 2.34), (.975, 2.5)], 'LN_Metal_Team', cap_top=False, cap_bottom=False),
octa('D_Runes', [(.905, 3.02), (.89, 3.3)], 'LN_Rune_Team', cap_top=False, cap_bottom=False, uv_band=.34),
octa('D_Corbel', [(.86, 3.7), (1.18, 3.96), (1.23, 4.2), (1.15, 4.26)], 'LN_Stone_Pale', jitter=.04, seed=5),
octa('D_Floor', [(1.0, 4.18), (1.0, 4.24)], 'LN_Stone_Dark'),
L.box('D_PennantPole', (0.12, 4.9, -1.0), (.07, 1.5, .07), 'LN_Wood', roll=.35, bev=.01),
horn('D_Horn_0', 0.0, .6, 4.2, .82, 6.05, .24, .24, .15, 'LN_Metal_Team'),
horn('D_Horn_1', pi, .6, 4.2, .86, 5.55, .62, .24, .15, 'LN_Metal_Team', bent=.35),
L.ring('D_Collar', (0, 4.6, 0), .38, .07, .08, 'LN_Metal_Team', sides=24, axis_tilt=(.18, .1)),
]
damaged += rivets('D_Rivet', .99, 2.42, 8, 'LN_Metal_Iron')
damaged += merlons('D_Merlon', 1.07, 4.22, 8, (.36, .34, .24), 'LN_Stone_Pale', skip=(1, 2, 5), jitter=.25, seed=3)
debris = rubble('Debris', (0, 0, 0), 1.9, 7, .34, 'LN_Stone_Pale', 3.0, outward_bias=-pi / 2)
crystal = L.crystal('Crystal', (0, 5.2, 0), .34, .64, .5, 'LN_Crystal_Team', sides=6, seed=2)
pennant = L.banner('Pennant', .62, 1.0, 'LN_Cloth_Team', top=(-.37, 5.72, -1.02), segments=(4, 8))
ruin = [
octa('R_Plinth', [(1.48, 0), (1.48, .26), (1.36, .32), (1.36, .46), (1.22, .52)], 'LN_Stone_Dark', jitter=.03, seed=1),
jagged_stump('R_Stump', 1.0, .96, .5, 1.95, 'LN_Stone_Pale', 4.0),
octa('R_Course', [(1.07, .5), (1.07, .66), (1.02, .7)], 'LN_Stone_Dark', cap_bottom=False),
horn('R_Horn', 2.4, .3, .05, .5, .5, 1.6, .24, .15, 'LN_Metal_Team'),
]
ruin += rubble('R_Rubble', (0, 0, 0), 2.4, 10, .62, 'LN_Stone_Pale', 7.0, outward_bias=-pi / 2)
ruin += rubble('R_Dark', (0, 0, 0), 2.2, 6, .5, 'LN_Stone_Dark', 17.0, outward_bias=-pi / 2)
ruin += rubble('R_Merlon', (0, 0, 0), 2.0, 4, .42, 'LN_Stone_Pale', 11.0, outward_bias=pi * .1)
ruin += shards('R_Shard', (0, .35, -.9), 5, .2, 'LN_Crystal_Team', 5.0)
return finish('grenzwacht', 'Grenzwacht', split, lower, upper, damaged, debris, [crystal], [pennant], ruin, 5.2, {})
# ================================================================ Bastion (Tower 2)
def buttress(name, angle, r_in, r_out, h_in, h_out, width, mat):
d = V((cos(angle), 0, sin(angle)))
t = V((-sin(angle), 0, cos(angle)))
verts = []
for r, h in ((r_in, h_in), (r_out, h_out)):
for s in (-1, 1):
base = d * r + t * s * width / 2
verts.append((base.x, 0.0, base.z))
verts.append((base.x, h, base.z))
# verts: 0 in-left-bottom, 1 in-left-top, 2 in-right-bottom, 3 in-right-top, 4..7 outer.
faces = [(0, 2, 6, 4), (1, 5, 7, 3), (0, 4, 5, 1), (2, 3, 7, 6), (0, 1, 3, 2), (4, 6, 7, 5)]
ob = L.make(name, verts, faces, mat)
L._outward(ob)
L.bevel(ob, .04, 1, 30.0)
return ob
def shield(name, angle, radius, y, mat, tilt=0.0, drop=0.0):
d = V((cos(angle), 0, sin(angle)))
outline = [(-.34, .42), (.34, .42), (.3, -.18), (0, -.52), (-.3, -.18)]
verts, faces = [], []
t = V((-sin(angle), 0, cos(angle)))
for depth in (0.0, .07):
for x, yy in outline:
p = d * (radius + depth) + t * x + V((0, y + yy - drop, 0))
verts.append(tuple(p))
n = len(outline)
faces = [(i, (i + 1) % n, n + (i + 1) % n, n + i) for i in range(n)] + [tuple(reversed(range(n))), tuple(range(n, 2 * n))]
ob = L.make(name, verts, faces, mat)
L._outward(ob)
L.bevel(ob, .025, 2, 30.0)
if tilt:
ob.rotation_euler = (0, tilt, 0)
return ob
def bastion():
L.reset()
split = 2.6
back = -pi / 2
lower = [
octa('Plinth', [(1.78, 0), (1.78, .34), (1.66, .4), (1.66, .62), (1.52, .7)], 'LN_Stone_Dark', jitter=.015, seed=2),
octa('Talus', [(1.5, .68), (1.43, 1.25), (1.31, 1.42)], 'LN_Stone_Dark', cap_bottom=False),
octa('ShaftLow', [(1.31, 1.4), (1.28, 2.0), (1.26, split + .02)], 'LN_Stone_Pale'),
octa('BandLow', [(1.35, 1.52), (1.35, 1.72)], 'LN_Metal_Team', cap_top=False, cap_bottom=False),
]
lower += rivets('RivetLow', 1.36, 1.62, 8, 'LN_Metal_Iron')
for i, a in enumerate((back - pi / 4, back, back + pi / 4)):
lower.append(buttress('Buttress_%d' % i, a, 1.1, 2.25, 2.7, .75, .5, 'LN_Stone_Dark'))
def upper_parts(prefix, broken=False):
parts = [
octa(prefix + 'ShaftHigh', [(1.26, split - .02), (1.22, 3.5), (1.18, 4.42)], 'LN_Stone_Pale'),
octa(prefix + 'BandHigh', [(1.29, 3.28), (1.29, 3.46)], 'LN_Metal_Team', cap_top=False, cap_bottom=False),
octa(prefix + 'Runes', [(1.23, 3.84), (1.21, 4.12)], 'LN_Rune_Team', cap_top=False, cap_bottom=False, uv_band=.36),
octa(prefix + 'Parapet', [(1.18, 4.4), (1.56, 4.62), (1.6, 5.0), (1.5, 5.06)], 'LN_Stone_Pale',
jitter=.05 if broken else 0.0, seed=8),
octa(prefix + 'Deck', [(1.36, 4.96), (1.36, 5.03)], 'LN_Stone_Dark'),
L.lathe(prefix + 'Plate', [(.62, 5.0), (.58, 5.18), (.44, 5.24)], 16, 'LN_Metal_Iron'),
]
parts += rivets(prefix + 'RivetHigh', 1.3, 3.37, 8, 'LN_Metal_Iron')
for k in range(8):
a = 2 * pi * (k + .5) / 8
parts.append(L.box('%sCorbel_%d' % (prefix, k), (cos(a) * 1.36, 4.48, sin(a) * 1.36), (.3, .28, .36), 'LN_Stone_Dark',
yaw=-a + pi / 2, bev=.03))
parts += merlons(prefix + 'Merlon', 1.42, 5.0, 8, (.5, .5, .3), 'LN_Stone_Pale',
skip=(0, 3, 4) if broken else (), jitter=.2 if broken else 0.0, seed=4)
lane = pi / 2
for i, a in enumerate((lane - pi / 4, lane, lane + pi / 4)):
loose = broken and i == 2
parts.append(shield('%sShield_%d' % (prefix, i), a, 1.17, 2.95, 'LN_Metal_Team', drop=.0 if not loose else .12))
for i, a in enumerate((pi / 6, pi / 6 + 2 * pi / 3, pi / 6 + 4 * pi / 3)):
if broken and i == 1:
parts.append(horn(prefix + 'Horn_%d' % i, a, .52, 5.0, .78, 6.2, .5, .26, .17, 'LN_Metal_Team', bent=.42))
else:
parts.append(horn(prefix + 'Horn_%d' % i, a, .52, 5.0, .82, 7.15, .2, .26, .17, 'LN_Metal_Team'))
parts.append(L.box(prefix + 'PennantPole', (.0, 5.9, -1.46), (.08, 1.9, .08), 'LN_Wood', roll=.3 if broken else 0.0, bev=.01))
return parts
upper = upper_parts('')
damaged = upper_parts('D_', True)
debris = rubble('Debris', (0, 0, 0), 2.3, 9, .42, 'LN_Stone_Pale', 5.0, outward_bias=-pi / 2)
debris += [shield('Debris_Shield', 1.2, 1.9, .12, 'LN_Metal_Team', drop=0.0)]
crystal = L.crystal('Crystal', (0, 6.2, 0), .48, .86, .66, 'LN_Crystal_Team', sides=6, seed=6)
ring = L.ring('Ring', (0, 6.2, 0), .82, .09, .1, 'LN_Metal_Team', sides=40, uv_cell=.3)
shard_objs = []
for i in range(3):
a = 2 * pi * i / 3
shard_objs.append(L.crystal('Shard_%d' % i, (cos(a) * 1.02, 6.35, sin(a) * 1.02), .11, .26, .18, 'LN_Crystal_Team', sides=5, seed=10 + i))
pennant = L.banner('Pennant', .7, 1.15, 'LN_Cloth_Team', top=(-.41, 6.8, -1.46), segments=(4, 8))
ruin = [
octa('R_Plinth', [(1.78, 0), (1.78, .34), (1.66, .4), (1.66, .62), (1.52, .7)], 'LN_Stone_Dark', jitter=.03, seed=2),
octa('R_Talus', [(1.5, .68), (1.43, 1.25), (1.31, 1.42)], 'LN_Stone_Dark', cap_bottom=False, jitter=.03),
jagged_stump('R_Stump', 1.3, 1.24, 1.4, 2.9, 'LN_Stone_Pale', 6.0, bite=.7),
buttress('R_Buttress_0', back - pi / 4, 1.1, 2.25, 2.1, .75, .5, 'LN_Stone_Dark'),
buttress('R_Buttress_1', back, 1.1, 2.25, 1.5, .6, .5, 'LN_Stone_Dark'),
shield('R_Shield', 2.1, 2.0, .14, 'LN_Metal_Team'),
horn('R_Horn', .5, .5, .05, .6, .45, 1.9, .26, .17, 'LN_Metal_Team'),
]
ruin += rubble('R_Rubble', (0, 0, 0), 2.7, 12, .7, 'LN_Stone_Pale', 9.0, outward_bias=-pi / 2)
ruin += rubble('R_Dark', (0, 0, 0), 2.4, 7, .6, 'LN_Stone_Dark', 19.0, outward_bias=-pi / 2)
ruin += rubble('R_Merlon', (0, 0, 0), 2.3, 5, .5, 'LN_Stone_Pale', 13.0, outward_bias=pi * .15)
ruin += shards('R_Shard', (0, .4, -1.0), 7, .24, 'LN_Crystal_Team', 8.0)
return finish('bastion', 'Bastion', split, lower, upper, damaged, debris, [crystal, ring] + shard_objs, [pennant], ruin, 6.2, {})
# ================================================================ Kernwacht (base tower)
BLADES = (pi / 2, pi / 2 + 2 * pi / 3, pi / 2 + 4 * pi / 3)
def blade(name, angle, mat, y0=2.62, broken=False, lower_only=False):
"""One of three crescent blades: from the drum up and out, curling in to the shared apex.
Lies in the vertical plane at `angle`; the broad face shows tangentially."""
d = V((cos(angle), 0, sin(angle)))
t = V((-sin(angle), 0, cos(angle)))
local = [(.72, y0), (1.3, 4.1), (1.42, 5.3), (1.04, 6.7), (.2, 7.66)]
if lower_only:
local = local[:3]
if broken:
local = [(.72, y0), (1.3, 4.1), (1.42, 5.3), (1.3, 5.9)]
widths = [.46, .44, .38, .28, .16][:len(local)]
depths = [.66, .6, .52, .42, .3][:len(local)]
return L.sweep(name, [d * r + V((0, y, 0)) for r, y in local], widths, depths, mat, up_hint=tuple(t), steps=26, lens=.25)
def edge_strip(name, angle, mat, broken=False):
d = V((cos(angle), 0, sin(angle)))
t = V((-sin(angle), 0, cos(angle)))
local = [(.96, 2.72), (1.56, 4.1), (1.66, 5.3), (1.24, 6.75), (.28, 7.8)]
if broken:
local = local[:3] + [(1.52, 5.92)]
return L.sweep(name, [d * r + V((0, y, 0)) for r, y in local], [.1] * len(local), [.46, .42, .36, .28, .2][:len(local)], mat,
up_hint=tuple(t), steps=26, lens=.1)
def kernwacht():
L.reset()
split = 2.7
lower = [
octa('Plinth', [(1.95, 0), (1.95, .3), (1.8, .36), (1.8, .62), (1.62, .7)], 'LN_Stone_Dark', jitter=.012, seed=3),
octa('Drum', [(1.28, .68), (1.24, 1.4), (1.12, 2.4), (1.06, split + .02)], 'LN_Stone_Pale'),
octa('DrumCap', [(1.14, 2.55), (1.2, 2.66), (1.1, split + .06)], 'LN_Stone_Dark'),
octa('Runes', [(1.272, 1.02), (1.192, 1.9)], 'LN_Rune_Team', cap_top=False, cap_bottom=False, uv_band=.46),
]
for k in range(8):
a = 2 * pi * k / 8
lower.append(L.box('Pilaster_%d' % k, (cos(a) * 1.19, 1.45, sin(a) * 1.19), (.2, 1.5, .14), 'LN_Stone_Dark', yaw=-a + pi / 2, bev=.02))
def upper_parts(prefix, broken=False):
parts = [
L.ring(prefix + 'Crown', (0, 7.5, 0), .42, .12, .2, 'LN_Metal_Team', sides=32, uv_cell=.25),
L.crystal(prefix + 'Apex', (0, 7.95, 0), .16, .38, .14, 'LN_Crystal_Team', sides=5, seed=4),
L.lathe(prefix + 'Seat', [(.6, split - .02), (.72, split + .12), (.5, split + .2)], 16, 'LN_Metal_Team'),
]
for i, a in enumerate(BLADES):
snapped = broken and i == 2
parts.append(blade('%sBlade_%d' % (prefix, i), a, 'LN_Stone_Pale', broken=snapped))
parts.append(edge_strip('%sEdge_%d' % (prefix, i), a, 'LN_Metal_Team', broken=snapped))
for i in (1, 2):
a = BLADES[i]
d = V((cos(a), 0, sin(a)))
t = V((-sin(a), 0, cos(a)))
at = d * 1.72 + V((0, 5.55, 0))
parts.append(L.box('%sBannerArm_%d' % (prefix, i), tuple(at), (.08, .08, .5), 'LN_Metal_Iron', yaw=-a, bev=.01))
return parts
upper = upper_parts('')
damaged = upper_parts('D_', True)
debris = rubble('Debris', (0, 0, 0), 2.4, 8, .4, 'LN_Stone_Pale', 2.0, outward_bias=-pi / 2)
crystal = L.crystal('Crystal', (0, 5.25, 0), .56, 1.0, .74, 'LN_Crystal_Team', sides=6, seed=9)
ring = L.ring('Ring', (0, 5.25, 0), .98, .1, .11, 'LN_Metal_Team', sides=40, uv_cell=.3)
banners = []
for i, key in ((1, 'Banner_L'), (2, 'Banner_R')):
a = BLADES[i]
d = V((cos(a), 0, sin(a)))
top = d * 1.98 + V((0, 5.5, 0))
banners.append(L.banner(key, .62, 1.6, 'LN_Cloth_Team', top=tuple(top), segments=(4, 10), yaw=-(a + pi / 2)))
ruin = [
octa('R_Plinth', [(1.95, 0), (1.95, .3), (1.8, .36), (1.8, .62), (1.62, .7)], 'LN_Stone_Dark', jitter=.03, seed=3),
jagged_stump('R_Drum', 1.28, 1.14, .68, 2.2, 'LN_Stone_Pale', 3.0, bite=.5),
octa('R_Runes', [(1.272, 1.02), (1.225, 1.6)], 'LN_Rune_Team', cap_top=False, cap_bottom=False, uv_band=.46),
]
fallen = blade('R_Blade', pi / 2, 'LN_Stone_Pale')
fallen.data.transform(Matrix.Rotation(pi / 2 - .12, 4, 'Y'))
fallen.location = L.G(-1.6, -2.3, -1.2)
ruin.append(fallen)
ruin += rubble('R_Rubble', (0, 0, 0), 2.8, 12, .66, 'LN_Stone_Pale', 5.0, outward_bias=-pi / 2)
ruin += rubble('R_Dark', (0, 0, 0), 2.4, 7, .58, 'LN_Stone_Dark', 15.0, outward_bias=-pi / 2)
ruin += shards('R_Shard', (0, .4, -1.0), 7, .26, 'LN_Crystal_Team', 3.0)
return finish('kernwacht', 'Kernwacht', split, lower, upper, damaged, debris, [crystal, ring], banners, ruin, 5.25, {})
# ================================================================ assembly
def finish(key, title, split, lower, upper, damaged, debris, energy, cloth, ruin, crystal_y, extra):
for ob in lower + upper + damaged + debris + ruin:
ob.data.update()
lower_ob = L.join(lower, 'Lower')
upper_ob = L.join(upper, 'Upper', origin=(0, split, 0))
damaged_ob = L.join(damaged, 'Upper_Damaged', origin=(0, split, 0))
debris_ob = L.join(debris, 'Debris')
ruin_ob = L.join(ruin, 'Ruin')
named = []
for ob in energy:
name = ob.name
c = sum((ob.matrix_world @ v.co for v in ob.data.vertices), V()) / len(ob.data.vertices)
g = L.to_godot(c)
if name.startswith('Ring'):
g = (0.0, crystal_y, 0.0)
named.append(L.join([ob], name, origin=g))
cloth_obs = [L.join([ob], ob.name) for ob in cloth]
intact = [lower_ob, upper_ob] + named
L.finalize(intact, occluders=intact)
L.finalize([damaged_ob], occluders=[lower_ob, damaged_ob] + named)
L.finalize([debris_ob], occluders=[lower_ob, debris_ob])
L.finalize([ruin_ob], occluders=[ruin_ob])
L.finalize(cloth_obs, ao=False)
objects = intact + [damaged_ob, debris_ob, ruin_ob] + cloth_obs
for ob in objects:
L.triangulate(ob)
doc, raw = L.export(objects, key + '.glb', key + '.blend')
entry = L.record(key, key + '.glb', key + '.blend', 'build_towers.py', [lower_ob, upper_ob] + named, doc, raw,
title=title, split_height=split, crystal_height=crystal_y,
triangles_intact=L.triangles(intact + cloth_obs), triangles_ruin=L.triangles([ruin_ob]), **extra)
print('TOWER_BUILD', key, entry['triangles'], 'triangles (intact %d, ruin %d)' % (entry['triangles_intact'], entry['triangles_ruin']),
'height %.2f' % entry['bounds_godot']['max'][1], flush=True)
return entry
only = sys.argv[sys.argv.index('--') + 1:] if '--' in sys.argv else []
entries = []
for key, fn in (('grenzwacht', grenzwacht), ('bastion', bastion), ('kernwacht', kernwacht)):
if only and key not in only:
continue
entries.append(fn())
L.write_manifest(entries)