diff --git a/src/app/(frontend)/logistics/game-vehicles/[id]/page.tsx b/src/app/(frontend)/logistics/game-vehicles/[id]/page.tsx new file mode 100644 index 0000000..dd4cc2b --- /dev/null +++ b/src/app/(frontend)/logistics/game-vehicles/[id]/page.tsx @@ -0,0 +1,320 @@ +import config from "@payload-config"; +import { getPayload } from "payload"; +import { notFound } from "next/navigation"; +import Link from "next/link"; +import type { + GameVehicle, + Vehicle, + GameStructure, + Structure, + Resource, +} from "@/payload-types"; +import { + ArrowLeftIcon, + FuelIcon, + HeartIcon, + MapPinIcon, + ShieldIcon, + TruckIcon, +} from "lucide-react"; +import { + Item, + ItemContent, + ItemDescription, + ItemHeader, + ItemMedia, + ItemTitle, +} from "@/components/ui/item"; + +interface GameVehicleDetailPageProps { + params: Promise<{ id: string }>; +} + +const statusColors: Record = { + idle: "bg-emerald-500/20 text-emerald-400", + assigned: "bg-blue-500/20 text-blue-400", + in_transit: "bg-amber-500/20 text-amber-400", + damaged: "bg-red-500/20 text-red-400", + destroyed: "bg-red-500/20 text-red-400", +}; + +export default async function GameVehicleDetailPage({ + params, +}: GameVehicleDetailPageProps) { + const { id } = await params; + const payload = await getPayload({ config }); + + const vehicle = await payload + .findByID({ + collection: "game-vehicles", + id: Number(id), + depth: 2, + }) + .catch(() => null); + + if (!vehicle) { + notFound(); + } + + const vehicleData = vehicle as unknown as GameVehicle & { + type: Vehicle; + }; + const vehicleType = + typeof vehicleData.type === "object" ? vehicleData.type : null; + const deployedAt = + typeof vehicleData.deployedAt === "object" && + vehicleData.deployedAt !== null + ? (vehicleData.deployedAt as GameStructure & { type: Structure }) + : null; + const faction = + typeof vehicleData.faction === "object" && vehicleData.faction !== null + ? vehicleData.faction + : null; + + const fuelPercent = vehicleType?.fuel?.fuelCapacity + ? (vehicleData.currentFuel / vehicleType.fuel.fuelCapacity) * 100 + : 0; + + const storedResources = vehicleData.storedResources ?? []; + const resourceCount = storedResources.length; + + return ( +
+ + + Back to Vehicles + + +
+
+
+ +

{vehicleData.name}

+
+
+ + {vehicleData.status === "in_transit" + ? "In Transit" + : vehicleData.status.charAt(0).toUpperCase() + + vehicleData.status.slice(1)} + + + {vehicleType?.name ?? "Unknown type"} + +
+
+
+ +
+ + + + + + Fuel + + {vehicleData.currentFuel?.toFixed(1)} /{" "} + {vehicleType?.fuel?.fuelCapacity ?? "?"} + + + + + + + + + Health + + {vehicleData.currentHealth} + + + + + + + + + Location + + {deployedAt?.name ?? "In transit"} + + + + + + + + + Faction + + {faction?.name ?? "Unassigned"} + + + +
+ +
+
+ {/* Fuel Gauge */} + + +
+ + Fuel Tank +
+
+ +
+
+
20 ? "bg-green-500" : "bg-red-500" + }`} + style={{ + width: `${Math.min(100, fuelPercent)}%`, + }} + /> +
+ + {fuelPercent.toFixed(0)}% + +
+
+ + {vehicleData.currentFuel?.toFixed(1)} units remaining + + + Capacity: {vehicleType?.fuel?.fuelCapacity ?? "?"} units + +
+ {vehicleType?.fuel?.fuelType && + typeof vehicleType.fuel.fuelType === "object" && ( +
+ Fuel type: {vehicleType.fuel.fuelType.name} · Consumption + rate: {vehicleType.fuel.fuelConsumptionRate}/unit +
+ )} + + + + {/* Cargo */} + + + Cargo + + {resourceCount} item type{resourceCount !== 1 ? "s" : ""} + + + + {storedResources.length > 0 ? ( +
+ + + + + + + + + {storedResources.map((entry, index) => { + const resource = + typeof entry.resource === "object" + ? (entry.resource as Resource) + : null; + return ( + + + + + ); + })} + +
+ Resource + + Amount +
+ {resource?.name ?? `Resource #${entry.resource}`} + + {entry.amount?.toLocaleString()} +
+
+ ) : ( +

+ Vehicle cargo is empty. +

+ )} +
+
+
+ + {/* Sidebar */} +
+ {/* Vehicle Type Info */} + + + Vehicle Type + + +
+
+

