style: remove em-dashes from user-visible prose (wave 1)
This commit is contained in:
parent
a96b5de52b
commit
a2a5f7877b
94 changed files with 248 additions and 282 deletions
|
|
@ -15,7 +15,7 @@ import { PreferencesForm } from "@/components/frontend/account/PreferencesForm";
|
||||||
import { RequestTransferDialog } from "@/components/frontend/account/RequestTransferDialog";
|
import { RequestTransferDialog } from "@/components/frontend/account/RequestTransferDialog";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Account — Polaris Task Force",
|
title: "Account (Polaris Task Force)",
|
||||||
};
|
};
|
||||||
|
|
||||||
function relationId(value: Assignment["leader"]): number | null {
|
function relationId(value: Assignment["leader"]): number | null {
|
||||||
|
|
@ -114,7 +114,9 @@ export default async function AccountPage() {
|
||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div className="flex min-w-0 flex-col gap-1">
|
<div className="flex min-w-0 flex-col gap-1">
|
||||||
<CardTitle>Assignment</CardTitle>
|
<CardTitle>Assignment</CardTitle>
|
||||||
<CardDescription>Review your current assignment and request a move.</CardDescription>
|
<CardDescription>
|
||||||
|
Review your current assignment and request a move.
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<RequestTransferDialog
|
<RequestTransferDialog
|
||||||
currentAssignments={currentAssignments}
|
currentAssignments={currentAssignments}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Bomb } from "lucide-react";
|
||||||
import { EodArena } from "@/components/frontend/eod/EodArena";
|
import { EodArena } from "@/components/frontend/eod/EodArena";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Minefield Clearance — Polaris Task Force",
|
title: "Minefield Clearance (Polaris Task Force)",
|
||||||
description: "Clear simulated minefields across three EOD sectors to earn XP for the task force.",
|
description: "Clear simulated minefields across three EOD sectors to earn XP for the task force.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Bird } from "lucide-react";
|
||||||
import { FlappyArena } from "@/components/frontend/flappy/FlappyArena";
|
import { FlappyArena } from "@/components/frontend/flappy/FlappyArena";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Flight Simulator — Polaris Task Force",
|
title: "Flight Simulator: Polaris Task Force",
|
||||||
description: "Side training sim. Definitely not Flappy Bird.",
|
description: "Side training sim. Definitely not Flappy Bird.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import Link from "next/link";
|
||||||
import { getTicketVoteSummaries } from "@/lib/tickets/voteState";
|
import { getTicketVoteSummaries } from "@/lib/tickets/voteState";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Ticket — Polaris Task Force",
|
title: "Ticket: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface TicketPageProps {
|
interface TicketPageProps {
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ export async function createTicket(input: {
|
||||||
userId: staffId,
|
userId: staffId,
|
||||||
type: "ticket:created",
|
type: "ticket:created",
|
||||||
title: "New ticket",
|
title: "New ticket",
|
||||||
message: `${title} — ${reporterName}`,
|
message: `${title} by ${reporterName}`,
|
||||||
link: `/helpdesk/${createdTicket.id}`,
|
link: `/helpdesk/${createdTicket.id}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -233,7 +233,7 @@ export async function replyToTicket(ticketId: number, content: string): Promise<
|
||||||
targetId: ticketId,
|
targetId: ticketId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const replyMessage = `"${ticket.title}" — you have a new reply.`;
|
const replyMessage = `"${ticket.title}" has a new reply.`;
|
||||||
if (isStaff) {
|
if (isStaff) {
|
||||||
if (reporterId != null && reporterId !== userId) {
|
if (reporterId != null && reporterId !== userId) {
|
||||||
await notifyUser(payload, {
|
await notifyUser(payload, {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { LifeBuoyIcon } from "lucide-react";
|
||||||
import { getTicketVoteSummaries } from "@/lib/tickets/voteState";
|
import { getTicketVoteSummaries } from "@/lib/tickets/voteState";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Helpdesk — Polaris Task Force",
|
title: "Helpdesk: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function HelpdeskPage() {
|
export default async function HelpdeskPage() {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import type { Media } from "@/payload-types";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Campaigns — Polaris Task Force",
|
title: "Campaigns: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function CampaignsPage() {
|
export default async function CampaignsPage() {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { ShieldIcon } from "lucide-react";
|
||||||
import { FactionCard } from "@/components/frontend/intelligence/FactionCard";
|
import { FactionCard } from "@/components/frontend/intelligence/FactionCard";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Factions — Polaris Task Force",
|
title: "Factions | Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function FactionsPage() {
|
export default async function FactionsPage() {
|
||||||
|
|
|
||||||
|
|
@ -430,7 +430,7 @@ export async function createFridaySideOp(
|
||||||
if (fridayConflict) {
|
if (fridayConflict) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `That Friday is already taken by "${fridayConflict.name}" (${fridayConflict.codeName}) — only one operation per Friday, first come first served.`,
|
error: `That Friday is already taken by "${fridayConflict.name}" (${fridayConflict.codeName}). Only one operation per Friday, first come first served.`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -583,7 +583,7 @@ export async function updateFridaySideOp(
|
||||||
if (fridayConflict) {
|
if (fridayConflict) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `That Friday is already taken by "${fridayConflict.name}" (${fridayConflict.codeName}) — only one operation per Friday, first come first served.`,
|
error: `That Friday is already taken by "${fridayConflict.name}" (${fridayConflict.codeName}). Only one operation per Friday, first come first served.`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { CrosshairIcon } from "lucide-react";
|
||||||
import { MissionsList } from "@/components/frontend/intelligence/MissionsList";
|
import { MissionsList } from "@/components/frontend/intelligence/MissionsList";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Missions — Polaris Task Force",
|
title: "Missions: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function MissionsPage() {
|
export default async function MissionsPage() {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { Item, ItemContent, ItemDescription, ItemHeader, ItemTitle } from "@/com
|
||||||
import { CampaignCard } from "@/components/frontend/intelligence/CampaignCard";
|
import { CampaignCard } from "@/components/frontend/intelligence/CampaignCard";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Intelligence — Polaris Task Force",
|
title: "Intelligence: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function IntelligenceOverviewPage() {
|
export default async function IntelligenceOverviewPage() {
|
||||||
|
|
|
||||||
|
|
@ -299,7 +299,7 @@ export async function moveLockerItem(
|
||||||
grid.height,
|
grid.height,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return { success: false, error: "Cannot move here — overlaps with another item." };
|
return { success: false, error: "Cannot move here: overlaps with another item." };
|
||||||
}
|
}
|
||||||
|
|
||||||
items[idx] =
|
items[idx] =
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { LockerView } from "@/components/frontend/locker/LockerView";
|
||||||
import { hasPermission } from "@/utils/access-control/hasPermission";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Locker — Polaris Task Force",
|
title: "Locker (Polaris Task Force)",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function LockerPage() {
|
export default async function LockerPage() {
|
||||||
|
|
@ -38,7 +38,7 @@ export default async function LockerPage() {
|
||||||
});
|
});
|
||||||
const loadouts = loadoutsRes.docs as unknown as Loadout[];
|
const loadouts = loadoutsRes.docs as unknown as Loadout[];
|
||||||
|
|
||||||
const isManager = user ? (await hasPermission(payload, user, "locker-storages:update")) : false;
|
const isManager = user ? await hasPermission(payload, user, "locker-storages:update") : false;
|
||||||
|
|
||||||
let assetsCatalog: Asset[] = [];
|
let assetsCatalog: Asset[] = [];
|
||||||
if (isManager) {
|
if (isManager) {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQu
|
||||||
import { getCurrencyConfig } from "@/lib/banking";
|
import { getCurrencyConfig } from "@/lib/banking";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Account — Polaris Task Force",
|
title: "Account: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AccountPageProps {
|
interface AccountPageProps {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQu
|
||||||
import { getCurrencyConfig } from "@/lib/banking";
|
import { getCurrencyConfig } from "@/lib/banking";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Banking — Polaris Task Force",
|
title: "Banking: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function BankingPage() {
|
export default async function BankingPage() {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
} from "@/components/ui/item";
|
} from "@/components/ui/item";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Vehicles — Polaris Task Force",
|
title: "Vehicles: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function GameVehiclesPage() {
|
export default async function GameVehiclesPage() {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,10 @@ async function authenticate() {
|
||||||
return { payload, user };
|
return { payload, user };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isMarketManager(payload: Awaited<ReturnType<typeof getPayload>>, user: User): Promise<boolean> {
|
async function isMarketManager(
|
||||||
|
payload: Awaited<ReturnType<typeof getPayload>>,
|
||||||
|
user: User,
|
||||||
|
): Promise<boolean> {
|
||||||
return await hasPermission(payload, user, "market-listings:update");
|
return await hasPermission(payload, user, "market-listings:update");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -344,7 +347,7 @@ async function resolveNpcOffer(
|
||||||
userId: buyer.id as number,
|
userId: buyer.id as number,
|
||||||
type: "market:counter",
|
type: "market:counter",
|
||||||
title: "The vendor countered",
|
title: "The vendor countered",
|
||||||
message: `${assetNameOf(listing)} — vendor proposes ${response.amount.toLocaleString()} each.`,
|
message: `${assetNameOf(listing)}: vendor proposes ${response.amount.toLocaleString()} each.`,
|
||||||
link: "/logistics/market",
|
link: "/logistics/market",
|
||||||
});
|
});
|
||||||
await emitGameEvent(payload, {
|
await emitGameEvent(payload, {
|
||||||
|
|
@ -1033,8 +1036,7 @@ export async function getReferencePrices(
|
||||||
for (const asset of assets.docs) {
|
for (const asset of assets.docs) {
|
||||||
const base = getBaseBuyPrice(asset);
|
const base = getBaseBuyPrice(asset);
|
||||||
const modifier = modifierByItemId.get(asset.id);
|
const modifier = modifierByItemId.get(asset.id);
|
||||||
references[asset.id] =
|
references[asset.id] = base == null ? null : Math.max(1, Math.round(base * (modifier ?? 1)));
|
||||||
base == null ? null : Math.max(1, Math.round(base * (modifier ?? 1)));
|
|
||||||
}
|
}
|
||||||
return { success: true, data: references };
|
return { success: true, data: references };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { ensureLockerStorage } from "@/lib/locker";
|
||||||
import { getCurrencyConfig } from "@/lib/banking";
|
import { getCurrencyConfig } from "@/lib/banking";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Market — Polaris Task Force",
|
title: "Market: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function MarketPage() {
|
export default async function MarketPage() {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { PackageOpenIcon } from "lucide-react";
|
||||||
import { ResourceCard } from "@/components/frontend/logistics/ResourceCard";
|
import { ResourceCard } from "@/components/frontend/logistics/ResourceCard";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Resources — Polaris Task Force",
|
title: "Resources: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function ResourcesPage() {
|
export default async function ResourcesPage() {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { TruckIcon } from "lucide-react";
|
||||||
import { ShipmentsList } from "@/components/frontend/logistics/ShipmentsList";
|
import { ShipmentsList } from "@/components/frontend/logistics/ShipmentsList";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Shipments — Polaris Task Force",
|
title: "Shipments: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function ShipmentsPage() {
|
export default async function ShipmentsPage() {
|
||||||
|
|
|
||||||
|
|
@ -683,7 +683,7 @@ export async function moveGridItem(
|
||||||
for (let dx = 0; dx < ow; dx++) {
|
for (let dx = 0; dx < ow; dx++) {
|
||||||
for (let dy = 0; dy < oh; dy++) {
|
for (let dy = 0; dy < oh; dy++) {
|
||||||
if (targetCells.has(`${other.gridX + dx},${other.gridY + dy}`)) {
|
if (targetCells.has(`${other.gridX + dx},${other.gridY + dy}`)) {
|
||||||
return { success: false, error: "Cannot move here — overlaps with another item." };
|
return { success: false, error: "Cannot move here: overlaps with another item." };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1134,7 +1134,7 @@ export async function retrieveFromVoid(structureId: number): Promise<GridActionR
|
||||||
message:
|
message:
|
||||||
recovered > 0
|
recovered > 0
|
||||||
? `Retrieved ${recovered} item(s) from void in "${structure.name}"`
|
? `Retrieved ${recovered} item(s) from void in "${structure.name}"`
|
||||||
: `No void items could be recovered in "${structure.name}" — grid full`,
|
: `No void items could be recovered in "${structure.name}": grid full`,
|
||||||
actor: user.id,
|
actor: user.id,
|
||||||
structure: structureId,
|
structure: structureId,
|
||||||
targetCollection: "game-structures",
|
targetCollection: "game-structures",
|
||||||
|
|
@ -1146,7 +1146,7 @@ export async function retrieveFromVoid(structureId: number): Promise<GridActionR
|
||||||
error:
|
error:
|
||||||
recovered > 0
|
recovered > 0
|
||||||
? `${recovered} item(s) retrieved from void storage.`
|
? `${recovered} item(s) retrieved from void storage.`
|
||||||
: "No items could be retrieved — grid is full.",
|
: "No items could be retrieved: the grid is full.",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
const message = error instanceof Error ? error.message : "Unknown error";
|
||||||
|
|
@ -1297,11 +1297,11 @@ export async function autoArrangeGrid(structureId: number): Promise<GridActionRe
|
||||||
|
|
||||||
const messages: string[] = [];
|
const messages: string[] = [];
|
||||||
if (recoveredCount > 0) messages.push(`${recoveredCount} item(s) recovered from void.`);
|
if (recoveredCount > 0) messages.push(`${recoveredCount} item(s) recovered from void.`);
|
||||||
if (overflow.length > 0) messages.push(`${overflow.length} item(s) moved to void — grid full.`);
|
if (overflow.length > 0) messages.push(`${overflow.length} item(s) moved to void (grid full).`);
|
||||||
|
|
||||||
await emitGameEvent(payload, {
|
await emitGameEvent(payload, {
|
||||||
type: EventTypes.GridArrange,
|
type: EventTypes.GridArrange,
|
||||||
message: `Arranged grid in "${structure.name}"${messages.length > 0 ? ` — ${messages.join("; ")}` : ""}`,
|
message: `Arranged grid in "${structure.name}"${messages.length > 0 ? `: ${messages.join("; ")}` : ""}`,
|
||||||
actor: user.id,
|
actor: user.id,
|
||||||
structure: structureId,
|
structure: structureId,
|
||||||
targetCollection: "game-structures",
|
targetCollection: "game-structures",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { TruckIcon } from "lucide-react";
|
||||||
import { VehiclesList } from "./VehiclesList";
|
import { VehiclesList } from "./VehiclesList";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Vehicles — Polaris Task Force",
|
title: "Vehicles: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function VehiclesPage() {
|
export default async function VehiclesPage() {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { BellIcon } from "lucide-react";
|
||||||
import { NotificationsInbox } from "@/components/frontend/notifications/NotificationsInbox";
|
import { NotificationsInbox } from "@/components/frontend/notifications/NotificationsInbox";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Notifications — Polaris Task Force",
|
title: "Notifications: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function NotificationsPage() {
|
export default async function NotificationsPage() {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { UsersIcon } from "lucide-react";
|
||||||
import { NpcDirectory } from "@/components/frontend/personnel/NpcDirectory";
|
import { NpcDirectory } from "@/components/frontend/personnel/NpcDirectory";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Contacts — Polaris Task Force",
|
title: "Contacts: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function ContactsPage() {
|
export default async function ContactsPage() {
|
||||||
|
|
@ -25,8 +25,7 @@ export default async function ContactsPage() {
|
||||||
<h1 className="text-lg font-semibold">Contacts</h1>
|
<h1 className="text-lg font-semibold">Contacts</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{npcs.length} contact{npcs.length !== 1 ? "s" : ""} recorded in the theater of
|
{npcs.length} contact{npcs.length !== 1 ? "s" : ""} recorded in the theater of operations.
|
||||||
operations.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,10 @@ import config from "@payload-config";
|
||||||
import { getPayload } from "payload";
|
import { getPayload } from "payload";
|
||||||
import type { GameNpc, MarketListing } from "@/payload-types";
|
import type { GameNpc, MarketListing } from "@/payload-types";
|
||||||
import { TrendingUp } from "lucide-react";
|
import { TrendingUp } from "lucide-react";
|
||||||
import {
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Statistics — Polaris Task Force",
|
title: "Statistics (Polaris Task Force)",
|
||||||
};
|
};
|
||||||
|
|
||||||
interface FactionStatistic {
|
interface FactionStatistic {
|
||||||
|
|
@ -49,10 +43,7 @@ export default async function StatisticsPage() {
|
||||||
payload.find({
|
payload.find({
|
||||||
collection: "market-listings",
|
collection: "market-listings",
|
||||||
where: {
|
where: {
|
||||||
and: [
|
and: [{ isAutoGenerated: { equals: true } }, { status: { equals: "active" } }],
|
||||||
{ isAutoGenerated: { equals: true } },
|
|
||||||
{ status: { equals: "active" } },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
limit: 200,
|
limit: 200,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
|
|
@ -196,10 +187,7 @@ export default async function StatisticsPage() {
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{topVendors.map((vendor) => (
|
{topVendors.map((vendor) => (
|
||||||
<div
|
<div key={vendor.name} className="flex items-center justify-between gap-4">
|
||||||
key={vendor.name}
|
|
||||||
className="flex items-center justify-between gap-4"
|
|
||||||
>
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm font-semibold">{vendor.name}</p>
|
<p className="truncate text-sm font-semibold">{vendor.name}</p>
|
||||||
<p className="truncate text-xs text-muted-foreground">
|
<p className="truncate text-xs text-muted-foreground">
|
||||||
|
|
|
||||||
|
|
@ -654,8 +654,8 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
||||||
<span>{xpLevel.currentLevelXP.toLocaleString()} XP</span>
|
<span>{xpLevel.currentLevelXP.toLocaleString()} XP</span>
|
||||||
{xpLevel.nextLevelXP !== null ? (
|
{xpLevel.nextLevelXP !== null ? (
|
||||||
<span>
|
<span>
|
||||||
{Math.floor(xpLevel.progress)}% — {xpLevel.nextLevelXP.toLocaleString()} XP to
|
{Math.floor(xpLevel.progress)}% ({xpLevel.nextLevelXP.toLocaleString()} XP to next
|
||||||
next level
|
level)
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span>Max level reached</span>
|
<span>Max level reached</span>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { ProjectBoardView } from "@/components/frontend/projects/ProjectBoardVie
|
||||||
import { hasPermission } from "@/utils/access-control/hasPermission";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Board — Polaris Task Force",
|
title: "Board | Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
const KANBAN_STATUSES = [
|
const KANBAN_STATUSES = [
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,10 @@ import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import { richTextToPlaintext } from "@/lib/projects/projectMeta";
|
import { richTextToPlaintext } from "@/lib/projects/projectMeta";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Releases — Polaris Task Force",
|
title: "Releases: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function ReleasesPage({
|
export default async function ReleasesPage({ params }: { params: Promise<{ key: string }> }) {
|
||||||
params,
|
|
||||||
}: {
|
|
||||||
params: Promise<{ key: string }>;
|
|
||||||
}) {
|
|
||||||
const { key } = await params;
|
const { key } = await params;
|
||||||
const headers = await nextHeaders();
|
const headers = await nextHeaders();
|
||||||
const payload = await getPayload({ config });
|
const payload = await getPayload({ config });
|
||||||
|
|
@ -62,7 +58,8 @@ export default async function ReleasesPage({
|
||||||
|
|
||||||
const releases = releaseResult.docs.map((r: any) => {
|
const releases = releaseResult.docs.map((r: any) => {
|
||||||
const releaseTickets = tickets.filter((ticket: any) => {
|
const releaseTickets = tickets.filter((ticket: any) => {
|
||||||
const releaseId = typeof ticket.release === "object" && ticket.release !== null
|
const releaseId =
|
||||||
|
typeof ticket.release === "object" && ticket.release !== null
|
||||||
? ticket.release.id
|
? ticket.release.id
|
||||||
: ticket.release;
|
: ticket.release;
|
||||||
return releaseId === r.id;
|
return releaseId === r.id;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { SprintsView } from "@/components/frontend/projects/SprintsView";
|
||||||
import { hasPermission } from "@/utils/access-control/hasPermission";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Sprints — Polaris Task Force",
|
title: "Sprints: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function SprintsPage({
|
export default async function SprintsPage({
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import { richTextToPlaintext } from "@/lib/projects/projectMeta";
|
import { richTextToPlaintext } from "@/lib/projects/projectMeta";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Projects — Polaris Task Force",
|
title: "Projects: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function ProjectsPage() {
|
export default async function ProjectsPage() {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { getPayload } from "payload";
|
||||||
import { ProjectSearchView } from "@/components/frontend/projects/ProjectSearchView";
|
import { ProjectSearchView } from "@/components/frontend/projects/ProjectSearchView";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Search — Polaris Task Force",
|
title: "Search: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function SearchPage() {
|
export default async function SearchPage() {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import type { AimTrainerStats } from "@/app/(frontend)/qualification/actions";
|
||||||
import { AimTrainerArena } from "@/components/frontend/qualification/AimTrainerArena";
|
import { AimTrainerArena } from "@/components/frontend/qualification/AimTrainerArena";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Qualification Course — Polaris Task Force",
|
title: "Qualification Course: Polaris Task Force",
|
||||||
description: "Complete a timed qualification course to sharpen your marksmanship and earn XP.",
|
description: "Complete a timed qualification course to sharpen your marksmanship and earn XP.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Radio } from "lucide-react";
|
||||||
import { RadioArena } from "@/components/frontend/radio/RadioArena";
|
import { RadioArena } from "@/components/frontend/radio/RadioArena";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Radio Traffic — Polaris Task Force",
|
title: "Radio Traffic (Polaris Task Force)",
|
||||||
description: "Intercept and type incoming radio traffic fast + accurately.",
|
description: "Intercept and type incoming radio traffic fast + accurately.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { UsersIcon } from "lucide-react";
|
||||||
import { RosterView } from "@/components/frontend/roster/RosterView";
|
import { RosterView } from "@/components/frontend/roster/RosterView";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Unit Roster — Polaris Task Force",
|
title: "Unit Roster (Polaris Task Force)",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RosterMember = {
|
export type RosterMember = {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||||
import { isSuperuser } from "@/utils/access-control/hasPermission";
|
import { isSuperuser } from "@/utils/access-control/hasPermission";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Transfers — Polaris Task Force",
|
title: "Transfers: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function TransfersPage() {
|
export default async function TransfersPage() {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import { getScriptForUser } from "@/lib/voiceover";
|
||||||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Record voiceover — Polaris Task Force",
|
title: "Record voiceover: Polaris Task Force",
|
||||||
description: "Read a Polaris Task Force voiceover script and submit a recording.",
|
description: "Read a Polaris Task Force voiceover script and submit a recording.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import { cn } from "@/lib/utils";
|
||||||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Voiceover — Polaris Task Force",
|
title: "Voiceover: Polaris Task Force",
|
||||||
description: "Record voiceover takes for active Polaris Task Force scripts.",
|
description: "Record voiceover takes for active Polaris Task Force scripts.",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { WikiEditor } from "@/components/frontend/wiki/WikiEditor";
|
||||||
import { listTemplates, templateMap } from "@/lib/wiki/service";
|
import { listTemplates, templateMap } from "@/lib/wiki/service";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Edit wiki page — Polaris Task Force",
|
title: "Edit wiki page: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
function templateCssMap(
|
function templateCssMap(
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { RevisionList, type WikiRevisionSummary } from "@/components/frontend/wi
|
||||||
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Wiki history — Polaris Task Force",
|
title: "Wiki history: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
function editorName(editor: WikiRevision["editor"]): string {
|
function editorName(editor: WikiRevision["editor"]): string {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { prepareMarkdown } from "@/lib/wiki/prepare";
|
||||||
import { buildTemplateStyles } from "@/lib/wiki/templateCss";
|
import { buildTemplateStyles } from "@/lib/wiki/templateCss";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Wiki page — Polaris Task Force",
|
title: "Wiki page: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
function relationName(relation: WikiPageDocument["lastEditor"]): string {
|
function relationName(relation: WikiPageDocument["lastEditor"]): string {
|
||||||
|
|
|
||||||
|
|
@ -251,7 +251,7 @@ export async function uploadWikiImage(
|
||||||
overrideAccess: true,
|
overrideAccess: true,
|
||||||
});
|
});
|
||||||
if (!doc.filename) {
|
if (!doc.filename) {
|
||||||
return { success: false, error: "Upload failed — no file was stored." };
|
return { success: false, error: "Upload failed: no file was stored." };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { WikiEditor } from "@/components/frontend/wiki/WikiEditor";
|
||||||
import { listTemplates, templateMap } from "@/lib/wiki/service";
|
import { listTemplates, templateMap } from "@/lib/wiki/service";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "New wiki page — Polaris Task Force",
|
title: "New wiki page (Polaris Task Force)",
|
||||||
};
|
};
|
||||||
|
|
||||||
function templateCssMap(
|
function templateCssMap(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { WikiIndex, type WikiIndexPage } from "@/components/frontend/wiki/WikiIn
|
||||||
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Wiki — Polaris Task Force",
|
title: "Wiki: Polaris Task Force",
|
||||||
};
|
};
|
||||||
|
|
||||||
function relationName(relation: WikiPageDocument["lastEditor"]): string {
|
function relationName(relation: WikiPageDocument["lastEditor"]): string {
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ const resolveTargetMissions = async (
|
||||||
return { error: `No mission matches "${trimmed}" (searched by ID, name, and code name).` };
|
return { error: `No mission matches "${trimmed}" (searched by ID, name, and code name).` };
|
||||||
}
|
}
|
||||||
if (docs.length > 1) {
|
if (docs.length > 1) {
|
||||||
const list = docs.map((m) => `• ${missionLabel(m)} — #${m.id}`).join("\n");
|
const list = docs.map((m) => `• ${missionLabel(m)} (#${m.id})`).join("\n");
|
||||||
return { error: `Multiple missions match "${trimmed}". Be more specific:\n${list}` };
|
return { error: `Multiple missions match "${trimmed}". Be more specific:\n${list}` };
|
||||||
}
|
}
|
||||||
return { mission: docs[0] };
|
return { mission: docs[0] };
|
||||||
|
|
@ -92,20 +92,20 @@ export const remindEvaluationsCommand: BotCommand = {
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
await interaction.editReply(
|
await interaction.editReply(
|
||||||
`**${missionLabel(mission)}** isn't evaluable yet — missions must be completed or past their scheduled start before reminders go out.`,
|
`**${missionLabel(mission)}** isn't evaluable yet: missions must be completed or past their scheduled start before reminders go out.`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sent = await processMission(client, payload, mission);
|
const sent = await processMission(client, payload, mission);
|
||||||
await interaction.editReply(
|
await interaction.editReply(
|
||||||
`Done — ${sent} evaluation reminder DM(s) sent for **${missionLabel(mission)}**.`,
|
`Done: ${sent} evaluation reminder DM(s) sent for **${missionLabel(mission)}**.`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { processed, sent } = await reconcile(client, payload);
|
const { processed, sent } = await reconcile(client, payload);
|
||||||
await interaction.editReply(
|
await interaction.editReply(
|
||||||
`Sweep complete — ${processed} pending mission(s) processed, ${sent} reminder DM(s) sent.`,
|
`Sweep complete: ${processed} pending mission(s) processed, ${sent} reminder DM(s) sent.`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await interaction.editReply(
|
await interaction.editReply(
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,11 @@ const main = async () => {
|
||||||
let shuttingDown = false;
|
let shuttingDown = false;
|
||||||
const shutdown = async (signal: NodeJS.Signals): Promise<void> => {
|
const shutdown = async (signal: NodeJS.Signals): Promise<void> => {
|
||||||
if (shuttingDown) {
|
if (shuttingDown) {
|
||||||
payload.logger.warn(`[Bot] ${signal} received again — forcing exit`);
|
payload.logger.warn(`[Bot] ${signal} received again, forcing exit`);
|
||||||
process.exit(143);
|
process.exit(143);
|
||||||
}
|
}
|
||||||
shuttingDown = true;
|
shuttingDown = true;
|
||||||
payload.logger.info(`[Bot] ${signal} received — shutting down gracefully`);
|
payload.logger.info(`[Bot] ${signal} received, shutting down gracefully`);
|
||||||
try {
|
try {
|
||||||
if (client.isReady()) {
|
if (client.isReady()) {
|
||||||
await client.destroy();
|
await client.destroy();
|
||||||
|
|
|
||||||
|
|
@ -61,11 +61,9 @@ const getReminderMissions = async (payload: Payload): Promise<Mission[]> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatLinks = (links: ReminderLink[], appUrl: string): string => {
|
const formatLinks = (links: ReminderLink[], appUrl: string): string => {
|
||||||
const lines = links.map(
|
const lines = links.map((link) => `• [${link.displayName}](${appUrl}${link.profilePath})`);
|
||||||
(link) => `• [${link.displayName}](${appUrl}${link.profilePath})`,
|
|
||||||
);
|
|
||||||
// Discord embed field values cap at 1024 chars.
|
// Discord embed field values cap at 1024 chars.
|
||||||
return (lines.join("\n").slice(0, 1024) || "—");
|
return lines.join("\n").slice(0, 1024) || "—";
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderReminderEmbed = (
|
const renderReminderEmbed = (
|
||||||
|
|
@ -75,7 +73,7 @@ const renderReminderEmbed = (
|
||||||
appUrl: string,
|
appUrl: string,
|
||||||
): EmbedBuilder => {
|
): EmbedBuilder => {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle(`Leadership feedback requested — ${missionTitle(mission)}`)
|
.setTitle(`Leadership feedback requested: ${missionTitle(mission)}`)
|
||||||
.setColor(EMBED_COLOR)
|
.setColor(EMBED_COLOR)
|
||||||
.setFooter({
|
.setFooter({
|
||||||
text: "Ratings are private and anonymous in aggregate. A leader's results become visible once 3+ members have rated them.",
|
text: "Ratings are private and anonymous in aggregate. A leader's results become visible once 3+ members have rated them.",
|
||||||
|
|
@ -122,7 +120,12 @@ const sendReminderDms = async (
|
||||||
const user = await client.users.fetch(target.discordId as string);
|
const user = await client.users.fetch(target.discordId as string);
|
||||||
await user.send({
|
await user.send({
|
||||||
embeds: [
|
embeds: [
|
||||||
renderReminderEmbed(mission, target.rateLeaders, target.rateSubordinates, botConfig.appUrl),
|
renderReminderEmbed(
|
||||||
|
mission,
|
||||||
|
target.rateLeaders,
|
||||||
|
target.rateSubordinates,
|
||||||
|
botConfig.appUrl,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
sent += 1;
|
sent += 1;
|
||||||
|
|
@ -152,7 +155,7 @@ export const processMission = async (
|
||||||
const { sent, capped } = await sendReminderDms(client, payload, mission);
|
const { sent, capped } = await sendReminderDms(client, payload, mission);
|
||||||
if (capped) {
|
if (capped) {
|
||||||
payload.logger.warn(
|
payload.logger.warn(
|
||||||
`[Bot] Evaluation reminders for mission #${mission.id}: DM cap hit — marker not set, will retry next tick.`,
|
`[Bot] Evaluation reminders for mission #${mission.id}: DM cap hit; marker not set, will retry next tick.`,
|
||||||
);
|
);
|
||||||
return sent;
|
return sent;
|
||||||
}
|
}
|
||||||
|
|
@ -173,10 +176,7 @@ export interface ReconcileSummary {
|
||||||
sent: number;
|
sent: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reconcile = async (
|
export const reconcile = async (client: Client, payload: Payload): Promise<ReconcileSummary> => {
|
||||||
client: Client,
|
|
||||||
payload: Payload,
|
|
||||||
): Promise<ReconcileSummary> => {
|
|
||||||
if (reconcileRunning) return { processed: 0, sent: 0 };
|
if (reconcileRunning) return { processed: 0, sent: 0 };
|
||||||
reconcileRunning = true;
|
reconcileRunning = true;
|
||||||
let processed = 0;
|
let processed = 0;
|
||||||
|
|
|
||||||
|
|
@ -246,10 +246,7 @@ interface ModlistInfo {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveModlist = async (
|
const resolveModlist = async (payload: Payload, mission: Mission): Promise<ModlistInfo | null> => {
|
||||||
payload: Payload,
|
|
||||||
mission: Mission,
|
|
||||||
): Promise<ModlistInfo | null> => {
|
|
||||||
const raw = mission.gameDetails.gameSetup?.requiredMods;
|
const raw = mission.gameDetails.gameSetup?.requiredMods;
|
||||||
if (raw == null || typeof raw === "number") return null;
|
if (raw == null || typeof raw === "number") return null;
|
||||||
const modlist = raw as ModList;
|
const modlist = raw as ModList;
|
||||||
|
|
@ -282,7 +279,7 @@ const renderModlistEmbed = (
|
||||||
kind: "new" | "updated",
|
kind: "new" | "updated",
|
||||||
): EmbedBuilder => {
|
): EmbedBuilder => {
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle(kind === "new" ? `Modlist — ${modlist.name}` : `Modlist updated — ${modlist.name}`)
|
.setTitle(kind === "new" ? `Modlist: ${modlist.name}` : `Modlist updated: ${modlist.name}`)
|
||||||
.setColor(EMBED_COLOR)
|
.setColor(EMBED_COLOR)
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
embed.setDescription(
|
embed.setDescription(
|
||||||
|
|
@ -505,7 +502,8 @@ export const reconcile = async (client: Client, payload: Payload): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const eligible = (await getEligibleMissions(payload)).filter(
|
const eligible = (await getEligibleMissions(payload)).filter(
|
||||||
(mission) => isRollCallDue(mission) || !!mission.discordMessageId || !!mission.discordAttendanceSentAt,
|
(mission) =>
|
||||||
|
isRollCallDue(mission) || !!mission.discordMessageId || !!mission.discordAttendanceSentAt,
|
||||||
);
|
);
|
||||||
for (const mission of eligible) {
|
for (const mission of eligible) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export const linkDiscordAccount = async (
|
||||||
): Promise<{ user: User; wasLinked: boolean }> => {
|
): Promise<{ user: User; wasLinked: boolean }> => {
|
||||||
const user = await findUserByDiscordUsername(payload, discordUsername);
|
const user = await findUserByDiscordUsername(payload, discordUsername);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new SignupError("No account with that Discord username — use /signup to create one.");
|
throw new SignupError("No account with that Discord username. Use /signup to create one.");
|
||||||
}
|
}
|
||||||
if (user.discordId && user.discordId !== discordId) {
|
if (user.discordId && user.discordId !== discordId) {
|
||||||
throw new SignupError(
|
throw new SignupError(
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ export const GameRules: GlobalConfig = {
|
||||||
defaultValue: 60,
|
defaultValue: 60,
|
||||||
admin: {
|
admin: {
|
||||||
description:
|
description:
|
||||||
"Suggested economy tick cadence in minutes, for cron documentation only — the tick itself is triggered externally.",
|
"Suggested economy tick cadence in minutes, for cron documentation only. The tick itself is triggered externally.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,8 @@ export const Missions: CollectionConfig = {
|
||||||
name: "discordAttendanceSentAt",
|
name: "discordAttendanceSentAt",
|
||||||
type: "date",
|
type: "date",
|
||||||
admin: {
|
admin: {
|
||||||
description: "When the attendance roll-call was first posted to Discord (managed by the bot).",
|
description:
|
||||||
|
"When the attendance roll-call was first posted to Discord (managed by the bot).",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -363,7 +364,7 @@ export const Missions: CollectionConfig = {
|
||||||
type: "date",
|
type: "date",
|
||||||
admin: {
|
admin: {
|
||||||
description:
|
description:
|
||||||
"When the mission transitioned to Completed. Set automatically by the lifecycle (auto-complete tick or manual status change) — managed here, not editable in the form.",
|
"When the mission transitioned to Completed. Set automatically by the lifecycle (auto-complete tick or manual status change); managed here, not editable in the form.",
|
||||||
disabled: true,
|
disabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -584,7 +585,11 @@ export const Missions: CollectionConfig = {
|
||||||
defaultValue: "ptf313",
|
defaultValue: "ptf313",
|
||||||
access: {
|
access: {
|
||||||
read: async ({ req }) => {
|
read: async ({ req }) => {
|
||||||
return await hasPermission(req.payload, req.user, "missions:read-sensitive");
|
return await hasPermission(
|
||||||
|
req.payload,
|
||||||
|
req.user,
|
||||||
|
"missions:read-sensitive",
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -633,7 +633,7 @@ export const Structures: CollectionConfig = {
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
admin: {
|
admin: {
|
||||||
description:
|
description:
|
||||||
"When enabled, ONLY the items listed in Allowed Storage can be stored here — everything else is rejected. When disabled, the Allowed Storage list still enforces per-item quantity caps, but other items are permitted up to the mass capacity.",
|
"When enabled, ONLY the items listed in Allowed Storage can be stored here. Everything else is rejected. When disabled, the Allowed Storage list still enforces per-item quantity caps, but other items are permitted up to the mass capacity.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export const Evaluations: CollectionConfig = {
|
||||||
required: true,
|
required: true,
|
||||||
admin: {
|
admin: {
|
||||||
description:
|
description:
|
||||||
"Who submitted this evaluation. Private — only visible to users with evaluations:read.",
|
"Who submitted this evaluation. Private, only visible to users with evaluations:read.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -80,8 +80,7 @@ export const Evaluations: CollectionConfig = {
|
||||||
name: "comment",
|
name: "comment",
|
||||||
type: "textarea",
|
type: "textarea",
|
||||||
admin: {
|
admin: {
|
||||||
description:
|
description: "Private free-text note. Never shown to normal users; managers only.",
|
||||||
"Private free-text note. Never shown to normal users; managers only.",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ export const Roles: CollectionConfig = {
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
description:
|
description:
|
||||||
"Superuser roles bypass ALL permission checks. Use with extreme caution — this grants unrestricted access.",
|
"Superuser roles bypass ALL permission checks. Use with extreme caution: this grants unrestricted access.",
|
||||||
position: "sidebar",
|
position: "sidebar",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -227,7 +227,7 @@ export const Users: CollectionConfig = {
|
||||||
},*/
|
},*/
|
||||||
admin: {
|
admin: {
|
||||||
description:
|
description:
|
||||||
"Legacy role enum — being replaced by the dynamic RBAC system (see 'Role Assignments' below).",
|
"Legacy role enum, being replaced by the dynamic RBAC system (see 'Role Assignments' below).",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ export const WikiTemplates: CollectionConfig = {
|
||||||
admin: {
|
admin: {
|
||||||
language: "css",
|
language: "css",
|
||||||
description:
|
description:
|
||||||
"Optional CSS injected on wiki pages while this template exists. Write plain selectors — they are automatically prefixed with a hash-based scope class (e.g. .ptf-tpl-3afb1c .my-class) so templates never collide with each other or the app shell.",
|
"Optional CSS injected on wiki pages while this template exists. Write plain selectors, which are automatically prefixed with a hash-based scope class (e.g. .ptf-tpl-3afb1c .my-class) so templates never collide with each other or the app shell.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -57,7 +57,7 @@ export const WikiTemplates: CollectionConfig = {
|
||||||
},
|
},
|
||||||
admin: {
|
admin: {
|
||||||
description:
|
description:
|
||||||
"Developer-only. Raw HTML rendered verbatim (sanitized) when this template is transcluded — replaces the markdown body.",
|
"Developer-only. Raw HTML rendered verbatim (sanitized) when this template is transcluded, replacing the markdown body.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export function DiscordUsernameRequestForm({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setValue("");
|
setValue("");
|
||||||
setSuccess("Request submitted — leadership has been notified.");
|
setSuccess("Request submitted. Leadership has been notified.");
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} catch {
|
} catch {
|
||||||
setError("Something went wrong. Please try again.");
|
setError("Something went wrong. Please try again.");
|
||||||
|
|
|
||||||
|
|
@ -314,7 +314,7 @@ export function StaffRoster({
|
||||||
|
|
||||||
{laborCrews.length === 0 ? (
|
{laborCrews.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
No labor crew — add workers via Hire Staff.
|
No labor crew. Add workers via Hire Staff.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
|
@ -329,7 +329,7 @@ export function StaffRoster({
|
||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="font-medium">
|
<p className="font-medium">
|
||||||
{crew.classificationName} — {crew.headcount} hired ·{" "}
|
{crew.classificationName}: {crew.headcount} hired ·{" "}
|
||||||
{crew.requiredHeadcount} required · max {crew.maxHeadcount}
|
{crew.requiredHeadcount} required · max {crew.maxHeadcount}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-mono text-sm tabular-nums text-muted-foreground">
|
<p className="font-mono text-sm tabular-nums text-muted-foreground">
|
||||||
|
|
|
||||||
|
|
@ -190,7 +190,7 @@ export function EodLeaderboard({
|
||||||
</div>
|
</div>
|
||||||
) : rows.length === 0 ? (
|
) : rows.length === 0 ? (
|
||||||
<p className="px-4 py-4 text-sm text-muted-foreground">
|
<p className="px-4 py-4 text-sm text-muted-foreground">
|
||||||
No clearances logged yet — take the first sector!
|
No clearances logged yet. Take the first sector!
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
|
|
@ -218,7 +218,7 @@ export function EodLeaderboard({
|
||||||
)}
|
)}
|
||||||
title={
|
title={
|
||||||
row.isOutsideTopTen
|
row.isOutsideTopTen
|
||||||
? "Operator status — not currently ranked in the top 10"
|
? "Operator status (not currently ranked in the top 10)"
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -238,7 +238,8 @@ export function EodLeaderboard({
|
||||||
unranked
|
unranked
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{row.userId !== null && activeTeams.some((team) => team.id === row.userId) && (
|
{row.userId !== null &&
|
||||||
|
activeTeams.some((team) => team.id === row.userId) && (
|
||||||
<span
|
<span
|
||||||
className="size-2 rounded-full bg-emerald-400"
|
className="size-2 rounded-full bg-emerald-400"
|
||||||
title="Clearing now"
|
title="Clearing now"
|
||||||
|
|
|
||||||
|
|
@ -807,8 +807,8 @@ export function MinesweeperBoard({
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
{phase === "ready" && <span>Awaiting deployment — select a sector.</span>}
|
{phase === "ready" && <span>Awaiting deployment. Select a sector.</span>}
|
||||||
{phase === "playing" && <span>Live sweep — mark every charge before you reveal.</span>}
|
{phase === "playing" && <span>Live sweep: mark every charge before you reveal.</span>}
|
||||||
{phase === "over" && lastRun?.won && (
|
{phase === "over" && lastRun?.won && (
|
||||||
<span>
|
<span>
|
||||||
Sector clear. {config.label} defused in {lastRun.seconds}s.
|
Sector clear. {config.label} defused in {lastRun.seconds}s.
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ export function FlappyLeaderboard({ refreshKey = 0, currentUserId, activePilots
|
||||||
</div>
|
</div>
|
||||||
) : rows.length === 0 ? (
|
) : rows.length === 0 ? (
|
||||||
<p className="px-4 py-4 text-sm text-muted-foreground">
|
<p className="px-4 py-4 text-sm text-muted-foreground">
|
||||||
No pilots on the board yet — be the first to pass a pipe!
|
No pilots on the board yet. Be the first to pass a pipe!
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
|
|
@ -156,7 +156,7 @@ export function FlappyLeaderboard({ refreshKey = 0, currentUserId, activePilots
|
||||||
row.isCurrentUser && "bg-accent/40",
|
row.isCurrentUser && "bg-accent/40",
|
||||||
row.isOutsideTopTen && "text-muted-foreground opacity-70",
|
row.isOutsideTopTen && "text-muted-foreground opacity-70",
|
||||||
)}
|
)}
|
||||||
title={row.isOutsideTopTen ? "Pilot status — not currently ranked in the top 10" : undefined}
|
title={row.isOutsideTopTen ? "Pilot status: not currently ranked in the top 10" : undefined}
|
||||||
>
|
>
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<span className={rankClass(i)}>{row.isOutsideTopTen ? "—" : i + 1}</span>
|
<span className={rankClass(i)}>{row.isOutsideTopTen ? "—" : i + 1}</span>
|
||||||
|
|
|
||||||
|
|
@ -246,7 +246,7 @@ export function CreateSideOpDialog({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isEdit && !defaultStartDateTime) {
|
if (!isEdit && !defaultStartDateTime) {
|
||||||
setError("Missing start date — schedule from a Friday on the calendar.");
|
setError("Missing start date: schedule from a Friday on the calendar.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const startDateIso = defaultStartDateTime ?? "";
|
const startDateIso = defaultStartDateTime ?? "";
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ export function FridayCalendar({
|
||||||
<ItemContent>
|
<ItemContent>
|
||||||
{entry.missions.length === 0 ? (
|
{entry.missions.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Nothing scheduled yet — claim this Friday.
|
Nothing scheduled yet. Claim this Friday.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ export function MissionAttendance({
|
||||||
{
|
{
|
||||||
id: `mission:${missionId}:rsvp-yes`,
|
id: `mission:${missionId}:rsvp-yes`,
|
||||||
group: "Mission RSVP",
|
group: "Mission RSVP",
|
||||||
label: "RSVP — Yes",
|
label: "RSVP: Yes",
|
||||||
icon: CheckIcon,
|
icon: CheckIcon,
|
||||||
keywords: ["attend", "going", "yes", "rsvp"],
|
keywords: ["attend", "going", "yes", "rsvp"],
|
||||||
onSelect: () => void handleSet("yes"),
|
onSelect: () => void handleSet("yes"),
|
||||||
|
|
@ -97,7 +97,7 @@ export function MissionAttendance({
|
||||||
{
|
{
|
||||||
id: `mission:${missionId}:rsvp-tentative`,
|
id: `mission:${missionId}:rsvp-tentative`,
|
||||||
group: "Mission RSVP",
|
group: "Mission RSVP",
|
||||||
label: "RSVP — Tentative",
|
label: "RSVP: Tentative",
|
||||||
icon: MinusIcon,
|
icon: MinusIcon,
|
||||||
keywords: ["maybe", "tentative", "unsure", "rsvp"],
|
keywords: ["maybe", "tentative", "unsure", "rsvp"],
|
||||||
onSelect: () => void handleSet("tentative"),
|
onSelect: () => void handleSet("tentative"),
|
||||||
|
|
@ -105,7 +105,7 @@ export function MissionAttendance({
|
||||||
{
|
{
|
||||||
id: `mission:${missionId}:rsvp-no`,
|
id: `mission:${missionId}:rsvp-no`,
|
||||||
group: "Mission RSVP",
|
group: "Mission RSVP",
|
||||||
label: "RSVP — No",
|
label: "RSVP: No",
|
||||||
icon: XIcon,
|
icon: XIcon,
|
||||||
keywords: ["decline", "not going", "no", "rsvp"],
|
keywords: ["decline", "not going", "no", "rsvp"],
|
||||||
onSelect: () => void handleSet("no"),
|
onSelect: () => void handleSet("no"),
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ export function SideOpNarrativeFields({ value, onChange }: SideOpNarrativeFields
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-5">
|
<div className="grid gap-5">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Plain text only — each line becomes a paragraph. Rich formatting written in the admin panel
|
Plain text only. Each line becomes a paragraph. Rich formatting written in the admin panel
|
||||||
is flattened if you save here.
|
is flattened if you save here.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ export function WardrobePanel({ userName, loadouts, equippedLoadout, items }: Wa
|
||||||
|
|
||||||
{!equippedLoadout && (
|
{!equippedLoadout && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Choose a loadout above — it becomes the active loadout that the wardrobe
|
Choose a loadout above. It becomes the active loadout that the wardrobe
|
||||||
reflects.
|
reflects.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ export function ResourceCard({ resource }: ResourceCardProps) {
|
||||||
</div>
|
</div>
|
||||||
</ItemContent>
|
</ItemContent>
|
||||||
<ItemDescription>
|
<ItemDescription>
|
||||||
{truncatedDescription ?? `${resource.type} resource — ${resource.unitOfMeasure}`}
|
{truncatedDescription ?? `${resource.type} resource (${resource.unitOfMeasure})`}
|
||||||
</ItemDescription>
|
</ItemDescription>
|
||||||
<span className="text-xs text-muted-foreground group-hover:text-primary transition-colors flex items-center gap-1">
|
<span className="text-xs text-muted-foreground group-hover:text-primary transition-colors flex items-center gap-1">
|
||||||
View details <ChevronRightIcon className="size-3" />
|
View details <ChevronRightIcon className="size-3" />
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
|
||||||
{
|
{
|
||||||
id: "shipments:filter-all",
|
id: "shipments:filter-all",
|
||||||
group: "Shipments",
|
group: "Shipments",
|
||||||
label: "Filter — All Shipments",
|
label: "Filter: All Shipments",
|
||||||
icon: ListFilterIcon,
|
icon: ListFilterIcon,
|
||||||
keywords: ["filter", "all", "shipments"],
|
keywords: ["filter", "all", "shipments"],
|
||||||
onSelect: () => setFilter(null),
|
onSelect: () => setFilter(null),
|
||||||
|
|
@ -42,7 +42,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
|
||||||
{
|
{
|
||||||
id: "shipments:filter-active",
|
id: "shipments:filter-active",
|
||||||
group: "Shipments",
|
group: "Shipments",
|
||||||
label: "Filter — Active Shipments",
|
label: "Filter: Active Shipments",
|
||||||
icon: ClockIcon,
|
icon: ClockIcon,
|
||||||
keywords: ["filter", "active", "in transit", "pending"],
|
keywords: ["filter", "active", "in transit", "pending"],
|
||||||
onSelect: () => setFilter("active"),
|
onSelect: () => setFilter("active"),
|
||||||
|
|
@ -50,7 +50,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
|
||||||
{
|
{
|
||||||
id: "shipments:filter-completed",
|
id: "shipments:filter-completed",
|
||||||
group: "Shipments",
|
group: "Shipments",
|
||||||
label: "Filter — Completed Shipments",
|
label: "Filter: Completed Shipments",
|
||||||
icon: CheckCircleIcon,
|
icon: CheckCircleIcon,
|
||||||
keywords: ["filter", "completed", "arrived", "done"],
|
keywords: ["filter", "completed", "arrived", "done"],
|
||||||
onSelect: () => setFilter("completed"),
|
onSelect: () => setFilter("completed"),
|
||||||
|
|
@ -58,7 +58,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
|
||||||
{
|
{
|
||||||
id: "shipments:filter-cancelled",
|
id: "shipments:filter-cancelled",
|
||||||
group: "Shipments",
|
group: "Shipments",
|
||||||
label: "Filter — Cancelled Shipments",
|
label: "Filter: Cancelled Shipments",
|
||||||
icon: XCircleIcon,
|
icon: XCircleIcon,
|
||||||
keywords: ["filter", "cancelled", "failed"],
|
keywords: ["filter", "cancelled", "failed"],
|
||||||
onSelect: () => setFilter("cancelled"),
|
onSelect: () => setFilter("cancelled"),
|
||||||
|
|
@ -66,7 +66,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
|
||||||
{
|
{
|
||||||
id: "shipments:filter-stranded",
|
id: "shipments:filter-stranded",
|
||||||
group: "Shipments",
|
group: "Shipments",
|
||||||
label: "Filter — Stranded Shipments",
|
label: "Filter: Stranded Shipments",
|
||||||
icon: TruckIcon,
|
icon: TruckIcon,
|
||||||
keywords: ["filter", "stranded", "stuck"],
|
keywords: ["filter", "stranded", "stuck"],
|
||||||
onSelect: () => setFilter("stranded"),
|
onSelect: () => setFilter("stranded"),
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,7 @@ export function CreateListingDialog({
|
||||||
value={String(s.assetId)}
|
value={String(s.assetId)}
|
||||||
disabled={!s.tradeable || !s.isLive}
|
disabled={!s.tradeable || !s.isLive}
|
||||||
>
|
>
|
||||||
{s.name} — {s.quantity.toLocaleString()}×
|
{s.name} ({s.quantity.toLocaleString()}×)
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
||||||
const boughtQty = thread?.quantity ?? negotiation?.quantity ?? stock;
|
const boughtQty = thread?.quantity ?? negotiation?.quantity ?? stock;
|
||||||
setAcceptedAt(Date.now());
|
setAcceptedAt(Date.now());
|
||||||
toast.success("Deal closed", {
|
toast.success("Deal closed", {
|
||||||
description: `Sold ${boughtQty}× at ${fmt(agreed)} each — total ${fmt(agreed * boughtQty)}. The items are in your locker.`,
|
description: `Sold ${boughtQty}× at ${fmt(agreed)} each, for a total of ${fmt(agreed * boughtQty)}. The items are in your locker.`,
|
||||||
});
|
});
|
||||||
window.setTimeout(() => router.refresh(), MIN_ACCEPT_VISIBLE_MS);
|
window.setTimeout(() => router.refresh(), MIN_ACCEPT_VISIBLE_MS);
|
||||||
setBusy(null);
|
setBusy(null);
|
||||||
|
|
@ -168,7 +168,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
||||||
setAcceptedAt(Date.now());
|
setAcceptedAt(Date.now());
|
||||||
const dealQty = data.quantity ?? stock;
|
const dealQty = data.quantity ?? stock;
|
||||||
toast.success("Deal closed", {
|
toast.success("Deal closed", {
|
||||||
description: `The vendor accepted your offer of ${fmt(data.amount ?? 0)} each for ${dealQty}× — total ${fmt((data.amount ?? 0) * dealQty)}.`,
|
description: `The vendor accepted your offer of ${fmt(data.amount ?? 0)} each for ${dealQty}×, for a total of ${fmt((data.amount ?? 0) * dealQty)}.`,
|
||||||
});
|
});
|
||||||
window.setTimeout(() => router.refresh(), MIN_ACCEPT_VISIBLE_MS);
|
window.setTimeout(() => router.refresh(), MIN_ACCEPT_VISIBLE_MS);
|
||||||
setBusy(null);
|
setBusy(null);
|
||||||
|
|
@ -236,7 +236,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Negotiate — {assetName}</DialogTitle>
|
<DialogTitle>Negotiate: {assetName}</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{isVendor ? "Haggle with the vendor." : "Send an offer to the seller."} Asking{" "}
|
{isVendor ? "Haggle with the vendor." : "Send an offer to the seller."} Asking{" "}
|
||||||
<span className="font-mono">{fmt(listing.price ?? 0)}</span> per unit.
|
<span className="font-mono">{fmt(listing.price ?? 0)}</span> per unit.
|
||||||
|
|
@ -304,7 +304,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
The {isVendor ? "vendor" : "seller"} counteroffers{" "}
|
The {isVendor ? "vendor" : "seller"} counteroffers{" "}
|
||||||
<span className="font-mono font-semibold">{fmt(negotiation?.amount ?? 0)}</span>{" "}
|
<span className="font-mono font-semibold">{fmt(negotiation?.amount ?? 0)}</span>{" "}
|
||||||
each for {negotiation?.quantity ?? stock}× — total{" "}
|
each for {negotiation?.quantity ?? stock}×, for a total of{" "}
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
{fmt((negotiation?.amount ?? 0) * (negotiation?.quantity ?? stock))}
|
{fmt((negotiation?.amount ?? 0) * (negotiation?.quantity ?? stock))}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -331,7 +331,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
||||||
</Badge>
|
</Badge>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Sold at <span className="font-mono">{fmt(outcome.price)}</span> each for{" "}
|
Sold at <span className="font-mono">{fmt(outcome.price)}</span> each for{" "}
|
||||||
{negotiation?.quantity ?? stock}× — total{" "}
|
{negotiation?.quantity ?? stock}×, for a total of{" "}
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
{fmt(outcome.price * (negotiation?.quantity ?? stock))}
|
{fmt(outcome.price * (negotiation?.quantity ?? stock))}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ function NegotiationRow({
|
||||||
if (key === "accept") {
|
if (key === "accept") {
|
||||||
const qty = negotiation.quantity ?? listing?.quantity ?? 1;
|
const qty = negotiation.quantity ?? listing?.quantity ?? 1;
|
||||||
toast.success("Deal closed", {
|
toast.success("Deal closed", {
|
||||||
description: `Sold ${qty}× at ${fmt(negotiation.amount ?? 0)} each — total ${fmt((negotiation.amount ?? 0) * qty)}.`,
|
description: `Sold ${qty}× at ${fmt(negotiation.amount ?? 0)} each, total ${fmt((negotiation.amount ?? 0) * qty)}.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export function NpcChat({ listing, negotiation, currencyLabel }: NpcChatProps) {
|
||||||
<div className="flex items-center gap-2 border-b px-3 py-2">
|
<div className="flex items-center gap-2 border-b px-3 py-2">
|
||||||
<StoreIcon className="size-4 text-muted-foreground" />
|
<StoreIcon className="size-4 text-muted-foreground" />
|
||||||
<span className="text-xs font-medium text-muted-foreground">Vendor</span>
|
<span className="text-xs font-medium text-muted-foreground">Vendor</span>
|
||||||
<span className="text-xs text-muted-foreground">— {assetName}</span>
|
<span className="text-xs text-muted-foreground">{assetName}</span>
|
||||||
</div>
|
</div>
|
||||||
<ChatBubbleList bubbles={bubbles} buyerLabel="You" sellerLabel="Vendor" />
|
<ChatBubbleList bubbles={bubbles} buyerLabel="You" sellerLabel="Vendor" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -148,16 +148,16 @@ export function AimTrainerArena({
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium text-foreground">Acquire</span>
|
<span className="font-medium text-foreground">Acquire</span>
|
||||||
<span>
|
<span>
|
||||||
Up to {AIM_TRAINER_DIFFICULTIES[difficulty].maxConcurrentTargets} hostile
|
Up to {AIM_TRAINER_DIFFICULTIES[difficulty].maxConcurrentTargets} hostile silhouettes
|
||||||
silhouettes are active at once, plus an occasional blue civilian that never counts
|
are active at once, plus an occasional blue civilian that never counts toward the
|
||||||
toward the limit. Targets rebound off each other and the range walls.
|
limit. Targets rebound off each other and the range walls.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium text-foreground">Prioritize</span>
|
<span className="font-medium text-foreground">Prioritize</span>
|
||||||
<span>
|
<span>
|
||||||
Headshots score 250 points. Torso hits score 100 points. Blue civilians cost 100
|
Headshots score 250 points. Torso hits score 100 points. Blue civilians cost 100
|
||||||
points — never shoot them.
|
points, so never shoot them.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
|
|
||||||
|
|
@ -624,7 +624,7 @@ export function AimTrainerGame({
|
||||||
Ready on the line
|
Ready on the line
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
Acquire every silhouette before it clears the range — and never shoot the blue
|
Acquire every silhouette before it clears the range, and never shoot the blue
|
||||||
civilians.
|
civilians.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ export function AimTrainerLeaderboard({
|
||||||
</div>
|
</div>
|
||||||
) : rows.length === 0 ? (
|
) : rows.length === 0 ? (
|
||||||
<p className="px-4 py-4 text-sm text-muted-foreground">
|
<p className="px-4 py-4 text-sm text-muted-foreground">
|
||||||
No scores logged for this lane yet — take the first qualification slot.
|
No scores logged for this lane yet. Take the first qualification slot.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
|
|
@ -172,7 +172,7 @@ export function AimTrainerLeaderboard({
|
||||||
)}
|
)}
|
||||||
title={
|
title={
|
||||||
row.isOutsideTopTen
|
row.isOutsideTopTen
|
||||||
? "Operator status — not currently ranked in the top 10"
|
? "Operator status: not currently ranked in the top 10"
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,7 @@ export function ManageStorageDialog({ structureId, open, onOpenChange }: ManageS
|
||||||
const vType = typeof v.type === "object" ? v.type : null;
|
const vType = typeof v.type === "object" ? v.type : null;
|
||||||
return (
|
return (
|
||||||
<SelectItem key={v.id} value={String(v.id)}>
|
<SelectItem key={v.id} value={String(v.id)}>
|
||||||
{v.name} ({vType?.name ?? "Unknown"}) — {v.currentFuel?.toFixed(0)} fuel
|
{v.name} ({vType?.name ?? "Unknown"}), {v.currentFuel?.toFixed(0)} fuel
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -683,7 +683,7 @@ export function ManageStorageDialog({ structureId, open, onOpenChange }: ManageS
|
||||||
{cap !== undefined && (
|
{cap !== undefined && (
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{" "}
|
{" "}
|
||||||
— {cap.toLocaleString()} max
|
({cap.toLocaleString()} max)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
@ -868,7 +868,7 @@ export function ManageStorageDialog({ structureId, open, onOpenChange }: ManageS
|
||||||
if (!isResourceWhitelisted(destType, resId)) {
|
if (!isResourceWhitelisted(destType, resId)) {
|
||||||
return (
|
return (
|
||||||
<span className="text-xs text-amber-400">
|
<span className="text-xs text-amber-400">
|
||||||
Destination only accepts items on its allowed storage list — this
|
Destination only accepts items on its allowed storage list. This
|
||||||
resource is not permitted.
|
resource is not permitted.
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export function StorageRules({
|
||||||
<div className="flex items-center gap-2 p-2.5 rounded-md border border-amber-500/30 bg-amber-500/5 text-xs text-amber-400">
|
<div className="flex items-center gap-2 p-2.5 rounded-md border border-amber-500/30 bg-amber-500/5 text-xs text-amber-400">
|
||||||
<ListRestartIcon className="size-3.5 shrink-0" />
|
<ListRestartIcon className="size-3.5 shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
Whitelist mode — only the items listed under "Allowed" can be stored here. All other
|
Whitelist mode: only the items listed under "Allowed" can be stored here. All other
|
||||||
items are rejected.
|
items are rejected.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ export function StructureCard({ structure }: StructureCardProps) {
|
||||||
</ItemContent>
|
</ItemContent>
|
||||||
<ItemDescription>
|
<ItemDescription>
|
||||||
{resourceCount === 0
|
{resourceCount === 0
|
||||||
? "Empty — no resources stored."
|
? "Empty: no resources stored."
|
||||||
: `${resourceCount} resource type${resourceCount !== 1 ? "s" : ""} stored`}
|
: `${resourceCount} resource type${resourceCount !== 1 ? "s" : ""} stored`}
|
||||||
</ItemDescription>
|
</ItemDescription>
|
||||||
<span className="text-xs text-muted-foreground group-hover:text-primary transition-colors flex items-center gap-1">
|
<span className="text-xs text-muted-foreground group-hover:text-primary transition-colors flex items-center gap-1">
|
||||||
|
|
|
||||||
|
|
@ -210,7 +210,7 @@ export function StructureStorageView({ structure }: StructureStorageViewProps) {
|
||||||
</ItemTitle>
|
</ItemTitle>
|
||||||
<ItemDescription className="flex items-center gap-2">
|
<ItemDescription className="flex items-center gap-2">
|
||||||
<span>
|
<span>
|
||||||
{voidStorageItems.length} item{voidStorageItems.length !== 1 ? "s" : ""} — these no
|
{voidStorageItems.length} item{voidStorageItems.length !== 1 ? "s" : ""}: these no
|
||||||
longer fit in the grid.
|
longer fit in the grid.
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -370,12 +370,12 @@ export function TeleprompterReader({ script }: { readonly script: string }) {
|
||||||
<div className="flex flex-col gap-1 rounded-md border border-border bg-background/50 p-3 text-xs text-muted-foreground">
|
<div className="flex flex-col gap-1 rounded-md border border-border bg-background/50 p-3 text-xs text-muted-foreground">
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium text-foreground">How to read:</span> read every word shown
|
<span className="font-medium text-foreground">How to read:</span> read every word shown
|
||||||
in large text — underlined and plain alike.
|
in large text, underlined and plain alike.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The small colored labels above the text (
|
The small colored labels above the text (
|
||||||
<span className="italic text-sky-300/80">like this</span>) are directions for how to
|
<span className="italic text-sky-300/80">like this</span>) are directions for how to
|
||||||
speak — never read them aloud. Each underlined stretch is the wording its label above
|
speak, never read them aloud. Each underlined stretch is the wording its label above
|
||||||
applies to.
|
applies to.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ export function MarkdownGuide() {
|
||||||
|
|
||||||
<GuideSection title="Footnotes & citations">
|
<GuideSection title="Footnotes & citations">
|
||||||
<Example>Text.[^1]</Example>
|
<Example>Text.[^1]</Example>
|
||||||
<Example>[^1]: Footnote or citation — source, URL</Example>
|
<Example>[^1]: Footnote or citation (source, URL)</Example>
|
||||||
</GuideSection>
|
</GuideSection>
|
||||||
|
|
||||||
<GuideSection title="Layout">
|
<GuideSection title="Layout">
|
||||||
|
|
@ -98,12 +98,12 @@ Right column content
|
||||||
</GuideSection>
|
</GuideSection>
|
||||||
|
|
||||||
<GuideSection title="Keybinds">
|
<GuideSection title="Keybinds">
|
||||||
<Example>Ctrl/Cmd+B — bold</Example>
|
<Example>Ctrl/Cmd+B: bold</Example>
|
||||||
<Example>Ctrl/Cmd+I — italic</Example>
|
<Example>Ctrl/Cmd+I: italic</Example>
|
||||||
<Example>Ctrl/Cmd+Shift+X — strikethrough</Example>
|
<Example>Ctrl/Cmd+Shift+X: strikethrough</Example>
|
||||||
<Example>Ctrl/Cmd+Shift+H — heading</Example>
|
<Example>Ctrl/Cmd+Shift+H: heading</Example>
|
||||||
<Example>Ctrl/Cmd+K — open wikilink dialog</Example>
|
<Example>Ctrl/Cmd+K: open wikilink dialog</Example>
|
||||||
<Example>Ctrl/Cmd+E — inline code</Example>
|
<Example>Ctrl/Cmd+E: inline code</Example>
|
||||||
</GuideSection>
|
</GuideSection>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export async function markStaleServersOffline(
|
||||||
});
|
});
|
||||||
await emitGameEvent(payload, {
|
await emitGameEvent(payload, {
|
||||||
type: EventTypes.ServerOffline,
|
type: EventTypes.ServerOffline,
|
||||||
message: `Server "${server.name}" (${server.serverId}) marked offline — no heartbeat for ${Math.round(
|
message: `Server "${server.name}" (${server.serverId}) marked offline: no heartbeat for ${Math.round(
|
||||||
SERVER_OFFLINE_AFTER_MS / 1000,
|
SERVER_OFFLINE_AFTER_MS / 1000,
|
||||||
)}s.`,
|
)}s.`,
|
||||||
targetCollection: "game-servers",
|
targetCollection: "game-servers",
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export async function processPlayerKill(
|
||||||
const user = users.docs[0];
|
const user = users.docs[0];
|
||||||
if (!user) {
|
if (!user) {
|
||||||
payload.logger.warn(
|
payload.logger.warn(
|
||||||
`[KillTracker] No user found for Steam ID ${kill.killerSteamId} — kill not recorded.`,
|
`[KillTracker] No user found for Steam ID ${kill.killerSteamId}; kill not recorded.`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +71,7 @@ export async function processPlayerKill(
|
||||||
const profile = profiles.docs[0];
|
const profile = profiles.docs[0];
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
payload.logger.warn(
|
payload.logger.warn(
|
||||||
`[KillTracker] No profile found for user ${user.id} (${user.username}) — kill not recorded.`,
|
`[KillTracker] No profile found for user ${user.id} (${user.username}); kill not recorded.`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@ export async function applyTransaction(
|
||||||
type,
|
type,
|
||||||
amount: -totalDebit,
|
amount: -totalDebit,
|
||||||
balanceAfter: fromNewBalance,
|
balanceAfter: fromNewBalance,
|
||||||
memo: memo ?? `${type} — out`,
|
memo: memo ?? `${type}: out`,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -249,7 +249,7 @@ export async function applyTransaction(
|
||||||
type,
|
type,
|
||||||
amount,
|
amount,
|
||||||
balanceAfter: toNewBalance,
|
balanceAfter: toNewBalance,
|
||||||
memo: memo ?? `${type} — in`,
|
memo: memo ?? `${type}: in`,
|
||||||
timestamp,
|
timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export async function hireStaff(
|
||||||
): Promise<NpcStaffing> {
|
): Promise<NpcStaffing> {
|
||||||
await assertStaffHiringEnabled(payload);
|
await assertStaffHiringEnabled(payload);
|
||||||
if (input.positionType === "labor") {
|
if (input.positionType === "labor") {
|
||||||
throw new Error("Basic labor is tracked by headcount — use the labor count control instead.");
|
throw new Error("Basic labor is tracked by headcount. Use the labor count control instead.");
|
||||||
}
|
}
|
||||||
const positionType = staffingPositionType(input.positionType);
|
const positionType = staffingPositionType(input.positionType);
|
||||||
const npc = await payload
|
const npc = await payload
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ async function processSalaries(
|
||||||
: staffing.positionTitle;
|
: staffing.positionTitle;
|
||||||
const memo = isLabor
|
const memo = isLabor
|
||||||
? `Labor: ${headcount} ${classificationLabel}(s) @ ${unitSalary}`
|
? `Labor: ${headcount} ${classificationLabel}(s) @ ${unitSalary}`
|
||||||
: `Salary: ${staffing.positionTitle} — ${npcName}`;
|
: `Salary: ${staffing.positionTitle} (${npcName})`;
|
||||||
try {
|
try {
|
||||||
await applyTransaction(payload, {
|
await applyTransaction(payload, {
|
||||||
type: "salary",
|
type: "salary",
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ export async function autoCompleteMissions(
|
||||||
});
|
});
|
||||||
await emitGameEvent(payload, {
|
await emitGameEvent(payload, {
|
||||||
type: EventTypes.MissionAutoComplete,
|
type: EventTypes.MissionAutoComplete,
|
||||||
message: `Mission "${mission.name}"${mission.codeName ? ` (${mission.codeName})` : ""} auto-completed — the scheduled day has passed.`,
|
message: `Mission "${mission.name}"${mission.codeName ? ` (${mission.codeName})` : ""} auto-completed: the scheduled day has passed.`,
|
||||||
targetCollection: "missions",
|
targetCollection: "missions",
|
||||||
targetId: mission.id,
|
targetId: mission.id,
|
||||||
data: { previousStatus },
|
data: { previousStatus },
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ type DialogueLine = (amount: string, asking: string) => string;
|
||||||
const LINES: Record<NpcDialogueKind, DialogueLine[]> = {
|
const LINES: Record<NpcDialogueKind, DialogueLine[]> = {
|
||||||
firm: [
|
firm: [
|
||||||
(amount) => `I'm not going any lower than ${amount}. Take it or leave it.`,
|
(amount) => `I'm not going any lower than ${amount}. Take it or leave it.`,
|
||||||
(amount) => `That's my final price — ${amount}. I've got other buyers knocking.`,
|
(amount) => `That's my final price: ${amount}. I've got other buyers knocking.`,
|
||||||
(amount) => `I'm holding at ${amount}. My prices are already fair.`,
|
(amount) => `I'm holding at ${amount}. My prices are already fair.`,
|
||||||
],
|
],
|
||||||
lowball: [
|
lowball: [
|
||||||
(amount) => `That's an insult! I'll come down to ${amount} — not a fraction less.`,
|
(amount) => `That's an insult! I'll come down to ${amount}, not a fraction less.`,
|
||||||
(amount) => `You're joking, right? ${amount} and not a fraction less.`,
|
(amount) => `You're joking, right? ${amount} and not a fraction less.`,
|
||||||
(amount) => `I paid more than that for lunch! ${amount} — final offer.`,
|
(amount) => `I paid more than that for lunch! ${amount}, final offer.`,
|
||||||
],
|
],
|
||||||
backward: [
|
backward: [
|
||||||
(amount) => `Now you're offering less than before? I'm holding firm at ${amount}.`,
|
(amount) => `Now you're offering less than before? I'm holding firm at ${amount}.`,
|
||||||
|
|
@ -26,22 +26,22 @@ const LINES: Record<NpcDialogueKind, DialogueLine[]> = {
|
||||||
concede: [
|
concede: [
|
||||||
(amount) => `Hmm, not sure about that... how about ${amount}?`,
|
(amount) => `Hmm, not sure about that... how about ${amount}?`,
|
||||||
(amount) => `I could stretch to ${amount}... if we're serious about this.`,
|
(amount) => `I could stretch to ${amount}... if we're serious about this.`,
|
||||||
(amount) => `Let's not waste each other's time — ${amount}.`,
|
(amount) => `Let's not waste each other's time. ${amount}.`,
|
||||||
(amount) => `Alright, I'll bend a little. ${amount}?`,
|
(amount) => `Alright, I'll bend a little. ${amount}?`,
|
||||||
],
|
],
|
||||||
good: [
|
good: [
|
||||||
() => "Now we're talking. That's a fair price — you've got a deal.",
|
() => "Now we're talking. That's a fair price, and you've got a deal.",
|
||||||
() => "You drive a hard bargain, but that's a price I can live with.",
|
() => "You drive a hard bargain, but that's a price I can live with.",
|
||||||
() => "That works for me. A man's gotta make a living, after all.",
|
() => "That works for me. A man's gotta make a living, after all.",
|
||||||
],
|
],
|
||||||
overpay: [
|
overpay: [
|
||||||
() => "Wait — more than I asked? Sold! You won't regret it.",
|
() => "Wait, more than I asked? Sold! You won't regret it.",
|
||||||
() => "Generous! I'd have taken less, but I won't argue. It's yours.",
|
() => "Generous! I'd have taken less, but I won't argue. It's yours.",
|
||||||
() => "Whoa, okay! Not going to talk you out of that one. Done deal.",
|
() => "Whoa, okay! Not going to talk you out of that one. Done deal.",
|
||||||
],
|
],
|
||||||
closed: [
|
closed: [
|
||||||
(_amount, asking) => `Alright, that's enough haggling. Buy it at ${asking} or not at all.`,
|
(_amount, asking) => `Alright, that's enough haggling. Buy it at ${asking} or not at all.`,
|
||||||
(_amount, asking) => `I'm done negotiating. The price is ${asking} — take it or leave it.`,
|
(_amount, asking) => `I'm done negotiating. The price is ${asking}, take it or leave it.`,
|
||||||
],
|
],
|
||||||
accept: [
|
accept: [
|
||||||
() => "I think that price is acceptable. Let's shake on it.",
|
() => "I think that price is acceptable. Let's shake on it.",
|
||||||
|
|
|
||||||
|
|
@ -102,12 +102,12 @@ export function storageViolationMessage(resourceName: string, check: StorageRule
|
||||||
case "prohibited":
|
case "prohibited":
|
||||||
return `This resource is prohibited from storage at this structure.`;
|
return `This resource is prohibited from storage at this structure.`;
|
||||||
case "not_whitelisted":
|
case "not_whitelisted":
|
||||||
return `This structure only accepts items on its allowed storage list — "${resourceName}" is not permitted.`;
|
return `This structure only accepts items on its allowed storage list: "${resourceName}" is not permitted.`;
|
||||||
case "over_cap": {
|
case "over_cap": {
|
||||||
const available = Math.max(0, (check.cap ?? 0) - (check.currentStored ?? 0));
|
const available = Math.max(0, (check.cap ?? 0) - (check.currentStored ?? 0));
|
||||||
return available <= 0
|
return available <= 0
|
||||||
? `${resourceName} is at its storage allowance here (${check.cap?.toLocaleString()}). No more can be stored.`
|
? `${resourceName} is at its storage allowance here (${check.cap?.toLocaleString()}). No more can be stored.`
|
||||||
: `${resourceName} is limited to ${check.cap?.toLocaleString()} units here — you can add at most ${available.toLocaleString()} more.`;
|
: `${resourceName} is limited to ${check.cap?.toLocaleString()} units here. You can add at most ${available.toLocaleString()} more.`;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return `This resource cannot be stored here.`;
|
return `This resource cannot be stored here.`;
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export const script = async (config: SanitizedConfig) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.disabled) {
|
if (result.disabled) {
|
||||||
log.info("[Economy Tick] Master gate is off — nothing processed.");
|
log.info("[Economy Tick] Master gate is off, nothing processed.");
|
||||||
} else {
|
} else {
|
||||||
await emitEconomyEvents(payload, result);
|
await emitEconomyEvents(payload, result);
|
||||||
log.info(
|
log.info(
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export const script = async (config: SanitizedConfig) => {
|
||||||
await emitGameEvent(payload, {
|
await emitGameEvent(payload, {
|
||||||
type: EventTypes.MarketListingExpire,
|
type: EventTypes.MarketListingExpire,
|
||||||
message: `Listing for ${listing.quantity}× ${assetName} expired${
|
message: `Listing for ${listing.quantity}× ${assetName} expired${
|
||||||
isUserListing ? " — stock returned to seller" : ""
|
isUserListing ? ", stock returned to seller" : ""
|
||||||
}`,
|
}`,
|
||||||
actor: sellerId ?? undefined,
|
actor: sellerId ?? undefined,
|
||||||
targetCollection: "market-listings",
|
targetCollection: "market-listings",
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ export async function processShipmentTick(
|
||||||
|
|
||||||
await emitGameEvent(payload, {
|
await emitGameEvent(payload, {
|
||||||
type: EventTypes.ShipmentFail,
|
type: EventTypes.ShipmentFail,
|
||||||
message: `Shipment "${shipment.name ?? `SHP-${shipment.id}`}" stranded — vehicle out of fuel`,
|
message: `Shipment "${shipment.name ?? `SHP-${shipment.id}`}" stranded: vehicle out of fuel`,
|
||||||
structure: originId,
|
structure: originId,
|
||||||
targetCollection: "shipments",
|
targetCollection: "shipments",
|
||||||
targetId: shipment.id,
|
targetId: shipment.id,
|
||||||
|
|
@ -231,7 +231,7 @@ async function completeShipment(
|
||||||
status: "failed",
|
status: "failed",
|
||||||
failureReason: `Destination storage rules prevented delivery: ${bounced
|
failureReason: `Destination storage rules prevented delivery: ${bounced
|
||||||
.map((b) => `${b.resourceName} ×${b.amount.toLocaleString()}`)
|
.map((b) => `${b.resourceName} ×${b.amount.toLocaleString()}`)
|
||||||
.join(", ")} — returned to origin.`,
|
.join(", ")}; returned to origin.`,
|
||||||
completedAt: new Date().toISOString(),
|
completedAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -321,7 +321,7 @@ async function createAutoReturnShipment(
|
||||||
if (vehicle.currentFuel < returnFuelCost) {
|
if (vehicle.currentFuel < returnFuelCost) {
|
||||||
await emitGameEvent(payload, {
|
await emitGameEvent(payload, {
|
||||||
type: EventTypes.ShipmentFail,
|
type: EventTypes.ShipmentFail,
|
||||||
message: `Auto-return failed — insufficient fuel`,
|
message: `Auto-return failed: insufficient fuel`,
|
||||||
structure: destinationId,
|
structure: destinationId,
|
||||||
targetCollection: "game-vehicles",
|
targetCollection: "game-vehicles",
|
||||||
targetId: vehicle.id,
|
targetId: vehicle.id,
|
||||||
|
|
|
||||||
|
|
@ -174,9 +174,7 @@ function run(
|
||||||
stderr += s;
|
stderr += s;
|
||||||
process.stderr.write(s);
|
process.stderr.write(s);
|
||||||
});
|
});
|
||||||
child.on("close", (code) =>
|
child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
||||||
resolve({ code: code ?? 1, stdout, stderr }),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,16 +184,14 @@ async function pickEnv(forced?: string): Promise<{ env: EnvName; uri: string }>
|
||||||
if (forced && forced in ENVS) {
|
if (forced && forced in ENVS) {
|
||||||
const uri = loadDatabaseUri(forced as EnvName);
|
const uri = loadDatabaseUri(forced as EnvName);
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
log(
|
log(`${RED}Cannot find DATABASE_URI in ${ENVS[forced as EnvName].file}${RESET}`);
|
||||||
`${RED}Cannot find DATABASE_URI in ${ENVS[forced as EnvName].file}${RESET}`,
|
|
||||||
);
|
|
||||||
log(`Create the file with: DATABASE_URI=postgres://user:pass@host:5432/db`);
|
log(`Create the file with: DATABASE_URI=postgres://user:pass@host:5432/db`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
return { env: forced as EnvName, uri };
|
return { env: forced as EnvName, uri };
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`\n${BOLD}⚡ Polaris Task Force — Migration Runner${RESET}\n`);
|
log(`\n${BOLD}⚡ Polaris Task Force: Migration Runner${RESET}\n`);
|
||||||
log("Target environment:\n");
|
log("Target environment:\n");
|
||||||
|
|
||||||
for (let i = 0; i < ENV_ORDER.length; i++) {
|
for (let i = 0; i < ENV_ORDER.length; i++) {
|
||||||
|
|
@ -220,9 +216,7 @@ async function pickEnv(forced?: string): Promise<{ env: EnvName; uri: string }>
|
||||||
const uri = loadDatabaseUri(env);
|
const uri = loadDatabaseUri(env);
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
log(`\n${RED}DATABASE_URI not found in ${ENVS[env].file}${RESET}`);
|
log(`\n${RED}DATABASE_URI not found in ${ENVS[env].file}${RESET}`);
|
||||||
log(
|
log(`Create the file with: ${BOLD}DATABASE_URI=postgres://user:pass@host:5432/db${RESET}`);
|
||||||
`Create the file with: ${BOLD}DATABASE_URI=postgres://user:pass@host:5432/db${RESET}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -259,9 +253,7 @@ async function main() {
|
||||||
if (freshOnly) {
|
if (freshOnly) {
|
||||||
if (env !== "dev") {
|
if (env !== "dev") {
|
||||||
log(`${RED}${BOLD}--fresh is only allowed for dev.${RESET}`);
|
log(`${RED}${BOLD}--fresh is only allowed for dev.${RESET}`);
|
||||||
log(
|
log(`${DIM}For staging/production, use migrate (up only) or migrate:fresh manually.${RESET}`);
|
||||||
`${DIM}For staging/production, use migrate (up only) or migrate:fresh manually.${RESET}`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const confirm = await ask(
|
const confirm = await ask(
|
||||||
|
|
@ -281,11 +273,9 @@ async function main() {
|
||||||
|
|
||||||
// Show current status
|
// Show current status
|
||||||
log(`${DIM}Migration status:${RESET}\n`);
|
log(`${DIM}Migration status:${RESET}\n`);
|
||||||
const statusResult = await run(
|
const statusResult = await run("bun", ["run", "payload", "migrate:status"], {
|
||||||
"bun",
|
DATABASE_URI: uri,
|
||||||
["run", "payload", "migrate:status"],
|
});
|
||||||
{ DATABASE_URI: uri },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check for pending migrations
|
// Check for pending migrations
|
||||||
const hasPending = statusResult.stdout.includes("│ No │");
|
const hasPending = statusResult.stdout.includes("│ No │");
|
||||||
|
|
@ -297,26 +287,16 @@ async function main() {
|
||||||
// Confirm
|
// Confirm
|
||||||
log("");
|
log("");
|
||||||
if (env === "prd") {
|
if (env === "prd") {
|
||||||
log(
|
log(`${RED}${BOLD} ╔══════════════════════════════════════════╗${RESET}`);
|
||||||
`${RED}${BOLD} ╔══════════════════════════════════════════╗${RESET}`,
|
log(`${RED}${BOLD} ║ YOU ARE ABOUT TO MIGRATE PRODUCTION ║${RESET}`);
|
||||||
);
|
log(`${RED}${BOLD} ╚══════════════════════════════════════════╝${RESET}\n`);
|
||||||
log(
|
const confirm = await ask(`${RED}Type "migrate" to confirm: ${RESET}`);
|
||||||
`${RED}${BOLD} ║ YOU ARE ABOUT TO MIGRATE PRODUCTION ║${RESET}`,
|
|
||||||
);
|
|
||||||
log(
|
|
||||||
`${RED}${BOLD} ╚══════════════════════════════════════════╝${RESET}\n`,
|
|
||||||
);
|
|
||||||
const confirm = await ask(
|
|
||||||
`${RED}Type "migrate" to confirm: ${RESET}`,
|
|
||||||
);
|
|
||||||
if (confirm.trim() !== "migrate") {
|
if (confirm.trim() !== "migrate") {
|
||||||
log("Aborted.");
|
log("Aborted.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const confirm = await ask(
|
const confirm = await ask(`${YELLOW}Run migrations on ${cfg.label}? (y/N): ${RESET}`);
|
||||||
`${YELLOW}Run migrations on ${cfg.label}? (y/N): ${RESET}`,
|
|
||||||
);
|
|
||||||
if (confirm.toLowerCase() !== "y") {
|
if (confirm.toLowerCase() !== "y") {
|
||||||
log("Aborted.");
|
log("Aborted.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -327,12 +307,7 @@ async function main() {
|
||||||
// push:true's batch=-1 record) is auto-accepted instead of blocking on
|
// push:true's batch=-1 record) is auto-accepted instead of blocking on
|
||||||
// non-interactive stdin.
|
// non-interactive stdin.
|
||||||
log(`\n${BOLD}Running migrations...${RESET}\n`);
|
log(`\n${BOLD}Running migrations...${RESET}\n`);
|
||||||
const result = await run(
|
const result = await run("bun", ["run", "payload", "migrate"], { DATABASE_URI: uri }, "y\n");
|
||||||
"bun",
|
|
||||||
["run", "payload", "migrate"],
|
|
||||||
{ DATABASE_URI: uri },
|
|
||||||
"y\n",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.code !== 0) {
|
if (result.code !== 0) {
|
||||||
log(`\n${RED}${BOLD}✗ Migration failed (exit ${result.code})${RESET}`);
|
log(`\n${RED}${BOLD}✗ Migration failed (exit ${result.code})${RESET}`);
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ function planImport(
|
||||||
async function resolveFuelConfig(payload: { find: (args: any) => Promise<{ docs: any[] }> }): Promise<{ id: number }> {
|
async function resolveFuelConfig(payload: { find: (args: any) => Promise<{ docs: any[] }> }): Promise<{ id: number }> {
|
||||||
const r = await payload.find({ collection: "resources", limit: 50, where: { name: { equals: "Fuel" } } });
|
const r = await payload.find({ collection: "resources", limit: 50, where: { name: { equals: "Fuel" } } });
|
||||||
const fuel = r.docs.find((d: any) => String(d.name).toLowerCase() === "fuel");
|
const fuel = r.docs.find((d: any) => String(d.name).toLowerCase() === "fuel");
|
||||||
if (!fuel) throw new Error("No resource named 'Fuel' found — cannot set vehicle fuel fields");
|
if (!fuel) throw new Error("No resource named 'Fuel' found. Cannot set vehicle fuel fields");
|
||||||
return { id: fuel.id };
|
return { id: fuel.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ const DEFAULT_XP_LEVELS = [
|
||||||
name: "Recruit",
|
name: "Recruit",
|
||||||
requiredPoints: 0,
|
requiredPoints: 0,
|
||||||
description: richText(
|
description: richText(
|
||||||
"Fresh out of boot camp. Everything is new — follow orders and learn the ropes.",
|
"Fresh out of boot camp. Everything is new; follow orders and learn the ropes.",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue