feat(map): political zone types, road label tri-state, authoring snapping lib
- map-zones type gains territory and country border (informational political boundaries that never gate placement, default fills amber 0.1 / violet 0.06) - map-roads labelVisible is tri-state: unset means auto, where paved roads always label and dirt/trail only label at 4 m/px or finer (roadLabelShown) - migration clears the old implicit label default on non-paved roads - new pure snapping lib: findSnap (vertices own their neighborhood, edges must be twice as close), closestPointOnSegment, segmentMidpoints - tests for label angles, tri-state rule, snapping, political zones
This commit is contained in:
parent
9ca85fce9b
commit
cc0a7facd0
13 changed files with 649 additions and 164 deletions
|
|
@ -198,13 +198,13 @@ Labor/staffing simulation for structures. Collections in `src/collections/game/`
|
|||
|
||||
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).
|
||||
- **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 (tri-state: unset = auto, paved labels always, dirt/trail only at close zoom via `roadLabelShown` in style.ts, 4 m/px threshold), `points` JSON array of [x, y] meters), `map-zones` (name, description, type select water/land/territory/border (territory/border are informational political boundaries that never gate placement; default fills amber 0.1 / violet 0.06), 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`.
|
||||
- **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`, `roadLabelShown`, `labelScreenAngle`; roads under 600 screen px render one straight midpoint label instead of curved text, since glyphs fold on tiny wiggly paths).
|
||||
- **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). Drafts get vertex editing: draggable square handles per draft vertex (drag-end snaps), right-click deletes (min 2 road / 3 zone points), cyan midpoint handles insert a vertex; clicks and drags snap within 12 screen px to feature vertices and zone/road edges (closing segments included; vertices beat edges unless the edge is 2x closer) via pure `src/lib/map/snapping.ts` (`findSnap`, `closestPointOnSegment`, `segmentMidpoints`; `AuthoringSnapLayer` + `DraftHandles` in MapClient). `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"`.
|
||||
|
||||
|
|
|
|||
|
|
@ -100,10 +100,14 @@ stays display-only in the placement dialog (no research system exists).
|
|||
|
||||
- **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.
|
||||
(stroke color hex, width 1-10, dash style, name-label select: Auto/Shown/
|
||||
Hidden, where Auto labels paved roads always and dirt/trail roads only at
|
||||
close zoom, 4 m/px or finer); 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.
|
||||
(water/land/territory/border), and styling (fill color, edge color, fill
|
||||
opacity 0-1, edge width, name-label toggle); Save. Territory and Country
|
||||
Border are informational political boundaries (they never gate placement;
|
||||
only water does) and default to amber 0.1 / violet 0.06 fill.
|
||||
- **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
|
||||
|
|
@ -112,6 +116,20 @@ stays display-only in the placement dialog (no research system exists).
|
|||
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.
|
||||
- **Vertex editing**: while a road/zone/node draft is active, every draft
|
||||
vertex renders as a draggable square handle: drag to move it (drag-end
|
||||
snaps), right-click to delete it (roads keep 2+ points, zones 3+), and the
|
||||
small cyan square at the middle of each segment inserts a vertex there when
|
||||
clicked.
|
||||
- **Snapping**: while authoring, clicks and dragged vertices snap to existing
|
||||
geometry within 12 screen px: feature vertices (roads/zones/nodes/
|
||||
structures, including the draft's own other vertices for closing polygons)
|
||||
and edges (nearest point on any road segment or zone side, closing segment
|
||||
included) so a road can run exactly along a zone border or two zones can
|
||||
share an edge without gaps or overlap. Vertices beat edges near corners (an
|
||||
edge must be at least twice as close as the nearest vertex to win); a
|
||||
moving square preview shows where the click will land (emerald = vertex,
|
||||
cyan = edge).
|
||||
|
||||
### Labels and styling on the live map
|
||||
|
||||
|
|
|
|||
|
|
@ -96,10 +96,10 @@ export const MapRoads: CollectionConfig = {
|
|||
{
|
||||
name: "labelVisible",
|
||||
type: "checkbox",
|
||||
defaultValue: true,
|
||||
label: "Show Name Label",
|
||||
admin: {
|
||||
description: "Renders the road's name along the line at zoom-dependent intervals.",
|
||||
description:
|
||||
"Auto (unset): only paved roads show labels. Check to force on, uncheck to force off. The map authoring toolbar's Label select writes this.",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -48,10 +48,12 @@ export const MapZones: CollectionConfig = {
|
|||
options: [
|
||||
{ label: "Water", value: "water" },
|
||||
{ label: "Land", value: "land" },
|
||||
{ label: "Territory", value: "territory" },
|
||||
{ label: "Country Border", value: "border" },
|
||||
],
|
||||
admin: {
|
||||
description:
|
||||
"Water zones block land-blueprint placement. Land zones are informational; default terrain is buildable land.",
|
||||
"Water zones block land-blueprint placement. Land zones are informational; default terrain is buildable land. Territory and Country Border are political boundaries (informational).",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -76,7 +78,8 @@ export const MapZones: CollectionConfig = {
|
|||
min: 0,
|
||||
max: 1,
|
||||
admin: {
|
||||
description: "Fill opacity 0-1. Empty = default (0.25 water, 0.08 land).",
|
||||
description:
|
||||
"Fill opacity 0-1. Empty = default (0.25 water, 0.08 land, 0.1 territory, 0.06 border).",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -114,7 +117,8 @@ export const MapZones: CollectionConfig = {
|
|||
required: true,
|
||||
validate: validatePolygon,
|
||||
admin: {
|
||||
description: "Polygon ring as an array of [x, y] meter tuples (need not repeat the first point).",
|
||||
description:
|
||||
"Polygon ring as an array of [x, y] meter tuples (need not repeat the first point).",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export type BlueprintTerrain = "land" | "water";
|
|||
export interface ZoneInput {
|
||||
id: number;
|
||||
name?: string | null;
|
||||
type: "water" | "land";
|
||||
type: "water" | "land" | "territory" | "border";
|
||||
/** Resource ids contained in this zone; a site inside satisfies their requirements. */
|
||||
resources?: number[];
|
||||
points: MapPoint[];
|
||||
|
|
@ -112,8 +112,7 @@ export function checkResourcesInRange(
|
|||
node.resources.includes(resourceId) && distanceSquared(node.position, point) <= maxSq,
|
||||
);
|
||||
const zoneMatch = zones.some(
|
||||
(zone) =>
|
||||
(zone.resources ?? []).includes(resourceId) && pointInPolygon(point, zone.points),
|
||||
(zone) => (zone.resources ?? []).includes(resourceId) && pointInPolygon(point, zone.points),
|
||||
);
|
||||
if (!nodeMatch && !zoneMatch) {
|
||||
return {
|
||||
|
|
|
|||
95
src/lib/map/snapping.ts
Normal file
95
src/lib/map/snapping.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { pointDistance, type MapPoint } from "./points";
|
||||
|
||||
export type SnapKind = "vertex" | "edge";
|
||||
|
||||
export interface SnapTarget {
|
||||
key: string;
|
||||
vertices: MapPoint[];
|
||||
/** Polygon target: the closing segment (last -> first vertex) participates. */
|
||||
closed: boolean;
|
||||
/** Offer edge snapping (nearest point on each segment), not just vertices. */
|
||||
edges: boolean;
|
||||
}
|
||||
|
||||
export interface SnapMatch {
|
||||
point: MapPoint;
|
||||
kind: SnapKind;
|
||||
targetKey: string;
|
||||
/** Distance in meters from the cursor to the matched geometry. */
|
||||
distance: number;
|
||||
}
|
||||
|
||||
/** Nearest point to p on segment ab (projection clamped to the endpoints). */
|
||||
export function closestPointOnSegment(p: MapPoint, a: MapPoint, b: MapPoint): MapPoint {
|
||||
const abx = b[0] - a[0];
|
||||
const aby = b[1] - a[1];
|
||||
const lengthSq = abx * abx + aby * aby;
|
||||
if (lengthSq === 0) return [a[0], a[1]];
|
||||
const t = Math.max(0, Math.min(1, ((p[0] - a[0]) * abx + (p[1] - a[1]) * aby) / lengthSq));
|
||||
return [a[0] + abx * t, a[1] + aby * t];
|
||||
}
|
||||
|
||||
/**
|
||||
* Nearest snap within radiusMeters. Vertices own their neighborhood: an edge
|
||||
* only wins when it is at least twice as close as the nearest vertex (clicks
|
||||
* near a corner mean the corner, not one of the edges meeting there). Returns null when
|
||||
* nothing is in range.
|
||||
*/
|
||||
export function findSnap(
|
||||
point: MapPoint,
|
||||
targets: readonly SnapTarget[],
|
||||
radiusMeters: number,
|
||||
): SnapMatch | null {
|
||||
if (radiusMeters <= 0) return null;
|
||||
let bestVertex: SnapMatch | null = null;
|
||||
let bestEdge: SnapMatch | null = null;
|
||||
|
||||
for (const target of targets) {
|
||||
for (const vertex of target.vertices) {
|
||||
const distance = pointDistance(point, vertex);
|
||||
if (distance <= radiusMeters && (!bestVertex || distance < bestVertex.distance)) {
|
||||
bestVertex = {
|
||||
point: [vertex[0], vertex[1]],
|
||||
kind: "vertex",
|
||||
targetKey: target.key,
|
||||
distance,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!target.edges) continue;
|
||||
const last = target.closed ? target.vertices.length : target.vertices.length - 1;
|
||||
for (let i = 0; i < last; i++) {
|
||||
const a = target.vertices[i];
|
||||
const b = target.vertices[(i + 1) % target.vertices.length];
|
||||
const projected = closestPointOnSegment(point, a, b);
|
||||
const distance = pointDistance(point, projected);
|
||||
if (distance <= radiusMeters && (!bestEdge || distance < bestEdge.distance)) {
|
||||
bestEdge = { point: projected, kind: "edge", targetKey: target.key, distance };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bestVertex && (!bestEdge || bestEdge.distance >= bestVertex.distance * 0.5)) {
|
||||
return bestVertex;
|
||||
}
|
||||
return bestEdge ?? bestVertex;
|
||||
}
|
||||
|
||||
export interface SegmentMidpoint {
|
||||
/** Insert position in the point array: i + 1 (appends for the closing segment). */
|
||||
index: number;
|
||||
point: MapPoint;
|
||||
}
|
||||
|
||||
/** Midpoint of every segment, for click-to-insert vertex handles. */
|
||||
export function segmentMidpoints(points: MapPoint[], closed: boolean): SegmentMidpoint[] {
|
||||
const midpoints: SegmentMidpoint[] = [];
|
||||
if (points.length < 2) return midpoints;
|
||||
const segments = closed ? points.length : points.length - 1;
|
||||
for (let i = 0; i < segments; i++) {
|
||||
const a = points[i];
|
||||
const b = points[(i + 1) % points.length];
|
||||
midpoints.push({ index: i + 1, point: [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2] });
|
||||
}
|
||||
return midpoints;
|
||||
}
|
||||
|
|
@ -147,3 +147,37 @@ function segmentAngleDeg(from: MapPoint, to: MapPoint): number {
|
|||
if (dx === 0 && dy === 0) return 0;
|
||||
return (Math.atan2(dy, dx) * 180) / Math.PI;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS rotation for a straight label running along the from->to direction:
|
||||
* normalized into [-90, 90] (never upside down) with vertical text reading
|
||||
* bottom-to-top.
|
||||
*/
|
||||
export function labelScreenAngle(from: MapPoint, to: MapPoint): number {
|
||||
const angle = segmentAngleDeg(from, to);
|
||||
if (angle > 90) return angle - 180;
|
||||
if (angle < -90) return angle + 180;
|
||||
if (angle === 90) return -90;
|
||||
return angle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minor roads (dirt/trail) only carry a name label this close to the ground,
|
||||
* in meters per pixel (zoom >= 3 on an 8192 m world).
|
||||
*/
|
||||
export const MINOR_ROAD_LABEL_MAX_M_PER_PX = 4;
|
||||
|
||||
/**
|
||||
* Tri-state road label rule: an explicit checkbox value wins; unset means
|
||||
* auto, where paved roads always label and dirt/trail roads label only at
|
||||
* close zoom (metersPerPx at or below MINOR_ROAD_LABEL_MAX_M_PER_PX; pass
|
||||
* null/undefined to evaluate the zoom-independent part).
|
||||
*/
|
||||
export function roadLabelShown(
|
||||
road: { labelVisible?: boolean | null; surface: string },
|
||||
metersPerPx?: number | null,
|
||||
): boolean {
|
||||
if (road.labelVisible != null) return road.labelVisible;
|
||||
if (road.surface === "paved") return true;
|
||||
return metersPerPx != null && metersPerPx <= MINOR_ROAD_LABEL_MAX_M_PER_PX;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export interface ZoneFeature {
|
|||
id: number;
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
type: "water" | "land";
|
||||
type: "water" | "land" | "territory" | "border";
|
||||
resources: { id: number; name: string }[];
|
||||
fillColor: string | null;
|
||||
strokeColor: string | null;
|
||||
|
|
|
|||
29
src/migrations/20260917_112258_add_zone_political_types.ts
Normal file
29
src/migrations/20260917_112258_add_zone_political_types.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { MigrateUpArgs, MigrateDownArgs, sql } from "@payloadcms/db-postgres";
|
||||
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
// Political-boundary zone types (Postgres 12+ allows ADD VALUE inside the
|
||||
// migration's transaction as long as the new values are not used in it).
|
||||
await db.execute(sql`
|
||||
ALTER TYPE "enum_map_zones_type" ADD VALUE IF NOT EXISTS 'territory';
|
||||
`);
|
||||
await db.execute(sql`
|
||||
ALTER TYPE "enum_map_zones_type" ADD VALUE IF NOT EXISTS 'border';
|
||||
`);
|
||||
|
||||
// Road labels become tri-state: NULL = auto (paved only). Existing rows all
|
||||
// carry the old column default (true), which was never a deliberate choice,
|
||||
// so clear it on non-paved roads to make the paved-only default real.
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "map_roads" ALTER COLUMN "label_visible" DROP DEFAULT;
|
||||
`);
|
||||
await db.execute(sql`
|
||||
UPDATE "map_roads" SET "label_visible" = NULL WHERE "surface" IS DISTINCT FROM 'paved';
|
||||
`);
|
||||
}
|
||||
|
||||
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||
// Enum values cannot be removed in Postgres; restore the old road default.
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "map_roads" ALTER COLUMN "label_visible" SET DEFAULT true;
|
||||
`);
|
||||
}
|
||||
|
|
@ -1,399 +1,405 @@
|
|||
import * as migration_20260820_004157 from './20260820_004157';
|
||||
import * as migration_20260825_220000 from './20260825_220000';
|
||||
import * as migration_20260826_050000_repair_shims_auxiliary_tables from './20260826_050000_repair_shims_auxiliary_tables';
|
||||
import * as migration_20260826_060000_add_game_servers_event_target from './20260826_060000_add_game_servers_event_target';
|
||||
import * as migration_20260827_005757 from './20260827_005757';
|
||||
import * as migration_20260827_022334_add_eod_minesweeper_best_flags from './20260827_022334_add_eod_minesweeper_best_flags';
|
||||
import * as migration_20260827_213857_add_evaluations_collection from './20260827_213857_add_evaluations_collection';
|
||||
import * as migration_20260827_233224_add_mission_completed_at from './20260827_233224_add_mission_completed_at';
|
||||
import * as migration_20260828_011500_add_background_texture_preference from './20260828_011500_add_background_texture_preference';
|
||||
import * as migration_20260828_041615_add_campaign_mode from './20260828_041615_add_campaign_mode';
|
||||
import * as migration_20260828_044506 from './20260828_044506';
|
||||
import * as migration_20260831_142503 from './20260831_142503';
|
||||
import * as migration_20260831_180233_assignment_transfers from './20260831_180233_assignment_transfers';
|
||||
import * as migration_20260901_014006_add_assignment_transfer_event_target from './20260901_014006_add_assignment_transfer_event_target';
|
||||
import * as migration_20260901_213108_add_ticket_votes_and_edit_history from './20260901_213108_add_ticket_votes_and_edit_history';
|
||||
import * as migration_20260901_222200_add_admin_page_permissions from './20260901_222200_add_admin_page_permissions';
|
||||
import * as migration_20260902_195336_add_medal_backgrounds from './20260902_195336_add_medal_backgrounds';
|
||||
import * as migration_20260903_035544 from './20260903_035544';
|
||||
import * as migration_20260903_044047 from './20260903_044047';
|
||||
import * as migration_20260903_053419 from './20260903_053419';
|
||||
import * as migration_20260903_060000_add_radio_mode_stats from './20260903_060000_add_radio_mode_stats';
|
||||
import * as migration_20260903_061000_add_radio_incorrect_counts from './20260903_061000_add_radio_incorrect_counts';
|
||||
import * as migration_20260903_062000_add_aim_trainer_stats from './20260903_062000_add_aim_trainer_stats';
|
||||
import * as migration_20260904_230838_add_wiki_collections from './20260904_230838_add_wiki_collections';
|
||||
import * as migration_20260905_183940_base_management from './20260905_183940_base_management';
|
||||
import * as migration_20260905_195456_labor_headcount from './20260905_195456_labor_headcount';
|
||||
import * as migration_20260905_210310_labor_classifications from './20260905_210310_labor_classifications';
|
||||
import * as migration_20260905_213824 from './20260905_213824';
|
||||
import * as migration_20260905_215658 from './20260905_215658';
|
||||
import * as migration_20260906_045502 from './20260906_045502';
|
||||
import * as migration_20260906_050900_add_evaluation_reminders_field from './20260906_050900_add_evaluation_reminders_field';
|
||||
import * as migration_20260906_061451_add_wiki_template_css from './20260906_061451_add_wiki_template_css';
|
||||
import * as migration_20260906_063753_add_personal_excerpt_richtext from './20260906_063753_add_personal_excerpt_richtext';
|
||||
import * as migration_20260906_065405_add_wiki_template_html from './20260906_065405_add_wiki_template_html';
|
||||
import * as migration_20260906_100908_add_market_economy_state from './20260906_100908_add_market_economy_state';
|
||||
import * as migration_20260906_165837_add_market_state_event_target from './20260906_165837_add_market_state_event_target';
|
||||
import * as migration_20260906_202925_add_voice_submissions from './20260906_202925_add_voice_submissions';
|
||||
import * as migration_20260907_012326_add_missions_created_by from './20260907_012326_add_missions_created_by';
|
||||
import * as migration_20260907_013724_make_missions_campaign_optional from './20260907_013724_make_missions_campaign_optional';
|
||||
import * as migration_20260907_041051_add_voice_script_spoiler from './20260907_041051_add_voice_script_spoiler';
|
||||
import * as migration_20260907_043339_add_voice_script_description from './20260907_043339_add_voice_script_description';
|
||||
import * as migration_20260907_044934_add_missions_discord_modlist_hash from './20260907_044934_add_missions_discord_modlist_hash';
|
||||
import * as migration_20260907_083010_add_voice_submission_description from './20260907_083010_add_voice_submission_description';
|
||||
import * as migration_20260907_090245_add_mission_ready_reminder_fields from './20260907_090245_add_mission_ready_reminder_fields';
|
||||
import * as migration_20260907_120754_add_story_beats from './20260907_120754_add_story_beats';
|
||||
import * as migration_20260907_124024_convert_story_beats_body_to_lexical from './20260907_124024_convert_story_beats_body_to_lexical';
|
||||
import * as migration_20260908_152021_add_opord_recall_minigame_stats from './20260908_152021_add_opord_recall_minigame_stats';
|
||||
import * as migration_20260908_172241_add_custom_minigame_collections from './20260908_172241_add_custom_minigame_collections';
|
||||
import * as migration_20260908_193107_add_creator_xp_bank_field from './20260908_193107_add_creator_xp_bank_field';
|
||||
import * as migration_20260908_194435_add_expose_edge_mines_field from './20260908_194435_add_expose_edge_mines_field';
|
||||
import * as migration_20260909_153427_add_opord_recall_best_times from './20260909_153427_add_opord_recall_best_times';
|
||||
import * as migration_20260909_223955_add_voice_script_draft_status from './20260909_223955_add_voice_script_draft_status';
|
||||
import * as migration_20260910_221018_make_missions_codename_optional from './20260910_221018_make_missions_codename_optional';
|
||||
import * as migration_20260910_222518_add_dashboard_layout_preference from './20260910_222518_add_dashboard_layout_preference';
|
||||
import * as migration_20260911_044754_add_poker_minigame_stats from './20260911_044754_add_poker_minigame_stats';
|
||||
import * as migration_20260911_052147_add_ribbon_submissions from './20260911_052147_add_ribbon_submissions';
|
||||
import * as migration_20260911_201559_add_award_advancement_series from './20260911_201559_add_award_advancement_series';
|
||||
import * as migration_20260912_051752_add_rank_promotion_ceiling from './20260912_051752_add_rank_promotion_ceiling';
|
||||
import * as migration_20260912_071740_add_blackjack_minigame_stats from './20260912_071740_add_blackjack_minigame_stats';
|
||||
import * as migration_20260912_231219_add_discord_cancelled_at from './20260912_231219_add_discord_cancelled_at';
|
||||
import * as migration_20260914_153046_add_chip_bank from './20260914_153046_add_chip_bank';
|
||||
import * as migration_20260914_171412_add_respawn_tickets from './20260914_171412_add_respawn_tickets';
|
||||
import * as migration_20260916_083158_add_game_map_system from './20260916_083158_add_game_map_system';
|
||||
import * as migration_20260916_185544_construction_timestamp_model from './20260916_185544_construction_timestamp_model';
|
||||
import * as migration_20260916_220224_add_map_labels_and_styling from './20260916_220224_add_map_labels_and_styling';
|
||||
import * as migration_20260917_023028_add_node_and_zone_resources from './20260917_023028_add_node_and_zone_resources';
|
||||
import * as migration_20260820_004157 from "./20260820_004157";
|
||||
import * as migration_20260825_220000 from "./20260825_220000";
|
||||
import * as migration_20260826_050000_repair_shims_auxiliary_tables from "./20260826_050000_repair_shims_auxiliary_tables";
|
||||
import * as migration_20260826_060000_add_game_servers_event_target from "./20260826_060000_add_game_servers_event_target";
|
||||
import * as migration_20260827_005757 from "./20260827_005757";
|
||||
import * as migration_20260827_022334_add_eod_minesweeper_best_flags from "./20260827_022334_add_eod_minesweeper_best_flags";
|
||||
import * as migration_20260827_213857_add_evaluations_collection from "./20260827_213857_add_evaluations_collection";
|
||||
import * as migration_20260827_233224_add_mission_completed_at from "./20260827_233224_add_mission_completed_at";
|
||||
import * as migration_20260828_011500_add_background_texture_preference from "./20260828_011500_add_background_texture_preference";
|
||||
import * as migration_20260828_041615_add_campaign_mode from "./20260828_041615_add_campaign_mode";
|
||||
import * as migration_20260828_044506 from "./20260828_044506";
|
||||
import * as migration_20260831_142503 from "./20260831_142503";
|
||||
import * as migration_20260831_180233_assignment_transfers from "./20260831_180233_assignment_transfers";
|
||||
import * as migration_20260901_014006_add_assignment_transfer_event_target from "./20260901_014006_add_assignment_transfer_event_target";
|
||||
import * as migration_20260901_213108_add_ticket_votes_and_edit_history from "./20260901_213108_add_ticket_votes_and_edit_history";
|
||||
import * as migration_20260901_222200_add_admin_page_permissions from "./20260901_222200_add_admin_page_permissions";
|
||||
import * as migration_20260902_195336_add_medal_backgrounds from "./20260902_195336_add_medal_backgrounds";
|
||||
import * as migration_20260903_035544 from "./20260903_035544";
|
||||
import * as migration_20260903_044047 from "./20260903_044047";
|
||||
import * as migration_20260903_053419 from "./20260903_053419";
|
||||
import * as migration_20260903_060000_add_radio_mode_stats from "./20260903_060000_add_radio_mode_stats";
|
||||
import * as migration_20260903_061000_add_radio_incorrect_counts from "./20260903_061000_add_radio_incorrect_counts";
|
||||
import * as migration_20260903_062000_add_aim_trainer_stats from "./20260903_062000_add_aim_trainer_stats";
|
||||
import * as migration_20260904_230838_add_wiki_collections from "./20260904_230838_add_wiki_collections";
|
||||
import * as migration_20260905_183940_base_management from "./20260905_183940_base_management";
|
||||
import * as migration_20260905_195456_labor_headcount from "./20260905_195456_labor_headcount";
|
||||
import * as migration_20260905_210310_labor_classifications from "./20260905_210310_labor_classifications";
|
||||
import * as migration_20260905_213824 from "./20260905_213824";
|
||||
import * as migration_20260905_215658 from "./20260905_215658";
|
||||
import * as migration_20260906_045502 from "./20260906_045502";
|
||||
import * as migration_20260906_050900_add_evaluation_reminders_field from "./20260906_050900_add_evaluation_reminders_field";
|
||||
import * as migration_20260906_061451_add_wiki_template_css from "./20260906_061451_add_wiki_template_css";
|
||||
import * as migration_20260906_063753_add_personal_excerpt_richtext from "./20260906_063753_add_personal_excerpt_richtext";
|
||||
import * as migration_20260906_065405_add_wiki_template_html from "./20260906_065405_add_wiki_template_html";
|
||||
import * as migration_20260906_100908_add_market_economy_state from "./20260906_100908_add_market_economy_state";
|
||||
import * as migration_20260906_165837_add_market_state_event_target from "./20260906_165837_add_market_state_event_target";
|
||||
import * as migration_20260906_202925_add_voice_submissions from "./20260906_202925_add_voice_submissions";
|
||||
import * as migration_20260907_012326_add_missions_created_by from "./20260907_012326_add_missions_created_by";
|
||||
import * as migration_20260907_013724_make_missions_campaign_optional from "./20260907_013724_make_missions_campaign_optional";
|
||||
import * as migration_20260907_041051_add_voice_script_spoiler from "./20260907_041051_add_voice_script_spoiler";
|
||||
import * as migration_20260907_043339_add_voice_script_description from "./20260907_043339_add_voice_script_description";
|
||||
import * as migration_20260907_044934_add_missions_discord_modlist_hash from "./20260907_044934_add_missions_discord_modlist_hash";
|
||||
import * as migration_20260907_083010_add_voice_submission_description from "./20260907_083010_add_voice_submission_description";
|
||||
import * as migration_20260907_090245_add_mission_ready_reminder_fields from "./20260907_090245_add_mission_ready_reminder_fields";
|
||||
import * as migration_20260907_120754_add_story_beats from "./20260907_120754_add_story_beats";
|
||||
import * as migration_20260907_124024_convert_story_beats_body_to_lexical from "./20260907_124024_convert_story_beats_body_to_lexical";
|
||||
import * as migration_20260908_152021_add_opord_recall_minigame_stats from "./20260908_152021_add_opord_recall_minigame_stats";
|
||||
import * as migration_20260908_172241_add_custom_minigame_collections from "./20260908_172241_add_custom_minigame_collections";
|
||||
import * as migration_20260908_193107_add_creator_xp_bank_field from "./20260908_193107_add_creator_xp_bank_field";
|
||||
import * as migration_20260908_194435_add_expose_edge_mines_field from "./20260908_194435_add_expose_edge_mines_field";
|
||||
import * as migration_20260909_153427_add_opord_recall_best_times from "./20260909_153427_add_opord_recall_best_times";
|
||||
import * as migration_20260909_223955_add_voice_script_draft_status from "./20260909_223955_add_voice_script_draft_status";
|
||||
import * as migration_20260910_221018_make_missions_codename_optional from "./20260910_221018_make_missions_codename_optional";
|
||||
import * as migration_20260910_222518_add_dashboard_layout_preference from "./20260910_222518_add_dashboard_layout_preference";
|
||||
import * as migration_20260911_044754_add_poker_minigame_stats from "./20260911_044754_add_poker_minigame_stats";
|
||||
import * as migration_20260911_052147_add_ribbon_submissions from "./20260911_052147_add_ribbon_submissions";
|
||||
import * as migration_20260911_201559_add_award_advancement_series from "./20260911_201559_add_award_advancement_series";
|
||||
import * as migration_20260912_051752_add_rank_promotion_ceiling from "./20260912_051752_add_rank_promotion_ceiling";
|
||||
import * as migration_20260912_071740_add_blackjack_minigame_stats from "./20260912_071740_add_blackjack_minigame_stats";
|
||||
import * as migration_20260912_231219_add_discord_cancelled_at from "./20260912_231219_add_discord_cancelled_at";
|
||||
import * as migration_20260914_153046_add_chip_bank from "./20260914_153046_add_chip_bank";
|
||||
import * as migration_20260914_171412_add_respawn_tickets from "./20260914_171412_add_respawn_tickets";
|
||||
import * as migration_20260916_083158_add_game_map_system from "./20260916_083158_add_game_map_system";
|
||||
import * as migration_20260916_185544_construction_timestamp_model from "./20260916_185544_construction_timestamp_model";
|
||||
import * as migration_20260916_220224_add_map_labels_and_styling from "./20260916_220224_add_map_labels_and_styling";
|
||||
import * as migration_20260917_023028_add_node_and_zone_resources from "./20260917_023028_add_node_and_zone_resources";
|
||||
import * as migration_20260917_112258_add_zone_political_types from "./20260917_112258_add_zone_political_types";
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
up: migration_20260820_004157.up,
|
||||
down: migration_20260820_004157.down,
|
||||
name: '20260820_004157',
|
||||
name: "20260820_004157",
|
||||
},
|
||||
{
|
||||
up: migration_20260825_220000.up,
|
||||
down: migration_20260825_220000.down,
|
||||
name: '20260825_220000',
|
||||
name: "20260825_220000",
|
||||
},
|
||||
{
|
||||
up: migration_20260826_050000_repair_shims_auxiliary_tables.up,
|
||||
down: migration_20260826_050000_repair_shims_auxiliary_tables.down,
|
||||
name: '20260826_050000_repair_shims_auxiliary_tables',
|
||||
name: "20260826_050000_repair_shims_auxiliary_tables",
|
||||
},
|
||||
{
|
||||
up: migration_20260826_060000_add_game_servers_event_target.up,
|
||||
down: migration_20260826_060000_add_game_servers_event_target.down,
|
||||
name: '20260826_060000_add_game_servers_event_target',
|
||||
name: "20260826_060000_add_game_servers_event_target",
|
||||
},
|
||||
{
|
||||
up: migration_20260827_005757.up,
|
||||
down: migration_20260827_005757.down,
|
||||
name: '20260827_005757',
|
||||
name: "20260827_005757",
|
||||
},
|
||||
{
|
||||
up: migration_20260827_022334_add_eod_minesweeper_best_flags.up,
|
||||
down: migration_20260827_022334_add_eod_minesweeper_best_flags.down,
|
||||
name: '20260827_022334_add_eod_minesweeper_best_flags',
|
||||
name: "20260827_022334_add_eod_minesweeper_best_flags",
|
||||
},
|
||||
{
|
||||
up: migration_20260827_213857_add_evaluations_collection.up,
|
||||
down: migration_20260827_213857_add_evaluations_collection.down,
|
||||
name: '20260827_213857_add_evaluations_collection',
|
||||
name: "20260827_213857_add_evaluations_collection",
|
||||
},
|
||||
{
|
||||
up: migration_20260827_233224_add_mission_completed_at.up,
|
||||
down: migration_20260827_233224_add_mission_completed_at.down,
|
||||
name: '20260827_233224_add_mission_completed_at',
|
||||
name: "20260827_233224_add_mission_completed_at",
|
||||
},
|
||||
{
|
||||
up: migration_20260828_011500_add_background_texture_preference.up,
|
||||
down: migration_20260828_011500_add_background_texture_preference.down,
|
||||
name: '20260828_011500_add_background_texture_preference',
|
||||
name: "20260828_011500_add_background_texture_preference",
|
||||
},
|
||||
{
|
||||
up: migration_20260828_041615_add_campaign_mode.up,
|
||||
down: migration_20260828_041615_add_campaign_mode.down,
|
||||
name: '20260828_041615_add_campaign_mode',
|
||||
name: "20260828_041615_add_campaign_mode",
|
||||
},
|
||||
{
|
||||
up: migration_20260828_044506.up,
|
||||
down: migration_20260828_044506.down,
|
||||
name: '20260828_044506',
|
||||
name: "20260828_044506",
|
||||
},
|
||||
{
|
||||
up: migration_20260831_142503.up,
|
||||
down: migration_20260831_142503.down,
|
||||
name: '20260831_142503',
|
||||
name: "20260831_142503",
|
||||
},
|
||||
{
|
||||
up: migration_20260831_180233_assignment_transfers.up,
|
||||
down: migration_20260831_180233_assignment_transfers.down,
|
||||
name: '20260831_180233_assignment_transfers',
|
||||
name: "20260831_180233_assignment_transfers",
|
||||
},
|
||||
{
|
||||
up: migration_20260901_014006_add_assignment_transfer_event_target.up,
|
||||
down: migration_20260901_014006_add_assignment_transfer_event_target.down,
|
||||
name: '20260901_014006_add_assignment_transfer_event_target',
|
||||
name: "20260901_014006_add_assignment_transfer_event_target",
|
||||
},
|
||||
{
|
||||
up: migration_20260901_213108_add_ticket_votes_and_edit_history.up,
|
||||
down: migration_20260901_213108_add_ticket_votes_and_edit_history.down,
|
||||
name: '20260901_213108_add_ticket_votes_and_edit_history',
|
||||
name: "20260901_213108_add_ticket_votes_and_edit_history",
|
||||
},
|
||||
{
|
||||
up: migration_20260901_222200_add_admin_page_permissions.up,
|
||||
down: migration_20260901_222200_add_admin_page_permissions.down,
|
||||
name: '20260901_222200_add_admin_page_permissions',
|
||||
name: "20260901_222200_add_admin_page_permissions",
|
||||
},
|
||||
{
|
||||
up: migration_20260902_195336_add_medal_backgrounds.up,
|
||||
down: migration_20260902_195336_add_medal_backgrounds.down,
|
||||
name: '20260902_195336_add_medal_backgrounds',
|
||||
name: "20260902_195336_add_medal_backgrounds",
|
||||
},
|
||||
{
|
||||
up: migration_20260903_035544.up,
|
||||
down: migration_20260903_035544.down,
|
||||
name: '20260903_035544',
|
||||
name: "20260903_035544",
|
||||
},
|
||||
{
|
||||
up: migration_20260903_044047.up,
|
||||
down: migration_20260903_044047.down,
|
||||
name: '20260903_044047',
|
||||
name: "20260903_044047",
|
||||
},
|
||||
{
|
||||
up: migration_20260903_053419.up,
|
||||
down: migration_20260903_053419.down,
|
||||
name: '20260903_053419',
|
||||
name: "20260903_053419",
|
||||
},
|
||||
{
|
||||
up: migration_20260903_060000_add_radio_mode_stats.up,
|
||||
down: migration_20260903_060000_add_radio_mode_stats.down,
|
||||
name: '20260903_060000_add_radio_mode_stats',
|
||||
name: "20260903_060000_add_radio_mode_stats",
|
||||
},
|
||||
{
|
||||
up: migration_20260903_061000_add_radio_incorrect_counts.up,
|
||||
down: migration_20260903_061000_add_radio_incorrect_counts.down,
|
||||
name: '20260903_061000_add_radio_incorrect_counts',
|
||||
name: "20260903_061000_add_radio_incorrect_counts",
|
||||
},
|
||||
{
|
||||
up: migration_20260903_062000_add_aim_trainer_stats.up,
|
||||
down: migration_20260903_062000_add_aim_trainer_stats.down,
|
||||
name: '20260903_062000_add_aim_trainer_stats',
|
||||
name: "20260903_062000_add_aim_trainer_stats",
|
||||
},
|
||||
{
|
||||
up: migration_20260904_230838_add_wiki_collections.up,
|
||||
down: migration_20260904_230838_add_wiki_collections.down,
|
||||
name: '20260904_230838_add_wiki_collections',
|
||||
name: "20260904_230838_add_wiki_collections",
|
||||
},
|
||||
{
|
||||
up: migration_20260905_183940_base_management.up,
|
||||
down: migration_20260905_183940_base_management.down,
|
||||
name: '20260905_183940_base_management',
|
||||
name: "20260905_183940_base_management",
|
||||
},
|
||||
{
|
||||
up: migration_20260905_195456_labor_headcount.up,
|
||||
down: migration_20260905_195456_labor_headcount.down,
|
||||
name: '20260905_195456_labor_headcount',
|
||||
name: "20260905_195456_labor_headcount",
|
||||
},
|
||||
{
|
||||
up: migration_20260905_210310_labor_classifications.up,
|
||||
down: migration_20260905_210310_labor_classifications.down,
|
||||
name: '20260905_210310_labor_classifications',
|
||||
name: "20260905_210310_labor_classifications",
|
||||
},
|
||||
{
|
||||
up: migration_20260905_213824.up,
|
||||
down: migration_20260905_213824.down,
|
||||
name: '20260905_213824',
|
||||
name: "20260905_213824",
|
||||
},
|
||||
{
|
||||
up: migration_20260905_215658.up,
|
||||
down: migration_20260905_215658.down,
|
||||
name: '20260905_215658',
|
||||
name: "20260905_215658",
|
||||
},
|
||||
{
|
||||
up: migration_20260906_045502.up,
|
||||
down: migration_20260906_045502.down,
|
||||
name: '20260906_045502',
|
||||
name: "20260906_045502",
|
||||
},
|
||||
{
|
||||
up: migration_20260906_050900_add_evaluation_reminders_field.up,
|
||||
down: migration_20260906_050900_add_evaluation_reminders_field.down,
|
||||
name: '20260906_050900_add_evaluation_reminders_field',
|
||||
name: "20260906_050900_add_evaluation_reminders_field",
|
||||
},
|
||||
{
|
||||
up: migration_20260906_061451_add_wiki_template_css.up,
|
||||
down: migration_20260906_061451_add_wiki_template_css.down,
|
||||
name: '20260906_061451_add_wiki_template_css',
|
||||
name: "20260906_061451_add_wiki_template_css",
|
||||
},
|
||||
{
|
||||
up: migration_20260906_063753_add_personal_excerpt_richtext.up,
|
||||
down: migration_20260906_063753_add_personal_excerpt_richtext.down,
|
||||
name: '20260906_063753_add_personal_excerpt_richtext',
|
||||
name: "20260906_063753_add_personal_excerpt_richtext",
|
||||
},
|
||||
{
|
||||
up: migration_20260906_065405_add_wiki_template_html.up,
|
||||
down: migration_20260906_065405_add_wiki_template_html.down,
|
||||
name: '20260906_065405_add_wiki_template_html',
|
||||
name: "20260906_065405_add_wiki_template_html",
|
||||
},
|
||||
{
|
||||
up: migration_20260906_100908_add_market_economy_state.up,
|
||||
down: migration_20260906_100908_add_market_economy_state.down,
|
||||
name: '20260906_100908_add_market_economy_state',
|
||||
name: "20260906_100908_add_market_economy_state",
|
||||
},
|
||||
{
|
||||
up: migration_20260906_165837_add_market_state_event_target.up,
|
||||
down: migration_20260906_165837_add_market_state_event_target.down,
|
||||
name: '20260906_165837_add_market_state_event_target',
|
||||
name: "20260906_165837_add_market_state_event_target",
|
||||
},
|
||||
{
|
||||
up: migration_20260906_202925_add_voice_submissions.up,
|
||||
down: migration_20260906_202925_add_voice_submissions.down,
|
||||
name: '20260906_202925_add_voice_submissions',
|
||||
name: "20260906_202925_add_voice_submissions",
|
||||
},
|
||||
{
|
||||
up: migration_20260907_012326_add_missions_created_by.up,
|
||||
down: migration_20260907_012326_add_missions_created_by.down,
|
||||
name: '20260907_012326_add_missions_created_by',
|
||||
name: "20260907_012326_add_missions_created_by",
|
||||
},
|
||||
{
|
||||
up: migration_20260907_013724_make_missions_campaign_optional.up,
|
||||
down: migration_20260907_013724_make_missions_campaign_optional.down,
|
||||
name: '20260907_013724_make_missions_campaign_optional',
|
||||
name: "20260907_013724_make_missions_campaign_optional",
|
||||
},
|
||||
{
|
||||
up: migration_20260907_041051_add_voice_script_spoiler.up,
|
||||
down: migration_20260907_041051_add_voice_script_spoiler.down,
|
||||
name: '20260907_041051_add_voice_script_spoiler',
|
||||
name: "20260907_041051_add_voice_script_spoiler",
|
||||
},
|
||||
{
|
||||
up: migration_20260907_043339_add_voice_script_description.up,
|
||||
down: migration_20260907_043339_add_voice_script_description.down,
|
||||
name: '20260907_043339_add_voice_script_description',
|
||||
name: "20260907_043339_add_voice_script_description",
|
||||
},
|
||||
{
|
||||
up: migration_20260907_044934_add_missions_discord_modlist_hash.up,
|
||||
down: migration_20260907_044934_add_missions_discord_modlist_hash.down,
|
||||
name: '20260907_044934_add_missions_discord_modlist_hash',
|
||||
name: "20260907_044934_add_missions_discord_modlist_hash",
|
||||
},
|
||||
{
|
||||
up: migration_20260907_083010_add_voice_submission_description.up,
|
||||
down: migration_20260907_083010_add_voice_submission_description.down,
|
||||
name: '20260907_083010_add_voice_submission_description',
|
||||
name: "20260907_083010_add_voice_submission_description",
|
||||
},
|
||||
{
|
||||
up: migration_20260907_090245_add_mission_ready_reminder_fields.up,
|
||||
down: migration_20260907_090245_add_mission_ready_reminder_fields.down,
|
||||
name: '20260907_090245_add_mission_ready_reminder_fields',
|
||||
name: "20260907_090245_add_mission_ready_reminder_fields",
|
||||
},
|
||||
{
|
||||
up: migration_20260907_120754_add_story_beats.up,
|
||||
down: migration_20260907_120754_add_story_beats.down,
|
||||
name: '20260907_120754_add_story_beats',
|
||||
name: "20260907_120754_add_story_beats",
|
||||
},
|
||||
{
|
||||
up: migration_20260907_124024_convert_story_beats_body_to_lexical.up,
|
||||
down: migration_20260907_124024_convert_story_beats_body_to_lexical.down,
|
||||
name: '20260907_124024_convert_story_beats_body_to_lexical',
|
||||
name: "20260907_124024_convert_story_beats_body_to_lexical",
|
||||
},
|
||||
{
|
||||
up: migration_20260908_152021_add_opord_recall_minigame_stats.up,
|
||||
down: migration_20260908_152021_add_opord_recall_minigame_stats.down,
|
||||
name: '20260908_152021_add_opord_recall_minigame_stats',
|
||||
name: "20260908_152021_add_opord_recall_minigame_stats",
|
||||
},
|
||||
{
|
||||
up: migration_20260908_172241_add_custom_minigame_collections.up,
|
||||
down: migration_20260908_172241_add_custom_minigame_collections.down,
|
||||
name: '20260908_172241_add_custom_minigame_collections',
|
||||
name: "20260908_172241_add_custom_minigame_collections",
|
||||
},
|
||||
{
|
||||
up: migration_20260908_193107_add_creator_xp_bank_field.up,
|
||||
down: migration_20260908_193107_add_creator_xp_bank_field.down,
|
||||
name: '20260908_193107_add_creator_xp_bank_field',
|
||||
name: "20260908_193107_add_creator_xp_bank_field",
|
||||
},
|
||||
{
|
||||
up: migration_20260908_194435_add_expose_edge_mines_field.up,
|
||||
down: migration_20260908_194435_add_expose_edge_mines_field.down,
|
||||
name: '20260908_194435_add_expose_edge_mines_field',
|
||||
name: "20260908_194435_add_expose_edge_mines_field",
|
||||
},
|
||||
{
|
||||
up: migration_20260909_153427_add_opord_recall_best_times.up,
|
||||
down: migration_20260909_153427_add_opord_recall_best_times.down,
|
||||
name: '20260909_153427_add_opord_recall_best_times',
|
||||
name: "20260909_153427_add_opord_recall_best_times",
|
||||
},
|
||||
{
|
||||
up: migration_20260909_223955_add_voice_script_draft_status.up,
|
||||
down: migration_20260909_223955_add_voice_script_draft_status.down,
|
||||
name: '20260909_223955_add_voice_script_draft_status',
|
||||
name: "20260909_223955_add_voice_script_draft_status",
|
||||
},
|
||||
{
|
||||
up: migration_20260910_221018_make_missions_codename_optional.up,
|
||||
down: migration_20260910_221018_make_missions_codename_optional.down,
|
||||
name: '20260910_221018_make_missions_codename_optional',
|
||||
name: "20260910_221018_make_missions_codename_optional",
|
||||
},
|
||||
{
|
||||
up: migration_20260910_222518_add_dashboard_layout_preference.up,
|
||||
down: migration_20260910_222518_add_dashboard_layout_preference.down,
|
||||
name: '20260910_222518_add_dashboard_layout_preference',
|
||||
name: "20260910_222518_add_dashboard_layout_preference",
|
||||
},
|
||||
{
|
||||
up: migration_20260911_044754_add_poker_minigame_stats.up,
|
||||
down: migration_20260911_044754_add_poker_minigame_stats.down,
|
||||
name: '20260911_044754_add_poker_minigame_stats',
|
||||
name: "20260911_044754_add_poker_minigame_stats",
|
||||
},
|
||||
{
|
||||
up: migration_20260911_052147_add_ribbon_submissions.up,
|
||||
down: migration_20260911_052147_add_ribbon_submissions.down,
|
||||
name: '20260911_052147_add_ribbon_submissions',
|
||||
name: "20260911_052147_add_ribbon_submissions",
|
||||
},
|
||||
{
|
||||
up: migration_20260911_201559_add_award_advancement_series.up,
|
||||
down: migration_20260911_201559_add_award_advancement_series.down,
|
||||
name: '20260911_201559_add_award_advancement_series',
|
||||
name: "20260911_201559_add_award_advancement_series",
|
||||
},
|
||||
{
|
||||
up: migration_20260912_051752_add_rank_promotion_ceiling.up,
|
||||
down: migration_20260912_051752_add_rank_promotion_ceiling.down,
|
||||
name: '20260912_051752_add_rank_promotion_ceiling',
|
||||
name: "20260912_051752_add_rank_promotion_ceiling",
|
||||
},
|
||||
{
|
||||
up: migration_20260912_071740_add_blackjack_minigame_stats.up,
|
||||
down: migration_20260912_071740_add_blackjack_minigame_stats.down,
|
||||
name: '20260912_071740_add_blackjack_minigame_stats',
|
||||
name: "20260912_071740_add_blackjack_minigame_stats",
|
||||
},
|
||||
{
|
||||
up: migration_20260912_231219_add_discord_cancelled_at.up,
|
||||
down: migration_20260912_231219_add_discord_cancelled_at.down,
|
||||
name: '20260912_231219_add_discord_cancelled_at',
|
||||
name: "20260912_231219_add_discord_cancelled_at",
|
||||
},
|
||||
{
|
||||
up: migration_20260914_153046_add_chip_bank.up,
|
||||
down: migration_20260914_153046_add_chip_bank.down,
|
||||
name: '20260914_153046_add_chip_bank',
|
||||
name: "20260914_153046_add_chip_bank",
|
||||
},
|
||||
{
|
||||
up: migration_20260914_171412_add_respawn_tickets.up,
|
||||
down: migration_20260914_171412_add_respawn_tickets.down,
|
||||
name: '20260914_171412_add_respawn_tickets',
|
||||
name: "20260914_171412_add_respawn_tickets",
|
||||
},
|
||||
{
|
||||
up: migration_20260916_083158_add_game_map_system.up,
|
||||
down: migration_20260916_083158_add_game_map_system.down,
|
||||
name: '20260916_083158_add_game_map_system',
|
||||
name: "20260916_083158_add_game_map_system",
|
||||
},
|
||||
{
|
||||
up: migration_20260916_185544_construction_timestamp_model.up,
|
||||
down: migration_20260916_185544_construction_timestamp_model.down,
|
||||
name: '20260916_185544_construction_timestamp_model',
|
||||
name: "20260916_185544_construction_timestamp_model",
|
||||
},
|
||||
{
|
||||
up: migration_20260916_220224_add_map_labels_and_styling.up,
|
||||
down: migration_20260916_220224_add_map_labels_and_styling.down,
|
||||
name: '20260916_220224_add_map_labels_and_styling',
|
||||
name: "20260916_220224_add_map_labels_and_styling",
|
||||
},
|
||||
{
|
||||
up: migration_20260917_023028_add_node_and_zone_resources.up,
|
||||
down: migration_20260917_023028_add_node_and_zone_resources.down,
|
||||
name: '20260917_023028_add_node_and_zone_resources'
|
||||
name: "20260917_023028_add_node_and_zone_resources",
|
||||
},
|
||||
{
|
||||
up: migration_20260917_112258_add_zone_political_types.up,
|
||||
down: migration_20260917_112258_add_zone_political_types.down,
|
||||
name: "20260917_112258_add_zone_political_types",
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -828,7 +828,17 @@ export const enum_releases_status = pgEnum("enum_releases_status", [
|
|||
]);
|
||||
export const enum_maps_basemap_mode = pgEnum("enum_maps_basemap_mode", ["image", "tiles"]);
|
||||
export const enum_map_roads_surface = pgEnum("enum_map_roads_surface", ["paved", "dirt", "trail"]);
|
||||
export const enum_map_zones_type = pgEnum("enum_map_zones_type", ["water", "land"]);
|
||||
export const enum_map_roads_stroke_style = pgEnum("enum_map_roads_stroke_style", [
|
||||
"solid",
|
||||
"dashed",
|
||||
"dotted",
|
||||
]);
|
||||
export const enum_map_zones_type = pgEnum("enum_map_zones_type", [
|
||||
"water",
|
||||
"land",
|
||||
"territory",
|
||||
"border",
|
||||
]);
|
||||
export const enum_narrative_events_triggers_type = pgEnum("enum_narrative_events_triggers_type", [
|
||||
"manual",
|
||||
"mission_complete",
|
||||
|
|
@ -1066,6 +1076,8 @@ export const game_structures = pgTable(
|
|||
}),
|
||||
maintenanceCost: numeric("maintenance_cost", { mode: "number" }).default(0),
|
||||
coordinates: geometryColumn("coordinates"),
|
||||
labelVisible: boolean("label_visible").default(true),
|
||||
labelPriority: numeric("label_priority", { mode: "number" }).default(0),
|
||||
updatedAt: timestamp("updated_at", { mode: "string", withTimezone: true, precision: 3 })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
|
|
@ -4787,6 +4799,7 @@ export const map_roads = pgTable(
|
|||
{
|
||||
id: serial("id").primaryKey(),
|
||||
name: varchar("name"),
|
||||
description: varchar("description"),
|
||||
map: integer("map_id")
|
||||
.notNull()
|
||||
.references(() => maps.id, {
|
||||
|
|
@ -4794,6 +4807,10 @@ export const map_roads = pgTable(
|
|||
}),
|
||||
surface: enum_map_roads_surface("surface").notNull().default("paved"),
|
||||
speedMultiplier: numeric("speed_multiplier", { mode: "number" }).notNull().default(1),
|
||||
strokeColor: varchar("stroke_color"),
|
||||
strokeWeight: numeric("stroke_weight", { mode: "number" }).default(2),
|
||||
strokeStyle: enum_map_roads_stroke_style("stroke_style").default("solid"),
|
||||
labelVisible: boolean("label_visible"),
|
||||
points: jsonb("points").notNull(),
|
||||
updatedAt: timestamp("updated_at", { mode: "string", withTimezone: true, precision: 3 })
|
||||
.defaultNow()
|
||||
|
|
@ -4814,12 +4831,18 @@ export const map_zones = pgTable(
|
|||
{
|
||||
id: serial("id").primaryKey(),
|
||||
name: varchar("name"),
|
||||
description: varchar("description"),
|
||||
map: integer("map_id")
|
||||
.notNull()
|
||||
.references(() => maps.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
type: enum_map_zones_type("type").notNull(),
|
||||
fillColor: varchar("fill_color"),
|
||||
strokeColor: varchar("stroke_color"),
|
||||
fillOpacity: numeric("fill_opacity", { mode: "number" }),
|
||||
strokeWeight: numeric("stroke_weight", { mode: "number" }).default(1),
|
||||
labelVisible: boolean("label_visible").default(true),
|
||||
points: jsonb("points").notNull(),
|
||||
updatedAt: timestamp("updated_at", { mode: "string", withTimezone: true, precision: 3 })
|
||||
.defaultNow()
|
||||
|
|
@ -4835,21 +4858,44 @@ export const map_zones = pgTable(
|
|||
],
|
||||
);
|
||||
|
||||
export const map_zones_rels = pgTable(
|
||||
"map_zones_rels",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
order: integer("order"),
|
||||
parent: integer("parent_id").notNull(),
|
||||
path: varchar("path").notNull(),
|
||||
resourcesID: integer("resources_id"),
|
||||
},
|
||||
(columns) => [
|
||||
index("map_zones_rels_order_idx").on(columns.order),
|
||||
index("map_zones_rels_parent_idx").on(columns.parent),
|
||||
index("map_zones_rels_path_idx").on(columns.path),
|
||||
index("map_zones_rels_resources_id_idx").on(columns.resourcesID),
|
||||
foreignKey({
|
||||
columns: [columns["parent"]],
|
||||
foreignColumns: [map_zones.id],
|
||||
name: "map_zones_rels_parent_fk",
|
||||
}).onDelete("cascade"),
|
||||
foreignKey({
|
||||
columns: [columns["resourcesID"]],
|
||||
foreignColumns: [resources.id],
|
||||
name: "map_zones_rels_resources_fk",
|
||||
}).onDelete("cascade"),
|
||||
],
|
||||
);
|
||||
|
||||
export const resource_nodes = pgTable(
|
||||
"resource_nodes",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
name: varchar("name").notNull(),
|
||||
description: varchar("description"),
|
||||
map: integer("map_id")
|
||||
.notNull()
|
||||
.references(() => maps.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
resource: integer("resource_id")
|
||||
.notNull()
|
||||
.references(() => resources.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
position: jsonb("position").notNull(),
|
||||
richness: numeric("richness", { mode: "number" }).default(100),
|
||||
updatedAt: timestamp("updated_at", { mode: "string", withTimezone: true, precision: 3 })
|
||||
|
|
@ -4861,12 +4907,38 @@ export const resource_nodes = pgTable(
|
|||
},
|
||||
(columns) => [
|
||||
index("resource_nodes_map_idx").on(columns.map),
|
||||
index("resource_nodes_resource_idx").on(columns.resource),
|
||||
index("resource_nodes_updated_at_idx").on(columns.updatedAt),
|
||||
index("resource_nodes_created_at_idx").on(columns.createdAt),
|
||||
],
|
||||
);
|
||||
|
||||
export const resource_nodes_rels = pgTable(
|
||||
"resource_nodes_rels",
|
||||
{
|
||||
id: serial("id").primaryKey(),
|
||||
order: integer("order"),
|
||||
parent: integer("parent_id").notNull(),
|
||||
path: varchar("path").notNull(),
|
||||
resourcesID: integer("resources_id"),
|
||||
},
|
||||
(columns) => [
|
||||
index("resource_nodes_rels_order_idx").on(columns.order),
|
||||
index("resource_nodes_rels_parent_idx").on(columns.parent),
|
||||
index("resource_nodes_rels_path_idx").on(columns.path),
|
||||
index("resource_nodes_rels_resources_id_idx").on(columns.resourcesID),
|
||||
foreignKey({
|
||||
columns: [columns["parent"]],
|
||||
foreignColumns: [resource_nodes.id],
|
||||
name: "resource_nodes_rels_parent_fk",
|
||||
}).onDelete("cascade"),
|
||||
foreignKey({
|
||||
columns: [columns["resourcesID"]],
|
||||
foreignColumns: [resources.id],
|
||||
name: "resource_nodes_rels_resources_fk",
|
||||
}).onDelete("cascade"),
|
||||
],
|
||||
);
|
||||
|
||||
export const narrative_events = pgTable(
|
||||
"narrative_events",
|
||||
{
|
||||
|
|
@ -8072,23 +8144,48 @@ export const relations_map_roads = relations(map_roads, ({ one }) => ({
|
|||
relationName: "map",
|
||||
}),
|
||||
}));
|
||||
export const relations_map_zones = relations(map_zones, ({ one }) => ({
|
||||
export const relations_map_zones_rels = relations(map_zones_rels, ({ one }) => ({
|
||||
parent: one(map_zones, {
|
||||
fields: [map_zones_rels.parent],
|
||||
references: [map_zones.id],
|
||||
relationName: "_rels",
|
||||
}),
|
||||
resourcesID: one(resources, {
|
||||
fields: [map_zones_rels.resourcesID],
|
||||
references: [resources.id],
|
||||
relationName: "resources",
|
||||
}),
|
||||
}));
|
||||
export const relations_map_zones = relations(map_zones, ({ one, many }) => ({
|
||||
map: one(maps, {
|
||||
fields: [map_zones.map],
|
||||
references: [maps.id],
|
||||
relationName: "map",
|
||||
}),
|
||||
_rels: many(map_zones_rels, {
|
||||
relationName: "_rels",
|
||||
}),
|
||||
}));
|
||||
export const relations_resource_nodes = relations(resource_nodes, ({ one }) => ({
|
||||
export const relations_resource_nodes_rels = relations(resource_nodes_rels, ({ one }) => ({
|
||||
parent: one(resource_nodes, {
|
||||
fields: [resource_nodes_rels.parent],
|
||||
references: [resource_nodes.id],
|
||||
relationName: "_rels",
|
||||
}),
|
||||
resourcesID: one(resources, {
|
||||
fields: [resource_nodes_rels.resourcesID],
|
||||
references: [resources.id],
|
||||
relationName: "resources",
|
||||
}),
|
||||
}));
|
||||
export const relations_resource_nodes = relations(resource_nodes, ({ one, many }) => ({
|
||||
map: one(maps, {
|
||||
fields: [resource_nodes.map],
|
||||
references: [maps.id],
|
||||
relationName: "map",
|
||||
}),
|
||||
resource: one(resources, {
|
||||
fields: [resource_nodes.resource],
|
||||
references: [resources.id],
|
||||
relationName: "resource",
|
||||
_rels: many(resource_nodes_rels, {
|
||||
relationName: "_rels",
|
||||
}),
|
||||
}));
|
||||
export const relations_narrative_events = relations(narrative_events, () => ({}));
|
||||
|
|
@ -8757,6 +8854,7 @@ type DatabaseSchema = {
|
|||
enum_releases_status: typeof enum_releases_status;
|
||||
enum_maps_basemap_mode: typeof enum_maps_basemap_mode;
|
||||
enum_map_roads_surface: typeof enum_map_roads_surface;
|
||||
enum_map_roads_stroke_style: typeof enum_map_roads_stroke_style;
|
||||
enum_map_zones_type: typeof enum_map_zones_type;
|
||||
enum_narrative_events_triggers_type: typeof enum_narrative_events_triggers_type;
|
||||
enum_story_beats_status: typeof enum_story_beats_status;
|
||||
|
|
@ -8887,7 +8985,9 @@ type DatabaseSchema = {
|
|||
maps: typeof maps;
|
||||
map_roads: typeof map_roads;
|
||||
map_zones: typeof map_zones;
|
||||
map_zones_rels: typeof map_zones_rels;
|
||||
resource_nodes: typeof resource_nodes;
|
||||
resource_nodes_rels: typeof resource_nodes_rels;
|
||||
narrative_events: typeof narrative_events;
|
||||
story_beats: typeof story_beats;
|
||||
story_beat_states: typeof story_beat_states;
|
||||
|
|
@ -9032,7 +9132,9 @@ type DatabaseSchema = {
|
|||
relations_labels: typeof relations_labels;
|
||||
relations_maps: typeof relations_maps;
|
||||
relations_map_roads: typeof relations_map_roads;
|
||||
relations_map_zones_rels: typeof relations_map_zones_rels;
|
||||
relations_map_zones: typeof relations_map_zones;
|
||||
relations_resource_nodes_rels: typeof relations_resource_nodes_rels;
|
||||
relations_resource_nodes: typeof relations_resource_nodes;
|
||||
relations_narrative_events: typeof relations_narrative_events;
|
||||
relations_story_beats: typeof relations_story_beats;
|
||||
|
|
|
|||
|
|
@ -3791,7 +3791,7 @@ export interface MapRoad {
|
|||
strokeWeight?: number | null;
|
||||
strokeStyle?: ('solid' | 'dashed' | 'dotted') | null;
|
||||
/**
|
||||
* Renders the road's name along the line at zoom-dependent intervals.
|
||||
* Auto (unset): only paved roads show labels. Check to force on, uncheck to force off. The map authoring toolbar's Label select writes this.
|
||||
*/
|
||||
labelVisible?: boolean | null;
|
||||
/**
|
||||
|
|
@ -3825,9 +3825,9 @@ export interface MapZone {
|
|||
description?: string | null;
|
||||
map: number | Map;
|
||||
/**
|
||||
* Water zones block land-blueprint placement. Land zones are informational; default terrain is buildable land.
|
||||
* Water zones block land-blueprint placement. Land zones are informational; default terrain is buildable land. Territory and Country Border are political boundaries (informational).
|
||||
*/
|
||||
type: 'water' | 'land';
|
||||
type: 'water' | 'land' | 'territory' | 'border';
|
||||
/**
|
||||
* Polygon fill as hex (e.g. #38bdf8). Empty = default color for the type.
|
||||
*/
|
||||
|
|
@ -3837,7 +3837,7 @@ export interface MapZone {
|
|||
*/
|
||||
strokeColor?: string | null;
|
||||
/**
|
||||
* Fill opacity 0-1. Empty = default (0.25 water, 0.08 land).
|
||||
* Fill opacity 0-1. Empty = default (0.25 water, 0.08 land, 0.1 territory, 0.06 border).
|
||||
*/
|
||||
fillOpacity?: number | null;
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -14,12 +14,20 @@ import type {
|
|||
} from "@/payload-types";
|
||||
import { buildRoadGraph, findRoute, type RoadInput } from "@/lib/map/routing";
|
||||
import {
|
||||
labelScreenAngle,
|
||||
roadLabelShown,
|
||||
metersPerPx,
|
||||
polygonCentroid,
|
||||
roadLabelPoints,
|
||||
splinePolyline,
|
||||
validateHexColor,
|
||||
} from "@/lib/map/style";
|
||||
import {
|
||||
closestPointOnSegment,
|
||||
findSnap,
|
||||
segmentMidpoints,
|
||||
type SnapTarget,
|
||||
} from "@/lib/map/snapping";
|
||||
import {
|
||||
checkResourcesInRange,
|
||||
checkStructuresInRange,
|
||||
|
|
@ -505,6 +513,28 @@ describe("Game Map System", () => {
|
|||
expect(short).toEqual(raw.slice(0, 2));
|
||||
});
|
||||
|
||||
it("derives upright straight-label angles with vertical text reading up", () => {
|
||||
expect(labelScreenAngle([0, 0], [1000, 0])).toBe(0);
|
||||
expect(labelScreenAngle([0, 0], [-1000, 0])).toBe(0);
|
||||
expect(labelScreenAngle([0, 0], [0, 1000])).toBe(-90);
|
||||
expect(labelScreenAngle([0, 0], [0, -1000])).toBe(-90);
|
||||
expect(labelScreenAngle([0, 0], [1000, 1000])).toBe(45);
|
||||
expect(labelScreenAngle([0, 0], [-1000, 1000])).toBe(-45);
|
||||
});
|
||||
|
||||
it("defaults road labels to paved roads, minor roads at close zoom, with explicit override", () => {
|
||||
expect(roadLabelShown({ labelVisible: null, surface: "paved" }, 32)).toBe(true);
|
||||
expect(roadLabelShown({ labelVisible: null, surface: "dirt" }, 32)).toBe(false);
|
||||
expect(roadLabelShown({ labelVisible: null, surface: "trail" }, 32)).toBe(false);
|
||||
expect(roadLabelShown({ labelVisible: null, surface: "dirt" }, 4)).toBe(true);
|
||||
expect(roadLabelShown({ labelVisible: null, surface: "dirt" }, 3.9)).toBe(true);
|
||||
expect(roadLabelShown({ labelVisible: null, surface: "trail" }, 4)).toBe(true);
|
||||
expect(roadLabelShown({ labelVisible: true, surface: "dirt" }, 32)).toBe(true);
|
||||
expect(roadLabelShown({ labelVisible: false, surface: "paved" }, 4)).toBe(false);
|
||||
expect(roadLabelShown({ labelVisible: null, surface: "paved" })).toBe(true);
|
||||
expect(roadLabelShown({ labelVisible: null, surface: "dirt" })).toBe(false);
|
||||
});
|
||||
|
||||
it("computes polygon centroids inside the ring", () => {
|
||||
const centroid = polygonCentroid([
|
||||
[0, 0],
|
||||
|
|
@ -523,6 +553,82 @@ describe("Game Map System", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("authoring snapping (pure)", () => {
|
||||
it("projects to segments and clamps to endpoints", () => {
|
||||
expect(closestPointOnSegment([500, 500], [0, 0], [1000, 0])).toEqual([500, 0]);
|
||||
expect(closestPointOnSegment([-50, -50], [0, 0], [1000, 0])).toEqual([0, 0]);
|
||||
expect(closestPointOnSegment([2000, 50], [0, 0], [1000, 0])).toEqual([1000, 0]);
|
||||
expect(closestPointOnSegment([10, 10], [10, 10], [10, 10])).toEqual([10, 10]);
|
||||
});
|
||||
|
||||
it("snaps to the nearest vertex, preferring vertices over edges on ties", () => {
|
||||
const zone: SnapTarget = {
|
||||
key: "zone-1",
|
||||
vertices: [
|
||||
[0, 0],
|
||||
[1000, 0],
|
||||
[1000, 1000],
|
||||
[0, 1000],
|
||||
],
|
||||
closed: true,
|
||||
edges: true,
|
||||
};
|
||||
const corner = findSnap([30, 20], [zone], 100);
|
||||
expect(corner?.kind).toBe("vertex");
|
||||
expect(corner?.point).toEqual([0, 0]);
|
||||
|
||||
// On the middle of an edge (>= 10% closer than any vertex) the edge wins.
|
||||
const edge = findSnap([500, 30], [zone], 100);
|
||||
expect(edge?.kind).toBe("edge");
|
||||
expect(edge?.point).toEqual([500, 0]);
|
||||
});
|
||||
|
||||
it("includes the closing segment of closed targets and respects the radius", () => {
|
||||
const zone: SnapTarget = {
|
||||
key: "zone-1",
|
||||
vertices: [
|
||||
[0, 0],
|
||||
[1000, 0],
|
||||
[1000, 1000],
|
||||
[0, 1000],
|
||||
],
|
||||
closed: true,
|
||||
edges: true,
|
||||
};
|
||||
const closing = findSnap([30, 500], [zone], 100);
|
||||
expect(closing?.kind).toBe("edge");
|
||||
expect(closing?.point).toEqual([0, 500]);
|
||||
|
||||
expect(findSnap([30, 500], [zone], 10)).toBeNull();
|
||||
});
|
||||
|
||||
it("builds segment midpoints with insert indices for open and closed drafts", () => {
|
||||
const open = segmentMidpoints(
|
||||
[
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
[100, 100],
|
||||
],
|
||||
false,
|
||||
);
|
||||
expect(open).toEqual([
|
||||
{ index: 1, point: [50, 0] },
|
||||
{ index: 2, point: [100, 50] },
|
||||
]);
|
||||
|
||||
const closed = segmentMidpoints(
|
||||
[
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
[100, 100],
|
||||
],
|
||||
true,
|
||||
);
|
||||
expect(closed).toHaveLength(3);
|
||||
expect(closed[2]).toEqual({ index: 3, point: [50, 50] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("road routing (pure)", () => {
|
||||
it("connects roads that share a vertex", () => {
|
||||
const roads: RoadInput[] = [
|
||||
|
|
@ -1083,6 +1189,98 @@ describe("Game Map System", () => {
|
|||
expect(outside.success).toBe(false);
|
||||
expect(outside.error).toContain("resource node");
|
||||
});
|
||||
|
||||
it("accepts political-boundary zone types and leaves placement unaffected", async () => {
|
||||
const territory = await payload.create({
|
||||
collection: "map-zones",
|
||||
data: {
|
||||
map: mapA.id,
|
||||
name: `${RUN} Territory`,
|
||||
type: "territory",
|
||||
points: [
|
||||
[0, 0],
|
||||
[1000, 0],
|
||||
[1000, 1000],
|
||||
[0, 1000],
|
||||
],
|
||||
},
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
});
|
||||
createdIds.zones.push(territory.id);
|
||||
expect(territory.type).toBe("territory");
|
||||
|
||||
const border = await payload.create({
|
||||
collection: "map-zones",
|
||||
data: {
|
||||
map: mapA.id,
|
||||
name: `${RUN} Border`,
|
||||
type: "border",
|
||||
points: [
|
||||
[4000, 6000],
|
||||
[5000, 6000],
|
||||
[5000, 7000],
|
||||
[4000, 7000],
|
||||
],
|
||||
},
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
});
|
||||
createdIds.zones.push(border.id);
|
||||
expect(border.type).toBe("border");
|
||||
|
||||
// Political zones are informational: a land blueprint inside one places
|
||||
// normally (only water zones gate placement).
|
||||
authSpy.mockResolvedValue({ user: logisticsUser });
|
||||
const inside = await placeStructure({
|
||||
mapId: mapA.id,
|
||||
blueprintId: depotBlueprint.id,
|
||||
name: `${RUN} Territory Site`,
|
||||
x: 500,
|
||||
y: 500,
|
||||
});
|
||||
expect(inside.success).toBe(true);
|
||||
createdIds.gameStructures.push((inside.data as GameStructure).id);
|
||||
});
|
||||
|
||||
it("stores road labels as tri-state with paved-only auto default", async () => {
|
||||
const dirt = await payload.create({
|
||||
collection: "map-roads",
|
||||
data: {
|
||||
map: mapA.id,
|
||||
name: `${RUN} Dirt Track`,
|
||||
surface: "dirt",
|
||||
speedMultiplier: 1,
|
||||
points: [
|
||||
[0, 9000],
|
||||
[1000, 9000],
|
||||
],
|
||||
},
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
});
|
||||
createdIds.roads.push(dirt.id);
|
||||
expect(dirt.labelVisible).toBeNull();
|
||||
|
||||
const paved = await payload.create({
|
||||
collection: "map-roads",
|
||||
data: {
|
||||
map: mapA.id,
|
||||
name: `${RUN} Paved Way`,
|
||||
surface: "paved",
|
||||
speedMultiplier: 1,
|
||||
labelVisible: false,
|
||||
points: [
|
||||
[0, 9500],
|
||||
[1000, 9500],
|
||||
],
|
||||
},
|
||||
overrideAccess: true,
|
||||
depth: 0,
|
||||
});
|
||||
createdIds.roads.push(paved.id);
|
||||
expect(paved.labelVisible).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("construction timestamps", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue