AGENTS.md: NWN:EE tileset construction reference (constraints, .mdl/.wok walkmesh rules, .set format, Blender->NWN pipeline, prototype->NWN mapping). wgt01/: Westgate Urban Foundation MVP kit. - generator/tiles.py: single source of truth (40-tile table, materials, 12x12 test map, pure paint/height/walkmesh/.set derivations). - generator/build.py: Blender 4.0.2 scene generator (VIS_/WOK_/BLOCK_ per tile, edge metadata, library grid + 12x12 district previews). - generator/make_wok.py: ASCII walkmesh emitter (verts+faces+surfacemat+AABB tree) from the same geometry. - generator/make_set.py: first-pass twu01.set (CRLF) from the tile grammar. - flake.nix: build/wok/set apps + dev shell (pinned Blender 4.0.2, NWN tools). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
107 lines
3.8 KiB
Python
107 lines
3.8 KiB
Python
"""Generate per-tile ASCII walkmeshes (`twu01_<code>.wok`) from tiles.py.
|
|
|
|
python make_wok.py [outdir] # default: ./wok
|
|
|
|
Pure Python, no Blender. Emits the Aurora ASCII walkmesh node (verts + faces with
|
|
per-face surface material + an AABB bounding-volume tree). This is the usable
|
|
intermediate the toolset/cleanmodels/nwnmdlcomp compile to a binary `.wok`
|
|
(AGENTS.md §6); NeverBlender would otherwise produce this. Geometry comes from
|
|
tiles.wok_quads so it matches the Blender WOK_/BLOCK_ proxies exactly.
|
|
|
|
Surface material ids (surfacemat.2da, AGENTS.md §3.3): walkable faces use WALK,
|
|
non-walkable proxy faces use BLOCK. Verify these two ids against your
|
|
surfacemat.2da before shipping.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
import tiles
|
|
|
|
WALK = 1 # a walkable stone-ish row in surfacemat.2da (verify)
|
|
BLOCK = 7 # a non-walkable row (verify)
|
|
|
|
|
|
def geometry(spec):
|
|
"""Return (verts, faces): dedup verts, faces = (a, b, c, surfacemat)."""
|
|
verts, vmap, faces = [], {}, []
|
|
|
|
def vid(v):
|
|
key = tuple(round(c, 4) for c in v)
|
|
if key not in vmap:
|
|
vmap[key] = len(verts)
|
|
verts.append(key)
|
|
return vmap[key]
|
|
|
|
for quad, walkable in tiles.wok_quads(spec):
|
|
s = WALK if walkable else BLOCK
|
|
a, b, c, d = (vid(p) for p in quad)
|
|
faces.append((a, b, c, s)) # triangulate quad -> 2 tris
|
|
faces.append((a, c, d, s))
|
|
return verts, faces
|
|
|
|
|
|
def _box(verts, face):
|
|
pts = [verts[face[0]], verts[face[1]], verts[face[2]]]
|
|
return ([min(p[i] for p in pts) for i in range(3)],
|
|
[max(p[i] for p in pts) for i in range(3)])
|
|
|
|
|
|
def aabb_tree(verts, faces):
|
|
"""Binary BVH, pre-order (node, left subtree, right subtree). Leaf carries its
|
|
face index; internal carries -1. Reader reconstructs by the same recursion."""
|
|
nodes = []
|
|
|
|
def rec(items): # items = [(face_index, (min, max)), ...]
|
|
mn = [min(b[0][i] for _, b in items) for i in range(3)]
|
|
mx = [max(b[1][i] for _, b in items) for i in range(3)]
|
|
if len(items) == 1:
|
|
nodes.append((mn, mx, items[0][0]))
|
|
return
|
|
ax = max(range(3), key=lambda i: mx[i] - mn[i]) # split longest axis
|
|
items.sort(key=lambda ib: ib[1][0][ax] + ib[1][1][ax]) # by box centre
|
|
mid = len(items) // 2
|
|
nodes.append((mn, mx, -1)) # internal placeholder
|
|
rec(items[:mid])
|
|
rec(items[mid:])
|
|
|
|
rec([(i, _box(verts, f)) for i, f in enumerate(faces)])
|
|
assert sum(1 for _, _, fi in nodes if fi >= 0) == len(faces) # every face is a leaf
|
|
assert len(nodes) == 2 * len(faces) - 1 # full binary tree
|
|
return nodes
|
|
|
|
|
|
def emit(spec):
|
|
verts, faces = geometry(spec)
|
|
nodes = aabb_tree(verts, faces)
|
|
name = tiles.resref(spec)
|
|
L = [f"# {name} walkmesh (ASCII, first-pass) - compile via nwnmdlcomp/cleanmodels",
|
|
f"node aabb {name}_wok",
|
|
f" parent {name}",
|
|
" position 0.0 0.0 0.0",
|
|
" orientation 0.0 0.0 0.0 0.0",
|
|
f" verts {len(verts)}"]
|
|
L += [f" {v[0]:.5f} {v[1]:.5f} {v[2]:.5f}" for v in verts]
|
|
L.append(f" faces {len(faces)}")
|
|
# face line: v1 v2 v3 smoothgroup tv1 tv2 tv3 surfacemat
|
|
L += [f" {a} {b} {c} 1 0 0 0 {s}" for a, b, c, s in faces]
|
|
L.append(" aabb")
|
|
L += [f" {mn[0]:.5f} {mn[1]:.5f} {mn[2]:.5f} {mx[0]:.5f} {mx[1]:.5f} {mx[2]:.5f} {fi}"
|
|
for mn, mx, fi in nodes]
|
|
L.append("endnode")
|
|
return name, "\n".join(L) + "\n"
|
|
|
|
|
|
def main():
|
|
outdir = sys.argv[1] if len(sys.argv) > 1 else "wok"
|
|
os.makedirs(outdir, exist_ok=True)
|
|
for spec in tiles.TILES:
|
|
name, text = emit(spec)
|
|
with open(os.path.join(outdir, name + ".wok"), "w") as fh:
|
|
fh.write(text)
|
|
print(f"wrote {len(tiles.TILES)} .wok files to {outdir}/")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|