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";
|
||||
|
||||
export const metadata = {
|
||||
title: "Account — Polaris Task Force",
|
||||
title: "Account (Polaris Task Force)",
|
||||
};
|
||||
|
||||
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 min-w-0 flex-col gap-1">
|
||||
<CardTitle>Assignment</CardTitle>
|
||||
<CardDescription>Review your current assignment and request a move.</CardDescription>
|
||||
<CardDescription>
|
||||
Review your current assignment and request a move.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<RequestTransferDialog
|
||||
currentAssignments={currentAssignments}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Bomb } from "lucide-react";
|
|||
import { EodArena } from "@/components/frontend/eod/EodArena";
|
||||
|
||||
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.",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Bird } from "lucide-react";
|
|||
import { FlappyArena } from "@/components/frontend/flappy/FlappyArena";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Flight Simulator — Polaris Task Force",
|
||||
title: "Flight Simulator: Polaris Task Force",
|
||||
description: "Side training sim. Definitely not Flappy Bird.",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import Link from "next/link";
|
|||
import { getTicketVoteSummaries } from "@/lib/tickets/voteState";
|
||||
|
||||
export const metadata = {
|
||||
title: "Ticket — Polaris Task Force",
|
||||
title: "Ticket: Polaris Task Force",
|
||||
};
|
||||
|
||||
interface TicketPageProps {
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export async function createTicket(input: {
|
|||
userId: staffId,
|
||||
type: "ticket:created",
|
||||
title: "New ticket",
|
||||
message: `${title} — ${reporterName}`,
|
||||
message: `${title} by ${reporterName}`,
|
||||
link: `/helpdesk/${createdTicket.id}`,
|
||||
});
|
||||
}
|
||||
|
|
@ -233,7 +233,7 @@ export async function replyToTicket(ticketId: number, content: string): Promise<
|
|||
targetId: ticketId,
|
||||
});
|
||||
|
||||
const replyMessage = `"${ticket.title}" — you have a new reply.`;
|
||||
const replyMessage = `"${ticket.title}" has a new reply.`;
|
||||
if (isStaff) {
|
||||
if (reporterId != null && reporterId !== userId) {
|
||||
await notifyUser(payload, {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { LifeBuoyIcon } from "lucide-react";
|
|||
import { getTicketVoteSummaries } from "@/lib/tickets/voteState";
|
||||
|
||||
export const metadata = {
|
||||
title: "Helpdesk — Polaris Task Force",
|
||||
title: "Helpdesk: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function HelpdeskPage() {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { Media } from "@/payload-types";
|
|||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export const metadata = {
|
||||
title: "Campaigns — Polaris Task Force",
|
||||
title: "Campaigns: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function CampaignsPage() {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { ShieldIcon } from "lucide-react";
|
|||
import { FactionCard } from "@/components/frontend/intelligence/FactionCard";
|
||||
|
||||
export const metadata = {
|
||||
title: "Factions — Polaris Task Force",
|
||||
title: "Factions | Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function FactionsPage() {
|
||||
|
|
|
|||
|
|
@ -430,7 +430,7 @@ export async function createFridaySideOp(
|
|||
if (fridayConflict) {
|
||||
return {
|
||||
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) {
|
||||
return {
|
||||
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";
|
||||
|
||||
export const metadata = {
|
||||
title: "Missions — Polaris Task Force",
|
||||
title: "Missions: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function MissionsPage() {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { Item, ItemContent, ItemDescription, ItemHeader, ItemTitle } from "@/com
|
|||
import { CampaignCard } from "@/components/frontend/intelligence/CampaignCard";
|
||||
|
||||
export const metadata = {
|
||||
title: "Intelligence — Polaris Task Force",
|
||||
title: "Intelligence: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function IntelligenceOverviewPage() {
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@ export async function moveLockerItem(
|
|||
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] =
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { LockerView } from "@/components/frontend/locker/LockerView";
|
|||
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||
|
||||
export const metadata = {
|
||||
title: "Locker — Polaris Task Force",
|
||||
title: "Locker (Polaris Task Force)",
|
||||
};
|
||||
|
||||
export default async function LockerPage() {
|
||||
|
|
@ -36,9 +36,9 @@ export default async function LockerPage() {
|
|||
limit: 100,
|
||||
depth: 1,
|
||||
});
|
||||
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[] = [];
|
||||
if (isManager) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQu
|
|||
import { getCurrencyConfig } from "@/lib/banking";
|
||||
|
||||
export const metadata = {
|
||||
title: "Account — Polaris Task Force",
|
||||
title: "Account: Polaris Task Force",
|
||||
};
|
||||
|
||||
interface AccountPageProps {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQu
|
|||
import { getCurrencyConfig } from "@/lib/banking";
|
||||
|
||||
export const metadata = {
|
||||
title: "Banking — Polaris Task Force",
|
||||
title: "Banking: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function BankingPage() {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from "@/components/ui/item";
|
||||
|
||||
export const metadata = {
|
||||
title: "Vehicles — Polaris Task Force",
|
||||
title: "Vehicles: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function GameVehiclesPage() {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@ async function authenticate() {
|
|||
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");
|
||||
}
|
||||
|
||||
|
|
@ -344,7 +347,7 @@ async function resolveNpcOffer(
|
|||
userId: buyer.id as number,
|
||||
type: "market:counter",
|
||||
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",
|
||||
});
|
||||
await emitGameEvent(payload, {
|
||||
|
|
@ -1033,8 +1036,7 @@ export async function getReferencePrices(
|
|||
for (const asset of assets.docs) {
|
||||
const base = getBaseBuyPrice(asset);
|
||||
const modifier = modifierByItemId.get(asset.id);
|
||||
references[asset.id] =
|
||||
base == null ? null : Math.max(1, Math.round(base * (modifier ?? 1)));
|
||||
references[asset.id] = base == null ? null : Math.max(1, Math.round(base * (modifier ?? 1)));
|
||||
}
|
||||
return { success: true, data: references };
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { ensureLockerStorage } from "@/lib/locker";
|
|||
import { getCurrencyConfig } from "@/lib/banking";
|
||||
|
||||
export const metadata = {
|
||||
title: "Market — Polaris Task Force",
|
||||
title: "Market: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function MarketPage() {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { PackageOpenIcon } from "lucide-react";
|
|||
import { ResourceCard } from "@/components/frontend/logistics/ResourceCard";
|
||||
|
||||
export const metadata = {
|
||||
title: "Resources — Polaris Task Force",
|
||||
title: "Resources: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function ResourcesPage() {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { TruckIcon } from "lucide-react";
|
|||
import { ShipmentsList } from "@/components/frontend/logistics/ShipmentsList";
|
||||
|
||||
export const metadata = {
|
||||
title: "Shipments — Polaris Task Force",
|
||||
title: "Shipments: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function ShipmentsPage() {
|
||||
|
|
|
|||
|
|
@ -683,7 +683,7 @@ export async function moveGridItem(
|
|||
for (let dx = 0; dx < ow; dx++) {
|
||||
for (let dy = 0; dy < oh; 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:
|
||||
recovered > 0
|
||||
? `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,
|
||||
structure: structureId,
|
||||
targetCollection: "game-structures",
|
||||
|
|
@ -1146,7 +1146,7 @@ export async function retrieveFromVoid(structureId: number): Promise<GridActionR
|
|||
error:
|
||||
recovered > 0
|
||||
? `${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) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
|
|
@ -1297,11 +1297,11 @@ export async function autoArrangeGrid(structureId: number): Promise<GridActionRe
|
|||
|
||||
const messages: string[] = [];
|
||||
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, {
|
||||
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,
|
||||
structure: structureId,
|
||||
targetCollection: "game-structures",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { TruckIcon } from "lucide-react";
|
|||
import { VehiclesList } from "./VehiclesList";
|
||||
|
||||
export const metadata = {
|
||||
title: "Vehicles — Polaris Task Force",
|
||||
title: "Vehicles: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function VehiclesPage() {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { BellIcon } from "lucide-react";
|
|||
import { NotificationsInbox } from "@/components/frontend/notifications/NotificationsInbox";
|
||||
|
||||
export const metadata = {
|
||||
title: "Notifications — Polaris Task Force",
|
||||
title: "Notifications: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function NotificationsPage() {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { UsersIcon } from "lucide-react";
|
|||
import { NpcDirectory } from "@/components/frontend/personnel/NpcDirectory";
|
||||
|
||||
export const metadata = {
|
||||
title: "Contacts — Polaris Task Force",
|
||||
title: "Contacts: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function ContactsPage() {
|
||||
|
|
@ -25,8 +25,7 @@ export default async function ContactsPage() {
|
|||
<h1 className="text-lg font-semibold">Contacts</h1>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{npcs.length} contact{npcs.length !== 1 ? "s" : ""} recorded in the theater of
|
||||
operations.
|
||||
{npcs.length} contact{npcs.length !== 1 ? "s" : ""} recorded in the theater of operations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,10 @@ import config from "@payload-config";
|
|||
import { getPayload } from "payload";
|
||||
import type { GameNpc, MarketListing } from "@/payload-types";
|
||||
import { TrendingUp } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export const metadata = {
|
||||
title: "Statistics — Polaris Task Force",
|
||||
title: "Statistics (Polaris Task Force)",
|
||||
};
|
||||
|
||||
interface FactionStatistic {
|
||||
|
|
@ -49,10 +43,7 @@ export default async function StatisticsPage() {
|
|||
payload.find({
|
||||
collection: "market-listings",
|
||||
where: {
|
||||
and: [
|
||||
{ isAutoGenerated: { equals: true } },
|
||||
{ status: { equals: "active" } },
|
||||
],
|
||||
and: [{ isAutoGenerated: { equals: true } }, { status: { equals: "active" } }],
|
||||
},
|
||||
limit: 200,
|
||||
depth: 0,
|
||||
|
|
@ -196,10 +187,7 @@ export default async function StatisticsPage() {
|
|||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
{topVendors.map((vendor) => (
|
||||
<div
|
||||
key={vendor.name}
|
||||
className="flex items-center justify-between gap-4"
|
||||
>
|
||||
<div key={vendor.name} className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-semibold">{vendor.name}</p>
|
||||
<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>
|
||||
{xpLevel.nextLevelXP !== null ? (
|
||||
<span>
|
||||
{Math.floor(xpLevel.progress)}% — {xpLevel.nextLevelXP.toLocaleString()} XP to
|
||||
next level
|
||||
{Math.floor(xpLevel.progress)}% ({xpLevel.nextLevelXP.toLocaleString()} XP to next
|
||||
level)
|
||||
</span>
|
||||
) : (
|
||||
<span>Max level reached</span>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { ProjectBoardView } from "@/components/frontend/projects/ProjectBoardVie
|
|||
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||
|
||||
export const metadata = {
|
||||
title: "Board — Polaris Task Force",
|
||||
title: "Board | Polaris Task Force",
|
||||
};
|
||||
|
||||
const KANBAN_STATUSES = [
|
||||
|
|
|
|||
|
|
@ -7,14 +7,10 @@ import { hasPermission } from "@/utils/access-control/hasPermission";
|
|||
import { richTextToPlaintext } from "@/lib/projects/projectMeta";
|
||||
|
||||
export const metadata = {
|
||||
title: "Releases — Polaris Task Force",
|
||||
title: "Releases: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function ReleasesPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ key: string }>;
|
||||
}) {
|
||||
export default async function ReleasesPage({ params }: { params: Promise<{ key: string }> }) {
|
||||
const { key } = await params;
|
||||
const headers = await nextHeaders();
|
||||
const payload = await getPayload({ config });
|
||||
|
|
@ -62,9 +58,10 @@ export default async function ReleasesPage({
|
|||
|
||||
const releases = releaseResult.docs.map((r: any) => {
|
||||
const releaseTickets = tickets.filter((ticket: any) => {
|
||||
const releaseId = typeof ticket.release === "object" && ticket.release !== null
|
||||
? ticket.release.id
|
||||
: ticket.release;
|
||||
const releaseId =
|
||||
typeof ticket.release === "object" && ticket.release !== null
|
||||
? ticket.release.id
|
||||
: ticket.release;
|
||||
return releaseId === r.id;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { SprintsView } from "@/components/frontend/projects/SprintsView";
|
|||
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||
|
||||
export const metadata = {
|
||||
title: "Sprints — Polaris Task Force",
|
||||
title: "Sprints: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function SprintsPage({
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { hasPermission } from "@/utils/access-control/hasPermission";
|
|||
import { richTextToPlaintext } from "@/lib/projects/projectMeta";
|
||||
|
||||
export const metadata = {
|
||||
title: "Projects — Polaris Task Force",
|
||||
title: "Projects: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function ProjectsPage() {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { getPayload } from "payload";
|
|||
import { ProjectSearchView } from "@/components/frontend/projects/ProjectSearchView";
|
||||
|
||||
export const metadata = {
|
||||
title: "Search — Polaris Task Force",
|
||||
title: "Search: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function SearchPage() {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type { AimTrainerStats } from "@/app/(frontend)/qualification/actions";
|
|||
import { AimTrainerArena } from "@/components/frontend/qualification/AimTrainerArena";
|
||||
|
||||
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.",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { Radio } from "lucide-react";
|
|||
import { RadioArena } from "@/components/frontend/radio/RadioArena";
|
||||
|
||||
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.",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { UsersIcon } from "lucide-react";
|
|||
import { RosterView } from "@/components/frontend/roster/RosterView";
|
||||
|
||||
export const metadata = {
|
||||
title: "Unit Roster — Polaris Task Force",
|
||||
title: "Unit Roster (Polaris Task Force)",
|
||||
};
|
||||
|
||||
export type RosterMember = {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
|||
import { isSuperuser } from "@/utils/access-control/hasPermission";
|
||||
|
||||
export const metadata = {
|
||||
title: "Transfers — Polaris Task Force",
|
||||
title: "Transfers: Polaris Task Force",
|
||||
};
|
||||
|
||||
export default async function TransfersPage() {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { getScriptForUser } from "@/lib/voiceover";
|
|||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||
|
||||
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.",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { cn } from "@/lib/utils";
|
|||
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Voiceover — Polaris Task Force",
|
||||
title: "Voiceover: Polaris Task Force",
|
||||
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";
|
||||
|
||||
export const metadata = {
|
||||
title: "Edit wiki page — Polaris Task Force",
|
||||
title: "Edit wiki page: Polaris Task Force",
|
||||
};
|
||||
|
||||
function templateCssMap(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { RevisionList, type WikiRevisionSummary } from "@/components/frontend/wi
|
|||
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
||||
|
||||
export const metadata = {
|
||||
title: "Wiki history — Polaris Task Force",
|
||||
title: "Wiki history: Polaris Task Force",
|
||||
};
|
||||
|
||||
function editorName(editor: WikiRevision["editor"]): string {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { prepareMarkdown } from "@/lib/wiki/prepare";
|
|||
import { buildTemplateStyles } from "@/lib/wiki/templateCss";
|
||||
|
||||
export const metadata = {
|
||||
title: "Wiki page — Polaris Task Force",
|
||||
title: "Wiki page: Polaris Task Force",
|
||||
};
|
||||
|
||||
function relationName(relation: WikiPageDocument["lastEditor"]): string {
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ export async function uploadWikiImage(
|
|||
overrideAccess: true,
|
||||
});
|
||||
if (!doc.filename) {
|
||||
return { success: false, error: "Upload failed — no file was stored." };
|
||||
return { success: false, error: "Upload failed: no file was stored." };
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { WikiEditor } from "@/components/frontend/wiki/WikiEditor";
|
|||
import { listTemplates, templateMap } from "@/lib/wiki/service";
|
||||
|
||||
export const metadata = {
|
||||
title: "New wiki page — Polaris Task Force",
|
||||
title: "New wiki page (Polaris Task Force)",
|
||||
};
|
||||
|
||||
function templateCssMap(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { WikiIndex, type WikiIndexPage } from "@/components/frontend/wiki/WikiIn
|
|||
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
|
||||
|
||||
export const metadata = {
|
||||
title: "Wiki — Polaris Task Force",
|
||||
title: "Wiki: Polaris Task Force",
|
||||
};
|
||||
|
||||
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).` };
|
||||
}
|
||||
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 { mission: docs[0] };
|
||||
|
|
@ -92,20 +92,20 @@ export const remindEvaluationsCommand: BotCommand = {
|
|||
)
|
||||
) {
|
||||
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;
|
||||
}
|
||||
const sent = await processMission(client, payload, mission);
|
||||
await interaction.editReply(
|
||||
`Done — ${sent} evaluation reminder DM(s) sent for **${missionLabel(mission)}**.`,
|
||||
`Done: ${sent} evaluation reminder DM(s) sent for **${missionLabel(mission)}**.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { processed, sent } = await reconcile(client, payload);
|
||||
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) {
|
||||
await interaction.editReply(
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ const main = async () => {
|
|||
let shuttingDown = false;
|
||||
const shutdown = async (signal: NodeJS.Signals): Promise<void> => {
|
||||
if (shuttingDown) {
|
||||
payload.logger.warn(`[Bot] ${signal} received again — forcing exit`);
|
||||
payload.logger.warn(`[Bot] ${signal} received again, forcing exit`);
|
||||
process.exit(143);
|
||||
}
|
||||
shuttingDown = true;
|
||||
payload.logger.info(`[Bot] ${signal} received — shutting down gracefully`);
|
||||
payload.logger.info(`[Bot] ${signal} received, shutting down gracefully`);
|
||||
try {
|
||||
if (client.isReady()) {
|
||||
await client.destroy();
|
||||
|
|
|
|||
|
|
@ -61,11 +61,9 @@ const getReminderMissions = async (payload: Payload): Promise<Mission[]> => {
|
|||
};
|
||||
|
||||
const formatLinks = (links: ReminderLink[], appUrl: string): string => {
|
||||
const lines = links.map(
|
||||
(link) => `• [${link.displayName}](${appUrl}${link.profilePath})`,
|
||||
);
|
||||
const lines = links.map((link) => `• [${link.displayName}](${appUrl}${link.profilePath})`);
|
||||
// Discord embed field values cap at 1024 chars.
|
||||
return (lines.join("\n").slice(0, 1024) || "—");
|
||||
return lines.join("\n").slice(0, 1024) || "—";
|
||||
};
|
||||
|
||||
const renderReminderEmbed = (
|
||||
|
|
@ -75,7 +73,7 @@ const renderReminderEmbed = (
|
|||
appUrl: string,
|
||||
): EmbedBuilder => {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`Leadership feedback requested — ${missionTitle(mission)}`)
|
||||
.setTitle(`Leadership feedback requested: ${missionTitle(mission)}`)
|
||||
.setColor(EMBED_COLOR)
|
||||
.setFooter({
|
||||
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);
|
||||
await user.send({
|
||||
embeds: [
|
||||
renderReminderEmbed(mission, target.rateLeaders, target.rateSubordinates, botConfig.appUrl),
|
||||
renderReminderEmbed(
|
||||
mission,
|
||||
target.rateLeaders,
|
||||
target.rateSubordinates,
|
||||
botConfig.appUrl,
|
||||
),
|
||||
],
|
||||
});
|
||||
sent += 1;
|
||||
|
|
@ -152,7 +155,7 @@ export const processMission = async (
|
|||
const { sent, capped } = await sendReminderDms(client, payload, mission);
|
||||
if (capped) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -173,10 +176,7 @@ export interface ReconcileSummary {
|
|||
sent: number;
|
||||
}
|
||||
|
||||
export const reconcile = async (
|
||||
client: Client,
|
||||
payload: Payload,
|
||||
): Promise<ReconcileSummary> => {
|
||||
export const reconcile = async (client: Client, payload: Payload): Promise<ReconcileSummary> => {
|
||||
if (reconcileRunning) return { processed: 0, sent: 0 };
|
||||
reconcileRunning = true;
|
||||
let processed = 0;
|
||||
|
|
|
|||
|
|
@ -246,10 +246,7 @@ interface ModlistInfo {
|
|||
updatedAt: string;
|
||||
}
|
||||
|
||||
const resolveModlist = async (
|
||||
payload: Payload,
|
||||
mission: Mission,
|
||||
): Promise<ModlistInfo | null> => {
|
||||
const resolveModlist = async (payload: Payload, mission: Mission): Promise<ModlistInfo | null> => {
|
||||
const raw = mission.gameDetails.gameSetup?.requiredMods;
|
||||
if (raw == null || typeof raw === "number") return null;
|
||||
const modlist = raw as ModList;
|
||||
|
|
@ -282,7 +279,7 @@ const renderModlistEmbed = (
|
|||
kind: "new" | "updated",
|
||||
): 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)
|
||||
.setTimestamp();
|
||||
embed.setDescription(
|
||||
|
|
@ -505,7 +502,8 @@ export const reconcile = async (client: Client, payload: Payload): Promise<void>
|
|||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export const linkDiscordAccount = async (
|
|||
): Promise<{ user: User; wasLinked: boolean }> => {
|
||||
const user = await findUserByDiscordUsername(payload, discordUsername);
|
||||
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) {
|
||||
throw new SignupError(
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ export const GameRules: GlobalConfig = {
|
|||
defaultValue: 60,
|
||||
admin: {
|
||||
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.",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -154,23 +154,23 @@ export const Missions: CollectionConfig = {
|
|||
value: "external",
|
||||
},
|
||||
],
|
||||
filterOptions: ({ req }) => {
|
||||
return [
|
||||
{
|
||||
label: "Side Operation",
|
||||
value: "side",
|
||||
},
|
||||
{
|
||||
label: "External Operation",
|
||||
value: "external",
|
||||
},
|
||||
{
|
||||
label: "Main Operation",
|
||||
value: "main",
|
||||
},
|
||||
];
|
||||
},
|
||||
defaultValue: ({ req }) => "side",
|
||||
filterOptions: ({ req }) => {
|
||||
return [
|
||||
{
|
||||
label: "Side Operation",
|
||||
value: "side",
|
||||
},
|
||||
{
|
||||
label: "External Operation",
|
||||
value: "external",
|
||||
},
|
||||
{
|
||||
label: "Main Operation",
|
||||
value: "main",
|
||||
},
|
||||
];
|
||||
},
|
||||
defaultValue: ({ req }) => "side",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -193,7 +193,8 @@ export const Missions: CollectionConfig = {
|
|||
name: "discordAttendanceSentAt",
|
||||
type: "date",
|
||||
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,
|
||||
},
|
||||
},
|
||||
|
|
@ -355,20 +356,20 @@ export const Missions: CollectionConfig = {
|
|||
value: "private",
|
||||
},
|
||||
],
|
||||
defaultValue: "leadership",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "completedAt",
|
||||
type: "date",
|
||||
admin: {
|
||||
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.",
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultValue: "leadership",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "completedAt",
|
||||
type: "date",
|
||||
admin: {
|
||||
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.",
|
||||
disabled: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "missionRoles",
|
||||
type: "group",
|
||||
|
|
@ -582,11 +583,15 @@ export const Missions: CollectionConfig = {
|
|||
type: "text",
|
||||
hidden: true,
|
||||
defaultValue: "ptf313",
|
||||
access: {
|
||||
read: async ({ req }) => {
|
||||
return await hasPermission(req.payload, req.user, "missions:read-sensitive");
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: async ({ req }) => {
|
||||
return await hasPermission(
|
||||
req.payload,
|
||||
req.user,
|
||||
"missions:read-sensitive",
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -633,7 +633,7 @@ export const Structures: CollectionConfig = {
|
|||
defaultValue: false,
|
||||
admin: {
|
||||
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,
|
||||
admin: {
|
||||
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",
|
||||
type: "textarea",
|
||||
admin: {
|
||||
description:
|
||||
"Private free-text note. Never shown to normal users; managers only.",
|
||||
description: "Private free-text note. Never shown to normal users; managers only.",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ export const Roles: CollectionConfig = {
|
|||
},
|
||||
admin: {
|
||||
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",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ export const Users: CollectionConfig = {
|
|||
},*/
|
||||
admin: {
|
||||
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: {
|
||||
language: "css",
|
||||
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,8 +57,8 @@ export const WikiTemplates: CollectionConfig = {
|
|||
},
|
||||
admin: {
|
||||
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;
|
||||
}
|
||||
setValue("");
|
||||
setSuccess("Request submitted — leadership has been notified.");
|
||||
setSuccess("Request submitted. Leadership has been notified.");
|
||||
router.refresh();
|
||||
} catch {
|
||||
setError("Something went wrong. Please try again.");
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ export function StaffRoster({
|
|||
|
||||
{laborCrews.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No labor crew — add workers via Hire Staff.
|
||||
No labor crew. Add workers via Hire Staff.
|
||||
</p>
|
||||
) : (
|
||||
<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="min-w-0">
|
||||
<p className="font-medium">
|
||||
{crew.classificationName} — {crew.headcount} hired ·{" "}
|
||||
{crew.classificationName}: {crew.headcount} hired ·{" "}
|
||||
{crew.requiredHeadcount} required · max {crew.maxHeadcount}
|
||||
</p>
|
||||
<p className="font-mono text-sm tabular-nums text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export function EodLeaderboard({
|
|||
</div>
|
||||
) : rows.length === 0 ? (
|
||||
<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>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
|
|
@ -218,7 +218,7 @@ export function EodLeaderboard({
|
|||
)}
|
||||
title={
|
||||
row.isOutsideTopTen
|
||||
? "Operator status — not currently ranked in the top 10"
|
||||
? "Operator status (not currently ranked in the top 10)"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
|
|
@ -238,13 +238,14 @@ export function EodLeaderboard({
|
|||
unranked
|
||||
</span>
|
||||
)}
|
||||
{row.userId !== null && activeTeams.some((team) => team.id === row.userId) && (
|
||||
<span
|
||||
className="size-2 rounded-full bg-emerald-400"
|
||||
title="Clearing now"
|
||||
aria-label="Clearing now"
|
||||
/>
|
||||
)}
|
||||
{row.userId !== null &&
|
||||
activeTeams.some((team) => team.id === row.userId) && (
|
||||
<span
|
||||
className="size-2 rounded-full bg-emerald-400"
|
||||
title="Clearing now"
|
||||
aria-label="Clearing now"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right tabular-nums text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -807,8 +807,8 @@ export function MinesweeperBoard({
|
|||
aria-live="polite"
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{phase === "ready" && <span>Awaiting deployment — select a sector.</span>}
|
||||
{phase === "playing" && <span>Live sweep — mark every charge before you reveal.</span>}
|
||||
{phase === "ready" && <span>Awaiting deployment. Select a sector.</span>}
|
||||
{phase === "playing" && <span>Live sweep: mark every charge before you reveal.</span>}
|
||||
{phase === "over" && lastRun?.won && (
|
||||
<span>
|
||||
Sector clear. {config.label} defused in {lastRun.seconds}s.
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export function FlappyLeaderboard({ refreshKey = 0, currentUserId, activePilots
|
|||
</div>
|
||||
) : rows.length === 0 ? (
|
||||
<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>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
|
|
@ -156,7 +156,7 @@ export function FlappyLeaderboard({ refreshKey = 0, currentUserId, activePilots
|
|||
row.isCurrentUser && "bg-accent/40",
|
||||
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">
|
||||
<span className={rankClass(i)}>{row.isOutsideTopTen ? "—" : i + 1}</span>
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ export function CreateSideOpDialog({
|
|||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
const startDateIso = defaultStartDateTime ?? "";
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export function FridayCalendar({
|
|||
<ItemContent>
|
||||
{entry.missions.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Nothing scheduled yet — claim this Friday.
|
||||
Nothing scheduled yet. Claim this Friday.
|
||||
</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export function MissionAttendance({
|
|||
{
|
||||
id: `mission:${missionId}:rsvp-yes`,
|
||||
group: "Mission RSVP",
|
||||
label: "RSVP — Yes",
|
||||
label: "RSVP: Yes",
|
||||
icon: CheckIcon,
|
||||
keywords: ["attend", "going", "yes", "rsvp"],
|
||||
onSelect: () => void handleSet("yes"),
|
||||
|
|
@ -97,7 +97,7 @@ export function MissionAttendance({
|
|||
{
|
||||
id: `mission:${missionId}:rsvp-tentative`,
|
||||
group: "Mission RSVP",
|
||||
label: "RSVP — Tentative",
|
||||
label: "RSVP: Tentative",
|
||||
icon: MinusIcon,
|
||||
keywords: ["maybe", "tentative", "unsure", "rsvp"],
|
||||
onSelect: () => void handleSet("tentative"),
|
||||
|
|
@ -105,7 +105,7 @@ export function MissionAttendance({
|
|||
{
|
||||
id: `mission:${missionId}:rsvp-no`,
|
||||
group: "Mission RSVP",
|
||||
label: "RSVP — No",
|
||||
label: "RSVP: No",
|
||||
icon: XIcon,
|
||||
keywords: ["decline", "not going", "no", "rsvp"],
|
||||
onSelect: () => void handleSet("no"),
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export function SideOpNarrativeFields({ value, onChange }: SideOpNarrativeFields
|
|||
return (
|
||||
<div className="grid gap-5">
|
||||
<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.
|
||||
</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ export function WardrobePanel({ userName, loadouts, equippedLoadout, items }: Wa
|
|||
|
||||
{!equippedLoadout && (
|
||||
<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.
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ export function ResourceCard({ resource }: ResourceCardProps) {
|
|||
</div>
|
||||
</ItemContent>
|
||||
<ItemDescription>
|
||||
{truncatedDescription ?? `${resource.type} resource — ${resource.unitOfMeasure}`}
|
||||
{truncatedDescription ?? `${resource.type} resource (${resource.unitOfMeasure})`}
|
||||
</ItemDescription>
|
||||
<span className="text-xs text-muted-foreground group-hover:text-primary transition-colors flex items-center gap-1">
|
||||
View details <ChevronRightIcon className="size-3" />
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
|
|||
{
|
||||
id: "shipments:filter-all",
|
||||
group: "Shipments",
|
||||
label: "Filter — All Shipments",
|
||||
label: "Filter: All Shipments",
|
||||
icon: ListFilterIcon,
|
||||
keywords: ["filter", "all", "shipments"],
|
||||
onSelect: () => setFilter(null),
|
||||
|
|
@ -42,7 +42,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
|
|||
{
|
||||
id: "shipments:filter-active",
|
||||
group: "Shipments",
|
||||
label: "Filter — Active Shipments",
|
||||
label: "Filter: Active Shipments",
|
||||
icon: ClockIcon,
|
||||
keywords: ["filter", "active", "in transit", "pending"],
|
||||
onSelect: () => setFilter("active"),
|
||||
|
|
@ -50,7 +50,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
|
|||
{
|
||||
id: "shipments:filter-completed",
|
||||
group: "Shipments",
|
||||
label: "Filter — Completed Shipments",
|
||||
label: "Filter: Completed Shipments",
|
||||
icon: CheckCircleIcon,
|
||||
keywords: ["filter", "completed", "arrived", "done"],
|
||||
onSelect: () => setFilter("completed"),
|
||||
|
|
@ -58,7 +58,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
|
|||
{
|
||||
id: "shipments:filter-cancelled",
|
||||
group: "Shipments",
|
||||
label: "Filter — Cancelled Shipments",
|
||||
label: "Filter: Cancelled Shipments",
|
||||
icon: XCircleIcon,
|
||||
keywords: ["filter", "cancelled", "failed"],
|
||||
onSelect: () => setFilter("cancelled"),
|
||||
|
|
@ -66,7 +66,7 @@ export function ShipmentsList({ shipments }: ShipmentsListProps) {
|
|||
{
|
||||
id: "shipments:filter-stranded",
|
||||
group: "Shipments",
|
||||
label: "Filter — Stranded Shipments",
|
||||
label: "Filter: Stranded Shipments",
|
||||
icon: TruckIcon,
|
||||
keywords: ["filter", "stranded", "stuck"],
|
||||
onSelect: () => setFilter("stranded"),
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ export function CreateListingDialog({
|
|||
value={String(s.assetId)}
|
||||
disabled={!s.tradeable || !s.isLive}
|
||||
>
|
||||
{s.name} — {s.quantity.toLocaleString()}×
|
||||
{s.name} ({s.quantity.toLocaleString()}×)
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
|||
const boughtQty = thread?.quantity ?? negotiation?.quantity ?? stock;
|
||||
setAcceptedAt(Date.now());
|
||||
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);
|
||||
setBusy(null);
|
||||
|
|
@ -168,7 +168,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
|||
setAcceptedAt(Date.now());
|
||||
const dealQty = data.quantity ?? stock;
|
||||
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);
|
||||
setBusy(null);
|
||||
|
|
@ -236,7 +236,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
|||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Negotiate — {assetName}</DialogTitle>
|
||||
<DialogTitle>Negotiate: {assetName}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isVendor ? "Haggle with the vendor." : "Send an offer to the seller."} Asking{" "}
|
||||
<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">
|
||||
The {isVendor ? "vendor" : "seller"} counteroffers{" "}
|
||||
<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">
|
||||
{fmt((negotiation?.amount ?? 0) * (negotiation?.quantity ?? stock))}
|
||||
</span>
|
||||
|
|
@ -331,7 +331,7 @@ export function MakeOfferDialog({ listing, currentUserId, currencyConfig }: Make
|
|||
</Badge>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
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">
|
||||
{fmt(outcome.price * (negotiation?.quantity ?? stock))}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ function NegotiationRow({
|
|||
if (key === "accept") {
|
||||
const qty = negotiation.quantity ?? listing?.quantity ?? 1;
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export function NpcChat({ listing, negotiation, currencyLabel }: NpcChatProps) {
|
|||
<div className="flex items-center gap-2 border-b px-3 py-2">
|
||||
<StoreIcon className="size-4 text-muted-foreground" />
|
||||
<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>
|
||||
<ChatBubbleList bubbles={bubbles} buyerLabel="You" sellerLabel="Vendor" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -148,16 +148,16 @@ export function AimTrainerArena({
|
|||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium text-foreground">Acquire</span>
|
||||
<span>
|
||||
Up to {AIM_TRAINER_DIFFICULTIES[difficulty].maxConcurrentTargets} hostile
|
||||
silhouettes are active at once, plus an occasional blue civilian that never counts
|
||||
toward the limit. Targets rebound off each other and the range walls.
|
||||
Up to {AIM_TRAINER_DIFFICULTIES[difficulty].maxConcurrentTargets} hostile silhouettes
|
||||
are active at once, plus an occasional blue civilian that never counts toward the
|
||||
limit. Targets rebound off each other and the range walls.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium text-foreground">Prioritize</span>
|
||||
<span>
|
||||
Headshots score 250 points. Torso hits score 100 points. Blue civilians cost 100
|
||||
points — never shoot them.
|
||||
points, so never shoot them.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
|
|
|
|||
|
|
@ -436,14 +436,14 @@ export function AimTrainerGame({
|
|||
setLive(getAimTrainerLiveStats(game));
|
||||
setPhaseSafe("over");
|
||||
playRoundComplete();
|
||||
void handleRoundCompleteRef.current({
|
||||
difficulty,
|
||||
headshots: game.headshots,
|
||||
torsoHits: game.torsoHits,
|
||||
civilianHits: game.civilianHits,
|
||||
shotsFired: game.shotsFired,
|
||||
despawnedUnhitTargets: game.despawnedUnhitTargets,
|
||||
});
|
||||
void handleRoundCompleteRef.current({
|
||||
difficulty,
|
||||
headshots: game.headshots,
|
||||
torsoHits: game.torsoHits,
|
||||
civilianHits: game.civilianHits,
|
||||
shotsFired: game.shotsFired,
|
||||
despawnedUnhitTargets: game.despawnedUnhitTargets,
|
||||
});
|
||||
};
|
||||
|
||||
const step = (now: number) => {
|
||||
|
|
@ -624,7 +624,7 @@ export function AimTrainerGame({
|
|||
Ready on the line
|
||||
</span>
|
||||
<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.
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ export function AimTrainerLeaderboard({
|
|||
</div>
|
||||
) : rows.length === 0 ? (
|
||||
<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>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
|
|
@ -172,7 +172,7 @@ export function AimTrainerLeaderboard({
|
|||
)}
|
||||
title={
|
||||
row.isOutsideTopTen
|
||||
? "Operator status — not currently ranked in the top 10"
|
||||
? "Operator status: not currently ranked in the top 10"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -509,7 +509,7 @@ export function ManageStorageDialog({ structureId, open, onOpenChange }: ManageS
|
|||
const vType = typeof v.type === "object" ? v.type : null;
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
|
|
@ -683,7 +683,7 @@ export function ManageStorageDialog({ structureId, open, onOpenChange }: ManageS
|
|||
{cap !== undefined && (
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
— {cap.toLocaleString()} max
|
||||
({cap.toLocaleString()} max)
|
||||
</span>
|
||||
)}
|
||||
</SelectItem>
|
||||
|
|
@ -868,7 +868,7 @@ export function ManageStorageDialog({ structureId, open, onOpenChange }: ManageS
|
|||
if (!isResourceWhitelisted(destType, resId)) {
|
||||
return (
|
||||
<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.
|
||||
</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">
|
||||
<ListRestartIcon className="size-3.5 shrink-0" />
|
||||
<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.
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export function StructureCard({ structure }: StructureCardProps) {
|
|||
</ItemContent>
|
||||
<ItemDescription>
|
||||
{resourceCount === 0
|
||||
? "Empty — no resources stored."
|
||||
? "Empty: no resources stored."
|
||||
: `${resourceCount} resource type${resourceCount !== 1 ? "s" : ""} stored`}
|
||||
</ItemDescription>
|
||||
<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>
|
||||
<ItemDescription className="flex items-center gap-2">
|
||||
<span>
|
||||
{voidStorageItems.length} item{voidStorageItems.length !== 1 ? "s" : ""} — these no
|
||||
{voidStorageItems.length} item{voidStorageItems.length !== 1 ? "s" : ""}: these no
|
||||
longer fit in the grid.
|
||||
</span>
|
||||
<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">
|
||||
<p>
|
||||
<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>
|
||||
The small colored labels above the text (
|
||||
<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.
|
||||
</p>
|
||||
<p>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export function MarkdownGuide() {
|
|||
|
||||
<GuideSection title="Footnotes & citations">
|
||||
<Example>Text.[^1]</Example>
|
||||
<Example>[^1]: Footnote or citation — source, URL</Example>
|
||||
<Example>[^1]: Footnote or citation (source, URL)</Example>
|
||||
</GuideSection>
|
||||
|
||||
<GuideSection title="Layout">
|
||||
|
|
@ -98,12 +98,12 @@ Right column content
|
|||
</GuideSection>
|
||||
|
||||
<GuideSection title="Keybinds">
|
||||
<Example>Ctrl/Cmd+B — bold</Example>
|
||||
<Example>Ctrl/Cmd+I — italic</Example>
|
||||
<Example>Ctrl/Cmd+Shift+X — strikethrough</Example>
|
||||
<Example>Ctrl/Cmd+Shift+H — heading</Example>
|
||||
<Example>Ctrl/Cmd+K — open wikilink dialog</Example>
|
||||
<Example>Ctrl/Cmd+E — inline code</Example>
|
||||
<Example>Ctrl/Cmd+B: bold</Example>
|
||||
<Example>Ctrl/Cmd+I: italic</Example>
|
||||
<Example>Ctrl/Cmd+Shift+X: strikethrough</Example>
|
||||
<Example>Ctrl/Cmd+Shift+H: heading</Example>
|
||||
<Example>Ctrl/Cmd+K: open wikilink dialog</Example>
|
||||
<Example>Ctrl/Cmd+E: inline code</Example>
|
||||
</GuideSection>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export async function markStaleServersOffline(
|
|||
});
|
||||
await emitGameEvent(payload, {
|
||||
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,
|
||||
)}s.`,
|
||||
targetCollection: "game-servers",
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export async function processPlayerKill(
|
|||
const user = users.docs[0];
|
||||
if (!user) {
|
||||
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;
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ export async function processPlayerKill(
|
|||
const profile = profiles.docs[0];
|
||||
if (!profile) {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ export async function applyTransaction(
|
|||
type,
|
||||
amount: -totalDebit,
|
||||
balanceAfter: fromNewBalance,
|
||||
memo: memo ?? `${type} — out`,
|
||||
memo: memo ?? `${type}: out`,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
|
@ -249,7 +249,7 @@ export async function applyTransaction(
|
|||
type,
|
||||
amount,
|
||||
balanceAfter: toNewBalance,
|
||||
memo: memo ?? `${type} — in`,
|
||||
memo: memo ?? `${type}: in`,
|
||||
timestamp,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export async function hireStaff(
|
|||
): Promise<NpcStaffing> {
|
||||
await assertStaffHiringEnabled(payload);
|
||||
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 npc = await payload
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ async function processSalaries(
|
|||
: staffing.positionTitle;
|
||||
const memo = isLabor
|
||||
? `Labor: ${headcount} ${classificationLabel}(s) @ ${unitSalary}`
|
||||
: `Salary: ${staffing.positionTitle} — ${npcName}`;
|
||||
: `Salary: ${staffing.positionTitle} (${npcName})`;
|
||||
try {
|
||||
await applyTransaction(payload, {
|
||||
type: "salary",
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export async function autoCompleteMissions(
|
|||
});
|
||||
await emitGameEvent(payload, {
|
||||
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",
|
||||
targetId: mission.id,
|
||||
data: { previousStatus },
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ type DialogueLine = (amount: string, asking: string) => string;
|
|||
const LINES: Record<NpcDialogueKind, DialogueLine[]> = {
|
||||
firm: [
|
||||
(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.`,
|
||||
],
|
||||
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) => `I paid more than that for lunch! ${amount} — final offer.`,
|
||||
(amount) => `I paid more than that for lunch! ${amount}, final offer.`,
|
||||
],
|
||||
backward: [
|
||||
(amount) => `Now you're offering less than before? I'm holding firm at ${amount}.`,
|
||||
|
|
@ -26,22 +26,22 @@ const LINES: Record<NpcDialogueKind, DialogueLine[]> = {
|
|||
concede: [
|
||||
(amount) => `Hmm, not sure about that... how about ${amount}?`,
|
||||
(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}?`,
|
||||
],
|
||||
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.",
|
||||
() => "That works for me. A man's gotta make a living, after all.",
|
||||
],
|
||||
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.",
|
||||
() => "Whoa, okay! Not going to talk you out of that one. Done deal.",
|
||||
],
|
||||
closed: [
|
||||
(_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: [
|
||||
() => "I think that price is acceptable. Let's shake on it.",
|
||||
|
|
|
|||
|
|
@ -102,12 +102,12 @@ export function storageViolationMessage(resourceName: string, check: StorageRule
|
|||
case "prohibited":
|
||||
return `This resource is prohibited from storage at this structure.`;
|
||||
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": {
|
||||
const available = Math.max(0, (check.cap ?? 0) - (check.currentStored ?? 0));
|
||||
return available <= 0
|
||||
? `${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:
|
||||
return `This resource cannot be stored here.`;
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const script = async (config: SanitizedConfig) => {
|
|||
});
|
||||
|
||||
if (result.disabled) {
|
||||
log.info("[Economy Tick] Master gate is off — nothing processed.");
|
||||
log.info("[Economy Tick] Master gate is off, nothing processed.");
|
||||
} else {
|
||||
await emitEconomyEvents(payload, result);
|
||||
log.info(
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export const script = async (config: SanitizedConfig) => {
|
|||
await emitGameEvent(payload, {
|
||||
type: EventTypes.MarketListingExpire,
|
||||
message: `Listing for ${listing.quantity}× ${assetName} expired${
|
||||
isUserListing ? " — stock returned to seller" : ""
|
||||
isUserListing ? ", stock returned to seller" : ""
|
||||
}`,
|
||||
actor: sellerId ?? undefined,
|
||||
targetCollection: "market-listings",
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export async function processShipmentTick(
|
|||
|
||||
await emitGameEvent(payload, {
|
||||
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,
|
||||
targetCollection: "shipments",
|
||||
targetId: shipment.id,
|
||||
|
|
@ -231,7 +231,7 @@ async function completeShipment(
|
|||
status: "failed",
|
||||
failureReason: `Destination storage rules prevented delivery: ${bounced
|
||||
.map((b) => `${b.resourceName} ×${b.amount.toLocaleString()}`)
|
||||
.join(", ")} — returned to origin.`,
|
||||
.join(", ")}; returned to origin.`,
|
||||
completedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
|
@ -321,7 +321,7 @@ async function createAutoReturnShipment(
|
|||
if (vehicle.currentFuel < returnFuelCost) {
|
||||
await emitGameEvent(payload, {
|
||||
type: EventTypes.ShipmentFail,
|
||||
message: `Auto-return failed — insufficient fuel`,
|
||||
message: `Auto-return failed: insufficient fuel`,
|
||||
structure: destinationId,
|
||||
targetCollection: "game-vehicles",
|
||||
targetId: vehicle.id,
|
||||
|
|
|
|||
|
|
@ -174,9 +174,7 @@ function run(
|
|||
stderr += s;
|
||||
process.stderr.write(s);
|
||||
});
|
||||
child.on("close", (code) =>
|
||||
resolve({ code: code ?? 1, stdout, stderr }),
|
||||
);
|
||||
child.on("close", (code) => 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) {
|
||||
const uri = loadDatabaseUri(forced as EnvName);
|
||||
if (!uri) {
|
||||
log(
|
||||
`${RED}Cannot find DATABASE_URI in ${ENVS[forced as EnvName].file}${RESET}`,
|
||||
);
|
||||
log(`${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`);
|
||||
process.exit(1);
|
||||
}
|
||||
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");
|
||||
|
||||
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);
|
||||
if (!uri) {
|
||||
log(`\n${RED}DATABASE_URI not found in ${ENVS[env].file}${RESET}`);
|
||||
log(
|
||||
`Create the file with: ${BOLD}DATABASE_URI=postgres://user:pass@host:5432/db${RESET}`,
|
||||
);
|
||||
log(`Create the file with: ${BOLD}DATABASE_URI=postgres://user:pass@host:5432/db${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
|
@ -259,9 +253,7 @@ async function main() {
|
|||
if (freshOnly) {
|
||||
if (env !== "dev") {
|
||||
log(`${RED}${BOLD}--fresh is only allowed for dev.${RESET}`);
|
||||
log(
|
||||
`${DIM}For staging/production, use migrate (up only) or migrate:fresh manually.${RESET}`,
|
||||
);
|
||||
log(`${DIM}For staging/production, use migrate (up only) or migrate:fresh manually.${RESET}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const confirm = await ask(
|
||||
|
|
@ -281,11 +273,9 @@ async function main() {
|
|||
|
||||
// Show current status
|
||||
log(`${DIM}Migration status:${RESET}\n`);
|
||||
const statusResult = await run(
|
||||
"bun",
|
||||
["run", "payload", "migrate:status"],
|
||||
{ DATABASE_URI: uri },
|
||||
);
|
||||
const statusResult = await run("bun", ["run", "payload", "migrate:status"], {
|
||||
DATABASE_URI: uri,
|
||||
});
|
||||
|
||||
// Check for pending migrations
|
||||
const hasPending = statusResult.stdout.includes("│ No │");
|
||||
|
|
@ -297,26 +287,16 @@ async function main() {
|
|||
// Confirm
|
||||
log("");
|
||||
if (env === "prd") {
|
||||
log(
|
||||
`${RED}${BOLD} ╔══════════════════════════════════════════╗${RESET}`,
|
||||
);
|
||||
log(
|
||||
`${RED}${BOLD} ║ YOU ARE ABOUT TO MIGRATE PRODUCTION ║${RESET}`,
|
||||
);
|
||||
log(
|
||||
`${RED}${BOLD} ╚══════════════════════════════════════════╝${RESET}\n`,
|
||||
);
|
||||
const confirm = await ask(
|
||||
`${RED}Type "migrate" to confirm: ${RESET}`,
|
||||
);
|
||||
log(`${RED}${BOLD} ╔══════════════════════════════════════════╗${RESET}`);
|
||||
log(`${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") {
|
||||
log("Aborted.");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const confirm = await ask(
|
||||
`${YELLOW}Run migrations on ${cfg.label}? (y/N): ${RESET}`,
|
||||
);
|
||||
const confirm = await ask(`${YELLOW}Run migrations on ${cfg.label}? (y/N): ${RESET}`);
|
||||
if (confirm.toLowerCase() !== "y") {
|
||||
log("Aborted.");
|
||||
return;
|
||||
|
|
@ -327,12 +307,7 @@ async function main() {
|
|||
// push:true's batch=-1 record) is auto-accepted instead of blocking on
|
||||
// non-interactive stdin.
|
||||
log(`\n${BOLD}Running migrations...${RESET}\n`);
|
||||
const result = await run(
|
||||
"bun",
|
||||
["run", "payload", "migrate"],
|
||||
{ DATABASE_URI: uri },
|
||||
"y\n",
|
||||
);
|
||||
const result = await run("bun", ["run", "payload", "migrate"], { DATABASE_URI: uri }, "y\n");
|
||||
|
||||
if (result.code !== 0) {
|
||||
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 }> {
|
||||
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");
|
||||
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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ const DEFAULT_XP_LEVELS = [
|
|||
name: "Recruit",
|
||||
requiredPoints: 0,
|
||||
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