wgt01
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
build/
|
||||
*.blend
|
||||
*.blend1
|
||||
@@ -0,0 +1,15 @@
|
||||
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.
|
||||
@@ -1,400 +0,0 @@
|
||||
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.
|
||||
-147
@@ -1,147 +0,0 @@
|
||||
# wgt01 — Build Status & Notes
|
||||
|
||||
Progress log for the **Westgate Urban Foundation MVP** tileset (prefix `twu01`).
|
||||
Spec: [KICKOFF.md](KICKOFF.md). How NWN needs things shaped: [../AGENTS.md](../AGENTS.md).
|
||||
|
||||
_Last updated: 2026-07-04._
|
||||
|
||||
---
|
||||
|
||||
## What exists — the generator kit (`generator/`)
|
||||
|
||||
One source of truth (`tiles.py`) feeds four tools; the two NWN emitters need no
|
||||
Blender, so they're directly testable.
|
||||
|
||||
| File | Role | Needs Blender? |
|
||||
|---|---|---|
|
||||
| `tiles.py` | **Single source of truth**: 40-tile table, 12 materials, the 12×12 test map, and all pure derivations (paint / height / walkmesh quads / `.set` corner+crosser mapping). `PREFIX=twu01`. | no |
|
||||
| `build.py` | Builds `westgate_urban_foundation_mvp.blend` — 40 tile collections (`VIS_`/`WOK_`/`BLOCK_` + edge custom props), `TILE_EDGE_METADATA` text block, `PREVIEW_tile_library_grid`, `PREVIEW_12x12_test_district`. Editable preview only (quads OK — the exporters triangulate). | yes (4.0.2) |
|
||||
| `make_mdl.py` | **Shipping model path.** Pure-Python `.mdl` + `.wok` + solid-colour `.tga`: faces triangulated, one `trimesh` node **per texture** (each with `bitmap`+`tverts`), embedded `aabb` walkmesh. | no |
|
||||
| `make_itp.py` | `twu01palstd.itp` (GFF `ITP ` via `nwn_gff`) — the palette the toolset needs to register the set. | no (needs `nwn_gff`) |
|
||||
| `make_wok.py` | Pure-Python ASCII `.wok` emitter (verts+faces+surfacemat+AABB tree); shared `aabb_node()` reused by `make_mdl`. | no |
|
||||
| `make_set.py` | `twu01.set` (INI, **CRLF**) from the tile grammar; field order/values mirror stock. | no |
|
||||
| `export_mdl.py` | _Alt path, not shipped._ Neverblender export — **flattens** multi-material tiles to one unloadable material, so superseded by `make_mdl.py`. | yes + addon |
|
||||
|
||||
`flake.nix` exposes these as apps + a dev shell (pinned **Blender 4.0.2**,
|
||||
`neverwinter-nim`, `cleanmodels`).
|
||||
|
||||
## Pipeline (commands)
|
||||
|
||||
```sh
|
||||
cd wgt01
|
||||
nix run .#mdl -- mdl/ # .mdl + .wok + .tga (shipping models)
|
||||
nix run .#set -- twu01.set # twu01.set (CRLF)
|
||||
nix run .#itp -- twu01palstd.itp # palette (needs nwn_gff)
|
||||
nix run .#build -- out.blend # optional: editable .blend preview
|
||||
# nix run .#export -- out.blend mdl/ # alt Neverblender path (flattens materials)
|
||||
```
|
||||
|
||||
## Verified (2026-07-04)
|
||||
|
||||
- **models (`make_mdl`)**: 40 `.mdl` + 40 `.wok` + 12 `.tga`. Each `.mdl` has
|
||||
`newmodel`, `classification TILE`, AuroraBase name == resref, one `trimesh`
|
||||
**per texture** with `bitmap`+`tverts` (tris), and the embedded `aabb`
|
||||
walkmesh. E.g. `st_s` → 3 trimesh nodes (swlk/curb/wcob); `cp1` → 1 with
|
||||
`bitmap twu01_wcob`. Walkmesh surfacemats: 4 (`wok_Stone`, walkable) / 7
|
||||
(`wok_Nonwalk`). Matches the stock `trs02` tile structure field-for-field.
|
||||
- **`.itp`**: `twu01palstd.itp` round-trips through `nwn_gff` — `ITP ` type,
|
||||
40 named entries. Names render, but the **structure is wrong** (flat folder of
|
||||
raw tile resrefs → unplaceable, no Terrain/eraser folder). See Diagnosis below;
|
||||
needs a rebuild to the 3-folder Terrain/Features/Groups shape.
|
||||
- **`.set`**: CRLF, 40 tiles, 3 terrains (`cobble`/`raised`/`canal`), 2 crossers
|
||||
(`ramp`/`canal_edge`). `Version=V1.0`, stock field order (`WalkMesh=msb01`,
|
||||
lights=1, `VisibilityNode=A`, `ImageMap2D` last). Corners/heights from each
|
||||
tile's N/E/S/W edge grammar; non-walkable tiles → `PathNode=N`.
|
||||
- **install path**: `~/Documents/Neverwinter Nights/development` is a symlink to
|
||||
`~/.local/share/Neverwinter Nights/development` (the toolset's real dir); one
|
||||
`nix run .#dev-install` stages all 94 loose files there.
|
||||
|
||||
### Why the models were rebuilt (2026-07-04, pass 2)
|
||||
The Neverblender export produced tiles the toolset couldn't load — **no `tverts`,
|
||||
no `bitmap`, a dangling `materialname` (>16 chars, no `.mtr`)** — and it
|
||||
**flattened** each multi-material tile to a single arbitrary material. NWN needs
|
||||
UV'd, bitmapped, one-texture-per-node trimeshes. Fixed by generating the `.mdl`
|
||||
in pure Python (`make_mdl.py`) with full control, plus the required
|
||||
`twu01palstd.itp`. Symptom that surfaced it: blank tileset name + "could not
|
||||
create area" in the New Area wizard.
|
||||
|
||||
## Testing in the toolset — no `.hak` needed
|
||||
|
||||
The toolset reads loose files from **`~/Documents/Neverwinter Nights/development/`**
|
||||
(confirmed present). One command does build → export → `.set` → stage:
|
||||
|
||||
```sh
|
||||
cd wgt01
|
||||
nix run .#dev-install # -> build/ (inspectable) + installs 40 .mdl/.wok + twu01.set into development/
|
||||
```
|
||||
|
||||
It replaces only the `twu01*` files in `development/` (leaves everything else
|
||||
alone) and keeps the intermediate outputs in `./build/` for inspection. Then in
|
||||
the toolset: create a small area using the `twu01` tileset, place tiles, walk-test
|
||||
pathing/heights. (Packing a `.hak` with `nwn_erf` is deferred until the set is
|
||||
validated.)
|
||||
|
||||
## First-pass caveats — tune before shipping
|
||||
|
||||
- **PathNode** is a placeholder (`A` walkable / `N` non-walkable) — verify creature
|
||||
routing per tile shape in the toolset; real pathnode letters matter.
|
||||
- **Textures are flat solid-colour `.tga`s** (one per material, KICKOFF "geometry
|
||||
not art"). They double as an in-toolset grammar read-out; swap in real art
|
||||
later without touching geometry.
|
||||
- **No minimaps** — `ImageMap2D` refs exist in the `.set` but the `.tga`s don't
|
||||
(skipped by decision; Westgate doesn't use them).
|
||||
- **Substitutions**: `RA_C` = straight-ramp geometry; `STA` = smooth ramp WOK
|
||||
proxy over stepped visual; `SW_C/T/X/E` share the straight-sidewalk visual.
|
||||
- `MAT_void_black` is defined but currently unpainted (11/12 materials realised).
|
||||
- Corner terrain is a heuristic from edges — hand-tune adjacency in the toolset.
|
||||
|
||||
## Git
|
||||
|
||||
Branch **`wgt01-tileset-kit`** (`main` still unborn — `git branch -f main
|
||||
wgt01-tileset-kit` to promote). Commits:
|
||||
- `60af184` — build guide + 40-tile generator kit
|
||||
- `1284f15` — real `.mdl`/`.wok` export via Neverblender
|
||||
|
||||
## Diagnosis 2026-07-04 — palette names show but nothing paints
|
||||
|
||||
Tiles now carry names, but they're **not selectable/paintable**, and the standard
|
||||
**Terrain** section (paint terrains + eraser, Raise/Lower) is missing. Root cause,
|
||||
confirmed against the working `wut01palstd.itp` + `wut01.set` (see ../AGENTS.md
|
||||
§2.3 for the full model):
|
||||
|
||||
- The toolset has **two placement systems** and our `.itp` uses neither correctly.
|
||||
Ground is laid by **Paint Terrain** (the toolset corner-matches and auto-picks
|
||||
the model); the palette only **click-places `[GROUP#]`-backed prefabs**. It never
|
||||
places raw tile models.
|
||||
- Our `.itp` is **one flat folder of 40 raw tile resrefs**, no Terrain folder, no
|
||||
`eraser`, and the `.set` has **`[GROUPS] Count=0`**. So: none of the 40 entries is
|
||||
a feature/group → unplaceable; and with no Terrain folder there's no paint/eraser
|
||||
UI. `wut01` (123 tiles) lists only its **21 groups** in the palette and paints the
|
||||
rest as terrain; its `PRIMARY/SECONDARY RULES` are `Count=0`, so rules aren't the
|
||||
blocker.
|
||||
|
||||
**The real palette (verified):** `MAIN` = 3 category structs `{ID, STRREF folder
|
||||
label, LIST}`; leaves `{NAME, RESREF}`. Folder 8282 = **Terrain** (leaf RESREF =
|
||||
terrain/crosser *name*, plus a magic `eraser` leaf STRREF 63291); 63261 =
|
||||
**Features** (1×1, RESREF = first-tile resref); 63262 = **Groups** (N×M).
|
||||
|
||||
## Next passes
|
||||
|
||||
1. **Rebuild `make_itp.py` + `make_set.py` to the real structure** (../AGENTS.md
|
||||
§2.3):
|
||||
- `.itp`: a **Terrain** folder (each `tiles.TERRAINS`/`CROSSERS` by name + an
|
||||
`eraser` leaf) so terrain-painting + eraser exist; a **Features** folder with a
|
||||
1×1 entry per tile we want click-placeable.
|
||||
- `.set`: add `[GROUP#]` (1×1 feature) entries backing those Features leaves
|
||||
(`Name` == palette NAME, `Tile0` == that tile's index).
|
||||
- Then in the toolset lay ground via **Paint Terrain** (cobble/raised/canal), not
|
||||
by clicking tiles.
|
||||
2. Walk-test → tune PathNodes, crossers, corner terrains; add a tileset TLK if
|
||||
we later want localized palette names (local `NAME` strings suffice for now).
|
||||
3. **Textures (art pass, separate note).** Source assets live in
|
||||
`/home/vicky/Blender/` (e.g. `textures/`, `PBR_Procampur/`, `td_floor/`,
|
||||
`new_floors/`, `pd textures main lib/`). Map each `MAT_*` in `tiles.TEXTURES`
|
||||
to a chosen texture, replacing the flat solid-colour `.tga`s. **Downscale to
|
||||
≤2k** — NWN handles 4k textures poorly (perf/artifacts). Geometry is
|
||||
unaffected; only the `.tga`/material path changes.
|
||||
4. Pack `.hak` (`nwn_erf`) once validated.
|
||||
@@ -0,0 +1 @@
|
||||
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.
|
||||
BIN
Binary file not shown.
-150
@@ -1,150 +0,0 @@
|
||||
{
|
||||
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 "$@"'';
|
||||
};
|
||||
|
||||
# Pure-Python .mdl + .wok + solid-colour .tga (the shipping model path;
|
||||
# no Blender, and unlike Neverblender it keeps per-texture trimesh nodes).
|
||||
mkmdlpy = pkgs.writeShellApplication {
|
||||
name = "wgt-mdl";
|
||||
runtimeInputs = [ py ];
|
||||
text = ''exec python ${gen}/make_mdl.py "$@"'';
|
||||
};
|
||||
|
||||
# Tileset palette (GFF ITP) so the toolset registers the set.
|
||||
mkitp = pkgs.writeShellApplication {
|
||||
name = "wgt-itp";
|
||||
runtimeInputs = [ py pkgs.neverwinter-nim ];
|
||||
text = ''exec python ${gen}/make_itp.py "$@"'';
|
||||
};
|
||||
|
||||
# Real .mdl + .wok export via the Neverblender addon (must be enabled in
|
||||
# this Blender's user config — confirmed working headless).
|
||||
mkmdl = pkgs.writeShellApplication {
|
||||
name = "wgt-export";
|
||||
runtimeInputs = [ blender ];
|
||||
text = ''
|
||||
blend="''${1:?usage: wgt-export <blend> <outdir> [CODE...]}"
|
||||
outdir="''${2:-mdl}"
|
||||
shift 2 || true
|
||||
exec blender-4.0.2 --background "$blend" --python ${gen}/export_mdl.py \
|
||||
--python-exit-code 1 -- "$outdir" "$@"
|
||||
'';
|
||||
};
|
||||
|
||||
# One shot: models (.mdl/.wok/.tga) + .set + .itp, staged into the toolset's
|
||||
# development/ folder (no .hak, no Blender). Work dir defaults to ./build.
|
||||
mkdev = pkgs.writeShellApplication {
|
||||
name = "wgt-dev-install";
|
||||
runtimeInputs = [ py pkgs.neverwinter-nim ];
|
||||
text = ''
|
||||
work="''${1:-$PWD/build}"
|
||||
dev="$HOME/Documents/Neverwinter Nights/development"
|
||||
mkdir -p "$work/mdl" "$dev"
|
||||
python ${gen}/make_mdl.py "$work/mdl" # .mdl + .wok + .tga
|
||||
python ${gen}/make_set.py "$work/twu01.set"
|
||||
python ${gen}/make_itp.py "$work/twu01palstd.itp"
|
||||
# replace only our own files in development/, leave everything else alone
|
||||
rm -f "$dev"/twu01_*.mdl "$dev"/twu01_*.wok "$dev"/twu01_*.tga \
|
||||
"$dev"/twu01.set "$dev"/twu01palstd.itp
|
||||
cp "$work"/mdl/twu01_*.mdl "$work"/mdl/twu01_*.wok "$work"/mdl/twu01_*.tga \
|
||||
"$work"/twu01.set "$work"/twu01palstd.itp "$dev"/
|
||||
n=$(find "$work/mdl" -name 'twu01_*.mdl' | wc -l)
|
||||
echo "installed $n tiles + twu01.set + twu01palstd.itp -> $dev"
|
||||
'';
|
||||
};
|
||||
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";
|
||||
};
|
||||
mdl = {
|
||||
type = "app";
|
||||
program = "${mkmdlpy}/bin/wgt-mdl";
|
||||
};
|
||||
itp = {
|
||||
type = "app";
|
||||
program = "${mkitp}/bin/wgt-itp";
|
||||
};
|
||||
export = {
|
||||
type = "app";
|
||||
program = "${mkmdl}/bin/wgt-export";
|
||||
};
|
||||
dev-install = {
|
||||
type = "app";
|
||||
program = "${mkdev}/bin/wgt-dev-install";
|
||||
};
|
||||
};
|
||||
|
||||
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
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -1,166 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,103 +0,0 @@
|
||||
"""Export every tile to real NWN ASCII .mdl + .wok via Neverblender (Blender 4.0.2).
|
||||
|
||||
blender-4.0.2 --background <built.blend> --python export_mdl.py -- <outdir> [CODE ...]
|
||||
|
||||
Requires the Neverblender addon enabled in this Blender (Import-Export:
|
||||
Neverblender). Confirmed working headless against ~/.config/blender/4.0. For each
|
||||
tile it assembles a Neverblender-native model in its own scene — an AuroraBase
|
||||
(Empty, classification TILE, name == resref), a trimesh child (the VIS geometry),
|
||||
and one aabb walkmesh child (from tiles.wok_quads) — then calls
|
||||
scene.nvb_mdlexport(export_walkmesh=True) to emit <resref>.mdl and <resref>.wok.
|
||||
|
||||
Walkmesh surface materials use Neverblender's name->surfacemat mapping
|
||||
(nvb_def.wok_materials): walkable faces = wok_Stone (idx 4), blockers = wok_Nonwalk
|
||||
(idx 7). Refine per tile later (wok_Water/wok_StoneBridge/etc.) if desired.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
import addon_utils
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import tiles # noqa: E402
|
||||
|
||||
# Neverblender writes each walkmesh face's material_index (slot) verbatim as the
|
||||
# surfacemat id (nvb_node.py), and create_wok_materials() fills slots 0..22 in
|
||||
# nvb_def.wok_materials order. So the slot index IS the surfacemat index:
|
||||
WALK_SURF = 4 # wok_Stone
|
||||
BLOCK_SURF = 7 # wok_Nonwalk
|
||||
|
||||
|
||||
def build_wok(spec, name):
|
||||
from neverblender import nvb_utils
|
||||
quads = tiles.wok_quads(spec)
|
||||
verts, faces, surf = [], [], []
|
||||
for quad, walk in quads:
|
||||
b = len(verts)
|
||||
verts += quad
|
||||
faces.append((b, b + 1, b + 2, b + 3))
|
||||
surf.append(WALK_SURF if walk else BLOCK_SURF)
|
||||
me = bpy.data.meshes.new(name)
|
||||
me.from_pydata(verts, [], faces)
|
||||
nvb_utils.create_wok_materials(me) # slots 0..22 == wok_materials order
|
||||
me.update()
|
||||
for poly, s in zip(me.polygons, surf):
|
||||
poly.material_index = s # slot == surfacemat id on export
|
||||
ob = bpy.data.objects.new(name, me)
|
||||
ob.nvb.meshtype = "aabb"
|
||||
return ob
|
||||
|
||||
|
||||
def export_tile(spec, outdir):
|
||||
ref = tiles.resref(spec)
|
||||
vis = bpy.data.objects.get("VIS_" + spec["code"])
|
||||
if vis is None:
|
||||
return {"code": spec["code"], "ok": False, "error": "missing VIS object"}
|
||||
|
||||
scn = bpy.data.scenes.new("exp_" + spec["code"])
|
||||
bpy.context.window.scene = scn
|
||||
|
||||
root = bpy.data.objects.new(ref, None) # AuroraBase; name == filename
|
||||
root.nvb.emptytype = "dummy"
|
||||
root.nvb.classification = "tile"
|
||||
scn.collection.objects.link(root)
|
||||
|
||||
v = vis.copy()
|
||||
v.data = vis.data.copy()
|
||||
v.name = ref + "_01"
|
||||
v.nvb.meshtype = "trimesh"
|
||||
scn.collection.objects.link(v)
|
||||
v.parent = root
|
||||
|
||||
w = build_wok(spec, ref + "_wg") # single aabb walkmesh (rule: 1 per tile)
|
||||
scn.collection.objects.link(w)
|
||||
w.parent = root
|
||||
|
||||
mdl = os.path.join(outdir, ref + ".mdl")
|
||||
bpy.ops.scene.nvb_mdlexport(filepath=mdl, export_walkmesh=True,
|
||||
check_existing=False, batch_mode="OFF")
|
||||
wok = os.path.join(outdir, ref + ".wok")
|
||||
return {"code": spec["code"], "ref": ref, "ok": os.path.isfile(mdl),
|
||||
"mdl": os.path.isfile(mdl), "wok": os.path.isfile(wok)}
|
||||
|
||||
|
||||
def main():
|
||||
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
outdir = os.path.abspath(argv[0]) if argv else os.path.abspath("mdl")
|
||||
only = set(argv[1:])
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
|
||||
addon_utils.enable("neverblender", default_set=False)
|
||||
|
||||
results = [export_tile(s, outdir) for s in tiles.TILES if not only or s["code"] in only]
|
||||
ok = sum(1 for r in results if r["ok"])
|
||||
bad = [r for r in results if not r["ok"]]
|
||||
print(f"EXPORTED {ok}/{len(results)} tiles -> {outdir}")
|
||||
if bad:
|
||||
print("FAILED:", bad)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,64 +0,0 @@
|
||||
"""Generate the tileset palette `twu01palstd.itp` from tiles.py.
|
||||
|
||||
python make_itp.py [out.itp] # default: twu01palstd.itp
|
||||
|
||||
The toolset loads `<set>palstd.itp` when it registers a tileset; without it the
|
||||
set fails to register (blank name in the New Area wizard, "could not create
|
||||
area"). Every stock/custom set on disk ships one. It's a GFF `ITP ` file: a MAIN
|
||||
list of palette categories, each a struct with an ID byte and a LIST of
|
||||
{RESREF, STRREF} tile entries.
|
||||
|
||||
We emit one flat category listing all 40 tile model resrefs. Each entry carries
|
||||
a local NAME cexostring (NOT a STRREF) — this is what stock/custom sets without a
|
||||
tileset TLK do (verified against wut01palstd.itp). Entries with neither NAME nor
|
||||
a valid STRREF render blank AND non-selectable in the toolset palette. Built as
|
||||
JSON and converted to GFF with neverwinter.nim's nwn_gff (must be on PATH).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import tiles
|
||||
|
||||
NO_STRREF = 0xFFFFFFFF # category folder: no dialog.tlk name (cosmetic, blank folder)
|
||||
|
||||
|
||||
def tile_label(s):
|
||||
"""Readable palette name, e.g. '[flat] cobble_plain'."""
|
||||
return f"[{s['kind']}] {s['name']}"
|
||||
|
||||
|
||||
def build():
|
||||
entries = [{"__struct_id": 0,
|
||||
"NAME": {"type": "cexostring", "value": tile_label(s)},
|
||||
"RESREF": {"type": "resref", "value": tiles.resref(s)}}
|
||||
for s in tiles.TILES]
|
||||
return {
|
||||
"__data_type": "ITP ",
|
||||
"MAIN": {"type": "list", "value": [
|
||||
{"__struct_id": 0,
|
||||
"ID": {"type": "byte", "value": 0},
|
||||
"STRREF": {"type": "dword", "value": NO_STRREF},
|
||||
"LIST": {"type": "list", "value": entries}},
|
||||
]},
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
out = sys.argv[1] if len(sys.argv) > 1 else f"{tiles.PREFIX}palstd.itp"
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as tf:
|
||||
json.dump(build(), tf)
|
||||
js = tf.name
|
||||
try:
|
||||
subprocess.run(["nwn_gff", "-i", js, "-l", "json", "-k", "gff", "-o", out],
|
||||
check=True)
|
||||
finally:
|
||||
os.unlink(js)
|
||||
print(f"wrote {out} ({len(tiles.TILES)} tile palette entries)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,140 +0,0 @@
|
||||
"""Generate loadable NWN tile models (`twu01_<code>.mdl` + `.wok` + textures).
|
||||
|
||||
python make_mdl.py [outdir] # default: ./mdl
|
||||
|
||||
Pure Python, no Blender. Emits Aurora ASCII models directly from tiles.py so the
|
||||
result is deterministic and testable without a headless Blender/Neverblender run.
|
||||
(Neverblender is kept as an alternate path in export_mdl.py, but it flattens our
|
||||
multi-material tiles to one unloadable material, so this is the shipping path.)
|
||||
|
||||
Each tile .mdl contains:
|
||||
* an AuroraBase dummy node named == resref (classification TILE),
|
||||
* one trimesh node PER texture used on the tile (NWN allows one bitmap/node) —
|
||||
faces triangulated, real tverts, and a `bitmap <resref>` the engine can load,
|
||||
* the aabb walkmesh node (shared with make_wok, embedded per NWN tile rules).
|
||||
The standalone `.wok` (same walkmesh) is written alongside.
|
||||
|
||||
Textures: a solid-colour <=16-char .tga per tiles.TEXTURES (KICKOFF.md: geometry
|
||||
not art; flat colours double as an in-toolset grammar read-out). Real art can
|
||||
replace these files later without touching geometry.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import tiles
|
||||
import make_wok
|
||||
|
||||
VIS_MATID = 65536 # constant last-field on visible faces (matches stock tiles)
|
||||
|
||||
|
||||
def write_tga(path, rgb):
|
||||
"""Minimal 2x2 uncompressed 24-bit TGA in solid `rgb` (0..1 floats)."""
|
||||
r, g, b = (max(0, min(255, round(c * 255))) for c in rgb)
|
||||
w = h = 2
|
||||
header = bytes([0, 0, 2, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0,
|
||||
w & 0xFF, w >> 8, h & 0xFF, h >> 8, 24, 0])
|
||||
body = bytes([b, g, r]) * (w * h) # TGA stores BGR
|
||||
with open(path, "wb") as fh:
|
||||
fh.write(header + body)
|
||||
|
||||
|
||||
def _trimesh(ref, idx, tex, verts, quads):
|
||||
"""One trimesh node: local verts + planar tverts + triangulated faces."""
|
||||
local, lmap, tvs = [], {}, []
|
||||
|
||||
def li(gi):
|
||||
if gi not in lmap:
|
||||
x, y, z = verts[gi]
|
||||
lmap[gi] = len(local)
|
||||
local.append((x, y, z))
|
||||
tvs.append(((x + tiles.HALF) / (2 * tiles.HALF),
|
||||
(y + tiles.HALF) / (2 * tiles.HALF)))
|
||||
return lmap[gi]
|
||||
|
||||
tris = []
|
||||
for a, b, c, d in quads:
|
||||
la, lb, lc, ld = li(a), li(b), li(c), li(d)
|
||||
tris.append((la, lb, lc))
|
||||
tris.append((la, lc, ld))
|
||||
|
||||
L = [f"node trimesh {ref}_{idx:02d}",
|
||||
f" parent {ref}",
|
||||
" position 0.00000 0.00000 0.00000",
|
||||
" orientation 0.00000 0.00000 0.00000 0.00000",
|
||||
" wirecolor 1.0 1.0 1.0",
|
||||
" ambient 1.0 1.0 1.0",
|
||||
" diffuse 1.0 1.0 1.0",
|
||||
" specular 0.0 0.0 0.0",
|
||||
" shininess 1",
|
||||
f" bitmap {tex}",
|
||||
" render 1",
|
||||
" shadow 0",
|
||||
" tilefade 0",
|
||||
" rotatetexture 0",
|
||||
f" verts {len(local)}"]
|
||||
L += [f" {v[0]:.5f} {v[1]:.5f} {v[2]:.5f}" for v in local]
|
||||
L.append(f" tverts {len(tvs)}")
|
||||
L += [f" {u:.6f} {v:.6f} 0" for u, v in tvs]
|
||||
L.append(f" faces {len(tris)}")
|
||||
# face: v1 v2 v3 smoothgroup tv1 tv2 tv3 matid (tvert idx == vert idx here)
|
||||
L += [f" {a} {b} {c} 1 {a} {b} {c} {VIS_MATID}" for a, b, c in tris]
|
||||
L.append("endnode")
|
||||
return L
|
||||
|
||||
|
||||
def emit_mdl(spec):
|
||||
ref = tiles.resref(spec)
|
||||
verts, faces = tiles.vis_geometry(spec)
|
||||
|
||||
# group quads by texture, preserving first-seen order for stable node numbers
|
||||
groups, order = {}, []
|
||||
for a, b, c, d, mat in faces:
|
||||
tex = tiles.texname(mat)
|
||||
if tex not in groups:
|
||||
groups[tex] = []
|
||||
order.append(tex)
|
||||
groups[tex].append((a, b, c, d))
|
||||
|
||||
L = [f"# {ref} - Westgate Urban Foundation MVP tile (ASCII, generated)",
|
||||
f"newmodel {ref}",
|
||||
f"setsupermodel {ref} null",
|
||||
"classification TILE",
|
||||
"setanimationscale 1.0",
|
||||
f"beginmodelgeom {ref}",
|
||||
f"node dummy {ref}",
|
||||
" parent null",
|
||||
"endnode"]
|
||||
for idx, tex in enumerate(order, start=1):
|
||||
L += _trimesh(ref, idx, tex, verts, groups[tex])
|
||||
_, aabb = make_wok.aabb_node(spec)
|
||||
L += aabb
|
||||
L += [f"endmodelgeom {ref}", f"donemodel {ref}"]
|
||||
return ref, "\n".join(L) + "\n", set(order)
|
||||
|
||||
|
||||
def main():
|
||||
outdir = sys.argv[1] if len(sys.argv) > 1 else "mdl"
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
|
||||
used = set()
|
||||
for spec in tiles.TILES:
|
||||
ref, mdl, texs = emit_mdl(spec)
|
||||
used |= texs
|
||||
with open(os.path.join(outdir, ref + ".mdl"), "w") as fh:
|
||||
fh.write(mdl)
|
||||
_, wok = make_wok.emit(spec)
|
||||
with open(os.path.join(outdir, ref + ".wok"), "w") as fh:
|
||||
fh.write(wok)
|
||||
|
||||
# write every texture (not just `used`) so nothing dangles if a tile changes
|
||||
for mat, tex in tiles.TEXTURES.items():
|
||||
write_tga(os.path.join(outdir, tex + ".tga"), tiles.COLORS[mat])
|
||||
|
||||
print(f"wrote {len(tiles.TILES)} .mdl + .wok and "
|
||||
f"{len(tiles.TEXTURES)} .tga to {outdir}/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,110 +0,0 @@
|
||||
"""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"
|
||||
# Field order/values mirror the stock BioWare sets (the toolset's .set reader
|
||||
# is picky); WalkMesh=msb01 is the universal legacy label, not a file.
|
||||
L = [f"[TILE{i}]"]
|
||||
L += [
|
||||
f"Model={ref}",
|
||||
"WalkMesh=msb01",
|
||||
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=1", "MainLight2=1", "SourceLight1=1", "SourceLight2=1",
|
||||
"AnimLoop1=1", "AnimLoop2=1", "AnimLoop3=1",
|
||||
"Doors=0",
|
||||
"Sounds=0",
|
||||
f"PathNode={'A' if walkable else 'N'}", # placeholder — verify in toolset
|
||||
"Orientation=0",
|
||||
"VisibilityNode=A",
|
||||
"VisibilityOrientation=0",
|
||||
f"ImageMap2D={ref}", # minimap resref (no .tga yet; blank in toolset)
|
||||
]
|
||||
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=V1.0",
|
||||
"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()
|
||||
@@ -1,118 +0,0 @@
|
||||
"""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 = 4 # surfacemat.2da row 4 = Stone, walkable (Neverblender wok_Stone)
|
||||
BLOCK = 7 # surfacemat.2da row 7 = Nonwalk, non-walkable (wok_Nonwalk)
|
||||
|
||||
|
||||
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 aabb_node(spec):
|
||||
"""The `node aabb <ref>_wok ... endnode` block (reused by make_mdl embed)."""
|
||||
verts, faces = geometry(spec)
|
||||
nodes = aabb_tree(verts, faces)
|
||||
name = tiles.resref(spec)
|
||||
L = [f"node aabb {name}_wg",
|
||||
f" parent {name}",
|
||||
" position 0.0 0.0 0.0",
|
||||
" orientation 0.0 0.0 0.0 0.0",
|
||||
" bitmap NULL",
|
||||
" ambient 0.0 0.0 0.0",
|
||||
" diffuse 0.0 0.0 0.0",
|
||||
" specular 0.0 0.0 0.0",
|
||||
" render 0",
|
||||
" shadow 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, L
|
||||
|
||||
|
||||
def emit(spec):
|
||||
name, L = aabb_node(spec)
|
||||
head = f"# {name} walkmesh (ASCII) - compile via nwnmdlcomp/cleanmodels"
|
||||
return name, head + "\n" + "\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()
|
||||
@@ -1,366 +0,0 @@
|
||||
"""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"
|
||||
|
||||
# NWN needs a real texture (bitmap) per trimesh; material name -> texture resref
|
||||
# (<=16 chars, prefix eats 6). make_mdl.py writes a solid-colour .tga per entry.
|
||||
TEXTURES = {
|
||||
"MAT_wet_cobble_dark": "twu01_wcob",
|
||||
"MAT_cobble_patch": "twu01_cpat",
|
||||
"MAT_plaza_slab": "twu01_plaz",
|
||||
"MAT_sidewalk_stone": "twu01_swlk",
|
||||
"MAT_curb_dark_stone": "twu01_curb",
|
||||
"MAT_canal_black_water": "twu01_watr",
|
||||
"MAT_void_black": "twu01_void",
|
||||
"MAT_ramp_stone": "twu01_ramp",
|
||||
"MAT_drain_iron": "twu01_iron",
|
||||
"MAT_foundation_pad": "twu01_fnd",
|
||||
"MAT_debug_walkmesh": "twu01_dwlk",
|
||||
"MAT_debug_blocker": "twu01_dblk",
|
||||
}
|
||||
|
||||
|
||||
def texname(mat):
|
||||
return TEXTURES.get(mat, "twu01_wcob")
|
||||
|
||||
|
||||
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)]
|
||||
|
||||
|
||||
# --- visible mesh: subdivided 10x10 plane, faces tagged by material ----------
|
||||
# Same source as build.py's build_vis so the Blender preview and the exported
|
||||
# .mdl never drift. Faces are quads here; make_mdl.py triangulates on emit.
|
||||
|
||||
def vis_geometry(spec):
|
||||
"""Return (verts, faces): 100-quad plane. faces = (a, b, c, d, matname)."""
|
||||
n, step = 10, 1.0
|
||||
paint = make_paint(spec)
|
||||
verts, vidx = [], {}
|
||||
|
||||
def vi(i, j):
|
||||
if (i, j) not in vidx:
|
||||
x, y = -HALF + i * step, -HALF + j * step
|
||||
vidx[(i, j)] = len(verts)
|
||||
verts.append((x, y, height(spec, x, y)))
|
||||
return vidx[(i, j)]
|
||||
|
||||
faces = []
|
||||
for i in range(n):
|
||||
for j in range(n):
|
||||
cx, cy = -HALF + (i + 0.5) * step, -HALF + (j + 0.5) * step
|
||||
faces.append((vi(i, j), vi(i + 1, j), vi(i + 1, j + 1), vi(i, j + 1),
|
||||
paint(cx, cy)))
|
||||
return verts, faces
|
||||
|
||||
|
||||
# --- 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, "")
|
||||
Binary file not shown.
Reference in New Issue
Block a user