diff --git a/AGENTS.md b/AGENTS.md index 0b01bd1..1b08866 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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]`). - **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 `:read` + `admin::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/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`). @@ -380,7 +394,7 @@ Where it fits: large feature/hero cards, section headers, showcase surfaces. Whe Whenever creating a new component or refactoring an existing component's styling, **ask the user** whether they want to try applying the tactical dossier style to it. Concretely: -- **Yes**: demo similar styling on that component first (same approach as the campaign cards: build it, let the user eyeball it, iterate on tuning). +- **Yes**: demo similar styling on that component first (same approach as the campaign cards: build it, let the user eyeball it, iterate on tuning). - **No, or no answer**: stick with the existing styling conventions (shadcn + the site's dark theme conventions). Do not apply tactical styling unasked, and do not nag: ask once, then proceed either way. ## Environment diff --git a/docs/map-system.md b/docs/map-system.md new file mode 100644 index 0000000..16b2b7a --- /dev/null +++ b/docs/map-system.md @@ -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=`, 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 | `:read` + `admin::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. diff --git a/src/collections/AGENTS.md b/src/collections/AGENTS.md index 8f87dc1..ec66fd9 100644 --- a/src/collections/AGENTS.md +++ b/src/collections/AGENTS.md @@ -25,7 +25,7 @@ collections/ # GameVehicles, GameNpcs, GameHardResources, GameEventLogs, # NpcStaffing, LaborClassifications (+ shared staffingFields.ts factory) 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) tickets/ 2 files # Tickets (Lexical rich text), TicketVotes wiki/ 3 files # WikiPages, WikiRevisions, WikiTemplates diff --git a/src/lib/AGENTS.md b/src/lib/AGENTS.md index 2530c26..4dda45e 100644 --- a/src/lib/AGENTS.md +++ b/src/lib/AGENTS.md @@ -27,6 +27,7 @@ lib/ base/ # Staffing (hire/fire/headcount), structure upgrades, storage, base tick evaluations/ # Evaluation levels + bot reminder plan (reminders.ts) 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 market/ # Buy/sell, NPC negotiation, dialogue, vendor resolution notifications/ # User notification helper + muteable types