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>
3
.gitignore
vendored
|
|
@ -2,3 +2,6 @@
|
||||||
*.tmp
|
*.tmp
|
||||||
docs/screenshots/
|
docs/screenshots/
|
||||||
test-results/
|
test-results/
|
||||||
|
__pycache__/
|
||||||
|
.DS_Store
|
||||||
|
override.cfg
|
||||||
|
|
|
||||||
BIN
assets-source/luneraria/g2/ancient.blend
Normal file
BIN
assets-source/luneraria/g2/bastion.blend
Normal file
BIN
assets-source/luneraria/g2/bossgrotte.blend
Normal file
290
assets-source/luneraria/g2/build_ancient.py
Normal file
|
|
@ -0,0 +1,290 @@
|
||||||
|
"""G2 Ancient of Champions of Luneraria: the "Mondherz" (moon heart) sanctuary.
|
||||||
|
|
||||||
|
The heart is the existing Saumkern crystal (phase 4, assets-source/phase4/blend/core_team0.blend:
|
||||||
|
Core_CrystalMain and its five satellites), appended here and scaled up. Around it:
|
||||||
|
- a wide, very low dais (three steps, 12 cm in total, so figures standing on it keep their
|
||||||
|
feet above the floor) with a rune circle,
|
||||||
|
- an octagonal basalt and moonstone pedestal with rune panels (radius 2.0 m at the floor:
|
||||||
|
melee attackers stand 2.3 m from the centre and never clip into it),
|
||||||
|
- four crescent blades rising from the pedestal, bowing out over the heads of the
|
||||||
|
attackers and meeting above the crystal (split into Lower and Upper so the upper halves
|
||||||
|
can fall when the Ancient is destroyed),
|
||||||
|
- two counter-rotating armillary rings with rune bands and six floating fragments,
|
||||||
|
- a crown per team: Crest_A (cool team) two crescent horns around a moon orb,
|
||||||
|
Crest_B (warm team) a ring of sun rays around a sun orb,
|
||||||
|
- two banner poles behind the sanctuary, banners take the team silhouette in the shader,
|
||||||
|
- ruin pieces: shattered crystal and blade rubble shown after the collapse.
|
||||||
|
The hitbox and every gameplay distance stay those of LuneMap (building radius 1.3 m); both
|
||||||
|
teams use the same geometry apart from the crest.
|
||||||
|
|
||||||
|
Local frame: +z faces the enemy (map centre), -z the base corner. Pivot at the dais centre.
|
||||||
|
Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/g2/build_ancient.py
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
import bpy
|
||||||
|
import lune_bpy as L
|
||||||
|
from lune_bpy import V, pi, sin, cos
|
||||||
|
from mathutils import noise, Matrix
|
||||||
|
|
||||||
|
OCT = pi / 8
|
||||||
|
SAUMKERN = L.P / 'assets-source/phase4/blend/core_team0.blend'
|
||||||
|
CRYSTAL_Y = 4.7
|
||||||
|
CRYSTAL_SCALE = 2.3
|
||||||
|
# Blades on the local axes: from the usual camera the crystal shows between two blades.
|
||||||
|
BLADE_ANGLES = (0.0, pi / 2, pi, 3 * pi / 2)
|
||||||
|
BLADE_SPLIT = 3.4
|
||||||
|
|
||||||
|
|
||||||
|
def octa(name, profile, mat, **kw):
|
||||||
|
return L.lathe(name, profile, 8, mat, rotation=OCT, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
def blade_points(angle, lower=True, upper=True):
|
||||||
|
d = V((cos(angle), 0, sin(angle)))
|
||||||
|
local = [(1.36, 1.18), (1.92, 2.3), (2.36, 3.4), (2.46, 4.6), (2.12, 6.2), (1.3, 7.7), (.52, 8.62)]
|
||||||
|
widths = [.66, .62, .56, .5, .42, .3, .18]
|
||||||
|
depths = [.52, .5, .46, .42, .36, .28, .18]
|
||||||
|
if lower and not upper:
|
||||||
|
sel = slice(0, 3)
|
||||||
|
elif upper and not lower:
|
||||||
|
sel = slice(2, 7)
|
||||||
|
else:
|
||||||
|
sel = slice(0, 7)
|
||||||
|
return [d * r + V((0, y, 0)) for r, y in local[sel]], widths[sel], depths[sel]
|
||||||
|
|
||||||
|
|
||||||
|
def blade(name, angle, part):
|
||||||
|
t = V((-sin(angle), 0, cos(angle)))
|
||||||
|
points, widths, depths = blade_points(angle, part != 'upper', part != 'lower')
|
||||||
|
return L.sweep(name, points, widths, depths, 'LN_Stone_Pale', up_hint=tuple(t), steps=16 if part != 'full' else 30, lens=.28)
|
||||||
|
|
||||||
|
|
||||||
|
def blade_trim(name, angle, part, offset, mat, width, depth_scale):
|
||||||
|
"""Metal cladding along the outer edge (offset > 0) or a rune vein along the inner edge
|
||||||
|
(offset < 0) of a blade; the offset follows the in-plane normal of the curve."""
|
||||||
|
t = V((-sin(angle), 0, cos(angle)))
|
||||||
|
points, widths, depths = blade_points(angle, part != 'upper', part != 'lower')
|
||||||
|
shifted = []
|
||||||
|
for i, p in enumerate(points):
|
||||||
|
a = points[max(0, i - 1)]
|
||||||
|
b = points[min(len(points) - 1, i + 1)]
|
||||||
|
tangent = (b - a).normalized()
|
||||||
|
outward = tangent.cross(t).normalized()
|
||||||
|
shifted.append(p + outward * offset * widths[i] * .5)
|
||||||
|
return L.sweep(name, shifted, [width] * len(points), [x * depth_scale for x in depths], mat, up_hint=tuple(t),
|
||||||
|
steps=16, lens=.1, uv_cells=.34)
|
||||||
|
|
||||||
|
|
||||||
|
def append_saumkern():
|
||||||
|
names = ['Core_CrystalMain'] + ['Core_CrystalSatellite%d' % i for i in range(5)]
|
||||||
|
with bpy.data.libraries.load(str(SAUMKERN), link=False) as (source, target):
|
||||||
|
target.objects = [n for n in names if n in source.objects]
|
||||||
|
result = []
|
||||||
|
for ob in target.objects:
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
world = ob.matrix_world.copy()
|
||||||
|
ob.parent = None
|
||||||
|
ob.data = ob.data.copy()
|
||||||
|
# Bake the object transform, then scale around the Saumkern crystal centre.
|
||||||
|
ob.data.transform(world)
|
||||||
|
ob.matrix_world = Matrix.Identity(4)
|
||||||
|
ob.modifiers.clear()
|
||||||
|
ob.hide_viewport = False
|
||||||
|
ob.hide_render = False
|
||||||
|
ob.hide_select = False
|
||||||
|
ob.hide_set(False)
|
||||||
|
ob.data.transform(Matrix.Translation(V((0, 0, -1.32))))
|
||||||
|
ob.data.transform(Matrix.Scale(CRYSTAL_SCALE, 4))
|
||||||
|
ob.data.transform(Matrix.Translation(V((0, 0, CRYSTAL_Y))))
|
||||||
|
ob.data.materials.clear()
|
||||||
|
ob.data.materials.append(L.material('LN_Crystal_Team'))
|
||||||
|
for poly in ob.data.polygons:
|
||||||
|
poly.material_index = 0
|
||||||
|
result.append(ob)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def crest_a():
|
||||||
|
"""Cool team: two crescent horns rising from the crown ring around a small moon orb."""
|
||||||
|
parts = []
|
||||||
|
for i, a in enumerate((0.0, pi)):
|
||||||
|
d = V((cos(a), 0, sin(a)))
|
||||||
|
pts = [d * .62 + V((0, 8.72, 0)), d * 1.0 + V((0, 9.3, 0)), d * .82 + V((0, 9.95, 0)), d * .22 + V((0, 10.35, 0))]
|
||||||
|
parts.append(L.sweep('CrestA_Horn_%d' % i, pts, [.3, .26, .16, .04], [.2, .18, .12, .03], 'LN_Metal_Team',
|
||||||
|
up_hint=(0, 0, 1), steps=18, lens=.35))
|
||||||
|
inlay = [p + V((0, 0, .1)) for p in pts]
|
||||||
|
parts.append(L.sweep('CrestA_Vein_%d' % i, inlay, [.08, .07, .05, .02], [.05, .05, .04, .02], 'LN_Rune_Team',
|
||||||
|
up_hint=(0, 0, 1), steps=18, uv_cells=.3))
|
||||||
|
parts.append(L.lathe('CrestA_Orb', [(0.0, 9.28), (.2, 9.36), (.28, 9.55), (.2, 9.74), (0.0, 9.82)], 16, 'LN_Crystal_Team', smooth=True))
|
||||||
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
def crest_b():
|
||||||
|
"""Warm team: a crown of nine sun rays around a sun orb."""
|
||||||
|
parts = []
|
||||||
|
for k in range(9):
|
||||||
|
a = 2 * pi * k / 9
|
||||||
|
d = V((cos(a), 0, sin(a)))
|
||||||
|
length = 1.15 if k % 2 == 0 else .82
|
||||||
|
pts = [d * .5 + V((0, 8.74, 0)), d * (.5 + length * .45) + V((0, 9.05 + length * .32, 0)), d * (.5 + length * .7) + V((0, 9.2 + length * .62, 0))]
|
||||||
|
t = V((-sin(a), 0, cos(a)))
|
||||||
|
parts.append(L.sweep('CrestB_Ray_%d' % k, pts, [.22, .14, .02], [.12, .09, .02], 'LN_Metal_Team', up_hint=tuple(t), steps=10, lens=.3))
|
||||||
|
parts.append(L.lathe('CrestB_Orb', [(0.0, 9.0), (.26, 9.08), (.34, 9.3), (.26, 9.52), (0.0, 9.6)], 18, 'LN_Crystal_Team', smooth=True))
|
||||||
|
parts.append(L.ring('CrestB_Halo', (0, 9.3, 0), .5, .06, .08, 'LN_Metal_Team', sides=32, axis_tilt=(pi / 2, 0)))
|
||||||
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
def armillary(name, radius, tilt, sides=56):
|
||||||
|
metal = L.ring(name + '_Metal', (0, 0, 0), radius, .13, .11, 'LN_Metal_Team', sides=sides, uv_cell=None)
|
||||||
|
runes = L.band(name + '_Runes', (0, -.035, 0), radius + .068, 0.0, .07, 'LN_Rune_Team', sides=sides, uv_cell=.3)
|
||||||
|
ob = L.join([metal, runes], name)
|
||||||
|
ob.data.transform(Matrix.Rotation(tilt[0], 4, 'X') @ Matrix.Rotation(tilt[1], 4, 'Y'))
|
||||||
|
ob.location = L.G(0, CRYSTAL_Y, 0)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def fragment(name, index):
|
||||||
|
a = 2 * pi * index / 6 + .3
|
||||||
|
r = 3.05
|
||||||
|
y = 3.3 + (index % 3) * .8
|
||||||
|
c = (cos(a) * r, y, sin(a) * r)
|
||||||
|
size = .17 + .05 * (index % 2)
|
||||||
|
return L.crystal(name, c, size, size * 2.6, size * 1.8, 'LN_Crystal_Team', sides=5, tilt=(.3 * noise.noise(V((index, 0, 0))), a), seed=index * 3.7)
|
||||||
|
|
||||||
|
|
||||||
|
def banner_pole(name, angle, radius):
|
||||||
|
d = V((cos(angle), 0, sin(angle)))
|
||||||
|
t = V((-sin(angle), 0, cos(angle)))
|
||||||
|
base = d * radius
|
||||||
|
parts = [
|
||||||
|
L.lathe(name + '_Foot', [(.36, -.05), (.36, .3), (.26, .38), (.2, .5)], 8, 'LN_Stone_Dark', center=(base.x, 0, base.z)),
|
||||||
|
L.lathe(name + '_Shaft', [(.1, .45), (.085, 6.2)], 10, 'LN_Metal_Iron', center=(base.x, 0, base.z)),
|
||||||
|
L.box(name + '_Bar', (base.x, 5.95, base.z), (.08, .08, 1.5), 'LN_Metal_Team', yaw=-angle, bev=.012),
|
||||||
|
L.crystal(name + '_Tip', (base.x, 6.45, base.z), .09, .3, .1, 'LN_Crystal_Team', sides=5, seed=angle),
|
||||||
|
]
|
||||||
|
return parts, base, t
|
||||||
|
|
||||||
|
|
||||||
|
def build():
|
||||||
|
L.reset()
|
||||||
|
parts = {}
|
||||||
|
# Dais: three shallow steps and the rune circle.
|
||||||
|
# Step tops at 5, 10 and 15 cm; the Godot world lifts figures standing on them (DAIS_STEPS).
|
||||||
|
parts['Dais'] = [
|
||||||
|
L.lathe('Dais_0', [(4.0, -.06), (4.0, .05), (3.93, .055)], 64, 'LN_Stone_Dark'),
|
||||||
|
L.lathe('Dais_1', [(3.9, .05), (3.4, .055), (3.4, .1), (3.33, .105)], 64, 'LN_Stone_Pale'),
|
||||||
|
L.lathe('Dais_2', [(3.3, .1), (2.8, .105), (2.8, .15), (2.73, .155)], 64, 'LN_Stone_Dark'),
|
||||||
|
L.disc_grid('Dais_Top', (0, 0, 0), 2.72, 'LN_Stone_Pale', rings=3, sides=64, y=.154),
|
||||||
|
]
|
||||||
|
rune_circle = L.lathe('Dais_Runes', [(3.26, .107), (3.02, .107)], 64, 'LN_Rune_Team', cap_top=False, cap_bottom=False, uv_band=.4)
|
||||||
|
parts['Dais'].append(rune_circle)
|
||||||
|
# Pedestal: basalt foot, moonstone body with rune panels, cap and the crystal seat.
|
||||||
|
parts['Pedestal'] = [
|
||||||
|
octa('Ped_Foot', [(2.0, .1), (2.0, .44), (1.86, .5)], 'LN_Stone_Dark', jitter=.01, seed=2),
|
||||||
|
octa('Ped_Body', [(1.74, .48), (1.66, .78), (1.56, 1.04), (1.5, 1.1)], 'LN_Stone_Pale'),
|
||||||
|
octa('Ped_Cap', [(1.5, 1.08), (1.54, 1.16), (1.42, 1.26), (1.3, 1.3)], 'LN_Stone_Dark'),
|
||||||
|
L.lathe('Ped_Seat', [(1.02, 1.28), (1.12, 1.42), (.9, 1.56), (.7, 1.6)], 32, 'LN_Metal_Team'),
|
||||||
|
octa('Ped_Runes', [(1.732, .58), (1.672, .98)], 'LN_Rune_Team', cap_top=False, cap_bottom=False, uv_band=.42),
|
||||||
|
]
|
||||||
|
for k in range(8):
|
||||||
|
a = 2 * pi * k / 8
|
||||||
|
parts['Pedestal'].append(L.box('Ped_Rib_%d' % k, (cos(a) * 1.64, .8, sin(a) * 1.64), (.2, .62, .16), 'LN_Stone_Dark', yaw=-a + pi / 2, bev=.02))
|
||||||
|
# Blades: Lower halves stay, Upper halves are separate objects that can fall.
|
||||||
|
lowers, uppers = [], {}
|
||||||
|
for i, a in enumerate(BLADE_ANGLES):
|
||||||
|
lowers.append(blade('Blade_%d_LowerStone' % i, a, 'lower'))
|
||||||
|
lowers.append(blade_trim('Blade_%d_LowerClad' % i, a, 'lower', 1.02, 'LN_Metal_Team', .1, 1.08))
|
||||||
|
lowers.append(blade_trim('Blade_%d_LowerVein' % i, a, 'lower', -1.0, 'LN_Rune_Team', .07, .6))
|
||||||
|
uppers[i] = [blade('Blade_%d_UpperStone' % i, a, 'upper'),
|
||||||
|
blade_trim('Blade_%d_UpperClad' % i, a, 'upper', 1.02, 'LN_Metal_Team', .1, 1.08),
|
||||||
|
blade_trim('Blade_%d_UpperVein' % i, a, 'upper', -1.0, 'LN_Rune_Team', .07, .6)]
|
||||||
|
crown = [L.ring('Crown_Ring', (0, 8.62, 0), .62, .16, .22, 'LN_Metal_Team', sides=40, uv_cell=.28),
|
||||||
|
L.band('Crown_Runes', (0, 8.55, 0), .71, 0.0, .12, 'LN_Rune_Team', sides=40, uv_cell=.28)]
|
||||||
|
heart = append_saumkern()
|
||||||
|
rings = {'Ring_Outer': armillary('Ring_Outer', 1.86, (.2, .0)), 'Ring_Inner': armillary('Ring_Inner', 1.46, (-.34, .6))}
|
||||||
|
fragments = [fragment('Fragment_%d' % i, i) for i in range(6)]
|
||||||
|
poles, cloth = [], []
|
||||||
|
for side, key in ((1, 'Banner_L'), (-1, 'Banner_R')):
|
||||||
|
angle = -pi / 2 + side * .95
|
||||||
|
pole_parts, base, t = banner_pole('Pole_%s' % key[-1], angle, 4.6)
|
||||||
|
poles += pole_parts
|
||||||
|
top = base + V((0, 5.9, 0)) + V((cos(angle), 0, sin(angle))) * .12
|
||||||
|
cloth.append(L.banner(key, 1.15, 2.9, 'LN_Cloth_Team', top=tuple(top), segments=(6, 14), yaw=-(angle + pi / 2)))
|
||||||
|
# Ruin pieces: shattered heart on the seat and blade rubble on the dais.
|
||||||
|
ruin_crystal = []
|
||||||
|
for k in range(9):
|
||||||
|
a = 2 * pi * k / 9 + .4
|
||||||
|
r = .35 + .5 * ((k * .37) % 1.0)
|
||||||
|
size = .16 + .1 * ((k * .53) % 1.0)
|
||||||
|
ruin_crystal.append(L.crystal('RuinCrystal_%d' % k, (cos(a) * r, 1.55 + size * .4, sin(a) * r), size, size * 2.2, size * .8,
|
||||||
|
'LN_Crystal_Team', sides=5, tilt=(1.0 + .5 * ((k * .29) % 1.0), a), seed=k))
|
||||||
|
for k in range(5):
|
||||||
|
a = 2 * pi * k / 5 + 1.1
|
||||||
|
r = 2.3 + .4 * ((k * .61) % 1.0)
|
||||||
|
size = .2 + .08 * ((k * .7) % 1.0)
|
||||||
|
ruin_crystal.append(L.crystal('RuinCrystalFar_%d' % k, (cos(a) * r, .17 + size * .3, sin(a) * r), size, size * 2.0, size * .6,
|
||||||
|
'LN_Crystal_Team', sides=5, tilt=(1.4, a), seed=k + 20))
|
||||||
|
ruin_rubble = []
|
||||||
|
# Broken caps on the blade stumps, so the cut at the split height reads as a fracture.
|
||||||
|
for i, a in enumerate(BLADE_ANGLES):
|
||||||
|
d = V((cos(a), 0, sin(a)))
|
||||||
|
for j in range(3):
|
||||||
|
at = d * (2.28 + .12 * (j - 1)) + V((0, BLADE_SPLIT - .05 + .08 * j, 0)) + V((-sin(a), 0, cos(a))) * (.12 * (j - 1))
|
||||||
|
ruin_rubble.append(L.chunk('Cap_%d_%d' % (i, j), tuple(at), (.3, .22, .24), 'LN_Stone_Pale', seed=i * 5.0 + j, yaw=a + j, tilt=.4 * (j - 1), sink=0.0))
|
||||||
|
for k in range(18):
|
||||||
|
a = 2 * pi * ((k * .618) % 1.0)
|
||||||
|
r = 2.1 + 2.0 * ((k * .377) % 1.0)
|
||||||
|
s = .28 + .3 * ((k * .733) % 1.0)
|
||||||
|
ruin_rubble.append(L.chunk('Rubble_%d' % k, (cos(a) * r, .1 + s * .2, sin(a) * r), (s * .9, s * .45, s * .7),
|
||||||
|
'LN_Stone_Pale' if k % 3 else 'LN_Metal_Team', seed=k * 1.7, yaw=a * 2.3, tilt=.2))
|
||||||
|
# Join into the scene objects the Godot view drives.
|
||||||
|
objects = []
|
||||||
|
dais = L.join(parts['Dais'], 'Dais')
|
||||||
|
pedestal = L.join(parts['Pedestal'] + lowers + poles, 'Pedestal')
|
||||||
|
objects += [dais, pedestal]
|
||||||
|
upper_obs = []
|
||||||
|
for i, a in enumerate(BLADE_ANGLES):
|
||||||
|
d = V((cos(a), 0, sin(a)))
|
||||||
|
pivot = d * 2.36 + V((0, BLADE_SPLIT, 0))
|
||||||
|
upper_obs.append(L.join(uppers[i], 'Blade_%d' % i, origin=tuple(pivot)))
|
||||||
|
crown_ob = L.join(crown, 'Crown', origin=(0, 8.62, 0))
|
||||||
|
crest_a_ob = L.join(crest_a(), 'Crest_A', origin=(0, 8.62, 0))
|
||||||
|
crest_b_ob = L.join(crest_b(), 'Crest_B', origin=(0, 8.62, 0))
|
||||||
|
heart_main = L.join([heart[0]], 'Crystal', origin=(0, CRYSTAL_Y, 0))
|
||||||
|
satellites = []
|
||||||
|
for i, ob in enumerate(heart[1:]):
|
||||||
|
c = sum((ob.matrix_world @ v.co for v in ob.data.vertices), V()) / len(ob.data.vertices)
|
||||||
|
satellites.append(L.join([ob], 'Satellite_%d' % i, origin=L.to_godot(c)))
|
||||||
|
ring_obs = []
|
||||||
|
for key, ob in rings.items():
|
||||||
|
ob.name = key
|
||||||
|
ring_obs.append(L.join([ob], key, origin=(0, CRYSTAL_Y, 0)))
|
||||||
|
frag_obs = []
|
||||||
|
for ob in fragments:
|
||||||
|
c = sum((ob.matrix_world @ v.co for v in ob.data.vertices), V()) / len(ob.data.vertices)
|
||||||
|
frag_obs.append(L.join([ob], ob.name, origin=L.to_godot(c)))
|
||||||
|
cloth_obs = [L.join([ob], ob.name) for ob in cloth]
|
||||||
|
ruin_c = L.join(ruin_crystal, 'Ruin_Crystal')
|
||||||
|
ruin_r = L.join(ruin_rubble, 'Ruin_Rubble')
|
||||||
|
structure = [dais, pedestal] + upper_obs + [crown_ob, crest_a_ob, crest_b_ob]
|
||||||
|
energy = [heart_main] + satellites + ring_obs + frag_obs
|
||||||
|
L.finalize(structure + energy, occluders=structure + ring_obs)
|
||||||
|
L.finalize([ruin_c, ruin_r], occluders=[dais, pedestal, ruin_c, ruin_r])
|
||||||
|
L.finalize(cloth_obs, ao=False)
|
||||||
|
everything = structure + energy + cloth_obs + [ruin_c, ruin_r]
|
||||||
|
for ob in everything:
|
||||||
|
L.triangulate(ob)
|
||||||
|
doc, raw = L.export(everything, 'ancient.glb', 'ancient.blend')
|
||||||
|
entry = L.record('ancient', 'ancient.glb', 'ancient.blend', 'build_ancient.py', structure + energy, doc, raw,
|
||||||
|
title='Mondherz (Ancient)', crystal_height=CRYSTAL_Y, blade_split=BLADE_SPLIT,
|
||||||
|
heart_source='assets-source/phase4/blend/core_team0.blend (Core_CrystalMain, Core_CrystalSatellite0-4), scale %.1f' % CRYSTAL_SCALE,
|
||||||
|
triangles_intact=L.triangles(structure + energy + cloth_obs) - L.triangles([crest_b_ob]),
|
||||||
|
triangles_ruin=L.triangles([ruin_c, ruin_r]))
|
||||||
|
L.write_manifest([entry])
|
||||||
|
print('ANCIENT_BUILD', entry['triangles'], 'triangles', 'height %.2f' % entry['bounds_godot']['max'][1],
|
||||||
|
'radius %.2f' % max(abs(entry['bounds_godot']['min'][0]), entry['bounds_godot']['max'][0]), flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
build()
|
||||||
386
assets-source/luneraria/g2/build_bossgrotte.py
Normal file
|
|
@ -0,0 +1,386 @@
|
||||||
|
"""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()
|
||||||
498
assets-source/luneraria/g2/build_schlundkrone.py
Normal file
|
|
@ -0,0 +1,498 @@
|
||||||
|
"""The Schlundkrone, superboss of Champions of Luneraria (G2): an ancient crystal-bound titan
|
||||||
|
of Luneraria that the old Lunarians chained in the Mondschlund.
|
||||||
|
|
||||||
|
Design (the silhouette carries it, not the glow):
|
||||||
|
* a hunched, broad basalt titan that walks on its knuckles: huge shoulder masses, very long
|
||||||
|
forearms and short pillar legs - no other unit stands like this;
|
||||||
|
* the "Krone": a broken crescent halo of crystal shards that floats behind its low head;
|
||||||
|
* the "Schlund": its chest opens like a maw (two plates with moonstone teeth) around a
|
||||||
|
glowing heart crystal;
|
||||||
|
* crescent horns, a crystal cluster that grows from the left shoulder only, broken bronze
|
||||||
|
shackles of the old Lunarians on both wrists;
|
||||||
|
* lunar veins in the basalt glow in the colour of the team it fights for (violet in the
|
||||||
|
grotto), so the same model reads as neutral boss and as lane reinforcement.
|
||||||
|
The footprint stays compact (the simulation keeps attackers 1.2-1.5 m from its centre).
|
||||||
|
|
||||||
|
Rig: rigid stone parts weighted 1.0 to 22 bones (Root, Hips, Spine, Chest, Neck, Head, Jaw,
|
||||||
|
Crown, Maw.L/R, Shoulder/UpperArm/Forearm/Hand.L/R, Thigh/Shin/Foot.L/R). The bone names used
|
||||||
|
by SkeletalFigure's overlays (Spine, Head, Forearm.L/R) exist. Clips at 30 fps: idle, start,
|
||||||
|
walk, stop, attack_a, attack_b (contact at 0.90 s as SkeletalFigure expects), hit, ability,
|
||||||
|
death, roar (the grotto boss wakes), victory (claim). Animations are presentation only; the
|
||||||
|
rules decide every hit.
|
||||||
|
|
||||||
|
Rest positions are written to UV (x, z) and UV2 (y) so the runtime hide shader does not swim
|
||||||
|
on the skinned mesh. Model units are metres at view scale 1; the game shows it at 1.7.
|
||||||
|
|
||||||
|
Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/g2/build_schlundkrone.py
|
||||||
|
Godot +z is the front (glTF +Z forward, Blender -Y), .L sits at +x.
|
||||||
|
"""
|
||||||
|
import sys, json, hashlib, struct
|
||||||
|
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
|
||||||
|
import bpy, bmesh
|
||||||
|
from mathutils import Matrix, Quaternion
|
||||||
|
|
||||||
|
FPS = 30
|
||||||
|
CONTACT = .90
|
||||||
|
STRIDE = 2.0
|
||||||
|
VIEW_SCALE = 1.7
|
||||||
|
CLIPS = {'idle': (3.0, True), 'start': (.4, False), 'walk': (1.6, True), 'stop': (.4, False), 'attack_a': (1.3, False),
|
||||||
|
'attack_b': (1.3, False), 'hit': (.5, False), 'ability': (1.3, False), 'death': (2.2, False), 'roar': (2.0, False),
|
||||||
|
'victory': (2.0, False)}
|
||||||
|
SIDES = (('L', 1), ('R', -1))
|
||||||
|
|
||||||
|
# Bones in Godot coordinates: head, tail, parent.
|
||||||
|
BONES = {'Root': ((0, 0, 0), (0, .3, 0), None),
|
||||||
|
'Hips': ((0, .98, -.12), (0, 1.2, -.06), 'Root'),
|
||||||
|
'Spine': ((0, 1.2, -.06), (0, 1.4, .04), 'Hips'),
|
||||||
|
'Chest': ((0, 1.4, .04), (0, 1.84, .2), 'Spine'),
|
||||||
|
'Neck': ((0, 1.7, .3), (0, 1.72, .46), 'Chest'),
|
||||||
|
'Head': ((0, 1.72, .46), (0, 1.75, .78), 'Neck'),
|
||||||
|
'Jaw': ((0, 1.64, .52), (0, 1.58, .78), 'Head'),
|
||||||
|
'Crown': ((0, 2.1, -.16), (0, 2.42, -.16), 'Chest')}
|
||||||
|
for _side, _sx in SIDES:
|
||||||
|
BONES['Maw.' + _side] = ((_sx * .28, 1.5, .45), (_sx * .28, 1.22, .47), 'Chest')
|
||||||
|
BONES['Shoulder.' + _side] = ((_sx * .18, 1.78, .08), (_sx * .6, 1.84, .06), 'Chest')
|
||||||
|
BONES['UpperArm.' + _side] = ((_sx * .6, 1.84, .06), (_sx * .82, 1.22, .22), 'Shoulder.' + _side)
|
||||||
|
BONES['Forearm.' + _side] = ((_sx * .82, 1.22, .22), (_sx * .84, .5, .42), 'UpperArm.' + _side)
|
||||||
|
BONES['Hand.' + _side] = ((_sx * .84, .5, .42), (_sx * .84, .16, .5), 'Forearm.' + _side)
|
||||||
|
BONES['Thigh.' + _side] = ((_sx * .3, .98, -.12), (_sx * .38, .52, .04), 'Hips')
|
||||||
|
BONES['Shin.' + _side] = ((_sx * .38, .52, .04), (_sx * .38, .14, -.08), 'Thigh.' + _side)
|
||||||
|
BONES['Foot.' + _side] = ((_sx * .38, .14, -.08), (_sx * .38, .06, .22), 'Shin.' + _side)
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
|
||||||
|
|
||||||
|
def add(ob, bone):
|
||||||
|
vg = ob.vertex_groups.new(name=bone)
|
||||||
|
vg.add(list(range(len(ob.data.vertices))), 1.0, 'REPLACE')
|
||||||
|
parts.append(ob)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def orient(ob, pivot, direction):
|
||||||
|
"""Turns an object built upright at the origin (Godot +y) towards `direction` and moves it
|
||||||
|
to `pivot` (Godot coordinates)."""
|
||||||
|
d = L.G(*direction).normalized()
|
||||||
|
rot = V((0, 0, 1)).rotation_difference(d).to_matrix().to_4x4()
|
||||||
|
ob.data.transform(Matrix.Translation(L.G(*pivot)) @ rot)
|
||||||
|
ob.data.update()
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def rock(name, a, b, r, bone, mat='LN_Boss_Hide', seed=0.0, squash=1.0, subdiv=2, cuts=7, amp=.16):
|
||||||
|
"""Faceted stone segment between Godot points a and b."""
|
||||||
|
a, b = V(a), V(b)
|
||||||
|
half = (b - a).length * .5
|
||||||
|
ob = L.cliff(name, (0, 0, 0), (r, half * 1.08, r * squash), mat, seed=seed, subdiv=subdiv, cuts=cuts, amp=amp)
|
||||||
|
return add(orient(ob, tuple((a + b) * .5), tuple(b - a)), bone)
|
||||||
|
|
||||||
|
|
||||||
|
def mass(name, center, radii, bone, mat='LN_Boss_Hide', seed=0.0, lean=(0.0, 0.0), yaw=0.0, subdiv=2, cuts=7, amp=.16):
|
||||||
|
return add(L.cliff(name, center, radii, mat, seed=seed, yaw=yaw, lean=lean, subdiv=subdiv, cuts=cuts, amp=amp), bone)
|
||||||
|
|
||||||
|
|
||||||
|
def spike(name, base, direction, radius, length, bone, mat='LN_Crystal_Boss', sides=6, seed=0):
|
||||||
|
ob = L.crystal(name, (0, 0, 0), radius, length, length * .22, mat, sides=sides, seed=seed)
|
||||||
|
return add(orient(ob, base, direction), bone)
|
||||||
|
|
||||||
|
|
||||||
|
def claw(name, points, width, bone, mat='LN_Boss_Bone', steps=10):
|
||||||
|
n = len(points)
|
||||||
|
widths = [width * (1 - .85 * k / (n - 1)) for k in range(n)]
|
||||||
|
return add(L.sweep(name, points, widths, [w * .85 for w in widths], mat, up_hint=(0, 1, 0), steps=steps, lens=.35), bone)
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ body
|
||||||
|
def build_body():
|
||||||
|
# Pelvis, belly and the leaning chest block, the back hump.
|
||||||
|
mass('Pelvis', (0, .98, -.1), (.42, .26, .32), 'Hips', seed=1.0)
|
||||||
|
mass('Loin', (0, .84, .14), (.26, .18, .1), 'Hips', seed=2.0)
|
||||||
|
mass('Belly', (0, 1.2, .02), (.38, .25, .3), 'Spine', seed=3.0)
|
||||||
|
mass('ChestMass', (0, 1.6, .12), (.6, .36, .42), 'Chest', seed=4.0, lean=(.3, 0))
|
||||||
|
mass('Hump', (0, 1.88, -.16), (.5, .28, .36), 'Chest', seed=5.0, lean=(-.2, 0))
|
||||||
|
for side, sx in SIDES:
|
||||||
|
mass('Flank.' + side, (sx * .3, 1.72, -.3), (.3, .26, .26), 'Chest', seed=6.0 + sx)
|
||||||
|
# The Schlund: two chest plates with moonstone teeth that open around the heart.
|
||||||
|
mass('MawPlate.' + side, (sx * .16, 1.36, .5), (.15, .21, .075), 'Maw.' + side, seed=8.0 + sx, subdiv=1, cuts=5)
|
||||||
|
for k in range(4):
|
||||||
|
y = 1.23 + k * .085
|
||||||
|
claw('Tooth.%s.%d' % (side, k), [(sx * .045, y, .55), (-sx * .015, y - .02, .57), (-sx * .05, y - .04, .565)], .05, 'Maw.' + side, steps=5)
|
||||||
|
# The heart inside the Schlund and the dark hollow around it.
|
||||||
|
mass('Hollow', (0, 1.37, .38), (.2, .24, .1), 'Chest', mat='LN_Boss_Hide', seed=9.0, subdiv=1, cuts=4)
|
||||||
|
spike('Heart', (0, 1.26, .43), (0, 1, .12), .11, .26, 'Chest', seed=3)
|
||||||
|
# Neck, skull, brow, eyes, crescent horns and the jaw with fangs.
|
||||||
|
mass('NeckMass', (0, 1.72, .36), (.17, .14, .15), 'Neck', seed=10.0)
|
||||||
|
mass('Skull', (0, 1.76, .62), (.19, .16, .22), 'Head', seed=11.0, lean=(.15, 0))
|
||||||
|
mass('Brow', (0, 1.83, .77), (.17, .05, .06), 'Head', mat='LN_Boss_Bone', seed=12.0, subdiv=1, cuts=4)
|
||||||
|
for side, sx in SIDES:
|
||||||
|
spike('Eye.' + side, (sx * .075, 1.775, .795), (sx * .3, .2, 1), .032, .05, 'Head', sides=5, seed=4 + sx)
|
||||||
|
claw('Horn.' + side, [(sx * .13, 1.83, .58), (sx * .34, 2.02, .52), (sx * .43, 2.2, .66), (sx * .32, 2.22, .92)], .16, 'Head', steps=14)
|
||||||
|
claw('Fang.' + side, [(sx * .08, 1.6, .74), (sx * .08, 1.69, .77), (sx * .07, 1.74, .78)], .05, 'Jaw', steps=5)
|
||||||
|
mass('JawMass', (0, 1.62, .66), (.15, .06, .16), 'Jaw', seed=13.0, subdiv=1, cuts=5)
|
||||||
|
# Shoulders: huge pauldron masses; crystals grow from the left one only (and a few from
|
||||||
|
# the right), so the silhouette is asymmetric and unmistakable.
|
||||||
|
for side, sx in SIDES:
|
||||||
|
mass('Pauldron.' + side, (sx * .64, 1.93, .02), (.37, .29, .37), 'Shoulder.' + side, seed=14.0 + sx, lean=(0, -sx * .25))
|
||||||
|
mass('PauldronRim.' + side, (sx * .8, 1.78, .08), (.2, .16, .3), 'Shoulder.' + side, mat='LN_Boss_Bone', seed=15.0 + sx,
|
||||||
|
subdiv=1, cuts=5)
|
||||||
|
for k, (base, direction, length) in enumerate([((.7, 2.1, .0), (.75, .75, -.15), .78), ((.86, 1.98, .12), (1, .35, .15), .5),
|
||||||
|
((.62, 2.08, -.2), (.55, .6, -.6), .55)]):
|
||||||
|
spike('ShoulderCrystal.L.%d' % k, base, direction, .09 - .012 * k, length, 'Shoulder.L', seed=20 + k)
|
||||||
|
spike('ShoulderCrystal.R.0', (-.74, 2.06, -.06), (-.8, .7, -.2), .06, .34, 'Shoulder.R', seed=30)
|
||||||
|
# Arms: upper arm, the massive forearm with crystal spurs and a bronze shackle, the fist
|
||||||
|
# with moonstone claws that the titan walks on.
|
||||||
|
for side, sx in SIDES:
|
||||||
|
sh, el, wr, fi = [V(p) for p in (BONES['UpperArm.' + side][0], BONES['Forearm.' + side][0], BONES['Hand.' + side][0],
|
||||||
|
BONES['Hand.' + side][1])]
|
||||||
|
rock('UpperArm.' + side, sh + V((0, .06, 0)), el, .2, 'UpperArm.' + side, seed=50 + sx)
|
||||||
|
mass('Elbow.' + side, tuple(el + V((sx * .04, 0, -.05))), (.17, .15, .17), 'Forearm.' + side, seed=52 + sx, subdiv=1, cuts=5)
|
||||||
|
rock('Forearm.' + side, el, wr + V((0, -.02, 0)), .25, 'Forearm.' + side, seed=54 + sx, squash=.92)
|
||||||
|
along = (wr - el).normalized()
|
||||||
|
out = V((sx, 0, -.35)).normalized()
|
||||||
|
for k in range(3):
|
||||||
|
base = el + (wr - el) * (.25 + .25 * k) + out * .2
|
||||||
|
spike('ArmSpur.%s.%d' % (side, k), tuple(base), tuple((out * 1.0 - along * .6 + V((0, .3, 0))).normalized()), .05,
|
||||||
|
.32 - .06 * k, 'Forearm.' + side, sides=5, seed=60 + k + (3 if sx < 0 else 0))
|
||||||
|
cuff = L.lathe('Shackle.' + side, [(.255, -.07), (.27, -.055), (.27, .055), (.255, .07), (.235, .07), (.235, -.07)], 10,
|
||||||
|
'LN_Boss_Bronze', cap_top=False, cap_bottom=False)
|
||||||
|
add(orient(cuff, tuple(wr + (el - wr).normalized() * .14), tuple(wr - el)), 'Forearm.' + side)
|
||||||
|
link = L.lathe('ShackleLink.' + side, [(.07, -.03), (.07, .03), (.045, .03), (.045, -.03)], 8, 'LN_Boss_Bronze', cap_top=False,
|
||||||
|
cap_bottom=False)
|
||||||
|
add(orient(link, tuple(wr + (el - wr).normalized() * .14 + V((sx * .3, -.1, -.05))), (1, .2, 0)), 'Forearm.' + side)
|
||||||
|
mass('Fist.' + side, tuple(wr + V((0, -.17, .05))), (.2, .19, .22), 'Hand.' + side, seed=70 + sx)
|
||||||
|
for k in range(3):
|
||||||
|
x = sx * (.1 - .1 * k)
|
||||||
|
claw('Claw.%s.%d' % (side, k), [tuple(fi + V((x, .02, .08))), tuple(fi + V((x * 1.1, -.08, .2))), tuple(fi + V((x * 1.1, -.15, .24)))],
|
||||||
|
.075, 'Hand.' + side, steps=6)
|
||||||
|
# Legs: short stone pillars, knee plates, broad feet with moonstone toes.
|
||||||
|
for side, sx in SIDES:
|
||||||
|
hip, knee, ankle, toe = [V(p) for p in (BONES['Thigh.' + side][0], BONES['Shin.' + side][0], BONES['Foot.' + side][0],
|
||||||
|
BONES['Foot.' + side][1])]
|
||||||
|
rock('Thigh.' + side, hip, knee, .22, 'Thigh.' + side, seed=80 + sx)
|
||||||
|
mass('Knee.' + side, tuple(knee + V((sx * .02, .02, .12))), (.15, .13, .1), 'Thigh.' + side, mat='LN_Boss_Bone', seed=82 + sx,
|
||||||
|
subdiv=1, cuts=5)
|
||||||
|
rock('Shin.' + side, knee, ankle, .19, 'Shin.' + side, seed=84 + sx)
|
||||||
|
mass('FootMass.' + side, (sx * .38, .1, .06), (.2, .1, .27), 'Foot.' + side, seed=86 + sx, subdiv=1, cuts=5)
|
||||||
|
for k in range(3):
|
||||||
|
x = sx * (.38 + (k - 1) * .11)
|
||||||
|
claw('Toe.%s.%d' % (side, k), [(x, .08, .26), (x, .05, .37), (x, .01, .42)], .07, 'Foot.' + side, steps=5)
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ crown
|
||||||
|
def build_crown():
|
||||||
|
"""A broken crescent halo of crystal shards behind the head, bound by an old bronze band."""
|
||||||
|
center = V((0, 2.1, -.16))
|
||||||
|
radius = .66
|
||||||
|
band = []
|
||||||
|
for k in range(15):
|
||||||
|
a = pi * (.1 + .8 * k / 14)
|
||||||
|
band.append(center + V((cos(a) * (radius - .05), sin(a) * (radius - .05), 0)))
|
||||||
|
add(L.sweep('CrownBand', band, [.07] * 5, [.05] * 5, 'LN_Boss_Bronze', up_hint=(0, 0, 1), steps=28), 'Crown')
|
||||||
|
# One shard is missing (the broken crown), the others grow longer towards the apex.
|
||||||
|
for k, a in enumerate([.13, .27, .42, .5, .58, .73]):
|
||||||
|
ang = pi * a
|
||||||
|
direction = V((cos(ang), sin(ang), -.08))
|
||||||
|
length = .3 + .26 * (1 - abs(a - .5) * 2.4)
|
||||||
|
base = center + V((cos(ang) * radius, sin(ang) * radius, 0))
|
||||||
|
spike('CrownShard.%d' % k, tuple(base), tuple(direction), .07 + .025 * (1 - abs(a - .5) * 2), length, 'Crown', seed=90 + k)
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ rig
|
||||||
|
def build_rig():
|
||||||
|
bpy.ops.object.armature_add(enter_editmode=True)
|
||||||
|
rig = bpy.context.object
|
||||||
|
rig.name = 'SchlundkroneRig'
|
||||||
|
ed = rig.data.edit_bones
|
||||||
|
ed.remove(ed[0])
|
||||||
|
for name, (head, tail, parent) in BONES.items():
|
||||||
|
b = ed.new(name)
|
||||||
|
b.head, b.tail = L.G(*head), L.G(*tail)
|
||||||
|
if parent:
|
||||||
|
b.parent = ed[parent]
|
||||||
|
bpy.ops.object.mode_set(mode='OBJECT')
|
||||||
|
return rig
|
||||||
|
|
||||||
|
|
||||||
|
def pose_rotation(pb, r):
|
||||||
|
"""Rotation r = (x, y, z) about the armature axes (Blender), turned into the bone's frame."""
|
||||||
|
q = Quaternion((0, 0, 1), r[2]) @ Quaternion((0, 1, 0), r[1]) @ Quaternion((1, 0, 0), r[0])
|
||||||
|
basis = pb.bone.matrix_local.to_quaternion()
|
||||||
|
return basis.inverted() @ q @ basis
|
||||||
|
|
||||||
|
|
||||||
|
def curve(t, keys):
|
||||||
|
for (a, x), (b, y) in zip(keys, keys[1:]):
|
||||||
|
if t <= b:
|
||||||
|
u = max(0.0, (t - a) / (b - a))
|
||||||
|
u = u * u * (3 - 2 * u)
|
||||||
|
return x * (1 - u) + y * u
|
||||||
|
return keys[-1][1]
|
||||||
|
|
||||||
|
|
||||||
|
# ================================================================ clips
|
||||||
|
def animate(name, t, dur):
|
||||||
|
"""Returns rotations (armature axes: x pitch, y roll, z yaw), locations (Blender axes, z up,
|
||||||
|
+y backwards) and scales per bone. Upward bones lean forward with +x; hanging limbs swing
|
||||||
|
forward with -x; the left arm spreads outward with -y, the right arm with +y."""
|
||||||
|
rs, locs, scl = {}, {}, {}
|
||||||
|
|
||||||
|
def rot(b, x=0.0, y=0.0, z=0.0):
|
||||||
|
o = rs.get(b, (0, 0, 0))
|
||||||
|
rs[b] = (o[0] + x, o[1] + y, o[2] + z)
|
||||||
|
|
||||||
|
def loc(b, x=0.0, y=0.0, z=0.0):
|
||||||
|
o = locs.get(b, (0, 0, 0))
|
||||||
|
locs[b] = (o[0] + x, o[1] + y, o[2] + z)
|
||||||
|
|
||||||
|
def spread(side, amount):
|
||||||
|
rot('UpperArm.' + side, 0, -amount if side == 'L' else amount, 0)
|
||||||
|
|
||||||
|
def maw(amount):
|
||||||
|
rot('Maw.L', 0, 0, .95 * amount)
|
||||||
|
rot('Maw.R', 0, 0, -.95 * amount)
|
||||||
|
|
||||||
|
# The crown always hovers a little behind the head.
|
||||||
|
hover = sin(2 * pi * t / 1.5)
|
||||||
|
loc('Crown', 0, 0, .035 * hover)
|
||||||
|
rot('Crown', 0, .03 * sin(2 * pi * t / 3.0), 0)
|
||||||
|
if name == 'idle':
|
||||||
|
b = sin(2 * pi * t / dur)
|
||||||
|
rot('Chest', .03 * b)
|
||||||
|
rot('Spine', -.012 * b)
|
||||||
|
rot('Head', -.02 * b, 0, .14 * sin(2 * pi * t / dur + .6))
|
||||||
|
rot('Neck', 0, 0, .05 * sin(2 * pi * t / dur + .6))
|
||||||
|
rot('UpperArm.L', .035 * b)
|
||||||
|
rot('UpperArm.R', .035 * sin(2 * pi * t / dur + 1.1))
|
||||||
|
maw(.05 + .05 * b)
|
||||||
|
rot('Jaw', .04 + .04 * b)
|
||||||
|
loc('Hips', 0, 0, -.012 * b)
|
||||||
|
elif name in ('walk', 'start', 'stop'):
|
||||||
|
fade = 1.0 if name == 'walk' else (sin(pi * .5 * t / dur) if name == 'start' else cos(pi * .5 * t / dur))
|
||||||
|
ph = 2 * pi * t / CLIPS['walk'][0]
|
||||||
|
for side, sx in SIDES:
|
||||||
|
q = ph + (0 if side == 'L' else pi)
|
||||||
|
thigh = -.36 * sin(q) * fade
|
||||||
|
knee = (.5 * max(0.0, cos(q)) + .05) * fade
|
||||||
|
rot('Thigh.' + side, thigh)
|
||||||
|
rot('Shin.' + side, knee)
|
||||||
|
rot('Foot.' + side, -(thigh + knee) * .6)
|
||||||
|
# Knuckle walk: the arm swings against its leg, the forearm folds on the swing.
|
||||||
|
rot('UpperArm.' + side, .32 * sin(q) * fade)
|
||||||
|
rot('Forearm.' + side, -.22 * max(0.0, -sin(q)) * fade)
|
||||||
|
loc('Hips', 0, 0, -.05 * (1 - cos(2 * ph)) * .5 * fade)
|
||||||
|
rot('Hips', 0, 0, .06 * sin(ph) * fade)
|
||||||
|
rot('Spine', .04 * fade, .04 * sin(ph) * fade, 0)
|
||||||
|
rot('Chest', 0, 0, -.06 * sin(ph) * fade)
|
||||||
|
rot('Head', 0, 0, .05 * sin(ph) * fade)
|
||||||
|
loc('Crown', 0, 0, .03 * sin(2 * ph - .8) * fade)
|
||||||
|
rot('Crown', 0, .05 * sin(ph - .6) * fade, 0)
|
||||||
|
elif name == 'attack_a':
|
||||||
|
# Double fist smash: both arms rise overhead, then slam down at contact (0.90 s).
|
||||||
|
arm = curve(t, [(0, 0), (.55, -2.3), (.8, -2.45), (.9, -.95), (1.05, -.95), (1.3, 0)])
|
||||||
|
fore = curve(t, [(0, 0), (.55, -.6), (.8, -.7), (.9, -.1), (1.05, -.1), (1.3, 0)])
|
||||||
|
chest = curve(t, [(0, 0), (.55, -.28), (.8, -.32), (.9, .42), (1.05, .42), (1.3, 0)])
|
||||||
|
crouch = curve(t, [(0, 0), (.55, .04), (.85, 0), (.9, -.11), (1.05, -.11), (1.3, 0)])
|
||||||
|
for side, sx in SIDES:
|
||||||
|
rot('UpperArm.' + side, arm)
|
||||||
|
rot('Forearm.' + side, fore)
|
||||||
|
spread(side, -.12 * min(0.0, arm) / 2.3)
|
||||||
|
rot('Thigh.' + side, -1.2 * crouch)
|
||||||
|
rot('Shin.' + side, 2.0 * crouch)
|
||||||
|
rot('Foot.' + side, -.8 * crouch)
|
||||||
|
rot('Chest', chest)
|
||||||
|
rot('Spine', chest * .45)
|
||||||
|
rot('Head', -chest * .5)
|
||||||
|
loc('Hips', 0, 0, crouch)
|
||||||
|
maw(.35 * max(0.0, -chest) / .32)
|
||||||
|
loc('Crown', 0, 0, .1 * max(0.0, -chest) / .32)
|
||||||
|
elif name == 'attack_b':
|
||||||
|
# Backhand sweep with the right arm: winds up across the body, strikes at 0.90 s.
|
||||||
|
wind = curve(t, [(0, 0), (.6, 1), (.82, 1.05), (.9, -1), (1.05, -1.05), (1.3, 0)])
|
||||||
|
rot('Chest', 0, 0, .45 * wind)
|
||||||
|
rot('Spine', 0, 0, .2 * wind)
|
||||||
|
rot('Hips', 0, 0, .08 * wind)
|
||||||
|
rot('Head', 0, 0, -.3 * wind)
|
||||||
|
# Raised to shoulder height, the right arm swings across the body (yaw), then out.
|
||||||
|
rot('UpperArm.R', -1.0 * min(1.0, abs(wind) * 1.6), 0, .9 * max(0.0, wind) - 1.0 * max(0.0, -wind))
|
||||||
|
rot('Forearm.R', -.55 * max(0.0, wind) - .15 * max(0.0, -wind))
|
||||||
|
rot('UpperArm.L', .2 * wind)
|
||||||
|
rot('Chest', .1 * abs(wind))
|
||||||
|
elif name == 'hit':
|
||||||
|
q = curve(t, [(0, 0), (.066667, 1), (.2, .4), (.5, 0)])
|
||||||
|
rot('Chest', -.18 * q)
|
||||||
|
rot('Head', -.2 * q, 0, .1 * q)
|
||||||
|
loc('Hips', 0, .05 * q, 0)
|
||||||
|
loc('Crown', 0, .04 * q, .06 * q)
|
||||||
|
rot('UpperArm.L', .15 * q)
|
||||||
|
rot('UpperArm.R', .15 * q)
|
||||||
|
maw(-.04 * q)
|
||||||
|
elif name == 'ability':
|
||||||
|
# "Mondschrei": the Schlund tears open, arms spread, the crown rises and flares.
|
||||||
|
o = curve(t, [(0, 0), (.5, 1), (.9, 1.15), (1.1, 1), (1.3, 0)])
|
||||||
|
maw(.9 * o)
|
||||||
|
spread('L', .7 * o)
|
||||||
|
spread('R', .7 * o)
|
||||||
|
rot('UpperArm.L', -.5 * o)
|
||||||
|
rot('UpperArm.R', -.5 * o)
|
||||||
|
rot('Chest', -.3 * o)
|
||||||
|
rot('Head', -.4 * o)
|
||||||
|
rot('Jaw', .5 * o)
|
||||||
|
loc('Crown', 0, 0, .18 * o)
|
||||||
|
scl['Crown'] = tuple([1 + .25 * o] * 3)
|
||||||
|
loc('Hips', 0, 0, .05 * o)
|
||||||
|
elif name == 'death':
|
||||||
|
stagger = curve(t, [(0, 0), (.35, 1), (.6, .6), (1.0, 0)])
|
||||||
|
buckle = curve(t, [(0, 0), (.5, 0), (1.1, 1), (2.2, 1)])
|
||||||
|
fall = curve(t, [(0, 0), (.9, 0), (1.6, 1), (1.8, .95), (2.2, 1)])
|
||||||
|
crown = curve(t, [(0, 0), (.3, 0), (1.25, 1), (1.4, .96), (2.2, 1)])
|
||||||
|
rot('Chest', -.25 * stagger + .2 * fall)
|
||||||
|
loc('Hips', 0, .1 * stagger, -.42 * buckle)
|
||||||
|
for side, sx in SIDES:
|
||||||
|
rot('Thigh.' + side, -.9 * buckle)
|
||||||
|
rot('Shin.' + side, 1.5 * buckle)
|
||||||
|
rot('Foot.' + side, -.6 * buckle)
|
||||||
|
rot('UpperArm.' + side, -.6 * fall + .2 * stagger)
|
||||||
|
spread(side, .25 * fall)
|
||||||
|
rot('Root', 1.1 * fall)
|
||||||
|
loc('Root', 0, 0, -.08 * fall)
|
||||||
|
rot('Head', .3 * fall, 0, .3 * fall)
|
||||||
|
maw(-.05 * fall)
|
||||||
|
# The crown breaks away and drops behind the titan.
|
||||||
|
loc('Crown', 0, .55 * crown, -1.55 * crown)
|
||||||
|
rot('Crown', 1.1 * crown, .6 * crown, 0)
|
||||||
|
elif name == 'roar':
|
||||||
|
rear = curve(t, [(0, 0), (.6, 1), (1.4, 1), (1.6, -.3), (1.75, -.3), (2.0, 0)])
|
||||||
|
up = max(0.0, rear)
|
||||||
|
rot('Spine', -.35 * up + .2 * max(0.0, -rear))
|
||||||
|
rot('Chest', -.25 * rear)
|
||||||
|
for side, sx in SIDES:
|
||||||
|
rot('UpperArm.' + side, -1.4 * up - .8 * max(0.0, -rear) / .3)
|
||||||
|
spread(side, .6 * up)
|
||||||
|
rot('Forearm.' + side, -.4 * up)
|
||||||
|
rot('Head', -.5 * up)
|
||||||
|
rot('Jaw', .6 * up)
|
||||||
|
maw(.8 * up)
|
||||||
|
loc('Crown', 0, 0, .2 * up)
|
||||||
|
scl['Crown'] = tuple([1 + .3 * up] * 3)
|
||||||
|
loc('Hips', 0, 0, .08 * up)
|
||||||
|
elif name == 'victory':
|
||||||
|
def pulse(center):
|
||||||
|
return max(0.0, 1 - abs(t - center) / .22)
|
||||||
|
left = pulse(.5) + pulse(1.3)
|
||||||
|
right = pulse(.9)
|
||||||
|
for side, beat in (('L', left), ('R', right)):
|
||||||
|
# Fist to the chest: raise forward, fold the forearm, turn inwards (yaw).
|
||||||
|
rot('UpperArm.' + side, -1.15 * beat, 0, (-.55 if side == 'L' else .55) * beat)
|
||||||
|
rot('Forearm.' + side, -1.3 * beat)
|
||||||
|
rot('Head', -.3 * curve(t, [(0, 0), (.3, 1), (1.7, 1), (2.0, 0)]))
|
||||||
|
rot('Jaw', .4 * curve(t, [(0, 0), (.3, 1), (1.7, 1), (2.0, 0)]))
|
||||||
|
loc('Crown', 0, 0, .1 * (left + right))
|
||||||
|
rot('Chest', -.12 * curve(t, [(0, 0), (.3, 1), (1.7, 1), (2.0, 0)]))
|
||||||
|
return rs, locs, scl
|
||||||
|
|
||||||
|
|
||||||
|
def bake(rig):
|
||||||
|
for name, (dur, loop) in CLIPS.items():
|
||||||
|
rig.animation_data_clear()
|
||||||
|
rig.animation_data_create()
|
||||||
|
action = bpy.data.actions.new(name)
|
||||||
|
action.use_fake_user = True
|
||||||
|
rig.animation_data.action = action
|
||||||
|
for frame in range(round(dur * FPS) + 1):
|
||||||
|
rs, locs, scl = animate(name, frame / FPS, dur)
|
||||||
|
for pb in rig.pose.bones:
|
||||||
|
pb.rotation_mode = 'QUATERNION'
|
||||||
|
pb.rotation_quaternion = pose_rotation(pb, rs.get(pb.name, (0, 0, 0)))
|
||||||
|
pb.location = pb.bone.matrix_local.to_3x3().inverted() @ V(locs.get(pb.name, (0, 0, 0)))
|
||||||
|
pb.scale = scl.get(pb.name, (1, 1, 1))
|
||||||
|
pb.keyframe_insert('rotation_quaternion', frame=frame)
|
||||||
|
pb.keyframe_insert('location', frame=frame)
|
||||||
|
if pb.name == 'Crown':
|
||||||
|
pb.keyframe_insert('scale', frame=frame)
|
||||||
|
action['loop'] = loop
|
||||||
|
action['contact_seconds'] = CONTACT if name in ('attack_a', 'attack_b') else -1
|
||||||
|
for layer in action.layers:
|
||||||
|
for strip in layer.strips:
|
||||||
|
for bag in strip.channelbags:
|
||||||
|
for fc in bag.fcurves:
|
||||||
|
for key in fc.keyframe_points:
|
||||||
|
key.interpolation = 'LINEAR'
|
||||||
|
rig.animation_data_clear()
|
||||||
|
rig.animation_data_create()
|
||||||
|
rig.animation_data.action = bpy.data.actions['idle']
|
||||||
|
bpy.context.scene.render.fps = FPS
|
||||||
|
bpy.context.scene.frame_set(0)
|
||||||
|
|
||||||
|
|
||||||
|
def rest_uvs(ob):
|
||||||
|
"""UV = rest (x, z), UV2 = (rest y, 0) in Godot coordinates for the hide shader."""
|
||||||
|
me = ob.data
|
||||||
|
while me.uv_layers:
|
||||||
|
me.uv_layers.remove(me.uv_layers[0])
|
||||||
|
first = me.uv_layers.new(name='UVMap')
|
||||||
|
second = me.uv_layers.new(name='UVRest')
|
||||||
|
for loop in me.loops:
|
||||||
|
g = L.to_godot(me.vertices[loop.vertex_index].co)
|
||||||
|
first.data[loop.index].uv = (g[0], g[2])
|
||||||
|
second.data[loop.index].uv = (g[1], 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def build():
|
||||||
|
L.reset()
|
||||||
|
build_body()
|
||||||
|
build_crown()
|
||||||
|
rocks = [ob for ob in parts if not any(m.name.startswith(('LN_Crystal', 'LN_Boss_Bronze')) for m in ob.data.materials)]
|
||||||
|
rest = [ob for ob in parts if ob not in rocks]
|
||||||
|
L.finalize(rocks, sharp_angle=16.0, occluders=parts, reach=.45, ground=0.0, rays=20)
|
||||||
|
L.finalize(rest, occluders=parts, reach=.45, ground=0.0, rays=20)
|
||||||
|
for ob in parts:
|
||||||
|
L.triangulate(ob)
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for ob in parts:
|
||||||
|
ob.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = parts[0]
|
||||||
|
bpy.ops.object.join()
|
||||||
|
body = bpy.context.view_layer.objects.active
|
||||||
|
body.name = body.data.name = 'SchlundkroneBody'
|
||||||
|
rest_uvs(body)
|
||||||
|
rig = build_rig()
|
||||||
|
mod = body.modifiers.new('Rig', 'ARMATURE')
|
||||||
|
mod.object = rig
|
||||||
|
body.parent = rig
|
||||||
|
bake(rig)
|
||||||
|
source_count = len(parts)
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(L.SRC / 'schlundkrone.blend'))
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
body.select_set(True)
|
||||||
|
rig.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = rig
|
||||||
|
path = L.OUT / 'schlundkrone.glb'
|
||||||
|
bpy.ops.export_scene.gltf(filepath=str(path), export_format='GLB', use_selection=True, export_animations=True,
|
||||||
|
export_animation_mode='ACTIONS', export_force_sampling=True, export_frame_range=False,
|
||||||
|
export_anim_slide_to_zero=True, export_skins=True, export_all_influences=False,
|
||||||
|
export_def_bones=True, export_anim_single_armature=True, export_materials='EXPORT', export_yup=True,
|
||||||
|
export_vertex_color='ACTIVE', export_texcoords=True, export_normals=True)
|
||||||
|
raw = path.read_bytes()
|
||||||
|
doc = json.loads(raw[20:20 + struct.unpack_from('<I', raw, 12)[0]])
|
||||||
|
bounds = L.bounds_godot([body])
|
||||||
|
clips = sorted(a['name'] for a in doc.get('animations', []))
|
||||||
|
entry = {'id': 'schlundkrone', 'title': 'Schlundkrone (Superboss)', 'resource_path': 'res://assets/luneraria/g2/schlundkrone.glb',
|
||||||
|
'source': 'assets-source/luneraria/g2/schlundkrone.blend', 'generator': 'assets-source/luneraria/g2/build_schlundkrone.py',
|
||||||
|
'triangles': L.glb_triangles(doc), 'source_parts': source_count, 'skeletal': True, 'bones': len(BONES),
|
||||||
|
'bone_names': list(BONES), 'clips': clips, 'clip_seconds': {k: v[0] for k, v in CLIPS.items()},
|
||||||
|
'attack_cycle': ['attack_a', 'attack_b'], 'contact_seconds': CONTACT, 'stride': STRIDE, 'view_scale': VIEW_SCALE,
|
||||||
|
'materials': sorted(m['name'] for m in doc.get('materials', [])), 'skins': len(doc.get('skins', [])),
|
||||||
|
'bounds_godot': bounds, 'pivot': 'ground centre (0, 0, 0), faces +z', 'units': 'metres at view scale 1',
|
||||||
|
'sha256': hashlib.sha256(raw).hexdigest()}
|
||||||
|
L.write_manifest([entry])
|
||||||
|
missing = [c for c in CLIPS if c not in clips]
|
||||||
|
if missing:
|
||||||
|
raise SystemExit('SCHLUNDKRONE_MISSING_CLIPS: %s' % missing)
|
||||||
|
print('SCHLUNDKRONE_BUILD', entry['triangles'], 'triangles', len(BONES), 'bones', 'clips', clips, 'height %.2f' % bounds['max'][1],
|
||||||
|
flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
build()
|
||||||
444
assets-source/luneraria/g2/build_towers.py
Normal file
|
|
@ -0,0 +1,444 @@
|
||||||
|
"""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)
|
||||||
BIN
assets-source/luneraria/g2/grenzwacht.blend
Normal file
1
assets-source/luneraria/g2/grotto_site.json
Normal file
BIN
assets-source/luneraria/g2/kernwacht.blend
Normal file
662
assets-source/luneraria/g2/lune_bpy.py
Normal file
|
|
@ -0,0 +1,662 @@
|
||||||
|
"""Shared Blender helpers for the G2 strategic buildings of Champions of Luneraria.
|
||||||
|
|
||||||
|
Every G2 generator (Ancient, towers, boss grotto, Schlundkrone) builds its geometry from
|
||||||
|
these primitives, so the pieces share one design language: bevelled hard-surface stone and
|
||||||
|
metal, faceted crystals, rune bands with UVs for the glyph shader and rock masses from
|
||||||
|
noise-displaced spheres. After building, `finalize` sets split normals by angle and bakes an
|
||||||
|
ambient-occlusion term into the vertex colour `Col` (exported as COLOR_0); the Godot shaders
|
||||||
|
multiply it into stone and metal and use it for wear.
|
||||||
|
|
||||||
|
Material names (LN_*) are the contract with scripts/luneraria/view/lune_materials.gd; the
|
||||||
|
Blender colours only serve the .blend preview. Godot coordinates are written through
|
||||||
|
G(x, y, z) (x right, y up, z towards the viewer); Blender is Z-up.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy, bmesh, json, hashlib, struct
|
||||||
|
from math import sin, cos, pi, sqrt, atan2
|
||||||
|
from mathutils import Vector, Matrix, noise
|
||||||
|
from mathutils.bvhtree import BVHTree
|
||||||
|
|
||||||
|
P = Path(__file__).resolve().parents[3]
|
||||||
|
SRC = Path(__file__).resolve().parent
|
||||||
|
OUT = P / 'assets/luneraria/g2'
|
||||||
|
V = Vector
|
||||||
|
|
||||||
|
|
||||||
|
def reset():
|
||||||
|
_materials.clear()
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
bpy.context.preferences.filepaths.save_version = 0
|
||||||
|
bpy.context.scene.render.fps = 30
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def G(x, y, z):
|
||||||
|
return V((x, -z, y))
|
||||||
|
|
||||||
|
|
||||||
|
def to_godot(v):
|
||||||
|
return (v.x, v.z, -v.y)
|
||||||
|
|
||||||
|
|
||||||
|
def lin(c):
|
||||||
|
return tuple(x ** 2.2 for x in c)
|
||||||
|
|
||||||
|
|
||||||
|
# Display colour (sRGB), roughness, metallic, emission strength.
|
||||||
|
PALETTE = {
|
||||||
|
'LN_Stone_Pale': ((.80, .76, .68), .88, 0.0, 0.0),
|
||||||
|
'LN_Stone_Dark': ((.26, .26, .29), .9, 0.0, 0.0),
|
||||||
|
'LN_Stone_Grotto': ((.46, .43, .47), .92, 0.0, 0.0),
|
||||||
|
'LN_Rock': ((.40, .39, .37), .95, 0.0, 0.0),
|
||||||
|
'LN_Metal_Team': ((.74, .76, .80), .34, .9, 0.0),
|
||||||
|
'LN_Metal_Iron': ((.22, .22, .24), .55, .8, 0.0),
|
||||||
|
'LN_Metal_Bronze': ((.62, .44, .22), .42, .9, 0.0),
|
||||||
|
'LN_Crystal_Team': ((.55, .80, 1.0), .08, 0.0, 3.0),
|
||||||
|
'LN_Crystal_Grotto': ((.70, .50, 1.0), .08, 0.0, 3.0),
|
||||||
|
'LN_Rune_Team': ((.55, .80, 1.0), .5, 0.0, 2.0),
|
||||||
|
'LN_Rune_Grotto': ((.70, .50, 1.0), .5, 0.0, 2.0),
|
||||||
|
'LN_Cloth_Team': ((.20, .40, .80), .85, 0.0, 0.0),
|
||||||
|
'LN_Wood': ((.36, .25, .16), .8, 0.0, 0.0),
|
||||||
|
'LN_Cave': ((.03, .02, .05), 1.0, 0.0, 0.0),
|
||||||
|
'LN_Water_Glow': ((.45, .35, .80), .1, 0.0, 1.2),
|
||||||
|
'LN_Boss_Hide': ((.24, .25, .29), .84, 0.0, 0.0),
|
||||||
|
'LN_Boss_Bone': ((.80, .77, .69), .62, 0.0, 0.0),
|
||||||
|
'LN_Boss_Bronze': ((.60, .44, .24), .42, .85, 0.0),
|
||||||
|
'LN_Crystal_Boss': ((.70, .50, 1.0), .08, 0.0, 3.0),
|
||||||
|
}
|
||||||
|
_materials = {}
|
||||||
|
|
||||||
|
|
||||||
|
def material(name):
|
||||||
|
if name in _materials:
|
||||||
|
return _materials[name]
|
||||||
|
color, rough, metal, emission = PALETTE[name]
|
||||||
|
m = bpy.data.materials.new(name)
|
||||||
|
c = lin(color)
|
||||||
|
m.diffuse_color = (*c, 1)
|
||||||
|
m.use_nodes = True
|
||||||
|
nodes, links = m.node_tree.nodes, m.node_tree.links
|
||||||
|
bsdf = nodes.get('Principled BSDF')
|
||||||
|
bsdf.inputs['Roughness'].default_value = rough
|
||||||
|
bsdf.inputs['Metallic'].default_value = metal
|
||||||
|
# Baked occlusion (vertex colour Col) darkens the preview like it does in Godot.
|
||||||
|
attribute = nodes.new('ShaderNodeVertexColor')
|
||||||
|
attribute.layer_name = 'Col'
|
||||||
|
mix = nodes.new('ShaderNodeMix')
|
||||||
|
mix.data_type = 'RGBA'
|
||||||
|
mix.blend_type = 'MULTIPLY'
|
||||||
|
mix.inputs['Factor'].default_value = 1.0
|
||||||
|
mix.inputs['A'].default_value = (*c, 1)
|
||||||
|
links.new(attribute.outputs['Color'], mix.inputs['B'])
|
||||||
|
links.new(mix.outputs['Result'], bsdf.inputs['Base Color'])
|
||||||
|
if emission:
|
||||||
|
bsdf.inputs['Emission Color'].default_value = (*c, 1)
|
||||||
|
bsdf.inputs['Emission Strength'].default_value = emission
|
||||||
|
_materials[name] = m
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- objects
|
||||||
|
def make(name, verts, faces, mat, uvs=None, smooth=False):
|
||||||
|
"""Object from Godot-space vertices (tuples or Vectors) and faces; uvs per vertex."""
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
me.from_pydata([tuple(G(*v)) for v in verts], [], faces)
|
||||||
|
me.update()
|
||||||
|
me.materials.append(material(mat))
|
||||||
|
if uvs is not None:
|
||||||
|
layer = me.uv_layers.new(name='UVMap')
|
||||||
|
for poly in me.polygons:
|
||||||
|
for li in poly.loop_indices:
|
||||||
|
layer.data[li].uv = uvs[me.loops[li].vertex_index]
|
||||||
|
for poly in me.polygons:
|
||||||
|
poly.use_smooth = smooth
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def bevel(ob, offset=.03, segments=2, angle=35.0, profile=.5):
|
||||||
|
"""Bevels every edge sharper than `angle` degrees (hard-surface highlight edges)."""
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(ob.data)
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=1e-6)
|
||||||
|
bm.normal_update()
|
||||||
|
limit = angle * pi / 180.0
|
||||||
|
edges = [e for e in bm.edges if len(e.link_faces) == 2 and e.calc_face_angle(0.0) > limit]
|
||||||
|
if edges:
|
||||||
|
bmesh.ops.bevel(bm, geom=edges, offset=offset, offset_type='OFFSET', segments=segments, profile=profile,
|
||||||
|
affect='EDGES', clamp_overlap=True)
|
||||||
|
bm.to_mesh(ob.data)
|
||||||
|
bm.free()
|
||||||
|
ob.data.update()
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def join(objects, name, origin=(0, 0, 0)):
|
||||||
|
"""Joins objects into one named object whose origin sits at the Godot point `origin`."""
|
||||||
|
objects = [o for o in objects if o is not None]
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for o in objects:
|
||||||
|
o.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = objects[0]
|
||||||
|
if len(objects) > 1:
|
||||||
|
bpy.ops.object.join()
|
||||||
|
ob = bpy.context.view_layer.objects.active
|
||||||
|
ob.name = name
|
||||||
|
ob.data.name = name
|
||||||
|
pivot = G(*origin)
|
||||||
|
ob.data.transform(Matrix.Translation(-pivot))
|
||||||
|
ob.location = pivot
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- primitives
|
||||||
|
def lathe(name, profile, sides, mat, center=(0, 0, 0), rotation=0.0, cap_top=True, cap_bottom=True, jitter=0.0, seed=0.0,
|
||||||
|
smooth=False, uv_band=None):
|
||||||
|
"""Revolves a (radius, height) profile. uv_band=(cell_length) gives UVs u along the
|
||||||
|
circumference in cells and v along the profile (0..1) for rune bands."""
|
||||||
|
cx, cy, cz = center
|
||||||
|
verts, faces, uvs = [], [], []
|
||||||
|
n = len(profile)
|
||||||
|
total = 0.0
|
||||||
|
lengths = [0.0]
|
||||||
|
for j in range(1, n):
|
||||||
|
total += ((profile[j][0] - profile[j - 1][0]) ** 2 + (profile[j][1] - profile[j - 1][1]) ** 2) ** .5
|
||||||
|
lengths.append(total)
|
||||||
|
for j, (r, y) in enumerate(profile):
|
||||||
|
for k in range(sides + (1 if uv_band else 0)):
|
||||||
|
a = 2 * pi * (k % sides) / sides + rotation
|
||||||
|
rr = r * (1 + jitter * noise.noise(V((cos(a) * 2.3, y * 1.7, sin(a) * 2.3 + seed)))) if jitter else r
|
||||||
|
verts.append((cx + cos(a) * rr, cy + y, cz + sin(a) * rr))
|
||||||
|
if uv_band:
|
||||||
|
circumference = 2 * pi * max(r, .01)
|
||||||
|
cells = max(1, round(circumference / uv_band))
|
||||||
|
uvs.append((cells * k / sides, lengths[j] / max(total, 1e-6)))
|
||||||
|
ring = sides + (1 if uv_band else 0)
|
||||||
|
for j in range(n - 1):
|
||||||
|
for k in range(sides):
|
||||||
|
a0, b0 = j * ring + k, j * ring + (k + 1) % ring if not uv_band else j * ring + k + 1
|
||||||
|
faces.append((a0, b0, b0 + ring, a0 + ring))
|
||||||
|
if cap_bottom:
|
||||||
|
faces.append(tuple(reversed(range(sides))))
|
||||||
|
if cap_top:
|
||||||
|
faces.append(tuple(range((n - 1) * ring, (n - 1) * ring + sides)))
|
||||||
|
ob = make(name, verts, faces, mat, uvs if uv_band else None, smooth)
|
||||||
|
# Faces must point outwards whatever the profile direction was.
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(ob.data)
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=1e-6)
|
||||||
|
bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
|
||||||
|
bm.to_mesh(ob.data)
|
||||||
|
bm.free()
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def box(name, center, size, mat, yaw=0.0, pitch=0.0, roll=0.0, bev=.025, segments=1):
|
||||||
|
hx, hy, hz = size[0] / 2, size[1] / 2, size[2] / 2
|
||||||
|
rot = Matrix.Rotation(yaw, 3, 'Y') @ Matrix.Rotation(pitch, 3, 'X') @ Matrix.Rotation(roll, 3, 'Z')
|
||||||
|
c = V(center)
|
||||||
|
verts = []
|
||||||
|
for dy in (-1, 1):
|
||||||
|
for dz in (-1, 1):
|
||||||
|
for dx in (-1, 1):
|
||||||
|
verts.append(tuple(c + rot @ V((dx * hx, dy * hy, dz * hz))))
|
||||||
|
faces = [(0, 1, 3, 2), (4, 6, 7, 5), (0, 4, 5, 1), (2, 3, 7, 6), (0, 2, 6, 4), (1, 5, 7, 3)]
|
||||||
|
ob = make(name, verts, faces, mat)
|
||||||
|
_outward(ob)
|
||||||
|
if bev > 0:
|
||||||
|
bevel(ob, bev, segments, 30.0)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def _outward(ob):
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(ob.data)
|
||||||
|
bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
|
||||||
|
bm.to_mesh(ob.data)
|
||||||
|
bm.free()
|
||||||
|
|
||||||
|
|
||||||
|
def prism(name, outline, y0, y1, mat, bev=.02, segments=1, top_scale=1.0):
|
||||||
|
"""Extrudes a closed Godot-xz outline from y0 to y1 (top optionally scaled around its centre)."""
|
||||||
|
n = len(outline)
|
||||||
|
cx = sum(p[0] for p in outline) / n
|
||||||
|
cz = sum(p[1] for p in outline) / n
|
||||||
|
verts = [(x, y0, z) for x, z in outline]
|
||||||
|
verts += [(cx + (x - cx) * top_scale, y1, cz + (z - cz) * top_scale) for x, z in outline]
|
||||||
|
faces = [(i, (i + 1) % n, n + (i + 1) % n, n + i) for i in range(n)]
|
||||||
|
faces += [tuple(reversed(range(n))), tuple(range(n, 2 * n))]
|
||||||
|
ob = make(name, verts, faces, mat)
|
||||||
|
_outward(ob)
|
||||||
|
if bev > 0:
|
||||||
|
bevel(ob, bev, segments, 30.0)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def sweep(name, points, widths, thicknesses, mat, up_hint=(0, 1, 0), steps=24, lens=0.0, closed_ends=True, uv_cells=None,
|
||||||
|
smooth=True, twist=0.0):
|
||||||
|
"""Sweeps a rectangle (optionally lens-shaped) along a Catmull-Rom path through Godot points.
|
||||||
|
widths/thicknesses are sampled along the path. uv_cells gives rune UVs (u along, v across)."""
|
||||||
|
pts = [V(p) for p in points]
|
||||||
|
|
||||||
|
def cat(t):
|
||||||
|
seg = min(len(pts) - 2, int(t * (len(pts) - 1)))
|
||||||
|
u = t * (len(pts) - 1) - seg
|
||||||
|
a, b, c, d = [pts[max(0, min(len(pts) - 1, q))] for q in (seg - 1, seg, seg + 1, seg + 2)]
|
||||||
|
return .5 * ((2 * b) + (-a + c) * u + (2 * a - 5 * b + 4 * c - d) * u * u + (-a + 3 * b - 3 * c + d) * u * u * u)
|
||||||
|
|
||||||
|
def sample(values, t):
|
||||||
|
f = t * (len(values) - 1)
|
||||||
|
i = min(len(values) - 2, int(f))
|
||||||
|
return values[i] * (1 - (f - i)) + values[i + 1] * (f - i)
|
||||||
|
|
||||||
|
# Rectangle corners with optional lens bulge: 8 points around the section.
|
||||||
|
section = [(-1, -1), (0, -1 - lens), (1, -1), (1, 0), (1, 1), (0, 1 + lens), (-1, 1), (-1, 0)]
|
||||||
|
verts, faces, uvs = [], [], []
|
||||||
|
along = 0.0
|
||||||
|
previous = None
|
||||||
|
for j in range(steps + 1):
|
||||||
|
t = j / steps
|
||||||
|
p = cat(t)
|
||||||
|
tangent = (cat(min(1, t + .01)) - cat(max(0, t - .01))).normalized()
|
||||||
|
hint = V(up_hint)
|
||||||
|
side = tangent.cross(hint)
|
||||||
|
if side.length < 1e-4:
|
||||||
|
side = tangent.cross(V((1, 0, 0)))
|
||||||
|
side.normalize()
|
||||||
|
normal = side.cross(tangent).normalized()
|
||||||
|
if twist:
|
||||||
|
ang = twist * t
|
||||||
|
side, normal = side * cos(ang) + normal * sin(ang), normal * cos(ang) - side * sin(ang)
|
||||||
|
if previous is not None:
|
||||||
|
along += (p - previous).length
|
||||||
|
previous = p
|
||||||
|
w, th = sample(widths, t) * .5, sample(thicknesses, t) * .5
|
||||||
|
for k, (sx, sy) in enumerate(section):
|
||||||
|
verts.append(tuple(p + side * sx * w + normal * sy * th))
|
||||||
|
uvs.append(((along / uv_cells) if uv_cells else t, k / 8.0))
|
||||||
|
if j:
|
||||||
|
base = (j - 1) * 8
|
||||||
|
for k in range(8):
|
||||||
|
a0, b0 = base + k, base + (k + 1) % 8
|
||||||
|
faces.append((a0, b0, b0 + 8, a0 + 8))
|
||||||
|
if closed_ends:
|
||||||
|
faces.append(tuple(reversed(range(8))))
|
||||||
|
faces.append(tuple(range(steps * 8, steps * 8 + 8)))
|
||||||
|
ob = make(name, verts, faces, mat, uvs, smooth)
|
||||||
|
_outward(ob)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def crystal(name, center, radius, top, bottom, mat, sides=6, tilt=(0.0, 0.0), twist=0.0, seed=0, shoulder=.62):
|
||||||
|
"""Faceted crystal: a prism with a pointed crown and a shorter pointed foot. Facet radii
|
||||||
|
vary slightly per side so the highlights break up."""
|
||||||
|
c = V(center)
|
||||||
|
rot = Matrix.Rotation(tilt[0], 3, 'X') @ Matrix.Rotation(tilt[1], 3, 'Z')
|
||||||
|
verts = [tuple(c + rot @ V((0, top, 0)))]
|
||||||
|
rings = [(shoulder * top, 1.0), (-shoulder * bottom * .5, 1.0)]
|
||||||
|
for y, scale in rings:
|
||||||
|
for k in range(sides):
|
||||||
|
a = 2 * pi * k / sides + twist
|
||||||
|
rr = radius * scale * (1 + .12 * noise.noise(V((k * 1.7, y, seed))))
|
||||||
|
verts.append(tuple(c + rot @ V((cos(a) * rr, y, sin(a) * rr))))
|
||||||
|
verts.append(tuple(c + rot @ V((0, -bottom, 0))))
|
||||||
|
faces = [(0, 1 + (k + 1) % sides, 1 + k) for k in range(sides)]
|
||||||
|
for k in range(sides):
|
||||||
|
faces.append((1 + k, 1 + (k + 1) % sides, 1 + sides + (k + 1) % sides, 1 + sides + k))
|
||||||
|
last = 1 + 2 * sides
|
||||||
|
faces += [(last, 1 + sides + k, 1 + sides + (k + 1) % sides) for k in range(sides)]
|
||||||
|
ob = make(name, verts, faces, mat)
|
||||||
|
_outward(ob)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def blob(name, center, radii, mat, seed=0.0, subdiv=3, amp=.25, flatten_below=None, yaw=0.0, crag=0.0):
|
||||||
|
"""Rock mass: icosphere displaced by fractal noise (and optional terraced crags)."""
|
||||||
|
bm = bmesh.new()
|
||||||
|
bmesh.ops.create_icosphere(bm, subdivisions=subdiv, radius=1.0)
|
||||||
|
c = V(center)
|
||||||
|
rot = Matrix.Rotation(yaw, 3, 'Y')
|
||||||
|
for v in bm.verts:
|
||||||
|
d = v.co.normalized()
|
||||||
|
n = noise.fractal(d * 1.6 + V((seed, seed * .7, seed * .3)), 1.0, 2.0, 4)
|
||||||
|
r = 1.0 + amp * n
|
||||||
|
if crag:
|
||||||
|
r += crag * abs(noise.noise(d * 3.1 + V((seed * 1.3, 0, 0))))
|
||||||
|
p = V((d.x * radii[0] * r, d.y * radii[1] * r, d.z * radii[2] * r))
|
||||||
|
if flatten_below is not None and p.y < flatten_below:
|
||||||
|
p.y = flatten_below + (p.y - flatten_below) * .15
|
||||||
|
v.co = G(*(c + rot @ p))
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
bm.to_mesh(me)
|
||||||
|
bm.free()
|
||||||
|
me.materials.append(material(mat))
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def chunk(name, center, size, mat, seed=0.0, yaw=0.0, tilt=0.0, sink=.25):
|
||||||
|
"""Broken masonry fragment: a low-poly displaced icosphere with a flat bed, irregular and
|
||||||
|
angular (rubble, debris, ruins). `sink` buries the bottom into the ground."""
|
||||||
|
bm = bmesh.new()
|
||||||
|
bmesh.ops.create_icosphere(bm, subdivisions=1, radius=1.0)
|
||||||
|
c = V(center)
|
||||||
|
rot = Matrix.Rotation(yaw, 3, 'Y') @ Matrix.Rotation(tilt, 3, 'X')
|
||||||
|
sx, sy, sz = size
|
||||||
|
for v in bm.verts:
|
||||||
|
d = v.co.normalized()
|
||||||
|
r = 1.0 + .28 * noise.noise(d * 2.1 + V((seed, seed * 1.9, seed * .4)))
|
||||||
|
p = V((d.x * sx * r, d.y * sy * r, d.z * sz * r))
|
||||||
|
# Flatten faces into cut planes so fragments look split, not rounded.
|
||||||
|
p.x = max(-sx * .82, min(sx * .82, p.x))
|
||||||
|
p.z = max(-sz * .82, min(sz * .82, p.z))
|
||||||
|
p.y = max(-sy * (1 - sink), min(sy * .9, p.y))
|
||||||
|
v.co = G(*(c + rot @ p))
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
bm.to_mesh(me)
|
||||||
|
bm.free()
|
||||||
|
me.materials.append(material(mat))
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def cliff(name, center, radii, mat, seed=0.0, cuts=7, subdiv=2, amp=.16, yaw=0.0, lean=(0.0, 0.0), flat_bottom=None):
|
||||||
|
"""Faceted cliff rock in the low-poly language of the map rocks: a displaced icosphere cut
|
||||||
|
by a few random planes (split faces), leaning optionally. Flat shading comes from
|
||||||
|
finalize()'s angle split. flat_bottom (Godot y, absolute) flattens everything below it."""
|
||||||
|
bm = bmesh.new()
|
||||||
|
bmesh.ops.create_icosphere(bm, subdivisions=subdiv, radius=1.0)
|
||||||
|
rng = seed * 7.31
|
||||||
|
planes = []
|
||||||
|
for k in range(cuts):
|
||||||
|
# Cut normals biased sideways and up, so faces read as cleft rock, not as a gem.
|
||||||
|
a = 2 * pi * ((k * .618 + seed * .17) % 1.0)
|
||||||
|
tilt = .25 + .9 * ((k * .377 + seed * .29) % 1.0)
|
||||||
|
n = V((cos(a) * sin(tilt * 1.2), cos(tilt * 1.2) * .6, sin(a) * sin(tilt * 1.2))).normalized()
|
||||||
|
planes.append((n, .6 + .26 * ((k * .731 + rng) % 1.0)))
|
||||||
|
c = V(center)
|
||||||
|
rot = Matrix.Rotation(yaw, 3, 'Y') @ Matrix.Rotation(lean[0], 3, 'X') @ Matrix.Rotation(lean[1], 3, 'Z')
|
||||||
|
for v in bm.verts:
|
||||||
|
d = v.co.normalized()
|
||||||
|
r = 1.0 + amp * noise.fractal(d * 1.9 + V((seed, seed * .7, seed * .3)), 1.0, 2.0, 3)
|
||||||
|
q = d * r
|
||||||
|
for n, off in planes:
|
||||||
|
over = q.dot(n) - off
|
||||||
|
if over > 0:
|
||||||
|
q -= n * over
|
||||||
|
p = V((q.x * radii[0], q.y * radii[1], q.z * radii[2]))
|
||||||
|
w = c + rot @ p
|
||||||
|
if flat_bottom is not None and w.y < flat_bottom:
|
||||||
|
w.y = flat_bottom
|
||||||
|
v.co = G(*w)
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
bm.to_mesh(me)
|
||||||
|
bm.free()
|
||||||
|
me.materials.append(material(mat))
|
||||||
|
ob = bpy.data.objects.new(name, me)
|
||||||
|
bpy.context.collection.objects.link(ob)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def ring(name, center, radius, width, thickness, mat, sides=48, axis_tilt=(0.0, 0.0), uv_cell=None, bev=.02):
|
||||||
|
"""Flat ring (rectangular section) around the Godot y axis, tilted by (x, z) angles.
|
||||||
|
The outer band carries rune UVs when uv_cell is given."""
|
||||||
|
c = V(center)
|
||||||
|
rot = Matrix.Rotation(axis_tilt[0], 3, 'X') @ Matrix.Rotation(axis_tilt[1], 3, 'Z')
|
||||||
|
section = [(radius - width / 2, -thickness / 2), (radius + width / 2, -thickness / 2),
|
||||||
|
(radius + width / 2, thickness / 2), (radius - width / 2, thickness / 2)]
|
||||||
|
verts, faces, uvs = [], [], []
|
||||||
|
cells = max(1, round(2 * pi * radius / uv_cell)) if uv_cell else 1
|
||||||
|
for k in range(sides + 1):
|
||||||
|
a = 2 * pi * k / sides
|
||||||
|
for s, (r, y) in enumerate(section):
|
||||||
|
verts.append(tuple(c + rot @ V((cos(a) * r, y, sin(a) * r))))
|
||||||
|
uvs.append((cells * k / sides, 0.0 if s in (0, 1) else 1.0))
|
||||||
|
for k in range(sides):
|
||||||
|
for s in range(4):
|
||||||
|
a0, b0 = k * 4 + s, k * 4 + (s + 1) % 4
|
||||||
|
faces.append((a0, b0, b0 + 4, a0 + 4))
|
||||||
|
ob = make(name, verts, faces, mat, uvs)
|
||||||
|
_outward(ob)
|
||||||
|
if bev > 0:
|
||||||
|
bevel(ob, bev, 1, 30.0)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def band(name, center, radius, y0, y1, mat, sides=48, uv_cell=.35, inset=0.0):
|
||||||
|
"""Vertical cylindrical band (rune frieze) with UVs: u in glyph cells, v across."""
|
||||||
|
c = V(center)
|
||||||
|
verts, faces, uvs = [], [], []
|
||||||
|
cells = max(1, round(2 * pi * radius / uv_cell))
|
||||||
|
for k in range(sides + 1):
|
||||||
|
a = 2 * pi * k / sides
|
||||||
|
for s, y in enumerate((y0, y1)):
|
||||||
|
verts.append((c.x + cos(a) * (radius + inset), c.y + y, c.z + sin(a) * (radius + inset)))
|
||||||
|
uvs.append((cells * k / sides, float(s)))
|
||||||
|
for k in range(sides):
|
||||||
|
faces.append((k * 2, k * 2 + 2, k * 2 + 3, k * 2 + 1))
|
||||||
|
ob = make(name, verts, faces, mat, uvs)
|
||||||
|
_outward(ob)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def panel(name, center, width, height, normal_yaw, mat, depth=.02, uv_cells=(1, 1)):
|
||||||
|
"""Thin rectangular inlay facing the Godot direction normal_yaw (for rune panels)."""
|
||||||
|
c = V(center)
|
||||||
|
right = V((cos(normal_yaw), 0, -sin(normal_yaw)))
|
||||||
|
out = V((sin(normal_yaw), 0, cos(normal_yaw)))
|
||||||
|
up = V((0, 1, 0))
|
||||||
|
verts, uvs = [], []
|
||||||
|
for sy in (-1, 1):
|
||||||
|
for sx in (-1, 1):
|
||||||
|
verts.append(tuple(c + right * sx * width / 2 + up * sy * height / 2 + out * depth))
|
||||||
|
uvs.append(((sx + 1) / 2 * uv_cells[0], (sy + 1) / 2 * uv_cells[1]))
|
||||||
|
faces = [(0, 1, 3, 2)]
|
||||||
|
ob = make(name, verts, faces, mat, uvs)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def disc_grid(name, center, radius, mat, rings=6, sides=48, y=0.0, uv_scale=None):
|
||||||
|
"""Flat disc (Godot y up) with polar UVs: u = angle cells, v = radius (for floor runes)."""
|
||||||
|
c = V(center)
|
||||||
|
verts, faces, uvs = [(c.x, c.y + y, c.z)], [], [(0.0, 0.0)]
|
||||||
|
for j in range(1, rings + 1):
|
||||||
|
r = radius * j / rings
|
||||||
|
for k in range(sides + 1):
|
||||||
|
a = 2 * pi * k / sides
|
||||||
|
verts.append((c.x + cos(a) * r, c.y + y, c.z + sin(a) * r))
|
||||||
|
uvs.append((k / sides * (uv_scale or 1.0), j / rings))
|
||||||
|
for k in range(sides):
|
||||||
|
faces.append((0, 1 + k + 1, 1 + k))
|
||||||
|
ring_n = sides + 1
|
||||||
|
for j in range(rings - 1):
|
||||||
|
for k in range(sides):
|
||||||
|
a0 = 1 + j * ring_n + k
|
||||||
|
faces.append((a0, a0 + 1, a0 + 1 + ring_n, a0 + ring_n))
|
||||||
|
ob = make(name, verts, faces, mat, uvs)
|
||||||
|
for poly in ob.data.polygons:
|
||||||
|
poly.use_smooth = True
|
||||||
|
# Faces up.
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(ob.data)
|
||||||
|
for f in bm.faces:
|
||||||
|
f.normal_update()
|
||||||
|
if f.normal.z < 0:
|
||||||
|
f.normal_flip()
|
||||||
|
bm.to_mesh(ob.data)
|
||||||
|
bm.free()
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def banner(name, width, height, mat, shape='swallowtail', segments=(6, 12), top=(0, 0, 0), yaw=0.0):
|
||||||
|
"""Hanging cloth below the Godot point `top`: UV v runs 0 at the pole to 1 at the hem, so
|
||||||
|
the cloth shader weights the wind by v. shape: 'swallowtail' (Team A, cool) or 'flame'
|
||||||
|
(Team B, warm: three flame tongues)."""
|
||||||
|
nx, ny = segments
|
||||||
|
t = V(top)
|
||||||
|
rot = Matrix.Rotation(yaw, 3, 'Y')
|
||||||
|
verts, uvs, faces = [], [], []
|
||||||
|
for j in range(ny + 1):
|
||||||
|
v = j / ny
|
||||||
|
for i in range(nx + 1):
|
||||||
|
u = i / nx
|
||||||
|
x = (u - .5) * width
|
||||||
|
y = -v * height
|
||||||
|
if j == ny:
|
||||||
|
if shape == 'swallowtail':
|
||||||
|
y += height * .28 * (1 - abs(u - .5) * 2)
|
||||||
|
else:
|
||||||
|
y -= height * .12 * max(0.0, cos((u - .5) * 3 * 2 * pi)) ** 2
|
||||||
|
verts.append(tuple(t + rot @ V((x, y, 0))))
|
||||||
|
uvs.append((u, v))
|
||||||
|
for j in range(ny):
|
||||||
|
for i in range(nx):
|
||||||
|
a = j * (nx + 1) + i
|
||||||
|
faces.append((a, a + 1, a + nx + 2, a + nx + 1))
|
||||||
|
ob = make(name, verts, faces, mat, uvs, smooth=True)
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- finishing
|
||||||
|
def finalize(objects, sharp_angle=40.0, ao=True, rays=24, reach=2.2, ground=0.0, occluders=None, seed=7):
|
||||||
|
"""Split normals by angle, then bake ambient occlusion into the vertex colour Col.
|
||||||
|
Occlusion is traced against all `occluders` (default: the objects themselves) plus the
|
||||||
|
ground plane at Godot height `ground`, so feet of walls and inner corners darken."""
|
||||||
|
for ob in objects:
|
||||||
|
me = ob.data
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(me)
|
||||||
|
bmesh.ops.remove_doubles(bm, verts=list(bm.verts), dist=1e-6)
|
||||||
|
bm.to_mesh(me)
|
||||||
|
bm.free()
|
||||||
|
me.shade_smooth()
|
||||||
|
me.set_sharp_from_angle(angle=sharp_angle * pi / 180.0)
|
||||||
|
me.update()
|
||||||
|
if not ao:
|
||||||
|
for ob in objects:
|
||||||
|
_fill_color(ob, lambda i: 1.0)
|
||||||
|
return
|
||||||
|
sources = occluders if occluders is not None else objects
|
||||||
|
verts, polys = [], []
|
||||||
|
for ob in sources:
|
||||||
|
mw = ob.matrix_world
|
||||||
|
base = len(verts)
|
||||||
|
verts += [mw @ v.co for v in ob.data.vertices]
|
||||||
|
polys += [tuple(base + i for i in p.vertices) for p in ob.data.polygons]
|
||||||
|
tree = BVHTree.FromPolygons(verts, polys, epsilon=0.0)
|
||||||
|
# Fixed, well spread hemisphere directions (golden spiral), rotated per vertex.
|
||||||
|
dirs = []
|
||||||
|
for i in range(rays):
|
||||||
|
z = 1 - (i + .5) / rays
|
||||||
|
r = sqrt(max(0.0, 1 - z * z))
|
||||||
|
a = i * 2.39996323
|
||||||
|
dirs.append(V((cos(a) * r, sin(a) * r, z)))
|
||||||
|
ground_z = ground # Blender z == Godot y
|
||||||
|
for ob in objects:
|
||||||
|
mw = ob.matrix_world
|
||||||
|
nm = mw.to_3x3().inverted().transposed()
|
||||||
|
values = []
|
||||||
|
for v in ob.data.vertices:
|
||||||
|
p = mw @ v.co
|
||||||
|
n = (nm @ v.normal).normalized()
|
||||||
|
t = n.orthogonal().normalized()
|
||||||
|
b = n.cross(t)
|
||||||
|
hit = 0.0
|
||||||
|
origin = p + n * .012
|
||||||
|
for d in dirs:
|
||||||
|
w = t * d.x + b * d.y + n * d.z
|
||||||
|
blocked = False
|
||||||
|
if w.z < -1e-4 and origin.z >= ground_z:
|
||||||
|
dist = (origin.z - ground_z) / -w.z
|
||||||
|
blocked = dist < reach
|
||||||
|
if not blocked:
|
||||||
|
loc, _, _, dist = tree.ray_cast(origin, w, reach)
|
||||||
|
blocked = loc is not None
|
||||||
|
if blocked:
|
||||||
|
hit += d.z
|
||||||
|
weight = sum(d.z for d in dirs)
|
||||||
|
values.append(max(0.0, 1.0 - hit / weight))
|
||||||
|
_fill_color(ob, lambda i, values=values: .28 + .72 * values[i] ** .8)
|
||||||
|
|
||||||
|
|
||||||
|
def _fill_color(ob, fn):
|
||||||
|
me = ob.data
|
||||||
|
attr = me.color_attributes.get('Col') or me.color_attributes.new(name='Col', type='FLOAT_COLOR', domain='POINT')
|
||||||
|
for i, item in enumerate(attr.data):
|
||||||
|
a = fn(i)
|
||||||
|
item.color = (a, a, a, 1.0)
|
||||||
|
me.color_attributes.active_color = attr
|
||||||
|
me.color_attributes.render_color_index = me.color_attributes.find('Col')
|
||||||
|
|
||||||
|
|
||||||
|
def triangulate(ob):
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(ob.data)
|
||||||
|
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
||||||
|
bm.to_mesh(ob.data)
|
||||||
|
bm.free()
|
||||||
|
|
||||||
|
|
||||||
|
def triangles(objects):
|
||||||
|
total = 0
|
||||||
|
for ob in objects:
|
||||||
|
total += sum(len(p.vertices) - 2 for p in ob.data.polygons)
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def export(objects, glb_name, blend_name, extra_objects=()):
|
||||||
|
"""Saves the .blend and exports the given objects (plus armatures) as GLB. Returns the
|
||||||
|
parsed glTF JSON and the raw bytes for the manifest."""
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / blend_name))
|
||||||
|
bpy.ops.object.select_all(action='DESELECT')
|
||||||
|
for ob in list(objects) + list(extra_objects):
|
||||||
|
ob.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = objects[0]
|
||||||
|
path = OUT / glb_name
|
||||||
|
bpy.ops.export_scene.gltf(filepath=str(path), export_format='GLB', use_selection=True, export_materials='EXPORT',
|
||||||
|
export_yup=True, export_animations=False, export_vertex_color='ACTIVE', export_normals=True,
|
||||||
|
export_apply=False)
|
||||||
|
raw = path.read_bytes()
|
||||||
|
doc = json.loads(raw[20:20 + struct.unpack_from('<I', raw, 12)[0]])
|
||||||
|
return doc, raw
|
||||||
|
|
||||||
|
|
||||||
|
def glb_triangles(doc):
|
||||||
|
return sum(doc['accessors'][q['indices']]['count'] // 3 for m in doc['meshes'] for q in m['primitives'])
|
||||||
|
|
||||||
|
|
||||||
|
def bounds_godot(objects):
|
||||||
|
lo = [1e9, 1e9, 1e9]
|
||||||
|
hi = [-1e9, -1e9, -1e9]
|
||||||
|
for ob in objects:
|
||||||
|
for v in ob.data.vertices:
|
||||||
|
g = to_godot(ob.matrix_world @ v.co)
|
||||||
|
for i in range(3):
|
||||||
|
lo[i] = min(lo[i], g[i])
|
||||||
|
hi[i] = max(hi[i], g[i])
|
||||||
|
return {'min': [round(x, 4) for x in lo], 'max': [round(x, 4) for x in hi]}
|
||||||
|
|
||||||
|
|
||||||
|
def record(asset_id, glb_name, blend_name, generator, objects, doc, raw, **extra):
|
||||||
|
nodes = sorted(n.get('name', '') for n in doc.get('nodes', []))
|
||||||
|
entry = {'id': asset_id, 'resource_path': 'res://assets/luneraria/g2/' + glb_name,
|
||||||
|
'source': 'assets-source/luneraria/g2/' + blend_name, 'generator': 'assets-source/luneraria/g2/' + generator,
|
||||||
|
'triangles': glb_triangles(doc), 'nodes': nodes, 'materials': sorted(m['name'] for m in doc.get('materials', [])),
|
||||||
|
'bounds_godot': bounds_godot(objects), 'pivot': 'ground centre (0, 0, 0)', 'units': 'metres (Godot 1 unit = 1 m)',
|
||||||
|
'sha256': hashlib.sha256(raw).hexdigest()}
|
||||||
|
entry.update(extra)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def write_manifest(entries):
|
||||||
|
"""Merges entries (by id) into assets/luneraria/g2/g2_manifest.json."""
|
||||||
|
path = OUT / 'g2_manifest.json'
|
||||||
|
data = json.loads(path.read_text()) if path.exists() else {'assets': []}
|
||||||
|
by_id = {e['id']: e for e in data['assets']}
|
||||||
|
for e in entries:
|
||||||
|
by_id[e['id']] = e
|
||||||
|
data['assets'] = [by_id[k] for k in sorted(by_id)]
|
||||||
|
path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + '\n')
|
||||||
BIN
assets-source/luneraria/g2/schlundkrone.blend
Normal file
|
|
@ -4,8 +4,9 @@ importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://qglgrg8yrddc"
|
uid="uid://qglgrg8yrddc"
|
||||||
path.bptc="res://.godot/imported/ringborke_oak-height-4k.png-cfd49dae7a34a547b10e0dd2f64aea40.bptc.ctex"
|
path.bptc="res://.godot/imported/ringborke_oak-height-4k.png-cfd49dae7a34a547b10e0dd2f64aea40.bptc.ctex"
|
||||||
|
path.astc="res://.godot/imported/ringborke_oak-height-4k.png-cfd49dae7a34a547b10e0dd2f64aea40.astc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
|
|
@ -15,7 +16,7 @@ generator_parameters={
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://assets/characters/ringborke_oak-height-4k.png"
|
source_file="res://assets/characters/ringborke_oak-height-4k.png"
|
||||||
dest_files=["res://.godot/imported/ringborke_oak-height-4k.png-cfd49dae7a34a547b10e0dd2f64aea40.bptc.ctex"]
|
dest_files=["res://.godot/imported/ringborke_oak-height-4k.png-cfd49dae7a34a547b10e0dd2f64aea40.bptc.ctex", "res://.godot/imported/ringborke_oak-height-4k.png-cfd49dae7a34a547b10e0dd2f64aea40.astc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://varymvp2etyi"
|
uid="uid://varymvp2etyi"
|
||||||
path.bptc="res://.godot/imported/ringborke_ringborke-cut-albedo-4k.png-73c85421d02040c31d6a8d7e1fd502e5.bptc.ctex"
|
path.bptc="res://.godot/imported/ringborke_ringborke-cut-albedo-4k.png-73c85421d02040c31d6a8d7e1fd502e5.bptc.ctex"
|
||||||
|
path.astc="res://.godot/imported/ringborke_ringborke-cut-albedo-4k.png-73c85421d02040c31d6a8d7e1fd502e5.astc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
|
|
@ -15,7 +16,7 @@ generator_parameters={
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://assets/characters/ringborke_ringborke-cut-albedo-4k.png"
|
source_file="res://assets/characters/ringborke_ringborke-cut-albedo-4k.png"
|
||||||
dest_files=["res://.godot/imported/ringborke_ringborke-cut-albedo-4k.png-73c85421d02040c31d6a8d7e1fd502e5.bptc.ctex"]
|
dest_files=["res://.godot/imported/ringborke_ringborke-cut-albedo-4k.png-73c85421d02040c31d6a8d7e1fd502e5.bptc.ctex", "res://.godot/imported/ringborke_ringborke-cut-albedo-4k.png-73c85421d02040c31d6a8d7e1fd502e5.astc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://bqy23d8gmsx38"
|
uid="uid://bqy23d8gmsx38"
|
||||||
path.bptc="res://.godot/imported/ringborke_ringborke-cut-normal-4k.png-9bdfa6557e64b2117e348dcc9fc861c3.bptc.ctex"
|
path.bptc="res://.godot/imported/ringborke_ringborke-cut-normal-4k.png-9bdfa6557e64b2117e348dcc9fc861c3.bptc.ctex"
|
||||||
|
path.astc="res://.godot/imported/ringborke_ringborke-cut-normal-4k.png-9bdfa6557e64b2117e348dcc9fc861c3.astc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
|
|
@ -15,7 +16,7 @@ generator_parameters={
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://assets/characters/ringborke_ringborke-cut-normal-4k.png"
|
source_file="res://assets/characters/ringborke_ringborke-cut-normal-4k.png"
|
||||||
dest_files=["res://.godot/imported/ringborke_ringborke-cut-normal-4k.png-9bdfa6557e64b2117e348dcc9fc861c3.bptc.ctex"]
|
dest_files=["res://.godot/imported/ringborke_ringborke-cut-normal-4k.png-9bdfa6557e64b2117e348dcc9fc861c3.bptc.ctex", "res://.godot/imported/ringborke_ringborke-cut-normal-4k.png-9bdfa6557e64b2117e348dcc9fc861c3.astc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://b1phtpe3asudy"
|
uid="uid://b1phtpe3asudy"
|
||||||
path.bptc="res://.godot/imported/ringborke_ringborke-cut-roughness-4k.png-d74b9087b174fb5af2fc65e3b578dce3.bptc.ctex"
|
path.bptc="res://.godot/imported/ringborke_ringborke-cut-roughness-4k.png-d74b9087b174fb5af2fc65e3b578dce3.bptc.ctex"
|
||||||
|
path.astc="res://.godot/imported/ringborke_ringborke-cut-roughness-4k.png-d74b9087b174fb5af2fc65e3b578dce3.astc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
generator_parameters={
|
generator_parameters={
|
||||||
|
|
@ -15,7 +16,7 @@ generator_parameters={
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://assets/characters/ringborke_ringborke-cut-roughness-4k.png"
|
source_file="res://assets/characters/ringborke_ringborke-cut-roughness-4k.png"
|
||||||
dest_files=["res://.godot/imported/ringborke_ringborke-cut-roughness-4k.png-d74b9087b174fb5af2fc65e3b578dce3.bptc.ctex"]
|
dest_files=["res://.godot/imported/ringborke_ringborke-cut-roughness-4k.png-d74b9087b174fb5af2fc65e3b578dce3.bptc.ctex", "res://.godot/imported/ringborke_ringborke-cut-roughness-4k.png-d74b9087b174fb5af2fc65e3b578dce3.astc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|
|
||||||
BIN
assets/luneraria/g2/ancient.glb
Normal file
45
assets/luneraria/g2/ancient.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://3q5tqaafok36"
|
||||||
|
path="res://.godot/imported/ancient.glb-9aa70e696eb559f0231a6e200afc4b17.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/luneraria/g2/ancient.glb"
|
||||||
|
dest_files=["res://.godot/imported/ancient.glb-9aa70e696eb559f0231a6e200afc4b17.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
BIN
assets/luneraria/g2/bastion.glb
Normal file
45
assets/luneraria/g2/bastion.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://b2rxost426mpt"
|
||||||
|
path="res://.godot/imported/bastion.glb-63ae45f3c7bd69ea38928675b5ca9526.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/luneraria/g2/bastion.glb"
|
||||||
|
dest_files=["res://.godot/imported/bastion.glb-63ae45f3c7bd69ea38928675b5ca9526.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
BIN
assets/luneraria/g2/bossgrotte.glb
Normal file
45
assets/luneraria/g2/bossgrotte.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://cyup5xtftwojd"
|
||||||
|
path="res://.godot/imported/bossgrotte.glb-90f1207b41dac974736a7c52b6ba987a.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/luneraria/g2/bossgrotte.glb"
|
||||||
|
dest_files=["res://.godot/imported/bossgrotte.glb-90f1207b41dac974736a7c52b6ba987a.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
373
assets/luneraria/g2/g2_manifest.json
Normal file
|
|
@ -0,0 +1,373 @@
|
||||||
|
{
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"id": "ancient",
|
||||||
|
"resource_path": "res://assets/luneraria/g2/ancient.glb",
|
||||||
|
"source": "assets-source/luneraria/g2/ancient.blend",
|
||||||
|
"generator": "assets-source/luneraria/g2/build_ancient.py",
|
||||||
|
"triangles": 19032,
|
||||||
|
"nodes": [
|
||||||
|
"Banner_L",
|
||||||
|
"Banner_R",
|
||||||
|
"Blade_0",
|
||||||
|
"Blade_1",
|
||||||
|
"Blade_2",
|
||||||
|
"Blade_3",
|
||||||
|
"Crest_A",
|
||||||
|
"Crest_B",
|
||||||
|
"Crown",
|
||||||
|
"Crystal",
|
||||||
|
"Dais",
|
||||||
|
"Fragment_0",
|
||||||
|
"Fragment_1",
|
||||||
|
"Fragment_2",
|
||||||
|
"Fragment_3",
|
||||||
|
"Fragment_4",
|
||||||
|
"Fragment_5",
|
||||||
|
"Pedestal",
|
||||||
|
"Ring_Inner",
|
||||||
|
"Ring_Outer",
|
||||||
|
"Ruin_Crystal",
|
||||||
|
"Ruin_Rubble",
|
||||||
|
"Satellite_0",
|
||||||
|
"Satellite_1",
|
||||||
|
"Satellite_2",
|
||||||
|
"Satellite_3",
|
||||||
|
"Satellite_4"
|
||||||
|
],
|
||||||
|
"materials": [
|
||||||
|
"LN_Cloth_Team",
|
||||||
|
"LN_Crystal_Team",
|
||||||
|
"LN_Metal_Iron",
|
||||||
|
"LN_Metal_Team",
|
||||||
|
"LN_Rune_Team",
|
||||||
|
"LN_Stone_Dark",
|
||||||
|
"LN_Stone_Pale"
|
||||||
|
],
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-4.2035,
|
||||||
|
-0.9881,
|
||||||
|
-4.0
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
4.2035,
|
||||||
|
10.3668,
|
||||||
|
4.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pivot": "ground centre (0, 0, 0)",
|
||||||
|
"units": "metres (Godot 1 unit = 1 m)",
|
||||||
|
"sha256": "a3d6ccaf1b9387e1df088f6375232ebc7f15402a80b6abd82885062926acc7dc",
|
||||||
|
"title": "Mondherz (Ancient)",
|
||||||
|
"crystal_height": 4.7,
|
||||||
|
"blade_split": 3.4,
|
||||||
|
"heart_source": "assets-source/phase4/blend/core_team0.blend (Core_CrystalMain, Core_CrystalSatellite0-4), scale 2.3",
|
||||||
|
"triangles_intact": 15984,
|
||||||
|
"triangles_ruin": 880
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bastion",
|
||||||
|
"resource_path": "res://assets/luneraria/g2/bastion.glb",
|
||||||
|
"source": "assets-source/luneraria/g2/bastion.blend",
|
||||||
|
"generator": "assets-source/luneraria/g2/build_towers.py",
|
||||||
|
"triangles": 9364,
|
||||||
|
"nodes": [
|
||||||
|
"Crystal",
|
||||||
|
"Debris",
|
||||||
|
"Lower",
|
||||||
|
"Pennant",
|
||||||
|
"Ring",
|
||||||
|
"Ruin",
|
||||||
|
"Shard_0",
|
||||||
|
"Shard_1",
|
||||||
|
"Shard_2",
|
||||||
|
"Upper",
|
||||||
|
"Upper_Damaged"
|
||||||
|
],
|
||||||
|
"materials": [
|
||||||
|
"LN_Cloth_Team",
|
||||||
|
"LN_Crystal_Team",
|
||||||
|
"LN_Metal_Iron",
|
||||||
|
"LN_Metal_Team",
|
||||||
|
"LN_Rune_Team",
|
||||||
|
"LN_Stone_Dark",
|
||||||
|
"LN_Stone_Pale",
|
||||||
|
"LN_Wood"
|
||||||
|
],
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-1.7395,
|
||||||
|
0.0,
|
||||||
|
-2.25
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
1.7395,
|
||||||
|
7.1605,
|
||||||
|
1.6437
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pivot": "ground centre (0, 0, 0)",
|
||||||
|
"units": "metres (Godot 1 unit = 1 m)",
|
||||||
|
"sha256": "80c13b7a5f54e76d07e83bfd74a98f6bc86b76033875f37f448dce416b5f678e",
|
||||||
|
"title": "Bastion",
|
||||||
|
"split_height": 2.6,
|
||||||
|
"crystal_height": 6.2,
|
||||||
|
"triangles_intact": 4718,
|
||||||
|
"triangles_ruin": 1382
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bossgrotte",
|
||||||
|
"resource_path": "res://assets/luneraria/g2/bossgrotte.glb",
|
||||||
|
"source": "assets-source/luneraria/g2/bossgrotte.blend",
|
||||||
|
"generator": "assets-source/luneraria/g2/build_bossgrotte.py",
|
||||||
|
"triangles": 15182,
|
||||||
|
"nodes": [
|
||||||
|
"Altar",
|
||||||
|
"Cave",
|
||||||
|
"CaveGlow",
|
||||||
|
"Channel",
|
||||||
|
"Floor",
|
||||||
|
"Gate",
|
||||||
|
"PillarCrystals",
|
||||||
|
"Pillars",
|
||||||
|
"Platform",
|
||||||
|
"Rocks",
|
||||||
|
"Runes",
|
||||||
|
"Source"
|
||||||
|
],
|
||||||
|
"materials": [
|
||||||
|
"LN_Cave",
|
||||||
|
"LN_Crystal_Grotto",
|
||||||
|
"LN_Metal_Bronze",
|
||||||
|
"LN_Rock",
|
||||||
|
"LN_Rune_Grotto",
|
||||||
|
"LN_Stone_Dark",
|
||||||
|
"LN_Stone_Grotto",
|
||||||
|
"LN_Water_Glow"
|
||||||
|
],
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-10.5442,
|
||||||
|
-1.93,
|
||||||
|
-10.4646
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
7.7775,
|
||||||
|
7.6927,
|
||||||
|
7.7976
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pivot": "ground centre (0, 0, 0)",
|
||||||
|
"units": "metres (Godot 1 unit = 1 m)",
|
||||||
|
"sha256": "63dab12607f6d2f26e12af70b8b561d13a853683b363160df76ec6392610d04c",
|
||||||
|
"title": "Mondschlund (Bossgrotte)",
|
||||||
|
"boss_point": [
|
||||||
|
-1.5,
|
||||||
|
0.0,
|
||||||
|
-1.5
|
||||||
|
],
|
||||||
|
"arena_radius": 7.8,
|
||||||
|
"gate": [
|
||||||
|
-6.329,
|
||||||
|
0.0,
|
||||||
|
-6.329
|
||||||
|
],
|
||||||
|
"lane_clearance_min": 0.6,
|
||||||
|
"cave_notch": {
|
||||||
|
"u": [
|
||||||
|
-11.95,
|
||||||
|
-8.45
|
||||||
|
],
|
||||||
|
"half_width": 1.94,
|
||||||
|
"depth": 1.0
|
||||||
|
},
|
||||||
|
"note": "symmetric across the local diagonal x = z; the Godot view places it at LuneMap.GROTTO on the terrace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grenzwacht",
|
||||||
|
"resource_path": "res://assets/luneraria/g2/grenzwacht.glb",
|
||||||
|
"source": "assets-source/luneraria/g2/grenzwacht.blend",
|
||||||
|
"generator": "assets-source/luneraria/g2/build_towers.py",
|
||||||
|
"triangles": 6516,
|
||||||
|
"nodes": [
|
||||||
|
"Crystal",
|
||||||
|
"Debris",
|
||||||
|
"Lower",
|
||||||
|
"Pennant",
|
||||||
|
"Ruin",
|
||||||
|
"Upper",
|
||||||
|
"Upper_Damaged"
|
||||||
|
],
|
||||||
|
"materials": [
|
||||||
|
"LN_Cloth_Team",
|
||||||
|
"LN_Crystal_Team",
|
||||||
|
"LN_Metal_Iron",
|
||||||
|
"LN_Metal_Team",
|
||||||
|
"LN_Rune_Team",
|
||||||
|
"LN_Stone_Dark",
|
||||||
|
"LN_Stone_Pale",
|
||||||
|
"LN_Wood"
|
||||||
|
],
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-1.3717,
|
||||||
|
0.0,
|
||||||
|
-1.3735
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
1.3752,
|
||||||
|
6.0601,
|
||||||
|
1.3678
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pivot": "ground centre (0, 0, 0)",
|
||||||
|
"units": "metres (Godot 1 unit = 1 m)",
|
||||||
|
"sha256": "f69a83b7a2c67202f41d5892984a2db8572206124ad366d2c91c9da653030536",
|
||||||
|
"title": "Grenzwacht",
|
||||||
|
"split_height": 2.2,
|
||||||
|
"crystal_height": 5.2,
|
||||||
|
"triangles_intact": 3174,
|
||||||
|
"triangles_ruin": 1038
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kernwacht",
|
||||||
|
"resource_path": "res://assets/luneraria/g2/kernwacht.glb",
|
||||||
|
"source": "assets-source/luneraria/g2/kernwacht.blend",
|
||||||
|
"generator": "assets-source/luneraria/g2/build_towers.py",
|
||||||
|
"triangles": 9224,
|
||||||
|
"nodes": [
|
||||||
|
"Banner_L",
|
||||||
|
"Banner_R",
|
||||||
|
"Crystal",
|
||||||
|
"Debris",
|
||||||
|
"Lower",
|
||||||
|
"Ring",
|
||||||
|
"Ruin",
|
||||||
|
"Upper",
|
||||||
|
"Upper_Damaged"
|
||||||
|
],
|
||||||
|
"materials": [
|
||||||
|
"LN_Cloth_Team",
|
||||||
|
"LN_Crystal_Team",
|
||||||
|
"LN_Metal_Iron",
|
||||||
|
"LN_Metal_Team",
|
||||||
|
"LN_Rune_Team",
|
||||||
|
"LN_Stone_Dark",
|
||||||
|
"LN_Stone_Pale"
|
||||||
|
],
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-1.8045,
|
||||||
|
0.0,
|
||||||
|
-1.8017
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
1.8139,
|
||||||
|
8.33,
|
||||||
|
1.806
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pivot": "ground centre (0, 0, 0)",
|
||||||
|
"units": "metres (Godot 1 unit = 1 m)",
|
||||||
|
"sha256": "7f7492d69d1782ee82f8e63c0b01c1bf6318f5ab685e3573200539843f056a07",
|
||||||
|
"title": "Kernwacht",
|
||||||
|
"split_height": 2.7,
|
||||||
|
"crystal_height": 5.25,
|
||||||
|
"triangles_intact": 4652,
|
||||||
|
"triangles_ruin": 1132
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "schlundkrone",
|
||||||
|
"title": "Schlundkrone (Superboss)",
|
||||||
|
"resource_path": "res://assets/luneraria/g2/schlundkrone.glb",
|
||||||
|
"source": "assets-source/luneraria/g2/schlundkrone.blend",
|
||||||
|
"generator": "assets-source/luneraria/g2/build_schlundkrone.py",
|
||||||
|
"triangles": 5712,
|
||||||
|
"source_parts": 82,
|
||||||
|
"skeletal": true,
|
||||||
|
"bones": 24,
|
||||||
|
"bone_names": [
|
||||||
|
"Root",
|
||||||
|
"Hips",
|
||||||
|
"Spine",
|
||||||
|
"Chest",
|
||||||
|
"Neck",
|
||||||
|
"Head",
|
||||||
|
"Jaw",
|
||||||
|
"Crown",
|
||||||
|
"Maw.L",
|
||||||
|
"Shoulder.L",
|
||||||
|
"UpperArm.L",
|
||||||
|
"Forearm.L",
|
||||||
|
"Hand.L",
|
||||||
|
"Thigh.L",
|
||||||
|
"Shin.L",
|
||||||
|
"Foot.L",
|
||||||
|
"Maw.R",
|
||||||
|
"Shoulder.R",
|
||||||
|
"UpperArm.R",
|
||||||
|
"Forearm.R",
|
||||||
|
"Hand.R",
|
||||||
|
"Thigh.R",
|
||||||
|
"Shin.R",
|
||||||
|
"Foot.R"
|
||||||
|
],
|
||||||
|
"clips": [
|
||||||
|
"ability",
|
||||||
|
"attack_a",
|
||||||
|
"attack_b",
|
||||||
|
"death",
|
||||||
|
"hit",
|
||||||
|
"idle",
|
||||||
|
"roar",
|
||||||
|
"start",
|
||||||
|
"stop",
|
||||||
|
"victory",
|
||||||
|
"walk"
|
||||||
|
],
|
||||||
|
"clip_seconds": {
|
||||||
|
"idle": 3.0,
|
||||||
|
"start": 0.4,
|
||||||
|
"walk": 1.6,
|
||||||
|
"stop": 0.4,
|
||||||
|
"attack_a": 1.3,
|
||||||
|
"attack_b": 1.3,
|
||||||
|
"hit": 0.5,
|
||||||
|
"ability": 1.3,
|
||||||
|
"death": 2.2,
|
||||||
|
"roar": 2.0,
|
||||||
|
"victory": 2.0
|
||||||
|
},
|
||||||
|
"attack_cycle": [
|
||||||
|
"attack_a",
|
||||||
|
"attack_b"
|
||||||
|
],
|
||||||
|
"contact_seconds": 0.9,
|
||||||
|
"stride": 2.0,
|
||||||
|
"view_scale": 1.7,
|
||||||
|
"materials": [
|
||||||
|
"LN_Boss_Bone",
|
||||||
|
"LN_Boss_Bronze",
|
||||||
|
"LN_Boss_Hide",
|
||||||
|
"LN_Crystal_Boss"
|
||||||
|
],
|
||||||
|
"skins": 1,
|
||||||
|
"bounds_godot": {
|
||||||
|
"min": [
|
||||||
|
-1.2307,
|
||||||
|
0.0053,
|
||||||
|
-0.5532
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
1.3273,
|
||||||
|
3.3182,
|
||||||
|
0.9253
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pivot": "ground centre (0, 0, 0), faces +z",
|
||||||
|
"units": "metres at view scale 1",
|
||||||
|
"sha256": "20c7c0adafeae31563d328a3a5906c9ede13ab22a4b3ef153c0114a6f1ec755a"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
assets/luneraria/g2/grenzwacht.glb
Normal file
45
assets/luneraria/g2/grenzwacht.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dqlur2vf8832t"
|
||||||
|
path="res://.godot/imported/grenzwacht.glb-db290edb90fa13ed9c8cb6d8db64d204.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/luneraria/g2/grenzwacht.glb"
|
||||||
|
dest_files=["res://.godot/imported/grenzwacht.glb-db290edb90fa13ed9c8cb6d8db64d204.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
BIN
assets/luneraria/g2/kernwacht.glb
Normal file
45
assets/luneraria/g2/kernwacht.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://dvksl6pcbnhjg"
|
||||||
|
path="res://.godot/imported/kernwacht.glb-8e1c1ddb5290ff9b4d54a79fd6b715c0.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/luneraria/g2/kernwacht.glb"
|
||||||
|
dest_files=["res://.godot/imported/kernwacht.glb-8e1c1ddb5290ff9b4d54a79fd6b715c0.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
BIN
assets/luneraria/g2/schlundkrone.glb
Normal file
45
assets/luneraria/g2/schlundkrone.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://xmht20y5rn2h"
|
||||||
|
path="res://.godot/imported/schlundkrone.glb-38228c20a8f54dd8b14675a8d4d95fb0.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/luneraria/g2/schlundkrone.glb"
|
||||||
|
dest_files=["res://.godot/imported/schlundkrone.glb-38228c20a8f54dd8b14675a8d4d95fb0.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
mesh_library/use_node_names_as_mesh_names=false
|
||||||
|
array_mesh/deduplicate_surfaces=true
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
|
gltf/texture_map_mode=1
|
||||||
|
|
@ -4,15 +4,16 @@ importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://gb8k2gh1fatp"
|
uid="uid://gb8k2gh1fatp"
|
||||||
path.bptc="res://.godot/imported/limestone-albedo.png-e2ef74eda1463a024457e6fa83926f32.bptc.ctex"
|
path.bptc="res://.godot/imported/limestone-albedo.png-e2ef74eda1463a024457e6fa83926f32.bptc.ctex"
|
||||||
|
path.astc="res://.godot/imported/limestone-albedo.png-e2ef74eda1463a024457e6fa83926f32.astc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://assets/materials/phase10/limestone-albedo.png"
|
source_file="res://assets/materials/phase10/limestone-albedo.png"
|
||||||
dest_files=["res://.godot/imported/limestone-albedo.png-e2ef74eda1463a024457e6fa83926f32.bptc.ctex"]
|
dest_files=["res://.godot/imported/limestone-albedo.png-e2ef74eda1463a024457e6fa83926f32.bptc.ctex", "res://.godot/imported/limestone-albedo.png-e2ef74eda1463a024457e6fa83926f32.astc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,16 @@ importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://c5j13kmnfjj6q"
|
uid="uid://c5j13kmnfjj6q"
|
||||||
path.bptc="res://.godot/imported/oak-albedo-4k.png-9073c445a98157f52562d09fd5d5f9a7.bptc.ctex"
|
path.bptc="res://.godot/imported/oak-albedo-4k.png-9073c445a98157f52562d09fd5d5f9a7.bptc.ctex"
|
||||||
|
path.astc="res://.godot/imported/oak-albedo-4k.png-9073c445a98157f52562d09fd5d5f9a7.astc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://assets/materials/phase10/oak-albedo-4k.png"
|
source_file="res://assets/materials/phase10/oak-albedo-4k.png"
|
||||||
dest_files=["res://.godot/imported/oak-albedo-4k.png-9073c445a98157f52562d09fd5d5f9a7.bptc.ctex"]
|
dest_files=["res://.godot/imported/oak-albedo-4k.png-9073c445a98157f52562d09fd5d5f9a7.bptc.ctex", "res://.godot/imported/oak-albedo-4k.png-9073c445a98157f52562d09fd5d5f9a7.astc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,16 @@ importer="texture"
|
||||||
type="CompressedTexture2D"
|
type="CompressedTexture2D"
|
||||||
uid="uid://dbx5xdwy7c2y1"
|
uid="uid://dbx5xdwy7c2y1"
|
||||||
path.bptc="res://.godot/imported/oak-height-4k.png-178f6878e8a21b5c0ef060bf738f5274.bptc.ctex"
|
path.bptc="res://.godot/imported/oak-height-4k.png-178f6878e8a21b5c0ef060bf738f5274.bptc.ctex"
|
||||||
|
path.astc="res://.godot/imported/oak-height-4k.png-178f6878e8a21b5c0ef060bf738f5274.astc.ctex"
|
||||||
metadata={
|
metadata={
|
||||||
"imported_formats": ["s3tc_bptc"],
|
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||||
"vram_texture": true
|
"vram_texture": true
|
||||||
}
|
}
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|
||||||
source_file="res://assets/materials/phase10/oak-height-4k.png"
|
source_file="res://assets/materials/phase10/oak-height-4k.png"
|
||||||
dest_files=["res://.godot/imported/oak-height-4k.png-178f6878e8a21b5c0ef060bf738f5274.bptc.ctex"]
|
dest_files=["res://.godot/imported/oak-height-4k.png-178f6878e8a21b5c0ef060bf738f5274.bptc.ctex", "res://.godot/imported/oak-height-4k.png-178f6878e8a21b5c0ef060bf738f5274.astc.ctex"]
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,19 @@
|
||||||
# Installation, Start, Steuerung und Export
|
# Installation, Start, Steuerung und Export
|
||||||
|
|
||||||
## Start aus dem Quellprojekt (Fedora 44)
|
## Spielen ohne Quellprojekt
|
||||||
|
|
||||||
|
Die Pakete liegen unter `../dist/luneraria/` (Version 0.17.0-g2):
|
||||||
|
|
||||||
|
| Paket | Start |
|
||||||
|
| --- | --- |
|
||||||
|
| `ChampionsOfLuneraria-macOS-universal-0.17.0-g2.zip` | Entpacken, „Champions of Luneraria“ öffnen. Die App ist ad-hoc signiert, nicht notarisiert: beim ersten Start Rechtsklick → Öffnen (oder `xattr -dr com.apple.quarantine "Champions of Luneraria.app"`). Apple Silicon und Intel, ab macOS 11. |
|
||||||
|
| `ChampionsOfLuneraria-Windows-x64-0.17.0-g2.zip` | `ChampionsOfLuneraria.exe` |
|
||||||
|
| `ChampionsOfLuneraria-Windows-ARM64-0.17.0-g2.zip` | `ChampionsOfLuneraria.exe` auf Windows on ARM (z. B. Parallels auf dem Mac) |
|
||||||
|
| `ChampionsOfLuneraria-Linux-x64-0.17.0-g2.zip` | `./ChampionsOfLuneraria.x86_64` |
|
||||||
|
|
||||||
|
Ohne Vulkan/Direct3D 12 (etwa in einer VM) wechselt das Spiel selbst auf OpenGL 3 (Kompatibilitätsmodus).
|
||||||
|
|
||||||
|
## Start aus dem Quellprojekt
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd saumwacht-godot
|
cd saumwacht-godot
|
||||||
|
|
@ -10,6 +23,14 @@ godot --path . -- --legacy # archivierter Saumwacht-Modus (scenes/main.tscn)
|
||||||
|
|
||||||
Voraussetzung ist Godot 4.7.2. Blender wird nur zum Neuerzeugen von Assets gebraucht, nicht zum Spielen.
|
Voraussetzung ist Godot 4.7.2. Blender wird nur zum Neuerzeugen von Assets gebraucht, nicht zum Spielen.
|
||||||
|
|
||||||
|
**macOS (seit G2):**
|
||||||
|
- Godot 4.7.2 liegt unter `~/Applications/Godot-4.7.2.app`, Blender 5.2.2 LTS unter `~/Applications/Blender-5.2.2.app`. Beide sind über `~/.local/bin` als `godot` und `blender` aufrufbar.
|
||||||
|
- Die Programme unter `/Applications` sind älter (Godot 4.6.2, Blender 5.1.2) und nicht für dieses Projekt gedacht.
|
||||||
|
- Solange die Xcode-Lizenz nicht bestätigt ist, scheitern `/usr/bin/python3` und `brew install`. Deshalb `/opt/homebrew/bin/python3.12` für die Python-Werkzeuge nutzen (oder einmal `sudo xcodebuild -license accept`).
|
||||||
|
- Das Projekt importiert Texturen zusätzlich als ETC2/ASTC; das braucht der Export für Apple Silicon.
|
||||||
|
|
||||||
|
**Fedora 44 (bis G1):** unverändert, `godot` und `blender` aus dem Paketmanager bzw. als 4.7.2/5.2.2.
|
||||||
|
|
||||||
## Eine Partie
|
## Eine Partie
|
||||||
|
|
||||||
1. **Neue Partie** → Bund (Glutpakt oder Wurzelbund) und deine Lane (Top, Mid, Bottom) wählen → **Partie beginnen**. Du spielst in Team Blau (Südwest) mit zwei Bot-Verbündeten gegen drei Bots in Team Rot. Top führt über das Hochland, Mid über die Mittelplattform, Bottom durch das Tiefland über die Deltabrücke; die Bossgrotte liegt zwischen Top und Mid.
|
1. **Neue Partie** → Bund (Glutpakt oder Wurzelbund) und deine Lane (Top, Mid, Bottom) wählen → **Partie beginnen**. Du spielst in Team Blau (Südwest) mit zwei Bot-Verbündeten gegen drei Bots in Team Rot. Top führt über das Hochland, Mid über die Mittelplattform, Bottom durch das Tiefland über die Deltabrücke; die Bossgrotte liegt zwischen Top und Mid.
|
||||||
|
|
@ -58,6 +79,16 @@ godot --path . --resolution 1920x1080 res://tools/luneraria/lune_benchmark.tscn
|
||||||
godot --path . --resolution 1920x1080 res://tools/luneraria/lune_review.tscn # Aufnahmen
|
godot --path . --resolution 1920x1080 res://tools/luneraria/lune_review.tscn # Aufnahmen
|
||||||
godot --path . --resolution 1920x1080 res://tools/luneraria/verify_g1.tscn # G1: Layout, Gelände, Kamera (443)
|
godot --path . --resolution 1920x1080 res://tools/luneraria/verify_g1.tscn # G1: Layout, Gelände, Kamera (443)
|
||||||
godot --path . --resolution 1920x1080 res://tools/luneraria/g_views.tscn -- --out=res://docs/luneraria/screenshots/g1/nachher # Vergleichsansichten
|
godot --path . --resolution 1920x1080 res://tools/luneraria/g_views.tscn -- --out=res://docs/luneraria/screenshots/g1/nachher # Vergleichsansichten
|
||||||
|
# G2: alles auf einmal (Regeln, Netz-Regression, G1, G2, echte Anwendung, Stress, Balance, Legacy):
|
||||||
|
sh tools/luneraria/run_g2_tests.sh # macOS: PYTHON=/opt/homebrew/bin/python3.12 sh tools/luneraria/run_g2_tests.sh
|
||||||
|
godot --path . --resolution 1920x1080 res://tools/luneraria/verify_g2.tscn # G2: Bauwerke, Grotte, Boss, Occlusion (245)
|
||||||
|
godot --path . --resolution 1920x1080 res://tools/luneraria/g2_views.tscn -- --out=res://docs/luneraria/screenshots/g2/nachher # Abnahmebilder
|
||||||
|
godot --path . --resolution 1920x1080 res://tools/luneraria/g2_cost_split.tscn # Kostenaufteilung eines Bildes
|
||||||
|
godot --path . --resolution 1920x1080 res://tools/luneraria/g2_boss_sheet.tscn -- --out=/tmp/boss/ # Boss-Clips neben Held und Truppe
|
||||||
|
godot --path . --resolution 1920x1080 res://tools/luneraria/g2_probe.tscn -- --out=/tmp/probe/ --shots="grotte@-13.5,-13.5,20" # freie Kamera-Probe
|
||||||
|
# Videos (1920 × 1080 erzwingt eine vorübergehende override.cfg mit window_width/height_override):
|
||||||
|
godot --path . --resolution 1920x1080 --fixed-fps 30 --write-movie /tmp/g2video/turm/frame.png res://tools/luneraria/g2_video.tscn -- --scene=turm
|
||||||
|
blender -b --python tools/luneraria/encode_video.py -- /tmp/g2video/turm docs/luneraria/videos/g2-turmkampf.mp4 30 1.0
|
||||||
# Legacy (Saumwacht) unverändert:
|
# Legacy (Saumwacht) unverändert:
|
||||||
godot --headless --path . res://tests/test_runner.tscn
|
godot --headless --path . res://tests/test_runner.tscn
|
||||||
godot --headless --path . -- --smoke-test
|
godot --headless --path . -- --smoke-test
|
||||||
|
|
@ -74,6 +105,12 @@ ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luner
|
||||||
ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/build_grotto.py
|
ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/build_grotto.py
|
||||||
ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/build_map_props.py
|
ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/build_map_props.py
|
||||||
ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/build_peoples.py
|
ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/build_peoples.py
|
||||||
|
# G2: strategische Bauwerke, Grotte, Boss (schreiben .blend, GLB und assets/luneraria/g2/g2_manifest.json)
|
||||||
|
godot --headless --path . -s res://tools/luneraria/dump_grotto_site.gd # Lane-Linien und Gelände um die Grotte für den Generator
|
||||||
|
ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/g2/build_towers.py
|
||||||
|
ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/g2/build_ancient.py
|
||||||
|
ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/g2/build_bossgrotte.py
|
||||||
|
ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/g2/build_schlundkrone.py
|
||||||
python3 tools/luneraria/build_asset_matrix.py
|
python3 tools/luneraria/build_asset_matrix.py
|
||||||
godot --headless --editor --import --path .
|
godot --headless --editor --import --path .
|
||||||
```
|
```
|
||||||
|
|
@ -85,7 +122,25 @@ python3 tools/luneraria/build_luneraria_demo.py --check-only
|
||||||
python3 tools/luneraria/build_luneraria_demo.py
|
python3 tools/luneraria/build_luneraria_demo.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Erzeugt `../dist/luneraria/ChampionsOfLuneraria-Linux-x64-<ver>.zip` und `…-Windows-x64-<ver>.zip` (Produktname Champions of Luneraria). Alte Saumwacht-Releases unter `../dist/Saumwacht-*.zip` bleiben unverändert. `tools/build_demo.py` baut weiterhin Saumwacht 0.15.0.
|
Erzeugt unter `../dist/luneraria/`:
|
||||||
|
- `ChampionsOfLuneraria-Linux-x64-<ver>.zip`
|
||||||
|
- `…-Windows-x64-<ver>.zip`
|
||||||
|
- `…-Windows-ARM64-<ver>.zip`
|
||||||
|
- `…-macOS-universal-<ver>.zip`
|
||||||
|
- `build-manifest.json` mit Größen und SHA-256.
|
||||||
|
|
||||||
|
Die Version steht einmal in `project.godot` unter `[luneraria] config/version` und erscheint auch im Menü. Mit `--only macos,windows,...` baut das Skript einzelne Pakete; Godot kommt aus `PATH` oder der Umgebungsvariable `GODOT`.
|
||||||
|
|
||||||
|
Das macOS-Paket schreibt Godot direkt als ZIP: Universal, ad-hoc signiert mit Hardened Runtime, nicht notarisiert. Windows ARM64 nutzt die installierten 4.7.2-Templates; Linux und Windows x64 nutzen die Templates im Repo.
|
||||||
|
|
||||||
|
Alte Saumwacht-Releases liegen unter `../_archiv/saumwacht-dist/`. `tools/build_demo.py` (Saumwacht 0.15.0) passt seit M4 nicht mehr zu den umbenannten Presets.
|
||||||
|
|
||||||
|
**Plattform-Kurztest eines Pakets:** spielt eine kurze Partie, fotografiert Planung, Kampf, Grotte und Ancient und schreibt `report.json`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"Champions of Luneraria.app/Contents/MacOS/Champions of Luneraria" -- --lune-smoke=/tmp/smoke/
|
||||||
|
ChampionsOfLuneraria.exe -- --lune-smoke=C:/Temp/smoke/
|
||||||
|
```
|
||||||
|
|
||||||
**Netzwerk (experimentell, pausiert):** Im Setup *Hosten* oder *Beitreten* mit Anzeigename, Adresse und Port (Standard 24571). Der Host ist autoritativ und rechnet den Kampf; Gäste sehen ihn über Momentaufnahmen. Freie Plätze sind Bots, bis jemand beitritt. Tempo, Pause und Revanche bestimmt der Host. „Bereit“ ist verbindlich. Ein Disconnect wird von einem Bot übernommen und ist kein Kernverlust; wer sich mit demselben Anzeigenamen wieder verbindet, bekommt seinen Platz zurück. Getestet sind Protokollchecks und ein lokaler Loopback über fünf Runden mit Wiedereinstieg – keine verifizierte Internetpartie auf sechs Rechnern, kein OpenGL- und kein Windows-11-Test. Kein Matchmaking, keine Hostmigration.
|
**Netzwerk (experimentell, pausiert):** Im Setup *Hosten* oder *Beitreten* mit Anzeigename, Adresse und Port (Standard 24571). Der Host ist autoritativ und rechnet den Kampf; Gäste sehen ihn über Momentaufnahmen. Freie Plätze sind Bots, bis jemand beitritt. Tempo, Pause und Revanche bestimmt der Host. „Bereit“ ist verbindlich. Ein Disconnect wird von einem Bot übernommen und ist kein Kernverlust; wer sich mit demselben Anzeigenamen wieder verbindet, bekommt seinen Platz zurück. Getestet sind Protokollchecks und ein lokaler Loopback über fünf Runden mit Wiedereinstieg – keine verifizierte Internetpartie auf sechs Rechnern, kein OpenGL- und kein Windows-11-Test. Kein Matchmaking, keine Hostmigration.
|
||||||
|
|
||||||
|
|
@ -97,4 +152,4 @@ godot --headless --path . res://tools/luneraria/net_pair.tscn -- --role=client -
|
||||||
|
|
||||||
## Benutzereinstellungen
|
## Benutzereinstellungen
|
||||||
|
|
||||||
Der neue Projektname legt einen neuen Benutzerordner an (`~/.local/share/godot/app_userdata/Champions of Luneraria`). Beim ersten Start übernimmt `Preferences` einmalig die `preferences.cfg` aus „Saumwacht · Glutpakt“. Die alte Datei bleibt unverändert; fehlt sie, gelten die Standardwerte.
|
Der neue Projektname legt einen neuen Benutzerordner an (Linux `~/.local/share/godot/app_userdata/Champions of Luneraria`, macOS `~/Library/Application Support/Godot/app_userdata/Champions of Luneraria`, Windows `%APPDATA%\Godot\app_userdata\Champions of Luneraria`). Beim ersten Start übernimmt `Preferences` einmalig die `preferences.cfg` aus „Saumwacht · Glutpakt“. Die alte Datei bleibt unverändert; fehlt sie, gelten die Standardwerte.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ Stand: 27. September 2026 (M0).
|
||||||
## Bestand (geprüft)
|
## Bestand (geprüft)
|
||||||
|
|
||||||
- **Werkzeuge:** Godot 4.7.2 (Fedora-Build; offizielle 4.7.2-Exportvorlagen unter `assets-source/phase6/export-templates`), Blender 5.2.2 LTS, Forward+ mit OpenGL-Kompatibilitätsmodus. Kein Engine-Update.
|
- **Werkzeuge:** Godot 4.7.2 (Fedora-Build; offizielle 4.7.2-Exportvorlagen unter `assets-source/phase6/export-templates`), Blender 5.2.2 LTS, Forward+ mit OpenGL-Kompatibilitätsmodus. Kein Engine-Update.
|
||||||
- **Repository:** Es gab kein Git. Angelegt wurde ein lokales Repository mit dem Baseline-Commit `c2b4476` auf `main` (unveränderter Stand 0.15.0, Phase 20A); die Arbeit läuft auf `luneraria/m1`. Screenshots und die Projekt-Doku außerhalb des Projektordners sind in `../backups/vor-luneraria-2026-09-27-docs-und-screenshots.tar.gz` gesichert. Keine uncommitteten fremden Änderungen; `../.mimocode` gehört zu einem fremden Werkzeug und bleibt unberührt. Ein Stand 20B existiert nicht. Keine CLAUDE.md.
|
- **Repository:** Es gab kein Git. Angelegt wurde ein lokales Repository mit dem Baseline-Commit `c2b4476` auf `main` (unveränderter Stand 0.15.0, Phase 20A); die Arbeit läuft auf `luneraria/m1`. Screenshots und die Projekt-Doku außerhalb des Projektordners sind in `../backups/vor-luneraria-2026-09-27-docs-und-screenshots.tar.gz` gesichert (seit G2: `../_archiv/backups/`). Keine uncommitteten fremden Änderungen; `../.mimocode` gehört zu einem fremden Werkzeug und bleibt unberührt. Ein Stand 20B existiert nicht. Keine CLAUDE.md.
|
||||||
- **Spiel:** eine Szene `scenes/main.tscn`, sieben Autoloads (`AssetLibrary`, `DataCatalog`, `GameState`, `EconomyManager`, `WaveManager`, `Preferences`, `Soundscape`), alle Oberflächen im Code. Kampf in `scripts/combat/`: Verteidiger gegen Angreifer je Lane, die Feindschaft hängt an `is_attacker`, die Lane an `team_id`.
|
- **Spiel:** eine Szene `scenes/main.tscn`, sieben Autoloads (`AssetLibrary`, `DataCatalog`, `GameState`, `EconomyManager`, `WaveManager`, `Preferences`, `Soundscape`), alle Oberflächen im Code. Kampf in `scripts/combat/`: Verteidiger gegen Angreifer je Lane, die Feindschaft hängt an `is_attacker`, die Lane an `team_id`.
|
||||||
- **Baseline-Tests** (`logs/m0-baseline/`): 209 · 33 · 463 · 206 · 1.665 bestanden.
|
- **Baseline-Tests** (`logs/m0-baseline/`): 209 · 33 · 463 · 206 · 1.665 bestanden.
|
||||||
|
|
||||||
|
|
@ -32,3 +32,16 @@ Stand: 27. September 2026 (M0).
|
||||||
- `lune_world.gd` baut Gelände, Wasser, Lanebeläge, Plattform, Deltabrücke, Basen, Grotte, Landmarken, Vegetationscluster und Aufmarschflächen aus Bestandsassets und Blockout-Primitiven.
|
- `lune_world.gd` baut Gelände, Wasser, Lanebeläge, Plattform, Deltabrücke, Basen, Grotte, Landmarken, Vegetationscluster und Aufmarschflächen aus Bestandsassets und Blockout-Primitiven.
|
||||||
- `lune_camera.gd` ist eine perspektivische Kamera, deren `zoom` die sichtbare Bodenhöhe bleibt; die Neigung hängt vom Zoom ab.
|
- `lune_camera.gd` ist eine perspektivische Kamera, deren `zoom` die sichtbare Bodenhöhe bleibt; die Neigung hängt vom Zoom ab.
|
||||||
- Die Grottenpunkte behalten ihre M2-Abstände zum Grottenmittelpunkt.
|
- Die Grottenpunkte behalten ihre M2-Abstände zum Grottenmittelpunkt.
|
||||||
|
11. **G2-Nachtrag (strategische Bauwerke):**
|
||||||
|
- **Views je Objekt:** Jedes strategische Objekt hat eine eigene View, die nur den autoritativen Zustand liest:
|
||||||
|
- `lune_tower_view.gd` (HP → Stufe, Angriffsfortschritt und Trefferzähler → Aufladen und Bolzenstart);
|
||||||
|
- `lune_ancient_view.gd`;
|
||||||
|
- `lune_grotto_view.gd` (fünf Zustände aus Runde, Claim, nächster Verfügbarkeit und dem Grottenboss des laufenden Kampfes).
|
||||||
|
Keine View schreibt in die Simulation.
|
||||||
|
- **Effekte:** `lune_tower_fx.gd` zeigt Zielring, Bolzen und Einschläge gepoolt; `lune_fx.gd` liefert begrenzte GPU-Partikelbausteine.
|
||||||
|
- **Welt:** `lune_world.gd` baut und synchronisiert alles in `sync_strategic` und `sync_grotto` und berechnet den Occlusion-Fade: Bildschirmrechteck des Gebäudes und Bodentiefe entlang der Blickrichtung, alle zwei Bilder.
|
||||||
|
- **Materialien:** `lune_materials.gd` ordnet `LN_*`-Oberflächen gemeinsamen ShaderMaterials zu (`view/materials/*.gdshader`). Alles Gebäude- oder Teamspezifische sind Instanzparameter mit festen Indizes (0–15). Der Fade wird im orthografischen Schattenpass übersprungen.
|
||||||
|
- **Assets:** Blender-Generatoren unter `assets-source/luneraria/g2/` mit gemeinsamem Baukasten `lune_bpy.py`. Sie schreiben `.blend`, GLB und `assets/luneraria/g2/g2_manifest.json`, das `AssetLibrary` zusätzlich liest (der Boss als skelettierte Figur).
|
||||||
|
- **Figuren:** `SkeletalFigure` kennt eine Schrittweite pro Modell und `play_event()` für Einmal-Clips (Brüllen, Sieg). `LuneUnitView` zeigt jede Boss-Einheit als Schlundkrone und färbt ihre Adern nach Team.
|
||||||
|
- **Gelände:** `LuneTerrain` hat eine kleine, symmetrische Senke hinter dem Grottentor (nur Darstellung). Die Grottenfläche und der Ancient-Dais heben Figuren an (`surface_lift`).
|
||||||
|
- **Plattform:** `lune_main.gd` startet mit `--lune-smoke` einen Plattform-Kurztest. `project.godot` importiert Texturen zusätzlich als ETC2/ASTC (Voraussetzung für den Apple-Silicon-Export) und führt die Luneraria-Version unter `[luneraria]`. Export-Presets: Linux, Windows x64, Windows ARM64, macOS Universal.
|
||||||
|
|
|
||||||
63
docs/luneraria/G2-ASSETS.md
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# G2 · Assetprüfung und Entscheidungsmatrix
|
||||||
|
|
||||||
|
Stand: 29. September 2026 · Branch `luneraria/g2` (von `luneraria/g1`, Commit `8b17563`).
|
||||||
|
|
||||||
|
Vor jeder neuen Modellierung wurde der Bestand geprüft: Kernkristalle, Türme, Podeste, Mauern, Basiselemente, Kristall-, Metall- und Steinmaterialien, Runen, Partikeleffekte und die vorhandenen Boss- und Monsterfiguren. Geprüft wurde aus der echten Spielkamera (G1-Vergleichsbilder `screenshots/g1/nachher`, G2-Vorher-Bilder `screenshots/g2/vorher`) und an den Quelldateien.
|
||||||
|
|
||||||
|
**Entscheidung:**
|
||||||
|
- **A** – unverändert wiederverwenden
|
||||||
|
- **B** – Material, Maßstab oder Licht überarbeiten
|
||||||
|
- **C** – modular erweitern
|
||||||
|
- **D** – neues Asset erforderlich
|
||||||
|
|
||||||
|
Funktionierende, hochwertige Assets bleiben erhalten; ersetzt wurde nur, was die G2-Anforderungen nicht tragen konnte (Zustände, Zerstörung, Silhouette aus der Spielkamera).
|
||||||
|
|
||||||
|
## Strategische Objekte
|
||||||
|
|
||||||
|
| Bestand | Pfad / Quelle | Befund in der Spielkamera | Entscheidung | G2-Ergebnis |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Saumkern Team 0/1 (Ancients bis G1) | `assets/models/core_team0.glb`, `…team1.glb` · `assets-source/phase4/blend/core_team*.blend` (36.652 / 36.848 Dreiecke, starre Gelenkgruppen) | Hochwertiger Kristall, aber aus der Spielkamera ein 3–4 m hoher Dekokristall. Kein Aufbau, der einen Ancient über einen Turm stellt; nur Aktivierung und Treffer, keine Zustandsstufen. | **C** | Der Kristallcluster (Core_CrystalMain und fünf Satelliten) ist das Herz des neuen **Mondherz** (`build_ancient.py` hängt ihn aus `core_team0.blend` an, Maßstab 2,3). Darum neu: Sockel, Podest, Klingen, Ringe, Krone, Wappen, Banner, Fragmente, Ruine. Die alten GLBs bleiben für den Legacy-Modus unverändert. |
|
||||||
|
| Laneturm M1 | `assets/luneraria/tower.glb` · `build_tower.py` (648 Dreiecke, ein Kristallmaterial) | Blockout-Qualität, eine Form für alle 16 Türme (Basistürme nur ×1,18), keine Teile für Schaden oder Zerstörung. Sank bei Zerstörung in den Boden. | **D** | Turmfamilie aus einem Generator: **Grenzwacht** (Tower 1), **Bastion** (Tower 2), **Kernwacht** (Basisturm), jeweils mit Lower/Upper/Upper_Damaged/Debris/Crystal/Ruin. `tower.glb` wird nicht mehr geladen (Datei bleibt als M1-Stand). |
|
||||||
|
| Bossgrotte M2 | `assets/luneraria/grotto.glb` · `build_grotto.py` (1.888 Dreiecke) | Felsbogen mit Höhlendunkel; in G1 nur noch Bogen und Kristalle über Blockout-Terrasse. Eingang aus der Spielkamera kaum als solcher lesbar. | **D** | **Mondschlund** (`build_bossgrotte.py`): Arena, Bossplattform, Altar mit Mondquelle, monumentales Tor mit Treppe in die Tiefe, Säulen, Mondwasserrinnen, Felsmassen; `grotto.glb` wird nicht mehr geladen. |
|
||||||
|
| G1-Grotten-Blockout | `lune_world.gd` (Arena-Scheibe, Runen-Torus, 11 Säulen, Klippen-Props, Treppen, Licht) | Platzhalter laut G1. | **D** (Treppen **A**) | Durch den Mondschlund ersetzt. Die zwei Teamtreppen und der Quell-Wasserfall bleiben unverändert. |
|
||||||
|
| Schlundkrone e10 (Grotten- und entsandter Boss bis G1) | `assets/models/enemy_e10.glb` · `phase4/blend/e10.blend` (64.694 Dreiecke, starr) | Heller Knochenschlund mit Zahnkranz, ×1,7 skaliert. Keine Todes-, Brüll- oder Siegesbewegung (starre Gruppen, „keine Todesanimation“). Wirkt als großer Wellengegner, nicht als uraltes Wesen der Welt. | **D** | Neuer **Schlundkrone**-Titan mit gewichtetem Skelett und 11 Clips (`build_schlundkrone.py`). Name, Schlund und „Krone“ bleiben als Designfaden erhalten. e10 bleibt für den Legacy-Modus. |
|
||||||
|
| Weitere Bosse e07 Bruchhirsch, e08 Riegelkoloss, e09 Maskenrichter | `phase4/blend/e07–e09.blend` (33.800–81.240 Dreiecke, starr) | Als Basis geprüft. Alle starr ohne Tod, Silhouetten aus der Saumwacht-Wellenlogik, keine lunarianische Identität. | — (verworfen) | Nicht verwendet. |
|
||||||
|
| Volksrepräsentant Troll „Steinhauer“ (p_troll) | `assets/luneraria/peoples/p_troll.glb` (22 Knochen, 9 Clips) | Als Rig-Basis geprüft: Er wäre nur ein großer Troll und damit eine weitere Einheit. | Konventionen **A**, Figur verworfen | Der Boss übernimmt die Pipeline-Konventionen des Rigs: Knochennamen für die Overlays (Spine, Head, Forearm.L/R), Clipnamen, Kontakt bei 0,90 s, 30 fps. |
|
||||||
|
| Kartenprops (Banner, Felsen, Klippen, Bordsteine, Brücke) | `assets/luneraria/map_props.glb` · `build_map_props.py` | Stimmig, MultiMesh, low-poly facettiert. | **A** (Banner an Bauwerken **C**) | Unverändert als Kartendekoration. Die alten Teambanner neben den Ancients ersetzt ein Stoffbanner je Bauwerk mit teamspezifischer Silhouette (LN_Cloth). Die facettierte Formensprache der Felsen übernimmt der neue `cliff()`-Baustein. |
|
||||||
|
| Basisplatz, Ringmauer, Torsäulen, Torkristalle (G1-Blockout) | `lune_world.gd` | Funktional, lesbar; G4-Thema. | **A** + **C** | Unverändert. Neu: Energieadern im Platz vom Ancient zu den Basistürmen und Toren, gestufter Sockel (Dais), Figuren werden auf Platz und Dais gehoben. |
|
||||||
|
| Ebene Turmsockel (Geländepads) | `LuneTerrain` | Passend. | **A** | Unverändert; jeder Turm bringt seinen eigenen Sockel mit. |
|
||||||
|
| Mittelplattform, Runensteine, Runenring | `lune_world.gd` | Blockout, Kartenthema (G4). | **A** | Unverändert. |
|
||||||
|
|
||||||
|
## Materialien
|
||||||
|
|
||||||
|
| Bestand | Befund | Entscheidung | G2-Ergebnis |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Kalkstein-Textursatz Phase 10 (`assets/materials/phase10/limestone-*`) | Hochwertig, bereits für Gelände und Kluft genutzt. | **A** | Triplanar in **LN_Stone** (Mondstein-Quader, Basalt, Grottenstein, Fels). |
|
||||||
|
| Kluft-Oberflächenshader (`sanctuary.gdshader`, ENV_*) | Für Gelände, Wege und Props richtig. | **A** | Bleibt für Karte und Props; Bauwerke bekommen die neue LN-Familie. |
|
||||||
|
| Kristallmaterialien: TW_Crystal (M1-Turm, emissives Standardmaterial), `prismatic_core.gdshader` (Saumkern), GB_/FL_Crystal (Einheiten) | Flach bzw. an ein Modell gebunden. Keine Zustandskanäle (Energie, Flackern, Bruch), keine Teamfarbe pro Instanz. | **D** | **LN_Crystal**: dunkler Körper, Adern in drei Tiefenlagen (Parallaxe), Herz, Fresnel, kontrollierte Emission, Instanzparameter energy/flicker/fracture/tint. |
|
||||||
|
| Metall: `forged_material.gdshader` (Glutpakt-Einheiten) | Einheitenlook (Schmiede), nicht für Bauwerke. | **D** | **LN_Metal**: Teammetall (Silberstahl kühl, Bronze warm), Eisen, alte Bronze mit Patina in Hohlräumen; nie spiegelnd. |
|
||||||
|
| Runen: emissive Standardmaterialien (G1-Ringe) | Nur leuchtende Flächen, keine Glyphen. | **D** (G1-Ringe **A**) | **LN_Rune**: Glyphen aus der UV-Zelle, drei Runenfamilien (Blau: Mondsicheln, Rot: Sonnenstrahlen, Grotte: Spiralen), Energie, Flackern, Bruch. |
|
||||||
|
| Stoff | Bannerprop mit einfarbigem Material. | **D** | **LN_Cloth**: Wind im Vertexshader, Teamsilhouette (Schwalbenschwanz / Flammensaum) und Emblem, Zerreißen bei Schaden. |
|
||||||
|
| — | Mondwasser der Grotte, Kreaturenhaut des Bosses | **D** | **LN_Water_Glow**, **LN_Boss_Hide/Bone/Bronze** (Muster aus Ruhepositionen, damit auf dem gehäuteten Körper nichts schwimmt), **LN_Crystal_Boss**. |
|
||||||
|
| Holz | Nur Tür, Wimpelstange (Türme). | **A** (Standardmaterial) | LN_Wood als einfaches Standardmaterial; mehr ist an den Bauwerken nicht sinnvoll. |
|
||||||
|
|
||||||
|
## Effekte
|
||||||
|
|
||||||
|
| Bestand | Befund | Entscheidung | G2-Ergebnis |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Partikel: Sanktuariums-Glimmer (`sanctuary.gd`), Menübühne (`menu_stage.gd`) | Menü/Legacy, großflächige Umgebungseffekte. | **A** (dort) | Unverändert. |
|
||||||
|
| Turmangriff bis G1 | Unsichtbares Schadensereignis, nur Lichtpuls. | **D** | Zielerfassungsring, Aufladen des Kristalls, Kristallbolzen mit Schweif, Einschlag mit Splittern; alles gepoolt (`lune_tower_fx.gd`). |
|
||||||
|
| Schaden/Zerstörung | Turm sank bei G1 ab und verschwand. | **D** | Gebündelte Effektbausteine (`lune_fx.gd`): Funken, Rauch, Staubstoß, Splitterstoß, Schwebeteilchen, Energieleck, Blitz; jeweils mit fester Obergrenze, keine physikalischen Trümmer pro Frame. |
|
||||||
|
|
||||||
|
## Neue Assets (G2)
|
||||||
|
|
||||||
|
| Asset | Generator | .blend | GLB | Dreiecke | Knoten / Rig |
|
||||||
|
| --- | --- | --- | --- | ---: | --- |
|
||||||
|
| Mondherz (Ancient) | `assets-source/luneraria/g2/build_ancient.py` | `ancient.blend` | `assets/luneraria/g2/ancient.glb` | 19.032 | 27 Teile (Kristall, Satelliten, Klingen, Ringe, Krone, Wappen A/B, Fragmente, Ruine) |
|
||||||
|
| Grenzwacht (Tower 1) | `…/g2/build_towers.py` | `grenzwacht.blend` | `…/g2/grenzwacht.glb` | 6.516 | Lower, Upper, Upper_Damaged, Debris, Crystal, Pennant, Ruin |
|
||||||
|
| Bastion (Tower 2) | `…/g2/build_towers.py` | `bastion.blend` | `…/g2/bastion.glb` | 9.364 | + Ring, Shard_0–2 |
|
||||||
|
| Kernwacht (Basisturm) | `…/g2/build_towers.py` | `kernwacht.blend` | `…/g2/kernwacht.glb` | 9.224 | + Banner_L/R, Ring |
|
||||||
|
| Mondschlund (Bossgrotte) | `…/g2/build_bossgrotte.py` | `bossgrotte.blend` | `…/g2/bossgrotte.glb` | 15.182 | 12 Teile (Boden, Plattform, Altar, Quelle, Tor, Höhle, Leuchten, Rinnen, Säulen, Säulenkristalle, Runen, Felsen) |
|
||||||
|
| Schlundkrone (Superboss) | `…/g2/build_schlundkrone.py` | `schlundkrone.blend` | `…/g2/schlundkrone.glb` | 5.712 | 24 Knochen, 11 Clips |
|
||||||
|
|
||||||
|
Gemeinsame Bausteine: `assets-source/luneraria/g2/lune_bpy.py` (Drehkörper, Quader mit Fase, Sweeps, Kristalle, facettierte Felsen, Ringe, Runenbänder, Banner, Ambient Occlusion in die Vertexfarbe). Laufzeitmaterialien: `scripts/luneraria/view/lune_materials.gd` und `scripts/luneraria/view/materials/*.gdshader`. Manifest mit Hash, Quelle, Generator, Knoten, Materialien und Maßen: `assets/luneraria/g2/g2_manifest.json`.
|
||||||
366
docs/luneraria/G2-STRATEGISCHE-BAUWERKE.md
Normal file
|
|
@ -0,0 +1,366 @@
|
||||||
|
# G2 · Ancients, Türme und Bossgrotte
|
||||||
|
|
||||||
|
Stand: 29. September 2026 · Branch `luneraria/g2` (von `luneraria/g1`, Commit `8b17563`) · Demo 0.17.0-g2.
|
||||||
|
Assetprüfung und Entscheidungsmatrix: [G2-ASSETS.md](G2-ASSETS.md).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Kurzfassung
|
||||||
|
|
||||||
|
- **Neue Bauwerke:** Die strategischen Objekte sind neu gebaut, jeweils mit reproduzierbarem Blender-Generator:
|
||||||
|
- zwei **Mondherz**-Ancients;
|
||||||
|
- eine dreistufige Turmfamilie: **Grenzwacht** (Tower 1), **Bastion** (Tower 2), **Kernwacht** (Basisturm);
|
||||||
|
- die Bossgrotte **Mondschlund**;
|
||||||
|
- der Superboss **Schlundkrone** mit Skelett und 11 Animationen.
|
||||||
|
- **Klare Zustände:** Alle Bauwerke haben Zustandsstufen, eine kurze Zerstörung und einen bleibenden Ruinenzustand. Die Grotte hat fünf Zustände.
|
||||||
|
- **Sichtbarer Turmangriff:** Zielerfassung, Aufladen, Bolzen, Einschlag. Der Schaden bleibt vollständig in der Simulation.
|
||||||
|
- **Gemeinsame Materialfamilie** `LN_*` für Stein, Metall, Kristall, Rune, Stoff, Mondwasser und Kreaturenhaut (auch für G4/G5).
|
||||||
|
- **Occlusion-Fade:** Große Gebäude werden oberhalb der Kopfhöhe dithernd durchsichtig, wenn Figuren oder der Kamerafokus dahinter liegen.
|
||||||
|
- **Regeln unverändert:** Die 40 Balancepartien ergeben exakt dieselben Werte wie in G1. Dieselbe Partie läuft mit und ohne Darstellung identisch.
|
||||||
|
- **Neue Plattformen:** macOS-Paket (Universal, ad-hoc signiert) und Windows ARM64; beide Windows-Pakete liefen in Windows 11 (Parallels, ARM).
|
||||||
|
|
||||||
|
## Ausgangszustand
|
||||||
|
|
||||||
|
- G1 (`8b17563`): spielerisches Drei-Lane-Layout, Blockout-Grotte, Saumkern-Ancients aus Saumwacht, der Laneturm aus M1 (648 Dreiecke) für alle 16 Türme. Bis auf die Ancients (Aktivierung, Treffer) gab es keine Zustände. Türme sanken bei Zerstörung in den Boden und verschwanden.
|
||||||
|
- G2 begann am 29. September unter Fedora 44 und wurde am selben Tag auf einem Mac (Apple M5, macOS 27) abgeschlossen. Unter Fedora entstanden bereits Turmfamilie, Mondherz, Materialshader und Turmeffekte. Auf dem Mac kamen hinzu: Bossgrotte, Superboss, Occlusion, Tests, Aufnahmen, Messungen, Plattformen und Doku.
|
||||||
|
- Die Vorher-Aufnahmen (`screenshots/g2/vorher`) stammen aus dem G1-Stand. Sie wurden auf demselben Mac mit demselben Aufnahmewerkzeug gemacht wie die Nachher-Aufnahmen.
|
||||||
|
|
||||||
|
## Wiederverwendete und neue Assets
|
||||||
|
|
||||||
|
Wiederverwendet sind:
|
||||||
|
- der Kristallcluster des Saumkerns als Herz des Ancients;
|
||||||
|
- Kalkstein-Textursatz, Kluft-Shader und Kartenprops;
|
||||||
|
- die G1-Basisbauteile (Plaza, Mauern, Tore) und die Grottentreppen.
|
||||||
|
|
||||||
|
Neu sind sechs GLBs: [Tabelle in G2-ASSETS.md](G2-ASSETS.md#neue-assets-g2).
|
||||||
|
|
||||||
|
| Asset | Dreiecke | Höhe (Spiel) | Besonderheit |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| Mondherz (Ancient) | 19.032 | 10,4 m | 27 bewegliche Teile, Wappen A/B, Ruinenteile |
|
||||||
|
| Grenzwacht (Tower 1) | 6.516 | 6,1 m | Upper/Lower getrennt, Schadensvariante, Ruine |
|
||||||
|
| Bastion (Tower 2) | 9.364 | 7,2 m | Ring und drei Satellitensplitter |
|
||||||
|
| Kernwacht (Basisturm) | 9.224 | 8,3 m | Klingenbogen in der Formensprache des Ancients |
|
||||||
|
| Mondschlund (Grotte) | 15.182 | 7,7 m | 12 Teile, prüft beim Bau den Lane-Abstand |
|
||||||
|
| Schlundkrone (Boss) | 5.712 | 5,6 m (×1,7) | 24 Knochen, 11 Clips, Adern in Teamfarbe |
|
||||||
|
|
||||||
|
Pipeline:
|
||||||
|
- Jeder Generator liegt unter `assets-source/luneraria/g2/` und schreibt `.blend`, GLB und Manifesteintrag: Hash, Quelle, Generator, Knoten, Materialien, Maße, Pivot am Boden. Ausgaben: `assets/luneraria/g2/`.
|
||||||
|
- **Reproduzierbarkeit:** Unter macOS (Blender 5.2.2) entstehen dieselben Dreiecke und Maße wie zuvor unter Fedora. Die Dateien sind aber nicht bytegleich (Gleitkomma in der Occlusion-Berechnung); `verify_g2` prüft, dass jedes GLB seinem Manifest-Hash entspricht.
|
||||||
|
- Der Import in Godot ist geprüft: Knoten, Clips, Skelett und Materialzuordnung (`verify_g2`, Teil A).
|
||||||
|
|
||||||
|
## Ancient: Mondherz
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Designsprache:** Ein lunarianisches Energieheiligtum statt eines Turms.
|
||||||
|
- Der Saumkern-Kristall (×2,3) schwebt als Herz über einem dreistufigen Dais mit Runenbändern.
|
||||||
|
- Vier Mondsichelklingen bilden eine offene Lanzette.
|
||||||
|
- Darüber: Krone und Teamwappen – Blau eine Mondsichel, Rot eine Sonnenscheibe.
|
||||||
|
- Zwei Armillarringe drehen sich gegenläufig, fünf Satelliten kreisen, sechs Fragmente schweben.
|
||||||
|
- Zwei Banner mit Teamsilhouette (Blau Schwalbenschwanz, Rot Flammensaum).
|
||||||
|
- Energieadern laufen im Platz zu den beiden Basistürmen und zu den drei Lanetoren. Stirbt ein Basisturm, erlischt seine Ader.
|
||||||
|
- Die Basistürme (Kernwacht) nehmen die Klingensprache auf, sodass Ancient und Basistürme als ein Heiligtum lesbar sind.
|
||||||
|
|
||||||
|
**Teamunterschied** bei identischer Geometrie, Hitbox und Position:
|
||||||
|
- Kristall- und Runenfarbe: kühl bzw. warm;
|
||||||
|
- Metall: Silberstahl bzw. Bronze;
|
||||||
|
- Runenfamilie: Mondglyphen bzw. Sonnenstrahlen;
|
||||||
|
- Bannersilhouette und Wappen.
|
||||||
|
|
||||||
|
**Zustände** aus der autoritativen HP (`lune_ancient_view.gd`):
|
||||||
|
|
||||||
|
| HP | Zustand | Darstellung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 100–61 % | stabil | ruhige Rotation, gleichmäßiges Licht, wenige Schwebeteilchen |
|
||||||
|
| 60–31 % | instabil | leuchtende Risse im Stein, Energie flackert, erste Funken, ein Satellit sackt ab und zittert, Ringe eiern |
|
||||||
|
| 30–1 % | kritisch | starke Risse, Herz gebrochen und unregelmäßig pulsierend, Energieleck (Funkenstöße), Rauch, zwei Klingen lose (eine weit ausgekippt), innerer Ring aus dem Lager gerutscht (schief, ruckelt), zwei Fragmente liegen auf dem Dais, Banner zerrissen |
|
||||||
|
| 0 % | Zusammenbruch (2,8 s) → Ruine | Herz überlädt, implodiert, Lichtblitz und Splitterstoß, Klingen zerfallen im Staub, Ringe und Krone stürzen auf den Sockel, Fragmente fallen; danach Trümmer und gebrochener Kristall, Rauch für 25 s |
|
||||||
|
|
||||||
|
**Der Sieg wartet nicht auf die Grafik.** Die Regel entscheidet im selben Simulationsschritt, in dem der Ancient 0 HP erreicht; die Partie ist ab dann vorbei. Der Zusammenbruch beginnt erst danach. `verify_g2` prüft beides: kein Kollaps vor der Entscheidung, Partie bleibt beendet, während die Animation läuft.
|
||||||
|
|
||||||
|
## Türme
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
| | Grenzwacht (Tower 1) | Bastion (Tower 2) | Kernwacht (Basisturm) |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Silhouette | schlanker Achteckturm, offene Galerie | breite geböschte Bastion mit Strebepfeilern | kein Rundschaft: Runentrommel, zwei Sichelklingen als Lanzettbogen |
|
||||||
|
| Schutz | wenig Metall | Schildplatten zur Lane, Pechnasen, mehr Stein und Metall | schwerer Sockel, Bronzebänder |
|
||||||
|
| Energie | mittlerer Kristall zwischen zwei Hörnern | großer Kristall, drei Hörner, Armillarring, drei Splitter | Kristall im Klingenbogen, Ring, zwei Banner |
|
||||||
|
| Höhe | 6,1 m | 7,2 m | 8,3 m |
|
||||||
|
|
||||||
|
Alle drei teilen:
|
||||||
|
- achteckigen Grundriss, Mondstein-Quader auf Basaltsockel;
|
||||||
|
- Teammetall, Runenfriese und die Kristallwiege.
|
||||||
|
|
||||||
|
Die Kernwacht ist kein skalierter Tower 2, sondern gehört zur Architektur des Ancients.
|
||||||
|
|
||||||
|
**Angriff** (`lune_tower_view.gd`, `lune_tower_fx.gd`):
|
||||||
|
1. **Zielerfassung:** Ein Ring unter dem Ziel der Regel zieht sich beim Aufladen zusammen.
|
||||||
|
2. **Aufladen:** Der Kristall dreht schneller und wird heller.
|
||||||
|
3. **Schuss:** Der Bolzen verlässt die Kristallspitze, sobald der Angriffszyklus der Regel zu 74 % voll ist. Er fliegt in Simulationszeit (Pause und Tempo wirken) und landet auf dem Treffer der Regel.
|
||||||
|
4. **Einschlag:** Blitz, Splitter, kurzes Zucken des getroffenen Gebäudes.
|
||||||
|
|
||||||
|
Die Bolzen sind gepoolt (maximal 24), haben die Teamfarbe und sind klein genug für große Kämpfe. Die Effekte lesen nur Ziel, Fortschritt und Trefferzähler der Regel; sie können keinen Schaden, kein Gold und kein Ziel erzeugen. Gemessen: 13 Treffer, 14 Bolzen in 12 s (ein Bolzen war vorausgeflogen).
|
||||||
|
|
||||||
|
**Schaden und Zerstörung:**
|
||||||
|
|
||||||
|
| Stufe | Darstellung |
|
||||||
|
| --- | --- |
|
||||||
|
| leicht (unter 60,5 %) | Risse, Ruß, dünner Rauch, Wimpel zerrissen |
|
||||||
|
| schwer (unter 30,5 %) | gebrochener Oberbau (fehlende Zinnen, verbogenes Horn, lose Platte), Schutt am Fuß, Funken, flackernder Kristall |
|
||||||
|
| zerstört (1,7 s) | Kristall überlädt und platzt (Splitterstoß), Oberbau kippt und sinkt, Staubstoß |
|
||||||
|
| Ruine | Stumpf, Schutt, gebrochener Kristall, Rauch für 18 s, kein Licht |
|
||||||
|
|
||||||
|
Trümmer sind vorgefertigte Teile, keine Physik pro Frame. Ruinen blockieren nichts; die Simulation kennt keine Gebäudekollision, und `verify_g2` prüft, dass Armeen an einer Ruine vorbeimarschieren.
|
||||||
|
|
||||||
|
## Bossarena: Mondschlund
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Aufbau** (Ort, Terrasse, Bosspunkt und Eingänge wie in G1):
|
||||||
|
- **Arena:** Grottenstein mit großem Runenkreis, zweistufige Bossplattform, dahinter ein Sichelaltar mit der Mondquelle.
|
||||||
|
- **Tor:** Das monumentale Tor steht in der Achse – zwei gehörnte Wächterpylonen, tiefer Bogen mit Bronze-Sichel, Treppe hinab ins Dunkel, leuchtender Nebel und ein Mondbrunnen.
|
||||||
|
- **Umgebung:** Alte Säulen stehen seitlich (intakt, gebrochen, umgestürzt), Mondwasser in zwei Rinnen, facettierte Felsmassen.
|
||||||
|
- **Sichtbarkeit:** Der Eingang ist schon aus der Lane-Kamera von Top und Mid lesbar.
|
||||||
|
|
||||||
|
**Geprüfter Befund aus G1:** Die Top-Lane läuft auf drei Seiten um die Grotte. Die erste Felsplanung ragte bis zu 2,8 m in ihren Korridor. Seitdem liest der Generator die echten Lane-Linien (`grotto_site.json` aus `tools/luneraria/dump_grotto_site.gd`) und bricht ab, wenn ein Fels oder das Tor näher als 0,6 m an einen Korridor reicht.
|
||||||
|
|
||||||
|
**Kleine Geländekorrektur (dokumentiert):** Die Höhle hinter dem Tor liegt 1,05 m unter der Terrasse.
|
||||||
|
- Umsetzung: `LuneTerrain._cave_notch`, 3 × 3,6 m, symmetrisch zur Achse.
|
||||||
|
- Grund: Ohne die Senke schnitt der ansteigende Hang die Höhle und ihre Treppe (Gebäudemaße).
|
||||||
|
- Wirkung auf das Spiel: keine. Die Senke ist reine Darstellung, wird von Felsen und Höhlenwänden verdeckt, und keine Figur steht dort.
|
||||||
|
- Prüfung: `verify_g1` (Spiegelung, Terrasse, Quelle) besteht unverändert.
|
||||||
|
|
||||||
|
**Zustände** (`lune_grotto_view.gd`, nur aus öffentlichem Zustand, Cooldown-Regeln unverändert):
|
||||||
|
|
||||||
|
| Zustand | Wann | Darstellung |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| verfügbar | Runde ≥ nächste Verfügbarkeit | Boss ruht auf dem Dais (Adern halb aktiv), Runen, Mondquelle, Höhlenlicht und Mondwasser an, Schwebeteilchen |
|
||||||
|
| Kampf | Expedition in der Grotte | stärkeres, pulsierendes Licht, Funken; der ruhende Boss wird zur echten Einheit, die beim Erwachen brüllt |
|
||||||
|
| nicht verfügbar | Boss ist für ein Team entsandt | gedämpft, Runen schwach in der Farbe des erobernden Teams, kein Boss |
|
||||||
|
| Cooldown | Boss gefallen / Sperrrunden | erschöpft: Quelle dunkel und gebrochen, Runen fast aus, Mondwasser leer; beim Fall des Bosses ein einmaliger Energiekollaps (Blitz, Splitter, Staub) |
|
||||||
|
| bald verfügbar | letzte Sperrrunde | erste Energie: Runen flackern auf, Quelle glimmt, wenige Teilchen |
|
||||||
|
|
||||||
|
Das Licht der Grotte ist bewusst zurückgenommen: zwei Punktlichter ohne Schatten (Höhle, Quelle), damit Arena und Boss nicht violett überstrahlen.
|
||||||
|
|
||||||
|
## Superboss: Schlundkrone
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*Clipbogen aus `tools/luneraria/g2_boss_sheet.tscn`: Spielkamera, Mittelplattform. Größenvergleich mit Truppe, Held und Angriffschampion im selben Bild: [schlundkrone-massstab-mitte.png](screenshots/g2/schlundkrone-massstab-mitte.png).*
|
||||||
|
|
||||||
|
**Prüfung des Bestands:** Kein Monster taugte als Basis (siehe [G2-ASSETS.md](G2-ASSETS.md)).
|
||||||
|
- Das bisherige e10 „Schlundkrone“ ist ein starrer Wellenboss ohne Tod.
|
||||||
|
- e07–e09 sind starr.
|
||||||
|
- Der Troll-Repräsentant wäre nur eine große Einheit.
|
||||||
|
|
||||||
|
Neu gebaut, der Name blieb.
|
||||||
|
|
||||||
|
**Design:** ein uralter, kristallgebundener Titan Lunerarias, der von den Lunariern in den Mondschlund gekettet wurde. Die Silhouette trägt das Bild, nicht das Leuchten:
|
||||||
|
- **Haltung:** gebeugt, auf den Knöcheln gehend, mit riesigen Schultermassen, sehr langen Unterarmen und kurzen Säulenbeinen. Keine andere Figur steht so.
|
||||||
|
- **„Krone“:** ein gebrochener Halbmond-Nimbus aus Kristallsplittern, der hinter dem tief sitzenden Kopf schwebt; ein Splitter fehlt.
|
||||||
|
- **„Schlund“:** Die Brust öffnet sich wie ein Maul (zwei Platten mit Mondsteinzähnen) um einen leuchtenden Herzkristall.
|
||||||
|
- **Details:** Sichelhörner; ein Kristallcluster wächst nur aus der linken Schulter (asymmetrisch); gebrochene Bronzefesseln an beiden Handgelenken.
|
||||||
|
- **Teamfarbe:** Feine Mondadern im Basalt glühen in der Farbe des Teams, für das der Boss kämpft (violett in der Grotte, blau oder orange als Verstärkung). So funktioniert dasselbe Modell neutral und als Lane-Verstärkung.
|
||||||
|
- **Abgrenzung:** keine Roshan-/Baron-Kopie – kein gehörnter Pelzbrute, kein Wurm; Stein, Kristall und Mondmotive.
|
||||||
|
|
||||||
|
**Rig und Animation:**
|
||||||
|
- 24 Knochen, starre Steinteile mit Gewicht 1,0; die Overlay-Knochen von `SkeletalFigure` sind vorhanden.
|
||||||
|
- Clips (30 fps): `idle`, `start`, `walk`, `stop`, `attack_a` (beidhändiger Schmetterschlag), `attack_b` (Rückhand), `hit`, `ability` („Mondschrei“: Schlund öffnet sich, Krone steigt), `death` (Knie geben nach, Sturz nach vorn, die Krone bricht ab und fällt), `roar` (beim Erwachen in der Grotte), `victory` (wenn die Expeditionen scheitern).
|
||||||
|
- Treffer landen bei 0,90 s wie bei allen Figuren; die Regel entscheidet jeden Treffer, die Clips folgen nur ihren Ereignissen.
|
||||||
|
- Neu in `SkeletalFigure`: `stride` pro Modell (Boss 2,0 m pro Schrittzyklus statt 0,52 m) und `play_event()` für Brüllen und Sieg.
|
||||||
|
- Alle Muster der Haut stammen aus Ruhepositionen (UV/UV2), damit auf dem gehäuteten Körper nichts schwimmt.
|
||||||
|
|
||||||
|
**Als Angriffseinheit geprüft** (`verify_g2` Teil F):
|
||||||
|
- Auf allen drei Lanes erscheint der entsandte Boss für das richtige Team, rückt vor, wird von Türmen beschossen und steht auf Boden, Brücke und Plattform.
|
||||||
|
- Maßstab ×1,7, Fußabdruck kompakt (ca. 1,4 m Radius), damit Angreifer im Nahkampfabstand der Regel nicht im Körper stehen.
|
||||||
|
- In großen Kämpfen bleibt er an Krone, Silhouette und Namensschild erkennbar ([Boss als Lane-Verstärkung](screenshots/g2/nachher/12-boss-lane-verstaerkung.png)).
|
||||||
|
- Grotten- und Lane-Boss nutzen ein gemeinsames Modell; es gibt keine zweite Version.
|
||||||
|
|
||||||
|
## Maßstab
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
| Objekt | Höhe im Spiel |
|
||||||
|
| --- | ---: |
|
||||||
|
| Truppe (Dornhopser) | 1,35 m |
|
||||||
|
| Angriffschampion (Keilbrecher) | 1,82 m |
|
||||||
|
| Held (Ringborke ×1,25) | 2,32 m |
|
||||||
|
| Schlundkrone (×1,7, mit Krone) | 5,64 m |
|
||||||
|
| Grenzwacht | 6,06 m |
|
||||||
|
| Bastion | 7,16 m |
|
||||||
|
| Kernwacht | 8,33 m |
|
||||||
|
| Mondherz | 10,37 m |
|
||||||
|
|
||||||
|
Die Hierarchie Einheiten < Boss < Tower 1 < Tower 2 < Basisturm < Ancient prüft `verify_g2`.
|
||||||
|
|
||||||
|
## Materialsystem
|
||||||
|
|
||||||
|
`scripts/luneraria/view/lune_materials.gd` ersetzt jede `LN_*`-Oberfläche eines GLB durch ein gemeinsames Material. Alles, was je Gebäude oder Team abweicht, sind Instanzparameter mit festen Plätzen: Teamfarbe, Energie, Flackern, Bruch, Schaden, Ruß, Runenfamilie, Bannerform, Fade. So teilen sich alle Gebäude wenige Materialien.
|
||||||
|
|
||||||
|
| Familie | Shader | Eigenschaften |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **STONE** | `lune_stone.gdshader` | Kalkstein triplanar, prozedurale Quader mit Farbdrift je Block, Fugen und Relief, Flechten auf Oberseiten, Occlusion aus Vertexfarbe, Risse (Voronoi) mit optionalem Energieleuchten, Ruß; Varianten Mondstein, Basalt, Grottenstein, Fels |
|
||||||
|
| **METAL** | `lune_metal.gdshader` | Teammetall (Silberstahl / Bronze), Eisen, alte Bronze mit Patina in Hohlräumen; gehämmert, Kanten poliert, nie spiegelnd |
|
||||||
|
| **CRYSTAL** | `lune_crystal.gdshader` | dunkler Körper, Adern in drei Tiefen (Parallaxe), Herz, Fresnel, facettierte Helligkeit, kontrollierte Emission, Zustandskanäle |
|
||||||
|
| **RUNE** | `lune_rune.gdshader` | Glyphen je UV-Zelle, drei Familien (Mond, Sonne, Grotte), Energie, Flackern, Bruch |
|
||||||
|
| **CLOTH** | `lune_cloth.gdshader` | Wind, Teamsilhouette, Emblem, Zerreißen |
|
||||||
|
| **Mondwasser** | `lune_water_glow.gdshader` | dunkles Wasser, nur Wellenkämme leuchten |
|
||||||
|
| **Kreaturenhaut** | `lune_hide.gdshader` | Basalt/Mondstein/Bronze aus Ruhepositionen, Mondadern in Teamfarbe, Trefferblitz |
|
||||||
|
| WOOD | Standardmaterial | nur Tür und Wimpelstange |
|
||||||
|
|
||||||
|
Der Fade wird im Schattenpass übersprungen, damit Schatten nie dithern.
|
||||||
|
|
||||||
|
## Licht
|
||||||
|
|
||||||
|
- **Anzahl:** 16 Turmlichter (7–9 m Reichweite), 2 Ancient-Lichter (13 m) und 2 Grottenlichter, keines mit Schatten. Schatten wirft weiterhin nur die Sonne.
|
||||||
|
- **Zweck:** Das Licht zeigt Teamzugehörigkeit (Farbe), Zustand (Energie, Flackern, aus in der Ruine) und Orientierung (Grotteneingang).
|
||||||
|
- **Gegen die Lampenansammlung:** Leuchtende Kristalle, dünne Ringe, Splitter und Stoffe werfen keinen Schatten (weniger Schattenpass-Draws), und die Grottenlichter sind gedimmt.
|
||||||
|
|
||||||
|
## Kamera und Occlusion
|
||||||
|
|
||||||
|
- **Befund:** Aus der Standardkamera verdeckt der Ancient den nördlichen Basisturm und alles dahinter; das Grottentor verdeckt einen Abschnitt der Top-Lane.
|
||||||
|
- **Fade:** Große Gebäude werden oberhalb von Kopfhöhe (Ancient ab 2,4 m, Türme ab 2,0 m, Grotte ab 2,2 m über dem Boden) per Bayer-Dithering bis 55 % ausgedünnt. Auslöser ist eine lebende Figur oder der Kamerafokus hinter ihnen, auf dem Bildschirm und auf der Bodenebene gemessen.
|
||||||
|
- **Nie unsichtbar:** Die Silhouette bleibt stehen, Sockel und Schatten bleiben voll.
|
||||||
|
- **Schlanke Laneturme** geben nur für Helden, Champions und den Boss nach, damit sie im Turmkampf nicht flackern. Bei der Grotte blenden nur Tor, Höhle und Felsen aus, nicht Säulen und Altar.
|
||||||
|
- **Kamerawinkel:** Die Abnahmebilder zeigen Basistürme und den Kampf am Ancient aus der Angreiferrichtung (Q/E-Drehung), so wie ein Spieler die Kamera stellen würde.
|
||||||
|
|
||||||
|
## Keine Spielregeländerungen
|
||||||
|
|
||||||
|
Unverändert sind:
|
||||||
|
- Goldökonomie, Helden, Items, Wellen, Turmwerte, Sperrregeln und Ancient-Sieg;
|
||||||
|
- Bossregeln und Boss-Cooldown, Lanezuweisung, KI und Netzwerkprotokoll.
|
||||||
|
|
||||||
|
Neue Dateien und Änderungen gibt es nur in Darstellung, Werkzeugen, `SkeletalFigure` und `AssetLibrary` (Manifest, Schritt, Ereignisclip), in `LuneTerrain` (Höhlensenke, nur Darstellung) und `project.godot` (ETC2/ASTC-Import für Apple Silicon, eigener Versionseintrag). Belege:
|
||||||
|
- Alle Regeltests bestehen mit unveränderten Erwartungswerten.
|
||||||
|
- Die 40 Balancepartien liefern Wert für Wert dieselbe Zusammenfassung wie G1.
|
||||||
|
- Eine ganze Bot-Partie endet mit Grafik exakt wie ohne.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
Gleiche Szene wie G1: Runde 13, sechs volle Armeen, Tempo 2×, gleiche Kamerapositionen (`tools/luneraria/lune_benchmark.tscn`). Jeder Stand wurde zweimal gemessen, auf demselben Mac mit denselben Einstellungen (Apple M5, macOS 27, Godot 4.7.2 Forward+ über Metal, 1920 × 1080, ETC2/ASTC). G1 lief aus einem Arbeitsbaum auf `8b17563` mit demselben Messskript.
|
||||||
|
|
||||||
|
| Messung | G1 | G2 |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| Lane-Ansicht: Bildzeit Mittel / p95 | 9,51 / 10,98 ms | 10,02 / 11,54 ms |
|
||||||
|
| Lane-Ansicht: CPU (Hauptthread) | 9,38 ms | 9,87 ms |
|
||||||
|
| Lane-Ansicht: Draw Calls / Objekte | 3.278 / 7.340 | 3.509 / 7.597 |
|
||||||
|
| Übersicht: Bildzeit Mittel / p95 | 9,48 / 10,29 ms | 9,95 / 10,74 ms |
|
||||||
|
| Übersicht: Draw Calls | 3.588 | 3.767 |
|
||||||
|
| Basis- und Grottenansicht | 8,33 ms (120-Hz-Grenze) | 8,33 ms (120-Hz-Grenze) |
|
||||||
|
| GPU-Last Basis / Grotte (ioreg) | 72 / 87 % | 76 / 90 % |
|
||||||
|
| Aktive Lichter / davon mit Schatten | 23 / 1 | 24 / 1 |
|
||||||
|
| Schattenwerfer (Knoten / Instanzen), Lane | 539 / 946 | 577 / 974 |
|
||||||
|
| Partikelsysteme aktiv / max. Teilchen | 0 / 0 | 2 / 44 |
|
||||||
|
| G2-Darstellungslogik je Bild | – | 0,25 ms |
|
||||||
|
| Szenenaufbau | 395 ms | 207 ms |
|
||||||
|
| Simulationsschritt | 1,8 ms | 1,9 ms |
|
||||||
|
| VRAM / Texturen | 819 / 411 MB | 825 / 399 MB |
|
||||||
|
| RAM statisch / Prozess (RSS) | 98 / 708 MB | 111 / 787 MB |
|
||||||
|
|
||||||
|
**Einordnung:**
|
||||||
|
- G2 kostet in den vollen Kampfansichten 0,5 ms (+5 %), sonst nichts Messbares.
|
||||||
|
- **Ursache** laut Kostenzerlegung (`tools/luneraria/g2_cost_split.tscn`, `logs/g2/cost-split.json`, pausierte Lane-Szene): Ohne Türme ist das Bild 0,54 ms schneller, mit schlichtem Material nur 0,07 ms. Es sind also die mehrteiligen Turmmodelle (mehr Draw Calls), nicht die Shader.
|
||||||
|
- **Größere Posten:** Figuren (2,4 ms) und Sonnenschatten (0,8 ms).
|
||||||
|
- **Maßnahme:** Nach der Analyse werfen Kristalle, Ringe, Splitter und Stoffe keinen Schatten mehr.
|
||||||
|
- **Keine pauschale Reduktion:** Qualität wurde nicht pauschal gesenkt. Mögliche spätere Schritte (G5/G6): Bauteile je Material zusammenfassen, Sichtbarkeitsbereiche für Kleinteile.
|
||||||
|
|
||||||
|
**Einschränkung der Messung auf dem Mac:**
|
||||||
|
- Godot 4.7.2 liefert unter Metal keine GPU-Zeitstempel (immer 0). Ersatzweise misst ein Thread die Auslastung der Grafikeinheit (`ioreg`, systemweit). Die Werte schwanken zwischen Läufen um bis zu ±15 Prozentpunkte.
|
||||||
|
- macOS begrenzt Fensterbilder auf die 120 Hz des Displays, auch mit VSync aus. Unter 8,33 ms ist die Bildzeit daher nicht messbar.
|
||||||
|
- Die G1-Referenz vom Fedora-PC (RTX 5060 Ti, `logs/g2/perf-g1-run*.json`) liegt bei 7,5 ms in der Lane-Ansicht bei 3,3 ms GPU. Die Zahlen beider Rechner sind nicht direkt vergleichbar.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
`sh tools/luneraria/run_g2_tests.sh` (Logs: `logs/g2/test-*.log`).
|
||||||
|
|
||||||
|
| Prüfung | Ergebnis |
|
||||||
|
| --- | --- |
|
||||||
|
| A · Regeln M1 `test_lune_core` (inkl. Spiegeltest) | 91/91 |
|
||||||
|
| A · Regeln M2 `test_lune_m2` | 73/73 |
|
||||||
|
| A · Stress M3 (Wellen bis 10⁶, Langlauf) | 17/17 |
|
||||||
|
| A · Balance M3-Methode, 40 Partien | Seitentausch 20/20, 9–13 Wellen, **identisch mit G1** |
|
||||||
|
| B · G1-Karte `verify_g1` | 443/443 |
|
||||||
|
| C–M · **G2-Abnahme `verify_g2`** | **245/245** |
|
||||||
|
| echte Anwendung `verify_m1` (1080p) | 38/38 |
|
||||||
|
| echte Anwendung `verify_m2` | 14/14 (Partie endete vor einer Bossentsendung, bekannte Variante) |
|
||||||
|
| Netzwerk (nur Regression) `test_lune_net` / Loopback `net_rounds.py` | 43/43 · 12/12 |
|
||||||
|
| Legacy Saumwacht | 209 · 463 |
|
||||||
|
|
||||||
|
`verify_g2` (echtes Spiel im Fenster, 78 s):
|
||||||
|
- **A · Assets (113):** Manifest, Hash je GLB, `.blend` und Generator vorhanden, Dreiecksbudget, Pivot am Boden, Laufzeitmaterial je `LN_*`-Material, Boss skelettiert mit allen 11 Clips, Kontaktzeit, Schrittweite, Overlay-Knochen.
|
||||||
|
- **B · Ansichten (28):** 6/6/4 Türme, 2 Ancients, Grotte mit ruhendem Boss, jede Ansicht exakt an ihrer Regelposition, Gebäuderadius der Regel unverändert, Teamunterschiede (Runenfamilie, Bannerform, Metall), höchstens 22 Lichter ohne Schatten, begrenzte Partikelsysteme.
|
||||||
|
- **C · Zustände (32):** Turm- und Ancient-Stufen an allen Grenzen (61/50/31/20/1 %), Schadensvariante, Energieleck, Zerstörung, Ruine bleibt stehen und hat kein Licht, alle fünf Grottenzustände samt Boss-Sichtbarkeit und Claim-Farbe.
|
||||||
|
- **D · Turmzielwahl und Angriff (6):** Türme zielen nur auf feindliche Figuren in Reichweite, der Zielring liegt unter dem Ziel der Regel, Bolzen fliegen zu Figuren, etwa ein Bolzen pro Treffer, kein Bolzen landet doppelt.
|
||||||
|
- **E · Regeln durch die Darstellung (8):** Keine gesperrte Stufe nimmt Schaden (Outer → Inner → Base); der Ancient nimmt keinen Schaden, solange ein Basisturm steht; Armeen marschieren an Ruinen vorbei; der Fall eines Ancients beendet die Partie sofort, der Kollaps folgt danach.
|
||||||
|
- **F · Boss (34):** Grotte erreichbar (Eingänge auf dem Boden, Weg innerhalb des Säulenrings, Boss auf dem Dais), Expedition weckt den Boss, er brüllt, fällt, spielt seinen Tod; die Grotte erschöpft, Blau erhält den Claim; Entsendung auf Top, Mid und Bottom mit Teamfarbe, Vorrücken, Turmbeschuss und Bodenkontakt.
|
||||||
|
- **G · Keine Doppelereignisse (3):** Vier Runden mit Seed 11, einmal nur Regeln, einmal mit voller Darstellung: jede Runde identisch (Gold, Gebäude, Boss).
|
||||||
|
- **H · Botpartien (7):** vier Seeds enden regulär nach 9–12 Wellen; eine ganze Partie auf dem Bildschirm endet exakt wie ohne, und jeder zerstörte Turm wird als zerstört gezeigt.
|
||||||
|
- **I · Occlusion und Maßstab (14):** Der Ancient dünnt aus, wenn der Fokus hinter ihm liegt (nie unsichtbar, Fade höchstens 0,55), und ist danach wieder voll; das Grottentor gibt der Top-Lane nach; Höhenordnung wie in der Tabelle „Maßstab“.
|
||||||
|
|
||||||
|
## Visuelle Prüfung
|
||||||
|
|
||||||
|
Echte Aufnahmen aus dem laufenden Spiel, 1920 × 1080, mit HUD. Nachher (G2) und vorher (G1) mit demselben Werkzeug `tools/luneraria/g2_views.tscn`, derselben Partie und denselben Kameras.
|
||||||
|
|
||||||
|
| Nr. | Ansicht | Nachher (G2) | Vorher (G1) |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | kompletter Ancient bei 100 % | [nachher](screenshots/g2/nachher/01-ancient-100.png) | [vorher](screenshots/g2/vorher/01-ancient-100.png) |
|
||||||
|
| 2 | Ancient bei ca. 50 % | [nachher](screenshots/g2/nachher/02-ancient-50.png) | [vorher](screenshots/g2/vorher/02-ancient-50.png) |
|
||||||
|
| 3 | Ancient unter 30 % | [nachher](screenshots/g2/nachher/03-ancient-unter-30.png) | [vorher](screenshots/g2/vorher/03-ancient-unter-30.png) |
|
||||||
|
| 4 | zerstörter Ancient (und Moment des Zusammenbruchs) | [nachher](screenshots/g2/nachher/04-ancient-zerstoert.png) · [Kollaps](screenshots/g2/nachher/04a-ancient-zerstoerung.png) | [vorher](screenshots/g2/vorher/04-ancient-zerstoert.png) |
|
||||||
|
| 5 | Tower 1 (Grenzwacht) | [nachher](screenshots/g2/nachher/05-tower1-grenzwacht.png) | [vorher](screenshots/g2/vorher/05-tower1-grenzwacht.png) |
|
||||||
|
| 6 | Tower 2 (Bastion) | [nachher](screenshots/g2/nachher/06-tower2-bastion.png) | [vorher](screenshots/g2/vorher/06-tower2-bastion.png) |
|
||||||
|
| 7 | Basistürme | [nachher](screenshots/g2/nachher/07-basistuerme.png) | [vorher](screenshots/g2/vorher/07-basistuerme.png) |
|
||||||
|
| 8 | zerstörter Turm (und Einsturz) | [nachher](screenshots/g2/nachher/08-turm-zerstoert.png) · [Einsturz](screenshots/g2/nachher/08a-turm-zerstoerung.png) | [vorher](screenshots/g2/vorher/08-turm-zerstoert.png) |
|
||||||
|
| 9 | Bossgrotte neutral (Boss entsandt) | [nachher](screenshots/g2/nachher/09-grotte-neutral-entsandt.png) | [vorher](screenshots/g2/vorher/09-grotte-neutral-entsandt.png) |
|
||||||
|
| 10 | Boss verfügbar | [nachher](screenshots/g2/nachher/10-boss-verfuegbar.png) | [vorher](screenshots/g2/vorher/10-boss-verfuegbar.png) |
|
||||||
|
| 11 | Bosskampf | [nachher](screenshots/g2/nachher/11-bosskampf.png) | [vorher](screenshots/g2/vorher/11-bosskampf.png) |
|
||||||
|
| 12 | Boss als Lane-Verstärkung | [nachher](screenshots/g2/nachher/12-boss-lane-verstaerkung.png) | [vorher](screenshots/g2/vorher/12-boss-lane-verstaerkung.png) |
|
||||||
|
| 13 | Kampf vor einem Tower | [nachher](screenshots/g2/nachher/13-kampf-vor-tower.png) | [vorher](screenshots/g2/vorher/13-kampf-vor-tower.png) |
|
||||||
|
| 14 | Kampf vor dem Ancient | [nachher](screenshots/g2/nachher/14-kampf-vor-ancient.png) | [vorher](screenshots/g2/vorher/14-kampf-vor-ancient.png) |
|
||||||
|
| 15 | Gesamtkarte | [nachher](screenshots/g2/nachher/15-gesamtkarte.png) | [vorher](screenshots/g2/vorher/15-gesamtkarte.png) |
|
||||||
|
| + | Maßstab, Ancient Rot, Grotte Cooldown / bald verfügbar | [16](screenshots/g2/nachher/16-massstab.png) · [17](screenshots/g2/nachher/17-ancient-rot-100.png) · [18a](screenshots/g2/nachher/18a-grotte-cooldown.png) · [18b](screenshots/g2/nachher/18b-grotte-bald-verfuegbar.png) | [16](screenshots/g2/vorher/16-massstab.png) · [17](screenshots/g2/vorher/17-ancient-rot-100.png) · [18a](screenshots/g2/vorher/18a-grotte-cooldown.png) · [18b](screenshots/g2/vorher/18b-grotte-bald-verfuegbar.png) |
|
||||||
|
|
||||||
|
**Videos** (1920 × 1080, 30 fps, H.264, echte Partie, Movie-Writer von Godot, Schnitte statt Zeitraffer):
|
||||||
|
- [g2-turmkampf.mp4](videos/g2-turmkampf.mp4), 22 s: Blaus Mid-Armee erreicht Rots Grenzwacht. Zielerfassung, Aufladen, Bolzen und Einschläge, Schnitt zu schwerem Schaden, dann Einsturz, Ruine und die Armee zieht vorbei.
|
||||||
|
- [g2-bossbegegnung.mp4](videos/g2-bossbegegnung.mp4), 26 s: Der Boss ruht, beide Expeditionen ziehen ein und kämpfen gegeneinander. Schnitt: Die Schlundkrone erwacht mit Gebrüll und kämpft; Schnitt zu ihrem Ende, die Grotte erschöpft.
|
||||||
|
|
||||||
|
Aufnahme und Kodierung: `tools/luneraria/g2_video.tscn` mit `--write-movie`, dann `tools/luneraria/encode_video.py` (FFmpeg aus Blender).
|
||||||
|
|
||||||
|
## Plattformen
|
||||||
|
|
||||||
|
| Paket | Test | Ergebnis |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| macOS Universal 0.17.0-g2 (App ad-hoc signiert, nicht notarisiert) | nach dem Entpacken wie im Finder: Signatur gültig; Kurzpartie `--lune-smoke` | Apple Silicon: Metal, Forward+, 120 fps · Intel-Teil unter Rosetta: Vulkan/MoltenVK, Forward+, 118 fps · beide **PASS** |
|
||||||
|
| Windows ARM64 0.17.0-g2 (neu) | Windows 11 Pro 24H2 ARM in Parallels, Kurzpartie im Desktop | Vulkan/D3D12 fehlen in der VM → automatischer Wechsel auf OpenGL 3 (Kompatibilitätsmodus), 65–69 fps, **PASS** |
|
||||||
|
| Windows x64 0.17.0-g2 | dieselbe VM, x64-Emulation (Prism) | ebenfalls OpenGL-Fallback, 46–61 fps, **PASS** |
|
||||||
|
| Linux x64 0.17.0-g2 | nur exportiert | auf diesem Mac nicht startbar (x86_64) |
|
||||||
|
|
||||||
|
Die Prüfsummen der Pakete stehen in `../dist/luneraria/build-manifest.json`; in der VM stimmen die Hashes überein. Berichte: `logs/g2/smoke-*.json`, Godot-Logs der VM `logs/g2/windows11-*-godot.log`, Bilder: `screenshots/g2/plattformen/`, darunter das echte Desktopfenster unter Windows.
|
||||||
|
|
||||||
|
**Einordnung:**
|
||||||
|
- Das ist der erste Lauf unter Windows 11, aber kein Test auf x64-Hardware mit echtem Vulkan/D3D12-Treiber.
|
||||||
|
- Zugleich ist es der erste Lauf des Kompatibilitätsmodus mit Luneraria: Die G2-Shader funktionieren dort, einschließlich der Instanzparameter; das Bild ist ohne SSAO und Glow dunkler.
|
||||||
|
|
||||||
|
## Bekannte Probleme
|
||||||
|
|
||||||
|
- GPU-Zeit ist auf dem Mac nur geschätzt (Metal ohne Zeitstempel in Godot 4.7.2), und die 120-Hz-Grenze verdeckt Unterschiede in leichten Ansichten.
|
||||||
|
- Die Occlusion ist bewusst grob: Bildschirmrechteck und Bodentiefe, alle zwei Bilder. Einzelne Figuren am Rand eines Gebäudes können kurz ein Ausdünnen auslösen. Das Dithering ist bei MSAA 2× als feines Raster sichtbar.
|
||||||
|
- Die Grottenfelsen stehen wegen der Top-Lane dicht an der Arena; aus manchen gedrehten Kameras wirkt die Felskulisse knapp.
|
||||||
|
- Der Boss als Lane-Verstärkung ist zwischen vielen blauen Einheiten der eigenen Seite an Größe und Krone erkennbar, farblich aber nah an ihnen.
|
||||||
|
- Der Victory-Clip läuft nur, wenn eine Grottenexpedition scheitert; beim Lane-Boss gibt es keine eigene Sieg- oder Einmarschsequenz, bewusst ohne Rutschen während des Marschs.
|
||||||
|
- Die GLBs sind zwischen Linux und macOS nicht bytegleich (Geometrie gleich).
|
||||||
|
- Die Windows-Tests liefen nur in einer ARM-VM mit OpenGL-Fallback.
|
||||||
|
- `tools/build_demo.py` (Saumwacht 0.15.0) passt seit M4 nicht mehr zu den umbenannten Presets. Die Luneraria-Pakete baut `tools/luneraria/build_luneraria_demo.py`.
|
||||||
|
|
||||||
|
## Offene Aufgaben für G3 (und später)
|
||||||
|
|
||||||
|
- **G3 HUD:**
|
||||||
|
- Zustände der Gebäude und der Grotte (bald verfügbar, Cooldown, Claim) auch im HUD und auf der Minimap spiegeln;
|
||||||
|
- Lebensbalken der Gebäude ins neue HUD-Design überführen;
|
||||||
|
- Ankündigungen für Bossentsendung und Turmfall mit den neuen Effekten verbinden.
|
||||||
|
- **Performance (G5/G6):** Turmteile je Material zusammenfassen, Sichtbarkeitsbereiche für Kleinteile, Figuren-Kosten (2,4 ms) angehen.
|
||||||
|
- **G4 Umgebung:** Basisplatz, Ringmauer, Tore und Landmarken noch Blockout; Felsen und Säulen der Grotte könnten Kartenmaterial und Gelände stärker aufnehmen.
|
||||||
|
- **G5 Licht:** Gesamtbalance der 20 Punktlichter mit dem neuen Himmel; Glow-Stärke im Kompatibilitätsmodus.
|
||||||
|
- **Windows auf x64-Hardware** mit nativem Vulkan/D3D12 (M4-Punkt bleibt offen); Notarisierung für macOS, falls die App weitergegeben werden soll.
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
# QA-Matrix
|
# QA-Matrix
|
||||||
|
|
||||||
Testumgebung aller Läufe: Fedora Linux 44 (KDE Plasma, XWayland), AMD Ryzen 5 5600X, NVIDIA GeForce RTX 5060 Ti (Vulkan 1.4.351), Godot 4.7.2 (Fedora-Build), Forward+. **Windows 11 ist nicht geprüft:** Es gibt keine Windows-Hardware in dieser Umgebung.
|
Testumgebung bis G1: Fedora Linux 44 (KDE Plasma, XWayland), AMD Ryzen 5 5600X, NVIDIA GeForce RTX 5060 Ti (Vulkan 1.4.351), Godot 4.7.2 (Fedora-Build), Forward+.
|
||||||
|
|
||||||
|
Testumgebung ab G2: Mac mit Apple M5 (10 GPU-Kerne, 16 GB), macOS 27, Godot 4.7.2 (offizieller Build) mit Forward+ über Metal 4, Blender 5.2.2 LTS. Alle M1–G1-Tests wurden dort erneut ausgeführt.
|
||||||
|
|
||||||
|
Windows 11 lief ab G2 nur in einer Parallels-VM (ARM, OpenGL-Fallback), nicht auf x64-Hardware.
|
||||||
|
|
||||||
Legende: ✅ automatisiert geprüft · 👁 in echten Aufnahmen visuell geprüft · ⏳ offen (Meilenstein) · — nicht zutreffend.
|
Legende: ✅ automatisiert geprüft · 👁 in echten Aufnahmen visuell geprüft · ⏳ offen (Meilenstein) · — nicht zutreffend.
|
||||||
|
|
||||||
|
|
@ -91,6 +95,30 @@ Weitere M2-Belege: `test_lune_m2` 73/73, darunter der Nachweis, dass die alte Ar
|
||||||
| Leistung vorher/nachher, gleiche Methode | ✅ | `logs/g1/perf-*-1920x1080.json` |
|
| Leistung vorher/nachher, gleiche Methode | ✅ | `logs/g1/perf-*-1920x1080.json` |
|
||||||
| OpenGL, Windows 11, Spieltests | ⏳ offen | — |
|
| OpenGL, Windows 11, Spieltests | ⏳ offen | — |
|
||||||
|
|
||||||
|
## G2 · Ancients, Türme und Bossgrotte
|
||||||
|
|
||||||
|
| Nachweis (Auftrag §25) | Stand | Beleg |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| A · alle Regeltests M1–M3 ohne geänderte Erwartungswerte | ✅ 91/91 · 73/73 · Stress 17/17 · Balance 40 Partien identisch mit G1 | `logs/g2/test-*.log`, `logs/g2/balance-summary.txt` |
|
||||||
|
| B · G1-Kartentests | ✅ 443/443 | `logs/g2/test-verify-g1.json` |
|
||||||
|
| C · Turmzielwahl (nur feindliche Figuren in Reichweite, Zielring unter dem Regelziel, Bolzen zum Ziel, ein Bolzen pro Treffer) | ✅ | `verify_g2` D |
|
||||||
|
| D · Tower-Reihenfolge (keine gesperrte Stufe nimmt Schaden) | ✅ | `verify_g2` E |
|
||||||
|
| E · Basistürme sperren den Ancient | ✅ | `verify_g2` E |
|
||||||
|
| F · Ancient-Zerstörung beendet die Partie sofort; der Kollaps folgt erst danach | ✅ | `verify_g2` E |
|
||||||
|
| G · Bossgrotte erreichbar (Eingänge auf dem Boden, Weg frei, Dais) | ✅ · 👁 | `verify_g2` F, `screenshots/g2/nachher/10-*.png` |
|
||||||
|
| H · Boss bekämpfbar (Erwachen mit Brüllen, Tod, Erschöpfung der Grotte, Claim) | ✅ · 👁 Video | `verify_g2` F, `videos/g2-bossbegegnung.mp4` |
|
||||||
|
| I · Boss entsendbar (Claim → Einsatz für das richtige Team) | ✅ | `verify_g2` F |
|
||||||
|
| J · Boss auf allen drei Lanes (Vorrücken, Turmbeschuss, Bodenkontakt, Teamfarbe) | ✅ Top/Mid/Bottom | `verify_g2` F |
|
||||||
|
| K · zerstörte Türme blockieren nicht; Ruine bleibt stehen | ✅ · 👁 Video | `verify_g2` E, `videos/g2-turmkampf.mp4` |
|
||||||
|
| L · keine Gold-/Schadensdoppelereignisse durch Animation/VFX | ✅ vier Runden mit und ohne Darstellung identisch; kein Bolzen landet doppelt | `verify_g2` G, D |
|
||||||
|
| M · mehrere komplette Botpartien | ✅ vier Seeds headless, eine ganze Partie mit Grafik exakt gleich | `verify_g2` H |
|
||||||
|
| Netzwerk nur als Regression | ✅ 43/43 · Loopback 12/12 | `logs/g2/test-net*.log` |
|
||||||
|
| Assets und Blender-Pipeline (Hash, Quelle, Generator, Pivot, Clips, Materialien) | ✅ 113 Prüfungen | `verify_g2` A, `assets/luneraria/g2/g2_manifest.json` |
|
||||||
|
| Zustände Türme/Ancients/Grotte, Teamidentität, Lichtbudget, Occlusion, Maßstab | ✅ | `verify_g2` B, C, I |
|
||||||
|
| 15 Pflichtansichten + Zusatzansichten, vorher/nachher, 1920 × 1080 | 👁 21 + 21 | `screenshots/g2/nachher`, `screenshots/g2/vorher` |
|
||||||
|
| Videos Turmkampf und Bossbegegnung | 👁 | `videos/` |
|
||||||
|
| Leistung G1 ↔ G2, gleiche Methode, gleicher Rechner | ✅ +0,5 ms in vollen Kampfansichten, Ursache ermittelt | `logs/g2/perf-vergleich-mac.json`, `cost-split.json` |
|
||||||
|
|
||||||
## Legacy-Regressionen (Saumwacht)
|
## Legacy-Regressionen (Saumwacht)
|
||||||
|
|
||||||
Unverändert über die Umleitung der alten QA-Schalter: 209 · 33 · 463 · 206 · 1.665 · Menü 20A 68. Logs: `logs/m0-baseline/`, `logs/m1/legacy-*.log`.
|
Unverändert über die Umleitung der alten QA-Schalter: 209 · 33 · 463 · 206 · 1.665 · Menü 20A 68. Logs: `logs/m0-baseline/`, `logs/m1/legacy-*.log`.
|
||||||
|
|
@ -99,6 +127,11 @@ Unverändert über die Umleitung der alten QA-Schalter: 209 · 33 · 463 · 206
|
||||||
|
|
||||||
| Plattform | Stand |
|
| Plattform | Stand |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Fedora 44, Forward+ | ✅ Quellprojekt (Tests, Aufnahmen, Leistung, Balance, Stress) |
|
| Fedora 44, Forward+ | ✅ Quellprojekt bis G1 (Tests, Aufnahmen, Leistung, Balance, Stress) |
|
||||||
| Fedora 44, Kompatibilitätsmodus (OpenGL) | ⏳ offen (für Saumwacht 0.15.0 geprüft, für Luneraria noch nicht; M4 pausiert) |
|
| Fedora 44, Kompatibilitätsmodus (OpenGL) | ⏳ offen (für Saumwacht 0.15.0 geprüft, für Luneraria noch nicht; M4 pausiert) |
|
||||||
| Windows 11 | ⏳ nicht geprüft (keine Hardware). Ein exportiertes Paket oder ein Wine-Lauf wäre kein nativer Test. |
|
| macOS 27, Apple M5, Forward+ (Metal) | ✅ Quellprojekt ab G2 (alle Tests, Aufnahmen, Videos, Leistung); Paket 0.17.0-g2 entpackt und mit `--lune-smoke` gespielt |
|
||||||
|
| macOS, Intel-Teil unter Rosetta (Vulkan/MoltenVK) | ✅ Paket 0.17.0-g2, `--lune-smoke` |
|
||||||
|
| Windows 11 Pro 24H2 ARM (Parallels), Paket ARM64 | ✅ `--lune-smoke`, automatisch OpenGL 3 (kein Vulkan/D3D12 in der VM), 65–69 fps |
|
||||||
|
| Windows 11 Pro 24H2 ARM (Parallels), Paket x64 unter Emulation | ✅ `--lune-smoke`, OpenGL 3, 46–61 fps |
|
||||||
|
| Windows 11 auf x64-Hardware | ⏳ nicht geprüft (keine Hardware) |
|
||||||
|
| Linux-Paket 0.17.0-g2 | exportiert, auf dem Mac nicht startbar |
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
# STATUS · Champions of Luneraria
|
# STATUS · Champions of Luneraria
|
||||||
|
|
||||||
Stand: 29. September 2026 · Branches `luneraria/m1` (M0/M1), `luneraria/m2`, `luneraria/m3`, `luneraria/m4`, `luneraria/g1` · Luneraria-Demo 0.16.0-m4.
|
Stand: 29. September 2026 · Branches `luneraria/m1` (M0/M1), `luneraria/m2`, `luneraria/m3`, `luneraria/m4`, `luneraria/g1`, `luneraria/g2` · Luneraria-Demo 0.17.0-g2 (macOS, Windows x64/ARM64, Linux).
|
||||||
|
|
||||||
|
Arbeitsrechner seit G2: Mac (Apple M5, macOS 27) mit Godot 4.7.2 und Blender 5.2.2 LTS; bis G1 Fedora 44 (RTX 5060 Ti). Werkzeugpfade und Plattformhinweise: [ANLEITUNG.md](ANLEITUNG.md).
|
||||||
|
|
||||||
| Meilenstein | Zustand |
|
| Meilenstein | Zustand |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
|
@ -8,9 +10,10 @@ Stand: 29. September 2026 · Branches `luneraria/m1` (M0/M1), `luneraria/m2`, `l
|
||||||
| M1 · Spielbarer Drei-Lane-Kern | **abgeschlossen** |
|
| M1 · Spielbarer Drei-Lane-Kern | **abgeschlossen** |
|
||||||
| M2 · Mechanikdurchlauf | **abgeschlossen** |
|
| M2 · Mechanikdurchlauf | **abgeschlossen** |
|
||||||
| M3 · Qualität und Balance | **abgeschlossen** (siehe unten) |
|
| M3 · Qualität und Balance | **abgeschlossen** (siehe unten) |
|
||||||
| M4 · Netzwerk und Pakete | **pausiert** (Host/Join, Mehr-Runden-Loopback, Wiedereinstieg; offen: OpenGL, Windows 11) |
|
| M4 · Netzwerk und Pakete | **pausiert** (Host/Join, Mehr-Runden-Loopback, Wiedereinstieg; offen: Windows auf x64-Hardware, Netz über mehrere Rechner) |
|
||||||
| G1 · Map-Blockout und Drei-Lane-Layout | **abgeschlossen** ([G1-KARTE.md](G1-KARTE.md)) |
|
| G1 · Map-Blockout und Drei-Lane-Layout | **abgeschlossen** ([G1-KARTE.md](G1-KARTE.md)) |
|
||||||
| G2–G6 · Ancients/Türme/Grotte, HUD, Umgebung, Licht, Einheiten | offen |
|
| G2 · Ancients, Türme und Bossgrotte | **abgeschlossen** ([G2-STRATEGISCHE-BAUWERKE.md](G2-STRATEGISCHE-BAUWERKE.md), [G2-ASSETS.md](G2-ASSETS.md)) |
|
||||||
|
| G3–G6 · HUD, Umgebung, Licht, Einheiten | offen |
|
||||||
|
|
||||||
## M1 · abgeschlossen
|
## M1 · abgeschlossen
|
||||||
|
|
||||||
|
|
@ -91,8 +94,8 @@ Stand: 29. September 2026 · Branches `luneraria/m1` (M0/M1), `luneraria/m2`, `l
|
||||||
|
|
||||||
## Blockiert
|
## Blockiert
|
||||||
|
|
||||||
- Nativer Windows-11-Test: keine Windows-Hardware.
|
- Windows auf x64-Hardware mit echtem Vulkan/D3D12: keine solche Hardware. Seit G2 laufen beide Windows-Pakete in Windows 11 ARM (Parallels) mit OpenGL-Fallback.
|
||||||
- Kompatibilitätsmodus (OpenGL) für Luneraria: erst mit M4-Exporten gezielt prüfen.
|
- Notarisierung des macOS-Pakets: kein Apple-Entwicklerkonto; die App ist ad-hoc signiert.
|
||||||
|
|
||||||
## M4 · pausiert
|
## M4 · pausiert
|
||||||
|
|
||||||
|
|
@ -118,9 +121,9 @@ Auf Entscheidung des Auftraggebers ruht M4 seit dem 28. September 2026 zugunsten
|
||||||
**Visuell geprüft:** Host und Gast in zwei Fenstern mit Grafik, Kampf in Echtzeit. Gastsicht in Planung und Kampf von Runde 1 und 2 (`screenshots/m4/`). Runde 1: 872, Runde 2: 730 empfangene Momentaufnahmen, Prüfsummen gleich.
|
**Visuell geprüft:** Host und Gast in zwei Fenstern mit Grafik, Kampf in Echtzeit. Gastsicht in Planung und Kampf von Runde 1 und 2 (`screenshots/m4/`). Runde 1: 872, Runde 2: 730 empfangene Momentaufnahmen, Prüfsummen gleich.
|
||||||
|
|
||||||
**Offen (eingefroren)**
|
**Offen (eingefroren)**
|
||||||
- Kompatibilitätsmodus (OpenGL) für Luneraria.
|
- Kompatibilitätsmodus (OpenGL) für Luneraria. *G2: in Windows 11 ARM (Parallels) automatisch aktiv und lauffähig; eine gezielte Sichtprüfung auf Desktop-Hardware steht aus.*
|
||||||
- Nativer Windows-11-Test (blockiert: keine Hardware).
|
- Nativer Windows-11-Test. *G2: beide Windows-Pakete liefen in Windows 11 ARM in einer VM; x64-Hardware mit echtem Treiber fehlt weiter.*
|
||||||
- Neubau der Demo-Pakete mit der umgebauten Sitzung.
|
- Neubau der Demo-Pakete mit der umgebauten Sitzung. *G2: erledigt mit 0.17.0-g2; Netzwerkfunktionen darin unverändert und nur als Regression geprüft.*
|
||||||
- Partie mit sechs Menschen auf mehreren Rechnern bzw. über das Internet (bisher nur lokaler Loopback).
|
- Partie mit sechs Menschen auf mehreren Rechnern bzw. über das Internet (bisher nur lokaler Loopback).
|
||||||
- Wiedereinstieg ist nur an den Anzeigenamen gebunden (keine Konten); Übertragungsrate bei vielen Gästen über das Internet ist nicht gemessen.
|
- Wiedereinstieg ist nur an den Anzeigenamen gebunden (keine Konten); Übertragungsrate bei vielen Gästen über das Internet ist nicht gemessen.
|
||||||
- Öffentliches Matchmaking, Accounts, Ranglisten: außerhalb des Umfangs.
|
- Öffentliches Matchmaking, Accounts, Ranglisten: außerhalb des Umfangs.
|
||||||
|
|
@ -155,6 +158,44 @@ Details, Vorher/Nachher-Bilder und Messwerte: [G1-KARTE.md](G1-KARTE.md).
|
||||||
|
|
||||||
**Ungetestet:** OpenGL-Modus, Windows 11, schwächere GPUs, menschliche Spieltests des Layouts.
|
**Ungetestet:** OpenGL-Modus, Windows 11, schwächere GPUs, menschliche Spieltests des Layouts.
|
||||||
|
|
||||||
|
## G2 · abgeschlossen
|
||||||
|
|
||||||
|
Details, Bilder, Videos und Messwerte: [G2-STRATEGISCHE-BAUWERKE.md](G2-STRATEGISCHE-BAUWERKE.md). Assetprüfung: [G2-ASSETS.md](G2-ASSETS.md).
|
||||||
|
|
||||||
|
**Implementiert**
|
||||||
|
- **Mondherz-Ancients** (Saumkern-Kristall als Herz, Sichelklingen, Ringe, Krone, Wappen, Fragmente, Dais, Banner, Energieadern zu Basistürmen und Toren):
|
||||||
|
- Stufen stabil, instabil und kritisch; danach ein 2,8 s langer Zusammenbruch und eine bleibende Ruine;
|
||||||
|
- der Sieg wartet nie auf die Grafik.
|
||||||
|
- **Turmfamilie** aus einem Generator: Grenzwacht (Tower 1), Bastion (Tower 2), Kernwacht (Basisturm in der Formensprache des Ancients).
|
||||||
|
- Schaden leicht und schwer, Einsturz und Ruine.
|
||||||
|
- Sichtbarer Angriff: Zielerfassung, Aufladen, Bolzen, Einschlag. Der Schaden bleibt in der Regel.
|
||||||
|
- **Bossgrotte Mondschlund:** Tor mit Treppe in die Tiefe, Altar mit Mondquelle, Säulen, Felsen, Mondwasser. Fünf Zustände: verfügbar, Kampf, nicht verfügbar (entsandt), Cooldown, bald verfügbar. Die Grotte hält beim Bau automatisch Abstand zur Top-Lane.
|
||||||
|
- **Superboss Schlundkrone:** neues Modell mit 24 Knochen und 11 Clips, darunter Brüllen beim Erwachen und Sieg. Adern in Teamfarbe; dasselbe Modell als Grotten- und Lane-Boss.
|
||||||
|
- **Materialfamilie `LN_*`:** Stein, Metall, Kristall, Rune, Stoff, Mondwasser, Kreaturenhaut. Pro Gebäude nur Instanzparameter; wiederverwendbar für G4/G5.
|
||||||
|
- **Occlusion-Fade** für Ancient, Basistürme und Grottentor (schlanke Türme nur für Helden, Champions und Boss); Schatten dithern nie.
|
||||||
|
- **Werkzeuge:** `verify_g2`, `run_g2_tests.sh`, Abnahmebilder, Videoaufnahme mit MP4-Kodierung über Blender, Kostenaufteilung, Boss-Clipbogen, Kamera-Probe, erweiterter Benchmark (GPU-Last unter macOS über `ioreg`).
|
||||||
|
- **Plattformen:**
|
||||||
|
- macOS-Paket (Universal, ad-hoc signiert) und Windows-ARM64-Paket;
|
||||||
|
- Plattform-Kurztest `--lune-smoke`;
|
||||||
|
- eigene Luneraria-Version im Menü.
|
||||||
|
|
||||||
|
**Automatisiert geprüft**
|
||||||
|
- `verify_g2` 245/245 (Assets, Ansichten, Zustände, Turmzielwahl, Turmreihenfolge, Basis-Sperre, Ancient-Ende, Grotte, Boss auf allen Lanes, Ruinen, keine Doppelereignisse, Botpartien, Occlusion, Maßstab).
|
||||||
|
- Regeln 91/91 und 73/73, Stress 17/17, `verify_g1` 443/443, `verify_m1` 38/38, `verify_m2` 14/14, Netz 43/43 und 12/12, Legacy 209 und 463.
|
||||||
|
- Balance 40 Partien: Ergebnis Wert für Wert identisch mit G1.
|
||||||
|
|
||||||
|
**Visuell geprüft:** 21 Ansichten nachher/vorher (1920 × 1080) aus der Spielkamera, zwei Videos (Turmkampf, Bossbegegnung), Plattformbilder macOS und Windows 11.
|
||||||
|
|
||||||
|
**Leistung** (Mac M5, 1080p): Die volle Kampfansicht braucht 10,0 statt 9,5 ms (+5 %). Ursache sind die mehrteiligen Turmmodelle (Draw Calls), nicht die Shader. Die Lichter bleiben ohne Schatten.
|
||||||
|
|
||||||
|
**Ungetestet**
|
||||||
|
- Windows auf x64-Hardware (nur Windows 11 ARM in Parallels mit OpenGL-Fallback);
|
||||||
|
- Linux-Paket (nur exportiert);
|
||||||
|
- schwächere GPUs;
|
||||||
|
- menschliche Spieltests der neuen Lesbarkeit.
|
||||||
|
|
||||||
## Nächster Schritt
|
## Nächster Schritt
|
||||||
|
|
||||||
G2 · Ancients, Türme und Bossgrotte: vorhandene Kristall- und Turmassets zu monumentalen Ancients (Zustandsstufen), einem modularen Turmsystem (Angriffsbereich, Treffer, Schaden, Zerstörung) und einer echten Grotte mit Zuständen ausbauen; Teamidentität über Banner, Runen und Architektur. Danach G3 (HUD). M4 ruht; beim Wiederaufnehmen zuerst OpenGL-Sichtprüfung, Windows-11-Protokoll auf fremder Hardware und Neubau der Demos.
|
G3 · HUD: das neue Ingame-HUD. Gebäude- und Grottenzustände (bald verfügbar, Cooldown, Claim) auch im HUD und auf der Minimap spiegeln. Lebensbalken der Gebäude ins HUD-Design überführen. Ankündigungen mit den neuen Effekten verbinden.
|
||||||
|
|
||||||
|
M4 ruht weiter. Beim Wiederaufnehmen zuerst Windows auf x64-Hardware prüfen (der OpenGL-Fallback ist in der VM bereits gelaufen) und die Netzpartie über mehrere Rechner.
|
||||||
|
|
|
||||||
6
docs/luneraria/logs/g2/balance-summary.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
Partien 40 · Ergebnisse {'blau': 20, 'rot': 20} · Testlimit 0 · Zeitfenster-Kämpfe 4 · längster Kampf 180 s
|
||||||
|
Wellen bis Ende: min 9 · median 11 · max 13
|
||||||
|
Seitentausch: dieselbe Zusammensetzung gewinnt beide Male in 20/20 Paaren
|
||||||
|
Profile (Siege/Einsätze): {'ausgewogen': '30/48 (62 %)', 'bossjagd': '8/34 (24 %)', 'druck': '20/34 (59 %)', 'fernkampf': '26/48 (54 %)', 'teamhilfe': '18/48 (38 %)', 'verteidigung': '18/28 (64 %)'}
|
||||||
|
Bünde: {'glutpakt': '66/124 (53 %)', 'wurzelbund': '54/116 (47 %)'}
|
||||||
|
Boss: {'won': 20, 'deployed': 12} · Heldenstufen am Ende (Median): 4
|
||||||
1756
docs/luneraria/logs/g2/balance.json
Normal file
43
docs/luneraria/logs/g2/cost-split.json
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
{
|
||||||
|
"engine": "4.7.2-stable (official)",
|
||||||
|
"gpu": "Apple M5 (Apple9)",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"draw_calls": 4181.0,
|
||||||
|
"frame_ms": 10.7308927613941,
|
||||||
|
"variant": "everything"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draw_calls": 4007.0,
|
||||||
|
"frame_ms": 10.1861577608142,
|
||||||
|
"variant": "towers hidden"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draw_calls": 4184.0,
|
||||||
|
"frame_ms": 10.6550106382979,
|
||||||
|
"variant": "towers with a plain material"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draw_calls": 4067.0,
|
||||||
|
"frame_ms": 11.0736519337017,
|
||||||
|
"variant": "Ancients hidden"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draw_calls": 4132.0,
|
||||||
|
"frame_ms": 10.9000054495913,
|
||||||
|
"variant": "grotto hidden"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draw_calls": 797.0,
|
||||||
|
"frame_ms": 8.3336,
|
||||||
|
"variant": "figures hidden"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"draw_calls": 2514.0,
|
||||||
|
"frame_ms": 9.91786386138614,
|
||||||
|
"variant": "sun shadows off"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"scene": "round 13, six full armies, lane view (-10, 10) zoom 34, combat paused",
|
||||||
|
"window": "(1920, 1080)"
|
||||||
|
}
|
||||||
137
docs/luneraria/logs/g2/perf-g1-mac-run1-1920x1080.json
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
{
|
||||||
|
"base_view": {
|
||||||
|
"camera_zoom": 26.0,
|
||||||
|
"cpu_frame_ms_mean": 8.20410277777778,
|
||||||
|
"draw_calls_mean": 793.4,
|
||||||
|
"frame_ms_mean": 8.33426111111112,
|
||||||
|
"frame_ms_p95": 9.07,
|
||||||
|
"frames": 720,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 72.9333333333333,
|
||||||
|
"Renderer Utilization %": 69.3333333333333,
|
||||||
|
"Tiler Utilization %": 31.4,
|
||||||
|
"gpu_memory_in_use_mb": 2069.40625,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 6.07845443703704,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 60.1319444444444,
|
||||||
|
"objects_mean": 2892.14722222222,
|
||||||
|
"primitives_mean": 2351347.26388889,
|
||||||
|
"process_ms_median": 9.765,
|
||||||
|
"render_setup_ms_median": 0.13,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 775,
|
||||||
|
"shadow_caster_nodes": 368,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buffer_mem_mb": 170.629276275635,
|
||||||
|
"buildings": 18,
|
||||||
|
"cpu": "Apple M5",
|
||||||
|
"engine": "4.7.2-stable (official)",
|
||||||
|
"gpu": "Apple M5 (Apple9)",
|
||||||
|
"grotto_view": {
|
||||||
|
"camera_zoom": 22.0,
|
||||||
|
"cpu_frame_ms_mean": 8.21077638888888,
|
||||||
|
"draw_calls_mean": 1544.86666666667,
|
||||||
|
"frame_ms_mean": 8.33355416666666,
|
||||||
|
"frame_ms_p95": 9.075,
|
||||||
|
"frames": 720,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 86.8,
|
||||||
|
"Renderer Utilization %": 85.1333333333333,
|
||||||
|
"Tiler Utilization %": 6.6,
|
||||||
|
"gpu_memory_in_use_mb": 2145.328125,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 7.23352501666666,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 31.1972222222222,
|
||||||
|
"objects_mean": 3799.31111111111,
|
||||||
|
"primitives_mean": 5039960.575,
|
||||||
|
"process_ms_median": 9.966,
|
||||||
|
"render_setup_ms_median": 0.125,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 666,
|
||||||
|
"shadow_caster_nodes": 259,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lane_view": {
|
||||||
|
"camera_zoom": 34.0,
|
||||||
|
"cpu_frame_ms_mean": 9.37060917721519,
|
||||||
|
"draw_calls_mean": 3277.73575949367,
|
||||||
|
"frame_ms_mean": 9.50233702531646,
|
||||||
|
"frame_ms_p95": 10.99,
|
||||||
|
"frames": 632,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 93.4,
|
||||||
|
"Renderer Utilization %": 93.2666666666667,
|
||||||
|
"Tiler Utilization %": 1.33333333333333,
|
||||||
|
"gpu_memory_in_use_mb": 2041.359375,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 8.87518278164558,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 117.460443037975,
|
||||||
|
"objects_mean": 7339.43670886076,
|
||||||
|
"primitives_mean": 16049036.7689873,
|
||||||
|
"process_ms_median": 11.034,
|
||||||
|
"render_setup_ms_median": 0.216,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 946,
|
||||||
|
"shadow_caster_nodes": 539,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mobile_units_at_start": 120,
|
||||||
|
"overview": {
|
||||||
|
"camera_zoom": 92.0,
|
||||||
|
"cpu_frame_ms_mean": 9.32390708661418,
|
||||||
|
"draw_calls_mean": 3588.19842519685,
|
||||||
|
"frame_ms_mean": 9.46169448818898,
|
||||||
|
"frame_ms_p95": 10.285,
|
||||||
|
"frames": 635,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 99.2666666666667,
|
||||||
|
"Renderer Utilization %": 99.2666666666667,
|
||||||
|
"Tiler Utilization %": 0.2,
|
||||||
|
"gpu_memory_in_use_mb": 2080.859375,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 9.39230872860892,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 95.6393700787402,
|
||||||
|
"objects_mean": 8119.78897637795,
|
||||||
|
"primitives_mean": 16745232.2362205,
|
||||||
|
"process_ms_median": 10.604,
|
||||||
|
"render_setup_ms_median": 0.19,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 883,
|
||||||
|
"shadow_caster_nodes": 476,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"process_rss_mb": 707.6875,
|
||||||
|
"renderer": "forward_plus",
|
||||||
|
"scene_build_ms": 410.292,
|
||||||
|
"sim_speed": "2x (40 steps/s)",
|
||||||
|
"sim_step_ms": 1.682385,
|
||||||
|
"static_mem_mb": 97.9976396560669,
|
||||||
|
"texture_mem_mb": 410.491943359375,
|
||||||
|
"video_mem_mb": 819.3125,
|
||||||
|
"window": "(1920, 1080)"
|
||||||
|
}
|
||||||
137
docs/luneraria/logs/g2/perf-g1-mac-run2-1920x1080.json
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
{
|
||||||
|
"base_view": {
|
||||||
|
"camera_zoom": 26.0,
|
||||||
|
"cpu_frame_ms_mean": 8.2084854368932,
|
||||||
|
"draw_calls_mean": 792.780859916782,
|
||||||
|
"frame_ms_mean": 8.33413453536755,
|
||||||
|
"frame_ms_p95": 9.032,
|
||||||
|
"frames": 721,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 71.0666666666667,
|
||||||
|
"Renderer Utilization %": 68.2,
|
||||||
|
"Tiler Utilization %": 30.3333333333333,
|
||||||
|
"gpu_memory_in_use_mb": 2299.359375,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 5.92279160980121,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 60.0263522884882,
|
||||||
|
"objects_mean": 2891.3120665742,
|
||||||
|
"primitives_mean": 2347600.24965326,
|
||||||
|
"process_ms_median": 9.686,
|
||||||
|
"render_setup_ms_median": 0.127,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 775,
|
||||||
|
"shadow_caster_nodes": 368,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buffer_mem_mb": 170.629276275635,
|
||||||
|
"buildings": 18,
|
||||||
|
"cpu": "Apple M5",
|
||||||
|
"engine": "4.7.2-stable (official)",
|
||||||
|
"gpu": "Apple M5 (Apple9)",
|
||||||
|
"grotto_view": {
|
||||||
|
"camera_zoom": 22.0,
|
||||||
|
"cpu_frame_ms_mean": 8.21486527777779,
|
||||||
|
"draw_calls_mean": 1542.56944444444,
|
||||||
|
"frame_ms_mean": 8.3336013888889,
|
||||||
|
"frame_ms_p95": 8.917,
|
||||||
|
"frames": 720,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 86.5333333333333,
|
||||||
|
"Renderer Utilization %": 84.7333333333333,
|
||||||
|
"Tiler Utilization %": 1.66666666666667,
|
||||||
|
"gpu_memory_in_use_mb": 2341.9375,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 7.21134306851853,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 31.0888888888889,
|
||||||
|
"objects_mean": 3795.80138888889,
|
||||||
|
"primitives_mean": 5025679.72222222,
|
||||||
|
"process_ms_median": 9.583,
|
||||||
|
"render_setup_ms_median": 0.116,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 666,
|
||||||
|
"shadow_caster_nodes": 259,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lane_view": {
|
||||||
|
"camera_zoom": 34.0,
|
||||||
|
"cpu_frame_ms_mean": 9.38248019017433,
|
||||||
|
"draw_calls_mean": 3278.88114104596,
|
||||||
|
"frame_ms_mean": 9.51605388272584,
|
||||||
|
"frame_ms_p95": 10.979,
|
||||||
|
"frames": 631,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 95.5333333333333,
|
||||||
|
"Renderer Utilization %": 94.6666666666667,
|
||||||
|
"Tiler Utilization %": 10.3333333333333,
|
||||||
|
"gpu_memory_in_use_mb": 2329.953125,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 9.09100347596408,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 117.462757527734,
|
||||||
|
"objects_mean": 7341.16006339144,
|
||||||
|
"primitives_mean": 16053505.473851,
|
||||||
|
"process_ms_median": 12.017,
|
||||||
|
"render_setup_ms_median": 0.218,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 946,
|
||||||
|
"shadow_caster_nodes": 539,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mobile_units_at_start": 120,
|
||||||
|
"overview": {
|
||||||
|
"camera_zoom": 92.0,
|
||||||
|
"cpu_frame_ms_mean": 9.35740031645571,
|
||||||
|
"draw_calls_mean": 3587.85443037975,
|
||||||
|
"frame_ms_mean": 9.49554430379748,
|
||||||
|
"frame_ms_p95": 10.297,
|
||||||
|
"frames": 632,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 73.7333333333333,
|
||||||
|
"Renderer Utilization %": 73.7333333333333,
|
||||||
|
"Tiler Utilization %": 0.333333333333333,
|
||||||
|
"gpu_memory_in_use_mb": 2457.90625,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 7.00138133333334,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 95.6123417721519,
|
||||||
|
"objects_mean": 8118.93037974684,
|
||||||
|
"primitives_mean": 16742561.7405063,
|
||||||
|
"process_ms_median": 12.178,
|
||||||
|
"render_setup_ms_median": 0.189,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 883,
|
||||||
|
"shadow_caster_nodes": 476,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"process_rss_mb": 707.828125,
|
||||||
|
"renderer": "forward_plus",
|
||||||
|
"scene_build_ms": 378.821,
|
||||||
|
"sim_speed": "2x (40 steps/s)",
|
||||||
|
"sim_step_ms": 1.8368,
|
||||||
|
"static_mem_mb": 97.9970102310181,
|
||||||
|
"texture_mem_mb": 410.491943359375,
|
||||||
|
"video_mem_mb": 819.4375,
|
||||||
|
"window": "(1920, 1080)"
|
||||||
|
}
|
||||||
97
docs/luneraria/logs/g2/perf-g1-run1-1920x1080.json
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
{
|
||||||
|
"base_view": {
|
||||||
|
"camera_zoom": 26.0,
|
||||||
|
"cpu_frame_ms_mean": 5.11963254593176,
|
||||||
|
"draw_calls_mean": 803.182852143482,
|
||||||
|
"frame_ms_mean": 5.25195538057743,
|
||||||
|
"frame_ms_p95": 6.964,
|
||||||
|
"frames": 1143,
|
||||||
|
"gpu_ms_mean": 1.74873141030523,
|
||||||
|
"mobile_units_mean": 61.9448818897638,
|
||||||
|
"objects_mean": 2906.01749781277,
|
||||||
|
"primitives_mean": 2414072.20822397,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 795,
|
||||||
|
"shadow_caster_nodes": 388,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buffer_mem_mb": 170.633762359619,
|
||||||
|
"buildings": 18,
|
||||||
|
"cpu": "AMD Ryzen 5 5600X 6-Core Processor",
|
||||||
|
"engine": "4.7.2-stable (fedora)",
|
||||||
|
"gpu": "NVIDIA GeForce RTX 5060 Ti",
|
||||||
|
"grotto_view": {
|
||||||
|
"camera_zoom": 22.0,
|
||||||
|
"cpu_frame_ms_mean": 5.34039344262295,
|
||||||
|
"draw_calls_mean": 1589.03369763206,
|
||||||
|
"frame_ms_mean": 5.46844444444444,
|
||||||
|
"frame_ms_p95": 6.592,
|
||||||
|
"frames": 1098,
|
||||||
|
"gpu_ms_mean": 2.32957012666379,
|
||||||
|
"mobile_units_mean": 33.5828779599271,
|
||||||
|
"objects_mean": 3872.60928961749,
|
||||||
|
"primitives_mean": 5337568.49180328,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 673,
|
||||||
|
"shadow_caster_nodes": 266,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lane_view": {
|
||||||
|
"camera_zoom": 34.0,
|
||||||
|
"cpu_frame_ms_mean": 7.34135455680399,
|
||||||
|
"draw_calls_mean": 3329.1847690387,
|
||||||
|
"frame_ms_mean": 7.48964169787765,
|
||||||
|
"frame_ms_p95": 10.278,
|
||||||
|
"frames": 801,
|
||||||
|
"gpu_ms_mean": 3.33393383294009,
|
||||||
|
"mobile_units_mean": 117.686641697878,
|
||||||
|
"objects_mean": 7420.0074906367,
|
||||||
|
"primitives_mean": 16254371.9275905,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 946,
|
||||||
|
"shadow_caster_nodes": 539,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mobile_units_at_start": 120,
|
||||||
|
"overview": {
|
||||||
|
"camera_zoom": 92.0,
|
||||||
|
"cpu_frame_ms_mean": 7.56214120667523,
|
||||||
|
"draw_calls_mean": 3601.80359435173,
|
||||||
|
"frame_ms_mean": 7.7092567394095,
|
||||||
|
"frame_ms_p95": 10.489,
|
||||||
|
"frames": 779,
|
||||||
|
"gpu_ms_mean": 3.55910141324201,
|
||||||
|
"mobile_units_mean": 96.586649550706,
|
||||||
|
"objects_mean": 8154.83825417202,
|
||||||
|
"primitives_mean": 16827184.0898588,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 890,
|
||||||
|
"shadow_caster_nodes": 483,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"process_rss_mb": 1419.484375,
|
||||||
|
"renderer": "forward_plus",
|
||||||
|
"scene_build_ms": 298.529,
|
||||||
|
"sim_speed": "2x (40 steps/s)",
|
||||||
|
"sim_step_ms": 2.78371,
|
||||||
|
"static_mem_mb": 128.257266044617,
|
||||||
|
"texture_mem_mb": 424.65576171875,
|
||||||
|
"video_mem_mb": 781.880187988281,
|
||||||
|
"window": "(1920, 1080)"
|
||||||
|
}
|
||||||
97
docs/luneraria/logs/g2/perf-g1-run2-1920x1080.json
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
{
|
||||||
|
"base_view": {
|
||||||
|
"camera_zoom": 26.0,
|
||||||
|
"cpu_frame_ms_mean": 4.65926198083067,
|
||||||
|
"draw_calls_mean": 806.240415335463,
|
||||||
|
"frame_ms_mean": 4.79412939297125,
|
||||||
|
"frame_ms_p95": 6.5,
|
||||||
|
"frames": 1252,
|
||||||
|
"gpu_ms_mean": 1.75445367429203,
|
||||||
|
"mobile_units_mean": 62.4952076677316,
|
||||||
|
"objects_mean": 2910.42811501597,
|
||||||
|
"primitives_mean": 2433559.71405751,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 795,
|
||||||
|
"shadow_caster_nodes": 388,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buffer_mem_mb": 170.633762359619,
|
||||||
|
"buildings": 18,
|
||||||
|
"cpu": "AMD Ryzen 5 5600X 6-Core Processor",
|
||||||
|
"engine": "4.7.2-stable (fedora)",
|
||||||
|
"gpu": "NVIDIA GeForce RTX 5060 Ti",
|
||||||
|
"grotto_view": {
|
||||||
|
"camera_zoom": 22.0,
|
||||||
|
"cpu_frame_ms_mean": 4.8310347107438,
|
||||||
|
"draw_calls_mean": 1593.26446280992,
|
||||||
|
"frame_ms_mean": 4.96114214876032,
|
||||||
|
"frame_ms_p95": 6.17,
|
||||||
|
"frames": 1210,
|
||||||
|
"gpu_ms_mean": 2.33310661000654,
|
||||||
|
"mobile_units_mean": 33.8380165289256,
|
||||||
|
"objects_mean": 3880.46033057851,
|
||||||
|
"primitives_mean": 5367975.61818182,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 673,
|
||||||
|
"shadow_caster_nodes": 266,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lane_view": {
|
||||||
|
"camera_zoom": 34.0,
|
||||||
|
"cpu_frame_ms_mean": 7.04803481392557,
|
||||||
|
"draw_calls_mean": 3333.61584633854,
|
||||||
|
"frame_ms_mean": 7.20265666266507,
|
||||||
|
"frame_ms_p95": 9.946,
|
||||||
|
"frames": 833,
|
||||||
|
"gpu_ms_mean": 3.33436734478872,
|
||||||
|
"mobile_units_mean": 117.68787515006,
|
||||||
|
"objects_mean": 7423.41656662665,
|
||||||
|
"primitives_mean": 16257552.909964,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 946,
|
||||||
|
"shadow_caster_nodes": 539,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mobile_units_at_start": 120,
|
||||||
|
"overview": {
|
||||||
|
"camera_zoom": 92.0,
|
||||||
|
"cpu_frame_ms_mean": 7.03338875598086,
|
||||||
|
"draw_calls_mean": 3605.72846889952,
|
||||||
|
"frame_ms_mean": 7.17908133971292,
|
||||||
|
"frame_ms_p95": 9.698,
|
||||||
|
"frames": 836,
|
||||||
|
"gpu_ms_mean": 3.56152751485697,
|
||||||
|
"mobile_units_mean": 96.8433014354067,
|
||||||
|
"objects_mean": 8164.83373205742,
|
||||||
|
"primitives_mean": 16851006.6220096,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 23,
|
||||||
|
"particle_systems": 0,
|
||||||
|
"particles_max": 0,
|
||||||
|
"shadow_caster_instances": 890,
|
||||||
|
"shadow_caster_nodes": 483,
|
||||||
|
"shadow_lights": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"process_rss_mb": 1418.43359375,
|
||||||
|
"renderer": "forward_plus",
|
||||||
|
"scene_build_ms": 295.259,
|
||||||
|
"sim_speed": "2x (40 steps/s)",
|
||||||
|
"sim_step_ms": 2.754265,
|
||||||
|
"static_mem_mb": 128.256976127625,
|
||||||
|
"texture_mem_mb": 424.65576171875,
|
||||||
|
"video_mem_mb": 781.880187988281,
|
||||||
|
"window": "(1920, 1080)"
|
||||||
|
}
|
||||||
141
docs/luneraria/logs/g2/perf-g2-mac-run1-1920x1080.json
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
{
|
||||||
|
"base_view": {
|
||||||
|
"camera_zoom": 26.0,
|
||||||
|
"cpu_frame_ms_mean": 8.18614166666667,
|
||||||
|
"draw_calls_mean": 917.075,
|
||||||
|
"frame_ms_mean": 8.33348611111111,
|
||||||
|
"frame_ms_p95": 9.007,
|
||||||
|
"frames": 720,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 76.0666666666667,
|
||||||
|
"Renderer Utilization %": 73.0666666666667,
|
||||||
|
"Tiler Utilization %": 9.13333333333333,
|
||||||
|
"gpu_memory_in_use_mb": 2067.265625,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 6.33900510185185,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 60.0041666666667,
|
||||||
|
"objects_mean": 3018.08333333333,
|
||||||
|
"primitives_mean": 2268093.53055556,
|
||||||
|
"process_ms_median": 9.996,
|
||||||
|
"render_setup_ms_median": 0.148,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 24,
|
||||||
|
"particle_systems": 2,
|
||||||
|
"particles_max": 44,
|
||||||
|
"shadow_caster_instances": 803,
|
||||||
|
"shadow_caster_nodes": 406,
|
||||||
|
"shadow_lights": 1
|
||||||
|
},
|
||||||
|
"strategic_view_ms_median": 0.225896273668343
|
||||||
|
},
|
||||||
|
"buffer_mem_mb": 173.200626373291,
|
||||||
|
"buildings": 18,
|
||||||
|
"cpu": "Apple M5",
|
||||||
|
"engine": "4.7.2-stable (official)",
|
||||||
|
"gpu": "Apple M5 (Apple9)",
|
||||||
|
"grotto_view": {
|
||||||
|
"camera_zoom": 22.0,
|
||||||
|
"cpu_frame_ms_mean": 8.19134812760055,
|
||||||
|
"draw_calls_mean": 1632.33980582524,
|
||||||
|
"frame_ms_mean": 8.33299306518724,
|
||||||
|
"frame_ms_p95": 9.073,
|
||||||
|
"frames": 721,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 90.0666666666667,
|
||||||
|
"Renderer Utilization %": 89.8,
|
||||||
|
"Tiler Utilization %": 1.13333333333333,
|
||||||
|
"gpu_memory_in_use_mb": 2065.28125,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 7.50524908737864,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 31.1081830790569,
|
||||||
|
"objects_mean": 3886.38834951456,
|
||||||
|
"primitives_mean": 5096285.88626907,
|
||||||
|
"process_ms_median": 9.557,
|
||||||
|
"render_setup_ms_median": 0.139,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 24,
|
||||||
|
"particle_systems": 2,
|
||||||
|
"particles_max": 44,
|
||||||
|
"shadow_caster_instances": 694,
|
||||||
|
"shadow_caster_nodes": 297,
|
||||||
|
"shadow_lights": 1
|
||||||
|
},
|
||||||
|
"strategic_view_ms_median": 0.218992503325459
|
||||||
|
},
|
||||||
|
"lane_view": {
|
||||||
|
"camera_zoom": 34.0,
|
||||||
|
"cpu_frame_ms_mean": 9.83786023294509,
|
||||||
|
"draw_calls_mean": 3508.5224625624,
|
||||||
|
"frame_ms_mean": 9.98521630615639,
|
||||||
|
"frame_ms_p95": 11.499,
|
||||||
|
"frames": 601,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 92.2666666666667,
|
||||||
|
"Renderer Utilization %": 91.4,
|
||||||
|
"Tiler Utilization %": 0.466666666666667,
|
||||||
|
"gpu_memory_in_use_mb": 2082.640625,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 9.21302624514697,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 117.447587354409,
|
||||||
|
"objects_mean": 7596.82362728785,
|
||||||
|
"primitives_mean": 15955220.5690516,
|
||||||
|
"process_ms_median": 12.548,
|
||||||
|
"render_setup_ms_median": 0.248,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 24,
|
||||||
|
"particle_systems": 2,
|
||||||
|
"particles_max": 44,
|
||||||
|
"shadow_caster_instances": 974,
|
||||||
|
"shadow_caster_nodes": 577,
|
||||||
|
"shadow_lights": 1
|
||||||
|
},
|
||||||
|
"strategic_view_ms_median": 0.246624349227022
|
||||||
|
},
|
||||||
|
"mobile_units_at_start": 120,
|
||||||
|
"overview": {
|
||||||
|
"camera_zoom": 92.0,
|
||||||
|
"cpu_frame_ms_mean": 9.76409421487603,
|
||||||
|
"draw_calls_mean": 3767.08595041322,
|
||||||
|
"frame_ms_mean": 9.9180826446281,
|
||||||
|
"frame_ms_p95": 10.707,
|
||||||
|
"frames": 605,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 98.4,
|
||||||
|
"Renderer Utilization %": 98.4,
|
||||||
|
"Tiler Utilization %": 0.466666666666667,
|
||||||
|
"gpu_memory_in_use_mb": 2240.109375,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 9.75939332231405,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 95.5107438016529,
|
||||||
|
"objects_mean": 8439.13719008265,
|
||||||
|
"primitives_mean": 16528799.1933884,
|
||||||
|
"process_ms_median": 12.067,
|
||||||
|
"render_setup_ms_median": 0.2,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 24,
|
||||||
|
"particle_systems": 2,
|
||||||
|
"particles_max": 44,
|
||||||
|
"shadow_caster_instances": 911,
|
||||||
|
"shadow_caster_nodes": 514,
|
||||||
|
"shadow_lights": 1
|
||||||
|
},
|
||||||
|
"strategic_view_ms_median": 0.253885407647745
|
||||||
|
},
|
||||||
|
"process_rss_mb": 785.796875,
|
||||||
|
"renderer": "forward_plus",
|
||||||
|
"scene_build_ms": 224.063,
|
||||||
|
"sim_speed": "2x (40 steps/s)",
|
||||||
|
"sim_step_ms": 1.779845,
|
||||||
|
"static_mem_mb": 110.706508636475,
|
||||||
|
"texture_mem_mb": 399.263427734375,
|
||||||
|
"video_mem_mb": 821.0,
|
||||||
|
"window": "(1920, 1080)"
|
||||||
|
}
|
||||||
141
docs/luneraria/logs/g2/perf-g2-mac-run2-1920x1080.json
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
{
|
||||||
|
"base_view": {
|
||||||
|
"camera_zoom": 26.0,
|
||||||
|
"cpu_frame_ms_mean": 8.18212916666667,
|
||||||
|
"draw_calls_mean": 917.081944444444,
|
||||||
|
"frame_ms_mean": 8.33228194444444,
|
||||||
|
"frame_ms_p95": 9.005,
|
||||||
|
"frames": 720,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 76.0666666666667,
|
||||||
|
"Renderer Utilization %": 73.2666666666667,
|
||||||
|
"Tiler Utilization %": 19.8666666666667,
|
||||||
|
"gpu_memory_in_use_mb": 2284.34375,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 6.33808913240741,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 60.0041666666667,
|
||||||
|
"objects_mean": 3018.08333333333,
|
||||||
|
"primitives_mean": 2268093.53055556,
|
||||||
|
"process_ms_median": 9.947,
|
||||||
|
"render_setup_ms_median": 0.147,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 24,
|
||||||
|
"particle_systems": 2,
|
||||||
|
"particles_max": 44,
|
||||||
|
"shadow_caster_instances": 803,
|
||||||
|
"shadow_caster_nodes": 406,
|
||||||
|
"shadow_lights": 1
|
||||||
|
},
|
||||||
|
"strategic_view_ms_median": 0.227367466784306
|
||||||
|
},
|
||||||
|
"buffer_mem_mb": 173.200626373291,
|
||||||
|
"buildings": 18,
|
||||||
|
"cpu": "Apple M5",
|
||||||
|
"engine": "4.7.2-stable (official)",
|
||||||
|
"gpu": "Apple M5 (Apple9)",
|
||||||
|
"grotto_view": {
|
||||||
|
"camera_zoom": 22.0,
|
||||||
|
"cpu_frame_ms_mean": 8.19609166666666,
|
||||||
|
"draw_calls_mean": 1631.97361111111,
|
||||||
|
"frame_ms_mean": 8.33719027777778,
|
||||||
|
"frame_ms_p95": 9.045,
|
||||||
|
"frames": 720,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 90.0666666666667,
|
||||||
|
"Renderer Utilization %": 89.4,
|
||||||
|
"Tiler Utilization %": 2.06666666666667,
|
||||||
|
"gpu_memory_in_use_mb": 2305.640625,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 7.50902937685186,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 31.0888888888889,
|
||||||
|
"objects_mean": 3885.85972222222,
|
||||||
|
"primitives_mean": 5093941.47222222,
|
||||||
|
"process_ms_median": 10.001,
|
||||||
|
"render_setup_ms_median": 0.142,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 24,
|
||||||
|
"particle_systems": 2,
|
||||||
|
"particles_max": 44,
|
||||||
|
"shadow_caster_instances": 694,
|
||||||
|
"shadow_caster_nodes": 297,
|
||||||
|
"shadow_lights": 1
|
||||||
|
},
|
||||||
|
"strategic_view_ms_median": 0.221862275993223
|
||||||
|
},
|
||||||
|
"lane_view": {
|
||||||
|
"camera_zoom": 34.0,
|
||||||
|
"cpu_frame_ms_mean": 9.90605695142379,
|
||||||
|
"draw_calls_mean": 3508.73534338358,
|
||||||
|
"frame_ms_mean": 10.0520100502513,
|
||||||
|
"frame_ms_p95": 11.587,
|
||||||
|
"frames": 597,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 67.2,
|
||||||
|
"Renderer Utilization %": 66.4,
|
||||||
|
"Tiler Utilization %": 0.933333333333333,
|
||||||
|
"gpu_memory_in_use_mb": 2313.1875,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 6.75495075376885,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 117.450586264657,
|
||||||
|
"objects_mean": 7597.24958123953,
|
||||||
|
"primitives_mean": 15956382.2881072,
|
||||||
|
"process_ms_median": 11.902,
|
||||||
|
"render_setup_ms_median": 0.256,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 24,
|
||||||
|
"particle_systems": 2,
|
||||||
|
"particles_max": 44,
|
||||||
|
"shadow_caster_instances": 974,
|
||||||
|
"shadow_caster_nodes": 577,
|
||||||
|
"shadow_lights": 1
|
||||||
|
},
|
||||||
|
"strategic_view_ms_median": 0.249242852776041
|
||||||
|
},
|
||||||
|
"mobile_units_at_start": 120,
|
||||||
|
"overview": {
|
||||||
|
"camera_zoom": 92.0,
|
||||||
|
"cpu_frame_ms_mean": 9.82654817275748,
|
||||||
|
"draw_calls_mean": 3767.78571428571,
|
||||||
|
"frame_ms_mean": 9.98152491694352,
|
||||||
|
"frame_ms_p95": 10.778,
|
||||||
|
"frames": 602,
|
||||||
|
"gpu_load": {
|
||||||
|
"Device Utilization %": 92.6666666666667,
|
||||||
|
"Renderer Utilization %": 92.6666666666667,
|
||||||
|
"Tiler Utilization %": 0.6,
|
||||||
|
"gpu_memory_in_use_mb": 2295.828125,
|
||||||
|
"samples": 15
|
||||||
|
},
|
||||||
|
"gpu_ms_estimate": 9.24954642303432,
|
||||||
|
"gpu_ms_mean": 0.0,
|
||||||
|
"mobile_units_mean": 95.563122923588,
|
||||||
|
"objects_mean": 8440.82558139535,
|
||||||
|
"primitives_mean": 16532393.0631229,
|
||||||
|
"process_ms_median": 11.807,
|
||||||
|
"render_setup_ms_median": 0.212,
|
||||||
|
"scene_max": {
|
||||||
|
"lights": 24,
|
||||||
|
"particle_systems": 2,
|
||||||
|
"particles_max": 44,
|
||||||
|
"shadow_caster_instances": 911,
|
||||||
|
"shadow_caster_nodes": 514,
|
||||||
|
"shadow_lights": 1
|
||||||
|
},
|
||||||
|
"strategic_view_ms_median": 0.260997822861227
|
||||||
|
},
|
||||||
|
"process_rss_mb": 787.375,
|
||||||
|
"renderer": "forward_plus",
|
||||||
|
"scene_build_ms": 190.539,
|
||||||
|
"sim_speed": "2x (40 steps/s)",
|
||||||
|
"sim_step_ms": 1.931505,
|
||||||
|
"static_mem_mb": 110.706504821777,
|
||||||
|
"texture_mem_mb": 399.263427734375,
|
||||||
|
"video_mem_mb": 829.21875,
|
||||||
|
"window": "(1920, 1080)"
|
||||||
|
}
|
||||||
174
docs/luneraria/logs/g2/perf-vergleich-mac.json
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
{
|
||||||
|
"g1": {
|
||||||
|
"lane_view": {
|
||||||
|
"frame_ms_mean": 9.50919545402115,
|
||||||
|
"frame_ms_p95": 10.9845,
|
||||||
|
"cpu_frame_ms_mean": 9.37654468369476,
|
||||||
|
"gpu_ms_estimate": 8.98309312880483,
|
||||||
|
"draw_calls_mean": 3278.308450269815,
|
||||||
|
"primitives_mean": 16051271.12141915,
|
||||||
|
"objects_mean": 7340.2983861261,
|
||||||
|
"process_ms_median": 11.525500000000001,
|
||||||
|
"render_setup_ms_median": 0.217,
|
||||||
|
"util": 94.46666666666665,
|
||||||
|
"strategic": 0,
|
||||||
|
"lights": 23,
|
||||||
|
"shadow_lights": 1,
|
||||||
|
"casters": 539,
|
||||||
|
"caster_inst": 946,
|
||||||
|
"psys": 0,
|
||||||
|
"pmax": 0
|
||||||
|
},
|
||||||
|
"overview": {
|
||||||
|
"frame_ms_mean": 9.478619395993231,
|
||||||
|
"frame_ms_p95": 10.291,
|
||||||
|
"cpu_frame_ms_mean": 9.340653701534945,
|
||||||
|
"gpu_ms_estimate": 8.19684503097113,
|
||||||
|
"draw_calls_mean": 3588.0264277882998,
|
||||||
|
"primitives_mean": 16743896.9883634,
|
||||||
|
"objects_mean": 8119.359678062396,
|
||||||
|
"process_ms_median": 11.391,
|
||||||
|
"render_setup_ms_median": 0.1895,
|
||||||
|
"util": 86.5,
|
||||||
|
"strategic": 0,
|
||||||
|
"lights": 23,
|
||||||
|
"shadow_lights": 1,
|
||||||
|
"casters": 476,
|
||||||
|
"caster_inst": 883,
|
||||||
|
"psys": 0,
|
||||||
|
"pmax": 0
|
||||||
|
},
|
||||||
|
"base_view": {
|
||||||
|
"frame_ms_mean": 8.334197823239336,
|
||||||
|
"frame_ms_p95": 9.051,
|
||||||
|
"cpu_frame_ms_mean": 8.206294107335491,
|
||||||
|
"gpu_ms_estimate": 6.000623023419125,
|
||||||
|
"draw_calls_mean": 793.090429958391,
|
||||||
|
"primitives_mean": 2349473.756771075,
|
||||||
|
"objects_mean": 2891.72964439821,
|
||||||
|
"process_ms_median": 9.7255,
|
||||||
|
"render_setup_ms_median": 0.1285,
|
||||||
|
"util": 72.0,
|
||||||
|
"strategic": 0,
|
||||||
|
"lights": 23,
|
||||||
|
"shadow_lights": 1,
|
||||||
|
"casters": 368,
|
||||||
|
"caster_inst": 775,
|
||||||
|
"psys": 0,
|
||||||
|
"pmax": 0
|
||||||
|
},
|
||||||
|
"grotto_view": {
|
||||||
|
"frame_ms_mean": 8.33357777777778,
|
||||||
|
"frame_ms_p95": 8.995999999999999,
|
||||||
|
"cpu_frame_ms_mean": 8.212820833333335,
|
||||||
|
"gpu_ms_estimate": 7.222434042592595,
|
||||||
|
"draw_calls_mean": 1543.718055555555,
|
||||||
|
"primitives_mean": 5032820.14861111,
|
||||||
|
"objects_mean": 3797.55625,
|
||||||
|
"process_ms_median": 9.7745,
|
||||||
|
"render_setup_ms_median": 0.1205,
|
||||||
|
"util": 86.66666666666666,
|
||||||
|
"strategic": 0,
|
||||||
|
"lights": 23,
|
||||||
|
"shadow_lights": 1,
|
||||||
|
"casters": 259,
|
||||||
|
"caster_inst": 666,
|
||||||
|
"psys": 0,
|
||||||
|
"pmax": 0
|
||||||
|
},
|
||||||
|
"build": 394.5565,
|
||||||
|
"step": 1.7595925000000001,
|
||||||
|
"vram": 819.375,
|
||||||
|
"tex": 410.491943359375,
|
||||||
|
"static": 97.9973249435425,
|
||||||
|
"rss": 707.7578125,
|
||||||
|
"buffer": 170.629276275635,
|
||||||
|
"gpu_mem": 2185.65625
|
||||||
|
},
|
||||||
|
"g2": {
|
||||||
|
"lane_view": {
|
||||||
|
"frame_ms_mean": 10.018613178203845,
|
||||||
|
"frame_ms_p95": 11.543,
|
||||||
|
"cpu_frame_ms_mean": 9.871958592184441,
|
||||||
|
"gpu_ms_estimate": 7.98398849945791,
|
||||||
|
"draw_calls_mean": 3508.62890297299,
|
||||||
|
"primitives_mean": 15955801.4285794,
|
||||||
|
"objects_mean": 7597.036604263691,
|
||||||
|
"process_ms_median": 12.225,
|
||||||
|
"render_setup_ms_median": 0.252,
|
||||||
|
"util": 79.73333333333335,
|
||||||
|
"strategic": 0.2479336010015315,
|
||||||
|
"lights": 24,
|
||||||
|
"shadow_lights": 1,
|
||||||
|
"casters": 577,
|
||||||
|
"caster_inst": 974,
|
||||||
|
"psys": 2,
|
||||||
|
"pmax": 44
|
||||||
|
},
|
||||||
|
"overview": {
|
||||||
|
"frame_ms_mean": 9.94980378078581,
|
||||||
|
"frame_ms_p95": 10.7425,
|
||||||
|
"cpu_frame_ms_mean": 9.795321193816754,
|
||||||
|
"gpu_ms_estimate": 9.504469872674186,
|
||||||
|
"draw_calls_mean": 3767.4358323494653,
|
||||||
|
"primitives_mean": 16530596.12825565,
|
||||||
|
"objects_mean": 8439.981385739,
|
||||||
|
"process_ms_median": 11.937000000000001,
|
||||||
|
"render_setup_ms_median": 0.20600000000000002,
|
||||||
|
"util": 95.53333333333336,
|
||||||
|
"strategic": 0.257441615254486,
|
||||||
|
"lights": 24,
|
||||||
|
"shadow_lights": 1,
|
||||||
|
"casters": 514,
|
||||||
|
"caster_inst": 911,
|
||||||
|
"psys": 2,
|
||||||
|
"pmax": 44
|
||||||
|
},
|
||||||
|
"base_view": {
|
||||||
|
"frame_ms_mean": 8.332884027777775,
|
||||||
|
"frame_ms_p95": 9.006,
|
||||||
|
"cpu_frame_ms_mean": 8.18413541666667,
|
||||||
|
"gpu_ms_estimate": 6.33854711712963,
|
||||||
|
"draw_calls_mean": 917.078472222222,
|
||||||
|
"primitives_mean": 2268093.53055556,
|
||||||
|
"objects_mean": 3018.08333333333,
|
||||||
|
"process_ms_median": 9.971499999999999,
|
||||||
|
"render_setup_ms_median": 0.1475,
|
||||||
|
"util": 76.0666666666667,
|
||||||
|
"strategic": 0.2266318702263245,
|
||||||
|
"lights": 24,
|
||||||
|
"shadow_lights": 1,
|
||||||
|
"casters": 406,
|
||||||
|
"caster_inst": 803,
|
||||||
|
"psys": 2,
|
||||||
|
"pmax": 44
|
||||||
|
},
|
||||||
|
"grotto_view": {
|
||||||
|
"frame_ms_mean": 8.33509167148251,
|
||||||
|
"frame_ms_p95": 9.059000000000001,
|
||||||
|
"cpu_frame_ms_mean": 8.193719897133604,
|
||||||
|
"gpu_ms_estimate": 7.50713923211525,
|
||||||
|
"draw_calls_mean": 1632.156708468175,
|
||||||
|
"primitives_mean": 5095113.679245645,
|
||||||
|
"objects_mean": 3886.12403586839,
|
||||||
|
"process_ms_median": 9.779,
|
||||||
|
"render_setup_ms_median": 0.1405,
|
||||||
|
"util": 90.0666666666667,
|
||||||
|
"strategic": 0.220427389659341,
|
||||||
|
"lights": 24,
|
||||||
|
"shadow_lights": 1,
|
||||||
|
"casters": 297,
|
||||||
|
"caster_inst": 694,
|
||||||
|
"psys": 2,
|
||||||
|
"pmax": 44
|
||||||
|
},
|
||||||
|
"build": 207.301,
|
||||||
|
"step": 1.855675,
|
||||||
|
"vram": 825.109375,
|
||||||
|
"tex": 399.263427734375,
|
||||||
|
"static": 110.706506729126,
|
||||||
|
"rss": 786.5859375,
|
||||||
|
"buffer": 173.200626373291,
|
||||||
|
"gpu_mem": 2197.9140625
|
||||||
|
}
|
||||||
|
}
|
||||||
21
docs/luneraria/logs/g2/smoke-macos-arm64.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"adapter": "Apple M5 (Apple9)",
|
||||||
|
"ancient_views": 2,
|
||||||
|
"architecture": "arm64",
|
||||||
|
"boss_model": true,
|
||||||
|
"combat_seconds": 44.8999999999996,
|
||||||
|
"driver": "metal",
|
||||||
|
"engine": "4.7.2-stable (official)",
|
||||||
|
"fps_at_end": 81.0,
|
||||||
|
"fps_in_combat": 120.0,
|
||||||
|
"grotto_state": "verfuegbar",
|
||||||
|
"missing_assets": [],
|
||||||
|
"os": "macOS",
|
||||||
|
"os_version": "27.0.0",
|
||||||
|
"phase": 0,
|
||||||
|
"renderer": "forward_plus",
|
||||||
|
"result": "PASS",
|
||||||
|
"seconds": 40.923,
|
||||||
|
"tower_views": 16,
|
||||||
|
"window": "(1920, 1080)"
|
||||||
|
}
|
||||||
21
docs/luneraria/logs/g2/smoke-macos-x86_64-rosetta.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"adapter": "Apple M5",
|
||||||
|
"ancient_views": 2,
|
||||||
|
"architecture": "x86_64",
|
||||||
|
"boss_model": true,
|
||||||
|
"combat_seconds": 44.8999999999996,
|
||||||
|
"driver": "vulkan",
|
||||||
|
"engine": "4.7.2-stable (official)",
|
||||||
|
"fps_at_end": 47.0,
|
||||||
|
"fps_in_combat": 118.0,
|
||||||
|
"grotto_state": "verfuegbar",
|
||||||
|
"missing_assets": [],
|
||||||
|
"os": "macOS",
|
||||||
|
"os_version": "27.0.0",
|
||||||
|
"phase": 0,
|
||||||
|
"renderer": "forward_plus",
|
||||||
|
"result": "PASS",
|
||||||
|
"seconds": 23.837,
|
||||||
|
"tower_views": 16,
|
||||||
|
"window": "(1920, 1080)"
|
||||||
|
}
|
||||||
21
docs/luneraria/logs/g2/smoke-windows11-arm64.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"adapter": "Parallels using Apple M5 (Compat)",
|
||||||
|
"ancient_views": 2,
|
||||||
|
"architecture": "arm64",
|
||||||
|
"boss_model": true,
|
||||||
|
"combat_seconds": 44.8999999999996,
|
||||||
|
"driver": "opengl3",
|
||||||
|
"engine": "4.7.2-stable (official)",
|
||||||
|
"fps_at_end": 69.0,
|
||||||
|
"fps_in_combat": 65.0,
|
||||||
|
"grotto_state": "verfuegbar",
|
||||||
|
"missing_assets": [],
|
||||||
|
"os": "Windows",
|
||||||
|
"os_version": "10.0.26100",
|
||||||
|
"phase": 0,
|
||||||
|
"renderer": "gl_compatibility",
|
||||||
|
"result": "PASS",
|
||||||
|
"seconds": 24.859,
|
||||||
|
"tower_views": 16,
|
||||||
|
"window": "(1440, 857)"
|
||||||
|
}
|
||||||
21
docs/luneraria/logs/g2/smoke-windows11-x64-emuliert.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"adapter": "Parallels using Apple M5 (Compat)",
|
||||||
|
"ancient_views": 2,
|
||||||
|
"architecture": "x86_64",
|
||||||
|
"boss_model": true,
|
||||||
|
"combat_seconds": 44.8999999999996,
|
||||||
|
"driver": "opengl3",
|
||||||
|
"engine": "4.7.2-stable (official)",
|
||||||
|
"fps_at_end": 61.0,
|
||||||
|
"fps_in_combat": 46.0,
|
||||||
|
"grotto_state": "verfuegbar",
|
||||||
|
"missing_assets": [],
|
||||||
|
"os": "Windows",
|
||||||
|
"os_version": "10.0.26100",
|
||||||
|
"phase": 0,
|
||||||
|
"renderer": "gl_compatibility",
|
||||||
|
"result": "PASS",
|
||||||
|
"seconds": 24.648,
|
||||||
|
"tower_views": 16,
|
||||||
|
"window": "(1440, 857)"
|
||||||
|
}
|
||||||
27
docs/luneraria/logs/g2/test-balance.log
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
BALANCE 00 ["ausgewogen", "druck", "teamhilfe"] | ["teamhilfe", "verteidigung", "ausgewogen"] -> blau (12) / swapped rot (12)
|
||||||
|
BALANCE 01 ["bossjagd", "bossjagd", "teamhilfe"] | ["fernkampf", "teamhilfe", "fernkampf"] -> rot (9) / swapped blau (9)
|
||||||
|
BALANCE 02 ["bossjagd", "ausgewogen", "teamhilfe"] | ["druck", "ausgewogen", "verteidigung"] -> rot (11) / swapped blau (11)
|
||||||
|
BALANCE 03 ["fernkampf", "druck", "ausgewogen"] | ["druck", "fernkampf", "verteidigung"] -> blau (12) / swapped rot (12)
|
||||||
|
BALANCE 04 ["ausgewogen", "teamhilfe", "fernkampf"] | ["ausgewogen", "verteidigung", "fernkampf"] -> blau (11) / swapped rot (11)
|
||||||
|
BALANCE 05 ["teamhilfe", "fernkampf", "verteidigung"] | ["fernkampf", "bossjagd", "bossjagd"] -> blau (11) / swapped rot (11)
|
||||||
|
BALANCE 06 ["bossjagd", "ausgewogen", "teamhilfe"] | ["fernkampf", "teamhilfe", "ausgewogen"] -> blau (9) / swapped rot (9)
|
||||||
|
BALANCE 07 ["ausgewogen", "ausgewogen", "ausgewogen"] | ["teamhilfe", "teamhilfe", "ausgewogen"] -> blau (12) / swapped rot (12)
|
||||||
|
BALANCE 08 ["teamhilfe", "ausgewogen", "bossjagd"] | ["druck", "ausgewogen", "druck"] -> rot (12) / swapped blau (12)
|
||||||
|
BALANCE 09 ["verteidigung", "druck", "druck"] | ["bossjagd", "ausgewogen", "teamhilfe"] -> blau (11) / swapped rot (11)
|
||||||
|
BALANCE 10 ["druck", "ausgewogen", "ausgewogen"] | ["teamhilfe", "bossjagd", "ausgewogen"] -> blau (12) / swapped rot (12)
|
||||||
|
BALANCE 11 ["bossjagd", "fernkampf", "fernkampf"] | ["teamhilfe", "verteidigung", "teamhilfe"] -> rot (10) / swapped blau (10)
|
||||||
|
BALANCE 12 ["fernkampf", "fernkampf", "teamhilfe"] | ["teamhilfe", "ausgewogen", "fernkampf"] -> rot (12) / swapped blau (12)
|
||||||
|
BALANCE 13 ["teamhilfe", "druck", "fernkampf"] | ["fernkampf", "druck", "ausgewogen"] -> rot (13) / swapped blau (13)
|
||||||
|
BALANCE 14 ["bossjagd", "druck", "fernkampf"] | ["bossjagd", "bossjagd", "bossjagd"] -> blau (10) / swapped rot (10)
|
||||||
|
BALANCE 15 ["fernkampf", "fernkampf", "verteidigung"] | ["druck", "fernkampf", "ausgewogen"] -> blau (12) / swapped rot (12)
|
||||||
|
BALANCE 16 ["teamhilfe", "verteidigung", "verteidigung"] | ["teamhilfe", "verteidigung", "ausgewogen"] -> rot (13) / swapped blau (13)
|
||||||
|
BALANCE 17 ["fernkampf", "verteidigung", "fernkampf"] | ["druck", "fernkampf", "bossjagd"] -> blau (10) / swapped rot (10)
|
||||||
|
BALANCE 18 ["verteidigung", "ausgewogen", "bossjagd"] | ["druck", "teamhilfe", "druck"] -> blau (9) / swapped rot (9)
|
||||||
|
BALANCE 19 ["druck", "teamhilfe", "teamhilfe"] | ["fernkampf", "verteidigung", "bossjagd"] -> rot (10) / swapped blau (10)
|
||||||
|
WARNING: 38 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 resources still in use at exit (run with --verbose for details).
|
||||||
|
at: clear (core/io/resource.cpp:822)
|
||||||
|
exit 0 after 344 s
|
||||||
5
docs/luneraria/logs/g2/test-core.log
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
LUNE_FULL_MATCH: seed 2 -> Rot after 9 rounds
|
||||||
|
LUNE_CORE_PASS: 91 checks; 0 failures
|
||||||
|
exit 0 after 21 s
|
||||||
4
docs/luneraria/logs/g2/test-legacy-runner.log
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
PASS: 209 checks across economy, combat and the full match lifecycle.
|
||||||
|
exit 0 after 0 s
|
||||||
5
docs/luneraria/logs/g2/test-legacy-smoke.log
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
EXPORT_SMOKE_PASS: 463 checks; 0 failures; engine 4.7.2-stable (official); cores [3000, 1035]
|
||||||
|
SMOKE_CAPTURES: /Users/martin/Library/Application Support/Godot/app_userdata/Champions of Luneraria/phase8-smoke
|
||||||
|
exit 0 after 5 s
|
||||||
8
docs/luneraria/logs/g2/test-m2.log
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
LUNE_M2_PASS: 73 checks; 0 failures
|
||||||
|
WARNING: 8 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 resources still in use at exit (run with --verbose for details).
|
||||||
|
at: clear (core/io/resource.cpp:822)
|
||||||
|
exit 0 after 14 s
|
||||||
75
docs/luneraria/logs/g2/test-net-rounds.json
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
{
|
||||||
|
"rounds": 5,
|
||||||
|
"drop_round": 2,
|
||||||
|
"compared_rounds": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"mismatches": [],
|
||||||
|
"host_sums": {
|
||||||
|
"1": "1392179697",
|
||||||
|
"2": "3342070386",
|
||||||
|
"3": "711640101",
|
||||||
|
"4": "346458639",
|
||||||
|
"5": "254645978"
|
||||||
|
},
|
||||||
|
"guest_sums": {
|
||||||
|
"1": "1392179697",
|
||||||
|
"2": "3342070386",
|
||||||
|
"3": "711640101",
|
||||||
|
"4": "346458639",
|
||||||
|
"5": "254645978"
|
||||||
|
},
|
||||||
|
"client_combats": [
|
||||||
|
{
|
||||||
|
"round": 1,
|
||||||
|
"frames": 4,
|
||||||
|
"local_steps": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"round": 2,
|
||||||
|
"frames": 26,
|
||||||
|
"local_steps": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"round": 3,
|
||||||
|
"frames": 39,
|
||||||
|
"local_steps": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"round": 4,
|
||||||
|
"frames": 51,
|
||||||
|
"local_steps": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"round": 5,
|
||||||
|
"frames": 9,
|
||||||
|
"local_steps": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"seats": {
|
||||||
|
"gast_a": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"gast_b": [
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"checks": {
|
||||||
|
"host_ok": true,
|
||||||
|
"guest_a_ok": true,
|
||||||
|
"guest_b_ok": true,
|
||||||
|
"spoof_rejected": true,
|
||||||
|
"rounds_compared": true,
|
||||||
|
"checksums_equal": true,
|
||||||
|
"client_never_simulates": true,
|
||||||
|
"client_receives_frames": true,
|
||||||
|
"rejoin_same_seat": true,
|
||||||
|
"bot_took_over": true,
|
||||||
|
"rejoin_announced": true,
|
||||||
|
"no_script_errors": true
|
||||||
|
}
|
||||||
|
}
|
||||||
16
docs/luneraria/logs/g2/test-net-rounds.log
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"host_ok": true,
|
||||||
|
"guest_a_ok": true,
|
||||||
|
"guest_b_ok": true,
|
||||||
|
"spoof_rejected": true,
|
||||||
|
"rounds_compared": true,
|
||||||
|
"checksums_equal": true,
|
||||||
|
"client_never_simulates": true,
|
||||||
|
"client_receives_frames": true,
|
||||||
|
"rejoin_same_seat": true,
|
||||||
|
"bot_took_over": true,
|
||||||
|
"rejoin_announced": true,
|
||||||
|
"no_script_errors": true
|
||||||
|
}
|
||||||
|
NET_ROUNDS_PASS: 12/12
|
||||||
|
exit 0 after 16 s
|
||||||
4
docs/luneraria/logs/g2/test-net.log
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
LUNE_NET_PASS: 43 checks; 0 failures
|
||||||
|
exit 0 after 1 s
|
||||||
79
docs/luneraria/logs/g2/test-stress.json
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
{
|
||||||
|
"checks": 17,
|
||||||
|
"failures": [],
|
||||||
|
"high_index": [
|
||||||
|
{
|
||||||
|
"end": "core",
|
||||||
|
"monster_hp": 12763.92,
|
||||||
|
"peak_mobile": 120,
|
||||||
|
"phase": "vorbei",
|
||||||
|
"steps": 2306,
|
||||||
|
"wall_ms": 1728,
|
||||||
|
"wave": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"end": "core",
|
||||||
|
"monster_hp": 771787.92,
|
||||||
|
"peak_mobile": 120,
|
||||||
|
"phase": "vorbei",
|
||||||
|
"steps": 1484,
|
||||||
|
"wall_ms": 1334,
|
||||||
|
"wave": 1000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"end": "core",
|
||||||
|
"monster_hp": 72514027.92,
|
||||||
|
"peak_mobile": 120,
|
||||||
|
"phase": "vorbei",
|
||||||
|
"steps": 1459,
|
||||||
|
"wall_ms": 1343,
|
||||||
|
"wave": 10000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"end": "core",
|
||||||
|
"monster_hp": 720051360427.92,
|
||||||
|
"peak_mobile": 120,
|
||||||
|
"phase": "vorbei",
|
||||||
|
"steps": 1456,
|
||||||
|
"wall_ms": 1328,
|
||||||
|
"wave": 1000000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"long_run": {
|
||||||
|
"final_gold": [
|
||||||
|
141844,
|
||||||
|
141844,
|
||||||
|
141844,
|
||||||
|
141894,
|
||||||
|
141894,
|
||||||
|
141894
|
||||||
|
],
|
||||||
|
"final_wave_monster_hp": 39840.0,
|
||||||
|
"memory_mb_every25": [
|
||||||
|
51.3685665130615,
|
||||||
|
52.5444202423096,
|
||||||
|
52.5169200897217,
|
||||||
|
52.5596561431885,
|
||||||
|
52.6210994720459,
|
||||||
|
52.5991535186768,
|
||||||
|
52.6706295013428,
|
||||||
|
52.7292499542236,
|
||||||
|
52.7079830169678
|
||||||
|
],
|
||||||
|
"objects_every25": [
|
||||||
|
1704.0,
|
||||||
|
1849.0,
|
||||||
|
1843.0,
|
||||||
|
1843.0,
|
||||||
|
1849.0,
|
||||||
|
1843.0,
|
||||||
|
1843.0,
|
||||||
|
1849.0,
|
||||||
|
1843.0
|
||||||
|
],
|
||||||
|
"result": "Testlimit erreicht / Partie nicht abgeschlossen",
|
||||||
|
"round_ms_first10": 583.8408,
|
||||||
|
"round_ms_last10": 1459.945,
|
||||||
|
"rounds": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
21
docs/luneraria/logs/g2/test-stress.log
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
STRESS_WAVE {"end":"core","monster_hp":12763.92,"peak_mobile":120,"phase":"vorbei","steps":2306,"wall_ms":1728,"wave":100}
|
||||||
|
STRESS_WAVE {"end":"core","monster_hp":771787.92,"peak_mobile":120,"phase":"vorbei","steps":1484,"wall_ms":1334,"wave":1000}
|
||||||
|
STRESS_WAVE {"end":"core","monster_hp":72514027.92,"peak_mobile":120,"phase":"vorbei","steps":1459,"wall_ms":1343,"wave":10000}
|
||||||
|
STRESS_WAVE {"end":"core","monster_hp":720051360427.92,"peak_mobile":120,"phase":"vorbei","steps":1456,"wall_ms":1328,"wave":1000000}
|
||||||
|
STRESS_PROGRESS round 26 · 1721 ms · 52.5 MB · 1849 objects
|
||||||
|
STRESS_PROGRESS round 51 · 1393 ms · 52.5 MB · 1843 objects
|
||||||
|
STRESS_PROGRESS round 76 · 1421 ms · 52.6 MB · 1843 objects
|
||||||
|
STRESS_PROGRESS round 101 · 1975 ms · 52.6 MB · 1849 objects
|
||||||
|
STRESS_PROGRESS round 126 · 1469 ms · 52.6 MB · 1843 objects
|
||||||
|
STRESS_PROGRESS round 151 · 1368 ms · 52.7 MB · 1843 objects
|
||||||
|
STRESS_PROGRESS round 176 · 1723 ms · 52.7 MB · 1849 objects
|
||||||
|
STRESS_PROGRESS round 201 · 1208 ms · 52.7 MB · 1843 objects
|
||||||
|
STRESS_LONG {"final_gold":[141844,141844,141844,141894,141894,141894],"final_wave_monster_hp":39840.0,"memory_mb_every25":[51.3685665130615,52.5444202423096,52.5169200897217,52.5596561431885,52.6210994720459,52.5991535186768,52.6706295013428,52.7292499542236,52.7079830169678],"objects_every25":[1704.0,1849.0,1843.0,1843.0,1849.0,1843.0,1843.0,1849.0,1843.0],"result":"Testlimit erreicht / Partie nicht abgeschlossen","round_ms_first10":583.8408,"round_ms_last10":1459.945,"rounds":200}
|
||||||
|
STRESS_PASS: 17 checks; 0 failures
|
||||||
|
WARNING: 4 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 resources still in use at exit (run with --verbose for details).
|
||||||
|
at: clear (core/io/resource.cpp:822)
|
||||||
|
exit 0 after 303 s
|
||||||
40
docs/luneraria/logs/g2/test-verify-g1.json
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"checks": 443,
|
||||||
|
"facts": {
|
||||||
|
"grotto_clearance_to_lanes": 3.0,
|
||||||
|
"lane_height_range": {
|
||||||
|
"max": {
|
||||||
|
"bot": 0.600000023841858,
|
||||||
|
"mid": 0.620000004768372,
|
||||||
|
"top": 3.29657602310181
|
||||||
|
},
|
||||||
|
"min": {
|
||||||
|
"bot": -0.159999996423721,
|
||||||
|
"mid": 0.114149838685989,
|
||||||
|
"top": 0.081579864025116
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lane_lengths": {
|
||||||
|
"bot": 96.82,
|
||||||
|
"mid": 61.16,
|
||||||
|
"top": 96.82
|
||||||
|
},
|
||||||
|
"overview": {
|
||||||
|
"distance": 150.459213256836,
|
||||||
|
"pitch_deg": -69.4643032570342,
|
||||||
|
"zoom": 92.0
|
||||||
|
},
|
||||||
|
"pitch_range_deg": [
|
||||||
|
-48.000003795596,
|
||||||
|
-69.9999992757703
|
||||||
|
],
|
||||||
|
"terrain_mirror_error": 0.0,
|
||||||
|
"unit_ground_error": 0.0000000750818887063076,
|
||||||
|
"water_points_under_lanes": {
|
||||||
|
"bot": 16,
|
||||||
|
"mid": 8,
|
||||||
|
"top": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"failures": []
|
||||||
|
}
|
||||||
9
docs/luneraria/logs/g2/test-verify-g1.log
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
Metal 4.0 - Forward+ - Using Device #0: Apple - Apple M5 (Apple9)
|
||||||
|
|
||||||
|
G1_VERIFY_PASS: 443 checks; 0 failures
|
||||||
|
WARNING: 14 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 resources still in use at exit (run with --verbose for details).
|
||||||
|
at: clear (core/io/resource.cpp:822)
|
||||||
|
exit 0 after 7 s
|
||||||
18
docs/luneraria/logs/g2/test-verify-g2.log
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
Metal 4.0 - Forward+ - Using Device #0: Apple - Apple M5 (Apple9)
|
||||||
|
|
||||||
|
G2_PART A assets
|
||||||
|
G2_PART B views
|
||||||
|
G2_PART C states
|
||||||
|
G2_PART D tower attacks
|
||||||
|
G2_PART E rules
|
||||||
|
G2_PART F boss
|
||||||
|
G2_PART G no feedback
|
||||||
|
G2_PART H bot matches
|
||||||
|
G2_PART I occlusion and scale
|
||||||
|
G2_VERIFY_PASS: 245 checks; 0 failures
|
||||||
|
WARNING: 40 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 resources still in use at exit (run with --verbose for details).
|
||||||
|
at: clear (core/io/resource.cpp:822)
|
||||||
|
exit 0 after 79 s
|
||||||
10
docs/luneraria/logs/g2/test-verify-m1.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"checks": 38,
|
||||||
|
"failures": [],
|
||||||
|
"object_counts": [
|
||||||
|
4533.0,
|
||||||
|
4533.0,
|
||||||
|
4533.0,
|
||||||
|
4533.0
|
||||||
|
]
|
||||||
|
}
|
||||||
19
docs/luneraria/logs/g2/test-verify-m1.log
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
Metal 4.0 - Forward+ - Using Device #0: Apple - Apple M5 (Apple9)
|
||||||
|
|
||||||
|
M1_MENU_READY_MS 966
|
||||||
|
M1_SHOT planning-1280x720
|
||||||
|
M1_SHOT planning-1920x1080
|
||||||
|
M1_SHOT combat-mid-live
|
||||||
|
M1_SHOT settlement
|
||||||
|
M1_SHOT combat-top
|
||||||
|
M1_SHOT overview-combat
|
||||||
|
M1_SHOT core-siege
|
||||||
|
M1_SHOT result
|
||||||
|
M1_MATCH_RESULT Ancient von Blau zerstört. after 11 waves
|
||||||
|
M1_VERIFY_PASS: 38 checks; 0 failures
|
||||||
|
WARNING: 4 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 resources still in use at exit (run with --verbose for details).
|
||||||
|
at: clear (core/io/resource.cpp:822)
|
||||||
|
exit 0 after 25 s
|
||||||
19
docs/luneraria/logs/g2/test-verify-m2.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"boss_log": [
|
||||||
|
{
|
||||||
|
"chooser": 4,
|
||||||
|
"event": "won",
|
||||||
|
"round": 12,
|
||||||
|
"team": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"checks": 14,
|
||||||
|
"failures": [],
|
||||||
|
"rounds": 12,
|
||||||
|
"seen": [
|
||||||
|
"announce",
|
||||||
|
"hero_field",
|
||||||
|
"hero_items",
|
||||||
|
"grotto"
|
||||||
|
]
|
||||||
|
}
|
||||||
15
docs/luneraria/logs/g2/test-verify-m2.log
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
Metal 4.0 - Forward+ - Using Device #0: Apple - Apple M5 (Apple9)
|
||||||
|
|
||||||
|
M2APP_SHOT m2-hud-hero-round1
|
||||||
|
M2APP_SHOT m2-announcements
|
||||||
|
M2APP_SHOT m2-hero-field
|
||||||
|
M2APP_SHOT m2-hero-items
|
||||||
|
M2APP_SHOT m2-grotto-fight
|
||||||
|
M2APP_BOSS_LOG [{ "round": 12, "event": "won", "team": 1, "chooser": 4 }]
|
||||||
|
M2APP_VERIFY_PASS: 14 checks; 0 failures
|
||||||
|
WARNING: 4 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 resources still in use at exit (run with --verbose for details).
|
||||||
|
at: clear (core/io/resource.cpp:822)
|
||||||
|
exit 0 after 21 s
|
||||||
80
docs/luneraria/logs/g2/verify-g2.json
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
{
|
||||||
|
"checks": 245,
|
||||||
|
"facts": {
|
||||||
|
"assets": [
|
||||||
|
"ancient",
|
||||||
|
"bastion",
|
||||||
|
"bossgrotte",
|
||||||
|
"grenzwacht",
|
||||||
|
"kernwacht",
|
||||||
|
"schlundkrone"
|
||||||
|
],
|
||||||
|
"boss_bones": 24,
|
||||||
|
"boss_lanes": {
|
||||||
|
"bot": true,
|
||||||
|
"mid": true,
|
||||||
|
"top": true
|
||||||
|
},
|
||||||
|
"boss_outcome": "success",
|
||||||
|
"bot_matches": [
|
||||||
|
{
|
||||||
|
"rounds": 11,
|
||||||
|
"seed": 21,
|
||||||
|
"winner": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rounds": 12,
|
||||||
|
"seed": 22,
|
||||||
|
"winner": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rounds": 9,
|
||||||
|
"seed": 23,
|
||||||
|
"winner": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rounds": 11,
|
||||||
|
"seed": 24,
|
||||||
|
"winner": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"checks_per_part": {
|
||||||
|
"A assets": 113,
|
||||||
|
"B views": 28,
|
||||||
|
"C states": 32,
|
||||||
|
"D tower attacks": 6,
|
||||||
|
"E rules": 8,
|
||||||
|
"F boss": 34,
|
||||||
|
"G no feedback": 3,
|
||||||
|
"H bot matches": 7,
|
||||||
|
"I occlusion and scale": 14
|
||||||
|
},
|
||||||
|
"heights_m": {
|
||||||
|
"ancient": 10.3668,
|
||||||
|
"bastion": 7.1605,
|
||||||
|
"boss": 5.64094,
|
||||||
|
"champion": 1.81999981403351,
|
||||||
|
"grenzwacht": 6.0601,
|
||||||
|
"hero": 2.31619253754616,
|
||||||
|
"kernwacht": 8.33,
|
||||||
|
"troop": 1.34999990463257
|
||||||
|
},
|
||||||
|
"local_lights": 20,
|
||||||
|
"no_feedback_bolts": 74,
|
||||||
|
"particle_systems": 80,
|
||||||
|
"seconds": 77.589,
|
||||||
|
"shown_match": {
|
||||||
|
"fallen_towers_shown": 9,
|
||||||
|
"rounds": 12,
|
||||||
|
"winner": 1
|
||||||
|
},
|
||||||
|
"tower_bolts_12s": 14,
|
||||||
|
"tower_hits_12s": 13,
|
||||||
|
"tower_views": {
|
||||||
|
"base": 4,
|
||||||
|
"inner": 6,
|
||||||
|
"outer": 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"failures": []
|
||||||
|
}
|
||||||
43
docs/luneraria/logs/g2/windows11-arm64-godot.log
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
ERROR: Required Vulkan instance extension VK_KHR_surface not found.
|
||||||
|
at: _initialize_instance_extensions (drivers/vulkan/rendering_context_driver_vulkan.cpp:494)
|
||||||
|
ERROR: Condition "err != OK" is true. Returning: err
|
||||||
|
at: initialize (drivers/vulkan/rendering_context_driver_vulkan.cpp:915)
|
||||||
|
ERROR: No support for any of the suitable shader models (6.0-6.6) has been found.
|
||||||
|
at: _check_capabilities (drivers/d3d12/rendering_device_driver_d3d12.cpp:6155)
|
||||||
|
ERROR: Condition "err != OK" is true. Returning: ERR_CANT_CREATE
|
||||||
|
at: initialize (drivers/d3d12/rendering_device_driver_d3d12.cpp:6402)
|
||||||
|
ERROR: Failed to initialize driver for device.
|
||||||
|
at: initialize (servers/rendering/rendering_device.cpp:8376)
|
||||||
|
WARNING: Your video card drivers seem not to support Direct3D 12 or Vulkan, switching to OpenGL 3.
|
||||||
|
at: DisplayServerWindows (platform/windows/display_server_windows.cpp:8136)
|
||||||
|
OpenGL API 4.1 Metal - 91.7 - Compatibility - Using Device: Parallels and Apple - Parallels using Apple M5 (Compat)
|
||||||
|
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
LUNE_SMOKE_SHOT C:/Users/martin/LuneTest/smoke-arm64/00-menue.png
|
||||||
|
LUNE_SMOKE_SHOT C:/Users/martin/LuneTest/smoke-arm64/01-planung.png
|
||||||
|
LUNE_SMOKE_SHOT C:/Users/martin/LuneTest/smoke-arm64/02-kampf.png
|
||||||
|
LUNE_SMOKE_SHOT C:/Users/martin/LuneTest/smoke-arm64/03-grotte.png
|
||||||
|
LUNE_SMOKE_SHOT C:/Users/martin/LuneTest/smoke-arm64/04-ancient.png
|
||||||
|
LUNE_SMOKE_PASS {"adapter":"Parallels using Apple M5 (Compat)","ancient_views":2,"architecture":"arm64","boss_model":true,"combat_seconds":44.8999999999996,"driver":"opengl3","engine":"4.7.2-stable (official)","fps_at_end":69.0,"fps_in_combat":65.0,"grotto_state":"verfuegbar","missing_assets":[],"os":"Windows","os_version":"10.0.26100","phase":0,"renderer":"gl_compatibility","result":"PASS","seconds":24.859,"tower_views":16,"window":"(1440, 857)"}
|
||||||
43
docs/luneraria/logs/g2/windows11-x64-godot.log
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org
|
||||||
|
ERROR: Required Vulkan instance extension VK_KHR_surface not found.
|
||||||
|
at: _initialize_instance_extensions (drivers/vulkan/rendering_context_driver_vulkan.cpp:494)
|
||||||
|
ERROR: Condition "err != OK" is true. Returning: err
|
||||||
|
at: initialize (drivers/vulkan/rendering_context_driver_vulkan.cpp:915)
|
||||||
|
ERROR: No support for any of the suitable shader models (6.0-6.6) has been found.
|
||||||
|
at: _check_capabilities (drivers/d3d12/rendering_device_driver_d3d12.cpp:6154)
|
||||||
|
ERROR: Condition "err != OK" is true. Returning: ERR_CANT_CREATE
|
||||||
|
at: initialize (drivers/d3d12/rendering_device_driver_d3d12.cpp:6402)
|
||||||
|
ERROR: Failed to initialize driver for device.
|
||||||
|
at: initialize (servers/rendering/rendering_device.cpp:8376)
|
||||||
|
WARNING: Your video card drivers seem not to support Direct3D 12 or Vulkan, switching to OpenGL 3.
|
||||||
|
at: DisplayServerWindows (platform/windows/display_server_windows.cpp:8136)
|
||||||
|
OpenGL API 4.1 Metal - 91.7 - Compatibility - Using Device: Parallels and Apple - Parallels using Apple M5 (Compat)
|
||||||
|
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
WARNING: Image format BPTC_RGBA not supported by hardware, converting to RGBA8.
|
||||||
|
at: _get_gl_image_and_format (drivers/gles3/storage/texture_storage.cpp:987)
|
||||||
|
LUNE_SMOKE_SHOT C:/Users/martin/LuneTest/smoke-x64/00-menue.png
|
||||||
|
LUNE_SMOKE_SHOT C:/Users/martin/LuneTest/smoke-x64/01-planung.png
|
||||||
|
LUNE_SMOKE_SHOT C:/Users/martin/LuneTest/smoke-x64/02-kampf.png
|
||||||
|
LUNE_SMOKE_SHOT C:/Users/martin/LuneTest/smoke-x64/03-grotte.png
|
||||||
|
LUNE_SMOKE_SHOT C:/Users/martin/LuneTest/smoke-x64/04-ancient.png
|
||||||
|
LUNE_SMOKE_PASS {"adapter":"Parallels using Apple M5 (Compat)","ancient_views":2,"architecture":"x86_64","boss_model":true,"combat_seconds":44.8999999999996,"driver":"opengl3","engine":"4.7.2-stable (official)","fps_at_end":61.0,"fps_in_combat":46.0,"grotto_state":"verfuegbar","missing_assets":[],"os":"Windows","os_version":"10.0.26100","phase":0,"renderer":"gl_compatibility","result":"PASS","seconds":24.648,"tower_views":16,"window":"(1440, 857)"}
|
||||||
0
docs/luneraria/screenshots/g2/.gdignore
Normal file
BIN
docs/luneraria/screenshots/g2/nachher/01-ancient-100.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/02-ancient-50.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/03-ancient-unter-30.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/04-ancient-zerstoert.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/05-tower1-grenzwacht.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/06-tower2-bastion.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/07-basistuerme.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/08-turm-zerstoert.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/08a-turm-zerstoerung.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/10-boss-verfuegbar.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/11-bosskampf.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/13-kampf-vor-tower.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/14-kampf-vor-ancient.png
Normal file
|
After Width: | Height: | Size: 2 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/15-gesamtkarte.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/16-massstab.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/17-ancient-rot-100.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/luneraria/screenshots/g2/nachher/18a-grotte-cooldown.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/luneraria/screenshots/g2/plattformen/macos-arm64-grotte.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
docs/luneraria/screenshots/g2/plattformen/macos-arm64-kampf.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1 MiB |