Update README with project details and setup instructions; add data directories to .gitignore
This commit is contained in:
@@ -1,2 +1,135 @@
|
||||
# the-living-world
|
||||
# The Living World
|
||||
|
||||
A web game whose map is a real place. The backend pulls a square of OpenStreetMap data once, turns it into an
|
||||
ECS world, and serves it to a PixiJS client that renders it as a vector map.
|
||||
|
||||
This is the first iteration: world generation and rendering only, no gameplay yet.
|
||||
|
||||
## Stack
|
||||
|
||||
| Piece | Choice |
|
||||
| --- | --- |
|
||||
| Backend | .NET 10, ASP.NET Core minimal APIs |
|
||||
| World model | [Arch](https://github.com/genaray/Arch) ECS |
|
||||
| Map data | OpenStreetMap via the Overpass API |
|
||||
| Frontend | PixiJS 8 + TypeScript + Vite |
|
||||
| Orchestration | .NET Aspire 13 |
|
||||
| Storage | Plain files under `data/` |
|
||||
|
||||
## Running it
|
||||
|
||||
On Windows, double-click `run.cmd` or run it from a terminal:
|
||||
|
||||
```bash
|
||||
run.cmd
|
||||
```
|
||||
|
||||
Anywhere else, or if you prefer the CLI directly:
|
||||
|
||||
```bash
|
||||
dotnet run --project src/TheLivingWorld.AppHost
|
||||
```
|
||||
|
||||
Aspire starts the API, runs `npm install` for the client, launches the Vite dev server, and prints a dashboard
|
||||
URL. Open the `web` endpoint from the dashboard, enter coordinates, and press **Generate world**.
|
||||
|
||||
The default coordinates are Robert Lee, Texas (`31.8966010, -100.4858591`) — a small town that generates in a
|
||||
few seconds.
|
||||
|
||||
To run the two halves separately instead:
|
||||
|
||||
```bash
|
||||
dotnet run --project src/TheLivingWorld.Api
|
||||
```
|
||||
|
||||
```bash
|
||||
npm --prefix src/TheLivingWorld.Web run dev
|
||||
```
|
||||
|
||||
Tests:
|
||||
|
||||
```bash
|
||||
dotnet test
|
||||
```
|
||||
|
||||
## How a world is made
|
||||
|
||||
1. **Fetch.** `OverpassClient` posts one bounding-box query to Overpass and streams the response into
|
||||
`data/osm-cache/<hash>.json`. The hash covers the query text, so the same box is never downloaded twice and
|
||||
editing the query invalidates the cache. Public mirrors are tried in order, with retries.
|
||||
2. **Project.** `LocalProjection` flattens WGS84 onto a metric plane centred on the requested point: X east,
|
||||
Y north, both in metres. Over a 20 km square the error stays under a metre, and distances are directly
|
||||
usable as game units — which Web Mercator would not give.
|
||||
3. **Import.** `OsmWorldBuilder` reads each element's tags, decides what it is, and creates one ECS entity per
|
||||
feature. Multipolygon relations are stitched into rings by `RingAssembler`; everything is clipped to the
|
||||
world square by `GeometryClipper`, so a highway crossing town does not drag geometry 40 km off the map.
|
||||
4. **Systems.** `ComputeBoundsSystem` fills each entity's extent, `AssignChunksSystem` buckets it into the
|
||||
chunk grid.
|
||||
5. **Export.** `ChunkExporter` walks the ECS world and writes one JSON file per chunk, plus an index.
|
||||
|
||||
### The ECS shape
|
||||
|
||||
Geometry does not live in components. `ShapeStore` holds the vertex arrays and components carry an integer
|
||||
handle, which keeps component data blittable and archetype chunks dense.
|
||||
|
||||
| Component | Meaning |
|
||||
| --- | --- |
|
||||
| `OsmSource` | Which OSM element this came from |
|
||||
| `Outline` / `Holes` | Closed ring and the rings cut out of it |
|
||||
| `Polyline` | Open centreline, for roads and streams |
|
||||
| `Bounds` / `InChunk` | Cached extent and spatial bucket, filled in by systems |
|
||||
| `Building` | Kind, height, levels |
|
||||
| `Road` | Class, width, lanes, bridge/tunnel/oneway flags |
|
||||
| `AreaFeature` / `Water` | Land cover and water classification |
|
||||
| `DisplayName` | The `name` tag |
|
||||
|
||||
There are no simulation systems yet — the pipeline is the two passes above. Gameplay systems slot in beside
|
||||
them without reworking the data model.
|
||||
|
||||
## HTTP API
|
||||
|
||||
| Endpoint | Purpose |
|
||||
| --- | --- |
|
||||
| `POST /api/worlds` | Start generating a world. Returns immediately with `status: "pending"` |
|
||||
| `GET /api/worlds` | List worlds, with live status for anything still generating |
|
||||
| `GET /api/worlds/{id}` | Status of one world |
|
||||
| `GET /api/worlds/{id}/map` | Metadata plus the chunk index |
|
||||
| `GET /api/worlds/{id}/chunks/{x}/{y}` | One chunk of geometry |
|
||||
| `DELETE /api/worlds/{id}` | Remove a world and its chunks |
|
||||
|
||||
Generation takes tens of seconds — mostly waiting on Overpass — so `POST` returns straight away and the client
|
||||
polls for status. Only one generation runs at a time, to stay a good citizen on the shared Overpass mirrors.
|
||||
|
||||
Geometry travels as flat `[x0, y0, x1, y1, …]` arrays of world metres, which is exactly what PixiJS
|
||||
`Graphics.poly()` accepts, so the client never reshapes it. Responses are compressed; chunk files are written
|
||||
in wire format and streamed straight from disk.
|
||||
|
||||
## The client
|
||||
|
||||
`MapView` owns one scaled container holding five layers — land cover, water, road casings, road fills,
|
||||
buildings — so a building in one chunk never ends up under a park from the next. `Camera` is the only place
|
||||
the Y flip lives; everything else thinks in map coordinates.
|
||||
|
||||
`ChunkManager` fetches chunks as the camera reaches them and drops their graphics once they are well out of
|
||||
view, keeping the parsed data cached so panning back is instant. Detail thins out as you zoom away: footpaths
|
||||
disappear first, then small buildings, and stroke widths gain a floor so hairlines stay visible.
|
||||
|
||||
## Configuration
|
||||
|
||||
`src/TheLivingWorld.Api/appsettings.json`:
|
||||
|
||||
- `WorldStorage:RootDirectory` — where generated worlds go (default `data/worlds`)
|
||||
- `Osm:Endpoints` — Overpass mirrors, tried in order
|
||||
- `Osm:CacheDirectory` — raw Overpass responses (default `data/osm-cache`)
|
||||
- `Osm:QueryTimeoutSeconds` / `Osm:RequestTimeoutSeconds` — server-side and client-side budgets
|
||||
|
||||
Relative paths resolve against the API's content root. Everything under `data/` is reproducible from
|
||||
coordinates and is not committed.
|
||||
|
||||
## Known limits
|
||||
|
||||
- The Overpass response is parsed in one pass rather than streamed. Fine for the small towns this targets; a
|
||||
dense 20 km city would want a streaming reader.
|
||||
- Chunk assignment buckets a feature by the centre of its extent and never splits geometry, so a long road
|
||||
overhangs its chunk. The exported chunk bounds are widened to match and the client culls against those.
|
||||
- Buildings are flat footprints shaded by height. No 3D, no roofs.
|
||||
|
||||
Reference in New Issue
Block a user