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>
386 lines
20 KiB
Python
386 lines
20 KiB
Python
"""G2 boss grotto of Champions of Luneraria: the "Mondschlund", an old Lunarian sanctuary cut
|
|
into the highland escarpment. Replaces the G1 blockout on the same spot (LuneMap.GROTTO,
|
|
terrace radius 7.8 m, 1.7 m high); the boss point and both entrances keep their M2 offsets.
|
|
|
|
Local frame: origin at the arena centre on the terrace (Godot y = 0 is the terrace top),
|
|
+x/+z are the map axes, the map diagonal x = z is the mirror line. u runs along the axis
|
|
towards Mid, a sideways towards the Blau side; the gate sits at the back (-u), where the
|
|
escarpment rises towards the Top lane.
|
|
|
|
Parts:
|
|
Floor arena paving and dark rim, plus the two moon-water troughs before the gate
|
|
Runes the great rune circle, the glyph ring round the dais, gate and altar glyphs
|
|
Platform low two-step boss dais at the boss point (figures are lifted onto it)
|
|
Altar crescent altar behind the dais that cradles the Mondquelle
|
|
Source the Mondquelle crystal cluster (the grotto's energy source, state driven)
|
|
Gate the monumental entrance: two horned guardian pylons, deep arch with a bronze
|
|
crescent keystone, threshold, stairs that lead down into the dark
|
|
Cave the dark hollow behind the gate (walls, stair treads, well rim)
|
|
CaveGlow light from inside: the glowing mist at the back and the moon well below,
|
|
plus crystals on the cave walls (state driven)
|
|
Channel the moon water in the troughs (state driven)
|
|
Pillars old columns along the sides of the arena: intact, broken, fallen
|
|
PillarCrystals small crystals on the intact capitals (state driven)
|
|
Rocks faceted cliff masses that frame the gate and merge into the escarpment
|
|
|
|
Everything is symmetric across the local diagonal, so neither team is favoured. Rocks are
|
|
placed on the real terrain (grotto_site.json, written by tools/luneraria/dump_grotto_site.gd)
|
|
and every rock and the gate are checked against the lane corridors (at least CLEAR metres).
|
|
The lower lying cave needs a small notch in the escarpment behind the gate (LuneTerrain,
|
|
presentation only, hidden by the rock shell).
|
|
|
|
Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/g2/build_bossgrotte.py
|
|
"""
|
|
import sys, json
|
|
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, sqrt
|
|
from math import atan2
|
|
from mathutils import Matrix
|
|
|
|
AXIS = V((.70710678, 0, .70710678))
|
|
BACK = -AXIS
|
|
SIDE = V((-.70710678, 0, .70710678))
|
|
BOSS = V((-1.5, 0, -1.5))
|
|
ARENA = 7.8
|
|
GATE_U = -8.95
|
|
PYLON_A = 2.6
|
|
CLEAR = .6
|
|
SITE = json.loads((L.SRC / 'grotto_site.json').read_text())
|
|
_problems = []
|
|
|
|
|
|
def at(u, a, y=0.0):
|
|
"""Point in the axis frame: u along the axis (+ towards Mid), a to the Blau side."""
|
|
return AXIS * u + SIDE * a + V((0, y, 0))
|
|
|
|
|
|
def yaw_to(direction):
|
|
"""Yaw (about Godot y) that turns local +z towards `direction`."""
|
|
return atan2(direction.x, direction.z)
|
|
|
|
|
|
def ground(p):
|
|
"""Terrain height (relative to the terrace) under a local point, bilinear."""
|
|
grid = SITE['height_grid']
|
|
ox, oz = grid['origin']
|
|
step, n, values = grid['step'], grid['count'], grid['values']
|
|
fx = max(0.0, min(n - 1.001, (p.x - ox) / step))
|
|
fz = max(0.0, min(n - 1.001, (p.z - oz) / step))
|
|
i, j = int(fx), int(fz)
|
|
tx, tz = fx - i, fz - j
|
|
a = values[j][i] * (1 - tx) + values[j][i + 1] * tx
|
|
b = values[j + 1][i] * (1 - tx) + values[j + 1][i + 1] * tx
|
|
return a * (1 - tz) + b * tz
|
|
|
|
|
|
def clearance(p):
|
|
best = 1e9
|
|
for lane in SITE['lanes']:
|
|
for x, z in lane['points']:
|
|
best = min(best, sqrt((p.x - x) ** 2 + (p.z - z) ** 2) - lane['corridor'])
|
|
return best
|
|
|
|
|
|
def check_clear(name, center, rx, rz, yaw=0.0):
|
|
"""Samples the footprint ellipse; records a problem if it reaches into a lane corridor."""
|
|
worst = 1e9
|
|
for k in range(16):
|
|
t = 2 * pi * k / 16
|
|
local = V((cos(t) * rx, 0, sin(t) * rz))
|
|
worst = min(worst, clearance(V(center) + Matrix.Rotation(yaw, 3, 'Y') @ local))
|
|
if worst < CLEAR:
|
|
_problems.append('%s reaches %.2f m into/near a lane corridor' % (name, CLEAR - worst))
|
|
return worst
|
|
|
|
|
|
# ---------------------------------------------------------------- floor and runes
|
|
def floor_parts():
|
|
parts = [L.lathe('Floor_Rim', [(ARENA - .02, -.4), (ARENA - .02, .07), (ARENA - .22, .1)], 72, 'LN_Stone_Dark', jitter=.008, seed=2),
|
|
L.disc_grid('Floor_Top', (0, 0, 0), ARENA - .2, 'LN_Stone_Grotto', rings=8, sides=72, y=.088)]
|
|
runes = [L.lathe('Rune_Circle', [(4.72, .104), (4.36, .104)], 72, 'LN_Rune_Grotto', cap_top=False, cap_bottom=False, uv_band=.46),
|
|
L.lathe('Rune_Inner', [(2.62, .106), (2.46, .106)], 48, 'LN_Rune_Grotto', cap_top=False, cap_bottom=False,
|
|
center=tuple(BOSS), uv_band=.36)]
|
|
return parts, runes
|
|
|
|
|
|
def platform_parts():
|
|
c = tuple(BOSS)
|
|
return [L.lathe('Plat_0', [(2.3, -.02), (2.3, .06), (2.24, .065)], 48, 'LN_Stone_Dark', center=c),
|
|
L.lathe('Plat_1', [(2.2, .06), (1.7, .065), (1.7, .12), (1.64, .125)], 48, 'LN_Stone_Grotto', center=c),
|
|
L.disc_grid('Plat_Top', c, 1.62, 'LN_Stone_Grotto', rings=2, sides=48, y=.124)]
|
|
|
|
|
|
def altar_parts():
|
|
"""A crescent slab behind the boss, facing the arena, with a cradle for the Mondquelle."""
|
|
base = BOSS + BACK * 2.3
|
|
parts = [L.lathe('Altar_Foot', [(1.1, -.05), (1.1, .35), (.95, .45), (.9, .6)], 8, 'LN_Stone_Dark', center=tuple(base), rotation=pi / 8)]
|
|
pts = []
|
|
for k in range(9):
|
|
ang = pi * (.12 + .76 * k / 8.0)
|
|
pts.append(base + SIDE * cos(ang) * 1.5 + V((0, .6 + sin(ang) * 2.5, 0)))
|
|
parts.append(L.sweep('Altar_Crescent', pts, [.55, .62, .66, .68, .7, .68, .66, .62, .55], [.5, .52, .55, .56, .56, .56, .55, .52, .5],
|
|
'LN_Stone_Grotto', up_hint=tuple(AXIS), steps=30, lens=.2))
|
|
inner = []
|
|
for k in range(9):
|
|
ang = pi * (.18 + .64 * k / 8.0)
|
|
inner.append(base + SIDE * cos(ang) * 1.14 + V((0, .6 + sin(ang) * 2.1, 0)) + AXIS * .26)
|
|
runes = [L.sweep('Altar_Glyphs', inner, [.14] * 9, [.06] * 9, 'LN_Rune_Grotto', up_hint=tuple(AXIS), steps=30, uv_cells=.34)]
|
|
source = []
|
|
for k in range(7):
|
|
a = 2 * pi * k / 7
|
|
r = .08 + .3 * (k % 3) / 2.0
|
|
height = 1.3 - .25 * (k % 3)
|
|
c = base + SIDE * cos(a) * r + BACK * sin(a) * r * .5 + V((0, .7, 0))
|
|
source.append(L.crystal('Source_%d' % k, tuple(c), .16 + .06 * (k % 2), height, .2, 'LN_Crystal_Grotto', sides=6,
|
|
tilt=(.25 * sin(a), .25 * cos(a)), seed=k))
|
|
source.append(L.crystal('Source_Main', tuple(base + V((0, .8, 0))), .32, 1.9, .3, 'LN_Crystal_Grotto', sides=6, seed=11))
|
|
return parts, runes, source
|
|
|
|
|
|
# ---------------------------------------------------------------- gate
|
|
def pylon(name, center, facing, height=6.1, width=1.34):
|
|
"""Guardian pylon: dark foot, four tapering drums, capital and two crescent horns that
|
|
curl towards the arch."""
|
|
c = V(center)
|
|
yaw = yaw_to(facing)
|
|
t = V((facing.z, 0, -facing.x))
|
|
parts = [L.box(name + '_Foot', tuple(c + V((0, .1, 0))), (width * 1.36, 1.2, width * 1.36), 'LN_Stone_Dark', yaw=yaw, bev=.05, segments=2)]
|
|
y = .7
|
|
for k in range(4):
|
|
h = (height - 1.7) / 4
|
|
w = width * (1 - .055 * k)
|
|
parts.append(L.box('%s_Drum_%d' % (name, k), tuple(c + V((0, y + h / 2, 0))), (w, h - .05, w), 'LN_Stone_Grotto', yaw=yaw, bev=.06, segments=2))
|
|
y += h
|
|
parts.append(L.box(name + '_Cap', tuple(c + V((0, y + .25, 0))), (width * 1.28, .5, width * 1.28), 'LN_Stone_Dark', yaw=yaw, bev=.05, segments=2))
|
|
top = y + .5
|
|
# t runs towards -SIDE: +1 turns the horn tips towards the gate's middle for the Blau-side pylon.
|
|
inward = 1 if (c - AXIS * c.dot(AXIS)).dot(SIDE) > 0 else -1
|
|
for s in (-1, 1):
|
|
base = c + t * s * .38 + V((0, top - .1, 0))
|
|
curl = inward * .35
|
|
pts = [base, base + t * s * .45 + V((0, .7, 0)), base + t * (s * .2 + curl) + V((0, 1.35, 0)), base + t * (curl * 1.6) + V((0, 1.6, 0))]
|
|
parts.append(L.sweep('%s_Horn_%d' % (name, s), pts, [.4, .32, .2, .04], [.34, .28, .18, .04], 'LN_Stone_Grotto',
|
|
up_hint=tuple(facing), steps=14, lens=.3))
|
|
runes = [L.panel(name + '_Glyphs', tuple(c + facing * (width * .5) + V((0, 2.9, 0))), width * .52, 2.9, yaw, 'LN_Rune_Grotto',
|
|
depth=.02, uv_cells=(1, 5))]
|
|
return parts, runes, top
|
|
|
|
|
|
def gate_parts():
|
|
"""Monumental entrance at the back of the arena, facing it (towards +axis)."""
|
|
face = AXIS
|
|
parts, runes = [], []
|
|
tops = []
|
|
for s in (-1, 1):
|
|
p, r, top = pylon('Pylon_%d' % s, tuple(at(GATE_U, s * PYLON_A)), face)
|
|
parts += p
|
|
runes += r
|
|
tops.append(top)
|
|
check_clear('Pylon_%d' % s, at(GATE_U, s * PYLON_A), 1.0, 1.0)
|
|
# Deep arch between the pylons (three rings in depth) with a crescent keystone.
|
|
spring = 4.2
|
|
radius = PYLON_A - .55
|
|
for d, depth in ((.35, .5), (-.25, .5), (-.85, .5)):
|
|
arch = [at(GATE_U + d, cos(pi * k / 12) * radius, spring + sin(pi * k / 12) * radius * .78) for k in range(13)]
|
|
parts.append(L.sweep('Gate_Arch_%d' % int(d * 100), arch, [.72] * 5, [depth] * 5, 'LN_Stone_Grotto' if d > 0 else 'LN_Stone_Dark',
|
|
up_hint=tuple(face), steps=26, lens=.08))
|
|
front = [at(GATE_U + .62, cos(pi * k / 12) * radius, spring + sin(pi * k / 12) * radius * .78) for k in range(13)]
|
|
runes.append(L.sweep('Gate_ArchGlyphs', front, [.2] * 5, [.04] * 5, 'LN_Rune_Grotto', up_hint=tuple(face), steps=26, uv_cells=.42))
|
|
apex = spring + radius * .78
|
|
key = [at(GATE_U + .7, -.62, apex + .1), at(GATE_U + .7, -.78, apex + .78), at(GATE_U + .7, 0, apex + 1.45),
|
|
at(GATE_U + .7, .78, apex + .78), at(GATE_U + .7, .62, apex + .1)]
|
|
parts.append(L.sweep('Gate_Keystone', key, [.34, .44, .5, .44, .34], [.42] * 5, 'LN_Metal_Bronze', up_hint=tuple(face), steps=16, lens=.3))
|
|
# Lintel block that ties the arch into the rock above.
|
|
parts.append(L.box('Gate_Lintel', tuple(at(GATE_U - .2, 0, apex + .55)), (PYLON_A * 2 + .8, .7, 1.5), 'LN_Stone_Dark',
|
|
yaw=yaw_to(AXIS), bev=.06, segments=2))
|
|
# Threshold at the arena rim and the stairs down into the dark (the lower lying cave).
|
|
parts.append(L.box('Gate_Threshold', tuple(at(GATE_U + .95, 0, .02)), ((PYLON_A - .6) * 2, .16, 1.0), 'LN_Stone_Dark', yaw=yaw_to(AXIS), bev=.03))
|
|
treads = []
|
|
for k in range(5):
|
|
u = GATE_U + .4 - k * .42
|
|
top = .02 - (k + 1) * .19
|
|
treads.append(L.box('Cave_Step_%d' % k, tuple(at(u, 0, top - .5)), ((PYLON_A - .72) * 2, 1.0, .44), 'LN_Stone_Dark', yaw=yaw_to(AXIS), bev=.02))
|
|
return parts, runes, treads, apex
|
|
|
|
|
|
def cave_parts():
|
|
"""The dark hollow behind the gate: side and back walls, the well rim at the bottom and the
|
|
light from inside (glowing mist and moon well), crystals on the walls."""
|
|
walls = []
|
|
inner = PYLON_A - .66
|
|
for s in (-1, 1):
|
|
walls.append(L.box('Cave_Wall_%d' % s, tuple(at(GATE_U - 1.35, s * (inner + .2), 1.4)), (.4, 4.6, 2.8), 'LN_Cave',
|
|
yaw=yaw_to(AXIS), bev=0.0))
|
|
walls.append(L.box('Cave_Back', tuple(at(GATE_U - 2.75, 0, 1.4)), (inner * 2 + .8, 4.6, .4), 'LN_Cave', yaw=yaw_to(AXIS), bev=0.0))
|
|
walls.append(L.box('Cave_Floor', tuple(at(GATE_U - 2.05, 0, -1.25)), (inner * 2 + .4, .4, 1.6), 'LN_Cave', yaw=yaw_to(AXIS), bev=0.0))
|
|
walls.append(L.lathe('Cave_WellRim', [(1.02, -1.08), (1.02, -.92), (.86, -.9), (.86, -1.02)], 20, 'LN_Stone_Dark',
|
|
center=tuple(at(GATE_U - 2.05, 0, 0))))
|
|
glow = [L.panel('Cave_Mist', tuple(at(GATE_U - 2.5, 0, .9)), inner * 2 - .1, 3.6, yaw_to(AXIS), 'LN_Water_Glow', depth=0.0),
|
|
L.disc_grid('Cave_Well', tuple(at(GATE_U - 2.05, 0, 0)), .86, 'LN_Water_Glow', rings=2, sides=20, y=-.97)]
|
|
for k in range(6):
|
|
s = -1 if k % 2 else 1
|
|
u = GATE_U - .9 - (k // 2) * .6
|
|
c = at(u, s * (inner - .05), .1 + (k // 2) * .55)
|
|
glow.append(L.crystal('Cave_Crystal_%d' % k, tuple(c), .14 + .04 * (k % 3), .7 + .2 * (k % 2), .18, 'LN_Crystal_Grotto',
|
|
sides=5, tilt=(-.5 * s, .6 * s), seed=k + 30))
|
|
return walls, glow
|
|
|
|
|
|
# ---------------------------------------------------------------- moon water troughs
|
|
def channel_parts():
|
|
"""Two raised troughs of moon water that flank the gate along the back rim."""
|
|
parts, water = [], []
|
|
for s in (-1, 1):
|
|
pts = []
|
|
for k in range(8):
|
|
ang = pi + pi / 4 + s * (.34 + .5 * k / 7)
|
|
pts.append(V((cos(ang) * 6.65, 0, sin(ang) * 6.65)))
|
|
parts.append(L.sweep('Channel_Lip_%d' % s, [p + V((0, .16, 0)) for p in pts], [1.0] * 3, [.2] * 3, 'LN_Stone_Dark',
|
|
up_hint=(0, 1, 0), steps=18, lens=0.0))
|
|
water.append(L.sweep('Channel_Water_%d' % s, [p + V((0, .27, 0)) for p in pts], [.72] * 3, [.02] * 3, 'LN_Water_Glow',
|
|
up_hint=(0, 1, 0), steps=18))
|
|
return parts, water
|
|
|
|
|
|
# ---------------------------------------------------------------- columns
|
|
def column(name, base, height, broken=False, seed=0.0):
|
|
parts = [L.lathe(name + '_Base', [(.62, -.05), (.62, .22), (.5, .3), (.48, .42)], 12, 'LN_Stone_Dark', center=tuple(base), rotation=seed)]
|
|
top = height if not broken else height * (.42 + .2 * ((seed * 7.3) % 1.0))
|
|
y, k = .4, 0
|
|
while y < top - .05:
|
|
h = min(.9, top - y)
|
|
r = .42 - .02 * k
|
|
parts.append(L.lathe('%s_Drum_%d' % (name, k), [(r, y), (r * .99, y + h - .03)], 14, 'LN_Stone_Grotto', center=tuple(base),
|
|
rotation=seed + k * .3, jitter=.02 if not broken else .05, seed=seed + k))
|
|
y += h
|
|
k += 1
|
|
crystal = None
|
|
if broken:
|
|
parts.append(L.chunk(name + '_Break', tuple(base + V((0, top + .05, 0))), (.4, .2, .38), 'LN_Stone_Grotto', seed=seed, sink=0.0))
|
|
else:
|
|
parts.append(L.lathe(name + '_Capital', [(.4, top), (.62, top + .28), (.66, top + .42), (.5, top + .48)], 12, 'LN_Stone_Dark',
|
|
center=tuple(base), rotation=seed))
|
|
out = V((base.x, 0, base.z)).normalized()
|
|
t = V((out.z, 0, -out.x))
|
|
for s in (-1, 1):
|
|
pts = [base + t * s * .3 + V((0, top + .44, 0)), base + t * s * .58 + V((0, top + .9, 0)), base + t * s * .22 + V((0, top + 1.25, 0))]
|
|
parts.append(L.sweep('%s_Horn_%d' % (name, s), pts, [.16, .12, .03], [.14, .1, .03], 'LN_Stone_Grotto', up_hint=tuple(out), steps=10, lens=.3))
|
|
crystal = L.crystal(name + '_Crystal', tuple(base + V((0, top + .95, 0))), .15, .38, .22, 'LN_Crystal_Grotto', sides=5, seed=seed)
|
|
return parts, crystal
|
|
|
|
|
|
def pillar_parts():
|
|
"""Columns along both sides of the arena rim; the sector before the gate stays open so the
|
|
entrance is seen from the game camera. Angles are measured from the back direction."""
|
|
parts, crystals = [], []
|
|
layout = [(62, 4.7, False), (84, 3.9, True), (106, 4.5, False), (128, 3.6, True)]
|
|
back = atan2(BACK.z, BACK.x)
|
|
index = 0
|
|
for degrees, height, broken in layout:
|
|
for s in (-1, 1):
|
|
ang = back + s * degrees * pi / 180
|
|
p = V((cos(ang) * 7.2, 0, sin(ang) * 7.2))
|
|
part, crystal = column('Column_%d' % index, p, height, broken, index * 1.37)
|
|
parts += part
|
|
if crystal is not None:
|
|
crystals.append(crystal)
|
|
index += 1
|
|
# A fallen column on each side, lying along the rim near the front.
|
|
for s in (-1, 1):
|
|
ang = back + s * 146 * pi / 180
|
|
p = V((cos(ang) * 7.0, .42, sin(ang) * 7.0))
|
|
d = V((-sin(ang), 0, cos(ang)))
|
|
for k in range(3):
|
|
c = p + d * (k - 1) * .92 * s
|
|
ob = L.lathe('Fallen_%d_%d' % (s, k), [(.4, -.44), (.39, .44)], 14, 'LN_Stone_Grotto', jitter=.03, seed=k + s)
|
|
ob.data.transform(Matrix.Rotation(pi / 2, 4, 'X'))
|
|
ob.data.transform(Matrix.Rotation(-ang, 4, 'Z'))
|
|
ob.location = L.G(c.x, c.y - k * .03, c.z)
|
|
parts.append(ob)
|
|
return parts, crystals
|
|
|
|
|
|
# ---------------------------------------------------------------- cliff rocks
|
|
def rock_parts(apex):
|
|
"""Faceted cliff masses in the map's low-poly rock language that frame the gate, crown the
|
|
arch and step down along the escarpment. Each rock stands on the real terrain."""
|
|
parts = []
|
|
# The Top lane wraps round the grotto on three sides, so the rocks stay in the lens between
|
|
# the arena and its corridor. (u, a, radius across, height above the foot, radius along
|
|
# the axis, lean, seed); every rock is mirrored.
|
|
specs = [(GATE_U - .45, 3.95, 1.2, 5.3, 1.1, .06, 1.0), (GATE_U - 2.05, 2.25, 1.0, 5.8, .8, .0, 2.0),
|
|
(GATE_U + 1.05, 5.55, 1.15, 3.5, 1.05, .1, 4.0), (-4.6, 7.55, .9, 2.3, .9, .14, 5.0)]
|
|
index = 0
|
|
for u, a, rx, height, rz, lean, seed in specs:
|
|
for s in (-1, 1):
|
|
c = at(u, s * a)
|
|
foot = min(ground(c + V((dx, 0, dz))) for dx in (-rx * .7, 0, rx * .7) for dz in (-rz * .7, 0, rz * .7))
|
|
yaw = yaw_to(AXIS) + s * .12 * seed
|
|
check_clear('Rock_%d' % index, c, rx * 1.15, rz * 1.15, yaw)
|
|
ry = height * .56
|
|
parts.append(L.cliff('Rock_%d' % index, tuple(c + V((0, foot - .45 + ry * .9, 0))), (rx, ry, rz), 'LN_Rock',
|
|
seed=seed * 2.3 + (7 if s < 0 else 0), yaw=yaw, lean=(lean * s, lean * .5), flat_bottom=foot - .5,
|
|
cuts=9, amp=.2))
|
|
index += 1
|
|
# The crown above the arch: a flat slab of rock that the lintel sinks into, and the rock
|
|
# that closes the cave at the back.
|
|
crown = at(GATE_U - .85, 0, apex + 1.0)
|
|
check_clear('Rock_Crown', crown, 3.35, 1.45, yaw_to(AXIS))
|
|
parts.append(L.cliff('Rock_Crown', tuple(crown), (3.3, 1.05, 1.4), 'LN_Rock', seed=11.0, yaw=yaw_to(AXIS), flat_bottom=apex + .2))
|
|
back = at(GATE_U - 2.35, 0)
|
|
foot = ground(back)
|
|
check_clear('Rock_Back', back, 1.9, .75, yaw_to(AXIS))
|
|
parts.append(L.cliff('Rock_Back', tuple(back + V((0, foot + 2.4, 0))), (1.95, 3.0, .7), 'LN_Rock', seed=13.0, yaw=yaw_to(AXIS),
|
|
flat_bottom=foot - .4))
|
|
return parts
|
|
|
|
|
|
def build():
|
|
L.reset()
|
|
floor, runes = floor_parts()
|
|
platform = platform_parts()
|
|
altar, altar_runes, source = altar_parts()
|
|
gate, gate_runes, treads, apex = gate_parts()
|
|
cave, glow = cave_parts()
|
|
channel, water = channel_parts()
|
|
pillars, pillar_crystals = pillar_parts()
|
|
rocks = rock_parts(apex)
|
|
if _problems:
|
|
raise SystemExit('GROTTO_CLEARANCE_FAILED: ' + '; '.join(_problems))
|
|
runes += altar_runes + gate_runes
|
|
objects = {
|
|
'Floor': L.join(floor + channel, 'Floor'),
|
|
'Platform': L.join(platform, 'Platform'),
|
|
'Altar': L.join(altar, 'Altar'),
|
|
'Gate': L.join(gate, 'Gate'),
|
|
'Pillars': L.join(pillars, 'Pillars'),
|
|
'Rocks': L.join(rocks, 'Rocks'),
|
|
'Cave': L.join(cave + treads, 'Cave'),
|
|
'Runes': L.join(runes, 'Runes'),
|
|
'Source': L.join(source, 'Source', origin=tuple(BOSS + BACK * 2.3 + V((0, 1.4, 0)))),
|
|
'PillarCrystals': L.join(pillar_crystals, 'PillarCrystals'),
|
|
'CaveGlow': L.join(glow, 'CaveGlow'),
|
|
'Channel': L.join(water, 'Channel'),
|
|
}
|
|
solid = [objects[k] for k in ('Floor', 'Platform', 'Altar', 'Gate', 'Pillars', 'Rocks', 'Cave', 'Runes')]
|
|
built = [ob for ob in solid if ob.name != 'Rocks']
|
|
L.finalize(built + [objects['Source'], objects['PillarCrystals']], occluders=solid, reach=3.0, ground=-1.3)
|
|
# Rocks keep hard facets like the map's low-poly rocks.
|
|
L.finalize([objects['Rocks']], sharp_angle=14.0, occluders=solid, reach=3.0, ground=-1.3)
|
|
L.finalize([objects['CaveGlow'], objects['Channel']], ao=False)
|
|
everything = list(objects.values())
|
|
for ob in everything:
|
|
L.triangulate(ob)
|
|
doc, raw = L.export(everything, 'bossgrotte.glb', 'bossgrotte.blend')
|
|
entry = L.record('bossgrotte', 'bossgrotte.glb', 'bossgrotte.blend', 'build_bossgrotte.py', everything, doc, raw,
|
|
title='Mondschlund (Bossgrotte)', boss_point=[-1.5, 0.0, -1.5], arena_radius=ARENA,
|
|
gate=[round(x, 3) for x in at(GATE_U, 0)], lane_clearance_min=CLEAR,
|
|
cave_notch={'u': [GATE_U - 3.0, GATE_U + .5], 'half_width': PYLON_A - .66, 'depth': 1.0},
|
|
note='symmetric across the local diagonal x = z; the Godot view places it at LuneMap.GROTTO on the terrace')
|
|
L.write_manifest([entry])
|
|
print('GROTTO_BUILD', entry['triangles'], 'triangles', 'bounds', entry['bounds_godot'], flush=True)
|
|
|
|
|
|
build()
|