|
| 1 | +"""Tombstone, Arizona Territory, 1881 --- as scenery. |
| 2 | +
|
| 3 | +WHAT THIS IS FOR. The showcase fight happens in a five-metre lot, and the |
| 4 | +replay showed that lot standing alone on a patch of dirt. PeterB, with the |
| 5 | +1881 town plat in hand: "can you render the whole town?" |
| 6 | +
|
| 7 | +SCENERY, NOT TERRAIN, and the distinction is load-bearing. Every |
| 8 | +`Construct` in a Scene becomes an `attack_construct` target --- |
| 9 | +`constructs_in` projects the lot, so before this the barrels and the |
| 10 | +boarding house were things you could shoot. Putting ninety buildings in |
| 11 | +the Scene would flood every menu in the fight and let a man take aim at a |
| 12 | +saloon four blocks away. So the town never enters the engine's Scene at |
| 13 | +all: the recorder merges it into the SNAPSHOT the renderer reads, and the |
| 14 | +fight is resolved exactly as it was before. |
| 15 | +
|
| 16 | +Nothing here blocks line of sight, gives cover or can be hit, and none of |
| 17 | +that matters for a thirty-second gunfight in a lot: the nearest thing this |
| 18 | +module draws is across the street. |
| 19 | +
|
| 20 | +THE GRID IS THE PLAT'S, THE SCALE IS NOT. Tombstone's blocks run about |
| 21 | +300 feet with 80-foot streets, which is a hundred times the fight --- at a |
| 22 | +zoom that shows the lot the town is off-screen, and at a zoom that shows |
| 23 | +the town the fight is one pixel. Blocks here are 44 metres and streets 9, |
| 24 | +which is a deliberate compression that keeps a few blocks in frame around |
| 25 | +the men. The ARRANGEMENT is the map's: Safford, Fremont, Allen and |
| 26 | +Toughnut running east-west, Second through Seventh running north-south, |
| 27 | +and the lot on the south side of Fremont between Third and Fourth. |
| 28 | +""" |
| 29 | +from __future__ import annotations |
| 30 | + |
| 31 | +import random |
| 32 | + |
| 33 | +#: The lot sits at x 0.6-5.6, y -3-4 and Fremont runs along its north |
| 34 | +#: side. Everything here is laid out around that, so the fight's own |
| 35 | +#: coordinates never move. |
| 36 | +FREMONT_S, FREMONT_N = 4.0, 13.0 |
| 37 | + |
| 38 | +BLOCK = 44.0 |
| 39 | +STREET = 9.0 |
| 40 | + |
| 41 | +#: East-west streets, north to south, as (name, south edge, north edge). |
| 42 | +EW_STREETS = [ |
| 43 | + ("Safford Street", FREMONT_N + BLOCK, FREMONT_N + BLOCK + STREET), |
| 44 | + ("Fremont Street", FREMONT_S, FREMONT_N), |
| 45 | + ("Allen Street", FREMONT_S - BLOCK - STREET, FREMONT_S - BLOCK), |
| 46 | + ("Toughnut Street", FREMONT_S - 2 * (BLOCK + STREET), |
| 47 | + FREMONT_S - 2 * BLOCK - STREET), |
| 48 | +] |
| 49 | + |
| 50 | +#: North-south streets, west to east, as (name, west edge, east edge). The |
| 51 | +#: lot is between Third and Fourth, so those two bracket it. |
| 52 | +#: The lot's block runs from Third to Fourth, so those two bracket it and |
| 53 | +#: everything else steps out from there by one block and one street. Laid |
| 54 | +#: out this way rather than by absolute coordinates so the blocks are |
| 55 | +#: actually BLOCK wide -- the first cut left an eighty-metre block between |
| 56 | +#: Third and Fourth because both were placed by hand. |
| 57 | +_THIRD_E = -36.0 |
| 58 | +_FOURTH_W = _THIRD_E + BLOCK |
| 59 | + |
| 60 | + |
| 61 | +def _ns(name: str, step: int) -> tuple[str, float, float]: |
| 62 | + """A north-south street `step` blocks east of Third.""" |
| 63 | + west = _THIRD_E - STREET + step * (BLOCK + STREET) |
| 64 | + return (name, west, west + STREET) |
| 65 | + |
| 66 | + |
| 67 | +NS_STREETS = [ |
| 68 | + _ns("Second Street", -1), |
| 69 | + _ns("Third Street", 0), |
| 70 | + _ns("Fourth Street", 1), |
| 71 | + _ns("Fifth Street", 2), |
| 72 | + _ns("Sixth Street", 3), |
| 73 | +] |
| 74 | + |
| 75 | + |
| 76 | +def ground() -> dict: |
| 77 | + """The desert the town sits on. |
| 78 | +
|
| 79 | + One tile under everything, drawn FIRST so the streets and the lot paint |
| 80 | + over it. Without it the block interiors are holes: buildings line the |
| 81 | + frontages and the yards behind them were void, so the town read as a |
| 82 | + grid of rectangles floating in black. |
| 83 | + """ |
| 84 | + west = NS_STREETS[0][1] - BLOCK * 1.5 |
| 85 | + east = NS_STREETS[-1][2] + BLOCK * 1.5 |
| 86 | + south = EW_STREETS[-1][1] - BLOCK * 1.5 |
| 87 | + north = EW_STREETS[0][2] + BLOCK * 1.5 |
| 88 | + return {"id": "desert", "name": "Tombstone", "surface_type": "ground", |
| 89 | + "polygon": [(west, south), (east, south), (east, north), (west, north)]} |
| 90 | + |
| 91 | + |
| 92 | +def streets() -> list[dict]: |
| 93 | + """The roadway itself, as flat surfaces the renderer paints. |
| 94 | +
|
| 95 | + Drawn as one wide band per street rather than a road network with |
| 96 | + junctions: at this scale the crossings look right anyway, and a |
| 97 | + polygon per intersection would be a lot of geometry for nothing. |
| 98 | + """ |
| 99 | + out = [] |
| 100 | + west = NS_STREETS[0][1] - BLOCK |
| 101 | + east = NS_STREETS[-1][2] + BLOCK |
| 102 | + south = EW_STREETS[-1][1] - BLOCK |
| 103 | + north = EW_STREETS[0][2] + BLOCK |
| 104 | + for name, y0, y1 in EW_STREETS: |
| 105 | + out.append({"id": f"st-{name}", "name": name, "surface_type": "street", |
| 106 | + "polygon": [(west, y0), (east, y0), (east, y1), (west, y1)]}) |
| 107 | + for name, x0, x1 in NS_STREETS: |
| 108 | + out.append({"id": f"st-{name}", "name": name, "surface_type": "street", |
| 109 | + "polygon": [(x0, south), (x1, south), (x1, north), (x0, north)]}) |
| 110 | + return out |
| 111 | + |
| 112 | + |
| 113 | +def _storefronts(x0: float, x1: float, y0: float, y1: float, rng, *, |
| 114 | + skip=()) -> list[dict]: |
| 115 | + """One block, cut into buildings along the street frontages. |
| 116 | +
|
| 117 | + The plat's blocks are solid rows of narrow storefronts facing the |
| 118 | + street with yards behind, which is what makes the map read as a town |
| 119 | + rather than a set of rectangles. This walks each frontage laying down |
| 120 | + buildings of varying width and depth, and leaves the middle empty. |
| 121 | + """ |
| 122 | + out = [] |
| 123 | + for edge, (a0, a1) in (("s", (x0, x1)), ("n", (x0, x1))): |
| 124 | + cursor = a0 + rng.uniform(0.0, 3.0) |
| 125 | + while cursor < a1 - 4.0: |
| 126 | + width = rng.uniform(4.0, 11.0) |
| 127 | + depth = rng.uniform(7.0, 15.0) |
| 128 | + if cursor + width > a1: |
| 129 | + width = a1 - cursor |
| 130 | + near_y = y0 if edge == "s" else y1 - depth |
| 131 | + rect = (cursor, near_y, cursor + width, near_y + depth) |
| 132 | + if not any(_overlaps(rect, s) for s in skip): |
| 133 | + out.append({ |
| 134 | + "id": f"b{len(out)}-{edge}-{int(cursor)}-{int(y0)}", |
| 135 | + "height": rng.choice([3.5, 4.0, 5.0, 6.0]), |
| 136 | + "polygon": [(rect[0], rect[1]), (rect[2], rect[1]), |
| 137 | + (rect[2], rect[3]), (rect[0], rect[3])], |
| 138 | + }) |
| 139 | + cursor += width + rng.uniform(0.0, 1.5) |
| 140 | + return out |
| 141 | + |
| 142 | + |
| 143 | +def _overlaps(rect, other) -> bool: |
| 144 | + ax0, ay0, ax1, ay1 = rect |
| 145 | + bx0, by0, bx1, by1 = other |
| 146 | + return not (ax1 <= bx0 or bx1 <= ax0 or ay1 <= by0 or by1 <= ay0) |
| 147 | + |
| 148 | + |
| 149 | +def buildings(seed: int = 7) -> list[dict]: |
| 150 | + """Every building in town, as {id, height, polygon}. |
| 151 | +
|
| 152 | + Seeded, so the town is the same town every time it is rendered --- a |
| 153 | + backdrop that reshuffled between takes would be worse than none. |
| 154 | + """ |
| 155 | + rng = random.Random(seed) |
| 156 | + #: Where the fight is. No generated building may land on the lot, the |
| 157 | + #: two houses that front it, or the stretch of Fremont the Earps |
| 158 | + #: walked in along. |
| 159 | + keep_clear = [(-14.0, -12.0, 20.0, 14.0)] |
| 160 | + |
| 161 | + out: list[dict] = [] |
| 162 | + ns_edges = [(NS_STREETS[i][2], NS_STREETS[i + 1][1]) |
| 163 | + for i in range(len(NS_STREETS) - 1)] |
| 164 | + ew_edges = [(EW_STREETS[i + 1][2], EW_STREETS[i][1]) |
| 165 | + for i in range(len(EW_STREETS) - 1)] |
| 166 | + for x0, x1 in ns_edges: |
| 167 | + for y0, y1 in ew_edges: |
| 168 | + out.extend(_storefronts(x0, x1, y0, y1, rng, skip=keep_clear)) |
| 169 | + # Unique ids across blocks. |
| 170 | + for i, b in enumerate(out): |
| 171 | + b["id"] = f"town-{i}" |
| 172 | + return out |
0 commit comments