wgt01
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
"""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.
|
||||
|
||||
Every NON-paint tile gets a 1x1 [GROUP#] (a "feature") so it is click-placeable
|
||||
from the palette (AGENTS.md §2.3) — the .itp Features folder points its leaves at
|
||||
these groups by first-tile resref, and Name must match tiles.group_name().
|
||||
|
||||
PAINT tiles (tiles.is_paint: cobble fill CP1/2/3, canal fill CN1, canal edges
|
||||
CE_*) are deliberately NOT grouped: a grouped tile is excluded from the toolset's
|
||||
terrain-paint pool, so leaving them ungrouped IS what makes cobble/canal
|
||||
paintable and the eraser work. Grouping every tile empties the fill pool and the
|
||||
New Area wizard crashes (verified vs stock tcn01, 2026-07-04). Transition tiles
|
||||
expand into 4 orientations (tiles.placements) so the corner matrix is complete;
|
||||
make_set.main() hard-fails if tiles.canal_paint_matrix() is missing any pattern.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
import tiles
|
||||
|
||||
PREFIX = tiles.PREFIX
|
||||
|
||||
|
||||
def tile_block(i, spec, orient, corners, edges):
|
||||
tl, tr, bl, br = corners
|
||||
n, e, s, w = edges
|
||||
ref = tiles.resref(spec)
|
||||
walkable = spec["kind"] != "canal"
|
||||
# Paint tiles connect by CORNER match only (like stock wut01 terrains) — blank
|
||||
# their edge crossers so the brush isn't over-constrained; features keep theirs.
|
||||
cr = (lambda edge: "") if tiles.is_paint(spec) else tiles.nwn_crosser
|
||||
# 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={cr(n)}",
|
||||
f"Right={cr(e)}",
|
||||
f"Bottom={cr(s)}",
|
||||
f"Left={cr(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
|
||||
f"Orientation={orient}", # 0/90/180/270 rotations of one model
|
||||
"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=0", # MVP paints flat cobble only; matches stock
|
||||
"Transition=1.5", # metres per height step = Z=1.5 raised tier (unused while HHT=0)
|
||||
"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", # optional; wut01 paints 6 terrains w/ 0
|
||||
"", "[SECONDARY RULES]", "Count=0"]
|
||||
# Expand each spec into its placement(s): transition tiles emit 4 rotations, so
|
||||
# one model backs several [TILE#] entries. Track each spec's FIRST tile index so
|
||||
# its [GROUP#] (if any) points at the right tile.
|
||||
placed, first_idx = [], {}
|
||||
for spec in tiles.TILES:
|
||||
for pl in tiles.placements(spec):
|
||||
first_idx.setdefault(spec["code"], len(placed))
|
||||
placed.append((spec, pl))
|
||||
L += ["", "[TILES]", f"Count={len(placed)}"]
|
||||
for i, (spec, (orient, corners, edges)) in enumerate(placed):
|
||||
L.append("")
|
||||
L += tile_block(i, spec, orient, corners, edges)
|
||||
# One 1x1 group ("feature") per NON-paint tile so the palette can click-place it.
|
||||
# Paint tiles (fills + terrain transitions) stay ungrouped -> the brush pool.
|
||||
feats = [s for s in tiles.TILES if not tiles.is_paint(s)]
|
||||
L += ["", "[GROUPS]", f"Count={len(feats)}"]
|
||||
for gi, spec in enumerate(feats):
|
||||
L += ["", f"[GROUP{gi}]", f"Name={tiles.group_name(spec)}",
|
||||
"Rows=1", "Columns=1", f"Tile0={first_idx[spec['code']]}"]
|
||||
L += [""]
|
||||
return L
|
||||
|
||||
|
||||
def main():
|
||||
_, _cov, missing = tiles.canal_paint_matrix()
|
||||
if missing: # incomplete matrix = paint crash; never stage
|
||||
raise SystemExit(f"INCOMPLETE canal paint matrix — {len(missing)} pattern(s) "
|
||||
f"have no tile: {sorted(missing)}")
|
||||
out = sys.argv[1] if len(sys.argv) > 1 else f"{PREFIX}.set"
|
||||
lines = build()
|
||||
with open(out, "w", newline="") as fh: # newline='' + explicit \r\n = CRLF
|
||||
fh.write("\r\n".join(lines))
|
||||
ntiles = sum(1 for l in lines if re.fullmatch(r"\[TILE\d+\]", l))
|
||||
print(f"wrote {out} ({ntiles} tile placements from {len(tiles.TILES)} models, "
|
||||
f"{len(tiles.TERRAINS)} terrains, {len(tiles.CROSSERS)} crossers, CRLF)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user