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:
@@ -0,0 +1,77 @@
|
|||||||
|
# =========================
|
||||||
|
# Neverwinter Nights - LFS Assets
|
||||||
|
# =========================
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Audio / Video
|
||||||
|
# -------------------------
|
||||||
|
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.bmu filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.mp3 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.bik filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Images / Textures
|
||||||
|
# -------------------------
|
||||||
|
*.tga filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.dds filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.plt filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.tif filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.psd filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# 3D Models / Geometry
|
||||||
|
# -------------------------
|
||||||
|
*.mdl filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.mdx filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.fbx filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.blend filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Walkmeshes / Collision
|
||||||
|
# -------------------------
|
||||||
|
*.wok filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.pwk filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.dwk filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# NWN Tileset / Layout
|
||||||
|
# -------------------------
|
||||||
|
*.lyt filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.vis filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# NWN GFF Files (CORE)
|
||||||
|
# -------------------------
|
||||||
|
*.are filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.git filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ifo filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.dlg filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.fac filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.jrl filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# NWN Objects (GFF-based)
|
||||||
|
# -------------------------
|
||||||
|
*.utc filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.utp filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.uti filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.utd filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.uts filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.utm filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.utw filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.ute filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.utt filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# Character Files
|
||||||
|
# -------------------------
|
||||||
|
*.bic filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# NWN Misc
|
||||||
|
# -------------------------
|
||||||
|
*.gff filter=lfs diff=lfs merge=lfs -text
|
||||||
|
*.gic filter=lfs diff=lfs merge=lfs -text
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
# 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 → ~6–7 tiles minimum (1 pure A, 1 pure B, 4–5 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 **2–3 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.x–4.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._
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
You are assisting inside Blender to rapidly prototype a Neverwinter Nights: Enhanced Edition urban foundation tileset for the project “Shadows Over Westgate.”
|
||||||
|
|
||||||
|
The goal is NOT to build a full city tileset with buildings. The goal is to generate a reusable tile library of streets, sidewalks, plazas, alleys, ramps, bridges, canals/voids, drains, raised terraces, and foundation pads. Buildings, facades, lamps, signs, roofs, walls, doors, clutter, and district decoration will be modular placeables later.
|
||||||
|
|
||||||
|
Westgate’s intended atmosphere is noir dark fantasy: rain-slick streets, mercantile decay, old stone, smoke, fog, gothic silhouettes, and grimy desperation. The city also has an early industrial-magical layer: steam, alchemy, bound elementals, lightning engines, magical lamps, printing presses, and machinery beneath the gutters. For this pass, represent that only through ground grammar: drains, grates, canal cuts, raised streets, bridge foundations, dark cobbles, and industrial service channels. Do not make buildings.
|
||||||
|
|
||||||
|
PROJECT GOAL
|
||||||
|
|
||||||
|
Create a Blender scene containing a prototype tile library named:
|
||||||
|
|
||||||
|
Westgate_Urban_Foundation_MVP
|
||||||
|
|
||||||
|
Each tile must be a 10m x 10m square module suitable for later NWN tileset conversion.
|
||||||
|
|
||||||
|
Use metric scale.
|
||||||
|
|
||||||
|
Coordinate convention:
|
||||||
|
|
||||||
|
- Each individual tile is centered at origin when built as a source asset.
|
||||||
|
- Tile bounds are X = -5 to +5, Y = -5 to +5.
|
||||||
|
- Ground level is Z = 0.
|
||||||
|
- Raised tier level is Z = 1.5.
|
||||||
|
- Canal/void water surface, if present, is Z = -0.3.
|
||||||
|
- Do not create true multi-level playable over/under spaces. Bridges may cross non-walkable canal/void areas only.
|
||||||
|
- Keep walkable slopes gentle and readable.
|
||||||
|
- Keep edges clean and snap-compatible.
|
||||||
|
|
||||||
|
Create each tile as its own Collection. Name collections exactly with their code and readable name, for example:
|
||||||
|
|
||||||
|
CP1_cobble_plain
|
||||||
|
ST_S_straight_street
|
||||||
|
RA_S_straight_ramp
|
||||||
|
|
||||||
|
Inside each tile collection, create these sub-objects where appropriate:
|
||||||
|
|
||||||
|
VIS_<tilecode> visual geometry
|
||||||
|
WOK_<tilecode> simple walkmesh proxy geometry
|
||||||
|
BLOCK_<tilecode> non-walkable blocker proxy geometry, if needed
|
||||||
|
MARK_<tilecode>_* edge/debug markers, if useful
|
||||||
|
|
||||||
|
The WOK proxy should be simple and functional: mostly flat planes or simple ramp planes. Avoid complex triangulation unless necessary.
|
||||||
|
|
||||||
|
Use simple prototype materials:
|
||||||
|
|
||||||
|
- MAT_wet_cobble_dark
|
||||||
|
- MAT_cobble_patch
|
||||||
|
- MAT_plaza_slab
|
||||||
|
- MAT_sidewalk_stone
|
||||||
|
- MAT_curb_dark_stone
|
||||||
|
- MAT_canal_black_water
|
||||||
|
- MAT_void_black
|
||||||
|
- MAT_ramp_stone
|
||||||
|
- MAT_drain_iron
|
||||||
|
- MAT_foundation_pad
|
||||||
|
- MAT_debug_walkmesh
|
||||||
|
- MAT_debug_blocker
|
||||||
|
|
||||||
|
Do not spend time on final textures. Use flat colors or simple procedural noise only. This is a geometry and tile-grammar prototype.
|
||||||
|
|
||||||
|
REQUIRED TILE LIBRARY
|
||||||
|
|
||||||
|
Generate these tiles first.
|
||||||
|
|
||||||
|
FLAT WALKABLE GROUND
|
||||||
|
|
||||||
|
1. CP1_cobble_plain
|
||||||
|
|
||||||
|
- Full 10x10 walkable dark cobble.
|
||||||
|
- Subtle bevel or edge trim allowed.
|
||||||
|
- WOK full square at Z=0.
|
||||||
|
|
||||||
|
2. CP2_cobble_plain_wet_variant
|
||||||
|
|
||||||
|
- Same as CP1 but with darker wet material patches.
|
||||||
|
- Geometry can be the same with different material regions.
|
||||||
|
|
||||||
|
3. CP3_cobble_plain_broken_variant
|
||||||
|
|
||||||
|
- Same as CP1 but with a few shallow cracked/patch shapes.
|
||||||
|
- Keep WOK flat.
|
||||||
|
|
||||||
|
4. AL1_narrow_alley_paving
|
||||||
|
|
||||||
|
- Full tile walkable, but visual paving suggests a narrower central alley.
|
||||||
|
- Add darker side strips/foundation margins where modular building placeables might sit.
|
||||||
|
- Do not place buildings.
|
||||||
|
|
||||||
|
5. PL1_plaza_slab
|
||||||
|
|
||||||
|
- Full tile walkable large stone slab plaza.
|
||||||
|
- Less busy than cobble.
|
||||||
|
|
||||||
|
6. DR1_drain_detail
|
||||||
|
|
||||||
|
- Full tile walkable cobble with a central or offset iron drain/manhole/grate.
|
||||||
|
- Drain is visual only unless it is a tiny non-walkable inset.
|
||||||
|
|
||||||
|
7. MU1_muddy_repair_patch
|
||||||
|
|
||||||
|
- Full tile walkable cobble with broken/muddy repair patch.
|
||||||
|
- Keep WOK flat.
|
||||||
|
|
||||||
|
8. LOT_foundation_pad
|
||||||
|
|
||||||
|
- A quiet, flat urban foundation pad for placing modular buildings or large placeables.
|
||||||
|
- Mostly plain stone, minimal decoration.
|
||||||
|
- Full WOK square.
|
||||||
|
|
||||||
|
STREET SHAPES
|
||||||
|
|
||||||
|
9. ST_S_straight_street
|
||||||
|
|
||||||
|
- Straight street path running north-south through the tile.
|
||||||
|
- Sidewalk/curb margins on east and west sides.
|
||||||
|
- Entire tile can remain walkable for flexibility, but VIS should clearly read as street + curb + sidewalk.
|
||||||
|
- WOK full square unless blockers are necessary.
|
||||||
|
|
||||||
|
10. ST_C_street_corner
|
||||||
|
|
||||||
|
- L-shaped street corner.
|
||||||
|
- Sidewalk/curb margins.
|
||||||
|
- Use visual contrast, not blockers, unless necessary.
|
||||||
|
|
||||||
|
11. ST_T_street_t_junction
|
||||||
|
|
||||||
|
- T-junction street.
|
||||||
|
|
||||||
|
12. ST_X_street_crossroads
|
||||||
|
|
||||||
|
- Four-way street crossing.
|
||||||
|
|
||||||
|
13. ST_E_street_end
|
||||||
|
|
||||||
|
- Dead-end street with a visual blocked/terminated paving edge.
|
||||||
|
- Do not add a wall or building; just create the ground condition.
|
||||||
|
|
||||||
|
14. ST_W_wide_boulevard
|
||||||
|
|
||||||
|
- Wider street segment, usable for civic or merchant districts.
|
||||||
|
- Include broad sidewalks.
|
||||||
|
|
||||||
|
15. ST_G_gate_mouth
|
||||||
|
|
||||||
|
- Street tile that visually suggests a gate approach or major threshold.
|
||||||
|
- No gate model. Just heavier paving, curb geometry, and alignment pads.
|
||||||
|
|
||||||
|
16. ST_D_street_with_central_drain
|
||||||
|
|
||||||
|
- Straight street with central gutter/drain channel.
|
||||||
|
- Channel should be shallow and walkable or visually recessed without pathing problems.
|
||||||
|
|
||||||
|
SIDEWALK / CURB / GUTTER
|
||||||
|
|
||||||
|
17. SW_S_sidewalk_straight
|
||||||
|
|
||||||
|
- Raised sidewalk/promromenade strip along one or both sides.
|
||||||
|
- Use low curb height, around Z=0.12 to 0.18.
|
||||||
|
- Keep WOK simple.
|
||||||
|
|
||||||
|
18. SW_C_sidewalk_corner
|
||||||
|
|
||||||
|
- Corner curb/sidewalk condition.
|
||||||
|
|
||||||
|
19. SW_T_sidewalk_t_split
|
||||||
|
|
||||||
|
- Sidewalk T connector.
|
||||||
|
|
||||||
|
20. SW_X_sidewalk_crossing
|
||||||
|
|
||||||
|
- Sidewalk/plaza connector or crossing tile.
|
||||||
|
|
||||||
|
21. SW_E_sidewalk_end
|
||||||
|
|
||||||
|
- Sidewalk ending/transition tile.
|
||||||
|
|
||||||
|
22. GD_S_gutter_straight
|
||||||
|
|
||||||
|
- Straight shallow gutter/drain channel.
|
||||||
|
|
||||||
|
23. GD_C_gutter_corner
|
||||||
|
|
||||||
|
- Gutter corner.
|
||||||
|
|
||||||
|
24. GD_X_gutter_crossing_grate
|
||||||
|
|
||||||
|
- Drain crossing with visible grate.
|
||||||
|
|
||||||
|
RAISED TIERS / RAMPS / STAIRS
|
||||||
|
|
||||||
|
25. UP1_raised_terrace
|
||||||
|
|
||||||
|
- Full tile raised walkable surface at Z=1.5.
|
||||||
|
- WOK full square at Z=1.5.
|
||||||
|
|
||||||
|
26. UP2_raised_plaza_variant
|
||||||
|
|
||||||
|
- Raised plaza/slab variant at Z=1.5.
|
||||||
|
|
||||||
|
27. TE_S_retaining_edge_straight
|
||||||
|
|
||||||
|
- Half or edge tile where one side is raised terrace at Z=1.5 and the lower side is at Z=0.
|
||||||
|
- Include vertical retaining wall face.
|
||||||
|
- The top and bottom walkable areas should be clear.
|
||||||
|
- Do not create inaccessible slivers.
|
||||||
|
|
||||||
|
28. TE_C_retaining_edge_outer_corner
|
||||||
|
|
||||||
|
- Raised/lower retaining corner.
|
||||||
|
|
||||||
|
29. TE_I_retaining_edge_inner_corner
|
||||||
|
|
||||||
|
- Raised/lower retaining inside corner.
|
||||||
|
|
||||||
|
30. RA_S_straight_ramp
|
||||||
|
|
||||||
|
- Straight ramp from Z=0 to Z=1.5 across the tile.
|
||||||
|
- Ramp direction north-south.
|
||||||
|
- Add simple side retaining edges.
|
||||||
|
- WOK should be a clean ramp plane.
|
||||||
|
|
||||||
|
31. RA_C_corner_or_dogleg_ramp
|
||||||
|
|
||||||
|
- A corner/dogleg ramp from lower to upper.
|
||||||
|
- Keep it simple and pathable.
|
||||||
|
|
||||||
|
32. STA_broad_stair
|
||||||
|
|
||||||
|
- Broad stair from Z=0 to Z=1.5.
|
||||||
|
- Since NWN pathing can be sensitive, create a simplified ramp-like WOK proxy over the visible stairs.
|
||||||
|
- Visual stairs may be stepped; WOK should be smooth or very simple.
|
||||||
|
|
||||||
|
CANAL / VOID / BRIDGES
|
||||||
|
|
||||||
|
33. CN1_canal_black_water
|
||||||
|
|
||||||
|
- Non-walkable canal/black water/void tile.
|
||||||
|
- Visual water at Z=-0.3.
|
||||||
|
- WOK absent or marked non-walkable.
|
||||||
|
- Add stone canal edge only if it does not imply mismatched adjacency.
|
||||||
|
|
||||||
|
34. CN2_narrow_drainage_cut
|
||||||
|
|
||||||
|
- Narrow non-walkable drainage cut through the tile.
|
||||||
|
- Include stone/cobble edges.
|
||||||
|
|
||||||
|
35. CE_S_canal_edge_straight
|
||||||
|
|
||||||
|
- Straight canal embankment edge.
|
||||||
|
- One side walkable at Z=0, one side canal/void non-walkable.
|
||||||
|
- Add clean retaining wall face.
|
||||||
|
|
||||||
|
36. CE_C_canal_edge_corner
|
||||||
|
|
||||||
|
- Canal edge corner.
|
||||||
|
|
||||||
|
37. CE_I_canal_edge_inside_corner
|
||||||
|
|
||||||
|
- Canal inside corner.
|
||||||
|
|
||||||
|
38. BR_1_narrow_bridge
|
||||||
|
|
||||||
|
- Narrow bridge crossing canal/void.
|
||||||
|
- Walkable bridge deck at Z=0 or slightly above Z=0.
|
||||||
|
- Non-walkable canal beneath.
|
||||||
|
- No playable underpass.
|
||||||
|
- Add simple side parapet placeholders if useful, but keep them low/blocker-safe.
|
||||||
|
|
||||||
|
39. BR_2_broad_bridge
|
||||||
|
|
||||||
|
- Broad bridge suitable for a main street.
|
||||||
|
- Same constraints as BR_1.
|
||||||
|
|
||||||
|
40. SEW_sewer_mouth_culvert
|
||||||
|
|
||||||
|
- Canal/drain edge with sewer mouth or culvert visual.
|
||||||
|
- Mostly decorative, but useful for noir city composition.
|
||||||
|
|
||||||
|
BUILDER GROUP / PREVIEW ASSEMBLIES
|
||||||
|
|
||||||
|
After generating source tile collections, create a separate Collection:
|
||||||
|
|
||||||
|
PREVIEW_tile_library_grid
|
||||||
|
|
||||||
|
In this collection, instantiate or duplicate each tile in a neat grid with labels so I can inspect the full library.
|
||||||
|
|
||||||
|
Also create another Collection:
|
||||||
|
|
||||||
|
PREVIEW_12x12_test_district
|
||||||
|
|
||||||
|
Build this exact 12x12 prototype map using the generated tiles. Place each tile on a 10m grid. Each tile should be positioned by row/column, 10m apart.
|
||||||
|
|
||||||
|
Map:
|
||||||
|
|
||||||
|
Row 01:
|
||||||
|
CN1 CN1 CE_S CE_S BR_2 CE_S CE_S CN1 CN1 CN1 CN1 CN1
|
||||||
|
|
||||||
|
Row 02:
|
||||||
|
CN1 CE_C SW_S ST_S ST_S ST_S SW_S CE_C CN1 CN1 CN1 CN1
|
||||||
|
|
||||||
|
Row 03:
|
||||||
|
CE_S SW_S ST_C ST_S ST_T ST_S ST_C SW_S CE_S CE_S BR_1 CE_S
|
||||||
|
|
||||||
|
Row 04:
|
||||||
|
SW_S AL1 AL1 CP1 PL1 PL1 CP1 AL1 AL1 SW_S ST_S SW_S
|
||||||
|
|
||||||
|
Row 05:
|
||||||
|
SW_S AL1 LOT CP2 PL1 PL1 CP2 LOT AL1 SW_S ST_S SW_S
|
||||||
|
|
||||||
|
Row 06:
|
||||||
|
ST_S ST_S ST_T ST_S ST_X ST_S ST_T ST_S ST_S ST_C ST_S SW_S
|
||||||
|
|
||||||
|
Row 07:
|
||||||
|
SW_S AL1 LOT CP1 ST_S ST_S CP1 LOT AL1 SW_S CP2 SW_S
|
||||||
|
|
||||||
|
Row 08:
|
||||||
|
SW_S AL1 AL1 CP1 PL1 PL1 CP1 AL1 AL1 SW_S CP2 SW_S
|
||||||
|
|
||||||
|
Row 09:
|
||||||
|
CE_S SW_S ST_C ST_S ST_T ST_S ST_C SW_S CE_S TE_S TE_C SW_S
|
||||||
|
|
||||||
|
Row 10:
|
||||||
|
CN1 CE_C SW_S CP1 RA_S UP1 UP1 TE_S TE_S UP1 RA_S SW_S
|
||||||
|
|
||||||
|
Row 11:
|
||||||
|
CN1 CN1 CE_S CE_S BR_2 CE_S CE_S CN1 CE_S SW_S ST_S SW_S
|
||||||
|
|
||||||
|
Row 12:
|
||||||
|
CN1 CN1 CN1 CN1 CN1 CN1 CN1 CN1 CN1 SW_S ST_E SW_S
|
||||||
|
|
||||||
|
Use tile names matching the generated collection names. If exact variants are not available yet, use the closest equivalent and note the substitution.
|
||||||
|
|
||||||
|
DESIGN CONSTRAINTS
|
||||||
|
|
||||||
|
- Do not model buildings.
|
||||||
|
- Do not model full walls except retaining/canal/bridge support walls needed by terrain.
|
||||||
|
- Do not model roofs.
|
||||||
|
- Do not model decorative clutter except minimal drains/grates required to communicate tile purpose.
|
||||||
|
- Avoid tiny details.
|
||||||
|
- Prioritize edge alignment, walkability, and builder usefulness.
|
||||||
|
- Keep polygon counts low.
|
||||||
|
- Keep all tile edges snap-clean.
|
||||||
|
- Avoid overhangs at tile boundaries.
|
||||||
|
- Avoid geometry extending beyond -5/+5 bounds unless it is an explicitly marked harmless visual trim.
|
||||||
|
- Use bevels sparingly.
|
||||||
|
- Prefer clean simple geometry over visual polish.
|
||||||
|
- Use material slots consistently so final textures can replace prototype materials later.
|
||||||
|
|
||||||
|
EDGE COMPATIBILITY
|
||||||
|
|
||||||
|
Think of each tile edge as one of these edge types:
|
||||||
|
|
||||||
|
FLAT walkable ground at Z=0
|
||||||
|
RAISED walkable ground at Z=1.5
|
||||||
|
CANAL non-walkable canal/void
|
||||||
|
RAMP ramp transition edge
|
||||||
|
MIXED special edge, document it
|
||||||
|
|
||||||
|
For each tile, create a custom property or add a Text object note listing its north/east/south/west edge types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
ST_S_straight_street
|
||||||
|
N: FLAT
|
||||||
|
E: FLAT
|
||||||
|
S: FLAT
|
||||||
|
W: FLAT
|
||||||
|
|
||||||
|
RA_S_straight_ramp
|
||||||
|
N: RAISED
|
||||||
|
E: FLAT
|
||||||
|
S: FLAT
|
||||||
|
W: FLAT
|
||||||
|
|
||||||
|
CE_S_canal_edge_straight
|
||||||
|
N: FLAT
|
||||||
|
E: CANAL
|
||||||
|
S: FLAT
|
||||||
|
W: FLAT
|
||||||
|
|
||||||
|
Add these notes in a scene text block called:
|
||||||
|
|
||||||
|
TILE_EDGE_METADATA
|
||||||
|
|
||||||
|
OUTPUT EXPECTATIONS
|
||||||
|
|
||||||
|
At the end:
|
||||||
|
|
||||||
|
1. Save the Blender file as:
|
||||||
|
westgate_urban_foundation_mvp.blend
|
||||||
|
|
||||||
|
2. Report:
|
||||||
|
- number of tile collections created;
|
||||||
|
- any tiles skipped;
|
||||||
|
- any substitutions made;
|
||||||
|
- any geometry/pathing concerns;
|
||||||
|
- next recommended pass.
|
||||||
|
|
||||||
|
3. Do not spend time making final art.
|
||||||
|
4. Do not get stuck on perfect NWN export.
|
||||||
|
5. The deliverable is a rapid, inspectable Blender prototype of the tile library and a test district.
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1,83 @@
|
|||||||
|
{
|
||||||
|
description = "Westgate Urban Foundation MVP — Blender 4.0.2 tileset generator kit";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
# Reuse the machine's pinned Blender 4.0.2 + NWN toolchain via the repo overlay.
|
||||||
|
theorem.url = "git+file:///nix/nixos";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
theorem,
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
overlays = [ theorem.overlays.default ];
|
||||||
|
};
|
||||||
|
blender = pkgs.blender-402-bin; # /bin/blender-4.0.2 (NWN needs 4.0.x, not 5.x)
|
||||||
|
py = pkgs.python3.withPackages (ps: [ ps.mcp ]);
|
||||||
|
|
||||||
|
gen = ./generator; # whole dir so tiles.py sits beside each script for import
|
||||||
|
|
||||||
|
build = pkgs.writeShellApplication {
|
||||||
|
name = "wgt-build";
|
||||||
|
runtimeInputs = [ blender ];
|
||||||
|
text = ''
|
||||||
|
out="''${1:-westgate_urban_foundation_mvp.blend}"
|
||||||
|
exec blender-4.0.2 --background --python ${gen}/build.py -- "$out"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
mkset = pkgs.writeShellApplication {
|
||||||
|
name = "wgt-set";
|
||||||
|
runtimeInputs = [ py ];
|
||||||
|
text = ''exec python ${gen}/make_set.py "$@"'';
|
||||||
|
};
|
||||||
|
|
||||||
|
mkwok = pkgs.writeShellApplication {
|
||||||
|
name = "wgt-wok";
|
||||||
|
runtimeInputs = [ py ];
|
||||||
|
text = ''exec python ${gen}/make_wok.py "$@"'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages.${system}.default = build;
|
||||||
|
|
||||||
|
apps.${system} = {
|
||||||
|
default = {
|
||||||
|
type = "app";
|
||||||
|
program = "${build}/bin/wgt-build";
|
||||||
|
};
|
||||||
|
build = {
|
||||||
|
type = "app";
|
||||||
|
program = "${build}/bin/wgt-build";
|
||||||
|
};
|
||||||
|
set = {
|
||||||
|
type = "app";
|
||||||
|
program = "${mkset}/bin/wgt-set";
|
||||||
|
};
|
||||||
|
wok = {
|
||||||
|
type = "app";
|
||||||
|
program = "${mkwok}/bin/wgt-wok";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.${system}.default = pkgs.mkShell {
|
||||||
|
packages = [
|
||||||
|
blender
|
||||||
|
py
|
||||||
|
pkgs.neverwinter-nim
|
||||||
|
pkgs.cleanmodels
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
export BLENDER_BIN=${blender}/bin/blender-4.0.2
|
||||||
|
echo "wgt01 dev shell — build: blender-4.0.2 --background --python generator/build.py -- out.blend" >&2
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
"""Build the Westgate_Urban_Foundation_MVP Blender scene (run in Blender 4.0.2).
|
||||||
|
|
||||||
|
blender-4.0.2 --background --python build.py -- out.blend
|
||||||
|
|
||||||
|
Realises tiles.py: one Collection per tile (VIS_/WOK_/BLOCK_ sub-objects + edge
|
||||||
|
custom props), a TILE_EDGE_METADATA text block, and the two preview assemblies.
|
||||||
|
Geometry is intentionally simple (KICKOFF.md: a geometry/grammar prototype).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import tiles # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def get_mat(name):
|
||||||
|
m = bpy.data.materials.get(name)
|
||||||
|
if m:
|
||||||
|
return m
|
||||||
|
m = bpy.data.materials.new(name)
|
||||||
|
r, g, b = tiles.COLORS.get(name, (0.5, 0.5, 0.5))
|
||||||
|
m.diffuse_color = (r, g, b, 1.0) # viewport (solid) colour
|
||||||
|
m.use_nodes = True
|
||||||
|
bsdf = m.node_tree.nodes.get("Principled BSDF")
|
||||||
|
if bsdf:
|
||||||
|
bsdf.inputs["Base Color"].default_value = (r, g, b, 1.0)
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def build_vis(spec):
|
||||||
|
"""Subdivided 10x10 plane (100 quads): per-face material via paint(), per-vertex Z."""
|
||||||
|
n, step = 10, 1.0
|
||||||
|
paint = tiles.make_paint(spec)
|
||||||
|
verts, vidx = [], {}
|
||||||
|
|
||||||
|
def vi(i, j):
|
||||||
|
if (i, j) not in vidx:
|
||||||
|
x, y = -5 + i * step, -5 + j * step
|
||||||
|
vidx[(i, j)] = len(verts)
|
||||||
|
verts.append((x, y, tiles.height(spec, x, y)))
|
||||||
|
return vidx[(i, j)]
|
||||||
|
|
||||||
|
faces, face_mat, mats, midx = [], [], [], {}
|
||||||
|
for i in range(n):
|
||||||
|
for j in range(n):
|
||||||
|
faces.append((vi(i, j), vi(i + 1, j), vi(i + 1, j + 1), vi(i, j + 1)))
|
||||||
|
cx, cy = -5 + (i + 0.5) * step, -5 + (j + 0.5) * step
|
||||||
|
mn = paint(cx, cy)
|
||||||
|
if mn not in midx:
|
||||||
|
midx[mn] = len(mats)
|
||||||
|
mats.append(mn)
|
||||||
|
face_mat.append(midx[mn])
|
||||||
|
|
||||||
|
me = bpy.data.meshes.new("VIS_" + spec["code"])
|
||||||
|
me.from_pydata(verts, [], faces)
|
||||||
|
for mn in mats:
|
||||||
|
me.materials.append(get_mat(mn))
|
||||||
|
for poly, mi in zip(me.polygons, face_mat):
|
||||||
|
poly.material_index = mi
|
||||||
|
me.update()
|
||||||
|
ob = bpy.data.objects.new("VIS_" + spec["code"], me)
|
||||||
|
n_, e_, s_, w_ = spec["edges"]
|
||||||
|
ob["edge_N"], ob["edge_E"], ob["edge_S"], ob["edge_W"] = n_, e_, s_, w_
|
||||||
|
return ob
|
||||||
|
|
||||||
|
|
||||||
|
def build_quads(name, quads, matname):
|
||||||
|
if not quads:
|
||||||
|
return None
|
||||||
|
verts, faces = [], []
|
||||||
|
for q in quads:
|
||||||
|
b = len(verts)
|
||||||
|
verts += q
|
||||||
|
faces.append((b, b + 1, b + 2, b + 3))
|
||||||
|
me = bpy.data.meshes.new(name)
|
||||||
|
me.from_pydata(verts, [], faces)
|
||||||
|
me.materials.append(get_mat(matname))
|
||||||
|
me.update()
|
||||||
|
return bpy.data.objects.new(name, me)
|
||||||
|
|
||||||
|
|
||||||
|
def build_tile(spec):
|
||||||
|
col = bpy.data.collections.new(f"{spec['code']}_{spec['name']}")
|
||||||
|
bpy.context.scene.collection.children.link(col)
|
||||||
|
col.objects.link(build_vis(spec))
|
||||||
|
quads = tiles.wok_quads(spec)
|
||||||
|
wok = build_quads("WOK_" + spec["code"], [q for q, w in quads if w], "MAT_debug_walkmesh")
|
||||||
|
blk = build_quads("BLOCK_" + spec["code"], [q for q, w in quads if not w], "MAT_debug_blocker")
|
||||||
|
if wok:
|
||||||
|
col.objects.link(wok)
|
||||||
|
if blk:
|
||||||
|
col.objects.link(blk)
|
||||||
|
return col
|
||||||
|
|
||||||
|
|
||||||
|
def instance(col, loc, parent, name):
|
||||||
|
ob = bpy.data.objects.new(name, None)
|
||||||
|
ob.instance_type = "COLLECTION"
|
||||||
|
ob.instance_collection = col
|
||||||
|
ob.location = loc
|
||||||
|
parent.objects.link(ob)
|
||||||
|
|
||||||
|
|
||||||
|
def label(text, loc, parent):
|
||||||
|
cu = bpy.data.curves.new(text, "FONT")
|
||||||
|
cu.body = text
|
||||||
|
cu.size = 1.4
|
||||||
|
ob = bpy.data.objects.new("LBL_" + text, cu)
|
||||||
|
ob.location = loc
|
||||||
|
parent.objects.link(ob)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||||
|
out = os.path.abspath(argv[0]) if argv else os.path.abspath("westgate_urban_foundation_mvp.blend")
|
||||||
|
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
bpy.context.scene.unit_settings.system = "METRIC"
|
||||||
|
|
||||||
|
tilecol = {s["code"]: build_tile(s) for s in tiles.TILES}
|
||||||
|
|
||||||
|
# edge metadata text block (KICKOFF.md)
|
||||||
|
txt = bpy.data.texts.new("TILE_EDGE_METADATA")
|
||||||
|
for s in tiles.TILES:
|
||||||
|
n_, e_, s_, w_ = s["edges"]
|
||||||
|
txt.write(f"{s['code']}_{s['name']}\nN: {n_}\nE: {e_}\nS: {s_}\nW: {w_}\n\n")
|
||||||
|
|
||||||
|
# library grid preview (8 wide, 12 m pitch, labelled)
|
||||||
|
grid = bpy.data.collections.new("PREVIEW_tile_library_grid")
|
||||||
|
bpy.context.scene.collection.children.link(grid)
|
||||||
|
cols = 8
|
||||||
|
for i, s in enumerate(tiles.TILES):
|
||||||
|
gx, gy = (i % cols) * 12, -(i // cols) * 12
|
||||||
|
instance(tilecol[s["code"]], (gx, gy, 0), grid, "GRID_" + s["code"])
|
||||||
|
label(s["code"], (gx - 4.5, gy + 5.5, 0), grid)
|
||||||
|
|
||||||
|
# 12x12 test district (10 m pitch), row south-ward, col east-ward
|
||||||
|
dist = bpy.data.collections.new("PREVIEW_12x12_test_district")
|
||||||
|
bpy.context.scene.collection.children.link(dist)
|
||||||
|
skipped = []
|
||||||
|
for r, row in enumerate(tiles.MAP):
|
||||||
|
for c, code in enumerate(row):
|
||||||
|
tc = tilecol.get(code)
|
||||||
|
if tc is None:
|
||||||
|
skipped.append([r, c, code])
|
||||||
|
continue
|
||||||
|
instance(tc, (c * 10, -r * 10, 0), dist, f"D_{r:02d}_{c:02d}_{code}")
|
||||||
|
|
||||||
|
bpy.ops.wm.save_as_mainfile(filepath=out)
|
||||||
|
|
||||||
|
report = {
|
||||||
|
"blend": out,
|
||||||
|
"tile_collections": len(tilecol),
|
||||||
|
"district_cells": sum(len(r) for r in tiles.MAP),
|
||||||
|
"district_skipped": skipped,
|
||||||
|
"materials": len(tiles.COLORS),
|
||||||
|
}
|
||||||
|
print("<<<WGT_REPORT_BEGIN>>>" + json.dumps(report, indent=2) + "<<<WGT_REPORT_END>>>")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"""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()
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
"""Single source of truth for the wgt01 (Westgate Urban Foundation) tileset.
|
||||||
|
|
||||||
|
Pure Python, NO bpy — imported by build.py (Blender), make_wok.py and make_set.py
|
||||||
|
alike. Defines the 40-tile table, materials, the 12x12 test map, and the pure
|
||||||
|
functions that derive visuals (paint), heights, walkmesh quads and the NWN .set
|
||||||
|
mapping from a tile spec. Blender realises these; the .wok/.set tools consume the
|
||||||
|
same data so nothing drifts.
|
||||||
|
|
||||||
|
Coordinate convention (matches KICKOFF.md): tile centred at origin, X/Y in
|
||||||
|
[-5,+5], Z=0 ground, Z=1.5 raised tier, Z=-0.3 canal water. Z up.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PREFIX = "twu01" # Tileset Westgate Urban 01 (5-char resref prefix, AGENTS.md §1)
|
||||||
|
HALF = 5.0
|
||||||
|
Z_RAISED = 1.5
|
||||||
|
Z_WATER = -0.3
|
||||||
|
|
||||||
|
# --- prototype materials (flat viewport colours; KICKOFF.md wants geometry, not art)
|
||||||
|
COLORS = {
|
||||||
|
"MAT_wet_cobble_dark": (0.05, 0.05, 0.06),
|
||||||
|
"MAT_cobble_patch": (0.12, 0.10, 0.09),
|
||||||
|
"MAT_plaza_slab": (0.30, 0.29, 0.27),
|
||||||
|
"MAT_sidewalk_stone": (0.22, 0.22, 0.23),
|
||||||
|
"MAT_curb_dark_stone": (0.10, 0.10, 0.11),
|
||||||
|
"MAT_canal_black_water": (0.02, 0.03, 0.05),
|
||||||
|
"MAT_void_black": (0.00, 0.00, 0.00),
|
||||||
|
"MAT_ramp_stone": (0.18, 0.16, 0.14),
|
||||||
|
"MAT_drain_iron": (0.08, 0.08, 0.09),
|
||||||
|
"MAT_foundation_pad": (0.25, 0.24, 0.22),
|
||||||
|
"MAT_debug_walkmesh": (0.00, 0.80, 0.20),
|
||||||
|
"MAT_debug_blocker": (0.80, 0.10, 0.10),
|
||||||
|
}
|
||||||
|
CURB = "MAT_curb_dark_stone"
|
||||||
|
|
||||||
|
F, R, C, RP, MX = "FLAT", "RAISED", "CANAL", "RAMP", "MIXED"
|
||||||
|
|
||||||
|
|
||||||
|
def T(code, name, kind, edges, style, **kw):
|
||||||
|
d = dict(code=code, name=name, kind=kind, edges=edges, style=style)
|
||||||
|
d.update(kw)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
# 40 tiles, KICKOFF.md order. edges = (N, E, S, W).
|
||||||
|
TILES = [
|
||||||
|
# --- flat walkable ground ---
|
||||||
|
T("CP1", "cobble_plain", "flat", (F, F, F, F), "solid", mat="MAT_wet_cobble_dark"),
|
||||||
|
T("CP2", "cobble_plain_wet_variant", "flat", (F, F, F, F), "patches",
|
||||||
|
mat="MAT_wet_cobble_dark", patch="MAT_cobble_patch"),
|
||||||
|
T("CP3", "cobble_plain_broken_variant", "flat", (F, F, F, F), "patches",
|
||||||
|
mat="MAT_wet_cobble_dark", patch="MAT_cobble_patch"),
|
||||||
|
T("AL1", "narrow_alley_paving", "flat", (F, F, F, F), "alley"),
|
||||||
|
T("PL1", "plaza_slab", "flat", (F, F, F, F), "solid", mat="MAT_plaza_slab"),
|
||||||
|
T("DR1", "drain_detail", "flat", (F, F, F, F), "drain"),
|
||||||
|
T("MU1", "muddy_repair_patch", "flat", (F, F, F, F), "mud"),
|
||||||
|
T("LOT", "foundation_pad", "flat", (F, F, F, F), "solid", mat="MAT_foundation_pad"),
|
||||||
|
# --- streets (fully walkable cobble with painted street grammar) ---
|
||||||
|
T("ST_S", "straight_street", "flat", (F, F, F, F), "street", shape="straight"),
|
||||||
|
T("ST_C", "street_corner", "flat", (F, F, F, F), "street", shape="corner"),
|
||||||
|
T("ST_T", "street_t_junction", "flat", (F, F, F, F), "street", shape="t"),
|
||||||
|
T("ST_X", "street_crossroads", "flat", (F, F, F, F), "street", shape="x"),
|
||||||
|
T("ST_E", "street_end", "flat", (F, F, F, F), "street", shape="end"),
|
||||||
|
T("ST_W", "wide_boulevard", "flat", (F, F, F, F), "street", shape="wide"),
|
||||||
|
T("ST_G", "gate_mouth", "flat", (F, F, F, F), "street", shape="gate"),
|
||||||
|
T("ST_D", "street_with_central_drain", "flat", (F, F, F, F), "street", shape="drain"),
|
||||||
|
# --- sidewalk / curb / gutter ---
|
||||||
|
T("SW_S", "sidewalk_straight", "flat", (F, F, F, F), "sidewalk"),
|
||||||
|
T("SW_C", "sidewalk_corner", "flat", (F, F, F, F), "sidewalk"),
|
||||||
|
T("SW_T", "sidewalk_t_split", "flat", (F, F, F, F), "sidewalk"),
|
||||||
|
T("SW_X", "sidewalk_crossing", "flat", (F, F, F, F), "sidewalk"),
|
||||||
|
T("SW_E", "sidewalk_end", "flat", (F, F, F, F), "sidewalk"),
|
||||||
|
T("GD_S", "gutter_straight", "flat", (F, F, F, F), "gutter", shape="straight"),
|
||||||
|
T("GD_C", "gutter_corner", "flat", (F, F, F, F), "gutter", shape="corner"),
|
||||||
|
T("GD_X", "gutter_crossing_grate", "flat", (F, F, F, F), "gutter", shape="x", grate=True),
|
||||||
|
# --- raised tiers / ramps / stairs ---
|
||||||
|
T("UP1", "raised_terrace", "raised", (R, R, R, R), "raised"),
|
||||||
|
T("UP2", "raised_plaza_variant", "raised", (R, R, R, R), "raised"),
|
||||||
|
T("TE_S", "retaining_edge_straight", "retain", (R, MX, F, MX), "retain"),
|
||||||
|
T("TE_C", "retaining_edge_outer_corner", "retain", (R, R, MX, MX), "retain"),
|
||||||
|
T("TE_I", "retaining_edge_inner_corner", "retain", (R, MX, MX, R), "retain"),
|
||||||
|
T("RA_S", "straight_ramp", "ramp", (R, RP, F, RP), "ramp"),
|
||||||
|
T("RA_C", "corner_or_dogleg_ramp", "ramp", (R, RP, F, RP), "ramp"), # simplified to straight ramp
|
||||||
|
T("STA", "broad_stair", "stair", (R, MX, F, MX), "stair"),
|
||||||
|
# --- canal / void / bridges ---
|
||||||
|
T("CN1", "canal_black_water", "canal", (C, C, C, C), "canal"),
|
||||||
|
T("CN2", "narrow_drainage_cut", "cut", (C, F, C, F), "cut"),
|
||||||
|
T("CE_S", "canal_edge_straight", "canal_edge", (F, C, F, F), "canal_edge", which="E"),
|
||||||
|
T("CE_C", "canal_edge_corner", "canal_edge", (F, C, C, F), "canal_edge", which="ES_outer"),
|
||||||
|
T("CE_I", "canal_edge_inside_corner", "canal_edge", (F, C, C, F), "canal_edge", which="ES_inner"),
|
||||||
|
T("BR_1", "narrow_bridge", "bridge", (F, C, F, C), "bridge", hw=1.5),
|
||||||
|
T("BR_2", "broad_bridge", "bridge", (F, C, F, C), "bridge", hw=3.0),
|
||||||
|
T("SEW", "sewer_mouth_culvert", "canal_edge", (F, F, C, F), "sewer", which="S"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 12x12 test district (KICKOFF.md). Placed by row (south-ward) / col (east-ward).
|
||||||
|
MAP = [
|
||||||
|
["CN1", "CN1", "CE_S", "CE_S", "BR_2", "CE_S", "CE_S", "CN1", "CN1", "CN1", "CN1", "CN1"],
|
||||||
|
["CN1", "CE_C", "SW_S", "ST_S", "ST_S", "ST_S", "SW_S", "CE_C", "CN1", "CN1", "CN1", "CN1"],
|
||||||
|
["CE_S", "SW_S", "ST_C", "ST_S", "ST_T", "ST_S", "ST_C", "SW_S", "CE_S", "CE_S", "BR_1", "CE_S"],
|
||||||
|
["SW_S", "AL1", "AL1", "CP1", "PL1", "PL1", "CP1", "AL1", "AL1", "SW_S", "ST_S", "SW_S"],
|
||||||
|
["SW_S", "AL1", "LOT", "CP2", "PL1", "PL1", "CP2", "LOT", "AL1", "SW_S", "ST_S", "SW_S"],
|
||||||
|
["ST_S", "ST_S", "ST_T", "ST_S", "ST_X", "ST_S", "ST_T", "ST_S", "ST_S", "ST_C", "ST_S", "SW_S"],
|
||||||
|
["SW_S", "AL1", "LOT", "CP1", "ST_S", "ST_S", "CP1", "LOT", "AL1", "SW_S", "CP2", "SW_S"],
|
||||||
|
["SW_S", "AL1", "AL1", "CP1", "PL1", "PL1", "CP1", "AL1", "AL1", "SW_S", "CP2", "SW_S"],
|
||||||
|
["CE_S", "SW_S", "ST_C", "ST_S", "ST_T", "ST_S", "ST_C", "SW_S", "CE_S", "TE_S", "TE_C", "SW_S"],
|
||||||
|
["CN1", "CE_C", "SW_S", "CP1", "RA_S", "UP1", "UP1", "TE_S", "TE_S", "UP1", "RA_S", "SW_S"],
|
||||||
|
["CN1", "CN1", "CE_S", "CE_S", "BR_2", "CE_S", "CE_S", "CN1", "CE_S", "SW_S", "ST_S", "SW_S"],
|
||||||
|
["CN1", "CN1", "CN1", "CN1", "CN1", "CN1", "CN1", "CN1", "CN1", "SW_S", "ST_E", "SW_S"],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# --- geometry predicates -----------------------------------------------------
|
||||||
|
|
||||||
|
def _disk(x, y, cx, cy, r):
|
||||||
|
return (x - cx) ** 2 + (y - cy) ** 2 <= r * r
|
||||||
|
|
||||||
|
|
||||||
|
def _road(shape, x, y, hw):
|
||||||
|
ns = abs(x) <= hw
|
||||||
|
ew = abs(y) <= hw
|
||||||
|
if shape == "straight":
|
||||||
|
return ns
|
||||||
|
if shape in ("wide", "gate"):
|
||||||
|
return abs(x) <= hw * 1.7
|
||||||
|
if shape == "corner":
|
||||||
|
return (ns and y >= -hw) or (ew and x <= hw) # N arm + E arm
|
||||||
|
if shape == "t":
|
||||||
|
return ew or (ns and y <= hw) # E-W bar + stub to S
|
||||||
|
if shape == "x":
|
||||||
|
return ns or ew
|
||||||
|
if shape == "end":
|
||||||
|
return ns and y <= 0.0 # enters from S, stops mid-tile
|
||||||
|
if shape == "drain":
|
||||||
|
return ns
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _land_fn(which):
|
||||||
|
"""Where a canal_edge tile is walkable land (True) vs canal (False)."""
|
||||||
|
return {
|
||||||
|
"E": lambda x, y: x <= 0.0, # canal on east
|
||||||
|
"S": lambda x, y: y >= 0.0, # canal on south
|
||||||
|
"ES_outer": lambda x, y: (x <= 0.0 and y >= 0.0), # land = NW quadrant only
|
||||||
|
"ES_inner": lambda x, y: not (x > 0.0 and y < 0.0), # land = all but SE quadrant
|
||||||
|
}[which]
|
||||||
|
|
||||||
|
|
||||||
|
# --- visuals: paint(x, y) -> material name -----------------------------------
|
||||||
|
|
||||||
|
def make_paint(spec):
|
||||||
|
st = spec["style"]
|
||||||
|
if st == "solid":
|
||||||
|
m = spec["mat"]
|
||||||
|
return lambda x, y: m
|
||||||
|
if st == "patches":
|
||||||
|
base, patch = spec["mat"], spec["patch"]
|
||||||
|
pts = [(-2, 2), (1.5, -1.5), (3, 2.5), (-2.5, -2)]
|
||||||
|
return lambda x, y: patch if any(_disk(x, y, cx, cy, 1.3) for cx, cy in pts) else base
|
||||||
|
if st == "alley":
|
||||||
|
return lambda x, y: "MAT_wet_cobble_dark" if abs(x) <= 2.2 else "MAT_foundation_pad"
|
||||||
|
if st == "drain":
|
||||||
|
return lambda x, y: "MAT_drain_iron" if (abs(x) <= 1 and abs(y) <= 1) else "MAT_wet_cobble_dark"
|
||||||
|
if st == "mud":
|
||||||
|
return lambda x, y: "MAT_cobble_patch" if _disk(x, y, 0.5, -0.5, 2.4) else "MAT_wet_cobble_dark"
|
||||||
|
if st == "street":
|
||||||
|
shape, hw = spec["shape"], spec.get("hw", 1.6)
|
||||||
|
|
||||||
|
def f(x, y):
|
||||||
|
if _road(shape, x, y, hw):
|
||||||
|
return "MAT_drain_iron" if (shape == "drain" and abs(x) <= 0.5) else "MAT_wet_cobble_dark"
|
||||||
|
if _road(shape, x, y, hw + 1.0):
|
||||||
|
return CURB
|
||||||
|
return "MAT_sidewalk_stone"
|
||||||
|
return f
|
||||||
|
if st == "sidewalk":
|
||||||
|
return lambda x, y: CURB if (abs(x) >= 4.2 or abs(y) >= 4.2) else "MAT_sidewalk_stone"
|
||||||
|
if st == "gutter":
|
||||||
|
shape, grate = spec["shape"], spec.get("grate", False)
|
||||||
|
|
||||||
|
def f(x, y):
|
||||||
|
if grate and abs(x) <= 0.9 and abs(y) <= 0.9:
|
||||||
|
return "MAT_drain_iron"
|
||||||
|
if _road(shape, x, y, 0.6):
|
||||||
|
return "MAT_drain_iron"
|
||||||
|
if _road(shape, x, y, 1.1):
|
||||||
|
return CURB
|
||||||
|
return "MAT_wet_cobble_dark"
|
||||||
|
return f
|
||||||
|
if st == "raised":
|
||||||
|
return lambda x, y: "MAT_plaza_slab"
|
||||||
|
if st == "retain":
|
||||||
|
return lambda x, y: "MAT_plaza_slab" if y >= 0 else "MAT_wet_cobble_dark"
|
||||||
|
if st in ("ramp", "stair"):
|
||||||
|
return lambda x, y: "MAT_ramp_stone"
|
||||||
|
if st == "canal":
|
||||||
|
return lambda x, y: "MAT_wet_cobble_dark" if (abs(x) >= 4.4 or abs(y) >= 4.4) else "MAT_canal_black_water"
|
||||||
|
if st == "cut":
|
||||||
|
return lambda x, y: "MAT_canal_black_water" if abs(x) <= 0.9 else "MAT_wet_cobble_dark"
|
||||||
|
if st == "canal_edge":
|
||||||
|
land = _land_fn(spec["which"])
|
||||||
|
return lambda x, y: "MAT_wet_cobble_dark" if land(x, y) else "MAT_canal_black_water"
|
||||||
|
if st == "bridge":
|
||||||
|
hw = spec.get("hw", 1.5)
|
||||||
|
return lambda x, y: ("MAT_ramp_stone" if abs(x) <= hw
|
||||||
|
else (CURB if abs(x) <= hw + 0.3 else "MAT_canal_black_water"))
|
||||||
|
if st == "sewer":
|
||||||
|
return lambda x, y: (("MAT_drain_iron" if _disk(x, y, 0, -3.2, 1.6) else "MAT_wet_cobble_dark")
|
||||||
|
if y >= 0 else "MAT_canal_black_water")
|
||||||
|
return lambda x, y: "MAT_wet_cobble_dark"
|
||||||
|
|
||||||
|
|
||||||
|
def height(spec, x, y):
|
||||||
|
"""Z of the visual top surface at (x, y). Drives the subdivided VIS plane."""
|
||||||
|
k = spec["kind"]
|
||||||
|
if k == "raised":
|
||||||
|
return Z_RAISED
|
||||||
|
if k in ("ramp", "stair"):
|
||||||
|
return (y + HALF) / (2 * HALF) * Z_RAISED # S=0 -> N=1.5
|
||||||
|
if k == "retain":
|
||||||
|
return Z_RAISED if y >= 0 else 0.0
|
||||||
|
if k == "canal":
|
||||||
|
return 0.0 if (abs(x) >= 4.4 or abs(y) >= 4.4) else Z_WATER
|
||||||
|
if k == "cut":
|
||||||
|
return Z_WATER if abs(x) <= 0.9 else 0.0
|
||||||
|
if k == "canal_edge":
|
||||||
|
return 0.0 if _land_fn(spec["which"])(x, y) else Z_WATER
|
||||||
|
if k == "bridge":
|
||||||
|
hw = spec.get("hw", 1.5)
|
||||||
|
return 0.2 if abs(x) <= hw + 0.3 else Z_WATER
|
||||||
|
if k == "sewer":
|
||||||
|
return 0.0 if y >= 0 else Z_WATER
|
||||||
|
if spec.get("style") == "sidewalk":
|
||||||
|
return 0.15 if (abs(x) >= 4.2 or abs(y) >= 4.2) else 0.0 # low curb (KICKOFF 0.12-0.18)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
# --- walkmesh proxy: list of (verts4, walkable) ------------------------------
|
||||||
|
|
||||||
|
def _rect(x0, x1, y0, y1, z):
|
||||||
|
return [(x0, y0, z), (x1, y0, z), (x1, y1, z), (x0, y1, z)]
|
||||||
|
|
||||||
|
|
||||||
|
def _ramp(x0, x1, y0, y1):
|
||||||
|
z0 = (y0 + HALF) / (2 * HALF) * Z_RAISED
|
||||||
|
z1 = (y1 + HALF) / (2 * HALF) * Z_RAISED
|
||||||
|
return [(x0, y0, z0), (x1, y0, z0), (x1, y1, z1), (x0, y1, z1)]
|
||||||
|
|
||||||
|
|
||||||
|
def wok_quads(spec):
|
||||||
|
k = spec["kind"]
|
||||||
|
if k == "flat":
|
||||||
|
return [(_rect(-5, 5, -5, 5, 0.0), True)]
|
||||||
|
if k == "raised":
|
||||||
|
return [(_rect(-5, 5, -5, 5, Z_RAISED), True)]
|
||||||
|
if k in ("ramp", "stair"):
|
||||||
|
return [(_ramp(-5, 5, -5, 5), True)]
|
||||||
|
if k == "retain":
|
||||||
|
return [(_rect(-5, 5, 0, 5, Z_RAISED), True), (_rect(-5, 5, -5, 0, 0.0), True)]
|
||||||
|
if k == "canal":
|
||||||
|
return [(_rect(-5, 5, -5, 5, 0.0), False)]
|
||||||
|
if k == "cut":
|
||||||
|
return [(_rect(-5, -0.9, -5, 5, 0.0), True),
|
||||||
|
(_rect(0.9, 5, -5, 5, 0.0), True),
|
||||||
|
(_rect(-0.9, 0.9, -5, 5, 0.0), False)]
|
||||||
|
if k == "canal_edge":
|
||||||
|
w = spec["which"]
|
||||||
|
if w == "E":
|
||||||
|
return [(_rect(-5, 0, -5, 5, 0.0), True), (_rect(0, 5, -5, 5, 0.0), False)]
|
||||||
|
if w == "S":
|
||||||
|
return [(_rect(-5, 5, 0, 5, 0.0), True), (_rect(-5, 5, -5, 0, 0.0), False)]
|
||||||
|
if w == "ES_outer":
|
||||||
|
return [(_rect(-5, 0, 0, 5, 0.0), True),
|
||||||
|
(_rect(0, 5, -5, 5, 0.0), False), (_rect(-5, 0, -5, 0, 0.0), False)]
|
||||||
|
if w == "ES_inner":
|
||||||
|
return [(_rect(-5, 0, -5, 5, 0.0), True), (_rect(0, 5, 0, 5, 0.0), True),
|
||||||
|
(_rect(0, 5, -5, 0, 0.0), False)]
|
||||||
|
if k == "bridge":
|
||||||
|
hw = spec.get("hw", 1.5)
|
||||||
|
return [(_rect(-hw, hw, -5, 5, 0.2), True),
|
||||||
|
(_rect(-5, -hw, -5, 5, 0.0), False), (_rect(hw, 5, -5, 5, 0.0), False)]
|
||||||
|
return [(_rect(-5, 5, -5, 5, 0.0), True)]
|
||||||
|
|
||||||
|
|
||||||
|
# --- NWN .set mapping (AGENTS.md §4, §7) -------------------------------------
|
||||||
|
# Minimal, deliberate vocabulary (AGENTS.md §2.1: every terrain pairing multiplies
|
||||||
|
# tile count). Ground tiles all share 'cobble' so they interconnect freely
|
||||||
|
# (KICKOFF.md: "entire tile can remain walkable for flexibility").
|
||||||
|
TERRAINS = ["cobble", "raised", "canal"]
|
||||||
|
CROSSERS = ["ramp", "canal_edge"]
|
||||||
|
|
||||||
|
|
||||||
|
def resref(spec):
|
||||||
|
return f"{PREFIX}_{spec['code'].lower()}" # <=16 chars, valid identifier
|
||||||
|
|
||||||
|
|
||||||
|
def _terr(e):
|
||||||
|
return {"RAISED": "raised", "CANAL": "canal"}.get(e, "cobble")
|
||||||
|
|
||||||
|
|
||||||
|
def _corner(ea, eb):
|
||||||
|
ts = {_terr(ea), _terr(eb)}
|
||||||
|
terr = "raised" if "raised" in ts else ("canal" if "canal" in ts else "cobble")
|
||||||
|
h = 1 if (ea == "RAISED" or eb == "RAISED") else 0
|
||||||
|
return terr, h
|
||||||
|
|
||||||
|
|
||||||
|
def nwn_corners(edges):
|
||||||
|
"""Corner (terrain, height) for TopLeft/TopRight/BottomLeft/BottomRight.
|
||||||
|
Top=N Bottom=S Left=W Right=E (AGENTS.md §4)."""
|
||||||
|
n, e, s, w = edges
|
||||||
|
return {
|
||||||
|
"TopLeft": _corner(n, w), "TopRight": _corner(n, e),
|
||||||
|
"BottomLeft": _corner(s, w), "BottomRight": _corner(s, e),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def nwn_crosser(edge):
|
||||||
|
return {"CANAL": "canal_edge", "RAMP": "ramp"}.get(edge, "")
|
||||||
Reference in New Issue
Block a user