Compare commits
5 commits
main
...
luneraria/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7c3a03e23 | ||
|
|
f53859a298 | ||
|
|
c27a0097c3 | ||
|
|
b565546c37 | ||
|
|
4b6a4f530e |
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)
|
||||||
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/grotto.blend
Normal file
BIN
assets-source/luneraria/tower.blend
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
|
|
@ -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
|
|
@ -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
|
||||||
BIN
assets/luneraria/grotto.glb
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
|
|
@ -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/tower.glb
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
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
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
|
|
@ -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
|
|
@ -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")
|
||||||
65
docs/luneraria/ANLEITUNG.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Installation, Start, Steuerung und Export
|
||||||
|
|
||||||
|
## Start aus dem Quellprojekt (Fedora 44)
|
||||||
|
|
||||||
|
```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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
2. **Planung:** Truppen kaufen (Knöpfe oder 1–5), mit T/M/B einer Lane zuweisen, 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 |
|
||||||
|
| Tab / Lane-Knöpfe links | Kamera auf die nächste Lane |
|
||||||
|
| O | Kartenübersicht |
|
||||||
|
| 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)
|
||||||
|
godot --path . --resolution 1920x1080 res://tools/luneraria/verify_m1.tscn # echte Bedienung, volle Partie
|
||||||
|
godot --headless --path . res://tools/luneraria/sim_match.tscn -- --seeds=1,2,3 --limit=80
|
||||||
|
godot --path . --resolution 1920x1080 res://tools/luneraria/lune_benchmark.tscn
|
||||||
|
godot --path . --resolution 1920x1080 res://tools/luneraria/lune_review.tscn # Aufnahmen
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Assets neu erzeugen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/build_tower.py
|
||||||
|
python3 tools/luneraria/build_asset_matrix.py
|
||||||
|
godot --headless --editor --import --path .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Export
|
||||||
|
|
||||||
|
Die Pakete für Champions of Luneraria kommen mit M4. Bis dahin baut `tools/build_demo.py` weiterhin die Saumwacht-Demo 0.15.0.
|
||||||
|
|
||||||
|
## Benutzereinstellungen
|
||||||
|
|
||||||
|
Der neue Projektname legt einen neuen Benutzerordner an (`~/.local/share/godot/app_userdata/Champions of Luneraria`). Beim ersten Start übernimmt `Preferences` einmalig die `preferences.cfg` aus „Saumwacht · Glutpakt“. Die alte Datei bleibt unverändert; fehlt sie, gelten die Standardwerte.
|
||||||
26
docs/luneraria/ARCHITEKTUR.md
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# 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. 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.
|
||||||
72
docs/luneraria/ASSETMATRIX.md
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
# Asset-Wiederverwendungsmatrix
|
||||||
|
|
||||||
|
Erzeugt mit `python3 tools/luneraria/build_asset_matrix.py` aus den Laufzeitmanifesten. „Volk/Bund“ ist die Datenzuordnung. Die sieben Zielvölker (Menschen, Elfen, Zwerge, Trolle, Feen, Orks, Biestmenschen) haben noch **keine eigenen Modelle**; 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 |
|
||||||
|
| 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 |
|
||||||
|
| compare-rock / compare-tree | Fels, Baum | res://assets/compare/ | assets-source/phase14/build_compare_props.py | statisch | 360 / 852 | Vergleichsszene | Kartendekoration Dschungel | — | einfach | keiner |
|
||||||
|
| 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) |
|
||||||
|
| 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 |
|
||||||
73
docs/luneraria/BALANCE.md
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
# 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.
|
||||||
55
docs/luneraria/QA-MATRIX.md
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# QA-Matrix
|
||||||
|
|
||||||
|
Testumgebung aller Läufe: Fedora Linux 44 (KDE Plasma, XWayland), AMD Ryzen 5 5600X, NVIDIA GeForce RTX 5060 Ti (Vulkan 1.4.351), Godot 4.7.2 (Fedora-Build), Forward+. **Windows 11 ist nicht geprüft:** Es gibt keine Windows-Hardware in dieser Umgebung.
|
||||||
|
|
||||||
|
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 | ⏳ M4 | — |
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
|
## 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 (Tests, Aufnahmen, Leistung) |
|
||||||
|
| Fedora 44, Kompatibilitätsmodus (OpenGL) | ⏳ M4 (für Saumwacht 0.15.0 geprüft, für Luneraria noch nicht) |
|
||||||
|
| Windows 11 | ⏳ nicht geprüft (keine Hardware). Ein exportiertes Paket oder ein Wine-Lauf wäre kein nativer Test. |
|
||||||
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, ×2,5 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. |
|
||||||
73
docs/luneraria/STATUS.md
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
# STATUS · Champions of Luneraria
|
||||||
|
|
||||||
|
Stand: 28. September 2026 · Branches `luneraria/m1` (M0/M1) und `luneraria/m2` · Version im Projekt 0.15.0 (Saumwacht-Demo), Luneraria-Paket erst mit M4.
|
||||||
|
|
||||||
|
| Meilenstein | Zustand |
|
||||||
|
| --- | --- |
|
||||||
|
| M0 · Migration vorbereiten | **abgeschlossen** |
|
||||||
|
| M1 · Spielbarer Drei-Lane-Kern | **abgeschlossen** (siehe unten) |
|
||||||
|
| M2 · Mechanikdurchlauf | **abgeschlossen** (siehe unten) |
|
||||||
|
| M3 · Qualität und Balance | offen |
|
||||||
|
| M4 · Netzwerk und Pakete | offen |
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Bekannt
|
||||||
|
|
||||||
|
- Ein exakt gespiegeltes Match bleibt unentschieden, wird also nicht beendet (Testlimit). Das ist ein Balancebefund für M3.
|
||||||
|
- Die Figuren der Monsterwellen sind nur über Teamring und Lichtfarbe dem Team zugeordnet.
|
||||||
|
- Bäume und Felsen stammen aus dem Vergleichspack und sind einfach; das Kartenbild ist funktional, noch nicht endgültig (M3).
|
||||||
|
- Der Bossjäger-Bot greift zu früh an (Fehlschläge in Runde 5–7, Sieg in Runde 8); nach den Szenarien wiegt der Boss die geschwächte Lane eher zu wenig auf (M3).
|
||||||
|
- Die Auswertung der Bot-Profile ist verzerrt, weil in jeder Partie dieselben Profilpaare gegeneinander stehen (M3: variierte Teams).
|
||||||
|
- Beim Beenden meldet Godot eine noch belegte Ressource (Hintergrundladen der Menübühne); ohne Folgen im Spiel.
|
||||||
|
|
||||||
|
## Blockiert
|
||||||
|
|
||||||
|
- Nativer Windows-11-Test: keine Windows-Hardware.
|
||||||
|
|
||||||
|
## Nächster Schritt
|
||||||
|
|
||||||
|
M3: mehrere Seeds und Teamzusammensetzungen, Seitentausch, Gegenstrategien, hohe Wellenindizes als Belastungstest, Boss- und Profilbalance, visuelle Überarbeitung der Karte, danach Repräsentanten der fehlenden Völker.
|
||||||
4
docs/luneraria/logs/core-tests.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"checks": 91,
|
||||||
|
"failures": []
|
||||||
|
}
|
||||||
4
docs/luneraria/logs/m0-baseline/ability-events.log
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
ABILITY_EVENT_PASS: 33 checks; 0 failures
|
||||||
|
EXIT 0
|
||||||
250
docs/luneraria/logs/m0-baseline/expedition-test.log
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
ERROR: Attempting to initialize the wrong RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:240)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "mem" is null.
|
||||||
|
at: initialize_rid (./core/templates/rid_owner.h:284)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_set_blend_shape_count (servers/rendering/dummy/storage/mesh_storage.h:74)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_set_blend_shape_mode (servers/rendering/dummy/storage/mesh_storage.h:110)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_set_blend_shape_mode (servers/rendering/dummy/storage/mesh_storage.h:110)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:90)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:62)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
EXPEDITION_MATCH: Standard gegen Standard; wave 20; cores [669, 669]; outcome -1
|
||||||
|
EXPEDITION_MATCH: Standard gegen Lernmodus; wave 10; cores [2038, 0]; outcome 0
|
||||||
|
EXPEDITION_MATCH: Leeres Baufeld gegen Standard; wave 7; cores [0, 3000]; outcome 1
|
||||||
|
EXPEDITION_PASS: 206 checks; 0 failures; report /home/martin/.local/share/godot/app_userdata/Saumwacht · Glutpakt/phase8-verification.json
|
||||||
|
WARNING: 6 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 RID allocations of type 'N13RendererDummy9DummyMeshE' were leaked at exit.
|
||||||
|
EXIT 0
|
||||||
8
docs/luneraria/logs/m0-baseline/faction-test.log
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
FACTION_MATCH: Wurzelbund gegen Glutpakt wave 20 cores [1244, 1709]
|
||||||
|
FACTION_MATCH: Glutpakt gegen Wurzelbund wave 20 cores [1709, 1244]
|
||||||
|
FACTION_MATCH: Wurzelbund Spiegel wave 20 cores [1244, 1244]
|
||||||
|
FACTION_MATCH: Wurzelbund Kurzpartie wave 8 cores [3000, 3000]
|
||||||
|
FACTION_PASS: 1665 checks; 0 failures; /home/martin/.local/share/godot/app_userdata/Saumwacht · Glutpakt/phase10-verification.json
|
||||||
|
EXIT 0
|
||||||
5
docs/luneraria/logs/m0-baseline/smoke-test.log
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
EXPORT_SMOKE_PASS: 463 checks; 0 failures; engine 4.7.2-stable (fedora); cores [3000, 1035]
|
||||||
|
SMOKE_CAPTURES: /home/martin/.local/share/godot/app_userdata/Saumwacht · Glutpakt/phase8-smoke
|
||||||
|
EXIT 0
|
||||||
4
docs/luneraria/logs/m0-baseline/test-runner.log
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
PASS: 209 checks across economy, combat and the full match lifecycle.
|
||||||
|
EXIT 0
|
||||||
10
docs/luneraria/logs/m1-acceptance.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"checks": 38,
|
||||||
|
"failures": [],
|
||||||
|
"object_counts": [
|
||||||
|
3850.0,
|
||||||
|
3850.0,
|
||||||
|
3850.0,
|
||||||
|
3850.0
|
||||||
|
]
|
||||||
|
}
|
||||||
8
docs/luneraria/logs/m1/bench-1280x720.log
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
Vulkan 1.4.351 - Forward+ - Using Device #0: NVIDIA - NVIDIA GeForce RTX 5060 Ti
|
||||||
|
|
||||||
|
LUNE_BENCH {"buildings":18,"cpu":"AMD Ryzen 5 5600X 6-Core Processor","draw_calls_mean":3175.86281588448,"engine":"4.7.2-stable (fedora)","frame_ms_mean":7.22646841155234,"frame_ms_p95":10.294,"frames":1108,"gpu":"NVIDIA GeForce RTX 5060 Ti","gpu_ms_mean":2.09521750809914,"mobile_units_at_start":114,"mobile_units_mean_measured":108.60559566787,"renderer":"forward_plus","scene_build_ms":198.002,"sim_speed":"2x (40 steps/s)","sim_step_ms":2.37125,"static_mem_mb":120.703545570374,"video_mem_mb":658.710235595703,"window":"(1280, 720)"}
|
||||||
|
WARNING: 24 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)
|
||||||
8
docs/luneraria/logs/m1/bench-1920x1080.log
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
Vulkan 1.4.351 - Forward+ - Using Device #0: NVIDIA - NVIDIA GeForce RTX 5060 Ti
|
||||||
|
|
||||||
|
LUNE_BENCH {"buildings":18,"cpu":"AMD Ryzen 5 5600X 6-Core Processor","draw_calls_mean":3172.21948912015,"engine":"4.7.2-stable (fedora)","frame_ms_mean":7.56970955534533,"frame_ms_p95":10.505,"frames":1057,"gpu":"NVIDIA GeForce RTX 5060 Ti","gpu_ms_mean":3.06410596431599,"mobile_units_at_start":114,"mobile_units_mean_measured":108.593188268685,"renderer":"forward_plus","scene_build_ms":221.213,"sim_speed":"2x (40 steps/s)","sim_step_ms":2.403405,"static_mem_mb":120.915734291077,"video_mem_mb":765.283081054688,"window":"(1920, 1080)"}
|
||||||
|
WARNING: 24 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)
|
||||||
4
docs/luneraria/logs/m1/legacy-ability.log
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
ABILITY_EVENT_PASS: 33 checks; 0 failures
|
||||||
|
EXIT 0
|
||||||
250
docs/luneraria/logs/m1/legacy-expedition.log
Normal file
|
|
@ -0,0 +1,250 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
ERROR: Attempting to initialize the wrong RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:240)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "mem" is null.
|
||||||
|
at: initialize_rid (./core/templates/rid_owner.h:284)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_set_blend_shape_count (servers/rendering/dummy/storage/mesh_storage.h:74)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_add_surface (servers/rendering/dummy/storage/mesh_storage.h:81)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_set_blend_shape_mode (servers/rendering/dummy/storage/mesh_storage.h:110)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Attempting to use an uninitialized RID
|
||||||
|
at: get_or_null (./core/templates/rid_owner.h:247)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
ERROR: Parameter "m" is null.
|
||||||
|
at: mesh_set_blend_shape_mode (servers/rendering/dummy/storage/mesh_storage.h:110)
|
||||||
|
GDScript backtrace (most recent call first):
|
||||||
|
[0] instantiate_fighter (res://scripts/presentation/asset_library.gd:110)
|
||||||
|
[1] _place_core (res://scripts/presentation/menu_stage.gd:172)
|
||||||
|
[2] _ready (res://scripts/presentation/menu_stage.gd:61)
|
||||||
|
[3] _build_stage (res://scripts/ui/main_menu.gd:97)
|
||||||
|
[4] _ready (res://scripts/ui/main_menu.gd:69)
|
||||||
|
[5] _make_overlay (res://scripts/ui/hud.gd:492)
|
||||||
|
[6] _ready (res://scripts/ui/hud.gd:112)
|
||||||
|
EXPEDITION_MATCH: Standard gegen Standard; wave 20; cores [669, 669]; outcome -1
|
||||||
|
EXPEDITION_MATCH: Standard gegen Lernmodus; wave 10; cores [2038, 0]; outcome 0
|
||||||
|
EXPEDITION_MATCH: Leeres Baufeld gegen Standard; wave 7; cores [0, 3000]; outcome 1
|
||||||
|
EXPEDITION_PASS: 206 checks; 0 failures; report /home/martin/.local/share/godot/app_userdata/Champions of Luneraria/phase8-verification.json
|
||||||
|
WARNING: 6 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 RID allocations of type 'N13RendererDummy9DummyMeshE' were leaked at exit.
|
||||||
|
EXIT 0
|
||||||
8
docs/luneraria/logs/m1/legacy-faction.log
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
FACTION_MATCH: Wurzelbund gegen Glutpakt wave 20 cores [1244, 1709]
|
||||||
|
FACTION_MATCH: Glutpakt gegen Wurzelbund wave 20 cores [1709, 1244]
|
||||||
|
FACTION_MATCH: Wurzelbund Spiegel wave 20 cores [1244, 1244]
|
||||||
|
FACTION_MATCH: Wurzelbund Kurzpartie wave 8 cores [3000, 3000]
|
||||||
|
FACTION_PASS: 1665 checks; 0 failures; /home/martin/.local/share/godot/app_userdata/Champions of Luneraria/phase10-verification.json
|
||||||
|
EXIT 0
|
||||||
5
docs/luneraria/logs/m1/legacy-smoke.log
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
EXPORT_SMOKE_PASS: 463 checks; 0 failures; engine 4.7.2-stable (fedora); cores [3000, 1035]
|
||||||
|
SMOKE_CAPTURES: /home/martin/.local/share/godot/app_userdata/Champions of Luneraria/phase8-smoke
|
||||||
|
EXIT 0
|
||||||
4
docs/luneraria/logs/m1/legacy-test-runner.log
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
PASS: 209 checks across economy, combat and the full match lifecycle.
|
||||||
|
EXIT 0
|
||||||
5
docs/luneraria/logs/m1/legacy-verify-menu.log
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
Vulkan 1.4.351 - Forward+ - Using Device #0: NVIDIA - NVIDIA GeForce RTX 5060 Ti
|
||||||
|
|
||||||
|
MENU_VERIFY_PASS: 68 checks; 0 failures
|
||||||
|
EXIT 0
|
||||||
9
docs/luneraria/logs/m1/lune-core.log
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
LUNE_FULL_MATCH: seed 2 -> Blau after 8 rounds
|
||||||
|
LUNE_CORE_PASS: 91 checks; 0 failures
|
||||||
|
WARNING: 10 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 resources still in use at exit (run with --verbose for details).
|
||||||
|
at: clear (core/io/resource.cpp:822)
|
||||||
|
EXIT 0
|
||||||
1940
docs/luneraria/logs/m1/sim-8-seeds.json
Normal file
106
docs/luneraria/logs/m1/sim-8-seeds.log
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
R01 cleared 46.7s buildings [9, 9] lost [] gold [123, 173, 123, 128, 123, 128]
|
||||||
|
R02 cleared 53.7s buildings [9, 9] lost [] gold [140, 105, 115, 125, 125, 150]
|
||||||
|
R03 cleared 55.5s buildings [9, 9] lost [] gold [160, 170, 150, 125, 160, 175]
|
||||||
|
R04 cleared 69.1s buildings [8, 9] lost ["blau_top_outer"] gold [182, 142, 132, 182, 172, 257]
|
||||||
|
R05 cleared 74.3s buildings [8, 9] lost [] gold [178, 183, 163, 133, 153, 153]
|
||||||
|
R06 cleared 70.1s buildings [7, 8] lost ["rot_mid_outer", "blau_top_inner"] gold [203, 223, 183, 243, 253, 203]
|
||||||
|
R07 cleared 73.3s buildings [7, 7] lost ["rot_mid_inner"] gold [270, 235, 270, 170, 195, 205]
|
||||||
|
R08 cleared 79.9s buildings [6, 7] lost ["blau_base_1"] gold [245, 185, 165, 275, 245, 280]
|
||||||
|
R09 core 86.9s buildings [4, 7] lost ["blau_base_2", "blau_ancient"] gold [57, 106, 66, 171, 176, 176]
|
||||||
|
SIM_MATCH seed=1 Sieg Rot after 9 rounds (Ancient von Blau zerstört.) in 7091 ms
|
||||||
|
R01 cleared 46.7s buildings [9, 9] lost [] gold [128, 123, 128, 173, 128, 123]
|
||||||
|
R02 cleared 53.7s buildings [9, 9] lost [] gold [125, 125, 150, 105, 125, 115]
|
||||||
|
R03 cleared 54.7s buildings [9, 9] lost [] gold [125, 160, 175, 165, 165, 115]
|
||||||
|
R04 cleared 73.1s buildings [9, 8] lost ["rot_top_outer"] gold [182, 172, 257, 137, 157, 187]
|
||||||
|
R05 cleared 81.9s buildings [8, 7] lost ["blau_mid_outer", "rot_top_inner"] gold [203, 223, 223, 218, 183, 193]
|
||||||
|
R06 cleared 67.3s buildings [8, 7] lost [] gold [158, 178, 133, 138, 153, 133]
|
||||||
|
R07 cleared 74.7s buildings [7, 6] lost ["blau_mid_inner", "rot_base_1"] gold [260, 285, 310, 225, 235, 255]
|
||||||
|
R08 core 92.9s buildings [7, 3] lost ["rot_bot_outer", "rot_base_2", "rot_ancient"] gold [207, 197, 217, 79, 105, 64]
|
||||||
|
SIM_MATCH seed=2 Sieg Blau after 8 rounds (Ancient von Rot zerstört.) in 6499 ms
|
||||||
|
R01 cleared 41.9s buildings [9, 9] lost [] gold [173, 128, 123, 128, 123, 173]
|
||||||
|
R02 cleared 46.7s buildings [9, 9] lost [] gold [105, 125, 115, 135, 140, 105]
|
||||||
|
R03 cleared 59.8s buildings [9, 9] lost [] gold [165, 165, 115, 175, 160, 170]
|
||||||
|
R04 cleared 61.4s buildings [9, 9] lost [] gold [137, 157, 187, 137, 182, 142]
|
||||||
|
R05 cleared 54.9s buildings [9, 9] lost [] gold [168, 133, 143, 133, 178, 183]
|
||||||
|
R06 cleared 57.9s buildings [9, 9] lost [] gold [163, 173, 173, 173, 153, 173]
|
||||||
|
R07 cleared 58.3s buildings [9, 9] lost [] gold [180, 185, 225, 185, 205, 185]
|
||||||
|
R08 cleared 61.9s buildings [8, 9] lost ["blau_bot_outer"] gold [200, 200, 155, 250, 255, 250]
|
||||||
|
R09 cleared 71.3s buildings [8, 9] lost [] gold [213, 218, 188, 198, 168, 198]
|
||||||
|
R10 cleared 74.2s buildings [7, 9] lost ["blau_bot_inner"] gold [215, 250, 215, 300, 265, 300]
|
||||||
|
R11 cleared 83.7s buildings [5, 9] lost ["blau_top_outer", "blau_base_2"] gold [228, 218, 218, 348, 358, 413]
|
||||||
|
R12 core 86.9s buildings [1, 9] lost ["blau_mid_outer", "blau_top_inner", "blau_base_1", "blau_ancient"] gold [101, 106, 133, 348, 318, 323]
|
||||||
|
SIM_MATCH seed=3 Sieg Rot after 12 rounds (Ancient von Blau zerstört.) in 9910 ms
|
||||||
|
R01 cleared 44.6s buildings [9, 9] lost [] gold [128, 123, 173, 123, 128, 123]
|
||||||
|
R02 cleared 46.5s buildings [9, 9] lost [] gold [135, 140, 105, 115, 125, 125]
|
||||||
|
R03 cleared 55.1s buildings [9, 9] lost [] gold [175, 160, 170, 150, 125, 160]
|
||||||
|
R04 cleared 63.1s buildings [9, 9] lost [] gold [137, 182, 142, 132, 132, 122]
|
||||||
|
R05 cleared 62.3s buildings [9, 9] lost [] gold [133, 178, 183, 163, 173, 153]
|
||||||
|
R06 cleared 59.7s buildings [9, 9] lost [] gold [173, 153, 173, 133, 213, 148]
|
||||||
|
R07 cleared 72.1s buildings [9, 8] lost ["rot_bot_outer"] gold [235, 255, 235, 160, 190, 165]
|
||||||
|
R08 cleared 62.5s buildings [8, 8] lost ["blau_top_outer"] gold [180, 180, 180, 205, 210, 215]
|
||||||
|
R09 cleared 74.8s buildings [8, 7] lost ["rot_bot_inner"] gold [268, 263, 268, 238, 223, 178]
|
||||||
|
R10 cleared 80.3s buildings [8, 7] lost [] gold [235, 205, 235, 210, 265, 185]
|
||||||
|
R11 core 117.5s buildings [7, 4] lost ["blau_mid_outer", "rot_base_2", "rot_base_1", "rot_ancient"] gold [288, 348, 303, 223, 178, 178]
|
||||||
|
SIM_MATCH seed=4 Sieg Blau after 11 rounds (Ancient von Rot zerstört.) in 9299 ms
|
||||||
|
R01 cleared 42.1s buildings [9, 9] lost [] gold [123, 128, 123, 128, 173, 128]
|
||||||
|
R02 cleared 50.3s buildings [9, 9] lost [] gold [115, 125, 125, 150, 105, 125]
|
||||||
|
R03 cleared 55.1s buildings [9, 9] lost [] gold [150, 125, 160, 175, 165, 165]
|
||||||
|
R04 cleared 66.9s buildings [9, 9] lost [] gold [132, 132, 122, 207, 137, 157]
|
||||||
|
R05 cleared 60.3s buildings [9, 9] lost [] gold [163, 173, 153, 198, 168, 133]
|
||||||
|
R06 cleared 61.9s buildings [9, 9] lost [] gold [133, 213, 148, 178, 163, 173]
|
||||||
|
R07 cleared 87.3s buildings [9, 8] lost ["rot_top_outer"] gold [210, 240, 215, 180, 180, 185]
|
||||||
|
R08 cleared 89.3s buildings [9, 8] lost [] gold [205, 210, 165, 165, 200, 200]
|
||||||
|
R09 cleared 80.3s buildings [8, 7] lost ["blau_bot_outer", "rot_top_inner"] gold [308, 293, 303, 218, 263, 268]
|
||||||
|
R10 cleared 84.1s buildings [8, 7] lost [] gold [220, 195, 195, 185, 190, 185]
|
||||||
|
R11 cleared 84.8s buildings [7, 6] lost ["blau_bot_inner", "rot_base_1"] gold [363, 343, 358, 323, 313, 308]
|
||||||
|
R12 cleared 83.7s buildings [7, 6] lost [] gold [267, 237, 237, 232, 272, 232]
|
||||||
|
R13 cleared 95.7s buildings [6, 5] lost ["rot_mid_outer", "blau_base_2"] gold [303, 338, 333, 388, 393, 393]
|
||||||
|
R14 cleared 82.1s buildings [6, 5] lost [] gold [390, 295, 305, 310, 320, 375]
|
||||||
|
R15 core 130.3s buildings [6, 3] lost ["rot_base_2", "rot_ancient"] gold [411, 511, 291, 216, 311, 351]
|
||||||
|
SIM_MATCH seed=5 Sieg Blau after 15 rounds (Ancient von Rot zerstört.) in 14879 ms
|
||||||
|
R01 cleared 48.3s buildings [9, 9] lost [] gold [128, 173, 128, 123, 128, 123]
|
||||||
|
R02 cleared 52.5s buildings [9, 9] lost [] gold [150, 105, 125, 115, 135, 140]
|
||||||
|
R03 cleared 60.7s buildings [9, 9] lost [] gold [175, 165, 165, 115, 175, 160]
|
||||||
|
R04 cleared 64.5s buildings [9, 9] lost [] gold [207, 137, 157, 187, 137, 182]
|
||||||
|
R05 cleared 59.1s buildings [9, 9] lost [] gold [198, 168, 133, 143, 133, 178]
|
||||||
|
R06 cleared 58.5s buildings [9, 8] lost ["rot_mid_outer"] gold [228, 213, 223, 173, 173, 153]
|
||||||
|
R07 cleared 72.7s buildings [8, 8] lost ["blau_top_outer"] gold [230, 155, 165, 275, 235, 255]
|
||||||
|
R08 cleared 86.1s buildings [7, 7] lost ["rot_mid_inner", "blau_top_inner"] gold [285, 245, 250, 230, 250, 250]
|
||||||
|
R09 cleared 72.4s buildings [7, 6] lost ["rot_bot_outer"] gold [218, 248, 273, 243, 203, 203]
|
||||||
|
R10 cleared 74.8s buildings [7, 6] lost [] gold [195, 220, 215, 270, 245, 250]
|
||||||
|
R11 cleared 92.9s buildings [7, 5] lost ["rot_bot_inner"] gold [288, 343, 278, 283, 218, 278]
|
||||||
|
R12 core 89.1s buildings [7, 2] lost ["rot_base_2", "rot_base_1", "rot_ancient"] gold [328, 288, 348, 108, 163, 87]
|
||||||
|
SIM_MATCH seed=6 Sieg Blau after 12 rounds (Ancient von Rot zerstört.) in 10244 ms
|
||||||
|
R01 cleared 41.3s buildings [9, 9] lost [] gold [123, 128, 123, 173, 123, 128]
|
||||||
|
R02 cleared 51.5s buildings [9, 9] lost [] gold [115, 135, 140, 105, 115, 125]
|
||||||
|
R03 cleared 52.5s buildings [9, 9] lost [] gold [115, 175, 160, 170, 150, 125]
|
||||||
|
R04 cleared 57.7s buildings [9, 9] lost [] gold [187, 137, 182, 142, 132, 132]
|
||||||
|
R05 cleared 57.5s buildings [9, 9] lost [] gold [143, 133, 178, 183, 163, 173]
|
||||||
|
R06 cleared 61.7s buildings [9, 9] lost [] gold [173, 173, 153, 173, 133, 213]
|
||||||
|
R07 cleared 61.1s buildings [9, 9] lost [] gold [225, 185, 205, 185, 160, 190]
|
||||||
|
R08 cleared 57.5s buildings [9, 9] lost [] gold [155, 200, 205, 200, 155, 160]
|
||||||
|
R09 cleared 72.9s buildings [9, 9] lost [] gold [188, 218, 193, 218, 188, 173]
|
||||||
|
R10 cleared 72.1s buildings [8, 9] lost ["blau_bot_outer"] gold [215, 250, 240, 280, 285, 265]
|
||||||
|
R11 cleared 84.4s buildings [5, 9] lost ["blau_mid_outer", "blau_top_outer", "blau_bot_inner"] gold [218, 218, 238, 398, 453, 408]
|
||||||
|
R12 core 98.9s buildings [0, 9] lost ["blau_mid_inner", "blau_top_inner", "blau_base_2", "blau_base_1", "blau_ancient"] gold [156, 101, 123, 463, 563, 528]
|
||||||
|
SIM_MATCH seed=7 Sieg Rot after 12 rounds (Ancient von Blau zerstört.) in 10269 ms
|
||||||
|
R01 cleared 41.9s buildings [9, 9] lost [] gold [173, 123, 128, 123, 128, 173]
|
||||||
|
R02 cleared 47.5s buildings [9, 9] lost [] gold [105, 115, 125, 125, 150, 105]
|
||||||
|
R03 cleared 55.5s buildings [9, 9] lost [] gold [170, 150, 125, 160, 175, 165]
|
||||||
|
R04 cleared 55.1s buildings [9, 9] lost [] gold [142, 132, 132, 122, 207, 137]
|
||||||
|
R05 cleared 54.5s buildings [9, 9] lost [] gold [183, 163, 173, 153, 198, 168]
|
||||||
|
R06 cleared 57.7s buildings [9, 9] lost [] gold [173, 133, 213, 148, 178, 163]
|
||||||
|
R07 cleared 58.7s buildings [9, 9] lost [] gold [185, 160, 190, 165, 180, 180]
|
||||||
|
R08 cleared 54.9s buildings [9, 9] lost [] gold [200, 155, 160, 165, 165, 200]
|
||||||
|
R09 cleared 66.5s buildings [9, 8] lost ["rot_top_outer"] gold [268, 238, 223, 178, 168, 213]
|
||||||
|
R10 cleared 87.5s buildings [9, 7] lost ["rot_top_inner"] gold [280, 280, 335, 210, 200, 215]
|
||||||
|
R11 cleared 87.7s buildings [9, 6] lost ["rot_bot_outer"] gold [323, 403, 303, 238, 218, 228]
|
||||||
|
R12 core 103.0s buildings [9, 1] lost ["rot_base_1", "rot_mid_outer", "rot_bot_inner", "rot_base_2", "rot_ancient"] gold [418, 493, 458, 143, 113, 118]
|
||||||
|
SIM_MATCH seed=8 Sieg Blau after 12 rounds (Ancient von Rot zerstört.) in 10274 ms
|
||||||
|
WARNING: 6 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 resources still in use at exit (run with --verbose for details).
|
||||||
|
at: clear (core/io/resource.cpp:822)
|
||||||
|
EXIT 0
|
||||||
15
docs/luneraria/logs/m1/verify-m1.log
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
Vulkan 1.4.351 - Forward+ - Using Device #0: NVIDIA - NVIDIA GeForce RTX 5060 Ti
|
||||||
|
|
||||||
|
M1_MENU_READY_MS 733
|
||||||
|
M1_SHOT planning-1280x720
|
||||||
|
M1_SHOT planning-1920x1080
|
||||||
|
M1_SHOT combat-mid-live
|
||||||
|
M1_SHOT settlement
|
||||||
|
M1_SHOT combat-top
|
||||||
|
M1_SHOT overview-combat
|
||||||
|
M1_SHOT core-siege
|
||||||
|
M1_SHOT result
|
||||||
|
M1_MATCH_RESULT Ancient von Rot zerstört. after 10 waves
|
||||||
|
M1_VERIFY_PASS: 38 checks; 0 failures
|
||||||
|
EXIT 0
|
||||||
44
docs/luneraria/logs/m2-acceptance.json
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"boss_log": [
|
||||||
|
{
|
||||||
|
"event": "failed",
|
||||||
|
"round": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"event": "failed",
|
||||||
|
"round": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"event": "failed",
|
||||||
|
"round": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chooser": 4,
|
||||||
|
"event": "won",
|
||||||
|
"round": 8,
|
||||||
|
"team": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"event": "deployed",
|
||||||
|
"lane": "top",
|
||||||
|
"round": 9,
|
||||||
|
"team": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"chooser": 4,
|
||||||
|
"event": "won",
|
||||||
|
"round": 12,
|
||||||
|
"team": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"checks": 15,
|
||||||
|
"failures": [],
|
||||||
|
"rounds": 12,
|
||||||
|
"seen": [
|
||||||
|
"grotto",
|
||||||
|
"hero_field",
|
||||||
|
"hero_items",
|
||||||
|
"announce",
|
||||||
|
"boss"
|
||||||
|
]
|
||||||
|
}
|
||||||
4
docs/luneraria/logs/m2-tests.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"checks": 73,
|
||||||
|
"failures": []
|
||||||
|
}
|
||||||
85
docs/luneraria/logs/m2/boss-scenarios-seed3.json
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"blau_gold_delta": 284,
|
||||||
|
"blau_hp_after_r8": 0.0,
|
||||||
|
"blau_hp_delta": -1436.5,
|
||||||
|
"blau_mid_towers_lost_r8": 0,
|
||||||
|
"claim_team": -1,
|
||||||
|
"grotto": "none",
|
||||||
|
"phase": "vorbei",
|
||||||
|
"rot_gold_delta": -235,
|
||||||
|
"rot_hp_delta": -50521.0198,
|
||||||
|
"rot_sent_value": 0,
|
||||||
|
"scenario": "kein_versuch",
|
||||||
|
"sent_value": 0,
|
||||||
|
"winner": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blau_gold_delta": 284,
|
||||||
|
"blau_hp_after_r8": -317.199999999997,
|
||||||
|
"blau_hp_delta": -1753.7,
|
||||||
|
"blau_mid_towers_lost_r8": 0,
|
||||||
|
"claim_team": -1,
|
||||||
|
"grotto": "failed",
|
||||||
|
"phase": "vorbei",
|
||||||
|
"rot_gold_delta": -235,
|
||||||
|
"rot_hp_delta": -50257.2862,
|
||||||
|
"rot_sent_value": 0,
|
||||||
|
"scenario": "gescheitert",
|
||||||
|
"sent_value": 210,
|
||||||
|
"winner": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blau_gold_delta": 543,
|
||||||
|
"blau_hp_after_r8": -13227.8841,
|
||||||
|
"blau_hp_delta": -15022.5341,
|
||||||
|
"blau_mid_towers_lost_r8": 2,
|
||||||
|
"boss_bounty_to_rot": 1060,
|
||||||
|
"boss_killed": true,
|
||||||
|
"boss_round": 9,
|
||||||
|
"claim_team": 0,
|
||||||
|
"grotto": "won",
|
||||||
|
"phase": "vorbei",
|
||||||
|
"rot_gold_delta": 794,
|
||||||
|
"rot_hp_delta": -35384.1102,
|
||||||
|
"rot_hp_lost_boss_round": 33218.6196,
|
||||||
|
"rot_sent_value": 0,
|
||||||
|
"scenario": "erfolgreich",
|
||||||
|
"sent_value": 1000,
|
||||||
|
"winner": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blau_gold_delta": 494,
|
||||||
|
"blau_hp_after_r8": -401.375,
|
||||||
|
"blau_hp_delta": -1434.51880000001,
|
||||||
|
"blau_mid_towers_lost_r8": 0,
|
||||||
|
"claim_team": -1,
|
||||||
|
"grotto": "failed",
|
||||||
|
"phase": "vorbei",
|
||||||
|
"rot_gold_delta": -185,
|
||||||
|
"rot_hp_delta": -51276.0262,
|
||||||
|
"rot_sent_value": 995,
|
||||||
|
"scenario": "umkaempft",
|
||||||
|
"sent_value": 1000,
|
||||||
|
"winner": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blau_gold_delta": -250,
|
||||||
|
"blau_hp_after_r8": -36433.06827,
|
||||||
|
"blau_hp_delta": -68294.9678,
|
||||||
|
"blau_mid_towers_lost_r8": 2,
|
||||||
|
"boss_bounty_to_rot": 1060,
|
||||||
|
"boss_killed": true,
|
||||||
|
"boss_round": 9,
|
||||||
|
"claim_team": 0,
|
||||||
|
"grotto": "won",
|
||||||
|
"phase": "vorbei",
|
||||||
|
"rot_gold_delta": 437,
|
||||||
|
"rot_hp_delta": -25391.9446,
|
||||||
|
"rot_hp_lost_boss_round": 25391.9446,
|
||||||
|
"rot_sent_value": 0,
|
||||||
|
"scenario": "ueberinvestition",
|
||||||
|
"sent_value": 2495,
|
||||||
|
"winner": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
85
docs/luneraria/logs/m2/boss-scenarios-seed7.json
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"blau_gold_delta": 174,
|
||||||
|
"blau_hp_after_r8": -3771.781,
|
||||||
|
"blau_hp_delta": -9222.031,
|
||||||
|
"blau_mid_towers_lost_r8": 0,
|
||||||
|
"claim_team": -1,
|
||||||
|
"grotto": "none",
|
||||||
|
"phase": "läuft",
|
||||||
|
"rot_gold_delta": 139,
|
||||||
|
"rot_hp_delta": -5573.1798,
|
||||||
|
"rot_sent_value": 0,
|
||||||
|
"scenario": "kein_versuch",
|
||||||
|
"sent_value": 0,
|
||||||
|
"winner": -2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blau_gold_delta": 174,
|
||||||
|
"blau_hp_after_r8": -4097.3806,
|
||||||
|
"blau_hp_delta": -9547.6306,
|
||||||
|
"blau_mid_towers_lost_r8": 0,
|
||||||
|
"claim_team": -1,
|
||||||
|
"grotto": "failed",
|
||||||
|
"phase": "läuft",
|
||||||
|
"rot_gold_delta": 139,
|
||||||
|
"rot_hp_delta": -4659.805,
|
||||||
|
"rot_sent_value": 0,
|
||||||
|
"scenario": "gescheitert",
|
||||||
|
"sent_value": 180,
|
||||||
|
"winner": -2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blau_gold_delta": -302,
|
||||||
|
"blau_hp_after_r8": -16247.2932,
|
||||||
|
"blau_hp_delta": -67736.578,
|
||||||
|
"blau_mid_towers_lost_r8": 2,
|
||||||
|
"boss_bounty_to_rot": 1060,
|
||||||
|
"boss_killed": true,
|
||||||
|
"boss_round": 9,
|
||||||
|
"claim_team": 0,
|
||||||
|
"grotto": "won",
|
||||||
|
"phase": "vorbei",
|
||||||
|
"rot_gold_delta": 319,
|
||||||
|
"rot_hp_delta": -22792.746,
|
||||||
|
"rot_hp_lost_boss_round": 22792.746,
|
||||||
|
"rot_sent_value": 0,
|
||||||
|
"scenario": "erfolgreich",
|
||||||
|
"sent_value": 930,
|
||||||
|
"winner": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blau_gold_delta": 174,
|
||||||
|
"blau_hp_after_r8": -4404.387,
|
||||||
|
"blau_hp_delta": -9854.637,
|
||||||
|
"blau_mid_towers_lost_r8": 0,
|
||||||
|
"claim_team": -1,
|
||||||
|
"grotto": "failed",
|
||||||
|
"phase": "läuft",
|
||||||
|
"rot_gold_delta": 139,
|
||||||
|
"rot_hp_delta": -4659.805,
|
||||||
|
"rot_sent_value": 865,
|
||||||
|
"scenario": "umkaempft",
|
||||||
|
"sent_value": 930,
|
||||||
|
"winner": -2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blau_gold_delta": -320,
|
||||||
|
"blau_hp_after_r8": -38337.2773,
|
||||||
|
"blau_hp_delta": -67736.578,
|
||||||
|
"blau_mid_towers_lost_r8": 2,
|
||||||
|
"boss_bounty_to_rot": 1060,
|
||||||
|
"boss_killed": true,
|
||||||
|
"boss_round": 9,
|
||||||
|
"claim_team": 0,
|
||||||
|
"grotto": "won",
|
||||||
|
"phase": "vorbei",
|
||||||
|
"rot_gold_delta": 277,
|
||||||
|
"rot_hp_delta": -20684.7692,
|
||||||
|
"rot_hp_lost_boss_round": 20684.7692,
|
||||||
|
"rot_sent_value": 0,
|
||||||
|
"scenario": "ueberinvestition",
|
||||||
|
"sent_value": 2580,
|
||||||
|
"winner": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
9
docs/luneraria/logs/m2/lune-core.log
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
LUNE_FULL_MATCH: seed 2 -> Blau after 7 rounds
|
||||||
|
LUNE_CORE_PASS: 91 checks; 0 failures
|
||||||
|
WARNING: 10 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 resources still in use at exit (run with --verbose for details).
|
||||||
|
at: clear (core/io/resource.cpp:822)
|
||||||
|
EXIT 0
|
||||||
8
docs/luneraria/logs/m2/lune-m2.log
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
|
||||||
|
LUNE_M2_PASS: 65 checks; 0 failures
|
||||||
|
WARNING: 14 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 resources still in use at exit (run with --verbose for details).
|
||||||
|
at: clear (core/io/resource.cpp:822)
|
||||||
|
EXIT 0
|
||||||
3327
docs/luneraria/logs/m2/sim-12-seeds.json
Normal file
15
docs/luneraria/logs/m2/verify-m1.log
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
Vulkan 1.4.351 - Forward+ - Using Device #0: NVIDIA - NVIDIA GeForce RTX 5060 Ti
|
||||||
|
|
||||||
|
M1_MENU_READY_MS 792
|
||||||
|
M1_SHOT planning-1280x720
|
||||||
|
M1_SHOT planning-1920x1080
|
||||||
|
M1_SHOT combat-mid-live
|
||||||
|
M1_SHOT settlement
|
||||||
|
M1_SHOT combat-top
|
||||||
|
M1_SHOT overview-combat
|
||||||
|
M1_SHOT core-siege
|
||||||
|
M1_SHOT result
|
||||||
|
M1_MATCH_RESULT Ancient von Rot zerstört. after 7 waves
|
||||||
|
M1_VERIFY_PASS: 38 checks; 0 failures
|
||||||
|
EXIT 0
|
||||||
16
docs/luneraria/logs/m2/verify-m2.log
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
Godot Engine v4.7.2.stable.fedora.ed1daf0bf - https://godotengine.org
|
||||||
|
Vulkan 1.4.351 - Forward+ - Using Device #0: NVIDIA - NVIDIA GeForce RTX 5060 Ti
|
||||||
|
|
||||||
|
M2APP_SHOT m2-hud-hero-round1
|
||||||
|
M2APP_SHOT m2-grotto-fight
|
||||||
|
M2APP_SHOT m2-hero-field
|
||||||
|
M2APP_SHOT m2-hero-items
|
||||||
|
M2APP_SHOT m2-announcements
|
||||||
|
M2APP_SHOT m2-boss-deployed
|
||||||
|
M2APP_BOSS_LOG [{ "round": 5, "event": "failed" }, { "round": 6, "event": "failed" }, { "round": 7, "event": "failed" }, { "round": 8, "event": "won", "team": 1, "chooser": 4 }, { "round": 9, "event": "deployed", "team": 1, "lane": "top" }, { "round": 12, "event": "won", "team": 1, "chooser": 4 }]
|
||||||
|
M2APP_VERIFY_PASS: 15 checks; 0 failures
|
||||||
|
WARNING: 8 ObjectDB instances were leaked at exit (run with `--verbose` for details).
|
||||||
|
at: cleanup (core/object/object.cpp:2536)
|
||||||
|
ERROR: 1 resources still in use at exit (run with --verbose for details).
|
||||||
|
at: clear (core/io/resource.cpp:822)
|
||||||
|
EXIT 0
|
||||||
20
docs/luneraria/logs/perf-1280x720.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"buildings": 18,
|
||||||
|
"cpu": "AMD Ryzen 5 5600X 6-Core Processor",
|
||||||
|
"draw_calls_mean": 3175.86281588448,
|
||||||
|
"engine": "4.7.2-stable (fedora)",
|
||||||
|
"frame_ms_mean": 7.22646841155234,
|
||||||
|
"frame_ms_p95": 10.294,
|
||||||
|
"frames": 1108,
|
||||||
|
"gpu": "NVIDIA GeForce RTX 5060 Ti",
|
||||||
|
"gpu_ms_mean": 2.09521750809914,
|
||||||
|
"mobile_units_at_start": 114,
|
||||||
|
"mobile_units_mean_measured": 108.60559566787,
|
||||||
|
"renderer": "forward_plus",
|
||||||
|
"scene_build_ms": 198.002,
|
||||||
|
"sim_speed": "2x (40 steps/s)",
|
||||||
|
"sim_step_ms": 2.37125,
|
||||||
|
"static_mem_mb": 120.703545570374,
|
||||||
|
"video_mem_mb": 658.710235595703,
|
||||||
|
"window": "(1280, 720)"
|
||||||
|
}
|
||||||
20
docs/luneraria/logs/perf-1920x1080.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"buildings": 18,
|
||||||
|
"cpu": "AMD Ryzen 5 5600X 6-Core Processor",
|
||||||
|
"draw_calls_mean": 3172.21948912015,
|
||||||
|
"engine": "4.7.2-stable (fedora)",
|
||||||
|
"frame_ms_mean": 7.56970955534533,
|
||||||
|
"frame_ms_p95": 10.505,
|
||||||
|
"frames": 1057,
|
||||||
|
"gpu": "NVIDIA GeForce RTX 5060 Ti",
|
||||||
|
"gpu_ms_mean": 3.06410596431599,
|
||||||
|
"mobile_units_at_start": 114,
|
||||||
|
"mobile_units_mean_measured": 108.593188268685,
|
||||||
|
"renderer": "forward_plus",
|
||||||
|
"scene_build_ms": 221.213,
|
||||||
|
"sim_speed": "2x (40 steps/s)",
|
||||||
|
"sim_step_ms": 2.403405,
|
||||||
|
"static_mem_mb": 120.915734291077,
|
||||||
|
"video_mem_mb": 765.283081054688,
|
||||||
|
"window": "(1920, 1080)"
|
||||||
|
}
|
||||||
BIN
docs/luneraria/screenshots/combat-mid-live.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
40
docs/luneraria/screenshots/combat-mid-live.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cxfgbyo316gip"
|
||||||
|
path="res://.godot/imported/combat-mid-live.png-1baea78d38cdbbdf1b2c012c8459c83c.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/luneraria/screenshots/combat-mid-live.png"
|
||||||
|
dest_files=["res://.godot/imported/combat-mid-live.png-1baea78d38cdbbdf1b2c012c8459c83c.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
|
||||||
BIN
docs/luneraria/screenshots/combat-mid.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
40
docs/luneraria/screenshots/combat-mid.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cdpr2aef1mtyw"
|
||||||
|
path="res://.godot/imported/combat-mid.png-47877f493d55e3cb715252d7b2b177f7.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/luneraria/screenshots/combat-mid.png"
|
||||||
|
dest_files=["res://.godot/imported/combat-mid.png-47877f493d55e3cb715252d7b2b177f7.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
|
||||||
BIN
docs/luneraria/screenshots/combat-top.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
40
docs/luneraria/screenshots/combat-top.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bcesqxwj7xds5"
|
||||||
|
path="res://.godot/imported/combat-top.png-409d757457196da3b4d7dde0737a858f.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/luneraria/screenshots/combat-top.png"
|
||||||
|
dest_files=["res://.godot/imported/combat-top.png-409d757457196da3b4d7dde0737a858f.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
|
||||||
BIN
docs/luneraria/screenshots/core-siege.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
40
docs/luneraria/screenshots/core-siege.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bdnafrcapn3qp"
|
||||||
|
path="res://.godot/imported/core-siege.png-6a2f95e61f8e13a0d0145605efd83a90.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/luneraria/screenshots/core-siege.png"
|
||||||
|
dest_files=["res://.godot/imported/core-siege.png-6a2f95e61f8e13a0d0145605efd83a90.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
|
||||||
BIN
docs/luneraria/screenshots/m2-announcements.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/luneraria/screenshots/m2-boss-deployed.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/luneraria/screenshots/m2-grotto-fight.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
docs/luneraria/screenshots/m2-hero-field.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
docs/luneraria/screenshots/m2-hero-items.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/luneraria/screenshots/m2-hud-hero-round1.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/luneraria/screenshots/menu-setup.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
40
docs/luneraria/screenshots/menu-setup.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bdogy4irokv2y"
|
||||||
|
path="res://.godot/imported/menu-setup.png-1991aae81ee557037e9c72f26ad60780.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/luneraria/screenshots/menu-setup.png"
|
||||||
|
dest_files=["res://.godot/imported/menu-setup.png-1991aae81ee557037e9c72f26ad60780.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
|
||||||
BIN
docs/luneraria/screenshots/menu-title.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
40
docs/luneraria/screenshots/menu-title.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dpmpwywqtf8gb"
|
||||||
|
path="res://.godot/imported/menu-title.png-c5fe9b889c2b85b3983f3c07b80447e5.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/luneraria/screenshots/menu-title.png"
|
||||||
|
dest_files=["res://.godot/imported/menu-title.png-c5fe9b889c2b85b3983f3c07b80447e5.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
|
||||||
BIN
docs/luneraria/screenshots/overview-combat.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
40
docs/luneraria/screenshots/overview-combat.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://c4cj5wabcwwpo"
|
||||||
|
path="res://.godot/imported/overview-combat.png-047b1bec10668f137599feea356858c9.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/luneraria/screenshots/overview-combat.png"
|
||||||
|
dest_files=["res://.godot/imported/overview-combat.png-047b1bec10668f137599feea356858c9.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
|
||||||
BIN
docs/luneraria/screenshots/overview-planning.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
40
docs/luneraria/screenshots/overview-planning.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://1vimfsrdmwes"
|
||||||
|
path="res://.godot/imported/overview-planning.png-36712c5e2948fd3a6d16bb3a7612d671.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/luneraria/screenshots/overview-planning.png"
|
||||||
|
dest_files=["res://.godot/imported/overview-planning.png-36712c5e2948fd3a6d16bb3a7612d671.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
|
||||||
BIN
docs/luneraria/screenshots/planning-1280x720.png
Normal file
|
After Width: | Height: | Size: 650 KiB |
40
docs/luneraria/screenshots/planning-1280x720.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://irf7f3pko78d"
|
||||||
|
path="res://.godot/imported/planning-1280x720.png-4961a39d8036e60c703f21f94b767a05.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/luneraria/screenshots/planning-1280x720.png"
|
||||||
|
dest_files=["res://.godot/imported/planning-1280x720.png-4961a39d8036e60c703f21f94b767a05.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
|
||||||
BIN
docs/luneraria/screenshots/planning-1920x1080.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
40
docs/luneraria/screenshots/planning-1920x1080.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://eg2x2tnm2r4a"
|
||||||
|
path="res://.godot/imported/planning-1920x1080.png-9474e0516c4f03723ac6abf839ff75d9.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/luneraria/screenshots/planning-1920x1080.png"
|
||||||
|
dest_files=["res://.godot/imported/planning-1920x1080.png-9474e0516c4f03723ac6abf839ff75d9.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
|
||||||
BIN
docs/luneraria/screenshots/planning.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
40
docs/luneraria/screenshots/planning.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cv4mo6gqrgr7l"
|
||||||
|
path="res://.godot/imported/planning.png-ea15c73d57d5147150129852dec1ced2.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/luneraria/screenshots/planning.png"
|
||||||
|
dest_files=["res://.godot/imported/planning.png-ea15c73d57d5147150129852dec1ced2.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
|
||||||
BIN
docs/luneraria/screenshots/result.png
Normal file
|
After Width: | Height: | Size: 595 KiB |
40
docs/luneraria/screenshots/result.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://coio2a87vbvp4"
|
||||||
|
path="res://.godot/imported/result.png-9d37465fb26c73ab171191d98bea3f04.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/luneraria/screenshots/result.png"
|
||||||
|
dest_files=["res://.godot/imported/result.png-9d37465fb26c73ab171191d98bea3f04.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
|
||||||
BIN
docs/luneraria/screenshots/settlement.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
40
docs/luneraria/screenshots/settlement.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cunkl1amyr2p2"
|
||||||
|
path="res://.godot/imported/settlement.png-fde2624ce641379c962c75deb3114891.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://docs/luneraria/screenshots/settlement.png"
|
||||||
|
dest_files=["res://.godot/imported/settlement.png-fde2624ce641379c962c75deb3114891.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
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"checks": 68,
|
"checks": 68,
|
||||||
"engine": "4.7.2-stable (fedora)",
|
"engine": "4.7.2-stable (fedora)",
|
||||||
"failures": [],
|
"failures": [],
|
||||||
"formations_ready_ms": 895,
|
"formations_ready_ms": 1089,
|
||||||
"menu_instantiate_ms": 515,
|
"menu_instantiate_ms": 571,
|
||||||
"renderer": "forward_plus"
|
"renderer": "forward_plus"
|
||||||
}
|
}
|
||||||
|
|
@ -14,10 +14,10 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
||||||
|
|
||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Saumwacht · Glutpakt"
|
config/name="Champions of Luneraria"
|
||||||
config/description="Glutpakt und Wurzelbund · 20 Wellen gegen die Rivalen-KI"
|
config/description="3 gegen 3 auf drei Lanes · Autobattler · Partie endet mit dem Fall eines Ancients"
|
||||||
config/version="0.15.0"
|
config/version="0.15.0"
|
||||||
run/main_scene="res://scenes/main.tscn"
|
run/main_scene="res://scenes/luneraria.tscn"
|
||||||
config/features=PackedStringArray("4.7", "Forward Plus")
|
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||||
config/icon="res://assets/icons/resources/res_core.png"
|
config/icon="res://assets/icons/resources/res_core.png"
|
||||||
|
|
||||||
|
|
|
||||||
6
scenes/luneraria.tscn
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/luneraria/lune_main.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="Luneraria" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
@ -14,8 +14,22 @@ func _ready() -> void:
|
||||||
read_settings()
|
read_settings()
|
||||||
apply()
|
apply()
|
||||||
|
|
||||||
|
## The project was renamed from "Saumwacht · Glutpakt"; its user folder changed with it.
|
||||||
|
const LEGACY_FOLDER := "Saumwacht · Glutpakt"
|
||||||
|
var migrated_from: String = ""
|
||||||
|
|
||||||
|
func _legacy_path() -> String:
|
||||||
|
var own: String = ProjectSettings.globalize_path("user://")
|
||||||
|
var parent: String = own.trim_suffix("/").get_base_dir()
|
||||||
|
return parent.path_join(LEGACY_FOLDER).path_join("preferences.cfg")
|
||||||
|
|
||||||
func read_settings() -> void:
|
func read_settings() -> void:
|
||||||
var config := ConfigFile.new()
|
var config := ConfigFile.new()
|
||||||
|
if storage_path == "user://preferences.cfg" and not FileAccess.file_exists(storage_path) and FileAccess.file_exists(_legacy_path()):
|
||||||
|
# One-time copy of the old options; the old file stays untouched.
|
||||||
|
if config.load(_legacy_path()) == OK and config.save(storage_path) == OK:
|
||||||
|
migrated_from = _legacy_path()
|
||||||
|
print("PREFERENCES: übernommen aus ", migrated_from)
|
||||||
if config.load(storage_path) != OK:
|
if config.load(storage_path) != OK:
|
||||||
return
|
return
|
||||||
volume = clampf(float(config.get_value("audio", "volume", .65)), 0.0, 1.0)
|
volume = clampf(float(config.get_value("audio", "volume", .65)), 0.0, 1.0)
|
||||||
|
|
|
||||||
69
scripts/luneraria/core/lune_balance.gd
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
class_name LuneBalance
|
||||||
|
extends Resource
|
||||||
|
## Every tunable number of Champions of Luneraria. The default instance lives in
|
||||||
|
## data/luneraria/balance_default.tres; tests may load variants, bots never get extras.
|
||||||
|
|
||||||
|
@export_group("Simulation")
|
||||||
|
## Fixed combat step in seconds.
|
||||||
|
@export var sim_step: float = 0.05
|
||||||
|
## Ends only the current combat phase, never the match (see REGELWERK).
|
||||||
|
@export var combat_time_limit: float = 180.0
|
||||||
|
## Lane marching speed relative to the catalogue speed of a unit.
|
||||||
|
@export var march_speed_factor: float = 1.6
|
||||||
|
@export var aggro_radius: float = 7.0
|
||||||
|
## Targets further away than this are dropped and re-acquired.
|
||||||
|
@export var leash_radius: float = 10.0
|
||||||
|
@export var lane_half_width: float = 2.6
|
||||||
|
|
||||||
|
@export_group("Economy")
|
||||||
|
@export var start_gold: int = 220
|
||||||
|
@export var base_income: int = 70
|
||||||
|
@export var income_per_round: int = 4
|
||||||
|
@export var income_growth_cap: int = 80
|
||||||
|
@export var supply_cap: int = 12
|
||||||
|
@export_range(0.0, 1.0) var sell_refund: float = 0.75
|
||||||
|
## Share of lane kill gold for the responsible lane player; the rest splits evenly in the team.
|
||||||
|
@export_range(0.0, 1.0) var lane_share: float = 0.5
|
||||||
|
|
||||||
|
@export_group("Waves")
|
||||||
|
@export var wave_base_count: int = 3
|
||||||
|
## One more monster per lane every N rounds, up to wave_max_count.
|
||||||
|
@export var wave_count_every: int = 3
|
||||||
|
@export var wave_max_count: int = 7
|
||||||
|
@export var wave_hp_growth: float = 0.11
|
||||||
|
@export var wave_damage_growth: float = 0.07
|
||||||
|
@export var wave_growth_quadratic: float = 0.0015
|
||||||
|
@export var monster_bounty_scale: float = 2.0
|
||||||
|
@export var bounty_growth: float = 0.04
|
||||||
|
## Monster types in rotation order; later indices unlock later (see LuneWaves).
|
||||||
|
@export var monster_pool: PackedStringArray = PackedStringArray(["e01", "e02", "e04", "e03", "e06", "e05"])
|
||||||
|
|
||||||
|
@export_group("Buildings")
|
||||||
|
@export var tower_outer_hp: float = 6000.0
|
||||||
|
@export var tower_inner_hp: float = 7500.0
|
||||||
|
@export var base_tower_hp: float = 9000.0
|
||||||
|
@export var core_hp: float = 12000.0
|
||||||
|
@export var tower_damage: float = 90.0
|
||||||
|
@export var base_tower_damage: float = 120.0
|
||||||
|
## Must exceed the longest unit reach (6.5 m + building radius) so towers cannot be out-ranged.
|
||||||
|
@export var tower_range: float = 8.6
|
||||||
|
@export var tower_attack_interval: float = 1.0
|
||||||
|
@export var building_radius: float = 1.3
|
||||||
|
@export var bounty_outer_tower: int = 150
|
||||||
|
@export var bounty_inner_tower: int = 210
|
||||||
|
@export var bounty_base_tower: int = 270
|
||||||
|
|
||||||
|
@export_group("Map")
|
||||||
|
## Inner / outer tower position as a fraction of the lane length from the own base.
|
||||||
|
@export var inner_tower_fraction: float = 0.17
|
||||||
|
@export var outer_tower_fraction: float = 0.35
|
||||||
|
@export var tower_side_offset: float = 3.4
|
||||||
|
|
||||||
|
static func default_balance() -> LuneBalance:
|
||||||
|
var path := "res://data/luneraria/balance_default.tres"
|
||||||
|
if ResourceLoader.exists(path):
|
||||||
|
return load(path) as LuneBalance
|
||||||
|
return LuneBalance.new()
|
||||||
|
|
||||||
|
func round_income(round_number: int) -> int:
|
||||||
|
return base_income + mini(income_growth_cap, income_per_round * maxi(0, round_number - 1))
|
||||||
1
scripts/luneraria/core/lune_balance.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://daf3k13ys862
|
||||||
157
scripts/luneraria/core/lune_bot.gd
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
class_name LuneBot
|
||||||
|
extends RefCounted
|
||||||
|
## Plans through LuneMatch.submit exactly like the human: same costs, same caps, same phase.
|
||||||
|
## Reads only public state (committed buildings, round, boss state, announcements) and its
|
||||||
|
## own draft, never other players' drafts.
|
||||||
|
|
||||||
|
const PROFILES: Dictionary = {
|
||||||
|
"ausgewogen": {"front": .30, "melee": .25, "ranged": .25, "splash": .10, "support": .10, "upgrade_from": 6, "support_lane": 0.0,
|
||||||
|
"hero": "held_ringborke", "hero_round": 3, "items": ["basaltschild", "glutklinge", "rankenmantel"], "talents": [0, 1], "champion": ""},
|
||||||
|
"verteidigung": {"front": .40, "melee": .15, "ranged": .20, "splash": .10, "support": .15, "upgrade_from": 5, "support_lane": 0.0,
|
||||||
|
"hero": "held_ringborke", "hero_round": 4, "items": ["rankenmantel", "basaltschild", "quellamulett"], "talents": [0, 0], "champion": ""},
|
||||||
|
"druck": {"front": .20, "melee": .35, "ranged": .25, "splash": .15, "support": .05, "upgrade_from": 8, "support_lane": 0.0,
|
||||||
|
"hero": "held_funkenlanze", "hero_round": 3, "items": ["glutklinge", "funkenfokus", "kristallsplitter"], "talents": [1, 1], "champion": "ch_belagerung"},
|
||||||
|
"fernkampf": {"front": .25, "melee": .10, "ranged": .40, "splash": .15, "support": .10, "upgrade_from": 6, "support_lane": 0.0,
|
||||||
|
"hero": "held_funkenlanze", "hero_round": 4, "items": ["funkenfokus", "glutklinge", "kristallsplitter"], "talents": [0, 0], "champion": "ch_druck"},
|
||||||
|
"teamhilfe": {"front": .30, "melee": .20, "ranged": .25, "splash": .10, "support": .15, "upgrade_from": 6, "support_lane": .35,
|
||||||
|
"hero": "held_quellgeflecht", "hero_round": 3, "items": ["quellamulett", "rankenmantel", "basaltschild"], "talents": [1, 0], "champion": "ch_schutz"},
|
||||||
|
"bossjagd": {"front": .35, "melee": .25, "ranged": .20, "splash": .10, "support": .10, "upgrade_from": 6, "support_lane": 0.0,
|
||||||
|
"hero": "held_ringborke", "hero_round": 3, "items": ["glutklinge", "basaltschild", "rankenmantel"], "talents": [1, 1], "champion": "",
|
||||||
|
"boss_from": 5, "boss_troops": 7},
|
||||||
|
}
|
||||||
|
|
||||||
|
static func profile_for(match_state: LuneMatch, slot: int) -> String:
|
||||||
|
var names: Array = PROFILES.keys()
|
||||||
|
return names[(slot * 7 + match_state.seed_value) % names.size()]
|
||||||
|
|
||||||
|
static func plan(match_state: LuneMatch, slot: int, profile_name: String = "") -> void:
|
||||||
|
var name: String = profile_name if not profile_name.is_empty() else profile_for(match_state, slot)
|
||||||
|
var profile: Dictionary = PROFILES[name]
|
||||||
|
var player: Dictionary = match_state.players[slot]
|
||||||
|
var ids: Array[String] = match_state.base_ids(player.bund)
|
||||||
|
var roles: Dictionary = {}
|
||||||
|
for id: String in ids:
|
||||||
|
roles[match_state.role_of(id)] = id
|
||||||
|
_plan_hero(match_state, slot, profile)
|
||||||
|
# Heroes get one planned item every second round before the army is topped up.
|
||||||
|
var hero: Dictionary = player.draft.hero
|
||||||
|
if not hero.is_empty() and (hero.items as Array).size() < LuneContent.ITEM_SLOTS and match_state.round_number % 2 == 0:
|
||||||
|
for item_id: String in profile.items:
|
||||||
|
if not (hero.items as Array).has(item_id):
|
||||||
|
match_state.submit(slot, {"type": "item_buy", "item_id": item_id})
|
||||||
|
break
|
||||||
|
# Champion bots invest in a pushing wave every third round before topping up the army.
|
||||||
|
if match_state.round_number % 3 == 0:
|
||||||
|
_plan_champion(match_state, slot, profile)
|
||||||
|
for guard: int in range(40):
|
||||||
|
var draft: Dictionary = player.draft
|
||||||
|
var roster: Array = draft.roster
|
||||||
|
var acted: bool = false
|
||||||
|
if roster.size() >= int(profile.upgrade_from) or roster.size() >= match_state.balance.supply_cap:
|
||||||
|
acted = _try_upgrade(match_state, slot, roster)
|
||||||
|
if not acted and roster.size() < match_state.balance.supply_cap:
|
||||||
|
var role: String = _needed_role(match_state, roster, profile)
|
||||||
|
var lane: String = player.lane
|
||||||
|
if float(profile.support_lane) > 0.0 and match_state.rng.randf() < float(profile.support_lane):
|
||||||
|
lane = _weakest_lane(match_state, int(player.team))
|
||||||
|
acted = match_state.submit(slot, {"type": "buy", "type_id": roles[role], "lane": lane}).ok
|
||||||
|
if not acted and roster.size() < int(profile.upgrade_from):
|
||||||
|
acted = _try_upgrade(match_state, slot, roster)
|
||||||
|
if not acted:
|
||||||
|
break
|
||||||
|
_plan_items(match_state, slot, profile)
|
||||||
|
_plan_boss(match_state, slot, profile)
|
||||||
|
match_state.set_ready(slot)
|
||||||
|
|
||||||
|
static func _plan_hero(match_state: LuneMatch, slot: int, profile: Dictionary) -> void:
|
||||||
|
var player: Dictionary = match_state.players[slot]
|
||||||
|
var hero: Dictionary = player.draft.hero
|
||||||
|
if hero.is_empty():
|
||||||
|
if match_state.round_number >= int(profile.hero_round):
|
||||||
|
match_state.submit(slot, {"type": "hero_buy", "hero_id": profile.hero})
|
||||||
|
return
|
||||||
|
var level: int = LuneContent.level_for(int(hero.xp))
|
||||||
|
var talents: Array = profile.talents
|
||||||
|
for tier: int in range(LuneContent.TALENT_LEVELS.size()):
|
||||||
|
if (hero.talents as Array).size() == tier and level >= LuneContent.TALENT_LEVELS[tier]:
|
||||||
|
match_state.submit(slot, {"type": "talent", "tier": tier, "choice": talents[tier]})
|
||||||
|
|
||||||
|
static func _plan_items(match_state: LuneMatch, slot: int, profile: Dictionary) -> void:
|
||||||
|
var hero: Dictionary = match_state.players[slot].draft.hero
|
||||||
|
if hero.is_empty():
|
||||||
|
return
|
||||||
|
for item_id: String in profile.items:
|
||||||
|
# Keep a reserve for the army: items only from surplus gold.
|
||||||
|
if int(match_state.players[slot].draft.gold) >= int(LuneContent.ITEMS[item_id].cost) + 60:
|
||||||
|
match_state.submit(slot, {"type": "item_buy", "item_id": item_id})
|
||||||
|
|
||||||
|
static func _plan_champion(match_state: LuneMatch, slot: int, profile: Dictionary) -> void:
|
||||||
|
var champion: String = str(profile.get("champion", ""))
|
||||||
|
if champion.is_empty() or match_state.round_number < 3:
|
||||||
|
return
|
||||||
|
var player: Dictionary = match_state.players[slot]
|
||||||
|
if int(player.draft.gold) >= int(LuneContent.CHAMPIONS[champion].cost) + 80:
|
||||||
|
match_state.submit(slot, {"type": "champion_book", "champion_id": champion, "lane": player.lane})
|
||||||
|
|
||||||
|
## Only boss-hunting bots go to the grotto, and only with a strong own army; the boss state
|
||||||
|
## and lock are public information.
|
||||||
|
static func _plan_boss(match_state: LuneMatch, slot: int, profile: Dictionary) -> void:
|
||||||
|
var player: Dictionary = match_state.players[slot]
|
||||||
|
var claim: Dictionary = match_state.boss.claim
|
||||||
|
if not claim.is_empty() and int(claim.chooser_slot) == slot and int(claim.deploy_round) == match_state.round_number:
|
||||||
|
var lane: String = _weakest_lane(match_state, 1 - int(player.team))
|
||||||
|
match_state.submit(slot, {"type": "boss_lane", "lane": lane})
|
||||||
|
if not profile.has("boss_from") or match_state.round_number < int(profile.boss_from) or not match_state.grotto_available():
|
||||||
|
return
|
||||||
|
var roster: Array = player.draft.roster
|
||||||
|
if roster.size() < int(profile.boss_troops):
|
||||||
|
return
|
||||||
|
var picks: Array = roster.duplicate()
|
||||||
|
picks.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
return int(a.seq) < int(b.seq) if int(a.invested) == int(b.invested) else int(a.invested) > int(b.invested))
|
||||||
|
for index: int in range(mini(int(profile.boss_troops) - 1, picks.size())):
|
||||||
|
match_state.submit(slot, {"type": "assign", "uid": picks[index].uid, "lane": LuneMatch.GROTTO})
|
||||||
|
var hero: Dictionary = player.draft.hero
|
||||||
|
if not hero.is_empty():
|
||||||
|
match_state.submit(slot, {"type": "assign", "uid": hero.uid, "lane": LuneMatch.GROTTO})
|
||||||
|
|
||||||
|
static func _needed_role(match_state: LuneMatch, roster: Array, profile: Dictionary) -> String:
|
||||||
|
var counts: Dictionary = {}
|
||||||
|
for entry: Dictionary in roster:
|
||||||
|
var role: String = match_state.role_of(entry.type_id)
|
||||||
|
counts[role] = int(counts.get(role, 0)) + 1
|
||||||
|
var best: String = "front"
|
||||||
|
var best_gap: float = -INF
|
||||||
|
for role: String in LuneMatch.ROW_ORDER:
|
||||||
|
var gap: float = float(profile[role]) * float(roster.size() + 1) - float(counts.get(role, 0))
|
||||||
|
if gap > best_gap + 0.0001:
|
||||||
|
best = role
|
||||||
|
best_gap = gap
|
||||||
|
return best
|
||||||
|
|
||||||
|
static func _try_upgrade(match_state: LuneMatch, slot: int, roster: Array) -> bool:
|
||||||
|
var best_uid: String = ""
|
||||||
|
var best_seq: int = 1 << 30
|
||||||
|
var best_cost: int = 1 << 30
|
||||||
|
for entry: Dictionary in roster:
|
||||||
|
var upgrade: String = str(DataCatalog.get_unit(entry.type_id).get("upgrade_id", ""))
|
||||||
|
if upgrade.is_empty():
|
||||||
|
continue
|
||||||
|
var cost: int = int(DataCatalog.get_unit(upgrade).cost)
|
||||||
|
if cost < best_cost or (cost == best_cost and int(entry.seq) < best_seq):
|
||||||
|
best_cost = cost
|
||||||
|
best_seq = int(entry.seq)
|
||||||
|
best_uid = str(entry.uid)
|
||||||
|
if best_uid.is_empty():
|
||||||
|
return false
|
||||||
|
return match_state.submit(slot, {"type": "upgrade", "uid": best_uid}).ok
|
||||||
|
|
||||||
|
static func _weakest_lane(match_state: LuneMatch, team: int) -> String:
|
||||||
|
var weakest: String = "mid"
|
||||||
|
var fewest: int = 99
|
||||||
|
for lane: String in LuneMap.LANES:
|
||||||
|
var towers: int = match_state.towers_alive(team, lane)
|
||||||
|
if towers < fewest:
|
||||||
|
fewest = towers
|
||||||
|
weakest = lane
|
||||||
|
return weakest
|
||||||
1
scripts/luneraria/core/lune_bot.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dy31vyfmvjk0s
|
||||||
641
scripts/luneraria/core/lune_combat.gd
Normal file
|
|
@ -0,0 +1,641 @@
|
||||||
|
class_name LuneCombat
|
||||||
|
extends RefCounted
|
||||||
|
## One automatic combat phase on all three lanes. Every step first gathers targets,
|
||||||
|
## movement, hits, heals and effects from the state at the start of the step, then applies
|
||||||
|
## them together, so processing order never decides who strikes first. Deaths of a step
|
||||||
|
## resolve together; two Ancients falling in the same step is a draw.
|
||||||
|
|
||||||
|
var map: LuneMap
|
||||||
|
var balance: LuneBalance
|
||||||
|
var round_number: int = 1
|
||||||
|
var units: Array[LuneUnit] = []
|
||||||
|
var buildings: Dictionary = {}
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
var steps: int = 0
|
||||||
|
var finished: bool = false
|
||||||
|
var end_reason: String = ""
|
||||||
|
var destroyed_cores: Array[int] = []
|
||||||
|
var events: Array[Dictionary] = []
|
||||||
|
## Neutral grotto boss (team 2) and the resulting claim for this combat phase.
|
||||||
|
const NEUTRAL: int = 2
|
||||||
|
var grotto_boss: LuneUnit = null
|
||||||
|
var grotto_resolved: bool = true
|
||||||
|
var grotto_claim_team: int = -1
|
||||||
|
var grotto_active_team: int = -1
|
||||||
|
var grotto_outcome: String = "none"
|
||||||
|
var _next_id: int = 1
|
||||||
|
var _team_count: Array[int] = [0, 0, 0]
|
||||||
|
## Units without a target look for one every RETARGET steps, staggered by team_index.
|
||||||
|
const RETARGET: int = 4
|
||||||
|
var _event_serial: int = 0
|
||||||
|
var _pending_damage: Dictionary = {}
|
||||||
|
var _pending_heal: Dictionary = {}
|
||||||
|
var _pending_effects: Array = []
|
||||||
|
var _pending_dots: Array = []
|
||||||
|
var _pending_shields: Array = []
|
||||||
|
# Per-step caches: building gates, march limits and a spatial grid of mobile units.
|
||||||
|
var _attackable: Dictionary = {}
|
||||||
|
var _limits: Dictionary = {}
|
||||||
|
var _grid: Dictionary = {}
|
||||||
|
const CELL: float = 4.0
|
||||||
|
## Optional section timings (microseconds) for performance work; off by default.
|
||||||
|
var profiling: bool = false
|
||||||
|
var profile: Dictionary = {}
|
||||||
|
var _mark: int = 0
|
||||||
|
|
||||||
|
func _lap(section: String) -> void:
|
||||||
|
if profiling:
|
||||||
|
var now: int = Time.get_ticks_usec()
|
||||||
|
profile[section] = int(profile.get(section, 0)) + now - _mark
|
||||||
|
_mark = now
|
||||||
|
|
||||||
|
func _init(match_map: LuneMap, config: LuneBalance, number: int) -> void:
|
||||||
|
map = match_map
|
||||||
|
balance = config
|
||||||
|
round_number = number
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- setup
|
||||||
|
func add_building(definition: Dictionary, hp: float) -> LuneUnit:
|
||||||
|
var unit := LuneUnit.new()
|
||||||
|
unit.id = _next_id
|
||||||
|
_next_id += 1
|
||||||
|
unit.kind = "core" if definition.tier == "core" else "tower"
|
||||||
|
unit.team = int(definition.team)
|
||||||
|
unit.lane = str(definition.lane)
|
||||||
|
unit.building_id = str(definition.id)
|
||||||
|
unit.team_index = _team_count[unit.team]
|
||||||
|
_team_count[unit.team] += 1
|
||||||
|
unit.def = {"id": definition.id, "display_name": definition.id, "max_hp": definition.max_hp, "damage": definition.damage,
|
||||||
|
"attack_range": definition.range, "attack_type": 1, "armor_type": 1, "speed": 0.0, "ability": "", "bounty": definition.bounty,
|
||||||
|
"tier": definition.tier}
|
||||||
|
unit.position = definition.position
|
||||||
|
unit.previous_position = unit.position
|
||||||
|
unit.max_hp = float(definition.max_hp)
|
||||||
|
unit.hp = clampf(hp, 0.0, unit.max_hp)
|
||||||
|
unit.alive = unit.hp > 0.0
|
||||||
|
units.append(unit)
|
||||||
|
buildings[unit.building_id] = unit
|
||||||
|
return unit
|
||||||
|
|
||||||
|
func spawn(kind: String, team: int, lane: String, definition: Dictionary, progress: float, lateral: float, slot: int = -1, uid: String = "") -> LuneUnit:
|
||||||
|
var unit := LuneUnit.new()
|
||||||
|
unit.id = _next_id
|
||||||
|
_next_id += 1
|
||||||
|
unit.kind = kind
|
||||||
|
unit.team = team
|
||||||
|
unit.lane = lane
|
||||||
|
unit.def = definition
|
||||||
|
unit.owner_slot = slot
|
||||||
|
unit.persistent_uid = uid
|
||||||
|
unit.team_index = _team_count[team]
|
||||||
|
_team_count[team] += 1
|
||||||
|
unit.progress = progress
|
||||||
|
unit.lateral = lateral
|
||||||
|
unit.position = map.lane_point(team, lane, progress, lateral)
|
||||||
|
unit.previous_position = unit.position
|
||||||
|
var forward: Vector2 = map.lane_forward(team, lane, progress)
|
||||||
|
unit.facing = atan2(forward.x, forward.y)
|
||||||
|
unit.max_hp = float(definition.get("max_hp", 1.0))
|
||||||
|
unit.hp = unit.max_hp
|
||||||
|
_start_ability(unit)
|
||||||
|
units.append(unit)
|
||||||
|
return unit
|
||||||
|
|
||||||
|
func spawn_at(kind: String, team: int, zone: String, definition: Dictionary, at: Vector2, slot: int = -1, uid: String = "") -> LuneUnit:
|
||||||
|
var unit: LuneUnit = spawn(kind, team, "mid", definition, 0.0, 0.0, slot, uid)
|
||||||
|
unit.zone = zone
|
||||||
|
unit.lane = zone
|
||||||
|
unit.position = at
|
||||||
|
unit.previous_position = at
|
||||||
|
return unit
|
||||||
|
|
||||||
|
func add_grotto_boss(definition: Dictionary) -> LuneUnit:
|
||||||
|
grotto_boss = spawn_at("boss", NEUTRAL, "grotto", definition, map.grotto_boss_point())
|
||||||
|
grotto_boss.dormant = true
|
||||||
|
grotto_resolved = false
|
||||||
|
return grotto_boss
|
||||||
|
|
||||||
|
func alive_map() -> Dictionary:
|
||||||
|
var result: Dictionary = {}
|
||||||
|
for id: String in buildings:
|
||||||
|
result[id] = (buildings[id] as LuneUnit).alive
|
||||||
|
return result
|
||||||
|
|
||||||
|
func building_attackable(unit: LuneUnit, gates: Dictionary) -> bool:
|
||||||
|
if not unit.alive:
|
||||||
|
return false
|
||||||
|
if _attackable.has(unit.building_id):
|
||||||
|
return _attackable[unit.building_id]
|
||||||
|
return LuneMap.is_attackable(map.building_def(unit.building_id), gates)
|
||||||
|
|
||||||
|
func _prepare_caches(living: Array[LuneUnit], gates: Dictionary) -> void:
|
||||||
|
_attackable.clear()
|
||||||
|
for id: String in buildings:
|
||||||
|
_attackable[id] = LuneMap.is_attackable(map.building_def(id), gates)
|
||||||
|
_limits.clear()
|
||||||
|
for team: int in range(2):
|
||||||
|
for lane: String in LuneMap.LANES:
|
||||||
|
var path: LuneMap.LanePath = map.lanes[lane]
|
||||||
|
var limit: float = path.length
|
||||||
|
for id: String in buildings:
|
||||||
|
var building: LuneUnit = buildings[id]
|
||||||
|
if building.alive and building.kind == "tower" and building.team != team and building.lane == lane:
|
||||||
|
limit = minf(limit, path.length - float(map.building_def(id).progress) - 1.5)
|
||||||
|
_limits[team * 3 + LuneMap.LANES.find(lane)] = limit
|
||||||
|
_grid = {0: {}, 1: {}, 2: {}}
|
||||||
|
for unit: LuneUnit in living:
|
||||||
|
if unit.is_mobile():
|
||||||
|
var cells: Dictionary = _grid[unit.team]
|
||||||
|
var key := Vector2i(floori(unit.position.x / CELL), floori(unit.position.y / CELL))
|
||||||
|
if not cells.has(key):
|
||||||
|
cells[key] = []
|
||||||
|
(cells[key] as Array).append(unit)
|
||||||
|
|
||||||
|
static func hostile_teams(team: int) -> Array:
|
||||||
|
return [0, 1] if team == NEUTRAL else [1 - team, NEUTRAL]
|
||||||
|
|
||||||
|
## Mobile units of the given teams near a point; the caller checks distance.
|
||||||
|
func _near_mobile(center: Vector2, radius: float, teams: Array) -> Array:
|
||||||
|
var result: Array = []
|
||||||
|
var reach: int = ceili(radius / CELL)
|
||||||
|
var cx: int = floori(center.x / CELL)
|
||||||
|
var cy: int = floori(center.y / CELL)
|
||||||
|
for side: int in teams:
|
||||||
|
var cells: Dictionary = _grid[side]
|
||||||
|
if cells.is_empty():
|
||||||
|
continue
|
||||||
|
for dx: int in range(-reach, reach + 1):
|
||||||
|
for dy: int in range(-reach, reach + 1):
|
||||||
|
var bucket: Variant = cells.get(Vector2i(cx + dx, cy + dy))
|
||||||
|
if bucket != null:
|
||||||
|
result.append_array(bucket)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- step
|
||||||
|
func step() -> void:
|
||||||
|
if finished:
|
||||||
|
return
|
||||||
|
var dt: float = balance.sim_step
|
||||||
|
elapsed += dt
|
||||||
|
steps += 1
|
||||||
|
_pending_damage.clear()
|
||||||
|
_pending_heal.clear()
|
||||||
|
_pending_effects.clear()
|
||||||
|
_pending_dots.clear()
|
||||||
|
_pending_shields.clear()
|
||||||
|
_mark = Time.get_ticks_usec()
|
||||||
|
var gates: Dictionary = alive_map()
|
||||||
|
var living: Array[LuneUnit] = []
|
||||||
|
for unit: LuneUnit in units:
|
||||||
|
if unit.alive:
|
||||||
|
living.append(unit)
|
||||||
|
unit.previous_position = unit.position
|
||||||
|
_prepare_caches(living, gates)
|
||||||
|
_lap("caches")
|
||||||
|
for unit: LuneUnit in living:
|
||||||
|
var dot: float = unit.advance_status(dt)
|
||||||
|
if dot > 0.0:
|
||||||
|
_queue_damage(unit, dot, gates)
|
||||||
|
_lap("status")
|
||||||
|
for unit: LuneUnit in living:
|
||||||
|
if unit.is_mobile():
|
||||||
|
_ability_step(unit, living)
|
||||||
|
_lap("abilities")
|
||||||
|
for unit: LuneUnit in living:
|
||||||
|
if unit.is_mobile() or (unit.kind == "tower"):
|
||||||
|
var keep: bool = unit.target != null and unit.target.alive
|
||||||
|
if keep or (steps + unit.team_index) % RETARGET == 0:
|
||||||
|
unit.target = _choose_target(unit, living, gates)
|
||||||
|
_lap("targets")
|
||||||
|
var moves: Dictionary = {}
|
||||||
|
for unit: LuneUnit in living:
|
||||||
|
if unit.is_mobile():
|
||||||
|
moves[unit.id] = _plan_move(unit, living, dt)
|
||||||
|
_lap("plan_move")
|
||||||
|
for unit: LuneUnit in living:
|
||||||
|
if moves.has(unit.id):
|
||||||
|
var plan: Array = moves[unit.id]
|
||||||
|
var next: Vector2 = plan[0]
|
||||||
|
var heading: Vector2 = next - unit.position
|
||||||
|
unit.position = next
|
||||||
|
if heading.length_squared() > 0.000001:
|
||||||
|
unit.facing = atan2(heading.x, heading.y)
|
||||||
|
if unit.zone == "lane":
|
||||||
|
_update_progress(unit, float(plan[1]))
|
||||||
|
_lap("apply_move")
|
||||||
|
for unit: LuneUnit in living:
|
||||||
|
_attack(unit, living, gates, dt)
|
||||||
|
_lap("attacks")
|
||||||
|
for unit: LuneUnit in living:
|
||||||
|
if _pending_heal.has(unit.id):
|
||||||
|
unit.heal(_pending_heal[unit.id])
|
||||||
|
for entry: Array in _pending_shields:
|
||||||
|
(entry[0] as LuneUnit).grant_shield(entry[1], entry[2])
|
||||||
|
for entry: Array in _pending_effects:
|
||||||
|
(entry[0] as LuneUnit).apply_effect(entry[1], entry[2], entry[3], entry[4])
|
||||||
|
for entry: Array in _pending_dots:
|
||||||
|
(entry[0] as LuneUnit).apply_dot(entry[1], entry[2], entry[3], entry[4])
|
||||||
|
for unit: LuneUnit in living:
|
||||||
|
if _pending_damage.has(unit.id):
|
||||||
|
unit.take(_pending_damage[unit.id])
|
||||||
|
for unit: LuneUnit in living:
|
||||||
|
if unit.hp <= 0.0 and unit.alive:
|
||||||
|
unit.alive = false
|
||||||
|
unit.death_time = elapsed
|
||||||
|
unit.target = null
|
||||||
|
_record_death(unit)
|
||||||
|
_update_grotto()
|
||||||
|
_lap("resolve")
|
||||||
|
_check_end()
|
||||||
|
|
||||||
|
## Expeditions fight each other first; the boss wakes when at most one team remains.
|
||||||
|
func _update_grotto() -> void:
|
||||||
|
if grotto_resolved or grotto_boss == null:
|
||||||
|
return
|
||||||
|
var teams_alive: Array[int] = []
|
||||||
|
for unit: LuneUnit in units:
|
||||||
|
if unit.alive and unit.zone == "grotto" and unit.team != NEUTRAL and not teams_alive.has(unit.team):
|
||||||
|
teams_alive.append(unit.team)
|
||||||
|
if grotto_boss.dormant and teams_alive.size() <= 1:
|
||||||
|
grotto_boss.dormant = false
|
||||||
|
grotto_active_team = teams_alive[0] if teams_alive.size() == 1 else -1
|
||||||
|
if not grotto_boss.alive:
|
||||||
|
grotto_resolved = true
|
||||||
|
grotto_claim_team = grotto_active_team
|
||||||
|
grotto_outcome = "success" if grotto_claim_team >= 0 else "failed"
|
||||||
|
elif teams_alive.is_empty():
|
||||||
|
grotto_resolved = true
|
||||||
|
grotto_outcome = "failed"
|
||||||
|
|
||||||
|
func _check_end() -> void:
|
||||||
|
if not destroyed_cores.is_empty():
|
||||||
|
finished = true
|
||||||
|
end_reason = "core"
|
||||||
|
return
|
||||||
|
var mobile_alive: bool = not grotto_resolved
|
||||||
|
for unit: LuneUnit in units:
|
||||||
|
if unit.alive and unit.is_mobile() and unit.zone == "lane":
|
||||||
|
mobile_alive = true
|
||||||
|
break
|
||||||
|
if not mobile_alive:
|
||||||
|
finished = true
|
||||||
|
end_reason = "cleared"
|
||||||
|
elif elapsed >= balance.combat_time_limit - 0.000001:
|
||||||
|
finished = true
|
||||||
|
end_reason = "timeout"
|
||||||
|
|
||||||
|
func _record_death(unit: LuneUnit) -> void:
|
||||||
|
_event_serial += 1
|
||||||
|
var event: Dictionary = {"serial": _event_serial, "step": steps, "time": elapsed, "team": unit.team, "lane": unit.lane,
|
||||||
|
"zone": unit.zone, "kind": unit.kind, "unit_id": unit.id, "def_id": str(unit.def.get("id", "")), "bounty": 0,
|
||||||
|
"owner_slot": unit.owner_slot, "persistent_uid": unit.persistent_uid}
|
||||||
|
if unit.kind in ["monster", "champion", "boss"]:
|
||||||
|
event.bounty = int(unit.def.get("bounty", 0))
|
||||||
|
elif unit.is_building():
|
||||||
|
event.bounty = int(unit.def.get("bounty", 0))
|
||||||
|
event.building_id = unit.building_id
|
||||||
|
if unit.kind == "core" and not destroyed_cores.has(unit.team):
|
||||||
|
destroyed_cores.append(unit.team)
|
||||||
|
# Participation XP: every opposing hero near the death shares it, whoever landed the hit.
|
||||||
|
var xp: int = 0
|
||||||
|
match unit.kind:
|
||||||
|
"monster": xp = 12 + round_number
|
||||||
|
"champion": xp = 30 + 2 * round_number
|
||||||
|
"boss": xp = LuneContent.BOSS_XP
|
||||||
|
"troop", "hero": xp = 15
|
||||||
|
"tower", "core": xp = LuneContent.BUILDING_XP
|
||||||
|
var heroes: Array[String] = []
|
||||||
|
var radius: float = LuneContent.XP_RADIUS + (2.0 if unit.is_building() else 0.0)
|
||||||
|
for other: LuneUnit in units:
|
||||||
|
if other.kind == "hero" and other.alive and other.zone == unit.zone and other.team != unit.team and other.position.distance_to(unit.position) <= radius:
|
||||||
|
heroes.append(other.persistent_uid)
|
||||||
|
event.xp = xp
|
||||||
|
event.xp_heroes = heroes
|
||||||
|
events.append(event)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- targeting and movement
|
||||||
|
func _hostile(unit: LuneUnit, other: LuneUnit) -> bool:
|
||||||
|
return other.alive and other.team != unit.team and other.zone == unit.zone and not other.dormant and not unit.dormant
|
||||||
|
|
||||||
|
func _choose_target(unit: LuneUnit, living: Array[LuneUnit], gates: Dictionary) -> LuneUnit:
|
||||||
|
var current: LuneUnit = unit.target
|
||||||
|
if current != null and _hostile(unit, current) and unit.position.distance_to(current.position) <= balance.leash_radius:
|
||||||
|
if not current.is_building() or building_attackable(current, gates):
|
||||||
|
if unit.kind != "tower" or unit.position.distance_to(current.position) <= unit.attack_range():
|
||||||
|
return current
|
||||||
|
var radius: float = unit.attack_range() if unit.kind == "tower" else balance.aggro_radius
|
||||||
|
var best: LuneUnit = null
|
||||||
|
var best_distance: float = INF
|
||||||
|
var priority: String = str(unit.def.get("priority", ""))
|
||||||
|
if priority == "building":
|
||||||
|
# Siege champions go for the nearest attackable building before anything else.
|
||||||
|
for id: String in buildings:
|
||||||
|
var building: LuneUnit = buildings[id]
|
||||||
|
if _hostile(unit, building) and building_attackable(building, gates):
|
||||||
|
var reach_d: float = unit.position.distance_to(building.position) - balance.building_radius
|
||||||
|
if reach_d <= balance.aggro_radius and reach_d < best_distance:
|
||||||
|
best = building
|
||||||
|
best_distance = reach_d
|
||||||
|
if best != null:
|
||||||
|
return best
|
||||||
|
for other: LuneUnit in _near_mobile(unit.position, radius, hostile_teams(unit.team)):
|
||||||
|
if not _hostile(unit, other):
|
||||||
|
continue
|
||||||
|
var d: float = unit.position.distance_to(other.position)
|
||||||
|
# Pressure champions treat support and ranged enemies as 6 m closer.
|
||||||
|
if priority == "support" and other.role() in ["support", "ranged"]:
|
||||||
|
d -= 6.0
|
||||||
|
if d <= radius and (d < best_distance - 0.0001 or (absf(d - best_distance) <= 0.0001 and other.id < best.id)):
|
||||||
|
best = other
|
||||||
|
best_distance = d
|
||||||
|
if best != null or unit.kind == "tower":
|
||||||
|
return best
|
||||||
|
# Buildings: nearby attackable ones; at the end of a lane the whole base is in reach.
|
||||||
|
for id: String in buildings:
|
||||||
|
var other: LuneUnit = buildings[id]
|
||||||
|
if not _hostile(unit, other) or not building_attackable(other, gates):
|
||||||
|
continue
|
||||||
|
var d: float = unit.position.distance_to(other.position) - balance.building_radius
|
||||||
|
if (d <= radius or unit.reached_base) and d < best_distance:
|
||||||
|
best = other
|
||||||
|
best_distance = d
|
||||||
|
return best
|
||||||
|
|
||||||
|
func _march_limit(unit: LuneUnit) -> float:
|
||||||
|
var key: int = unit.team * 3 + LuneMap.LANES.find(unit.lane)
|
||||||
|
if _limits.has(key):
|
||||||
|
return _limits[key]
|
||||||
|
var path: LuneMap.LanePath = map.lanes[unit.lane]
|
||||||
|
var limit: float = path.length
|
||||||
|
for id: String in buildings:
|
||||||
|
var building: LuneUnit = buildings[id]
|
||||||
|
if building.alive and building.kind == "tower" and building.team != unit.team and building.lane == unit.lane:
|
||||||
|
limit = minf(limit, path.length - float(map.building_def(id).progress) - 1.5)
|
||||||
|
return limit
|
||||||
|
|
||||||
|
## Returns [next position, marching progress]. Marching advances the lane parameter
|
||||||
|
## directly, so lateral formation offsets cannot stall a unit on the inside of a bend.
|
||||||
|
func _plan_move(unit: LuneUnit, living: Array[LuneUnit], dt: float) -> Array:
|
||||||
|
var step_length: float = unit.speed() * balance.march_speed_factor * unit.movement_factor() * dt
|
||||||
|
var goal: Vector2 = unit.position
|
||||||
|
var travel: float = 0.0
|
||||||
|
var marched: float = unit.progress
|
||||||
|
if unit.target != null:
|
||||||
|
var reach: float = unit.attack_range() + (balance.building_radius if unit.target.is_building() else 0.0)
|
||||||
|
var gap: float = unit.position.distance_to(unit.target.position) - reach
|
||||||
|
if gap > 0.0:
|
||||||
|
goal = unit.target.position
|
||||||
|
travel = minf(step_length, gap + 0.02)
|
||||||
|
elif unit.zone == "grotto":
|
||||||
|
if unit.kind != "boss" and grotto_boss != null and grotto_boss.alive:
|
||||||
|
var gap_boss: float = unit.position.distance_to(grotto_boss.position) - unit.attack_range()
|
||||||
|
if gap_boss > 0.0:
|
||||||
|
goal = grotto_boss.position
|
||||||
|
travel = minf(step_length, gap_boss + 0.02)
|
||||||
|
elif unit.zone == "lane":
|
||||||
|
var limit: float = _march_limit(unit)
|
||||||
|
var anchor: Vector2 = map.lane_point(unit.team, unit.lane, unit.progress, unit.lateral)
|
||||||
|
var off: float = unit.position.distance_to(anchor)
|
||||||
|
if off > 1.2:
|
||||||
|
# Rejoin the formation line after a chase before advancing again.
|
||||||
|
goal = anchor
|
||||||
|
travel = minf(step_length, unit.position.distance_to(anchor))
|
||||||
|
elif unit.progress < limit - 0.01:
|
||||||
|
marched = minf(unit.progress + step_length, limit)
|
||||||
|
goal = map.lane_point(unit.team, unit.lane, marched, unit.lateral)
|
||||||
|
travel = unit.position.distance_to(goal)
|
||||||
|
elif off > 0.05:
|
||||||
|
goal = anchor
|
||||||
|
travel = minf(step_length, unit.position.distance_to(anchor))
|
||||||
|
var next: Vector2 = unit.position
|
||||||
|
if travel > 0.0 and goal.distance_squared_to(unit.position) > 0.000001:
|
||||||
|
next += (goal - unit.position).normalized() * minf(travel, unit.position.distance_to(goal))
|
||||||
|
# Separation from allies so bodies do not stack; opponents may still close to melee range.
|
||||||
|
var push := Vector2.ZERO
|
||||||
|
for other: LuneUnit in _near_mobile(unit.position, 1.1, [unit.team]):
|
||||||
|
if other == unit or other.zone != unit.zone:
|
||||||
|
continue
|
||||||
|
var offset: Vector2 = unit.position - other.position
|
||||||
|
var distance: float = offset.length()
|
||||||
|
if distance < 1.1:
|
||||||
|
if distance < 0.0001:
|
||||||
|
# Mirrored teams sidestep in mirrored directions.
|
||||||
|
offset = Vector2.RIGHT if unit.id < other.id else Vector2.LEFT
|
||||||
|
if unit.team == 1:
|
||||||
|
offset = LuneMap.mirror(offset)
|
||||||
|
distance = 0.001
|
||||||
|
push += offset.normalized() * (1.1 - distance) * 2.0
|
||||||
|
next += push.limit_length(0.9) * dt
|
||||||
|
next.x = clampf(next.x, -LuneMap.HALF_EXTENT, LuneMap.HALF_EXTENT)
|
||||||
|
next.y = clampf(next.y, -LuneMap.HALF_EXTENT, LuneMap.HALF_EXTENT)
|
||||||
|
return [next, marched]
|
||||||
|
|
||||||
|
func _update_progress(unit: LuneUnit, marched: float) -> void:
|
||||||
|
var path: LuneMap.LanePath = map.lanes[unit.lane]
|
||||||
|
var limit: float = _march_limit(unit)
|
||||||
|
if unit.target != null:
|
||||||
|
# After a chase the unit continues from where it now stands, never further back.
|
||||||
|
var blue: Vector2 = unit.position if unit.team == 0 else LuneMap.mirror(unit.position)
|
||||||
|
marched = maxf(marched, minf(path.project(blue), limit))
|
||||||
|
unit.progress = maxf(unit.progress, minf(marched, maxf(limit, unit.progress)))
|
||||||
|
if unit.progress >= path.length - 0.6:
|
||||||
|
unit.reached_base = true
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- attacks
|
||||||
|
func _attack(unit: LuneUnit, living: Array[LuneUnit], gates: Dictionary, dt: float) -> void:
|
||||||
|
unit.in_range = false
|
||||||
|
var target: LuneUnit = unit.target
|
||||||
|
if target == null or not target.alive or unit.kind == "core":
|
||||||
|
unit.attack_progress = 0.0
|
||||||
|
return
|
||||||
|
var reach: float = unit.attack_range() + (balance.building_radius if target.is_building() else 0.0) + 0.035
|
||||||
|
if unit.position.distance_to(target.position) > reach:
|
||||||
|
unit.attack_progress = 0.0
|
||||||
|
return
|
||||||
|
if target.is_building() and not building_attackable(target, gates):
|
||||||
|
unit.attack_progress = 0.0
|
||||||
|
return
|
||||||
|
unit.in_range = true
|
||||||
|
var interval: float = balance.tower_attack_interval if unit.kind == "tower" else 1.0
|
||||||
|
unit.attack_progress += dt * unit.attack_speed_factor() / interval
|
||||||
|
while unit.attack_progress >= 1.0 - 0.000001:
|
||||||
|
unit.attack_progress = maxf(0.0, unit.attack_progress - 1.0)
|
||||||
|
unit.hit_count += 1
|
||||||
|
unit.attack_serial += 1
|
||||||
|
var damage: float = float(unit.def.get("damage", 0.0)) * (1.0 + unit.damage_bonus())
|
||||||
|
if unit.hit_count % 4 == 0:
|
||||||
|
if unit.ability() == "g05":
|
||||||
|
damage *= 2.1 if unit.has_talent("durchschlag") else 1.5
|
||||||
|
elif unit.ability() == "g06":
|
||||||
|
damage *= 1.75
|
||||||
|
if unit.hit_count % 3 == 0 and unit.has_talent("stammschlag"):
|
||||||
|
damage *= 1.6
|
||||||
|
if target.is_building():
|
||||||
|
damage *= float(unit.def.get("siege_factor", 1.0))
|
||||||
|
_queue_damage(target, target.mitigated(damage, int(unit.def.get("attack_type", 0))), gates)
|
||||||
|
_on_hit(unit, target, living, gates)
|
||||||
|
# Item effects come last and never trigger further on-hit effects.
|
||||||
|
var item_splash: float = float(unit.def.get("item_splash", 0.0))
|
||||||
|
if item_splash > 0.0:
|
||||||
|
_splash(unit, target, living, gates, item_splash, 1.5, 2)
|
||||||
|
|
||||||
|
func _queue_damage(target: LuneUnit, amount: float, gates: Dictionary) -> void:
|
||||||
|
if amount <= 0.0 or not target.alive:
|
||||||
|
return
|
||||||
|
# The building gate applies to every source: ranged, splash, DoT and abilities.
|
||||||
|
if target.is_building() and not building_attackable(target, gates):
|
||||||
|
return
|
||||||
|
_pending_damage[target.id] = float(_pending_damage.get(target.id, 0.0)) + amount
|
||||||
|
|
||||||
|
func _queue_heal(target: LuneUnit, amount: float) -> void:
|
||||||
|
_pending_heal[target.id] = float(_pending_heal.get(target.id, 0.0)) + amount
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- abilities (ported from the Saumwacht rules)
|
||||||
|
func _start_ability(unit: LuneUnit) -> void:
|
||||||
|
unit.ability_ready = 6.0
|
||||||
|
match unit.ability():
|
||||||
|
"w03", "w04": unit.ability_ready = 5.0
|
||||||
|
"w09", "w10": unit.ability_ready = 4.0
|
||||||
|
"s02", "ch_schutz": unit.grant_shield(unit.max_hp * 0.15)
|
||||||
|
"e08": unit.grant_shield(unit.max_hp * 0.20)
|
||||||
|
|
||||||
|
func _nearby(unit: LuneUnit, living: Array[LuneUnit], allied: bool, center: Vector2, radius: float, maximum: int, excluded: LuneUnit = null, health_priority: bool = false) -> Array[LuneUnit]:
|
||||||
|
var result: Array[LuneUnit] = []
|
||||||
|
# Buildings can be splash victims; they are few, so check them directly.
|
||||||
|
var pool: Array = _near_mobile(center, radius, [unit.team] if allied else hostile_teams(unit.team))
|
||||||
|
if not allied:
|
||||||
|
for id: String in buildings:
|
||||||
|
pool.append(buildings[id])
|
||||||
|
for other: LuneUnit in pool:
|
||||||
|
if not other.alive or other == excluded or other.zone != unit.zone:
|
||||||
|
continue
|
||||||
|
if (other.team == unit.team) != allied or other.dormant:
|
||||||
|
continue
|
||||||
|
if allied and other.is_building():
|
||||||
|
continue
|
||||||
|
if other.position.distance_to(center) <= radius + 0.000001:
|
||||||
|
result.append(other)
|
||||||
|
result.sort_custom(func(a: LuneUnit, b: LuneUnit) -> bool:
|
||||||
|
if health_priority and not is_equal_approx(a.hp / a.max_hp, b.hp / b.max_hp):
|
||||||
|
return a.hp / a.max_hp < b.hp / b.max_hp
|
||||||
|
var ad: float = a.position.distance_to(center)
|
||||||
|
var bd: float = b.position.distance_to(center)
|
||||||
|
return a.id < b.id if is_equal_approx(ad, bd) else ad < bd)
|
||||||
|
if result.size() > maximum:
|
||||||
|
result.resize(maximum)
|
||||||
|
return result
|
||||||
|
|
||||||
|
func _ability_step(unit: LuneUnit, living: Array[LuneUnit]) -> void:
|
||||||
|
var regen: float = float(unit.def.get("regen", 0.0))
|
||||||
|
if regen > 0.0 and unit.hp < unit.max_hp:
|
||||||
|
_queue_heal(unit, unit.max_hp * regen * balance.sim_step)
|
||||||
|
if unit.has_talent("schutzwurzeln") and unit.combat_time + 0.000001 >= unit.aura_ready:
|
||||||
|
for ally: LuneUnit in _nearby(unit, living, true, unit.position, 4.0, 4):
|
||||||
|
_pending_effects.append([ally, "bark_guard", "reduction", 0.10, 4.5])
|
||||||
|
unit.aura_ready = unit.combat_time + 4.0
|
||||||
|
if unit.combat_time + 0.000001 < unit.ability_ready:
|
||||||
|
return
|
||||||
|
var ability: String = unit.ability()
|
||||||
|
var success: bool = false
|
||||||
|
match ability:
|
||||||
|
"w03", "w04":
|
||||||
|
if unit.hp < unit.max_hp:
|
||||||
|
_queue_heal(unit, 100.0 if ability == "w04" else 35.0)
|
||||||
|
success = true
|
||||||
|
"w09", "w10":
|
||||||
|
var upgraded: bool = ability == "w10"
|
||||||
|
var radius: float = 4.5 if upgraded else 4.0
|
||||||
|
var count: int = 3 if upgraded else 1
|
||||||
|
var amount: float = 80.0 if upgraded else 65.0
|
||||||
|
if unit.has_talent("tiefe_quelle"):
|
||||||
|
count = 1
|
||||||
|
amount = 190.0
|
||||||
|
elif unit.has_talent("weite_quelle"):
|
||||||
|
radius = 5.5
|
||||||
|
count = 5
|
||||||
|
amount = 65.0
|
||||||
|
for ally: LuneUnit in _nearby(unit, living, true, unit.position, radius, count, null, true):
|
||||||
|
if ally.hp < ally.max_hp:
|
||||||
|
_queue_heal(ally, amount)
|
||||||
|
if unit.has_talent("quellschild"):
|
||||||
|
_pending_shields.append([ally, 70.0, 4.0])
|
||||||
|
success = true
|
||||||
|
"ch_schutz":
|
||||||
|
var allies: Array[LuneUnit] = _nearby(unit, living, true, unit.position, 4.0, 4)
|
||||||
|
for ally: LuneUnit in allies:
|
||||||
|
_pending_shields.append([ally, 120.0, 6.0])
|
||||||
|
success = not allies.is_empty()
|
||||||
|
"g09", "g10":
|
||||||
|
var upgraded: bool = ability == "g10"
|
||||||
|
var allies: Array[LuneUnit] = _nearby(unit, living, true, unit.position, 4.0 if upgraded else 3.0, 4 if upgraded else 3, null, true)
|
||||||
|
for ally: LuneUnit in allies:
|
||||||
|
_pending_effects.append([ally, "hearth_warmth", "damage", 0.15 if upgraded else 0.10, 4.0])
|
||||||
|
if upgraded:
|
||||||
|
_queue_heal(ally, 45.0)
|
||||||
|
success = not allies.is_empty()
|
||||||
|
"e06":
|
||||||
|
for ally: LuneUnit in _nearby(unit, living, true, unit.position, 4.0, 100, null, true):
|
||||||
|
if ally.hp < ally.max_hp:
|
||||||
|
_queue_heal(ally, 30.0 * float(unit.def.get("hp_factor", 1.0)))
|
||||||
|
success = true
|
||||||
|
break
|
||||||
|
"s03", "e09":
|
||||||
|
var victims: Array[LuneUnit] = _nearby(unit, living, false, unit.position, 6.0 if ability == "e09" else 3.0, 3 if ability == "e09" else 4)
|
||||||
|
for victim: LuneUnit in victims:
|
||||||
|
_pending_effects.append([victim, "dissonance", "attack_slow", 0.15, 3.0])
|
||||||
|
success = not victims.is_empty()
|
||||||
|
"s05":
|
||||||
|
var victims: Array[LuneUnit] = _nearby(unit, living, false, unit.position, 5.0, 1)
|
||||||
|
for victim: LuneUnit in victims:
|
||||||
|
_pending_effects.append([victim, "rift_wound", "healing_reduction", 0.30, 4.0])
|
||||||
|
success = not victims.is_empty()
|
||||||
|
if success:
|
||||||
|
unit.ability_ready = unit.combat_time + (5.0 if ability in ["w03", "w04"] else (4.0 if ability in ["w09", "w10"] else 6.0))
|
||||||
|
unit.ability_serial += 1
|
||||||
|
|
||||||
|
func _on_hit(unit: LuneUnit, primary: LuneUnit, living: Array[LuneUnit], gates: Dictionary) -> void:
|
||||||
|
var ability: String = unit.ability()
|
||||||
|
var hits: int = unit.hit_count
|
||||||
|
if unit.has_talent("wurzelbeben") and hits % 3 == 0:
|
||||||
|
_splash(unit, primary, living, gates, 0.50, 2.0, 3)
|
||||||
|
if unit.has_talent("splitterschuss") and hits % 4 == 0:
|
||||||
|
_splash(unit, primary, living, gates, 0.45, 2.0, 3)
|
||||||
|
if unit.has_talent("keimsaat") and hits % 2 == 0:
|
||||||
|
_pending_dots.append([primary, "seed_burn", 9.0, 4.0, int(unit.def.attack_type)])
|
||||||
|
match ability:
|
||||||
|
"w01", "w02":
|
||||||
|
if hits % 3 == 0:
|
||||||
|
_pending_effects.append([primary, "root_grasp", "movement_slow", 0.30 if ability == "w02" else 0.20, 3.0 if ability == "w02" else 2.0])
|
||||||
|
if ability == "w02":
|
||||||
|
_pending_effects.append([primary, "root_grasp_attack", "attack_slow", 0.10, 3.0])
|
||||||
|
"w05", "w06":
|
||||||
|
if hits % 4 == 0:
|
||||||
|
_pending_dots.append([primary, "seed_burn", 10.0 if ability == "w06" else 3.0, 4.0, int(unit.def.attack_type)])
|
||||||
|
"w07", "w08":
|
||||||
|
if hits % 3 == 0:
|
||||||
|
_splash(unit, primary, living, gates, 0.80 if ability == "w08" else 0.60, 2.5 if ability == "w08" else 2.0, 4 if ability == "w08" else 3)
|
||||||
|
"g01", "g02":
|
||||||
|
if hits % 3 == 0:
|
||||||
|
_pending_dots.append([primary, "burn", 12.0 if ability == "g02" else 4.0, 3.0, int(unit.def.attack_type)])
|
||||||
|
"g07": _splash(unit, primary, living, gates, 0.30, 1.5, 3)
|
||||||
|
"g08": _splash(unit, primary, living, gates, 0.40, 2.0, 4)
|
||||||
|
"e05":
|
||||||
|
if hits % 4 == 0: _splash(unit, primary, living, gates, 0.40, 1.5, 2)
|
||||||
|
"e07":
|
||||||
|
if hits % 4 == 0: _splash(unit, primary, living, gates, 0.60, 2.0, 3)
|
||||||
|
"e10":
|
||||||
|
if hits % 4 == 0: _splash(unit, primary, living, gates, 0.60, 2.5, 4)
|
||||||
|
"s04":
|
||||||
|
if hits % 4 == 0: _splash(unit, primary, living, gates, 0.50, 2.0, 3)
|
||||||
|
|
||||||
|
func _splash(unit: LuneUnit, primary: LuneUnit, living: Array[LuneUnit], gates: Dictionary, factor: float, radius: float, maximum: int) -> void:
|
||||||
|
for victim: LuneUnit in _nearby(unit, living, false, primary.position, radius, maximum, primary):
|
||||||
|
_queue_damage(victim, victim.mitigated(float(unit.def.damage) * factor, int(unit.def.attack_type)), gates)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- queries for views and tests
|
||||||
|
func mobile_count(team: int = -1) -> int:
|
||||||
|
var count: int = 0
|
||||||
|
for unit: LuneUnit in units:
|
||||||
|
if unit.alive and unit.is_mobile() and (team < 0 or unit.team == team):
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
1
scripts/luneraria/core/lune_combat.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://rj0ekj8y2rsa
|
||||||
159
scripts/luneraria/core/lune_content.gd
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
class_name LuneContent
|
||||||
|
extends RefCounted
|
||||||
|
## M2 content: heroes, talents, items, attack champions and the grotto boss.
|
||||||
|
## All figures reuse existing models; hero and champion roles are provisional assignments
|
||||||
|
## of those models ("vorläufig"), not newly created peoples.
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- heroes
|
||||||
|
const HEROES: Dictionary = {
|
||||||
|
"held_ringborke": {"name": "Ringborke", "title": "Wächterin der Wurzeln", "model": "w04", "cost": 260, "role": "Front / Schutz",
|
||||||
|
"provisional": true, "def": {"max_hp": 2200.0, "damage": 34.0, "attack_type": 1, "armor_type": 1, "attack_range": 1.4, "speed": 2.4, "ability": "w04"},
|
||||||
|
"summary": "Hält die Front, heilt sich selbst und schützt nahe Verbündete."},
|
||||||
|
"held_quellgeflecht": {"name": "Quellgeflecht", "title": "Hüterin der Quelle", "model": "w10", "cost": 260, "role": "Unterstützung / Heilung",
|
||||||
|
"provisional": true, "def": {"max_hp": 1300.0, "damage": 28.0, "attack_type": 2, "armor_type": 2, "attack_range": 4.5, "speed": 2.0, "ability": "w10"},
|
||||||
|
"summary": "Heilt Verbündete in 4,5 m und stärkt die Armee im Gefecht."},
|
||||||
|
"held_funkenlanze": {"name": "Funkenlanze", "title": "Schützin des Glutpakts", "model": "g05", "cost": 260, "role": "Fernkampf / Schaden",
|
||||||
|
"provisional": true, "def": {"max_hp": 950.0, "damage": 46.0, "attack_type": 1, "armor_type": 0, "attack_range": 6.0, "speed": 2.0, "ability": "g05"},
|
||||||
|
"summary": "Hoher Einzelschaden aus der zweiten Reihe."},
|
||||||
|
}
|
||||||
|
## Cumulative XP needed for levels 1..10.
|
||||||
|
const LEVEL_XP: Array[int] = [0, 120, 280, 480, 720, 1000, 1320, 1680, 2080, 2520]
|
||||||
|
const HP_PER_LEVEL: float = 0.09
|
||||||
|
const DAMAGE_PER_LEVEL: float = 0.07
|
||||||
|
const XP_PARTICIPATION: int = 40
|
||||||
|
const XP_PER_ROUND: int = 6
|
||||||
|
const XP_RADIUS: float = 12.0
|
||||||
|
const TALENT_LEVELS: Array[int] = [3, 6]
|
||||||
|
|
||||||
|
## Two tiers of two mutually exclusive choices per hero; each changes the hero's job.
|
||||||
|
const TALENTS: Dictionary = {
|
||||||
|
"held_ringborke": [
|
||||||
|
[{"id": "borkenhaut", "name": "Borkenhaut", "text": "Eigenschutz: +15 % Schadensminderung für die Heldin."},
|
||||||
|
{"id": "schutzwurzeln", "name": "Schutzwurzeln", "text": "Gruppenschutz: alle 4 s erhalten bis zu 4 Verbündete in 4 m 10 % Schadensminderung für 4,5 s."}],
|
||||||
|
[{"id": "stammschlag", "name": "Stammschlag", "text": "Einzelziel: jeder 3. Treffer +60 % Schaden."},
|
||||||
|
{"id": "wurzelbeben", "name": "Wurzelbeben", "text": "Fläche: jeder 3. Treffer 50 % Schaden an bis zu 3 Gegner in 2 m."}]],
|
||||||
|
"held_quellgeflecht": [
|
||||||
|
[{"id": "tiefe_quelle", "name": "Tiefe Quelle", "text": "Einzelziel: heilt den schwächsten Verbündeten um 190 statt drei um je 80."},
|
||||||
|
{"id": "weite_quelle", "name": "Weite Quelle", "text": "Fläche: heilt bis zu 5 Verbündete in 5,5 m um je 65."}],
|
||||||
|
[{"id": "quellschild", "name": "Quellschild", "text": "Schutz: Geheilte erhalten 4 s lang einen Schild von 70."},
|
||||||
|
{"id": "keimsaat", "name": "Keimsaat", "text": "Druck: jeder 2. Treffer setzt Keimbrand, 9 Schaden/s für 4 s."}]],
|
||||||
|
"held_funkenlanze": [
|
||||||
|
[{"id": "durchschlag", "name": "Durchschlag", "text": "Einzelziel: jeder 4. Treffer ×2,1 statt ×1,5."},
|
||||||
|
{"id": "splitterschuss", "name": "Splitterschuss", "text": "Fläche: jeder 4. Treffer zusätzlich 45 % an bis zu 3 Gegner in 2 m."}],
|
||||||
|
[{"id": "fernsicht", "name": "Fernsicht", "text": "+1,5 m Reichweite."},
|
||||||
|
{"id": "glutsalve", "name": "Glutsalve", "text": "+20 % Angriffstempo."}]],
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- items
|
||||||
|
## Permanent, unique per hero (no two of the same item), three slots. Bonuses of different
|
||||||
|
## items add up within their category; caps: damage reduction 35 %, attack tempo +50 %.
|
||||||
|
## Order in combat: primary hit -> base ability -> talent -> item effect. Item effects never
|
||||||
|
## trigger other on-hit effects, heals never trigger anything, so no loops are possible.
|
||||||
|
const ITEMS: Dictionary = {
|
||||||
|
"basaltschild": {"name": "Basaltschild", "cost": 180, "text": "+350 Lebenspunkte", "hp_flat": 350.0},
|
||||||
|
"glutklinge": {"name": "Glutklinge", "cost": 180, "text": "+18 % Schaden", "damage": 0.18},
|
||||||
|
"rankenmantel": {"name": "Rankenmantel", "cost": 200, "text": "+10 % Schadensminderung", "reduction": 0.10},
|
||||||
|
"funkenfokus": {"name": "Funkenfokus", "cost": 200, "text": "+15 % Angriffstempo", "attack_speed": 0.15},
|
||||||
|
"quellamulett": {"name": "Quellamulett", "cost": 220, "text": "Regeneriert 1,5 % der Lebenspunkte pro Sekunde", "regen": 0.015},
|
||||||
|
"kristallsplitter": {"name": "Kristallsplitter", "cost": 260, "text": "Jeder Treffer: 25 % an bis zu 2 Gegner in 1,5 m · ab Stufe 3", "splash": 0.25, "min_level": 3},
|
||||||
|
}
|
||||||
|
const ITEM_SLOTS: int = 3
|
||||||
|
const ITEM_REFUND: float = 0.5
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- attack champions
|
||||||
|
## Bought in round n for one lane, fight once in round n+1, then vanish. Killing one pays gold.
|
||||||
|
const CHAMPIONS: Dictionary = {
|
||||||
|
"ch_schutz": {"name": "Rostschieber", "role": "Schutz", "model": "s02", "cost": 150, "base": "s02",
|
||||||
|
"text": "Schildträger: startet mit Schild, gibt alle 6 s bis zu 4 Verbündeten in 4 m einen Schild von 120.", "view_scale": 1.25},
|
||||||
|
"ch_druck": {"name": "Dunstsänger", "role": "Druck auf Unterstützer", "model": "s03", "cost": 150, "base": "s03",
|
||||||
|
"text": "Sucht Unterstützer und Fernkämpfer und verlangsamt deren Angriffe.", "priority": "support", "view_scale": 1.25},
|
||||||
|
"ch_belagerung": {"name": "Keilbrecher", "role": "Belagerung", "model": "s04", "cost": 180, "base": "s04",
|
||||||
|
"text": "Greift bevorzugt Gebäude an und verursacht dort ×2,5 Schaden.", "priority": "building", "siege_factor": 2.5, "view_scale": 1.3},
|
||||||
|
}
|
||||||
|
const CHAMPION_HP: float = 2.2
|
||||||
|
const CHAMPION_DAMAGE: float = 1.6
|
||||||
|
const CHAMPION_BOUNTY: int = 70
|
||||||
|
const CHAMPION_BOUNTY_PER_ROUND: int = 6
|
||||||
|
const MAX_BOOKINGS: int = 2
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- boss
|
||||||
|
const BOSS_MODEL: String = "e10"
|
||||||
|
const BOSS_GROTTO_HP: float = 4200.0
|
||||||
|
const BOSS_GROTTO_DAMAGE: float = 55.0
|
||||||
|
const BOSS_DEPLOY_HP: float = 5200.0
|
||||||
|
const BOSS_DEPLOY_DAMAGE: float = 70.0
|
||||||
|
const BOSS_BOUNTY: int = 520
|
||||||
|
const BOSS_BOUNTY_PER_ROUND: int = 60
|
||||||
|
## Victory in round n: deployment in n+1, locked in n+1..n+3, available again from n+4.
|
||||||
|
const BOSS_LOCK_ROUNDS: int = 3
|
||||||
|
const BOSS_XP: int = 220
|
||||||
|
const BUILDING_XP: int = 60
|
||||||
|
|
||||||
|
static func level_for(xp: int) -> int:
|
||||||
|
var level: int = 1
|
||||||
|
for index: int in range(LEVEL_XP.size()):
|
||||||
|
if xp >= LEVEL_XP[index]:
|
||||||
|
level = index + 1
|
||||||
|
return level
|
||||||
|
|
||||||
|
static func hero_definition(hero: Dictionary) -> Dictionary:
|
||||||
|
var spec: Dictionary = HEROES[hero.hero_id]
|
||||||
|
var definition: Dictionary = (spec.def as Dictionary).duplicate(true)
|
||||||
|
var level: int = level_for(int(hero.xp))
|
||||||
|
definition.id = spec.model
|
||||||
|
definition.hero_id = hero.hero_id
|
||||||
|
definition.display_name = spec.name
|
||||||
|
definition.max_hp = float(definition.max_hp) * (1.0 + HP_PER_LEVEL * float(level - 1))
|
||||||
|
definition.damage = float(definition.damage) * (1.0 + DAMAGE_PER_LEVEL * float(level - 1))
|
||||||
|
definition.level = level
|
||||||
|
definition.view_scale = 1.25
|
||||||
|
var damage_bonus: float = 0.0
|
||||||
|
var reduction: float = 0.0
|
||||||
|
var attack_speed: float = 0.0
|
||||||
|
for item_id: String in hero.items:
|
||||||
|
var item: Dictionary = ITEMS[item_id]
|
||||||
|
definition.max_hp = float(definition.max_hp) + float(item.get("hp_flat", 0.0))
|
||||||
|
damage_bonus += float(item.get("damage", 0.0))
|
||||||
|
reduction += float(item.get("reduction", 0.0))
|
||||||
|
attack_speed += float(item.get("attack_speed", 0.0))
|
||||||
|
definition.regen = float(definition.get("regen", 0.0)) + float(item.get("regen", 0.0))
|
||||||
|
definition.item_splash = float(definition.get("item_splash", 0.0)) + float(item.get("splash", 0.0))
|
||||||
|
var talents: Array = hero.get("talents", [])
|
||||||
|
if talents.has("borkenhaut"):
|
||||||
|
reduction += 0.15
|
||||||
|
if talents.has("fernsicht"):
|
||||||
|
definition.attack_range = float(definition.attack_range) + 1.5
|
||||||
|
if talents.has("glutsalve"):
|
||||||
|
attack_speed += 0.20
|
||||||
|
definition.talents = talents.duplicate()
|
||||||
|
definition.damage_bonus = damage_bonus
|
||||||
|
definition.reduction_bonus = reduction
|
||||||
|
definition.attack_speed_bonus = minf(0.5, attack_speed)
|
||||||
|
return definition
|
||||||
|
|
||||||
|
static func champion_definition(champion_id: String, round_number: int, balance: LuneBalance) -> Dictionary:
|
||||||
|
var spec: Dictionary = CHAMPIONS[champion_id]
|
||||||
|
var base: Dictionary = DataCatalog.SENDS[spec.base].duplicate(true)
|
||||||
|
base.id = spec.model
|
||||||
|
base.champion_id = champion_id
|
||||||
|
base.display_name = spec.name
|
||||||
|
base.max_hp = float(base.max_hp) * CHAMPION_HP * LuneWaves.strength(balance, round_number, balance.wave_hp_growth)
|
||||||
|
base.damage = float(base.damage) * CHAMPION_DAMAGE * LuneWaves.strength(balance, round_number, balance.wave_damage_growth)
|
||||||
|
base.bounty = CHAMPION_BOUNTY + CHAMPION_BOUNTY_PER_ROUND * round_number
|
||||||
|
base.priority = str(spec.get("priority", ""))
|
||||||
|
base.siege_factor = float(spec.get("siege_factor", 1.0))
|
||||||
|
base.view_scale = float(spec.view_scale)
|
||||||
|
if champion_id == "ch_schutz":
|
||||||
|
base.ability = "ch_schutz"
|
||||||
|
return base
|
||||||
|
|
||||||
|
static func boss_definition(round_number: int, balance: LuneBalance, deployed: bool) -> Dictionary:
|
||||||
|
var base: Dictionary = DataCatalog.ENEMIES[BOSS_MODEL].duplicate(true)
|
||||||
|
base.display_name = "Schlundkrone"
|
||||||
|
base.max_hp = (BOSS_DEPLOY_HP if deployed else BOSS_GROTTO_HP) * LuneWaves.strength(balance, round_number, balance.wave_hp_growth)
|
||||||
|
base.damage = (BOSS_DEPLOY_DAMAGE if deployed else BOSS_GROTTO_DAMAGE) * LuneWaves.strength(balance, round_number, balance.wave_damage_growth)
|
||||||
|
base.bounty = BOSS_BOUNTY + BOSS_BOUNTY_PER_ROUND * round_number if deployed else 0
|
||||||
|
base.is_boss = true
|
||||||
|
base.view_scale = 1.7
|
||||||
|
base.speed = 1.6
|
||||||
|
return base
|
||||||
1
scripts/luneraria/core/lune_content.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://blkpu61mfghb2
|
||||||
176
scripts/luneraria/core/lune_map.gd
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
class_name LuneMap
|
||||||
|
extends RefCounted
|
||||||
|
## Map data in ground coordinates Vector2(x, z): Blau (team 0) in the south-west,
|
||||||
|
## Rot (team 1) in the north-east. Rot is the mirror image of Blau across x = z
|
||||||
|
## (mirror(v) = Vector2(v.y, v.x)), Bottom is Top mirrored across the Mid line.
|
||||||
|
## Both hold by construction, so strecken, towers and spawns are functionally equal.
|
||||||
|
|
||||||
|
const LANES: Array[String] = ["top", "mid", "bot"]
|
||||||
|
const LANE_NAMES: Dictionary = {"top": "Top", "mid": "Mid", "bot": "Bottom"}
|
||||||
|
# First halves from the Blau end to the centre line; the Rot half is mirrored.
|
||||||
|
const TOP_HALF: Array[Vector2] = [Vector2(-24, 22), Vector2(-26, 6), Vector2(-26.5, -12), Vector2(-25, -25)]
|
||||||
|
const MID_HALF: Array[Vector2] = [Vector2(-21, 21), Vector2(-11, 10), Vector2(0, 0)]
|
||||||
|
const CORE_BLUE := Vector2(-30, 30)
|
||||||
|
const BASE_TOWERS_BLUE: Array[Vector2] = [Vector2(-28.8, 24.6), Vector2(-24.6, 28.8)]
|
||||||
|
## Neutral boss grotto, north-west of Mid and on the symmetry axis (M2).
|
||||||
|
const GROTTO := Vector2(-11.5, -11.5)
|
||||||
|
const HALF_EXTENT: float = 36.0
|
||||||
|
|
||||||
|
var balance: LuneBalance
|
||||||
|
var lanes: Dictionary = {}
|
||||||
|
var buildings: Array[Dictionary] = []
|
||||||
|
|
||||||
|
class LanePath:
|
||||||
|
extends RefCounted
|
||||||
|
var points: PackedVector2Array
|
||||||
|
var cumulative: PackedFloat32Array
|
||||||
|
var length: float = 0.0
|
||||||
|
|
||||||
|
func _init(source: Array[Vector2]) -> void:
|
||||||
|
points = PackedVector2Array(source)
|
||||||
|
cumulative.append(0.0)
|
||||||
|
for i: int in range(1, points.size()):
|
||||||
|
length += points[i].distance_to(points[i - 1])
|
||||||
|
cumulative.append(length)
|
||||||
|
|
||||||
|
func point_at(s: float) -> Vector2:
|
||||||
|
var d: float = clampf(s, 0.0, length)
|
||||||
|
for i: int in range(1, points.size()):
|
||||||
|
if d <= cumulative[i] or i == points.size() - 1:
|
||||||
|
var span: float = maxf(0.0001, cumulative[i] - cumulative[i - 1])
|
||||||
|
return points[i - 1].lerp(points[i], clampf((d - cumulative[i - 1]) / span, 0.0, 1.0))
|
||||||
|
return points[points.size() - 1]
|
||||||
|
|
||||||
|
func tangent_at(s: float) -> Vector2:
|
||||||
|
var d: float = clampf(s, 0.0, length)
|
||||||
|
for i: int in range(1, points.size()):
|
||||||
|
if d <= cumulative[i] or i == points.size() - 1:
|
||||||
|
return (points[i] - points[i - 1]).normalized()
|
||||||
|
return Vector2.RIGHT
|
||||||
|
|
||||||
|
## Point and unit tangent at s in one pass.
|
||||||
|
func sample(s: float) -> Array:
|
||||||
|
var d: float = clampf(s, 0.0, length)
|
||||||
|
for i: int in range(1, points.size()):
|
||||||
|
if d <= cumulative[i] or i == points.size() - 1:
|
||||||
|
var a: Vector2 = points[i - 1]
|
||||||
|
var b: Vector2 = points[i]
|
||||||
|
var span: float = maxf(0.0001, cumulative[i] - cumulative[i - 1])
|
||||||
|
return [a.lerp(b, clampf((d - cumulative[i - 1]) / span, 0.0, 1.0)), (b - a) / span]
|
||||||
|
return [points[points.size() - 1], Vector2.RIGHT]
|
||||||
|
|
||||||
|
func project(position: Vector2) -> float:
|
||||||
|
var best: float = INF
|
||||||
|
var best_s: float = 0.0
|
||||||
|
for i: int in range(1, points.size()):
|
||||||
|
var a: Vector2 = points[i - 1]
|
||||||
|
var b: Vector2 = points[i]
|
||||||
|
var ab: Vector2 = b - a
|
||||||
|
var t: float = clampf((position - a).dot(ab) / maxf(0.0001, ab.length_squared()), 0.0, 1.0)
|
||||||
|
var d: float = position.distance_squared_to(a + ab * t)
|
||||||
|
if d < best:
|
||||||
|
best = d
|
||||||
|
best_s = cumulative[i - 1] + ab.length() * t
|
||||||
|
return best_s
|
||||||
|
|
||||||
|
static func mirror(v: Vector2) -> Vector2:
|
||||||
|
return Vector2(v.y, v.x)
|
||||||
|
|
||||||
|
static func mirror_mid(v: Vector2) -> Vector2:
|
||||||
|
return Vector2(-v.y, -v.x)
|
||||||
|
|
||||||
|
static func full_lane(half: Array[Vector2]) -> Array[Vector2]:
|
||||||
|
var result: Array[Vector2] = half.duplicate()
|
||||||
|
for i: int in range(half.size() - 2, -1, -1):
|
||||||
|
result.append(mirror(half[i]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
func _init(config: LuneBalance = null) -> void:
|
||||||
|
balance = config if config != null else LuneBalance.default_balance()
|
||||||
|
var bot_half: Array[Vector2] = []
|
||||||
|
for point: Vector2 in TOP_HALF:
|
||||||
|
bot_half.append(mirror_mid(point))
|
||||||
|
lanes["top"] = LanePath.new(full_lane(TOP_HALF))
|
||||||
|
lanes["mid"] = LanePath.new(full_lane(MID_HALF))
|
||||||
|
lanes["bot"] = LanePath.new(full_lane(bot_half))
|
||||||
|
_build_buildings()
|
||||||
|
|
||||||
|
## Lane coordinate s for a team's own marching progress p (0 at its own base).
|
||||||
|
func s_for(team: int, lane: String, progress: float) -> float:
|
||||||
|
var path: LanePath = lanes[lane]
|
||||||
|
return progress if team == 0 else path.length - progress
|
||||||
|
|
||||||
|
func progress_for(team: int, lane: String, s: float) -> float:
|
||||||
|
var path: LanePath = lanes[lane]
|
||||||
|
return s if team == 0 else path.length - s
|
||||||
|
|
||||||
|
## Blau frame point, mirrored for Rot so both sides are exact images.
|
||||||
|
func lane_point(team: int, lane: String, progress: float, lateral: float = 0.0) -> Vector2:
|
||||||
|
var path: LanePath = lanes[lane]
|
||||||
|
var sampled: Array = path.sample(progress)
|
||||||
|
var tangent: Vector2 = sampled[1]
|
||||||
|
var blue: Vector2 = sampled[0] + Vector2(-tangent.y, tangent.x) * lateral
|
||||||
|
return blue if team == 0 else Vector2(blue.y, blue.x)
|
||||||
|
|
||||||
|
func lane_forward(team: int, lane: String, progress: float) -> Vector2:
|
||||||
|
var path: LanePath = lanes[lane]
|
||||||
|
var tangent: Vector2 = path.tangent_at(clampf(progress, 0.0, path.length))
|
||||||
|
return tangent if team == 0 else mirror(tangent)
|
||||||
|
|
||||||
|
## Grotto deployment points: one mirrored entrance per team, boss slightly inside.
|
||||||
|
func grotto_entry(team: int) -> Vector2:
|
||||||
|
var blue: Vector2 = GROTTO + Vector2(1.2, 4.6)
|
||||||
|
return blue if team == 0 else mirror(blue)
|
||||||
|
|
||||||
|
func grotto_boss_point() -> Vector2:
|
||||||
|
return GROTTO + Vector2(-1.5, -1.5)
|
||||||
|
|
||||||
|
func core_position(team: int) -> Vector2:
|
||||||
|
return CORE_BLUE if team == 0 else mirror(CORE_BLUE)
|
||||||
|
|
||||||
|
func _build_buildings() -> void:
|
||||||
|
buildings.clear()
|
||||||
|
for team: int in range(2):
|
||||||
|
for lane: String in LANES:
|
||||||
|
var path: LanePath = lanes[lane]
|
||||||
|
for tier: String in ["inner", "outer"]:
|
||||||
|
var fraction: float = balance.inner_tower_fraction if tier == "inner" else balance.outer_tower_fraction
|
||||||
|
var progress: float = path.length * fraction
|
||||||
|
buildings.append({"id": "%s_%s_%s" % ["blau" if team == 0 else "rot", lane, tier], "team": team, "lane": lane,
|
||||||
|
"tier": tier, "progress": progress, "position": lane_point(team, lane, progress, balance.tower_side_offset),
|
||||||
|
"max_hp": balance.tower_inner_hp if tier == "inner" else balance.tower_outer_hp,
|
||||||
|
"damage": balance.tower_damage, "range": balance.tower_range,
|
||||||
|
"bounty": balance.bounty_inner_tower if tier == "inner" else balance.bounty_outer_tower})
|
||||||
|
for index: int in range(2):
|
||||||
|
var point: Vector2 = BASE_TOWERS_BLUE[index]
|
||||||
|
buildings.append({"id": "%s_base_%d" % ["blau" if team == 0 else "rot", index + 1], "team": team, "lane": "base",
|
||||||
|
"tier": "base", "progress": -1.0, "position": point if team == 0 else mirror(point),
|
||||||
|
"max_hp": balance.base_tower_hp, "damage": balance.base_tower_damage, "range": balance.tower_range,
|
||||||
|
"bounty": balance.bounty_base_tower})
|
||||||
|
buildings.append({"id": "%s_ancient" % ("blau" if team == 0 else "rot"), "team": team, "lane": "base", "tier": "core",
|
||||||
|
"progress": -1.0, "position": core_position(team), "max_hp": balance.core_hp, "damage": 0.0, "range": 0.0, "bounty": 0})
|
||||||
|
|
||||||
|
var _index: Dictionary = {}
|
||||||
|
|
||||||
|
func building_def(id: String) -> Dictionary:
|
||||||
|
if _index.is_empty():
|
||||||
|
for entry: Dictionary in buildings:
|
||||||
|
_index[entry.id] = entry
|
||||||
|
return _index.get(id, {})
|
||||||
|
|
||||||
|
## Gate rule shared by every damage source. alive maps building id -> bool.
|
||||||
|
static func is_attackable(definition: Dictionary, alive: Dictionary) -> bool:
|
||||||
|
var prefix: String = "blau" if int(definition.team) == 0 else "rot"
|
||||||
|
match str(definition.tier):
|
||||||
|
"outer":
|
||||||
|
return true
|
||||||
|
"inner":
|
||||||
|
return not alive.get("%s_%s_outer" % [prefix, definition.lane], false)
|
||||||
|
"base":
|
||||||
|
for lane: String in LANES:
|
||||||
|
if not alive.get("%s_%s_outer" % [prefix, lane], false) and not alive.get("%s_%s_inner" % [prefix, lane], false):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
"core":
|
||||||
|
return not alive.get(prefix + "_base_1", false) and not alive.get(prefix + "_base_2", false)
|
||||||
|
return false
|
||||||
1
scripts/luneraria/core/lune_map.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bqnx0kaccktd5
|
||||||
621
scripts/luneraria/core/lune_match.gd
Normal file
|
|
@ -0,0 +1,621 @@
|
||||||
|
class_name LuneMatch
|
||||||
|
extends RefCounted
|
||||||
|
## Authoritative match: six player slots in two teams, persistent ownership, planning
|
||||||
|
## drafts that are validated and committed atomically, combat, settlement and victory.
|
||||||
|
## A match ends only when an Ancient falls. There is no round limit and no HP decision.
|
||||||
|
|
||||||
|
enum Phase { PLANNING, COMBAT, OVER }
|
||||||
|
const ONGOING := -2
|
||||||
|
const DRAW := -1
|
||||||
|
const GROTTO := "grotto"
|
||||||
|
const BOT_NAMES: Array[String] = ["Eichrufer", "Aschenzunge", "Quellwächter", "Glutfaust", "Moosauge", "Basaltherz", "Rindenkrone", "Funkenschritt"]
|
||||||
|
const ROLES: Dictionary = {"g01": "melee", "g03": "front", "g05": "ranged", "g07": "splash", "g09": "support",
|
||||||
|
"w01": "melee", "w03": "front", "w05": "ranged", "w07": "splash", "w09": "support"}
|
||||||
|
const HERO_ROLES: Dictionary = {"held_ringborke": "front", "held_quellgeflecht": "support", "held_funkenlanze": "ranged"}
|
||||||
|
const ROW_ORDER: Array[String] = ["front", "melee", "splash", "ranged", "support"]
|
||||||
|
|
||||||
|
var balance: LuneBalance
|
||||||
|
var map: LuneMap
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
var seed_value: int = 0
|
||||||
|
var round_number: int = 1
|
||||||
|
var phase: int = Phase.PLANNING
|
||||||
|
var winner: int = ONGOING
|
||||||
|
var end_note: String = ""
|
||||||
|
var players: Array[Dictionary] = []
|
||||||
|
var building_hp: Dictionary = {}
|
||||||
|
var combat: LuneCombat
|
||||||
|
var last_settlement: Dictionary = {}
|
||||||
|
var history: Array[Dictionary] = []
|
||||||
|
## Public boss state. claim = {team, won_round, deploy_round, chooser_slot, lane} or {}.
|
||||||
|
var boss: Dictionary = {}
|
||||||
|
var boss_log: Array[Dictionary] = []
|
||||||
|
## Remainder rotation per team so both teams follow the same distribution pattern.
|
||||||
|
var _reward_rotation: Array[int] = [0, 0]
|
||||||
|
|
||||||
|
func _init(config: LuneBalance = null) -> void:
|
||||||
|
balance = config if config != null else LuneBalance.default_balance()
|
||||||
|
map = LuneMap.new(balance)
|
||||||
|
|
||||||
|
## options: seed, human_slot (-1 for six bots), bunds (Array of six).
|
||||||
|
func setup(options: Dictionary = {}) -> void:
|
||||||
|
seed_value = int(options.get("seed", 1))
|
||||||
|
rng.seed = seed_value
|
||||||
|
round_number = 1
|
||||||
|
phase = Phase.PLANNING
|
||||||
|
winner = ONGOING
|
||||||
|
end_note = ""
|
||||||
|
history.clear()
|
||||||
|
last_settlement = {}
|
||||||
|
_reward_rotation = [0, 0]
|
||||||
|
boss = {"next_available_round": 1, "claim": {}}
|
||||||
|
boss_log.clear()
|
||||||
|
players.clear()
|
||||||
|
var human: int = int(options.get("human_slot", 1))
|
||||||
|
var bunds: Array = options.get("bunds", [])
|
||||||
|
for slot: int in range(6):
|
||||||
|
var bund: String = str(bunds[slot]) if slot < bunds.size() else ("glutpakt" if (slot + seed_value) % 2 == 0 else "wurzelbund")
|
||||||
|
var player := {"slot": slot, "team": slot / 3, "lane": LuneMap.LANES[slot % 3], "is_human": slot == human,
|
||||||
|
"name": "Du" if slot == human else BOT_NAMES[(slot + seed_value) % BOT_NAMES.size()], "bund": bund,
|
||||||
|
"gold": balance.start_gold, "roster": [], "hero": {}, "bookings": [], "ready": slot != human,
|
||||||
|
"earned": 0, "spent": 0, "next_seq": 1}
|
||||||
|
player.draft = _snapshot(player)
|
||||||
|
player.undo = []
|
||||||
|
players.append(player)
|
||||||
|
building_hp.clear()
|
||||||
|
for definition: Dictionary in map.buildings:
|
||||||
|
building_hp[definition.id] = float(definition.max_hp)
|
||||||
|
|
||||||
|
func _snapshot(player: Dictionary) -> Dictionary:
|
||||||
|
return {"gold": int(player.gold), "roster": (player.roster as Array).duplicate(true), "next_seq": int(player.next_seq),
|
||||||
|
"hero": (player.hero as Dictionary).duplicate(true), "new_bookings": [], "boss_lane": ""}
|
||||||
|
|
||||||
|
func slot_of(team: int, lane: String) -> int:
|
||||||
|
return team * 3 + LuneMap.LANES.find(lane)
|
||||||
|
|
||||||
|
func base_ids(bund: String) -> Array[String]:
|
||||||
|
return DataCatalog.faction_ids(bund)
|
||||||
|
|
||||||
|
func grotto_available() -> bool:
|
||||||
|
return round_number >= int(boss.next_available_round)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- planning commands
|
||||||
|
func submit(slot: int, command: Dictionary) -> Dictionary:
|
||||||
|
if phase != Phase.PLANNING:
|
||||||
|
return _result(false, "Befehle sind nur in der Planung möglich.")
|
||||||
|
if slot < 0 or slot >= players.size():
|
||||||
|
return _result(false, "Unbekannter Spieler.")
|
||||||
|
var player: Dictionary = players[slot]
|
||||||
|
var draft: Dictionary = player.draft
|
||||||
|
var before: Dictionary = draft.duplicate(true)
|
||||||
|
var result: Dictionary = _apply(player, draft, command)
|
||||||
|
if result.ok:
|
||||||
|
(player.undo as Array).append(before)
|
||||||
|
return result
|
||||||
|
|
||||||
|
func undo(slot: int) -> Dictionary:
|
||||||
|
var player: Dictionary = players[slot]
|
||||||
|
if phase != Phase.PLANNING or (player.undo as Array).is_empty():
|
||||||
|
return _result(false, "Nichts rückgängig zu machen.")
|
||||||
|
player.draft = (player.undo as Array).pop_back()
|
||||||
|
return _result(true, "Letzte Planung zurückgenommen.")
|
||||||
|
|
||||||
|
func _find(roster: Array, uid: String) -> int:
|
||||||
|
for index: int in range(roster.size()):
|
||||||
|
if str(roster[index].uid) == uid:
|
||||||
|
return index
|
||||||
|
return -1
|
||||||
|
|
||||||
|
func _next_seq(player: Dictionary, draft: Dictionary) -> int:
|
||||||
|
var seq: int = int(draft.get("next_seq", player.next_seq))
|
||||||
|
draft.next_seq = seq + 1
|
||||||
|
return seq
|
||||||
|
|
||||||
|
func _valid_destination(lane: String) -> bool:
|
||||||
|
return LuneMap.LANES.has(lane) or (lane == GROTTO and grotto_available())
|
||||||
|
|
||||||
|
func _apply(player: Dictionary, draft: Dictionary, command: Dictionary) -> Dictionary:
|
||||||
|
var roster: Array = draft.roster
|
||||||
|
var hero: Dictionary = draft.hero
|
||||||
|
match str(command.get("type", "")):
|
||||||
|
"buy":
|
||||||
|
var type_id: String = str(command.get("type_id", ""))
|
||||||
|
if not base_ids(player.bund).has(type_id):
|
||||||
|
return _result(false, "Diese Truppe gehört nicht zu deinem Bund.")
|
||||||
|
if roster.size() >= balance.supply_cap:
|
||||||
|
return _result(false, "Versorgungsgrenze erreicht (%d Truppen)." % balance.supply_cap)
|
||||||
|
var cost: int = int(DataCatalog.get_unit(type_id).cost)
|
||||||
|
if int(draft.gold) < cost:
|
||||||
|
return _result(false, "Es fehlen %d Gold." % (cost - int(draft.gold)))
|
||||||
|
var lane: String = str(command.get("lane", player.lane))
|
||||||
|
if not LuneMap.LANES.has(lane):
|
||||||
|
return _result(false, "Unbekannte Lane.")
|
||||||
|
draft.gold = int(draft.gold) - cost
|
||||||
|
# Per-player numbering keeps both teams' ids in the same order; seq sorts numerically.
|
||||||
|
var seq: int = _next_seq(player, draft)
|
||||||
|
roster.append({"uid": "p%d-%d" % [player.slot, seq], "seq": seq, "type_id": type_id, "lane": lane, "invested": cost, "fresh": true})
|
||||||
|
return _result(true, "%s gekauft." % DataCatalog.get_unit(type_id).display_name)
|
||||||
|
"upgrade":
|
||||||
|
var index: int = _find(roster, str(command.get("uid", "")))
|
||||||
|
if index < 0:
|
||||||
|
return _result(false, "Diese Truppe gehört dir nicht.")
|
||||||
|
var entry: Dictionary = roster[index]
|
||||||
|
var upgrade_id: String = str(DataCatalog.get_unit(entry.type_id).get("upgrade_id", ""))
|
||||||
|
if upgrade_id.is_empty():
|
||||||
|
return _result(false, "Bereits vollständig aufgewertet.")
|
||||||
|
var cost: int = int(DataCatalog.get_unit(upgrade_id).cost)
|
||||||
|
if int(draft.gold) < cost:
|
||||||
|
return _result(false, "Es fehlen %d Gold." % (cost - int(draft.gold)))
|
||||||
|
draft.gold = int(draft.gold) - cost
|
||||||
|
entry.type_id = upgrade_id
|
||||||
|
entry.invested = int(entry.invested) + cost
|
||||||
|
return _result(true, "Zu %s aufgewertet." % DataCatalog.get_unit(upgrade_id).display_name)
|
||||||
|
"sell":
|
||||||
|
var index: int = _find(roster, str(command.get("uid", "")))
|
||||||
|
if index < 0:
|
||||||
|
return _result(false, "Diese Truppe gehört dir nicht.")
|
||||||
|
var entry: Dictionary = roster[index]
|
||||||
|
# A purchase from this planning is cancelled at full price; committed troops sell at the refund rate.
|
||||||
|
var refund: int = int(entry.invested) if bool(entry.get("fresh", false)) else floori(float(entry.invested) * balance.sell_refund)
|
||||||
|
draft.gold = int(draft.gold) + refund
|
||||||
|
roster.remove_at(index)
|
||||||
|
return _result(true, "Für %d Gold zurückgegeben." % refund)
|
||||||
|
"assign":
|
||||||
|
var uid: String = str(command.get("uid", ""))
|
||||||
|
var lane: String = str(command.get("lane", ""))
|
||||||
|
if not _valid_destination(lane):
|
||||||
|
return _result(false, "Die Grotte ist bis Runde %d gesperrt." % int(boss.next_available_round) if lane == GROTTO else "Ungültige Zuweisung.")
|
||||||
|
if not hero.is_empty() and str(hero.uid) == uid:
|
||||||
|
hero.lane = lane
|
||||||
|
else:
|
||||||
|
var index: int = _find(roster, uid)
|
||||||
|
if index < 0:
|
||||||
|
return _result(false, "Ungültige Zuweisung.")
|
||||||
|
roster[index].lane = lane
|
||||||
|
return _result(true, "In die Bossgrotte entsandt." if lane == GROTTO else "Auf %s eingeteilt." % LuneMap.LANE_NAMES[lane])
|
||||||
|
"hero_buy":
|
||||||
|
var hero_id: String = str(command.get("hero_id", ""))
|
||||||
|
if not LuneContent.HEROES.has(hero_id):
|
||||||
|
return _result(false, "Unbekannter Held.")
|
||||||
|
if not hero.is_empty():
|
||||||
|
return _result(false, "Pro Spieler ist ein aktiver Held erlaubt.")
|
||||||
|
var cost: int = int(LuneContent.HEROES[hero_id].cost)
|
||||||
|
if int(draft.gold) < cost:
|
||||||
|
return _result(false, "Es fehlen %d Gold." % (cost - int(draft.gold)))
|
||||||
|
draft.gold = int(draft.gold) - cost
|
||||||
|
var seq: int = _next_seq(player, draft)
|
||||||
|
draft.hero = {"uid": "h%d-%d" % [player.slot, seq], "seq": seq, "hero_id": hero_id, "lane": player.lane, "xp": 0,
|
||||||
|
"items": [], "talents": [], "invested": cost, "fresh": true, "fresh_items": []}
|
||||||
|
return _result(true, "%s angeworben." % LuneContent.HEROES[hero_id].name)
|
||||||
|
"hero_cancel":
|
||||||
|
if hero.is_empty() or not bool(hero.get("fresh", false)):
|
||||||
|
return _result(false, "Nur ein Held aus dieser Planung lässt sich zurückgeben.")
|
||||||
|
var refund: int = int(hero.invested)
|
||||||
|
for item_id: String in hero.items:
|
||||||
|
refund += int(LuneContent.ITEMS[item_id].cost)
|
||||||
|
draft.gold = int(draft.gold) + refund
|
||||||
|
draft.hero = {}
|
||||||
|
return _result(true, "Held zurückgegeben (%d Gold)." % refund)
|
||||||
|
"item_buy":
|
||||||
|
var item_id: String = str(command.get("item_id", ""))
|
||||||
|
if command.has("uid") and (hero.is_empty() or str(command.uid) != str(hero.uid)):
|
||||||
|
return _result(false, "Normale Truppen tragen keine Items.")
|
||||||
|
if hero.is_empty():
|
||||||
|
return _result(false, "Items benötigen einen Helden.")
|
||||||
|
if not LuneContent.ITEMS.has(item_id):
|
||||||
|
return _result(false, "Unbekanntes Item.")
|
||||||
|
var item: Dictionary = LuneContent.ITEMS[item_id]
|
||||||
|
if (hero.items as Array).has(item_id):
|
||||||
|
return _result(false, "%s trägt dein Held bereits; Items sind einzigartig." % item.name)
|
||||||
|
if (hero.items as Array).size() >= LuneContent.ITEM_SLOTS:
|
||||||
|
return _result(false, "Alle %d Itemplätze sind belegt." % LuneContent.ITEM_SLOTS)
|
||||||
|
if LuneContent.level_for(int(hero.xp)) < int(item.get("min_level", 1)):
|
||||||
|
return _result(false, "%s erfordert Stufe %d." % [item.name, int(item.min_level)])
|
||||||
|
if int(draft.gold) < int(item.cost):
|
||||||
|
return _result(false, "Es fehlen %d Gold." % (int(item.cost) - int(draft.gold)))
|
||||||
|
draft.gold = int(draft.gold) - int(item.cost)
|
||||||
|
(hero.items as Array).append(item_id)
|
||||||
|
(hero.fresh_items as Array).append(item_id)
|
||||||
|
return _result(true, "%s ausgerüstet." % item.name)
|
||||||
|
"item_sell":
|
||||||
|
var item_id: String = str(command.get("item_id", ""))
|
||||||
|
if hero.is_empty() or not (hero.items as Array).has(item_id):
|
||||||
|
return _result(false, "Dein Held trägt dieses Item nicht.")
|
||||||
|
var cost: int = int(LuneContent.ITEMS[item_id].cost)
|
||||||
|
var fresh: bool = (hero.fresh_items as Array).has(item_id)
|
||||||
|
var refund: int = cost if fresh else floori(float(cost) * LuneContent.ITEM_REFUND)
|
||||||
|
(hero.items as Array).erase(item_id)
|
||||||
|
(hero.fresh_items as Array).erase(item_id)
|
||||||
|
draft.gold = int(draft.gold) + refund
|
||||||
|
return _result(true, "Item für %d Gold verkauft." % refund)
|
||||||
|
"talent":
|
||||||
|
if hero.is_empty():
|
||||||
|
return _result(false, "Talente benötigen einen Helden.")
|
||||||
|
var tier: int = int(command.get("tier", -1))
|
||||||
|
var choice: int = int(command.get("choice", -1))
|
||||||
|
var talents: Array = hero.talents
|
||||||
|
if tier != talents.size() or tier < 0 or tier >= LuneContent.TALENT_LEVELS.size() or choice < 0 or choice > 1:
|
||||||
|
return _result(false, "Dieses Talent ist jetzt nicht wählbar.")
|
||||||
|
if LuneContent.level_for(int(hero.xp)) < LuneContent.TALENT_LEVELS[tier]:
|
||||||
|
return _result(false, "Talentstufe %d erfordert Heldenstufe %d." % [tier + 1, LuneContent.TALENT_LEVELS[tier]])
|
||||||
|
var option: Dictionary = LuneContent.TALENTS[hero.hero_id][tier][choice]
|
||||||
|
talents.append(option.id)
|
||||||
|
return _result(true, "Talent %s gewählt." % option.name)
|
||||||
|
"champion_book":
|
||||||
|
var champion_id: String = str(command.get("champion_id", ""))
|
||||||
|
var lane: String = str(command.get("lane", player.lane))
|
||||||
|
if not LuneContent.CHAMPIONS.has(champion_id) or not LuneMap.LANES.has(lane):
|
||||||
|
return _result(false, "Ungültige Buchung.")
|
||||||
|
if (draft.new_bookings as Array).size() >= LuneContent.MAX_BOOKINGS:
|
||||||
|
return _result(false, "Höchstens %d Angriffschampions pro Runde." % LuneContent.MAX_BOOKINGS)
|
||||||
|
var cost: int = int(LuneContent.CHAMPIONS[champion_id].cost)
|
||||||
|
if int(draft.gold) < cost:
|
||||||
|
return _result(false, "Es fehlen %d Gold." % (cost - int(draft.gold)))
|
||||||
|
draft.gold = int(draft.gold) - cost
|
||||||
|
var seq: int = _next_seq(player, draft)
|
||||||
|
(draft.new_bookings as Array).append({"uid": "c%d-%d" % [player.slot, seq], "seq": seq, "champion_id": champion_id, "lane": lane, "cost": cost})
|
||||||
|
return _result(true, "%s für die nächste Runde auf %s gebucht." % [LuneContent.CHAMPIONS[champion_id].name, LuneMap.LANE_NAMES[lane]])
|
||||||
|
"champion_cancel":
|
||||||
|
var uid: String = str(command.get("uid", ""))
|
||||||
|
var bookings: Array = draft.new_bookings
|
||||||
|
for index: int in range(bookings.size()):
|
||||||
|
if str(bookings[index].uid) == uid:
|
||||||
|
draft.gold = int(draft.gold) + int(bookings[index].cost)
|
||||||
|
bookings.remove_at(index)
|
||||||
|
return _result(true, "Buchung zurückgenommen.")
|
||||||
|
return _result(false, "Nur Buchungen aus dieser Planung lassen sich zurücknehmen.")
|
||||||
|
"boss_lane":
|
||||||
|
var claim: Dictionary = boss.claim
|
||||||
|
var lane: String = str(command.get("lane", ""))
|
||||||
|
if claim.is_empty() or int(claim.deploy_round) != round_number or int(claim.chooser_slot) != int(player.slot):
|
||||||
|
return _result(false, "Du bestimmst die Boss-Lane nicht.")
|
||||||
|
if not LuneMap.LANES.has(lane):
|
||||||
|
return _result(false, "Unbekannte Lane.")
|
||||||
|
draft.boss_lane = lane
|
||||||
|
return _result(true, "Der Boss greift über %s an." % LuneMap.LANE_NAMES[lane])
|
||||||
|
return _result(false, "Unbekannter Befehl.")
|
||||||
|
|
||||||
|
func _result(ok: bool, message: String) -> Dictionary:
|
||||||
|
return {"ok": ok, "message": message}
|
||||||
|
|
||||||
|
func set_ready(slot: int, value: bool = true) -> void:
|
||||||
|
if phase == Phase.PLANNING:
|
||||||
|
players[slot].ready = value
|
||||||
|
|
||||||
|
func all_ready() -> bool:
|
||||||
|
for player: Dictionary in players:
|
||||||
|
if not bool(player.ready):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
## Public information for everyone: champion bookings and the boss deployment of this round.
|
||||||
|
func announcements() -> Array[Dictionary]:
|
||||||
|
var result: Array[Dictionary] = []
|
||||||
|
for player: Dictionary in players:
|
||||||
|
for booking: Dictionary in player.bookings:
|
||||||
|
if int(booking.deploy_round) == round_number:
|
||||||
|
result.append({"kind": "champion", "team": player.team, "slot": player.slot, "lane": booking.lane, "champion_id": booking.champion_id})
|
||||||
|
var claim: Dictionary = boss.claim
|
||||||
|
if not claim.is_empty() and int(claim.deploy_round) == round_number:
|
||||||
|
result.append({"kind": "boss", "team": claim.team, "slot": claim.chooser_slot, "lane": claim.lane,
|
||||||
|
"bounty": LuneContent.BOSS_BOUNTY + LuneContent.BOSS_BOUNTY_PER_ROUND * round_number})
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- phase changes
|
||||||
|
## Validates every draft again and commits all of them at once, then starts combat.
|
||||||
|
func begin_combat() -> bool:
|
||||||
|
if phase != Phase.PLANNING:
|
||||||
|
return false
|
||||||
|
for player: Dictionary in players:
|
||||||
|
if not _draft_valid(player):
|
||||||
|
player.draft = _snapshot(player)
|
||||||
|
for player: Dictionary in players:
|
||||||
|
var draft: Dictionary = player.draft
|
||||||
|
player.spent = int(player.spent) + maxi(0, int(player.gold) - int(draft.gold))
|
||||||
|
player.gold = int(draft.gold)
|
||||||
|
player.next_seq = int(draft.get("next_seq", player.next_seq))
|
||||||
|
player.roster = (draft.roster as Array).duplicate(true)
|
||||||
|
for entry: Dictionary in player.roster:
|
||||||
|
entry.erase("fresh")
|
||||||
|
player.hero = (draft.hero as Dictionary).duplicate(true)
|
||||||
|
if not (player.hero as Dictionary).is_empty():
|
||||||
|
player.hero.erase("fresh")
|
||||||
|
player.hero.fresh_items = []
|
||||||
|
for booking: Dictionary in draft.new_bookings:
|
||||||
|
var committed: Dictionary = booking.duplicate()
|
||||||
|
committed.deploy_round = round_number + 1
|
||||||
|
(player.bookings as Array).append(committed)
|
||||||
|
var claim: Dictionary = boss.claim
|
||||||
|
if not claim.is_empty() and int(claim.chooser_slot) == int(player.slot) and not str(draft.boss_lane).is_empty():
|
||||||
|
claim.lane = draft.boss_lane
|
||||||
|
player.undo = []
|
||||||
|
combat = LuneCombat.new(map, balance, round_number)
|
||||||
|
for definition: Dictionary in map.buildings:
|
||||||
|
if float(building_hp[definition.id]) > 0.0:
|
||||||
|
combat.add_building(definition, float(building_hp[definition.id]))
|
||||||
|
_spawn_waves()
|
||||||
|
_spawn_champions()
|
||||||
|
_spawn_deployed_boss()
|
||||||
|
_spawn_armies()
|
||||||
|
phase = Phase.COMBAT
|
||||||
|
return true
|
||||||
|
|
||||||
|
func _draft_valid(player: Dictionary) -> bool:
|
||||||
|
var draft: Dictionary = player.draft
|
||||||
|
if int(draft.gold) < 0 or (draft.roster as Array).size() > balance.supply_cap:
|
||||||
|
return false
|
||||||
|
var allowed: Array[String] = base_ids(player.bund)
|
||||||
|
for entry: Dictionary in draft.roster:
|
||||||
|
var id: String = str(entry.type_id)
|
||||||
|
var base: String = id
|
||||||
|
for candidate: String in allowed:
|
||||||
|
if str(DataCatalog.get_unit(candidate).get("upgrade_id", "")) == id:
|
||||||
|
base = candidate
|
||||||
|
if not allowed.has(base) or not _valid_destination(str(entry.lane)):
|
||||||
|
return false
|
||||||
|
var hero: Dictionary = draft.hero
|
||||||
|
if not hero.is_empty():
|
||||||
|
var items: Array = hero.items
|
||||||
|
if items.size() > LuneContent.ITEM_SLOTS or not _valid_destination(str(hero.lane)):
|
||||||
|
return false
|
||||||
|
for index: int in range(items.size()):
|
||||||
|
if items.find(items[index]) != index or not LuneContent.ITEMS.has(items[index]):
|
||||||
|
return false
|
||||||
|
if (hero.talents as Array).size() > LuneContent.TALENT_LEVELS.size():
|
||||||
|
return false
|
||||||
|
return (draft.new_bookings as Array).size() <= LuneContent.MAX_BOOKINGS
|
||||||
|
|
||||||
|
func _spawn_waves() -> void:
|
||||||
|
var wave: Array[Dictionary] = LuneWaves.build(balance, round_number)
|
||||||
|
for team: int in range(2):
|
||||||
|
for lane: String in LuneMap.LANES:
|
||||||
|
for index: int in range(wave.size()):
|
||||||
|
var row: int = index / 3
|
||||||
|
var lateral: float = (float(index % 3) - 1.0) * 1.3
|
||||||
|
combat.spawn("monster", team, lane, wave[index].duplicate(true), 11.0 - float(row) * 1.4, lateral)
|
||||||
|
|
||||||
|
func _spawn_champions() -> void:
|
||||||
|
for team: int in range(2):
|
||||||
|
for lane: String in LuneMap.LANES:
|
||||||
|
var index: int = 0
|
||||||
|
for slot: int in range(team * 3, team * 3 + 3):
|
||||||
|
for booking: Dictionary in players[slot].bookings:
|
||||||
|
if int(booking.deploy_round) == round_number and booking.lane == lane:
|
||||||
|
var definition: Dictionary = LuneContent.champion_definition(booking.champion_id, round_number, balance)
|
||||||
|
combat.spawn("champion", team, lane, definition, 13.0, (float(index % 3) - 1.0) * 1.4, slot, str(booking.uid))
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
func _spawn_deployed_boss() -> void:
|
||||||
|
var claim: Dictionary = boss.claim
|
||||||
|
if claim.is_empty() or int(claim.deploy_round) != round_number:
|
||||||
|
return
|
||||||
|
var definition: Dictionary = LuneContent.boss_definition(round_number, balance, true)
|
||||||
|
combat.spawn("boss", int(claim.team), str(claim.lane), definition, 14.0, 0.0, int(claim.chooser_slot), "boss-%d" % round_number)
|
||||||
|
boss_log.append({"round": round_number, "event": "deployed", "team": claim.team, "lane": claim.lane})
|
||||||
|
|
||||||
|
func _army_entries(slot: int) -> Array:
|
||||||
|
var player: Dictionary = players[slot]
|
||||||
|
var entries: Array = []
|
||||||
|
for entry: Dictionary in player.roster:
|
||||||
|
entries.append({"slot": slot, "uid": entry.uid, "seq": int(entry.seq), "lane": entry.lane, "role": role_of(entry.type_id),
|
||||||
|
"invested": int(entry.invested), "definition": _troop_definition(entry.type_id), "kind": "troop"})
|
||||||
|
var hero: Dictionary = player.hero
|
||||||
|
if not hero.is_empty():
|
||||||
|
var invested: int = int(hero.invested)
|
||||||
|
for item_id: String in hero.items:
|
||||||
|
invested += int(LuneContent.ITEMS[item_id].cost)
|
||||||
|
var definition: Dictionary = LuneContent.hero_definition(hero)
|
||||||
|
definition.role = HERO_ROLES[hero.hero_id]
|
||||||
|
entries.append({"slot": slot, "uid": hero.uid, "seq": int(hero.seq), "lane": hero.lane, "role": definition.role,
|
||||||
|
"invested": invested, "definition": definition, "kind": "hero"})
|
||||||
|
return entries
|
||||||
|
|
||||||
|
func _troop_definition(type_id: String) -> Dictionary:
|
||||||
|
var definition: Dictionary = DataCatalog.get_unit(type_id)
|
||||||
|
definition.role = role_of(type_id)
|
||||||
|
return definition
|
||||||
|
|
||||||
|
func _sorted(entries: Array) -> Array:
|
||||||
|
entries.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
var ra: int = ROW_ORDER.find(a.role)
|
||||||
|
var rb: int = ROW_ORDER.find(b.role)
|
||||||
|
return a.seq < b.seq if ra == rb else ra < rb)
|
||||||
|
return entries
|
||||||
|
|
||||||
|
func _spawn_armies() -> void:
|
||||||
|
for team: int in range(2):
|
||||||
|
for lane: String in LuneMap.LANES:
|
||||||
|
# The lane owner's troops form the first rows; supporting players follow behind.
|
||||||
|
var owner: int = slot_of(team, lane)
|
||||||
|
var ordered: Array = []
|
||||||
|
for slot: int in [owner] + [team * 3, team * 3 + 1, team * 3 + 2].filter(func(s: int) -> bool: return s != owner):
|
||||||
|
ordered.append_array(_sorted(_army_entries(slot).filter(func(e: Dictionary) -> bool: return e.lane == lane)))
|
||||||
|
for index: int in range(ordered.size()):
|
||||||
|
var entry: Dictionary = ordered[index]
|
||||||
|
var row: int = index / 4
|
||||||
|
var lateral: float = (float(index % 4) - 1.5) * 1.2
|
||||||
|
combat.spawn(entry.kind, team, lane, entry.definition, maxf(0.4, 7.2 - float(row) * 1.5), lateral, entry.slot, str(entry.uid))
|
||||||
|
# Grotto expeditions leave their lanes for the whole combat phase.
|
||||||
|
var expedition: bool = false
|
||||||
|
for team: int in range(2):
|
||||||
|
var ordered: Array = []
|
||||||
|
for slot: int in range(team * 3, team * 3 + 3):
|
||||||
|
ordered.append_array(_sorted(_army_entries(slot).filter(func(e: Dictionary) -> bool: return e.lane == GROTTO)))
|
||||||
|
var entry_point: Vector2 = map.grotto_entry(team)
|
||||||
|
var inward: Vector2 = (map.grotto_boss_point() - entry_point).normalized()
|
||||||
|
var across := Vector2(-inward.y, inward.x) * (1.0 if team == 0 else -1.0)
|
||||||
|
for index: int in range(ordered.size()):
|
||||||
|
var entry: Dictionary = ordered[index]
|
||||||
|
var at: Vector2 = entry_point - inward * float(index / 3) * 1.2 + across * (float(index % 3) - 1.0) * 1.1
|
||||||
|
combat.spawn_at(entry.kind, team, "grotto", entry.definition, at, entry.slot, str(entry.uid))
|
||||||
|
expedition = true
|
||||||
|
if expedition:
|
||||||
|
combat.add_grotto_boss(LuneContent.boss_definition(round_number, balance, false))
|
||||||
|
|
||||||
|
func role_of(type_id: String) -> String:
|
||||||
|
var id: String = type_id
|
||||||
|
for base: String in ROLES:
|
||||||
|
if base == id or str(DataCatalog.get_unit(base).get("upgrade_id", "")) == id:
|
||||||
|
return ROLES[base]
|
||||||
|
return "melee"
|
||||||
|
|
||||||
|
func step_combat() -> void:
|
||||||
|
if phase != Phase.COMBAT or combat == null:
|
||||||
|
return
|
||||||
|
combat.step()
|
||||||
|
if combat.finished:
|
||||||
|
_settle()
|
||||||
|
|
||||||
|
## Runs the current combat phase to its end (tests, bots-only simulations).
|
||||||
|
func finish_combat() -> void:
|
||||||
|
while phase == Phase.COMBAT:
|
||||||
|
step_combat()
|
||||||
|
|
||||||
|
func _settle() -> void:
|
||||||
|
var gains: Array[int] = [0, 0, 0, 0, 0, 0]
|
||||||
|
var lane_kills: Dictionary = {}
|
||||||
|
var buildings_lost: Array[String] = []
|
||||||
|
var xp_gain: Dictionary = {}
|
||||||
|
for event: Dictionary in combat.events:
|
||||||
|
var dying: int = int(event.team)
|
||||||
|
var killer: int = 1 - dying if dying < 2 else -1
|
||||||
|
var bounty: int = int(event.bounty)
|
||||||
|
if event.kind == "boss" and event.zone == "lane" and bounty > 0 and killer >= 0:
|
||||||
|
# The defenders of a deployed boss share its bounty evenly.
|
||||||
|
_split(gains, killer, bounty)
|
||||||
|
elif event.kind in ["monster", "champion"] and bounty > 0 and event.zone == "lane" and killer >= 0:
|
||||||
|
var owner: int = slot_of(killer, str(event.lane))
|
||||||
|
var lane_part: int = int(floor(float(bounty) * balance.lane_share))
|
||||||
|
gains[owner] += lane_part
|
||||||
|
_split(gains, killer, bounty - lane_part)
|
||||||
|
lane_kills[event.lane] = int(lane_kills.get(event.lane, 0)) + 1
|
||||||
|
elif event.kind in ["tower", "core"]:
|
||||||
|
buildings_lost.append(str(event.building_id))
|
||||||
|
_split(gains, killer, bounty)
|
||||||
|
var heroes: Array = event.get("xp_heroes", [])
|
||||||
|
if not heroes.is_empty():
|
||||||
|
var share: int = maxi(1, int(event.xp) / heroes.size())
|
||||||
|
for uid: String in heroes:
|
||||||
|
xp_gain[uid] = int(xp_gain.get(uid, 0)) + share
|
||||||
|
for unit: LuneUnit in combat.units:
|
||||||
|
if unit.is_building():
|
||||||
|
building_hp[unit.building_id] = unit.hp if unit.alive else 0.0
|
||||||
|
elif unit.kind == "hero":
|
||||||
|
xp_gain[unit.persistent_uid] = int(xp_gain.get(unit.persistent_uid, 0)) + LuneContent.XP_PARTICIPATION + LuneContent.XP_PER_ROUND * round_number
|
||||||
|
for player: Dictionary in players:
|
||||||
|
var hero: Dictionary = player.hero
|
||||||
|
if not hero.is_empty() and xp_gain.has(hero.uid):
|
||||||
|
hero.xp = int(hero.xp) + int(xp_gain[hero.uid])
|
||||||
|
var income: int = balance.round_income(round_number)
|
||||||
|
for player: Dictionary in players:
|
||||||
|
var total: int = gains[player.slot] + (0 if not combat.destroyed_cores.is_empty() else income)
|
||||||
|
player.gold = int(player.gold) + total
|
||||||
|
player.earned = int(player.earned) + total
|
||||||
|
# Deployed bookings are consumed; they never return as property.
|
||||||
|
player.bookings = (player.bookings as Array).filter(func(b: Dictionary) -> bool: return int(b.deploy_round) > round_number)
|
||||||
|
# The claim is decided from this round's expedition before its units go home.
|
||||||
|
var boss_result: String = _settle_boss()
|
||||||
|
for player: Dictionary in players:
|
||||||
|
# An expedition is an order for one round: units return to their owner's lane.
|
||||||
|
for entry: Dictionary in player.roster:
|
||||||
|
if entry.lane == GROTTO:
|
||||||
|
entry.lane = player.lane
|
||||||
|
if not (player.hero as Dictionary).is_empty() and player.hero.lane == GROTTO:
|
||||||
|
player.hero.lane = player.lane
|
||||||
|
last_settlement = {"round": round_number, "gains": gains, "income": income, "buildings_lost": buildings_lost,
|
||||||
|
"end_reason": combat.end_reason, "combat_time": combat.elapsed, "lane_kills": lane_kills, "xp": xp_gain, "boss": boss_result}
|
||||||
|
history.append({"round": round_number, "end_reason": combat.end_reason, "combat_time": combat.elapsed,
|
||||||
|
"buildings": _alive_count(), "gold": players.map(func(p: Dictionary) -> int: return int(p.gold)),
|
||||||
|
"lost": buildings_lost.duplicate(), "boss": boss_result,
|
||||||
|
"hero_levels": players.map(func(p: Dictionary) -> int: return 0 if (p.hero as Dictionary).is_empty() else LuneContent.level_for(int(p.hero.xp)))})
|
||||||
|
if not combat.destroyed_cores.is_empty():
|
||||||
|
winner = DRAW if combat.destroyed_cores.size() >= 2 else 1 - combat.destroyed_cores[0]
|
||||||
|
end_note = "Beide Ancients fielen im selben Schritt." if winner == DRAW else "Ancient von %s zerstört." % ("Blau" if combat.destroyed_cores[0] == 0 else "Rot")
|
||||||
|
phase = Phase.OVER
|
||||||
|
return
|
||||||
|
round_number += 1
|
||||||
|
phase = Phase.PLANNING
|
||||||
|
for player: Dictionary in players:
|
||||||
|
player.draft = _snapshot(player)
|
||||||
|
player.undo = []
|
||||||
|
player.ready = not bool(player.is_human)
|
||||||
|
|
||||||
|
## Boss bookkeeping at settlement. Returns a short outcome label for reports.
|
||||||
|
## Idempotent: a repeated settlement signal for the same combat changes nothing.
|
||||||
|
func _settle_boss() -> String:
|
||||||
|
var fought: int = combat.round_number
|
||||||
|
if int(boss.get("settled_round", 0)) == fought:
|
||||||
|
return "repeat"
|
||||||
|
boss.settled_round = fought
|
||||||
|
var result: String = "none"
|
||||||
|
var claim: Dictionary = boss.claim
|
||||||
|
if not claim.is_empty() and int(claim.deploy_round) == fought:
|
||||||
|
# The deployment happens exactly once; afterwards the claim is gone.
|
||||||
|
boss.claim = {}
|
||||||
|
result = "deployed"
|
||||||
|
if combat.grotto_outcome == "success" and (boss.claim as Dictionary).is_empty() and fought >= int(boss.next_available_round):
|
||||||
|
var team: int = combat.grotto_claim_team
|
||||||
|
var chooser: int = _boss_chooser(team)
|
||||||
|
boss.claim = {"team": team, "won_round": fought, "deploy_round": fought + 1, "chooser_slot": chooser,
|
||||||
|
"lane": players[chooser].lane}
|
||||||
|
boss.next_available_round = fought + 1 + LuneContent.BOSS_LOCK_ROUNDS
|
||||||
|
boss_log.append({"round": fought, "event": "won", "team": team, "chooser": chooser})
|
||||||
|
result = "won"
|
||||||
|
elif combat.grotto_outcome == "failed":
|
||||||
|
boss_log.append({"round": fought, "event": "failed"})
|
||||||
|
result = "failed" if result == "none" else result
|
||||||
|
return result
|
||||||
|
|
||||||
|
## Contributing lane player of the winning team with the highest army value in the grotto;
|
||||||
|
## ties go to the lower slot.
|
||||||
|
func _boss_chooser(team: int) -> int:
|
||||||
|
var best: int = team * 3
|
||||||
|
var best_value: int = -1
|
||||||
|
for slot: int in range(team * 3, team * 3 + 3):
|
||||||
|
var value: int = 0
|
||||||
|
for entry: Dictionary in _army_entries(slot):
|
||||||
|
if entry.lane == GROTTO:
|
||||||
|
value += int(entry.invested)
|
||||||
|
if value > best_value:
|
||||||
|
best = slot
|
||||||
|
best_value = value
|
||||||
|
return best
|
||||||
|
|
||||||
|
## Distributes an amount evenly over a team; the remainder rotates so no slot is favoured.
|
||||||
|
func _split(gains: Array[int], team: int, amount: int) -> void:
|
||||||
|
if amount <= 0 or team < 0:
|
||||||
|
return
|
||||||
|
var share: int = amount / 3
|
||||||
|
var rest: int = amount % 3
|
||||||
|
for offset: int in range(3):
|
||||||
|
gains[team * 3 + offset] += share
|
||||||
|
for index: int in range(rest):
|
||||||
|
gains[team * 3 + (_reward_rotation[team] + index) % 3] += 1
|
||||||
|
_reward_rotation[team] = (_reward_rotation[team] + rest) % 3
|
||||||
|
|
||||||
|
func _alive_count() -> Array[int]:
|
||||||
|
var counts: Array[int] = [0, 0]
|
||||||
|
for id: String in building_hp:
|
||||||
|
if float(building_hp[id]) > 0.0:
|
||||||
|
counts[0 if id.begins_with("blau") else 1] += 1
|
||||||
|
return counts
|
||||||
|
|
||||||
|
func building_alive(id: String) -> bool:
|
||||||
|
return float(building_hp.get(id, 0.0)) > 0.0
|
||||||
|
|
||||||
|
## Live state for displays: during combat the combat instance is authoritative.
|
||||||
|
func building_alive_now(id: String) -> bool:
|
||||||
|
if phase == Phase.COMBAT and combat != null and combat.buildings.has(id):
|
||||||
|
return (combat.buildings[id] as LuneUnit).alive
|
||||||
|
return building_alive(id)
|
||||||
|
|
||||||
|
func towers_alive(team: int, lane: String, live: bool = false) -> int:
|
||||||
|
var prefix: String = "blau" if team == 0 else "rot"
|
||||||
|
var probe: Callable = building_alive_now if live else building_alive
|
||||||
|
return int(probe.call("%s_%s_outer" % [prefix, lane])) + int(probe.call("%s_%s_inner" % [prefix, lane]))
|
||||||
|
|
||||||
|
func core_hp(team: int) -> float:
|
||||||
|
return float(building_hp["blau_ancient" if team == 0 else "rot_ancient"])
|
||||||
|
|
||||||
|
func gold_of(slot: int) -> int:
|
||||||
|
return int(players[slot].draft.gold) if phase == Phase.PLANNING else int(players[slot].gold)
|
||||||