"""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, "")