Bots planen Items/Champions vor der Armee und helfen deterministisch, damit Spiegelmatches fair bleiben. Boss und Profile neu abgestimmt; Balance-, Stress- und Volks-Werkzeuge sowie Kartenprops ergänzt.
248 lines
11 KiB
Python
248 lines
11 KiB
Python
"""Map dressing for Champions of Luneraria: river bridge, curb stone, trees, rocks, banner
|
|
and edge cliffs, exported as separate named meshes in one GLB. Godot instances them with
|
|
MultiMeshes. Stone, bark and leaves use ENV_* names so the Kluft shaders (with wind on
|
|
foliage) apply at runtime.
|
|
|
|
Run: ALSOFT_DRIVERS=null blender -b --python-exit-code 1 --python assets-source/luneraria/build_map_props.py
|
|
Godot frame per prop: y up, prop origin at ground level; the bridge spans local +x.
|
|
"""
|
|
from pathlib import Path
|
|
import bpy, bmesh, json, hashlib, struct
|
|
from math import sin, cos, pi
|
|
from mathutils import Vector, noise
|
|
|
|
P = Path(__file__).resolve().parents[2]
|
|
SRC = Path(__file__).resolve().parent
|
|
OUT = P / 'assets/luneraria'
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
bpy.context.preferences.filepaths.save_version = 0
|
|
|
|
|
|
def G(x, y, z):
|
|
return Vector((x, -z, y))
|
|
|
|
|
|
def material(name, color, rough=.9, metal=0.0, emission=0.0):
|
|
m = bpy.data.materials.new(name)
|
|
m.diffuse_color = (*color, 1)
|
|
m.use_nodes = True
|
|
bs = m.node_tree.nodes.get('Principled BSDF')
|
|
bs.inputs['Base Color'].default_value = (*color, 1)
|
|
bs.inputs['Roughness'].default_value = rough
|
|
bs.inputs['Metallic'].default_value = metal
|
|
if emission:
|
|
bs.inputs['Emission Color'].default_value = (*color, 1)
|
|
bs.inputs['Emission Strength'].default_value = emission
|
|
return m
|
|
|
|
|
|
M = {
|
|
'limestone': material('ENV_limestone', (.283, .246, .178)),
|
|
'paving': material('ENV_paving', (.19, .185, .15), .92),
|
|
'cliff': material('ENV_cliff', (.133, .153, .138)),
|
|
'bark': material('ENV_bark', (.087, .056, .032), .93),
|
|
'leaf_dark': material('ENV_leaf_dark', (.031, .07, .04), .89),
|
|
'leaf_mid': material('ENV_leaf_mid', (.084, .144, .054), .85),
|
|
'leaf_light': material('ENV_leaf_light', (.258, .305, .091), .79),
|
|
'soil': material('ENV_soil', (.10, .15, .065), .97),
|
|
'brass': material('ENV_brass', (.515, .366, .153), .45, .7),
|
|
'cloth': material('PR_Banner', (.9, .9, .9), .8),
|
|
}
|
|
groups = {}
|
|
|
|
|
|
def add(group, name, vs, fs, kind, smooth=False):
|
|
me = bpy.data.meshes.new(name)
|
|
me.from_pydata([tuple(v) for v in vs], [], fs)
|
|
me.update()
|
|
me.materials.append(M[kind])
|
|
for p in me.polygons:
|
|
p.use_smooth = smooth
|
|
ob = bpy.data.objects.new(name, me)
|
|
bpy.context.collection.objects.link(ob)
|
|
groups.setdefault(group, []).append(ob)
|
|
return ob
|
|
|
|
|
|
def box(group, name, c, size, kind, yaw=0.0):
|
|
cx, cy, cz = c
|
|
hx, hy, hz = size[0] / 2, size[1] / 2, size[2] / 2
|
|
vs = []
|
|
for dy in (-1, 1):
|
|
for dz in (-1, 1):
|
|
for dx in (-1, 1):
|
|
x, z = dx * hx, dz * hz
|
|
vs.append(G(cx + x * cos(yaw) - z * sin(yaw), cy + dy * hy, cz + x * sin(yaw) + z * cos(yaw)))
|
|
return add(group, name, vs, [(0, 1, 3, 2), (4, 6, 7, 5), (0, 4, 5, 1), (2, 3, 7, 6), (0, 2, 6, 4), (1, 5, 7, 3)], kind)
|
|
|
|
|
|
def lathe(group, name, profile, sides, kind, c=(0, 0, 0), jitter=0.0, seed=0.0, smooth=False):
|
|
cx, cy, cz = c
|
|
vs, fs = [], []
|
|
for r, y in profile:
|
|
for k in range(sides):
|
|
a = 2 * pi * k / sides
|
|
rr = r * (1 + jitter * noise.noise(Vector((cos(a) * 2, y * 1.3, sin(a) * 2 + seed))))
|
|
vs.append(G(cx + cos(a) * rr, cy + y, cz + sin(a) * rr))
|
|
n = len(profile)
|
|
for j in range(n - 1):
|
|
for k in range(sides):
|
|
a, b = j * sides + k, j * sides + (k + 1) % sides
|
|
fs.append((a, b, b + sides, a + sides))
|
|
fs += [tuple(reversed(range(sides))), tuple(range((n - 1) * sides, n * sides))]
|
|
return add(group, name, vs, fs, kind, smooth)
|
|
|
|
|
|
def blob(group, name, c, size, kind, seed, subdiv=2, amp=.3, floor=None, smooth=False):
|
|
bm = bmesh.new()
|
|
bmesh.ops.create_icosphere(bm, subdivisions=subdiv, radius=1.0)
|
|
cc = G(*c)
|
|
for v in bm.verts:
|
|
d = v.co.normalized()
|
|
r = 1 + amp * noise.noise(d * 1.7 + Vector((seed, seed * .7, seed * .3)))
|
|
gx, gy, gz = d.x * r * size[0], d.z * r * size[1], -d.y * r * size[2]
|
|
if floor is not None and gy < floor * size[1]:
|
|
gy = floor * size[1]
|
|
v.co = cc + Vector((gx, -gz, gy))
|
|
me = bpy.data.meshes.new(name)
|
|
bm.to_mesh(me)
|
|
bm.free()
|
|
me.materials.append(M[kind])
|
|
for p in me.polygons:
|
|
p.use_smooth = smooth
|
|
ob = bpy.data.objects.new(name, me)
|
|
bpy.context.collection.objects.link(ob)
|
|
groups.setdefault(group, []).append(ob)
|
|
return ob
|
|
|
|
|
|
def tube(group, name, points, radii, kind, sides=8, steps=10, seed=0.0, gnarl=0.0):
|
|
pts = [G(*p) for p in points]
|
|
vs, fs = [], []
|
|
for j in range(steps + 1):
|
|
t = j / steps
|
|
seg = min(len(pts) - 2, int(t * (len(pts) - 1)))
|
|
u = t * (len(pts) - 1) - seg
|
|
p = pts[seg].lerp(pts[seg + 1], u)
|
|
tangent = (pts[seg + 1] - pts[seg]).normalized()
|
|
ref = Vector((0, 0, 1)) if abs(tangent.z) < .9 else Vector((1, 0, 0))
|
|
a1 = tangent.cross(ref).normalized()
|
|
a2 = tangent.cross(a1)
|
|
rs = min(len(radii) - 2, int(t * (len(radii) - 1)))
|
|
ru = t * (len(radii) - 1) - rs
|
|
radius = radii[rs] * (1 - ru) + radii[rs + 1] * ru
|
|
for k in range(sides):
|
|
ang = 2 * pi * k / sides
|
|
w = 1 + gnarl * noise.noise(Vector((cos(ang), t * 5, sin(ang) + seed)))
|
|
vs.append(p + (a1 * cos(ang) + a2 * sin(ang)) * radius * w)
|
|
for j in range(steps):
|
|
for k in range(sides):
|
|
a, b = j * sides + k, j * sides + (k + 1) % sides
|
|
fs.append((a, b, b + sides, a + sides))
|
|
fs += [tuple(reversed(range(sides))), tuple(range(steps * sides, (steps + 1) * sides))]
|
|
return add(group, name, vs, fs, kind, True)
|
|
|
|
|
|
# ---------------------------------------------------------------- bridge (spans local x, 11 m long, 7 m wide)
|
|
# The simulation is flat: the deck sits at ground level so units walk on it, not inside it.
|
|
L, Wd = 11.0, 7.0
|
|
for i in range(12):
|
|
x = -L / 2 + (i + .5) * L / 12
|
|
box('Bridge', 'Deck_%d' % i, (x, .05, 0), (L / 12 + .02, .1, Wd), 'paving')
|
|
for s_ in (-1, 1):
|
|
box('Bridge', 'Parapet_%d_%d' % (i, s_), (x, .42, s_ * (Wd / 2 - .22)), (L / 12 + .03, .62, .38), 'limestone')
|
|
for x in (-L / 2 + .4, -1.8, 1.8, L / 2 - .4):
|
|
for s_ in (-1, 1):
|
|
lathe('Bridge', 'Post_%d_%d' % (int(x * 10), s_), [(.32, 0), (.32, 1.05), (.26, 1.15), (.2, 1.3)], 6, 'limestone', (x, 0, s_ * (Wd / 2 - .22)))
|
|
lathe('Bridge', 'Cap_%d_%d' % (int(x * 10), s_), [(.22, 0), (.22, .1), (.14, .16)], 6, 'brass', (x, 1.3, s_ * (Wd / 2 - .22)))
|
|
for x in (-2.6, 2.6):
|
|
for s_ in (-1, 1):
|
|
box('Bridge', 'Pier_%d_%d' % (int(x), s_), (x, -.6, s_ * (Wd / 2 + .15)), (1.1, 1.4, .5), 'limestone')
|
|
|
|
# ---------------------------------------------------------------- curb stone (placed along lane edges)
|
|
box('Curb', 'Curb', (0, .12, 0), (1.3, .26, .42), 'limestone')
|
|
|
|
# ---------------------------------------------------------------- trees
|
|
for v, (h, seed) in enumerate([(3.8, 1.0), (4.6, 2.0), (3.2, 3.0)]):
|
|
group = 'Pine_%d' % v
|
|
tube(group, 'Trunk', [(0, 0, 0), (0, h * .55, 0), (0, h, 0)], [.24, .17, .08], 'bark', 8, 6, seed, .1)
|
|
for k in range(5):
|
|
y = h * (.28 + k * .15)
|
|
r = (1.35 - k * .22) * (1 + .08 * v)
|
|
lathe(group, 'Cone_%d' % k, [(r, y), (r * .75, y + .35), (.05, y + h * .26)], 9, ('leaf_dark', 'leaf_mid')[k % 2], (0, 0, 0), .12, seed + k)
|
|
for v, (h, seed) in enumerate([(3.4, 4.0), (4.0, 5.0), (2.8, 6.0)]):
|
|
group = 'Oak_%d' % v
|
|
tube(group, 'Trunk', [(0, 0, 0), (.15, h * .5, .05), (-.1, h * .85, 0)], [.34, .24, .15], 'bark', 9, 8, seed, .15)
|
|
for k in range(3):
|
|
ang = k * 2.1 + seed
|
|
tube(group, 'Branch_%d' % k, [(0, h * .55, 0), (cos(ang) * .9, h * .78, sin(ang) * .9)], [.13, .06], 'bark', 6, 4, seed + k, .1)
|
|
for k in range(9):
|
|
ang = k * 2.4 + seed
|
|
rr = .55 + .5 * ((k * .618) % 1.0)
|
|
s = .95 - .2 * ((k * .37) % 1.0)
|
|
blob(group, 'Leaves_%d' % k, (cos(ang) * rr, h * .88 + .5 * ((k * .53) % 1.0), sin(ang) * rr), (s, s * .78, s),
|
|
('leaf_mid', 'leaf_light', 'leaf_dark')[k % 3], seed * 10 + k, 2, .28)
|
|
for k in range(4):
|
|
ang = k * 1.57 + seed
|
|
tube(group, 'Root_%d' % k, [(0, .25, 0), (cos(ang) * .7, .05, sin(ang) * .7)], [.14, .05], 'bark', 6, 4, seed + k)
|
|
|
|
# ---------------------------------------------------------------- rocks and shrubs
|
|
for v, (size, seed) in enumerate([((1.2, .8, 1.0), 7.0), ((1.8, 1.1, 1.3), 8.0), ((.8, .5, .7), 9.0)]):
|
|
group = 'Rock_%d' % v
|
|
blob(group, 'Rock', (0, size[1] * .3, 0), size, 'cliff', seed, 2, .34, -.35)
|
|
blob(group, 'Moss', (size[0] * .15, size[1] * .82, 0), (size[0] * .55, size[1] * .25, size[2] * .5), 'leaf_dark', seed + 1, 1, .3, -.2)
|
|
for k in range(2):
|
|
blob(group, 'Pebble_%d' % k, (size[0] * (.9 + .3 * k), .1, size[2] * (.5 - k)), (.28, .18, .24), 'limestone', seed + 3 + k, 1, .3, -.3)
|
|
for v in range(2):
|
|
blob('Shrub_%d' % v, 'Shrub', (0, .3, 0), (.8 + .3 * v, .55, .7 + .2 * v), ('leaf_mid', 'leaf_dark')[v], 11 + v, 2, .35, -.3)
|
|
|
|
# ---------------------------------------------------------------- banner (team colour at runtime via PR_Banner)
|
|
lathe('Banner', 'Pole', [(.07, 0), (.07, 4.2), (.1, 4.3), (.02, 4.5)], 6, 'brass')
|
|
vs, fs = [], []
|
|
for j in range(6):
|
|
for i in range(4):
|
|
x = .08 + i * .35
|
|
y = 4.1 - j * .45
|
|
z = .08 * sin(i * 1.3 + j * .6)
|
|
vs.append(G(x, y - (.25 * (i / 3) if j == 5 else 0), z))
|
|
for j in range(5):
|
|
for i in range(3):
|
|
a = j * 4 + i
|
|
fs.append((a, a + 1, a + 5, a + 4))
|
|
add('Banner', 'Cloth', vs, fs, 'cloth', True)
|
|
|
|
# ---------------------------------------------------------------- edge cliffs (a 12 m wall segment, local x)
|
|
for i in range(7):
|
|
x = -6 + i * 2
|
|
h = 3.4 + 1.6 * noise.noise(Vector((i * .7, 3.1, 0)))
|
|
blob('EdgeCliff', 'Block_%d' % i, (x, h * .45, 0), (1.6, h * .6, 1.8), 'cliff', 20 + i, 2, .3, -.5)
|
|
blob('EdgeCliff', 'Grass_%d' % i, (x, h * .85, -.3), (1.2, .35, 1.3), 'leaf_mid', 30 + i, 1, .3, -.2)
|
|
|
|
# Join each group into one mesh object per prop and triangulate.
|
|
exported = []
|
|
for group, objects in groups.items():
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
for ob in objects:
|
|
ob.select_set(True)
|
|
bpy.context.view_layer.objects.active = objects[0]
|
|
bpy.ops.object.join()
|
|
joined = bpy.context.object
|
|
joined.name = group
|
|
bm = bmesh.new()
|
|
bm.from_mesh(joined.data)
|
|
bmesh.ops.triangulate(bm, faces=list(bm.faces))
|
|
bm.to_mesh(joined.data)
|
|
bm.free()
|
|
exported.append(joined)
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
for ob in exported:
|
|
ob.select_set(True)
|
|
bpy.ops.wm.save_as_mainfile(filepath=str(SRC / 'map_props.blend'))
|
|
bpy.ops.export_scene.gltf(filepath=str(OUT / 'map_props.glb'), export_format='GLB', use_selection=True,
|
|
export_materials='EXPORT', export_yup=True, export_animations=False)
|
|
raw = (OUT / 'map_props.glb').read_bytes()
|
|
doc = json.loads(raw[20:20 + struct.unpack_from('<I', raw, 12)[0]])
|
|
counts = {m['name']: sum(doc['accessors'][q['indices']]['count'] // 3 for q in m['primitives']) for m in doc['meshes']}
|
|
(OUT / 'map_props_manifest.json').write_text(json.dumps({'assets': [{'id': 'map_props', 'resource_path': 'res://assets/luneraria/map_props.glb',
|
|
'source': 'assets-source/luneraria/map_props.blend', 'props': counts, 'sha256': hashlib.sha256(raw).hexdigest()}]}, indent=2) + '\n')
|
|
print('MAP_PROPS_BUILD', counts, flush=True)
|