"""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()