+ {vehicleType?.name ?? "Unknown"} +

+ {vehicleType?.classname && ( +

+ {vehicleType.classname} +

+ )} +
+ {vehicleType?.category && + vehicleType.category.length > 0 && ( +
+ {vehicleType.category.map((cat) => ( + + {cat} + + ))} +
+ )} +
+
+
+ + {/* Location */} + {deployedAt && ( + + + +
+ + Stationed At +
+
+ +

+ {deployedAt.name} +

+
+
+ + )} +
+
+
+ ); +} diff --git a/src/app/(frontend)/logistics/game-vehicles/page.tsx b/src/app/(frontend)/logistics/game-vehicles/page.tsx new file mode 100644 index 0000000..92c6584 --- /dev/null +++ b/src/app/(frontend)/logistics/game-vehicles/page.tsx @@ -0,0 +1,137 @@ +import config from "@payload-config"; +import { getPayload } from "payload"; +import type { GameVehicle, Vehicle } from "@/payload-types"; +import { TruckIcon } from "lucide-react"; +import Link from "next/link"; +import { + Item, + ItemContent, + ItemDescription, + ItemHeader, + ItemMedia, + ItemTitle, +} from "@/components/ui/item"; + +export const metadata = { + title: "Vehicles — Polaris Task Force", +}; + +export default async function GameVehiclesPage() { + const payload = await getPayload({ config }); + + const vehiclesRes = await payload.find({ + collection: "game-vehicles", + limit: 500, + depth: 2, + }); + + const vehicles = vehiclesRes.docs as unknown as (GameVehicle & { + type: Vehicle; + })[]; + + const statusColors: Record = { + idle: "bg-emerald-500/20 text-emerald-400", + assigned: "bg-blue-500/20 text-blue-400", + in_transit: "bg-amber-500/20 text-amber-400", + damaged: "bg-red-500/20 text-red-400", + destroyed: "bg-red-500/20 text-red-400", + }; + + return ( +
+
+
+ +

Vehicles

+
+

+ {vehicles.length} vehicle{vehicles.length !== 1 ? "s" : ""} deployed. +

+
+ + {vehicles.length > 0 ? ( +
+ {vehicles.map((vehicle) => { + const vehicleType = + typeof vehicle.type === "object" ? vehicle.type : null; + const fuelPercent = vehicleType?.fuel?.fuelCapacity + ? (vehicle.currentFuel / vehicleType.fuel.fuelCapacity) * 100 + : 0; + + return ( + + + +
+ + + + + {vehicle.name} + + {vehicleType?.name ?? "Unknown type"} + + +
+ + {vehicle.status === "in_transit" + ? "In Transit" + : vehicle.status.charAt(0).toUpperCase() + + vehicle.status.slice(1)} + +
+ +
+
+
+
20 + ? "bg-green-500" + : "bg-red-500" + }`} + style={{ + width: `${Math.min(100, fuelPercent)}%`, + }} + /> +
+ + {vehicle.currentFuel?.toFixed(0)} /{" "} + {vehicleType?.fuel?.fuelCapacity ?? "?"} fuel + +
+
+ HP: {vehicle.currentHealth} + {vehicleType?.fuel?.fuelType && + typeof vehicleType.fuel.fuelType === "object" && ( + + {vehicleType.fuel.fuelType.name} + + )} +
+
+ + + {vehicleType?.name ?? "Unknown type"} + + + + ); + })} +
+ ) : ( +
+

No vehicles deployed.

+
+ )} +
+ ); +} diff --git a/src/collections/game/GameVehicles.ts b/src/collections/game/GameVehicles.ts new file mode 100644 index 0000000..325828a --- /dev/null +++ b/src/collections/game/GameVehicles.ts @@ -0,0 +1,154 @@ +import { CollectionConfig } from "payload"; +import { isDeveloper } from "@/utils/access-control/isRole"; + +export const GameVehicles: CollectionConfig = { + slug: "game-vehicles", + admin: { + group: "Game", + useAsTitle: "name", + }, + access: { + read: ({ req }) => !!req.user, + create: isDeveloper, + update: isDeveloper, + delete: isDeveloper, + }, + fields: [ + { + name: "name", + type: "text", + required: true, + admin: { + description: "Display name or callsign for this vehicle instance.", + }, + }, + { + name: "type", + type: "relationship", + relationTo: "vehicles", + required: true, + admin: { + description: "The vehicle type/template this instance is based on.", + }, + }, + { + name: "deployedAt", + type: "relationship", + relationTo: "game-structures", + admin: { + description: "The structure where this vehicle is currently stationed. Null if in transit.", + }, + }, + { + name: "faction", + type: "relationship", + relationTo: "factions", + admin: { + description: "The faction that owns this vehicle.", + }, + }, + { + name: "status", + type: "select", + options: [ + { label: "Idle", value: "idle" }, + { label: "Assigned", value: "assigned" }, + { label: "In Transit", value: "in_transit" }, + { label: "Damaged", value: "damaged" }, + { label: "Destroyed", value: "destroyed" }, + ], + defaultValue: "idle", + required: true, + }, + { + name: "currentFuel", + type: "number", + required: true, + defaultValue: 0, + admin: { + description: "Current fuel level. Must be between 0 and the vehicle type's fuel capacity.", + }, + }, + { + name: "currentHealth", + type: "number", + required: true, + admin: { + description: "Current health. Must be between 0 and the vehicle type's max health.", + }, + }, + { + name: "storedResources", + type: "array", + fields: [ + { + name: "resource", + type: "relationship", + relationTo: "resources", + required: true, + }, + { + name: "amount", + type: "number", + required: true, + min: 1, + }, + { + name: "gridX", + type: "number", + required: true, + min: 0, + }, + { + name: "gridY", + type: "number", + required: true, + min: 0, + }, + { + name: "rotated", + type: "checkbox", + defaultValue: false, + }, + ], + }, + { + name: "voidStorage", + type: "array", + admin: { + description: "Items displaced by grid resizing.", + }, + fields: [ + { + name: "resource", + type: "relationship", + relationTo: "resources", + required: true, + }, + { + name: "amount", + type: "number", + required: true, + min: 1, + }, + { + name: "gridX", + type: "number", + required: true, + min: 0, + }, + { + name: "gridY", + type: "number", + required: true, + min: 0, + }, + { + name: "rotated", + type: "checkbox", + defaultValue: false, + }, + ], + }, + ], +}; diff --git a/src/collections/logistics/Vehicles.ts b/src/collections/logistics/Vehicles.ts index 4c7226f..741c83b 100644 --- a/src/collections/logistics/Vehicles.ts +++ b/src/collections/logistics/Vehicles.ts @@ -92,6 +92,44 @@ export const Vehicles: CollectionConfig = { }, ], }, + { + name: "fuel", + type: "group", + admin: { + description: + "Fuel configuration for this vehicle. Defines what fuel type it uses, how much it can hold, and how fast it consumes fuel.", + }, + fields: [ + { + name: "fuelType", + type: "relationship", + relationTo: "resources", + required: true, + admin: { + description: + "The type of fuel this vehicle consumes (e.g., gasoline, diesel, jet fuel).", + }, + }, + { + name: "fuelCapacity", + type: "number", + required: true, + admin: { + description: "Maximum fuel units this vehicle's tank can hold.", + }, + }, + { + name: "fuelConsumptionRate", + type: "number", + required: true, + defaultValue: 1, + admin: { + description: + "Fuel consumed per unit of distance traveled.", + }, + }, + ], + }, ], },