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>
444 lines
24 KiB
Python
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)
|