saumwacht-godot/assets-source/luneraria/build_tower.py
Martin Stickelbroeck c27a0097c3 M1: spielbarer Drei-Lane-Kern mit Karte, HUD, Menü und Abnahme
Hauptszene scenes/luneraria.tscn, Saumwacht bleibt über --legacy und die
alten QA-Schalter erreichbar. Karte aus Bestandsassets plus neuem Turm,
HUD mit Planung, Lanes und Minimap, Menü umbenannt, Einstellungen werden
aus dem alten Benutzerordner übernommen. Simulation 6,6x schneller
(Raster, Caches, faire Staffelung) und exakt spiegelsymmetrisch.
Regeln 91/91, echte Anwendung 38/38, Legacy unverändert grün.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:37:23 +02:00

144 lines
6.3 KiB
Python

"""Lane tower for Champions of Luneraria: stone plinth, tapered shaft with brass bands,
crenellated platform and a hovering crystal. The crystal material (TW_Crystal) is tinted
per team at runtime; stone surfaces use the Kluft material names (ENV_*).
Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/build_tower.py
Godot coordinates are written through G(x, y, z) (x right, y up, z towards the viewer).
"""
from pathlib import Path
import bpy, bmesh, json, hashlib, struct
from math import sin, cos, pi
from mathutils import Vector, noise
P = Path(__file__).resolve().parents[2]
SRC = Path(__file__).resolve().parent
OUT = P / 'assets/luneraria'
OUT.mkdir(parents=True, exist_ok=True)
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.context.preferences.filepaths.save_version = 0
parts = []
def G(x, y, z):
return Vector((x, -z, y))
def material(name, color, rough=.9, metal=0.0, emission=0.0):
m = bpy.data.materials.new(name)
m.diffuse_color = (*color, 1)
m.use_nodes = True
bs = m.node_tree.nodes.get('Principled BSDF')
bs.inputs['Base Color'].default_value = (*color, 1)
bs.inputs['Roughness'].default_value = rough
bs.inputs['Metallic'].default_value = metal
if emission:
bs.inputs['Emission Color'].default_value = (*color, 1)
bs.inputs['Emission Strength'].default_value = emission
return m
M = {
'stone': material('ENV_limestone', (.283, .246, .178)),
'dark': material('ENV_cliff', (.133, .153, .138)),
'paving': material('ENV_paving', (.162, .171, .136), .92),
'brass': material('ENV_brass', (.515, .366, .153), .45, .7),
'crystal': material('TW_Crystal', (.55, .8, 1.0), .15, 0, 2.5),
}
def obj(name, vs, fs, kind, smooth=False):
me = bpy.data.meshes.new(name)
me.from_pydata([tuple(v) for v in vs], [], fs)
me.update()
me.materials.append(M[kind])
for p in me.polygons:
p.use_smooth = smooth
ob = bpy.data.objects.new(name, me)
bpy.context.collection.objects.link(ob)
parts.append(ob)
return ob
def lathe(name, profile, sides, kind, center=(0, 0, 0), rotation=0.0, jitter=0.0, seed=0):
cx, cy, cz = center
vs, fs = [], []
for j, (r, y) in enumerate(profile):
for k in range(sides):
a = 2 * pi * k / sides + rotation
rr = r * (1 + jitter * noise.noise(Vector((cos(a) * 3, y * 2, sin(a) * 3 + seed))))
vs.append(G(cx + cos(a) * rr, cy + y, cz + sin(a) * rr))
n = len(profile)
for j in range(n - 1):
for k in range(sides):
a, b = j * sides + k, j * sides + (k + 1) % sides
fs.append((a, b, b + sides, a + sides))
fs += [tuple(reversed(range(sides))), tuple(range((n - 1) * sides, n * sides))]
return obj(name, vs, fs, kind)
def box(name, center, size, yaw, kind):
cx, cy, cz = center
hx, hy, hz = size[0] / 2, size[1] / 2, size[2] / 2
vs = []
for dy in (-1, 1):
for dz in (-1, 1):
for dx in (-1, 1):
x, z = dx * hx, dz * hz
vs.append(G(cx + x * cos(yaw) - z * sin(yaw), cy + dy * hy, cz + x * sin(yaw) + z * cos(yaw)))
return obj(name, vs, [(0, 1, 3, 2), (4, 6, 7, 5), (0, 4, 5, 1), (2, 3, 7, 6), (0, 2, 6, 4), (1, 5, 7, 3)], kind)
# Plinth: two stepped octagons of dark stone with a paving top.
lathe('Plinth', [(1.65, 0.0), (1.65, .28), (1.45, .34), (1.45, .52), (1.3, .58)], 8, 'dark', rotation=pi / 8, jitter=.03, seed=1)
lathe('Plinth_Top', [(1.28, .57), (1.28, .62)], 8, 'paving', rotation=pi / 8)
# Shaft: tapered, slightly irregular limestone blocks.
shaft = [(1.02, .6), (.98, 1.4), (.9, 2.4), (.84, 3.2), (.9, 3.4)]
lathe('Shaft', shaft, 10, 'stone', rotation=pi / 10, jitter=.025, seed=2)
for h in (1.05, 2.25, 3.25):
r = .99 - (h - .6) * .05
lathe('Band_%d' % int(h * 10), [(r + .06, h - .07), (r + .06, h + .07)], 10, 'brass', rotation=pi / 10)
# Platform with crenellations.
lathe('Platform', [(.95, 3.35), (1.25, 3.6), (1.25, 3.85), (1.1, 3.9)], 10, 'stone', rotation=pi / 10, jitter=.02, seed=3)
for k in range(10):
a = 2 * pi * (k + .5) / 10
box('Merlon_%d' % k, (cos(a) * 1.08, 4.08, sin(a) * 1.08), (.42, .42, .26), -a + pi / 2, 'stone')
# Crystal cradle: four brass prongs and a hovering bipyramid crystal.
for k in range(4):
a = pi / 4 + k * pi / 2
pts = [(cos(a) * .55, 3.9, sin(a) * .55), (cos(a) * .42, 4.55, sin(a) * .42), (cos(a) * .22, 5.0, sin(a) * .22)]
for i in range(2):
p0, p1 = Vector(pts[i]), Vector(pts[i + 1])
mid = (p0 + p1) / 2
d = p1 - p0
box('Prong_%d_%d' % (k, i), tuple(mid), (.09, d.length + .05, .09), 0, 'brass')
crystal = [G(0, 5.95, 0)] + [G(cos(2 * pi * k / 6) * .36, 5.05, sin(2 * pi * k / 6) * .36) for k in range(6)] + [G(0, 4.45, 0)]
cf = [(0, 1 + k, 1 + (k + 1) % 6) for k in range(6)] + [(7, 1 + (k + 1) % 6, 1 + k) for k in range(6)]
obj('Crystal', crystal, cf, 'crystal')
# A small door and window slits keep the scale readable from the game camera.
box('Door', (0, .95, 1.0), (.5, .75, .12), 0, 'dark')
for k, h in enumerate((1.7, 2.7)):
box('Slit_%d' % k, (0, h, .93 - (h - 1.7) * .05), (.14, .42, .1), 0, 'dark')
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()
tower = bpy.context.object
tower.name = 'LaneTower'
bm = bmesh.new()
bm.from_mesh(tower.data)
bmesh.ops.triangulate(bm, faces=list(bm.faces))
bm.to_mesh(tower.data)
bm.free()
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'tower.blend'))
bpy.ops.export_scene.gltf(filepath=str(OUT / 'tower.glb'), export_format='GLB', use_selection=True,
export_materials='EXPORT', export_yup=True, export_animations=False)
raw = (OUT / 'tower.glb').read_bytes()
doc = json.loads(raw[20:20 + struct.unpack_from('<I', raw, 12)[0]])
triangles = sum(doc['accessors'][q['indices']]['count'] // 3 for m in doc['meshes'] for q in m['primitives'])
(P / 'assets/luneraria/tower_manifest.json').write_text(json.dumps({'assets': [{
'id': 'lane_tower', 'resource_path': 'res://assets/luneraria/tower.glb', 'source': 'assets-source/luneraria/tower.blend',
'triangles': triangles, 'height': 5.95, 'sha256': hashlib.sha256(raw).hexdigest(),
'materials': [m['name'] for m in doc['materials']]}]}, indent=2) + '\n')
print('TOWER_BUILD', triangles, 'triangles', flush=True)