Files
sow-module-tilesets/AGENTS.md
T
archvillainetteandClaude Opus 4.8 60af184be5 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>
2026-07-04 16:55:43 +02:00

375 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENTS.md — Building NWN:EE Tilesets
Operational guide for building **Neverwinter Nights: Enhanced Edition** tilesets in
this repo. Read this before touching any tileset directory. It bridges our
Blender-prototype conventions to the hard requirements of the NWN engine and
toolset, so geometry built here converts cleanly to a shippable `.hak`.
> This is a reference, not a tutorial to read top-to-bottom. Jump to the section
> you need. When a fact here disagrees with the linked nwn.wiki pages
> ([Sources](#sources)), the wiki wins — tell the user and fix this file.
---
## 0. Repo layout
Each tileset lives in its own directory. One tileset = one 5-char prefix = one
`.set` + its models/walkmeshes/palette.
```
sow-module-tilesets/
├── AGENTS.md ← you are here (applies to all tilesets)
├── wgt01/ ← Westgate Urban Foundation MVP (streets, canals, ramps)
│ ├── KICKOFF.md ← design brief / tile list for this tileset (the spec)
│ ├── concept-art-1.png
│ ├── flake.nix ← Blender 4.0.2 + generator entry points
│ └── generator/ ← bpy scripts that build the .blend source
└── <future-tileset>/ ← same shape
```
- **wgt01** is the *urban foundation* set: ground grammar only (streets,
sidewalks, plazas, ramps, terraces, canals, bridges). Buildings/props are
placeables layered on top later — **not** part of any tileset here.
- Other tilesets (interiors, dungeons, wilderness) may follow. They share these
rules; only their `KICKOFF.md` brief and prefix differ.
- A tileset's `KICKOFF.md` is authoritative for *what tiles to build*. This file
is authoritative for *how NWN needs them shaped and packaged*.
---
## 1. Hard constraints (memorize these)
| Constraint | Value | Consequence if violated |
|---|---|---|
| Tile footprint | **10 m × 10 m** on XY, centered at origin (X,Y ∈ [5, +5]) | Misaligned adjacency, gaps |
| Ground level | **Z = 0**; walkable floor cannot go below 0 | — |
| Axes | **Z up, X = West→East, Y = South→North** | — |
| Resref (any model/wok/tga name) | **≤ 16 characters** | Silent truncation / breakage |
| Tileset prefix | **5 chars**, e.g. `TWU01` (Tileset Westgate Urban 01) | — |
| Walkmesh polycount | **~100 faces per tile** ideal | GMax/exporter crash, broken AABB |
| Faces per walkmesh vertex | **≤ 8** | Export failure |
| Lights per tile | **≤ 2 mainlight + ≤ 2 sourcelight** | — |
| Walkable faces | **must face upward, single level, no overlap** | Downward-facing walkable *crashes the exporter* and makes players jerk/hop |
| Walkmesh edges | exactly at **±5 m** from center | Creatures can't cross tile boundary |
| `.set` file | **INI text, DOS/CRLF line endings** | Toolset crashes on area creation |
| Area size | **≤ 32 × 32 tiles** | — |
| Module | **≤ 100 tilesets** loaded | — |
Prototype-phase relief (from `KICKOFF.md`): geometry may be simple, materials
flat, exact NWN export is **not** required for the Blender MVP. But build with
these numbers in mind so conversion is mechanical, not a rework.
---
## 2. NWN tile fundamentals
### 2.1 Corners, edges, terrains, crossers
A tile connects to neighbors by matching **corners** and **edges**:
- **4 corners** — `TopLeft, TopRight, BottomLeft, BottomRight`. Each carries a
**terrain type** (string, no whitespace) *and* a **height** (integer number of
transition steps above 0). Two tiles are adjacency-compatible only if the
shared corners' terrain **and** height match.
- **4 edges** — `Top, Right, Bottom, Left`. Each carries an optional **crosser**
(linear feature that splits the edge: road, stream, wall, canal). Blank = none.
**Terrain** = the surface a corner sits on (`cobble`, `plaza`, `canal`, `void`).
**Crosser** = a linear feature crossing an edge (`street`, `sidewalk`, `canal_edge`).
Both are referenced **by name** in the `.set` and `.itp` — names are
case-sensitive and cannot contain spaces.
Mixing terrains is expensive: every pair of terrains that can meet needs
transition tiles.
- 2 terrains → ~67 tiles minimum (1 pure A, 1 pure B, 45 transitions).
- 3 terrains → +9 tiles per added terrain.
- 4 terrains → full matrix, substantially more.
Keep the terrain/crosser vocabulary **small and deliberate** per tileset.
### 2.2 Heights
- `Transition` (in `[GENERAL]`) = meters between height step 0 and step 1.
Decimals allowed. BioWare original content used **5 m**; custom sets use **23 m**.
- **wgt01 uses a raised tier at Z = 1.5 m**, so its `Transition = 1.5` and the
raised tiles sit at corner height `1`.
- Walkable ground can only be *raised* in whole steps, never lowered below Z=0.
Visual water/void may dip (e.g. canal at Z = 0.3) **only under a non-walkable
walkmesh** — never a playable under/over space (matches `KICKOFF.md`).
---
## 3. The tile model (`.mdl`)
ASCII MDL. Binary MDLs must be decompiled first. Structure:
### 3.1 AuroraBase (root dummy)
Mandatory root node. **Node name must equal the filename** (e.g. `twu01_a01_01`).
Set its classification/**Type = `Tile`** for any tile that has a `.wok`. Holds:
tile name, animation frames, supermodel link, and all child nodes.
### 3.2 Renderable geometry (trimesh)
The visible tile. Children of AuroraBase. Keep polys low, edges snap-clean at
±5 m, no geometry past bounds except explicitly-marked harmless trim.
### 3.3 Walkmesh (AABB node) — the `.wok`
Invisible collision + pathfinding mesh. **Parented to AuroraBase only, exactly
one AABB node per tile.** Governs walkable/non-walkable triangles, surface
material per triangle, water splashes, dynamic grass, camera collision, blood.
**The 8 walkmesh rules** (violating these crashes exporters or breaks pathing):
1. ~100 faces max.
2. ≤ 8 faces per vertex (non-walkable often exempt, still keep minimal).
3. Single-level walkable — walkable-face vertices must not overlap other
walkable faces. Non-walkable faces unrestricted.
4. Walkable faces point **up**. Downward-facing walkable = exporter crash +
player jerking. Downward non-walkable (ceilings) is fine.
5. One AABB node only.
6. No straight vertical edges (breaks no-overlap). Offset height changes ≥ 1 cm
on XY per axis.
7. Origin at tile center `(0,0,0)`, matching AuroraBase. No scale/rotation
transforms — including on raised tiles.
8. Walkmesh edges at exactly ±5 m for cross-tile traversal. Sub-cm gaps usually
still cross; larger gaps block.
**Surface material types** (per triangle; defined globally in `surfacemat.2da`,
sounds in `footstepsounds.2da`):
`Dirt, Grass, Stone, Wood, Water, Carpet, Metal, Puddles, Leaves, Sand, Snow`.
Non-walkable proxies use a non-walkable surfacemat (e.g. the "barebones" row that
blocks walking + camera but allows spell targeting). **Surfacemat #3 (Grass)**
triggers engine dynamic grass (configured in the `.set` `[GRASS]` section).
### 3.4 Lights (dummy nodes, not real lamps)
Named `<tilename>ml1`, `<tilename>ml2` (mainlights — floating ambient, area-
controllable) and `<tilename>sl1`, `<tilename>sl2` (sourcelights — tied to
torches/fires, respond to day/night). Max 2 of each. Colors restricted to
`tilecolor.2da`.
### 3.5 Animations (on AuroraBase)
Standard 50 frames @ 30 FPS:
`animloop1/2/3` (independently toggleable mesh subsets), `Day`, `Night`,
`Day2Night`, `Night2Day` (1-hour transitions), `tiledefault` (optional fallback).
Bind individual mesh parts to an `animloopN` so one tile can serve multiple states.
### 3.6 A-node (transparency layer, optional)
Dummy `<tilename>a` parented to AuroraBase; its mesh children render in the
dynamic phase (after static meshes) for correct transparency ordering (glass,
foliage, water). Note: VFX particles are still occluded by A-node transparent
objects unless their texture's alpha = 0.
---
## 4. The `.set` file (INI, CRLF line endings)
Named `<prefix>.set` (e.g. `twu01.set`). **Strict section order:**
`[GENERAL] → [GRASS] → Terrain Types → Crosser Types → Primary Rules →
Secondary Rules → Tiles → Groups`. Tiles are referenced **by numeric index**
here (the `.itp` references the same tiles **by name** — keep them in sync or
placement silently breaks).
### `[GENERAL]`
`Name` (internal id, referenced by doortypes/loadscreens), `Type=SET`,
`Version`, `Interior` (0=exterior w/ weather+day/night, 1=interior),
`HasHeightTransition` (1 if raise/lower used), `EnvMap` (water env map),
`Transition` (meters/step, decimals ok), `SelectorHeight` (wireframe visual),
`DisplayName` (tlk ref) or `UnlocalizedName`, `Border` (edge terrain),
`Default` (terrain for new areas), `Floor` (eraser terrain).
### `[GRASS]`
`Grass` (0/1), `GrassTextureName`, `Density`, `Height` (m),
`Ambient{Red,Green,Blue}`, `Diffuse{Red,Green,Blue}`.
### Terrain / Crosser types
`[TERRAIN TYPES] Count=N`, then `[TERRAIN0..N-1]` each with `Name` (case-sensitive,
no spaces) and optional `StrRef`. `[CROSSER TYPES]` + `[CROSSER#]` identical shape.
### Primary rules (toolset auto-picking)
`[PRIMARY RULES] Count=N`, then `[PRIMARY RULE#]` with
`Placed, PlacedHeight, Adjacent, AdjacentHeight, Changed, ChangedHeight`.
When a builder paints adjacent matching terrain, the toolset swaps in the
`Changed` tile automatically. `[SECONDARY RULES]` is unused in vanilla (Count=0).
### `[TILE#]` (per-tile entry — the core)
```
[TILE0]
Model=twu01_a01_01 ; .mdl basename, ≤16 chars, == AuroraBase node name
WalkMesh=msb01 ; legacy walkmesh ref (BioWare default "msb01")
ImageMap2D=twu01_a01_01 ; minimap .tga/.dds, ≥16×16 px
TopLeft=cobble ; corner terrain
TopLeftHeight=0 ; corner height step
TopRight=cobble
TopRightHeight=0
BottomLeft=cobble
BottomLeftHeight=0
BottomRight=cobble
BottomRightHeight=0
Top=street ; edge crosser (blank if none)
Right=
Bottom=street
Left=
MainLight1=0 ; tilecolor.2da index
MainLight2=0
SourceLight1=0
SourceLight2=0
AnimLoop1=0
AnimLoop2=0
AnimLoop3=0
Orientation=0 ; rotation in ±90° steps; positive = counter-clockwise
PathNode=A ; pathfinding descriptor (A-Z,a-z, case-sensitive)
VisibilityNode= ; LOS; defaults to PathNode if blank (keep simpler)
Doors=0
Sounds=0
```
- **PathNode is required.** Without it, creatures and click-to-move break.
Choose the simplest valid pattern that lets a creature cross the tile. NWN:EE
added no new pathnode types — use the fixed set.
- **VisibilityNode** should usually be simpler than the pathnode (straight
edge-to-edge). Omit to inherit the pathnode.
- **Padding:** reserve future slots with `Model=padding` and corners `(null)`.
### `[TILE#_DOOR#]` (only if that tile's `Doors>0`)
`Type` (doortypes.2da index; 0 = force generic from genericdoors.2da),
`X/Y/Z` (meters), `Orientation` (facing).
### Groups / Features
`[GROUPS] Count=N`, `[GROUP#]` with `Name`, optional `StrRef`, `Rows`, `Columns`,
and `Tile0..` indices **ordered bottom-left → right → up → top-right**. A
1-tile group is a **feature**. Rules:
- First tile must have a **unique** filename (ITP detects the group by it) and
**cannot** be `-1` (random) — that crashes.
- Other positions may use `-1` for a randomized part.
- Whole group rotates as a unit; no per-tile orientation.
- Any non-group tile sharing the first tile's filename is blocked from autopicking.
---
## 5. Supporting files (packaged into the `.hak`)
| File | Purpose | Notes |
|---|---|---|
| `<prefix>.set` | tileset definition | CRLF, references tiles by index |
| `<prefix>palstd.itp` | toolset palette tree | **GFF binary**; references tiles **by name** — must match `.set` |
| `*.mdl` | tile models (ASCII) | one per tile/variant |
| `*.wok` | tile walkmeshes | baked from the AABB node |
| `*.tga` / `*.dds` | minimap icons (`ImageMap2D`) | ≥16×16 px, name ≤16 chars |
| `<prefix>edge.2da` | edge/border tiles (optional) | boundary tiles, grayed + non-walkable, prefix often `z` |
| `doortypes.2da` | tileset-specific doors | **mandatory if custom doors** — must be merged with all other sets' versions and shipped to clients, or doors render blank |
| `genericdoors.2da` | tileset-agnostic doors | index 0 = player picks |
| `surfacemat.2da` | global walkmesh surface behavior | only edit if adding materials |
| `footstepsounds.2da` | per-surface audio | |
| `tilecolor.2da` | allowed light colors | mainlight/sourcelight picker source |
| `loadscreens.2da` | area load screens (optional) | `TileSet` column binds images |
| `areag.ini` | generic area presets (optional) | ambient sound, music, scripts, PvP, interior flag |
**Name-sync gotcha:** `.set` = tiles by **index**, `.itp` = tiles by **name**.
Any mismatch breaks placement silently. Regenerate both from one source of truth.
---
## 6. Blender → NWN pipeline
This repo standardizes on **Blender 4.0.2** (`blender-402-bin`, exposed by the
NixOS overlay) specifically because the NWN toolchain (NeverBlender, cleanmodels)
tracks Blender 3.x4.x, **not** nixpkgs' Blender 5.x. Do not "upgrade" it.
### 6.1 Tools (all on `PATH` in the dev shell)
- **NeverBlender** — Blender addon that exports NWN `.mdl` + `.wok` and defines
node types (trimesh, **AABB walkmesh**, dummy, light). Install a NeverBlender
release compatible with **Blender 4.0.x** into this Blender
(`Edit ▸ Preferences ▸ Add-ons ▸ Install`). It is **not** bundled in the stock
binary — an agent cannot assume it is present; check and tell the user if a
real MDL export is requested.
- **cleanmodels** — cleans/optimizes ASCII MDL, fixes common walkmesh problems.
- **neverwinter-nim** — CLI (`nwn_erf`, `nwn_gff`, …): pack the `.hak`/ERF, and
read/write GFF (the `.itp` is GFF).
- **blender-mcp-nwn** (see `/nix/nixos/blender-mcp-nwn`) — MCP server exposing
`run_blender_script`, `read_blend_metadata`, `export_for_nwn` so Claude can
drive Blender 4.0.2 headless against these `.blend` sources.
### 6.2 Build → export → pack
1. **Generate `.blend` source**`nix run .#build -- out.blend` (or drive via the
MCP server). Produces the tile library scene.
2. **Model in NWN conventions** — one AuroraBase per tile (name == resref, Type
`Tile`), trimesh children, one AABB walkmesh, ≤2+2 lights, animations on base.
3. **Export** — NeverBlender → ASCII `.mdl` + `.wok` per tile.
4. **Clean** — run through `cleanmodels` (walkmesh sanity, tri limits).
5. **Author `.set`** — terrains, crossers, tiles (corners/edges/heights/pathnodes),
groups. **CRLF endings.**
6. **Author `.itp`** — palette tree (GFF), names matching the `.set`.
7. **Minimaps** — render/emit `ImageMap2D` `.tga`/`.dds` per tile.
8. **Pack**`nwn_erf` bundles `.mdl` + `.wok` + `.set` + `.itp` + minimaps +
any 2das into `<prefix>.hak`. Ship `doortypes.2da` to clients if custom doors.
---
## 7. Mapping *our* prototype conventions → NWN
`KICKOFF.md` prototypes deliberately mirror NWN so conversion is mechanical:
| Prototype convention | NWN target |
|---|---|
| Tile centered at origin, X/Y ∈ [5,+5], Z=0 ground, metric | Exactly NWN tile space |
| Raised tier `Z = 1.5` | Height step 1 with `Transition = 1.5` |
| Canal water `Z = 0.3`, void | Visual only, **under a non-walkable walkmesh** |
| Collection `CODE_name` (e.g. `ST_S_straight_street`) | Design ID → export resref `<prefix>_A01_01` (grid position). Keep a code→resref map. |
| `VIS_<code>` objects | Renderable trimesh children of AuroraBase |
| `WOK_<code>` objects | The AABB walkmesh; assign walkable surfacemat (Stone/Cobble→Stone) |
| `BLOCK_<code>` objects | Non-walkable triangles in the walkmesh (blocker surfacemat) |
| `MARK_<code>_*` / `TILE_<code>` empties | Helpers/edge metadata — dropped or become dummies on export |
| `MAT_debug_walkmesh` (green) | Walkable surface material |
| `MAT_debug_blocker` (red) | Non-walkable surface material |
| Edge metadata `N/E/S/W ∈ {FLAT,RAISED,CANAL,RAMP,MIXED}` | `.set` corner terrain+height (FLAT=h0, RAISED=h1) and edge crossers (CANAL=non-walkable edge, RAMP=transition tile). Document each MIXED edge. |
| `TILE_EDGE_METADATA` text block | Source for authoring `.set` corners/edges |
**wgt01 suggested prefix:** `TWU01` (Tileset Westgate Urban 01). Terrains e.g.
`cobble, plaza, foundation, canal, void, raised`; crossers e.g.
`street, sidewalk, curb, gutter, canal_edge, ramp`.
---
## 8. New-tileset checklist
1. Create `<name>/` with a `KICKOFF.md` brief (tiles, edge grammar, height plan).
2. Pick a unique **5-char prefix**; define the **terrain + crosser vocabulary**
(keep it minimal — every terrain pairing multiplies tile count, §2.1).
3. Choose `Transition` (m) and whether `Interior=0/1`.
4. Map the **pathnode** type for each tile shape *before* modeling.
5. Copy the `flake.nix` + `generator/` pattern from `wgt01`; adjust prefix/tiles.
6. Build `.blend`, verify with `read_blend_metadata` (collection/object counts).
7. Export (NeverBlender) → clean (cleanmodels) → author `.set`/`.itp` → minimaps.
8. Pack `.hak` with `nwn_erf`; test in the toolset (create a small area, place
every tile, walk it).
---
## 9. Gotchas (the stuff that silently breaks)
- **`.set` must be CRLF.** Unix line endings crash the toolset on area creation.
- **`.set` index vs `.itp` name mismatch** → tiles won't place, no error.
- **Resref > 16 chars** truncates silently — plan short prefixes/positions.
- **AuroraBase name ≠ filename** → tile won't load correctly.
- **Downward-facing walkable face** → exporter crash + player jerking. Keep
walkable faces up, single-level, non-overlapping.
- **Vertical walkmesh edges / >100 walkmesh faces** → broken AABB.
- **Custom doors not in `doortypes.2da`** (and not merged/shipped) → blank doors.
- **`Transition=0`** disables adjacent placement of groups/features.
- **Group first tile `-1` or non-unique filename** → crash / no autopick.
- **NeverBlender not installed** in this Blender → `export_for_nwn` gives you
OBJ/FBX + an `mdl.json` config, **not** a real `.mdl`. Install the addon for
true MDL/WOK export; say so rather than claiming MDL output.
- **Don't upgrade Blender past 4.x** here — breaks NeverBlender/cleanmodels.
---
## Sources
- [Tileset Construction Tutorial — nwn.wiki](https://nwn.wiki/display/NWN1/Tileset+Construction+Tutorial)
- [SET (file format) — nwn.wiki](https://nwn.wiki/display/NWN1/SET)
- [Tilesets — nwn.wiki](https://nwn.wiki/display/NWN1/Tilesets)
- [NeverBlender — nwn.wiki](https://nwn.wiki/display/NWN1/NeverBlender)
- [Walkmesh — NWNWiki](https://nwn.fandom.com/wiki/Walkmesh)
- [Tile — NWNWiki](https://nwn.fandom.com/wiki/Tile)
- [Common Errors and Their Causes — nwn.wiki](https://nwn.wiki/display/NWN1/Common%20Errors%20and%20Their%20Causes)
_Verify specifics against the wiki before relying on them; correct this file when
you find drift._