Compare commits
11 commits
main
...
luneraria/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b540f2a55 | ||
|
|
9f62e30377 | ||
|
|
8b17563814 | ||
|
|
c15533831a | ||
|
|
214ed5a1cc | ||
|
|
faf0fbdef0 | ||
|
|
d7c3a03e23 | ||
|
|
f53859a298 | ||
|
|
c27a0097c3 | ||
|
|
b565546c37 | ||
|
|
4b6a4f530e |
653 changed files with 53601 additions and 42 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -2,3 +2,6 @@
|
|||
*.tmp
|
||||
docs/screenshots/
|
||||
test-results/
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
override.cfg
|
||||
|
|
|
|||
158
assets-source/luneraria/build_grotto.py
Normal file
158
assets-source/luneraria/build_grotto.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
"""Boss grotto for Champions of Luneraria: a rock island in the river with a crescent of
|
||||
cliffs, a cave mouth and violet crystals. Symmetric across the map diagonal, so both
|
||||
entrances (towards Blau and towards Rot) are equal. Stone uses ENV_* material names.
|
||||
|
||||
Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/build_grotto.py
|
||||
Local Godot frame: +x/+z are the map axes; the map diagonal (x = z) is the mirror line.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import bpy, bmesh, json, hashlib, struct
|
||||
from math import sin, cos, pi, sqrt
|
||||
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 = {
|
||||
'cliff': material('ENV_cliff', (.133, .153, .138)),
|
||||
'soil': material('ENV_soil', (.10, .15, .065), .97),
|
||||
'limestone': material('ENV_limestone', (.283, .246, .178)),
|
||||
'dark': material('GR_Shadow', (.02, .02, .03), 1.0),
|
||||
'crystal': material('GR_Crystal', (.62, .38, 1.0), .15, 0, 3.0),
|
||||
}
|
||||
|
||||
|
||||
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 island(name, radius, top, bottom, seed, kind='soil'):
|
||||
sides, levels = 64, 6
|
||||
vs, fs = [], []
|
||||
for li in range(levels):
|
||||
y = top - (top - bottom) * li / (levels - 1)
|
||||
for k in range(sides):
|
||||
a = 2 * pi * k / sides
|
||||
# Radius depends on |angle - 45 deg| so the island is symmetric across x = z.
|
||||
m = abs(((a - pi / 4) + pi) % (2 * pi) - pi)
|
||||
r = radius * (1 + .08 * noise.noise(Vector((cos(m) * 2, li * .5, seed)))) * (1 - .06 * li / levels)
|
||||
vs.append(G(cos(a) * r, y, sin(a) * r))
|
||||
for li in range(levels - 1):
|
||||
for k in range(sides):
|
||||
a, b = li * sides + k, li * sides + (k + 1) % sides
|
||||
fs.append((a, a + sides, b + sides, b))
|
||||
fs += [tuple(range(sides)), tuple(reversed(range((levels - 1) * sides, levels * sides)))]
|
||||
return obj(name, vs, fs, kind)
|
||||
|
||||
|
||||
def boulder(name, center, size, seed, kind='cliff'):
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_icosphere(bm, subdivisions=2, radius=1.0)
|
||||
c = G(*center)
|
||||
for v in bm.verts:
|
||||
d = v.co.normalized()
|
||||
r = 1 + .32 * noise.noise(d * 1.8 + Vector((seed, seed * .5, seed * .3)))
|
||||
gx, gy, gz = d.x * r * size[0], d.z * r * size[1], -d.y * r * size[2]
|
||||
if gy < -.25 * size[1]:
|
||||
gy = -.25 * size[1]
|
||||
v.co = c + Vector((gx, -gz, gy))
|
||||
me = bpy.data.meshes.new(name)
|
||||
bm.to_mesh(me)
|
||||
bm.free()
|
||||
me.materials.append(M[kind])
|
||||
ob = bpy.data.objects.new(name, me)
|
||||
bpy.context.collection.objects.link(ob)
|
||||
parts.append(ob)
|
||||
return ob
|
||||
|
||||
|
||||
def gem(name, center, height, radius, tilt, kind='crystal'):
|
||||
cx, cy, cz = center
|
||||
tip = G(cx + tilt[0], cy + height, cz + tilt[1])
|
||||
base = [G(cx + cos(2 * pi * k / 6) * radius, cy + height * .25, cz + sin(2 * pi * k / 6) * radius) for k in range(6)]
|
||||
root = G(cx, cy - .1, cz)
|
||||
vs = [tip] + base + [root]
|
||||
fs = [(0, 1 + (k + 1) % 6, 1 + k) for k in range(6)] + [(7, 1 + k, 1 + (k + 1) % 6) for k in range(6)]
|
||||
return obj(name, vs, fs, kind)
|
||||
|
||||
|
||||
def mirror(p):
|
||||
return (p[2], p[1], p[0])
|
||||
|
||||
|
||||
island('Island', 7.2, .35, -1.2, 3)
|
||||
# Crescent of cliffs on the far side from Mid (towards -x-z), open towards both entrances.
|
||||
ring = []
|
||||
for k in range(9):
|
||||
a = pi + pi / 4 + (k - 4) * .30
|
||||
ring.append((cos(a) * 5.6, 0.0, sin(a) * 5.6, 1.5 + (1.2 if abs(k - 4) <= 1 else .5 * (4 - abs(k - 4)) / 4)))
|
||||
for i, (x, y, z, h) in enumerate(ring):
|
||||
boulder('Cliff_%d' % i, (x, y + h * .45, z), (1.6, h, 1.4), 10 + min(i, 8 - i))
|
||||
# Cave mouth: dark recess under an arch in the middle of the crescent.
|
||||
a = pi + pi / 4
|
||||
boulder('Cave_Shadow', (cos(a) * 4.6, .8, sin(a) * 4.6), (1.4, 1.1, .7), 31, 'dark')
|
||||
boulder('Arch_Top', (cos(a) * 4.8, 3.1, sin(a) * 4.8), (2.2, .8, 1.3), 32)
|
||||
# Violet crystals, mirrored pairs so neither entrance looks different.
|
||||
for i, (x, z, h, r) in enumerate([(-2.2, -3.6, 1.8, .32), (-3.9, -1.2, 1.4, .28), (-1.0, -4.3, 1.1, .22), (-3.2, -3.2, 2.3, .4)]):
|
||||
for p in {(x, 0.3, z), mirror((x, 0.3, z))}:
|
||||
gem('Crystal_%d_%d' % (i, int(p[0] * 10)), p, h, r, (.12 * (1 if p[0] < p[2] else -1), .08))
|
||||
# Entrance stones: symmetric pair of limestone steps facing Blau (+x) and Rot (+z)... see manifest.
|
||||
for p in [(4.6, .18, 1.2), mirror((4.6, .18, 1.2))]:
|
||||
boulder('Step_%d' % int(p[0] * 10), p, (1.1, .25, .8), 40, 'limestone')
|
||||
|
||||
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()
|
||||
grotto = bpy.context.object
|
||||
grotto.name = 'BossGrotto'
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(grotto.data)
|
||||
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
||||
bm.to_mesh(grotto.data)
|
||||
bm.free()
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'grotto.blend'))
|
||||
bpy.ops.export_scene.gltf(filepath=str(OUT / 'grotto.glb'), export_format='GLB', use_selection=True,
|
||||
export_materials='EXPORT', export_yup=True, export_animations=False)
|
||||
raw = (OUT / 'grotto.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'])
|
||||
(OUT / 'grotto_manifest.json').write_text(json.dumps({'assets': [{
|
||||
'id': 'boss_grotto', 'resource_path': 'res://assets/luneraria/grotto.glb', 'source': 'assets-source/luneraria/grotto.blend',
|
||||
'triangles': triangles, 'sha256': hashlib.sha256(raw).hexdigest(),
|
||||
'materials': [m['name'] for m in doc['materials']]}]}, indent=2) + '\n')
|
||||
print('GROTTO_BUILD', triangles, 'triangles', flush=True)
|
||||
248
assets-source/luneraria/build_map_props.py
Normal file
248
assets-source/luneraria/build_map_props.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
"""Map dressing for Champions of Luneraria: river bridge, curb stone, trees, rocks, banner
|
||||
and edge cliffs, exported as separate named meshes in one GLB. Godot instances them with
|
||||
MultiMeshes. Stone, bark and leaves use ENV_* names so the Kluft shaders (with wind on
|
||||
foliage) apply at runtime.
|
||||
|
||||
Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/build_map_props.py
|
||||
Godot frame per prop: y up, prop origin at ground level; the bridge spans local +x.
|
||||
"""
|
||||
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'
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.context.preferences.filepaths.save_version = 0
|
||||
|
||||
|
||||
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 = {
|
||||
'limestone': material('ENV_limestone', (.283, .246, .178)),
|
||||
'paving': material('ENV_paving', (.19, .185, .15), .92),
|
||||
'cliff': material('ENV_cliff', (.133, .153, .138)),
|
||||
'bark': material('ENV_bark', (.087, .056, .032), .93),
|
||||
'leaf_dark': material('ENV_leaf_dark', (.031, .07, .04), .89),
|
||||
'leaf_mid': material('ENV_leaf_mid', (.084, .144, .054), .85),
|
||||
'leaf_light': material('ENV_leaf_light', (.258, .305, .091), .79),
|
||||
'soil': material('ENV_soil', (.10, .15, .065), .97),
|
||||
'brass': material('ENV_brass', (.515, .366, .153), .45, .7),
|
||||
'cloth': material('PR_Banner', (.9, .9, .9), .8),
|
||||
}
|
||||
groups = {}
|
||||
|
||||
|
||||
def add(group, 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)
|
||||
groups.setdefault(group, []).append(ob)
|
||||
return ob
|
||||
|
||||
|
||||
def box(group, name, c, size, kind, yaw=0.0):
|
||||
cx, cy, cz = c
|
||||
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 add(group, 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)
|
||||
|
||||
|
||||
def lathe(group, name, profile, sides, kind, c=(0, 0, 0), jitter=0.0, seed=0.0, smooth=False):
|
||||
cx, cy, cz = c
|
||||
vs, fs = [], []
|
||||
for r, y in profile:
|
||||
for k in range(sides):
|
||||
a = 2 * pi * k / sides
|
||||
rr = r * (1 + jitter * noise.noise(Vector((cos(a) * 2, y * 1.3, sin(a) * 2 + 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 add(group, name, vs, fs, kind, smooth)
|
||||
|
||||
|
||||
def blob(group, name, c, size, kind, seed, subdiv=2, amp=.3, floor=None, smooth=False):
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_icosphere(bm, subdivisions=subdiv, radius=1.0)
|
||||
cc = G(*c)
|
||||
for v in bm.verts:
|
||||
d = v.co.normalized()
|
||||
r = 1 + amp * noise.noise(d * 1.7 + Vector((seed, seed * .7, seed * .3)))
|
||||
gx, gy, gz = d.x * r * size[0], d.z * r * size[1], -d.y * r * size[2]
|
||||
if floor is not None and gy < floor * size[1]:
|
||||
gy = floor * size[1]
|
||||
v.co = cc + Vector((gx, -gz, gy))
|
||||
me = bpy.data.meshes.new(name)
|
||||
bm.to_mesh(me)
|
||||
bm.free()
|
||||
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)
|
||||
groups.setdefault(group, []).append(ob)
|
||||
return ob
|
||||
|
||||
|
||||
def tube(group, name, points, radii, kind, sides=8, steps=10, seed=0.0, gnarl=0.0):
|
||||
pts = [G(*p) for p in points]
|
||||
vs, fs = [], []
|
||||
for j in range(steps + 1):
|
||||
t = j / steps
|
||||
seg = min(len(pts) - 2, int(t * (len(pts) - 1)))
|
||||
u = t * (len(pts) - 1) - seg
|
||||
p = pts[seg].lerp(pts[seg + 1], u)
|
||||
tangent = (pts[seg + 1] - pts[seg]).normalized()
|
||||
ref = Vector((0, 0, 1)) if abs(tangent.z) < .9 else Vector((1, 0, 0))
|
||||
a1 = tangent.cross(ref).normalized()
|
||||
a2 = tangent.cross(a1)
|
||||
rs = min(len(radii) - 2, int(t * (len(radii) - 1)))
|
||||
ru = t * (len(radii) - 1) - rs
|
||||
radius = radii[rs] * (1 - ru) + radii[rs + 1] * ru
|
||||
for k in range(sides):
|
||||
ang = 2 * pi * k / sides
|
||||
w = 1 + gnarl * noise.noise(Vector((cos(ang), t * 5, sin(ang) + seed)))
|
||||
vs.append(p + (a1 * cos(ang) + a2 * sin(ang)) * radius * w)
|
||||
for j in range(steps):
|
||||
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(steps * sides, (steps + 1) * sides))]
|
||||
return add(group, name, vs, fs, kind, True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- bridge (spans local x, 11 m long, 7 m wide)
|
||||
# The simulation is flat: the deck sits at ground level so units walk on it, not inside it.
|
||||
L, Wd = 11.0, 7.0
|
||||
for i in range(12):
|
||||
x = -L / 2 + (i + .5) * L / 12
|
||||
box('Bridge', 'Deck_%d' % i, (x, .05, 0), (L / 12 + .02, .1, Wd), 'paving')
|
||||
for s_ in (-1, 1):
|
||||
box('Bridge', 'Parapet_%d_%d' % (i, s_), (x, .42, s_ * (Wd / 2 - .22)), (L / 12 + .03, .62, .38), 'limestone')
|
||||
for x in (-L / 2 + .4, -1.8, 1.8, L / 2 - .4):
|
||||
for s_ in (-1, 1):
|
||||
lathe('Bridge', 'Post_%d_%d' % (int(x * 10), s_), [(.32, 0), (.32, 1.05), (.26, 1.15), (.2, 1.3)], 6, 'limestone', (x, 0, s_ * (Wd / 2 - .22)))
|
||||
lathe('Bridge', 'Cap_%d_%d' % (int(x * 10), s_), [(.22, 0), (.22, .1), (.14, .16)], 6, 'brass', (x, 1.3, s_ * (Wd / 2 - .22)))
|
||||
for x in (-2.6, 2.6):
|
||||
for s_ in (-1, 1):
|
||||
box('Bridge', 'Pier_%d_%d' % (int(x), s_), (x, -.6, s_ * (Wd / 2 + .15)), (1.1, 1.4, .5), 'limestone')
|
||||
|
||||
# ---------------------------------------------------------------- curb stone (placed along lane edges)
|
||||
box('Curb', 'Curb', (0, .12, 0), (1.3, .26, .42), 'limestone')
|
||||
|
||||
# ---------------------------------------------------------------- trees
|
||||
for v, (h, seed) in enumerate([(3.8, 1.0), (4.6, 2.0), (3.2, 3.0)]):
|
||||
group = 'Pine_%d' % v
|
||||
tube(group, 'Trunk', [(0, 0, 0), (0, h * .55, 0), (0, h, 0)], [.24, .17, .08], 'bark', 8, 6, seed, .1)
|
||||
for k in range(5):
|
||||
y = h * (.28 + k * .15)
|
||||
r = (1.35 - k * .22) * (1 + .08 * v)
|
||||
lathe(group, 'Cone_%d' % k, [(r, y), (r * .75, y + .35), (.05, y + h * .26)], 9, ('leaf_dark', 'leaf_mid')[k % 2], (0, 0, 0), .12, seed + k)
|
||||
for v, (h, seed) in enumerate([(3.4, 4.0), (4.0, 5.0), (2.8, 6.0)]):
|
||||
group = 'Oak_%d' % v
|
||||
tube(group, 'Trunk', [(0, 0, 0), (.15, h * .5, .05), (-.1, h * .85, 0)], [.34, .24, .15], 'bark', 9, 8, seed, .15)
|
||||
for k in range(3):
|
||||
ang = k * 2.1 + seed
|
||||
tube(group, 'Branch_%d' % k, [(0, h * .55, 0), (cos(ang) * .9, h * .78, sin(ang) * .9)], [.13, .06], 'bark', 6, 4, seed + k, .1)
|
||||
for k in range(9):
|
||||
ang = k * 2.4 + seed
|
||||
rr = .55 + .5 * ((k * .618) % 1.0)
|
||||
s = .95 - .2 * ((k * .37) % 1.0)
|
||||
blob(group, 'Leaves_%d' % k, (cos(ang) * rr, h * .88 + .5 * ((k * .53) % 1.0), sin(ang) * rr), (s, s * .78, s),
|
||||
('leaf_mid', 'leaf_light', 'leaf_dark')[k % 3], seed * 10 + k, 2, .28)
|
||||
for k in range(4):
|
||||
ang = k * 1.57 + seed
|
||||
tube(group, 'Root_%d' % k, [(0, .25, 0), (cos(ang) * .7, .05, sin(ang) * .7)], [.14, .05], 'bark', 6, 4, seed + k)
|
||||
|
||||
# ---------------------------------------------------------------- rocks and shrubs
|
||||
for v, (size, seed) in enumerate([((1.2, .8, 1.0), 7.0), ((1.8, 1.1, 1.3), 8.0), ((.8, .5, .7), 9.0)]):
|
||||
group = 'Rock_%d' % v
|
||||
blob(group, 'Rock', (0, size[1] * .3, 0), size, 'cliff', seed, 2, .34, -.35)
|
||||
blob(group, 'Moss', (size[0] * .15, size[1] * .82, 0), (size[0] * .55, size[1] * .25, size[2] * .5), 'leaf_dark', seed + 1, 1, .3, -.2)
|
||||
for k in range(2):
|
||||
blob(group, 'Pebble_%d' % k, (size[0] * (.9 + .3 * k), .1, size[2] * (.5 - k)), (.28, .18, .24), 'limestone', seed + 3 + k, 1, .3, -.3)
|
||||
for v in range(2):
|
||||
blob('Shrub_%d' % v, 'Shrub', (0, .3, 0), (.8 + .3 * v, .55, .7 + .2 * v), ('leaf_mid', 'leaf_dark')[v], 11 + v, 2, .35, -.3)
|
||||
|
||||
# ---------------------------------------------------------------- banner (team colour at runtime via PR_Banner)
|
||||
lathe('Banner', 'Pole', [(.07, 0), (.07, 4.2), (.1, 4.3), (.02, 4.5)], 6, 'brass')
|
||||
vs, fs = [], []
|
||||
for j in range(6):
|
||||
for i in range(4):
|
||||
x = .08 + i * .35
|
||||
y = 4.1 - j * .45
|
||||
z = .08 * sin(i * 1.3 + j * .6)
|
||||
vs.append(G(x, y - (.25 * (i / 3) if j == 5 else 0), z))
|
||||
for j in range(5):
|
||||
for i in range(3):
|
||||
a = j * 4 + i
|
||||
fs.append((a, a + 1, a + 5, a + 4))
|
||||
add('Banner', 'Cloth', vs, fs, 'cloth', True)
|
||||
|
||||
# ---------------------------------------------------------------- edge cliffs (a 12 m wall segment, local x)
|
||||
for i in range(7):
|
||||
x = -6 + i * 2
|
||||
h = 3.4 + 1.6 * noise.noise(Vector((i * .7, 3.1, 0)))
|
||||
blob('EdgeCliff', 'Block_%d' % i, (x, h * .45, 0), (1.6, h * .6, 1.8), 'cliff', 20 + i, 2, .3, -.5)
|
||||
blob('EdgeCliff', 'Grass_%d' % i, (x, h * .85, -.3), (1.2, .35, 1.3), 'leaf_mid', 30 + i, 1, .3, -.2)
|
||||
|
||||
# Join each group into one mesh object per prop and triangulate.
|
||||
exported = []
|
||||
for group, objects in groups.items():
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for ob in objects:
|
||||
ob.select_set(True)
|
||||
bpy.context.view_layer.objects.active = objects[0]
|
||||
bpy.ops.object.join()
|
||||
joined = bpy.context.object
|
||||
joined.name = group
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(joined.data)
|
||||
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
||||
bm.to_mesh(joined.data)
|
||||
bm.free()
|
||||
exported.append(joined)
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for ob in exported:
|
||||
ob.select_set(True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'map_props.blend'))
|
||||
bpy.ops.export_scene.gltf(filepath=str(OUT / 'map_props.glb'), export_format='GLB', use_selection=True,
|
||||
export_materials='EXPORT', export_yup=True, export_animations=False)
|
||||
raw = (OUT / 'map_props.glb').read_bytes()
|
||||
doc = json.loads(raw[20:20 + struct.unpack_from('<I', raw, 12)[0]])
|
||||
counts = {m['name']: sum(doc['accessors'][q['indices']]['count'] // 3 for q in m['primitives']) for m in doc['meshes']}
|
||||
(OUT / 'map_props_manifest.json').write_text(json.dumps({'assets': [{'id': 'map_props', 'resource_path': 'res://assets/luneraria/map_props.glb',
|
||||
'source': 'assets-source/luneraria/map_props.blend', 'props': counts, 'sha256': hashlib.sha256(raw).hexdigest()}]}, indent=2) + '\n')
|
||||
print('MAP_PROPS_BUILD', counts, flush=True)
|
||||
1457
assets-source/luneraria/build_peoples.py
Normal file
1457
assets-source/luneraria/build_peoples.py
Normal file
File diff suppressed because it is too large
Load diff
144
assets-source/luneraria/build_tower.py
Normal file
144
assets-source/luneraria/build_tower.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
"""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)
|
||||
BIN
assets-source/luneraria/g2/ancient.blend
Normal file
BIN
assets-source/luneraria/g2/ancient.blend
Normal file
Binary file not shown.
BIN
assets-source/luneraria/g2/bastion.blend
Normal file
BIN
assets-source/luneraria/g2/bastion.blend
Normal file
Binary file not shown.
BIN
assets-source/luneraria/g2/bossgrotte.blend
Normal file
BIN
assets-source/luneraria/g2/bossgrotte.blend
Normal file
Binary file not shown.
290
assets-source/luneraria/g2/build_ancient.py
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
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
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
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
BIN
assets-source/luneraria/g2/grenzwacht.blend
Normal file
Binary file not shown.
1
assets-source/luneraria/g2/grotto_site.json
Normal file
1
assets-source/luneraria/g2/grotto_site.json
Normal file
File diff suppressed because one or more lines are too long
BIN
assets-source/luneraria/g2/kernwacht.blend
Normal file
BIN
assets-source/luneraria/g2/kernwacht.blend
Normal file
Binary file not shown.
662
assets-source/luneraria/g2/lune_bpy.py
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
BIN
assets-source/luneraria/g2/schlundkrone.blend
Normal file
Binary file not shown.
BIN
assets-source/luneraria/grotto.blend
Normal file
BIN
assets-source/luneraria/grotto.blend
Normal file
Binary file not shown.
BIN
assets-source/luneraria/map_props.blend
Normal file
BIN
assets-source/luneraria/map_props.blend
Normal file
Binary file not shown.
245
assets-source/luneraria/peoples-audit.json
Normal file
245
assets-source/luneraria/peoples-audit.json
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
{
|
||||
"figures": [
|
||||
{
|
||||
"id": "p_mensch",
|
||||
"source_parts": 84,
|
||||
"exported_clips": [
|
||||
"ability",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"death",
|
||||
"hit",
|
||||
"idle",
|
||||
"start",
|
||||
"stop",
|
||||
"walk"
|
||||
],
|
||||
"skins": 1,
|
||||
"materials": [
|
||||
"LP_Mensch_Cloth",
|
||||
"LP_Mensch_Skin",
|
||||
"LP_Mensch_Metal",
|
||||
"LP_Mensch_TrimMetal",
|
||||
"LP_Mensch_Leather",
|
||||
"LP_Mensch_Dark",
|
||||
"LP_Mensch_ClothAccent"
|
||||
],
|
||||
"max_wrist_contact_error_m": 0.0,
|
||||
"worst_sample": [
|
||||
"walk",
|
||||
1.1,
|
||||
"R"
|
||||
],
|
||||
"samples": 696,
|
||||
"contact_validation": "Analytic two-bone arm IK against the grip bones, baked at 30 Hz for all nine clips."
|
||||
},
|
||||
{
|
||||
"id": "p_elf",
|
||||
"source_parts": 89,
|
||||
"exported_clips": [
|
||||
"ability",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"death",
|
||||
"hit",
|
||||
"idle",
|
||||
"start",
|
||||
"stop",
|
||||
"walk"
|
||||
],
|
||||
"skins": 1,
|
||||
"materials": [
|
||||
"LP_Elf_Leather",
|
||||
"LP_Elf_Skin",
|
||||
"LP_Elf_Cloth",
|
||||
"LP_Elf_ClothAccent",
|
||||
"LP_Elf_TrimMetal",
|
||||
"LP_Elf_Dark",
|
||||
"LP_Elf_Hair",
|
||||
"LP_Elf_Glow",
|
||||
"LP_Elf_Wood"
|
||||
],
|
||||
"max_wrist_contact_error_m": 0.0,
|
||||
"worst_sample": [
|
||||
"walk",
|
||||
1.1,
|
||||
"R"
|
||||
],
|
||||
"samples": 696,
|
||||
"contact_validation": "Analytic two-bone arm IK against the grip bones, baked at 30 Hz for all nine clips."
|
||||
},
|
||||
{
|
||||
"id": "p_zwerg",
|
||||
"source_parts": 91,
|
||||
"exported_clips": [
|
||||
"ability",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"death",
|
||||
"hit",
|
||||
"idle",
|
||||
"start",
|
||||
"stop",
|
||||
"walk"
|
||||
],
|
||||
"skins": 1,
|
||||
"materials": [
|
||||
"LP_Zwerg_Cloth",
|
||||
"LP_Zwerg_Skin",
|
||||
"LP_Zwerg_Leather",
|
||||
"LP_Zwerg_Metal",
|
||||
"LP_Zwerg_TrimMetal",
|
||||
"LP_Zwerg_Dark",
|
||||
"LP_Zwerg_Hair",
|
||||
"LP_Zwerg_Bone",
|
||||
"LP_Zwerg_Wood",
|
||||
"LP_Zwerg_Glow"
|
||||
],
|
||||
"max_wrist_contact_error_m": 0.0,
|
||||
"worst_sample": [
|
||||
"walk",
|
||||
1.1,
|
||||
"R"
|
||||
],
|
||||
"samples": 696,
|
||||
"contact_validation": "Analytic two-bone arm IK against the grip bones, baked at 30 Hz for all nine clips."
|
||||
},
|
||||
{
|
||||
"id": "p_troll",
|
||||
"source_parts": 72,
|
||||
"exported_clips": [
|
||||
"ability",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"death",
|
||||
"hit",
|
||||
"idle",
|
||||
"start",
|
||||
"stop",
|
||||
"walk"
|
||||
],
|
||||
"skins": 1,
|
||||
"materials": [
|
||||
"LP_Troll_Skin",
|
||||
"LP_Troll_Leather",
|
||||
"LP_Troll_Bone",
|
||||
"LP_Troll_Stone",
|
||||
"LP_Troll_EyeGlow",
|
||||
"LP_Troll_Hair",
|
||||
"LP_Troll_Wood"
|
||||
],
|
||||
"max_wrist_contact_error_m": 0.06613055871718698,
|
||||
"worst_sample": [
|
||||
"ability",
|
||||
1.1,
|
||||
"L"
|
||||
],
|
||||
"samples": 696,
|
||||
"contact_validation": "Analytic two-bone arm IK against the grip bones, baked at 30 Hz for all nine clips."
|
||||
},
|
||||
{
|
||||
"id": "p_fee",
|
||||
"source_parts": 72,
|
||||
"exported_clips": [
|
||||
"ability",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"death",
|
||||
"hit",
|
||||
"idle",
|
||||
"start",
|
||||
"stop",
|
||||
"walk"
|
||||
],
|
||||
"skins": 1,
|
||||
"materials": [
|
||||
"LP_Fee_ClothAccent",
|
||||
"LP_Fee_Skin",
|
||||
"LP_Fee_Cloth",
|
||||
"LP_Fee_TrimMetal",
|
||||
"LP_Fee_Dark",
|
||||
"LP_Fee_Hair",
|
||||
"LP_Fee_Glow",
|
||||
"LP_Fee_Wood"
|
||||
],
|
||||
"max_wrist_contact_error_m": 0.0,
|
||||
"worst_sample": [
|
||||
"walk",
|
||||
1.1,
|
||||
"R"
|
||||
],
|
||||
"samples": 696,
|
||||
"contact_validation": "Analytic two-bone arm IK against the grip bones, baked at 30 Hz for all nine clips."
|
||||
},
|
||||
{
|
||||
"id": "p_ork",
|
||||
"source_parts": 84,
|
||||
"exported_clips": [
|
||||
"ability",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"death",
|
||||
"hit",
|
||||
"idle",
|
||||
"start",
|
||||
"stop",
|
||||
"walk"
|
||||
],
|
||||
"skins": 1,
|
||||
"materials": [
|
||||
"LP_Ork_Skin",
|
||||
"LP_Ork_Leather",
|
||||
"LP_Ork_Metal",
|
||||
"LP_Ork_TrimMetal",
|
||||
"LP_Ork_Bone",
|
||||
"LP_Ork_Fur",
|
||||
"LP_Ork_Dark",
|
||||
"LP_Ork_Hair",
|
||||
"LP_Ork_Wood"
|
||||
],
|
||||
"max_wrist_contact_error_m": 0.0,
|
||||
"worst_sample": [
|
||||
"walk",
|
||||
1.1,
|
||||
"R"
|
||||
],
|
||||
"samples": 696,
|
||||
"contact_validation": "Analytic two-bone arm IK against the grip bones, baked at 30 Hz for all nine clips."
|
||||
},
|
||||
{
|
||||
"id": "p_biest",
|
||||
"source_parts": 82,
|
||||
"exported_clips": [
|
||||
"ability",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"death",
|
||||
"hit",
|
||||
"idle",
|
||||
"start",
|
||||
"stop",
|
||||
"walk"
|
||||
],
|
||||
"skins": 1,
|
||||
"materials": [
|
||||
"LP_Biest_Fur",
|
||||
"LP_Biest_FurLight",
|
||||
"LP_Biest_Leather",
|
||||
"LP_Biest_Metal",
|
||||
"LP_Biest_Cloth",
|
||||
"LP_Biest_Bone",
|
||||
"LP_Biest_Dark",
|
||||
"LP_Biest_EyeGlow",
|
||||
"LP_Biest_Skin"
|
||||
],
|
||||
"max_wrist_contact_error_m": 0.0,
|
||||
"worst_sample": [
|
||||
"walk",
|
||||
1.1,
|
||||
"R"
|
||||
],
|
||||
"samples": 696,
|
||||
"contact_validation": "Analytic two-bone arm IK against the grip bones, baked at 30 Hz for all nine clips."
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
assets-source/luneraria/peoples/p_biest.blend
Normal file
BIN
assets-source/luneraria/peoples/p_biest.blend
Normal file
Binary file not shown.
BIN
assets-source/luneraria/peoples/p_elf.blend
Normal file
BIN
assets-source/luneraria/peoples/p_elf.blend
Normal file
Binary file not shown.
BIN
assets-source/luneraria/peoples/p_fee.blend
Normal file
BIN
assets-source/luneraria/peoples/p_fee.blend
Normal file
Binary file not shown.
BIN
assets-source/luneraria/peoples/p_mensch.blend
Normal file
BIN
assets-source/luneraria/peoples/p_mensch.blend
Normal file
Binary file not shown.
BIN
assets-source/luneraria/peoples/p_ork.blend
Normal file
BIN
assets-source/luneraria/peoples/p_ork.blend
Normal file
Binary file not shown.
BIN
assets-source/luneraria/peoples/p_troll.blend
Normal file
BIN
assets-source/luneraria/peoples/p_troll.blend
Normal file
Binary file not shown.
BIN
assets-source/luneraria/peoples/p_zwerg.blend
Normal file
BIN
assets-source/luneraria/peoples/p_zwerg.blend
Normal file
Binary file not shown.
BIN
assets-source/luneraria/tower.blend
Normal file
BIN
assets-source/luneraria/tower.blend
Normal file
Binary file not shown.
40
assets/ChatGPT-Bild 27. Sept. 2026, 19_46_52-1.png.import
Normal file
40
assets/ChatGPT-Bild 27. Sept. 2026, 19_46_52-1.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cdyqe8uxp766t"
|
||||
path="res://.godot/imported/ChatGPT-Bild 27. Sept. 2026, 19_46_52-1.png-2d037606185a25aabd1841674c9a8676.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/ChatGPT-Bild 27. Sept. 2026, 19_46_52-1.png"
|
||||
dest_files=["res://.godot/imported/ChatGPT-Bild 27. Sept. 2026, 19_46_52-1.png-2d037606185a25aabd1841674c9a8676.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
assets/ChatGPT-Bild 27. Sept. 2026, 19_46_52-2.png.import
Normal file
40
assets/ChatGPT-Bild 27. Sept. 2026, 19_46_52-2.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cm5ok6wwo5k4c"
|
||||
path="res://.godot/imported/ChatGPT-Bild 27. Sept. 2026, 19_46_52-2.png-ad15328ef82598f8d29650086961c8e2.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/ChatGPT-Bild 27. Sept. 2026, 19_46_52-2.png"
|
||||
dest_files=["res://.godot/imported/ChatGPT-Bild 27. Sept. 2026, 19_46_52-2.png-ad15328ef82598f8d29650086961c8e2.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
40
assets/ChatGPT-Bild 27. Sept. 2026, 19_46_53-3.png.import
Normal file
40
assets/ChatGPT-Bild 27. Sept. 2026, 19_46_53-3.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bjtulyxjl22dy"
|
||||
path="res://.godot/imported/ChatGPT-Bild 27. Sept. 2026, 19_46_53-3.png-7e556551f7b3efde4bca6a6f73eb8860.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/ChatGPT-Bild 27. Sept. 2026, 19_46_53-3.png"
|
||||
dest_files=["res://.godot/imported/ChatGPT-Bild 27. Sept. 2026, 19_46_53-3.png-7e556551f7b3efde4bca6a6f73eb8860.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
|
|
@ -4,8 +4,9 @@ importer="texture"
|
|||
type="CompressedTexture2D"
|
||||
uid="uid://qglgrg8yrddc"
|
||||
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={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
|
|
@ -15,7 +16,7 @@ generator_parameters={
|
|||
[deps]
|
||||
|
||||
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]
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ importer="texture"
|
|||
type="CompressedTexture2D"
|
||||
uid="uid://varymvp2etyi"
|
||||
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={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
|
|
@ -15,7 +16,7 @@ generator_parameters={
|
|||
[deps]
|
||||
|
||||
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]
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ importer="texture"
|
|||
type="CompressedTexture2D"
|
||||
uid="uid://bqy23d8gmsx38"
|
||||
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={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
|
|
@ -15,7 +16,7 @@ generator_parameters={
|
|||
[deps]
|
||||
|
||||
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]
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ importer="texture"
|
|||
type="CompressedTexture2D"
|
||||
uid="uid://b1phtpe3asudy"
|
||||
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={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
|
|
@ -15,7 +16,7 @@ generator_parameters={
|
|||
[deps]
|
||||
|
||||
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]
|
||||
|
||||
|
|
|
|||
BIN
assets/luneraria/g2/ancient.glb
Normal file
BIN
assets/luneraria/g2/ancient.glb
Normal file
Binary file not shown.
45
assets/luneraria/g2/ancient.glb.import
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
BIN
assets/luneraria/g2/bastion.glb
Normal file
Binary file not shown.
45
assets/luneraria/g2/bastion.glb.import
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
BIN
assets/luneraria/g2/bossgrotte.glb
Normal file
Binary file not shown.
45
assets/luneraria/g2/bossgrotte.glb.import
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
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
BIN
assets/luneraria/g2/grenzwacht.glb
Normal file
Binary file not shown.
45
assets/luneraria/g2/grenzwacht.glb.import
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
BIN
assets/luneraria/g2/kernwacht.glb
Normal file
Binary file not shown.
45
assets/luneraria/g2/kernwacht.glb.import
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
BIN
assets/luneraria/g2/schlundkrone.glb
Normal file
Binary file not shown.
45
assets/luneraria/g2/schlundkrone.glb.import
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
|
||||
BIN
assets/luneraria/grotto.glb
Normal file
BIN
assets/luneraria/grotto.glb
Normal file
Binary file not shown.
45
assets/luneraria/grotto.glb.import
Normal file
45
assets/luneraria/grotto.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://g2wd2l3wkvb7"
|
||||
path="res://.godot/imported/grotto.glb-415ef169549c859f990847e218fcf14a.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/luneraria/grotto.glb"
|
||||
dest_files=["res://.godot/imported/grotto.glb-415ef169549c859f990847e218fcf14a.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
|
||||
18
assets/luneraria/grotto_manifest.json
Normal file
18
assets/luneraria/grotto_manifest.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"assets": [
|
||||
{
|
||||
"id": "boss_grotto",
|
||||
"resource_path": "res://assets/luneraria/grotto.glb",
|
||||
"source": "assets-source/luneraria/grotto.blend",
|
||||
"triangles": 1888,
|
||||
"sha256": "92e7a4b4c98ebafb167523fb2816d91e997add201bf223ab6e9c8326bf77daf9",
|
||||
"materials": [
|
||||
"ENV_soil",
|
||||
"ENV_cliff",
|
||||
"GR_Shadow",
|
||||
"GR_Crystal",
|
||||
"ENV_limestone"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
assets/luneraria/map_props.glb
Normal file
BIN
assets/luneraria/map_props.glb
Normal file
Binary file not shown.
45
assets/luneraria/map_props.glb.import
Normal file
45
assets/luneraria/map_props.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://bdygvqh1lru4a"
|
||||
path="res://.godot/imported/map_props.glb-ee8d7dcb8819cf2777bc63c68f327e1d.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/luneraria/map_props.glb"
|
||||
dest_files=["res://.godot/imported/map_props.glb-ee8d7dcb8819cf2777bc63c68f327e1d.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
|
||||
27
assets/luneraria/map_props_manifest.json
Normal file
27
assets/luneraria/map_props_manifest.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"assets": [
|
||||
{
|
||||
"id": "map_props",
|
||||
"resource_path": "res://assets/luneraria/map_props.glb",
|
||||
"source": "assets-source/luneraria/map_props.blend",
|
||||
"props": {
|
||||
"Pole": 74,
|
||||
"Deck_0": 1088,
|
||||
"Curb": 12,
|
||||
"Block_0": 700,
|
||||
"Trunk.003": 1270,
|
||||
"Trunk.004": 1270,
|
||||
"Trunk.005": 1270,
|
||||
"Trunk": 358,
|
||||
"Trunk.001": 358,
|
||||
"Trunk.002": 358,
|
||||
"Rock": 140,
|
||||
"Rock.001": 140,
|
||||
"Rock.002": 140,
|
||||
"Shrub": 80,
|
||||
"Shrub.001": 80
|
||||
},
|
||||
"sha256": "d997546df3d537dae03ff5d8925f0af9457c3cc5ee63b6f20018baa994e8ff9d"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
assets/luneraria/peoples/p_biest.glb
Normal file
BIN
assets/luneraria/peoples/p_biest.glb
Normal file
Binary file not shown.
45
assets/luneraria/peoples/p_biest.glb.import
Normal file
45
assets/luneraria/peoples/p_biest.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://vlty7tphfgnx"
|
||||
path="res://.godot/imported/p_biest.glb-8e5e6e1fa11c916fb2bef94a33cda329.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/luneraria/peoples/p_biest.glb"
|
||||
dest_files=["res://.godot/imported/p_biest.glb-8e5e6e1fa11c916fb2bef94a33cda329.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/peoples/p_elf.glb
Normal file
BIN
assets/luneraria/peoples/p_elf.glb
Normal file
Binary file not shown.
45
assets/luneraria/peoples/p_elf.glb.import
Normal file
45
assets/luneraria/peoples/p_elf.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://pw4ristm1wtt"
|
||||
path="res://.godot/imported/p_elf.glb-d890d18acc6c79e79d4b8598982a53f2.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/luneraria/peoples/p_elf.glb"
|
||||
dest_files=["res://.godot/imported/p_elf.glb-d890d18acc6c79e79d4b8598982a53f2.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/peoples/p_fee.glb
Normal file
BIN
assets/luneraria/peoples/p_fee.glb
Normal file
Binary file not shown.
45
assets/luneraria/peoples/p_fee.glb.import
Normal file
45
assets/luneraria/peoples/p_fee.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://biourdwo5b8lq"
|
||||
path="res://.godot/imported/p_fee.glb-84ebd920f33b8ac4eaaa887665f7685d.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/luneraria/peoples/p_fee.glb"
|
||||
dest_files=["res://.godot/imported/p_fee.glb-84ebd920f33b8ac4eaaa887665f7685d.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/peoples/p_mensch.glb
Normal file
BIN
assets/luneraria/peoples/p_mensch.glb
Normal file
Binary file not shown.
45
assets/luneraria/peoples/p_mensch.glb.import
Normal file
45
assets/luneraria/peoples/p_mensch.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://irnp3eyfc180"
|
||||
path="res://.godot/imported/p_mensch.glb-39d67a910d0ef6afd640de8d0f7f2922.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/luneraria/peoples/p_mensch.glb"
|
||||
dest_files=["res://.godot/imported/p_mensch.glb-39d67a910d0ef6afd640de8d0f7f2922.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/peoples/p_ork.glb
Normal file
BIN
assets/luneraria/peoples/p_ork.glb
Normal file
Binary file not shown.
45
assets/luneraria/peoples/p_ork.glb.import
Normal file
45
assets/luneraria/peoples/p_ork.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://ch346bo30gv7u"
|
||||
path="res://.godot/imported/p_ork.glb-2fc1770fe3cbd6425271bd6ed96455c7.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/luneraria/peoples/p_ork.glb"
|
||||
dest_files=["res://.godot/imported/p_ork.glb-2fc1770fe3cbd6425271bd6ed96455c7.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/peoples/p_troll.glb
Normal file
BIN
assets/luneraria/peoples/p_troll.glb
Normal file
Binary file not shown.
45
assets/luneraria/peoples/p_troll.glb.import
Normal file
45
assets/luneraria/peoples/p_troll.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://cgmg6jsstbn0w"
|
||||
path="res://.godot/imported/p_troll.glb-1e8b28d4cdbb22a6f14981dad1c5577b.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/luneraria/peoples/p_troll.glb"
|
||||
dest_files=["res://.godot/imported/p_troll.glb-1e8b28d4cdbb22a6f14981dad1c5577b.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/peoples/p_zwerg.glb
Normal file
BIN
assets/luneraria/peoples/p_zwerg.glb
Normal file
Binary file not shown.
45
assets/luneraria/peoples/p_zwerg.glb.import
Normal file
45
assets/luneraria/peoples/p_zwerg.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://cl7r1ec10jtmg"
|
||||
path="res://.godot/imported/p_zwerg.glb-e90f71dd7991679fd78ec39f4df7c5be.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/luneraria/peoples/p_zwerg.glb"
|
||||
dest_files=["res://.godot/imported/p_zwerg.glb-e90f71dd7991679fd78ec39f4df7c5be.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
|
||||
333
assets/luneraria/peoples_manifest.json
Normal file
333
assets/luneraria/peoples_manifest.json
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
{
|
||||
"assets": [
|
||||
{
|
||||
"id": "p_mensch",
|
||||
"name": "Ritter",
|
||||
"people": "Menschen",
|
||||
"klasse": "Ritter",
|
||||
"rolle": "front",
|
||||
"resource_path": "res://assets/luneraria/peoples/p_mensch.glb",
|
||||
"triangles": 17480,
|
||||
"mesh_parts": 1,
|
||||
"parts": [
|
||||
"MenschBody"
|
||||
],
|
||||
"skeletal": true,
|
||||
"bones": 22,
|
||||
"clips": [
|
||||
"idle",
|
||||
"start",
|
||||
"walk",
|
||||
"stop",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"hit",
|
||||
"ability",
|
||||
"death"
|
||||
],
|
||||
"attack_cycle": [
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"attack_a",
|
||||
"ability"
|
||||
],
|
||||
"contact_seconds": 0.9,
|
||||
"bounds_godot": {
|
||||
"min": [
|
||||
-0.40699902176856995,
|
||||
-0.01069701835513115,
|
||||
-0.27477124333381653
|
||||
],
|
||||
"max": [
|
||||
0.42016029357910156,
|
||||
1.9657350778579712,
|
||||
0.6130658388137817
|
||||
]
|
||||
},
|
||||
"sha256": "218b6d020a0d70e6693e6b61c9eb2d3090e8365b23afe675268a9d0084bacd73",
|
||||
"source": "assets-source/luneraria/peoples/p_mensch.blend"
|
||||
},
|
||||
{
|
||||
"id": "p_elf",
|
||||
"name": "Waldl\u00e4uferin",
|
||||
"people": "Elfen",
|
||||
"klasse": "Waldl\u00e4uferin",
|
||||
"rolle": "ranged",
|
||||
"resource_path": "res://assets/luneraria/peoples/p_elf.glb",
|
||||
"triangles": 18502,
|
||||
"mesh_parts": 1,
|
||||
"parts": [
|
||||
"ElfBody"
|
||||
],
|
||||
"skeletal": true,
|
||||
"bones": 24,
|
||||
"clips": [
|
||||
"idle",
|
||||
"start",
|
||||
"walk",
|
||||
"stop",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"hit",
|
||||
"ability",
|
||||
"death"
|
||||
],
|
||||
"attack_cycle": [
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"attack_a",
|
||||
"ability"
|
||||
],
|
||||
"contact_seconds": 0.9,
|
||||
"bounds_godot": {
|
||||
"min": [
|
||||
-0.274183988571167,
|
||||
0.00043252474279142916,
|
||||
-0.5287083387374878
|
||||
],
|
||||
"max": [
|
||||
0.29548221826553345,
|
||||
2.230403423309326,
|
||||
1.1226369142532349
|
||||
]
|
||||
},
|
||||
"sha256": "9c155a1cf3868185cbabdc4ee26fd23e53006d4a093c2f2d623eb42317da02a6",
|
||||
"source": "assets-source/luneraria/peoples/p_elf.blend"
|
||||
},
|
||||
{
|
||||
"id": "p_zwerg",
|
||||
"name": "Schildtr\u00e4ger",
|
||||
"people": "Zwerge",
|
||||
"klasse": "Schildtr\u00e4ger",
|
||||
"rolle": "front",
|
||||
"resource_path": "res://assets/luneraria/peoples/p_zwerg.glb",
|
||||
"triangles": 20214,
|
||||
"mesh_parts": 1,
|
||||
"parts": [
|
||||
"ZwergBody"
|
||||
],
|
||||
"skeletal": true,
|
||||
"bones": 22,
|
||||
"clips": [
|
||||
"idle",
|
||||
"start",
|
||||
"walk",
|
||||
"stop",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"hit",
|
||||
"ability",
|
||||
"death"
|
||||
],
|
||||
"attack_cycle": [
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"attack_a",
|
||||
"ability"
|
||||
],
|
||||
"contact_seconds": 0.9,
|
||||
"bounds_godot": {
|
||||
"min": [
|
||||
-0.3604458272457123,
|
||||
-0.018512045964598656,
|
||||
-0.17344079911708832
|
||||
],
|
||||
"max": [
|
||||
0.4053660035133362,
|
||||
1.3439915180206299,
|
||||
0.4210769236087799
|
||||
]
|
||||
},
|
||||
"sha256": "bb6706d13f8cf28d3142a919f66d69192167577d769a5de781a4ee7ac0b42559",
|
||||
"source": "assets-source/luneraria/peoples/p_zwerg.blend"
|
||||
},
|
||||
{
|
||||
"id": "p_troll",
|
||||
"name": "Steinhauer",
|
||||
"people": "Trolle",
|
||||
"klasse": "Steinhauer",
|
||||
"rolle": "splash",
|
||||
"resource_path": "res://assets/luneraria/peoples/p_troll.glb",
|
||||
"triangles": 14686,
|
||||
"mesh_parts": 1,
|
||||
"parts": [
|
||||
"TrollBody"
|
||||
],
|
||||
"skeletal": true,
|
||||
"bones": 22,
|
||||
"clips": [
|
||||
"idle",
|
||||
"start",
|
||||
"walk",
|
||||
"stop",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"hit",
|
||||
"ability",
|
||||
"death"
|
||||
],
|
||||
"attack_cycle": [
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"attack_a",
|
||||
"ability"
|
||||
],
|
||||
"contact_seconds": 0.9,
|
||||
"bounds_godot": {
|
||||
"min": [
|
||||
-0.5339592695236206,
|
||||
-0.03151940554380417,
|
||||
-0.2999359965324402
|
||||
],
|
||||
"max": [
|
||||
0.49503999948501587,
|
||||
2.198559045791626,
|
||||
1.255454421043396
|
||||
]
|
||||
},
|
||||
"sha256": "1c250068745bd7305569fd7b4db2848b86fea486dc5af139798939141597c259",
|
||||
"source": "assets-source/luneraria/peoples/p_troll.blend"
|
||||
},
|
||||
{
|
||||
"id": "p_fee",
|
||||
"name": "Lichtheilerin",
|
||||
"people": "Feen",
|
||||
"klasse": "Lichtheilerin",
|
||||
"rolle": "support",
|
||||
"resource_path": "res://assets/luneraria/peoples/p_fee.glb",
|
||||
"triangles": 16744,
|
||||
"mesh_parts": 1,
|
||||
"parts": [
|
||||
"FeeBody"
|
||||
],
|
||||
"skeletal": true,
|
||||
"bones": 24,
|
||||
"clips": [
|
||||
"idle",
|
||||
"start",
|
||||
"walk",
|
||||
"stop",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"hit",
|
||||
"ability",
|
||||
"death"
|
||||
],
|
||||
"attack_cycle": [
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"attack_a",
|
||||
"ability"
|
||||
],
|
||||
"contact_seconds": 0.9,
|
||||
"bounds_godot": {
|
||||
"min": [
|
||||
-0.35030224919319153,
|
||||
0.31140172481536865,
|
||||
-0.1565617471933365
|
||||
],
|
||||
"max": [
|
||||
0.35030224919319153,
|
||||
1.6979674100875854,
|
||||
0.2717953622341156
|
||||
]
|
||||
},
|
||||
"sha256": "f084d1f922d1a19d421b38e8ed68e944582dd0a1c4768e310773eca084f3b236",
|
||||
"source": "assets-source/luneraria/peoples/p_fee.blend"
|
||||
},
|
||||
{
|
||||
"id": "p_ork",
|
||||
"name": "Kriegsh\u00e4uptling",
|
||||
"people": "Orks",
|
||||
"klasse": "Kriegsh\u00e4uptling",
|
||||
"rolle": "melee",
|
||||
"resource_path": "res://assets/luneraria/peoples/p_ork.glb",
|
||||
"triangles": 15890,
|
||||
"mesh_parts": 1,
|
||||
"parts": [
|
||||
"OrkBody"
|
||||
],
|
||||
"skeletal": true,
|
||||
"bones": 22,
|
||||
"clips": [
|
||||
"idle",
|
||||
"start",
|
||||
"walk",
|
||||
"stop",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"hit",
|
||||
"ability",
|
||||
"death"
|
||||
],
|
||||
"attack_cycle": [
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"attack_a",
|
||||
"ability"
|
||||
],
|
||||
"contact_seconds": 0.9,
|
||||
"bounds_godot": {
|
||||
"min": [
|
||||
-0.5301840901374817,
|
||||
-0.0254998579621315,
|
||||
-0.18939527869224548
|
||||
],
|
||||
"max": [
|
||||
0.5364153385162354,
|
||||
2.0665111541748047,
|
||||
0.760603666305542
|
||||
]
|
||||
},
|
||||
"sha256": "6d9552f8c4e5fdfbba2a9a933a861c4a59b7dc3ab406e7070a7e2553e7b39bff",
|
||||
"source": "assets-source/luneraria/peoples/p_ork.blend"
|
||||
},
|
||||
{
|
||||
"id": "p_biest",
|
||||
"name": "Klauenj\u00e4ger",
|
||||
"people": "Biestmenschen",
|
||||
"klasse": "Klauenj\u00e4ger",
|
||||
"rolle": "melee",
|
||||
"resource_path": "res://assets/luneraria/peoples/p_biest.glb",
|
||||
"triangles": 16858,
|
||||
"mesh_parts": 1,
|
||||
"parts": [
|
||||
"BiestBody"
|
||||
],
|
||||
"skeletal": true,
|
||||
"bones": 24,
|
||||
"clips": [
|
||||
"idle",
|
||||
"start",
|
||||
"walk",
|
||||
"stop",
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"hit",
|
||||
"ability",
|
||||
"death"
|
||||
],
|
||||
"attack_cycle": [
|
||||
"attack_a",
|
||||
"attack_b",
|
||||
"attack_a",
|
||||
"ability"
|
||||
],
|
||||
"contact_seconds": 0.9,
|
||||
"bounds_godot": {
|
||||
"min": [
|
||||
-0.4814721643924713,
|
||||
0.009468089789152145,
|
||||
-0.5130493640899658
|
||||
],
|
||||
"max": [
|
||||
0.4814721643924713,
|
||||
2.072967052459717,
|
||||
0.6682941913604736
|
||||
]
|
||||
},
|
||||
"sha256": "d990197e3030d34455f8ad070a1f047cccea03d1dcccc81950a4b4b692a5f2d1",
|
||||
"source": "assets-source/luneraria/peoples/p_biest.blend"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
assets/luneraria/tower.glb
Normal file
BIN
assets/luneraria/tower.glb
Normal file
Binary file not shown.
45
assets/luneraria/tower.glb.import
Normal file
45
assets/luneraria/tower.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://fqj2uftgmgig"
|
||||
path="res://.godot/imported/tower.glb-367b5fb07a0c6d34829755c1c6ffa2f0.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/luneraria/tower.glb"
|
||||
dest_files=["res://.godot/imported/tower.glb-367b5fb07a0c6d34829755c1c6ffa2f0.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
|
||||
19
assets/luneraria/tower_manifest.json
Normal file
19
assets/luneraria/tower_manifest.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"assets": [
|
||||
{
|
||||
"id": "lane_tower",
|
||||
"resource_path": "res://assets/luneraria/tower.glb",
|
||||
"source": "assets-source/luneraria/tower.blend",
|
||||
"triangles": 648,
|
||||
"height": 5.95,
|
||||
"sha256": "52de81cafa3712efb78413d28d16fe112330d27af0e66a8c9b31fde7d6dd1442",
|
||||
"materials": [
|
||||
"ENV_cliff",
|
||||
"ENV_paving",
|
||||
"ENV_limestone",
|
||||
"ENV_brass",
|
||||
"TW_Crystal"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -4,15 +4,16 @@ importer="texture"
|
|||
type="CompressedTexture2D"
|
||||
uid="uid://gb8k2gh1fatp"
|
||||
path.bptc="res://.godot/imported/limestone-albedo.png-e2ef74eda1463a024457e6fa83926f32.bptc.ctex"
|
||||
path.astc="res://.godot/imported/limestone-albedo.png-e2ef74eda1463a024457e6fa83926f32.astc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
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]
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,16 @@ importer="texture"
|
|||
type="CompressedTexture2D"
|
||||
uid="uid://c5j13kmnfjj6q"
|
||||
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={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
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]
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,16 @@ importer="texture"
|
|||
type="CompressedTexture2D"
|
||||
uid="uid://dbx5xdwy7c2y1"
|
||||
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={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"imported_formats": ["s3tc_bptc", "etc2_astc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
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]
|
||||
|
||||
|
|
|
|||
17
assets/ui/emblems/luneraria.svg
Normal file
17
assets/ui/emblems/luneraria.svg
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<defs>
|
||||
<linearGradient id="stone" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#1f2733"/><stop offset="1" stop-color="#0c1016"/></linearGradient>
|
||||
<linearGradient id="moon" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fbf1d4"/><stop offset="1" stop-color="#c9a562"/></linearGradient>
|
||||
<linearGradient id="gem" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#d8f0ff"/><stop offset=".55" stop-color="#5aa8ff"/><stop offset="1" stop-color="#27508a"/></linearGradient>
|
||||
<radialGradient id="halo" cx=".5" cy=".46" r=".5"><stop offset="0" stop-color="#9fd0ff" stop-opacity=".45"/><stop offset="1" stop-color="#9fd0ff" stop-opacity="0"/></radialGradient>
|
||||
</defs>
|
||||
<circle cx="256" cy="256" r="232" fill="url(#stone)" stroke="#c99a58" stroke-width="12"/>
|
||||
<circle cx="256" cy="256" r="204" fill="none" stroke="#6b4f2e" stroke-width="4"/>
|
||||
<circle cx="256" cy="236" r="160" fill="url(#halo)"/>
|
||||
<path d="M300 70 A186 186 0 1 0 300 442 A214 214 0 0 1 300 70 Z" fill="url(#moon)" stroke="#fff4d8" stroke-width="4"/>
|
||||
<path d="M256 150 L300 236 L256 360 L212 236 Z" fill="url(#gem)" stroke="#eaf6ff" stroke-width="5" stroke-linejoin="round"/>
|
||||
<path d="M212 236 L256 262 L300 236 M256 150 L256 360" fill="none" stroke="#eaf6ff" stroke-width="2.5" opacity=".8"/>
|
||||
<path d="M256 70 l10 22 l22 10 l-22 10 l-10 22 l-10 -22 l-22 -10 l22 -10 Z" fill="#f6e2b0"/>
|
||||
<path d="M120 400 Q190 372 256 400 T392 400" fill="none" stroke="#6fd3b4" stroke-width="7" stroke-linecap="round" opacity=".85"/>
|
||||
<path d="M150 432 L178 404 M362 432 L334 404" stroke="#c99a58" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
43
assets/ui/emblems/luneraria.svg.import
Normal file
43
assets/ui/emblems/luneraria.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dq03newjr6neo"
|
||||
path="res://.godot/imported/luneraria.svg-12b34911b283c50c0fc77d4b9fb43e87.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/ui/emblems/luneraria.svg"
|
||||
dest_files=["res://.godot/imported/luneraria.svg-12b34911b283c50c0fc77d4b9fb43e87.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
6
data/luneraria/balance_default.tres
Normal file
6
data/luneraria/balance_default.tres
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[gd_resource type="Resource" script_class="LuneBalance" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/luneraria/core/lune_balance.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
155
docs/luneraria/ANLEITUNG.md
Normal file
155
docs/luneraria/ANLEITUNG.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# Installation, Start, Steuerung und Export
|
||||
|
||||
## 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
|
||||
cd saumwacht-godot
|
||||
godot --path . # Champions of Luneraria (Hauptszene scenes/luneraria.tscn)
|
||||
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.
|
||||
|
||||
**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
|
||||
|
||||
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.
|
||||
2. **Planung:** Truppen kaufen (Knöpfe oder 1–5), mit T/M/B einer Lane zuweisen (die blau umrandeten Aufmarschflächen vor den Basistoren zeigen, wo die Armee antritt), mit ↑ aufwerten, mit × zurückgeben. Käufe aus derselben Planung werden voll erstattet, ältere mit 75 %. **Startaufstellung** kauft Front, Nahkampf und Fernkampf. **Bereit** (Leertaste) übernimmt alle Befehle verbindlich.
|
||||
3. **Kampf:** Er läuft automatisch. Deine Truppen rücken mit der eigenen Monsterwelle über ihre Lane vor. Leertaste oder P pausiert, Tempo 1×/2×/4×.
|
||||
4. **Abrechnung:** Getötete Monster und Gebäude bringen Gold, dazu kommt das Rundeneinkommen. Deine Truppen kehren geheilt zurück. Türme und Kerne behalten ihren Schaden.
|
||||
5. Die Partie endet erst, wenn ein Ancient fällt. Es gibt kein Rundenlimit.
|
||||
|
||||
## Steuerung
|
||||
|
||||
| Eingabe | Wirkung |
|
||||
| --- | --- |
|
||||
| 1–5 | Truppe kaufen (Planung) |
|
||||
| Leertaste | Bereit / im Kampf Pause |
|
||||
| P | Pause |
|
||||
| WASD, Pfeiltasten, mittlere Maustaste ziehen | Kamera verschieben |
|
||||
| Mausrad | Zoom (nah flacher, weit steiler) |
|
||||
| F2 / F3 / F4 | Kamera auf Top / Mid / Bottom |
|
||||
| Tab / Lane-Knöpfe links | Kamera auf die nächste Lane |
|
||||
| B | eigene Basis (Ancient) |
|
||||
| G | Bossgrotte |
|
||||
| H | eigener Held |
|
||||
| O | ganze Karte |
|
||||
| R | Kamera zurücksetzen (Drehung, Zoom, eigene Lane) |
|
||||
| Q / E | Kamera um 45° drehen |
|
||||
| Minimap klicken/ziehen | Kamera springt dorthin |
|
||||
| F1 | Optionen und Steuerung |
|
||||
| M / F11 | Ton / Vollbild |
|
||||
|
||||
## Prüfungen
|
||||
|
||||
```bash
|
||||
godot --headless --path . res://tests/luneraria/test_lune_core.tscn # Regeln M1 (91, inkl. Spiegeltest)
|
||||
godot --headless --path . res://tests/luneraria/test_lune_m2.tscn # Regeln M2 (73)
|
||||
godot --headless --path . res://tests/luneraria/test_lune_net.tscn # Netzwerkprotokoll (43)
|
||||
python3 tools/luneraria/net_rounds.py # Host + 2 Gäste, 5 Runden, Wiedereinstieg
|
||||
godot --path . --resolution 1280x720 res://tools/luneraria/verify_m1.tscn # echte Bedienung, volle Partie (38)
|
||||
godot --path . --resolution 1920x1080 res://tools/luneraria/verify_m2.tscn # Helden, Items, Grotte, Boss (15)
|
||||
godot --headless --path . res://tools/luneraria/sim_match.tscn -- --seeds=1,2,3 --limit=80
|
||||
godot --headless --path . res://tools/luneraria/balance_run.tscn -- --compositions=20 --limit=60 --out=res://docs/luneraria/logs/m3/balance.json
|
||||
godot --headless --path . res://tools/luneraria/balance_run.tscn -- --matchups --limit=60 --out=res://docs/luneraria/logs/m3/matchups.json
|
||||
python3 tools/luneraria/analyze_balance.py docs/luneraria/logs/m3/balance.json
|
||||
godot --headless --path . res://tools/luneraria/stress_run.tscn # Wellen bis 10^6, Langlauf
|
||||
godot --path . res://tools/luneraria/people_lineup.tscn -- --out=res://docs/luneraria/screenshots
|
||||
godot --path . --resolution 1920x1080 res://tools/luneraria/lune_benchmark.tscn -- --out=res://docs/luneraria/logs/g1/perf-nachher-1920x1080.json
|
||||
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/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:
|
||||
godot --headless --path . res://tests/test_runner.tscn
|
||||
godot --headless --path . -- --smoke-test
|
||||
godot --headless --path . -- --faction-test
|
||||
godot --headless --path . -- --expedition-test
|
||||
```
|
||||
|
||||
`verify_m1` und `verify_m2` brauchen ein Fenster (echte Maus-/Tastaturereignisse), nicht `--headless`.
|
||||
|
||||
## Assets neu erzeugen
|
||||
|
||||
```bash
|
||||
ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/build_tower.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_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
|
||||
godot --headless --editor --import --path .
|
||||
```
|
||||
|
||||
## Export
|
||||
|
||||
```bash
|
||||
python3 tools/luneraria/build_luneraria_demo.py --check-only
|
||||
python3 tools/luneraria/build_luneraria_demo.py
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
godot --headless --path . res://tools/luneraria/net_pair.tscn -- --role=host --rounds=4 &
|
||||
godot --headless --path . res://tools/luneraria/net_pair.tscn -- --role=client --name=Gast --rounds=4
|
||||
# mit Fenster und Gast-Aufnahmen: ohne --headless, Host mit --steps=0, Gast mit --shots=<Ordner>
|
||||
```
|
||||
|
||||
## Benutzereinstellungen
|
||||
|
||||
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.
|
||||
47
docs/luneraria/ARCHITEKTUR.md
Normal file
47
docs/luneraria/ARCHITEKTUR.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Architektur- und Migrationsentscheidungen
|
||||
|
||||
Stand: 27. September 2026 (M0).
|
||||
|
||||
## 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.
|
||||
- **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`.
|
||||
- **Baseline-Tests** (`logs/m0-baseline/`): 209 · 33 · 463 · 206 · 1.665 bestanden.
|
||||
|
||||
## Entscheidungen
|
||||
|
||||
1. **Neuer Modus neben dem Legacy-Modus.** Die Saumwacht-Regeln (zwei Bretter, 8/20 Wellen, Kern-HP-Entscheid, Arbeiter/Funken) widersprechen den neuen Produktregeln. Sie werden nicht umgeschrieben, sondern als **Legacy-Modus** archiviert: `scenes/main.tscn` und die zugehörigen Tests bleiben lauffähig. Die neue Hauptszene `scenes/luneraria.tscn` startet Champions of Luneraria. Die bisherigen QA-Schalter (`--smoke-test`, `--faction-test`, `--expedition-test`) leitet sie in die Legacy-Szene um. So vermischen sich die Wirtschaftsmodelle nicht (Arbeitsannahme E), und die technischen Regressionen bleiben erhalten.
|
||||
2. **Neue autoritative Simulation** in `scripts/luneraria/core/` als reine Datenklassen (`RefCounted`) ohne Szenenbaum. Der alte Kampf ist an zwei Bretter und `is_attacker` gebunden. Übernommen werden Einheitenwerte (`DataCatalog`), Schadensmatrix, Fähigkeitsregeln (auf Teamfeindschaft umgeschrieben), Modelle und die Animationsanbindung (`SkeletalFigure.update_combat`, `AnimatedFigure.pose`).
|
||||
- `LuneBalance` (Resource) enthält alle Zahlen.
|
||||
- `LuneMap` enthält Lane-Polylinien, 16 Türme und 2 Kerne, Aufmarschflächen, Sperrlogik und die Grotte. Die Karte entsteht per Konstruktion spiegelsymmetrisch zur Diagonale x = z: Team 1 ist das Spiegelbild von Team 0.
|
||||
- `LuneCombat` rechnet einen festen Schritt in zwei Phasen, die Ergebnisse sind reihenfolgeunabhängig.
|
||||
- `LuneMatch` verwaltet Teams, Spieler, Gold, Besitz mit eindeutigen IDs, Befehle, Runden, Abrechnung, Sieg und Ereignisprotokoll.
|
||||
- `LuneWaves` erzeugt datengetriebene, unbegrenzte Wellen mit Mengengrenze.
|
||||
- `LuneBot` nutzt dieselbe Befehlsschnittstelle wie der Mensch.
|
||||
3. **Autoritative Matchinstanz von Anfang an.** Oberfläche und Bots senden nur Befehle (`LuneMatch.submit`), die beim Phasenwechsel atomar geprüft werden. Darstellung und Tests lesen denselben Zustand. Zufall kommt nur aus einer `RandomNumberGenerator`-Instanz mit Seed (Wellenrotation, Bots). Das ist die Grundlage für M4 (Host/Join), ohne Bit-Identität über Plattformen zu behaupten.
|
||||
4. **Darstellung** in `scripts/luneraria/view/`: Kartenaufbau aus Bestandsmaterialien (Kluft-Shader, Kerne, Felsen, Bäume), neu erzeugte Türme (Blender-Generator), eine Einheitenansicht pro Kampfinstanz mit Positionsinterpolation zwischen den Simulationsschritten. Sichtbarkeit und Kamera beeinflussen die Simulation nicht.
|
||||
5. **Oberfläche** in `scripts/luneraria/ui/` mit dem gemeinsamen `SaumwachtTheme` und seinen Bauteilen. Titel und Menü werden auf Champions of Luneraria umgestellt. Die Eingabeabschirmung aus 20A bleibt.
|
||||
6. **Benutzereinstellungen:** Der Projektname wird zu „Champions of Luneraria“, dadurch ändert sich der `user://`-Pfad. `Preferences` übernimmt beim ersten Start einmalig die alte `preferences.cfg` aus „Saumwacht · Glutpakt“ und protokolliert das. Fehlt sie, gelten die Standardwerte.
|
||||
7. **Tests:** Neue Tests liegen unter `tests/luneraria/` und `tools/luneraria/`, die Legacy-Tests bleiben unverändert und werden als „Legacy“ geführt. Neue Referenzen werden erst nach fachlicher Prüfung festgeschrieben.
|
||||
8. **M3-Nachtrag:** Bots planen Items/Champions vor der Armee und helfen deterministisch (kein geteilter `rng`), damit Spiegelmatches bit-symmetrisch bleiben. Kartenprops stammen aus `map_props.glb` (MultiMesh, ENV_*-Kluftshader). Volksrepräsentanten `p_*` sind Asset- und Datenziele ohne Matcheinbindung; siehe [VOELKER.md](VOELKER.md). Balance- und Stresswerkzeuge liegen unter `tools/luneraria/`.
|
||||
9. **M4-Nachtrag:** `scripts/luneraria/net/lune_session.gd` (Node) kapselt ENet Host/Join. Der Host besitzt `LuneMatch`, rechnet jeden Kampf und sendet je Sitz den öffentlichen Zustand plus nur dessen eigenen Entwurf (`rpc_state`), zum Kampfbeginn die Einheitenliste (`rpc_combat_begin`), danach Momentaufnahmen (`rpc_frame`, 20/s, `PackedFloat32Array`). Der Client baut daraus ein reines Anzeige-Abbild von `LuneCombat`, auf dem nie eine Regel läuft. Clients senden nur Befehle (`rpc_hello` mit Protokollversion, `rpc_submit`, `rpc_ready`). Peer-ID → Sitz, ein Peer ein Sitz. Disconnect: Bot-Übernahme, kein Kernverlust, keine Hostmigration; Wiedereinstieg über den Anzeigenamen. `LuneGame` spricht offline direkt mit `LuneMatch`, im Netzspiel ausschließlich mit der Sitzung. Protokolltests `tests/luneraria/test_lune_net.gd`, Mehr-Runden-Loopback `tools/luneraria/net_pair.tscn` mit Treiber `tools/luneraria/net_rounds.py`. Export-Presets `Luneraria-Linux` / `Luneraria-Windows`, Demo-Bau `tools/luneraria/build_luneraria_demo.py`.
|
||||
10. **G1-Nachtrag (Karte):**
|
||||
- `LuneMap` baut jede Lane aus Catmull-Rom-Kontrollpunkten ihrer blauen Hälfte (Endpunkt auf der Achse, gespiegelter Phantompunkt, dadurch knickfrei). Bottom wird per Bisektion auf die Länge von Top gelöst. `LanePath` sucht Segmente binär.
|
||||
- Die Simulation bleibt zweidimensional. Die Darstellung liest Höhen aus `scripts/luneraria/view/lune_terrain.gd` (`LuneTerrain`): Höhenfeld aus Achsenkoordinate, Achsenabstand und gespiegelten Kartenelementen, daher exakt symmetrisch. Es wird für die Blau-Hälfte berechnet und gespiegelt, je Prozess einmal erzeugt; die `walk_height` liefert Deckhöhen statt Flussbett.
|
||||
- `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.
|
||||
- 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.
|
||||
80
docs/luneraria/ASSETMATRIX.md
Normal file
80
docs/luneraria/ASSETMATRIX.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Asset-Wiederverwendungsmatrix
|
||||
|
||||
Erzeugt mit `python3 tools/luneraria/build_asset_matrix.py` aus den Laufzeitmanifesten. „Volk/Bund“ ist die Datenzuordnung. Die sieben Zielvölker haben ab M3 **eigene Repräsentantenmodelle** (`p_*`); sie sind noch nicht als spielbare Fraktionen eingebunden. Bestehende Wesen bleiben als eigene Bünde Teil der Welt. Als „vorläufig“ markierte Rollen sind Zuordnungen vorhandener Modelle, keine neu erstellten Völker.
|
||||
|
||||
| ID | Name | Pfad | Quelle | Rig / Clips | Dreiecke | bisherige Rolle | neue Rolle | Volk / Bund | Materialzustand | Eingriff |
|
||||
| --- | --- | --- | --- | --- | ---: | --- | --- | --- | --- | --- |
|
||||
| core_team0 | Saumkern · Team 0 | res://assets/models/core_team0.glb | assets-source/phase4/blend/core_team0.blend | starre Gelenkgruppen | 36652 | Saumkern Team 0 | Ancient Blau (SW) | — | HD, starre Teile | keiner |
|
||||
| core_team1 | Saumkern · Team 1 | res://assets/models/core_team1.glb | assets-source/phase4/blend/core_team1.blend | starre Gelenkgruppen | 36848 | Saumkern Team 1 | Ancient Rot (NO) | — | HD, starre Teile | keiner |
|
||||
| e01 | Splitterling | res://assets/models/enemy_e01.glb | assets-source/phase4/blend/e01.blend | starre Gelenkgruppen | 19228 | Wellengegner | Monsterwelle (teamgebunden, rotierend) | Riftbrut (neutral, pro Team eingefärbt) | HD, starre Teile | Teamkennzeichnung |
|
||||
| e02 | Schalenläufer | res://assets/models/enemy_e02.glb | assets-source/phase4/blend/e02.blend | starre Gelenkgruppen | 20180 | Wellengegner | Monsterwelle | Riftbrut | HD, starre Teile | Teamkennzeichnung |
|
||||
| e03 | Nebelrest | res://assets/models/enemy_e03.glb | assets-source/phase4/blend/e03.blend | starre Gelenkgruppen | 35898 | Wellengegner | Monsterwelle | Riftbrut | HD, starre Teile | Teamkennzeichnung |
|
||||
| e04 | Fadenschütze | res://assets/models/enemy_e04.glb | assets-source/phase4/blend/e04.blend | starre Gelenkgruppen | 51050 | Wellengegner | Monsterwelle | Riftbrut | HD, starre Teile | Teamkennzeichnung |
|
||||
| e05 | Bruchstampfer | res://assets/models/enemy_e05.glb | assets-source/phase4/blend/e05.blend | starre Gelenkgruppen | 62798 | Wellengegner | Monsterwelle (späte Variante) | Riftbrut | HD, starre Teile | Teamkennzeichnung |
|
||||
| e06 | Knotenrufer | res://assets/models/enemy_e06.glb | assets-source/phase4/blend/e06.blend | starre Gelenkgruppen | 48336 | Wellengegner | Monsterwelle (Unterstützer) | Riftbrut | HD, starre Teile | Teamkennzeichnung |
|
||||
| e07 | Bruchhirsch | res://assets/models/enemy_e07.glb | assets-source/phase4/blend/e07.blend | starre Gelenkgruppen | 33800 | Boss Welle 5 | Reserve (Wellen-Elite ab höheren Indizes) | Riftbrut | HD, starre Teile | offen |
|
||||
| e08 | Riegelkoloss | res://assets/models/enemy_e08.glb | assets-source/phase4/blend/e08.blend | starre Gelenkgruppen | 81240 | Boss Welle 8 | Reserve (Wellen-Elite) | Riftbrut | HD, starre Teile | offen |
|
||||
| e09 | Maskenrichter | res://assets/models/enemy_e09.glb | assets-source/phase4/blend/e09.blend | starre Gelenkgruppen | 57310 | Boss Welle 15 | Reserve | Riftbrut | HD, starre Teile | offen |
|
||||
| e10 | Schlundkrone | res://assets/models/enemy_e10.glb | assets-source/phase4/blend/e10.blend | starre Gelenkgruppen | 64694 | Boss Welle 20 | neutraler Grottenboss und entsandter Boss (×1,7) | Riftbrut / Grotte | HD, starre Teile | umgesetzt (M2) |
|
||||
| g01 | Kohlenbeißer | res://assets/models/unit_g01.glb | assets-source/phase4/blend/g01.blend | starre Gelenkgruppen | 79780 | Glutpakt Nahkampf | Truppe Nahkampf | Glutkonstrukt / Glutpakt | HD, starre Teile | keiner |
|
||||
| g02 | Schlackenklaue | res://assets/models/unit_g02.glb | assets-source/phase4/blend/g02.blend | starre Gelenkgruppen | 75624 | Glutpakt Nahkampf+ | Truppenaufwertung | Glutkonstrukt / Glutpakt | HD, starre Teile | keiner |
|
||||
| g03 | Schlackenwacht | res://assets/models/unit_g03.glb | assets-source/phase4/blend/g03.blend | starre Gelenkgruppen | 67232 | Glutpakt Tank | Truppe Front | Glutkonstrukt / Glutpakt | HD, starre Teile | keiner |
|
||||
| g04 | Basaltbastion | res://assets/characters/basaltbastion.glb | assets-source/phase15/basaltbastion.blend | 17 Knochen · 9 Clips | 17824 | Glutpakt Tank+ | Truppenaufwertung | Glutkonstrukt / Glutpakt | gewichtetes Skelett (phase15) | keiner |
|
||||
| g05 | Funkenlanze | res://assets/characters/funkenlanze.glb | assets-source/phase18/funkenlanze.blend | 26 Knochen · 9 Clips | 24036 | Glutpakt Fernkampf | Truppe Fernkampf; zusätzlich Held „Funkenlanze“ (vorläufig; Goldring, Namensschild, ×1,25) | Glutkonstrukt / Glutpakt | gewichtetes Skelett (phase18) | umgesetzt (M2) |
|
||||
| g06 | Glasballiste | res://assets/characters/glasballiste.glb | assets-source/phase19/glasballiste.blend | 20 Knochen · 9 Clips | 15784 | Glutpakt Fernkampf+ | Truppenaufwertung (Belagerung) | Glutkonstrukt / Glutpakt | gewichtetes Skelett (phase19) | keiner |
|
||||
| g07 | Aschenwerfer | res://assets/models/unit_g07.glb | assets-source/phase4/blend/g07.blend | starre Gelenkgruppen | 81492 | Glutpakt Fläche | Truppe Fläche | Glutkonstrukt / Glutpakt | HD, starre Teile | keiner |
|
||||
| g08 | Kraterrufer | res://assets/models/unit_g08.glb | assets-source/phase4/blend/g08.blend | starre Gelenkgruppen | 77130 | Glutpakt Fläche+ | Truppenaufwertung | Glutkonstrukt / Glutpakt | HD, starre Teile | keiner |
|
||||
| g09 | Herdhüter | res://assets/models/unit_g09.glb | assets-source/phase4/blend/g09.blend | starre Gelenkgruppen | 84568 | Glutpakt Unterstützung | Truppe Unterstützung | Glutkonstrukt / Glutpakt | HD, starre Teile | keiner |
|
||||
| g10 | Sonnenesse | res://assets/models/unit_g10.glb | assets-source/phase4/blend/g10.blend | starre Gelenkgruppen | 96706 | Glutpakt Unterstützung+ | Truppenaufwertung | Glutkonstrukt / Glutpakt | HD, starre Teile | keiner |
|
||||
| m01 | Kesselwicht | res://assets/models/unit_m01.glb | assets-source/phase4/blend/m01.blend | starre Gelenkgruppen | 1284 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nietenkonvent (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| m02 | Druckhüter | res://assets/models/unit_m02.glb | assets-source/phase4/blend/m02.blend | starre Gelenkgruppen | 1360 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nietenkonvent (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| m03 | Scherschreiter | res://assets/models/unit_m03.glb | assets-source/phase4/blend/m03.blend | starre Gelenkgruppen | 932 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nietenkonvent (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| m04 | Sechsklingler | res://assets/models/unit_m04.glb | assets-source/phase4/blend/m04.blend | starre Gelenkgruppen | 1398 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nietenkonvent (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| m05 | Bolzenknecht | res://assets/models/unit_m05.glb | assets-source/phase4/blend/m05.blend | starre Gelenkgruppen | 1148 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nietenkonvent (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| m06 | Linienmeister | res://assets/models/unit_m06.glb | assets-source/phase4/blend/m06.blend | starre Gelenkgruppen | 1432 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nietenkonvent (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| m07 | Klangkolben | res://assets/models/unit_m07.glb | assets-source/phase4/blend/m07.blend | starre Gelenkgruppen | 740 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nietenkonvent (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| m08 | Resonanzramme | res://assets/models/unit_m08.glb | assets-source/phase4/blend/m08.blend | starre Gelenkgruppen | 1316 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nietenkonvent (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| m09 | Taktspindel | res://assets/models/unit_m09.glb | assets-source/phase4/blend/m09.blend | starre Gelenkgruppen | 1160 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nietenkonvent (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| m10 | Taktprälat | res://assets/models/unit_m10.glb | assets-source/phase4/blend/m10.blend | starre Gelenkgruppen | 946 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nietenkonvent (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| map_lane_segment | Lane-Segment | res://assets/models/map_lane_segment.glb | assets-source/phase4/blend/map_lane_segment.blend | starre Gelenkgruppen | 10700 | Lane-Kachel alt | nicht verwendet; Lanes neu erzeugt | — | HD, starre Teile | keiner |
|
||||
| n01 | Saummaske | res://assets/models/unit_n01.glb | assets-source/phase4/blend/n01.blend | starre Gelenkgruppen | 1132 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nebelchor (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| n02 | Grenzmaske | res://assets/models/unit_n02.glb | assets-source/phase4/blend/n02.blend | starre Gelenkgruppen | 1156 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nebelchor (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| n03 | Fadentänzer | res://assets/models/unit_n03.glb | assets-source/phase4/blend/n03.blend | starre Gelenkgruppen | 934 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nebelchor (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| n04 | Saumzertrenner | res://assets/models/unit_n04.glb | assets-source/phase4/blend/n04.blend | starre Gelenkgruppen | 1360 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nebelchor (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| n05 | Fernflüsterer | res://assets/models/unit_n05.glb | assets-source/phase4/blend/n05.blend | starre Gelenkgruppen | 1118 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nebelchor (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| n06 | Fernrufer | res://assets/models/unit_n06.glb | assets-source/phase4/blend/n06.blend | starre Gelenkgruppen | 1498 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nebelchor (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| n07 | Glockenschlepper | res://assets/models/unit_n07.glb | assets-source/phase4/blend/n07.blend | starre Gelenkgruppen | 1274 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nebelchor (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| n08 | Hohlgeläut | res://assets/models/unit_n08.glb | assets-source/phase4/blend/n08.blend | starre Gelenkgruppen | 1482 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nebelchor (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| n09 | Maskenkantor | res://assets/models/unit_n09.glb | assets-source/phase4/blend/n09.blend | starre Gelenkgruppen | 1314 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nebelchor (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| n10 | Vielstimmenkantor | res://assets/models/unit_n10.glb | assets-source/phase4/blend/n10.blend | starre Gelenkgruppen | 1734 | nicht spielbar, niedrige Detailstufe | Reserve für spätere Völker | Nebelchor (nicht spielbar) | niedrige Detailstufe | Neuaufbau nötig vor Nutzung |
|
||||
| s01 | Nadelspäher | res://assets/models/unit_s01.glb | assets-source/phase4/blend/s01.blend | starre Gelenkgruppen | 10340 | Söldner | Reserve Angriffschampion | Söldner | HD, starre Teile | offen |
|
||||
| s02 | Rostschieber | res://assets/models/unit_s02.glb | assets-source/phase4/blend/s02.blend | starre Gelenkgruppen | 12040 | Söldner | Angriffschampion Schutz (Rostschieber) | Söldner | HD, starre Teile | umgesetzt (M2) |
|
||||
| s03 | Dunstsänger | res://assets/models/unit_s03.glb | assets-source/phase4/blend/s03.blend | starre Gelenkgruppen | 7832 | Söldner | Angriffschampion Druck auf Unterstützer (Dunstsänger) | Söldner | HD, starre Teile | umgesetzt (M2) |
|
||||
| s04 | Keilbrecher | res://assets/models/unit_s04.glb | assets-source/phase4/blend/s04.blend | starre Gelenkgruppen | 10458 | Söldner | Angriffschampion Belagerung (Keilbrecher) | Söldner | HD, starre Teile | umgesetzt (M2) |
|
||||
| s05 | Risszehrer | res://assets/models/unit_s05.glb | assets-source/phase4/blend/s05.blend | starre Gelenkgruppen | 10440 | Söldner | Reserve Angriffschampion | Söldner | HD, starre Teile | offen |
|
||||
| w01 | Dornhopser | res://assets/models/unit_w01.glb | assets-source/phase4/blend/w01.blend | starre Gelenkgruppen | 61376 | Wurzelbund Nahkampf | Truppe Nahkampf | Wurzelwesen / Wurzelbund | HD, starre Teile | keiner |
|
||||
| w02 | Rankenschnapper | res://assets/models/unit_w02.glb | assets-source/phase4/blend/w02.blend | starre Gelenkgruppen | 78020 | Wurzelbund Nahkampf+ | Truppenaufwertung | Wurzelwesen / Wurzelbund | HD, starre Teile | keiner |
|
||||
| w03 | Korkrücken | res://assets/models/unit_w03.glb | assets-source/phase4/blend/w03.blend | starre Gelenkgruppen | 84786 | Wurzelbund Tank | Truppe Front | Wurzelwesen / Wurzelbund | HD, starre Teile | keiner |
|
||||
| w04 | Ringborke | res://assets/characters/ringborke.glb | assets-source/phase11/ringborke.blend | 27 Knochen · 9 Clips | 118064 | Wurzelbund Tank+ | Truppenaufwertung; zusätzlich Held „Ringborke“ (vorläufig) | Wurzelwesen / Wurzelbund | gewichtetes Skelett (phase11) | umgesetzt (M2) |
|
||||
| w05 | Samenschleuder | res://assets/models/unit_w05.glb | assets-source/phase4/blend/w05.blend | starre Gelenkgruppen | 50536 | Wurzelbund Fernkampf | Truppe Fernkampf | Wurzelwesen / Wurzelbund | HD, starre Teile | keiner |
|
||||
| w06 | Zapfenkrone | res://assets/models/unit_w06.glb | assets-source/phase4/blend/w06.blend | starre Gelenkgruppen | 62588 | Wurzelbund Fernkampf+ | Truppenaufwertung | Wurzelwesen / Wurzelbund | HD, starre Teile | keiner |
|
||||
| w07 | Sporenkelch | res://assets/models/unit_w07.glb | assets-source/phase4/blend/w07.blend | starre Gelenkgruppen | 69164 | Wurzelbund Fläche | Truppe Fläche | Wurzelwesen / Wurzelbund | HD, starre Teile | keiner |
|
||||
| w08 | Pilzkathedrale | res://assets/models/unit_w08.glb | assets-source/phase4/blend/w08.blend | starre Gelenkgruppen | 103748 | Wurzelbund Fläche+ | Truppenaufwertung | Wurzelwesen / Wurzelbund | HD, starre Teile | keiner |
|
||||
| w09 | Tauhüter | res://assets/models/unit_w09.glb | assets-source/phase4/blend/w09.blend | starre Gelenkgruppen | 52040 | Wurzelbund Heiler | Truppe Unterstützung | Wurzelwesen / Wurzelbund | HD, starre Teile | keiner |
|
||||
| w10 | Quellgeflecht | res://assets/characters/quellgeflecht.glb | assets-source/phase16/quellgeflecht.blend | 17 Knochen · 9 Clips | 14276 | Wurzelbund Heiler+ | Truppenaufwertung; zusätzlich Held „Quellgeflecht“ (vorläufig) | Wurzelwesen / Wurzelbund | gewichtetes Skelett (phase16) | umgesetzt (M2) |
|
||||
| worker | Funkenleser | res://assets/characters/arbeiter.glb | assets-source/phase12/arbeiter.blend | 27 Knochen · 10 Clips | 7184 | Arbeiter/Funken | nicht im neuen Modus (Arbeitsannahme E); Modell bleibt erhalten | — | gewichtetes Skelett (phase12) | keiner |
|
||||
| p_biest | Klauenjäger | res://assets/luneraria/peoples/p_biest.glb | assets-source/luneraria/peoples/p_biest.blend | 24 Knochen · 9 Clips | 16858 | — | Volksrepräsentant Nahkampf (Klauenjäger) | Biestmenschen | gewichtetes Skelett (peoples, LP_*-Materialien) | neu (M3) |
|
||||
| p_elf | Waldläuferin | res://assets/luneraria/peoples/p_elf.glb | assets-source/luneraria/peoples/p_elf.blend | 24 Knochen · 9 Clips | 18502 | — | Volksrepräsentant Fernkampf (Waldläuferin) | Elfen | gewichtetes Skelett (peoples, LP_*-Materialien) | neu (M3) |
|
||||
| p_fee | Lichtheilerin | res://assets/luneraria/peoples/p_fee.glb | assets-source/luneraria/peoples/p_fee.blend | 24 Knochen · 9 Clips | 16744 | — | Volksrepräsentant Unterstützung (Lichtheilerin) | Feen | gewichtetes Skelett (peoples, LP_*-Materialien) | neu (M3) |
|
||||
| p_mensch | Ritter | res://assets/luneraria/peoples/p_mensch.glb | assets-source/luneraria/peoples/p_mensch.blend | 22 Knochen · 9 Clips | 17480 | — | Volksrepräsentant Front (Ritter) | Menschen | gewichtetes Skelett (peoples, LP_*-Materialien) | neu (M3) |
|
||||
| p_ork | Kriegshäuptling | res://assets/luneraria/peoples/p_ork.glb | assets-source/luneraria/peoples/p_ork.blend | 22 Knochen · 9 Clips | 15890 | — | Volksrepräsentant Nahkampf (Kriegshäuptling) | Orks | gewichtetes Skelett (peoples, LP_*-Materialien) | neu (M3) |
|
||||
| p_troll | Steinhauer | res://assets/luneraria/peoples/p_troll.glb | assets-source/luneraria/peoples/p_troll.blend | 22 Knochen · 9 Clips | 14686 | — | Volksrepräsentant Fläche (Steinhauer) | Trolle | gewichtetes Skelett (peoples, LP_*-Materialien) | neu (M3) |
|
||||
| p_zwerg | Schildträger | res://assets/luneraria/peoples/p_zwerg.glb | assets-source/luneraria/peoples/p_zwerg.blend | 22 Knochen · 9 Clips | 20214 | — | Volksrepräsentant Front (Schildträger) | Zwerge | gewichtetes Skelett (peoples, LP_*-Materialien) | neu (M3) |
|
||||
| menu-stage | Menü-Diorama | res://assets/environment/menu-stage.glb | assets-source/phase20/build_menu_stage.py | statisch | 48660 | Hauptmenü Phase 20A | Hauptmenü (Titel angepasst) | — | Kluft-Shader | keiner |
|
||||
| verdant-rift | Grüne Kluft (2-Bretter-Arena) | res://assets/environment/verdant-rift.glb | assets-source/phase10/verdant-rift.blend | statisch | — | Spielfeld Saumwacht | Legacy-Modus; Materialien/Shader für die neue Karte wiederverwendet | — | triplanare Kluft-Shader | keine neue Nutzung als Karte |
|
||||
| lane_tower | Laneturm | res://assets/luneraria/tower.glb | assets-source/luneraria/build_tower.py | statisch | 648 | — | 16 Lanetürme und Basistürme, Kristall zur Laufzeit in Teamfarbe | — | ENV_*-Kluftshader, Kristall emissiv | neu (M1) |
|
||||
| boss_grotto | Bossgrotte | res://assets/luneraria/grotto.glb | assets-source/luneraria/build_grotto.py | statisch | 1888 | — | Felsinsel mit Höhle auf der Symmetrieachse, zwei gespiegelte Zugänge | — | ENV_*-Kluftshader, violette Kristalle | neu (M2) |
|
||||
| map_props | Kartenprops (Bäume, Felsen, Büsche, Brücken, Bordsteine, Klippen, Banner) | res://assets/luneraria/map_props.glb | assets-source/luneraria/build_map_props.py | statisch | MultiMesh | — | 3D-Kartendekoration der drei Lanes, Brücken über die Kluft, Klampterrahmen | — | ENV_*-Kluftshader | neu (M3) |
|
||||
| luneraria-emblem | Emblem Luneraria | res://assets/ui/emblems/luneraria.svg | SVG | — | — | — | Titel und Menü | — | Vektor | neu (M1) |
|
||||
| Emblem/Theme | Saumwacht-Theme | scripts/ui/theme/, assets/ui/ | Code / SVG | — | — | Menü 20A | Theme weiterverwendet, Titel/Emblem Luneraria | — | Vektor | Titelschrift/Emblem ergänzen |
|
||||
| compare-rock / compare-tree | Fels, Baum (Alt) | res://assets/compare/ | assets-source/phase14/build_compare_props.py | statisch | 360 / 852 | Vergleichsszene | ab M3 durch map_props ersetzt | — | einfach | abgelöst (M3) |
|
||||
119
docs/luneraria/BALANCE.md
Normal file
119
docs/luneraria/BALANCE.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# Balancekonfiguration und Auswertung
|
||||
|
||||
Alle Werte stehen in `scripts/luneraria/core/lune_balance.gd`. Die aktive Standardinstanz ist `data/luneraria/balance_default.tres`, die derzeit alle Klassenwerte übernimmt.
|
||||
|
||||
## Startwerte (M1) und Begründung
|
||||
|
||||
| Wert | Stand | Begründung |
|
||||
| --- | ---: | --- |
|
||||
| Startgold | 220 | Wie im Saumwacht-Einstieg; reicht für die Startaufstellung (180/185) plus Rest. |
|
||||
| Grundeinkommen | 70 + 4 je Runde, Obergrenze +80 | Etwa eine Truppe pro Runde; der Zuwachs hält mit den Wellen mit, ohne endlos zu steigen. |
|
||||
| Versorgungsgrenze | 12 Truppen je Spieler | Begrenzt aktive Einheiten; später investiert man in Aufwertungen statt in Masse. |
|
||||
| Verkauf | 75 % (in derselben Planung 100 %) | Aus Saumwacht übernommen; die Rücknahme im selben Planungsschritt ist ein Bedienkomfort. |
|
||||
| Wellen | 3 Monster je Lane und Team, +1 alle 3 Runden, max. 7 | Pro Runde höchstens 42 Monster auf der Karte. |
|
||||
| Wellenstärke | HP ×(1 + 0,11·r + 0,0015·r²), Schaden ×(1 + 0,07·r + 0,0015·r²) | Stetig und quadratisch leicht zunehmend, ohne Obergrenze; bei r = 10⁶ endlich (Test). |
|
||||
| Monsterkopfgeld | Katalogwert ×2 ×(1 + 0,04·r) | Monster sind die Hauptgoldquelle; wächst langsamer als die Stärke. |
|
||||
| Türme | außen 6.000 / innen 7.500 / Basis 9.000 HP, 90 / 120 Schaden, Reichweite 8,6 m | Eine Welle allein kommt nicht durch. Die Reichweite liegt über der größten Einheitenreichweite (6,5 m + Gebäuderadius), sonst wären Türme ungestraft auszuschießen. |
|
||||
| Ancient | 12.000 HP | Braucht eine voll durchbrochene Basis und mehrere Runden Druck. |
|
||||
| Gebäudekopfgeld | 150 / 210 / 270 | Belohnt Durchbrüche gleichmäßig im Team. |
|
||||
| Kampffenster | 180 s | In 91 Bot-Kampfphasen (8 Seeds) dauerte die längste 130 s, im Mittel 68 s; keine erreichte das Fenster. Es greift nur bei festgefahrenen Kämpfen. |
|
||||
| Marschtempo | Katalogtempo ×1,6 | Eine Lane (~95 m) ist in ~30 s durchquerbar, damit Angriffe die Basis innerhalb eines Kampfes erreichen. |
|
||||
|
||||
## Auswertung M1 (reine Bot-Partien, 1 Mensch = Bot)
|
||||
|
||||
[sim-8-seeds.json](logs/m1/sim-8-seeds.json): acht Seeds mit gemischten Bünden und fünf Bot-Profilen (ausgewogen, verteidigung, druck, fernkampf, teamhilfe). Die Ergebnisse stehen in [STATUS.md](STATUS.md).
|
||||
|
||||
- Alle acht Partien enden regulär durch einen Ancient-Fall, nach 8–15 Wellen (Blau 5, Rot 3). 83 Kampfphasen enden, weil alle beweglichen Einheiten gefallen sind, 8 durch einen Kernfall, keine durch das Zeitfenster.
|
||||
- **Exakter Spiegel** (sechsmal derselbe Bund, dasselbe deterministische Profil): Die Partie bleibt über 60 Runden Bit für Bit symmetrisch. Sie wird aber nicht entschieden, weil sich gleich starke Armeen und Wellen gegenseitig aufheben. Das ist der erwartbare Grenzfall identischen Spiels. Kein Timer oder Gebäudeverfall erzwingt ein Ende. Heldenwachstum, Angriffschampions und Boss (M2) liefern die beabsichtigten Asymmetrien.
|
||||
- Kleine Botstichproben belegen keine ausgewogene Spielerfahrung. Die Bünde und Profile sind noch nicht getrennt ausgewertet (M3).
|
||||
|
||||
## M2 · Helden, Items, Champions, Boss
|
||||
|
||||
Werte in `scripts/luneraria/core/lune_content.gd`, Begründungen im [Regelwerk](REGELWERK.md) §4.
|
||||
|
||||
| Wert | Stand | Begründung |
|
||||
| --- | ---: | --- |
|
||||
| Heldenpreis | 260 | Mehr als das Startgold: Ein Held kommt frühestens in Runde 2 und ersetzt keine frühe Armee. |
|
||||
| Heldenwerte | Ringborke 2.200 HP / 34, Quellgeflecht 1.300 / 28, Funkenlanze 950 / 46 | Aus den Truppenwerten der Aufwertungsstufe mit etwa 1,4-fachem Leben abgeleitet. |
|
||||
| Stufen | 10; +9 % Leben, +7 % Schaden je Stufe | In 12 Bot-Partien erreichen Helden bis Partieende Stufe 3–6, Talentstufe 2 also erst spät. |
|
||||
| Items | 180–260 | Ein Item kostet etwa eine aufgewertete Truppe. |
|
||||
| Champions | 150/150/180, Leben ×2,2, Schaden ×1,6 der Söldnerwerte, Kopfgeld 70 + 6·r | Einmalige Welle etwa zum Preis von zwei Truppen. |
|
||||
| Grottenboss | 4.200 HP × Wellenstärke, 55 Schaden | Nur mit großer Expedition schaffbar (Szenarien unten). |
|
||||
| Entsandter Boss | 5.200 HP × Wellenstärke, 70 Schaden, Kopfgeld 520 + 60·r | Das Kopfgeld liegt deutlich über einem Turm (150–270) und über einem Monster (~10–30). |
|
||||
|
||||
### Bot-Serie (12 Seeds, [sim-12-seeds.json](logs/m2/sim-12-seeds.json))
|
||||
|
||||
- Alle 12 Partien enden regulär nach 7–10 Wellen (Blau 7, Rot 5), keine Kampfphase im Zeitfenster; die längste dauerte 128 s.
|
||||
- Bossversuche: 36 gescheitert, 11 erobert, 7 entsandt (die übrigen Partien endeten vorher).
|
||||
- **Befund:** Der Bossjäger-Bot versucht es in jeder Partie ab Runde 5, scheitert bis Runde 7 und siegt in Runde 8. Das Muster ist zu gleichförmig; der Bot schätzt seine Stärke nicht ein (M3).
|
||||
- Siegquote des Teams mit dem jeweiligen Profil: fernkampf 11/12, druck 9/12, teamhilfe 7/12, verteidigung 5/12, bossjagd 3/12, ausgewogen 1/12. **Vorsicht:** Jede Partie enthält jedes Profil genau einmal, und die Paare fernkampf/ausgewogen, druck/bossjagd und verteidigung/teamhilfe stehen immer in verschiedenen Teams. Die Zahlen sind daher nicht unabhängig; eine saubere Auswertung mit variierter Teamzusammensetzung folgt in M3.
|
||||
|
||||
### Bossstrategie im direkten Vergleich ([boss_scenarios.gd](../../tools/luneraria/boss_scenarios.gd))
|
||||
|
||||
Identischer Stand nach Runde 7, danach vier Runden mit gewöhnlichen Bots ohne Bossjagd. Wert = eingesetzter Armeewert (Blau / Rot). ΔHP und ΔGold jeweils Blau / Rot über die vier Runden.
|
||||
|
||||
| Seed | Szenario | Wert | Grotte | Mid-Türme Blau verloren | Bossrunde | Gebäudeschaden Rot durch Boss | Kopfgeld für Rot | ΔHP Gebäude | ΔGold | Sieger |
|
||||
| --- | --- | ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |
|
||||
| 3 | kein_versuch | 0 | none | 0 | — | 0 | — | -1.436 / -50.521 | 284 / -235 | Blau |
|
||||
| 3 | gescheitert | 210 | failed | 0 | — | 0 | — | -1.754 / -50.257 | 284 / -235 | Blau |
|
||||
| 3 | erfolgreich | 1000 | won | 2 | 9 | 33.219 | 1060 | -15.023 / -35.384 | 543 / 794 | Blau |
|
||||
| 3 | umkaempft | 1000 / 995 | failed | 0 | — | 0 | — | -1.435 / -51.276 | 494 / -185 | Blau |
|
||||
| 3 | ueberinvestition | 2495 | won | 2 | 9 | 25.392 | 1060 | -68.295 / -25.392 | -250 / 437 | Rot |
|
||||
| 7 | kein_versuch | 0 | none | 0 | — | 0 | — | -9.222 / -5.573 | 174 / 139 | offen |
|
||||
| 7 | gescheitert | 180 | failed | 0 | — | 0 | — | -9.548 / -4.660 | 174 / 139 | offen |
|
||||
| 7 | erfolgreich | 930 | won | 2 | 9 | 22.793 | 1060 | -67.737 / -22.793 | -302 / 319 | Rot |
|
||||
| 7 | umkaempft | 930 / 865 | failed | 0 | — | 0 | — | -9.855 / -4.660 | 174 / 139 | offen |
|
||||
| 7 | ueberinvestition | 2580 | won | 2 | 9 | 20.685 | 1060 | -67.737 / -20.685 | -320 / 277 | Rot |
|
||||
|
||||
**Deutung:**
|
||||
- Eine gescheiterte kleine Expedition kostet wenig.
|
||||
- In der umkämpften Grotte reiben sich beide Expeditionen auf; kein Team erhält den Anspruch.
|
||||
- Eine erfolgreiche Eroberung zieht die Mid-Lane so weit ab, dass Blau dort beide Türme verliert. Der Boss richtet danach 20.000–33.000 HP Gebäudeschaden an, stirbt und bringt Rot 1.060 Gold.
|
||||
- Überinvestition kippt die Partie in beiden Seeds zu Rot, und auch die normale Eroberung verliert bei Seed 7.
|
||||
- Die Bossstrategie ist damit riskant und nicht dominant. Nach diesen Stichproben wiegt der Boss die geschwächte Lane eher zu wenig auf: ein Balancepunkt für M3 (z. B. mehr Bosswirkung oder schwächerer Grottenboss). Es gibt weder eine kostenlose Sofortrückkehr noch eine Doppelbelohnung (Tests); das Goldrisiko steht im Kopfgeld offen angekündigt.
|
||||
|
||||
## M3 · Qualität und Balance
|
||||
|
||||
### Geänderte Werte und Bot-Regeln
|
||||
|
||||
| Thema | Vorher | Nachher | Begründung |
|
||||
| --- | --- | --- | --- |
|
||||
| Keilbrecher Belagerungsfaktor | ×2,5 | ×1,8 | Belagerungs-Champion dominierte Gebäude zu stark. |
|
||||
| Grottenboss HP | 4.200 | 3.600 | Expeditionen scheiterten zu oft vor Runde 8. |
|
||||
| Entsandter Boss | 5.200 HP / 70 Schaden | 6.800 HP / 88 Schaden | Die Belohnung wiegt die geschwächte Lane nun besser auf. |
|
||||
| Bot Item-/Championkauf | nach der Armee | **vor** der Armee (Reserve 110 bzw. 40 Gold) | Die Roster-Schleife verzehrte die gesamte Reserve; Helden blieben ohne Items, Champions wurden nie gebucht (`test_lune_m2` bots_m2). |
|
||||
| Bot Lane-Hilfe | geteilter Zufall (`rng`) | deterministisch `(Runde + Laneindex) % Periode` | Der geteilte Zufall brach die Spiegelsymmetrie (`test_lune_core` mirror_fairness). |
|
||||
| Bossjagd-Schwelle | Lanchester 2,0 | 1,35 (+1,25 je Fehlversuch) | Frühere Versuche erst ab Runde 8; Ziel: sinnvolle Risikoabschätzung. |
|
||||
| Expeditionstärke | 6 Truppen + Held | 5 Truppen + Held | Weniger Lane-Abzug bei der Jagd. |
|
||||
| Ausgewogen-Held | ab Runde 3 | ab Runde 4 | Reines „ausgewogen“ schlug in Spiegel-Duellen alle Spezialprofile. |
|
||||
| Verteidigung | kein Champion, 40 % Front | Champion Schutz, 15 % Lane-Hilfe | Reines Verteidigen gewann zu selten (14 %). |
|
||||
|
||||
### Bot-Serie (20 Zusammensetzungen × Seitentausch, [balance-step4.json](logs/m3/balance-step4.json))
|
||||
|
||||
- 40 Partien, alle regulär durch Ancient-Fall nach 9–14 Wellen (Median 11). Kein Testlimit, kein Zeitfenster-Kampf.
|
||||
- **Seitentausch 20/20:** dieselbe Zusammensetzung gewinnt beide Male. Keine Seitenbevorzugung.
|
||||
- Profile (Siege/Einsätze): verteidigung 64 %, ausgewogen 58 %, fernkampf 54 %, druck 53 %, teamhilfe 46 %, bossjagd 24 %.
|
||||
- Bünde: Glutpakt 55 % / Wurzelbund 45 %.
|
||||
- Boss: 20 Erfolge, 8 Entsendungen. Heldenstufe am Ende (Median) 4.
|
||||
|
||||
### Reine Profil-Duelle ([matchups-step4.json](logs/m3/matchups-step4.json))
|
||||
|
||||
15 Paare × 2 Seiten = 30 Partien. Seitentausch 15/15 konsistent.
|
||||
|
||||
| Profil | Siege | Duellkette |
|
||||
| --- | ---: | --- |
|
||||
| ausgewogen | 10/10 | schlägt alle Spezialprofile |
|
||||
| verteidigung | 8/10 | verliert nur gegen ausgewogen |
|
||||
| teamhilfe | 6/10 | schlägt druck, fernkampf, bossjagd |
|
||||
| fernkampf | 4/10 | schlägt druck, bossjagd |
|
||||
| druck | 2/10 | schlägt bossjagd |
|
||||
| bossjagd | 0/10 | verliert gegen jedes Spezialprofil |
|
||||
|
||||
**Befund:** In reinen 3-gleichen Spiegeln ergibt sich eine lineare Kette, kein Zyklus. In gemischten Sechser-Aufstellungen (die eigentliche Partie) liegen alle Profile im Band 24–64 %. Kleine Botstichproben beweisen keine ausgewogene Spielerfahrung; menschliche Spieltests bleiben offen. Bossjagd ist als reines Teamprofil schwach, in Mischteams aber spielbar (24 %).
|
||||
|
||||
### Belastungstests ([stress.json](logs/m3/stress.json))
|
||||
|
||||
- 17/17 Prüfungen, keine Fehler.
|
||||
- Wellenindizes 10², 10³, 10⁴, 10⁶ enden regulär über Kernfall; Monster-HP endlich (bei 10⁶ ≈ 7,2·10¹¹), kein NaN, kein Arrayfehler, mobile Einheiten gedeckelt (120).
|
||||
- Langlauf 200 Runden: „Testlimit erreicht / Partie nicht abgeschlossen“ (korrekt, kein Unentschieden). Objektzahl stabil (1706 → 1853), Speicher 52 → 53 MB. Rundenzeit 1,05 s → 2,01 s (wachsende Wellenmenge, kein Leck).
|
||||
|
||||
182
docs/luneraria/G1-KARTE.md
Normal file
182
docs/luneraria/G1-KARTE.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
# G1 · Map-Blockout und spielerisches Drei-Lane-Layout
|
||||
|
||||
Stand: 29. September 2026 · Branch `luneraria/g1` (von `luneraria/m4`, Commit `c155338`).
|
||||
Grundlage: Grafikauftrag vom 28. September 2026, Konzeptbild `assets/ChatGPT-Bild 27. Sept. 2026, 19_46_52-2.png` als visuelle Richtung (keine Vorlage zum Kopieren).
|
||||
|
||||

|
||||
|
||||
*Kartenplan: orthografische Draufsicht ohne HUD, nur zur Dokumentation. Alle Abnahmebilder unten stammen aus der echten Spielkamera.*
|
||||
|
||||
## Vorab: M4 gesichert und eingefroren
|
||||
|
||||
- M4-Stand dokumentiert (STATUS, QA-MATRIX, ARCHITEKTUR, ANLEITUNG) und als `c155338` auf `luneraria/m4` committet.
|
||||
- Vor dem Commit erneut ausgeführt: `test_lune_net` 43/43, `net_rounds.py` 12/12.
|
||||
- Offen und eingefroren: OpenGL-Kompatibilitätsmodus, nativer Windows-11-Test, Neubau der Demo-Pakete, Partie über mehrere Rechner.
|
||||
- Während G1 keine Netzwerk-Featurearbeit. Einzige Änderung am Netzwerk-Testtreiber: Gast A trennt jetzt bei der ersten Momentaufnahme des Kampfes der Trennrunde statt erst nach sechs (kurze Kämpfe lieferten zu wenige; siehe Tests).
|
||||
|
||||
## Neues Kartenlayout
|
||||
|
||||
Die Karte bleibt ein Quadrat von 72 × 72 m mit Blau im Südwesten und Rot im Nordosten. Rot ist weiterhin das exakte Spiegelbild von Blau an der Achse x = z. Jede Lane wird aus ihrer blauen Hälfte und deren Spiegelung gebaut; Türme, Aufmarschpunkte und Grotteneingänge sind dadurch funktional gleich. Neu ist, dass die Lanes als geglättete Kurven (Catmull-Rom, alle 2 m abgetastet) entstehen und sich Top und Bottom in der Form unterscheiden.
|
||||
|
||||
| Lane | Länge | Identität | Querung der Kluft |
|
||||
| --- | --- | --- | --- |
|
||||
| Top | 96,82 m (M3: 94,5) | windet sich über das nordwestliche Hochland (bis 3,3 m), alte Wachtwege, Ruinentor, Mauerreste, Aussichtsturm an der Ecke | keine |
|
||||
| Mid | 61,16 m (M3: 59,4) | direkteste, breitere Route (Belag 7,8 m statt 6), leicht gewellt, runde Mittelplattform mit Runenkreis und vier Runensteinen, Blick die Kluft hinauf zur Grotte | über die Mittelplattform (8 m Wasser) |
|
||||
| Bottom | 96,82 m (exakt wie Top) | Tiefland im Südosten (bis −0,16 m), Talbogen, verwachsene Mauerstümpfe, Schilf am Ufer | über die Deltabrücke (16 m Wasser) |
|
||||
|
||||
Die Länge von Bottom wird beim Laden per Bisektion auf die Länge von Top gelöst, damit beide Lane-Rollen gleich lange Wege haben.
|
||||
|
||||
**Kluft (Fluss):** Sie entspringt am Fuß der Grottenterrasse, läuft genau auf der Symmetrieachse unter der Mittelplattform hindurch und mündet im Südosten in ein Delta mit See, das Bottom auf der Brücke quert und das durch das Randgebirge abfließt. Top berührt kein Wasser.
|
||||
|
||||
**Basen:** Plateau (Radius 10,6 m, 0,6 m hoch), Platz mit Teamring, Ringmauer mit drei Toren an den Lane-Ausgängen und Torsäulen mit Teamkristall. Die zwei Basistürme stehen 6 m vor dem Ancient, 40° beiderseits der Mid-Richtung.
|
||||
|
||||
**Türme:** je Team 2 Türme pro Lane plus 2 Basistürme, also 16 Türme und 2 Ancients auf der Karte, alle auf ebenen Sockeln in Höhe der Lane daneben. Die Position folgt wie bisher dem Anteil an der Lanelänge (innen 17 %, außen 35 %) mit 3,4 m Seitenabstand.
|
||||
|
||||
**Bossgrotte:** auf der Symmetrieachse zwischen Top und Mid, 19,1 m neben Mid. Terrasse (Radius 7,8 m, 1,7 m hoch) vor der Hochlandkante mit Runenkreis, elf Säulen mit Grottenkristallen um die Rückseite, dem M2-Felsbogen als Höhlenmund (ohne dessen alte Bodenscheibe und Findlinge) und Klippen dahinter. Je Team führt eine Treppe zum Eingang, dazwischen fällt die Quelle als kleiner Wasserfall in die Kluft. Die Abstände von Eingängen und Bosspunkt zum Grottenmittelpunkt sind seit M2 unverändert; die Grottenkämpfe sind also nur verschoben, nicht verändert. Bei gesperrter Grotte werden Licht und Runen gedimmt, bei Anspruch leuchten sie in Teamfarbe (Zustände im Detail: G2).
|
||||
|
||||
**Aufmarschbereiche:** je Team und Lane eine Fläche vor dem eigenen Basistor (Lanefortschritt 0–7,6 m, 4,8 m breit), mit Reihenlinien im Abstand von 1,5 m (vorne Front, dahinter Nahkampf, Flächenschaden, Fernkampf, Unterstützung, wie in `LuneMatch._spawn_armies`) und einem Pfeil in Marschrichtung. In der Planung sieht man die drei eigenen Flächen; Lanes mit eigenen Einheiten sind hervorgehoben. Die Grotteneingänge zeigen einen Ring, wenn die Grotte offen ist. Im Kampf ist alles ausgeblendet.
|
||||
|
||||
**Höhenfeld (`LuneTerrain`):** Hochland im Nordwesten hinter einer Abbruchkante, Tal in der Mitte, Tiefland im Südosten, Basisplateaus, Grottenterrasse, Randgebirge als abgerundetes Quadrat mit unregelmäßigem Fuß, feines Raster (1 m bis ±60 m) plus grober Außenring (4 m bis ±132 m). Jeder Term hängt nur von der Achsenkoordinate, dem Abstand zur Achse oder von gespiegelten Kartenelementen ab; das Gelände ist exakt spiegelsymmetrisch (Abweichung 0,0). Figuren stehen auf der „Laufhöhe“ (Lanebeläge, Plattform, Brücke, Terrasse), nie im Flussbett.
|
||||
|
||||
## Kamera
|
||||
|
||||
- Perspektivische MOBA-Kamera (34° Blickwinkel) statt der Orthokamera. Die Neigung wird beim Herauszoomen steiler: −48° nah, −70° in der Übersicht. `zoom` bedeutet weiterhin die sichtbare Bodenhöhe am Fokus, alle bisherigen Kamerarahmen behalten ihre Bedeutung.
|
||||
- Der Fokus folgt der Geländehöhe; der Schattenbereich passt sich der Kameradistanz an.
|
||||
- Tasten: **F2 / F3 / F4** Top / Mid / Bottom (F1 bleibt Optionen), **Tab** nächste Lane, **B** eigene Basis, **G** Bossgrotte, **H** eigener Held (Vorschau in der Planung, lebende Einheit im Kampf), **O** ganze Karte, **R** Rücksetzen (Drehung, Zoom, eigene Lane), **Q / E** Drehen in 45°-Schritten; WASD, Pfeiltasten, mittlere Maustaste und Mausrad wie bisher.
|
||||
- Die Minimap zeigt den tatsächlichen Bodenausschnitt der Kamera (Trapez) und die Kluft mit Delta.
|
||||
|
||||
## Assets
|
||||
|
||||
**Wiederverwendet, unverändert:** Laneturm (`tower.glb`, Blender-Generator aus M1), Ancients (Saumkern-Modelle `core_team0/1`), Kartenrequisiten aus `map_props.glb` (drei Kiefern, drei Eichen, drei Felsen, zwei Büsche, Bordsteine, Brücke, Randklippe, Banner), Kluft-Oberflächenshader (`sanctuary.gdshader`) mit dem Kalkstein-Textursatz, Flussshader, Beleuchtung, alle Figuren und Animationen.
|
||||
|
||||
**Wiederverwendet, verändert eingesetzt:**
|
||||
- M2-Grottenmodell (`grotto.glb`): nur Felsbogen, Höhlendunkel und Kristalle, Bodenscheibe und Findlinge ausgeblendet, Maßstab 1,05.
|
||||
- Brücke: um 34 % gestreckt als Deltabrücke.
|
||||
- Randklippe: gestaucht als Klippen hinter der Grotte.
|
||||
- Büsche: gestreckt als Schilf.
|
||||
|
||||
**Neu (Blockout, kein neues Blender-Asset):**
|
||||
- Höhenfeld mit Zonenfarben und eigenem Geländeshader (`lune_terrain.gdshader`).
|
||||
- Lanebeläge mit Böschung.
|
||||
- Mittelplattform, Runensteine und Runenkreis.
|
||||
- Basisplätze, Ringmauern, Torsäulen und Torkristalle.
|
||||
- Grottenterrasse, Säulen, Treppen und Wasserfall.
|
||||
- Ruinentor, Mauerreste und Aussichtsturm.
|
||||
- Aufmarschflächen (`lune_pad.gdshader`).
|
||||
|
||||
Diese Primitive ersetzen G2 (Ancients, Türme, Grotte) und G4 (Umgebung) durch Blender-Assets. Neue Blender-Assets entstanden in G1 bewusst nicht.
|
||||
|
||||
## Vorher / nachher (1920 × 1080, Spielkamera)
|
||||
|
||||
Gleiche Partie (Seed 4, Wurzelbund auf Mid), gleiche benannte Kamerarahmen, aufgenommen mit `tools/luneraria/g_views.tscn`.
|
||||
|
||||
| Ansicht | Vorher (M4) | Nachher (G1) |
|
||||
| --- | --- | --- |
|
||||
| 1 · ganze Karte | [vorher](screenshots/g1/vorher/01-karte.png) | [nachher](screenshots/g1/nachher/01-karte.png) |
|
||||
| 2 · Top Lane (Kampf) | [vorher](screenshots/g1/vorher/02-top-lane.png) | [nachher](screenshots/g1/nachher/02-top-lane.png) |
|
||||
| 3 · Mid Lane (Kampf) | [vorher](screenshots/g1/vorher/03-mid-lane.png) | [nachher](screenshots/g1/nachher/03-mid-lane.png) |
|
||||
| 4 · Bottom Lane (Kampf) | [vorher](screenshots/g1/vorher/04-bottom-lane.png) | [nachher](screenshots/g1/nachher/04-bottom-lane.png) |
|
||||
| 5 · eigene Basis | [vorher](screenshots/g1/vorher/05-eigene-basis.png) | [nachher](screenshots/g1/nachher/05-eigene-basis.png) |
|
||||
| 6 · Gegnerbasis | [vorher](screenshots/g1/vorher/06-gegnerbasis.png) | [nachher](screenshots/g1/nachher/06-gegnerbasis.png) |
|
||||
| 7 · Bossgrotte | [vorher](screenshots/g1/vorher/07-bossgrotte.png) | [nachher](screenshots/g1/nachher/07-bossgrotte.png) |
|
||||
| 8 · Kampf mit vielen Einheiten | [vorher](screenshots/g1/vorher/08-kampf-viele-einheiten.png) | [nachher](screenshots/g1/nachher/08-kampf-viele-einheiten.png) |
|
||||
| 9 · HUD (Planung) | [vorher](screenshots/g1/vorher/09-hud-planung.png) | [nachher](screenshots/g1/nachher/09-hud-planung.png) |
|
||||
| 10 · Held ausgewählt | [vorher](screenshots/g1/vorher/10-held-ausgewaehlt.png) | [nachher](screenshots/g1/nachher/10-held-ausgewaehlt.png) |
|
||||
| 11 · Zoom nah | [vorher](screenshots/g1/vorher/11-zoom-nah.png) | [nachher](screenshots/g1/nachher/11-zoom-nah.png) |
|
||||
| 12 · Kamera in Bewegung | [vorher](screenshots/g1/vorher/12-kamera-bewegt.png) | [nachher](screenshots/g1/nachher/12-kamera-bewegt.png) |
|
||||
| 13 · Grottenkampf (aus `verify_m2`) | [vorher](screenshots/g1/vorher/13-grottenkampf.png) | [nachher](screenshots/g1/nachher/13-grottenkampf.png) |
|
||||
|
||||
Die Ansichten 8 und 11 nutzen eine Aufnahme-Vorgabe (Runde 13, sechs volle Armeen, Gold nur für die Aufnahme).
|
||||
|
||||
## Gameplay-Befunde durch das neue Layout
|
||||
|
||||
- **Regeln unverändert:** Alle Regeltests bestehen ohne geänderte Erwartungswerte, auch der bitgenaue Spiegeltest über 12 Runden.
|
||||
- **Wege etwas länger:** Top und Bottom +2,5 %, Mid +3 %. Türme stehen dadurch absolut minimal weiter auseinander. Die Partielänge ist unverändert: 9–13 Wellen, Median 11 (M3: 9–14, Median 11).
|
||||
- **Neu, aber selten – Kampfphasen am Zeitlimit:** 4 von 442 Kampfphasen erreichen das 180-s-Fenster (M3: 0). Betroffen sind zwei fernkampflastige Zusammensetzungen, jeweils in Runde 12 und auf beiden Seiten gleich. Ein Zeitlimit beendet nur die Kampfphase, nie die Partie. Der Median der längsten Kampfphase je Partie sank von 113 auf 109 s. Beobachten, noch kein Handlungsbedarf.
|
||||
- **Balance-Verschiebungen im Rahmen:**
|
||||
- Profil „teamhilfe“ fällt von 46 auf 38 %, „ausgewogen“ steigt von 58 auf 62 %, „druck“ von 53 auf 59 %.
|
||||
- Bünde 53/47 (M3 55/45).
|
||||
- Bossentsendungen 12 statt 8 bei 40 Partien.
|
||||
- Seitentausch weiterhin 20/20 konsistent: Die Karte bevorzugt kein Team.
|
||||
- **Äußerer Mid-Turm nah an der Plattform:** Er steht 9,8 m vom Kartenmittelpunkt. Die Plattform ist damit gleichzeitig Kampfplatz und Vorfeld des Turms. Das ist gewollt, sollte aber bei G2 (Turmsilhouette) im Blick bleiben.
|
||||
- **Grotte nah an der Top-Ecke:** Die Arena liegt 3,0 m außerhalb des Top-Lanekorridors. Kämpfe an der Top-Ecke und in der Grotte sind aus derselben Kamera sichtbar. Das unterstützt die gewünschte Blickbeziehung, verlangt in G2 aber, dass Grotteneffekte die Top-Figuren nicht überdecken.
|
||||
- **Figuren am Ufer:** Wer im Kampf eine Lane verlässt und ans Ufer läuft, wird auf der Wasseroberfläche dargestellt, nicht im Flussbett. Das ist rein optisch; die Simulation ist zweidimensional.
|
||||
|
||||
## Leistung
|
||||
|
||||
Gleiche Messmethode für beide Stände (`tools/luneraria/lune_benchmark.tscn`). Fedora 44, Ryzen 5 5600X, RTX 5060 Ti, Forward+, 1920 × 1080, VSync aus, Tempo 2×, Runde 13 mit sechs vollen Armeen (120 bewegliche Einheiten zu Beginn). CPU = Hauptthread vom ersten `_process` bis zum fertig gezeichneten Bild.
|
||||
|
||||
| Messung | Vorher (M4) | Nachher (G1) |
|
||||
| --- | --- | --- |
|
||||
| Lane-Ansicht: Bildzeit Mittel / p95 | 6,16 / 8,58 ms | 6,95 / 9,66 ms |
|
||||
| Lane-Ansicht: CPU / GPU | 6,04 / 3,17 ms | 6,82 / 3,29 ms |
|
||||
| Lane-Ansicht: Draw Calls / Objekte | 3 609 / 7 060 | 3 340 / 7 438 |
|
||||
| Übersicht: Bildzeit Mittel / p95 | 7,30 / 9,31 ms (Zoom 46, nur Teil der Karte) | 6,97 / 9,28 ms (Zoom 92, ganze Karte) |
|
||||
| Übersicht: CPU / GPU | 7,17 / 4,11 ms | 6,84 / 3,52 ms |
|
||||
| Übersicht: Draw Calls | 5 448 | 3 605 |
|
||||
| Szenenaufbau (inkl. Höhenfeld) | 180 ms | 293 ms |
|
||||
| Simulationsschritt | 2,54 ms | 2,71 ms |
|
||||
| VRAM / Texturen / statischer RAM | 780 / 425 / 126 MB | 782 / 425 / 128 MB |
|
||||
|
||||
Optimiert statt entfernt:
|
||||
- Schattenreichweite an die Kameradistanz gekoppelt (70–220 m). Das senkte die zusätzliche GPU-Zeit der Lane-Ansicht von +0,56 auf +0,12 ms.
|
||||
- Kein Schattenwurf für den fernen Gebirgsring.
|
||||
- Alle Wiederholteile als MultiMesh.
|
||||
- Das Höhenfeld wird nur für die Blau-Hälfte berechnet und gespiegelt; es wird je Prozess einmal erzeugt und bei einer Revanche wiederverwendet.
|
||||
- Lane-Abfragen per binärer Suche.
|
||||
|
||||
Das Bild bleibt CPU-gebunden, vor allem durch die rund 120 gerenderten Skelettfiguren. Logs: `logs/g1/perf-vorher-1920x1080.json`, `logs/g1/perf-nachher-1920x1080.json`.
|
||||
|
||||
## Tests
|
||||
|
||||
| Prüfung | Ergebnis |
|
||||
| --- | --- |
|
||||
| G1-Abnahme `verify_g1` (echte Anwendung, Tasten, Mausrad) | 443/443 |
|
||||
| Regeln `test_lune_core` (inkl. Spiegeltest) | 91/91 |
|
||||
| Regeln M2 (Helden, Items, Talente, Champions, Grotte, Boss, Bots) | 73/73 |
|
||||
| Netzwerkprotokoll `test_lune_net` (Regression) | 43/43 |
|
||||
| Mehr-Runden-Loopback `net_rounds.py` (Regression) | 12/12 (nach Anpassung des Trennzeitpunkts im Testtreiber, siehe oben) |
|
||||
| Echte Anwendung M1 | 38/38 bei 1920 × 1080. Bei 1280 × 720: 5 von 7 Läufen 38/38; zwei Läufe scheiterten an je einem Klick (einmal im Menü vor dem Partiestart, einmal im Shop), danach vier Läufe in Folge bestanden. Der M4-Stand bestand 3/3. Da der Menü-Fehlklick vor jedem G1-Code auftrat, gehen wir von einer zeitweiligen Eingabestörung aus; nicht abschließend geklärt. |
|
||||
| Echte Anwendung M2 | 14/14 (Partie endete vor der Bossentsendung, bekannte Variante) |
|
||||
| Balance 40 Partien (wie M3) | Seitentausch 20/20, alle regulär nach 9–13 Wellen, 0 Testlimit |
|
||||
| Legacy Saumwacht | 209 · 463 unverändert |
|
||||
|
||||
`verify_g1` prüft unter anderem:
|
||||
- **Lanes und Wasser:** Jede Lane ist überall begehbar; Wasser unter einer Lane gibt es nur auf der Plattform und der Deltabrücke; Top bleibt trocken und erreicht das Hochland; Bottom liegt im Tiefland; Mid quert auf der Plattform.
|
||||
- **Gebäude:** Alle 16 Türme und beide Ancients liegen im Spielfeld, stehen trocken, eben und mit Abstand zueinander; die Basistürme stehen vor ihrem Ancient.
|
||||
- **Grotte:** Sie liegt auf der Achse, blockiert Mid nicht und hält alle Lanekorridore frei; Eingänge und Bosspunkt liegen auf der Terrasse, die Quelle entspringt an ihrem Fuß.
|
||||
- **Aufmarschflächen:** Sie liegen trocken, innen und auf der eigenen Seite.
|
||||
- **Gelände:** Es ist exakt gespiegelt.
|
||||
- **Kamera:** Alle Sprungtasten treffen ihr Ziel; die Übersicht zeigt alle Gebäude und die Grotte; Drehen, Rücksetzen, Zoomgrenzen und die Neigung je Zoom funktionieren; der Minimap-Umriss ist korrekt; ein weicher Sprung kommt in 1,2 s an.
|
||||
- **Planung und Kampf:** In der Planung sind nur die eigenen drei Flächen sichtbar, hervorgehoben dort, wo eigene Einheiten stehen. Im Kampf sind sie ausgeblendet. Alle Figuren stehen auf dem Gelände (Fehler < 0,001 m), und F3 zeigt im Kampf die eigene Mid-Armee.
|
||||
|
||||
## Vier Kategorien
|
||||
|
||||
**Implementiert:**
|
||||
- Drei-Lane-Layout mit Lane-Identitäten, Höhenfeld, Kluft, Basen, 16 Türmen (12 Lane- und 4 Basistürme) und 2 Ancients, Bossgrotte (Blockout), Aufmarschbereiche.
|
||||
- Perspektivische Spielkamera mit Sprungtasten.
|
||||
- Minimap-Anpassung, Vergleichs- und Messwerkzeuge.
|
||||
|
||||
**Automatisiert geprüft:** siehe Tabelle „Tests“.
|
||||
|
||||
**Visuell geprüft:** alle 13 Vergleichsansichten und der Kartenplan in der Spielkamera bei 1920 × 1080, dazu Lane-Kampf, hohe Einheitenzahl, Nahzoom, bewegte Kamera und Grottenkampf.
|
||||
|
||||
**Ungetestet:**
|
||||
- OpenGL-Kompatibilitätsmodus mit dem neuen Gelände.
|
||||
- Windows 11.
|
||||
- Schwächere GPUs.
|
||||
- Menschliche Spieltests des neuen Layouts.
|
||||
- Übertragungsverhalten des Netzspiels auf der neuen Karte über echte Netze (nur Loopback).
|
||||
|
||||
## Offen und nächste Schritte
|
||||
|
||||
- **G2 Ancients, Türme, Bossgrotte:**
|
||||
- monumentale Ancients mit Zustandsstufen;
|
||||
- modulares Turmsystem mit Angriffsbereich, Treffer- und Zerstörungszuständen;
|
||||
- Grotte als echtes Blender-Asset mit den Zuständen neutral, verfügbar, umkämpft und Abklingzeit;
|
||||
- Teamidentität über Banner, Runen und Architektur.
|
||||
- **G3 HUD:** das vollständige neue Ingame-HUD. G1 hat hier nur Tastenhinweise und die Minimap angepasst.
|
||||
- **G4 Umgebung:** Klippen- und Ruinen-Assets statt Primitiven, 3–5 neue Baumarten, Uferzonen, neuer Wassershader.
|
||||
- **G5:** Licht und Materialien.
|
||||
- **G6:** Einheitendarstellung und Völker.
|
||||
- Beobachten: Kampfphasen am Zeitlimit bei fernkampflastigen Teams.
|
||||
63
docs/luneraria/G2-ASSETS.md
Normal file
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
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.
|
||||
137
docs/luneraria/G3-HUD-BESTAND.md
Normal file
137
docs/luneraria/G3-HUD-BESTAND.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# G3 · HUD-Bestand vor dem Umbau
|
||||
|
||||
Stand: 29. September 2026 · Grundlage `luneraria/g2` (`9f62e30`), Arbeitszweig `luneraria/g3`.
|
||||
Diese Aufnahme beschreibt das HUD, wie es G2 ausliefert. Sie ist die Referenz für die Entscheidungen in [G3-HUD.md](G3-HUD.md).
|
||||
|
||||
## Aufnahmen
|
||||
|
||||
Alle Bilder stammen aus dem echten Spiel (`LuneGame` mit dem echten HUD). `tools/luneraria/g3_views.gd` setzt nur öffentlichen Partiezustand (Runde, Gold, Gebäude-HP, Boss-Claim) und bedient das HUD über dessen eigene Methoden; dasselbe Skript fotografiert später das G3-HUD.
|
||||
|
||||
| Auflösung | Ordner | logische Fläche |
|
||||
| --- | --- | --- |
|
||||
| 1920×1080 | `screenshots/g3/vorher-1920x1080/` | 1706×960 (Skalierung 1,125) |
|
||||
| 2560×1440 | `screenshots/g3/vorher-2560x1440/` | 1706×960 (1,5) |
|
||||
| 1280×720 | `screenshots/g3/vorher-1280x720/` | 1706×960 (0,75) |
|
||||
|
||||
Motive je Ordner: `01-planung`, `02-held-ausgewaehlt`, `03-itemauswahl`, `04-einheit-ausgewaehlt`, `05-angriffschampion`, `06-boss-verfuegbar`, `07-boss-angekuendigt`, `08/09/10-kampf-top/mid/bottom`, `11-eigener-ancient-kritisch`, `12-gegnerischer-ancient-kritisch`, `13-turm-zerstoert`, `14-grosse-kampfansicht`.
|
||||
|
||||
Wichtig für das Layout: Das Projekt skaliert mit `canvas_items`/`expand` von 1600×960 aus. Jede 16:9-Auflösung ergibt dieselbe logische Fläche von 1706×960; 1280×720 zeigt also exakt dasselbe Layout wie 2560×1440, nur mit halb so vielen Pixeln. Ein 16-px-Text ist bei 720p physisch 12 px hoch. Ultrawide (21:9) verbreitert die Fläche auf etwa 2275×960, 16:10 und das Mindestfenster 1152×720 schmälern sie auf 1600×960.
|
||||
|
||||
## Aufbau
|
||||
|
||||
| Datei | Zeilen | Inhalt |
|
||||
| --- | ---: | --- |
|
||||
| `scripts/luneraria/ui/lune_hud.gd` | 484 | das gesamte HUD in einem Skript: Kopfleiste, Lane-Panel, Planungsseite, Kampfpanel, Bericht, Minimap-Rahmen, Ergebnis |
|
||||
| `scripts/luneraria/ui/lune_hero_panel.gd` | 186 | Reiter „Held“: Anwerben, Stufe/EP, Einsatz-Lane, Items, Talente, Live-Porträt |
|
||||
| `scripts/luneraria/ui/lune_reinforce_panel.gd` | 113 | Reiter „Verstärkung“: Champion-Buchung, Ankündigungen, Boss-Lane |
|
||||
| `scripts/luneraria/ui/lune_minimap.gd` | 80 | Minimap aus dem Partiezustand |
|
||||
| `scripts/luneraria/ui/lune_settings.gd` | 68 | Optionen und Steuerungstext |
|
||||
| `scripts/ui/theme/saumwacht_theme.gd`, `forged_style.gd` | 247 + 69 | gemeinsames Menü-Theme (Basaltplatten, Bronzekanten) |
|
||||
| `scripts/ui/components/choice_group.gd` | – | Segmentauswahl, im HUD für die drei Reiter |
|
||||
|
||||
Bildschirmaufteilung (logische Pixel bei 1706×960):
|
||||
|
||||
- **Kopfleiste** über die ganze Breite (Höhe ≈ 96): Titel, Phase, „Welle n“, zwei Ancient-Balken mit Zahl, Gold, Einkommen, Armeegröße, Identität (Team · Lane · Bund), Pause/Tempo (nur Kampf), Optionen, Menü.
|
||||
- **Links** (300 breit, ≈ 400 hoch): „LANES · TÜRME“ mit drei Knöpfen `TOP Blau ●● Rot ●●` plus Besitzernamen, darunter Teamlisten mit Basistürmen und der Grottenstatus als Fließtext.
|
||||
- **Rechts** (484 breit ≈ 28–30 % der Breite, 112 bis 270 über dem Boden): Reiter Truppen / Held / Verstärkung, Kaufkacheln, Armeeliste mit T/M/B/G-Umschaltern, ↑-Aufwertung, ×-Verkauf, Startaufstellung, Rückgängig, Bereit. Im Kampf ersetzt ein Textpanel mit Einheitenzahlen pro Lane die Seite.
|
||||
- **Unten links** (bis 760 breit): Rundenbericht und Hinweis-/Fehlerzeile.
|
||||
- **Unten rechts**: Minimap 236×236.
|
||||
- **Ergebnis**: Vollbild-Abdunklung mit Sieg/Niederlage.
|
||||
|
||||
## Theme und Komponenten
|
||||
|
||||
- Das HUD nutzt das Menü-Theme `SaumwachtTheme` unverändert: Varianten `CaptionPanel`, `MenuPanel`, `SectionLabel`, `MutedLabel`, `CardTitleLabel`, `PrimaryButton`, `QuietButton`, `ChoiceButton`.
|
||||
- Es gibt keine HUD-Komponenten. Knöpfe, Balken und Zeilen entstehen lokal; `_bar()` legt für jeden Balken eigene `StyleBoxFlat`-Kopien an.
|
||||
- Die Menüplatten (`MenuPanel`: 30/26 px Innenabstand, Nieten, 18 px Schatten) sind für ein Spielfeld-HUD zu schwer; der rechte Block wirkt wie ein Menü über dem Feld.
|
||||
|
||||
## Porträts
|
||||
|
||||
- Kauf- und Armeekacheln zeigen `AssetLibrary.icon()`, also die 2D-Konzeptbilder aus Phase 3, nicht die Spielmodelle. Heldenmodelle, Champions, Monster und die Schlundkrone haben dort gar kein Bild.
|
||||
- Nur der Heldenreiter rendert das echte Modell: eine eigene `SubViewport` mit eigener Welt, `UPDATE_WHEN_VISIBLE`, jede Frame animiert, solange der Reiter offen ist.
|
||||
- Keine einheitliche Kamera, kein Cache.
|
||||
|
||||
## Eingabe
|
||||
|
||||
| Taste | Wirkung (G2) | Ort |
|
||||
| --- | --- | --- |
|
||||
| 1–5 | Truppe kaufen | `lune_game._input` |
|
||||
| Leertaste | Planung: Bereit · Kampf: Pause | `lune_game._input` |
|
||||
| P | Pause | `lune_game._input` |
|
||||
| Tab | nächste Lane | `lune_game._input` |
|
||||
| F2 / F3 / F4 | Kamera Top / Mid / Bottom | `lune_game._input` |
|
||||
| B · G · H · O · R | eigene Basis · Grotte · Held · Übersicht · Kamera zurück | `lune_game._input` |
|
||||
| Q / E | Kamera drehen | `lune_game._input` |
|
||||
| WASD, Pfeile, mittlere Maustaste, Mausrad | Kamera | `lune_camera` |
|
||||
| F1 · M · F11 · Esc | Optionen · Ton · Vollbild · Optionen schließen | `lune_main._unhandled_input` |
|
||||
|
||||
Frei sind unter anderem C, V, X, Z, N, U, I, J, K, L, T, Y und F5–F10. Die Welt kennt keine Auswahl per Linksklick; Einheiten wählt man nur über die Armeeliste.
|
||||
|
||||
## Tooltips
|
||||
|
||||
Alle Hinweise sind Godot-Standard-Tooltips (`tooltip_text`): reiner Text, feste Verzögerung, Stil aus dem Menü-Theme. Einheitenwerte stehen nur im Tooltip der Kaufkachel (HP, „Schaden/s“ – tatsächlich Schaden pro Treffer bei 1 Angriff/s –, Reichweite, Fähigkeit). Item- und Talenttexte erscheinen nur als Tooltip.
|
||||
|
||||
## Optionen
|
||||
|
||||
Lautstärke, Ton stumm, Vollbild, Bewegung reduzieren, dazu ein Steuerungstext. Es gibt keine UI-Größe. „Bewegung reduzieren“ wirkt auf Kamera, Menü und Effekte; das HUD hat keine Animationen.
|
||||
|
||||
## Minimap
|
||||
|
||||
- Zeichnet Lanes, Fluss, Grotte (Farbe: frei/gesperrt/Team), alle Gebäude (voll/zerstört) und im Kampf **jede** bewegliche Einheit als Punkt. Bei 90–120 Einheiten entsteht eine Punktwolke; Helden (gold) und Boss (größer) gehen darin unter.
|
||||
- Türme beider Teams unterscheiden sich nur durch die Farbe.
|
||||
- Klick und Ziehen bewegen die Kamera; das Trapez des Kamerabilds ist sichtbar.
|
||||
- `queue_redraw()` in jeder Frame, unabhängig von Änderungen.
|
||||
|
||||
## Lanes, Türme, Ancients
|
||||
|
||||
- Lane-Zustand nur als `●●`/`●○` für eigene und gegnerische Außen-/Innentürme; kein Schadensstand, keine Bedrohung, keine Angabe, welche Seite drückt, keine angekündigten Champions.
|
||||
- Basistürme stehen als „Basistürme 2/2“ im Teamtext.
|
||||
- Ancients: zwei gleich gewichtete Balken mit Zahl in der Kopfleiste; kein kritischer Zustand.
|
||||
- Klick auf eine Lane springt mit der Kamera dorthin (gut, bleibt).
|
||||
|
||||
## Held, Items, Talente
|
||||
|
||||
- Heldenreiter mit Live-Porträt, Stufe, EP-Balken (Werte nur im Tooltip), Einsatz-Lane, drei Itemknöpfen (Name + ×), sechs Item-Kaufknöpfen, Talentwahl.
|
||||
- Keine Grundwerte (HP, Schaden, Tempo, Reichweite, Rüstung) und keine Fähigkeitswerte.
|
||||
- Item-Zustände nur über „deaktiviert“; warum ein Item nicht geht (Gold, Stufe, schon getragen, Plätze voll), steht nirgends.
|
||||
- Bei 1280×720 ist der Reiter abgeschnitten: Itemkauf und Talente liegen unter „Startaufstellung/Rückgängig“ und brauchen Scrollen (`vorher-1280x720/03-itemauswahl.png`).
|
||||
|
||||
## Normale Einheiten
|
||||
|
||||
- Eine „Auswahl“ gibt es nicht; jede Armeezeile hat Porträt (2D), abgeschnittenen Namen („Korkı“, „Dorn“, „Same“), T/M/B/G, ↑ Kosten, ×.
|
||||
- Werte der gekauften Einheit, Aufwertungsziel und Rückgabebetrag nur im Tooltip.
|
||||
- G (Grotte) ohne Warnung, dass die Einheit ihrer Lane die ganze Kampfphase fehlt (nur im Tooltip und im Grottentext links).
|
||||
|
||||
## Angriffschampions, Boss, Wellen
|
||||
|
||||
- Champions: Textliste im Reiter „Verstärkung“ mit Ziel-Lane-Schaltern; Buchungen als „Gebucht: … Runde n“. Optisch wie normale Knöpfe, kein Unterschied zu dauerhaften Truppen.
|
||||
- Ankündigungen (öffentlich) nur in diesem Reiter.
|
||||
- Bossgrotte: Fließtext links („verfügbar“, „gesperrt bis Runde n“, „von Blau erobert … greift in Runde 12 über Mid an“). Kein Hinweis auf dem Spielfeld, kein Kopfgeld im Blick, keine Unterscheidung „umkämpft“.
|
||||
- Wellen: „Welle n“ ohne Obergrenze (richtig). Was die nächste Welle bringt, zeigt das HUD nicht, obwohl `LuneWaves.build()` öffentlich und deterministisch ist.
|
||||
- Ereignisse: nur der Rundenbericht nach dem Kampf und eine Hinweiszeile; Turmfall, Bossangriff oder kritischer Ancient erscheinen nicht im Moment des Geschehens.
|
||||
|
||||
## Netzwerk und Privatsphäre
|
||||
|
||||
- Der Gast erhält `rpc_state` mit dem **Gold aller Spieler**, deren bestätigten Armeen, Helden und Buchungen; den Entwurf (`draft`) nur für den eigenen Sitz.
|
||||
- Das G2-HUD zeigt Gold nur für den eigenen Sitz (`gold_of(human_slot)`) – das bleibt Pflicht.
|
||||
- Bereit-Status, Mensch/Bot und Verbindungsabbrüche zeigt das HUD nicht; Sitzungsmeldungen erscheinen nur als Hinweiszeile.
|
||||
- Nach „Bereit“ lehnt der Host weitere Befehle ab („Bereits bereit gemeldet …“); das HUD zeigt dann „WARTE AUF ANDERE …“, lässt aber Kaufkacheln, Lane-Knöpfe und Tasten 1–5 aktiv.
|
||||
|
||||
## Leistung
|
||||
|
||||
- `refresh()` läuft jede Frame: alle Beschriftungen werden neu formatiert, im Kampf wird zusätzlich über alle Einheiten gezählt (O(n) je Frame), in der Planung `JSON.stringify(draft)` gebildet.
|
||||
- Held- und Verstärkungsreiter bilden jede Frame eine JSON-Signatur.
|
||||
- Die Minimap zeichnet jede Frame alle Einheiten.
|
||||
- Die Heldenporträt-`SubViewport` rendert jede Frame, solange der Reiter offen ist.
|
||||
|
||||
## Konsequenzen für G3
|
||||
|
||||
1. HUD in Komponenten zerlegen, Theme um HUD-Varianten erweitern statt lokaler StyleBox-Kopien.
|
||||
2. Ancients und Welle in eine schmale Kopfzeile; die Ancients bekommen mehr Gewicht als Einzeltürme.
|
||||
3. Lane-Übersicht mit echten Turmsymbolen (Form + Schadensstand), deterministischer Bedrohungsstufe aus öffentlichem Zustand und angekündigten Gefahren.
|
||||
4. Kauf, Armee und Held nach unten; rechts nur Kontext (Auswahl, nächste Welle, Boss) statt einer dauerhaften 30-%-Leiste.
|
||||
5. Porträts aus den echten Modellen, einmal gerendert und zwischengespeichert, statt 2D-Skizzen und Live-Viewport.
|
||||
6. Minimap mit zusammengefassten Armeen, deutlichen Helden, Champions und Boss, Teamformen statt nur Farben.
|
||||
7. Ereignisleiste für Turmfall, Champions, Boss, kritische Ancients.
|
||||
8. Bereit-Sperre sichtbar machen, fremdes Gold und fremde Entwürfe weiter nie anzeigen, Mensch/Bot/Verbindung pro Mitspieler zeigen.
|
||||
9. Aktualisierung nach Änderungen statt jeder Frame; Minimap gedrosselt.
|
||||
10. Layout für 1600–2300 logische Breite und eine UI-Größe in den Optionen; Mindestschriftgröße so, dass 720p lesbar bleibt.
|
||||
59
docs/luneraria/G3-HUD.md
Normal file
59
docs/luneraria/G3-HUD.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# G3 · Ingame-HUD (Zwischenstand)
|
||||
|
||||
Stand: 29. September 2026 · Zweig `luneraria/g3` (von `luneraria/g2`, `9f62e30`).
|
||||
|
||||
**G3 ist nicht abgenommen.** Nach dem ersten Spieltest hat der Projektinhaber entschieden, Champions of Luneraria nach Unity zu portieren. Dieser Stand ist die Referenz für das HUD in Unity (Portphase U3). Ausgangslage vor dem Umbau: [G3-HUD-BESTAND.md](G3-HUD-BESTAND.md).
|
||||
|
||||
## Was steht
|
||||
|
||||
**Informationsarchitektur** (logische Pixel bei 1706×960, UI-Größe darauf):
|
||||
|
||||
| Bereich | Inhalt |
|
||||
| --- | --- |
|
||||
| oben Mitte | eigener Ancient (Emblem Mondsichel/Sonnenscheibe, HP-Balken mit Wert, zwei Basistürme, Zustand), **WELLE n** ohne Obergrenze, Phase (PLANUNG, PLANUNG · BEREIT, KAMPF mm:ss), gegnerischer Ancient |
|
||||
| oben rechts | Pause, Tempo (nur im Kampf), Optionen F1, Menü |
|
||||
| links | drei Lane-Zeilen: Bedrohung, Turmkette von der eigenen zur gegnerischen Basis mit HP, Spieler und Helden beider Seiten, angekündigte Champions/Boss, Einheitenzahl im Kampf; darunter das eigene Team und die Ereignisleiste |
|
||||
| unten | Gold und Einkommen, Rückgängig; Kaufkarten 1–5 oder Reiter Angriffschampions; eigene Armee als Porträt-Chips mit Lane und Stufe; Heldenkarte |
|
||||
| rechts | Bossgrotte, nächste Welle, ausgewählte Einheit; nie eine dauerhafte Seitenleiste über 30 % |
|
||||
| unten rechts | Bereit (Planung) bzw. Kampfuhr, darunter die Minimap |
|
||||
|
||||
**Komponenten** (`scripts/luneraria/ui/components/`, Theme `ui/theme/lune_ui_theme.gd`): StatusBadge, HpBar, ResourceDisplay, TowerIndicator, LaneIndicator, TeamPanel, UnitCard (Champion-Variante kupfern, schraffiert, „1 WELLE“), ArmyChip, HeroCard, HeroDetail (Anwerben, Werte, Items, Talente), ItemSlot mit acht Zuständen, UnitDetail, ChampionPanel, BossIndicator, NextWave, EventFeed, TabBar, ModalPanel, ein gemeinsamer Tooltip. Symbole sind Vektorgrafiken (`LuneGlyphs`), Porträts kommen aus den echten Modellen (`LunePortraits`: eine Offscreen-Kamera, jedes Modell einmal gerendert und zwischengespeichert).
|
||||
|
||||
**Datenzugang und Privatsphäre:** Alles läuft über `LuneHudModel`. Gold, Entwurf und neue Buchungen gibt es nur für den eigenen Sitz; von anderen Spielern nur Name, Lane, Bereitschaft, Mensch/Bot/offline und der bestätigte Held. Die Lane-Bedrohung (`LuneLaneThreat`: RUHIG, UMKÄMPFT, UNTER DRUCK, KRITISCH) rechnet nur mit öffentlichen Daten und für Host und Gast gleich; auf einem Gast werden Positionen auf die Lane projiziert.
|
||||
|
||||
**Bedienung:** Klick auf Lane, Ereignis, Bossgrotte oder Minimap bewegt die Kamera; F2–F4, G, B, H wie in G2, alle in den Optionen gelistet. Linksklick auf eine eigene Figur im Feld wählt sie aus. Nach „Bereit“ sind Karten, Tasten 1–5, Rückgängig und Einsatzknöpfe gesperrt. Bereit ignoriert eine Sekunde nach Kampfende Klicks und Leertaste. Das Mausrad über HUD-Flächen zoomt die Welt nicht.
|
||||
|
||||
**Aktualisierung:** 10 Hz in der Planung, 5 Hz im Kampf, Minimap 10 Hz mit eigener Ebene für den Kamerarahmen; jede Komponente vergleicht eine Signatur und baut nur bei Änderungen neu.
|
||||
|
||||
**Team-Erkennung ohne Farbe:** Blau rund (Mondsichel), Rot eckig (Sonnenscheibe), Grotte sechseckig: in HUD, Minimap, Einheitenringen im Feld. Die entsandte Schlundkrone trägt einen zweiten Besitzring und „für Blau/Rot“ im Namen (G2-Risiko der Farbnähe).
|
||||
|
||||
**Legion TD 2 / Dota 2:** Die Wellenvorschau zeigt die Rüstung jedes Monsters und welcher Schadenstyp stark dagegen ist (Schadensmatrix).
|
||||
|
||||
## Prüfung
|
||||
|
||||
| Prüfung | Ergebnis |
|
||||
| --- | --- |
|
||||
| `tools/luneraria/verify_g3.tscn` (Layout 1152×720 bis 2560×1440 und 2560×1080 bei 90/100/130 %, Status, Held/Items/Einheiten, Champions/Boss, Bereit-Sperre, Bots, Netzgast mit Privatsphäre, Eingabe, Ereignisse, Bewegung reduzieren, Raten, Porträt-Cache) | 715/715 |
|
||||
| Regeln `test_lune_core` / `test_lune_m2` / `test_lune_net` | 91/91 · 73/73 · 43/43 |
|
||||
| `verify_m1` / `verify_m2` (UI-Treiber an das neue HUD angepasst, Regelprüfungen unverändert) | 38/38 · 14/14 |
|
||||
|
||||
Bilder: `screenshots/g3/zwischenstand-1920x1080/`, Spieltest `screenshots/g3/spieltest/`, Ablauf `logs/g3/playtest-log.txt`.
|
||||
|
||||
## Spieltest (Partie über die echte Oberfläche bis Welle 8)
|
||||
|
||||
Werkzeug: `tools/luneraria/g3_playtest.tscn` (Mausklicks, Tasten und Mausrad an Fensterkoordinaten, Bildschirmfoto nach jedem Schritt; keine Eingriffe in den Spielzustand).
|
||||
|
||||
| Befund | Status |
|
||||
| --- | --- |
|
||||
| Pause/Tempo lagen im Kampf genau dort, wo danach „Bereit“ erscheint; ein später Klick übersprang die Planung von Welle 2 | behoben: Pause/Tempo oben rechts, Bereit 1 s gesperrt |
|
||||
| Heldenkarte bot im Kampf „Held anwerben“ an | behoben: „Kein Held · Anwerben in der Planung“ |
|
||||
| „1 WELLE“ auf Champion-Karten überdeckt die Köpfe der Porträts | offen: als Band am unteren Porträtrand |
|
||||
| Gut lesbar: Hotkey-Kauf mit Rückmeldung, Lane-Tooltip mit Turm-HP und Gründen, gefallene Einheiten durchgestrichen, Stufenaufstieg und Turmfall in der Ereignisleiste, „TALENT!“, Item-Zustände, Champion-Ankündigung „FÜR EUCH“ in Lane und Wellenvorschau | – |
|
||||
|
||||
## Offen (für U3 übernehmen)
|
||||
|
||||
- Champion-Etikett (siehe oben).
|
||||
- Schadens- und Rüstungstyp als Symbol auf Einheitenkarten, farblich passend zur Wellenvorschau (Legion-TD-Prinzip).
|
||||
- Armeewert (Summe des Einsatzes) in der Armeezeile.
|
||||
- Heldenporträts statt Punkten auf der Minimap (Dota-Prinzip).
|
||||
- Aufnahmen 2560×1440, 1280×720, Ultrawide für den Endstand, Video, Leistungsvergleich G2 → G3.
|
||||
47
docs/luneraria/HELDEN-KONZEPT.md
Normal file
47
docs/luneraria/HELDEN-KONZEPT.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Heldenkonzept · Champions of Luneraria
|
||||
|
||||
Stand: 29. September 2026 · Vorgabe des Projektinhabers, während G3 eingegangen.
|
||||
**Noch nicht umgesetzt.** G3 schließt neue Einheiten und Regeländerungen aus; die Umsetzung gehört in eine eigene Phase (Einheiten/Helden, frühestens mit G6).
|
||||
Die vier Helden sollen die drei vorläufigen Helden ablösen. Diese nutzen bisher vorhandene Modelle: Ringborke (w04), Quellgeflecht (w10) und Funkenlanze (g05); siehe `scripts/luneraria/core/lune_content.gd`.
|
||||
|
||||
## Die vier Helden
|
||||
|
||||
| | Lucy | Ghaliga | Siegfried | Feena |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Volk | Dunkelelfe | Zwerg | Mensch | Waldelfe |
|
||||
| Rolle | Kontrolle, Schwächung, Wellen stören | Tank, Bruiser, Frontkämpfer | Anführer, Paladin, Team-Buff, Front | Fernkampf-DPS, Boss- und Elitekiller |
|
||||
| Waffe | Bogen | Axt | Schwert (gold-blau) | Bogen |
|
||||
| Fähigkeit | Frostpfeile verlangsamen Gegner | Wirbelhieb: Flächenangriff um ihn herum | Schlachtruf stärkt nahe Verbündete | Giftpfeile: Schaden über Zeit |
|
||||
| Ultimate (Command-Menü) | Multischuss: mehrere Pfeile auf mehrere Ziele, Kontrolle und Flächendruck statt Burst | Versteinerung: absorbiert massiv Schaden, extrem zäh, optional Bindung/Aggro | Excalibur: heiliger Schlag mit hohem Schaden und kurzer Betäubung mehrerer Gegner | Turbopfeile: kurz stark erhöhte Angriffsgeschwindigkeit |
|
||||
| Spielziel | gefährliche Wellen verlangsamen, Formationen stören | Front halten, Gegner binden, Wellen abfangen | Verbündete stärken, kritische Kämpfe drehen | hoher anhaltender Fernkampfschaden, besonders gegen starke Einzelziele |
|
||||
| Optik | lange weiße Haare, grüne Augen, dunkle Haut, dunkle Lederkleidung mit dunkelblauen Akzenten, frostige Nachtästhetik | rote Haare, langer Bart, blaue Augen, goldene Zwergenrüstung, schwere robuste Silhouette | kurze blonde Haare, blaue Augen, goldene Paladinrüstung | lange rote Haare, grüne Augen, grün-braune Lederrüstung, flinke naturverbundene Schützin |
|
||||
|
||||
## Was die Umsetzung berührt (Notizen für die spätere Phase)
|
||||
|
||||
**Neue Mechanik: Ultimates über ein Command-Menü.** Bisher laufen Kämpfe automatisch, Helden handeln allein und Spieler geben nur in der Planung Befehle. Ein Ultimate im Kampf braucht deshalb:
|
||||
- Regeln: Auslösen, Abklingzeit oder Ladung, Zielwahl (Spieler oder automatisch), Dauer, Reihenfolge im Simulationsschritt, Determinismus.
|
||||
- Netzwerk: einen Kampfbefehl vom Gast an den Host; heute gibt es nur Planungsbefehle.
|
||||
- Bots: wann sie auslösen.
|
||||
- HUD: ein Command-Menü. G3 hat bewusst keine MOBA-Aktionsleiste; Heldenkarte und Heldenfenster haben aber Platz für Fähigkeit und Ultimate.
|
||||
|
||||
**Effekte gegenüber dem heutigen Regelwerk:**
|
||||
|
||||
| Effekt | Stand heute |
|
||||
| --- | --- |
|
||||
| Verlangsamung (Frost) | vorhanden: `movement_slow`, `attack_slow` |
|
||||
| Gift über Zeit | vorhanden: DoTs wie `seed_burn`, `burn` |
|
||||
| Fläche um den Helden (Wirbelhieb) | vorhanden: Splash-Logik |
|
||||
| Buff naher Verbündeter (Schlachtruf) | vorhanden: `damage`-Effekt |
|
||||
| Angriffstempo (Turbopfeile) | vorhanden: `attack_speed`-Effekt |
|
||||
| Schadensabsorption / Zähigkeit (Versteinerung) | vorhanden: Schild und Schadensminderung (Deckel 35 %) |
|
||||
| Betäubung (Excalibur) | **neu** |
|
||||
| Bindung / Aggro (Versteinerung) | **neu**: Zielwahl-Regel |
|
||||
|
||||
**Assets:** vier neue skelettierte Modelle (Blender) mit mindestens idle, walk, attack, ability, ultimate, hit und death, dazu die Waffen (zwei Bögen, Axt, Schwert). Porträts entstehen automatisch über den G3-Porträt-Cache aus den Modellen.
|
||||
|
||||
**Offene Fragen:**
|
||||
- Bleibt es bei einem Helden pro Spieler?
|
||||
- Hängen Helden an Bünden (Glutpakt/Wurzelbund) oder sind sie frei wählbar?
|
||||
- Ersetzen die vier die drei vorläufigen Helden vollständig?
|
||||
- Wie oft gibt es das Ultimate (je Welle, Abklingzeit in Sekunden, Ladung über Stufe)?
|
||||
- Wie werden Talente und Items auf die neuen Rollen abgestimmt?
|
||||
137
docs/luneraria/QA-MATRIX.md
Normal file
137
docs/luneraria/QA-MATRIX.md
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
# QA-Matrix
|
||||
|
||||
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.
|
||||
|
||||
## Abnahmepunkte aus dem Masterauftrag (§15)
|
||||
|
||||
| # | Anforderung | Stand | Beleg |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | Zwei Teams, sechs Eigentümerrollen, drei Lanes, 16 Türme, 2 Kerne | ✅ M1 | `test_lune_core` · structure |
|
||||
| 2 | Turmreihenfolge, Basisschutz, Kernsieg; gleichzeitige Kernzerstörung ohne Reihenfolgevorteil | ✅ M1 | `test_lune_core` · gates, victory_and_draw, mirror_fairness |
|
||||
| 3 | Fortsetzung nach Welle 20, hohe Indizes ohne Final-Wave-Logik, Fehler oder Objektwachstum | ✅ M1 (Wellen bis 10⁶, Runde 21; Menge ≤ 7 je Lane) | `test_lune_core` · endless_waves |
|
||||
| 4 | Gold exakt einmal, Besitzprüfung, keine Reset-/Despawn-/Beschwörungsfarmen | ✅ M1 (Monster, Gebäude, Truppentod ohne Kopfgeld); Beschwörungen gibt es noch nicht | `test_lune_core` · rewards, commands |
|
||||
| 5 | Heilung und Persistenz nach der Runde; neue Partie ohne übernommene Werte | ✅ M1 Truppen · ✅ M2 Helden (XP, Stufe, Talente, Items bleiben; Rückkehr geheilt, auch nach dem Tod; neue Partie ohne Held) | `test_lune_core` · persistence; `test_lune_m2` · xp_and_persistence |
|
||||
| 6 | Truppen ohne Heldeninventar; Talente, drei Itemplätze, Stapeln | ✅ M2 | `test_lune_m2` · heroes_and_items |
|
||||
| 7 | Verstärkungskauf n → Einsatz n+1, richtige Lane und richtiges Team | ✅ M2 (öffentliche Ankündigung, keine Verschiebung nach Übernahme, Verbrauch nach n+1, Kopfgeld, Belagerungsziel) | `test_lune_m2` · champions |
|
||||
| 8 | Grottenabzug schwächt die Lane für die ganze Kampfphase | ✅ M2 | `test_lune_m2` · grotto_withdrawal_and_contest |
|
||||
| 9 | Beide Expeditionen, Scheitern, Erfolg, genau eine Entsendung, Kopfgeld, Verfügbarkeit ab Runde 10 | ✅ M2 (Sieg in Runde 6 → Einsatz 7 → gesperrt 7–9 → frei ab 10; wiederholtes Signal ohne Wirkung) | `test_lune_m2` · boss_*; Szenarien `boss_scenarios` |
|
||||
| 10 | Laneverlust, Phasenende, Pause, Neustart, Rückkehr ins Menü | ✅ M1/M2 | `verify_m1`, `test_lune_core` · victory (timeout), `test_lune_m2` · lane_loss |
|
||||
| 11 | Bots ohne Sonderressourcen und ohne Sicht auf private Befehle | ✅ Bots nutzen dieselben Befehle ohne Sondergold; sie lesen nur den eigenen Entwurf, veröffentlichte Gebäudestände, den öffentlichen Bosszustand und Ankündigungen (Code-Durchsicht, `LuneBot`) | `test_lune_core` · commands; `test_lune_m2` · bots_m2 |
|
||||
| 12 | Netzwerk: doppelte, veraltete und fremde Befehle; synchroner Phasenwechsel, Disconnect | ✅ Protokoll + Mehr-Runden-Loopback (5 Runden, Wiedereinstieg) | `test_lune_net` 43/43 · `net_rounds.py` 12/12 |
|
||||
|
||||
## M3 · Qualität und Balance
|
||||
|
||||
| Nachweis | Stand | Beleg |
|
||||
| --- | --- | --- |
|
||||
| Mehrere Seeds und gemischte Teamzusammensetzungen, Seitentausch | ✅ 20 Zusammensetzungen × 2 Seiten, 20/20 konsistent | `balance_run` · `logs/m3/balance-step4.json` |
|
||||
| Gegenstrategien / Profil-Duelle | ✅ 15 Paare, 15/15 Seitenfair; lineare Kette (Befund) | `balance_run --matchups` · `matchups-step4.json` |
|
||||
| Hohe Wellenindizes und Langlauf | ✅ 10²–10⁶ ohne Fehler; 200 Runden = Testlimit, keine Objektflut | `stress_run` · `logs/m3/stress.json` |
|
||||
| Bot kauft Items und Angriffschampions | ✅ Regression `test_lune_m2` bots_m2 (war 0 Buchungen) | `test_lune_m2` 73/73 |
|
||||
| Spiegelsymmetrie mit erweiterten Bots | ✅ glutpakt/wurzelbund je 12 Runden bit-symmetrisch | `test_lune_core` mirror_fairness |
|
||||
| Karte mit echten Props (Bäume, Felsen, Brücken, Bordsteine, Banner) | 👁 Übersicht, Kampf Top/Mid, Planung | `screenshots/overview-*.png`, `combat-*.png` |
|
||||
| Sieben Volksrepräsentanten (Modelle + Clips) | 👁 Ruhe, Kontakt, Laufen, Fähigkeit, Tod, Nahaufnahme | `screenshots/peoples-*.png`, `people_lineup` |
|
||||
| Assetmatrix mit `p_*` und map_props | ✅ 74 Zeilen | `python3 tools/luneraria/build_asset_matrix.py` |
|
||||
|
||||
## Echte Anwendung (M1)
|
||||
|
||||
`tools/luneraria/verify_m1.tscn`, 38 Prüfungen mit echten Maus- und Tastaturereignissen:
|
||||
|
||||
- Titel und Projektname, Lane- und Bundwahl per Klick, Start.
|
||||
- Eingabeschutz während der Überblendung: Mausrad, 1 und Leertaste erreichen das Spiel nicht.
|
||||
- Kauf, Startaufstellung, Rückgängig, Laneeinteilung und die Leertaste übernehmen die Planung; im Kampf wird nichts gekauft.
|
||||
- Truppen kämpfen auf ihrer Lane; Pause hält die Kampfuhr an; Tempo schaltet um.
|
||||
- Abrechnung und Rückkehr der Truppen.
|
||||
- HUD innerhalb des Fensters bei 1280 × 720 und 1920 × 1080.
|
||||
- Menü, neue Partie ohne Altlasten, eine vollständige Partie bis zum Ancient-Fall mit Live-Aufnahme der Belagerung, Ergebnisbildschirm, Rematch.
|
||||
- Vier Menü-/Partiewechsel ohne wachsende Objektzahl.
|
||||
|
||||
Nur die Kampfuhr wird dabei beschleunigt (mehr feste Schritte pro Bild), die Regeln bleiben unverändert.
|
||||
|
||||
## Echte Anwendung (M2)
|
||||
|
||||
`tools/luneraria/verify_m2.tscn`, 15 Prüfungen. Teil 1 bedient das HUD mit echten Klicks: Reiter, Held anwerben, Held-Lane, Truppe kaufen und in die Grotte schicken, Verstärkungsseite, Rückgängig. Teil 2 spielt eine gewöhnliche Partie ohne Sondergold bis zum Ancient-Fall; der menschliche Platz wird vom Bot-Planer über dieselben Befehle gespielt. Aufgenommen: Held mit Items, Held im Feld, Grottenkampf, Ankündigung, Bossentsendung.
|
||||
|
||||
Weitere M2-Belege: `test_lune_m2` 73/73, darunter der Nachweis, dass die alte Arbeiter- und Funkenökonomie nicht mitläuft (Arbeitsannahme E). M2-App nach M3-Botfix erneut 15/15 (bzw. 14/14, wenn die Partie vor einer Bossentsendung endet).
|
||||
|
||||
## M4 · Netzwerk und Pakete
|
||||
|
||||
| Nachweis | Stand | Beleg |
|
||||
| --- | --- | --- |
|
||||
| Sechs Slots, Host/Join Direktverbindung, Bots für freie Plätze | ✅ | `LuneSession`, Menü „Hosten/Beitreten“ |
|
||||
| Fremder Sitz / fremde UID / veralteter Befehl / Doppelkauf | ✅ | `test_lune_net` 24/24 |
|
||||
| Disconnect ≠ Kernzerstörung, Bot-Übernahme | ✅ | `test_lune_net` disconnect |
|
||||
| Zwei-Prozess-Loopback (lokal, nicht Internet) | ✅ 5 Runden, echtes `LuneGame` auf beiden Seiten | `tools/luneraria/net_rounds.py` · `logs/m4/net-rounds.json` |
|
||||
| Gleicher öffentlicher Zustand nach jeder Abrechnung | ✅ 5/5 Prüfsummen gleich | `net-rounds.json` |
|
||||
| Client rechnet keinen Kampf, zeigt Host-Momentaufnahmen | ✅ 0 lokale Schritte · 👁 zwei Fenster | `net-rounds.json`, `screenshots/m4/` |
|
||||
| Nur eigener Planungsentwurf beim Client | ✅ | `test_lune_net` ready_commits_and_privacy |
|
||||
| Linux-Export + entpackbare Demo | ✅ 0.16.0-m4 | `../dist/luneraria/ChampionsOfLuneraria-Linux-x64-0.16.0-m4.zip` |
|
||||
| Windows-Export + entpackbare Demo | ✅ Dateien erzeugt | `../dist/luneraria/ChampionsOfLuneraria-Windows-x64-0.16.0-m4.zip` |
|
||||
| Nativer Windows-11-Lauf | ⏳ blockiert | keine Windows-Hardware; Export oder Wine wäre kein Nativtest |
|
||||
| Mehr-Runden-Phasensync im Zwei-Fenster-Spiel | ✅ headless 5 Runden · 👁 zwei Fenster, 2 Runden Echtzeit | `net-rounds.json`, `screenshots/m4/` |
|
||||
| Wiedereinstieg als Mensch (neben Bot-Übernahme) | ✅ derselbe Sitz über den Anzeigenamen, auch im Kampf | `test_lune_net` hello_and_rejoin · `net-rounds.json` |
|
||||
| Kompatibilitätsmodus (OpenGL) | ⏳ offen, M4 pausiert | — |
|
||||
| Sechs Menschen auf mehreren Rechnern / Internet | ⏳ nicht geprüft | nur lokaler Loopback |
|
||||
|
||||
## G1 · Karte und Layout
|
||||
|
||||
| Nachweis | Stand | Beleg |
|
||||
| --- | --- | --- |
|
||||
| Drei Lanes, Top/Bottom gleich lang, Mid direkt | ✅ | `verify_g1`, `test_lune_core` symmetry |
|
||||
| Lanes überall begehbar, Wasser nur an Plattform und Deltabrücke, Top trocken | ✅ | `verify_g1` |
|
||||
| 16 Türme und 2 Ancients im Feld, trocken, eben, mit Abstand; Basistürme vor dem Ancient | ✅ | `verify_g1` |
|
||||
| Grotte auf der Achse, frei von allen Lanekorridoren, blockiert Mid nicht, Eingänge auf der Terrasse | ✅ | `verify_g1` · 👁 `screenshots/g1/nachher/07-bossgrotte.png`, `13-grottenkampf.png` |
|
||||
| Aufmarschbereiche trocken und auf der eigenen Seite; in der Planung nur die eigenen, im Kampf ausgeblendet | ✅ · 👁 | `verify_g1`, `09-hud-planung.png` |
|
||||
| Gelände exakt gespiegelt, Figuren stehen auf dem Gelände | ✅ Abweichung 0,0 / < 0,001 m | `verify_g1` |
|
||||
| Kamera: F2–F4, B, G, H, O, R, Q/E, Zoomgrenzen, Neigung je Zoom, Minimap-Umriss | ✅ echte Tastatur- und Mausereignisse | `verify_g1` |
|
||||
| Regeln unverändert | ✅ 91/91 · 73/73 ohne geänderte Erwartungswerte | `test_lune_core`, `test_lune_m2` |
|
||||
| Balance auf der neuen Karte | ✅ Seitentausch 20/20, 9–13 Wellen; 4/442 Kampfphasen am Zeitlimit (Befund) | `logs/g1/balance.json`, `balance-summary.txt` |
|
||||
| Vorher/Nachher 1920 × 1080 aus der Spielkamera | 👁 13 Ansichten + Kartenplan | `screenshots/g1/vorher`, `screenshots/g1/nachher` |
|
||||
| Leistung vorher/nachher, gleiche Methode | ✅ | `logs/g1/perf-*-1920x1080.json` |
|
||||
| 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)
|
||||
|
||||
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`.
|
||||
|
||||
## Plattformen
|
||||
|
||||
| Plattform | Stand |
|
||||
| --- | --- |
|
||||
| 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) |
|
||||
| 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 |
|
||||
72
docs/luneraria/REGELWERK.md
Normal file
72
docs/luneraria/REGELWERK.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Champions of Luneraria · Regelwerk
|
||||
|
||||
Stand: 27. September 2026. Drei Ebenen, getrennt gehalten:
|
||||
|
||||
1. **Verbindliche Produktregeln** aus dem Masterauftrag. Änderungen nur per ausdrücklicher Entscheidung des Auftraggebers.
|
||||
2. **Arbeitsannahmen A–F** aus dem Masterauftrag, als anpassbare Designentscheidungen formuliert. Es sind keine Nutzerzitate.
|
||||
3. **Eigene Detailentscheidungen** dieser Umsetzung, anpassbar und mit Konfigurationsort.
|
||||
|
||||
## 1 · Verbindliche Produktregeln
|
||||
|
||||
- Titel: **Champions of Luneraria**.
|
||||
- Zwei Teams mit je drei eigenständigen Spielerrollen.
|
||||
- Drei zusammenhängende Lanes: Top, Mid, Bottom.
|
||||
- Kämpfe laufen automatisch. Es gibt weder direkte Heldensteuerung noch manuelles Last-Hitting.
|
||||
- Die Partie endet ausschließlich durch Zerstörung eines Ancient-Kerns. Es gibt keine maximale Rundenzahl, keinen Sieg nach einer Wellengrenze, keine Entscheidung nach Kern-HP und keinen Sieger per Zeitlimit.
|
||||
- Werden beide Kerne im selben Simulationsschritt zerstört, ist das ein Unentschieden, unabhängig von Array-, Render- oder Netzwerkreihenfolge. Ein manueller Testabbruch oder technischer Abbruch ist kein Kern-Sieg.
|
||||
- Monsterwellen werden mit jeder Welle stärker.
|
||||
- Je Team und Lane zwei Lanetürme; zusätzliche Türme schützen den Ancient.
|
||||
- Normale Truppen werden dauerhaft gekauft und aufgewertet; sie haben kein Levelsystem und keine Items.
|
||||
- Helden werden gekauft, sammeln in der Partie Erfahrung und entwickeln sich über Items und Talente.
|
||||
- Angriffschampions werden mit Gold als einmalige Verstärkung einer Angriffswelle gekauft.
|
||||
- Besiegte Monster und Angriffschampions geben Gold.
|
||||
- Eine gemeinsame Bossgrotte bindet Einheiten, die dafür ihre Lane verlassen.
|
||||
- Nach erfolgreicher Eroberung verstärkt der Boss einmalig den Angriff des **siegreichen** Teams in der folgenden Runde, gegen das **gegnerische** Team. Er läuft zum gegnerischen Ancient.
|
||||
- Der entsandte Boss gibt dem verteidigenden Team bei seinem Tod deutlich mehr Gold als ein normaler Gegner.
|
||||
- Nach einem Bosssieg ist die Grotte drei vollständige Runden gesperrt. Beispiel: Sieg in Runde 6, Einsatz in Runde 7, Sperre 7–9, wieder bekämpfbar ab 10.
|
||||
- Vorhandene Assets werden bevorzugt erhalten. Inhaltsziel: Elfen, Trolle, Zwerge, Menschen, Feen, Orks, Biestmenschen; bestehende Wesen und Bünde bleiben Teil der Welt.
|
||||
- Ziel ist taktische Wiederspielbarkeit.
|
||||
|
||||
## 2 · Arbeitsannahmen (Masterauftrag, anpassbar)
|
||||
|
||||
- **A** Dauerhafte Truppen und Helden rücken mit den eigenen Monsterwellen über ihre Lane vor und greifen danach zulässige gegnerische Gebäude an.
|
||||
- **B** Jeder Spieler hat eigenes Gold und eine eigene Armee. Seine Lane ist die anfängliche Zuständigkeit; Unterstützung anderer Lanes ist möglich. Das Team ist unabhängig von Volk und Bund.
|
||||
- **C** Ein aktiver Held pro Spieler mit drei Itemplätzen; mehrere Helden zur Wahl. Normale Truppen bleiben relevant.
|
||||
- **D** Genau zwei gemeinsame Basistürme pro Team: acht Türme je Team, sechzehn auf der Karte, plus zwei Ancients.
|
||||
- **E** Die alte Arbeiter- und Funkenökonomie läuft im neuen Modus nicht. Gold finanziert Armee, Aufwertungen, Helden, Items und Angriffschampions.
|
||||
- **F** Erster Modus: echte Sechs-Spieler-Simulation mit einem Menschen und fünf Bots. Netzwerk folgt als eigener Meilenstein.
|
||||
|
||||
## 3 · Detailentscheidungen dieser Umsetzung
|
||||
|
||||
Alle Zahlen stehen zentral in `data/luneraria/balance_default.tres` (Klasse `LuneBalance`).
|
||||
|
||||
| Thema | Entscheidung |
|
||||
| --- | --- |
|
||||
| Runde | Planung → Befehle werden atomar übernommen → automatischer Kampf → Abrechnung → nächste Planung. Die Wellennummer steigt unbegrenzt. |
|
||||
| Simulation | Feste Schrittweite 1/20 s. Jeder Schritt läuft in zwei Phasen: erst werden alle Treffer, Heilungen und Effekte aus dem Zustand zu Schrittbeginn berechnet, dann gleichzeitig angewendet. Todesfälle eines Schritts gelten gemeinsam. So entsteht kein Vorteil aus der Verarbeitungsreihenfolge. |
|
||||
| Aufmarsch | Armeen und Wellen starten jede Runde an festen, symmetrischen Aufmarschflächen ihrer Lane. Formationsplätze sind Offsets vor/quer zur Laufrichtung. Keine Front-Teleports. |
|
||||
| Vormarsch | Ohne Ziel folgen Einheiten ihrer Lane. Sie kommen nicht an einem lebenden gegnerischen Turm ihrer Lane vorbei. Am Laneende greifen sie das nächste zulässige Basisgebäude an. |
|
||||
| Gebäudesperren | Der innere Turm ist erst nach dem äußeren verwundbar. Die Basistürme sind verwundbar, sobald eine Lane des Teams ganz durchbrochen ist. Der Ancient ist erst verwundbar, wenn beide Basistürme gefallen sind. Die Sperre gilt für jeden Schaden (Fernkampf, Fläche, Fähigkeiten, Boss). Gesperrte Gebäude schießen weiter. |
|
||||
| Kampfende | Eine Kampfphase endet, wenn keine beweglichen Einheiten mehr leben, wenn ein Kern fällt oder wenn das Zeitfenster abläuft (Standard 180 s). Das Zeitfenster beendet nur die Kampfphase, nie die Partie. Übrige temporäre Einheiten verschwinden ohne Belohnung, dauerhafte kehren zurück. Es gibt keinen Kernschaden durch den Abbruch. |
|
||||
| Rückkehr | Dauerhafte Truppen und Helden kehren nach jeder Kampfphase vollständig geheilt zurück, auch wenn sie im Kampf gefallen sind. Schilde, Brand, Verlangsamungen, Stärkungen und Abklingzeiten werden auf Startwerte zurückgesetzt. Aufwertungen, Erfahrung, Items und Talente bleiben. |
|
||||
| Gebäude | Türme und Kerne behalten Schaden; zerstörte Gebäude kehren nicht zurück. Gebäude skalieren nicht mit der Welle. |
|
||||
| Belohnung | Pro gegnerischem Todesereignis genau eine Belohnung, egal wer tötet. Lane-Killgold geht zur Hälfte an den zuständigen Lane-Spieler des tötenden Teams und zur Hälfte gleichmäßig an alle drei. Der Rest der Ganzzahlteilung geht in stabiler Slotreihenfolge. Gebäude- und Boss-Kopfgelder werden gleichmäßig im Team verteilt. Dauerhafte Truppen und Helden geben kein Kopfgeld. |
|
||||
| Laneverlust | Kein Spieler scheidet aus. Er baut weiter auf, verteidigt und unterstützt. |
|
||||
| Pause | Offline darf die Planung auf den Menschen warten; Pause und Tempo 1×/2×/4× gelten nur lokal. |
|
||||
| QA-Grenze | Testläufe dürfen eine Rundenobergrenze haben. Eine dort nicht beendete Partie wird als „Testlimit erreicht / Partie nicht abgeschlossen“ protokolliert, nie als Sieg oder Unentschieden. |
|
||||
|
||||
## 4 · Detailentscheidungen M2 (anpassbar, Werte in `scripts/luneraria/core/lune_content.gd`)
|
||||
|
||||
| Thema | Entscheidung |
|
||||
| --- | --- |
|
||||
| Helden | Drei Helden aus vorhandenen Skelettmodellen, als **vorläufige Rollenzuordnung**: Ringborke (Front/Schutz), Quellgeflecht (Heilung), Funkenlanze (Fernkampf). Jeder Spieler kann einen aktiven Helden anwerben, unabhängig vom Bund, für 260 Gold. Damit ist ein Held frühestens ab Runde 2 erschwinglich. Ein Held aus derselben Planung lässt sich voll erstatten, danach bleibt er bis Partieende. |
|
||||
| Erfahrung | Nicht an den letzten Treffer gebunden. Jeder Held, der an einer Kampfphase teilnimmt, erhält 40 + 6 × Runde. Jeder gegnerische Tod im Umkreis von 12 m (Gebäude 14 m) wird zu gleichen Teilen auf alle Helden in der Nähe verteilt: Monster 12 + Runde, Champion 30 + 2 × Runde, Boss 220, Truppe/Held 15, Gebäude 60. So erhalten auch Heiler in der zweiten Reihe Erfahrung. Stufen 1–10, je Stufe +9 % Leben und +7 % Schaden. |
|
||||
| Talente | Auf Stufe 3 und 6 je eine endgültige Wahl zwischen zwei Richtungen: Eigen- gegen Gruppenschutz oder Einzelziel gegen Fläche, je Held eigene Optionen. |
|
||||
| Items | Sechs dauerhafte Items, drei Plätze. Jedes Item höchstens einmal pro Held. Kristallsplitter setzt Heldenstufe 3 voraus. Verkauf bringt 50 %, ein Kauf aus derselben Planung wird voll erstattet. Boni verschiedener Items addieren sich; die Schadensminderung ist auf 35 %, das Angriffstempo auf +50 % begrenzt. |
|
||||
| Reihenfolge im Kampf | Primärtreffer → Grundfähigkeit → Talent → Item-Effekt. Item- und Talenteffekte lösen keine weiteren Treffer-Effekte aus, Heilungen lösen nichts aus; dadurch sind Schleifen ausgeschlossen. Normale Truppen haben kein Inventar. |
|
||||
| Angriffschampions | Rostschieber (Schutz, 150 Gold), Dunstsänger (Druck auf Unterstützer, 150), Keilbrecher (Belagerung, ×1,8 gegen Gebäude, 180). Ein Kauf in Runde n wird für Runde n+1 auf eine Lane gebucht, höchstens zwei Buchungen pro Runde. Bis zum Ende der Planung lässt sich eine Buchung voll erstatten, danach ist sie weder verschiebbar noch erstattbar. Buchungen werden zu Beginn von n+1 öffentlich angekündigt. Der Champion kämpft einmal und verschwindet. Die Stärke skaliert mit der Einsatzrunde, das Kopfgeld beträgt 70 + 6 × Runde und wird wie Monstergold verteilt. |
|
||||
| Grotte · Zuweisung | Die Zuweisung „G“ gilt für genau eine Runde. Die Einheiten fehlen ihrer Lane die ganze Kampfphase, starten an festen, gespiegelten Grotteneingängen und kehren bei der Abrechnung auf die Lane ihres Besitzers zurück. In Sperrrunden ist die Zuweisung abgelehnt. |
|
||||
| Grotte · Kampf | Der neutrale Boss ruht, solange Expeditionen beider Teams leben; die Expeditionen kämpfen zuerst gegeneinander. Erst wenn höchstens ein Team übrig ist, erwacht der Boss mit vollen Lebenspunkten. Tod aller Expeditionen oder Ablauf des Zeitfensters: kein Anspruch. Der nächste Versuch trifft einen vollständig neuen Boss. Todesfälle in der Grotte bringen kein Gold. |
|
||||
| Anspruch | Genau ein Anspruch pro Sieg; ein wiederholtes Abrechnungssignal ändert nichts. Ziel-Lane: die Lane des beitragenden Spielers mit dem höchsten eingesetzten Armeewert in der Grotte (Held einschließlich Items); bei Gleichstand der niedrigere Slot. Dieser Spieler kann die Lane in der folgenden Planung ändern; ohne Eingabe gilt seine eigene Lane. |
|
||||
| Entsendung | Automatisch in der folgenden Runde, für das siegreiche Team, gegen den gegnerischen Ancient. Gebäudesperren gelten unverändert. Stärke und Kopfgeld (520 + 60 × Runde) richten sich nach der Einsatzrunde. Das Kopfgeld geht bei seinem Tod gleichmäßig an das verteidigende Team. Überlebt er, verschwindet er ohne Belohnung. |
|
||||
| Sperre | `next_available_round = Siegrunde + 4`. Beispiel: Sieg in Runde 6, Einsatz in Runde 7, gesperrt in 7–9, bekämpfbar ab Runde 10. Die Sperre hängt am Grottensieg, nicht am Tod des entsandten Bosses. |
|
||||
204
docs/luneraria/STATUS.md
Normal file
204
docs/luneraria/STATUS.md
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
# STATUS · Champions of Luneraria
|
||||
|
||||
Stand: 29. September 2026 · Branches `luneraria/m1` (M0/M1), `luneraria/m2`, `luneraria/m3`, `luneraria/m4`, `luneraria/g1`, `luneraria/g2`, `luneraria/g3` (Zwischenstand) · Luneraria-Demo 0.17.0-g2 (macOS, Windows x64/ARM64, Linux).
|
||||
|
||||
**Engine-Wechsel (29. September 2026):** Der Projektinhaber hat entschieden, Champions of Luneraria nach Unity 6 zu portieren, um eine höhere Grafikqualität zu erreichen. Das Unity-Projekt liegt in `../luneraria-unity`. Dieses Godot-Projekt bleibt als Referenz für Regeln, Tests, Assets und HUD-Design erhalten; neue Arbeit findet im Unity-Projekt statt.
|
||||
|
||||
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 |
|
||||
| --- | --- |
|
||||
| M0 · Migration vorbereiten | **abgeschlossen** |
|
||||
| M1 · Spielbarer Drei-Lane-Kern | **abgeschlossen** |
|
||||
| M2 · Mechanikdurchlauf | **abgeschlossen** |
|
||||
| M3 · Qualität und Balance | **abgeschlossen** (siehe unten) |
|
||||
| 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)) |
|
||||
| G2 · Ancients, Türme und Bossgrotte | **abgeschlossen** ([G2-STRATEGISCHE-BAUWERKE.md](G2-STRATEGISCHE-BAUWERKE.md), [G2-ASSETS.md](G2-ASSETS.md)) |
|
||||
| G3 · Ingame-HUD | **Zwischenstand, nicht abgenommen** ([G3-HUD.md](G3-HUD.md)); abgebrochen zugunsten des Unity-Ports |
|
||||
| G4–G6 · Umgebung, Licht, Einheiten | in Godot nicht mehr geplant; werden nach dem Port in Unity fortgesetzt |
|
||||
|
||||
## M1 · abgeschlossen
|
||||
|
||||
**Implementiert**
|
||||
- Autoritative Partie mit sechs Spielerrollen (ein Mensch, fünf Bots), drei spiegelsymmetrischen Lanes, 16 Türmen und zwei Ancients.
|
||||
- Gebäudesperren für jede Schadensquelle.
|
||||
- Vorrückende Armeen mit eigenen, unbegrenzt wachsenden Monsterwellen.
|
||||
- Planung mit atomarer Übernahme, Besitz-IDs, Einmal-Belohnung mit verlustfreier Restverteilung, Rückkehr und Heilung der Truppen, persistenter Gebäudeschaden.
|
||||
- Partieende nur durch Kernfall; bei gleichzeitigem Kernfall Unentschieden.
|
||||
- Bots über dieselbe Befehlsschnittstelle.
|
||||
- 3D-Karte aus Bestandsassets plus neuem Turm (Blender), Einheiten mit den vorhandenen Modellen und Animationen.
|
||||
- HUD mit Planung, Laneübersicht, Minimap, Ergebnis; Titelmenü auf Champions of Luneraria umgestellt.
|
||||
- Übernahme der alten Benutzereinstellungen. Legacy-Modus bleibt unter `--legacy` und den alten QA-Schaltern erreichbar.
|
||||
|
||||
**Automatisiert geprüft**
|
||||
- Regeln 91/91, darunter der exakte Spiegeltest über 12 Runden.
|
||||
- Echte Anwendung 38/38, mit einer vollständigen Partie bis zum Ancient-Fall.
|
||||
- Acht Bot-Seeds enden regulär nach 8–15 Wellen.
|
||||
- Legacy 209 · 33 · 463 · 206 · 1.665 · 68 unverändert.
|
||||
|
||||
**Visuell geprüft:** Menü, Planung (720p/1080p), Kampf Mid/Top, Übersicht, Belagerung eines Ancients, Ergebnis (`screenshots/`).
|
||||
|
||||
**Leistung** (RTX 5060 Ti, Ryzen 5 5600X, Forward+, VSync aus, 2×): 114 bewegliche Einheiten und 18 Gebäude. 1080p: 7,6 ms/Bild (p95 10,5 ms), GPU 3,1 ms, ~3.170 Draw Calls, Simulationsschritt 2,4 ms (vor der Optimierung 15,8 ms). Es gibt keine allgemeine FPS-Zusage.
|
||||
|
||||
**Ungetestet:** Windows 11, Kompatibilitätsmodus für Luneraria, menschliche Spieltests.
|
||||
|
||||
## M2 · abgeschlossen
|
||||
|
||||
**Implementiert**
|
||||
- Drei Helden (vorläufige Rollen der vorhandenen Skelettmodelle Ringborke, Quellgeflecht, Funkenlanze) mit Erfahrung aus Gefechtsnähe und Rundenteilnahme, Stufen, je zwei Talentstufen mit echten Aufgabenwechseln und drei Itemplätzen für sechs Items.
|
||||
- Drei Angriffschampions (Schutz, Druck auf Unterstützer, Belagerung) mit Buchung n → n+1 und öffentlicher Ankündigung.
|
||||
- Bossgrotte mit ruhendem Boss bei zwei Expeditionen, Scheitern, Eroberung, genau einer Entsendung für das siegreiche Team gegen den gegnerischen Ancient, hohem Kopfgeld für die Verteidiger und Sperre ab `Sieg + 4`.
|
||||
- Wahl der Ziel-Lane durch den beitragenden Spieler; Grottenaufträge gelten für genau eine Runde.
|
||||
- HUD mit Reitern Truppen / Held / Verstärkung, Heldenporträt aus dem echten Modell, Grottenstatus, Ankündigungen und Minimap-Grotte.
|
||||
- Neues Grotten-Asset (Blender). Bots mit Helden, Items, Talenten, Champions und Bossjagd.
|
||||
|
||||
**Automatisiert geprüft**
|
||||
- Regeln M2 73/73 und M1 91/91 (mit Spiegeltest).
|
||||
- Echte Anwendung M2 15/15 und M1 38/38.
|
||||
- 12 Bot-Seeds enden regulär nach 7–10 Wellen.
|
||||
- Szenariovergleich der Bossstrategie (2 Seeds × 5 Varianten).
|
||||
|
||||
**Visuell geprüft:** Held mit Items im HUD, Held im Feld, Grottenkampf, Ankündigung, Bossentsendung (`screenshots/m2-*`).
|
||||
|
||||
**Ungetestet:** menschliche Spieltests der neuen Systeme, Windows 11.
|
||||
|
||||
## M3 · abgeschlossen
|
||||
|
||||
**Implementiert**
|
||||
- Bot-Priorisierung: Items und Angriffschampions werden **vor** der Armee geplant (Reserve), damit Helden Ausrüstung erhalten und Champions überhaupt gebucht werden. Regression `bots_m2` gefixt.
|
||||
- Deterministische Lane-Hilfe der Bots statt geteiltem Zufall — erhält die exakte Spiegelsymmetrie.
|
||||
- Bossbalance: Grottenboss 3.600 HP, entsandter Boss 6.800 HP / 88 Schaden, Belagerungsfaktor 1,8. Bossjäger senden 5 Truppen + Held und bewerten Expeditionen per öffentlicher Lanchester-Schätzung.
|
||||
- Profilbalance: verteidigung erhält Champion und Teamhilfe; ausgewogen startet den Held eine Runde später; teamhilfe hilft seltener.
|
||||
- 3D-Kartenvisuals: `map_props.glb` (Blender-Generator) mit Kiefern, Eichen, Felsen, Büschen, Brücken über die Kluft, Bordsteinen, Klamprahmen und Team-Bannern an den Ancients. Alte Compare-Props abgelöst.
|
||||
- Sieben Volksrepräsentanten (`p_mensch` … `p_biest`) mit gewichtetem Skelett, 9 Clips, LP_*-Materialien, Audit und Sichtprüfung. Entwicklungsplan in [VOELKER.md](VOELKER.md). Noch nicht spielbar im Match.
|
||||
- Balancewerkzeuge: `balance_run` (Zusammensetzungen, Seitentausch, reine Duelle), `analyze_balance.py`, `stress_run`, `people_lineup`.
|
||||
|
||||
**Automatisiert geprüft**
|
||||
- Regeln: Core 91/91 (inkl. Spiegel) und M2 73/73 nach den Bot-Änderungen.
|
||||
- Echte Anwendung: M1 38/38 und M2 15/15 (nach Fix der Item-Ausrüstung).
|
||||
- Balance 40 Partien: Seitentausch 20/20, Profile 24–64 %, Bünde 55/45, alle regulär nach 9–14 Wellen.
|
||||
- Profil-Duelle 30 Partien: Seitentausch 15/15.
|
||||
- Stress 17/17: Wellen bis 10⁶, Langlauf 200 Runden als „Testlimit erreicht“.
|
||||
- Legacy unverändert.
|
||||
|
||||
**Visuell geprüft:** Kartenübersicht mit Props und Brücken, Kampf Top/Mid, Planung mit Helden-Items (Funkenlanze Stufe 3, Glutklinge), sieben Völker in Ruhe/Kontakt/Laufen/Fähigkeit/Tod und Nahaufnahme (`screenshots/peoples-*`).
|
||||
|
||||
**Bekannte Befunde**
|
||||
- Reine 3-gleichen Profil-Duelle ergeben eine lineare Kette (ausgewogen > verteidigung > teamhilfe > fernkampf > druck > bossjagd), keinen Zyklus. In gemischten Teams sind alle Profile im Band 24–64 %.
|
||||
- Bossjagd als reines Teamprofil 0/10, in Mischteams 24 %. Die Risikoabschätzung ist besser geworden, die Jagd bleibt teuer.
|
||||
|
||||
## Bekannt
|
||||
|
||||
- Ein exakt gespiegeltes Match bleibt unentschieden, wird also nicht beendet (Testlimit). Das ist der erwartbare Grenzfall identischen Spiels; Asymmetrie kommt aus Helden, Champions und Boss.
|
||||
- Die Monsterwellen sind nur über Teamring und Lichtfarbe dem Team zugeordnet.
|
||||
- Beim Beenden meldet Godot eine noch belegte Ressource (Hintergrundladen der Menübühne); ohne Folgen im Spiel.
|
||||
- Die sieben Volksrepräsentanten sind noch nicht spielbar (Daten und Modelle liegen vor, Plan in VOELKER.md).
|
||||
|
||||
## Blockiert
|
||||
|
||||
- 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.
|
||||
- Notarisierung des macOS-Pakets: kein Apple-Entwicklerkonto; die App ist ad-hoc signiert.
|
||||
|
||||
## M4 · pausiert
|
||||
|
||||
Auf Entscheidung des Auftraggebers ruht M4 seit dem 28. September 2026 zugunsten der Grafik- und Kartenphasen G1–G6. Während dieser Phasen gibt es keine Netzwerk-Featurearbeit; die Netzwerktests laufen nur als Regression mit.
|
||||
|
||||
**Implementiert**
|
||||
- `LuneSession` (autoritativ): Der Host besitzt `LuneMatch` und rechnet jeden Kampf. Clients senden nur Absichten (Kauf, Aufwertung, Zuweisung, Rückgängig, Bereit) und zeigen den Kampf aus Host-Momentaufnahmen (20/s, interpoliert). Ein Client rechnet keinen einzigen Kampfschritt.
|
||||
- Das echte Spiel (`LuneGame`) läuft vollständig über die Sitzung: Bereitschaft, Startaufstellung und Rückgängig gehen an den Host; Bots planen nur freie Sitze und höchstens einmal pro Runde; Tempo und Pause bestimmt der Host.
|
||||
- „Bereit“ ist verbindlich: Danach nimmt der Host für diesen Sitz keine Planungsbefehle mehr an.
|
||||
- Privatsphäre: Jeder Sitz erhält nur den eigenen Planungsentwurf, von allen anderen den übernommenen Stand.
|
||||
- Verbindungsaufbau mit Protokollprüfung (`PROTOCOL` 4). Sitzplätze an Peer-IDs, ein Peer ein Sitz.
|
||||
- Disconnect → Bot-Übernahme, nie Kernverlust, keine Hostmigration. **Wiedereinstieg:** Wer sich mit demselben Anzeigenamen erneut verbindet, erhält seinen alten Sitz zurück, auch mitten im Kampf (vollständiger Abgleich inkl. laufender Einheiten). Neue Gäste nehmen reservierte Sitze nur als letzte Möglichkeit.
|
||||
- Übernahme eines Bot-Sitzes in der Planung: Der noch nicht übernommene Bot-Entwurf verfällt, das Gold gehört dem Menschen.
|
||||
- Revanche startet der Host; Gäste starten automatisch neu. Verbindungsverlust führt den Gast mit Meldung ins Menü.
|
||||
- Host/Join im Setup-Menü (Direktverbindung, Standardport 24571), freie Plätze als Bots.
|
||||
- Exporte `Luneraria-Linux` und `Luneraria-Windows`, entpackbare Demos 0.16.0-m4 unter `../dist/luneraria/` (Stand vor dem Sitzungsumbau; nicht neu gebaut).
|
||||
|
||||
**Automatisiert geprüft**
|
||||
- Protokoll `test_lune_net` 43/43: fremder Sitz, fremde UID, veralteter Befehl, Doppelkauf, Disconnect ohne Kernschaden, Protokollversion, Wiedereinstieg auf denselben Sitz, Reservierung gegen neue Gäste, Übernahme mit verfallendem Bot-Entwurf, Bot plant einmal pro Runde, Bereit ist verbindlich, nur eigener Entwurf im Zustand.
|
||||
- Mehr-Runden-Loopback mit drei Prozessen (`python3 tools/luneraria/net_rounds.py`) 12/12: Host und Gast spielen fünf Runden mit dem echten `LuneGame`; nach jeder Abrechnung ist die Prüfsumme des öffentlichen Zustands gleich; der Gast rechnet 0 Kampfschritte und erhält Momentaufnahmen; Gast A trennt im Kampf der Runde 2, der Bot übernimmt, Gast B kehrt mit demselben Namen auf Slot 1 zurück (`logs/m4/net-rounds.json`).
|
||||
- Regressionen nach dem Umbau: Regeln 91/91, M2 73/73, echte Anwendung M1 38/38 und M2 14/14 (Partie endete vor einer Bossentsendung).
|
||||
|
||||
**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)**
|
||||
- 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. *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. *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).
|
||||
- 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.
|
||||
|
||||
## G1 · abgeschlossen
|
||||
|
||||
Details, Vorher/Nachher-Bilder und Messwerte: [G1-KARTE.md](G1-KARTE.md).
|
||||
|
||||
**Implementiert**
|
||||
- Neues Drei-Lane-Layout aus geglätteten, gespiegelten Lanes:
|
||||
- Top über das Hochland im Nordwesten;
|
||||
- Mid als breite, direkte Route über die Mittelplattform mit Runensteinen;
|
||||
- Bottom durch das Tiefland über die Deltabrücke.
|
||||
- Top und Bottom exakt gleich lang (96,82 m), Mid 61,16 m.
|
||||
- Höhenfeld `LuneTerrain`: Hochland, Tal, Tiefland, Kluft auf der Symmetrieachse, Basisplateaus, Grottenterrasse, Randgebirge; exakt gespiegelt. Figuren stehen auf dem Gelände.
|
||||
- Basen mit Plateau, Ringmauer und drei Lane-Toren. 16 Türme (12 Lane-, 4 Basistürme) und 2 Ancients auf ebenen Sockeln.
|
||||
- Bossgrotte (Blockout) zwischen Top und Mid auf der Achse: Terrasse, Höhlenmund, Säulen, zwei Teamtreppen, Quelle der Kluft. Sie blockiert Mid nicht.
|
||||
- Aufmarschbereiche je Team und Lane mit Reihenlinien, in der Planung sichtbar und hervorgehoben, wo eigene Einheiten stehen.
|
||||
- Perspektivische MOBA-Kamera mit zoomabhängiger Neigung und Sprungtasten F2–F4, B, G, H, O, R, Q/E. Minimap mit Kamera-Umriss und Kluft.
|
||||
- Werkzeuge: `g_views` (Vergleichsaufnahmen), `verify_g1` (Abnahme), erweiterter Benchmark (Lane-Ansicht und Übersicht).
|
||||
|
||||
**Automatisiert geprüft**
|
||||
- `verify_g1` 443/443. Regeln 91/91 (inkl. Spiegeltest) und M2 73/73, jeweils ohne geänderte Erwartungswerte.
|
||||
- Echte Anwendung: M1 38/38 (1080p; bei 720p zwei zeitweilige Klickfehler in 7 Läufen), M2 14/14.
|
||||
- Netzwerk-Regression 43/43 und Loopback 12/12.
|
||||
- Balance 40 Partien: Seitentausch 20/20, alle regulär nach 9–13 Wellen.
|
||||
- Legacy 209 · 463.
|
||||
|
||||
**Visuell geprüft:** 13 Vergleichsansichten vorher/nachher aus der Spielkamera (1920 × 1080) und der Kartenplan.
|
||||
|
||||
**Befunde:** 4 von 442 Kampfphasen erreichen das Zeitlimit (M3: 0; fernkampflastige Teams, Runde 12). Wege 2,5–3 % länger, Partielänge unverändert (Median 11 Wellen). Leistung: Lane-Ansicht 6,95 ms statt 6,16 ms pro Bild, die ganze Karte in der Übersicht mit 6,97 ms.
|
||||
|
||||
**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
|
||||
|
||||
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.
|
||||
40
docs/luneraria/VOELKER.md
Normal file
40
docs/luneraria/VOELKER.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Völker und Repräsentanten
|
||||
|
||||
Stand: 28. September 2026 (M3). Trennung der Daten: **Volk** · **Bund** · **Klasse** · **Kampfrolle**. Ein Volk ist kein eigener Kampfmechanismus; Rollen kommen aus dem gemeinsamen Truppen-/Heldenkatalog.
|
||||
|
||||
## Repräsentanten M3
|
||||
|
||||
Jedes der sieben Zielvölker besitzt genau ein eigenständiges Blender-Modell mit gewichtetem Skelett (22–24 Knochen, 9 Clips: idle, start, walk, stop, attack_a, attack_b, hit, ability, death). Erzeugt mit `assets-source/luneraria/build_peoples.py`, exportiert nach `assets/luneraria/peoples/p_*.glb`, Manifest `assets/luneraria/peoples_manifest.json`, Prüfbefund `assets-source/luneraria/peoples-audit.json`.
|
||||
|
||||
| Volk | ID | Name | Klasse | Kampfrolle | Besonderheit |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Menschen | `p_mensch` | Ritter | Ritter | Front | Schild + Schwert, Plattenrüstung |
|
||||
| Elfen | `p_elf` | Waldläuferin | Waldläuferin | Fernkampf | Bogen, leichte Rüstung, spitze Ohren |
|
||||
| Zwerge | `p_zwerg` | Schildträger | Schildträger | Front | Vollbart, Hornhelm, Rundschild |
|
||||
| Trolle | `p_troll` | Steinhauer | Steinhauer | Fläche | massive Keule, Felsplatten |
|
||||
| Feen | `p_fee` | Lichtheilerin | Lichtheilerin | Unterstützung | Flügel, Glüheffekte |
|
||||
| Orks | `p_ork` | Kriegshäuptling | Kriegshäuptling | Nahkampf | Krallenwaffe, Fell |
|
||||
| Biestmenschen | `p_biest` | Klauenjäger | Klauenjäger | Nahkampf | Bestienanatomie, Klauen |
|
||||
|
||||
Sichtprüfung: `screenshots/peoples-ruhe.png`, `-kontakt`, `-laufen`, `-faehigkeit`, `-tod`, `-detail-a/b`. Werkzeug: `tools/luneraria/people_lineup.tscn`.
|
||||
|
||||
## Was noch nicht implementiert ist
|
||||
|
||||
- Keine der sieben Figuren ist derzeit **spielbar**. Die laufenden Bünde Glutpakt und Wurzelbund behalten ihre vorhandenen Modelle (Glutkonstrukte, Wurzelwesen).
|
||||
- Es gibt **keine** Volkswahl im Menü und **keine** volksspezifischen Boni oder Fähigkeiten.
|
||||
- Die Repräsentanten sind Bestandteil der Assetmatrix und der Entwicklungsplanung, nicht des Matchrosters.
|
||||
|
||||
## Entwicklungsplan (nach M4)
|
||||
|
||||
1. **Volksdaten in den Katalog:** `people`, `bund`, `klasse`, `rolle` als getrennte Felder in `DataCatalog` / `LuneContent`. Einheitentypen verweisen auf ein Volk, ein Bund bleibt eine spielbare Fraktion mit eigener Aufwertungslinie.
|
||||
2. **Spielbare Vertretung pro Volk:** je ein normaler Truppentyp und ein Aufwertungsschritt aus den Repräsentanten-Animationen, übernommen in die bestehenden Rollen Front / Nahkampf / Fernkampf / Fläche / Unterstützung. Kein eigenes Kampfsystem pro Volk.
|
||||
3. **Bündekopplung:** Jeder Bund öffnet eine Teilmenge der Völker (z. B. Wurzelbund: Elfen, Feen, Biestmenschen; Glutpakt: Menschen, Orks, Zwerge, Trolle). Volk formt Startboni und Heldenauswahl, nicht die Grundregeln.
|
||||
4. **Helden pro Volk:** je ein Held aus dem Repräsentantenmodell mit zwei Talentrichtungen, analog zu Ringborke / Quellgeflecht / Funkenlanze. Vorläufige Bestandshelden bleiben erhalten.
|
||||
5. **Balancierung über Rollen, nicht Sammelboni:** Volkssynergien sind höchstens eine kleine, sichtbare Verstärkung (z. B. Formationsschild), nie der einzige Hebel.
|
||||
6. **Inhaltliche Ausweitung:** weitere Klassen pro Volk (Aufwertungen, Champion-Varianten) erst nach stabilen sechs Rollen und laufendem Netzwerkmodus.
|
||||
|
||||
## Bestandsschutz
|
||||
|
||||
- Ausgearbeitete Modelle (Ringborke, Funkenleser, Basaltbastion, Quellgeflecht, Funkenlanze, Glasballiste, Kernkristalle) bleiben unverändert erhalten.
|
||||
- Nietenkonvent (`m*`) und Nebelchor (`n*`) bleiben als Reserve in der Assetmatrix und werden nicht als Völker ausgegeben.
|
||||
- Die LP_*-Materialien der Völkermodelle laufen über denselben Kluft-/Sanctuary-Pfad wie die übrigen Figuren (`asset_library.gd`).
|
||||
4
docs/luneraria/logs/core-tests.json
Normal file
4
docs/luneraria/logs/core-tests.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"checks": 91,
|
||||
"failures": []
|
||||
}
|
||||
6
docs/luneraria/logs/g1/balance-summary.txt
Normal file
6
docs/luneraria/logs/g1/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/g1/balance.json
Normal file
1756
docs/luneraria/logs/g1/balance.json
Normal file
File diff suppressed because it is too large
Load diff
26
docs/luneraria/logs/g1/balance.log
Normal file
26
docs/luneraria/logs/g1/balance.log
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
Godot Engine v4.7.2.stable.fedora.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)
|
||||
39
docs/luneraria/logs/g1/perf-nachher-1920x1080.json
Normal file
39
docs/luneraria/logs/g1/perf-nachher-1920x1080.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"buildings": 18,
|
||||
"cpu": "AMD Ryzen 5 5600X 6-Core Processor",
|
||||
"engine": "4.7.2-stable (fedora)",
|
||||
"gpu": "NVIDIA GeForce RTX 5060 Ti",
|
||||
"lane_view": {
|
||||
"camera_zoom": 34.0,
|
||||
"cpu_frame_ms_mean": 6.82015972222222,
|
||||
"draw_calls_mean": 3340.02083333333,
|
||||
"frame_ms_mean": 6.94640740740741,
|
||||
"frame_ms_p95": 9.656,
|
||||
"frames": 864,
|
||||
"gpu_ms_mean": 3.29306944790814,
|
||||
"mobile_units_mean": 117.75462962963,
|
||||
"objects_mean": 7437.95949074074,
|
||||
"primitives_mean": 16302012.3611111
|
||||
},
|
||||
"mobile_units_at_start": 120,
|
||||
"overview": {
|
||||
"camera_zoom": 92.0,
|
||||
"cpu_frame_ms_mean": 6.84469489559164,
|
||||
"draw_calls_mean": 3604.76450116009,
|
||||
"frame_ms_mean": 6.9669071925754,
|
||||
"frame_ms_p95": 9.283,
|
||||
"frames": 862,
|
||||
"gpu_ms_mean": 3.51868097826389,
|
||||
"mobile_units_mean": 96.8259860788863,
|
||||
"objects_mean": 8162.20765661253,
|
||||
"primitives_mean": 16845863.4895592
|
||||
},
|
||||
"renderer": "forward_plus",
|
||||
"scene_build_ms": 293.127,
|
||||
"sim_speed": "2x (40 steps/s)",
|
||||
"sim_step_ms": 2.71302,
|
||||
"static_mem_mb": 128.053088188171,
|
||||
"texture_mem_mb": 424.65576171875,
|
||||
"video_mem_mb": 782.140380859375,
|
||||
"window": "(1920, 1080)"
|
||||
}
|
||||
39
docs/luneraria/logs/g1/perf-vorher-1920x1080.json
Normal file
39
docs/luneraria/logs/g1/perf-vorher-1920x1080.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"buildings": 18,
|
||||
"cpu": "AMD Ryzen 5 5600X 6-Core Processor",
|
||||
"engine": "4.7.2-stable (fedora)",
|
||||
"gpu": "NVIDIA GeForce RTX 5060 Ti",
|
||||
"lane_view": {
|
||||
"camera_zoom": 34.0,
|
||||
"cpu_frame_ms_mean": 6.03641128205129,
|
||||
"draw_calls_mean": 3608.65641025641,
|
||||
"frame_ms_mean": 6.15782974358974,
|
||||
"frame_ms_p95": 8.584,
|
||||
"frames": 975,
|
||||
"gpu_ms_mean": 3.16637640928611,
|
||||
"mobile_units_mean": 117.465641025641,
|
||||
"objects_mean": 7059.66871794872,
|
||||
"primitives_mean": 14455054.2379487
|
||||
},
|
||||
"mobile_units_at_start": 120,
|
||||
"overview": {
|
||||
"camera_zoom": 46.0,
|
||||
"cpu_frame_ms_mean": 7.17415674362089,
|
||||
"draw_calls_mean": 5447.83110571081,
|
||||
"frame_ms_mean": 7.29744106925881,
|
||||
"frame_ms_p95": 9.307,
|
||||
"frames": 823,
|
||||
"gpu_ms_mean": 4.11414337940378,
|
||||
"mobile_units_mean": 97.5261239368165,
|
||||
"objects_mean": 10639.1822600243,
|
||||
"primitives_mean": 27029403.7618469
|
||||
},
|
||||
"renderer": "forward_plus",
|
||||
"scene_build_ms": 179.527,
|
||||
"sim_speed": "2x (40 steps/s)",
|
||||
"sim_step_ms": 2.53624,
|
||||
"static_mem_mb": 126.140163421631,
|
||||
"texture_mem_mb": 424.65576171875,
|
||||
"video_mem_mb": 780.451141357422,
|
||||
"window": "(1920, 1080)"
|
||||
}
|
||||
40
docs/luneraria/logs/g1/verify-g1.json
Normal file
40
docs/luneraria/logs/g1/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.0000000882349220354683,
|
||||
"water_points_under_lanes": {
|
||||
"bot": 16,
|
||||
"mid": 8,
|
||||
"top": 0
|
||||
}
|
||||
},
|
||||
"failures": []
|
||||
}
|
||||
6
docs/luneraria/logs/g2/balance-summary.txt
Normal file
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
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue