1
0
Fork 0

docs: document the game map system and construction timestamp model

Add docs/map-system.md with the mapping table and update AGENTS.md plus sub-AGENTS files for the map collections, routing, placement, and construction model.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
Jason Fraley 2026-09-16 23:14:20 -04:00
parent a770c62d6f
commit 8cab545b9d
4 changed files with 247 additions and 2 deletions

View file

@ -194,6 +194,20 @@ Labor/staffing simulation for structures. Collections in `src/collections/game/`
- **UI**: `src/components/frontend/baseManagement/` (`StaffRoster`, `HireStaffDialog`, `EmploymentCard`, `UpgradeCard`) mounted on the structure detail page (`logistics/structures/[id]`). - **UI**: `src/components/frontend/baseManagement/` (`StaffRoster`, `HireStaffDialog`, `EmploymentCard`, `UpgradeCard`) mounted on the structure detail page (`logistics/structures/[id]`).
- **Events**: `staff:hired` / `staff:terminated` / `staff:headcount-set` / `staff:salary-paid` / `staff:salary-unpaid` / `structure:maintenance-paid` / `structure:maintenance-unpaid` / `structure:upgraded` / `structure:upkeep-short`. Event target: `npc-staffing`. - **Events**: `staff:hired` / `staff:terminated` / `staff:headcount-set` / `staff:salary-paid` / `staff:salary-unpaid` / `structure:maintenance-paid` / `structure:maintenance-unpaid` / `structure:upgraded` / `structure:upkeep-short`. Event target: `npc-staffing`.
## Game Map System
Leaflet world map at `/map` (top-level sidebar entry, whole unit views it). Full docs: `docs/map-system.md`.
- **Collections** (`src/collections/world/`): `maps` (redesigned: `worldSizeWidth`/`worldSizeHeight` meters, `basemapMode` select `image`/`tiles`, `basemapImage` upload, `tileUrlTemplate`, zoom defaults, `isActive`), `map-roads` (name, description, map rel, surface select, `speedMultiplier`, strokeColor/strokeWeight/strokeStyle/labelVisible, `points` JSON array of [x, y] meters), `map-zones` (name, description, type select water/land, fillColor/strokeColor/fillOpacity/strokeWeight/labelVisible, polygon points JSON), `resource-nodes` (`resources` hasMany rel, `position` JSON [x, y], richness, name, description). Feature collections read: any logged-in user; write: `maps:*` permissions. Admin-panel visibility needs `<slug>:read` + `admin:<slug>:manage` ("Map Features" permission group).
- **Coordinates**: real meters, per-map Cartesian grid, origin top-left, y runs south (Arma convention). JSON geometry for map features; `game-structures.coordinates` keeps its PostGIS point but stores meters (`validate: () => true` overrides Payload's geographic lng/lat validation; meters are not degrees).
- **Routing**: `src/lib/map/routing.ts` (pure) builds a graph from road polylines (1 m vertex epsilon; shared vertex = junction), Dijkstra weighted by `length / speedMultiplier`; origin/destination snap to the nearest vertex within 1000 m; no connected route refuses ground shipments with a clear error. Air/sea (`vehicles.transportMode`) go straight-line. Distance = geometric route length in meters; `routePath` JSON snapshot on shipments (auto-return reverses it). Speed math converts meters to km once in `calculateTransitTime`; fuel stays `meters x rate`.
- **Placement**: `src/lib/map/placement.ts` (pure: terrain point-in-polygon, resourcesInRange vs multi-resource nodes OR resource-bearing zones at the site, structuresInRange) + `placeStructure` action (`src/app/(frontend)/logistics/map/actions.ts`, hasLogisticsQualification gate, emits `structure:placed`). Map UI placement mode for logistics-qualified users (click to place, dialog with blueprint/name/faction, client pre-validation + server enforcement). Water zones block land blueprints; water blueprints need a water zone; no zones = everything allowed.
- **Construction**: `src/lib/map/construction.ts`. Construction runs on timestamps: `maybeStartConstruction(payload, siteId, now)` is called at delivery time (shipment `completeShipment`, the structures `addResource`/`transferResource` actions via `addResourceInternal`, and `placeStructure` for materialless blueprints) and, when every blueprint material is stored, consumes them via `consumeStorage`, sets `constructionStartedAt`/`constructionCompletesAt` from the blueprint's `constructionDurationMinutes` (minutes, replacing the old tick-based `constructionTime`), and flips `awaiting_materials` to `building`. The game tick's `processConstructionTick` is a sweeper only: building sites past `constructionCompletesAt` flip to `complete` + `structure:construction-complete` (idempotent). Default constructionStatus on game-structures is `complete` (backward compat). ConstructionPanel on the structure detail page shows required vs delivered + a timestamp-derived progress bar; map markers interpolate progress from the timestamps.
- **UI**: `/map` (`src/app/(frontend)/map/page.tsx` + `MapLoader` ssr:false + `src/components/frontend/map/MapClient.tsx`): CRS.Simple with y-flip (latlng = [-y, x]), image (ImageOverlay) or XYZ (TileLayer) basemap, map switcher, layer toggles (structures/nodes/roads/zones/shipments/ranges), structure markers as status-colored pins (emerald building icon = operational, blue hammer = building, amber package = awaiting materials, with a progress bar under construction sites), popovers linking to `/logistics/structures/[id]`, shipment markers interpolated along `routePath` from `dispatchedAt`/`estimatedArrival`, refreshed by `useGameTick`. Feature data served by `GET /api/map-features?mapId=` (view surface, logged-in). Map labels: zone names render at the polygon centroid, road names render as curved SVG textPath labels following a Catmull-Rom smoothed road line every 400 screen px (max 12/road, flipped to a reversed path when the direction would render upside down or straight down, so vertical labels read bottom-to-top; dark casing stroke for contrast), structure names render as persistent labels with cluster priority (`labelVisible` + `labelPriority` on game-structures; winner = highest `labelPriority`, then stored mass, then id; cluster radius 56 screen px, so zooming in reveals suppressed labels). Road/zone stroke + fill colors, widths, dash styles, and opacities are editable in the authoring toolbar (`strokeColor`/`strokeWeight`/`strokeStyle` on roads; `fillColor`/`strokeColor`/`fillOpacity`/`strokeWeight` on zones; hex validated by `validateHexColor` in `src/lib/map/style.ts`, empty = per-type defaults). Pure label/styling math lives in `src/lib/map/style.ts` (`metersPerPx`, `roadLabelPoints`, `polygonCentroid`, `dashArrayFor`).
- **Authoring**: edit mode on `/map` (maps:*-gated) draws roads/zones/nodes, edits existing features (popup Edit loads geometry + attributes into the authoring toolbar; Save passes the feature id and requires `maps:update`), and deletes features (server actions in the same actions.ts); `POST /api/map-import` imports a GeoJSON FeatureCollection (LineString to roads, Polygon to zones, Point to nodes; [x, y] positions used directly as map meters). Mapping table in `docs/map-system.md`.
- **GameRules**: `proximityThreshold` now means meters (default 1000; migration backfilled rows still at the old default 10). Both call sites (shipments/actions.ts, structures/actions.ts transferResource) use meters.
- **Tests**: `tests/int/map-system.int.spec.ts` (routing graph/snap/refusals, cross-map refusal, ground route + path snapshot, air straight-line, proximity, terrain/range validation, placeStructure happy/water/range cases, construction tick lifecycle, permission gating, label/styling helpers + save-action styling validation). Gotcha: Payload applies select `filterOptions` server-side, so test fixtures must use `approvalStatus: "in_progress"`.
## Personnel (NPC directory) ## Personnel (NPC directory)
- `/personnel/contacts` — NPC directory over `game-npcs` (`src/components/frontend/personnel/`: `NpcDirectory`, `NpcCard`); `/personnel/contacts/[id]` — NPC profile (faction, in-game vs generated badges, vendor listings, `NpcProfile`). - `/personnel/contacts` — NPC directory over `game-npcs` (`src/components/frontend/personnel/`: `NpcDirectory`, `NpcCard`); `/personnel/contacts/[id]` — NPC profile (faction, in-game vs generated badges, vendor listings, `NpcProfile`).

230
docs/map-system.md Normal file
View file

@ -0,0 +1,230 @@
# Game Map System
Leaflet-based interactive world map at `/map` with per-map road routing, resource
nodes, GM terrain zones, and structure placement. This document covers the data
model, coordinate conventions, gameplay rules, authoring workflows, and the
GeoJSON import format.
## Collections
| Collection | Slug | Purpose | Access |
| -------------------- | ----------------- | ------------------------------------------------------------------------------------- | -------------------------------- |
| Maps | `maps` | Per-map config: world size, basemode, view defaults | Config: `maps:*` |
| Map Roads | `map-roads` | Road polylines used by ground shipment routing | Read: logged-in; write: `maps:*` |
| Map Zones | `map-zones` | Terrain polygons (water blocks land placement) | Read: logged-in; write: `maps:*` |
| Resource Nodes | `resource-nodes` | Deposits validated by blueprint ranges | Read: logged-in; write: `maps:*` |
| Game Structures | `game-structures` | Gained `map`, `constructionStatus`, `constructionStartedAt`/`constructionCompletesAt` | unchanged |
| Shipments | `shipments` | Gained `map`, `routePath` (rendered polyline snapshot) | unchanged |
| Vehicles (blueprint) | `vehicles` | Gained `transportMode` (ground/air/sea) | unchanged |
The map page (`/map`) serves feature data to any logged-in user through
`GET /api/map-features?mapId=<id>`, while the collections themselves stay
gated in the admin panel. Editing map features is possible in two places:
1. The map UI's "Edit features" mode (requires `maps:*` permissions), or
2. The Payload admin panel (World group).
## Coordinate conventions
- All map feature geometry (`map-roads.points`, `map-zones.points`,
`resource-nodes.position`) is stored as JSON arrays of `[x, y]` tuples in
**real meters** on the map grid. Origin is the top-left corner, x runs east,
y runs **south** (matching Arma 3 world coordinates).
- `game-structures.coordinates` keeps its existing PostGIS point column and is
read as an `[x, y]` tuple of map meters.
- Per-map world size (`worldSizeWidth`/`worldSizeHeight`, e.g. 8192 for Altis)
bounds every coordinate. `GameRules.proximityThreshold` is meters (default
1000).
- The map UI flips the y axis for Leaflet's `CRS.Simple`: Leaflet latlng
`[-y, x]`.
## Routing (ground shipments)
`createShipment` (src/app/(frontend)/logistics/shipments/actions.ts) resolves
the route per vehicle `transportMode`:
- **ground**: Dijkstra over the map's road graph
(`src/lib/map/routing.ts`). The graph is built from all road polylines on
the map; roads connect where they share a vertex within a 1 m epsilon, so
author junctions by reusing identical endpoint coordinates. Origin and
destination structures snap to the nearest graph vertex within
`DEFAULT_SNAP_RADIUS_METERS` (1000 m). If either end cannot snap, or the two
components are disconnected, the shipment is **refused** with a clear error.
Edge weight is `length / speedMultiplier`, so `speedMultiplier` steers which
route wins; the shipment's stored `distance` is always the geometric route
length in meters (fuel is charged on that).
- **air / sea**: straight-line distance, `routePath` is the two endpoints.
Cross-map shipments are refused (origin and destination must carry the same
`map`). Shipment speed math converts meters to km once in
`src/lib/shipping.ts` (`calculateTransitTime`); fuel stays
`distance_m * fuelConsumptionRate`. Active shipments render interpolated
along `routePath` on the map between `dispatchedAt` and `estimatedArrival`,
refreshed by the game tick SSE stream.
## Placement and construction
`placeStructure` (src/app/(frontend)/logistics/map/actions.ts) is gated by
`hasLogisticsQualification` (admins/developers pass) and enforces server-side:
1. Map exists and is active, coordinates inside world bounds.
2. Terrain: land blueprints are rejected inside drawn water polygons; water
blueprints require a water polygon. A map with no zones accepts everything
(default terrain is buildable land).
3. `resourcesInRange`: every blueprint requirement needs a matching resource
node within range (meters) on the same map.
4. `structuresInRange`: same check against existing game structures.
Created sites start as `constructionStatus: "awaiting_materials"`. Construction
runs on timestamps, not tick counts (src/lib/map/construction.ts):
- `maybeStartConstruction` is called at delivery time: from
`completeShipment` (shipment arrival), the structures `addResource` /
`transferResource` server actions, and `placeStructure` (blueprints without
materials start immediately). When every blueprint material is present in
the site's storage (grid + void) it consumes them (`consumeStorage`), sets
`constructionStartedAt = now` and `constructionCompletesAt = now +` the
blueprint's `constructionDurationMinutes`, and flips the site to `building`.
- the game tick sweep (`processConstructionTick`) only completes: sites whose
`constructionCompletesAt` has passed flip to `complete` and emit
`structure:construction-complete` (idempotent).
`structure:placed` is emitted at placement time. Blueprint `requiredTech`
stays display-only in the placement dialog (no research system exists).
## Authoring
### Map UI edit mode
"Edit features" on `/map` (requires `maps:*`) supports:
- **New road**: click vertices along the route, at least 2; set name,
description, surface (paved/dirt/trail), speed multiplier, and styling
(stroke color hex, width 1-10, dash style, name-label toggle); Save.
- **New zone**: click at least 3 vertices; set name, description, type
(water/land), and styling (fill color, edge color, fill opacity 0-1, edge
width, name-label toggle); Save.
- **New node**: click once to set the position; set name, description,
resource, richness; Save.
- Existing roads/zones/nodes show Edit + Delete buttons in their popup while
in edit mode. Edit loads the feature's geometry and attributes into the
authoring toolbar as a draft: click the map to add/reposition points, adjust
the fields, then "Save changes" (requires `maps:update`). Renaming, retyping,
restyling, and description edits work the same way; the feature's map cannot
be changed.
### Labels and styling on the live map
- **Descriptions**: every road, zone, and node can carry a description; it
shows in the hover tooltip (second, non-uppercase line) and in the popup.
- **Resources**: nodes can yield multiple resources (the authoring toolbar
lists every resource as a toggle; tooltips and popups show them all), and
zones can carry resources as areas (oil fields and the like): a zone with
resources is listed in its tooltip and popup, and a blueprint's
`resourcesInRange` requirement is satisfied when the site sits INSIDE such
a zone, even with no node nearby.
- **Zone labels**: zones with a name and "Show Name Label" enabled render the
name at the polygon's area-weighted centroid (mono uppercase on a subtle
dark chip with a halo).
- **Road labels**: named roads with "Show Name Label" enabled render their
name as curved SVG `textPath` text that follows a Catmull-Rom smoothed
version of the road's bends, centered on the line, with a dark casing
outline for contrast. Labels appear only once the road is at least 120
screen px long, roughly every 400 screen px, capped at 12 per road,
recomputed on zoom. Text on segments that would render upside down, or
exactly vertical reading downward, is flipped to a reversed path: text
always reads left-to-right, and vertical labels read bottom-to-top.
- **Structure labels**: structures with "Show Name Label" enabled (Game
Structures admin, default on) get a persistent name label next to their pin.
When structures cluster closer than 56 screen px at the current zoom, only
one label wins: highest `labelPriority` (0-100, admin-settable), tie-broken
by stored mass, then id. Zooming in spreads the cluster and reveals the
losing labels. Structures with the label off keep hover-only tooltips.
Persistent labels sit on a subtle dark chip for contrast.
- **Road/zone styling**: stroke color (`#rrggbb`), width, and dash style
(solid/dashed/dotted) are editable per road; fill color, edge color, fill
opacity, and edge width per zone. Empty values fall back to per-type/surface
defaults (`paved` light gray, `dirt` tan, `trail` dimmer; water `#38bdf8`
0.25 fill, land `#4ade80` 0.08 fill). Invalid hex is rejected at save time
(both the authoring UI actions and the import route).
### GeoJSON import
`POST /api/map-import` (admin/developer, `maps:create`) accepts a JSON body:
```json
{
"mapId": 1,
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [
[100, 200],
[400, 200]
]
},
"properties": { "name": "Highway 1", "surface": "paved", "speedMultiplier": 1.5 }
},
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[0, 0],
[0, 500],
[500, 500],
[500, 0],
[0, 0]
]
]
},
"properties": { "name": "Bay", "type": "water" }
},
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [1200, 3400] },
"properties": { "name": "North copper", "resource": 12, "richness": 80 }
}
]
}
```
Geometry mapping:
| GeoJSON geometry | Collection | Properties |
| ---------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `LineString` | `map-roads` | `name`, `description` (optional), `surface` (`paved`\|`dirt`\|`trail`, default `paved`), `speedMultiplier` (default 1), `strokeColor` (`#rrggbb`), `strokeWeight`, `strokeStyle` (`solid` default, `dashed`, `dotted`) |
| `Polygon` | `map-zones` | `name`, `description` (optional), `type` (`water` default, or `land`); only the outer ring is used; `fillColor`, `strokeColor` (`#rrggbb`), `fillOpacity` (0-1), `strokeWeight` |
| `Point` | `resource-nodes` | `name`, `description` (optional), `resource` (resource ID) or `resourceName` (exact match), `richness` (default 100) |
GeoJSON positions are interpreted directly as map meters: `[x, y]` =
[east, south] on the map grid (no axis flip on import; the map UI applies its
flip only for display). The response reports per-type created counts plus a
list of per-feature errors; valid features are imported even when others fail.
## Permissions
| Action | Requirement |
| ------------------------------------------------- | --------------------------------------------------------------------------- |
| View the map + features | Any logged-in user |
| Place structures | `hasLogisticsQualification` (logistics qualification, admin/developer pass) |
| Configure maps, roads, zones, nodes | `maps:create` / `maps:update` / `maps:delete` |
| Admin panel visibility of map feature collections | `<slug>:read` + `admin:<slug>:manage` |
## Basemap configuration
Per map, in the Payload admin panel (World: Maps):
- `basemapMode: image`: upload a top-down map image (Media); it is stretched
across the full world bounds via `ImageOverlay`.
- `basemapMode: tiles`: set `tileUrlTemplate` (XYZ, e.g.
`https://tiles.example.com/{z}/{x}/{y}.png`) rendered via `TileLayer`.
Tile pyramids follow the standard XYZ convention: zoom 0 is one 256 px tile
covering the whole map, and each zoom level doubles the resolution (set the
map's `minZoom` to the lowest level your pyramid actually serves).
- `minZoom`/`maxZoom`/`defaultZoom` and `defaultCenterX`/`defaultCenterY`
(meters) tune the initial view. `isActive` hides maps from the page.

View file

@ -25,7 +25,7 @@ collections/
# GameVehicles, GameNpcs, GameHardResources, GameEventLogs, # GameVehicles, GameNpcs, GameHardResources, GameEventLogs,
# NpcStaffing, LaborClassifications (+ shared staffingFields.ts factory) # NpcStaffing, LaborClassifications (+ shared staffingFields.ts factory)
server/ 5 files # MissionFiles, ModLists, GameServers, ArmaCommands, ArmaSyncEvents server/ 5 files # MissionFiles, ModLists, GameServers, ArmaCommands, ArmaSyncEvents
world/ 2 files # Maps, NarrativeEvents (React Flow editor) world/ 5 files # Maps (basemap config), MapRoads/MapZones/ResourceNodes (map features), NarrativeEvents
projects/ 4 files # Projects, Labels, Releases, Sprints (Jira-like) projects/ 4 files # Projects, Labels, Releases, Sprints (Jira-like)
tickets/ 2 files # Tickets (Lexical rich text), TicketVotes tickets/ 2 files # Tickets (Lexical rich text), TicketVotes
wiki/ 3 files # WikiPages, WikiRevisions, WikiTemplates wiki/ 3 files # WikiPages, WikiRevisions, WikiTemplates

View file

@ -27,6 +27,7 @@ lib/
base/ # Staffing (hire/fire/headcount), structure upgrades, storage, base tick base/ # Staffing (hire/fire/headcount), structure upgrades, storage, base tick
evaluations/ # Evaluation levels + bot reminder plan (reminders.ts) evaluations/ # Evaluation levels + bot reminder plan (reminders.ts)
locker/ # Grid logic, placement validation, loadouts, attachments/skins locker/ # Grid logic, placement validation, loadouts, attachments/skins
map/ # World map: routing.ts (Dijkstra road graph), placement.ts (terrain/ranges), construction.ts (tick), points.ts (pure geometry)
logistics/ # Mission lifecycle + mission reminder logic logistics/ # Mission lifecycle + mission reminder logic
market/ # Buy/sell, NPC negotiation, dialogue, vendor resolution market/ # Buy/sell, NPC negotiation, dialogue, vendor resolution
notifications/ # User notification helper + muteable types notifications/ # User notification helper + muteable types