wgt01: tileset build guide + 40-tile generator kit

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>
This commit is contained in:
2026-07-04 16:55:43 +02:00
co-authored by Claude Opus 4.8
commit 60af184be5
10 changed files with 1636 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
"""Generate the first-pass NWN tileset definition `twu01.set` from tiles.py.
python make_set.py [out.set] # default: twu01.set
Pure Python, no Blender. Emits a valid, DOS/CRLF `.set` (AGENTS.md §4/§9: Unix
line endings crash the toolset). Corners/heights/crossers derive from each tile's
edge grammar; this is a DRAFT to load and hand-tune in the toolset, not a final
set. See CAVEATS below.
CAVEATS (first pass — tune in the toolset):
* PathNode is a placeholder ('A' walkable / 'N' non-walkable). Real pathnode
letters govern creature routing and must be verified against the toolset.
* ImageMap2D references minimaps (twu01_<code>.tga) that are not rendered yet.
* Groups/features Count=0; add multi-tile groups later.
"""
import sys
import tiles
PREFIX = tiles.PREFIX
def tile_block(i, spec):
cor = tiles.nwn_corners(spec["edges"])
n, e, s, w = spec["edges"]
tl, tr, bl, br = cor["TopLeft"], cor["TopRight"], cor["BottomLeft"], cor["BottomRight"]
ref = tiles.resref(spec)
walkable = spec["kind"] != "canal"
L = [f"[TILE{i}]"]
L += [
f"Model={ref}",
"WalkMesh=", # legacy field; per-model .wok is matched by name
f"ImageMap2D={ref}",
f"TopLeft={tl[0]}", f"TopLeftHeight={tl[1]}",
f"TopRight={tr[0]}", f"TopRightHeight={tr[1]}",
f"BottomLeft={bl[0]}", f"BottomLeftHeight={bl[1]}",
f"BottomRight={br[0]}", f"BottomRightHeight={br[1]}",
f"Top={tiles.nwn_crosser(n)}",
f"Right={tiles.nwn_crosser(e)}",
f"Bottom={tiles.nwn_crosser(s)}",
f"Left={tiles.nwn_crosser(w)}",
"MainLight1=0", "MainLight2=0", "SourceLight1=0", "SourceLight2=0",
"AnimLoop1=0", "AnimLoop2=0", "AnimLoop3=0",
"Orientation=0",
f"PathNode={'A' if walkable else 'N'}", # placeholder — verify in toolset
"VisibilityNode=",
"Doors=0",
"Sounds=0",
]
return L
def build():
L = []
L += [
"[GENERAL]",
f"Name={PREFIX}",
"Type=SET",
"Interior=0",
"HasHeightTransition=1",
"Transition=1.5", # metres per height step = Z=1.5 raised tier
"Border=cobble",
"Default=cobble",
"Floor=cobble",
"DisplayName=-1",
"UnlocalizedName=Westgate Urban Foundation",
"Version=1",
"EnvMap=",
"SelectorHeight=3",
"",
"[GRASS]",
"Grass=0",
"GrassTextureName=",
"Density=0.0",
"Height=0.0",
"AmbientRed=0.0", "AmbientGreen=0.0", "AmbientBlue=0.0",
"DiffuseRed=0.0", "DiffuseGreen=0.0", "DiffuseBlue=0.0",
"",
"[TERRAIN TYPES]",
f"Count={len(tiles.TERRAINS)}",
]
for idx, t in enumerate(tiles.TERRAINS):
L += ["", f"[TERRAIN{idx}]", f"Name={t}", "StrRef=-1"]
L += ["", "[CROSSER TYPES]", f"Count={len(tiles.CROSSERS)}"]
for idx, cr in enumerate(tiles.CROSSERS):
L += ["", f"[CROSSER{idx}]", f"Name={cr}", "StrRef=-1"]
L += ["", "[PRIMARY RULES]", "Count=0",
"", "[SECONDARY RULES]", "Count=0",
"", "[TILES]", f"Count={len(tiles.TILES)}"]
for i, spec in enumerate(tiles.TILES):
L.append("")
L += tile_block(i, spec)
L += ["", "[GROUPS]", "Count=0", ""]
return L
def main():
out = sys.argv[1] if len(sys.argv) > 1 else f"{PREFIX}.set"
with open(out, "w", newline="") as fh: # newline='' + explicit \r\n = CRLF
fh.write("\r\n".join(build()))
print(f"wrote {out} ({len(tiles.TILES)} tiles, "
f"{len(tiles.TERRAINS)} terrains, {len(tiles.CROSSERS)} crossers, CRLF)")
if __name__ == "__main__":
main()