From 474aff734dd303fd25de3ee8b03e68a7b9abd625 Mon Sep 17 00:00:00 2001 From: Z8MB1E Date: Mon, 14 Sep 2026 15:38:05 -0400 Subject: [PATCH] feat(blackjack): add blackjack minigame with realtime table wiring Engine, felt, controls, sounds, card skins, and dev popups; SSE events in the realtime hook, nav and command palette entries, stats migration, and the six blackjack test suites. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../(frontend)/minigames/blackjack/actions.ts | 182 + .../(frontend)/minigames/blackjack/page.tsx | 84 + .../command-palette/commands/navigation.ts | 8 + src/components/frontend/blocks/NavUser.tsx | 7 + .../blackjack/BlackjackActionConsole.tsx | 201 + .../blackjack/BlackjackActionToolbar.tsx | 96 + .../minigames/blackjack/BlackjackArena.tsx | 350 + .../blackjack/BlackjackCardStyleToggle.tsx | 59 + .../blackjack/BlackjackChipStack.tsx | 132 + .../minigames/blackjack/BlackjackClock.tsx | 61 + .../minigames/blackjack/BlackjackControls.tsx | 198 + .../blackjack/BlackjackFeltCanvas.tsx | 373 + .../blackjack/BlackjackGameTable.tsx | 186 + .../minigames/blackjack/BlackjackGuide.tsx | 87 + .../blackjack/BlackjackLeaderboard.tsx | 240 + .../minigames/blackjack/BlackjackLobby.tsx | 325 + .../minigames/blackjack/BlackjackPopups.tsx | 12 + .../minigames/blackjack/BlackjackSeat.tsx | 152 + .../blackjack/BlackjackShoeOrigin.tsx | 11 + .../blackjack/BlackjackSoundToggle.tsx | 63 + .../minigames/blackjack/BlackjackTable.tsx | 285 + .../frontend/minigames/blackjack/CardView.tsx | 123 + .../minigames/blackjack/RenameDialogGuard.tsx | 107 + .../minigames/blackjack/RenameTableDialog.tsx | 100 + .../minigames/blackjack/blackjackPopups.ts | 176 + .../minigames/blackjack/blackjackSound.ts | 348 + .../blackjack/blackjackSoundEvents.ts | 190 + .../minigames/blackjack/cardStyle.tsx | 91 + .../frontend/minigames/blackjack/events.ts | 88 + .../minigames/blackjack/seatPosition.ts | 41 + .../frontend/minigames/blackjack/types.ts | 7 + .../minigames/blackjack/useBlackjackPopups.ts | 111 + .../minigames/blackjack/useBlackjackSounds.ts | 195 + .../frontend/realtime/GameTickRealtime.tsx | 33 + src/hooks/useGameTick.ts | 23 + src/lib/minigames/blackjack/actions.ts | 360 + src/lib/minigames/blackjack/constants.ts | 51 + src/lib/minigames/blackjack/engine.ts | 545 + src/lib/minigames/blackjack/index.ts | 13 + src/lib/minigames/blackjack/rules.ts | 98 + src/lib/minigames/blackjack/shoe.ts | 75 + src/lib/minigames/blackjack/stats.ts | 186 + src/lib/minigames/blackjack/tables.ts | 896 + src/lib/minigames/blackjack/types.ts | 215 + src/lib/minigames/blackjack/views.ts | 157 + ...2_071740_add_blackjack_minigame_stats.json | 26786 ++++++++++++++++ ...912_071740_add_blackjack_minigame_stats.ts | 31 + tests/int/blackjack-card-skins.int.spec.ts | 45 + tests/int/blackjack-engine.int.spec.ts | 546 + tests/int/blackjack-popups.int.spec.ts | 247 + tests/int/blackjack-sound.int.spec.ts | 198 + tests/int/blackjack-stats.int.spec.ts | 133 + tests/int/blackjack-tables.int.spec.ts | 189 + 53 files changed, 35516 insertions(+) create mode 100644 src/app/(frontend)/minigames/blackjack/actions.ts create mode 100644 src/app/(frontend)/minigames/blackjack/page.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackActionConsole.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackActionToolbar.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackArena.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackCardStyleToggle.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackChipStack.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackClock.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackControls.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackFeltCanvas.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackGameTable.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackGuide.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackLeaderboard.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackLobby.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackPopups.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackSeat.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackShoeOrigin.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackSoundToggle.tsx create mode 100644 src/components/frontend/minigames/blackjack/BlackjackTable.tsx create mode 100644 src/components/frontend/minigames/blackjack/CardView.tsx create mode 100644 src/components/frontend/minigames/blackjack/RenameDialogGuard.tsx create mode 100644 src/components/frontend/minigames/blackjack/RenameTableDialog.tsx create mode 100644 src/components/frontend/minigames/blackjack/blackjackPopups.ts create mode 100644 src/components/frontend/minigames/blackjack/blackjackSound.ts create mode 100644 src/components/frontend/minigames/blackjack/blackjackSoundEvents.ts create mode 100644 src/components/frontend/minigames/blackjack/cardStyle.tsx create mode 100644 src/components/frontend/minigames/blackjack/events.ts create mode 100644 src/components/frontend/minigames/blackjack/seatPosition.ts create mode 100644 src/components/frontend/minigames/blackjack/types.ts create mode 100644 src/components/frontend/minigames/blackjack/useBlackjackPopups.ts create mode 100644 src/components/frontend/minigames/blackjack/useBlackjackSounds.ts create mode 100644 src/lib/minigames/blackjack/actions.ts create mode 100644 src/lib/minigames/blackjack/constants.ts create mode 100644 src/lib/minigames/blackjack/engine.ts create mode 100644 src/lib/minigames/blackjack/index.ts create mode 100644 src/lib/minigames/blackjack/rules.ts create mode 100644 src/lib/minigames/blackjack/shoe.ts create mode 100644 src/lib/minigames/blackjack/stats.ts create mode 100644 src/lib/minigames/blackjack/tables.ts create mode 100644 src/lib/minigames/blackjack/types.ts create mode 100644 src/lib/minigames/blackjack/views.ts create mode 100644 src/migrations/20260912_071740_add_blackjack_minigame_stats.json create mode 100644 src/migrations/20260912_071740_add_blackjack_minigame_stats.ts create mode 100644 tests/int/blackjack-card-skins.int.spec.ts create mode 100644 tests/int/blackjack-engine.int.spec.ts create mode 100644 tests/int/blackjack-popups.int.spec.ts create mode 100644 tests/int/blackjack-sound.int.spec.ts create mode 100644 tests/int/blackjack-stats.int.spec.ts create mode 100644 tests/int/blackjack-tables.int.spec.ts diff --git a/src/app/(frontend)/minigames/blackjack/actions.ts b/src/app/(frontend)/minigames/blackjack/actions.ts new file mode 100644 index 0000000..9efa5e3 --- /dev/null +++ b/src/app/(frontend)/minigames/blackjack/actions.ts @@ -0,0 +1,182 @@ +"use server"; + +/** + * Server-action surface for the blackjack minigame. + * All table logic lives in the registry (src/lib/minigames/blackjack/tables.ts). + */ + +import config from "@payload-config"; +import { getPayload } from "payload"; +import type { BlackjackActionKind } from "@/lib/minigames/blackjack/types"; +import { isSuperuser } from "@/utils/access-control/hasPermission"; +import { + applyBlackjackAction, + createTable, + forceExpireTable, + getSnapshotFor, + joinTableWithBuyIn, + leaveTableWithBuyIn, + listSummaries, + renameTable, + spectateTable, + startGameWithBuyIn, + type BlackjackSnapshot, + type BlackjackTableSummary, +} from "@/lib/minigames/blackjack/tables"; + +export type { BlackjackSnapshot, BlackjackTableSummary }; + +interface ActionResult { + success: boolean; + error?: string; + data?: T; +} + +async function authenticate() { + const payloadConfig = await config; + const payload = await getPayload({ config: payloadConfig }); + const { headers } = await import("next/headers"); + const hdrs = await headers(); + const { user } = await payload.auth({ + headers: hdrs, + canSetHeaders: false, + }); + + if (!user) { + throw new Error("Unauthorized"); + } + + return { payload, user }; +} + +function toError(error: unknown): { success: false; error: string } { + const message = error instanceof Error ? error.message : "Unknown error"; + if (message === "Unauthorized") { + return { success: false, error: "You must be logged in." }; + } + return { success: false, error: message }; +} + +interface UserLike { + id: number; + displayName?: string | null; + username?: string | null; +} + +function userName(user: UserLike): string { + if (typeof user.displayName === "string" && user.displayName) return user.displayName; + if (typeof user.username === "string" && user.username) return user.username; + return `Player ${user.id}`; +} + +export async function createBlackjackTable(): Promise> { + try { + const { user } = await authenticate(); + const tableId = createTable(Number(user.id), userName(user)); + return { success: true, data: { tableId } }; + } catch (error) { + return toError(error); + } +} + +export async function listBlackjackTables(): Promise> { + try { + await authenticate(); + return { success: true, data: listSummaries() }; + } catch (error) { + return toError(error); + } +} + +export async function forceExpireBlackjackTable(tableId: string): Promise> { + try { + const { payload, user } = await authenticate(); + const allowed = await isSuperuser(payload, user); + if (!allowed) { + return { success: false, error: "Only administrators can force-end tables." }; + } + forceExpireTable(tableId, payload); + return { success: true }; + } catch (error) { + return toError(error); + } +} + +export async function joinBlackjackTable(tableId: string): Promise> { + try { + const { user } = await authenticate(); + const snapshot = await joinTableWithBuyIn(tableId, Number(user.id), userName(user)); + return { success: true, data: snapshot }; + } catch (error) { + return toError(error); + } +} + +export async function spectateBlackjackTable( + tableId: string, +): Promise> { + try { + const { user } = await authenticate(); + const snapshot = spectateTable(tableId, Number(user.id)); + return { success: true, data: snapshot }; + } catch (error) { + return toError(error); + } +} + +export async function leaveBlackjackTable(tableId: string): Promise> { + try { + const { user } = await authenticate(); + await leaveTableWithBuyIn(tableId, Number(user.id)); + return { success: true }; + } catch (error) { + return toError(error); + } +} + +export async function startBlackjackGame(tableId: string): Promise> { + try { + const { user } = await authenticate(); + const snapshot = await startGameWithBuyIn(tableId, Number(user.id)); + return { success: true, data: snapshot }; + } catch (error) { + return toError(error); + } +} + +export async function renameBlackjackTable( + tableId: string, + rawName: string, +): Promise> { + try { + const { user } = await authenticate(); + const snapshot = renameTable(tableId, Number(user.id), rawName); + return { success: true, data: snapshot }; + } catch (error) { + return toError(error); + } +} + +export async function fetchBlackjackTable(tableId: string): Promise> { + try { + const { user } = await authenticate(); + const snapshot = getSnapshotFor(tableId, Number(user.id)); + return { success: true, data: snapshot }; + } catch (error) { + return toError(error); + } +} + +export async function blackjackAction( + tableId: string, + kind: BlackjackActionKind, + amount?: number, +): Promise> { + try { + const { user } = await authenticate(); + const snapshot = applyBlackjackAction(tableId, Number(user.id), kind, amount); + return { success: true, data: snapshot }; + } catch (error) { + return toError(error); + } +} diff --git a/src/app/(frontend)/minigames/blackjack/page.tsx b/src/app/(frontend)/minigames/blackjack/page.tsx new file mode 100644 index 0000000..890c27a --- /dev/null +++ b/src/app/(frontend)/minigames/blackjack/page.tsx @@ -0,0 +1,84 @@ +import type { Metadata } from "next"; +import config from "@payload-config"; +import { getPayload } from "payload"; +import { headers as nextHeaders } from "next/headers"; +import { redirect } from "next/navigation"; +import { Club } from "lucide-react"; +import { BlackjackArena } from "@/components/frontend/minigames/blackjack/BlackjackArena"; +import { readBlackjackStats } from "@/lib/minigames/blackjack/stats"; +import { isSuperuser, superuserUserIds } from "@/utils/access-control/hasPermission"; + +export const metadata: Metadata = { + title: "Blackjack: Polaris Task Force", + description: "A multiplayer blackjack table for Polaris Task Force operators, solo or with up to six players.", +}; + +interface BlackjackPageProps { + searchParams: Promise<{ + table?: string | string[]; + }>; +} + +export default async function BlackjackPage({ searchParams }: BlackjackPageProps) { + const headers = await nextHeaders(); + const payload = await getPayload({ config }); + const { user } = await payload.auth({ headers, canSetHeaders: false }); + + if (!user) { + redirect(`/login?returnTo=${encodeURIComponent("/minigames/blackjack")}`); + } + + const superuser = await isSuperuser(payload, user); + const superuserIds = await superuserUserIds(payload); + + const profileRes = await payload.find({ + collection: "profiles", + where: { user: { equals: Number(user.id) } }, + limit: 1, + depth: 0, + overrideAccess: true, + select: { + progression: { + minigames: true, + chipBank: true, + chipBankRefilledAt: true, + }, + }, + }); + + const stats = readBlackjackStats(profileRes.docs[0]); + const progressionDoc = (profileRes.docs[0]?.progression ?? {}) as { + chipBank?: number | null; + chipBankRefilledAt?: string | null; + }; + const initialChipBank = progressionDoc.chipBank ?? 0; + const initialChipTopUpAt = progressionDoc.chipBankRefilledAt ?? null; + const params = await searchParams; + const tableValue = params.table; + const initialTableId = Array.isArray(tableValue) ? tableValue[0] : tableValue; + + return ( +
+
+
+ +

Blackjack

+
+

+ Take a seat at the table, solo or with up to five others. Beat the dealer to 21 without + going bust. +

+
+ + +
+ ); +} diff --git a/src/components/command-palette/commands/navigation.ts b/src/components/command-palette/commands/navigation.ts index 0223a26..fb71e70 100644 --- a/src/components/command-palette/commands/navigation.ts +++ b/src/components/command-palette/commands/navigation.ts @@ -26,6 +26,7 @@ import { Bomb, ScrollText, Spade, + Club, } from "lucide-react"; export interface NavigationCommand { @@ -277,6 +278,13 @@ export const navigationCommands: NavigationCommand[] = [ href: "/minigames/poker", keywords: ["minigame", "poker", "cards", "holdem", "training"], }, + { + id: "nav:blackjack", + label: "Blackjack", + icon: Club, + href: "/minigames/blackjack", + keywords: ["minigame", "blackjack", "21", "cards", "training"], + }, { id: "nav:account", label: "Account", diff --git a/src/components/frontend/blocks/NavUser.tsx b/src/components/frontend/blocks/NavUser.tsx index e01e525..69b7d8b 100644 --- a/src/components/frontend/blocks/NavUser.tsx +++ b/src/components/frontend/blocks/NavUser.tsx @@ -12,6 +12,7 @@ import { Mic, Radio, Spade, + Club, Users2, } from "lucide-react"; import Link from "next/link"; @@ -180,6 +181,12 @@ export function NavUser({ Poker + + + + Blackjack + + diff --git a/src/components/frontend/minigames/blackjack/BlackjackActionConsole.tsx b/src/components/frontend/minigames/blackjack/BlackjackActionConsole.tsx new file mode 100644 index 0000000..ab53914 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackActionConsole.tsx @@ -0,0 +1,201 @@ +"use client"; + +import { Eye, Shield } from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { PublicPlayerView } from "@/lib/minigames/blackjack"; +import { handValue } from "@/lib/minigames/blackjack"; +import type { BlackjackSnapshot } from "@/app/(frontend)/minigames/blackjack/actions"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { BlackjackClock } from "./BlackjackClock"; + +type SeatedSnapshot = BlackjackSnapshot & { + readonly you: NonNullable; +}; + +const PHASE_COPY: Record = { + betting: "Place your bet", + insurance: "Insurance offered", + player: "Play your hand", + dealer: "Dealer plays", + "round-complete": "Round settled", +}; + +interface BlackjackActionConsoleProps { + readonly snapshot: SeatedSnapshot; + readonly hostSeatId: number | null; + readonly spectator?: boolean; + readonly onJoin?: () => void; + readonly isSuperuser?: boolean; + readonly starting: boolean; + readonly leaving: boolean; + readonly onStart: () => void; + readonly onLeave: () => void; +} + +export function BlackjackActionConsole({ + snapshot, + hostSeatId, + spectator = false, + onJoin, + isSuperuser = false, + starting, + leaving, + onStart, + onLeave, +}: BlackjackActionConsoleProps) { + const view = snapshot.you; + const viewerPlayer: PublicPlayerView | undefined = view.players.find( + (player) => player.seatId === view.viewerSeat, + ); + const seatsOpen = snapshot.seats.filter((seat) => seat === null).length; + const chipsInPlay = viewerPlayer?.hands.reduce((sum, hand) => sum + hand.bet, 0) ?? 0; + const viewerInitial = (viewerPlayer?.name ?? "You").trim().charAt(0).toUpperCase() || "?"; + const yourHandTotals = view.yourHands.map((hand) => handValue(hand.cards).total); + const isHost = view.viewerSeat === hostSeatId; + + return ( +
+
+ {spectator ? ( + <> + +
+
+
+

Spectating

+

+ {seatsOpen > 0 + ? `${seatsOpen} seat${seatsOpen > 1 ? "s" : ""} open` + : "No open seats"} +

+
+
+
+ + ) : ( + <> + +
+
+
+

+ {viewerPlayer?.name ?? "You"} + {isSuperuser && ( + + )} + (you) +

+

+ Bankroll {viewerPlayer?.bankroll.toLocaleString() ?? "0"} +

+
+
+ {isHost && Host} + {viewerPlayer && viewerPlayer.bankrupt && ( + Busted + )} +
+
+ {yourHandTotals.length > 0 && ( + + You have:{" "} + {yourHandTotals + .map((total, index) => + yourHandTotals.length > 1 ? `H${index + 1} ${total}` : String(total), + ) + .join(" / ")} + + )} +
+ + )} +
+ +
+ {!spectator && ( +
+
+ )} + {spectator && seatsOpen > 0 && onJoin && ( + + )} + {view.phase !== "round-complete" && ( +
+ +
+ )} +
+ + {view.phase === "round-complete" && ( +
+
+
+ Round settled + + The next betting round starts automatically. + +
+
+ {snapshot.nextRoundAt && ( + + )} + {!spectator && ( + + )} +
+
+
+ )} + {starting && Starting} +
+ ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackActionToolbar.tsx b/src/components/frontend/minigames/blackjack/BlackjackActionToolbar.tsx new file mode 100644 index 0000000..d5423d1 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackActionToolbar.tsx @@ -0,0 +1,96 @@ +"use client"; + +import type { BlackjackSnapshot } from "@/app/(frontend)/minigames/blackjack/actions"; +import { BlackjackChipStack } from "./BlackjackChipStack"; +import { BlackjackControls } from "./BlackjackControls"; + +type SeatedSnapshot = BlackjackSnapshot & { + readonly you: NonNullable; +}; + +interface BlackjackActionToolbarProps { + readonly snapshot: SeatedSnapshot; + readonly onSnapshot: (snapshot: BlackjackSnapshot) => void; + readonly onActionFeedback: () => void; +} + +/** + * Minimal felt overlay: one bottom-centered column holding the bankroll + * pill plus either the waiting pill or the acting player's controls. + * Rendering them as a single stack keeps the pair collision-free at every + * phase; on a settled round only the bankroll pill remains. + */ +export function BlackjackActionToolbar({ + snapshot, + onSnapshot, + onActionFeedback, +}: BlackjackActionToolbarProps) { + const view = snapshot.you; + const viewerPlayer = view.players.find((player) => player.seatId === view.viewerSeat); + const viewerBet = viewerPlayer?.hands.reduce((sum, hand) => sum + hand.bet, 0) ?? 0; + const settled = view.phase === "round-complete"; + + const bankrollPill = viewerPlayer && ( + + + + Bankroll + + + {viewerPlayer.bankroll.toLocaleString()} + + {viewerBet > 0 && ( + <> + + ); + + if (settled) { + return ( +
+ {bankrollPill} +
+ ); + } + + return ( +
+
+ {bankrollPill} + {!view.yourTurn ? ( + + {(() => { + const actingPlayer = view.players.find( + (player) => player.seatId === view.activeSeat, + ); + return actingPlayer ? `Waiting for ${actingPlayer.name}` : "Waiting for the table"; + })()} + + ) : ( + + )} +
+
+ ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackArena.tsx b/src/components/frontend/minigames/blackjack/BlackjackArena.tsx new file mode 100644 index 0000000..60a025e --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackArena.tsx @@ -0,0 +1,350 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import { + fetchBlackjackTable, + joinBlackjackTable, + leaveBlackjackTable, + spectateBlackjackTable, + startBlackjackGame, + type BlackjackSnapshot, +} from "@/app/(frontend)/minigames/blackjack/actions"; +import type { BlackjackPhase } from "@/lib/minigames/blackjack"; +import { useRealtimePresence } from "@/hooks/useRealtimePresence"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { ChipBankCard } from "@/components/frontend/minigames/ChipBankCard"; +import { BlackjackGuide } from "./BlackjackGuide"; +import { + BLACKJACK_STATE_EVENT, + BLACKJACK_TABLES_EVENT, + BLACKJACK_TERMINATED_EVENT, + readBlackjackStateEvent, + readBlackjackTerminatedEvent, +} from "./events"; +import { BlackjackLeaderboard } from "./BlackjackLeaderboard"; +import { BlackjackLobby } from "./BlackjackLobby"; +import { BlackjackTable } from "./BlackjackTable"; +import type { BlackjackStats } from "./types"; + +interface BlackjackArenaProps { + readonly initialStats: BlackjackStats; + readonly currentUserId: number | string; + readonly initialTableId?: string; + readonly isSuperuser: boolean; + readonly superuserIds: readonly number[]; + readonly initialChipBank: number; + readonly initialChipTopUpAt: string | null; +} + +type ArenaView = "lobby" | "table"; + +const STAT_ITEMS = [ + ["Sessions", "gamesPlayed"], + ["Rounds won", "roundsWon"], + ["Blackjacks", "blackjacksHit"], + ["Best bankroll", "bestScore"], + ["XP earned", "xpEarned"], +] as const satisfies readonly (readonly [string, keyof BlackjackStats])[]; + +export function BlackjackArena({ + initialStats, + currentUserId, + initialTableId, + isSuperuser, + superuserIds, + initialChipBank, + initialChipTopUpAt, +}: BlackjackArenaProps) { + const [view, setView] = useState(initialTableId ? "table" : "lobby"); + const [snapshot, setSnapshot] = useState(null); + const [activeTableId, setActiveTableId] = useState(initialTableId ?? null); + const [tableError, setTableError] = useState(null); + const [loading, setLoading] = useState(Boolean(initialTableId)); + const [terminated, setTerminated] = useState(null); + const [leaderboardRefreshKey, setLeaderboardRefreshKey] = useState(0); + const [lobbyRefreshKey, setLobbyRefreshKey] = useState(0); + const lastRoundRef = useRef(null); + const syncedTableIdRef = useRef(null); + + const applySnapshot = useCallback((next: BlackjackSnapshot) => { + setSnapshot((current) => { + if (current && next.version <= current.version) return current; + return next; + }); + setActiveTableId(next.tableId); + setTerminated((current) => (current !== next.tableId ? null : current)); + if (syncedTableIdRef.current !== next.tableId) { + syncedTableIdRef.current = next.tableId; + window.history.replaceState( + null, + "", + `/minigames/blackjack?table=${encodeURIComponent(next.tableId)}`, + ); + } + setView("table"); + setTableError(null); + setLoading(false); + + const nextRound = next.you?.roundNumber ?? null; + if (nextRound !== null && lastRoundRef.current !== null && nextRound !== lastRoundRef.current) { + setLeaderboardRefreshKey((key) => key + 1); + } + lastRoundRef.current = nextRound; + }, []); + + useEffect(() => { + if (!initialTableId) { + setLoading(false); + return; + } + + let cancelled = false; + setLoading(true); + void fetchBlackjackTable(initialTableId) + .then((result) => { + if (cancelled) return; + if (result.success && result.data) { + applySnapshot(result.data); + } else { + setTableError(result.error ?? "This blackjack table was not found."); + setLoading(false); + } + }) + .catch((caught: unknown) => { + if (cancelled) return; + setTableError( + caught instanceof Error ? caught.message : "This blackjack table could not be loaded.", + ); + setLoading(false); + }); + + return () => { + cancelled = true; + }; + }, [applySnapshot, initialTableId]); + + useEffect(() => { + const handleState = (event: Event) => { + const detail = readBlackjackStateEvent(event); + if (!detail || detail.tableId !== activeTableId) return; + applySnapshot(detail.snapshot); + }; + const handleTables = () => setLobbyRefreshKey((key) => key + 1); + const handleTerminated = (event: Event) => { + const detail = readBlackjackTerminatedEvent(event); + if (!detail || detail.tableId !== activeTableId) return; + setTerminated(detail.tableId); + }; + window.addEventListener(BLACKJACK_STATE_EVENT, handleState); + window.addEventListener(BLACKJACK_TABLES_EVENT, handleTables); + window.addEventListener(BLACKJACK_TERMINATED_EVENT, handleTerminated); + return () => { + window.removeEventListener(BLACKJACK_STATE_EVENT, handleState); + window.removeEventListener(BLACKJACK_TABLES_EVENT, handleTables); + window.removeEventListener(BLACKJACK_TERMINATED_EVENT, handleTerminated); + }; + }, [activeTableId, applySnapshot]); + + // Polls are the correctness path: the in-memory SSE bus cannot reliably + // deliver server-action pushes (dev per-bundle module duplication). The + // SSE push stays as an instant bonus; applySnapshot dedupes by version. + useEffect(() => { + if (view !== "table" || activeTableId === null || terminated === activeTableId) return; + let cancelled = false; + const poll = window.setInterval(() => { + void fetchBlackjackTable(activeTableId) + .then((result) => { + if (cancelled) return; + if (result.success && result.data) applySnapshot(result.data); + }) + .catch(() => undefined); + }, 1250); + return () => { + cancelled = true; + window.clearInterval(poll); + }; + }, [activeTableId, terminated, view, applySnapshot]); + + const resetToLobby = useCallback(() => { + setView("lobby"); + setSnapshot(null); + setActiveTableId(null); + setTableError(null); + setLoading(false); + setTerminated(null); + lastRoundRef.current = null; + if (syncedTableIdRef.current !== null) { + syncedTableIdRef.current = null; + window.history.replaceState(null, "", "/minigames/blackjack"); + } + }, []); + + const leaveTable = useCallback(async () => { + const seated = snapshot?.seats.some((id) => String(id) === String(currentUserId)) ?? false; + if (activeTableId && seated) { + try { + window.dispatchEvent(new Event("chipBank:showSpinner")); + const result = await leaveBlackjackTable(activeTableId); + if (!result.success) { + setTableError(result.error ?? "Could not leave the blackjack table."); + return; + } + } catch (caught) { + setTableError( + caught instanceof Error ? caught.message : "Could not leave the blackjack table.", + ); + return; + } + } + resetToLobby(); + }, [activeTableId, currentUserId, resetToLobby, snapshot]); + + const joinCurrentTable = useCallback(async () => { + if (!activeTableId) return; + setLoading(true); + setTableError(null); + try { + window.dispatchEvent(new Event("chipBank:showSpinner")); + const result = await joinBlackjackTable(activeTableId); + if (result.success && result.data) { + applySnapshot(result.data); + } else { + setTableError(result.error ?? "Could not join the blackjack table."); + setLoading(false); + } + } catch (caught) { + setTableError( + caught instanceof Error ? caught.message : "Could not join the blackjack table.", + ); + setLoading(false); + } + }, [activeTableId, applySnapshot]); + + const spectateCurrentTable = useCallback(async () => { + if (!activeTableId) return; + setLoading(true); + setTableError(null); + try { + const result = await spectateBlackjackTable(activeTableId); + if (result.success && result.data) { + applySnapshot(result.data); + } else { + setTableError(result.error ?? "Could not spectate this table."); + setLoading(false); + } + } catch (caught: unknown) { + setTableError(caught instanceof Error ? caught.message : "Could not spectate this table."); + setLoading(false); + } + }, [activeTableId, applySnapshot]); + + const startCurrentTable = useCallback(async () => { + if (!activeTableId) return; + try { + window.dispatchEvent(new Event("chipBank:showSpinner")); + const result = await startBlackjackGame(activeTableId); + if (result.success && result.data) { + applySnapshot(result.data); + } else { + setTableError(result.error ?? "Could not start the blackjack game."); + } + } catch (caught) { + setTableError( + caught instanceof Error ? caught.message : "Could not start the blackjack game.", + ); + } + }, [activeTableId, applySnapshot]); + + const activePlayers = useRealtimePresence( + "blackjack", + view === "table" && activeTableId !== null, + true, + ); + + return ( +
+ + +
+
+ Your blackjack record +

+ Results are recorded when a session ends. +

+
+ +
+
+ + {STAT_ITEMS.map(([label, key]) => ( +
+ + {label} + + + {initialStats[key].toLocaleString()} + +
+ ))} +
+
+ + {tableError && snapshot && ( +

+ {tableError} +

+ )} + +
+
+ {view === "lobby" ? ( + + ) : terminated === activeTableId ? ( + + + + Game ended by an administrator + + + +

+ An administrator forcefully ended this blackjack table. +

+ +
+
+ ) : ( + + )} +
+ +
+ + +
+ ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackCardStyleToggle.tsx b/src/components/frontend/minigames/blackjack/BlackjackCardStyleToggle.tsx new file mode 100644 index 0000000..ae9c956 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackCardStyleToggle.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { Images, Moon, Sun } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + BLACKJACK_CARD_STYLE_OPTIONS, + normalizeCardStyle, + useCardStyle, + type BlackjackCardStyle, +} from "./cardStyle"; + +const STYLE_ICONS: Record = { + light: Sun, + dark: Moon, + classic: Images, +}; + +export function BlackjackCardStyleToggle() { + const { style, setStyle } = useCardStyle(); + const Icon = STYLE_ICONS[style]; + const label = + BLACKJACK_CARD_STYLE_OPTIONS.find((option) => option.value === style)?.label ?? "Light"; + + return ( + + + + + + setStyle(normalizeCardStyle(value))} + > + {BLACKJACK_CARD_STYLE_OPTIONS.map((option) => ( + + {option.label} + + ))} + + + + ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackChipStack.tsx b/src/components/frontend/minigames/blackjack/BlackjackChipStack.tsx new file mode 100644 index 0000000..309f10b --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackChipStack.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; + +/** + * Renders an amount as a pile of casino chips: denomination-colored CSS + * discs stacked into piles, popping in as the amount grows. Purely visual; + * the exact number always rides alongside in a text label. + */ + +interface ChipDenomination { + readonly value: number; + readonly tone: string; + readonly label: string; +} + +const DENOMINATIONS: readonly ChipDenomination[] = [ + { value: 1000, tone: "bj-chip-gold", label: "1k" }, + { value: 500, tone: "bj-chip-purple", label: "500" }, + { value: 100, tone: "bj-chip-black", label: "100" }, + { value: 25, tone: "bj-chip-green", label: "25" }, + { value: 10, tone: "bj-chip-blue", label: "10" }, + { value: 5, tone: "bj-chip-red", label: "5" }, + { value: 1, tone: "bj-chip-white", label: "1" }, +]; + +/** Max chips drawn per pile; larger counts compress into the same pile. */ +const MAX_CHIPS_PER_PILE = 5; + +export interface FlightChip { + readonly key: string; + readonly tone: string; +} + +/** + * Denomination breakdown for the flight animation: greedy largest-first, + * capped so big payouts do not spawn endless chips (excess is dropped; the + * exact number always rides in a text label). + */ +export function chipFlightChips(amount: number, maxChips = 8): FlightChip[] { + const chips: FlightChip[] = []; + let remaining = Math.max(0, Math.floor(amount)); + for (const denom of DENOMINATIONS) { + const count = Math.min(Math.floor(remaining / denom.value), maxChips - chips.length); + for (let index = 0; index < count; index += 1) { + chips.push({ key: `${denom.label}-${index}`, tone: denom.tone }); + } + remaining -= count * denom.value; + if (chips.length >= maxChips) break; + } + if (chips.length === 0 && remaining > 0) { + chips.push({ key: "1-0", tone: "bj-chip-white" }); + } + return chips; +} + +/** Must match the .bj-chip height + vertical step in styles.css. */ +const CHIP_HEIGHT_REM = 0.95; +const CHIP_STEP_REM = 0.3; + +interface ChipPile { + readonly key: string; + readonly tone: string; + /** Discs actually drawn (capped by MAX_CHIPS_PER_PILE). */ + readonly count: number; + /** True denomination count, used for the hover tooltip. */ + readonly trueCount: number; + readonly value: number; +} + +function pileUp(amount: number): ChipPile[] { + const piles: ChipPile[] = []; + let remaining = Math.max(0, Math.floor(amount)); + for (const denom of DENOMINATIONS) { + const trueCount = Math.floor(remaining / denom.value); + const drawn = Math.min(MAX_CHIPS_PER_PILE, trueCount); + remaining -= trueCount * denom.value; + if (drawn > 0) { + piles.push({ + key: denom.label, + tone: denom.tone, + count: drawn, + trueCount, + value: denom.value, + }); + } + } + // Amounts below the smallest chip still show a single chip marker. + if (piles.length === 0 && remaining > 0) { + piles.push({ key: "1", tone: "bj-chip-white", count: 1, trueCount: 1, value: 1 }); + } + return piles; +} + +export function BlackjackChipStack({ + amount, + className, +}: { + readonly amount: number; + readonly className?: string; +}) { + if (amount <= 0) return null; + const piles = pileUp(amount); + return ( + + + + ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackClock.tsx b/src/components/frontend/minigames/blackjack/BlackjackClock.tsx new file mode 100644 index 0000000..a5184db --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackClock.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useEffect, useState } from "react"; + +interface BlackjackClockProps { + readonly deadline: string | null; + readonly totalMs?: number; + readonly label?: string; + readonly mode?: "bar" | "text"; +} + +export function BlackjackClock({ + deadline, + totalMs = 0, + label = "Time remaining", + mode = "bar", +}: BlackjackClockProps) { + const deadlineMs = deadline ? Date.parse(deadline) : Number.NaN; + const [remainingMs, setRemainingMs] = useState(() => + Number.isFinite(deadlineMs) ? Math.max(0, deadlineMs - Date.now()) : 0, + ); + + useEffect(() => { + if (!Number.isFinite(deadlineMs)) { + setRemainingMs(0); + return; + } + + const update = () => setRemainingMs(Math.max(0, deadlineMs - Date.now())); + update(); + const interval = window.setInterval(update, 250); + return () => window.clearInterval(interval); + }, [deadlineMs]); + + if (!deadline || !Number.isFinite(deadlineMs)) return null; + + const seconds = Math.ceil(remainingMs / 1000); + if (mode === "text") { + return ( + + {label} {seconds}s + + ); + } + + const percentage = totalMs > 0 ? Math.min(100, (remainingMs / totalMs) * 100) : 0; + return ( +
+
+ {label} + {seconds}s +
+ + ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackControls.tsx b/src/components/frontend/minigames/blackjack/BlackjackControls.tsx new file mode 100644 index 0000000..f58e989 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackControls.tsx @@ -0,0 +1,198 @@ +"use client"; + +import { useEffect, useState } from "react"; +import type { BlackjackActionKind, BlackjackLegalActions } from "@/lib/minigames/blackjack"; +import { + blackjackAction, + type BlackjackSnapshot, +} from "@/app/(frontend)/minigames/blackjack/actions"; + +interface BlackjackControlsProps { + readonly tableId: string; + readonly snapshotVersion: number; + readonly legalActions: BlackjackLegalActions; + readonly yourTurn: boolean; + readonly onSnapshot: (snapshot: BlackjackSnapshot) => void; + readonly onActionFeedback?: () => void; +} + +const BET_STEP = 5; + +function clampAmount(value: number, min: number, max: number): number { + if (!Number.isFinite(value)) return min; + return Math.min(max, Math.max(min, Math.round(value))); +} + +/** + * Minimal felt-overlay controls: one glass pill row per phase, no panels. + */ +export function BlackjackControls({ + tableId, + snapshotVersion, + legalActions, + yourTurn, + onSnapshot, + onActionFeedback, +}: BlackjackControlsProps) { + const [pendingAction, setPendingAction] = useState(null); + const [error, setError] = useState(null); + const [amount, setAmount] = useState(legalActions.minBet); + + const betting = legalActions.phase === "betting"; + const insuring = legalActions.phase === "insurance"; + const playing = legalActions.phase === "player"; + const betMax = Math.max(legalActions.minBet, legalActions.maxBet); + const canAct = yourTurn && pendingAction === null; + + useEffect(() => { + setAmount((current) => clampAmount(current, legalActions.minBet, betMax)); + }, [betMax, legalActions.minBet]); + + useEffect(() => { + setPendingAction(null); + setError(null); + }, [snapshotVersion]); + + const submit = async (kind: BlackjackActionKind, wager?: number) => { + setError(null); + setPendingAction(kind); + try { + const result = await blackjackAction(tableId, kind, wager); + if (!result.success) { + setError(result.error ?? "That action was rejected by the table."); + setPendingAction(null); + return; + } + if (result.data) onSnapshot(result.data); + } catch (caught) { + setError(caught instanceof Error ? caught.message : "Could not send the table action."); + setPendingAction(null); + } + }; + + const submitFromControl = (kind: BlackjackActionKind, wager?: number) => { + onActionFeedback?.(); + void submit(kind, wager); + }; + + const step = (direction: 1 | -1) => { + setAmount((current) => + clampAmount(current + direction * BET_STEP, legalActions.minBet, betMax), + ); + }; + + return ( +
+
+ {betting && ( + <> + +
+
+ {pendingAction && Table update...} + {error && {error}} +
+
+ ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackFeltCanvas.tsx b/src/components/frontend/minigames/blackjack/BlackjackFeltCanvas.tsx new file mode 100644 index 0000000..6b9020f --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackFeltCanvas.tsx @@ -0,0 +1,373 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { cn } from "@/lib/utils"; +import { handValue, type Card, type PublicPlayerView } from "@/lib/minigames/blackjack"; +import type { BlackjackSnapshot } from "@/app/(frontend)/minigames/blackjack/actions"; +import { Badge } from "@/components/ui/badge"; +import { CardView } from "./CardView"; +import { BlackjackActionToolbar } from "./BlackjackActionToolbar"; +import { BlackjackChipStack, chipFlightChips, type FlightChip } from "./BlackjackChipStack"; +import { BlackjackClock } from "./BlackjackClock"; +import { BlackjackShoeOrigin } from "./BlackjackShoeOrigin"; +import { BlackjackPopupBubble } from "./BlackjackPopups"; +import type { BlackjackActionLogEntry } from "./blackjackPopups"; +import { BlackjackSeat } from "./BlackjackSeat"; +import type { ActiveBlackjackPopup } from "./useBlackjackPopups"; +import { isTopSeatPosition, seatPosition } from "./seatPosition"; + +export type SeatedSnapshot = BlackjackSnapshot & { + readonly you: NonNullable; +}; + +interface BlackjackFeltCanvasProps { + readonly snapshot: SeatedSnapshot; + readonly onSnapshot: (snapshot: BlackjackSnapshot) => void; + readonly onActionFeedback: () => void; + readonly popups: readonly ActiveBlackjackPopup[]; + readonly actionLog: readonly BlackjackActionLogEntry[]; + readonly spectator?: boolean; + readonly onJoin?: () => void; + readonly superuserIds?: readonly number[]; + readonly fullscreen?: boolean; +} + +function cardKey(card: Card): string { + return `${card.rank}-${card.suit}`; +} + +function signedAmount(value: number): string { + return `${value >= 0 ? "+" : ""}${value.toLocaleString()}`; +} + +export function BlackjackFeltCanvas({ + snapshot, + onSnapshot, + onActionFeedback, + popups, + actionLog, + spectator = false, + onJoin, + superuserIds, + fullscreen = false, +}: BlackjackFeltCanvasProps) { + const view = snapshot.you; + const result = view.roundResult; + const viewerPlayer = view.players.find((player) => player.seatId === view.viewerSeat); + const winnerSeats = new Set( + (result?.hands ?? []).filter((hand) => hand.netChange > 0).map((hand) => hand.seatId), + ); + const opponents = view.players.filter((player) => player.seatId !== view.viewerSeat); + const popupsForSeat = (seatId: number): readonly ActiveBlackjackPopup[] => + popups.filter((popup) => popup.seatId === seatId && popup.tone !== "leave"); + const viewerPopups = popupsForSeat(view.viewerSeat); + const tableAnnouncements = popups.filter((popup) => popup.tone === "leave"); + const liveTurn = view.phase !== "round-complete"; + + // Chip flight: on the settle transition, chips launch one after another + // from the hand spot with denomination colors matching the money moved: + // payouts (and pushes) toward the bankroll pill, lost bets up to the + // dealer. A mixed round flies both directions at once. + const previousSettleRoundRef = useRef(null); + const [chipFlights, setChipFlights] = useState< + { key: number; direction: "win" | "loss"; chips: FlightChip[] }[] + >([]); + useEffect(() => { + if (view.phase !== "round-complete" || !view.roundResult) { + previousSettleRoundRef.current = null; + return; + } + if (previousSettleRoundRef.current === view.roundNumber) return; + previousSettleRoundRef.current = view.roundNumber; + // Result rows carry payouts but not the stake; the hands on the players + // view still hold each hand's bet at settle time. + const handsBySeat = new Map(view.players.map((player) => [player.seatId, player.hands])); + let incoming = 0; + let outgoing = 0; + let touched = 0; + for (const hand of view.roundResult.hands) { + if (hand.seatId !== view.viewerSeat) continue; + touched += 1; + const bet = handsBySeat.get(hand.seatId)?.[hand.handIndex]?.bet ?? 0; + if (hand.netChange > 0) incoming += bet + hand.netChange; + else if (hand.netChange < 0) outgoing += bet; + else incoming += bet; + } + for (const insurance of view.roundResult.insurance) { + if (insurance.seatId !== view.viewerSeat) continue; + if (insurance.won) incoming += insurance.amount * 3; + else outgoing += insurance.amount; + } + if (touched === 0) return; + const flights: { key: number; direction: "win" | "loss"; chips: FlightChip[] }[] = []; + if (incoming > 0) { + flights.push({ key: Date.now(), direction: "win", chips: chipFlightChips(incoming) }); + } + if (outgoing > 0) { + flights.push({ key: Date.now() + 1, direction: "loss", chips: chipFlightChips(outgoing) }); + } + if (flights.length === 0) return; + setChipFlights(flights); + const timer = setTimeout(() => setChipFlights([]), 2400); + return () => clearTimeout(timer); + }, [view.phase, view.roundNumber, view.roundResult, view.viewerSeat]); + + return ( +
+
+ {opponents.map((player: PublicPlayerView, opponentIndex) => { + const positionClass = seatPosition(opponentIndex, opponents.length); + return ( +
+ + {popupsForSeat(player.seatId).length > 0 && ( + + )} +
+ ); + })} + +
+
+ +
+
+ {view.dealer.cards.length > 0 ? ( + view.dealer.cards.map((card, index) => ( + + )) + ) : ( + + Dealer + + )} + {view.dealer.holeHidden && view.dealer.cards.length > 0 && ( + + )} +
+ {view.dealer.total !== null && ( + + Dealer {view.dealer.total} + + )} + {view.dealer.holeHidden && view.dealer.cards.length > 0 && ( + + Dealer showing{" "} + {view.dealer.cards[0]?.rank === 14 ? "ace" : (view.dealer.cards[0]?.rank ?? "")} + + )} +
+
+
+ Round + {view.roundNumber} + + Bets {view.config.minBet.toLocaleString()}-{view.config.maxBet.toLocaleString()} + +
+ {liveTurn && ( +
+ +
+ )} +
+ + {view.phase !== "round-complete" && view.yourHands.length > 0 && ( +
+ {view.yourHands.map((hand, handIndex) => { + const statusLabel = + hand.doubled && hand.status === "active" + ? " · Doubled" + : hand.status === "bust" + ? " · Bust" + : hand.status === "blackjack" + ? " · Blackjack!" + : ""; + return ( +
+
+ {hand.cards.map((card, cardIndex) => ( + + ))} +
+ + {handValue(hand.cards).total} + {statusLabel} + + +
+ ); + })} +
+ )} + + {actionLog.length > 0 && ( +
+ {actionLog.map((entry) => ( +

+ {entry.text} +

+ ))} +
+ )} + + {!spectator && ( + + )} + + {chipFlights.map((flight) => ( + + ))} + + {viewerPopups.length > 0 && ( + + )} + + {tableAnnouncements.length > 0 && ( +
+ {tableAnnouncements.map((popup) => ( + + ))} +
+ )} + + {view.phase === "round-complete" && result && ( +
+
+

+ {result.hands.length === 0 && view.dealer.cards.length === 0 + ? "No bets this round" + : result.dealerHasBlackjack + ? "Dealer blackjack" + : result.dealerBusted + ? `Dealer busts at ${result.dealerTotal}` + : `Dealer stands at ${result.dealerTotal}`} +

+ {result.hands.length > 0 && ( +
+ {result.hands.map((hand) => { + const player = view.players.find((item) => item.seatId === hand.seatId); + return ( + + {hand.netChange > 0 && } + + {player?.name ?? `Seat ${hand.seatId + 1}`} + + {hand.total} + 0 && "text-emerald-400", + hand.netChange < 0 && "text-rose-400", + hand.netChange === 0 && "text-white/50", + )} + > + {hand.outcome === "blackjack" ? "BJ " : ""} + {signedAmount(hand.netChange)} + + + ); + })} +
+ )} + {snapshot.nextRoundAt && ( + + )} +
+
+ )} + + {viewerPlayer === undefined && onJoin && ( +
+ +
+ )} +
+
+ ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackGameTable.tsx b/src/components/frontend/minigames/blackjack/BlackjackGameTable.tsx new file mode 100644 index 0000000..61b9746 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackGameTable.tsx @@ -0,0 +1,186 @@ +"use client"; + +import { createPortal } from "react-dom"; +import { useEffect, useState } from "react"; +import { Maximize2, Minimize2 } from "lucide-react"; +import type { BlackjackPhase } from "@/lib/minigames/blackjack"; +import type { BlackjackSnapshot } from "@/app/(frontend)/minigames/blackjack/actions"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card as UiCard, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { BlackjackCardStyleToggle } from "./BlackjackCardStyleToggle"; +import { BlackjackFeltCanvas, type SeatedSnapshot } from "./BlackjackFeltCanvas"; +import { BlackjackSoundToggle } from "./BlackjackSoundToggle"; +import type { BlackjackSoundControls } from "./useBlackjackSounds"; +import type { ActiveBlackjackPopup } from "./useBlackjackPopups"; +import type { BlackjackActionLogEntry } from "./blackjackPopups"; +import { RenameDialogGuard } from "./RenameDialogGuard"; + +interface BlackjackGameTableProps { + readonly snapshot: SeatedSnapshot; + readonly onSnapshot: (snapshot: BlackjackSnapshot) => void; + readonly onLeave: () => Promise; + readonly onJoin: () => Promise; + readonly sound: BlackjackSoundControls; + readonly popups: readonly ActiveBlackjackPopup[]; + readonly actionLog: readonly BlackjackActionLogEntry[]; + readonly isSuperuser: boolean; + readonly superuserIds: readonly number[]; +} + +const PHASE_LABELS: Record = { + betting: "Betting", + insurance: "Insurance", + player: "In play", + dealer: "Dealer plays", + "round-complete": "Round settled", +}; + +export function BlackjackGameTable({ + snapshot, + onSnapshot, + onLeave, + onJoin, + sound, + popups, + actionLog, + isSuperuser, + superuserIds, +}: BlackjackGameTableProps) { + const [leaving, setLeaving] = useState(false); + const [fullscreen, setFullscreen] = useState(false); + const [mounted, setMounted] = useState(false); + const view = snapshot.you; + const viewerPlayer = view.players.find((player) => player.seatId === view.viewerSeat); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!fullscreen) return; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") setFullscreen(false); + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [fullscreen]); + + const leave = async () => { + setLeaving(true); + await onLeave(); + setLeaving(false); + }; + + const tableName = snapshot.name ?? `Table ${snapshot.tableId}`; + const spectator = Boolean(snapshot.spectator); + const feltCanvas = ( + + ); + + if (fullscreen) { + if (!mounted) return null; + + return createPortal( +
+
+
+ {tableName} + {spectator && Spectating} + {PHASE_LABELS[view.phase]} +
+
+ + + + +
+
+
{feltCanvas}
+
, + document.body, + ); + } + + return ( + + +
+
+ {tableName} + + {spectator && Spectating} + {PHASE_LABELS[view.phase]} +
+

+ Round {view.roundNumber} · Bets {view.config.minBet.toLocaleString()}- + {view.config.maxBet.toLocaleString()} + {viewerPlayer && ` · Bankroll ${viewerPlayer.bankroll.toLocaleString()}`} +

+
+
+ + v{snapshot.version} + + + + + +
+
+ {feltCanvas} +
+ ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackGuide.tsx b/src/components/frontend/minigames/blackjack/BlackjackGuide.tsx new file mode 100644 index 0000000..c0bdd32 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackGuide.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { BookOpen } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; + +const RULES: readonly { readonly title: string; readonly lines: readonly string[] }[] = [ + { + title: "The goal", + lines: [ + "Beat the dealer's hand without going over 21.", + "Cards count face value; face cards are 10 and an ace is 11 or 1, whichever helps.", + "A two-card 21 on the deal is a natural blackjack.", + ], + }, + { + title: "The round", + lines: [ + "Betting: place a wager (10 to 500 chips) or sit the round out.", + "Insurance: when the dealer shows an ace you may insure up to half your bet.", + "Play: hit, stand, double down, or split matching pairs, one hand at a time.", + "The dealer then draws to 17 and stands on all 17s, soft included.", + ], + }, + { + title: "Payouts", + lines: [ + "Natural blackjack pays 3 to 2.", + "A regular win pays 1 to 1; a tie pushes your bet back.", + "Insurance pays 2 to 1 when the dealer has blackjack.", + ], + }, + { + title: "Table rules", + lines: [ + "Split up to four hands; split aces receive one card each and stand.", + "Doubling is allowed on the first two cards of any hand, including after a split.", + "A 21 made after splitting counts as a plain 21, not a natural.", + "A bankroll below the table minimum ends your session with stats recorded.", + ], + }, +]; + +export function BlackjackGuide() { + return ( + + + + + + + Blackjack house rules + + Six-deck shoe, dealer stands on all 17s, blackjack pays 3 to 2. + + +
+ {RULES.map((section) => ( +
+

{section.title}

+
    + {section.lines.map((line) => ( +
  • + + {line} +
  • + ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackLeaderboard.tsx b/src/components/frontend/minigames/blackjack/BlackjackLeaderboard.tsx new file mode 100644 index 0000000..89efad6 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackLeaderboard.tsx @@ -0,0 +1,240 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Trophy } from "lucide-react"; +import type { PresenceParticipant } from "@/hooks/useRealtimePresence"; +import { cn } from "@/lib/utils"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +interface ApiUser { + readonly id: number; + readonly username?: string | null; + readonly displayName?: string | null; +} + +interface ApiProfile { + readonly id: number; + readonly profileTitle?: string | null; + readonly user?: ApiUser | number | null; + readonly progression?: { + readonly minigames?: { + readonly blackjack?: { + readonly gamesPlayed?: number | null; + readonly roundsWon?: number | null; + readonly blackjacksHit?: number | null; + readonly bestScore?: number | null; + readonly xpEarned?: number | null; + } | null; + } | null; + } | null; +} + +interface LeaderboardRow { + readonly id: number; + readonly userId: number | null; + readonly name: string; + readonly gamesPlayed: number; + readonly roundsWon: number; + readonly blackjacksHit: number; + readonly bestScore: number; + readonly xpEarned: number; + readonly isCurrentUser: boolean; + readonly isOutsideTopTen?: boolean; +} + +interface BlackjackLeaderboardProps { + readonly refreshKey: number; + readonly currentUserId?: number | string; + readonly activePlayers: readonly PresenceParticipant[]; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function readProfiles(value: unknown): readonly ApiProfile[] { + if (!isRecord(value) || !Array.isArray(value.docs)) return []; + return value.docs.filter((doc): doc is ApiProfile => { + if (!isRecord(doc) || typeof doc.id !== "number") return false; + const user = doc.user; + return user === null || user === undefined || typeof user === "number" || isRecord(user); + }); +} + +function rankClass(index: number): string { + if (index === 0) return "font-bold text-amber-400"; + if (index === 1) return "font-semibold text-slate-300"; + if (index === 2) return "font-semibold text-amber-700"; + return "text-muted-foreground"; +} + +export function BlackjackLeaderboard({ + refreshKey, + currentUserId, + activePlayers, +}: BlackjackLeaderboardProps) { + const [rows, setRows] = useState(null); + const [failed, setFailed] = useState(false); + + useEffect(() => { + let cancelled = false; + setFailed(false); + const params = new URLSearchParams({ + sort: "-progression.minigames.blackjack.bestScore", + limit: "100", + depth: "1", + }); + + fetch(`/api/profiles?${params.toString()}`) + .then(async (response) => { + if (!response.ok) throw new Error("leaderboard fetch failed"); + return readProfiles(await response.json()); + }) + .then((profiles) => { + if (cancelled) return; + const mapped = profiles.map((profile): LeaderboardRow => { + const blackjack = profile.progression?.minigames?.blackjack ?? {}; + const user = + typeof profile.user === "object" && profile.user !== null ? profile.user : null; + return { + id: profile.id, + userId: user?.id ?? null, + name: + user?.displayName || + profile.profileTitle || + user?.username || + `Player #${profile.id}`, + gamesPlayed: blackjack.gamesPlayed ?? 0, + roundsWon: blackjack.roundsWon ?? 0, + blackjacksHit: blackjack.blackjacksHit ?? 0, + bestScore: blackjack.bestScore ?? 0, + xpEarned: blackjack.xpEarned ?? 0, + isCurrentUser: user ? String(user.id) === String(currentUserId) : false, + }; + }); + const visible = mapped.filter((row) => row.bestScore > 0).slice(0, 10); + const current = mapped.find((row) => row.isCurrentUser); + if (current && !visible.some((row) => row.id === current.id)) { + visible.push({ ...current, isOutsideTopTen: true }); + } + setRows(visible); + }) + .catch(() => { + if (!cancelled) setFailed(true); + }); + + return () => { + cancelled = true; + }; + }, [currentUserId, refreshKey]); + + return ( + + +
+ + Blackjack leaderboard +
+ Top 10 by best bankroll +
+ +
+ {activePlayers.length}{" "} + {activePlayers.length === 1 ? "player is" : "players are"} at the tables + {activePlayers.length > 0 && ( + + ({activePlayers.map((player) => player.name).join(", ")}) + + )} +
+ + {failed ? ( +

Could not load the leaderboard.

+ ) : rows === null ? ( +
+ + + +
+ ) : rows.length === 0 ? ( +

+ No finished sessions yet. Take the first seat. +

+ ) : ( + + + + # + Player + Rounds + 21s + Best bankroll + XP + + + + {rows.map((row, index) => ( + + + + {row.isOutsideTopTen ? "-" : index + 1} + + + + + {row.name} + {row.isCurrentUser && ( + (you) + )} + {row.userId !== null && + activePlayers.some((player) => player.id === row.userId) && ( + + )} + + + + {row.roundsWon} + + + {row.blackjacksHit} + + + {row.bestScore.toLocaleString()} + + + {row.xpEarned} + + + ))} + +
+ )} +
+
+ ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackLobby.tsx b/src/components/frontend/minigames/blackjack/BlackjackLobby.tsx new file mode 100644 index 0000000..07a6227 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackLobby.tsx @@ -0,0 +1,325 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { Ban, ClipboardCopy, CirclePlus, LoaderCircle, RefreshCw, Users } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { + createBlackjackTable, + forceExpireBlackjackTable, + joinBlackjackTable, + listBlackjackTables, + type BlackjackSnapshot, + type BlackjackTableSummary, +} from "@/app/(frontend)/minigames/blackjack/actions"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { BLACKJACK_TABLES_EVENT, readBlackjackTablesEvent } from "./events"; + +interface BlackjackLobbyProps { + readonly refreshKey: number; + readonly onOpenTable: (snapshot: BlackjackSnapshot) => void; + readonly isSuperuser?: boolean; +} + +function createdLabel(createdAt: string): string { + const date = new Date(createdAt); + return Number.isNaN(date.getTime()) + ? "Recently created" + : date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }); +} + +export function BlackjackLobby({ refreshKey, onOpenTable, isSuperuser = false }: BlackjackLobbyProps) { + const [tables, setTables] = useState(null); + const [refreshing, setRefreshing] = useState(false); + const [pendingTable, setPendingTable] = useState(null); + const [forceEndingTable, setForceEndingTable] = useState(null); + const [creating, setCreating] = useState(false); + const [copiedTable, setCopiedTable] = useState(null); + const [error, setError] = useState(null); + + const loadTables = useCallback(async () => { + try { + const result = await listBlackjackTables(); + if (!result.success) { + setError(result.error ?? "Could not load blackjack tables."); + return; + } + setError(null); + setTables(result.data ?? []); + } catch (caught) { + setError(caught instanceof Error ? caught.message : "Could not load blackjack tables."); + } + }, []); + + const manualRefresh = useCallback(async () => { + setRefreshing(true); + try { + await loadTables(); + } finally { + setRefreshing(false); + } + }, [loadTables]); + + useEffect(() => { + void loadTables(); + }, [loadTables, refreshKey]); + + useEffect(() => { + const handleTables = (event: Event) => { + const detail = readBlackjackTablesEvent(event); + if (detail) setTables(detail.tables); + }; + window.addEventListener(BLACKJACK_TABLES_EVENT, handleTables); + // Poll fallback for the same SSE-reliability reason as BlackjackArena. + const poll = window.setInterval(() => void loadTables(), 3000); + return () => { + window.removeEventListener(BLACKJACK_TABLES_EVENT, handleTables); + window.clearInterval(poll); + }; + }, [loadTables]); + + const join = async (tableId: string) => { + setPendingTable(tableId); + setError(null); + try { + const result = await joinBlackjackTable(tableId); + if (result.success && result.data) { + onOpenTable(result.data); + } else { + setError(result.error ?? "Could not join that table."); + } + } catch (caught) { + setError(caught instanceof Error ? caught.message : "Could not join that table."); + } finally { + setPendingTable(null); + } + }; + + const forceEnd = async (tableId: string) => { + setForceEndingTable(tableId); + setError(null); + try { + const result = await forceExpireBlackjackTable(tableId); + if (!result.success) { + setError(result.error ?? "Could not force-end the blackjack table."); + } + } catch (caught) { + setError(caught instanceof Error ? caught.message : "Could not force-end the blackjack table."); + } finally { + setForceEndingTable(null); + } + }; + + const create = async () => { + setCreating(true); + setError(null); + try { + const created = await createBlackjackTable(); + if (!created.success || !created.data) { + setError(created.error ?? "Could not create a blackjack table."); + return; + } + await join(created.data.tableId); + } catch (caught) { + setError(caught instanceof Error ? caught.message : "Could not create a blackjack table."); + } finally { + setCreating(false); + } + }; + + const copyLink = async (tableId: string) => { + if (!navigator.clipboard) { + setError("Clipboard access is not available in this browser."); + return; + } + try { + await navigator.clipboard.writeText( + `${window.location.origin}/minigames/blackjack?table=${encodeURIComponent(tableId)}`, + ); + setCopiedTable(tableId); + window.setTimeout( + () => setCopiedTable((current) => (current === tableId ? null : current)), + 1600, + ); + } catch (caught) { + setError(caught instanceof Error ? caught.message : "Could not copy the table link."); + } + }; + + return ( + + +
+ Blackjack lobby + Join an open table or deal yourself in, solo or with a squad. +
+
+ + +
+
+ + {error && ( +

{error}

+ )} + {tables === null ? ( +

+ Scanning for open tables... +

+ ) : tables.length === 0 ? ( +
+ +
+

No active tables

+

+ Create the first room and take on the dealer. +

+
+ +
+ ) : ( + + + + Table + Players + Status + Actions + + + + {tables.map((table) => { + const full = table.seatedCount >= table.maxSeats; + const joining = pendingTable === table.tableId; + return ( + + +
+ + {table.name ?? table.tableId} + + + Host {table.hostName} · {createdLabel(table.createdAt)} + +
+
+ + {table.seatedCount}/{table.maxSeats} + + + + {table.inGame ? "In progress" : full ? "Full" : "Open"} + + + +
+ + {isSuperuser && ( + + + + + + + Force-end this table? + + Seated players will be notified. + + + + Cancel + void forceEnd(table.tableId)} + > + Force-end table + + + + + )} + +
+ {copiedTable === table.tableId && ( +

Link copied

+ )} +
+
+ ); + })} +
+
+ )} +
+
+ ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackPopups.tsx b/src/components/frontend/minigames/blackjack/BlackjackPopups.tsx new file mode 100644 index 0000000..4ca3e95 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackPopups.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import type { ActiveBlackjackPopup } from "./useBlackjackPopups"; + +export function BlackjackPopupBubble({ popup }: { readonly popup: ActiveBlackjackPopup }) { + return ( + + ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackSeat.tsx b/src/components/frontend/minigames/blackjack/BlackjackSeat.tsx new file mode 100644 index 0000000..ea773b0 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackSeat.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { Shield } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Badge } from "@/components/ui/badge"; +import type { PublicPlayerView } from "@/lib/minigames/blackjack"; +import { handValue } from "@/lib/minigames/blackjack"; +import { CardView } from "./CardView"; +import { BlackjackChipStack } from "./BlackjackChipStack"; + +interface BlackjackSeatProps { + readonly player: PublicPlayerView; + readonly isCurrentUser: boolean; + readonly isActing: boolean; + readonly isWinner: boolean; + readonly isSuperuser?: boolean; +} + +type StackFlash = "up" | "down" | null; + +const STACK_FLASH_MS = 900; + +const STATUS_LABELS: Record = { + active: "Playing", + stood: "Stand", + bust: "Bust", + blackjack: "Blackjack", +}; + +export function BlackjackSeat({ + player, + isCurrentUser, + isActing, + isWinner, + isSuperuser = false, +}: BlackjackSeatProps) { + const initial = player.name.trim().charAt(0).toUpperCase() || "?"; + const totalBet = player.hands.reduce((sum, hand) => sum + hand.bet, 0); + const handStatus = player.hands[0]?.status ?? null; + const statusLabel = player.left + ? "Left" + : player.bankrupt + ? "Out of chips" + : isActing + ? "Acting" + : handStatus + ? (STATUS_LABELS[handStatus] ?? null) + : player.sittingOut + ? "Sitting out" + : null; + const [stackFlash, setStackFlash] = useState(null); + const previousStackRef = useRef(null); + + useEffect(() => { + const previousStack = previousStackRef.current; + previousStackRef.current = player.bankroll; + if (previousStack === null || previousStack === player.bankroll) { + return undefined; + } + setStackFlash(player.bankroll > previousStack ? "up" : "down"); + const timer = setTimeout(() => setStackFlash(null), STACK_FLASH_MS); + return () => clearTimeout(timer); + }, [player.bankroll]); + + return ( +
+
+ +
+

+ {player.name} + {isSuperuser && ( + + )} + {isCurrentUser && (you)} +

+

+

+ {(statusLabel || isWinner) && ( +
+ {statusLabel && {statusLabel}} + {isWinner && ( + Winner + )} +
+ )} +
+ {totalBet > 0 && ( +
+ + Bet + + + + {totalBet.toLocaleString()} + +
+ )} +
+ + {player.hands.map((hand, handIndex) => ( +
1 && "opacity-80", + )} + aria-label={`Hand ${handIndex + 1}: ${handValue(hand.cards).total}${ + hand.doubled ? ", doubled" : "" + }`} + > + {hand.cards.map((card, cardIndex) => ( + + ))} + + {handValue(hand.cards).total} + +
+ ))} +
+ ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackShoeOrigin.tsx b/src/components/frontend/minigames/blackjack/BlackjackShoeOrigin.tsx new file mode 100644 index 0000000..10c7d36 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackShoeOrigin.tsx @@ -0,0 +1,11 @@ +"use client"; + +export function BlackjackShoeOrigin() { + return ( + + ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackSoundToggle.tsx b/src/components/frontend/minigames/blackjack/BlackjackSoundToggle.tsx new file mode 100644 index 0000000..f3b4268 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackSoundToggle.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { AudioWaveform, Coins, Volume2, VolumeX } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import type { BlackjackWinSoundMode } from "./blackjackSound"; + +interface BlackjackSoundToggleProps { + readonly muted: boolean; + readonly volume: number; + readonly winSound: BlackjackWinSoundMode; + readonly onToggleMuted: () => void; + readonly onVolumeChange: (volume: number) => void; + readonly onWinSoundChange: (mode: BlackjackWinSoundMode) => void; +} + +export function BlackjackSoundToggle({ + muted, + volume, + winSound, + onToggleMuted, + onVolumeChange, + onWinSoundChange, +}: BlackjackSoundToggleProps) { + const nextWinSound = winSound === "chaching" ? "procedural" : "chaching"; + const winSoundLabel = + winSound === "chaching" + ? "Win sound: cash register (click for procedural)" + : "Win sound: procedural fanfare (click for cash register)"; + return ( +
+ + + onVolumeChange(Number(event.target.value))} + aria-label="Blackjack sound volume" + className="poker-sound-volume" + /> +
+ ); +} diff --git a/src/components/frontend/minigames/blackjack/BlackjackTable.tsx b/src/components/frontend/minigames/blackjack/BlackjackTable.tsx new file mode 100644 index 0000000..681eb84 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/BlackjackTable.tsx @@ -0,0 +1,285 @@ +"use client"; + +import { useState } from "react"; +import { cn } from "@/lib/utils"; +import type { BlackjackSnapshot } from "@/app/(frontend)/minigames/blackjack/actions"; +import { MIN_PLAYERS } from "@/lib/minigames/blackjack"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; +import { BlackjackCardStyleProvider } from "./cardStyle"; +import { BlackjackGameTable } from "./BlackjackGameTable"; +import { RenameTableDialog } from "./RenameTableDialog"; +import { useBlackjackPopups } from "./useBlackjackPopups"; +import { useBlackjackSounds } from "./useBlackjackSounds"; +import { seatPosition } from "./seatPosition"; + +interface BlackjackTableProps { + readonly snapshot: BlackjackSnapshot | null; + readonly currentUserId: number | string; + readonly loading: boolean; + readonly tableError: string | null; + readonly onSnapshot: (snapshot: BlackjackSnapshot) => void; + readonly onJoin: () => Promise; + readonly onSpectate: () => Promise; + readonly onStart: () => Promise; + readonly onLeave: () => Promise; + readonly onBack: () => void; + readonly isSuperuser: boolean; + readonly superuserIds: readonly number[]; +} + +type SeatedSnapshot = BlackjackSnapshot & { + readonly you: NonNullable; +}; + +function isSeatedSnapshot(snapshot: BlackjackSnapshot): snapshot is SeatedSnapshot { + return snapshot.you !== null; +} + +function WaitingTable({ + snapshot, + currentUserId, + onSnapshot, + onJoin, + onSpectate, + onStart, + onLeave, + onBack, +}: { + readonly snapshot: BlackjackSnapshot; + readonly currentUserId: number | string; + readonly onSnapshot: (snapshot: BlackjackSnapshot) => void; + readonly onJoin: () => Promise; + readonly onSpectate: () => Promise; + readonly onStart: () => Promise; + readonly onLeave: () => Promise; + readonly onBack: () => void; +}) { + const [starting, setStarting] = useState(false); + const [leaving, setLeaving] = useState(false); + const viewerSeat = snapshot.seats.findIndex((id) => String(id) === String(currentUserId)); + const seated = viewerSeat >= 0; + const full = snapshot.seats.length > 0 && snapshot.seats.every((id) => id !== null); + const seatedCount = snapshot.seats.filter((id) => id !== null).length; + const enoughPlayers = seatedCount >= MIN_PLAYERS; + const isHost = seated && snapshot.hostSeatId === viewerSeat; + const opponentCount = snapshot.seats.length - (seated ? 1 : 0); + + const start = async () => { + setStarting(true); + await onStart(); + setStarting(false); + }; + + const leave = async () => { + setLeaving(true); + await onLeave(); + setLeaving(false); + }; + + return ( + + +
+
+ + {snapshot.name ?? `Table ${snapshot.tableId}`} + + {isHost && ( + + )} +
+

+ {seated + ? "You have a seat reserved. Solo players can start right away." + : "Join an open seat to play against the dealer."} +

+
+ Waiting room +
+ +
+ {snapshot.seats.map((userId, seatId) => ( +
previousSeatId !== viewerSeat).length, + opponentCount, + ), + userId !== null && "poker-waiting-seat-occupied", + )} + > + + + {userId === null ? `Seat ${seatId + 1}` : (snapshot.names[seatId] ?? `Seat ${seatId + 1}`)} + + + {userId === null + ? "Open seat" + : String(userId) === String(currentUserId) + ? "You" + : "Occupied"} + +
+ ))} +
+ + Polaris Blackjack + + + {seated ? "Waiting for the host to start" : "Open seating"} + +
+
+
+ + {seated ? ( + <> + {isHost && ( + + )} + + + ) : ( + <> + + {snapshot.inGame && ( + + )} + + + )} + +
+ ); +} + +function BlackjackTableInner({ + snapshot, + currentUserId, + loading, + tableError, + onSnapshot, + onJoin, + onSpectate, + onStart, + onLeave, + onBack, + isSuperuser, + superuserIds, +}: BlackjackTableProps) { + // Sound + popup transitions must live here, ABOVE the waiting/game split: + // the hooks have to observe the waiting-room-to-first-round transition, + // which happens before the game table (and its seat view) mounts. + const sound = useBlackjackSounds(snapshot); + const { popups, actionLog } = useBlackjackPopups(snapshot); + + if (loading && !snapshot) { + return ( + + + + + + + + + + ); + } + + if (!snapshot) { + return ( + + + Table unavailable + + +

+ {tableError ?? "This table could not be loaded."} +

+
+ + + +
+ ); + } + + if (!isSeatedSnapshot(snapshot)) { + return ( + + ); + } + + return ( + + ); +} + +export function BlackjackTable(props: BlackjackTableProps) { + return ( + + + + ); +} diff --git a/src/components/frontend/minigames/blackjack/CardView.tsx b/src/components/frontend/minigames/blackjack/CardView.tsx new file mode 100644 index 0000000..1f1282d --- /dev/null +++ b/src/components/frontend/minigames/blackjack/CardView.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import type { Card, Rank, Suit } from "@/lib/minigames/blackjack"; +import { mapCardToSvg, useCardStyle } from "./cardStyle"; + +const RANK_LABELS: Record = { + 2: "2", + 3: "3", + 4: "4", + 5: "5", + 6: "6", + 7: "7", + 8: "8", + 9: "9", + 10: "10", + 11: "J", + 12: "Q", + 13: "K", + 14: "A", +}; + +const SUIT_SYMBOLS: Record = { + s: "♠", + h: "♥", + d: "♦", + c: "♣", +}; + +const RED_SUITS = new Set(["h", "d"]); + +interface CardViewProps { + readonly card: Card | null; + readonly faceUp?: boolean; + readonly size?: "mini" | "sm" | "md"; + readonly dealIndex?: number; + readonly className?: string; + readonly highlighted?: boolean; + readonly dimmed?: boolean; +} + +export function CardView({ + card, + faceUp = true, + size = "md", + dealIndex = 0, + className, + highlighted = false, + dimmed = false, +}: CardViewProps) { + const { style: cardStyle } = useCardStyle(); + const lightFace = cardStyle === "light"; + const classicFace = cardStyle === "classic"; + const revealed = card !== null && faceUp; + const redSuitClass = lightFace ? "text-rose-600" : "text-rose-400"; + const label = card + ? `${RANK_LABELS[card.rank]} of ${ + card.suit === "s" + ? "spades" + : card.suit === "h" + ? "hearts" + : card.suit === "d" + ? "diamonds" + : "clubs" + }` + : "Face-down card"; + + return ( + 0 && "poker-card-deal", + highlighted && + "relative z-10 brightness-110 ring-2 ring-amber-300 shadow-lg shadow-amber-300/35", + dimmed && "opacity-40", + className, + )} + style={dealIndex > 0 ? { animationDelay: `${dealIndex * 60}ms` } : undefined} + aria-label={label} + > + + + + {card && classicFace && ( + + )} + {card && !classicFace && ( + <> + + {RANK_LABELS[card.rank]} + {SUIT_SYMBOLS[card.suit]} + + {size !== "mini" && ( + + {SUIT_SYMBOLS[card.suit]} + + )} + + )} + + + + + + ); +} diff --git a/src/components/frontend/minigames/blackjack/RenameDialogGuard.tsx b/src/components/frontend/minigames/blackjack/RenameDialogGuard.tsx new file mode 100644 index 0000000..412589b --- /dev/null +++ b/src/components/frontend/minigames/blackjack/RenameDialogGuard.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { useState } from "react"; +import { Pencil } from "lucide-react"; +import { MAX_TABLE_NAME_LENGTH } from "@/lib/minigames/blackjack/constants"; +import { + renameBlackjackTable, + type BlackjackSnapshot, +} from "@/app/(frontend)/minigames/blackjack/actions"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; + +interface RenameDialogGuardProps { + readonly canRename: boolean; + readonly tableId: string; + readonly currentName: string | null; + readonly onSnapshot: (snapshot: BlackjackSnapshot) => void; +} + +export function RenameDialogGuard({ + canRename, + tableId, + currentName, + onSnapshot, +}: RenameDialogGuardProps) { + const [open, setOpen] = useState(false); + const [value, setValue] = useState(currentName ?? ""); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + if (!canRename) return null; + + const save = async () => { + setSaving(true); + setError(null); + try { + const result = await renameBlackjackTable(tableId, value); + if (!result.success || !result.data) { + setError(result.error ?? "Could not rename the table."); + return; + } + onSnapshot(result.data); + setOpen(false); + } catch (caught) { + setError(caught instanceof Error ? caught.message : "Could not rename the table."); + } finally { + setSaving(false); + } + }; + + return ( + { + if (next) setValue(currentName ?? ""); + setOpen(next); + }} + > + + + + + + Name this table + + The name shows in the lobby and at the table. Share links stay the same. + + + setValue(event.target.value.slice(0, MAX_TABLE_NAME_LENGTH))} + placeholder="Friday night blackjack" + maxLength={MAX_TABLE_NAME_LENGTH} + aria-label="Table name" + onKeyDown={(event) => { + if (event.key === "Enter") void save(); + }} + /> + {error &&

{error}

} + + + + +
+
+ ); +} diff --git a/src/components/frontend/minigames/blackjack/RenameTableDialog.tsx b/src/components/frontend/minigames/blackjack/RenameTableDialog.tsx new file mode 100644 index 0000000..477969e --- /dev/null +++ b/src/components/frontend/minigames/blackjack/RenameTableDialog.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useState } from "react"; +import { Pencil } from "lucide-react"; +import { MAX_TABLE_NAME_LENGTH } from "@/lib/minigames/blackjack/constants"; +import { + renameBlackjackTable, + type BlackjackSnapshot, +} from "@/app/(frontend)/minigames/blackjack/actions"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; + +interface RenameTableDialogProps { + readonly tableId: string; + readonly currentName: string | null; + readonly onSnapshot: (snapshot: BlackjackSnapshot) => void; +} + +export function RenameTableDialog({ tableId, currentName, onSnapshot }: RenameTableDialogProps) { + const [open, setOpen] = useState(false); + const [value, setValue] = useState(currentName ?? ""); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + const save = async () => { + setSaving(true); + setError(null); + try { + const result = await renameBlackjackTable(tableId, value); + if (!result.success || !result.data) { + setError(result.error ?? "Could not rename the table."); + return; + } + onSnapshot(result.data); + setOpen(false); + } catch (caught) { + setError(caught instanceof Error ? caught.message : "Could not rename the table."); + } finally { + setSaving(false); + } + }; + + return ( + { + if (next) setValue(currentName ?? ""); + setOpen(next); + }} + > + + + + + + Name this table + + The name shows in the lobby and at the table. Share links stay the same. + + + setValue(event.target.value.slice(0, MAX_TABLE_NAME_LENGTH))} + placeholder="Friday night blackjack" + maxLength={MAX_TABLE_NAME_LENGTH} + aria-label="Table name" + onKeyDown={(event) => { + if (event.key === "Enter") void save(); + }} + /> + {error &&

{error}

} + + + + +
+
+ ); +} diff --git a/src/components/frontend/minigames/blackjack/blackjackPopups.ts b/src/components/frontend/minigames/blackjack/blackjackPopups.ts new file mode 100644 index 0000000..7fef412 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/blackjackPopups.ts @@ -0,0 +1,176 @@ +import type { PublicPlayerView } from "@/lib/minigames/blackjack"; +import type { BlackjackSoundSnapshot } from "./blackjackSoundEvents"; + +export type BlackjackPopupTone = "action" | "wager" | "win" | "fold" | "leave"; + +export interface BlackjackPopupEvent { + readonly key: string; + readonly seatId: number; + readonly text: string; + readonly tone: BlackjackPopupTone; + /** ms to wait before this popup appears, default 0. */ + readonly delay?: number; +} + +export interface BlackjackActionLogEntry { + readonly key: string; + readonly text: string; + readonly tone: BlackjackPopupTone; +} + +/** + * Compose the action-log line for a popup event, joining the acting player's + * name from the snapshot's player list (leavers are already named). + */ +export function actionLogText( + event: BlackjackPopupEvent, + players: readonly PublicPlayerView[], +): string { + if (event.tone === "leave") return event.text; + const actor = players.find((player) => player.seatId === event.seatId); + const name = actor?.name ?? `Seat ${event.seatId + 1}`; + if (event.tone === "win") return `${name} wins ${event.text}`; + if (event.tone === "fold") return `${name}: ${event.text}`; + return `${name}: ${event.text}`; +} + +function playerBySeat( + players: readonly PublicPlayerView[], + seatId: number, +): PublicPlayerView | null { + return players.find((player) => player.seatId === seatId) ?? null; +} + +export function popupEventsForTransition( + previous: BlackjackSoundSnapshot | null, + next: BlackjackSoundSnapshot, +): readonly BlackjackPopupEvent[] { + if (!previous || previous.version === next.version) return []; + + const previousView = previous.you; + const nextView = next.you; + if (!nextView) return []; + if (!previousView) return []; + const events: BlackjackPopupEvent[] = []; + const transitionKey = `${next.version}:${nextView.roundNumber}:${nextView.phase}`; + const sameRound = previousView.roundNumber === nextView.roundNumber; + const samePhase = previousView.phase === nextView.phase; + + if (sameRound && samePhase && nextView.phase !== "round-complete") { + for (const nextPlayer of nextView.players) { + const previousPlayer = playerBySeat(previousView.players, nextPlayer.seatId); + if (!previousPlayer) continue; + + if (!previousPlayer.hasBet && nextPlayer.hasBet && nextPlayer.hands.length > 0) { + events.push({ + key: `${transitionKey}:popup:bet:${nextPlayer.seatId}`, + seatId: nextPlayer.seatId, + text: `Bet +${(nextPlayer.hands[0]?.bet ?? 0).toLocaleString()}`, + tone: "wager", + }); + continue; + } + + if (previousPlayer.insuranceBet === 0 && nextPlayer.insuranceBet > 0) { + events.push({ + key: `${transitionKey}:popup:insurance:${nextPlayer.seatId}`, + seatId: nextPlayer.seatId, + text: `Insurance +${nextPlayer.insuranceBet.toLocaleString()}`, + tone: "wager", + }); + } + + if (nextPlayer.hands.length > previousPlayer.hands.length) { + events.push({ + key: `${transitionKey}:popup:split:${nextPlayer.seatId}`, + seatId: nextPlayer.seatId, + text: "Split", + tone: "action", + }); + } + + const previousDoubled = previousPlayer.hands.some((hand) => hand.doubled); + const nextDoubled = nextPlayer.hands.some((hand) => hand.doubled); + if (!previousDoubled && nextDoubled) { + const doubledHand = nextPlayer.hands.find((hand) => hand.doubled); + events.push({ + key: `${transitionKey}:popup:double:${nextPlayer.seatId}`, + seatId: nextPlayer.seatId, + text: `Double +${(doubledHand?.bet ?? 0).toLocaleString()}`, + tone: "wager", + }); + } + + nextPlayer.hands.forEach((hand, handIndex) => { + const previousHand = previousPlayer.hands[handIndex]; + if (previousHand?.status === hand.status) return; + if (hand.status === "bust") { + events.push({ + key: `${transitionKey}:popup:bust:${nextPlayer.seatId}:${handIndex}`, + seatId: nextPlayer.seatId, + text: "Bust", + tone: "fold", + }); + } else if (hand.status === "blackjack") { + events.push({ + key: `${transitionKey}:popup:blackjack:${nextPlayer.seatId}:${handIndex}`, + seatId: nextPlayer.seatId, + text: "Blackjack!", + tone: "win", + }); + } + }); + } + } + + // Leaving is any of: a vanished seat (removed between rounds), a mid-round + // leaver whose view flips the left flag immediately, or a bankrupt seat. + for (const previousPlayer of previousView.players) { + const nextPlayer = playerBySeat(nextView.players, previousPlayer.seatId); + if (!nextPlayer) { + events.push({ + key: `${transitionKey}:popup:leave:${previousPlayer.seatId}`, + seatId: previousPlayer.seatId, + text: `${previousPlayer.name} left the table`, + tone: "leave", + }); + continue; + } + const leftBefore = previousPlayer.left || previousPlayer.bankrupt; + const leftNow = nextPlayer.left || nextPlayer.bankrupt; + if (leftNow && !leftBefore) { + events.push({ + key: `${transitionKey}:popup:leave:${previousPlayer.seatId}`, + seatId: previousPlayer.seatId, + text: nextPlayer.bankrupt + ? `${previousPlayer.name} is out of chips` + : `${previousPlayer.name} left the table`, + tone: "leave", + }); + } + } + + // A round settles exactly once: every seat ahead of the house gets one + // aggregate "+N" popup, never twice. + if (nextView.phase === "round-complete" && previousView.phase !== "round-complete" && nextView.roundResult) { + const netBySeat = new Map(); + for (const hand of nextView.roundResult.hands) { + netBySeat.set(hand.seatId, (netBySeat.get(hand.seatId) ?? 0) + hand.netChange); + } + for (const insurance of nextView.roundResult.insurance) { + const delta = insurance.won ? insurance.amount * 2 : -insurance.amount; + netBySeat.set(insurance.seatId, (netBySeat.get(insurance.seatId) ?? 0) + delta); + } + for (const [seatId, net] of netBySeat) { + if (net <= 0) continue; + events.push({ + key: `${transitionKey}:popup:win:${seatId}:${net}`, + seatId, + text: `+${net.toLocaleString()}`, + tone: "win", + }); + } + } + + return events; +} diff --git a/src/components/frontend/minigames/blackjack/blackjackSound.ts b/src/components/frontend/minigames/blackjack/blackjackSound.ts new file mode 100644 index 0000000..96985e8 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/blackjackSound.ts @@ -0,0 +1,348 @@ +import type { BlackjackSoundEvent } from "./blackjackSoundEvents"; + +export const DEFAULT_BLACKJACK_SOUND_VOLUME = 0.28; + +/** Sampled award sound; decoded once and routed through the master gain. */ +export const CHACHING_SAMPLE_URL = "/assets/chaching.mp3"; + +export type BlackjackWinSoundMode = "chaching" | "procedural"; + +type OscillatorShape = OscillatorType; + +function assertNever(value: never): never { + throw new Error(`Unknown blackjack sound event: ${JSON.stringify(value)}`); +} + +function clampVolume(value: number): number { + if (!Number.isFinite(value)) return DEFAULT_BLACKJACK_SOUND_VOLUME; + return Math.min(1, Math.max(0, value)); +} + +export class BlackjackSoundEngine { + private context: AudioContext | null = null; + private masterGain: GainNode | null = null; + private jazzTimer: number | null = null; + private jazzRunning = false; + private jazzNextAt = 0; + private muted = false; + private volume = DEFAULT_BLACKJACK_SOUND_VOLUME; + private winSoundMode: BlackjackWinSoundMode = "chaching"; + private chachingBuffer: AudioBuffer | null = null; + private chachingLoading = false; + + public setMuted(muted: boolean): void { + this.muted = muted; + this.setMasterGain(); + if (muted) { + this.stopJazzSchedule(); + } else if (this.context) { + this.startJazzSchedule(); + } + } + + public setVolume(volume: number): void { + this.volume = clampVolume(volume); + this.setMasterGain(); + } + + public setWinSound(mode: BlackjackWinSoundMode): void { + this.winSoundMode = mode; + if (mode === "chaching") this.ensureChachingBuffer(); + } + + public unlock(): void { + const context = this.ensureContext(); + if (!context) return; + if (context.state === "suspended") { + void context.resume().catch(() => undefined); + } + if (this.winSoundMode === "chaching") this.ensureChachingBuffer(); + if (!this.muted) this.startJazzSchedule(); + } + + public playUiClick(): void { + this.unlock(); + const context = this.context; + if (!context || this.muted) return; + this.scheduleTone(720, 0.045, 0.045, context.currentTime, "triangle"); + } + + public playEvent(event: BlackjackSoundEvent): void { + const context = this.context; + if (!context || this.muted) return; + + switch (event.type) { + case "round-start": + this.playRoundStart(context.currentTime); + return; + case "turn-gained": + this.playTurnGained(context.currentTime); + return; + case "turn-lost": + this.scheduleTone(330, 0.12, 0.07, context.currentTime, "sine"); + return; + case "cards-dealt": + this.playCardsDealt(event.cardCount, context.currentTime); + return; + case "wager": + this.playWager(context.currentTime); + return; + case "insurance": + this.playInsurance(context.currentTime); + return; + case "bust": + this.playBust(context.currentTime); + return; + case "blackjack": + this.playBlackjackFanfare(context.currentTime); + return; + case "settle": + this.playSettle(context.currentTime); + return; + case "hand-result": + this.playHandResult(event.result, context.currentTime); + return; + default: + return assertNever(event); + } + } + + public dispose(): void { + this.stopJazzSchedule(); + const context = this.context; + this.context = null; + this.masterGain = null; + if (context && context.state !== "closed") void context.close().catch(() => undefined); + } + + private ensureContext(): AudioContext | null { + if (this.context) return this.context; + if (typeof window === "undefined" || typeof window.AudioContext !== "function") return null; + + try { + const context = new window.AudioContext(); + const masterGain = context.createGain(); + masterGain.connect(context.destination); + this.context = context; + this.masterGain = masterGain; + this.setMasterGain(); + return context; + } catch (error) { + if (error instanceof Error) return null; + return null; + } + } + + private setMasterGain(): void { + if (!this.masterGain || !this.context) return; + const target = this.muted ? 0 : this.volume; + this.masterGain.gain.setTargetAtTime(target, this.context.currentTime, 0.035); + } + + private scheduleTone( + frequency: number, + duration: number, + level: number, + start: number, + shape: OscillatorShape, + detune = 0, + ): void { + const context = this.context; + const masterGain = this.masterGain; + if (!context || !masterGain) return; + + const oscillator = context.createOscillator(); + const gain = context.createGain(); + oscillator.type = shape; + oscillator.frequency.setValueAtTime(frequency, start); + oscillator.detune.setValueAtTime(detune, start); + gain.gain.setValueAtTime(0.0001, start); + gain.gain.exponentialRampToValueAtTime(Math.max(0.0001, level), start + 0.012); + gain.gain.exponentialRampToValueAtTime(0.0001, start + duration); + oscillator.connect(gain); + gain.connect(masterGain); + oscillator.start(start); + oscillator.stop(start + duration + 0.04); + } + + private scheduleNoise(duration: number, level: number, start: number, highPass = 900): void { + const context = this.context; + const masterGain = this.masterGain; + if (!context || !masterGain) return; + + const frameCount = Math.max(1, Math.floor(context.sampleRate * duration)); + const buffer = context.createBuffer(1, frameCount, context.sampleRate); + const channel = buffer.getChannelData(0); + for (let index = 0; index < channel.length; index += 1) { + channel[index] = Math.random() * 2 - 1; + } + + const source = context.createBufferSource(); + const filter = context.createBiquadFilter(); + const gain = context.createGain(); + source.buffer = buffer; + filter.type = "highpass"; + filter.frequency.setValueAtTime(highPass, start); + gain.gain.setValueAtTime(level, start); + gain.gain.exponentialRampToValueAtTime(0.0001, start + duration); + source.connect(filter); + filter.connect(gain); + gain.connect(masterGain); + source.start(start); + source.stop(start + duration + 0.04); + } + + private playTurnGained(start: number): void { + this.scheduleTone(660, 0.16, 0.08, start, "sine"); + this.scheduleTone(880, 0.2, 0.07, start + 0.11, "sine"); + } + + private playCardsDealt(cardCount: number, start: number): void { + for (let index = 0; index < cardCount; index += 1) { + const offset = index * 0.075; + this.scheduleNoise(0.12, 0.035, start + offset, 1200); + this.scheduleTone(440 + index * 35, 0.08, 0.025, start + offset, "triangle"); + } + } + + private playRoundStart(start: number): void { + this.scheduleNoise(0.1, 0.03, start, 1400); + this.scheduleTone(494, 0.14, 0.05, start + 0.05, "triangle"); + } + + private playWager(start: number): void { + this.scheduleNoise(0.07, 0.08, start, 650); + this.scheduleTone(540, 0.1, 0.055, start + 0.035, "triangle"); + } + + private playInsurance(start: number): void { + this.scheduleTone(587, 0.09, 0.05, start, "sine"); + this.scheduleTone(494, 0.12, 0.045, start + 0.08, "sine"); + } + + private playBust(start: number): void { + this.scheduleTone(311, 0.14, 0.06, start, "sawtooth"); + this.scheduleTone(233, 0.22, 0.05, start + 0.1, "sawtooth"); + this.scheduleNoise(0.16, 0.03, start, 500); + } + + private playBlackjackFanfare(start: number): void { + [523, 659, 784].forEach((frequency, index) => { + this.scheduleTone(frequency, 0.22, 0.06, start + index * 0.09, "triangle"); + }); + } + + private playSettle(start: number): void { + this.scheduleNoise(0.18, 0.04, start, 700); + this.scheduleTone(392, 0.3, 0.04, start, "sine"); + } + + private playHandResult(result: "win" | "loss" | "push", start: number): void { + if (result === "win") { + if (this.winSoundMode === "chaching") { + this.playChachingSample(start); + return; + } + this.scheduleTone(523, 0.22, 0.06, start, "triangle"); + this.scheduleTone(659, 0.25, 0.06, start + 0.12, "triangle"); + this.scheduleTone(784, 0.34, 0.07, start + 0.24, "triangle"); + return; + } + if (result === "push") { + this.scheduleTone(440, 0.18, 0.045, start, "sine"); + this.scheduleTone(440, 0.18, 0.04, start + 0.14, "sine"); + return; + } + this.scheduleTone(330, 0.24, 0.055, start, "sine"); + this.scheduleTone(247, 0.34, 0.05, start + 0.16, "sine"); + } + + /** + * Decode chaching.mp3 once into an AudioBuffer so the sample plays through + * the master gain (mute and volume apply like any other sound). Any + * failure leaves the buffer null and wins fall back to the procedural + * fanfare. + */ + private ensureChachingBuffer(): void { + if (this.chachingBuffer || this.chachingLoading) return; + const context = this.ensureContext(); + if (!context) return; + if (typeof window === "undefined" || typeof window.fetch !== "function") return; + + this.chachingLoading = true; + window + .fetch(CHACHING_SAMPLE_URL) + .then((response) => { + if (!response.ok) throw new Error(`chaching sample fetch failed: ${response.status}`); + return response.arrayBuffer(); + }) + .then((data) => context.decodeAudioData(data)) + .then((buffer) => { + this.chachingBuffer = buffer; + }) + .catch(() => { + this.chachingBuffer = null; + }) + .finally(() => { + this.chachingLoading = false; + }); + } + + private playChachingSample(start: number): void { + const context = this.context; + const masterGain = this.masterGain; + const buffer = this.chachingBuffer; + if (!context || !masterGain || !buffer) { + // Sample not loaded yet (or failed to decode): procedural fallback. + this.scheduleTone(523, 0.22, 0.06, start, "triangle"); + this.scheduleTone(659, 0.25, 0.06, start + 0.12, "triangle"); + this.scheduleTone(784, 0.34, 0.07, start + 0.24, "triangle"); + return; + } + const source = context.createBufferSource(); + source.buffer = buffer; + source.connect(masterGain); + source.start(start); + } + + private startJazzSchedule(): void { + if (this.jazzRunning || !this.context || this.muted) return; + this.jazzRunning = true; + this.jazzNextAt = Math.max(this.context.currentTime + 0.08, this.jazzNextAt); + this.scheduleJazzBar(); + } + + private scheduleJazzBar(): void { + const context = this.context; + if (!context || !this.jazzRunning || this.muted) return; + + const beat = 60 / 92; + const start = Math.max(context.currentTime + 0.04, this.jazzNextAt); + const chordSets = [ + [261.63, 329.63, 392], + [220, 277.18, 329.63], + [246.94, 311.13, 369.99], + [196, 246.94, 293.66], + ]; + const chord = chordSets[Math.floor(start / (beat * 4)) % chordSets.length] ?? chordSets[0]; + + chord.forEach((frequency, index) => { + this.scheduleTone(frequency, beat * 2.8, 0.012, start, "sine", index * 4 - 4); + }); + for (let beatIndex = 0; beatIndex < 4; beatIndex += 1) { + const beatStart = start + beatIndex * beat; + this.scheduleTone(82.41 + beatIndex * 3, beat * 0.7, 0.018, beatStart, "triangle"); + this.scheduleNoise(0.045, 0.012, beatStart + beat * 0.5, 3200); + this.scheduleNoise(0.035, 0.009, beatStart + beat * 0.75, 4200); + } + + this.jazzNextAt = start + beat * 4; + this.jazzTimer = window.setTimeout(() => this.scheduleJazzBar(), beat * 4 * 1000 - 80); + } + + private stopJazzSchedule(): void { + if (this.jazzTimer !== null) window.clearTimeout(this.jazzTimer); + this.jazzTimer = null; + this.jazzRunning = false; + } +} diff --git a/src/components/frontend/minigames/blackjack/blackjackSoundEvents.ts b/src/components/frontend/minigames/blackjack/blackjackSoundEvents.ts new file mode 100644 index 0000000..bd3a0f0 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/blackjackSoundEvents.ts @@ -0,0 +1,190 @@ +import type { BlackjackSnapshot } from "@/app/(frontend)/minigames/blackjack/actions"; +import type { PublicPlayerView } from "@/lib/minigames/blackjack"; + +type BlackjackSoundView = NonNullable; + +export type BlackjackSoundSnapshot = Pick & { + readonly you: Pick< + BlackjackSoundView, + | "roundNumber" + | "phase" + | "players" + | "dealer" + | "roundResult" + | "viewerSeat" + | "yourTurn" + | "yourHands" + > | null; +}; + +type BlackjackSoundYou = NonNullable; + +export type BlackjackSoundEvent = + | { readonly type: "round-start"; readonly key: string; readonly roundNumber: number } + | { readonly type: "turn-gained"; readonly key: string } + | { readonly type: "turn-lost"; readonly key: string } + | { + readonly type: "cards-dealt"; + readonly key: string; + readonly cardCount: number; + } + | { + readonly type: "wager"; + readonly key: string; + readonly amount: number; + readonly seatId: number; + } + | { readonly type: "insurance"; readonly key: string; readonly seatId: number } + | { readonly type: "bust"; readonly key: string; readonly seatId: number } + | { readonly type: "blackjack"; readonly key: string; readonly seatId: number } + | { readonly type: "settle"; readonly key: string } + | { + readonly type: "hand-result"; + readonly key: string; + readonly result: "win" | "loss" | "push"; + readonly netChange: number; + }; + +function totalCardCount(view: BlackjackSoundYou): number { + let count = view.dealer.cards.length; + for (const player of view.players) { + for (const hand of player.hands) count += hand.cards.length; + } + return count; +} + +function handStatuses(view: BlackjackSoundYou): Map { + const statuses = new Map(); + for (const player of view.players) { + statuses.set(player.seatId, player.hands.map((hand) => hand.status)); + } + return statuses; +} + +function viewerRoundNet(view: BlackjackSoundYou): number | null { + const result = view.roundResult; + if (!result) return null; + let net = 0; + let hands = 0; + for (const hand of result.hands) { + if (hand.seatId !== view.viewerSeat) continue; + net += hand.netChange; + hands += 1; + } + if (hands === 0) return null; + for (const insurance of result.insurance) { + if (insurance.seatId !== view.viewerSeat) continue; + net += insurance.won ? insurance.amount * 2 : -insurance.amount; + } + return net; +} + +export function soundEventsForTransition( + previous: BlackjackSoundSnapshot | null, + next: BlackjackSoundSnapshot, +): readonly BlackjackSoundEvent[] { + if (!previous || previous.version === next.version) return []; + + const previousView = previous.you; + const nextView = next.you; + if (!nextView) return []; + const events: BlackjackSoundEvent[] = []; + const transitionKey = `${next.version}:${nextView.roundNumber}:${nextView.phase}`; + + if ( + nextView.phase === "betting" && + (previousView === null || nextView.roundNumber > previousView.roundNumber) + ) { + events.push({ + type: "round-start", + key: `${transitionKey}:round-start:${nextView.roundNumber}`, + roundNumber: nextView.roundNumber, + }); + return events; + } + + if (!previousView) return events; + + if (previousView.yourTurn !== nextView.yourTurn) { + events.push({ + type: nextView.yourTurn ? "turn-gained" : "turn-lost", + key: `${transitionKey}:turn:${nextView.yourTurn ? "gained" : "lost"}:${nextView.phase}`, + }); + } + + // Cards hitting the felt: initial deal, hits, doubles, splits, and the + // dealer's draws all show up as a total-card-count increase. + const previousCards = totalCardCount(previousView); + const nextCards = totalCardCount(nextView); + const dealt = nextCards - previousCards; + if (dealt > 0) { + events.push({ + type: "cards-dealt", + key: `${transitionKey}:cards:${previousCards}:${nextCards}`, + cardCount: Math.min(6, dealt), + }); + } + + const sameRound = previousView.roundNumber === nextView.roundNumber; + if (sameRound && previousView.phase === nextView.phase) { + for (const nextPlayer of nextView.players) { + const previousPlayer = previousView.players.find( + (candidate) => candidate.seatId === nextPlayer.seatId, + ); + if (!previousPlayer) continue; + + if (!previousPlayer.hasBet && nextPlayer.hasBet && nextPlayer.hands.length > 0) { + events.push({ + type: "wager", + key: `${transitionKey}:wager:${nextPlayer.seatId}:${nextPlayer.hands[0]?.bet ?? 0}`, + amount: nextPlayer.hands[0]?.bet ?? 0, + seatId: nextPlayer.seatId, + }); + } + if (previousPlayer.insuranceBet === 0 && nextPlayer.insuranceBet > 0) { + events.push({ + type: "insurance", + key: `${transitionKey}:insurance:${nextPlayer.seatId}`, + seatId: nextPlayer.seatId, + }); + } + + const previousStatuses = handStatuses(previousView).get(nextPlayer.seatId) ?? []; + nextPlayer.hands.forEach((hand, handIndex) => { + const previousStatus = previousStatuses[handIndex]; + if (previousStatus === hand.status) return; + if (hand.status === "bust") { + events.push({ + type: "bust", + key: `${transitionKey}:bust:${nextPlayer.seatId}:${handIndex}`, + seatId: nextPlayer.seatId, + }); + } else if (hand.status === "blackjack") { + events.push({ + type: "blackjack", + key: `${transitionKey}:blackjack:${nextPlayer.seatId}:${handIndex}`, + seatId: nextPlayer.seatId, + }); + } + }); + } + } + + if (nextView.phase === "round-complete" && previousView.phase !== "round-complete") { + events.push({ type: "settle", key: `${transitionKey}:settle` }); + const net = viewerRoundNet(nextView); + if (net !== null) { + const result = net > 0 ? "win" : net < 0 ? "loss" : "push"; + events.push({ + type: "hand-result", + key: `${transitionKey}:result:${result}:${net}`, + result, + netChange: net, + }); + } + } + + return events; +} + +export type { PublicPlayerView }; diff --git a/src/components/frontend/minigames/blackjack/cardStyle.tsx b/src/components/frontend/minigames/blackjack/cardStyle.tsx new file mode 100644 index 0000000..90f703b --- /dev/null +++ b/src/components/frontend/minigames/blackjack/cardStyle.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react"; +import type { Card } from "@/lib/minigames/blackjack"; + +export type BlackjackCardStyle = "light" | "dark" | "classic"; + +// Shared with the poker table on purpose: one card-style preference for both +// card games, so switching skins in one applies to the other. +const CARD_STYLE_STORAGE_KEY = "poker-card-style"; + +const RANK_FILE_PREFIXES: Record = { + 11: "jack", + 12: "queen", + 13: "king", + 14: "ace", +}; + +const SUIT_FILE_SUFFIXES = { + s: "spades", + h: "hearts", + d: "diamonds", + c: "clubs", +} as const; + +export function mapCardToSvg(card: Card): string { + const prefix = RANK_FILE_PREFIXES[card.rank] ?? String(card.rank); + const suffix = SUIT_FILE_SUFFIXES[card.suit]; + return `/images/assets/playing-cards/${prefix}_of_${suffix}.svg`; +} + +export function normalizeCardStyle(value: unknown): BlackjackCardStyle { + return value === "dark" || value === "classic" ? value : "light"; +} + +export const BLACKJACK_CARD_STYLE_OPTIONS: readonly { + readonly value: BlackjackCardStyle; + readonly label: string; +}[] = [ + { value: "light", label: "Light" }, + { value: "dark", label: "Dark" }, + { value: "classic", label: "Classic SVG" }, +]; + +interface CardStyleContextValue { + readonly style: BlackjackCardStyle; + readonly setStyle: (style: BlackjackCardStyle) => void; +} + +const CardStyleContext = createContext({ + style: "light", + setStyle: () => {}, +}); + +export function BlackjackCardStyleProvider({ children }: { readonly children: ReactNode }) { + const [style, setStyleState] = useState("light"); + + useEffect(() => { + try { + const stored = window.localStorage.getItem(CARD_STYLE_STORAGE_KEY); + if (stored !== null) setStyleState(normalizeCardStyle(stored)); + } catch { + // storage unavailable (private mode, disabled): keep the default + } + }, []); + + const setStyle = useCallback((next: BlackjackCardStyle) => { + setStyleState(next); + try { + window.localStorage.setItem(CARD_STYLE_STORAGE_KEY, next); + } catch { + // non-fatal: the choice just will not persist + } + }, []); + + const value = useMemo(() => ({ style, setStyle }), [style, setStyle]); + + return {children}; +} + +export function useCardStyle(): CardStyleContextValue { + return useContext(CardStyleContext); +} diff --git a/src/components/frontend/minigames/blackjack/events.ts b/src/components/frontend/minigames/blackjack/events.ts new file mode 100644 index 0000000..4ae7449 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/events.ts @@ -0,0 +1,88 @@ +import type { + BlackjackSnapshot, + BlackjackTableSummary, +} from "@/app/(frontend)/minigames/blackjack/actions"; + +export { + BLACKJACK_STATE_EVENT, + BLACKJACK_TABLES_EVENT, + BLACKJACK_TERMINATED_EVENT, +} from "@/hooks/useGameTick"; + +export interface BlackjackStateEventDetail { + readonly tableId: string; + readonly snapshot: BlackjackSnapshot; +} + +export interface BlackjackTablesEventDetail { + readonly tables: readonly BlackjackTableSummary[]; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function isTableSummary(value: unknown): value is BlackjackTableSummary { + if (!isRecord(value)) return false; + return ( + typeof value.tableId === "string" && + typeof value.hostName === "string" && + typeof value.seatedCount === "number" && + typeof value.maxSeats === "number" && + typeof value.inGame === "boolean" && + typeof value.createdAt === "string" + ); +} + +function isBlackjackSnapshot(value: unknown): value is BlackjackSnapshot { + if (!isRecord(value)) return false; + const seats = value.seats; + const names = value.names; + return ( + typeof value.tableId === "string" && + typeof value.version === "number" && + (value.turnDeadline === null || typeof value.turnDeadline === "string") && + (value.nextRoundAt === null || typeof value.nextRoundAt === "string") && + (value.you === null || isRecord(value.you)) && + Array.isArray(seats) && + seats.every((seat) => seat === null || typeof seat === "number") && + Array.isArray(names) && + names.every((name) => name === null || typeof name === "string") && + (value.hostSeatId === null || typeof value.hostSeatId === "number") + ); +} + +export function readBlackjackStateEvent(event: Event): BlackjackStateEventDetail | null { + if (!(event instanceof CustomEvent) || !isRecord(event.detail)) return null; + const detail = event.detail; + if (typeof detail.tableId !== "string" || !isBlackjackSnapshot(detail.snapshot)) return null; + return { + tableId: detail.tableId, + snapshot: detail.snapshot, + }; +} + +export function readBlackjackTablesEvent(event: Event): BlackjackTablesEventDetail | null { + if (!(event instanceof CustomEvent) || !isRecord(event.detail)) return null; + const rawTables = event.detail.tables; + if (!Array.isArray(rawTables)) return null; + + const tables: BlackjackTableSummary[] = []; + for (const table of rawTables) { + if (!isTableSummary(table)) return null; + tables.push(table); + } + return { tables }; +} + +export function readBlackjackTerminatedEvent( + event: Event, +): { tableId: string; reason: string } | null { + if (!(event instanceof CustomEvent) || !isRecord(event.detail)) return null; + const detail = event.detail; + if (typeof detail.tableId !== "string") return null; + return { + tableId: detail.tableId, + reason: typeof detail.reason === "string" ? detail.reason : "force-ended", + }; +} diff --git a/src/components/frontend/minigames/blackjack/seatPosition.ts b/src/components/frontend/minigames/blackjack/seatPosition.ts new file mode 100644 index 0000000..d4fc2e8 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/seatPosition.ts @@ -0,0 +1,41 @@ +// Blackjack seat arcs: every player seat sits on the bottom two-thirds of +// the felt and the top third stays clear for the dealer. Six rim slots +// (three per side: corner 30%, mid 50%, low 70% of the felt height, bulging +// outward at the middle to follow the ellipse) keep every panel inside the +// curved edge with even vertical rhythm. The viewer is pinned bottom-center, +// so odd counts put the extra seat on the right side. Order is right-to-left, +// casino "first base" first. +const ARC_POSITION_CLASSES = [ + ["blackjack-seat-arc-corner-right"], + ["blackjack-seat-arc-corner-right", "blackjack-seat-arc-corner-left"], + ["blackjack-seat-arc-mid-right", "blackjack-seat-arc-mid-left", "blackjack-seat-arc-low-right"], + [ + "blackjack-seat-arc-corner-right", + "blackjack-seat-arc-low-right", + "blackjack-seat-arc-low-left", + "blackjack-seat-arc-corner-left", + ], + [ + "blackjack-seat-arc-corner-right", + "blackjack-seat-arc-mid-right", + "blackjack-seat-arc-low-right", + "blackjack-seat-arc-low-left", + "blackjack-seat-arc-corner-left", + ], +] as const; + +export function seatPosition(opponentIndex: number, opponentCount: number): string { + const normalizedCount = Math.min(ARC_POSITION_CLASSES.length, Math.max(1, opponentCount)); + const positions = ARC_POSITION_CLASSES[normalizedCount - 1] ?? ARC_POSITION_CLASSES[0]; + const normalizedIndex = Math.min(positions.length - 1, Math.max(0, opponentIndex)); + return positions[normalizedIndex] ?? positions[0] ?? "blackjack-seat-arc-corner-right"; +} + +const TOP_SEAT_CLASSES = new Set([ + "blackjack-seat-arc-corner-right", + "blackjack-seat-arc-corner-left", +]); + +export function isTopSeatPosition(positionClass: string): boolean { + return TOP_SEAT_CLASSES.has(positionClass); +} diff --git a/src/components/frontend/minigames/blackjack/types.ts b/src/components/frontend/minigames/blackjack/types.ts new file mode 100644 index 0000000..b55c57e --- /dev/null +++ b/src/components/frontend/minigames/blackjack/types.ts @@ -0,0 +1,7 @@ +export interface BlackjackStats { + readonly gamesPlayed: number; + readonly roundsWon: number; + readonly blackjacksHit: number; + readonly bestScore: number; + readonly xpEarned: number; +} diff --git a/src/components/frontend/minigames/blackjack/useBlackjackPopups.ts b/src/components/frontend/minigames/blackjack/useBlackjackPopups.ts new file mode 100644 index 0000000..fefaec1 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/useBlackjackPopups.ts @@ -0,0 +1,111 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import type { BlackjackSnapshot } from "@/app/(frontend)/minigames/blackjack/actions"; +import { + actionLogText, + popupEventsForTransition, + type BlackjackActionLogEntry, + type BlackjackPopupEvent, +} from "./blackjackPopups"; +import type { BlackjackSoundSnapshot } from "./blackjackSoundEvents"; + +export type ActiveBlackjackPopup = BlackjackPopupEvent; + +const POPUP_LIFETIME_MS = 2000; +const LEAVE_POPUP_LIFETIME_MS = 3000; +const MAX_ACTIVE_POPUPS = 12; +const MAX_SEEN_KEYS = 256; +const MAX_LOG_ENTRIES = 5; + +const POPUP_LIFETIMES_MS: Record = { + action: POPUP_LIFETIME_MS, + wager: POPUP_LIFETIME_MS, + win: POPUP_LIFETIME_MS, + fold: POPUP_LIFETIME_MS, + leave: LEAVE_POPUP_LIFETIME_MS, +}; + +export function useBlackjackPopups(snapshot: BlackjackSnapshot | null): { + popups: readonly ActiveBlackjackPopup[]; + actionLog: readonly BlackjackActionLogEntry[]; +} { + const [popups, setPopups] = useState([]); + const [actionLog, setActionLog] = useState([]); + const previousSnapshotRef = useRef(null); + const seenKeysRef = useRef>(new Set()); + const timersRef = useRef[]>([]); + + useEffect(() => { + if (!snapshot) { + previousSnapshotRef.current = null; + return; + } + const previousSnapshot = previousSnapshotRef.current; + if (previousSnapshot) { + const fresh = popupEventsForTransition(previousSnapshot, snapshot).filter((event) => { + if (seenKeysRef.current.has(event.key)) return false; + seenKeysRef.current.add(event.key); + return true; + }); + while (seenKeysRef.current.size > MAX_SEEN_KEYS) { + const oldest = seenKeysRef.current.values().next().value; + if (typeof oldest !== "string") break; + seenKeysRef.current.delete(oldest); + } + if (fresh.length > 0) { + const immediate: ActiveBlackjackPopup[] = []; + for (const event of fresh) { + const delay = event.delay ?? 0; + if (delay > 0) { + timersRef.current.push( + setTimeout(() => { + setPopups((current) => [...current, event].slice(-MAX_ACTIVE_POPUPS)); + timersRef.current.push( + setTimeout(() => { + setPopups((current) => current.filter((popup) => popup.key !== event.key)); + }, POPUP_LIFETIMES_MS[event.tone]), + ); + }, delay), + ); + } else { + immediate.push(event); + timersRef.current.push( + setTimeout(() => { + setPopups((current) => current.filter((popup) => popup.key !== event.key)); + }, POPUP_LIFETIMES_MS[event.tone]), + ); + } + } + if (immediate.length > 0) { + setPopups((current) => [...current, ...immediate].slice(-MAX_ACTIVE_POPUPS)); + } + const nextView = snapshot.you; + if (nextView) { + setActionLog((current) => + [ + ...fresh + .map((event) => ({ + key: event.key, + text: actionLogText(event, nextView.players), + tone: event.tone, + })) + .reverse(), + ...current, + ].slice(0, MAX_LOG_ENTRIES), + ); + } + } + } + previousSnapshotRef.current = snapshot; + }, [snapshot]); + + useEffect(() => { + return () => { + for (const timer of timersRef.current) clearTimeout(timer); + timersRef.current = []; + }; + }, []); + + return { popups, actionLog }; +} diff --git a/src/components/frontend/minigames/blackjack/useBlackjackSounds.ts b/src/components/frontend/minigames/blackjack/useBlackjackSounds.ts new file mode 100644 index 0000000..f100223 --- /dev/null +++ b/src/components/frontend/minigames/blackjack/useBlackjackSounds.ts @@ -0,0 +1,195 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; +import type { BlackjackSnapshot } from "@/app/(frontend)/minigames/blackjack/actions"; +import { soundEventsForTransition, type BlackjackSoundSnapshot } from "./blackjackSoundEvents"; +import { + DEFAULT_BLACKJACK_SOUND_VOLUME, + BlackjackSoundEngine, + type BlackjackWinSoundMode, +} from "./blackjackSound"; + +const SOUND_SETTINGS_KEY = "polaris:blackjack-sound-settings"; + +export interface BlackjackSoundSettings { + readonly muted: boolean; + readonly volume: number; + readonly winSound: BlackjackWinSoundMode; +} + +export interface UseBlackjackSoundsResult extends BlackjackSoundSettings { + readonly toggleMuted: () => void; + readonly setVolume: (volume: number) => void; + /** Switches the win sound mode and previews it (hears what you chose). */ + readonly setWinSound: (mode: BlackjackWinSoundMode) => void; + readonly playUiClick: () => void; +} + +const DEFAULT_SOUND_SETTINGS: BlackjackSoundSettings = { + muted: false, + volume: DEFAULT_BLACKJACK_SOUND_VOLUME, + winSound: "chaching", +}; + +export type BlackjackSoundControls = ReturnType; + +function isStoredSettings( + value: unknown, +): value is { readonly muted: unknown; readonly volume: unknown; readonly winSound?: unknown } { + return typeof value === "object" && value !== null && "muted" in value && "volume" in value; +} + +function isWinSoundMode(value: unknown): value is BlackjackWinSoundMode { + return value === "chaching" || value === "procedural"; +} + +function clampVolume(value: number): number { + if (!Number.isFinite(value)) return DEFAULT_BLACKJACK_SOUND_VOLUME; + return Math.min(1, Math.max(0, value)); +} + +function readStoredSettings(): BlackjackSoundSettings { + if (typeof window === "undefined") return DEFAULT_SOUND_SETTINGS; + + try { + const raw = window.localStorage.getItem(SOUND_SETTINGS_KEY); + if (!raw) return DEFAULT_SOUND_SETTINGS; + const parsed: unknown = JSON.parse(raw); + if ( + !isStoredSettings(parsed) || + typeof parsed.muted !== "boolean" || + typeof parsed.volume !== "number" + ) { + return DEFAULT_SOUND_SETTINGS; + } + return { + muted: parsed.muted, + volume: clampVolume(parsed.volume), + winSound: isWinSoundMode(parsed.winSound) ? parsed.winSound : DEFAULT_SOUND_SETTINGS.winSound, + }; + } catch (error) { + if (error instanceof Error) return DEFAULT_SOUND_SETTINGS; + return DEFAULT_SOUND_SETTINGS; + } +} + +function writeStoredSettings(settings: BlackjackSoundSettings): void { + if (typeof window === "undefined") return; + + try { + window.localStorage.setItem(SOUND_SETTINGS_KEY, JSON.stringify(settings)); + } catch (error) { + if (error instanceof Error) return; + } +} + +export function useBlackjackSounds(snapshot: BlackjackSnapshot | null): UseBlackjackSoundsResult { + const [settings, setSettings] = useState(DEFAULT_SOUND_SETTINGS); + // Mirror of settings the setters can update synchronously: ensureEngine + // must never re-assert a stale mode between setSettings and the re-render. + const settingsRef = useRef(DEFAULT_SOUND_SETTINGS); + settingsRef.current = settings; + const engineRef = useRef(null); + const previousSnapshotRef = useRef(null); + const playedEventKeysRef = useRef>(new Set()); + const timersRef = useRef[]>([]); + const [settingsLoaded, setSettingsLoaded] = useState(false); + + const ensureEngine = useCallback((): BlackjackSoundEngine => { + const engine = engineRef.current ?? new BlackjackSoundEngine(); + engineRef.current = engine; + engine.setVolume(settingsRef.current.volume); + engine.setMuted(settingsRef.current.muted); + engine.setWinSound(settingsRef.current.winSound); + engine.unlock(); + return engine; + }, []); + + const playUiClick = useCallback(() => { + ensureEngine().playUiClick(); + }, [ensureEngine]); + + const toggleMuted = useCallback(() => { + const nextMuted = !settingsRef.current.muted; + settingsRef.current = { ...settingsRef.current, muted: nextMuted }; + setSettings((current) => ({ ...current, muted: nextMuted })); + ensureEngine(); + }, [ensureEngine]); + + const setVolume = useCallback( + (volume: number) => { + const nextVolume = clampVolume(volume); + settingsRef.current = { ...settingsRef.current, volume: nextVolume }; + setSettings((current) => ({ ...current, volume: nextVolume })); + ensureEngine(); + }, + [ensureEngine], + ); + + const setWinSound = useCallback( + (mode: BlackjackWinSoundMode) => { + settingsRef.current = { ...settingsRef.current, winSound: mode }; + setSettings((current) => ({ ...current, winSound: mode })); + ensureEngine().playEvent({ + type: "hand-result", + key: `preview:${Date.now()}`, + result: "win", + netChange: 0, + }); + }, + [ensureEngine], + ); + + useEffect(() => { + setSettings(readStoredSettings()); + setSettingsLoaded(true); + }, []); + + useEffect(() => { + if (settingsLoaded) writeStoredSettings(settings); + }, [settings, settingsLoaded]); + + useEffect(() => { + if (!snapshot) { + previousSnapshotRef.current = null; + return; + } + const previousSnapshot = previousSnapshotRef.current; + if (previousSnapshot) { + const events = soundEventsForTransition(previousSnapshot, snapshot); + const engine = engineRef.current; + if (engine) { + for (const event of events) { + if (playedEventKeysRef.current.has(event.key)) continue; + playedEventKeysRef.current.add(event.key); + engine.playEvent(event); + } + } + while (playedEventKeysRef.current.size > 256) { + const oldest = playedEventKeysRef.current.values().next().value; + if (typeof oldest !== "string") break; + playedEventKeysRef.current.delete(oldest); + } + } + previousSnapshotRef.current = snapshot; + }, [snapshot]); + + useEffect(() => { + return () => { + for (const timer of timersRef.current) clearTimeout(timer); + timersRef.current = []; + engineRef.current?.dispose(); + engineRef.current = null; + }; + }, []); + + return { + muted: settings.muted, + volume: settings.volume, + winSound: settings.winSound, + toggleMuted, + setVolume, + setWinSound, + playUiClick, + }; +} diff --git a/src/components/frontend/realtime/GameTickRealtime.tsx b/src/components/frontend/realtime/GameTickRealtime.tsx index 93554f7..ea24d5a 100644 --- a/src/components/frontend/realtime/GameTickRealtime.tsx +++ b/src/components/frontend/realtime/GameTickRealtime.tsx @@ -3,6 +3,9 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { + BLACKJACK_STATE_EVENT, + BLACKJACK_TABLES_EVENT, + BLACKJACK_TERMINATED_EVENT, GAME_EVENT_EVENT, GAME_TICK_EVENT, NOTIFICATION_EVENT, @@ -10,6 +13,9 @@ import { POKER_TABLES_EVENT, POKER_TERMINATED_EVENT, PROFILE_UPDATE_EVENT, + type BlackjackStateDetail, + type BlackjackTablesDetail, + type BlackjackTerminatedDetail, type GameTickDetail, type PokerStateDetail, type PokerTablesDetail, @@ -90,6 +96,33 @@ export function GameTickRealtime() { } window.dispatchEvent(new CustomEvent(POKER_TERMINATED_EVENT, { detail })); }); + source.addEventListener("blackjack-state", (event) => { + let detail: BlackjackStateDetail = {}; + try { + detail = JSON.parse(event.data ?? "{}"); + } catch { + // ignore malformed payload + } + window.dispatchEvent(new CustomEvent(BLACKJACK_STATE_EVENT, { detail })); + }); + source.addEventListener("blackjack-tables", (event) => { + let detail: BlackjackTablesDetail = {}; + try { + detail = JSON.parse(event.data ?? "{}"); + } catch { + // ignore malformed payload + } + window.dispatchEvent(new CustomEvent(BLACKJACK_TABLES_EVENT, { detail })); + }); + source.addEventListener("blackjack-terminated", (event) => { + let detail: BlackjackTerminatedDetail = {}; + try { + detail = JSON.parse(event.data ?? "{}"); + } catch { + // ignore malformed payload + } + window.dispatchEvent(new CustomEvent(BLACKJACK_TERMINATED_EVENT, { detail })); + }); return () => { if (refreshTimer) clearTimeout(refreshTimer); source.close(); diff --git a/src/hooks/useGameTick.ts b/src/hooks/useGameTick.ts index f5f931c..7bdd047 100644 --- a/src/hooks/useGameTick.ts +++ b/src/hooks/useGameTick.ts @@ -1,6 +1,10 @@ "use client"; import { useEffect, useRef, useState } from "react"; +import type { + BlackjackSnapshot, + BlackjackTableSummary, +} from "@/app/(frontend)/minigames/blackjack/actions"; import type { PokerSnapshot, PokerTableSummary } from "@/app/(frontend)/minigames/poker/actions"; export const GAME_TICK_EVENT = "ptf:game-tick"; @@ -11,6 +15,9 @@ export const SESSION_EXPIRED_EVENT = "ptf:session-expired"; export const POKER_STATE_EVENT = "ptf:poker-state"; export const POKER_TABLES_EVENT = "ptf:poker-tables"; export const POKER_TERMINATED_EVENT = "ptf:poker-terminated"; +export const BLACKJACK_STATE_EVENT = "ptf:blackjack-state"; +export const BLACKJACK_TABLES_EVENT = "ptf:blackjack-tables"; +export const BLACKJACK_TERMINATED_EVENT = "ptf:blackjack-terminated"; export interface GameTickDetail { processedAt?: string; @@ -19,6 +26,8 @@ export interface GameTickDetail { export interface ProfileUpdateDetail { reason?: string; xpGained?: number; + /** Absolute new chip-pool balance; present on chip-affecting updates. */ + chipBank?: number; } export interface PokerStateDetail { @@ -35,6 +44,20 @@ export interface PokerTerminatedDetail { reason?: string; } +export interface BlackjackStateDetail { + tableId?: string; + snapshot?: BlackjackSnapshot; +} + +export interface BlackjackTablesDetail { + tables?: BlackjackTableSummary[]; +} + +export interface BlackjackTerminatedDetail { + tableId?: string; + reason?: string; +} + export function useGameTick(onTick?: () => void): number { const [count, setCount] = useState(0); const callbackRef = useRef(onTick); diff --git a/src/lib/minigames/blackjack/actions.ts b/src/lib/minigames/blackjack/actions.ts new file mode 100644 index 0000000..73c90fa --- /dev/null +++ b/src/lib/minigames/blackjack/actions.ts @@ -0,0 +1,360 @@ +/** + * Player action application for blackjack rounds. + * Pure: validates legality, produces the next state, and auto-advances the + * round whenever the action unblocks progression (next bettor, next hand, + * dealer play, or settle). + */ + +import { MAX_BET } from "./constants"; +import { advanceState, createEmptyHand, drawCards } from "./engine"; +import { canDoubleHand, canSplitHand, handValue, insuranceMaxFor } from "./rules"; +import type { + BlackjackActionKind, + BlackjackHand, + BlackjackPlayerState, + BlackjackSeatId, + BlackjackTableState, +} from "./types"; + +export type BlackjackActionResult = + | { readonly success: true; readonly state: BlackjackTableState } + | { readonly success: false; readonly error: string }; + +function failure(error: string): BlackjackActionResult { + return { success: false, error }; +} + +function playerAt( + state: BlackjackTableState, + seatId: BlackjackSeatId, +): BlackjackPlayerState | null { + return state.players[seatId] ?? null; +} + +function handAt( + player: BlackjackPlayerState, + handIndex: number, +): BlackjackHand | null { + return player.hands[handIndex] ?? null; +} + +function placeBet( + state: BlackjackTableState, + seatId: BlackjackSeatId, + amount: number | undefined, +): BlackjackActionResult { + const player = playerAt(state, seatId); + if (!player) return failure("No player occupies that seat."); + if (player.hasBet) return failure("You have already bet this round."); + if (player.sittingOut) return failure("You sat out this round."); + if (player.bankrupt) return failure("Your bankroll cannot cover the table minimum."); + if (amount === undefined) return failure("A bet requires an amount."); + if (!Number.isInteger(amount)) return failure("Bets must be whole chips."); + if (amount < state.config.minBet) { + return failure(`The minimum bet is ${state.config.minBet}.`); + } + if (amount > state.config.maxBet) { + return failure(`The maximum bet is ${state.config.maxBet}.`); + } + if (amount > player.bankroll) { + return failure("The bet exceeds your bankroll."); + } + const updated: BlackjackPlayerState = { + ...player, + bankroll: player.bankroll - amount, + hands: [createEmptyHand(amount)], + hasBet: true, + }; + const next: BlackjackTableState = { + ...state, + players: state.players.map((candidate) => + candidate.seatId === seatId ? updated : candidate, + ), + }; + return { success: true, state: advanceState(next) }; +} + +function takeInsurance( + state: BlackjackTableState, + seatId: BlackjackSeatId, + amount: number | undefined, +): BlackjackActionResult { + const player = playerAt(state, seatId); + if (!player) return failure("No player occupies that seat."); + if (!player.hasBet) return failure("Insurance requires a bet this round."); + if (player.insuranceAnswered) { + return failure("You have already answered the insurance offer."); + } + const cap = Math.min(insuranceMaxFor(player.hands[0]?.bet ?? 0), player.bankroll); + if (cap <= 0) return failure("Insurance is not available right now."); + if (amount === undefined) return failure("Insurance requires an amount."); + if (!Number.isInteger(amount) || amount < 1) { + return failure("Insurance must be at least one whole chip."); + } + if (amount > cap) return failure(`Insurance is capped at ${cap}.`); + const updated: BlackjackPlayerState = { + ...player, + bankroll: player.bankroll - amount, + insuranceBet: amount, + insuranceAnswered: true, + }; + const next: BlackjackTableState = { + ...state, + players: state.players.map((candidate) => + candidate.seatId === seatId ? updated : candidate, + ), + }; + return { success: true, state: advanceState(next) }; +} + +function declineInsurance( + state: BlackjackTableState, + seatId: BlackjackSeatId, +): BlackjackActionResult { + const player = playerAt(state, seatId); + if (!player) return failure("No player occupies that seat."); + if (!player.hasBet) return failure("Insurance requires a bet this round."); + if (player.insuranceAnswered) { + return failure("You have already answered the insurance offer."); + } + const updated: BlackjackPlayerState = { ...player, insuranceAnswered: true }; + const next: BlackjackTableState = { + ...state, + players: state.players.map((candidate) => + candidate.seatId === seatId ? updated : candidate, + ), + }; + return { success: true, state: advanceState(next) }; +} + +/** Draw one card onto a hand and resolve 21/bust (or forced stand) immediately. */ +function drawToHand( + state: BlackjackTableState, + player: BlackjackPlayerState, + handIndex: number, + hand: BlackjackHand, + autoStand: boolean, +): { state: BlackjackTableState; player: BlackjackPlayerState; hand: BlackjackHand } { + const { cards, shoe } = drawCards(state, 1); + const card = cards[0]; + const withCard: BlackjackHand = card ? { ...hand, cards: [...hand.cards, card] } : hand; + const total = handValue(withCard.cards).total; + const status = + total > 21 + ? ("bust" as const) + : total === 21 || autoStand + ? ("stood" as const) + : withCard.status; + const resolved: BlackjackHand = { ...withCard, status }; + const updatedPlayer: BlackjackPlayerState = { + ...player, + hands: player.hands.map((candidate, index) => (index === handIndex ? resolved : candidate)), + }; + const updatedState: BlackjackTableState = { + ...state, + shoe, + players: state.players.map((candidate) => + candidate.seatId === player.seatId ? updatedPlayer : candidate, + ), + }; + return { state: updatedState, player: updatedPlayer, hand: resolved }; +} + +function playHand( + state: BlackjackTableState, + seatId: BlackjackSeatId, + action: "hit" | "stand" | "double" | "split", +): BlackjackActionResult { + const player = playerAt(state, seatId); + if (!player) return failure("No player occupies that seat."); + const handIndex = state.activeHandIndex; + const hand = handAt(player, handIndex); + if (!hand || hand.status !== "active") { + return failure("That hand is not awaiting an action."); + } + + if (action === "stand") { + const stood: BlackjackHand = { ...hand, status: "stood" }; + const updatedPlayer: BlackjackPlayerState = { + ...player, + hands: player.hands.map((candidate, index) => (index === handIndex ? stood : candidate)), + }; + const next: BlackjackTableState = { + ...state, + players: state.players.map((candidate) => + candidate.seatId === seatId ? updatedPlayer : candidate, + ), + }; + return { success: true, state: advanceState(next) }; + } + + if (action === "hit") { + const drawn = drawToHand(state, player, handIndex, hand, false); + return { success: true, state: advanceState(drawn.state) }; + } + + if (action === "double") { + if (!canDoubleHand(hand, player.bankroll)) { + if (hand.cards.length !== 2) { + return failure("Doubling is only allowed on the first two cards."); + } + return failure("Doubling requires your bankroll to cover the extra stake."); + } + const extraStake = hand.bet; + const reduced: BlackjackPlayerState = { ...player, bankroll: player.bankroll - extraStake }; + const doubled: BlackjackHand = { ...hand, bet: hand.bet * 2, doubled: true }; + const staged: BlackjackTableState = { + ...state, + players: state.players.map((candidate) => + candidate.seatId === seatId + ? { + ...reduced, + hands: reduced.hands.map((candidateHand, index) => + index === handIndex ? doubled : candidateHand, + ), + } + : candidate, + ), + }; + const stagedPlayer = playerAt(staged, seatId); + if (!stagedPlayer) return failure("No player occupies that seat."); + const stagedHand = handAt(stagedPlayer, handIndex); + if (!stagedHand) return failure("That hand is not awaiting an action."); + // Doubling draws exactly one card and the hand then stands (or busts). + const drawn = drawToHand(staged, stagedPlayer, handIndex, stagedHand, true); + return { success: true, state: advanceState(drawn.state) }; + } + + // Split: matching ranks already verified by canSplitHand. + if (!canSplitHand(player.hands, hand, player.bankroll)) { + if (hand.cards.length !== 2 || hand.cards[0].rank !== hand.cards[1].rank) { + return failure("Splitting requires a pair of matching ranks."); + } + if (hand.splitAces) { + return failure("Split aces cannot be split again."); + } + return failure("Splitting requires your bankroll to cover the second stake."); + } + const secondStake = hand.bet; + const reduced: BlackjackPlayerState = { ...player, bankroll: player.bankroll - secondStake }; + const { cards, shoe } = drawCards(state, 2); + const [cardA, cardB] = cards; + if (!cardA || !cardB) { + return failure("The shoe is out of cards; try again in a moment."); + } + const splitAces = hand.cards[0].rank === 14; + const autoStatus = splitAces ? ("stood" as const) : ("active" as const); + const handA: BlackjackHand = { + ...createEmptyHand(secondStake), + cards: [hand.cards[0], cardA], + fromSplit: true, + splitAces, + status: handValue([hand.cards[0], cardA]).total === 21 ? ("stood" as const) : autoStatus, + }; + const handB: BlackjackHand = { + ...createEmptyHand(secondStake), + cards: [hand.cards[1], cardB], + fromSplit: true, + splitAces, + status: handValue([hand.cards[1], cardB]).total === 21 ? ("stood" as const) : autoStatus, + }; + const splitHands = [ + ...player.hands.slice(0, handIndex), + handA, + handB, + ...player.hands.slice(handIndex + 1), + ]; + const updatedPlayer: BlackjackPlayerState = { ...reduced, hands: splitHands }; + const next: BlackjackTableState = { + ...state, + shoe, + activeHandIndex: handIndex, + players: state.players.map((candidate) => + candidate.seatId === seatId ? updatedPlayer : candidate, + ), + }; + return { success: true, state: advanceState(next) }; +} + +/** + * Apply an action for `seatId`. Bet amounts are whole chips within the table + * limits; insurance is capped at half the main bet. On success the state is + * auto-advanced (next bettor, next hand, dealer play, or settle). + */ +export function applyAction( + state: BlackjackTableState, + seatId: BlackjackSeatId, + action: BlackjackActionKind, + amount?: number, +): BlackjackActionResult { + const player = playerAt(state, seatId); + if (!player) return failure("No player occupies that seat."); + if (player.bankrupt && action !== "decline-insurance") { + return failure("That seat is out of chips."); + } + + switch (action) { + case "bet": { + if (state.phase !== "betting") { + return failure("The table is not accepting bets right now."); + } + if (state.toActSeat !== seatId) { + return failure("It is not that seat's turn to bet."); + } + return placeBet(state, seatId, amount); + } + case "insurance": + case "decline-insurance": { + if (state.phase !== "insurance") { + return failure("Insurance is not being offered right now."); + } + if (state.toActSeat !== seatId) { + return failure("It is not that seat's turn to answer the insurance offer."); + } + return action === "insurance" + ? takeInsurance(state, seatId, amount) + : declineInsurance(state, seatId); + } + case "hit": + case "stand": + case "double": + case "split": { + if (state.phase !== "player") { + return failure("The table is not accepting hand actions right now."); + } + if (state.toActSeat !== seatId) { + return failure("It is not that seat's turn to act."); + } + return playHand(state, seatId, action); + } + default: { + const exhaustive: never = action; + return failure(`Unknown action: ${String(exhaustive)}`); + } + } +} + +/** + * Mark a seat as sitting out the current betting round. Used by the registry + * turn-timer timeout so an idle player does not stall the table. + */ +export function sitOut( + state: BlackjackTableState, + seatId: BlackjackSeatId, +): BlackjackActionResult { + if (state.phase !== "betting") { + return failure("The table is not accepting bets right now."); + } + const player = playerAt(state, seatId); + if (!player) return failure("No player occupies that seat."); + if (player.hasBet) return failure("You have already bet this round."); + if (player.sittingOut) return failure("You are already sitting out this round."); + const updated: BlackjackPlayerState = { ...player, sittingOut: true }; + const next: BlackjackTableState = { + ...state, + players: state.players.map((candidate) => + candidate.seatId === seatId ? updated : candidate, + ), + }; + return { success: true, state: advanceState(next) }; +} diff --git a/src/lib/minigames/blackjack/constants.ts b/src/lib/minigames/blackjack/constants.ts new file mode 100644 index 0000000..577416e --- /dev/null +++ b/src/lib/minigames/blackjack/constants.ts @@ -0,0 +1,51 @@ +/** + * Tunable constants for the blackjack engine. + * Pure data, no imports: safe for client and server. + */ + +/** Play chips each player starts a session with. */ +export const STARTING_BANKROLL = 1000; + +/** + * Chips each player must buy in with from their persistent chip pool when a + * session starts. The buy-in returns to the pool (plus or minus winnings) + * when the session settles; blackjack tables play at half poker's stakes. + */ +export const BUY_IN = 500; + +/** Table minimum bet per hand. */ +export const MIN_BET = 10; + +/** Table maximum bet per hand. */ +export const MAX_BET = 500; + +/** + * How long a player has to act, in milliseconds. The engine itself never + * enforces this (it is pure and synchronous); the table registry reads it to + * run turn timers and auto-act for stale players (sit out / decline / stand). + */ +export const TURN_TIMEOUT_MS = 30_000; + +/** Supported player count: solo play through 6-handed tables. */ +export const MIN_PLAYERS = 1; +export const MAX_PLAYERS = 6; + +export const MAX_TABLE_NAME_LENGTH = 48; + +/** Number of standard 52-card decks in the shoe. */ +export const SHOE_DECKS = 6; + +/** Reshuffle the shoe between rounds once this fraction of cards remains. */ +export const RESHUFFLE_PENETRATION = 0.25; + +/** Maximum hands a player may hold at once (3 splits). */ +export const MAX_HANDS = 4; + +/** Natural blackjack pays 3 to 2. */ +export const BLACKJACK_PAYOUT = 1.5; + +/** Insurance pays 2 to 1 (stake returned plus twice the stake on a win). */ +export const INSURANCE_PAYOUT = 2; + +/** Dealer stands on all 17s, soft included (S17). */ +export const DEALER_STAND_TOTAL = 17; diff --git a/src/lib/minigames/blackjack/engine.ts b/src/lib/minigames/blackjack/engine.ts new file mode 100644 index 0000000..3a7595d --- /dev/null +++ b/src/lib/minigames/blackjack/engine.ts @@ -0,0 +1,545 @@ +/** + * Core table lifecycle: session creation and round progression for + * blackjack. Action application lives in actions.ts. + * + * Pure and server-authoritative: every function takes a state and returns a + * new state (never mutates), all randomness flows from the table seed, and + * nothing here imports Payload, Next.js, or process. The registry wrapper + * holds states, runs turn timers, and exposes views. + */ + +import { + MAX_BET, + MAX_PLAYERS, + MIN_BET, + MIN_PLAYERS, + STARTING_BANKROLL, + TURN_TIMEOUT_MS, +} from "./constants"; +import { + blackjackWinAmount, + dealerShouldHit, + handValue, + insuranceMaxFor, + insuranceWinAmount, + isNaturalBlackjack, + isTenValue, +} from "./rules"; +import { + createShuffledShoe, + createShoe, + mulberry32, + needsShuffle, + shoeSeed, + shuffleShoe, +} from "./shoe"; +import type { + BlackjackHand, + BlackjackPlayerState, + BlackjackRoundHandResult, + BlackjackSeatId, + BlackjackTableConfig, + BlackjackTableState, + Card, + RoundOutcome, +} from "./types"; + +export interface BlackjackPlayerInit { + readonly name: string; + /** Overrides the table's starting bankroll for this seat (tests/scenarios). */ + readonly bankroll?: number; +} + +export interface CreateTableOptions { + readonly startingBankroll?: number; + readonly minBet?: number; + readonly maxBet?: number; +} + +/** A fresh, empty hand carrying only its stake (filled in when cards land). */ +export function createEmptyHand(bet: number): BlackjackHand { + return { + cards: [], + bet, + status: "active", + doubled: false, + fromSplit: false, + splitAces: false, + outcome: null, + netChange: null, + }; +} + +const emptyHand = createEmptyHand; + +/** + * Draw `count` cards from the shoe. Defensive: an exhausted shoe (never + * expected with a 6-deck shoe and penetration reshuffles) is refilled with a + * fresh deterministic shuffle so the engine can never stall. + */ +export function drawCards( + source: Pick, + count: number, +): { cards: Card[]; shoe: readonly Card[] } { + let shoe = [...source.shoe]; + const cards: Card[] = []; + for (let i = 0; i < count; i += 1) { + if (shoe.length === 0) { + shoe = shuffleShoe( + createShoe(), + mulberry32(shoeSeed(source.seed, source.roundNumber + 4096 + cards.length)), + ); + } + const card = shoe[0]; + if (card) { + cards.push(card); + shoe = shoe.slice(1); + } + } + return { cards, shoe }; +} + +/** Whether the seat still owes a bet this round. */ +function needsBet(player: BlackjackPlayerState): boolean { + return !player.bankrupt && !player.hasBet && !player.sittingOut; +} + +/** Largest legal insurance stake for a seated player right now. */ +function insuranceCapFor(player: BlackjackPlayerState): number { + const mainBet = player.hands[0]?.bet ?? 0; + return Math.min(insuranceMaxFor(mainBet), player.bankroll); +} + +/** True when the dealer's upcard allows a blackjack peek (ace or ten-value). */ +function dealerPeekApplies(dealerCards: readonly Card[]): boolean { + const upcard = dealerCards[0]; + return upcard !== undefined && (upcard.rank === 14 || isTenValue(upcard.rank)); +} + +/** Deal two cards to every bettor plus the dealer, then open the round. */ +function dealInitialHands(state: BlackjackTableState): BlackjackTableState { + const bettors = state.players.filter((player) => player.hasBet && !player.sittingOut); + if (bettors.length === 0) { + // Nobody wagered anything: record an empty round and rest until the + // registry's next-round timer starts the following betting phase. + const empty: RoundOutcome = { + roundNumber: state.roundNumber, + dealerTotal: 0, + dealerBusted: false, + dealerHasBlackjack: false, + hands: [], + insurance: [], + }; + return { + ...state, + toActSeat: null, + activeHandIndex: 0, + phase: "round-complete", + roundResult: empty, + }; + } + + // Casino dealing order: first card to each bettor, dealer upcard, second + // card to each bettor, dealer hole card. + const { cards, shoe } = drawCards(state, bettors.length * 2 + 2); + let cursor = 0; + const firstCards = new Map(); + for (const player of bettors) { + const card = cards[cursor]; + cursor += 1; + if (card) firstCards.set(player.seatId, card); + } + const dealerUp = cards[cursor]; + cursor += 1; + const secondCards = new Map(); + for (const player of bettors) { + const card = cards[cursor]; + cursor += 1; + if (card) secondCards.set(player.seatId, card); + } + const dealerHole = cards[cursor]; + + if (!dealerUp || !dealerHole) { + // Defensive: a shoe this depleted should be impossible; rest the round. + return { ...state, toActSeat: null, phase: "round-complete" }; + } + + const players = state.players.map((player) => { + const first = firstCards.get(player.seatId); + const second = secondCards.get(player.seatId); + if (!first || !second || player.hands.length === 0) return player; + const dealt: BlackjackHand = { ...player.hands[0], cards: [first, second] }; + const status = isNaturalBlackjack([first, second]) ? ("blackjack" as const) : ("active" as const); + const hands = [dealt, ...player.hands.slice(1)].map((hand, index) => + index === 0 ? { ...hand, status } : hand, + ); + return { ...player, hands }; + }); + + const withCards: BlackjackTableState = { + ...state, + players, + shoe, + dealer: { cards: [dealerUp, dealerHole], holeRevealed: false }, + toActSeat: null, + }; + + // Auto-decline insurance for players who cannot fund even one chip of it. + const prepared = withCards.players.map((player) => { + if (!player.hasBet || player.insuranceAnswered) return player; + if (insuranceCapFor(player) > 0) return player; + return { ...player, insuranceAnswered: true }; + }); + const preparedState: BlackjackTableState = { ...withCards, players: prepared }; + + if (dealerUp.rank === 14) { + const offerTo = preparedState.players.find( + (player) => player.hasBet && !player.insuranceAnswered, + ); + if (offerTo) { + return { ...preparedState, phase: "insurance", toActSeat: offerTo.seatId }; + } + } + return afterDeals(preparedState); +} + +/** All insurance decisions are in: peek for dealer blackjack or play on. */ +function afterDeals(state: BlackjackTableState): BlackjackTableState { + if (dealerPeekApplies(state.dealer.cards) && isNaturalBlackjack(state.dealer.cards)) { + return settleRound(state); + } + return openPlayerPhase(state); +} + +/** First (seat, hand) pair that still owes an action, in playing order. */ +function findActionableHand( + state: BlackjackTableState, +): { seatId: BlackjackSeatId; handIndex: number } | null { + for (const player of state.players) { + if (player.bankrupt || player.sittingOut || !player.hasBet) continue; + for (let handIndex = 0; handIndex < player.hands.length; handIndex += 1) { + if (player.hands[handIndex]?.status === "active") { + return { seatId: player.seatId, handIndex }; + } + } + } + return null; +} + +function openPlayerPhase(state: BlackjackTableState): BlackjackTableState { + const actionable = findActionableHand(state); + if (!actionable) { + return runDealerAndSettle(state); + } + return { + ...state, + phase: "player", + toActSeat: actionable.seatId, + activeHandIndex: actionable.handIndex, + }; +} + +function advanceBetting(state: BlackjackTableState): BlackjackTableState { + const current = state.toActSeat; + if (current !== null) { + const actor = state.players[current]; + if (actor && needsBet(actor)) return state; + } + const startIndex = current === null ? 0 : current + 1; + for (let seat = startIndex; seat < state.players.length; seat += 1) { + const player = state.players[seat]; + if (player && needsBet(player)) { + return { ...state, toActSeat: seat }; + } + } + return dealInitialHands({ ...state, toActSeat: null }); +} + +function advanceInsurance(state: BlackjackTableState): BlackjackTableState { + const current = state.toActSeat; + if (current !== null) { + const actor = state.players[current]; + if (actor && actor.hasBet && !actor.insuranceAnswered && insuranceCapFor(actor) > 0) { + return state; + } + } + const startIndex = current === null ? 0 : current + 1; + for (let seat = startIndex; seat < state.players.length; seat += 1) { + const player = state.players[seat]; + if (player && player.hasBet && !player.insuranceAnswered && insuranceCapFor(player) > 0) { + return { ...state, toActSeat: seat }; + } + } + return afterDeals(state); +} + +function advancePlayer(state: BlackjackTableState): BlackjackTableState { + const actionable = findActionableHand(state); + if (!actionable) return runDealerAndSettle(state); + if (actionable.seatId === state.toActSeat && actionable.handIndex === state.activeHandIndex) { + return state; + } + return { + ...state, + toActSeat: actionable.seatId, + activeHandIndex: actionable.handIndex, + }; +} + +/** Dealer reveals the hole card, draws to 17 (S17), then the round settles. */ +function runDealerAndSettle(state: BlackjackTableState): BlackjackTableState { + // The dealer only draws while some played-out hand can still lose or win + // by comparison: all-bust tables and all-natural tables just reveal. + const anyStoodHands = state.players.some( + (player) => + player.hasBet && + !player.sittingOut && + player.hands.some((hand) => hand.status === "stood"), + ); + let next: BlackjackTableState = { + ...state, + dealer: { ...state.dealer, holeRevealed: true }, + }; + // Practical draw cap: a dealer under 17 never needs more than a handful of + // cards from a freshly shuffled multi-deck shoe. + let guard = 0; + while (anyStoodHands && dealerShouldHit(next.dealer.cards) && guard < 16) { + const { cards, shoe } = drawCards(next, 1); + const card = cards[0]; + if (!card) break; + next = { ...next, shoe, dealer: { ...next.dealer, cards: [...next.dealer.cards, card] } }; + guard += 1; + } + return settleRound(next); +} + +/** + * Resolve every live hand against the dealer, credit payouts, bump session + * counters, flag bankrupts, and attach the round result. Also used for the + * dealer-blackjack peek settle: the dealer's two cards already contain the + * natural, so no draw happens and non-natural hands simply lose. + */ +export function settleRound(state: BlackjackTableState): BlackjackTableState { + const dealerTotal = handValue(state.dealer.cards).total; + const dealerBusted = dealerTotal > 21; + const dealerHasBlackjack = isNaturalBlackjack(state.dealer.cards); + + const handResults: BlackjackRoundHandResult[] = []; + const creditBySeat = new Map(); + const netBySeat = new Map(); + const naturalsBySeat = new Map(); + const activeSeats = new Set(); + + let players = state.players.map((player) => { + if (!player.hasBet || player.sittingOut) return player; + activeSeats.add(player.seatId); + let stakeReturns = 0; + let winnings = 0; + let net = 0; + let naturals = 0; + const hands = player.hands.map((hand, handIndex) => { + const total = handValue(hand.cards).total; + let outcome: "win" | "lose" | "push" | "blackjack"; + let payout = 0; + if (hand.status === "blackjack") { + naturals += 1; + outcome = dealerHasBlackjack ? "push" : "blackjack"; + if (outcome === "blackjack") payout = blackjackWinAmount(hand.bet); + } else if (hand.status === "bust") { + outcome = "lose"; + } else if (dealerHasBlackjack) { + // Dealer blackjack (peek settle) beats everything except naturals. + outcome = "lose"; + } else if (dealerBusted || total > dealerTotal) { + outcome = "win"; + payout = hand.bet; + } else if (total < dealerTotal) { + outcome = "lose"; + } else { + outcome = "push"; + } + const netChange = outcome === "lose" ? -hand.bet : outcome === "push" ? 0 : payout; + net += netChange; + if (outcome !== "lose") stakeReturns += hand.bet; + if (outcome === "win" || outcome === "blackjack") winnings += payout; + handResults.push({ seatId: player.seatId, handIndex, total, outcome, payout, netChange }); + return { ...hand, outcome, netChange }; + }); + + let insuranceCredit = 0; + if (player.insuranceBet > 0) { + if (dealerHasBlackjack) { + insuranceCredit = player.insuranceBet + insuranceWinAmount(player.insuranceBet); + net += insuranceWinAmount(player.insuranceBet); + } else { + net -= player.insuranceBet; + } + } + + creditBySeat.set(player.seatId, stakeReturns + winnings + insuranceCredit); + netBySeat.set(player.seatId, net); + naturalsBySeat.set(player.seatId, naturals); + return { ...player, hands }; + }); + + players = players.map((player) => { + if (!activeSeats.has(player.seatId)) return player; + return { + ...player, + bankroll: player.bankroll + (creditBySeat.get(player.seatId) ?? 0), + roundsPlayed: player.roundsPlayed + 1, + roundsWon: player.roundsWon + ((netBySeat.get(player.seatId) ?? 0) > 0 ? 1 : 0), + blackjacksHit: player.blackjacksHit + (naturalsBySeat.get(player.seatId) ?? 0), + }; + }); + + // Below the table minimum there is no way to bet again: the seat is done. + players = players.map((player) => + !player.bankrupt && player.bankroll < state.config.minBet + ? { ...player, bankrupt: true } + : player, + ); + + const insuranceResults: RoundOutcome["insurance"] = players + .filter((player) => player.insuranceBet > 0) + .map((player) => ({ + seatId: player.seatId, + amount: player.insuranceBet, + won: dealerHasBlackjack, + })); + + const roundResult: RoundOutcome = { + roundNumber: state.roundNumber, + dealerTotal, + dealerBusted, + dealerHasBlackjack, + hands: handResults, + insurance: insuranceResults, + }; + return { + ...state, + players, + phase: "round-complete", + toActSeat: null, + activeHandIndex: 0, + roundResult, + }; +} + +/** + * Progress the state whenever it can progress on its own: + * - betting: advance to the next seat that owes a bet, or deal; + * - insurance: advance to the next unanswered bettor, or peek/play; + * - player: advance to the next actionable hand, or run the dealer out; + * - round-complete: resting until the registry starts the next round. + * Idempotent: calling it on a resting state returns that state unchanged. + */ +export function advanceState(state: BlackjackTableState): BlackjackTableState { + switch (state.phase) { + case "betting": + return advanceBetting(state); + case "insurance": + return advanceInsurance(state); + case "player": + return advancePlayer(state); + case "dealer": + return runDealerAndSettle(state); + case "round-complete": + return state; + default: { + const exhaustive: never = state.phase; + void exhaustive; + return state; + } + } +} + +/** + * Start the next betting round from a round-complete state. Seats whose + * array indexes appear in `dropSeatIds` are removed first, together with any + * bankrupt seats. The registry must apply the exact same index filter to its + * seat map so the seatId === array-index invariant holds. Reshuffles the + * shoe when it is below the penetration floor. + */ +export function startNextRound( + state: BlackjackTableState, + dropSeatIds: readonly number[] = [], +): BlackjackTableState { + const dropped = new Set(dropSeatIds); + const players = state.players + .filter((player, index) => !dropped.has(index) && !player.bankrupt) + .map((player, index) => ({ ...player, seatId: index })); + const nextRoundNumber = state.roundNumber + 1; + const shoe = needsShuffle(state.shoe) + ? createShuffledShoe(state.seed, nextRoundNumber) + : state.shoe; + const resetPlayers = players.map((player) => ({ + ...player, + hands: [], + insuranceBet: 0, + insuranceAnswered: false, + hasBet: false, + sittingOut: false, + })); + const nextToAct = resetPlayers.findIndex((player) => !player.bankrupt); + return { + ...state, + players: resetPlayers, + shoe, + dealer: { cards: [], holeRevealed: false }, + roundNumber: nextRoundNumber, + phase: "betting", + toActSeat: nextToAct >= 0 ? nextToAct : null, + activeHandIndex: 0, + roundResult: null, + }; +} + +export function createTableState( + players: readonly BlackjackPlayerInit[], + options: CreateTableOptions = {}, + rng: () => number = Math.random, +): BlackjackTableState { + if (players.length < MIN_PLAYERS || players.length > MAX_PLAYERS) { + throw new Error(`Blackjack tables support ${MIN_PLAYERS} to ${MAX_PLAYERS} players.`); + } + const startingBankroll = options.startingBankroll ?? STARTING_BANKROLL; + const minBet = options.minBet ?? MIN_BET; + const maxBet = options.maxBet ?? MAX_BET; + if (minBet <= 0 || maxBet < minBet) { + throw new Error("Bet limits must be positive and the maximum must be at least the minimum."); + } + const config: BlackjackTableConfig = { + startingBankroll, + minBet, + maxBet, + turnTimeoutMs: TURN_TIMEOUT_MS, + }; + const seed = Math.floor(rng() * 0x100000000) >>> 0; + const seated: BlackjackPlayerState[] = players.map((player, index) => ({ + seatId: index, + name: player.name, + bankroll: player.bankroll ?? startingBankroll, + hands: [], + insuranceBet: 0, + insuranceAnswered: false, + hasBet: false, + sittingOut: false, + bankrupt: false, + roundsPlayed: 0, + roundsWon: 0, + blackjacksHit: 0, + })); + return { + config, + seed, + roundNumber: 1, + players: seated, + shoe: createShuffledShoe(seed, 1), + dealer: { cards: [], holeRevealed: false }, + phase: "betting", + toActSeat: seated.length > 0 ? 0 : null, + activeHandIndex: 0, + roundResult: null, + }; +} diff --git a/src/lib/minigames/blackjack/index.ts b/src/lib/minigames/blackjack/index.ts new file mode 100644 index 0000000..8bd3dd6 --- /dev/null +++ b/src/lib/minigames/blackjack/index.ts @@ -0,0 +1,13 @@ +/** + * Public API of the pure blackjack engine. + * Everything exported here is client-safe: no Payload, Next.js, process, or + * database imports, and no module-level mutable state. + */ + +export * from "./constants"; +export * from "./types"; +export * from "./shoe"; +export * from "./rules"; +export * from "./engine"; +export * from "./actions"; +export * from "./views"; diff --git a/src/lib/minigames/blackjack/rules.ts b/src/lib/minigames/blackjack/rules.ts new file mode 100644 index 0000000..a93093a --- /dev/null +++ b/src/lib/minigames/blackjack/rules.ts @@ -0,0 +1,98 @@ +/** + * Blackjack rules helpers: hand values, natural detection, action legality, + * and dealer draw logic. Pure: every function takes data and returns data. + */ + +import { BLACKJACK_PAYOUT, DEALER_STAND_TOTAL, INSURANCE_PAYOUT, MAX_HANDS } from "./constants"; +import type { BlackjackHand, Card, Rank } from "./types"; + +export interface HandValue { + /** Best total at or under 21 when possible; otherwise the bust total. */ + readonly total: number; + /** True when an ace is still counted as 11 (a "soft" hand). */ + readonly soft: boolean; +} + +/** + * Best hand total: aces count 11 while the total stays at or under 21, then + * degrade to 1. A hand that exceeds 21 reports its hard (all aces = 1) total. + */ +export function handValue(cards: readonly Card[]): HandValue { + let total = 0; + let aces = 0; + for (const card of cards) { + if (card.rank === 14) { + aces += 1; + total += 11; + } else if (card.rank >= 10) { + total += 10; + } else { + total += card.rank; + } + } + while (total > 21 && aces > 0) { + total -= 10; + aces -= 1; + } + return { total, soft: aces > 0 }; +} + +/** A natural blackjack: exactly two cards totalling 21 (aces + a ten-value). */ +export function isNaturalBlackjack(cards: readonly Card[]): boolean { + return cards.length === 2 && handValue(cards).total === 21; +} + +/** Ten-value card for peek rules: 10, J, Q, K (rank 14 is the ace). */ +export function isTenValue(rank: Rank): boolean { + return rank >= 10 && rank <= 13; +} + +/** + * Dealer draw rule (S17): draw while under 17, stand on any 17 including + * soft 17. + */ +export function dealerShouldHit(cards: readonly Card[]): boolean { + return handValue(cards).total < DEALER_STAND_TOTAL; +} + +/** The hand is still awaiting an action. */ +export function isHandLive(hand: BlackjackHand): boolean { + return hand.status === "active"; +} + +/** Double is allowed on the first two cards of any live hand. */ +export function canDoubleHand(hand: BlackjackHand, bankroll: number): boolean { + return hand.status === "active" && hand.cards.length === 2 && bankroll >= hand.bet; +} + +/** + * Split is allowed on a live two-card hand of matching ranks while the player + * still has room for another hand and the bankroll covers the second stake. + * Aces from a previous split can never be re-split. + */ +export function canSplitHand( + hands: readonly BlackjackHand[], + hand: BlackjackHand, + bankroll: number, +): boolean { + if (hand.status !== "active" || hand.cards.length !== 2) return false; + if (hand.cards[0].rank !== hand.cards[1].rank) return false; + if (hand.splitAces) return false; + if (hands.length >= MAX_HANDS) return false; + return bankroll >= hand.bet; +} + +/** Winnings for a natural blackjack (stake returned separately). */ +export function blackjackWinAmount(bet: number): number { + return Math.floor(bet * BLACKJACK_PAYOUT); +} + +/** Winnings for a winning insurance wager (stake returned separately). */ +export function insuranceWinAmount(amount: number): number { + return amount * INSURANCE_PAYOUT; +} + +/** Largest legal insurance stake for a main bet (up to half the bet). */ +export function insuranceMaxFor(bet: number): number { + return Math.floor(bet / 2); +} diff --git a/src/lib/minigames/blackjack/shoe.ts b/src/lib/minigames/blackjack/shoe.ts new file mode 100644 index 0000000..eff1588 --- /dev/null +++ b/src/lib/minigames/blackjack/shoe.ts @@ -0,0 +1,75 @@ +/** + * Shoe creation and deterministic shuffling for the blackjack engine. + * Pure: randomness is always injected; no module-level state. + */ + +import { SHOE_DECKS, RESHUFFLE_PENETRATION } from "./constants"; +import type { Card, Rank, Suit } from "./types"; + +const SUITS: readonly Suit[] = ["s", "h", "d", "c"]; +const MIN_RANK: Rank = 2; +const MAX_RANK: Rank = 14; + +const CARDS_PER_DECK = 52; + +/** A fresh, ordered multi-deck shoe (order is deterministic before shuffling). */ +export function createShoe(decks: number = SHOE_DECKS): Card[] { + const shoe: Card[] = []; + for (let deckIndex = 0; deckIndex < decks; deckIndex += 1) { + for (const suit of SUITS) { + for (let rank = MIN_RANK; rank <= MAX_RANK; rank += 1) { + shoe.push({ rank: rank as Rank, suit }); + } + } + } + return shoe; +} + +/** + * Small, fast seeded PRNG (mulberry32). Used so a whole multi-round session + * can be replayed from a single integer seed stored in the table state. + */ +export function mulberry32(seed: number): () => number { + let a = seed >>> 0; + return () => { + a = (a + 0x6d2b79f5) >>> 0; + let t = a; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +/** Fisher-Yates shuffle; returns a new array, never mutates the input. */ +export function shuffleShoe(cards: readonly Card[], rng: () => number): Card[] { + const shoe = [...cards]; + for (let i = shoe.length - 1; i > 0; i -= 1) { + const j = Math.floor(rng() * (i + 1)); + const tmp = shoe[i]; + shoe[i] = shoe[j]; + shoe[j] = tmp; + } + return shoe; +} + +const GOLDEN_RATIO_32 = 0x9e3779b9; + +/** Per-round seed derived from the table's base seed and the round number. */ +export function shoeSeed(baseSeed: number, roundNumber: number): number { + return (baseSeed + Math.imul(roundNumber, GOLDEN_RATIO_32)) >>> 0; +} + +/** The exact shuffled shoe used for a given round of a given session seed. */ +export function createShuffledShoe(baseSeed: number, roundNumber: number): Card[] { + return shuffleShoe(createShoe(), mulberry32(shoeSeed(baseSeed, roundNumber))); +} + +/** Minimum card count the shoe must hold before the next reshuffle. */ +export function shoePenetrationFloor(): number { + return Math.floor(SHOE_DECKS * CARDS_PER_DECK * RESHUFFLE_PENETRATION); +} + +/** True when the shoe is low enough that the next round needs a reshuffle. */ +export function needsShuffle(shoe: readonly Card[]): boolean { + return shoe.length < shoePenetrationFloor(); +} diff --git a/src/lib/minigames/blackjack/stats.ts b/src/lib/minigames/blackjack/stats.ts new file mode 100644 index 0000000..b8f68d1 --- /dev/null +++ b/src/lib/minigames/blackjack/stats.ts @@ -0,0 +1,186 @@ +/** + * Blackjack XP + profile-stats awarding (Payload-dependent, server-only). + * The table registry calls awardBlackjackResult when a session ends (player + * leave, bankrupt removal, or table disposal). + */ + +import type { Payload } from "payload"; +import { broadcastToUser } from "@/lib/realtime/bus"; +import { emitGameEvent } from "@/utils/event-log/emit"; +import { EventTypes } from "@/utils/event-log/eventTypes"; +import { STARTING_BANKROLL } from "./constants"; + +export interface BlackjackSessionResult { + userId: number; + /** Rounds the player actually bet into during the session. */ + roundsPlayed: number; + /** Rounds that ended with the player ahead (net > 0). */ + roundsWon: number; + /** Natural blackjacks hit during the session. */ + blackjacksHit: number; + /** Bankroll the player finished the session with. */ + finalBankroll: number; +} + +export interface BlackjackStats { + gamesPlayed: number; + roundsWon: number; + blackjacksHit: number; + bestScore: number; + xpEarned: number; +} + +/** + * XP rework in progress: when false, finished sessions still record games + * played, rounds won, blackjacks hit, and best bankroll, but xpEarned is + * untouched and no XP push fires. Flip to true to resume profit-based XP + * payouts. + */ +export const BLACKJACK_XP_ENABLED = false; + +/** Hard cap for a single session's XP payout. */ +export const BLACKJACK_XP_CAP = 50; + +/** + * XP per finished session, scaled by profit over the starting bankroll: + * 5 XP for finishing a session, plus 10 XP per full 25% profit bracket + * (>=25% -> +10, >=50% -> +20, and so on). Tunable here only; the registry + * and UI must not hardcode XP values. + */ +export function xpForProfit(finalBankroll: number): number { + const profit = finalBankroll - STARTING_BANKROLL; + const base = 5; + const brackets = profit > 0 ? Math.floor(profit / (STARTING_BANKROLL * 0.25)) : 0; + return Math.min(base + brackets * 10, BLACKJACK_XP_CAP); +} + +/** + * Defensive reader for a profile's progression.minigames.blackjack stats. + * Accepts anything (profiles may predate the blackjack group) and returns a + * fully-populated BlackjackStats with zeros for missing values. + */ +export function readBlackjackStats(profile: unknown): BlackjackStats { + const blackjack = + (profile as { progression?: { minigames?: { blackjack?: Partial } } }) + ?.progression?.minigames?.blackjack ?? {}; + return { + gamesPlayed: blackjack.gamesPlayed ?? 0, + roundsWon: blackjack.roundsWon ?? 0, + blackjacksHit: blackjack.blackjacksHit ?? 0, + bestScore: blackjack.bestScore ?? 0, + xpEarned: blackjack.xpEarned ?? 0, + }; +} + +/** + * Pure stat transition for one finished session: bumps gamesPlayed, + * accumulates rounds won and blackjacks hit, keeps the best bankroll, and + * adds the session XP. + */ +export function computeBlackjackStatsDelta( + current: BlackjackStats, + result: BlackjackSessionResult, +): BlackjackStats { + return { + gamesPlayed: current.gamesPlayed + 1, + roundsWon: current.roundsWon + result.roundsWon, + blackjacksHit: current.blackjacksHit + result.blackjacksHit, + bestScore: Math.max(current.bestScore, result.finalBankroll), + xpEarned: current.xpEarned + xpForProfit(result.finalBankroll), + }; +} + +/** + * Awards XP and updates progression.minigames.blackjack for every player in + * a finished blackjack session. Emits the profile-update SSE push and a + * single game event log entry (EventTypes.MinigameBlackjack). Never throws: + * per-user failures are logged and skipped so one broken profile cannot + * block the table. + */ +export async function awardBlackjackResult( + payload: Payload, + results: BlackjackSessionResult[], +): Promise<{ xpByUser: Record }> { + if (results.length === 0) { + return { xpByUser: {} }; + } + + const xpByUser: Record = {}; + + for (const result of results) { + const xp = BLACKJACK_XP_ENABLED ? xpForProfit(result.finalBankroll) : 0; + xpByUser[result.userId] = xp; + + try { + const profileRes = await payload.find({ + collection: "profiles", + where: { user: { equals: result.userId } }, + limit: 1, + depth: 0, + overrideAccess: true, + }); + const profile = profileRes.docs[0]; + if (!profile) { + payload.logger.error(`Blackjack stats: no profile found for user ${result.userId}`); + continue; + } + + const current = readBlackjackStats(profile); + const delta = computeBlackjackStatsDelta(current, result); + const next: BlackjackStats = BLACKJACK_XP_ENABLED + ? delta + : { ...delta, xpEarned: current.xpEarned }; + const progression = (profile.progression ?? {}) as Record; + const minigames = (progression.minigames ?? {}) as Record; + // The final bankroll returns to the player's persistent chip pool: the + // table only moves chips between the pool and the felt, never destroys + // them. + const chipBank = + Math.max(0, ((progression.chipBank as number | undefined) ?? 0) + result.finalBankroll); + + await payload.update({ + collection: "profiles", + id: profile.id, + data: { + progression: { + ...progression, + chipBank, + minigames: { + ...minigames, + blackjack: next, + }, + }, + }, + overrideAccess: true, + depth: 0, + }); + + broadcastToUser("profile-update", result.userId, { + reason: "xp", + xpGained: xp, + chipBank, + profileId: profile.id, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + payload.logger.error( + `Blackjack stats: failed to award result for user ${result.userId}: ${message}`, + ); + } + } + + const first = results[0]; + await emitGameEvent(payload, { + type: EventTypes.MinigameBlackjack, + message: `finished a blackjack session (${first.roundsPlayed} round${first.roundsPlayed === 1 ? "" : "s"} played)`, + actor: first.userId, + targetCollection: "users", + targetId: first.userId, + data: { + results, + xpByUser, + }, + }); + + return { xpByUser }; +} diff --git a/src/lib/minigames/blackjack/tables.ts b/src/lib/minigames/blackjack/tables.ts new file mode 100644 index 0000000..de0340b --- /dev/null +++ b/src/lib/minigames/blackjack/tables.ts @@ -0,0 +1,896 @@ +/** + * Server-side multiplayer registry for the blackjack minigame. + * + * Holds live table records in a module-level Map (same pattern as + * src/lib/realtime/bus.ts). SINGLE-INSTANCE ONLY: table state lives in + * process memory, so this will not work across multiple server instances; + * the SSE pipeline it pushes through has the same limitation. + * + * The pure engine (./engine, ./actions) never touches timers or I/O. This + * wrapper owns turn timeouts, next-round pauses, lobby broadcasts, session + * stat awards, and the per-user snapshot pushes. Seat map invariant: + * seatMap is padded to MAX_PLAYERS slots with trailing nulls, so engine + * player i always corresponds to seatMap[i] (drops reindex both together). + */ + +import { getPayload } from "payload"; +import type { Payload } from "payload"; +import { broadcast, broadcastToUser } from "@/lib/realtime/bus"; +import { notifyUser } from "@/lib/notifications"; +import { creditChips, readChipBankState, spendChipBuyIn } from "@/lib/minigames/chips"; +import { applyAction, sitOut } from "./actions"; +import { + BUY_IN, + MAX_PLAYERS, + MAX_TABLE_NAME_LENGTH, + MIN_PLAYERS, + TURN_TIMEOUT_MS, +} from "./constants"; +import { createTableState, startNextRound } from "./engine"; +import { generateTableName } from "@/lib/minigames/tableNames"; +import { awardBlackjackResult, type BlackjackSessionResult } from "./stats"; +import type { + BlackjackActionKind, + BlackjackPlayerState, + BlackjackSeatId, + BlackjackTableState, + PrivatePlayerView, +} from "./types"; +import { buildPrivateView } from "./views"; + +/** Maximum number of live tables; table creation rejects beyond this. */ +export const MAX_TABLES = 20; + +/** Pause after a round settles before the next betting phase auto-starts. */ +export const NEXT_ROUND_DELAY_MS = 3000; + +/** A table with no client interaction for this long is swept as abandoned. */ +export const TABLE_EXPIRY_MS = 5 * 60_000; + +const TABLE_SWEEP_INTERVAL_MS = 30_000; + +export interface BlackjackTableSummary { + tableId: string; + name: string | null; + hostName: string; + seatedCount: number; + maxSeats: number; + inGame: boolean; + createdAt: string; +} + +export interface BlackjackSnapshot { + tableId: string; + name: string | null; + version: number; + /** ISO timestamp when the current turn auto-acts; null when no live turn. */ + turnDeadline: string | null; + /** ISO timestamp when the next betting round auto-starts after the pause. */ + nextRoundAt: string | null; + /** Full table state plus the viewer's options; null when not seated. */ + you: PrivatePlayerView | null; + /** seatId to userId (null = empty seat); lets the UI map names and hosts. */ + seats: (number | null)[]; + /** seatId to display name (null = empty or unknown); drives lobby seat labels. */ + names: (string | null)[]; + hostSeatId: number | null; + /** True when the viewer is watching without a seat (you is a spectator view). */ + spectator?: boolean; + /** Whether a game is currently running (useful for viewers without a seat). */ + inGame?: boolean; +} + +interface PendingJoin { + userId: number; + name: string; +} + +interface BlackjackTableRecord { + id: string; + name: string | null; + version: number; + hostUserId: number | null; + /** Engine seat order, padded to MAX_PLAYERS with trailing nulls. */ + seatMap: (number | null)[]; + state: BlackjackTableState | null; + inGame: boolean; + /** Users already awarded their session stats this game (idempotency). */ + awardedUsers: Set; + /** Users whose buy-in is currently staked on the table (pool deducted). */ + chipBoughtIn: Set; + /** Users whose chips have been settled back to their pool (idempotency). */ + chipSettled: Set; + /** Pending joiners who already paid the buy-in and wait to seat next round. */ + pendingChipPaid: Set; + turnTimer: ReturnType | null; + /** Version the pending turn timer was scheduled for (stale timers bail). */ + turnTimerVersion: number | null; + turnDeadlineAt: string | null; + nextRoundTimer: ReturnType | null; + nextRoundAt: string | null; + pendingJoins: PendingJoin[]; + awayUsers: Set; + /** Users who left mid-game; kept in seatMap so the UI can show "left". */ + leftUsers: Set; + /** Users watching the running game without a seat. */ + spectators: Set; + userNames: Map; + createdAt: string; + /** Last client interaction (poll, action, join/leave/start); drives expiry. */ + lastActivityAt: number; +} + +// Anchored on globalThis because Next.js dev gives each client session its +// own copy of server-action modules; a plain module-level Map would fork per +// session and tables would vanish between users. Single-process still. +type BlackjackTablesStore = Map; +interface BlackjackGlobalRegistry { + __blackjackTables?: BlackjackTablesStore; +} +const globalRegistry = globalThis as typeof globalThis & BlackjackGlobalRegistry; +const tables: BlackjackTablesStore = (globalRegistry.__blackjackTables ??= new Map()); + +function isSeatedAt(record: BlackjackTableRecord, userId: number): boolean { + return record.seatMap.includes(userId) && !record.leftUsers.has(userId); +} + +function seatedCountOf(record: BlackjackTableRecord): number { + let count = 0; + for (const id of record.seatMap) { + if (id !== null && !record.leftUsers.has(id)) count += 1; + } + return count; +} + +function clearTurnTimer(record: BlackjackTableRecord): void { + if (record.turnTimer) clearTimeout(record.turnTimer); + record.turnTimer = null; + record.turnTimerVersion = null; + record.turnDeadlineAt = null; +} + +/** + * Lobby mutations (join/leave) bump the version without touching game state; + * re-key the pending turn timer so it stays valid instead of going stale. + */ +function rekeyTurnTimer(record: BlackjackTableRecord): void { + if (record.turnTimer !== null) record.turnTimerVersion = record.version; +} + +function scheduleTurnTimer(record: BlackjackTableRecord): void { + clearTurnTimer(record); + const state = record.state; + if (!state || state.toActSeat === null) return; + if (state.phase !== "betting" && state.phase !== "insurance" && state.phase !== "player") { + return; + } + record.turnTimerVersion = record.version; + record.turnDeadlineAt = new Date(Date.now() + TURN_TIMEOUT_MS).toISOString(); + record.turnTimer = setTimeout(() => onTurnTimeout(record), TURN_TIMEOUT_MS); +} + +/** Turn timeout: sit out / decline insurance / stand for whoever is acting. */ +function onTurnTimeout(record: BlackjackTableRecord): void { + const scheduledVersion = record.turnTimerVersion; + record.turnTimer = null; + record.turnTimerVersion = null; + record.turnDeadlineAt = null; + if (scheduledVersion === null || scheduledVersion !== record.version) return; + const state = record.state; + if (!state || state.toActSeat === null) return; + const seat = state.toActSeat; + let result; + if (state.phase === "betting") { + result = sitOut(state, seat); + } else if (state.phase === "insurance") { + result = applyAction(state, seat, "decline-insurance"); + } else if (state.phase === "player") { + result = applyAction(state, seat, "stand"); + } else { + return; + } + if (!result.success) return; + record.state = result.state; + record.version += 1; + afterMutation(record); +} + +function scheduleNextRound(record: BlackjackTableRecord): void { + if (record.nextRoundTimer) clearTimeout(record.nextRoundTimer); + record.nextRoundAt = new Date(Date.now() + NEXT_ROUND_DELAY_MS).toISOString(); + record.nextRoundTimer = setTimeout(() => onNextRoundTimeout(record), NEXT_ROUND_DELAY_MS); +} + +function clearNextRoundTimer(record: BlackjackTableRecord): void { + if (record.nextRoundTimer) clearTimeout(record.nextRoundTimer); + record.nextRoundTimer = null; + record.nextRoundAt = null; +} + +function createFreshPlayer(seatId: BlackjackSeatId, name: string): BlackjackPlayerState { + return { + seatId, + name, + bankroll: BUY_IN, + hands: [], + insuranceBet: 0, + insuranceAnswered: false, + hasBet: false, + sittingOut: false, + bankrupt: false, + roundsPlayed: 0, + roundsWon: 0, + blackjacksHit: 0, + }; +} + +/** + * Next-round pause over: apply pending joins, drop leavers and bankrupts + * (with a session-stat award for each), and start the next betting phase. + */ +function onNextRoundTimeout(record: BlackjackTableRecord): void { + record.nextRoundTimer = null; + const current = record.state; + if (!current) return; + + const dropIndexes: number[] = []; + const dropUsers: number[] = []; + current.players.forEach((player, index) => { + const userId = record.seatMap[index]; + if (userId === null) return; + if (record.leftUsers.has(userId) || player.bankrupt) { + dropIndexes.push(index); + dropUsers.push(userId); + } + }); + void awardSessionResults(record, dropUsers); + + const survivors = record.seatMap.filter( + (id, index) => !dropIndexes.includes(index) && id !== null, + ); + record.seatMap = [...survivors]; + while (record.seatMap.length < MAX_PLAYERS) record.seatMap.push(null); + + let next = startNextRound(current, dropIndexes); + for (const pending of record.pendingJoins) { + // Paid pending joiners bring their buy-in into the engine now. + if (record.pendingChipPaid.has(pending.userId)) { + record.pendingChipPaid.delete(pending.userId); + record.chipBoughtIn.add(pending.userId); + } + const slot = record.seatMap.indexOf(null); + if (slot < 0) break; + record.seatMap[slot] = pending.userId; + next = { + ...next, + players: [...next.players, createFreshPlayer(next.players.length, pending.name)], + }; + } + record.pendingJoins = []; + // Fresh joiners have no recorded session yet; nothing else to clean up. + record.state = next; + record.version += 1; + afterMutation(record); +} + +/** In-app bell notification: solo player keeps the table to themselves. */ +async function notifySoloPlayer(record: BlackjackTableRecord): Promise { + const lastUserId = record.seatMap.find( + (id): id is number => id !== null && !record.leftUsers.has(id), + ); + if (lastUserId === undefined) return; + try { + const config = (await import("@payload-config")).default; + const payload = await getPayload({ config }); + await notifyUser(payload, { + userId: lastUserId, + type: "blackjack:everyone-left", + title: "Everyone left the table", + message: "The other players left. You can keep playing blackjack solo.", + link: "/minigames/blackjack", + }); + } catch { + // Payload unavailable in this runtime; nothing to notify. + } +} + +/** Fire-and-forget XP/stats award; never blocks the table. */ +async function awardSessionResults( + record: BlackjackTableRecord, + userIds: readonly number[], +): Promise { + const state = record.state; + if (!state) return; + const results: BlackjackSessionResult[] = []; + const refunds: { userId: number; amount: number }[] = []; + for (const userId of userIds) { + if (record.awardedUsers.has(userId) || record.chipSettled.has(userId)) continue; + const seatIndex = record.seatMap.indexOf(userId); + if (seatIndex < 0 || seatIndex >= state.players.length) continue; + const player = state.players[seatIndex]; + if (!player) continue; + if (player.roundsPlayed <= 0) { + // Never played a hand: the untouched buy-in returns to the pool. + record.chipSettled.add(userId); + refunds.push({ userId, amount: player.bankroll }); + continue; + } + record.awardedUsers.add(userId); + // Settled up front: awardBlackjackResult credits the final bankroll back + // to the player's pool inside its profile update. + record.chipSettled.add(userId); + results.push({ + userId, + roundsPlayed: player.roundsPlayed, + roundsWon: player.roundsWon, + blackjacksHit: player.blackjacksHit, + finalBankroll: player.bankroll, + }); + } + if (results.length === 0 && refunds.length === 0) return; + try { + const config = (await import("@payload-config")).default; + const payload = await getPayload({ config }); + try { + if (results.length > 0) { + await awardBlackjackResult(payload, results); + } + for (const refund of refunds) { + if (refund.amount <= 0) continue; + try { + await creditChips(payload, refund.userId, refund.amount); + } catch (error) { + payload.logger.error( + `[blackjack] Failed to settle chips for user ${refund.userId}: ${error}`, + ); + } + } + } catch (error) { + payload.logger.error( + `[blackjack] Failed to award session results for table ${record.id}: ${error}`, + ); + } + } catch { + // Payload unavailable in this runtime; skip awarding. + } +} + +/** Award every not-yet-awarded seated player (disposal / force expiry). */ +function awardAllRemaining(record: BlackjackTableRecord): void { + const userIds = record.seatMap.filter((id): id is number => id !== null); + void awardSessionResults(record, userIds); +} + +/** Shared post-mutation pipeline: timers, award triggers, push. */ +function afterMutation(record: BlackjackTableRecord): void { + const state = record.state; + if (state && state.phase === "round-complete") { + clearTurnTimer(record); + scheduleNextRound(record); + } else { + scheduleTurnTimer(record); + } + pushTable(record); +} + +function buildSnapshot(record: BlackjackTableRecord, userId: number): BlackjackSnapshot { + const seatIndex = record.seatMap.indexOf(userId); + const seated = seatIndex >= 0 && !record.leftUsers.has(userId); + const spectator = !seated && record.spectators.has(userId); + const state = record.state; + const inEngine = seated && state !== null && seatIndex < state.players.length; + let base = state && inEngine ? buildPrivateView(state, seatIndex) : null; + if (base === null && spectator && state) { + // Spectators receive the public view with no seat: the synthetic viewer + // seat (-1) never matches a real player, so all seats render as others. + base = buildPrivateView(state, -1); + } + const you = + base === null + ? null + : { + ...base, + players: base.players.map((player) => { + const playerUserId = record.seatMap[player.seatId]; + if (playerUserId === null || !record.leftUsers.has(playerUserId)) return player; + return { ...player, left: true }; + }), + }; + const hostSeat = record.hostUserId !== null ? record.seatMap.indexOf(record.hostUserId) : -1; + return { + tableId: record.id, + name: record.name, + version: record.version, + turnDeadline: record.turnDeadlineAt, + nextRoundAt: record.nextRoundAt, + you, + seats: [...record.seatMap], + names: record.seatMap.map((id) => (id === null ? null : (record.userNames.get(id) ?? null))), + hostSeatId: hostSeat >= 0 ? hostSeat : null, + spectator: spectator || undefined, + inGame: record.inGame || undefined, + }; +} + +/** Push a per-user snapshot to every seated user and spectator, then refresh the lobby. */ +function pushTable(record: BlackjackTableRecord): void { + for (const userId of record.seatMap) { + if (userId === null || record.leftUsers.has(userId)) continue; + broadcastToUser("blackjack-state", userId, { + tableId: record.id, + snapshot: buildSnapshot(record, userId), + }); + } + for (const spectatorId of record.spectators) { + broadcastToUser("blackjack-state", spectatorId, { + tableId: record.id, + snapshot: buildSnapshot(record, spectatorId), + }); + } + broadcast("blackjack-tables", { tables: listSummaries() }); +} + +/** + * Remove the table entirely. Session stats are awarded for every player who + * played at least one round and has not been awarded yet, so closed tabs and + * emptied tables still record their sessions. + */ +function disposeTable(record: BlackjackTableRecord): void { + clearTurnTimer(record); + clearNextRoundTimer(record); + awardAllRemaining(record); + void settleUnsettledChips(record); + tables.delete(record.id); + broadcast("blackjack-tables", { tables: listSummaries() }); +} + +/** Payload handle, or null when the config cannot load (tests, early boot). */ +async function getPayloadOrNull(): Promise { + try { + const config = (await import("@payload-config")).default; + return await getPayload({ config }); + } catch { + return null; + } +} + +/** + * Credit the current bankroll (or an unseated pending joiner's full buy-in) + * back to the pool for every bought-in player not yet settled. Marks each + * user settled synchronously so concurrent teardowns cannot double-credit. + */ +async function settleUnsettledChips(record: BlackjackTableRecord): Promise { + const candidates = [...record.chipBoughtIn, ...record.pendingChipPaid]; + const state = record.state; + const credits: { userId: number; amount: number }[] = []; + for (const userId of candidates) { + if (record.chipSettled.has(userId)) continue; + const seatIndex = record.seatMap.indexOf(userId); + const player = state && seatIndex >= 0 ? state.players[seatIndex] : undefined; + const amount = player ? player.bankroll : BUY_IN; + record.chipSettled.add(userId); + credits.push({ userId, amount }); + } + if (credits.length === 0) return; + const payload = await getPayloadOrNull(); + if (!payload) return; + for (const credit of credits) { + if (credit.amount <= 0) continue; + try { + await creditChips(payload, credit.userId, credit.amount); + } catch (error) { + payload.logger.error( + `[blackjack] Failed to settle chips for user ${credit.userId}: ${error}`, + ); + } + } +} + +/** In-app bell notification when a pending joiner cannot cover the buy-in. */ +async function notifyBuyInFailed(record: BlackjackTableRecord, userId: number): Promise { + try { + const payload = await getPayloadOrNull(); + if (!payload) return; + await notifyUser(payload, { + userId, + type: "blackjack:buy-in-failed", + title: "Not enough chips", + message: `You need ${BUY_IN} chips to join the next round. Play the other minigames to earn more chips.`, + link: "/minigames/blackjack", + }); + } catch { + // Notifications are best-effort. + } +} + +/** + * Buy-in-aware wrappers used by the server actions. The sync core above + * (startGame/joinTable/leaveTable) stays registry-pure for tests; pool + * accounting lives here where a Payload handle is available. + */ + +/** + * Start a session: settle any unsettled chips from a previous session on + * this record, collect the buy-in from every seated player (all-or-nothing), + * then start. Throws without moving chips when anyone cannot cover. + */ +export async function startGameWithBuyIn( + tableId: string, + userId: number, +): Promise { + const record = tables.get(tableId); + if (!record) throw new Error("Table not found."); + await settleUnsettledChips(record); + const seated = record.seatMap.filter( + (id): id is number => id !== null && !record.leftUsers.has(id), + ); + const payload = await getPayloadOrNull(); + const paid: number[] = []; + try { + if (payload) { + for (const seatedId of seated) { + const bank = await readChipBankState(payload, seatedId); + if (!bank) continue; // Untracked user (no profile): plays without pool accounting. + if (bank.chipBank < BUY_IN) { + const name = record.userNames.get(seatedId) ?? `Player ${seatedId}`; + throw new Error( + `${name} needs ${BUY_IN} chips to sit down and has ${bank.chipBank}. Play the other minigames to earn more chips.`, + ); + } + } + for (const seatedId of seated) { + const result = await spendChipBuyIn(payload, seatedId, BUY_IN); + if (result.settled) paid.push(seatedId); + } + } + const snapshot = startGame(tableId, userId); + record.chipBoughtIn = new Set(paid); + record.chipSettled = new Set(); + record.pendingChipPaid = new Set(); + return snapshot; + } catch (error) { + if (paid.length > 0 && payload) { + for (const paidId of paid) { + try { + await creditChips(payload, paidId, BUY_IN); + } catch { + // Best-effort refund. + } + } + } + throw error; + } +} + +/** + * Join a table: mid-game joins pay the buy-in up front (pendingChipPaid) so + * they can seat at the next round; the payment refunds if the join fails. + */ +export async function joinTableWithBuyIn( + tableId: string, + userId: number, + displayName: string, +): Promise { + const record = tables.get(tableId); + if (!record) throw new Error("Table not found."); + if (record.inGame && !record.pendingChipPaid.has(userId)) { + const payload = await getPayloadOrNull(); + if (payload) { + const bank = await readChipBankState(payload, userId); + if (bank && bank.chipBank < BUY_IN) { + throw new Error( + `You need ${BUY_IN} chips to join this table mid-game and have ${bank.chipBank}. Play the other minigames to earn more chips.`, + ); + } + if (bank) { + await spendChipBuyIn(payload, userId, BUY_IN); + record.pendingChipPaid.add(userId); + try { + return joinTable(tableId, userId, displayName); + } catch (error) { + record.pendingChipPaid.delete(userId); + try { + await creditChips(payload, userId, BUY_IN); + } catch { + // Best-effort refund. + } + throw error; + } + } + } + } + return joinTable(tableId, userId, displayName); +} + +/** Leave a table: pending joiners who paid but never seated get their buy-in back. */ +export async function leaveTableWithBuyIn(tableId: string, userId: number): Promise { + const record = tables.get(tableId); + if (!record) throw new Error("Table not found."); + const wasPending = record.pendingChipPaid.has(userId); + leaveTable(tableId, userId); + if (!wasPending) return; + record.pendingChipPaid.delete(userId); + const payload = await getPayloadOrNull(); + if (!payload) return; + try { + await creditChips(payload, userId, BUY_IN); + } catch (error) { + payload.logger.error( + `[blackjack] Failed to refund pending buy-in for user ${userId}: ${error}`, + ); + } +} + +export function forceExpireTable(tableId: string, payload: Payload): void { + const record = tables.get(tableId); + if (!record) throw new Error("Table not found."); + + for (const userId of record.seatMap) { + if (userId === null || record.leftUsers.has(userId)) continue; + + broadcastToUser("blackjack-terminated", userId, { + tableId: record.id, + reason: "force-ended", + }); + + try { + void notifyUser(payload, { + userId, + type: "blackjack:force-ended", + title: "Blackjack game ended", + message: "An administrator ended your blackjack game.", + link: "/minigames/blackjack", + }); + } catch (error) { + payload.logger.error( + `[blackjack] Failed to notify user ${userId} for table ${record.id}: ${error}`, + ); + } + } + + disposeTable(record); +} + +/** Tables with nobody seated (everyone left) are removed immediately. */ +function scheduleDisposalIfEmpty(record: BlackjackTableRecord): void { + if (seatedCountOf(record) > 0) return; + disposeTable(record); +} + +function promoteHost(record: BlackjackTableRecord): void { + const nextHost = record.seatMap.find((id) => id !== null && !record.leftUsers.has(id)); + record.hostUserId = nextHost ?? null; +} + +export function createTable(userId: number, displayName: string): string { + if (tables.size >= MAX_TABLES) { + throw new Error("The maximum number of blackjack tables exists. Try again later."); + } + const seatMap: (number | null)[] = [userId]; + while (seatMap.length < MAX_PLAYERS) seatMap.push(null); + const record: BlackjackTableRecord = { + id: crypto.randomUUID(), + name: generateTableName(), + version: 1, + hostUserId: userId, + seatMap, + state: null, + inGame: false, + awardedUsers: new Set(), + chipBoughtIn: new Set(), + chipSettled: new Set(), + pendingChipPaid: new Set(), + turnTimer: null, + turnTimerVersion: null, + turnDeadlineAt: null, + nextRoundTimer: null, + nextRoundAt: null, + pendingJoins: [], + awayUsers: new Set(), + leftUsers: new Set(), + spectators: new Set(), + userNames: new Map([[userId, displayName]]), + createdAt: new Date().toISOString(), + lastActivityAt: Date.now(), + }; + tables.set(record.id, record); + pushTable(record); + return record.id; +} + +export function listSummaries(): BlackjackTableSummary[] { + return [...tables.values()].map((record) => ({ + tableId: record.id, + name: record.name, + hostName: + record.hostUserId !== null + ? (record.userNames.get(record.hostUserId) ?? "Unknown") + : "Unknown", + seatedCount: seatedCountOf(record), + maxSeats: MAX_PLAYERS, + inGame: record.inGame, + createdAt: record.createdAt, + })); +} + +export function getSnapshotFor(tableId: string, userId: number): BlackjackSnapshot { + const record = tables.get(tableId); + if (!record) throw new Error("Table not found."); + record.lastActivityAt = Date.now(); + return buildSnapshot(record, userId); +} + +export function joinTable(tableId: string, userId: number, displayName: string): BlackjackSnapshot { + const record = tables.get(tableId); + if (!record) throw new Error("Table not found."); + if (record.leftUsers.has(userId)) { + throw new Error("You already left this table. You can rejoin when a new game starts."); + } + if (isSeatedAt(record, userId)) return buildSnapshot(record, userId); + for (const other of tables.values()) { + if (other.id !== record.id && isSeatedAt(other, userId)) { + throw new Error("You are already seated at another table. Leave it first."); + } + } + const slot = record.seatMap.indexOf(null); + if (slot < 0) throw new Error("The table is full."); + record.spectators.delete(userId); + record.seatMap[slot] = userId; + record.userNames.set(userId, displayName); + record.lastActivityAt = Date.now(); + if (record.inGame) record.pendingJoins.push({ userId, name: displayName }); + scheduleDisposalIfEmpty(record); + record.version += 1; + rekeyTurnTimer(record); + pushTable(record); + return buildSnapshot(record, userId); +} + +/** Register a spectator on a running game; they get the public view with no seat. */ +export function spectateTable(tableId: string, userId: number): BlackjackSnapshot { + const record = tables.get(tableId); + if (!record) throw new Error("Table not found."); + if (isSeatedAt(record, userId)) return buildSnapshot(record, userId); + if (!record.inGame || !record.state) { + throw new Error("No game is running at this table."); + } + record.spectators.add(userId); + record.lastActivityAt = Date.now(); + return buildSnapshot(record, userId); +} + +export function leaveTable(tableId: string, userId: number): void { + const record = tables.get(tableId); + if (!record) throw new Error("Table not found."); + if (record.spectators.delete(userId)) { + // Spectators just stop watching; no seat logic applies. + record.lastActivityAt = Date.now(); + return; + } + if (!isSeatedAt(record, userId)) throw new Error("You are not seated at this table."); + if (record.inGame) { + // Mid-game: keep the seat visible; the player is dropped (and their + // session awarded) when the next betting round starts. + record.leftUsers.add(userId); + record.awayUsers.add(userId); + } else { + const seatIndex = record.seatMap.indexOf(userId); + record.seatMap = record.seatMap.filter((_, index) => index !== seatIndex); + while (record.seatMap.length < MAX_PLAYERS) record.seatMap.push(null); + record.pendingJoins = record.pendingJoins.filter((pending) => pending.userId !== userId); + } + if (record.hostUserId === userId) promoteHost(record); + record.lastActivityAt = Date.now(); + record.version += 1; + rekeyTurnTimer(record); + scheduleDisposalIfEmpty(record); + if (record.inGame && seatedCountOf(record) === 1) { + void notifySoloPlayer(record); + } + pushTable(record); +} + +export function startGame(tableId: string, userId: number): BlackjackSnapshot { + const record = tables.get(tableId); + if (!record) throw new Error("Table not found."); + if (record.hostUserId !== userId) throw new Error("Only the host can start the game."); + const seated = record.seatMap.filter( + (id): id is number => id !== null && !record.leftUsers.has(id), + ); + if (seated.length < MIN_PLAYERS) { + throw new Error("At least one seated player is needed to start the game."); + } + // Compact the seat map for the new game: leavers are out, order preserved. + // Pad a COPY: aliasing `seated` here once spawned phantom "Player null" seats. + const padded: (number | null)[] = [...seated]; + while (padded.length < MAX_PLAYERS) padded.push(null); + record.seatMap = padded; + record.leftUsers.clear(); + record.awayUsers.clear(); + record.pendingJoins = []; + record.awardedUsers = new Set(); + record.nextRoundAt = null; + if (record.nextRoundTimer) { + clearTimeout(record.nextRoundTimer); + record.nextRoundTimer = null; + } + record.state = createTableState( + seated.map((id) => ({ name: record.userNames.get(id) ?? `Player ${id}` })), + { startingBankroll: BUY_IN }, + undefined, + ); + record.inGame = true; + record.lastActivityAt = Date.now(); + record.version += 1; + afterMutation(record); + return buildSnapshot(record, userId); +} + +export function renameTable(tableId: string, userId: number, rawName: string): BlackjackSnapshot { + const record = tables.get(tableId); + if (!record) throw new Error("Table not found."); + if (record.hostUserId !== userId) throw new Error("Only the host can rename the table."); + const name = rawName.trim().replace(/\s+/g, " ").slice(0, MAX_TABLE_NAME_LENGTH).trim(); + record.name = name.length > 0 ? name : null; + record.lastActivityAt = Date.now(); + record.version += 1; + pushTable(record); + return buildSnapshot(record, userId); +} + +export function applyBlackjackAction( + tableId: string, + userId: number, + kind: BlackjackActionKind, + amount?: number, +): BlackjackSnapshot { + const record = tables.get(tableId); + if (!record) throw new Error("Table not found."); + const state = record.state; + if (!state) throw new Error("The game has not started yet."); + if (record.leftUsers.has(userId)) throw new Error("You are not seated at this table."); + const seatIndex = record.seatMap.indexOf(userId); + if (seatIndex < 0) throw new Error("You are not seated at this table."); + if (seatIndex >= state.players.length) { + throw new Error("You will join the game at the next round."); + } + const result = applyAction(state, seatIndex, kind, amount); + if (!result.success) throw new Error(result.error); + record.state = result.state; + record.lastActivityAt = Date.now(); + record.version += 1; + afterMutation(record); + return buildSnapshot(record, userId); +} + +/** + * Sweeps tables whose last client interaction is older than TABLE_EXPIRY_MS: + * tabs closed without leaving, abandoned waiting rooms, gone games. Session + * stats are awarded on disposal. Safe to run from several module copies in + * dev because the store is shared via globalThis and disposal is idempotent. + */ +function sweepExpiredTables(): void { + const now = Date.now(); + let expired = false; + for (const record of tables.values()) { + if (now - record.lastActivityAt <= TABLE_EXPIRY_MS) continue; + disposeTable(record); + expired = true; + } + if (expired) broadcast("blackjack-tables", { tables: listSummaries() }); +} + +const sweepInterval = setInterval(sweepExpiredTables, TABLE_SWEEP_INTERVAL_MS); +sweepInterval.unref?.(); + +/** + * Test-only helper: dispose every table without awards so vitest files can + * start each run against a clean registry. + */ +export function clearTablesForTests(): void { + for (const record of tables.values()) { + clearTurnTimer(record); + clearNextRoundTimer(record); + } + tables.clear(); +} diff --git a/src/lib/minigames/blackjack/types.ts b/src/lib/minigames/blackjack/types.ts new file mode 100644 index 0000000..82074d3 --- /dev/null +++ b/src/lib/minigames/blackjack/types.ts @@ -0,0 +1,215 @@ +/** + * Shared types for the blackjack engine. + * + * Every type is plain JSON-serializable data: the whole table state can be + * stored in a registry Map, sent over SSE, and rebuilt on the client without + * any transformation. No Payload, Next.js, or process imports here. + */ + +export type Suit = "s" | "h" | "d" | "c"; + +/** 2 through 10, then 11 = jack, 12 = queen, 13 = king, 14 = ace. */ +export type Rank = 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14; + +export interface Card { + readonly rank: Rank; + readonly suit: Suit; +} + +/** Seat index into `BlackjackTableState.players`; seatId === array index. */ +export type BlackjackSeatId = number; + +export type BlackjackActionKind = + | "bet" + | "insurance" + | "decline-insurance" + | "hit" + | "stand" + | "double" + | "split"; + +/** + * Lifecycle of a round: + * - "betting": players place (or skip) their bets one at a time. + * - "insurance": dealer shows an ace; players with a bet take or decline. + * - "player": players act on their hands (hit/stand/double/split). + * - "dealer": transient; the dealer draws and the round settles immediately. + * - "round-complete": results attached; the registry pauses before the next + * betting round. Rounds repeat indefinitely until the session ends. + */ +export type BlackjackPhase = "betting" | "insurance" | "player" | "dealer" | "round-complete"; + +/** Per-hand lifecycle within a round. */ +export type BlackjackHandStatus = "active" | "stood" | "bust" | "blackjack"; + +export type BlackjackOutcome = "win" | "lose" | "push" | "blackjack"; + +export interface BlackjackTableConfig { + readonly startingBankroll: number; + readonly minBet: number; + readonly maxBet: number; + readonly turnTimeoutMs: number; +} + +export interface BlackjackHand { + readonly cards: readonly Card[]; + /** Stake deducted from the bankroll when the hand was created. */ + readonly bet: number; + readonly status: BlackjackHandStatus; + readonly doubled: boolean; + /** True when this hand was created by a split (never counts as natural). */ + readonly fromSplit: boolean; + /** True when this hand came from splitting aces (one card, auto-stood). */ + readonly splitAces: boolean; + /** Attached at settle; null while the round is live. */ + readonly outcome: BlackjackOutcome | null; + /** Winnings excluding the returned stake (negative on a loss). */ + readonly netChange: number | null; +} + +export interface BlackjackPlayerState { + readonly seatId: BlackjackSeatId; + readonly name: string; + /** Chips not currently wagered; bets are deducted immediately. */ + readonly bankroll: number; + readonly hands: readonly BlackjackHand[]; + /** Insurance stake for the current round (deducted when taken). */ + readonly insuranceBet: number; + /** Whether the player answered the insurance offer this round. */ + readonly insuranceAnswered: boolean; + /** True once the player bet this round. */ + readonly hasBet: boolean; + /** True when the player skipped (or timed out of) the current round. */ + readonly sittingOut: boolean; + /** Bankroll fell below the table minimum; removed at the next round start. */ + readonly bankrupt: boolean; + /** Session counters, accumulated across rounds for the stats award. */ + readonly roundsPlayed: number; + readonly roundsWon: number; + readonly blackjacksHit: number; +} + +export interface DealerState { + readonly cards: readonly Card[]; + /** True once the hole card has been revealed (peek or dealer play). */ + readonly holeRevealed: boolean; +} + +export interface BlackjackRoundHandResult { + readonly seatId: BlackjackSeatId; + readonly handIndex: number; + readonly total: number; + readonly outcome: BlackjackOutcome; + /** Winnings excluding the returned stake (0 on losses and pushes). */ + readonly payout: number; + /** payout minus bet on wins; negative bet on losses; 0 on pushes. */ + readonly netChange: number; +} + +export interface BlackjackRoundInsuranceResult { + readonly seatId: BlackjackSeatId; + readonly amount: number; + readonly won: boolean; +} + +export interface RoundOutcome { + readonly roundNumber: number; + readonly dealerTotal: number; + readonly dealerBusted: boolean; + readonly dealerHasBlackjack: boolean; + readonly hands: readonly BlackjackRoundHandResult[]; + readonly insurance: readonly BlackjackRoundInsuranceResult[]; +} + +export interface BlackjackTableState { + readonly config: BlackjackTableConfig; + /** + * Base seed for shoe shuffling. Each round derives its own seed from this + * (see shoe.ts), so a session is fully reproducible from the seed alone. + */ + readonly seed: number; + /** 1-based round counter; 1 is set by createTableState. */ + readonly roundNumber: number; + readonly players: readonly BlackjackPlayerState[]; + readonly shoe: readonly Card[]; + readonly dealer: DealerState; + readonly phase: BlackjackPhase; + /** Seat that must act (bet, answer insurance, or play a hand). */ + readonly toActSeat: BlackjackSeatId | null; + /** Index of the acting player's hand currently awaiting an action. */ + readonly activeHandIndex: number; + readonly roundResult: RoundOutcome | null; +} + +/** What the acting player is allowed to do right now. */ +export interface BlackjackLegalActions { + readonly seatId: BlackjackSeatId; + readonly phase: BlackjackPhase; + readonly activeHandIndex: number; + readonly canBet: boolean; + readonly minBet: number; + /** Table max capped by the bankroll (what can actually be wagered). */ + readonly maxBet: number; + readonly canHit: boolean; + readonly canStand: boolean; + readonly canDouble: boolean; + readonly canSplit: boolean; + readonly canInsure: boolean; + readonly insuranceMax: number; +} + +export interface PublicHandView { + readonly cards: readonly Card[]; + readonly bet: number; + readonly status: BlackjackHandStatus; + readonly doubled: boolean; + readonly fromSplit: boolean; + readonly splitAces: boolean; + readonly total: number; + readonly outcome: BlackjackOutcome | null; + readonly netChange: number | null; +} + +export interface PublicPlayerView { + readonly seatId: BlackjackSeatId; + readonly name: string; + readonly bankroll: number; + readonly hands: readonly PublicHandView[]; + readonly insuranceBet: number; + readonly insuranceAnswered: boolean; + readonly hasBet: boolean; + readonly sittingOut: boolean; + readonly bankrupt: boolean; + /** True from the moment the user leaves mid-round (registry sets this). */ + readonly left: boolean; + readonly roundsPlayed: number; + readonly roundsWon: number; + readonly blackjacksHit: number; +} + +export interface PublicDealerView { + /** Only the upcard while the hole card is hidden. */ + readonly cards: readonly Card[]; + readonly holeHidden: boolean; + /** Visible total (null while the hole card is hidden). */ + readonly total: number | null; + readonly hasBlackjack: boolean | null; +} + +export interface PublicTableView { + readonly players: readonly PublicPlayerView[]; + readonly dealer: PublicDealerView; + readonly phase: BlackjackPhase; + readonly roundNumber: number; + readonly activeSeat: BlackjackSeatId | null; + readonly activeHandIndex: number; + readonly config: BlackjackTableConfig; + readonly roundResult: RoundOutcome | null; +} + +export interface PrivatePlayerView extends PublicTableView { + readonly viewerSeat: BlackjackSeatId; + readonly yourHands: readonly PublicHandView[]; + readonly legalActions: BlackjackLegalActions; + readonly yourTurn: boolean; +} diff --git a/src/lib/minigames/blackjack/views.ts b/src/lib/minigames/blackjack/views.ts new file mode 100644 index 0000000..982c02c --- /dev/null +++ b/src/lib/minigames/blackjack/views.ts @@ -0,0 +1,157 @@ +/** + * Projection helpers that turn the full table state into safe views. + * + * The full state (including the shoe order and the dealer hole card) must + * never leave the server; clients only ever receive these views. Player + * cards are all face up at a blackjack table, so the only hidden card is the + * dealer's hole card until it is revealed. Spectators use the synthetic + * viewer seat -1, which never matches a real player. + */ + +import { canDoubleHand, canSplitHand, handValue, insuranceMaxFor } from "./rules"; +import type { + BlackjackLegalActions, + BlackjackSeatId, + BlackjackTableState, + PrivatePlayerView, + PublicDealerView, + PublicHandView, + PublicPlayerView, + PublicTableView, +} from "./types"; + +/** Largest legal insurance stake right now: capped at half the bet and the bankroll. */ +function insuranceCapFor(mainBet: number, bankroll: number): number { + return Math.min(insuranceMaxFor(mainBet), bankroll); +} + +/** What the acting player is allowed to do right now. */ +export function getLegalActions( + state: BlackjackTableState, + seatId: BlackjackSeatId, +): BlackjackLegalActions { + const player = state.players[seatId]; + const isTurn = state.toActSeat === seatId; + const phase = state.phase; + const hand = player?.hands[state.activeHandIndex] ?? null; + + const canBet = + phase === "betting" && + isTurn && + player !== undefined && + !player.bankrupt && + !player.hasBet && + !player.sittingOut; + const maxBet = Math.min(state.config.maxBet, player?.bankroll ?? 0); + const insuranceCap = insuranceCapFor(player?.hands[0]?.bet ?? 0, player?.bankroll ?? 0); + const canInsure = + phase === "insurance" && + isTurn && + player !== undefined && + player.hasBet && + !player.insuranceAnswered && + insuranceCap > 0; + + return { + seatId, + phase, + activeHandIndex: state.activeHandIndex, + canBet, + minBet: state.config.minBet, + maxBet, + canHit: phase === "player" && isTurn && hand?.status === "active", + canStand: phase === "player" && isTurn && hand?.status === "active", + canDouble: + phase === "player" && isTurn && hand !== null && canDoubleHand(hand, player?.bankroll ?? 0), + canSplit: + phase === "player" && + isTurn && + hand !== null && + canSplitHand(player?.hands ?? [], hand, player?.bankroll ?? 0), + canInsure, + insuranceMax: canInsure ? insuranceCap : 0, + }; +} + +function projectHand(hand: BlackjackTableState["players"][number]["hands"][number]): PublicHandView { + return { + cards: hand.cards, + bet: hand.bet, + status: hand.status, + doubled: hand.doubled, + fromSplit: hand.fromSplit, + splitAces: hand.splitAces, + total: handValue(hand.cards).total, + outcome: hand.outcome, + netChange: hand.netChange, + }; +} + +function projectPlayer( + player: BlackjackTableState["players"][number], +): PublicPlayerView { + return { + seatId: player.seatId, + name: player.name, + bankroll: player.bankroll, + hands: player.hands.map(projectHand), + insuranceBet: player.insuranceBet, + insuranceAnswered: player.insuranceAnswered, + hasBet: player.hasBet, + sittingOut: player.sittingOut, + bankrupt: player.bankrupt, + // The engine has no leave concept; the registry layer overrides this + // for users recorded in leftUsers (see buildSnapshot in tables.ts). + left: false, + roundsPlayed: player.roundsPlayed, + roundsWon: player.roundsWon, + blackjacksHit: player.blackjacksHit, + }; +} + +function projectDealer(dealer: BlackjackTableState["dealer"]): PublicDealerView { + const cards = dealer.holeRevealed ? dealer.cards : dealer.cards.slice(0, 1); + return { + cards, + holeHidden: !dealer.holeRevealed, + total: dealer.holeRevealed ? handValue(dealer.cards).total : null, + hasBlackjack: dealer.holeRevealed ? handValue(dealer.cards).total === 21 && dealer.cards.length === 2 : null, + }; +} + +/** + * Public view of the table. Player cards are always face up; the dealer's + * hole card stays hidden until reveal. The viewer seat exists only so the + * registry can mark the caller's own flags in buildSnapshot. + */ +export function buildPublicView( + state: BlackjackTableState, + _viewerSeat: BlackjackSeatId | null, +): PublicTableView { + return { + players: state.players.map(projectPlayer), + dealer: projectDealer(state.dealer), + phase: state.phase, + roundNumber: state.roundNumber, + activeSeat: state.toActSeat, + activeHandIndex: state.activeHandIndex, + config: state.config, + roundResult: state.roundResult, + }; +} + +/** Everything one seated player is allowed to know, including their options. */ +export function buildPrivateView( + state: BlackjackTableState, + seatId: BlackjackSeatId, +): PrivatePlayerView { + const publicView = buildPublicView(state, seatId); + const player = state.players[seatId]; + return { + ...publicView, + viewerSeat: seatId, + yourHands: player?.hands.map(projectHand) ?? [], + legalActions: getLegalActions(state, seatId), + yourTurn: state.toActSeat === seatId && state.phase !== "round-complete", + }; +} diff --git a/src/migrations/20260912_071740_add_blackjack_minigame_stats.json b/src/migrations/20260912_071740_add_blackjack_minigame_stats.json new file mode 100644 index 0000000..917b32a --- /dev/null +++ b/src/migrations/20260912_071740_add_blackjack_minigame_stats.json @@ -0,0 +1,26786 @@ +{ + "version": "7", + "dialect": "postgresql", + "tables": { + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "alt": { + "name": "alt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "media_updated_at_idx": { + "name": "media_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_created_at_idx": { + "name": "media_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_filename_idx": { + "name": "media_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_hard_resources": { + "name": "game_hard_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "game_hard_resources_name_idx": { + "name": "game_hard_resources_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_hard_resources_updated_at_idx": { + "name": "game_hard_resources_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_hard_resources_created_at_idx": { + "name": "game_hard_resources_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_structures_labor_requirements": { + "name": "game_structures_labor_requirements", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "classification_id": { + "name": "classification_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "required_headcount": { + "name": "required_headcount", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_headcount": { + "name": "max_headcount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "enforced_salary": { + "name": "enforced_salary", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "game_structures_labor_requirements_order_idx": { + "name": "game_structures_labor_requirements_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_structures_labor_requirements_parent_id_idx": { + "name": "game_structures_labor_requirements_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_structures_labor_requirements_classification_idx": { + "name": "game_structures_labor_requirements_classification_idx", + "columns": [ + { + "expression": "classification_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_structures_labor_requirements_classification_id_labor_classifications_id_fk": { + "name": "game_structures_labor_requirements_classification_id_labor_classifications_id_fk", + "tableFrom": "game_structures_labor_requirements", + "tableTo": "labor_classifications", + "columnsFrom": [ + "classification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "game_structures_labor_requirements_parent_id_fk": { + "name": "game_structures_labor_requirements_parent_id_fk", + "tableFrom": "game_structures_labor_requirements", + "tableTo": "game_structures", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_structures_staff_positions": { + "name": "game_structures_staff_positions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "position_type": { + "name": "position_type", + "type": "enum_game_structures_staff_positions_position_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "max": { + "name": "max", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "enforced_salary": { + "name": "enforced_salary", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "game_structures_staff_positions_order_idx": { + "name": "game_structures_staff_positions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_structures_staff_positions_parent_id_idx": { + "name": "game_structures_staff_positions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_structures_staff_positions_parent_id_fk": { + "name": "game_structures_staff_positions_parent_id_fk", + "tableFrom": "game_structures_staff_positions", + "tableTo": "game_structures", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_structures_stored_resources": { + "name": "game_structures_stored_resources", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "grid_x": { + "name": "grid_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "grid_y": { + "name": "grid_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "rotated": { + "name": "rotated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "game_structures_stored_resources_order_idx": { + "name": "game_structures_stored_resources_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_structures_stored_resources_parent_id_idx": { + "name": "game_structures_stored_resources_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_structures_stored_resources_resource_idx": { + "name": "game_structures_stored_resources_resource_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_structures_stored_resources_resource_id_resources_id_fk": { + "name": "game_structures_stored_resources_resource_id_resources_id_fk", + "tableFrom": "game_structures_stored_resources", + "tableTo": "resources", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "game_structures_stored_resources_parent_id_fk": { + "name": "game_structures_stored_resources_parent_id_fk", + "tableFrom": "game_structures_stored_resources", + "tableTo": "game_structures", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_structures_void_storage": { + "name": "game_structures_void_storage", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "grid_x": { + "name": "grid_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "grid_y": { + "name": "grid_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "rotated": { + "name": "rotated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "game_structures_void_storage_order_idx": { + "name": "game_structures_void_storage_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_structures_void_storage_parent_id_idx": { + "name": "game_structures_void_storage_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_structures_void_storage_resource_idx": { + "name": "game_structures_void_storage_resource_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_structures_void_storage_resource_id_resources_id_fk": { + "name": "game_structures_void_storage_resource_id_resources_id_fk", + "tableFrom": "game_structures_void_storage", + "tableTo": "resources", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "game_structures_void_storage_parent_id_fk": { + "name": "game_structures_void_storage_parent_id_fk", + "tableFrom": "game_structures_void_storage", + "tableTo": "game_structures", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_structures": { + "name": "game_structures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "faction_id": { + "name": "faction_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "maintenance_cost": { + "name": "maintenance_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "coordinates": { + "name": "coordinates", + "type": "geometry(Point)", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "game_structures_type_idx": { + "name": "game_structures_type_idx", + "columns": [ + { + "expression": "type_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_structures_faction_idx": { + "name": "game_structures_faction_idx", + "columns": [ + { + "expression": "faction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_structures_updated_at_idx": { + "name": "game_structures_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_structures_created_at_idx": { + "name": "game_structures_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_structures_type_id_structures_id_fk": { + "name": "game_structures_type_id_structures_id_fk", + "tableFrom": "game_structures", + "tableTo": "structures", + "columnsFrom": [ + "type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "game_structures_faction_id_factions_id_fk": { + "name": "game_structures_faction_id_factions_id_fk", + "tableFrom": "game_structures", + "tableTo": "factions", + "columnsFrom": [ + "faction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_vehicles_stored_resources": { + "name": "game_vehicles_stored_resources", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "grid_x": { + "name": "grid_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "grid_y": { + "name": "grid_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "rotated": { + "name": "rotated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "game_vehicles_stored_resources_order_idx": { + "name": "game_vehicles_stored_resources_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_vehicles_stored_resources_parent_id_idx": { + "name": "game_vehicles_stored_resources_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_vehicles_stored_resources_resource_idx": { + "name": "game_vehicles_stored_resources_resource_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_vehicles_stored_resources_resource_id_resources_id_fk": { + "name": "game_vehicles_stored_resources_resource_id_resources_id_fk", + "tableFrom": "game_vehicles_stored_resources", + "tableTo": "resources", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "game_vehicles_stored_resources_parent_id_fk": { + "name": "game_vehicles_stored_resources_parent_id_fk", + "tableFrom": "game_vehicles_stored_resources", + "tableTo": "game_vehicles", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_vehicles_void_storage": { + "name": "game_vehicles_void_storage", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "grid_x": { + "name": "grid_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "grid_y": { + "name": "grid_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "rotated": { + "name": "rotated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "game_vehicles_void_storage_order_idx": { + "name": "game_vehicles_void_storage_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_vehicles_void_storage_parent_id_idx": { + "name": "game_vehicles_void_storage_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_vehicles_void_storage_resource_idx": { + "name": "game_vehicles_void_storage_resource_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_vehicles_void_storage_resource_id_resources_id_fk": { + "name": "game_vehicles_void_storage_resource_id_resources_id_fk", + "tableFrom": "game_vehicles_void_storage", + "tableTo": "resources", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "game_vehicles_void_storage_parent_id_fk": { + "name": "game_vehicles_void_storage_parent_id_fk", + "tableFrom": "game_vehicles_void_storage", + "tableTo": "game_vehicles", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_vehicles": { + "name": "game_vehicles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "type_id": { + "name": "type_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deployed_at_id": { + "name": "deployed_at_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "faction_id": { + "name": "faction_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_game_vehicles_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "current_fuel": { + "name": "current_fuel", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "current_health": { + "name": "current_health", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "game_vehicles_type_idx": { + "name": "game_vehicles_type_idx", + "columns": [ + { + "expression": "type_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_vehicles_deployed_at_idx": { + "name": "game_vehicles_deployed_at_idx", + "columns": [ + { + "expression": "deployed_at_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_vehicles_faction_idx": { + "name": "game_vehicles_faction_idx", + "columns": [ + { + "expression": "faction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_vehicles_updated_at_idx": { + "name": "game_vehicles_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_vehicles_created_at_idx": { + "name": "game_vehicles_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_vehicles_type_id_vehicles_id_fk": { + "name": "game_vehicles_type_id_vehicles_id_fk", + "tableFrom": "game_vehicles", + "tableTo": "vehicles", + "columnsFrom": [ + "type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "game_vehicles_deployed_at_id_game_structures_id_fk": { + "name": "game_vehicles_deployed_at_id_game_structures_id_fk", + "tableFrom": "game_vehicles", + "tableTo": "game_structures", + "columnsFrom": [ + "deployed_at_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "game_vehicles_faction_id_factions_id_fk": { + "name": "game_vehicles_faction_id_factions_id_fk", + "tableFrom": "game_vehicles", + "tableTo": "factions", + "columnsFrom": [ + "faction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_npcs": { + "name": "game_npcs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "occupation": { + "name": "occupation", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "location_id": { + "name": "location_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "faction_id": { + "name": "faction_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "is_generated": { + "name": "is_generated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_in_game": { + "name": "is_in_game", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "vendor_enabled": { + "name": "vendor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vendor_price_modifier": { + "name": "vendor_price_modifier", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "game_npcs_location_idx": { + "name": "game_npcs_location_idx", + "columns": [ + { + "expression": "location_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_npcs_faction_idx": { + "name": "game_npcs_faction_idx", + "columns": [ + { + "expression": "faction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_npcs_updated_at_idx": { + "name": "game_npcs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_npcs_created_at_idx": { + "name": "game_npcs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_npcs_location_id_game_structures_id_fk": { + "name": "game_npcs_location_id_game_structures_id_fk", + "tableFrom": "game_npcs", + "tableTo": "game_structures", + "columnsFrom": [ + "location_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "game_npcs_faction_id_factions_id_fk": { + "name": "game_npcs_faction_id_factions_id_fk", + "tableFrom": "game_npcs", + "tableTo": "factions", + "columnsFrom": [ + "faction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_npcs_rels": { + "name": "game_npcs_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "assets_id": { + "name": "assets_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "game_npcs_rels_order_idx": { + "name": "game_npcs_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_npcs_rels_parent_idx": { + "name": "game_npcs_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_npcs_rels_path_idx": { + "name": "game_npcs_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_npcs_rels_assets_id_idx": { + "name": "game_npcs_rels_assets_id_idx", + "columns": [ + { + "expression": "assets_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_npcs_rels_parent_fk": { + "name": "game_npcs_rels_parent_fk", + "tableFrom": "game_npcs_rels", + "tableTo": "game_npcs", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "game_npcs_rels_assets_fk": { + "name": "game_npcs_rels_assets_fk", + "tableFrom": "game_npcs_rels", + "tableTo": "assets", + "columnsFrom": [ + "assets_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.npc_staffing": { + "name": "npc_staffing", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "npc_id": { + "name": "npc_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "structure_id": { + "name": "structure_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "position_type": { + "name": "position_type", + "type": "enum_npc_staffing_position_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'labor'" + }, + "position_title": { + "name": "position_title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "headcount": { + "name": "headcount", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "labor_classification_id": { + "name": "labor_classification_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "salary": { + "name": "salary", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "enum_npc_staffing_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'active'" + }, + "hired_at": { + "name": "hired_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "terminated_at": { + "name": "terminated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "npc_staffing_npc_idx": { + "name": "npc_staffing_npc_idx", + "columns": [ + { + "expression": "npc_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "npc_staffing_structure_idx": { + "name": "npc_staffing_structure_idx", + "columns": [ + { + "expression": "structure_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "npc_staffing_labor_classification_idx": { + "name": "npc_staffing_labor_classification_idx", + "columns": [ + { + "expression": "labor_classification_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "npc_staffing_updated_at_idx": { + "name": "npc_staffing_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "npc_staffing_created_at_idx": { + "name": "npc_staffing_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "npc_staffing_npc_id_game_npcs_id_fk": { + "name": "npc_staffing_npc_id_game_npcs_id_fk", + "tableFrom": "npc_staffing", + "tableTo": "game_npcs", + "columnsFrom": [ + "npc_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "npc_staffing_structure_id_game_structures_id_fk": { + "name": "npc_staffing_structure_id_game_structures_id_fk", + "tableFrom": "npc_staffing", + "tableTo": "game_structures", + "columnsFrom": [ + "structure_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "npc_staffing_labor_classification_id_labor_classifications_id_fk": { + "name": "npc_staffing_labor_classification_id_labor_classifications_id_fk", + "tableFrom": "npc_staffing", + "tableTo": "labor_classifications", + "columnsFrom": [ + "labor_classification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labor_classifications": { + "name": "labor_classifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "default_unit_salary": { + "name": "default_unit_salary", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "available_headcount": { + "name": "available_headcount", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "labor_classifications_name_idx": { + "name": "labor_classifications_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labor_classifications_key_idx": { + "name": "labor_classifications_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labor_classifications_updated_at_idx": { + "name": "labor_classifications_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labor_classifications_created_at_idx": { + "name": "labor_classifications_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_event_logs": { + "name": "game_event_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "system": { + "name": "system", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "structure_id": { + "name": "structure_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "target_collection": { + "name": "target_collection", + "type": "enum_game_event_logs_target_collection", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "game_event_logs_type_idx": { + "name": "game_event_logs_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_event_logs_actor_idx": { + "name": "game_event_logs_actor_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_event_logs_structure_idx": { + "name": "game_event_logs_structure_idx", + "columns": [ + { + "expression": "structure_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_event_logs_updated_at_idx": { + "name": "game_event_logs_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_event_logs_created_at_idx": { + "name": "game_event_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_event_logs_actor_id_users_id_fk": { + "name": "game_event_logs_actor_id_users_id_fk", + "tableFrom": "game_event_logs", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "game_event_logs_structure_id_game_structures_id_fk": { + "name": "game_event_logs_structure_id_game_structures_id_fk", + "tableFrom": "game_event_logs", + "tableTo": "game_structures", + "columnsFrom": [ + "structure_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users_roles": { + "name": "users_roles", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_users_roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "users_roles_order_idx": { + "name": "users_roles_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_roles_parent_idx": { + "name": "users_roles_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_roles_parent_fk": { + "name": "users_roles_parent_fk", + "tableFrom": "users_roles", + "tableTo": "users", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users_preferences_notifications_muted_types": { + "name": "users_preferences_notifications_muted_types", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_users_preferences_notifications_muted_types", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "users_preferences_notifications_muted_types_order_idx": { + "name": "users_preferences_notifications_muted_types_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_preferences_notifications_muted_types_parent_idx": { + "name": "users_preferences_notifications_muted_types_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_preferences_notifications_muted_types_parent_fk": { + "name": "users_preferences_notifications_muted_types_parent_fk", + "tableFrom": "users_preferences_notifications_muted_types", + "tableTo": "users", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users_preferences_discord_muted_types": { + "name": "users_preferences_discord_muted_types", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_users_preferences_discord_muted_types", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "users_preferences_discord_muted_types_order_idx": { + "name": "users_preferences_discord_muted_types_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_preferences_discord_muted_types_parent_idx": { + "name": "users_preferences_discord_muted_types_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_preferences_discord_muted_types_parent_fk": { + "name": "users_preferences_discord_muted_types_parent_fk", + "tableFrom": "users_preferences_discord_muted_types", + "tableTo": "users", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users_sessions": { + "name": "users_sessions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "users_sessions_order_idx": { + "name": "users_sessions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_sessions_parent_id_idx": { + "name": "users_sessions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_sessions_parent_id_fk": { + "name": "users_sessions_parent_id_fk", + "tableFrom": "users_sessions", + "tableTo": "users", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "discord_username": { + "name": "discord_username", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "discord_id": { + "name": "discord_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "payload_display_name": { + "name": "payload_display_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "steam_id": { + "name": "steam_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "preferences_notifications_muted_all": { + "name": "preferences_notifications_muted_all", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "preferences_display_show_callsign": { + "name": "preferences_display_show_callsign", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "preferences_display_lead_or_actual": { + "name": "preferences_display_lead_or_actual", + "type": "enum_users_preferences_display_lead_or_actual", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'lead'" + }, + "preferences_display_show_version_overlay": { + "name": "preferences_display_show_version_overlay", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "preferences_display_xp_display_mini": { + "name": "preferences_display_xp_display_mini", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "preferences_display_show_background_texture": { + "name": "preferences_display_show_background_texture", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "preferences_discord_enabled": { + "name": "preferences_discord_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "preferences_dashboard_layout": { + "name": "preferences_dashboard_layout", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "reset_password_token": { + "name": "reset_password_token", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reset_password_expiration": { + "name": "reset_password_expiration", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "salt": { + "name": "salt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "login_attempts": { + "name": "login_attempts", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "lock_until": { + "name": "lock_until", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_discord_username_idx": { + "name": "users_discord_username_idx", + "columns": [ + { + "expression": "discord_username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_discord_id_idx": { + "name": "users_discord_id_idx", + "columns": [ + { + "expression": "discord_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_updated_at_idx": { + "name": "users_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_created_at_idx": { + "name": "users_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_username_idx": { + "name": "users_username_idx", + "columns": [ + { + "expression": "username", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users_rels": { + "name": "users_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "roles_id": { + "name": "roles_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_rels_order_idx": { + "name": "users_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_rels_parent_idx": { + "name": "users_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_rels_path_idx": { + "name": "users_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_rels_roles_id_idx": { + "name": "users_rels_roles_id_idx", + "columns": [ + { + "expression": "roles_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_rels_parent_fk": { + "name": "users_rels_parent_fk", + "tableFrom": "users_rels", + "tableTo": "users", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_rels_roles_fk": { + "name": "users_rels_roles_fk", + "tableFrom": "users_rels", + "tableTo": "roles", + "columnsFrom": [ + "roles_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles_permissions": { + "name": "roles_permissions", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_roles_permissions", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "roles_permissions_order_idx": { + "name": "roles_permissions_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "roles_permissions_parent_idx": { + "name": "roles_permissions_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "roles_permissions_parent_fk": { + "name": "roles_permissions_parent_fk", + "tableFrom": "roles_permissions", + "tableTo": "roles", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles": { + "name": "roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "is_system": { + "name": "is_system", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_superuser": { + "name": "is_superuser", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "roles_name_idx": { + "name": "roles_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "roles_slug_idx": { + "name": "roles_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "roles_updated_at_idx": { + "name": "roles_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "roles_created_at_idx": { + "name": "roles_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.roles_rels": { + "name": "roles_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "roles_id": { + "name": "roles_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "roles_rels_order_idx": { + "name": "roles_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "roles_rels_parent_idx": { + "name": "roles_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "roles_rels_path_idx": { + "name": "roles_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "roles_rels_roles_id_idx": { + "name": "roles_rels_roles_id_idx", + "columns": [ + { + "expression": "roles_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "roles_rels_parent_fk": { + "name": "roles_rels_parent_fk", + "tableFrom": "roles_rels", + "tableTo": "roles", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "roles_rels_roles_fk": { + "name": "roles_rels_roles_fk", + "tableFrom": "roles_rels", + "tableTo": "roles", + "columnsFrom": [ + "roles_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ranks": { + "name": "ranks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "_order": { + "name": "_order", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "abbreviation": { + "name": "abbreviation", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "insignia_id": { + "name": "insignia_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "promotion_ceiling_id": { + "name": "promotion_ceiling_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ranks__order_idx": { + "name": "ranks__order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ranks_name_idx": { + "name": "ranks_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ranks_abbreviation_idx": { + "name": "ranks_abbreviation_idx", + "columns": [ + { + "expression": "abbreviation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ranks_insignia_idx": { + "name": "ranks_insignia_idx", + "columns": [ + { + "expression": "insignia_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ranks_promotion_ceiling_idx": { + "name": "ranks_promotion_ceiling_idx", + "columns": [ + { + "expression": "promotion_ceiling_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ranks_updated_at_idx": { + "name": "ranks_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ranks_created_at_idx": { + "name": "ranks_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ranks_insignia_id_media_id_fk": { + "name": "ranks_insignia_id_media_id_fk", + "tableFrom": "ranks", + "tableTo": "media", + "columnsFrom": [ + "insignia_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ranks_promotion_ceiling_id_ranks_id_fk": { + "name": "ranks_promotion_ceiling_id_ranks_id_fk", + "tableFrom": "ranks", + "tableTo": "ranks", + "columnsFrom": [ + "promotion_ceiling_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles_progression_awards": { + "name": "profiles_progression_awards", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "award_id": { + "name": "award_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "awarded_by_id": { + "name": "awarded_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "awarded_at": { + "name": "awarded_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "award_reason": { + "name": "award_reason", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "upgraded_from_id": { + "name": "upgraded_from_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "upgraded_at": { + "name": "upgraded_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "profiles_progression_awards_order_idx": { + "name": "profiles_progression_awards_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "profiles_progression_awards_parent_id_idx": { + "name": "profiles_progression_awards_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "profiles_progression_awards_award_idx": { + "name": "profiles_progression_awards_award_idx", + "columns": [ + { + "expression": "award_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "profiles_progression_awards_awarded_by_idx": { + "name": "profiles_progression_awards_awarded_by_idx", + "columns": [ + { + "expression": "awarded_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "profiles_progression_awards_upgraded_from_idx": { + "name": "profiles_progression_awards_upgraded_from_idx", + "columns": [ + { + "expression": "upgraded_from_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "profiles_progression_awards_award_id_awards_id_fk": { + "name": "profiles_progression_awards_award_id_awards_id_fk", + "tableFrom": "profiles_progression_awards", + "tableTo": "awards", + "columnsFrom": [ + "award_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "profiles_progression_awards_awarded_by_id_profiles_id_fk": { + "name": "profiles_progression_awards_awarded_by_id_profiles_id_fk", + "tableFrom": "profiles_progression_awards", + "tableTo": "profiles", + "columnsFrom": [ + "awarded_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "profiles_progression_awards_upgraded_from_id_awards_id_fk": { + "name": "profiles_progression_awards_upgraded_from_id_awards_id_fk", + "tableFrom": "profiles_progression_awards", + "tableTo": "awards", + "columnsFrom": [ + "upgraded_from_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "profiles_progression_awards_parent_id_fk": { + "name": "profiles_progression_awards_parent_id_fk", + "tableFrom": "profiles_progression_awards", + "tableTo": "profiles", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rank_id": { + "name": "rank_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "dossier_enlistment_date": { + "name": "dossier_enlistment_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "dossier_personal_excerpt": { + "name": "dossier_personal_excerpt", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dossier_intel_excerpt": { + "name": "dossier_intel_excerpt", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "progression_scoreboard_infantry_kills": { + "name": "progression_scoreboard_infantry_kills", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_scoreboard_vehicle_kills": { + "name": "progression_scoreboard_vehicle_kills", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_scoreboard_armor_kills": { + "name": "progression_scoreboard_armor_kills", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_scoreboard_air_kills": { + "name": "progression_scoreboard_air_kills", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_scoreboard_deaths": { + "name": "progression_scoreboard_deaths", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_flappy_bird_games_played": { + "name": "progression_minigames_flappy_bird_games_played", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_flappy_bird_total_passes": { + "name": "progression_minigames_flappy_bird_total_passes", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_flappy_bird_best_score": { + "name": "progression_minigames_flappy_bird_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_flappy_bird_xp_earned": { + "name": "progression_minigames_flappy_bird_xp_earned", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_eod_minesweeper_games_played": { + "name": "progression_minigames_eod_minesweeper_games_played", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_eod_minesweeper_wins": { + "name": "progression_minigames_eod_minesweeper_wins", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_eod_minesweeper_losses": { + "name": "progression_minigames_eod_minesweeper_losses", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_eod_minesweeper_best_score": { + "name": "progression_minigames_eod_minesweeper_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_eod_minesweeper_xp_earned": { + "name": "progression_minigames_eod_minesweeper_xp_earned", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_eod_minesweeper_best_training_time": { + "name": "progression_minigames_eod_minesweeper_best_training_time", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_eod_minesweeper_best_perimeter_time": { + "name": "progression_minigames_eod_minesweeper_best_perimeter_time", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_eod_minesweeper_best_supply_road_time": { + "name": "progression_minigames_eod_minesweeper_best_supply_road_time", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_eod_minesweeper_best_training_flags": { + "name": "progression_minigames_eod_minesweeper_best_training_flags", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_eod_minesweeper_best_perimeter_flags": { + "name": "progression_minigames_eod_minesweeper_best_perimeter_flags", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_eod_minesweeper_best_supply_road_flags": { + "name": "progression_minigames_eod_minesweeper_best_supply_road_flags", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_games_played": { + "name": "progression_minigames_radio_traffic_games_played", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_total_messages": { + "name": "progression_minigames_radio_traffic_total_messages", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_incorrect_messages": { + "name": "progression_minigames_radio_traffic_incorrect_messages", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_best_score": { + "name": "progression_minigames_radio_traffic_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_best_accuracy": { + "name": "progression_minigames_radio_traffic_best_accuracy", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_best_sprint_time_ms": { + "name": "progression_minigames_radio_traffic_best_sprint_time_ms", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_sprint_games_played": { + "name": "progression_minigames_radio_traffic_sprint_games_played", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_sprint_total_messages": { + "name": "progression_minigames_radio_traffic_sprint_total_messages", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_sprint_incorrect_messages": { + "name": "progression_minigames_radio_traffic_sprint_incorrect_messages", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_sprint_best_score": { + "name": "progression_minigames_radio_traffic_sprint_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_sprint_best_accuracy": { + "name": "progression_minigames_radio_traffic_sprint_best_accuracy", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_watch_games_played": { + "name": "progression_minigames_radio_traffic_watch_games_played", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_watch_total_messages": { + "name": "progression_minigames_radio_traffic_watch_total_messages", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_watch_incorrect_messages": { + "name": "progression_minigames_radio_traffic_watch_incorrect_messages", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_watch_best_score": { + "name": "progression_minigames_radio_traffic_watch_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_watch_best_accuracy": { + "name": "progression_minigames_radio_traffic_watch_best_accuracy", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_radio_traffic_xp_earned": { + "name": "progression_minigames_radio_traffic_xp_earned", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_aim_trainer_games_played": { + "name": "progression_minigames_aim_trainer_games_played", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_aim_trainer_best_score": { + "name": "progression_minigames_aim_trainer_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_aim_trainer_recruit_best_score": { + "name": "progression_minigames_aim_trainer_recruit_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_aim_trainer_veteran_best_score": { + "name": "progression_minigames_aim_trainer_veteran_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_aim_trainer_elite_best_score": { + "name": "progression_minigames_aim_trainer_elite_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_aim_trainer_recruit_best_accuracy": { + "name": "progression_minigames_aim_trainer_recruit_best_accuracy", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_aim_trainer_veteran_best_accuracy": { + "name": "progression_minigames_aim_trainer_veteran_best_accuracy", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_aim_trainer_elite_best_accuracy": { + "name": "progression_minigames_aim_trainer_elite_best_accuracy", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_aim_trainer_xp_earned": { + "name": "progression_minigames_aim_trainer_xp_earned", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_opord_recall_games_played": { + "name": "progression_minigames_opord_recall_games_played", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_opord_recall_best_score": { + "name": "progression_minigames_opord_recall_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_opord_recall_best_accuracy": { + "name": "progression_minigames_opord_recall_best_accuracy", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_opord_recall_recruit_best_score": { + "name": "progression_minigames_opord_recall_recruit_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_opord_recall_veteran_best_score": { + "name": "progression_minigames_opord_recall_veteran_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_opord_recall_elite_best_score": { + "name": "progression_minigames_opord_recall_elite_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_opord_recall_recruit_best_time_ms": { + "name": "progression_minigames_opord_recall_recruit_best_time_ms", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "progression_minigames_opord_recall_veteran_best_time_ms": { + "name": "progression_minigames_opord_recall_veteran_best_time_ms", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "progression_minigames_opord_recall_elite_best_time_ms": { + "name": "progression_minigames_opord_recall_elite_best_time_ms", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "progression_minigames_opord_recall_xp_earned": { + "name": "progression_minigames_opord_recall_xp_earned", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_poker_games_played": { + "name": "progression_minigames_poker_games_played", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_poker_wins": { + "name": "progression_minigames_poker_wins", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_poker_second_places": { + "name": "progression_minigames_poker_second_places", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_poker_third_places": { + "name": "progression_minigames_poker_third_places", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_poker_fourth_places": { + "name": "progression_minigames_poker_fourth_places", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_poker_hands_won": { + "name": "progression_minigames_poker_hands_won", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_poker_best_score": { + "name": "progression_minigames_poker_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_poker_xp_earned": { + "name": "progression_minigames_poker_xp_earned", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_blackjack_games_played": { + "name": "progression_minigames_blackjack_games_played", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_blackjack_rounds_won": { + "name": "progression_minigames_blackjack_rounds_won", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_blackjack_blackjacks_hit": { + "name": "progression_minigames_blackjack_blackjacks_hit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_blackjack_best_score": { + "name": "progression_minigames_blackjack_best_score", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_minigames_blackjack_xp_earned": { + "name": "progression_minigames_blackjack_xp_earned", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "progression_voiceover_xp_earned": { + "name": "progression_voiceover_xp_earned", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "profiles_user_idx": { + "name": "profiles_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "profiles_rank_idx": { + "name": "profiles_rank_idx", + "columns": [ + { + "expression": "rank_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "profiles_updated_at_idx": { + "name": "profiles_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "profiles_created_at_idx": { + "name": "profiles_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "profiles_user_id_users_id_fk": { + "name": "profiles_user_id_users_id_fk", + "tableFrom": "profiles", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "profiles_rank_id_ranks_id_fk": { + "name": "profiles_rank_id_ranks_id_fk", + "tableFrom": "profiles", + "tableTo": "ranks", + "columnsFrom": [ + "rank_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles_rels": { + "name": "profiles_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "qualifications_id": { + "name": "qualifications_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "profiles_rels_order_idx": { + "name": "profiles_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "profiles_rels_parent_idx": { + "name": "profiles_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "profiles_rels_path_idx": { + "name": "profiles_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "profiles_rels_qualifications_id_idx": { + "name": "profiles_rels_qualifications_id_idx", + "columns": [ + { + "expression": "qualifications_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "profiles_rels_parent_fk": { + "name": "profiles_rels_parent_fk", + "tableFrom": "profiles_rels", + "tableTo": "profiles", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "profiles_rels_qualifications_fk": { + "name": "profiles_rels_qualifications_fk", + "tableFrom": "profiles_rels", + "tableTo": "qualifications", + "columnsFrom": [ + "qualifications_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.awards_grant_config_allowed_grantor_assignment_types": { + "name": "awards_grant_config_allowed_grantor_assignment_types", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_awards_grant_config_allowed_grantor_assignment_types", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "awards_grant_config_allowed_grantor_assignment_types_order_idx": { + "name": "awards_grant_config_allowed_grantor_assignment_types_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "awards_grant_config_allowed_grantor_assignment_types_parent_idx": { + "name": "awards_grant_config_allowed_grantor_assignment_types_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "awards_grant_config_allowed_grantor_assignment_types_parent_fk": { + "name": "awards_grant_config_allowed_grantor_assignment_types_parent_fk", + "tableFrom": "awards_grant_config_allowed_grantor_assignment_types", + "tableTo": "awards", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.awards": { + "name": "awards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "enum_awards_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'commendation'" + }, + "commendation_color": { + "name": "commendation_color", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'#92400e'" + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "experience_points": { + "name": "experience_points", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "ribbon_design": { + "name": "ribbon_design", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "background_image_id": { + "name": "background_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "medal_design": { + "name": "medal_design", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "advancement_series_key": { + "name": "advancement_series_key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "advancement_tier": { + "name": "advancement_tier", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "grant_config_max_grants_per_grantor": { + "name": "grant_config_max_grants_per_grantor", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "grant_config_recipient_scope": { + "name": "grant_config_recipient_scope", + "type": "enum_awards_grant_config_recipient_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'same_command'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "awards_name_idx": { + "name": "awards_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "awards_image_idx": { + "name": "awards_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "awards_background_image_idx": { + "name": "awards_background_image_idx", + "columns": [ + { + "expression": "background_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "awards_updated_at_idx": { + "name": "awards_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "awards_created_at_idx": { + "name": "awards_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "awards_image_id_media_id_fk": { + "name": "awards_image_id_media_id_fk", + "tableFrom": "awards", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "awards_background_image_id_media_id_fk": { + "name": "awards_background_image_id_media_id_fk", + "tableFrom": "awards", + "tableTo": "media", + "columnsFrom": [ + "background_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.awards_rels": { + "name": "awards_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "assignments_id": { + "name": "assignments_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "awards_rels_order_idx": { + "name": "awards_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "awards_rels_parent_idx": { + "name": "awards_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "awards_rels_path_idx": { + "name": "awards_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "awards_rels_assignments_id_idx": { + "name": "awards_rels_assignments_id_idx", + "columns": [ + { + "expression": "assignments_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "awards_rels_parent_fk": { + "name": "awards_rels_parent_fk", + "tableFrom": "awards_rels", + "tableTo": "awards", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "awards_rels_assignments_fk": { + "name": "awards_rels_assignments_fk", + "tableFrom": "awards_rels", + "tableTo": "assignments", + "columnsFrom": [ + "assignments_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.qualifications": { + "name": "qualifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "qualifications_name_idx": { + "name": "qualifications_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "qualifications_updated_at_idx": { + "name": "qualifications_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "qualifications_created_at_idx": { + "name": "qualifications_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assignments": { + "name": "assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "enum_assignments_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "callsign": { + "name": "callsign", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "leader_id": { + "name": "leader_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assignments_leader_idx": { + "name": "assignments_leader_idx", + "columns": [ + { + "expression": "leader_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignments_updated_at_idx": { + "name": "assignments_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignments_created_at_idx": { + "name": "assignments_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assignments_leader_id_users_id_fk": { + "name": "assignments_leader_id_users_id_fk", + "tableFrom": "assignments", + "tableTo": "users", + "columnsFrom": [ + "leader_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assignments_rels": { + "name": "assignments_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "assignments_id": { + "name": "assignments_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "assignments_rels_order_idx": { + "name": "assignments_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignments_rels_parent_idx": { + "name": "assignments_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignments_rels_path_idx": { + "name": "assignments_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignments_rels_assignments_id_idx": { + "name": "assignments_rels_assignments_id_idx", + "columns": [ + { + "expression": "assignments_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignments_rels_users_id_idx": { + "name": "assignments_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assignments_rels_parent_fk": { + "name": "assignments_rels_parent_fk", + "tableFrom": "assignments_rels", + "tableTo": "assignments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assignments_rels_assignments_fk": { + "name": "assignments_rels_assignments_fk", + "tableFrom": "assignments_rels", + "tableTo": "assignments", + "columnsFrom": [ + "assignments_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assignments_rels_users_fk": { + "name": "assignments_rels_users_fk", + "tableFrom": "assignments_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assignment_transfers": { + "name": "assignment_transfers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "requester_id": { + "name": "requester_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "from_assignment_id": { + "name": "from_assignment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "to_assignment_id": { + "name": "to_assignment_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "enum_assignment_transfers_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "escalation_reason": { + "name": "escalation_reason", + "type": "enum_assignment_transfers_escalation_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "from_leader_decision": { + "name": "from_leader_decision", + "type": "enum_assignment_transfers_from_leader_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "to_leader_decision": { + "name": "to_leader_decision", + "type": "enum_assignment_transfers_to_leader_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "from_decision_by_id": { + "name": "from_decision_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "to_decision_by_id": { + "name": "to_decision_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "from_decided_at": { + "name": "from_decided_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "to_decided_at": { + "name": "to_decided_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "from_leader_snapshot_id": { + "name": "from_leader_snapshot_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "to_leader_snapshot_id": { + "name": "to_leader_snapshot_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "request_note": { + "name": "request_note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "appeal_note": { + "name": "appeal_note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "resolved_by_id": { + "name": "resolved_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "resolution_note": { + "name": "resolution_note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "discord_from_leader_message_id": { + "name": "discord_from_leader_message_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "discord_to_leader_message_id": { + "name": "discord_to_leader_message_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "discord_requester_message_id": { + "name": "discord_requester_message_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "source_notified_to_user_id_id": { + "name": "source_notified_to_user_id_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "destination_notified_to_user_id_id": { + "name": "destination_notified_to_user_id_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "discord_delivered_keys": { + "name": "discord_delivered_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assignment_transfers_requester_idx": { + "name": "assignment_transfers_requester_idx", + "columns": [ + { + "expression": "requester_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_transfers_from_assignment_idx": { + "name": "assignment_transfers_from_assignment_idx", + "columns": [ + { + "expression": "from_assignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_transfers_to_assignment_idx": { + "name": "assignment_transfers_to_assignment_idx", + "columns": [ + { + "expression": "to_assignment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_transfers_status_idx": { + "name": "assignment_transfers_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_transfers_from_decision_by_idx": { + "name": "assignment_transfers_from_decision_by_idx", + "columns": [ + { + "expression": "from_decision_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_transfers_to_decision_by_idx": { + "name": "assignment_transfers_to_decision_by_idx", + "columns": [ + { + "expression": "to_decision_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_transfers_from_leader_snapshot_idx": { + "name": "assignment_transfers_from_leader_snapshot_idx", + "columns": [ + { + "expression": "from_leader_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_transfers_to_leader_snapshot_idx": { + "name": "assignment_transfers_to_leader_snapshot_idx", + "columns": [ + { + "expression": "to_leader_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_transfers_resolved_by_idx": { + "name": "assignment_transfers_resolved_by_idx", + "columns": [ + { + "expression": "resolved_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_transfers_source_notified_to_user_id_idx": { + "name": "assignment_transfers_source_notified_to_user_id_idx", + "columns": [ + { + "expression": "source_notified_to_user_id_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_transfers_destination_notified_to_user_id_idx": { + "name": "assignment_transfers_destination_notified_to_user_id_idx", + "columns": [ + { + "expression": "destination_notified_to_user_id_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_transfers_updated_at_idx": { + "name": "assignment_transfers_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assignment_transfers_created_at_idx": { + "name": "assignment_transfers_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assignment_transfers_requester_id_users_id_fk": { + "name": "assignment_transfers_requester_id_users_id_fk", + "tableFrom": "assignment_transfers", + "tableTo": "users", + "columnsFrom": [ + "requester_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "assignment_transfers_from_assignment_id_assignments_id_fk": { + "name": "assignment_transfers_from_assignment_id_assignments_id_fk", + "tableFrom": "assignment_transfers", + "tableTo": "assignments", + "columnsFrom": [ + "from_assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "assignment_transfers_to_assignment_id_assignments_id_fk": { + "name": "assignment_transfers_to_assignment_id_assignments_id_fk", + "tableFrom": "assignment_transfers", + "tableTo": "assignments", + "columnsFrom": [ + "to_assignment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "assignment_transfers_from_decision_by_id_users_id_fk": { + "name": "assignment_transfers_from_decision_by_id_users_id_fk", + "tableFrom": "assignment_transfers", + "tableTo": "users", + "columnsFrom": [ + "from_decision_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "assignment_transfers_to_decision_by_id_users_id_fk": { + "name": "assignment_transfers_to_decision_by_id_users_id_fk", + "tableFrom": "assignment_transfers", + "tableTo": "users", + "columnsFrom": [ + "to_decision_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "assignment_transfers_from_leader_snapshot_id_users_id_fk": { + "name": "assignment_transfers_from_leader_snapshot_id_users_id_fk", + "tableFrom": "assignment_transfers", + "tableTo": "users", + "columnsFrom": [ + "from_leader_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "assignment_transfers_to_leader_snapshot_id_users_id_fk": { + "name": "assignment_transfers_to_leader_snapshot_id_users_id_fk", + "tableFrom": "assignment_transfers", + "tableTo": "users", + "columnsFrom": [ + "to_leader_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "assignment_transfers_resolved_by_id_users_id_fk": { + "name": "assignment_transfers_resolved_by_id_users_id_fk", + "tableFrom": "assignment_transfers", + "tableTo": "users", + "columnsFrom": [ + "resolved_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "assignment_transfers_source_notified_to_user_id_id_users_id_fk": { + "name": "assignment_transfers_source_notified_to_user_id_id_users_id_fk", + "tableFrom": "assignment_transfers", + "tableTo": "users", + "columnsFrom": [ + "source_notified_to_user_id_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "assignment_transfers_destination_notified_to_user_id_id_users_id_fk": { + "name": "assignment_transfers_destination_notified_to_user_id_id_users_id_fk", + "tableFrom": "assignment_transfers", + "tableTo": "users", + "columnsFrom": [ + "destination_notified_to_user_id_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.experience": { + "name": "experience", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "required_points": { + "name": "required_points", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "experience_name_idx": { + "name": "experience_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "experience_updated_at_idx": { + "name": "experience_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "experience_created_at_idx": { + "name": "experience_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_notifications": { + "name": "user_notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "link": { + "name": "link", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "read": { + "name": "read", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_notifications_user_idx": { + "name": "user_notifications_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_notifications_read_idx": { + "name": "user_notifications_read_idx", + "columns": [ + { + "expression": "read", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_notifications_updated_at_idx": { + "name": "user_notifications_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_notifications_created_at_idx": { + "name": "user_notifications_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_notifications_user_id_users_id_fk": { + "name": "user_notifications_user_id_users_id_fk", + "tableFrom": "user_notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.evaluations": { + "name": "evaluations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "rater_id": { + "name": "rater_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ratee_id": { + "name": "ratee_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mission_id": { + "name": "mission_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "enum_evaluations_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "enum_evaluations_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "evaluations_rater_idx": { + "name": "evaluations_rater_idx", + "columns": [ + { + "expression": "rater_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "evaluations_ratee_idx": { + "name": "evaluations_ratee_idx", + "columns": [ + { + "expression": "ratee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "evaluations_mission_idx": { + "name": "evaluations_mission_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "evaluations_updated_at_idx": { + "name": "evaluations_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "evaluations_created_at_idx": { + "name": "evaluations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "evaluations_rater_id_users_id_fk": { + "name": "evaluations_rater_id_users_id_fk", + "tableFrom": "evaluations", + "tableTo": "users", + "columnsFrom": [ + "rater_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "evaluations_ratee_id_users_id_fk": { + "name": "evaluations_ratee_id_users_id_fk", + "tableFrom": "evaluations", + "tableTo": "users", + "columnsFrom": [ + "ratee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "evaluations_mission_id_missions_id_fk": { + "name": "evaluations_mission_id_missions_id_fk", + "tableFrom": "evaluations", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ribbon_submissions_grantor_types": { + "name": "ribbon_submissions_grantor_types", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_ribbon_submissions_grantor_types", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "ribbon_submissions_grantor_types_order_idx": { + "name": "ribbon_submissions_grantor_types_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ribbon_submissions_grantor_types_parent_idx": { + "name": "ribbon_submissions_grantor_types_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ribbon_submissions_grantor_types_parent_fk": { + "name": "ribbon_submissions_grantor_types_parent_fk", + "tableFrom": "ribbon_submissions_grantor_types", + "tableTo": "ribbon_submissions", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ribbon_submissions": { + "name": "ribbon_submissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "design": { + "name": "design", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "enum_ribbon_submissions_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "review_note": { + "name": "review_note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "reviewed_by_id": { + "name": "reviewed_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "award_id": { + "name": "award_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "award_xp": { + "name": "award_xp", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "recipient_scope": { + "name": "recipient_scope", + "type": "enum_ribbon_submissions_recipient_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'same_command'" + }, + "max_grants_per_grantor": { + "name": "max_grants_per_grantor", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ribbon_submissions_user_idx": { + "name": "ribbon_submissions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ribbon_submissions_reviewed_by_idx": { + "name": "ribbon_submissions_reviewed_by_idx", + "columns": [ + { + "expression": "reviewed_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ribbon_submissions_award_idx": { + "name": "ribbon_submissions_award_idx", + "columns": [ + { + "expression": "award_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ribbon_submissions_updated_at_idx": { + "name": "ribbon_submissions_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ribbon_submissions_created_at_idx": { + "name": "ribbon_submissions_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ribbon_submissions_user_id_users_id_fk": { + "name": "ribbon_submissions_user_id_users_id_fk", + "tableFrom": "ribbon_submissions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ribbon_submissions_reviewed_by_id_users_id_fk": { + "name": "ribbon_submissions_reviewed_by_id_users_id_fk", + "tableFrom": "ribbon_submissions", + "tableTo": "users", + "columnsFrom": [ + "reviewed_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ribbon_submissions_award_id_awards_id_fk": { + "name": "ribbon_submissions_award_id_awards_id_fk", + "tableFrom": "ribbon_submissions", + "tableTo": "awards", + "columnsFrom": [ + "award_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions_mission_roles_provided_roles": { + "name": "missions_mission_roles_provided_roles", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "role_name": { + "name": "role_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "mission_critical": { + "name": "mission_critical", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "minimum_rank_id": { + "name": "minimum_rank_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "missions_mission_roles_provided_roles_order_idx": { + "name": "missions_mission_roles_provided_roles_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_mission_roles_provided_roles_parent_id_idx": { + "name": "missions_mission_roles_provided_roles_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_mission_roles_provided_roles_minimum_rank_idx": { + "name": "missions_mission_roles_provided_roles_minimum_rank_idx", + "columns": [ + { + "expression": "minimum_rank_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_mission_roles_provided_roles_minimum_rank_id_ranks_id_fk": { + "name": "missions_mission_roles_provided_roles_minimum_rank_id_ranks_id_fk", + "tableFrom": "missions_mission_roles_provided_roles", + "tableTo": "ranks", + "columnsFrom": [ + "minimum_rank_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_mission_roles_provided_roles_parent_id_fk": { + "name": "missions_mission_roles_provided_roles_parent_id_fk", + "tableFrom": "missions_mission_roles_provided_roles", + "tableTo": "missions", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions_briefing": { + "name": "missions_briefing", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "section_name": { + "name": "section_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "missions_briefing_order_idx": { + "name": "missions_briefing_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_briefing_parent_id_idx": { + "name": "missions_briefing_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_briefing_parent_id_fk": { + "name": "missions_briefing_parent_id_fk", + "tableFrom": "missions_briefing", + "tableTo": "missions", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions_mission_objectives": { + "name": "missions_mission_objectives", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mission_critical": { + "name": "mission_critical", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "success_or_fail_conditions": { + "name": "success_or_fail_conditions", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "redacted": { + "name": "redacted", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": { + "missions_mission_objectives_order_idx": { + "name": "missions_mission_objectives_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_mission_objectives_parent_id_idx": { + "name": "missions_mission_objectives_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_mission_objectives_parent_id_fk": { + "name": "missions_mission_objectives_parent_id_fk", + "tableFrom": "missions_mission_objectives", + "tableTo": "missions", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions_factions_involved": { + "name": "missions_factions_involved", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "faction_id": { + "name": "faction_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "threat_level": { + "name": "threat_level", + "type": "enum_missions_factions_involved_threat_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'hostile'" + } + }, + "indexes": { + "missions_factions_involved_order_idx": { + "name": "missions_factions_involved_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_factions_involved_parent_id_idx": { + "name": "missions_factions_involved_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_factions_involved_faction_idx": { + "name": "missions_factions_involved_faction_idx", + "columns": [ + { + "expression": "faction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_factions_involved_faction_id_factions_id_fk": { + "name": "missions_factions_involved_faction_id_factions_id_fk", + "tableFrom": "missions_factions_involved", + "tableTo": "factions", + "columnsFrom": [ + "faction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_factions_involved_parent_id_fk": { + "name": "missions_factions_involved_parent_id_fk", + "tableFrom": "missions_factions_involved", + "tableTo": "missions", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions_logistics_and_support_insertion": { + "name": "missions_logistics_and_support_insertion", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_missions_logistics_and_support_insertion", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "missions_logistics_and_support_insertion_order_idx": { + "name": "missions_logistics_and_support_insertion_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_logistics_and_support_insertion_parent_idx": { + "name": "missions_logistics_and_support_insertion_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_logistics_and_support_insertion_parent_fk": { + "name": "missions_logistics_and_support_insertion_parent_fk", + "tableFrom": "missions_logistics_and_support_insertion", + "tableTo": "missions", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions_logistics_and_support_extraction": { + "name": "missions_logistics_and_support_extraction", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_missions_logistics_and_support_extraction", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "missions_logistics_and_support_extraction_order_idx": { + "name": "missions_logistics_and_support_extraction_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_logistics_and_support_extraction_parent_idx": { + "name": "missions_logistics_and_support_extraction_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_logistics_and_support_extraction_parent_fk": { + "name": "missions_logistics_and_support_extraction_parent_fk", + "tableFrom": "missions_logistics_and_support_extraction", + "tableTo": "missions", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions_logistics_and_support_assets_available": { + "name": "missions_logistics_and_support_assets_available", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "asset_type": { + "name": "asset_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "asset_count": { + "name": "asset_count", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "does_respawn": { + "name": "does_respawn", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "asset_callsign": { + "name": "asset_callsign", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "missions_logistics_and_support_assets_available_order_idx": { + "name": "missions_logistics_and_support_assets_available_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_logistics_and_support_assets_available_parent_id_idx": { + "name": "missions_logistics_and_support_assets_available_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_logistics_and_support_assets_available_parent_id_fk": { + "name": "missions_logistics_and_support_assets_available_parent_id_fk", + "tableFrom": "missions_logistics_and_support_assets_available", + "tableTo": "missions", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions_comms_and_docs_frequencies": { + "name": "missions_comms_and_docs_frequencies", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "frequency": { + "name": "frequency", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "net_name": { + "name": "net_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "freq_usage": { + "name": "freq_usage", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "missions_comms_and_docs_frequencies_order_idx": { + "name": "missions_comms_and_docs_frequencies_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_comms_and_docs_frequencies_parent_id_idx": { + "name": "missions_comms_and_docs_frequencies_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_comms_and_docs_frequencies_parent_id_fk": { + "name": "missions_comms_and_docs_frequencies_parent_id_fk", + "tableFrom": "missions_comms_and_docs_frequencies", + "tableTo": "missions", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions": { + "name": "missions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "code_name": { + "name": "code_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "operation_type": { + "name": "operation_type", + "type": "enum_missions_operation_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "discord_message_id": { + "name": "discord_message_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "discord_attendance_hash": { + "name": "discord_attendance_hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "discord_attendance_sent_at": { + "name": "discord_attendance_sent_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "discord_modlist_hash": { + "name": "discord_modlist_hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "evaluation_reminders_sent_at": { + "name": "evaluation_reminders_sent_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "ready_reminder_web_sent_at": { + "name": "ready_reminder_web_sent_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "ready_reminder_dm_sent_at": { + "name": "ready_reminder_dm_sent_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "cover_image_id": { + "name": "cover_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "classification_map_id": { + "name": "classification_map_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "classification_mission_type": { + "name": "classification_mission_type", + "type": "enum_missions_classification_mission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'Zeus'" + }, + "classification_campaign_id": { + "name": "classification_campaign_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "classification_custom_scripts": { + "name": "classification_custom_scripts", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "classification_start_date_time": { + "name": "classification_start_date_time", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "classification_estimated_duration": { + "name": "classification_estimated_duration", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 240 + }, + "ownership_and_status_status": { + "name": "ownership_and_status_status", + "type": "enum_missions_ownership_and_status_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'Concept'" + }, + "ownership_and_status_visibility": { + "name": "ownership_and_status_visibility", + "type": "enum_missions_ownership_and_status_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'leadership'" + }, + "ownership_and_status_completed_at": { + "name": "ownership_and_status_completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "mission_roles_max_players": { + "name": "mission_roles_max_players", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 32 + }, + "mission_roles_notes": { + "name": "mission_roles_notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "civilian_presence": { + "name": "civilian_presence", + "type": "enum_missions_civilian_presence", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'None'" + }, + "game_details_server_details_server_ip": { + "name": "game_details_server_details_server_ip", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'arma.onyxsimple.com'" + }, + "game_details_server_details_server_port": { + "name": "game_details_server_details_server_port", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 2302 + }, + "game_details_server_details_server_password": { + "name": "game_details_server_details_server_password", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'ptf313'" + }, + "game_details_game_setup_mission_file_id": { + "name": "game_details_game_setup_mission_file_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "game_details_game_setup_required_mods_id": { + "name": "game_details_game_setup_required_mods_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "comms_and_docs_radio_plan": { + "name": "comms_and_docs_radio_plan", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "missions_code_name_idx": { + "name": "missions_code_name_idx", + "columns": [ + { + "expression": "code_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_created_by_idx": { + "name": "missions_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_cover_image_idx": { + "name": "missions_cover_image_idx", + "columns": [ + { + "expression": "cover_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_classification_classification_map_idx": { + "name": "missions_classification_classification_map_idx", + "columns": [ + { + "expression": "classification_map_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_classification_classification_campaign_idx": { + "name": "missions_classification_classification_campaign_idx", + "columns": [ + { + "expression": "classification_campaign_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_game_details_game_setup_game_details_game_setup_idx": { + "name": "missions_game_details_game_setup_game_details_game_setup_idx", + "columns": [ + { + "expression": "game_details_game_setup_mission_file_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_game_details_game_setup_game_details_game_set_1_idx": { + "name": "missions_game_details_game_setup_game_details_game_set_1_idx", + "columns": [ + { + "expression": "game_details_game_setup_required_mods_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_updated_at_idx": { + "name": "missions_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_created_at_idx": { + "name": "missions_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_created_by_id_users_id_fk": { + "name": "missions_created_by_id_users_id_fk", + "tableFrom": "missions", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_cover_image_id_media_id_fk": { + "name": "missions_cover_image_id_media_id_fk", + "tableFrom": "missions", + "tableTo": "media", + "columnsFrom": [ + "cover_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_classification_map_id_maps_id_fk": { + "name": "missions_classification_map_id_maps_id_fk", + "tableFrom": "missions", + "tableTo": "maps", + "columnsFrom": [ + "classification_map_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_classification_campaign_id_campaigns_id_fk": { + "name": "missions_classification_campaign_id_campaigns_id_fk", + "tableFrom": "missions", + "tableTo": "campaigns", + "columnsFrom": [ + "classification_campaign_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_game_details_game_setup_mission_file_id_mission_files_id_fk": { + "name": "missions_game_details_game_setup_mission_file_id_mission_files_id_fk", + "tableFrom": "missions", + "tableTo": "mission_files", + "columnsFrom": [ + "game_details_game_setup_mission_file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "missions_game_details_game_setup_required_mods_id_mod_lists_id_fk": { + "name": "missions_game_details_game_setup_required_mods_id_mod_lists_id_fk", + "tableFrom": "missions", + "tableTo": "mod_lists", + "columnsFrom": [ + "game_details_game_setup_required_mods_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.missions_rels": { + "name": "missions_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "qualifications_id": { + "name": "qualifications_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "missions_rels_order_idx": { + "name": "missions_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_rels_parent_idx": { + "name": "missions_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_rels_path_idx": { + "name": "missions_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_rels_users_id_idx": { + "name": "missions_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "missions_rels_qualifications_id_idx": { + "name": "missions_rels_qualifications_id_idx", + "columns": [ + { + "expression": "qualifications_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "missions_rels_parent_fk": { + "name": "missions_rels_parent_fk", + "tableFrom": "missions_rels", + "tableTo": "missions", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "missions_rels_users_fk": { + "name": "missions_rels_users_fk", + "tableFrom": "missions_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "missions_rels_qualifications_fk": { + "name": "missions_rels_qualifications_fk", + "tableFrom": "missions_rels", + "tableTo": "qualifications", + "columnsFrom": [ + "qualifications_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mission_attendances": { + "name": "mission_attendances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "mission_id": { + "name": "mission_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "response": { + "name": "response", + "type": "enum_mission_attendances_response", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mission_attendances_mission_idx": { + "name": "mission_attendances_mission_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_attendances_user_idx": { + "name": "mission_attendances_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_attendances_updated_at_idx": { + "name": "mission_attendances_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_attendances_created_at_idx": { + "name": "mission_attendances_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mission_attendances_mission_id_missions_id_fk": { + "name": "mission_attendances_mission_id_missions_id_fk", + "tableFrom": "mission_attendances", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mission_attendances_user_id_users_id_fk": { + "name": "mission_attendances_user_id_users_id_fk", + "tableFrom": "mission_attendances", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.campaigns": { + "name": "campaigns", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'An Arma 3 campaign.'" + }, + "status": { + "name": "status", + "type": "enum_campaigns_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'concept'" + }, + "campaign_mode": { + "name": "campaign_mode", + "type": "enum_campaigns_campaign_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'custom'" + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "cover_image_id": { + "name": "cover_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "campaigns_name_idx": { + "name": "campaigns_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaigns_cover_image_idx": { + "name": "campaigns_cover_image_idx", + "columns": [ + { + "expression": "cover_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaigns_owner_idx": { + "name": "campaigns_owner_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaigns_updated_at_idx": { + "name": "campaigns_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "campaigns_created_at_idx": { + "name": "campaigns_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "campaigns_cover_image_id_media_id_fk": { + "name": "campaigns_cover_image_id_media_id_fk", + "tableFrom": "campaigns", + "tableTo": "media", + "columnsFrom": [ + "cover_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "campaigns_owner_id_users_id_fk": { + "name": "campaigns_owner_id_users_id_fk", + "tableFrom": "campaigns", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.factions_relationships": { + "name": "factions_relationships", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "target_faction_id": { + "name": "target_faction_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "notes": { + "name": "notes", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "factions_relationships_order_idx": { + "name": "factions_relationships_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "factions_relationships_parent_id_idx": { + "name": "factions_relationships_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "factions_relationships_target_faction_idx": { + "name": "factions_relationships_target_faction_idx", + "columns": [ + { + "expression": "target_faction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "factions_relationships_target_faction_id_factions_id_fk": { + "name": "factions_relationships_target_faction_id_factions_id_fk", + "tableFrom": "factions_relationships", + "tableTo": "factions", + "columnsFrom": [ + "target_faction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "factions_relationships_parent_id_fk": { + "name": "factions_relationships_parent_id_fk", + "tableFrom": "factions_relationships", + "tableTo": "factions", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.factions": { + "name": "factions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "enum_factions_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'minorFaction'" + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reputation": { + "name": "reputation", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "playable_faction": { + "name": "playable_faction", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "starting_faction": { + "name": "starting_faction", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cover_image_id": { + "name": "cover_image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "factions_name_idx": { + "name": "factions_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "factions_cover_image_idx": { + "name": "factions_cover_image_idx", + "columns": [ + { + "expression": "cover_image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "factions_updated_at_idx": { + "name": "factions_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "factions_created_at_idx": { + "name": "factions_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "factions_cover_image_id_media_id_fk": { + "name": "factions_cover_image_id_media_id_fk", + "tableFrom": "factions", + "tableTo": "media", + "columnsFrom": [ + "cover_image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.factions_rels": { + "name": "factions_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "campaigns_id": { + "name": "campaigns_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "factions_rels_order_idx": { + "name": "factions_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "factions_rels_parent_idx": { + "name": "factions_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "factions_rels_path_idx": { + "name": "factions_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "factions_rels_campaigns_id_idx": { + "name": "factions_rels_campaigns_id_idx", + "columns": [ + { + "expression": "campaigns_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "factions_rels_parent_fk": { + "name": "factions_rels_parent_fk", + "tableFrom": "factions_rels", + "tableTo": "factions", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "factions_rels_campaigns_fk": { + "name": "factions_rels_campaigns_fk", + "tableFrom": "factions_rels", + "tableTo": "campaigns", + "columnsFrom": [ + "campaigns_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.technologies_requirements_prerequisite_technologies": { + "name": "technologies_requirements_prerequisite_technologies", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "technologies_requirements_prerequisite_technologies_order_idx": { + "name": "technologies_requirements_prerequisite_technologies_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "technologies_requirements_prerequisite_technologies_parent_id_idx": { + "name": "technologies_requirements_prerequisite_technologies_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "technologies_requirements_prerequisite_technologies_parent_id_fk": { + "name": "technologies_requirements_prerequisite_technologies_parent_id_fk", + "tableFrom": "technologies_requirements_prerequisite_technologies", + "tableTo": "technologies", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.technologies_research_costs_resources": { + "name": "technologies_research_costs_resources", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": { + "technologies_research_costs_resources_order_idx": { + "name": "technologies_research_costs_resources_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "technologies_research_costs_resources_parent_id_idx": { + "name": "technologies_research_costs_resources_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "technologies_research_costs_resources_resource_idx": { + "name": "technologies_research_costs_resources_resource_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "technologies_research_costs_resources_resource_id_resources_id_fk": { + "name": "technologies_research_costs_resources_resource_id_resources_id_fk", + "tableFrom": "technologies_research_costs_resources", + "tableTo": "resources", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "technologies_research_costs_resources_parent_id_fk": { + "name": "technologies_research_costs_resources_parent_id_fk", + "tableFrom": "technologies_research_costs_resources", + "tableTo": "technologies", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.technologies": { + "name": "technologies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "approval_status": { + "name": "approval_status", + "type": "enum_technologies_approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'in_progress'" + }, + "type": { + "name": "type", + "type": "enum_technologies_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'upgrade'" + }, + "summary": { + "name": "summary", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "research_costs_minimum_research_duration": { + "name": "research_costs_minimum_research_duration", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "research_costs_maximum_research_duration": { + "name": "research_costs_maximum_research_duration", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "technologies_updated_at_idx": { + "name": "technologies_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "technologies_created_at_idx": { + "name": "technologies_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.technologies_rels": { + "name": "technologies_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "technologies_id": { + "name": "technologies_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "technologies_rels_order_idx": { + "name": "technologies_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "technologies_rels_parent_idx": { + "name": "technologies_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "technologies_rels_path_idx": { + "name": "technologies_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "technologies_rels_technologies_id_idx": { + "name": "technologies_rels_technologies_id_idx", + "columns": [ + { + "expression": "technologies_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "technologies_rels_parent_fk": { + "name": "technologies_rels_parent_fk", + "tableFrom": "technologies_rels", + "tableTo": "technologies", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "technologies_rels_technologies_fk": { + "name": "technologies_rels_technologies_fk", + "tableFrom": "technologies_rels", + "tableTo": "technologies", + "columnsFrom": [ + "technologies_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets_skin_configuration_applies_to_asset_types": { + "name": "assets_skin_configuration_applies_to_asset_types", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_assets_skin_configuration_applies_to_asset_types", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "assets_skin_configuration_applies_to_asset_types_order_idx": { + "name": "assets_skin_configuration_applies_to_asset_types_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_skin_configuration_applies_to_asset_types_parent_idx": { + "name": "assets_skin_configuration_applies_to_asset_types_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_skin_configuration_applies_to_asset_types_parent_fk": { + "name": "assets_skin_configuration_applies_to_asset_types_parent_fk", + "tableFrom": "assets_skin_configuration_applies_to_asset_types", + "tableTo": "assets", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets_crafting_crafting_data_inputs": { + "name": "assets_crafting_crafting_data_inputs", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "lended": { + "name": "lended", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "assets_crafting_crafting_data_inputs_order_idx": { + "name": "assets_crafting_crafting_data_inputs_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_crafting_crafting_data_inputs_parent_id_idx": { + "name": "assets_crafting_crafting_data_inputs_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_crafting_crafting_data_inputs_parent_id_fk": { + "name": "assets_crafting_crafting_data_inputs_parent_id_fk", + "tableFrom": "assets_crafting_crafting_data_inputs", + "tableTo": "assets", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets_crafting_crafting_data_byproducts": { + "name": "assets_crafting_crafting_data_byproducts", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "assets_crafting_crafting_data_byproducts_order_idx": { + "name": "assets_crafting_crafting_data_byproducts_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_crafting_crafting_data_byproducts_parent_id_idx": { + "name": "assets_crafting_crafting_data_byproducts_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_crafting_crafting_data_byproducts_parent_id_fk": { + "name": "assets_crafting_crafting_data_byproducts_parent_id_fk", + "tableFrom": "assets_crafting_crafting_data_byproducts", + "tableTo": "assets", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "approval_status": { + "name": "approval_status", + "type": "enum_assets_approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'in_progress'" + }, + "class_name": { + "name": "class_name", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "asset_type": { + "name": "asset_type", + "type": "enum_assets_asset_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'weapon'" + }, + "weapon_configuration_slot_type": { + "name": "weapon_configuration_slot_type", + "type": "enum_assets_weapon_configuration_slot_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "nightvision_configuration_nvg_generation": { + "name": "nightvision_configuration_nvg_generation", + "type": "enum_assets_nightvision_configuration_nvg_generation", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "nightvision_configuration_nvg_has_thermals": { + "name": "nightvision_configuration_nvg_has_thermals", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "container_configuration_capacity_kg": { + "name": "container_configuration_capacity_kg", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "container_configuration_weight_kg": { + "name": "container_configuration_weight_kg", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "magazine_configuration_round_capacity": { + "name": "magazine_configuration_round_capacity", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "magazine_configuration_ammo_type": { + "name": "magazine_configuration_ammo_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "magazine_configuration_greenmag_compatible": { + "name": "magazine_configuration_greenmag_compatible", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "asset_metadata_mod": { + "name": "asset_metadata_mod", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "asset_metadata_thumbnail_id": { + "name": "asset_metadata_thumbnail_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "asset_metadata_game_description": { + "name": "asset_metadata_game_description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "tradeable": { + "name": "tradeable", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "market_trading_rarity": { + "name": "market_trading_rarity", + "type": "enum_assets_market_trading_rarity", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "market_trading_perceived_value_base_buy_price": { + "name": "market_trading_perceived_value_base_buy_price", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "market_trading_perceived_value_base_sell_price": { + "name": "market_trading_perceived_value_base_sell_price", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "economy_enabled": { + "name": "economy_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "economy_baseline_supply": { + "name": "economy_baseline_supply", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 100 + }, + "economy_baseline_demand": { + "name": "economy_baseline_demand", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 50 + }, + "economy_max_modifier": { + "name": "economy_max_modifier", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "crafting_craftable": { + "name": "crafting_craftable", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "crafting_blueprint_required": { + "name": "crafting_blueprint_required", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "crafting_technology_required_to_use": { + "name": "crafting_technology_required_to_use", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "crafting_crafting_data_crafting_time_per_unit": { + "name": "crafting_crafting_data_crafting_time_per_unit", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "crafting_crafting_data_batch_size": { + "name": "crafting_crafting_data_batch_size", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "storage_dimensions_grid_width": { + "name": "storage_dimensions_grid_width", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "storage_dimensions_grid_height": { + "name": "storage_dimensions_grid_height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "storage_dimensions_stackable": { + "name": "storage_dimensions_stackable", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "storage_dimensions_max_stack_size": { + "name": "storage_dimensions_max_stack_size", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_dimensions_rotatable": { + "name": "storage_dimensions_rotatable", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "is_live": { + "name": "is_live", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assets_asset_metadata_asset_metadata_thumbnail_idx": { + "name": "assets_asset_metadata_asset_metadata_thumbnail_idx", + "columns": [ + { + "expression": "asset_metadata_thumbnail_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_updated_at_idx": { + "name": "assets_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_created_at_idx": { + "name": "assets_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_asset_metadata_thumbnail_id_media_id_fk": { + "name": "assets_asset_metadata_thumbnail_id_media_id_fk", + "tableFrom": "assets", + "tableTo": "media", + "columnsFrom": [ + "asset_metadata_thumbnail_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets_rels": { + "name": "assets_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "assets_id": { + "name": "assets_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "technologies_id": { + "name": "technologies_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "resources_id": { + "name": "resources_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "structures_id": { + "name": "structures_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "assets_rels_order_idx": { + "name": "assets_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_rels_parent_idx": { + "name": "assets_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_rels_path_idx": { + "name": "assets_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_rels_assets_id_idx": { + "name": "assets_rels_assets_id_idx", + "columns": [ + { + "expression": "assets_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_rels_technologies_id_idx": { + "name": "assets_rels_technologies_id_idx", + "columns": [ + { + "expression": "technologies_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_rels_resources_id_idx": { + "name": "assets_rels_resources_id_idx", + "columns": [ + { + "expression": "resources_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_rels_structures_id_idx": { + "name": "assets_rels_structures_id_idx", + "columns": [ + { + "expression": "structures_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_rels_parent_fk": { + "name": "assets_rels_parent_fk", + "tableFrom": "assets_rels", + "tableTo": "assets", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assets_rels_assets_fk": { + "name": "assets_rels_assets_fk", + "tableFrom": "assets_rels", + "tableTo": "assets", + "columnsFrom": [ + "assets_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assets_rels_technologies_fk": { + "name": "assets_rels_technologies_fk", + "tableFrom": "assets_rels", + "tableTo": "technologies", + "columnsFrom": [ + "technologies_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assets_rels_resources_fk": { + "name": "assets_rels_resources_fk", + "tableFrom": "assets_rels", + "tableTo": "resources", + "columnsFrom": [ + "resources_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "assets_rels_structures_fk": { + "name": "assets_rels_structures_fk", + "tableFrom": "assets_rels", + "tableTo": "structures", + "columnsFrom": [ + "structures_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resources": { + "name": "resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "code_name": { + "name": "code_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "approval_status": { + "name": "approval_status", + "type": "enum_resources_approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'in_progress'" + }, + "type": { + "name": "type", + "type": "enum_resources_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'physical'" + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "icon_id": { + "name": "icon_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "base_value": { + "name": "base_value", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "tradeable": { + "name": "tradeable", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "rarity": { + "name": "rarity", + "type": "enum_resources_rarity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'common'" + }, + "economy_enabled": { + "name": "economy_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "economy_baseline_supply": { + "name": "economy_baseline_supply", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 100 + }, + "economy_baseline_demand": { + "name": "economy_baseline_demand", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 50 + }, + "economy_max_modifier": { + "name": "economy_max_modifier", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "unit_of_measure": { + "name": "unit_of_measure", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "mass_per_unit": { + "name": "mass_per_unit", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stack_size": { + "name": "stack_size", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "grid_width": { + "name": "grid_width", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "grid_height": { + "name": "grid_height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "stackable": { + "name": "stackable", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "max_stack_size": { + "name": "max_stack_size", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "rotatable": { + "name": "rotatable", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "resources_icon_idx": { + "name": "resources_icon_idx", + "columns": [ + { + "expression": "icon_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resources_updated_at_idx": { + "name": "resources_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resources_created_at_idx": { + "name": "resources_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resources_icon_id_media_id_fk": { + "name": "resources_icon_id_media_id_fk", + "tableFrom": "resources", + "tableTo": "media", + "columnsFrom": [ + "icon_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vehicles_category": { + "name": "vehicles_category", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_vehicles_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "vehicles_category_order_idx": { + "name": "vehicles_category_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_category_parent_idx": { + "name": "vehicles_category_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vehicles_category_parent_fk": { + "name": "vehicles_category_parent_fk", + "tableFrom": "vehicles_category", + "tableTo": "vehicles", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vehicles_materials": { + "name": "vehicles_materials", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "vehicles_materials_order_idx": { + "name": "vehicles_materials_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_materials_parent_id_idx": { + "name": "vehicles_materials_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_materials_resource_idx": { + "name": "vehicles_materials_resource_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vehicles_materials_resource_id_resources_id_fk": { + "name": "vehicles_materials_resource_id_resources_id_fk", + "tableFrom": "vehicles_materials", + "tableTo": "resources", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "vehicles_materials_parent_id_fk": { + "name": "vehicles_materials_parent_id_fk", + "tableFrom": "vehicles_materials", + "tableTo": "vehicles", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vehicles_scrap_returns": { + "name": "vehicles_scrap_returns", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "chance": { + "name": "chance", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 100 + } + }, + "indexes": { + "vehicles_scrap_returns_order_idx": { + "name": "vehicles_scrap_returns_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_scrap_returns_parent_id_idx": { + "name": "vehicles_scrap_returns_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_scrap_returns_resource_idx": { + "name": "vehicles_scrap_returns_resource_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vehicles_scrap_returns_resource_id_resources_id_fk": { + "name": "vehicles_scrap_returns_resource_id_resources_id_fk", + "tableFrom": "vehicles_scrap_returns", + "tableTo": "resources", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "vehicles_scrap_returns_parent_id_fk": { + "name": "vehicles_scrap_returns_parent_id_fk", + "tableFrom": "vehicles_scrap_returns", + "tableTo": "vehicles", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vehicles_upkeep_costs": { + "name": "vehicles_upkeep_costs", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + } + }, + "indexes": { + "vehicles_upkeep_costs_order_idx": { + "name": "vehicles_upkeep_costs_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_upkeep_costs_parent_id_idx": { + "name": "vehicles_upkeep_costs_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_upkeep_costs_resource_idx": { + "name": "vehicles_upkeep_costs_resource_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vehicles_upkeep_costs_resource_id_resources_id_fk": { + "name": "vehicles_upkeep_costs_resource_id_resources_id_fk", + "tableFrom": "vehicles_upkeep_costs", + "tableTo": "resources", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "vehicles_upkeep_costs_parent_id_fk": { + "name": "vehicles_upkeep_costs_parent_id_fk", + "tableFrom": "vehicles_upkeep_costs", + "tableTo": "vehicles", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vehicles_allowed_virtual_cargo": { + "name": "vehicles_allowed_virtual_cargo", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + } + }, + "indexes": { + "vehicles_allowed_virtual_cargo_order_idx": { + "name": "vehicles_allowed_virtual_cargo_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_allowed_virtual_cargo_parent_id_idx": { + "name": "vehicles_allowed_virtual_cargo_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vehicles_allowed_virtual_cargo_parent_id_fk": { + "name": "vehicles_allowed_virtual_cargo_parent_id_fk", + "tableFrom": "vehicles_allowed_virtual_cargo", + "tableTo": "vehicles", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vehicles_pylons": { + "name": "vehicles_pylons", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "pylon_name": { + "name": "pylon_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "pylon_type": { + "name": "pylon_type", + "type": "enum_vehicles_pylons_pylon_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'mountedWeapon'" + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "ammo_type_id": { + "name": "ammo_type_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "vehicles_pylons_order_idx": { + "name": "vehicles_pylons_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_pylons_parent_id_idx": { + "name": "vehicles_pylons_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_pylons_ammo_type_idx": { + "name": "vehicles_pylons_ammo_type_idx", + "columns": [ + { + "expression": "ammo_type_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vehicles_pylons_ammo_type_id_assets_id_fk": { + "name": "vehicles_pylons_ammo_type_id_assets_id_fk", + "tableFrom": "vehicles_pylons", + "tableTo": "assets", + "columnsFrom": [ + "ammo_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "vehicles_pylons_parent_id_fk": { + "name": "vehicles_pylons_parent_id_fk", + "tableFrom": "vehicles_pylons", + "tableTo": "vehicles", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vehicles": { + "name": "vehicles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "classname": { + "name": "classname", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "approval_status": { + "name": "approval_status", + "type": "enum_vehicles_approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'in_progress'" + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "fuel_fuel_type_id": { + "name": "fuel_fuel_type_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "fuel_fuel_capacity": { + "name": "fuel_fuel_capacity", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "fuel_fuel_consumption_rate": { + "name": "fuel_fuel_consumption_rate", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "construction_time": { + "name": "construction_time", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "max_mass": { + "name": "max_mass", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "ace_cargo_slots": { + "name": "ace_cargo_slots", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "max_health": { + "name": "max_health", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "armor_rating": { + "name": "armor_rating", + "type": "enum_vehicles_armor_rating", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "max_speed_on_road": { + "name": "max_speed_on_road", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "max_speed_off_road": { + "name": "max_speed_off_road", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "max_airspeed": { + "name": "max_airspeed", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "max_knots": { + "name": "max_knots", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "vehicles_image_idx": { + "name": "vehicles_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_fuel_fuel_fuel_type_idx": { + "name": "vehicles_fuel_fuel_fuel_type_idx", + "columns": [ + { + "expression": "fuel_fuel_type_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_updated_at_idx": { + "name": "vehicles_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_created_at_idx": { + "name": "vehicles_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vehicles_image_id_media_id_fk": { + "name": "vehicles_image_id_media_id_fk", + "tableFrom": "vehicles", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "vehicles_fuel_fuel_type_id_resources_id_fk": { + "name": "vehicles_fuel_fuel_type_id_resources_id_fk", + "tableFrom": "vehicles", + "tableTo": "resources", + "columnsFrom": [ + "fuel_fuel_type_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vehicles_rels": { + "name": "vehicles_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "technologies_id": { + "name": "technologies_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "assets_id": { + "name": "assets_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vehicles_id": { + "name": "vehicles_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "resources_id": { + "name": "resources_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "vehicles_rels_order_idx": { + "name": "vehicles_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_rels_parent_idx": { + "name": "vehicles_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_rels_path_idx": { + "name": "vehicles_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_rels_technologies_id_idx": { + "name": "vehicles_rels_technologies_id_idx", + "columns": [ + { + "expression": "technologies_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_rels_assets_id_idx": { + "name": "vehicles_rels_assets_id_idx", + "columns": [ + { + "expression": "assets_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_rels_vehicles_id_idx": { + "name": "vehicles_rels_vehicles_id_idx", + "columns": [ + { + "expression": "vehicles_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "vehicles_rels_resources_id_idx": { + "name": "vehicles_rels_resources_id_idx", + "columns": [ + { + "expression": "resources_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vehicles_rels_parent_fk": { + "name": "vehicles_rels_parent_fk", + "tableFrom": "vehicles_rels", + "tableTo": "vehicles", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vehicles_rels_technologies_fk": { + "name": "vehicles_rels_technologies_fk", + "tableFrom": "vehicles_rels", + "tableTo": "technologies", + "columnsFrom": [ + "technologies_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vehicles_rels_assets_fk": { + "name": "vehicles_rels_assets_fk", + "tableFrom": "vehicles_rels", + "tableTo": "assets", + "columnsFrom": [ + "assets_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vehicles_rels_vehicles_fk": { + "name": "vehicles_rels_vehicles_fk", + "tableFrom": "vehicles_rels", + "tableTo": "vehicles", + "columnsFrom": [ + "vehicles_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vehicles_rels_resources_fk": { + "name": "vehicles_rels_resources_fk", + "tableFrom": "vehicles_rels", + "tableTo": "resources", + "columnsFrom": [ + "resources_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.structures_materials": { + "name": "structures_materials", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "structures_materials_order_idx": { + "name": "structures_materials_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_materials_parent_id_idx": { + "name": "structures_materials_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_materials_resource_idx": { + "name": "structures_materials_resource_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "structures_materials_resource_id_resources_id_fk": { + "name": "structures_materials_resource_id_resources_id_fk", + "tableFrom": "structures_materials", + "tableTo": "resources", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "structures_materials_parent_id_fk": { + "name": "structures_materials_parent_id_fk", + "tableFrom": "structures_materials", + "tableTo": "structures", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.structures_resources_in_range": { + "name": "structures_resources_in_range", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "range": { + "name": "range", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "structures_resources_in_range_order_idx": { + "name": "structures_resources_in_range_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_resources_in_range_parent_id_idx": { + "name": "structures_resources_in_range_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_resources_in_range_resource_idx": { + "name": "structures_resources_in_range_resource_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "structures_resources_in_range_resource_id_resources_id_fk": { + "name": "structures_resources_in_range_resource_id_resources_id_fk", + "tableFrom": "structures_resources_in_range", + "tableTo": "resources", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "structures_resources_in_range_parent_id_fk": { + "name": "structures_resources_in_range_parent_id_fk", + "tableFrom": "structures_resources_in_range", + "tableTo": "structures", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.structures_structures_in_range": { + "name": "structures_structures_in_range", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "structure_id": { + "name": "structure_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "range": { + "name": "range", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "structures_structures_in_range_order_idx": { + "name": "structures_structures_in_range_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_structures_in_range_parent_id_idx": { + "name": "structures_structures_in_range_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_structures_in_range_structure_idx": { + "name": "structures_structures_in_range_structure_idx", + "columns": [ + { + "expression": "structure_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "structures_structures_in_range_structure_id_structures_id_fk": { + "name": "structures_structures_in_range_structure_id_structures_id_fk", + "tableFrom": "structures_structures_in_range", + "tableTo": "structures", + "columnsFrom": [ + "structure_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "structures_structures_in_range_parent_id_fk": { + "name": "structures_structures_in_range_parent_id_fk", + "tableFrom": "structures_structures_in_range", + "tableTo": "structures", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.structures_upkeep": { + "name": "structures_upkeep", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "structures_upkeep_order_idx": { + "name": "structures_upkeep_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_upkeep_parent_id_idx": { + "name": "structures_upkeep_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_upkeep_resource_idx": { + "name": "structures_upkeep_resource_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "structures_upkeep_resource_id_resources_id_fk": { + "name": "structures_upkeep_resource_id_resources_id_fk", + "tableFrom": "structures_upkeep", + "tableTo": "resources", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "structures_upkeep_parent_id_fk": { + "name": "structures_upkeep_parent_id_fk", + "tableFrom": "structures_upkeep", + "tableTo": "structures", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.structures_labor_requirements": { + "name": "structures_labor_requirements", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "classification_id": { + "name": "classification_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "required_headcount": { + "name": "required_headcount", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_headcount": { + "name": "max_headcount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "enforced_salary": { + "name": "enforced_salary", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "structures_labor_requirements_order_idx": { + "name": "structures_labor_requirements_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_labor_requirements_parent_id_idx": { + "name": "structures_labor_requirements_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_labor_requirements_classification_idx": { + "name": "structures_labor_requirements_classification_idx", + "columns": [ + { + "expression": "classification_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "structures_labor_requirements_classification_id_labor_classifications_id_fk": { + "name": "structures_labor_requirements_classification_id_labor_classifications_id_fk", + "tableFrom": "structures_labor_requirements", + "tableTo": "labor_classifications", + "columnsFrom": [ + "classification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "structures_labor_requirements_parent_id_fk": { + "name": "structures_labor_requirements_parent_id_fk", + "tableFrom": "structures_labor_requirements", + "tableTo": "structures", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.structures_staff_positions": { + "name": "structures_staff_positions", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "position_type": { + "name": "position_type", + "type": "enum_structures_staff_positions_position_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "max": { + "name": "max", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "enforced_salary": { + "name": "enforced_salary", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "structures_staff_positions_order_idx": { + "name": "structures_staff_positions_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_staff_positions_parent_id_idx": { + "name": "structures_staff_positions_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "structures_staff_positions_parent_id_fk": { + "name": "structures_staff_positions_parent_id_fk", + "tableFrom": "structures_staff_positions", + "tableTo": "structures", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.structures_auto_generated_resources": { + "name": "structures_auto_generated_resources", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "min_amount": { + "name": "min_amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "max_amount": { + "name": "max_amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "min_frequency": { + "name": "min_frequency", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'1'" + }, + "max_frequency": { + "name": "max_frequency", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "produce_chance": { + "name": "produce_chance", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 100 + } + }, + "indexes": { + "structures_auto_generated_resources_order_idx": { + "name": "structures_auto_generated_resources_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_auto_generated_resources_parent_id_idx": { + "name": "structures_auto_generated_resources_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_auto_generated_resources_resource_idx": { + "name": "structures_auto_generated_resources_resource_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "structures_auto_generated_resources_resource_id_resources_id_fk": { + "name": "structures_auto_generated_resources_resource_id_resources_id_fk", + "tableFrom": "structures_auto_generated_resources", + "tableTo": "resources", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "structures_auto_generated_resources_parent_id_fk": { + "name": "structures_auto_generated_resources_parent_id_fk", + "tableFrom": "structures_auto_generated_resources", + "tableTo": "structures", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.structures_allowed_storage": { + "name": "structures_allowed_storage", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "structures_allowed_storage_order_idx": { + "name": "structures_allowed_storage_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_allowed_storage_parent_id_idx": { + "name": "structures_allowed_storage_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "structures_allowed_storage_parent_id_fk": { + "name": "structures_allowed_storage_parent_id_fk", + "tableFrom": "structures_allowed_storage", + "tableTo": "structures", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.structures_prohibited_storage": { + "name": "structures_prohibited_storage", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "structures_prohibited_storage_order_idx": { + "name": "structures_prohibited_storage_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_prohibited_storage_parent_id_idx": { + "name": "structures_prohibited_storage_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "structures_prohibited_storage_parent_id_fk": { + "name": "structures_prohibited_storage_parent_id_fk", + "tableFrom": "structures_prohibited_storage", + "tableTo": "structures", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.structures": { + "name": "structures", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "code_name": { + "name": "code_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "approval_status": { + "name": "approval_status", + "type": "enum_structures_approval_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'in_progress'" + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "enum_structures_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "construction_time": { + "name": "construction_time", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "requires_upgrade": { + "name": "requires_upgrade", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "upgrades_into_id": { + "name": "upgrades_into_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "upgrade_cost_multiplier": { + "name": "upgrade_cost_multiplier", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "terrain_type": { + "name": "terrain_type", + "type": "enum_structures_terrain_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sell_price": { + "name": "sell_price", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "maintenance_cost": { + "name": "maintenance_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "function_type": { + "name": "function_type", + "type": "enum_structures_function_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "max_capacity_mass": { + "name": "max_capacity_mass", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "restrict_to_allowed": { + "name": "restrict_to_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "x_size": { + "name": "x_size", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "y_size": { + "name": "y_size", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "max_health": { + "name": "max_health", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "repair_speed": { + "name": "repair_speed", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "structures_image_idx": { + "name": "structures_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_upgrades_into_idx": { + "name": "structures_upgrades_into_idx", + "columns": [ + { + "expression": "upgrades_into_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_updated_at_idx": { + "name": "structures_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_created_at_idx": { + "name": "structures_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "structures_image_id_media_id_fk": { + "name": "structures_image_id_media_id_fk", + "tableFrom": "structures", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "structures_upgrades_into_id_structures_id_fk": { + "name": "structures_upgrades_into_id_structures_id_fk", + "tableFrom": "structures", + "tableTo": "structures", + "columnsFrom": [ + "upgrades_into_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.structures_rels": { + "name": "structures_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "technologies_id": { + "name": "technologies_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "assets_id": { + "name": "assets_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vehicles_id": { + "name": "vehicles_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "resources_id": { + "name": "resources_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "structures_rels_order_idx": { + "name": "structures_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_rels_parent_idx": { + "name": "structures_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_rels_path_idx": { + "name": "structures_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_rels_technologies_id_idx": { + "name": "structures_rels_technologies_id_idx", + "columns": [ + { + "expression": "technologies_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_rels_assets_id_idx": { + "name": "structures_rels_assets_id_idx", + "columns": [ + { + "expression": "assets_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_rels_vehicles_id_idx": { + "name": "structures_rels_vehicles_id_idx", + "columns": [ + { + "expression": "vehicles_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "structures_rels_resources_id_idx": { + "name": "structures_rels_resources_id_idx", + "columns": [ + { + "expression": "resources_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "structures_rels_parent_fk": { + "name": "structures_rels_parent_fk", + "tableFrom": "structures_rels", + "tableTo": "structures", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "structures_rels_technologies_fk": { + "name": "structures_rels_technologies_fk", + "tableFrom": "structures_rels", + "tableTo": "technologies", + "columnsFrom": [ + "technologies_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "structures_rels_assets_fk": { + "name": "structures_rels_assets_fk", + "tableFrom": "structures_rels", + "tableTo": "assets", + "columnsFrom": [ + "assets_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "structures_rels_vehicles_fk": { + "name": "structures_rels_vehicles_fk", + "tableFrom": "structures_rels", + "tableTo": "vehicles", + "columnsFrom": [ + "vehicles_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "structures_rels_resources_fk": { + "name": "structures_rels_resources_fk", + "tableFrom": "structures_rels", + "tableTo": "resources", + "columnsFrom": [ + "resources_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shipments_cargo": { + "name": "shipments_cargo", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "shipments_cargo_order_idx": { + "name": "shipments_cargo_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shipments_cargo_parent_id_idx": { + "name": "shipments_cargo_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shipments_cargo_parent_id_fk": { + "name": "shipments_cargo_parent_id_fk", + "tableFrom": "shipments_cargo", + "tableTo": "shipments", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shipments": { + "name": "shipments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "origin_id": { + "name": "origin_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "destination_id": { + "name": "destination_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "transport_vehicle_id": { + "name": "transport_vehicle_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "distance": { + "name": "distance", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "fuel_cost": { + "name": "fuel_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "fuel_consumed": { + "name": "fuel_consumed", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "enum_shipments_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "dispatched_at": { + "name": "dispatched_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "estimated_arrival": { + "name": "estimated_arrival", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_return": { + "name": "auto_return", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "shipments_origin_idx": { + "name": "shipments_origin_idx", + "columns": [ + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shipments_destination_idx": { + "name": "shipments_destination_idx", + "columns": [ + { + "expression": "destination_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shipments_transport_vehicle_idx": { + "name": "shipments_transport_vehicle_idx", + "columns": [ + { + "expression": "transport_vehicle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shipments_created_by_idx": { + "name": "shipments_created_by_idx", + "columns": [ + { + "expression": "created_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shipments_updated_at_idx": { + "name": "shipments_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shipments_created_at_idx": { + "name": "shipments_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shipments_origin_id_game_structures_id_fk": { + "name": "shipments_origin_id_game_structures_id_fk", + "tableFrom": "shipments", + "tableTo": "game_structures", + "columnsFrom": [ + "origin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "shipments_destination_id_game_structures_id_fk": { + "name": "shipments_destination_id_game_structures_id_fk", + "tableFrom": "shipments", + "tableTo": "game_structures", + "columnsFrom": [ + "destination_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "shipments_transport_vehicle_id_game_vehicles_id_fk": { + "name": "shipments_transport_vehicle_id_game_vehicles_id_fk", + "tableFrom": "shipments", + "tableTo": "game_vehicles", + "columnsFrom": [ + "transport_vehicle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "shipments_created_by_id_users_id_fk": { + "name": "shipments_created_by_id_users_id_fk", + "tableFrom": "shipments", + "tableTo": "users", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shipments_rels": { + "name": "shipments_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "resources_id": { + "name": "resources_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "assets_id": { + "name": "assets_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vehicles_id": { + "name": "vehicles_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "shipments_rels_order_idx": { + "name": "shipments_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shipments_rels_parent_idx": { + "name": "shipments_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shipments_rels_path_idx": { + "name": "shipments_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shipments_rels_resources_id_idx": { + "name": "shipments_rels_resources_id_idx", + "columns": [ + { + "expression": "resources_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shipments_rels_assets_id_idx": { + "name": "shipments_rels_assets_id_idx", + "columns": [ + { + "expression": "assets_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shipments_rels_vehicles_id_idx": { + "name": "shipments_rels_vehicles_id_idx", + "columns": [ + { + "expression": "vehicles_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shipments_rels_parent_fk": { + "name": "shipments_rels_parent_fk", + "tableFrom": "shipments_rels", + "tableTo": "shipments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shipments_rels_resources_fk": { + "name": "shipments_rels_resources_fk", + "tableFrom": "shipments_rels", + "tableTo": "resources", + "columnsFrom": [ + "resources_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shipments_rels_assets_fk": { + "name": "shipments_rels_assets_fk", + "tableFrom": "shipments_rels", + "tableTo": "assets", + "columnsFrom": [ + "assets_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shipments_rels_vehicles_fk": { + "name": "shipments_rels_vehicles_fk", + "tableFrom": "shipments_rels", + "tableTo": "vehicles", + "columnsFrom": [ + "vehicles_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bank_accounts": { + "name": "bank_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "enum_bank_accounts_account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "owner_faction_id": { + "name": "owner_faction_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "currency_id": { + "name": "currency_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "balance": { + "name": "balance", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "enum_bank_accounts_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bank_accounts_owner_faction_idx": { + "name": "bank_accounts_owner_faction_idx", + "columns": [ + { + "expression": "owner_faction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bank_accounts_owner_user_idx": { + "name": "bank_accounts_owner_user_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bank_accounts_currency_idx": { + "name": "bank_accounts_currency_idx", + "columns": [ + { + "expression": "currency_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bank_accounts_updated_at_idx": { + "name": "bank_accounts_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bank_accounts_owner_faction_id_factions_id_fk": { + "name": "bank_accounts_owner_faction_id_factions_id_fk", + "tableFrom": "bank_accounts", + "tableTo": "factions", + "columnsFrom": [ + "owner_faction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "bank_accounts_owner_user_id_users_id_fk": { + "name": "bank_accounts_owner_user_id_users_id_fk", + "tableFrom": "bank_accounts", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "bank_accounts_currency_id_resources_id_fk": { + "name": "bank_accounts_currency_id_resources_id_fk", + "tableFrom": "bank_accounts", + "tableTo": "resources", + "columnsFrom": [ + "currency_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bank_transactions": { + "name": "bank_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "transaction_number": { + "name": "transaction_number", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "enum_bank_transactions_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "from_account_id": { + "name": "from_account_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "to_account_id": { + "name": "to_account_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "fee": { + "name": "fee", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "memo": { + "name": "memo", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_bank_transactions_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'completed'" + }, + "reference_id": { + "name": "reference_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bank_transactions_transaction_number_idx": { + "name": "bank_transactions_transaction_number_idx", + "columns": [ + { + "expression": "transaction_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bank_transactions_from_account_idx": { + "name": "bank_transactions_from_account_idx", + "columns": [ + { + "expression": "from_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bank_transactions_to_account_idx": { + "name": "bank_transactions_to_account_idx", + "columns": [ + { + "expression": "to_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bank_transactions_actor_idx": { + "name": "bank_transactions_actor_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bank_transactions_reference_idx": { + "name": "bank_transactions_reference_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bank_transactions_updated_at_idx": { + "name": "bank_transactions_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bank_transactions_created_at_idx": { + "name": "bank_transactions_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bank_transactions_from_account_id_bank_accounts_id_fk": { + "name": "bank_transactions_from_account_id_bank_accounts_id_fk", + "tableFrom": "bank_transactions", + "tableTo": "bank_accounts", + "columnsFrom": [ + "from_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "bank_transactions_to_account_id_bank_accounts_id_fk": { + "name": "bank_transactions_to_account_id_bank_accounts_id_fk", + "tableFrom": "bank_transactions", + "tableTo": "bank_accounts", + "columnsFrom": [ + "to_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "bank_transactions_actor_id_users_id_fk": { + "name": "bank_transactions_actor_id_users_id_fk", + "tableFrom": "bank_transactions", + "tableTo": "users", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "bank_transactions_reference_id_bank_transactions_id_fk": { + "name": "bank_transactions_reference_id_bank_transactions_id_fk", + "tableFrom": "bank_transactions", + "tableTo": "bank_transactions", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ledger_entries": { + "name": "ledger_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "transaction_id": { + "name": "transaction_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "enum_ledger_entries_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "balance_after": { + "name": "balance_after", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "memo": { + "name": "memo", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ledger_entries_account_idx": { + "name": "ledger_entries_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_transaction_idx": { + "name": "ledger_entries_transaction_idx", + "columns": [ + { + "expression": "transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_updated_at_idx": { + "name": "ledger_entries_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_created_at_idx": { + "name": "ledger_entries_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ledger_entries_account_id_bank_accounts_id_fk": { + "name": "ledger_entries_account_id_bank_accounts_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "bank_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ledger_entries_transaction_id_bank_transactions_id_fk": { + "name": "ledger_entries_transaction_id_bank_transactions_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "bank_transactions", + "columnsFrom": [ + "transaction_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.locker_storages_items_equipment_attachments": { + "name": "locker_storages_items_equipment_attachments", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "locker_storages_items_equipment_attachments_order_idx": { + "name": "locker_storages_items_equipment_attachments_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "locker_storages_items_equipment_attachments_parent_id_idx": { + "name": "locker_storages_items_equipment_attachments_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "locker_storages_items_equipment_attachments_asset_idx": { + "name": "locker_storages_items_equipment_attachments_asset_idx", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "locker_storages_items_equipment_attachments_asset_id_assets_id_fk": { + "name": "locker_storages_items_equipment_attachments_asset_id_assets_id_fk", + "tableFrom": "locker_storages_items_equipment_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "locker_storages_items_equipment_attachments_parent_id_fk": { + "name": "locker_storages_items_equipment_attachments_parent_id_fk", + "tableFrom": "locker_storages_items_equipment_attachments", + "tableTo": "locker_storages_items", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.locker_storages_items": { + "name": "locker_storages_items", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "grid_x": { + "name": "grid_x", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "grid_y": { + "name": "grid_y", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rotated": { + "name": "rotated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "equipment_skin_id": { + "name": "equipment_skin_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "equipment_custom_name": { + "name": "equipment_custom_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "locker_storages_items_order_idx": { + "name": "locker_storages_items_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "locker_storages_items_parent_id_idx": { + "name": "locker_storages_items_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "locker_storages_items_asset_idx": { + "name": "locker_storages_items_asset_idx", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "locker_storages_items_equipment_equipment_skin_idx": { + "name": "locker_storages_items_equipment_equipment_skin_idx", + "columns": [ + { + "expression": "equipment_skin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "locker_storages_items_asset_id_assets_id_fk": { + "name": "locker_storages_items_asset_id_assets_id_fk", + "tableFrom": "locker_storages_items", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "locker_storages_items_equipment_skin_id_assets_id_fk": { + "name": "locker_storages_items_equipment_skin_id_assets_id_fk", + "tableFrom": "locker_storages_items", + "tableTo": "assets", + "columnsFrom": [ + "equipment_skin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "locker_storages_items_parent_id_fk": { + "name": "locker_storages_items_parent_id_fk", + "tableFrom": "locker_storages_items", + "tableTo": "locker_storages", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.locker_storages": { + "name": "locker_storages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'Personal Locker'" + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "item_count": { + "name": "item_count", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "locker_storages_owner_user_idx": { + "name": "locker_storages_owner_user_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "locker_storages_updated_at_idx": { + "name": "locker_storages_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "locker_storages_created_at_idx": { + "name": "locker_storages_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "locker_storages_owner_user_id_users_id_fk": { + "name": "locker_storages_owner_user_id_users_id_fk", + "tableFrom": "locker_storages", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.loadouts": { + "name": "loadouts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "equipped": { + "name": "equipped", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "slots_primary_id": { + "name": "slots_primary_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slots_secondary_id": { + "name": "slots_secondary_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slots_launcher_id": { + "name": "slots_launcher_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slots_uniform_id": { + "name": "slots_uniform_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slots_vest_id": { + "name": "slots_vest_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slots_backpack_id": { + "name": "slots_backpack_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slots_helmet_id": { + "name": "slots_helmet_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slots_facewear_id": { + "name": "slots_facewear_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slots_nvg_id": { + "name": "slots_nvg_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slots_radio_id": { + "name": "slots_radio_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slots_optic_id": { + "name": "slots_optic_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slots_muzzle_id": { + "name": "slots_muzzle_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "slots_bipod_id": { + "name": "slots_bipod_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "loadouts_owner_user_idx": { + "name": "loadouts_owner_user_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_slots_slots_primary_idx": { + "name": "loadouts_slots_slots_primary_idx", + "columns": [ + { + "expression": "slots_primary_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_slots_slots_secondary_idx": { + "name": "loadouts_slots_slots_secondary_idx", + "columns": [ + { + "expression": "slots_secondary_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_slots_slots_launcher_idx": { + "name": "loadouts_slots_slots_launcher_idx", + "columns": [ + { + "expression": "slots_launcher_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_slots_slots_uniform_idx": { + "name": "loadouts_slots_slots_uniform_idx", + "columns": [ + { + "expression": "slots_uniform_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_slots_slots_vest_idx": { + "name": "loadouts_slots_slots_vest_idx", + "columns": [ + { + "expression": "slots_vest_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_slots_slots_backpack_idx": { + "name": "loadouts_slots_slots_backpack_idx", + "columns": [ + { + "expression": "slots_backpack_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_slots_slots_helmet_idx": { + "name": "loadouts_slots_slots_helmet_idx", + "columns": [ + { + "expression": "slots_helmet_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_slots_slots_facewear_idx": { + "name": "loadouts_slots_slots_facewear_idx", + "columns": [ + { + "expression": "slots_facewear_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_slots_slots_nvg_idx": { + "name": "loadouts_slots_slots_nvg_idx", + "columns": [ + { + "expression": "slots_nvg_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_slots_slots_radio_idx": { + "name": "loadouts_slots_slots_radio_idx", + "columns": [ + { + "expression": "slots_radio_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_slots_slots_optic_idx": { + "name": "loadouts_slots_slots_optic_idx", + "columns": [ + { + "expression": "slots_optic_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_slots_slots_muzzle_idx": { + "name": "loadouts_slots_slots_muzzle_idx", + "columns": [ + { + "expression": "slots_muzzle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_slots_slots_bipod_idx": { + "name": "loadouts_slots_slots_bipod_idx", + "columns": [ + { + "expression": "slots_bipod_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_updated_at_idx": { + "name": "loadouts_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "loadouts_created_at_idx": { + "name": "loadouts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "loadouts_owner_user_id_users_id_fk": { + "name": "loadouts_owner_user_id_users_id_fk", + "tableFrom": "loadouts", + "tableTo": "users", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "loadouts_slots_primary_id_assets_id_fk": { + "name": "loadouts_slots_primary_id_assets_id_fk", + "tableFrom": "loadouts", + "tableTo": "assets", + "columnsFrom": [ + "slots_primary_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "loadouts_slots_secondary_id_assets_id_fk": { + "name": "loadouts_slots_secondary_id_assets_id_fk", + "tableFrom": "loadouts", + "tableTo": "assets", + "columnsFrom": [ + "slots_secondary_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "loadouts_slots_launcher_id_assets_id_fk": { + "name": "loadouts_slots_launcher_id_assets_id_fk", + "tableFrom": "loadouts", + "tableTo": "assets", + "columnsFrom": [ + "slots_launcher_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "loadouts_slots_uniform_id_assets_id_fk": { + "name": "loadouts_slots_uniform_id_assets_id_fk", + "tableFrom": "loadouts", + "tableTo": "assets", + "columnsFrom": [ + "slots_uniform_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "loadouts_slots_vest_id_assets_id_fk": { + "name": "loadouts_slots_vest_id_assets_id_fk", + "tableFrom": "loadouts", + "tableTo": "assets", + "columnsFrom": [ + "slots_vest_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "loadouts_slots_backpack_id_assets_id_fk": { + "name": "loadouts_slots_backpack_id_assets_id_fk", + "tableFrom": "loadouts", + "tableTo": "assets", + "columnsFrom": [ + "slots_backpack_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "loadouts_slots_helmet_id_assets_id_fk": { + "name": "loadouts_slots_helmet_id_assets_id_fk", + "tableFrom": "loadouts", + "tableTo": "assets", + "columnsFrom": [ + "slots_helmet_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "loadouts_slots_facewear_id_assets_id_fk": { + "name": "loadouts_slots_facewear_id_assets_id_fk", + "tableFrom": "loadouts", + "tableTo": "assets", + "columnsFrom": [ + "slots_facewear_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "loadouts_slots_nvg_id_assets_id_fk": { + "name": "loadouts_slots_nvg_id_assets_id_fk", + "tableFrom": "loadouts", + "tableTo": "assets", + "columnsFrom": [ + "slots_nvg_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "loadouts_slots_radio_id_assets_id_fk": { + "name": "loadouts_slots_radio_id_assets_id_fk", + "tableFrom": "loadouts", + "tableTo": "assets", + "columnsFrom": [ + "slots_radio_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "loadouts_slots_optic_id_assets_id_fk": { + "name": "loadouts_slots_optic_id_assets_id_fk", + "tableFrom": "loadouts", + "tableTo": "assets", + "columnsFrom": [ + "slots_optic_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "loadouts_slots_muzzle_id_assets_id_fk": { + "name": "loadouts_slots_muzzle_id_assets_id_fk", + "tableFrom": "loadouts", + "tableTo": "assets", + "columnsFrom": [ + "slots_muzzle_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "loadouts_slots_bipod_id_assets_id_fk": { + "name": "loadouts_slots_bipod_id_assets_id_fk", + "tableFrom": "loadouts", + "tableTo": "assets", + "columnsFrom": [ + "slots_bipod_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.market_listings": { + "name": "market_listings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "seller_id": { + "name": "seller_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "npc_id": { + "name": "npc_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "price": { + "name": "price", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "desired_price": { + "name": "desired_price", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "min_price": { + "name": "min_price", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "currency_id": { + "name": "currency_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_market_listings_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "is_auto_generated": { + "name": "is_auto_generated", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "listed_at": { + "name": "listed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "sold_at": { + "name": "sold_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "buyer_id": { + "name": "buyer_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "market_listings_asset_idx": { + "name": "market_listings_asset_idx", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_listings_seller_idx": { + "name": "market_listings_seller_idx", + "columns": [ + { + "expression": "seller_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_listings_npc_idx": { + "name": "market_listings_npc_idx", + "columns": [ + { + "expression": "npc_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_listings_currency_idx": { + "name": "market_listings_currency_idx", + "columns": [ + { + "expression": "currency_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_listings_status_idx": { + "name": "market_listings_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_listings_buyer_idx": { + "name": "market_listings_buyer_idx", + "columns": [ + { + "expression": "buyer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_listings_updated_at_idx": { + "name": "market_listings_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_listings_created_at_idx": { + "name": "market_listings_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "market_listings_asset_id_assets_id_fk": { + "name": "market_listings_asset_id_assets_id_fk", + "tableFrom": "market_listings", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "market_listings_seller_id_users_id_fk": { + "name": "market_listings_seller_id_users_id_fk", + "tableFrom": "market_listings", + "tableTo": "users", + "columnsFrom": [ + "seller_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "market_listings_npc_id_game_npcs_id_fk": { + "name": "market_listings_npc_id_game_npcs_id_fk", + "tableFrom": "market_listings", + "tableTo": "game_npcs", + "columnsFrom": [ + "npc_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "market_listings_currency_id_resources_id_fk": { + "name": "market_listings_currency_id_resources_id_fk", + "tableFrom": "market_listings", + "tableTo": "resources", + "columnsFrom": [ + "currency_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "market_listings_buyer_id_users_id_fk": { + "name": "market_listings_buyer_id_users_id_fk", + "tableFrom": "market_listings", + "tableTo": "users", + "columnsFrom": [ + "buyer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.market_negotiations_history": { + "name": "market_negotiations_history", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "from": { + "name": "from", + "type": "enum_market_negotiations_history_from", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "at": { + "name": "at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "market_negotiations_history_order_idx": { + "name": "market_negotiations_history_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_negotiations_history_parent_id_idx": { + "name": "market_negotiations_history_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "market_negotiations_history_parent_id_fk": { + "name": "market_negotiations_history_parent_id_fk", + "tableFrom": "market_negotiations_history", + "tableTo": "market_negotiations", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.market_negotiations": { + "name": "market_negotiations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "listing_id": { + "name": "listing_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "buyer_id": { + "name": "buyer_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "enum_market_negotiations_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "patience": { + "name": "patience", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "quantity": { + "name": "quantity", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "amount": { + "name": "amount", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "proposed_by": { + "name": "proposed_by", + "type": "enum_market_negotiations_proposed_by", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'buyer'" + }, + "accepted_price": { + "name": "accepted_price", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "market_negotiations_listing_idx": { + "name": "market_negotiations_listing_idx", + "columns": [ + { + "expression": "listing_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_negotiations_buyer_idx": { + "name": "market_negotiations_buyer_idx", + "columns": [ + { + "expression": "buyer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_negotiations_status_idx": { + "name": "market_negotiations_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_negotiations_updated_at_idx": { + "name": "market_negotiations_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_negotiations_created_at_idx": { + "name": "market_negotiations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "market_negotiations_listing_id_market_listings_id_fk": { + "name": "market_negotiations_listing_id_market_listings_id_fk", + "tableFrom": "market_negotiations", + "tableTo": "market_listings", + "columnsFrom": [ + "listing_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "market_negotiations_buyer_id_users_id_fk": { + "name": "market_negotiations_buyer_id_users_id_fk", + "tableFrom": "market_negotiations", + "tableTo": "users", + "columnsFrom": [ + "buyer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.market_state": { + "name": "market_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "item_type": { + "name": "item_type", + "type": "enum_market_state_item_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "item_id": { + "name": "item_id", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "gm_baseline_supply": { + "name": "gm_baseline_supply", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "gm_baseline_demand": { + "name": "gm_baseline_demand", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "current_supply": { + "name": "current_supply", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "artificial_demand": { + "name": "artificial_demand", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "real_demand": { + "name": "real_demand", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "price_modifier": { + "name": "price_modifier", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_computed_at": { + "name": "last_computed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "market_state_item_type_idx": { + "name": "market_state_item_type_idx", + "columns": [ + { + "expression": "item_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_state_item_id_idx": { + "name": "market_state_item_id_idx", + "columns": [ + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_state_updated_at_idx": { + "name": "market_state_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "market_state_created_at_idx": { + "name": "market_state_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "itemType_itemId_idx": { + "name": "itemType_itemId_idx", + "columns": [ + { + "expression": "item_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.voice_scripts": { + "name": "voice_scripts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "script": { + "name": "script", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "xp_amount": { + "name": "xp_amount", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": 250 + }, + "spoiler": { + "name": "spoiler", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "status": { + "name": "status", + "type": "enum_voice_scripts_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "voice_scripts_updated_at_idx": { + "name": "voice_scripts_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "voice_scripts_created_at_idx": { + "name": "voice_scripts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.voice_submissions": { + "name": "voice_submissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "script_id": { + "name": "script_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_voice_submissions_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "review_note": { + "name": "review_note", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "voice_submissions_script_idx": { + "name": "voice_submissions_script_idx", + "columns": [ + { + "expression": "script_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "voice_submissions_user_idx": { + "name": "voice_submissions_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "voice_submissions_updated_at_idx": { + "name": "voice_submissions_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "voice_submissions_created_at_idx": { + "name": "voice_submissions_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "voice_submissions_filename_idx": { + "name": "voice_submissions_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "voice_submissions_script_id_voice_scripts_id_fk": { + "name": "voice_submissions_script_id_voice_scripts_id_fk", + "tableFrom": "voice_submissions", + "tableTo": "voice_scripts", + "columnsFrom": [ + "script_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "voice_submissions_user_id_users_id_fk": { + "name": "voice_submissions_user_id_users_id_fk", + "tableFrom": "voice_submissions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tickets_activity": { + "name": "tickets_activity", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "enum_tickets_activity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "tickets_activity_order_idx": { + "name": "tickets_activity_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_activity_parent_id_idx": { + "name": "tickets_activity_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_activity_author_idx": { + "name": "tickets_activity_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tickets_activity_author_id_users_id_fk": { + "name": "tickets_activity_author_id_users_id_fk", + "tableFrom": "tickets_activity", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tickets_activity_parent_id_fk": { + "name": "tickets_activity_parent_id_fk", + "tableFrom": "tickets_activity", + "tableTo": "tickets", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tickets_edit_history": { + "name": "tickets_edit_history", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "before": { + "name": "before", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "after": { + "name": "after", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "edited_by_id": { + "name": "edited_by_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "edited_at": { + "name": "edited_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tickets_edit_history_order_idx": { + "name": "tickets_edit_history_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_edit_history_parent_id_idx": { + "name": "tickets_edit_history_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_edit_history_edited_by_idx": { + "name": "tickets_edit_history_edited_by_idx", + "columns": [ + { + "expression": "edited_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tickets_edit_history_edited_by_id_users_id_fk": { + "name": "tickets_edit_history_edited_by_id_users_id_fk", + "tableFrom": "tickets_edit_history", + "tableTo": "users", + "columnsFrom": [ + "edited_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tickets_edit_history_parent_id_fk": { + "name": "tickets_edit_history_parent_id_fk", + "tableFrom": "tickets_edit_history", + "tableTo": "tickets", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tickets": { + "name": "tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "enum_tickets_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'support_request'" + }, + "priority": { + "name": "priority", + "type": "enum_tickets_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "enum_tickets_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "reporter_id": { + "name": "reporter_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "assignee_id": { + "name": "assignee_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sprint_id": { + "name": "sprint_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "release_id": { + "name": "release_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_ticket_id": { + "name": "parent_ticket_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "enum_tickets_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'task'" + }, + "story_points": { + "name": "story_points", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "estimate": { + "name": "estimate", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "resolution": { + "name": "resolution", + "type": "enum_tickets_resolution", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tickets_category_idx": { + "name": "tickets_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_priority_idx": { + "name": "tickets_priority_idx", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_status_idx": { + "name": "tickets_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_reporter_idx": { + "name": "tickets_reporter_idx", + "columns": [ + { + "expression": "reporter_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_assignee_idx": { + "name": "tickets_assignee_idx", + "columns": [ + { + "expression": "assignee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_project_idx": { + "name": "tickets_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_sprint_idx": { + "name": "tickets_sprint_idx", + "columns": [ + { + "expression": "sprint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_release_idx": { + "name": "tickets_release_idx", + "columns": [ + { + "expression": "release_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_parent_ticket_idx": { + "name": "tickets_parent_ticket_idx", + "columns": [ + { + "expression": "parent_ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_updated_at_idx": { + "name": "tickets_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_created_at_idx": { + "name": "tickets_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tickets_reporter_id_users_id_fk": { + "name": "tickets_reporter_id_users_id_fk", + "tableFrom": "tickets", + "tableTo": "users", + "columnsFrom": [ + "reporter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tickets_assignee_id_users_id_fk": { + "name": "tickets_assignee_id_users_id_fk", + "tableFrom": "tickets", + "tableTo": "users", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tickets_project_id_projects_id_fk": { + "name": "tickets_project_id_projects_id_fk", + "tableFrom": "tickets", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tickets_sprint_id_sprints_id_fk": { + "name": "tickets_sprint_id_sprints_id_fk", + "tableFrom": "tickets", + "tableTo": "sprints", + "columnsFrom": [ + "sprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tickets_release_id_releases_id_fk": { + "name": "tickets_release_id_releases_id_fk", + "tableFrom": "tickets", + "tableTo": "releases", + "columnsFrom": [ + "release_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tickets_parent_ticket_id_tickets_id_fk": { + "name": "tickets_parent_ticket_id_tickets_id_fk", + "tableFrom": "tickets", + "tableTo": "tickets", + "columnsFrom": [ + "parent_ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tickets_rels": { + "name": "tickets_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "labels_id": { + "name": "labels_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "tickets_rels_order_idx": { + "name": "tickets_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_rels_parent_idx": { + "name": "tickets_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_rels_path_idx": { + "name": "tickets_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tickets_rels_labels_id_idx": { + "name": "tickets_rels_labels_id_idx", + "columns": [ + { + "expression": "labels_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tickets_rels_parent_fk": { + "name": "tickets_rels_parent_fk", + "tableFrom": "tickets_rels", + "tableTo": "tickets", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tickets_rels_labels_fk": { + "name": "tickets_rels_labels_fk", + "tableFrom": "tickets_rels", + "tableTo": "labels", + "columnsFrom": [ + "labels_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ticket_votes": { + "name": "ticket_votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ticket_votes_ticket_idx": { + "name": "ticket_votes_ticket_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ticket_votes_user_idx": { + "name": "ticket_votes_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ticket_votes_updated_at_idx": { + "name": "ticket_votes_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ticket_votes_created_at_idx": { + "name": "ticket_votes_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ticket_user_idx": { + "name": "ticket_user_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ticket_votes_ticket_id_tickets_id_fk": { + "name": "ticket_votes_ticket_id_tickets_id_fk", + "tableFrom": "ticket_votes", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ticket_votes_user_id_users_id_fk": { + "name": "ticket_votes_user_id_users_id_fk", + "tableFrom": "ticket_votes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wiki_pages": { + "name": "wiki_pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "enum_wiki_pages_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "lockdown": { + "name": "lockdown", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "locked_by_id": { + "name": "locked_by_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "last_editor_id": { + "name": "last_editor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "wiki_pages_slug_idx": { + "name": "wiki_pages_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wiki_pages_category_idx": { + "name": "wiki_pages_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wiki_pages_locked_by_idx": { + "name": "wiki_pages_locked_by_idx", + "columns": [ + { + "expression": "locked_by_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wiki_pages_last_editor_idx": { + "name": "wiki_pages_last_editor_idx", + "columns": [ + { + "expression": "last_editor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wiki_pages_updated_at_idx": { + "name": "wiki_pages_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wiki_pages_created_at_idx": { + "name": "wiki_pages_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "wiki_pages_locked_by_id_users_id_fk": { + "name": "wiki_pages_locked_by_id_users_id_fk", + "tableFrom": "wiki_pages", + "tableTo": "users", + "columnsFrom": [ + "locked_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "wiki_pages_last_editor_id_users_id_fk": { + "name": "wiki_pages_last_editor_id_users_id_fk", + "tableFrom": "wiki_pages", + "tableTo": "users", + "columnsFrom": [ + "last_editor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wiki_pages_texts": { + "name": "wiki_pages_texts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "wiki_pages_texts_order_parent": { + "name": "wiki_pages_texts_order_parent", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "wiki_pages_texts_parent_fk": { + "name": "wiki_pages_texts_parent_fk", + "tableFrom": "wiki_pages_texts", + "tableTo": "wiki_pages", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wiki_revisions": { + "name": "wiki_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "title_snapshot": { + "name": "title_snapshot", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "content_snapshot": { + "name": "content_snapshot", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "editor_id": { + "name": "editor_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "enum_wiki_revisions_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "restored_from_revision": { + "name": "restored_from_revision", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "wiki_revisions_page_idx": { + "name": "wiki_revisions_page_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wiki_revisions_editor_idx": { + "name": "wiki_revisions_editor_idx", + "columns": [ + { + "expression": "editor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wiki_revisions_updated_at_idx": { + "name": "wiki_revisions_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wiki_revisions_created_at_idx": { + "name": "wiki_revisions_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "wiki_revisions_page_id_wiki_pages_id_fk": { + "name": "wiki_revisions_page_id_wiki_pages_id_fk", + "tableFrom": "wiki_revisions", + "tableTo": "wiki_pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "wiki_revisions_editor_id_users_id_fk": { + "name": "wiki_revisions_editor_id_users_id_fk", + "tableFrom": "wiki_revisions", + "tableTo": "users", + "columnsFrom": [ + "editor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.wiki_templates": { + "name": "wiki_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "css": { + "name": "css", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "html": { + "name": "html", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "wiki_templates_name_idx": { + "name": "wiki_templates_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wiki_templates_updated_at_idx": { + "name": "wiki_templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "wiki_templates_created_at_idx": { + "name": "wiki_templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "lead_id": { + "name": "lead_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_projects_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'active'" + }, + "default_assignee_id": { + "name": "default_assignee_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projects_key_idx": { + "name": "projects_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "projects_lead_idx": { + "name": "projects_lead_idx", + "columns": [ + { + "expression": "lead_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "projects_default_assignee_idx": { + "name": "projects_default_assignee_idx", + "columns": [ + { + "expression": "default_assignee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "projects_updated_at_idx": { + "name": "projects_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "projects_created_at_idx": { + "name": "projects_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_lead_id_users_id_fk": { + "name": "projects_lead_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "lead_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "projects_default_assignee_id_users_id_fk": { + "name": "projects_default_assignee_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "default_assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sprints": { + "name": "sprints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "goal": { + "name": "goal", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_sprints_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'planning'" + }, + "capacity": { + "name": "capacity", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sprints_project_idx": { + "name": "sprints_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sprints_updated_at_idx": { + "name": "sprints_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sprints_created_at_idx": { + "name": "sprints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sprints_project_id_projects_id_fk": { + "name": "sprints_project_id_projects_id_fk", + "tableFrom": "sprints", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.releases": { + "name": "releases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_releases_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "release_date": { + "name": "release_date", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "target_version": { + "name": "target_version", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "releases_project_idx": { + "name": "releases_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "releases_updated_at_idx": { + "name": "releases_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "releases_created_at_idx": { + "name": "releases_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "releases_project_id_projects_id_fk": { + "name": "releases_project_id_projects_id_fk", + "tableFrom": "releases", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'#6366f1'" + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "labels_project_idx": { + "name": "labels_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_updated_at_idx": { + "name": "labels_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_created_at_idx": { + "name": "labels_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_project_id_projects_id_fk": { + "name": "labels_project_id_projects_id_fk", + "tableFrom": "labels", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.maps": { + "name": "maps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "file_name": { + "name": "file_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "maps_name_idx": { + "name": "maps_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "maps_file_name_idx": { + "name": "maps_file_name_idx", + "columns": [ + { + "expression": "file_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "maps_updated_at_idx": { + "name": "maps_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "maps_created_at_idx": { + "name": "maps_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.narrative_events": { + "name": "narrative_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "flow_data": { + "name": "flow_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "triggers_type": { + "name": "triggers_type", + "type": "enum_narrative_events_triggers_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'manual'" + }, + "triggers_condition": { + "name": "triggers_condition", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "narrative_events_updated_at_idx": { + "name": "narrative_events_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "narrative_events_created_at_idx": { + "name": "narrative_events_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.story_beats": { + "name": "story_beats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "order": { + "name": "order", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "enum_story_beats_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "body": { + "name": "body", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "mission_id": { + "name": "mission_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "story_beats_status_idx": { + "name": "story_beats_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "story_beats_mission_idx": { + "name": "story_beats_mission_idx", + "columns": [ + { + "expression": "mission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "story_beats_updated_at_idx": { + "name": "story_beats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "story_beats_created_at_idx": { + "name": "story_beats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "story_beats_mission_id_missions_id_fk": { + "name": "story_beats_mission_id_missions_id_fk", + "tableFrom": "story_beats", + "tableTo": "missions", + "columnsFrom": [ + "mission_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.story_beat_states": { + "name": "story_beat_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "beat_id": { + "name": "beat_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "reaction": { + "name": "reaction", + "type": "enum_story_beat_states_reaction", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "story_beat_states_user_idx": { + "name": "story_beat_states_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "story_beat_states_beat_idx": { + "name": "story_beat_states_beat_idx", + "columns": [ + { + "expression": "beat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "story_beat_states_updated_at_idx": { + "name": "story_beat_states_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "story_beat_states_created_at_idx": { + "name": "story_beat_states_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "story_beat_states_user_id_users_id_fk": { + "name": "story_beat_states_user_id_users_id_fk", + "tableFrom": "story_beat_states", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "story_beat_states_beat_id_story_beats_id_fk": { + "name": "story_beat_states_beat_id_story_beats_id_fk", + "tableFrom": "story_beat_states", + "tableTo": "story_beats", + "columnsFrom": [ + "beat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mission_files": { + "name": "mission_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "mission_file_name": { + "name": "mission_file_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "mission_files_author_idx": { + "name": "mission_files_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_files_updated_at_idx": { + "name": "mission_files_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_files_created_at_idx": { + "name": "mission_files_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mission_files_filename_idx": { + "name": "mission_files_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mission_files_author_id_users_id_fk": { + "name": "mission_files_author_id_users_id_fk", + "tableFrom": "mission_files", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mod_lists": { + "name": "mod_lists", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "modlist_name": { + "name": "modlist_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "thumbnail_u_r_l": { + "name": "thumbnail_u_r_l", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "filesize": { + "name": "filesize", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_x": { + "name": "focal_x", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "focal_y": { + "name": "focal_y", + "type": "numeric", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "mod_lists_author_idx": { + "name": "mod_lists_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mod_lists_updated_at_idx": { + "name": "mod_lists_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mod_lists_created_at_idx": { + "name": "mod_lists_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mod_lists_filename_idx": { + "name": "mod_lists_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mod_lists_author_id_users_id_fk": { + "name": "mod_lists_author_id_users_id_fk", + "tableFrom": "mod_lists", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_servers": { + "name": "game_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "enum_game_servers_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'offline'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "game_servers_server_id_idx": { + "name": "game_servers_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_servers_updated_at_idx": { + "name": "game_servers_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_servers_created_at_idx": { + "name": "game_servers_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.arma_sync_events": { + "name": "arma_sync_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "arma_sync_events_event_id_idx": { + "name": "arma_sync_events_event_id_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "arma_sync_events_server_idx": { + "name": "arma_sync_events_server_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "arma_sync_events_type_idx": { + "name": "arma_sync_events_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "arma_sync_events_updated_at_idx": { + "name": "arma_sync_events_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "arma_sync_events_created_at_idx": { + "name": "arma_sync_events_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "arma_sync_events_server_id_game_servers_id_fk": { + "name": "arma_sync_events_server_id_game_servers_id_fk", + "tableFrom": "arma_sync_events", + "tableTo": "game_servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.arma_commands": { + "name": "arma_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "command_id": { + "name": "command_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "enum_arma_commands_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "arma_commands_command_id_idx": { + "name": "arma_commands_command_id_idx", + "columns": [ + { + "expression": "command_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "arma_commands_server_idx": { + "name": "arma_commands_server_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "arma_commands_updated_at_idx": { + "name": "arma_commands_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "arma_commands_created_at_idx": { + "name": "arma_commands_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "arma_commands_server_id_game_servers_id_fk": { + "name": "arma_commands_server_id_game_servers_id_fk", + "tableFrom": "arma_commands", + "tableTo": "game_servers", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_minefields": { + "name": "custom_minefields", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rows": { + "name": "rows", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "cols": { + "name": "cols", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "mines": { + "name": "mines", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "mine_count": { + "name": "mine_count", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "expose_edge_mines": { + "name": "expose_edge_mines", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "play_count": { + "name": "play_count", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "creator_xp_bank": { + "name": "creator_xp_bank", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_minefields_author_idx": { + "name": "custom_minefields_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_minefields_updated_at_idx": { + "name": "custom_minefields_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_minefields_created_at_idx": { + "name": "custom_minefields_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_minefields_author_id_users_id_fk": { + "name": "custom_minefields_author_id_users_id_fk", + "tableFrom": "custom_minefields", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_minefield_scores": { + "name": "custom_minefield_scores", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "minefield_id": { + "name": "minefield_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "seconds": { + "name": "seconds", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "flags": { + "name": "flags", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "won": { + "name": "won", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_minefield_scores_minefield_idx": { + "name": "custom_minefield_scores_minefield_idx", + "columns": [ + { + "expression": "minefield_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_minefield_scores_user_idx": { + "name": "custom_minefield_scores_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_minefield_scores_updated_at_idx": { + "name": "custom_minefield_scores_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_minefield_scores_created_at_idx": { + "name": "custom_minefield_scores_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_minefield_scores_minefield_id_custom_minefields_id_fk": { + "name": "custom_minefield_scores_minefield_id_custom_minefields_id_fk", + "tableFrom": "custom_minefield_scores", + "tableTo": "custom_minefields", + "columnsFrom": [ + "minefield_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "custom_minefield_scores_user_id_users_id_fk": { + "name": "custom_minefield_scores_user_id_users_id_fk", + "tableFrom": "custom_minefield_scores", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_radio_tests": { + "name": "custom_radio_tests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "message_count": { + "name": "message_count", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "order": { + "name": "order", + "type": "enum_custom_radio_tests_order", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'fixed'" + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "play_count": { + "name": "play_count", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_radio_tests_author_idx": { + "name": "custom_radio_tests_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_radio_tests_updated_at_idx": { + "name": "custom_radio_tests_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_radio_tests_created_at_idx": { + "name": "custom_radio_tests_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_radio_tests_author_id_users_id_fk": { + "name": "custom_radio_tests_author_id_users_id_fk", + "tableFrom": "custom_radio_tests", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_radio_test_scores": { + "name": "custom_radio_test_scores", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "test_id": { + "name": "test_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "accuracy": { + "name": "accuracy", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "correct_messages": { + "name": "correct_messages", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "incorrect_messages": { + "name": "incorrect_messages", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "enum_custom_radio_test_scores_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "time_ms": { + "name": "time_ms", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_radio_test_scores_test_idx": { + "name": "custom_radio_test_scores_test_idx", + "columns": [ + { + "expression": "test_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_radio_test_scores_user_idx": { + "name": "custom_radio_test_scores_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_radio_test_scores_updated_at_idx": { + "name": "custom_radio_test_scores_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_radio_test_scores_created_at_idx": { + "name": "custom_radio_test_scores_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_radio_test_scores_test_id_custom_radio_tests_id_fk": { + "name": "custom_radio_test_scores_test_id_custom_radio_tests_id_fk", + "tableFrom": "custom_radio_test_scores", + "tableTo": "custom_radio_tests", + "columnsFrom": [ + "test_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "custom_radio_test_scores_user_id_users_id_fk": { + "name": "custom_radio_test_scores_user_id_users_id_fk", + "tableFrom": "custom_radio_test_scores", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_ai_instructions_images": { + "name": "plugin_ai_instructions_images", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "image_id": { + "name": "image_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "plugin_ai_instructions_images_order_idx": { + "name": "plugin_ai_instructions_images_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_ai_instructions_images_parent_id_idx": { + "name": "plugin_ai_instructions_images_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_ai_instructions_images_image_idx": { + "name": "plugin_ai_instructions_images_image_idx", + "columns": [ + { + "expression": "image_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_ai_instructions_images_image_id_media_id_fk": { + "name": "plugin_ai_instructions_images_image_id_media_id_fk", + "tableFrom": "plugin_ai_instructions_images", + "tableTo": "media", + "columnsFrom": [ + "image_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "plugin_ai_instructions_images_parent_id_fk": { + "name": "plugin_ai_instructions_images_parent_id_fk", + "tableFrom": "plugin_ai_instructions_images", + "tableTo": "plugin_ai_instructions", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_ai_instructions": { + "name": "plugin_ai_instructions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "schema_path": { + "name": "schema_path", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "field_type": { + "name": "field_type", + "type": "enum_plugin_ai_instructions_field_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'text'" + }, + "relation_to": { + "name": "relation_to", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "model_id": { + "name": "model_id", + "type": "enum_plugin_ai_instructions_model_id", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "prompt": { + "name": "prompt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "system": { + "name": "system", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'INSTRUCTIONS:\nYou are a highly skilled and professional blog writer,\nrenowned for crafting engaging and well-organized articles.\nWhen given a title, you meticulously create blogs that are not only\ninformative and accurate but also captivating and beautifully structured.'" + }, + "layout": { + "name": "layout", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'[paragraph] - Write a concise introduction (2-3 sentences) that outlines the main topic.\n[horizontalrule] - Insert a horizontal rule to separate the introduction from the main content.\n[list] - Create a list with 3-5 items. Each list item should contain:\n a. [heading] - A brief, descriptive heading (up to 5 words)\n b. [paragraph] - A short explanation or elaboration (1-2 sentences)\n[horizontalrule] - Insert another horizontal rule to separate the main content from the conclusion.\n[paragraph] - Compose a brief conclusion (2-3 sentences) summarizing the key points.\n[quote] - Include a relevant quote from a famous person, directly related to the topic. Format: \"Quote text.\" - Author Name'" + }, + "unsloth_text_settings_model": { + "name": "unsloth_text_settings_model", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'unsloth/gemma-4-E4B-it-qat-GGUF'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_ai_instructions_schema_path_idx": { + "name": "plugin_ai_instructions_schema_path_idx", + "columns": [ + { + "expression": "schema_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_ai_instructions_updated_at_idx": { + "name": "plugin_ai_instructions_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_ai_instructions_created_at_idx": { + "name": "plugin_ai_instructions_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_mcp_api_keys": { + "name": "payload_mcp_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "media_find": { + "name": "media_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "media_create": { + "name": "media_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "media_update": { + "name": "media_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "media_delete": { + "name": "media_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_hard_resources_find": { + "name": "game_hard_resources_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_hard_resources_create": { + "name": "game_hard_resources_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_hard_resources_update": { + "name": "game_hard_resources_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_hard_resources_delete": { + "name": "game_hard_resources_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_structures_find": { + "name": "game_structures_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_structures_create": { + "name": "game_structures_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_structures_update": { + "name": "game_structures_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_structures_delete": { + "name": "game_structures_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_vehicles_find": { + "name": "game_vehicles_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_vehicles_create": { + "name": "game_vehicles_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_vehicles_update": { + "name": "game_vehicles_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_vehicles_delete": { + "name": "game_vehicles_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_npcs_find": { + "name": "game_npcs_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_npcs_create": { + "name": "game_npcs_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_npcs_update": { + "name": "game_npcs_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_npcs_delete": { + "name": "game_npcs_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "npc_staffing_find": { + "name": "npc_staffing_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "npc_staffing_create": { + "name": "npc_staffing_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "npc_staffing_update": { + "name": "npc_staffing_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "npc_staffing_delete": { + "name": "npc_staffing_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "labor_classifications_find": { + "name": "labor_classifications_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "labor_classifications_create": { + "name": "labor_classifications_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "labor_classifications_update": { + "name": "labor_classifications_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "labor_classifications_delete": { + "name": "labor_classifications_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_event_logs_find": { + "name": "game_event_logs_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_event_logs_create": { + "name": "game_event_logs_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_event_logs_update": { + "name": "game_event_logs_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_event_logs_delete": { + "name": "game_event_logs_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "users_find": { + "name": "users_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "users_create": { + "name": "users_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "users_update": { + "name": "users_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "users_delete": { + "name": "users_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "roles_find": { + "name": "roles_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "roles_create": { + "name": "roles_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "roles_update": { + "name": "roles_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "roles_delete": { + "name": "roles_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ranks_find": { + "name": "ranks_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ranks_create": { + "name": "ranks_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ranks_update": { + "name": "ranks_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ranks_delete": { + "name": "ranks_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "profiles_find": { + "name": "profiles_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "profiles_create": { + "name": "profiles_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "profiles_update": { + "name": "profiles_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "profiles_delete": { + "name": "profiles_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "awards_find": { + "name": "awards_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "awards_create": { + "name": "awards_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "awards_update": { + "name": "awards_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "awards_delete": { + "name": "awards_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "qualifications_find": { + "name": "qualifications_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "qualifications_create": { + "name": "qualifications_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "qualifications_update": { + "name": "qualifications_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "qualifications_delete": { + "name": "qualifications_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "assignments_find": { + "name": "assignments_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "assignments_create": { + "name": "assignments_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "assignments_update": { + "name": "assignments_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "assignments_delete": { + "name": "assignments_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "assignment_transfers_find": { + "name": "assignment_transfers_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "assignment_transfers_create": { + "name": "assignment_transfers_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "assignment_transfers_update": { + "name": "assignment_transfers_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "assignment_transfers_delete": { + "name": "assignment_transfers_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "experience_find": { + "name": "experience_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "experience_create": { + "name": "experience_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "experience_update": { + "name": "experience_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "experience_delete": { + "name": "experience_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_notifications_find": { + "name": "user_notifications_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_notifications_create": { + "name": "user_notifications_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_notifications_update": { + "name": "user_notifications_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_notifications_delete": { + "name": "user_notifications_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "evaluations_find": { + "name": "evaluations_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "evaluations_create": { + "name": "evaluations_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "evaluations_update": { + "name": "evaluations_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "evaluations_delete": { + "name": "evaluations_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ribbon_submissions_find": { + "name": "ribbon_submissions_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ribbon_submissions_create": { + "name": "ribbon_submissions_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ribbon_submissions_update": { + "name": "ribbon_submissions_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ribbon_submissions_delete": { + "name": "ribbon_submissions_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "missions_find": { + "name": "missions_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "missions_create": { + "name": "missions_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "missions_update": { + "name": "missions_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "missions_delete": { + "name": "missions_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mission_attendances_find": { + "name": "mission_attendances_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mission_attendances_create": { + "name": "mission_attendances_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mission_attendances_update": { + "name": "mission_attendances_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mission_attendances_delete": { + "name": "mission_attendances_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "campaigns_find": { + "name": "campaigns_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "campaigns_create": { + "name": "campaigns_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "campaigns_update": { + "name": "campaigns_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "campaigns_delete": { + "name": "campaigns_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "factions_find": { + "name": "factions_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "factions_create": { + "name": "factions_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "factions_update": { + "name": "factions_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "factions_delete": { + "name": "factions_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "technologies_find": { + "name": "technologies_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "technologies_create": { + "name": "technologies_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "technologies_update": { + "name": "technologies_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "technologies_delete": { + "name": "technologies_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "assets_find": { + "name": "assets_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "assets_create": { + "name": "assets_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "assets_update": { + "name": "assets_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "assets_delete": { + "name": "assets_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "resources_find": { + "name": "resources_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "resources_create": { + "name": "resources_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "resources_update": { + "name": "resources_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "resources_delete": { + "name": "resources_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vehicles_find": { + "name": "vehicles_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vehicles_create": { + "name": "vehicles_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vehicles_update": { + "name": "vehicles_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "vehicles_delete": { + "name": "vehicles_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "structures_find": { + "name": "structures_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "structures_create": { + "name": "structures_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "structures_update": { + "name": "structures_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "structures_delete": { + "name": "structures_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "shipments_find": { + "name": "shipments_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "shipments_create": { + "name": "shipments_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "shipments_update": { + "name": "shipments_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "shipments_delete": { + "name": "shipments_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "bank_accounts_find": { + "name": "bank_accounts_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "bank_accounts_create": { + "name": "bank_accounts_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "bank_accounts_update": { + "name": "bank_accounts_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "bank_accounts_delete": { + "name": "bank_accounts_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "bank_transactions_find": { + "name": "bank_transactions_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "bank_transactions_create": { + "name": "bank_transactions_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "bank_transactions_update": { + "name": "bank_transactions_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "bank_transactions_delete": { + "name": "bank_transactions_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ledger_entries_find": { + "name": "ledger_entries_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ledger_entries_create": { + "name": "ledger_entries_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ledger_entries_update": { + "name": "ledger_entries_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ledger_entries_delete": { + "name": "ledger_entries_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "locker_storages_find": { + "name": "locker_storages_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "locker_storages_create": { + "name": "locker_storages_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "locker_storages_update": { + "name": "locker_storages_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "locker_storages_delete": { + "name": "locker_storages_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "loadouts_find": { + "name": "loadouts_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "loadouts_create": { + "name": "loadouts_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "loadouts_update": { + "name": "loadouts_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "loadouts_delete": { + "name": "loadouts_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "market_listings_find": { + "name": "market_listings_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "market_listings_create": { + "name": "market_listings_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "market_listings_update": { + "name": "market_listings_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "market_listings_delete": { + "name": "market_listings_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "market_negotiations_find": { + "name": "market_negotiations_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "market_negotiations_create": { + "name": "market_negotiations_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "market_negotiations_update": { + "name": "market_negotiations_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "market_negotiations_delete": { + "name": "market_negotiations_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "market_state_find": { + "name": "market_state_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "market_state_create": { + "name": "market_state_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "market_state_update": { + "name": "market_state_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "market_state_delete": { + "name": "market_state_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "voice_scripts_find": { + "name": "voice_scripts_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "voice_scripts_create": { + "name": "voice_scripts_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "voice_scripts_update": { + "name": "voice_scripts_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "voice_scripts_delete": { + "name": "voice_scripts_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "voice_submissions_find": { + "name": "voice_submissions_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "voice_submissions_create": { + "name": "voice_submissions_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "voice_submissions_update": { + "name": "voice_submissions_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "voice_submissions_delete": { + "name": "voice_submissions_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tickets_find": { + "name": "tickets_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tickets_create": { + "name": "tickets_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tickets_update": { + "name": "tickets_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "tickets_delete": { + "name": "tickets_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ticket_votes_find": { + "name": "ticket_votes_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ticket_votes_create": { + "name": "ticket_votes_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ticket_votes_update": { + "name": "ticket_votes_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ticket_votes_delete": { + "name": "ticket_votes_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wiki_pages_find": { + "name": "wiki_pages_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wiki_pages_create": { + "name": "wiki_pages_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wiki_pages_update": { + "name": "wiki_pages_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wiki_pages_delete": { + "name": "wiki_pages_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wiki_revisions_find": { + "name": "wiki_revisions_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wiki_revisions_create": { + "name": "wiki_revisions_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wiki_revisions_update": { + "name": "wiki_revisions_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wiki_revisions_delete": { + "name": "wiki_revisions_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wiki_templates_find": { + "name": "wiki_templates_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wiki_templates_create": { + "name": "wiki_templates_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wiki_templates_update": { + "name": "wiki_templates_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "wiki_templates_delete": { + "name": "wiki_templates_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "projects_find": { + "name": "projects_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "projects_create": { + "name": "projects_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "projects_update": { + "name": "projects_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "projects_delete": { + "name": "projects_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sprints_find": { + "name": "sprints_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sprints_create": { + "name": "sprints_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sprints_update": { + "name": "sprints_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sprints_delete": { + "name": "sprints_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "releases_find": { + "name": "releases_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "releases_create": { + "name": "releases_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "releases_update": { + "name": "releases_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "releases_delete": { + "name": "releases_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "labels_find": { + "name": "labels_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "labels_create": { + "name": "labels_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "labels_update": { + "name": "labels_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "labels_delete": { + "name": "labels_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "maps_find": { + "name": "maps_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "maps_create": { + "name": "maps_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "maps_update": { + "name": "maps_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "maps_delete": { + "name": "maps_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "narrative_events_find": { + "name": "narrative_events_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "narrative_events_create": { + "name": "narrative_events_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "narrative_events_update": { + "name": "narrative_events_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "narrative_events_delete": { + "name": "narrative_events_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "story_beats_find": { + "name": "story_beats_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "story_beats_create": { + "name": "story_beats_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "story_beats_update": { + "name": "story_beats_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "story_beats_delete": { + "name": "story_beats_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "story_beat_states_find": { + "name": "story_beat_states_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "story_beat_states_create": { + "name": "story_beat_states_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "story_beat_states_update": { + "name": "story_beat_states_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "story_beat_states_delete": { + "name": "story_beat_states_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mission_files_find": { + "name": "mission_files_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mission_files_create": { + "name": "mission_files_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mission_files_update": { + "name": "mission_files_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mission_files_delete": { + "name": "mission_files_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mod_lists_find": { + "name": "mod_lists_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mod_lists_create": { + "name": "mod_lists_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mod_lists_update": { + "name": "mod_lists_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "mod_lists_delete": { + "name": "mod_lists_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_servers_find": { + "name": "game_servers_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_servers_create": { + "name": "game_servers_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_servers_update": { + "name": "game_servers_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "game_servers_delete": { + "name": "game_servers_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "arma_sync_events_find": { + "name": "arma_sync_events_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "arma_sync_events_create": { + "name": "arma_sync_events_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "arma_sync_events_update": { + "name": "arma_sync_events_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "arma_sync_events_delete": { + "name": "arma_sync_events_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "arma_commands_find": { + "name": "arma_commands_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "arma_commands_create": { + "name": "arma_commands_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "arma_commands_update": { + "name": "arma_commands_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "arma_commands_delete": { + "name": "arma_commands_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_minefields_find": { + "name": "custom_minefields_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_minefields_create": { + "name": "custom_minefields_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_minefields_update": { + "name": "custom_minefields_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_minefields_delete": { + "name": "custom_minefields_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_minefield_scores_find": { + "name": "custom_minefield_scores_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_minefield_scores_create": { + "name": "custom_minefield_scores_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_minefield_scores_update": { + "name": "custom_minefield_scores_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_minefield_scores_delete": { + "name": "custom_minefield_scores_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_radio_tests_find": { + "name": "custom_radio_tests_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_radio_tests_create": { + "name": "custom_radio_tests_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_radio_tests_update": { + "name": "custom_radio_tests_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_radio_tests_delete": { + "name": "custom_radio_tests_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_radio_test_scores_find": { + "name": "custom_radio_test_scores_find", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_radio_test_scores_create": { + "name": "custom_radio_test_scores_create", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_radio_test_scores_update": { + "name": "custom_radio_test_scores_update", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "custom_radio_test_scores_delete": { + "name": "custom_radio_test_scores_delete", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "enable_a_p_i_key": { + "name": "enable_a_p_i_key", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "api_key": { + "name": "api_key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "api_key_index": { + "name": "api_key_index", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_mcp_api_keys_user_idx": { + "name": "payload_mcp_api_keys_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_mcp_api_keys_updated_at_idx": { + "name": "payload_mcp_api_keys_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_mcp_api_keys_created_at_idx": { + "name": "payload_mcp_api_keys_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_mcp_api_keys_user_id_users_id_fk": { + "name": "payload_mcp_api_keys_user_id_users_id_fk", + "tableFrom": "payload_mcp_api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_kv": { + "name": "payload_kv", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "payload_kv_key_idx": { + "name": "payload_kv_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents": { + "name": "payload_locked_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "global_slug": { + "name": "global_slug", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_locked_documents_global_slug_idx": { + "name": "payload_locked_documents_global_slug_idx", + "columns": [ + { + "expression": "global_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_updated_at_idx": { + "name": "payload_locked_documents_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_created_at_idx": { + "name": "payload_locked_documents_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_locked_documents_rels": { + "name": "payload_locked_documents_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "game_hard_resources_id": { + "name": "game_hard_resources_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "game_structures_id": { + "name": "game_structures_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "game_vehicles_id": { + "name": "game_vehicles_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "game_npcs_id": { + "name": "game_npcs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "npc_staffing_id": { + "name": "npc_staffing_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "labor_classifications_id": { + "name": "labor_classifications_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "game_event_logs_id": { + "name": "game_event_logs_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "roles_id": { + "name": "roles_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ranks_id": { + "name": "ranks_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "profiles_id": { + "name": "profiles_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "awards_id": { + "name": "awards_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "qualifications_id": { + "name": "qualifications_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "assignments_id": { + "name": "assignments_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "assignment_transfers_id": { + "name": "assignment_transfers_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "experience_id": { + "name": "experience_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_notifications_id": { + "name": "user_notifications_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "evaluations_id": { + "name": "evaluations_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ribbon_submissions_id": { + "name": "ribbon_submissions_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "missions_id": { + "name": "missions_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mission_attendances_id": { + "name": "mission_attendances_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "campaigns_id": { + "name": "campaigns_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "factions_id": { + "name": "factions_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "technologies_id": { + "name": "technologies_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "assets_id": { + "name": "assets_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "resources_id": { + "name": "resources_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "vehicles_id": { + "name": "vehicles_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "structures_id": { + "name": "structures_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "shipments_id": { + "name": "shipments_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "bank_accounts_id": { + "name": "bank_accounts_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "bank_transactions_id": { + "name": "bank_transactions_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ledger_entries_id": { + "name": "ledger_entries_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "locker_storages_id": { + "name": "locker_storages_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "loadouts_id": { + "name": "loadouts_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "market_listings_id": { + "name": "market_listings_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "market_negotiations_id": { + "name": "market_negotiations_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "market_state_id": { + "name": "market_state_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "voice_scripts_id": { + "name": "voice_scripts_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "voice_submissions_id": { + "name": "voice_submissions_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tickets_id": { + "name": "tickets_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ticket_votes_id": { + "name": "ticket_votes_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "wiki_pages_id": { + "name": "wiki_pages_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "wiki_revisions_id": { + "name": "wiki_revisions_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "wiki_templates_id": { + "name": "wiki_templates_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "projects_id": { + "name": "projects_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "sprints_id": { + "name": "sprints_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "releases_id": { + "name": "releases_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "labels_id": { + "name": "labels_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "maps_id": { + "name": "maps_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "narrative_events_id": { + "name": "narrative_events_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "story_beats_id": { + "name": "story_beats_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "story_beat_states_id": { + "name": "story_beat_states_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mission_files_id": { + "name": "mission_files_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "mod_lists_id": { + "name": "mod_lists_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "game_servers_id": { + "name": "game_servers_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "arma_sync_events_id": { + "name": "arma_sync_events_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "arma_commands_id": { + "name": "arma_commands_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "custom_minefields_id": { + "name": "custom_minefields_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "custom_minefield_scores_id": { + "name": "custom_minefield_scores_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "custom_radio_tests_id": { + "name": "custom_radio_tests_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "custom_radio_test_scores_id": { + "name": "custom_radio_test_scores_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "plugin_ai_instructions_id": { + "name": "plugin_ai_instructions_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "payload_mcp_api_keys_id": { + "name": "payload_mcp_api_keys_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_locked_documents_rels_order_idx": { + "name": "payload_locked_documents_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_parent_idx": { + "name": "payload_locked_documents_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_path_idx": { + "name": "payload_locked_documents_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_media_id_idx": { + "name": "payload_locked_documents_rels_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_game_hard_resources_id_idx": { + "name": "payload_locked_documents_rels_game_hard_resources_id_idx", + "columns": [ + { + "expression": "game_hard_resources_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_game_structures_id_idx": { + "name": "payload_locked_documents_rels_game_structures_id_idx", + "columns": [ + { + "expression": "game_structures_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_game_vehicles_id_idx": { + "name": "payload_locked_documents_rels_game_vehicles_id_idx", + "columns": [ + { + "expression": "game_vehicles_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_game_npcs_id_idx": { + "name": "payload_locked_documents_rels_game_npcs_id_idx", + "columns": [ + { + "expression": "game_npcs_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_npc_staffing_id_idx": { + "name": "payload_locked_documents_rels_npc_staffing_id_idx", + "columns": [ + { + "expression": "npc_staffing_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_labor_classifications_id_idx": { + "name": "payload_locked_documents_rels_labor_classifications_id_idx", + "columns": [ + { + "expression": "labor_classifications_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_game_event_logs_id_idx": { + "name": "payload_locked_documents_rels_game_event_logs_id_idx", + "columns": [ + { + "expression": "game_event_logs_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_users_id_idx": { + "name": "payload_locked_documents_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_roles_id_idx": { + "name": "payload_locked_documents_rels_roles_id_idx", + "columns": [ + { + "expression": "roles_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_ranks_id_idx": { + "name": "payload_locked_documents_rels_ranks_id_idx", + "columns": [ + { + "expression": "ranks_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_profiles_id_idx": { + "name": "payload_locked_documents_rels_profiles_id_idx", + "columns": [ + { + "expression": "profiles_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_awards_id_idx": { + "name": "payload_locked_documents_rels_awards_id_idx", + "columns": [ + { + "expression": "awards_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_qualifications_id_idx": { + "name": "payload_locked_documents_rels_qualifications_id_idx", + "columns": [ + { + "expression": "qualifications_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_assignments_id_idx": { + "name": "payload_locked_documents_rels_assignments_id_idx", + "columns": [ + { + "expression": "assignments_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_assignment_transfers_id_idx": { + "name": "payload_locked_documents_rels_assignment_transfers_id_idx", + "columns": [ + { + "expression": "assignment_transfers_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_experience_id_idx": { + "name": "payload_locked_documents_rels_experience_id_idx", + "columns": [ + { + "expression": "experience_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_user_notifications_id_idx": { + "name": "payload_locked_documents_rels_user_notifications_id_idx", + "columns": [ + { + "expression": "user_notifications_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_evaluations_id_idx": { + "name": "payload_locked_documents_rels_evaluations_id_idx", + "columns": [ + { + "expression": "evaluations_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_ribbon_submissions_id_idx": { + "name": "payload_locked_documents_rels_ribbon_submissions_id_idx", + "columns": [ + { + "expression": "ribbon_submissions_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_missions_id_idx": { + "name": "payload_locked_documents_rels_missions_id_idx", + "columns": [ + { + "expression": "missions_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_mission_attendances_id_idx": { + "name": "payload_locked_documents_rels_mission_attendances_id_idx", + "columns": [ + { + "expression": "mission_attendances_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_campaigns_id_idx": { + "name": "payload_locked_documents_rels_campaigns_id_idx", + "columns": [ + { + "expression": "campaigns_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_factions_id_idx": { + "name": "payload_locked_documents_rels_factions_id_idx", + "columns": [ + { + "expression": "factions_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_technologies_id_idx": { + "name": "payload_locked_documents_rels_technologies_id_idx", + "columns": [ + { + "expression": "technologies_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_assets_id_idx": { + "name": "payload_locked_documents_rels_assets_id_idx", + "columns": [ + { + "expression": "assets_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_resources_id_idx": { + "name": "payload_locked_documents_rels_resources_id_idx", + "columns": [ + { + "expression": "resources_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_vehicles_id_idx": { + "name": "payload_locked_documents_rels_vehicles_id_idx", + "columns": [ + { + "expression": "vehicles_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_structures_id_idx": { + "name": "payload_locked_documents_rels_structures_id_idx", + "columns": [ + { + "expression": "structures_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_shipments_id_idx": { + "name": "payload_locked_documents_rels_shipments_id_idx", + "columns": [ + { + "expression": "shipments_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_bank_accounts_id_idx": { + "name": "payload_locked_documents_rels_bank_accounts_id_idx", + "columns": [ + { + "expression": "bank_accounts_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_bank_transactions_id_idx": { + "name": "payload_locked_documents_rels_bank_transactions_id_idx", + "columns": [ + { + "expression": "bank_transactions_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_ledger_entries_id_idx": { + "name": "payload_locked_documents_rels_ledger_entries_id_idx", + "columns": [ + { + "expression": "ledger_entries_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_locker_storages_id_idx": { + "name": "payload_locked_documents_rels_locker_storages_id_idx", + "columns": [ + { + "expression": "locker_storages_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_loadouts_id_idx": { + "name": "payload_locked_documents_rels_loadouts_id_idx", + "columns": [ + { + "expression": "loadouts_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_market_listings_id_idx": { + "name": "payload_locked_documents_rels_market_listings_id_idx", + "columns": [ + { + "expression": "market_listings_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_market_negotiations_id_idx": { + "name": "payload_locked_documents_rels_market_negotiations_id_idx", + "columns": [ + { + "expression": "market_negotiations_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_market_state_id_idx": { + "name": "payload_locked_documents_rels_market_state_id_idx", + "columns": [ + { + "expression": "market_state_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_voice_scripts_id_idx": { + "name": "payload_locked_documents_rels_voice_scripts_id_idx", + "columns": [ + { + "expression": "voice_scripts_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_voice_submissions_id_idx": { + "name": "payload_locked_documents_rels_voice_submissions_id_idx", + "columns": [ + { + "expression": "voice_submissions_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_tickets_id_idx": { + "name": "payload_locked_documents_rels_tickets_id_idx", + "columns": [ + { + "expression": "tickets_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_ticket_votes_id_idx": { + "name": "payload_locked_documents_rels_ticket_votes_id_idx", + "columns": [ + { + "expression": "ticket_votes_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_wiki_pages_id_idx": { + "name": "payload_locked_documents_rels_wiki_pages_id_idx", + "columns": [ + { + "expression": "wiki_pages_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_wiki_revisions_id_idx": { + "name": "payload_locked_documents_rels_wiki_revisions_id_idx", + "columns": [ + { + "expression": "wiki_revisions_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_wiki_templates_id_idx": { + "name": "payload_locked_documents_rels_wiki_templates_id_idx", + "columns": [ + { + "expression": "wiki_templates_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_projects_id_idx": { + "name": "payload_locked_documents_rels_projects_id_idx", + "columns": [ + { + "expression": "projects_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_sprints_id_idx": { + "name": "payload_locked_documents_rels_sprints_id_idx", + "columns": [ + { + "expression": "sprints_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_releases_id_idx": { + "name": "payload_locked_documents_rels_releases_id_idx", + "columns": [ + { + "expression": "releases_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_labels_id_idx": { + "name": "payload_locked_documents_rels_labels_id_idx", + "columns": [ + { + "expression": "labels_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_maps_id_idx": { + "name": "payload_locked_documents_rels_maps_id_idx", + "columns": [ + { + "expression": "maps_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_narrative_events_id_idx": { + "name": "payload_locked_documents_rels_narrative_events_id_idx", + "columns": [ + { + "expression": "narrative_events_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_story_beats_id_idx": { + "name": "payload_locked_documents_rels_story_beats_id_idx", + "columns": [ + { + "expression": "story_beats_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_story_beat_states_id_idx": { + "name": "payload_locked_documents_rels_story_beat_states_id_idx", + "columns": [ + { + "expression": "story_beat_states_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_mission_files_id_idx": { + "name": "payload_locked_documents_rels_mission_files_id_idx", + "columns": [ + { + "expression": "mission_files_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_mod_lists_id_idx": { + "name": "payload_locked_documents_rels_mod_lists_id_idx", + "columns": [ + { + "expression": "mod_lists_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_game_servers_id_idx": { + "name": "payload_locked_documents_rels_game_servers_id_idx", + "columns": [ + { + "expression": "game_servers_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_arma_sync_events_id_idx": { + "name": "payload_locked_documents_rels_arma_sync_events_id_idx", + "columns": [ + { + "expression": "arma_sync_events_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_arma_commands_id_idx": { + "name": "payload_locked_documents_rels_arma_commands_id_idx", + "columns": [ + { + "expression": "arma_commands_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_custom_minefields_id_idx": { + "name": "payload_locked_documents_rels_custom_minefields_id_idx", + "columns": [ + { + "expression": "custom_minefields_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_custom_minefield_scores_id_idx": { + "name": "payload_locked_documents_rels_custom_minefield_scores_id_idx", + "columns": [ + { + "expression": "custom_minefield_scores_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_custom_radio_tests_id_idx": { + "name": "payload_locked_documents_rels_custom_radio_tests_id_idx", + "columns": [ + { + "expression": "custom_radio_tests_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_custom_radio_test_scores_i_idx": { + "name": "payload_locked_documents_rels_custom_radio_test_scores_i_idx", + "columns": [ + { + "expression": "custom_radio_test_scores_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_plugin_ai_instructions_id_idx": { + "name": "payload_locked_documents_rels_plugin_ai_instructions_id_idx", + "columns": [ + { + "expression": "plugin_ai_instructions_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_locked_documents_rels_payload_mcp_api_keys_id_idx": { + "name": "payload_locked_documents_rels_payload_mcp_api_keys_id_idx", + "columns": [ + { + "expression": "payload_mcp_api_keys_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_locked_documents_rels_parent_fk": { + "name": "payload_locked_documents_rels_parent_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "payload_locked_documents", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_media_fk": { + "name": "payload_locked_documents_rels_media_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_game_hard_resources_fk": { + "name": "payload_locked_documents_rels_game_hard_resources_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "game_hard_resources", + "columnsFrom": [ + "game_hard_resources_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_game_structures_fk": { + "name": "payload_locked_documents_rels_game_structures_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "game_structures", + "columnsFrom": [ + "game_structures_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_game_vehicles_fk": { + "name": "payload_locked_documents_rels_game_vehicles_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "game_vehicles", + "columnsFrom": [ + "game_vehicles_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_game_npcs_fk": { + "name": "payload_locked_documents_rels_game_npcs_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "game_npcs", + "columnsFrom": [ + "game_npcs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_npc_staffing_fk": { + "name": "payload_locked_documents_rels_npc_staffing_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "npc_staffing", + "columnsFrom": [ + "npc_staffing_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_labor_classifications_fk": { + "name": "payload_locked_documents_rels_labor_classifications_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "labor_classifications", + "columnsFrom": [ + "labor_classifications_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_game_event_logs_fk": { + "name": "payload_locked_documents_rels_game_event_logs_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "game_event_logs", + "columnsFrom": [ + "game_event_logs_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_users_fk": { + "name": "payload_locked_documents_rels_users_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_roles_fk": { + "name": "payload_locked_documents_rels_roles_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "roles", + "columnsFrom": [ + "roles_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_ranks_fk": { + "name": "payload_locked_documents_rels_ranks_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "ranks", + "columnsFrom": [ + "ranks_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_profiles_fk": { + "name": "payload_locked_documents_rels_profiles_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "profiles", + "columnsFrom": [ + "profiles_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_awards_fk": { + "name": "payload_locked_documents_rels_awards_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "awards", + "columnsFrom": [ + "awards_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_qualifications_fk": { + "name": "payload_locked_documents_rels_qualifications_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "qualifications", + "columnsFrom": [ + "qualifications_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_assignments_fk": { + "name": "payload_locked_documents_rels_assignments_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "assignments", + "columnsFrom": [ + "assignments_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_assignment_transfers_fk": { + "name": "payload_locked_documents_rels_assignment_transfers_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "assignment_transfers", + "columnsFrom": [ + "assignment_transfers_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_experience_fk": { + "name": "payload_locked_documents_rels_experience_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "experience", + "columnsFrom": [ + "experience_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_user_notifications_fk": { + "name": "payload_locked_documents_rels_user_notifications_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "user_notifications", + "columnsFrom": [ + "user_notifications_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_evaluations_fk": { + "name": "payload_locked_documents_rels_evaluations_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "evaluations", + "columnsFrom": [ + "evaluations_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_ribbon_submissions_fk": { + "name": "payload_locked_documents_rels_ribbon_submissions_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "ribbon_submissions", + "columnsFrom": [ + "ribbon_submissions_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_missions_fk": { + "name": "payload_locked_documents_rels_missions_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "missions", + "columnsFrom": [ + "missions_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_mission_attendances_fk": { + "name": "payload_locked_documents_rels_mission_attendances_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "mission_attendances", + "columnsFrom": [ + "mission_attendances_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_campaigns_fk": { + "name": "payload_locked_documents_rels_campaigns_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "campaigns", + "columnsFrom": [ + "campaigns_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_factions_fk": { + "name": "payload_locked_documents_rels_factions_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "factions", + "columnsFrom": [ + "factions_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_technologies_fk": { + "name": "payload_locked_documents_rels_technologies_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "technologies", + "columnsFrom": [ + "technologies_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_assets_fk": { + "name": "payload_locked_documents_rels_assets_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "assets", + "columnsFrom": [ + "assets_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_resources_fk": { + "name": "payload_locked_documents_rels_resources_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "resources", + "columnsFrom": [ + "resources_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_vehicles_fk": { + "name": "payload_locked_documents_rels_vehicles_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "vehicles", + "columnsFrom": [ + "vehicles_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_structures_fk": { + "name": "payload_locked_documents_rels_structures_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "structures", + "columnsFrom": [ + "structures_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_shipments_fk": { + "name": "payload_locked_documents_rels_shipments_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "shipments", + "columnsFrom": [ + "shipments_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_bank_accounts_fk": { + "name": "payload_locked_documents_rels_bank_accounts_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "bank_accounts", + "columnsFrom": [ + "bank_accounts_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_bank_transactions_fk": { + "name": "payload_locked_documents_rels_bank_transactions_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "bank_transactions", + "columnsFrom": [ + "bank_transactions_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_ledger_entries_fk": { + "name": "payload_locked_documents_rels_ledger_entries_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "ledger_entries", + "columnsFrom": [ + "ledger_entries_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_locker_storages_fk": { + "name": "payload_locked_documents_rels_locker_storages_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "locker_storages", + "columnsFrom": [ + "locker_storages_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_loadouts_fk": { + "name": "payload_locked_documents_rels_loadouts_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "loadouts", + "columnsFrom": [ + "loadouts_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_market_listings_fk": { + "name": "payload_locked_documents_rels_market_listings_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "market_listings", + "columnsFrom": [ + "market_listings_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_market_negotiations_fk": { + "name": "payload_locked_documents_rels_market_negotiations_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "market_negotiations", + "columnsFrom": [ + "market_negotiations_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_market_state_fk": { + "name": "payload_locked_documents_rels_market_state_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "market_state", + "columnsFrom": [ + "market_state_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_voice_scripts_fk": { + "name": "payload_locked_documents_rels_voice_scripts_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "voice_scripts", + "columnsFrom": [ + "voice_scripts_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_voice_submissions_fk": { + "name": "payload_locked_documents_rels_voice_submissions_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "voice_submissions", + "columnsFrom": [ + "voice_submissions_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_tickets_fk": { + "name": "payload_locked_documents_rels_tickets_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "tickets", + "columnsFrom": [ + "tickets_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_ticket_votes_fk": { + "name": "payload_locked_documents_rels_ticket_votes_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "ticket_votes", + "columnsFrom": [ + "ticket_votes_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_wiki_pages_fk": { + "name": "payload_locked_documents_rels_wiki_pages_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "wiki_pages", + "columnsFrom": [ + "wiki_pages_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_wiki_revisions_fk": { + "name": "payload_locked_documents_rels_wiki_revisions_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "wiki_revisions", + "columnsFrom": [ + "wiki_revisions_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_wiki_templates_fk": { + "name": "payload_locked_documents_rels_wiki_templates_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "wiki_templates", + "columnsFrom": [ + "wiki_templates_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_projects_fk": { + "name": "payload_locked_documents_rels_projects_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "projects", + "columnsFrom": [ + "projects_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_sprints_fk": { + "name": "payload_locked_documents_rels_sprints_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "sprints", + "columnsFrom": [ + "sprints_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_releases_fk": { + "name": "payload_locked_documents_rels_releases_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "releases", + "columnsFrom": [ + "releases_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_labels_fk": { + "name": "payload_locked_documents_rels_labels_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "labels", + "columnsFrom": [ + "labels_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_maps_fk": { + "name": "payload_locked_documents_rels_maps_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "maps", + "columnsFrom": [ + "maps_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_narrative_events_fk": { + "name": "payload_locked_documents_rels_narrative_events_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "narrative_events", + "columnsFrom": [ + "narrative_events_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_story_beats_fk": { + "name": "payload_locked_documents_rels_story_beats_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "story_beats", + "columnsFrom": [ + "story_beats_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_story_beat_states_fk": { + "name": "payload_locked_documents_rels_story_beat_states_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "story_beat_states", + "columnsFrom": [ + "story_beat_states_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_mission_files_fk": { + "name": "payload_locked_documents_rels_mission_files_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "mission_files", + "columnsFrom": [ + "mission_files_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_mod_lists_fk": { + "name": "payload_locked_documents_rels_mod_lists_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "mod_lists", + "columnsFrom": [ + "mod_lists_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_game_servers_fk": { + "name": "payload_locked_documents_rels_game_servers_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "game_servers", + "columnsFrom": [ + "game_servers_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_arma_sync_events_fk": { + "name": "payload_locked_documents_rels_arma_sync_events_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "arma_sync_events", + "columnsFrom": [ + "arma_sync_events_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_arma_commands_fk": { + "name": "payload_locked_documents_rels_arma_commands_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "arma_commands", + "columnsFrom": [ + "arma_commands_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_custom_minefields_fk": { + "name": "payload_locked_documents_rels_custom_minefields_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "custom_minefields", + "columnsFrom": [ + "custom_minefields_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_custom_minefield_scores_fk": { + "name": "payload_locked_documents_rels_custom_minefield_scores_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "custom_minefield_scores", + "columnsFrom": [ + "custom_minefield_scores_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_custom_radio_tests_fk": { + "name": "payload_locked_documents_rels_custom_radio_tests_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "custom_radio_tests", + "columnsFrom": [ + "custom_radio_tests_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_custom_radio_test_scores_fk": { + "name": "payload_locked_documents_rels_custom_radio_test_scores_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "custom_radio_test_scores", + "columnsFrom": [ + "custom_radio_test_scores_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_plugin_ai_instructions_fk": { + "name": "payload_locked_documents_rels_plugin_ai_instructions_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "plugin_ai_instructions", + "columnsFrom": [ + "plugin_ai_instructions_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_locked_documents_rels_payload_mcp_api_keys_fk": { + "name": "payload_locked_documents_rels_payload_mcp_api_keys_fk", + "tableFrom": "payload_locked_documents_rels", + "tableTo": "payload_mcp_api_keys", + "columnsFrom": [ + "payload_mcp_api_keys_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences": { + "name": "payload_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_preferences_key_idx": { + "name": "payload_preferences_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_updated_at_idx": { + "name": "payload_preferences_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_created_at_idx": { + "name": "payload_preferences_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_preferences_rels": { + "name": "payload_preferences_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "payload_mcp_api_keys_id": { + "name": "payload_mcp_api_keys_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "payload_preferences_rels_order_idx": { + "name": "payload_preferences_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_parent_idx": { + "name": "payload_preferences_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_path_idx": { + "name": "payload_preferences_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_users_id_idx": { + "name": "payload_preferences_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_preferences_rels_payload_mcp_api_keys_id_idx": { + "name": "payload_preferences_rels_payload_mcp_api_keys_id_idx", + "columns": [ + { + "expression": "payload_mcp_api_keys_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payload_preferences_rels_parent_fk": { + "name": "payload_preferences_rels_parent_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "payload_preferences", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_users_fk": { + "name": "payload_preferences_rels_users_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "payload_preferences_rels_payload_mcp_api_keys_fk": { + "name": "payload_preferences_rels_payload_mcp_api_keys_fk", + "tableFrom": "payload_preferences_rels", + "tableTo": "payload_mcp_api_keys", + "columnsFrom": [ + "payload_mcp_api_keys_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payload_migrations": { + "name": "payload_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "batch": { + "name": "batch", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payload_migrations_updated_at_idx": { + "name": "payload_migrations_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payload_migrations_created_at_idx": { + "name": "payload_migrations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_rules": { + "name": "game_rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "main_currency_id": { + "name": "main_currency_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency_name": { + "name": "currency_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "currency_symbol": { + "name": "currency_symbol", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "currency_singular": { + "name": "currency_singular", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "currency_plural": { + "name": "currency_plural", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "currency_denomination": { + "name": "currency_denomination", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "proximity_threshold": { + "name": "proximity_threshold", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "game_tick_interval_minutes": { + "name": "game_tick_interval_minutes", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "locker_grid_width": { + "name": "locker_grid_width", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "locker_grid_height": { + "name": "locker_grid_height", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 6 + }, + "staff_hiring_enabled": { + "name": "staff_hiring_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "staff_hiring_disabled_message": { + "name": "staff_hiring_disabled_message", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "economy_enabled": { + "name": "economy_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "economy_default_price_band": { + "name": "economy_default_price_band", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0.5 + }, + "economy_real_demand_window_ticks": { + "name": "economy_real_demand_window_ticks", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 24 + }, + "economy_real_demand_decay": { + "name": "economy_real_demand_decay", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0.5 + }, + "economy_artificial_demand_drift": { + "name": "economy_artificial_demand_drift", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 0.05 + }, + "economy_cold_start_observations": { + "name": "economy_cold_start_observations", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "economy_tick_interval_hint_minutes": { + "name": "economy_tick_interval_hint_minutes", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 60 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "game_rules_main_currency_idx": { + "name": "game_rules_main_currency_idx", + "columns": [ + { + "expression": "main_currency_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_rules_main_currency_id_resources_id_fk": { + "name": "game_rules_main_currency_id_resources_id_fk", + "tableFrom": "game_rules", + "tableTo": "resources", + "columnsFrom": [ + "main_currency_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.game_rules_rels": { + "name": "game_rules_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "assignments_id": { + "name": "assignments_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "game_rules_rels_order_idx": { + "name": "game_rules_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_rules_rels_parent_idx": { + "name": "game_rules_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_rules_rels_path_idx": { + "name": "game_rules_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "game_rules_rels_assignments_id_idx": { + "name": "game_rules_rels_assignments_id_idx", + "columns": [ + { + "expression": "assignments_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "game_rules_rels_parent_fk": { + "name": "game_rules_rels_parent_fk", + "tableFrom": "game_rules_rels", + "tableTo": "game_rules", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "game_rules_rels_assignments_fk": { + "name": "game_rules_rels_assignments_fk", + "tableFrom": "game_rules_rels", + "tableTo": "assignments", + "columnsFrom": [ + "assignments_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shims_shims_targeting_roles": { + "name": "shims_shims_targeting_roles", + "schema": "", + "columns": { + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "enum_shims_shims_targeting_roles", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + } + }, + "indexes": { + "shims_shims_targeting_roles_order_idx": { + "name": "shims_shims_targeting_roles_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shims_shims_targeting_roles_parent_idx": { + "name": "shims_shims_targeting_roles_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shims_shims_targeting_roles_parent_fk": { + "name": "shims_shims_targeting_roles_parent_fk", + "tableFrom": "shims_shims_targeting_roles", + "tableTo": "shims_shims", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shims_shims": { + "name": "shims_shims", + "schema": "", + "columns": { + "_order": { + "name": "_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "_parent_id": { + "name": "_parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "type": { + "name": "type", + "type": "enum_shims_shims_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'css'" + }, + "content": { + "name": "content", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "targeting_audience": { + "name": "targeting_audience", + "type": "enum_shims_shims_targeting_audience", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "targeting_percentage": { + "name": "targeting_percentage", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": 50 + }, + "paths_match_type": { + "name": "paths_match_type", + "type": "enum_shims_shims_paths_match_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'all'" + } + }, + "indexes": { + "shims_shims_order_idx": { + "name": "shims_shims_order_idx", + "columns": [ + { + "expression": "_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shims_shims_parent_id_idx": { + "name": "shims_shims_parent_id_idx", + "columns": [ + { + "expression": "_parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shims_shims_name_idx": { + "name": "shims_shims_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shims_shims_parent_id_fk": { + "name": "shims_shims_parent_id_fk", + "tableFrom": "shims_shims", + "tableTo": "shims", + "columnsFrom": [ + "_parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shims": { + "name": "shims", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shims_texts": { + "name": "shims_texts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "shims_texts_order_parent": { + "name": "shims_texts_order_parent", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shims_texts_parent_fk": { + "name": "shims_texts_parent_fk", + "tableFrom": "shims_texts", + "tableTo": "shims", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shims_rels": { + "name": "shims_rels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "order": { + "name": "order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "users_id": { + "name": "users_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "shims_rels_order_idx": { + "name": "shims_rels_order_idx", + "columns": [ + { + "expression": "order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shims_rels_parent_idx": { + "name": "shims_rels_parent_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shims_rels_path_idx": { + "name": "shims_rels_path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "shims_rels_users_id_idx": { + "name": "shims_rels_users_id_idx", + "columns": [ + { + "expression": "users_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "shims_rels_parent_fk": { + "name": "shims_rels_parent_fk", + "tableFrom": "shims_rels", + "tableTo": "shims", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "shims_rels_users_fk": { + "name": "shims_rels_users_fk", + "tableFrom": "shims_rels", + "tableTo": "users", + "columnsFrom": [ + "users_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.enum_game_structures_staff_positions_position_type": { + "name": "enum_game_structures_staff_positions_position_type", + "schema": "public", + "values": [ + "administrative", + "technical", + "other" + ] + }, + "public.enum_game_vehicles_status": { + "name": "enum_game_vehicles_status", + "schema": "public", + "values": [ + "idle", + "assigned", + "in_transit", + "damaged", + "destroyed" + ] + }, + "public.enum_npc_staffing_position_type": { + "name": "enum_npc_staffing_position_type", + "schema": "public", + "values": [ + "labor", + "administrative", + "technical", + "other" + ] + }, + "public.enum_npc_staffing_status": { + "name": "enum_npc_staffing_status", + "schema": "public", + "values": [ + "active", + "terminated" + ] + }, + "public.enum_game_event_logs_target_collection": { + "name": "enum_game_event_logs_target_collection", + "schema": "public", + "values": [ + "game-structures", + "game-vehicles", + "game-npcs", + "npc-staffing", + "structures", + "resources", + "assets", + "vehicles", + "factions", + "missions", + "campaigns", + "users", + "technologies", + "maps", + "shipments", + "bank-accounts", + "bank-transactions", + "ledger-entries", + "locker-storages", + "loadouts", + "market-listings", + "market-negotiations", + "market-state", + "evaluations", + "tickets", + "wiki-pages", + "wiki-revisions", + "wiki-templates", + "game-servers", + "assignment-transfers", + "voice-scripts", + "voice-submissions", + "story-beats", + "story-beat-states", + "custom-minefields", + "custom-minefield-scores", + "custom-radio-tests", + "custom-radio-test-scores", + "ribbon-submissions" + ] + }, + "public.enum_users_roles": { + "name": "enum_users_roles", + "schema": "public", + "values": [ + "guest", + "user", + "trusted", + "admin", + "developer" + ] + }, + "public.enum_users_preferences_notifications_muted_types": { + "name": "enum_users_preferences_notifications_muted_types", + "schema": "public", + "values": [ + "market:offer", + "market:counter", + "market:accept", + "market:reject", + "market:withdrawn", + "market:closed", + "market:sold", + "market:expired", + "award:granted", + "account:discord-request", + "finance:deposit", + "finance:withdraw", + "finance:transfer", + "shipment:completed", + "ticket:created", + "ticket:assigned", + "ticket:reply", + "ticket:status", + "poker:force-ended", + "poker:everyone-left", + "blackjack:force-ended", + "blackjack:everyone-left" + ] + }, + "public.enum_users_preferences_discord_muted_types": { + "name": "enum_users_preferences_discord_muted_types", + "schema": "public", + "values": [ + "market:offer", + "market:counter", + "market:accept", + "market:reject", + "market:withdrawn", + "market:closed", + "market:sold", + "market:expired", + "award:granted", + "account:discord-request", + "finance:deposit", + "finance:withdraw", + "finance:transfer", + "shipment:completed", + "ticket:created", + "ticket:assigned", + "ticket:reply", + "ticket:status", + "poker:force-ended", + "poker:everyone-left", + "blackjack:force-ended", + "blackjack:everyone-left" + ] + }, + "public.enum_users_preferences_display_lead_or_actual": { + "name": "enum_users_preferences_display_lead_or_actual", + "schema": "public", + "values": [ + "lead", + "actual" + ] + }, + "public.enum_roles_permissions": { + "name": "enum_roles_permissions", + "schema": "public", + "values": [ + "system:admin-access", + "media:create", + "media:read", + "media:update", + "media:delete", + "admin:media:manage", + "users:create", + "users:read", + "users:update", + "users:delete", + "users:unlock", + "users:assign-roles", + "admin:users:manage", + "roles:create", + "roles:read", + "roles:update", + "roles:delete", + "admin:roles:manage", + "ranks:create", + "ranks:read", + "ranks:update", + "ranks:delete", + "admin:ranks:manage", + "profiles:create", + "profiles:read", + "profiles:update", + "profiles:delete", + "admin:profiles:manage", + "awards:create", + "awards:read", + "awards:update", + "awards:delete", + "admin:awards:manage", + "qualifications:create", + "qualifications:read", + "qualifications:update", + "qualifications:delete", + "admin:qualifications:manage", + "assignments:create", + "assignments:read", + "assignments:update", + "assignments:delete", + "admin:assignments:manage", + "assignment-transfers:create", + "assignment-transfers:read", + "assignment-transfers:update", + "assignment-transfers:delete", + "admin:assignment-transfers:manage", + "experience:create", + "experience:read", + "experience:update", + "experience:delete", + "admin:experience:manage", + "user-notifications:create", + "user-notifications:read", + "user-notifications:update", + "user-notifications:delete", + "admin:user-notifications:manage", + "evaluations:create", + "evaluations:read", + "evaluations:update", + "evaluations:delete", + "admin:evaluations:manage", + "missions:create", + "missions:read", + "missions:update", + "missions:delete", + "missions:read-sensitive", + "admin:missions:manage", + "mission-attendances:create", + "mission-attendances:read", + "mission-attendances:update", + "mission-attendances:delete", + "admin:mission-attendances:manage", + "campaigns:create", + "campaigns:read", + "campaigns:update", + "campaigns:delete", + "admin:campaigns:manage", + "campaigns:manage", + "factions:create", + "factions:read", + "factions:update", + "factions:delete", + "admin:factions:manage", + "technologies:create", + "technologies:read", + "technologies:update", + "technologies:delete", + "admin:technologies:manage", + "assets:create", + "assets:read", + "assets:update", + "assets:delete", + "admin:assets:manage", + "resources:create", + "resources:read", + "resources:update", + "resources:delete", + "admin:resources:manage", + "vehicles:create", + "vehicles:read", + "vehicles:update", + "vehicles:delete", + "admin:vehicles:manage", + "structures:create", + "structures:read", + "structures:update", + "structures:delete", + "admin:structures:manage", + "shipments:create", + "shipments:read", + "shipments:update", + "shipments:delete", + "admin:shipments:manage", + "bank-accounts:create", + "bank-accounts:read", + "bank-accounts:update", + "bank-accounts:delete", + "admin:bank-accounts:manage", + "bank-transactions:create", + "bank-transactions:read", + "bank-transactions:update", + "bank-transactions:delete", + "admin:bank-transactions:manage", + "ledger-entries:create", + "ledger-entries:read", + "ledger-entries:update", + "ledger-entries:delete", + "admin:ledger-entries:manage", + "locker-storages:create", + "locker-storages:read", + "locker-storages:update", + "locker-storages:delete", + "admin:locker-storages:manage", + "loadouts:create", + "loadouts:read", + "loadouts:update", + "loadouts:delete", + "admin:loadouts:manage", + "market-listings:create", + "market-listings:read", + "market-listings:update", + "market-listings:delete", + "admin:market-listings:manage", + "market-negotiations:create", + "market-negotiations:read", + "market-negotiations:update", + "market-negotiations:delete", + "admin:market-negotiations:manage", + "market-state:create", + "market-state:read", + "market-state:update", + "market-state:delete", + "admin:market-state:manage", + "projects:create", + "projects:read", + "projects:update", + "projects:delete", + "admin:projects:manage", + "labels:create", + "labels:read", + "labels:update", + "labels:delete", + "admin:labels:manage", + "sprints:create", + "sprints:read", + "sprints:update", + "sprints:delete", + "admin:sprints:manage", + "releases:create", + "releases:read", + "releases:update", + "releases:delete", + "admin:releases:manage", + "tickets:create", + "tickets:read", + "tickets:update", + "tickets:delete", + "admin:tickets:manage", + "ticket-votes:create", + "ticket-votes:read", + "ticket-votes:update", + "ticket-votes:delete", + "admin:ticket-votes:manage", + "game-structures:create", + "game-structures:read", + "game-structures:update", + "game-structures:delete", + "admin:game-structures:manage", + "game-vehicles:create", + "game-vehicles:read", + "game-vehicles:update", + "game-vehicles:delete", + "admin:game-vehicles:manage", + "game-npcs:create", + "game-npcs:read", + "game-npcs:update", + "game-npcs:delete", + "admin:game-npcs:manage", + "game-hard-resources:create", + "game-hard-resources:read", + "game-hard-resources:update", + "game-hard-resources:delete", + "admin:game-hard-resources:manage", + "game-event-logs:create", + "game-event-logs:read", + "game-event-logs:update", + "game-event-logs:delete", + "admin:game-event-logs:manage", + "maps:create", + "maps:read", + "maps:update", + "maps:delete", + "admin:maps:manage", + "narrative-events:create", + "narrative-events:read", + "narrative-events:update", + "narrative-events:delete", + "admin:narrative-events:manage", + "mission-files:create", + "mission-files:read", + "mission-files:update", + "mission-files:delete", + "admin:mission-files:manage", + "mod-lists:create", + "mod-lists:read", + "mod-lists:update", + "mod-lists:delete", + "admin:mod-lists:manage", + "game-servers:create", + "game-servers:read", + "game-servers:update", + "game-servers:delete", + "admin:game-servers:manage", + "arma-sync-events:create", + "arma-sync-events:read", + "arma-sync-events:update", + "arma-sync-events:delete", + "admin:arma-sync-events:manage", + "arma-commands:create", + "arma-commands:read", + "arma-commands:update", + "arma-commands:delete", + "admin:arma-commands:manage", + "game-rules:read", + "game-rules:update", + "shims:read", + "shims:update", + "logistics:manage", + "intelligence:manage", + "profiles:intel_excerpt:update", + "banking:manage", + "tickets:staff", + "discord:staff", + "discord:announce", + "custom-minefields:update", + "custom-minefields:delete", + "custom-minefield-scores:delete", + "custom-radio-tests:update", + "custom-radio-tests:delete", + "custom-radio-test-scores:delete" + ] + }, + "public.enum_awards_grant_config_allowed_grantor_assignment_types": { + "name": "enum_awards_grant_config_allowed_grantor_assignment_types", + "schema": "public", + "values": [ + "division", + "squad", + "team", + "special" + ] + }, + "public.enum_awards_type": { + "name": "enum_awards_type", + "schema": "public", + "values": [ + "commendation", + "ribbon", + "medal" + ] + }, + "public.enum_awards_grant_config_recipient_scope": { + "name": "enum_awards_grant_config_recipient_scope", + "schema": "public", + "values": [ + "same_command", + "whitelist", + "anyone" + ] + }, + "public.enum_assignments_type": { + "name": "enum_assignments_type", + "schema": "public", + "values": [ + "division", + "squad", + "team", + "special" + ] + }, + "public.enum_assignment_transfers_status": { + "name": "enum_assignment_transfers_status", + "schema": "public", + "values": [ + "pending", + "rejected", + "awaiting_superuser", + "completed", + "cancelled", + "denied" + ] + }, + "public.enum_assignment_transfers_escalation_reason": { + "name": "enum_assignment_transfers_escalation_reason", + "schema": "public", + "values": [ + "no_leaders", + "appeal", + "leader_changed" + ] + }, + "public.enum_assignment_transfers_from_leader_decision": { + "name": "enum_assignment_transfers_from_leader_decision", + "schema": "public", + "values": [ + "pending", + "approved", + "rejected", + "not_required" + ] + }, + "public.enum_assignment_transfers_to_leader_decision": { + "name": "enum_assignment_transfers_to_leader_decision", + "schema": "public", + "values": [ + "pending", + "approved", + "rejected", + "not_required" + ] + }, + "public.enum_evaluations_kind": { + "name": "enum_evaluations_kind", + "schema": "public", + "values": [ + "leader-direct", + "leader-indirect", + "subordinate" + ] + }, + "public.enum_evaluations_level": { + "name": "enum_evaluations_level", + "schema": "public", + "values": [ + "exceptional", + "excellent", + "proficient", + "adequate", + "moderate", + "under-review" + ] + }, + "public.enum_ribbon_submissions_grantor_types": { + "name": "enum_ribbon_submissions_grantor_types", + "schema": "public", + "values": [ + "division", + "squad", + "team", + "special" + ] + }, + "public.enum_ribbon_submissions_status": { + "name": "enum_ribbon_submissions_status", + "schema": "public", + "values": [ + "pending", + "approved", + "rejected" + ] + }, + "public.enum_ribbon_submissions_recipient_scope": { + "name": "enum_ribbon_submissions_recipient_scope", + "schema": "public", + "values": [ + "same_command", + "whitelist", + "anyone" + ] + }, + "public.enum_missions_factions_involved_threat_level": { + "name": "enum_missions_factions_involved_threat_level", + "schema": "public", + "values": [ + "ally", + "friendly", + "neutral", + "hostile" + ] + }, + "public.enum_missions_logistics_and_support_insertion": { + "name": "enum_missions_logistics_and_support_insertion", + "schema": "public", + "values": [ + "helicopter", + "aircraft", + "paradrop", + "boat", + "car", + "onFoot" + ] + }, + "public.enum_missions_logistics_and_support_extraction": { + "name": "enum_missions_logistics_and_support_extraction", + "schema": "public", + "values": [ + "helicopter", + "aircraft", + "paradrop", + "boat", + "car", + "onFoot" + ] + }, + "public.enum_missions_operation_type": { + "name": "enum_missions_operation_type", + "schema": "public", + "values": [ + "main", + "side", + "external" + ] + }, + "public.enum_missions_classification_mission_type": { + "name": "enum_missions_classification_mission_type", + "schema": "public", + "values": [ + "Zeus", + "PvE", + "PvP", + "PvPvE", + "Training", + "Special" + ] + }, + "public.enum_missions_ownership_and_status_status": { + "name": "enum_missions_ownership_and_status_status", + "schema": "public", + "values": [ + "Concept", + "Planning", + "Ready", + "Scheduled", + "Active", + "Completed", + "Cancelled" + ] + }, + "public.enum_missions_ownership_and_status_visibility": { + "name": "enum_missions_ownership_and_status_visibility", + "schema": "public", + "values": [ + "unit", + "leadership", + "intel", + "private" + ] + }, + "public.enum_missions_civilian_presence": { + "name": "enum_missions_civilian_presence", + "schema": "public", + "values": [ + "None", + "Low", + "Medium", + "High" + ] + }, + "public.enum_mission_attendances_response": { + "name": "enum_mission_attendances_response", + "schema": "public", + "values": [ + "yes", + "no", + "tentative" + ] + }, + "public.enum_campaigns_status": { + "name": "enum_campaigns_status", + "schema": "public", + "values": [ + "concept", + "inProgress", + "completed" + ] + }, + "public.enum_campaigns_campaign_mode": { + "name": "enum_campaigns_campaign_mode", + "schema": "public", + "values": [ + "official", + "custom" + ] + }, + "public.enum_factions_type": { + "name": "enum_factions_type", + "schema": "public", + "values": [ + "playerFaction", + "majorFaction", + "minorFaction", + "corporation", + "special" + ] + }, + "public.enum_technologies_approval_status": { + "name": "enum_technologies_approval_status", + "schema": "public", + "values": [ + "in_progress", + "ready_for_review", + "approved", + "rejected", + "revision_requested" + ] + }, + "public.enum_technologies_type": { + "name": "enum_technologies_type", + "schema": "public", + "values": [ + "asset", + "upgrade" + ] + }, + "public.enum_assets_skin_configuration_applies_to_asset_types": { + "name": "enum_assets_skin_configuration_applies_to_asset_types", + "schema": "public", + "values": [ + "weapon", + "uniform", + "vest", + "backpack", + "helmet" + ] + }, + "public.enum_assets_approval_status": { + "name": "enum_assets_approval_status", + "schema": "public", + "values": [ + "in_progress", + "ready_for_review", + "approved", + "rejected", + "revision_requested" + ] + }, + "public.enum_assets_asset_type": { + "name": "enum_assets_asset_type", + "schema": "public", + "values": [ + "weapon", + "magazine", + "optic", + "muzzle", + "bipod", + "uniform", + "vest", + "backpack", + "helmet", + "facewear", + "nvg", + "radio", + "medical", + "tool", + "explosive", + "skin", + "miscellaneous" + ] + }, + "public.enum_assets_weapon_configuration_slot_type": { + "name": "enum_assets_weapon_configuration_slot_type", + "schema": "public", + "values": [ + "primary", + "secondary", + "launcher" + ] + }, + "public.enum_assets_nightvision_configuration_nvg_generation": { + "name": "enum_assets_nightvision_configuration_nvg_generation", + "schema": "public", + "values": [ + "gen1", + "gen2", + "gen3" + ] + }, + "public.enum_assets_market_trading_rarity": { + "name": "enum_assets_market_trading_rarity", + "schema": "public", + "values": [ + "basic", + "common", + "uncommon", + "rare", + "veryRare", + "epic", + "legendary", + "unique" + ] + }, + "public.enum_resources_approval_status": { + "name": "enum_resources_approval_status", + "schema": "public", + "values": [ + "in_progress", + "ready_for_review", + "approved", + "rejected", + "revision_requested" + ] + }, + "public.enum_resources_type": { + "name": "enum_resources_type", + "schema": "public", + "values": [ + "physical", + "fluid", + "energy", + "abstract" + ] + }, + "public.enum_resources_rarity": { + "name": "enum_resources_rarity", + "schema": "public", + "values": [ + "common", + "uncommon", + "rare", + "epic", + "legendary", + "exotic", + "unique" + ] + }, + "public.enum_vehicles_category": { + "name": "enum_vehicles_category", + "schema": "public", + "values": [ + "civilian", + "utility", + "transport", + "weaponized", + "military" + ] + }, + "public.enum_vehicles_pylons_pylon_type": { + "name": "enum_vehicles_pylons_pylon_type", + "schema": "public", + "values": [ + "mountedWeapon", + "pylon" + ] + }, + "public.enum_vehicles_approval_status": { + "name": "enum_vehicles_approval_status", + "schema": "public", + "values": [ + "in_progress", + "ready_for_review", + "approved", + "rejected", + "revision_requested" + ] + }, + "public.enum_vehicles_armor_rating": { + "name": "enum_vehicles_armor_rating", + "schema": "public", + "values": [ + "none", + "light", + "medium", + "heavy", + "composite" + ] + }, + "public.enum_structures_staff_positions_position_type": { + "name": "enum_structures_staff_positions_position_type", + "schema": "public", + "values": [ + "administrative", + "technical", + "other" + ] + }, + "public.enum_structures_approval_status": { + "name": "enum_structures_approval_status", + "schema": "public", + "values": [ + "in_progress", + "ready_for_review", + "approved", + "rejected", + "revision_requested" + ] + }, + "public.enum_structures_category": { + "name": "enum_structures_category", + "schema": "public", + "values": [ + "logistics", + "military", + "civilian" + ] + }, + "public.enum_structures_terrain_type": { + "name": "enum_structures_terrain_type", + "schema": "public", + "values": [ + "land", + "water" + ] + }, + "public.enum_structures_function_type": { + "name": "enum_structures_function_type", + "schema": "public", + "values": [ + "producer", + "storage", + "sensor", + "converter", + "defense", + "miscellaneous" + ] + }, + "public.enum_shipments_status": { + "name": "enum_shipments_status", + "schema": "public", + "values": [ + "pending", + "dispatched", + "in_transit", + "arrived", + "completed", + "cancelled", + "failed", + "stranded" + ] + }, + "public.enum_bank_accounts_account_type": { + "name": "enum_bank_accounts_account_type", + "schema": "public", + "values": [ + "treasury", + "faction", + "personal" + ] + }, + "public.enum_bank_accounts_status": { + "name": "enum_bank_accounts_status", + "schema": "public", + "values": [ + "open", + "frozen", + "closed" + ] + }, + "public.enum_bank_transactions_type": { + "name": "enum_bank_transactions_type", + "schema": "public", + "values": [ + "deposit", + "withdrawal", + "transfer", + "payment", + "fee", + "salary", + "adjustment" + ] + }, + "public.enum_bank_transactions_status": { + "name": "enum_bank_transactions_status", + "schema": "public", + "values": [ + "completed", + "reversed" + ] + }, + "public.enum_ledger_entries_type": { + "name": "enum_ledger_entries_type", + "schema": "public", + "values": [ + "deposit", + "withdrawal", + "transfer", + "payment", + "fee", + "salary", + "adjustment" + ] + }, + "public.enum_market_listings_status": { + "name": "enum_market_listings_status", + "schema": "public", + "values": [ + "active", + "sold", + "cancelled", + "expired" + ] + }, + "public.enum_market_negotiations_history_from": { + "name": "enum_market_negotiations_history_from", + "schema": "public", + "values": [ + "buyer", + "seller" + ] + }, + "public.enum_market_negotiations_status": { + "name": "enum_market_negotiations_status", + "schema": "public", + "values": [ + "open", + "accepted", + "rejected", + "closed", + "cancelled", + "expired" + ] + }, + "public.enum_market_negotiations_proposed_by": { + "name": "enum_market_negotiations_proposed_by", + "schema": "public", + "values": [ + "buyer", + "seller" + ] + }, + "public.enum_market_state_item_type": { + "name": "enum_market_state_item_type", + "schema": "public", + "values": [ + "resource", + "asset" + ] + }, + "public.enum_voice_scripts_status": { + "name": "enum_voice_scripts_status", + "schema": "public", + "values": [ + "draft", + "open", + "closed" + ] + }, + "public.enum_voice_submissions_status": { + "name": "enum_voice_submissions_status", + "schema": "public", + "values": [ + "pending", + "accepted", + "rejected" + ] + }, + "public.enum_tickets_activity_type": { + "name": "enum_tickets_activity_type", + "schema": "public", + "values": [ + "system", + "agent_message", + "client_message" + ] + }, + "public.enum_tickets_category": { + "name": "enum_tickets_category", + "schema": "public", + "values": [ + "bug_report", + "feature_request", + "support_request", + "other" + ] + }, + "public.enum_tickets_priority": { + "name": "enum_tickets_priority", + "schema": "public", + "values": [ + "low", + "medium", + "high", + "critical" + ] + }, + "public.enum_tickets_status": { + "name": "enum_tickets_status", + "schema": "public", + "values": [ + "open", + "in_review", + "planning", + "in_progress", + "waiting_on_reporter", + "in_testing", + "resolved", + "canceled", + "closed" + ] + }, + "public.enum_tickets_type": { + "name": "enum_tickets_type", + "schema": "public", + "values": [ + "task", + "bug", + "story", + "epic", + "improvement" + ] + }, + "public.enum_tickets_resolution": { + "name": "enum_tickets_resolution", + "schema": "public", + "values": [ + "fixed", + "wontfix", + "duplicate", + "invalid" + ] + }, + "public.enum_wiki_pages_category": { + "name": "enum_wiki_pages_category", + "schema": "public", + "values": [ + "Campaign", + "World", + "Lore", + "Characters", + "Plot", + "Media", + "Guides", + "Meta" + ] + }, + "public.enum_wiki_revisions_type": { + "name": "enum_wiki_revisions_type", + "schema": "public", + "values": [ + "create", + "edit", + "restore" + ] + }, + "public.enum_projects_status": { + "name": "enum_projects_status", + "schema": "public", + "values": [ + "active", + "archived" + ] + }, + "public.enum_sprints_status": { + "name": "enum_sprints_status", + "schema": "public", + "values": [ + "planning", + "active", + "completed" + ] + }, + "public.enum_releases_status": { + "name": "enum_releases_status", + "schema": "public", + "values": [ + "draft", + "in_progress", + "released" + ] + }, + "public.enum_narrative_events_triggers_type": { + "name": "enum_narrative_events_triggers_type", + "schema": "public", + "values": [ + "manual", + "mission_complete", + "timer", + "condition" + ] + }, + "public.enum_story_beats_status": { + "name": "enum_story_beats_status", + "schema": "public", + "values": [ + "draft", + "published" + ] + }, + "public.enum_story_beat_states_reaction": { + "name": "enum_story_beat_states_reaction", + "schema": "public", + "values": [ + "like", + "heart", + "salute" + ] + }, + "public.enum_game_servers_status": { + "name": "enum_game_servers_status", + "schema": "public", + "values": [ + "online", + "offline" + ] + }, + "public.enum_arma_commands_status": { + "name": "enum_arma_commands_status", + "schema": "public", + "values": [ + "queued", + "delivered", + "succeeded", + "failed" + ] + }, + "public.enum_custom_radio_tests_order": { + "name": "enum_custom_radio_tests_order", + "schema": "public", + "values": [ + "fixed", + "shuffled" + ] + }, + "public.enum_custom_radio_test_scores_mode": { + "name": "enum_custom_radio_test_scores_mode", + "schema": "public", + "values": [ + "sprint", + "watch" + ] + }, + "public.enum_plugin_ai_instructions_field_type": { + "name": "enum_plugin_ai_instructions_field_type", + "schema": "public", + "values": [ + "text", + "textarea", + "upload", + "richText" + ] + }, + "public.enum_plugin_ai_instructions_model_id": { + "name": "enum_plugin_ai_instructions_model_id", + "schema": "public", + "values": [ + "unsloth-text" + ] + }, + "public.enum_shims_shims_targeting_roles": { + "name": "enum_shims_shims_targeting_roles", + "schema": "public", + "values": [ + "guest", + "user", + "trusted", + "admin", + "developer" + ] + }, + "public.enum_shims_shims_type": { + "name": "enum_shims_shims_type", + "schema": "public", + "values": [ + "css", + "js" + ] + }, + "public.enum_shims_shims_targeting_audience": { + "name": "enum_shims_shims_targeting_audience", + "schema": "public", + "values": [ + "all", + "percentage", + "users", + "roles" + ] + }, + "public.enum_shims_shims_paths_match_type": { + "name": "enum_shims_shims_paths_match_type", + "schema": "public", + "values": [ + "all", + "include", + "exclude" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "id": "75684db3-1628-45dc-afbd-422a7012dba0", + "prevId": "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/src/migrations/20260912_071740_add_blackjack_minigame_stats.ts b/src/migrations/20260912_071740_add_blackjack_minigame_stats.ts new file mode 100644 index 0000000..95082c5 --- /dev/null +++ b/src/migrations/20260912_071740_add_blackjack_minigame_stats.ts @@ -0,0 +1,31 @@ +import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres' + +export async function up({ db, payload, req }: MigrateUpArgs): Promise { + await db.execute(sql` + ALTER TYPE "public"."enum_users_preferences_notifications_muted_types" ADD VALUE 'blackjack:force-ended'; + ALTER TYPE "public"."enum_users_preferences_notifications_muted_types" ADD VALUE 'blackjack:everyone-left'; + ALTER TYPE "public"."enum_users_preferences_discord_muted_types" ADD VALUE 'blackjack:force-ended'; + ALTER TYPE "public"."enum_users_preferences_discord_muted_types" ADD VALUE 'blackjack:everyone-left'; + ALTER TABLE "profiles" ADD COLUMN "progression_minigames_blackjack_games_played" numeric DEFAULT 0; + ALTER TABLE "profiles" ADD COLUMN "progression_minigames_blackjack_rounds_won" numeric DEFAULT 0; + ALTER TABLE "profiles" ADD COLUMN "progression_minigames_blackjack_blackjacks_hit" numeric DEFAULT 0; + ALTER TABLE "profiles" ADD COLUMN "progression_minigames_blackjack_best_score" numeric DEFAULT 0; + ALTER TABLE "profiles" ADD COLUMN "progression_minigames_blackjack_xp_earned" numeric DEFAULT 0;`) +} + +export async function down({ db, payload, req }: MigrateDownArgs): Promise { + await db.execute(sql` + ALTER TABLE "users_preferences_notifications_muted_types" ALTER COLUMN "value" SET DATA TYPE text; + DROP TYPE "public"."enum_users_preferences_notifications_muted_types"; + CREATE TYPE "public"."enum_users_preferences_notifications_muted_types" AS ENUM('market:offer', 'market:counter', 'market:accept', 'market:reject', 'market:withdrawn', 'market:closed', 'market:sold', 'market:expired', 'award:granted', 'account:discord-request', 'finance:deposit', 'finance:withdraw', 'finance:transfer', 'shipment:completed', 'ticket:created', 'ticket:assigned', 'ticket:reply', 'ticket:status', 'poker:force-ended', 'poker:everyone-left'); + ALTER TABLE "users_preferences_notifications_muted_types" ALTER COLUMN "value" SET DATA TYPE "public"."enum_users_preferences_notifications_muted_types" USING "value"::"public"."enum_users_preferences_notifications_muted_types"; + ALTER TABLE "users_preferences_discord_muted_types" ALTER COLUMN "value" SET DATA TYPE text; + DROP TYPE "public"."enum_users_preferences_discord_muted_types"; + CREATE TYPE "public"."enum_users_preferences_discord_muted_types" AS ENUM('market:offer', 'market:counter', 'market:accept', 'market:reject', 'market:withdrawn', 'market:closed', 'market:sold', 'market:expired', 'award:granted', 'account:discord-request', 'finance:deposit', 'finance:withdraw', 'finance:transfer', 'shipment:completed', 'ticket:created', 'ticket:assigned', 'ticket:reply', 'ticket:status', 'poker:force-ended', 'poker:everyone-left'); + ALTER TABLE "users_preferences_discord_muted_types" ALTER COLUMN "value" SET DATA TYPE "public"."enum_users_preferences_discord_muted_types" USING "value"::"public"."enum_users_preferences_discord_muted_types"; + ALTER TABLE "profiles" DROP COLUMN "progression_minigames_blackjack_games_played"; + ALTER TABLE "profiles" DROP COLUMN "progression_minigames_blackjack_rounds_won"; + ALTER TABLE "profiles" DROP COLUMN "progression_minigames_blackjack_blackjacks_hit"; + ALTER TABLE "profiles" DROP COLUMN "progression_minigames_blackjack_best_score"; + ALTER TABLE "profiles" DROP COLUMN "progression_minigames_blackjack_xp_earned";`) +} diff --git a/tests/int/blackjack-card-skins.int.spec.ts b/tests/int/blackjack-card-skins.int.spec.ts new file mode 100644 index 0000000..0c467bd --- /dev/null +++ b/tests/int/blackjack-card-skins.int.spec.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import type { Card } from "@/lib/minigames/blackjack"; +import { mapCardToSvg, normalizeCardStyle } from "@/components/frontend/minigames/blackjack/cardStyle"; + +function cardOf(rank: Card["rank"], suit: Card["suit"]): Card { + return { rank, suit }; +} + +describe("mapCardToSvg", () => { + it("maps number cards to their numbered asset path", () => { + expect(mapCardToSvg(cardOf(7, "h"))).toBe("/images/assets/playing-cards/7_of_hearts.svg"); + expect(mapCardToSvg(cardOf(10, "s"))).toBe("/images/assets/playing-cards/10_of_spades.svg"); + expect(mapCardToSvg(cardOf(2, "c"))).toBe("/images/assets/playing-cards/2_of_clubs.svg"); + }); + + it("maps face ranks to word asset paths", () => { + expect(mapCardToSvg(cardOf(11, "d"))).toBe("/images/assets/playing-cards/jack_of_diamonds.svg"); + expect(mapCardToSvg(cardOf(12, "c"))).toBe("/images/assets/playing-cards/queen_of_clubs.svg"); + expect(mapCardToSvg(cardOf(13, "h"))).toBe("/images/assets/playing-cards/king_of_hearts.svg"); + expect(mapCardToSvg(cardOf(14, "s"))).toBe("/images/assets/playing-cards/ace_of_spades.svg"); + }); + + it("covers all four suits for a spot-check rank", () => { + expect(mapCardToSvg(cardOf(9, "s"))).toBe("/images/assets/playing-cards/9_of_spades.svg"); + expect(mapCardToSvg(cardOf(9, "h"))).toBe("/images/assets/playing-cards/9_of_hearts.svg"); + expect(mapCardToSvg(cardOf(9, "d"))).toBe("/images/assets/playing-cards/9_of_diamonds.svg"); + expect(mapCardToSvg(cardOf(9, "c"))).toBe("/images/assets/playing-cards/9_of_clubs.svg"); + }); +}); + +describe("normalizeCardStyle", () => { + it("accepts the three known style values", () => { + expect(normalizeCardStyle("light")).toBe("light"); + expect(normalizeCardStyle("dark")).toBe("dark"); + expect(normalizeCardStyle("classic")).toBe("classic"); + }); + + it("falls back to light for unknown or missing values", () => { + expect(normalizeCardStyle("bogus")).toBe("light"); + expect(normalizeCardStyle(null)).toBe("light"); + expect(normalizeCardStyle(undefined)).toBe("light"); + expect(normalizeCardStyle(42)).toBe("light"); + expect(normalizeCardStyle("")).toBe("light"); + }); +}); diff --git a/tests/int/blackjack-engine.int.spec.ts b/tests/int/blackjack-engine.int.spec.ts new file mode 100644 index 0000000..6b64885 --- /dev/null +++ b/tests/int/blackjack-engine.int.spec.ts @@ -0,0 +1,546 @@ +/** + * Pure engine tests for the blackjack rules engine. + * + * These are intentionally database-free: no Payload, no Next.js imports, no + * setup or teardown. Everything runs against the pure modules under + * src/lib/minigames/blackjack with injected, deterministic randomness. + */ + +import { describe, expect, it } from "vitest"; +import { + BLACKJACK_PAYOUT, + MAX_HANDS, + MAX_PLAYERS, + MIN_BET, + MIN_PLAYERS, + SHOE_DECKS, + STARTING_BANKROLL, + advanceState, + applyAction, + buildPrivateView, + createShuffledShoe, + createShoe, + createTableState, + dealerShouldHit, + getLegalActions, + handValue, + insuranceMaxFor, + isNaturalBlackjack, + isTenValue, + mulberry32, + needsShuffle, + settleRound, + shoePenetrationFloor, + sitOut, + startNextRound, + type BlackjackTableState, + type Card, + type Rank, + type Suit, +} from "@/lib/minigames/blackjack"; + +const c = (rank: Rank, suit: Suit): Card => ({ rank, suit }); + +/** Craft a shoe whose first cards come out in exactly the given order. */ +function shoeOf(...cards: Card[]): Card[] { + return [...cards, ...createShoe().slice(0, 40)]; +} + +function makeTable(bankrolls?: number[], seed = 1): BlackjackTableState { + return createTableState( + (bankrolls ?? [STARTING_BANKROLL]).map((bankroll, index) => ({ + name: `P${index + 1}`, + bankroll, + })), + {}, + mulberry32(seed), + ); +} + +function soloTable(shoe: Card[], bankroll = STARTING_BANKROLL): BlackjackTableState { + const base = createTableState([{ name: "Solo", bankroll }], {}, mulberry32(1)); + return { ...base, shoe }; +} + +function act( + state: BlackjackTableState, + seatId: number, + action: Parameters[2], + amount?: number, +): BlackjackTableState { + const result = applyAction(state, seatId, action, amount); + if (!result.success) throw new Error(`Unexpected action failure: ${result.error}`); + return result.state; +} + +function expectFailure( + state: BlackjackTableState, + seatId: number, + action: Parameters[2], + amount?: number, + messagePart?: string, +): void { + const result = applyAction(state, seatId, action, amount); + expect(result.success).toBe(false); + if (!result.success && messagePart) { + expect(result.error).toContain(messagePart); + } +} + +/** Bet (defaults to the table minimum) and return the dealt-to state. */ +function bet(state: BlackjackTableState, seat = state.toActSeat ?? 0, amount = MIN_BET) { + return act(state, seat, "bet", amount); +} + +describe("shoe composition and reshuffle", () => { + it("builds a 6-deck shoe with 24 of every rank", () => { + const shoe = createShoe(); + expect(shoe).toHaveLength(SHOE_DECKS * 52); + for (let rank = 2; rank <= 14; rank += 1) { + const count = shoe.filter((card) => card.rank === rank).length; + expect(count).toBe(24); + } + }); + + it("shuffles deterministically from a seed", () => { + const a = createShuffledShoe(7, 1); + const b = createShuffledShoe(7, 1); + const other = createShuffledShoe(8, 1); + expect(a).toEqual(b); + expect(a).not.toEqual(other); + }); + + it("flags a reshuffle once the shoe drops below the penetration floor", () => { + const floor = shoePenetrationFloor(); + expect(floor).toBe(Math.floor(SHOE_DECKS * 52 * 0.25)); + expect(needsShuffle(createShoe().slice(0, floor - 1))).toBe(true); + expect(needsShuffle(createShoe().slice(0, floor))).toBe(false); + expect(needsShuffle(createShoe())).toBe(false); + }); +}); + +describe("hand values", () => { + it("counts aces as 11 while the total stays at or under 21", () => { + expect(handValue([c(14, "s"), c(6, "h")])).toEqual({ total: 17, soft: true }); + expect(handValue([c(14, "s"), c(14, "h")])).toEqual({ total: 12, soft: true }); + }); + + it("degrades aces to 1 when 11 would bust", () => { + expect(handValue([c(14, "s"), c(10, "h"), c(6, "d")])).toEqual({ total: 17, soft: false }); + // Three aces: two degrade to 1, one still counts as 11 (a soft 13). + expect(handValue([c(14, "s"), c(14, "h"), c(14, "d")])).toEqual({ total: 13, soft: true }); + }); + + it("reports bust totals over 21", () => { + expect(handValue([c(10, "s"), c(10, "h"), c(5, "d")]).total).toBe(25); + }); + + it("detects natural blackjacks (two cards totalling 21 only)", () => { + expect(isNaturalBlackjack([c(14, "s"), c(13, "h")])).toBe(true); + expect(isNaturalBlackjack([c(10, "s"), c(9, "h"), c(2, "d")])).toBe(false); + expect(isNaturalBlackjack([c(10, "s"), c(9, "h")])).toBe(false); + }); + + it("classifies ten-value ranks for the peek rule", () => { + expect(isTenValue(10)).toBe(true); + expect(isTenValue(13)).toBe(true); + expect(isTenValue(14)).toBe(false); + expect(isTenValue(9)).toBe(false); + }); + + it("stands the dealer on any 17 including soft 17 (S17)", () => { + expect(dealerShouldHit([c(10, "s"), c(7, "h")])).toBe(false); + expect(dealerShouldHit([c(14, "s"), c(7, "h")])).toBe(false); + expect(dealerShouldHit([c(14, "s"), c(3, "h"), c(3, "d")])).toBe(false); + expect(dealerShouldHit([c(10, "s"), c(6, "h")])).toBe(true); + expect(dealerShouldHit([c(14, "s"), c(5, "h")])).toBe(true); + }); +}); + +describe("betting phase", () => { + it("deals as soon as the only player bets", () => { + const state = bet(soloTable(shoeOf(c(10, "s"), c(6, "h"), c(9, "s"), c(7, "d")))); + expect(state.phase).toBe("player"); + expect(state.players[0]?.hands[0]?.cards).toHaveLength(2); + expect(state.dealer.cards).toHaveLength(2); + }); + + it("walks the betting turn across multiple players before dealing", () => { + let state = makeTable([STARTING_BANKROLL, STARTING_BANKROLL]); + expect(state.phase).toBe("betting"); + expect(state.toActSeat).toBe(0); + state = bet(state); + expect(state.toActSeat).toBe(1); + expect(state.players[0]?.hasBet).toBe(true); + expect(state.players[1]?.hasBet).toBe(false); + state = bet(state); + expect(state.phase).not.toBe("betting"); + expect(state.players[0]?.hands[0]?.cards).toHaveLength(2); + expect(state.players[1]?.hands[0]?.cards).toHaveLength(2); + }); + + it("rejects bets outside the limits, non-integers, and repeats", () => { + const state = makeTable([50, STARTING_BANKROLL]); + expectFailure(state, 0, "bet", MIN_BET - 1, "minimum bet"); + expectFailure(state, 0, "bet", 501, "maximum bet"); + expectFailure(state, 0, "bet", 10.5, "whole chips"); + expectFailure(state, 0, "bet", 100, "bankroll"); + const afterBet = bet(state); + expectFailure(afterBet, 0, "bet", MIN_BET, "turn"); + }); + + it("sits a player out and continues the round without them", () => { + const state = makeTable([STARTING_BANKROLL, STARTING_BANKROLL]); + const sitResult = sitOut(state, 0); + if (!sitResult.success) throw new Error(sitResult.error); + const afterSit = sitResult.state; + expect(afterSit.players[0]?.sittingOut).toBe(true); + expect(afterSit.toActSeat).toBe(1); + const dealt = bet(afterSit); + expect(dealt.players[0]?.hands).toHaveLength(0); + expect(dealt.players[0]?.bankroll).toBe(STARTING_BANKROLL); + expect(dealt.players[1]?.hands[0]?.cards).toHaveLength(2); + expect(dealt.phase).toBe("player"); + }); +}); + +describe("player actions", () => { + it("stands and settles against the dealer", () => { + // Player 19 stands; dealer 6+10 = 16 draws the crafted 4 and lands on 20. + const state = bet(soloTable(shoeOf(c(10, "s"), c(6, "h"), c(9, "s"), c(10, "d"), c(4, "s")))); + expect(state.phase).toBe("player"); + const settled = act(state, 0, "stand"); + expect(settled.phase).toBe("round-complete"); + const hand = settled.players[0]?.hands[0]; + expect(hand?.outcome).toBe("lose"); + expect(hand?.netChange).toBe(-MIN_BET); + expect(settled.players[0]?.bankroll).toBe(STARTING_BANKROLL - MIN_BET); + expect(settled.dealer.holeRevealed).toBe(true); + expect(settled.roundResult?.dealerTotal).toBe(20); + }); + + it("hits to a made hand and auto-stands on 21", () => { + const state = bet(soloTable(shoeOf(c(5, "s"), c(10, "h"), c(6, "s"), c(7, "d"), c(10, "d")))); + // 5 + 6 = 11; hit the crafted 10 -> 21 auto-stands. + const hit = act(state, 0, "hit"); + expect(hit.players[0]?.hands[0]?.status).toBe("stood"); + expect(handValue(hit.players[0]?.hands[0]?.cards ?? []).total).toBe(21); + }); + + it("busts on a hit and loses the stake", () => { + const state = bet(soloTable(shoeOf(c(10, "s"), c(10, "h"), c(9, "s"), c(7, "d"), c(5, "s")))); + const hit = act(state, 0, "hit"); + expect(hit.players[0]?.hands[0]?.status).toBe("bust"); + expect(hit.players[0]?.bankroll).toBe(STARTING_BANKROLL - MIN_BET); + expect(hit.phase).toBe("round-complete"); + }); + + it("rejects acting out of turn", () => { + const state = makeTable([STARTING_BANKROLL, STARTING_BANKROLL]); + const dealt = bet(bet(state)); + expectFailure(dealt, 1, "hit", undefined, "turn"); + expect(getLegalActions(dealt, 0).canHit).toBe(true); + }); +}); + +describe("double down", () => { + it("doubles the stake, draws one card, and auto-stands", () => { + const state = bet(soloTable(shoeOf(c(5, "s"), c(10, "h"), c(6, "s"), c(7, "d"), c(5, "s")))); + const doubled = act(state, 0, "double"); + const hand = doubled.players[0]?.hands[0]; + expect(hand?.bet).toBe(MIN_BET * 2); + expect(hand?.doubled).toBe(true); + expect(hand?.cards).toHaveLength(3); + expect(hand?.status).toBe("stood"); + expect(doubled.players[0]?.bankroll).toBe(STARTING_BANKROLL - MIN_BET * 2); + }); + + it("refuses doubles without the bankroll to cover the extra stake", () => { + const poor = bet( + soloTable(shoeOf(c(10, "s"), c(6, "h"), c(9, "s"), c(7, "d")), MIN_BET + 5), + ); + expectFailure(poor, 0, "double", undefined, "bankroll"); + }); +}); + +describe("splitting pairs", () => { + it("splits a pair into two funded hands and plays each", () => { + const state = bet( + soloTable(shoeOf(c(8, "s"), c(6, "h"), c(8, "h"), c(10, "d"), c(4, "s"), c(5, "s"))), + ); + const split = act(state, 0, "split"); + const hands = split.players[0]?.hands ?? []; + expect(hands).toHaveLength(2); + expect(hands[0]?.cards).toEqual([c(8, "s"), c(4, "s")]); + expect(hands[1]?.cards).toEqual([c(8, "h"), c(5, "s")]); + expect(split.players[0]?.bankroll).toBe(STARTING_BANKROLL - MIN_BET * 2); + expect(split.toActSeat).toBe(0); + expect(split.activeHandIndex).toBe(0); + // Play hand one to a stand, then hand two becomes active. + const stoodA = act(split, 0, "stand"); + expect(stoodA.activeHandIndex).toBe(1); + const stoodB = act(stoodA, 0, "stand"); + expect(stoodB.phase).toBe("round-complete"); + expect(stoodB.roundResult?.hands).toHaveLength(2); + }); + + it("refuses non-matching ranks and overdrafted splits", () => { + const notPair = bet(soloTable(shoeOf(c(8, "s"), c(6, "h"), c(9, "h"), c(10, "d")))); + expectFailure(notPair, 0, "split", undefined, "matching ranks"); + const poor = bet( + soloTable(shoeOf(c(8, "s"), c(6, "h"), c(8, "h"), c(10, "d")), MIN_BET + 5), + ); + expectFailure(poor, 0, "split", undefined, "bankroll"); + }); + + it("caps splitting at four hands", () => { + // Every split hand draws another 8 so the active hand keeps being a pair. + let state = bet( + soloTable( + shoeOf( + c(8, "s"), c(6, "h"), c(8, "h"), c(10, "d"), + c(8, "d"), c(3, "s"), + c(8, "c"), c(2, "s"), + c(8, "s"), c(2, "h"), + ), + ), + ); + state = act(state, 0, "split"); + expect(state.players[0]?.hands).toHaveLength(2); + state = act(state, 0, "split"); + expect(state.players[0]?.hands).toHaveLength(3); + state = act(state, 0, "split"); + expect(state.players[0]?.hands).toHaveLength(MAX_HANDS); + expectFailure(state, 0, "split", undefined); + }); + + it("gives split aces one card each and auto-stands both", () => { + const state = bet( + soloTable(shoeOf(c(14, "s"), c(6, "h"), c(14, "h"), c(10, "d"), c(9, "s"), c(5, "s"))), + ); + const split = act(state, 0, "split"); + const hands = split.players[0]?.hands ?? []; + expect(hands[0]?.splitAces).toBe(true); + expect(hands[0]?.status).toBe("stood"); + expect(hands[1]?.status).toBe("stood"); + expect(hands[0]?.cards).toHaveLength(2); + expect(hands[1]?.cards).toHaveLength(2); + expect(split.phase).toBe("round-complete"); + }); +}); + +describe("insurance and dealer peek", () => { + it("offers insurance only on an ace upcard and caps it at half the bet", () => { + const state = bet(soloTable(shoeOf(c(10, "s"), c(14, "h"), c(9, "s"), c(9, "d")))); + expect(state.phase).toBe("insurance"); + expect(state.toActSeat).toBe(0); + expect(insuranceMaxFor(MIN_BET)).toBe(Math.floor(MIN_BET / 2)); + expectFailure(state, 0, "insurance", Math.floor(MIN_BET / 2) + 1, "capped at"); + const insured = act(state, 0, "insurance", Math.floor(MIN_BET / 2)); + expect(insured.players[0]?.insuranceBet).toBe(Math.floor(MIN_BET / 2)); + expect(insured.players[0]?.bankroll).toBe( + STARTING_BANKROLL - MIN_BET - Math.floor(MIN_BET / 2), + ); + // The dealer's hole card (9) is no blackjack, so play continues. + expect(insured.phase).toBe("player"); + }); + + it("pays insurance 2:1 when the dealer has blackjack and pushes naturals", () => { + const state = bet(soloTable(shoeOf(c(14, "s"), c(14, "h"), c(13, "s"), c(13, "h")))); + expect(state.phase).toBe("insurance"); + // After the insurance answer the peek settles the round immediately: + // the player's natural pushes (stake back) and insurance pays 2:1. + const insured = act(state, 0, "insurance", 5); + expect(insured.phase).toBe("round-complete"); + expect(insured.roundResult?.dealerHasBlackjack).toBe(true); + expect(insured.players[0]?.hands[0]?.outcome).toBe("push"); + // 1000 - 10 (bet) - 5 (insurance) + 10 (push stake) + 5 + 10 (insurance 2:1) + expect(insured.players[0]?.bankroll).toBe(1010); + }); + + it("settles immediately on a ten-value upcard peek without insurance", () => { + const state = bet(soloTable(shoeOf(c(10, "s"), c(10, "h"), c(9, "s"), c(14, "d")))); + expect(state.phase).toBe("round-complete"); + expect(state.roundResult?.dealerHasBlackjack).toBe(true); + expect(state.players[0]?.hands[0]?.outcome).toBe("lose"); + expect(state.players[0]?.bankroll).toBe(STARTING_BANKROLL - MIN_BET); + }); + + it("declines insurance and continues the round", () => { + const state = bet(soloTable(shoeOf(c(10, "s"), c(14, "h"), c(9, "s"), c(6, "d")))); + expect(state.phase).toBe("insurance"); + const declined = act(state, 0, "decline-insurance"); + expect(declined.players[0]?.insuranceBet).toBe(0); + expect(declined.phase).toBe("player"); + }); +}); + +describe("settlement and payouts", () => { + it("pays a natural blackjack 3:2", () => { + const state = bet(soloTable(shoeOf(c(14, "s"), c(5, "h"), c(13, "s"), c(9, "d")))); + expect(state.phase).toBe("round-complete"); + const hand = state.players[0]?.hands[0]; + expect(hand?.outcome).toBe("blackjack"); + expect(hand?.netChange).toBe(Math.floor(MIN_BET * BLACKJACK_PAYOUT)); + expect(state.roundResult?.hands[0]?.payout).toBe(Math.floor(MIN_BET * BLACKJACK_PAYOUT)); + expect(state.players[0]?.bankroll).toBe( + STARTING_BANKROLL - MIN_BET + MIN_BET + Math.floor(MIN_BET * BLACKJACK_PAYOUT), + ); + expect(state.players[0]?.blackjacksHit).toBe(1); + expect(state.players[0]?.roundsWon).toBe(1); + }); + + it("pushes equal totals and returns the stake", () => { + // Player stands on 20 (10+K); dealer 10+K also makes 20 (draws nothing). + const state = bet( + soloTable(shoeOf(c(10, "s"), c(10, "h"), c(13, "s"), c(13, "h"), c(2, "s"))), + ); + expect(state.phase).toBe("player"); + const settled = act(state, 0, "stand"); + const hand = settled.players[0]?.hands[0]; + expect(hand?.outcome).toBe("push"); + expect(hand?.netChange).toBe(0); + expect(settled.players[0]?.bankroll).toBe(STARTING_BANKROLL); + expect(settled.players[0]?.roundsWon).toBe(0); + }); + + it("pays a regular win 1:1", () => { + // Player 20 stands; dealer 16 draws the crafted 7 and busts. + const state = bet(soloTable(shoeOf(c(10, "s"), c(10, "h"), c(10, "s"), c(6, "h"), c(7, "s")))); + const settled = act(state, 0, "stand"); + const hand = settled.players[0]?.hands[0]; + expect(hand?.outcome).toBe("win"); + expect(hand?.netChange).toBe(MIN_BET); + expect(settled.players[0]?.bankroll).toBe(STARTING_BANKROLL + MIN_BET); + expect(settled.roundResult?.dealerBusted).toBe(true); + }); + + it("flags a seat bankrupt once the bankroll drops below the table minimum", () => { + // Player 16 loses to a dealer 17; the 15-chip bankroll drops to 5. + const state = bet(soloTable(shoeOf(c(9, "s"), c(10, "h"), c(7, "d"), c(7, "s")), 15)); + const settled = act(state, 0, "stand"); + expect(settled.players[0]?.bankroll).toBe(5); + expect(settled.players[0]?.bankrupt).toBe(true); + }); + + it("starts the next round from a settled state and resets per-round data", () => { + const state = bet(soloTable(shoeOf(c(10, "s"), c(6, "h"), c(9, "s"), c(7, "d"), c(4, "s")))); + const settled = act(state, 0, "stand"); + const next = startNextRound(settled); + expect(next.roundNumber).toBe(2); + expect(next.phase).toBe("betting"); + expect(next.toActSeat).toBe(0); + expect(next.players[0]?.hands).toHaveLength(0); + expect(next.players[0]?.hasBet).toBe(false); + expect(next.roundResult).toBeNull(); + expect(next.dealer.cards).toHaveLength(0); + }); + + it("drops bankrupt and listed seats when the next round starts", () => { + const state = bet(soloTable(shoeOf(c(10, "s"), c(10, "h"), c(9, "s"), c(7, "d")), 15)); + const settled = act(state, 0, "stand"); + const next = startNextRound(settled, [0]); + expect(next.players).toHaveLength(0); + expect(next.toActSeat).toBeNull(); + }); +}); + +describe("views and legal actions", () => { + it("hides the dealer hole card until reveal", () => { + const state = bet(soloTable(shoeOf(c(10, "s"), c(6, "h"), c(9, "s"), c(7, "d")))); + const view = buildPrivateView(state, 0); + expect(view.dealer.cards).toHaveLength(1); + expect(view.dealer.holeHidden).toBe(true); + expect(view.dealer.total).toBeNull(); + const settled = act(state, 0, "stand"); + expect(buildPrivateView(settled, 0).dealer.holeHidden).toBe(false); + }); + + it("exposes turn-scoped legal actions", () => { + const betting = makeTable([STARTING_BANKROLL, STARTING_BANKROLL]); + expect(getLegalActions(betting, 0).canBet).toBe(true); + expect(getLegalActions(betting, 0).maxBet).toBe(500); + expect(getLegalActions(betting, 1).canBet).toBe(false); + const state = bet(soloTable(shoeOf(c(8, "s"), c(6, "h"), c(8, "h"), c(10, "d")))); + const actions = getLegalActions(state, 0); + expect(actions.canHit).toBe(true); + expect(actions.canStand).toBe(true); + expect(actions.canDouble).toBe(true); + expect(actions.canSplit).toBe(true); + expect(actions.canInsure).toBe(false); + const spectator = buildPrivateView(state, -1); + expect(spectator.viewerSeat).toBe(-1); + expect(spectator.yourTurn).toBe(false); + }); + + it("does not mutate the state on idempotent advances", () => { + const state = bet(soloTable(shoeOf(c(10, "s"), c(6, "h"), c(9, "s"), c(7, "d")))); + expect(advanceState(state)).toEqual(state); + }); +}); + +describe("settleRound direct resolution", () => { + it("resolves hand-built states with known totals", () => { + const base = makeTable([1000, 1000], 3); + const playerFor = (name: string, cards: Card[], betAmount: number) => ({ + seatId: 0, + name, + bankroll: 1000 - betAmount, + hands: [ + { + cards, + bet: betAmount, + status: "stood" as const, + doubled: false, + fromSplit: false, + splitAces: false, + outcome: null, + netChange: null, + }, + ], + insuranceBet: 0, + insuranceAnswered: false, + hasBet: true, + sittingOut: false, + bankrupt: false, + roundsPlayed: 0, + roundsWon: 0, + blackjacksHit: 0, + }); + const a = playerFor("A", [c(10, "s"), c(10, "h")], 50); + const b = { ...playerFor("B", [c(10, "d"), c(6, "s")], 20), seatId: 1 as const }; + const withHands: BlackjackTableState = { + ...base, + players: [a, b], + dealer: { cards: [c(9, "h"), c(10, "c")], holeRevealed: true }, + phase: "player", + toActSeat: null, + }; + const settled = settleRound(withHands); + expect(settled.players[0]?.bankroll).toBe(1000 + 50); + expect(settled.players[1]?.bankroll).toBe(1000 - 20); + expect(settled.roundResult?.hands).toHaveLength(2); + expect(settled.roundResult?.hands[0]?.outcome).toBe("win"); + expect(settled.roundResult?.hands[1]?.outcome).toBe("lose"); + }); +}); + +describe("table limits", () => { + it("supports solo through six players and rejects beyond", () => { + expect(MIN_PLAYERS).toBe(1); + expect(MAX_PLAYERS).toBe(6); + const six = createTableState( + Array.from({ length: 6 }, (_, index) => ({ name: `P${index + 1}` })), + {}, + mulberry32(2), + ); + expect(six.players).toHaveLength(6); + expect(() => + createTableState( + Array.from({ length: 7 }, (_, index) => ({ name: `P${index + 1}` })), + {}, + mulberry32(2), + ), + ).toThrow("1 to 6 players"); + }); +}); diff --git a/tests/int/blackjack-popups.int.spec.ts b/tests/int/blackjack-popups.int.spec.ts new file mode 100644 index 0000000..72d96b1 --- /dev/null +++ b/tests/int/blackjack-popups.int.spec.ts @@ -0,0 +1,247 @@ +import { describe, expect, it } from "vitest"; +import type { PublicDealerView, PublicPlayerView } from "@/lib/minigames/blackjack"; +import { + actionLogText, + popupEventsForTransition, + type BlackjackPopupEvent, +} from "@/components/frontend/minigames/blackjack/blackjackPopups"; +import { + isTopSeatPosition, + seatPosition, +} from "@/components/frontend/minigames/blackjack/seatPosition"; +import type { BlackjackSoundSnapshot } from "@/components/frontend/minigames/blackjack/blackjackSoundEvents"; + +function hand( + overrides: Partial = {}, +): PublicPlayerView["hands"][number] { + return { + cards: [], + bet: 25, + status: "active", + doubled: false, + fromSplit: false, + splitAces: false, + total: 21, + outcome: null, + netChange: null, + ...overrides, + }; +} + +function player(seatId: number, overrides: Partial = {}): PublicPlayerView { + return { + seatId, + name: `Player ${seatId + 1}`, + bankroll: 1000, + hands: [], + insuranceBet: 0, + insuranceAnswered: false, + hasBet: false, + sittingOut: false, + bankrupt: false, + left: false, + roundsPlayed: 0, + roundsWon: 0, + blackjacksHit: 0, + ...overrides, + }; +} + +function dealerView(overrides: Partial = {}): PublicDealerView { + return { + cards: [], + holeHidden: true, + total: null, + hasBlackjack: null, + ...overrides, + }; +} + +function view( + overrides: Partial = {}, +): BlackjackSoundSnapshot["you"] { + return { + roundNumber: 1, + phase: "betting", + players: [player(0), player(1)], + dealer: dealerView(), + roundResult: null, + viewerSeat: 0, + yourTurn: false, + yourHands: [], + ...overrides, + }; +} + +function snapshot( + version: number, + nextView: BlackjackSoundSnapshot["you"] = view(), +): BlackjackSoundSnapshot { + return { version, you: nextView }; +} + +function bySeat(events: readonly BlackjackPopupEvent[], seatId: number): BlackjackPopupEvent | undefined { + return events.find((event) => event.seatId === seatId); +} + +describe("blackjack popup transition events", () => { + it("labels a placed bet with its amount over the acting seat", () => { + const events = popupEventsForTransition( + snapshot(1, view()), + snapshot( + 2, + view({ players: [player(0, { hasBet: true, hands: [hand({ bet: 25 })] }), player(1)] }), + ), + ); + const bettor = bySeat(events, 0); + expect(bettor?.tone).toBe("wager"); + expect(bettor?.text).toBe("Bet +25"); + }); + + it("labels insurance, splits, and doubles over the acting seat", () => { + const events = popupEventsForTransition( + snapshot( + 1, + view({ + players: [ + player(0, { hasBet: true, hands: [hand()] }), + player(1, { hasBet: true, hands: [hand()] }), + ], + }), + ), + snapshot( + 2, + view({ + players: [ + player(0, { + hasBet: true, + insuranceBet: 5, + hands: [hand({ doubled: true, bet: 50 })], + }), + player(1, { hasBet: true, hands: [hand(), hand({ fromSplit: true })] }), + ], + }), + ), + ); + expect(events.some((event) => event.text === "Insurance +5")).toBe(true); + expect(events.some((event) => event.text.startsWith("Double +"))).toBe(true); + const splitter = events.find((event) => event.seatId === 1 && event.text === "Split"); + expect(splitter?.tone).toBe("action"); + }); + + it("labels busts and naturals as hand-status changes", () => { + const events = popupEventsForTransition( + snapshot( + 1, + view({ + phase: "player", + players: [player(0, { hasBet: true, hands: [hand()] }), player(1, { hasBet: true, hands: [hand()] })], + }), + ), + snapshot( + 2, + view({ + phase: "player", + players: [ + player(0, { hasBet: true, hands: [hand({ status: "bust" })] }), + player(1, { hasBet: true, hands: [hand({ status: "blackjack" })] }), + ], + }), + ), + ); + expect(bySeat(events, 0)?.text).toBe("Bust"); + expect(bySeat(events, 0)?.tone).toBe("fold"); + expect(bySeat(events, 1)?.text).toBe("Blackjack!"); + expect(bySeat(events, 1)?.tone).toBe("win"); + }); + + it("fires one aggregated win popup per seat when a round settles", () => { + const settledView = view({ + phase: "round-complete", + roundResult: { + roundNumber: 1, + dealerTotal: 22, + dealerBusted: true, + dealerHasBlackjack: false, + hands: [ + { + seatId: 0, + handIndex: 0, + total: 20, + outcome: "win", + payout: 50, + netChange: 50, + }, + { + seatId: 1, + handIndex: 0, + total: 18, + outcome: "win", + payout: 25, + netChange: 25, + }, + ], + insurance: [], + }, + }); + const events = popupEventsForTransition(snapshot(1, view()), snapshot(2, settledView)); + expect(bySeat(events, 0)?.text).toBe("+50"); + expect(bySeat(events, 0)?.tone).toBe("win"); + expect(bySeat(events, 1)?.text).toBe("+25"); + }); + + it("announces leavers and bankrupt players with leave tone", () => { + const events = popupEventsForTransition( + snapshot(1, view()), + snapshot( + 2, + view({ + players: [player(0, { left: true }), player(1, { bankrupt: true })], + }), + ), + ); + expect(bySeat(events, 0)?.tone).toBe("leave"); + expect(bySeat(events, 0)?.text).toBe("Player 1 left the table"); + expect(bySeat(events, 1)?.text).toBe("Player 2 is out of chips"); + }); + + it("returns no events for the same version", () => { + const current = snapshot(3, view()); + expect(popupEventsForTransition(current, current)).toEqual([]); + }); +}); + +describe("actionLogText", () => { + it("prefixes the acting player's name", () => { + const players = [player(0, { name: "Harlan" })]; + expect( + actionLogText({ key: "k", seatId: 0, text: "Bet +25", tone: "wager" }, players), + ).toBe("Harlan: Bet +25"); + expect( + actionLogText({ key: "k", seatId: 0, text: "+50", tone: "win" }, players), + ).toBe("Harlan wins +50"); + expect( + actionLogText({ key: "k", seatId: 0, text: "Gone", tone: "leave" }, players), + ).toBe("Gone"); + expect( + actionLogText({ key: "k", seatId: 4, text: "Bet +25", tone: "wager" }, players), + ).toBe("Seat 5: Bet +25"); + }); +}); + +describe("seatPosition", () => { + it("spreads one through five opponents across the bottom two-thirds arc", () => { + expect(seatPosition(0, 1)).toBe("blackjack-seat-arc-corner-right"); + expect(seatPosition(0, 2)).toBe("blackjack-seat-arc-corner-right"); + expect(seatPosition(1, 2)).toBe("blackjack-seat-arc-corner-left"); + expect(seatPosition(2, 5)).toBe("blackjack-seat-arc-low-right"); + expect(seatPosition(4, 5)).toBe("blackjack-seat-arc-corner-left"); + }); + + it("classifies the dealer-adjacent corner seats for popup placement", () => { + expect(isTopSeatPosition("blackjack-seat-arc-corner-right")).toBe(true); + expect(isTopSeatPosition("blackjack-seat-arc-corner-left")).toBe(true); + expect(isTopSeatPosition("blackjack-seat-arc-low-right")).toBe(false); + expect(isTopSeatPosition("blackjack-seat-arc-mid-left")).toBe(false); + }); +}); diff --git a/tests/int/blackjack-sound.int.spec.ts b/tests/int/blackjack-sound.int.spec.ts new file mode 100644 index 0000000..94690cf --- /dev/null +++ b/tests/int/blackjack-sound.int.spec.ts @@ -0,0 +1,198 @@ +import { describe, expect, it } from "vitest"; +import type { Card, PublicPlayerView } from "@/lib/minigames/blackjack"; +import { + soundEventsForTransition, + type BlackjackSoundEvent, + type BlackjackSoundSnapshot, +} from "@/components/frontend/minigames/blackjack/blackjackSoundEvents"; + +const card = (rank: Card["rank"], suit: Card["suit"]): Card => ({ rank, suit }); + +function player(seatId: number, overrides: Partial = {}): PublicPlayerView { + return { + seatId, + name: `Player ${seatId + 1}`, + bankroll: 1000, + hands: [], + insuranceBet: 0, + insuranceAnswered: false, + hasBet: false, + sittingOut: false, + bankrupt: false, + left: false, + roundsPlayed: 0, + roundsWon: 0, + blackjacksHit: 0, + ...overrides, + }; +} + +function hand( + overrides: Partial = {}, +): PublicPlayerView["hands"][number] { + return { + cards: [], + bet: 25, + status: "active", + doubled: false, + fromSplit: false, + splitAces: false, + total: 21, + outcome: null, + netChange: null, + ...overrides, + }; +} + +function view( + overrides: Partial = {}, +): BlackjackSoundSnapshot["you"] { + return { + roundNumber: 1, + phase: "betting", + players: [player(0), player(1)], + dealer: { cards: [], holeHidden: true, total: null, hasBlackjack: null }, + roundResult: null, + viewerSeat: 0, + yourTurn: false, + yourHands: [], + ...overrides, + }; +} + +function snapshot( + version: number, + nextView: BlackjackSoundSnapshot["you"] = view(), +): BlackjackSoundSnapshot { + return { version, you: nextView }; +} + +function byType(events: readonly BlackjackSoundEvent[], type: BlackjackSoundEvent["type"]) { + return events.find((event) => event.type === type); +} + +describe("blackjack sound transition events", () => { + it("plays a round-start chime when the round counter advances", () => { + const events = soundEventsForTransition( + snapshot(1, view({ roundNumber: 1, phase: "round-complete" })), + snapshot(2, view({ roundNumber: 2, phase: "betting" })), + ); + expect(byType(events, "round-start")).toBeDefined(); + }); + + it("announces turn changes", () => { + const events = soundEventsForTransition( + snapshot(1, view({ yourTurn: false })), + snapshot(2, view({ yourTurn: true, phase: "player" })), + ); + expect(byType(events, "turn-gained")).toBeDefined(); + + const back = soundEventsForTransition( + snapshot(2, view({ yourTurn: true, phase: "player" })), + snapshot(3, view({ yourTurn: false, phase: "player" })), + ); + expect(byType(back, "turn-lost")).toBeDefined(); + }); + + it("announces cards hitting the felt", () => { + const dealtCards = [card(10, "s"), card(9, "h")]; + const events = soundEventsForTransition( + snapshot(1, view()), + snapshot( + 2, + view({ + phase: "player", + players: [player(0, { hasBet: true, hands: [hand({ cards: dealtCards })] }), player(1)], + dealer: { cards: [card(6, "d"), card(7, "c")], holeHidden: true, total: null, hasBlackjack: null }, + }), + ), + ); + const deal = byType(events, "cards-dealt"); + expect(deal).toBeDefined(); + if (deal?.type === "cards-dealt") expect(deal.cardCount).toBe(4); + }); + + it("announces a wager and a bust", () => { + const events = soundEventsForTransition( + snapshot(1, view({ players: [player(0, { hands: [hand()] }), player(1)] })), + snapshot( + 2, + view({ + players: [player(0, { hasBet: true, hands: [hand({ cards: [card(5, "s"), card(6, "s")] })] }), player(1)], + }), + ), + ); + expect(byType(events, "wager")).toBeDefined(); + + const bustEvents = soundEventsForTransition( + snapshot( + 3, + view({ + phase: "player", + players: [player(0, { hasBet: true, hands: [hand({ cards: [card(5, "s"), card(6, "s")] })] }), player(1)], + }), + ), + snapshot( + 4, + view({ + phase: "player", + players: [ + player(0, { + hasBet: true, + hands: [hand({ status: "bust", cards: [card(5, "s"), card(6, "s"), card(10, "d")] })], + }), + player(1), + ], + }), + ), + ); + expect(byType(bustEvents, "bust")).toBeDefined(); + }); + + it("settles with a viewer result event", () => { + const settled = view({ + phase: "round-complete", + roundResult: { + roundNumber: 1, + dealerTotal: 19, + dealerBusted: false, + dealerHasBlackjack: false, + hands: [ + { seatId: 0, handIndex: 0, total: 20, outcome: "win", payout: 25, netChange: 25 }, + ], + insurance: [], + }, + }); + const events = soundEventsForTransition(snapshot(1, view()), snapshot(2, settled)); + expect(byType(events, "settle")).toBeDefined(); + const result = byType(events, "hand-result"); + expect(result).toBeDefined(); + if (result?.type === "hand-result") { + expect(result.result).toBe("win"); + expect(result.netChange).toBe(25); + } + }); + + it("reports a push result on a tie", () => { + const settled = view({ + phase: "round-complete", + roundResult: { + roundNumber: 1, + dealerTotal: 20, + dealerBusted: false, + dealerHasBlackjack: false, + hands: [{ seatId: 0, handIndex: 0, total: 20, outcome: "push", payout: 0, netChange: 0 }], + insurance: [], + }, + }); + const events = soundEventsForTransition(snapshot(1, view()), snapshot(2, settled)); + const result = byType(events, "hand-result"); + expect(result).toBeDefined(); + if (result?.type === "hand-result") expect(result.result).toBe("push"); + }); + + it("returns no events for the same version", () => { + const current = snapshot(5, view()); + expect(soundEventsForTransition(current, current)).toEqual([]); + }); +}); diff --git a/tests/int/blackjack-stats.int.spec.ts b/tests/int/blackjack-stats.int.spec.ts new file mode 100644 index 0000000..96d9947 --- /dev/null +++ b/tests/int/blackjack-stats.int.spec.ts @@ -0,0 +1,133 @@ +import { describe, expect, it } from "vitest"; +import { + BLACKJACK_XP_ENABLED, + BLACKJACK_XP_CAP, + computeBlackjackStatsDelta, + readBlackjackStats, + xpForProfit, + type BlackjackSessionResult, + type BlackjackStats, +} from "@/lib/minigames/blackjack/stats"; + +function result(overrides: Partial = {}): BlackjackSessionResult { + return { + userId: 1, + roundsPlayed: 10, + roundsWon: 4, + blackjacksHit: 1, + finalBankroll: 1250, + ...overrides, + }; +} + +function current(overrides: Partial = {}): BlackjackStats { + return { + gamesPlayed: 0, + roundsWon: 0, + blackjacksHit: 0, + bestScore: 0, + xpEarned: 0, + ...overrides, + }; +} + +describe("xpForProfit", () => { + it("awards the base XP for a breakeven or losing session", () => { + expect(xpForProfit(1000)).toBe(5); + expect(xpForProfit(500)).toBe(5); + }); + + it("awards no bracket bonus below a full 25% profit", () => { + expect(xpForProfit(1100)).toBe(5); + expect(xpForProfit(1249)).toBe(5); + }); + + it("awards 10 XP per full 25% profit bracket", () => { + expect(xpForProfit(1250)).toBe(15); + expect(xpForProfit(1500)).toBe(25); + expect(xpForProfit(1750)).toBe(35); + expect(xpForProfit(2000)).toBe(45); + }); + + it("caps the payout at the session cap", () => { + expect(BLACKJACK_XP_CAP).toBe(50); + expect(xpForProfit(2500)).toBe(BLACKJACK_XP_CAP); + expect(xpForProfit(100000)).toBe(BLACKJACK_XP_CAP); + }); +}); + +describe("BLACKJACK_XP_ENABLED", () => { + it("is disabled by default until the XP system is reworked", () => { + expect(BLACKJACK_XP_ENABLED).toBe(false); + }); +}); + +describe("computeBlackjackStatsDelta", () => { + it("increments gamesPlayed by exactly one", () => { + const next = computeBlackjackStatsDelta(current({ gamesPlayed: 7 }), result()); + expect(next.gamesPlayed).toBe(8); + }); + + it("accumulates rounds won and blackjacks hit", () => { + const next = computeBlackjackStatsDelta( + current({ roundsWon: 10, blackjacksHit: 2 }), + result({ roundsWon: 4, blackjacksHit: 1 }), + ); + expect(next.roundsWon).toBe(14); + expect(next.blackjacksHit).toBe(3); + }); + + it("keeps bestScore when the final bankroll is lower", () => { + const next = computeBlackjackStatsDelta( + current({ bestScore: 9000 }), + result({ finalBankroll: 1250 }), + ); + expect(next.bestScore).toBe(9000); + }); + + it("raises bestScore when the final bankroll is higher", () => { + const next = computeBlackjackStatsDelta( + current({ bestScore: 1000 }), + result({ finalBankroll: 2400 }), + ); + expect(next.bestScore).toBe(2400); + }); + + it("adds the profit XP to the existing xpEarned total", () => { + const next = computeBlackjackStatsDelta( + current({ xpEarned: 100 }), + result({ finalBankroll: 1500 }), + ); + expect(next.xpEarned).toBe(125); + }); + + it("does not mutate the current stats object", () => { + const before = current({ gamesPlayed: 1, xpEarned: 125 }); + computeBlackjackStatsDelta(before, result()); + expect(before).toEqual(current({ gamesPlayed: 1, xpEarned: 125 })); + }); +}); + +describe("readBlackjackStats", () => { + it("reads blackjack stats from a profile that also carries sibling games", () => { + const profile = { + progression: { + minigames: { + poker: { gamesPlayed: 12, xpEarned: 40 }, + blackjack: { gamesPlayed: 3, roundsWon: 5, blackjacksHit: 2, bestScore: 1500, xpEarned: 15 }, + }, + }, + }; + expect(readBlackjackStats(profile)).toEqual( + current({ gamesPlayed: 3, roundsWon: 5, blackjacksHit: 2, bestScore: 1500, xpEarned: 15 }), + ); + }); + + it("returns all zeros when the blackjack group is missing", () => { + expect(readBlackjackStats({ progression: { minigames: {} } })).toEqual(current()); + }); + + it("returns all zeros for a nullish profile", () => { + expect(readBlackjackStats(undefined)).toEqual(current()); + }); +}); diff --git a/tests/int/blackjack-tables.int.spec.ts b/tests/int/blackjack-tables.int.spec.ts new file mode 100644 index 0000000..26a637f --- /dev/null +++ b/tests/int/blackjack-tables.int.spec.ts @@ -0,0 +1,189 @@ +import { beforeEach, describe, expect, it } from "vitest"; + +import { + applyBlackjackAction, + clearTablesForTests, + createTable, + getSnapshotFor, + joinTable, + leaveTable, + renameTable, + spectateTable, + startGame, +} from "@/lib/minigames/blackjack/tables"; + +/** + * Regression tests for the blackjack registry: game start, seating, lobby + * rules, and snapshots. Database-free except for the fire-and-forget award + * path, which skips users who never played a round. + */ + +beforeEach(() => { + clearTablesForTests(); +}); + +describe("blackjack table registry: startGame", () => { + it("starts a solo game immediately against the dealer", () => { + const tableId = createTable(9001, "Host"); + const snapshot = startGame(tableId, 9001); + expect(snapshot.inGame).toBe(true); + expect(snapshot.you?.phase).toBe("betting"); + expect(snapshot.you?.activeSeat).toBe(0); + expect(snapshot.you?.yourTurn).toBe(true); + expect(snapshot.you?.legalActions.canBet).toBe(true); + }); + + it("starts a six-player game with every seat backed by a user", () => { + const tableId = createTable(9101, "Host"); + joinTable(tableId, 9102, "Second"); + joinTable(tableId, 9103, "Third"); + joinTable(tableId, 9104, "Fourth"); + joinTable(tableId, 9105, "Fifth"); + joinTable(tableId, 9106, "Sixth"); + + const snapshot = startGame(tableId, 9101); + const seatedIds = snapshot.seats.filter((id) => id !== null); + expect(seatedIds).toHaveLength(6); + const players = snapshot.you?.players ?? []; + expect(players).toHaveLength(6); + for (const player of players) { + expect(player.name).not.toBe("Player null"); + expect(player.name.length).toBeGreaterThan(0); + const userId = snapshot.seats[player.seatId]; + expect(userId).not.toBeNull(); + } + expect(snapshot.seats).toHaveLength(6); + }); + + it("keeps the seat map padded to six with trailing nulls", () => { + const tableId = createTable(9201, "Host"); + joinTable(tableId, 9202, "Second"); + startGame(tableId, 9201); + const snapshot = getSnapshotFor(tableId, 9201); + expect(snapshot.seats).toEqual([9201, 9202, null, null, null, null]); + }); +}); + +describe("blackjack table registry: lobby rules", () => { + it("rejects a second table for a user already seated elsewhere", () => { + const first = createTable(9301, "Host"); + createTable(9302, "Other"); + expect(() => joinTable(first, 9302, "Other")).toThrow("already seated at another table"); + }); + + it("lets the host start with a single player and rejects unstarted actions", () => { + const tableId = createTable(9401, "Host"); + expect(() => applyBlackjackAction(tableId, 9401, "bet", 10)).toThrow( + "The game has not started yet.", + ); + startGame(tableId, 9401); + expect(() => startGame(tableId, 9999)).toThrow("Only the host can start the game."); + }); + + it("queues mid-game joins for the next round", () => { + const tableId = createTable(9501, "Host"); + joinTable(tableId, 9502, "Second"); + startGame(tableId, 9501); + const latecomer = joinTable(tableId, 9503, "Latecomer"); + // Pending joiners have a seat in the map but no engine hand until the + // next round starts, so their snapshot carries no private view yet. + expect(latecomer.you).toBeNull(); + expect(latecomer.seats).toContain(9503); + expect(() => applyBlackjackAction(tableId, 9503, "bet", 10)).toThrow( + "You will join the game at the next round.", + ); + }); + + it("spectates a running game with the public view and no seat", () => { + const tableId = createTable(9601, "Host"); + joinTable(tableId, 9602, "Guest"); + startGame(tableId, 9601); + const snapshot = spectateTable(tableId, 9603); + expect(snapshot.spectator).toBe(true); + expect(snapshot.you?.viewerSeat).toBe(-1); + expect(snapshot.you?.players).toHaveLength(2); + expect(snapshot.you?.yourTurn).toBe(false); + expect(snapshot.you?.legalActions.canBet).toBe(false); + expect(() => spectateTable(createTable(9604, "Idle"), 9603)).toThrow( + "No game is running at this table.", + ); + }); + + it("compacts the seat map and promotes the host when leaving pre-game", () => { + const tableId = createTable(9701, "Host"); + joinTable(tableId, 9702, "Second"); + joinTable(tableId, 9703, "Third"); + leaveTable(tableId, 9701); + const snapshot = getSnapshotFor(tableId, 9702); + expect(snapshot.seats).toEqual([9702, 9703, null, null, null, null]); + expect(snapshot.hostSeatId).toBe(0); + }); + + it("marks mid-game leavers as left in every snapshot", () => { + const tableId = createTable(9801, "Host"); + joinTable(tableId, 9802, "Guest"); + startGame(tableId, 9801); + leaveTable(tableId, 9802); + const host = getSnapshotFor(tableId, 9801); + expect(host.you?.players.find((player) => player.seatId === 1)?.left).toBe(true); + expect(host.you?.players.find((player) => player.seatId === 0)?.left).toBe(false); + }); +}); + +describe("blackjack table registry: gameplay through the registry", () => { + it("runs a solo betting round end to end", () => { + const tableId = createTable(9901, "Host"); + startGame(tableId, 9901); + let afterBet = applyBlackjackAction(tableId, 9901, "bet", 25); + // The shoe is random: an ace upcard opens an insurance offer, and a + // dealt natural settles the round on the spot. Walk whichever path comes. + if (afterBet.you?.phase === "insurance") { + afterBet = applyBlackjackAction(tableId, 9901, "decline-insurance"); + } + if (afterBet.you?.phase === "player") { + afterBet = applyBlackjackAction(tableId, 9901, "stand"); + } + expect(afterBet.you?.phase).toBe("round-complete"); + expect(afterBet.you?.yourHands).toHaveLength(1); + expect(afterBet.you?.yourHands[0]?.bet).toBe(25); + expect(afterBet.version).toBeGreaterThan(1); + expect(afterBet.you?.roundResult).not.toBeNull(); + expect(afterBet.you?.roundResult?.hands).toHaveLength(1); + expect(afterBet.nextRoundAt).not.toBeNull(); + // The next round has not auto-started yet (the registry timer owns that). + expect(() => applyBlackjackAction(tableId, 9901, "bet", 10)).toThrow( + "not accepting bets", + ); + }); + + it("validates bet limits through the registry", () => { + const tableId = createTable(9911, "Host"); + startGame(tableId, 9911); + expect(() => applyBlackjackAction(tableId, 9911, "bet", 9)).toThrow("minimum bet"); + expect(() => applyBlackjackAction(tableId, 9911, "bet", 501)).toThrow("maximum bet"); + }); +}); + +describe("blackjack table registry: table management", () => { + it("renames tables as host with length truncation", () => { + const tableId = createTable(9921, "Host"); + const renamed = renameTable(tableId, 9921, " High Roller Room "); + expect(renamed.name).toBe("High Roller Room"); + const longName = "X".repeat(60); + expect(renameTable(tableId, 9921, longName).name).toHaveLength(48); + expect(() => renameTable(tableId, 9999, "Nope")).toThrow("Only the host"); + }); + + it("caps live tables at the registry maximum", () => { + for (let index = 0; index < 20; index += 1) { + createTable(10000 + index, `Host ${index}`); + } + expect(() => createTable(10999, "Overflow")).toThrow("maximum number of blackjack tables"); + }); + + it("disposes a table once everyone leaves", () => { + const tableId = createTable(11001, "Host"); + leaveTable(tableId, 11001); + expect(() => getSnapshotFor(tableId, 11001)).toThrow("Table not found."); + }); +});