From 752041a375889849ebd295862099705636f1445e Mon Sep 17 00:00:00 2001 From: noe Date: Mon, 15 Jul 2024 14:13:23 -0400 Subject: [PATCH] sync --- .envrc | 1 + .gitignore | 1 + app/components/alert-timer.tsx | 6 +- app/components/faction-bar-sxs.css.ts | 15 + app/components/faction-bar-sxs.tsx | 41 + app/components/faction-bar.css.ts | 13 +- app/components/faction-bar.tsx | 2 +- app/components/faction-pie.tsx | 10 +- app/components/footer.css.ts | 2 +- app/components/index-world-container.tsx | 4 +- app/components/index-world.tsx | 20 +- app/components/world-zone-container.css.ts | 58 + app/components/world-zone-container.tsx | 98 + app/components/world.css.ts | 1 - app/entry.client.tsx | 18 - app/entry.server.tsx | 38 - app/images/icon_engi.png | Bin 0 -> 1065 bytes app/images/icon_heavy.png | Bin 0 -> 844 bytes app/images/icon_infil.png | Bin 0 -> 1008 bytes app/images/icon_light.png | Bin 0 -> 1278 bytes app/images/icon_max.png | Bin 0 -> 902 bytes app/images/icon_medic.png | Bin 0 -> 266 bytes app/images/vehicles/ant.png | Bin 0 -> 231 bytes app/images/vehicles/chimera.png | Bin 0 -> 2188 bytes app/images/vehicles/corsair.png | Bin 0 -> 375 bytes app/images/vehicles/dervish.png | Bin 0 -> 2841 bytes app/images/vehicles/flash.png | Bin 0 -> 429 bytes app/images/vehicles/galaxy.png | Bin 0 -> 466 bytes app/images/vehicles/harasser.png | Bin 0 -> 505 bytes app/images/vehicles/javelin.png | Bin 0 -> 361 bytes app/images/vehicles/liberator.png | Bin 0 -> 505 bytes app/images/vehicles/lightning.png | Bin 0 -> 505 bytes app/images/vehicles/magrider.png | Bin 0 -> 585 bytes app/images/vehicles/mosquito.png | Bin 0 -> 429 bytes app/images/vehicles/prowler.png | Bin 0 -> 554 bytes app/images/vehicles/reaver.png | Bin 0 -> 467 bytes app/images/vehicles/scythe.png | Bin 0 -> 535 bytes app/images/vehicles/sunderer.png | Bin 0 -> 511 bytes app/images/vehicles/valkyrie.png | Bin 0 -> 555 bytes app/images/vehicles/vanguard.png | Bin 0 -> 488 bytes app/root.tsx | 13 +- app/routes/_index.tsx | 14 +- app/routes/about.tsx | 4 +- app/routes/debug.components.tsx | 11 +- app/routes/worlds.$id.tsx | 56 +- app/utils/class-icons.ts | 17 + app/utils/saerro.ts | 12 +- app/utils/sorting.ts | 22 + app/utils/theme.ts | 8 + app/utils/vehicle-icons.ts | 60 + build/index.js | 1904 +++ build/index.js.map | 7 + build/metafile.css.json | 1 + build/metafile.js.json | 1 + build/metafile.server.json | 1 + build/version.txt | 1 + bun.lockb | Bin 0 -> 341358 bytes flake.lock | 58 + flake.nix | 23 + hack/download-vehicle-icons.mjs | 42 + package-lock.json | 12214 ++++++++----------- package.json | 52 +- remix.config.js | 23 - remix.env.d.ts | 2 - server.mjs | 26 + server.ts | 8 - shell.nix | 8 + tsconfig.json | 3 - vite.config.js | 7 + wrangler.toml | 1 - 70 files changed, 7484 insertions(+), 7443 deletions(-) create mode 100644 .envrc create mode 100644 app/components/faction-bar-sxs.css.ts create mode 100644 app/components/faction-bar-sxs.tsx create mode 100644 app/components/world-zone-container.css.ts create mode 100644 app/components/world-zone-container.tsx delete mode 100644 app/entry.client.tsx delete mode 100644 app/entry.server.tsx create mode 100644 app/images/icon_engi.png create mode 100644 app/images/icon_heavy.png create mode 100644 app/images/icon_infil.png create mode 100644 app/images/icon_light.png create mode 100644 app/images/icon_max.png create mode 100644 app/images/icon_medic.png create mode 100644 app/images/vehicles/ant.png create mode 100644 app/images/vehicles/chimera.png create mode 100644 app/images/vehicles/corsair.png create mode 100644 app/images/vehicles/dervish.png create mode 100644 app/images/vehicles/flash.png create mode 100644 app/images/vehicles/galaxy.png create mode 100644 app/images/vehicles/harasser.png create mode 100644 app/images/vehicles/javelin.png create mode 100644 app/images/vehicles/liberator.png create mode 100644 app/images/vehicles/lightning.png create mode 100644 app/images/vehicles/magrider.png create mode 100644 app/images/vehicles/mosquito.png create mode 100644 app/images/vehicles/prowler.png create mode 100644 app/images/vehicles/reaver.png create mode 100644 app/images/vehicles/scythe.png create mode 100644 app/images/vehicles/sunderer.png create mode 100644 app/images/vehicles/valkyrie.png create mode 100644 app/images/vehicles/vanguard.png create mode 100644 app/utils/class-icons.ts create mode 100644 app/utils/theme.ts create mode 100644 app/utils/vehicle-icons.ts create mode 100644 build/index.js create mode 100644 build/index.js.map create mode 100644 build/metafile.css.json create mode 100644 build/metafile.js.json create mode 100644 build/metafile.server.json create mode 100644 build/version.txt create mode 100755 bun.lockb create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 hack/download-vehicle-icons.mjs delete mode 100644 remix.config.js create mode 100644 server.mjs delete mode 100644 server.ts create mode 100644 shell.nix create mode 100644 vite.config.js delete mode 100644 wrangler.toml diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..44610e5 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake; diff --git a/.gitignore b/.gitignore index c523f8d..f7852a6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules .DS_Store **/.DS_Store +.direnv diff --git a/app/components/alert-timer.tsx b/app/components/alert-timer.tsx index ac035e1..b0b37e6 100644 --- a/app/components/alert-timer.tsx +++ b/app/components/alert-timer.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import type { MetagameWorld } from "~/utils/metagame"; -import { humanTimeAgo } from "~/utils/strings"; +import type { MetagameWorld } from "../utils/metagame"; +import { humanTimeAgo } from "../utils/strings"; import * as styles from "./alert-timer.css"; const endTime = (alert: Required["alert"]) => { @@ -51,7 +51,7 @@ export const AlertTimer = ({ }: { alert: MetagameWorld["zones"][0]["alert"]; }) => { - const [timeLeft, setTimeLeft] = useState(timeLeftString(alert)); + const [timeLeft, setTimeLeft] = useState(<>s); useEffect(() => { if (alert) { diff --git a/app/components/faction-bar-sxs.css.ts b/app/components/faction-bar-sxs.css.ts new file mode 100644 index 0000000..a0b40cb --- /dev/null +++ b/app/components/faction-bar-sxs.css.ts @@ -0,0 +1,15 @@ +import { style } from "@vanilla-extract/css"; +import { background100, background200 } from "../utils/theme"; + +export const bar = style({ + backgroundColor: background200, + width: "0.3em", + height: "1em", + border: `1px solid ${background100}`, + // margin: 2 +}); + +export const container = style({ + display: "flex", +}); + diff --git a/app/components/faction-bar-sxs.tsx b/app/components/faction-bar-sxs.tsx new file mode 100644 index 0000000..5d8ff34 --- /dev/null +++ b/app/components/faction-bar-sxs.tsx @@ -0,0 +1,41 @@ +import { useMemo } from "react"; +import { Population } from "../utils/saerro"; +import * as styles from "./faction-bar-sxs.css"; +import { background200, ncFaction, trFaction, vsFaction } from "../utils/theme"; + +export const FactionBarSxS = ({ + population: { nc, vs, tr }, +}: { + population: Population; +}) => { + const { vsPercent, ncPercent, trPercent } = useMemo(() => { + const total = nc + vs + tr; + return { + vsPercent: Math.round((vs / total) * 100) || 0, + ncPercent: Math.round((nc / total) * 100) || 0, + trPercent: Math.round((tr / total) * 100) || 0, + }; + }, [vs, nc, tr]); + + return ( +
+ + + +
+ ); +}; + +const Bar = (props: { percent: number; color: string }) => ( +
+   +
+); diff --git a/app/components/faction-bar.css.ts b/app/components/faction-bar.css.ts index ef2609c..e3055f6 100644 --- a/app/components/faction-bar.css.ts +++ b/app/components/faction-bar.css.ts @@ -1,5 +1,6 @@ import type { ComplexStyleRule } from "@vanilla-extract/css"; import { style } from "@vanilla-extract/css"; +import { edge, ncFaction, trFaction, vsFaction } from "../utils/theme"; export const bar = style({ display: "flex", @@ -8,7 +9,7 @@ export const bar = style({ flexDirection: "row", overflow: "hidden", borderRadius: "0.4rem", - border: "2px solid #4d4d4d", + border: `2px solid ${edge}`, }); export const tinyBar = style({ @@ -27,16 +28,16 @@ const shared: ComplexStyleRule = { export const left = style({ ...shared, - backgroundColor: "#991cba", + backgroundColor: vsFaction, }); export const center = style({ ...shared, - backgroundColor: "#1564cc", - borderLeft: "1px solid #4d4d4d", - borderRight: "2px solid #4d4d4d", + backgroundColor: ncFaction, + borderLeft: `1px solid ${edge}`, + borderRight: `2px solid ${edge}`, boxShadow: "inset 0 0 0.5rem rgb(180 180 180 / 10%)", }); export const right = style({ ...shared, - backgroundColor: "#d30101", + backgroundColor: trFaction, }); diff --git a/app/components/faction-bar.tsx b/app/components/faction-bar.tsx index 727c65c..1eb7d2a 100644 --- a/app/components/faction-bar.tsx +++ b/app/components/faction-bar.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import type { Population } from "~/utils/saerro"; +import type { Population } from "../utils/saerro"; import * as styles from "./faction-bar.css"; export const FactionBar = ({ diff --git a/app/components/faction-pie.tsx b/app/components/faction-pie.tsx index 2d9ed32..4298994 100644 --- a/app/components/faction-pie.tsx +++ b/app/components/faction-pie.tsx @@ -1,4 +1,5 @@ -import type { Population } from "~/utils/saerro"; +import { background200, ncFaction, trFaction, vsFaction } from "../utils/theme"; +import type { Population } from "../utils/saerro"; import { pieRoot } from "./faction-pie.css"; export const FactionPie = ({ @@ -24,10 +25,11 @@ export const FactionPie = ({ style={ { fontSize: size || "1em", + backgroundColor: background200, backgroundImage: `conic-gradient( - #d30101 0% ${trPct}%, - #991cba ${trPct}% ${trPct + vsPct}%, - #1564cc ${trPct + vsPct}% 100% + ${trFaction} 0% ${trPct}%, + ${vsFaction} ${trPct}% ${trPct + vsPct}%, + ${ncFaction} ${trPct + vsPct}% 100% )`, "--inner-margin": innerMargin ? `${innerMargin}px` : "0", "--inner-bg": innerBackground || "none", diff --git a/app/components/footer.css.ts b/app/components/footer.css.ts index f652d2e..35177b7 100644 --- a/app/components/footer.css.ts +++ b/app/components/footer.css.ts @@ -1,5 +1,5 @@ import { style } from "@vanilla-extract/css"; -import footer from "~/images/footer.jpg"; +import footer from "../images/footer.jpg"; export const root = style({ height: 300, diff --git a/app/components/index-world-container.tsx b/app/components/index-world-container.tsx index 44ae6f0..32d6b11 100644 --- a/app/components/index-world-container.tsx +++ b/app/components/index-world-container.tsx @@ -1,7 +1,7 @@ import { IndexWorld } from "./index-world"; import * as styles from "./index-world-container.css"; -import type { MetagameWorld } from "~/utils/metagame"; -import type { PopulationWorld } from "~/utils/population"; +import type { MetagameWorld } from "../utils/metagame"; +import type { PopulationWorld } from "../utils/population"; export const WorldContainer = ({ metagame, diff --git a/app/components/index-world.tsx b/app/components/index-world.tsx index 31a61f3..54c2548 100644 --- a/app/components/index-world.tsx +++ b/app/components/index-world.tsx @@ -1,19 +1,13 @@ import { Link } from "@remix-run/react"; -import { - humanTimeAgo, - snakeCaseToTitleCase, - worlds, - zones, -} from "~/utils/strings"; +import { snakeCaseToTitleCase, worlds, zones } from "../utils/strings"; import * as styles from "./index-world.css"; -import vsLogo from "~/images/vs-100.png"; -import ncLogo from "~/images/nc-100.png"; -import trLogo from "~/images/tr-100.png"; +import vsLogo from "../images/vs-100.png"; +import ncLogo from "../images/nc-100.png"; +import trLogo from "../images/tr-100.png"; import { FactionBar } from "./faction-bar"; -import type { MetagameWorld } from "~/utils/metagame"; -import type { PopulationWorld } from "~/utils/population"; -import { c } from "~/utils/classes"; -import { useEffect, useState } from "react"; +import type { MetagameWorld } from "../utils/metagame"; +import type { PopulationWorld } from "../utils/population"; +import { c } from "../utils/classes"; import { AlertTimer } from "./alert-timer"; export type IndexWorldProps = { diff --git a/app/components/world-zone-container.css.ts b/app/components/world-zone-container.css.ts new file mode 100644 index 0000000..9dd9e8d --- /dev/null +++ b/app/components/world-zone-container.css.ts @@ -0,0 +1,58 @@ +import { style } from "@vanilla-extract/css"; + +export const zoneContainer = style({ + margin: "0.5em 1em", + backgroundColor: "#222", + padding: "1em", + boxSizing: "border-box", +}); + +export const zoneHeader = style({ + display: "flex", +}); + +export const chartTileTotal = style({ + textAlign: "center", + lineHeight: 1, + minWidth: "2em", + maxWidth: "2em", + fontSize: "3rem", + overflowY: "hidden", +}); + +export const chartTilePopLine = style({ + display: "flex", + fontSize: "1.5rem", + fontWeight: "bold", + alignItems: "center", + justifyContent: "center", + lineHeight: 1.1, + margin: 0, +}); + +export const chartTilePopImage = style({ + width: "1em", + // height: "1em", + marginRight: 4, +}); + +export const chartTile = style({ + fontSize: "4rem", + flex: "0 3 33%", + display: "flex", + marginTop: "0.2em", +}); + +export const classesContainer = style({ + display: "flex", + flex: "1 3 100%", + flexWrap: "wrap", + justifyContent: "space-evenly", +}); + +export const chartTileChart = style({ + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", +}); diff --git a/app/components/world-zone-container.tsx b/app/components/world-zone-container.tsx new file mode 100644 index 0000000..ac3c748 --- /dev/null +++ b/app/components/world-zone-container.tsx @@ -0,0 +1,98 @@ +import { + allClasses, + Population, + totalPopulation, + World, + Zone, +} from "../utils/saerro"; +import { headerFont } from "./world.css"; +import { + chartTile, + chartTileChart, + chartTilePopImage, + chartTilePopLine, + chartTileTotal, + classesContainer, + zoneContainer, + zoneHeader, +} from "./world-zone-container.css"; +import { classIconMap } from "../utils/class-icons"; +import { FactionBarSxS } from "./faction-bar-sxs"; +import { c } from "../utils/classes"; +import vsLogo from "../images/vs-100.png"; +import ncLogo from "../images/nc-100.png"; +import trLogo from "../images/tr-100.png"; + +export type WZCProps = { + world: World; + zone: Zone; +}; + +export const WorldZoneContainer = (props: WZCProps) => { + return ( +
+
+

{props.zone.name.toUpperCase()}

+ {/* TODO: metagame */} +
+
+

CLASSES

+
+ +
+   +
+
+   +
+
+
+
+ ); +}; + +const Classes = (props: { classes: Zone["classes"] }) => { + if (props.classes === undefined) { + return null; + } + + return ( +
+ {allClasses.map((name) => ( + )[name]} + /> + ))} +
+ ); +}; + +const ChartTile = (props: { name: string; pop: Population }) => ( +
+
+ + +
+
+
+ {totalPopulation(props.pop)} +
+
+
+ VS{" "} + {props.pop.vs} +
+
+ NC{" "} + {props.pop.nc} +
+
+ TR{" "} + {props.pop.tr} +
+
+
+
+); diff --git a/app/components/world.css.ts b/app/components/world.css.ts index edd9208..31d8909 100644 --- a/app/components/world.css.ts +++ b/app/components/world.css.ts @@ -28,7 +28,6 @@ export const headerSub = style({ export const outer = style({ display: "flex", - justifyContent: "center", minHeight: "100vh", flexDirection: "column", maxWidth: "1920px", diff --git a/app/entry.client.tsx b/app/entry.client.tsx deleted file mode 100644 index 94d5dc0..0000000 --- a/app/entry.client.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * By default, Remix will handle hydrating your app on the client for you. - * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ - * For more information, see https://remix.run/file-conventions/entry.client - */ - -import { RemixBrowser } from "@remix-run/react"; -import { startTransition, StrictMode } from "react"; -import { hydrateRoot } from "react-dom/client"; - -startTransition(() => { - hydrateRoot( - document, - - - - ); -}); diff --git a/app/entry.server.tsx b/app/entry.server.tsx deleted file mode 100644 index 7fd5b88..0000000 --- a/app/entry.server.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/** - * By default, Remix will handle generating the HTTP Response for you. - * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ - * For more information, see https://remix.run/file-conventions/entry.server - */ - -import type { EntryContext } from "@remix-run/cloudflare"; -import { RemixServer } from "@remix-run/react"; -import isbot from "isbot"; -import { renderToReadableStream } from "react-dom/server"; - -export default async function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext -) { - const body = await renderToReadableStream( - , - { - signal: request.signal, - onError(error: unknown) { - console.error(error); - responseStatusCode = 500; - }, - } - ); - - if (isbot(request.headers.get("user-agent"))) { - await body.allReady; - } - - responseHeaders.set("Content-Type", "text/html"); - return new Response(body, { - headers: responseHeaders, - status: responseStatusCode, - }); -} diff --git a/app/images/icon_engi.png b/app/images/icon_engi.png new file mode 100644 index 0000000000000000000000000000000000000000..8a31388cb3c8916c6a4f708af31793cd58fb886f GIT binary patch literal 1065 zcmV+^1lIeBP){Ca{l%f@Q5*1d@nUCc-$kbKyLCJl?!H=ic|O_l+L-FC6FHd;Y(9@7?b*Aw=1F z)>>9x2|9hWBe;9s{uKSWkd#o+m^p0CNqc0 zSB3DLR$*Cl#JXjU2g3v)jgJs3pAXS^ArSM#Tt+A$g8sP^G{i984XQTKd#lE}s33;W z3mJf8e2Gx_Z_uEoaoMi#RRl4JITVYfLLmGK?j#z&A4Jwi&7gKrFD4_~{hwIg3aCvH z;4Uoh4yCn`zEl8|V;`RzUpx?E4M6G$ z=sN?zDk`=B{D9QsFPmq8*u$*H7=SEu#OJ;nUNHbP*a+aTt1EgNv`OKS_)7%PY$JeP zpQ)}S13*3F4mf5bfQ|xl0SAalwle^nvh@)tG$C@DkYpbNz!@6>Y%h{@Uy&pqG60;n zwG}A#5_^VY0{1ly2D=wTQi6@(q<+--7`#L9n!2*NQwORRfRFt*yOB-BBi2NXE}m(ehv zVay@UU~a~?%5p$_WP-@xHIW$)1Ax=*y#!ihfM^LQuZ3ZJ5$jIqcnIr7Rm%kN#si`S zKjRp$4I}rc4os{PJ0@dYeF^V>}i|X8b_t40B_9Xv(h-F~KliGRNjN j4o-@Vqq7@_r>)jM-`RV?)Lw!x00000NkvXXu0mjfuu{xv literal 0 HcmV?d00001 diff --git a/app/images/icon_heavy.png b/app/images/icon_heavy.png new file mode 100644 index 0000000000000000000000000000000000000000..68605059100deec9f91625e29d47e5fb20d43192 GIT binary patch literal 844 zcmV-S1GD^zP)fTvePbXwxR*(neggF@td9Mk4wTauW)EP_zpdNnCQ_N>WZ4h`6woOZS3` zAT0t-aOE}#3ZoE%ThlxA9vFuCI&;psGuHdUvwP0W`~1$=WzO7$F_!$#N(g}e!52YO zpvAzN!d^NGfKRy1;;C(42tMJjgLYVawS&FY3ZNf!%;GKH+wHml41<2M_>1@WuxbFd z@XrD~!TFr+vJh;B3!r&v@V2C(fUmJ3ML%NAV+Jn({J>2G-F%`$P&0yec@ukSYp#e* z6NIASnH0Ve0f2@Cffa$E8+6HVzpvw@4Fv!`$tdWYWAk@GcO1QZ%twkKVlV54zL$ys zSk(+NV`#7k^Z|VvpevR@6QCcUZ=f-Y_o{G3z6aefcxN=dua*TcX9<5Q`9B*Vyitsw z8Nz>+yeAe2b9DePz3Sd+sH@KGB|P(T@hc%!_$M`EuUKA@9R+ZzX>vnz>9YdBbPwZ` zm;mT%PvIRa2w>0PCjbP10K^SoU*R1w_(v4}{j&f-T%vj{sHN}@g5GL0sPMO>F~zx} z0T7$$7NCb90U!VbfB+DH`T<2_BQ-7`j@-9)bbrb^KV7 z1IQe_nE=40dd~uY)B`|D00;m9AOHk_01yBIKmZ5;0U!VbfB+DHC;>Db8ax&jL>3(U z3&Mhk%eLHR=rzHXrNL3b%R?O*yhj481dlb<%g}3rmqFv~?}jK<9b$+)RKMjm$k3C+ z;>R8s)*C0VhmHW4tzb>C=q=#;445qoU|BQSbIF(DSu%-#@a#hWfP;Sky$%`fjP zd!gxlxe5Th)|h%bTWF=&8><39w`KkQV`LCg?8)@Kp}PnGR|tI(gzxhWZv&8TP&Pfd zzJneF41XVc5bL*~SIn|;+jSzoOIx=a^cU!9fbdT={p8^vGlyO4MJ6;sh^ct^dl~~pCS&=d%G!wuN*-B>u)#0@VlB{g|+;1G&Trc;mZKXAxyLI{~ez84PO?(KcPR7 Wj1aahZWW&Z0000Q`u_LN8J@qL-e;ilP@0^rUAC z9#p7bAVL%`S}S-_C}^z)!3ZXV=S^le$%EgeEz^0wot@0QnIs&?NmwEW zyr<-B^}|;3Y2dGmbSnucdI%`C1+gAUjvxZ~cnb7IS-^f^*71+90$AJwHbqH5mtx{r0{&1g zML__0&%Xvp{)2*m1w{dbcR;r-0b@W-A?b&}Szu#;kHHgpv?6`5~WSKMgE5xDxc6aEXKsrUa}376t$KH;`}8 zQaA-%mq?g3CE&3n`JEQ_0+T)?5mkWfz0W)LHbbJ3PysSn0%Svf%JOb7MVH8-Uj>za zQgSt1r_4lTJG>eACXld8SHKF7r_ThWo#@m94hjU^*A+18(P%x$BqJHRWv|%`3jVB)O-RQyIZo+B>}V&~u5SNE zDBGBToa=}RD_$a1ar}kR9TYV`CTotlSV}0An{;DbL%_Jw!?8Phzw&DMB-W!duE~}7MG&v zu&JVGT*~5$rpn@kRg${9?SqjH$zkae%%iarlJ)e6MFH0Yefo)cG{#1O$aFQtxjcq| z(~^gB*~ICv>{X}+90@uCIwVhCdFY$EAVDrhE;A@#t4}a0*(N4 zKFJ%uBBLsSogifp1nl-nJaqE9WpUMd-6P-(us(o*AlKwVCy#M1Feq7scm;Wqx)&vD z9CNz+5{w<#Iy&>JhVE5ami#o`u}0>y^njuWT>&%nsVAc*pi$T5lP)6tg$XiDzXTLe z=XmYJ@Cy@OkXij4ttR;ixCKl&P4JbO_LrvG0>tD!1mxQST1WudBUHHLmGAa8ivTtX eDwMpfe*6Qj9pUJ3LefV70000qAF2@E0JWJHmXMhg{hci;>irhD$(J8x#*Q|H2m;+%8tcRuIdnS1Y<5MwNKm=OvD z2m}ZO=;N42SUBLox%L?XOtQnn$(2xhSaES)- zQV;qc(s>j7$x(!yFLSg2nl;e?F^+2pkODpp4*D~BUPkTRI zl!MJlj%kO4>LA9Tg6$yK);a8J1|?r5v1_vK2t^6Qbx7V6Vch{KouqWc5^+5R{>IPG zlJ|`Nw!|_x)(D4bmuBKhg(NCn>T8CGzXrJq_XNw~wimT#WS7O!2C$ozyU>3%RTAGE za;fNU=~9Sq^2kE`ZYvCFa#OTj;#`2e&u;|Xt#gc@I9mF%2%_2b=yhjkHM>9qy^etKm4pEa{ECLuu70oUyO?;@WD#r`&kKFe5~YJ~1c zM6eqQmymr};nt^;zr*5OB=uafP?7m%AlnQZTWZ1eoFzb&n6dY^L=bGV@%;iDBr=;e zBK8v6|BHK>T(xZLS>pHFvWsiQdC8W2HA}EM!ES=+2acnXC&4grEg5`8t(zrip(M4H zMm^@tLI%;bal0+;L=@n(W$A2p%U%Ge4yj+zM?#m%RuLk6<;Yg>&jwpj@>Op0^{ODL zUvl7j#AbJgIzR09f~%KRS?@VECA-7a?vAx>LacpPEuKpVKplTk$7Y+FOXY+rm!~GP zcO2=+E*<{n+4DYL0vMo*qn;s~XN1kD*Zlmd(wrj_9AI=2)|?;Q{1U);)^BSZcWo2z z2mAVynMMr(8vWwscfs or)-gU@d!Sg3?c1Bpycz)1CRw!#z~K)w zLMs!dnKXbUn7nN%4Pbc!0W4PlDgb%_ZK1g%qIMAg(?{rWH(dd61ke!bC6b?~9idB# zlg4ScitxM+6|rv#4HL=xkH*=+1kf`Sl&=a17lb|{6st%9;arZ`Jxl=o90T1Knw16` zL-~XRL5>|JpJ#1-ZU7?)awarNf=~(o4hW$lcH|QPUm%nUAS-2CjJLyT*#JfZZ86t` zU7rAOKnN9&-(8;olm(%U3E(2aV6$q%XCw$M>bU?G1XU(bLqMAhjRS;`dN$Y?LkLa) z>XPEm0>YXQAwbv>I*SRQlMJ?=3K|e*0tSS~*Z>X`SIlP!=l~#r5HQw+$Oi!O7omtS z6EJH6>0~GxgWG|8Ht0J9mg#WhaZO-*AHZ1?0t*PNFGMII1O+pJ{f&_86~l&5$}2IK zD~27{8NjJjfQO9tuFWJTj=Pt!{~)#BX;%PunG2Y0Ca56XaRsm-ooH>0APl6x-z);) z00{zTP1sB3EwcIVMF7OxH=!$@X2KUmO*k(7ep~2Q#_|6xl3LW9$ae^H8E>v)^S?F6 zE`YR9a%c(Ri6;==E3W*|VE*5~&TEPTsUcw21XTv;Jp37@K4_N8KGFpEA#@c1f_(2b z3qaUW#L1xj#nQe)P?>*O#609!EzSXd^P4W_0jX`y1Yy2l+{+A7di%>^bK}31N|xVAOSHC z=I)Nz_i7%N%tFgAn%(Ktz)rxyXI$M*)| c7_08_zj7n4Fb2kG!~g&Q07*qoM6N<$f|$E~b^rhX literal 0 HcmV?d00001 diff --git a/app/images/icon_medic.png b/app/images/icon_medic.png new file mode 100644 index 0000000000000000000000000000000000000000..f44a6c30c052bad81179efcb7c3e5d385ae46fa3 GIT binary patch literal 266 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=7d>4ZLn>~)y}g&W$w8vwq4cqH zm*&oCytRXCT8Pg=y++{;2P;`ydl{2_`1U=Yv|aUK<~#Lr&F6EJrUNy9xViPqoyzZ* zo=Q8+PVSLrVA#lb%akGZ<(`MT;u&Z4`j_&8EMl~n;%6+oyo~dL{+wl1KoJ;t#r7h6 z-ibbL#$%%QYtA(M6-`+2q~o=yYywO{1V<#-Z5~D#mw_Qb+2Mz_QC=rdD503)VSe}f pLZGC=yWIz$f0-ZG$HXwA>rw5aR}uWc;ZvV5HLo$ov6yXV~d+~=Nqp7XoUIlue6YHxQyOju4B0K}{; z%@3`g=!O&U>#=s9Cl`R=Xk~8d6!~Sm%QZlHKj3>AFSc{O#qaX-QNDcy(UX%*^CD{4 zckHvTlTg{F?66}>SnbMHWeZ7373}syZy31#1m6?P_eacC+~`5?)#+ltXHdv0SZz#Q zh&t

PXXAlN)cYX*G4()TmOolIH%5JCWU~!?2Y7y{DG~AdUgb1||#;{+B$s^Myjb zQqj6*Qk$A!q~dMjKSS`aPN~up9CTM|H=baYbPS4BCnqK-VfG&Dhw~~CgWBr@!9`fu z+cN7_=b7nQxv0~iJf^D#o@ewvvgZG9{A+yCdzHIFZWMhumJuh zm|W&=4s_#~1MDs}sWBr-qwS_a;=ZE=l-pNaU%dUO`yoce-~I4Xu|i2e^3G4UaX>|~ zPq8FEe%wZSe6aM2+it9@s>qQB)_FzmaId)E< zMMOB;0R3Thnb#r)S<{qcd=%r|0Ci9fn8jDdKPq~8X_wWr+OXP&8z=D{Ey`}sopikP`5MlNt`FzMb z3$Rp=Rmf2;co)Kru&eVKh%IM9igJAZgBS)bhfEb@DvMPd^mfTuyJ*psrn*?L5*~CVriFhX4I}l%AE7OnfpG;anD#_U8|0MCsdn zHp62JFAkR5HuG*-&`V30m+uy=Z{C* zCYUu7la-KiS|@P*_Lq0vjcoA)P%wm~BJ41PL&Nw8qHVTH-wjYP^qf?<6s%W^!mxZf zSitpa8~|JBC8HJxpbV+r1&#_x2L^QqKLx}|fF6BUKz&kL$JWx;8WL$3nH&--=S%lz z1uMLp0d&Iqj%QO>bPH0XKNjPvt>yhT@E9Z-Vq~%qsKFOTi=jaGK{n7ER76FM zU?m0~o;>;x>+vV2De_(}dO{Yq3DPH)H5oZd#_E9;_Z~fKBhnw9`Zc_Q4Iwq=iT%3A z;O~Uzi1QQY#B-~jZ8MdTmw)@IhU@jV*L} za@b)=AhApuwnLJaqG|2WbpFSV#Y!A}<>@P>AlaV+Hg)k78T&lf3J_E(2RG}GefBV{ z24jP3Nc2ksa~OIJI7!kri@-X|kb@j-uOW9>)&>Tqe$#e^b{)L7Bz!AX_Jm|_(cZp` zND*HVnl*(mk^8*BhJ@w&;bZ^RS=x^6)E6I7Lg=AZfDD$-%#uf8BCkv96?tZT# z7i)NdtM&52EhAJh9on7~&AF!`C2-CK6Kv`cqnVMO0QT8QUiuC`^`x26&Ty6v8djB> z@w#fB=F95(zIN`Hcx%zqXu+wFkqZtYG1kt__0%*0x(;hs_aqq^w*D-?-&Ha0w!U1w zI{%EKZ3;fgNdR4~ywESt$(1@w9x*U-J^#5DNzwxXO>dVs;h4+NE>V~6S^QaPxJk#` zQ;d7kdGoOzTR`-LS;bBfBG=G&(czjvkU;ehs>hxBln6B+kf@V9_#qa(POU7X+Xnzm zYXW;sC8$t}%RgH6A+aMI4U7AAWO;7tr_$GRS|f|&T>?tu9dbD~P6pjV%cK^}7-D4c zjqA|-NV$8l%HmO|X*}??82WaDh%=C9F~&TWj-f|`p(u1yUfqA`3-o7eeh?g| zVDvV4;D`9V9PvGcrI~>(kF#267h^=4!9%duu2x3@R%RIWthmJRmqMT=1}R2xvo|1- zWpe!UX4sBF&sesUEF3;$A#(Ls%}CkU`GkexIYXuX2vM~zvpR%=0Iu0A$GTyzybfFZ zSIH~uW-}G$?rm!5cGf?1b=A;(;+G=(b;*>0dlCDpek%O@1gzDQ-ftsLVnKpP&*nX2 zW$nywuM$GQ9Qb;><7z+c2`3^Qs;ztW7n?mNyu}jJAyKts@ulQ3h3PFe`x)cy4Ly2b z|1t^_6S}boC@pX{j`$e|^=*SRbWw>gEy}*GxOGzS5>J=XsEHmlG|>dYYcl$Os{J1w cx15^Zx?7djXP%LIf8#l;{dVS07#{Ke29h70-2eap literal 0 HcmV?d00001 diff --git a/app/images/vehicles/corsair.png b/app/images/vehicles/corsair.png new file mode 100644 index 0000000000000000000000000000000000000000..d0c0609ff7b89b262d1acf7dda7ff523897d3274 GIT binary patch literal 375 zcmV--0f_#IP)01!gcXP@p*r>VVXNa==EY16B@{1AapBTY5Wy6^J$dqXJ8MJD>o~ z0qOL1z;-kT9HF-Z(7|UQ=BAeeoc?16w)Ap9Ck_Y9qL%|MKrOrg<)47^Ptd~w!a)25 zY9Xuy%!cy612I3X9S{n&4CDwds2CekH2Bim0W+Z%O+yj`IbaG@diT{uq4wy`06ilMT0gO<53(1fo5<&pI$wn0i1d!+e zA8I%NIT-axhywIh^CWT|0K%$3ycLM~NDKj3!&QUUb;1A9x`5seXh5>40go8b7y$G` V&A9;n%q9Q;002ovPDHLkV1nSfj-vno literal 0 HcmV?d00001 diff --git a/app/images/vehicles/dervish.png b/app/images/vehicles/dervish.png new file mode 100644 index 0000000000000000000000000000000000000000..8dbcee03aa7b6f1459f2dfdd295a592ae87d2a61 GIT binary patch literal 2841 zcmZ9OX*|^b7RSH8nUOIV6S6NOk&{qs|ejm;Qj$6j#Cb#sM!(FNS)4R%g?f8=`iih)-x%iMXmV1L}HtGgB1Xy^-fz z5%hays%4B84bMh|SGA6>{iq#?%F_mehM&SqGAY1AMh={0Bb9&`{2&uSxe6>X@I}b{ z1o$*)kW4-v*1`Jf=1`H`aNIW2BNu_YaOp&S$L!cL1lRTTZ;w0$+eo}93kTqz=OG3c zmC)1p&uj=Wp8A{5=s}=Mk&RlDC>y456%1DK$N`^vREoke;4vTv{wBx|?e##U789tt zsz!EO4GIY^+MpGvL7$&2MzB%yrwbOZQH0q+M3Rn&t-!LEz5GyGyNt%95hPbw%v*ta zU9V!B5qE*v*M1uT|IR>|J6ItHM4&;%p)C?FX%`b-g4RC{y3ED+^wwSwr7pLkC>8m{jojM*FltF~+~J5{U~ zm^wf8#}5%tA*)FTH`P-rU`rWlno$9w{2m z79v~-Db&8tzq{mRw>vn8Ul-kwhuyr%0L$)VVhqT*jj*ho?YBA9ic-pq8-B^q)ZM@s zG{}J@vwkr-h;(Jd%*JKRjM3t1at?EA&i*9`BsRL;zKWkwd_`v>&EKpQmL5m?6(?XK zl$5ACjpm6}dGeiV$lP4P0z@!MWGq2)(NN@Irs z6K?Z3g8_-}W>TUZ^KiNgT^AArqll!S+IlDJFPz{rZI@$eGCKu1_^l}l7yDfgVp#b3 z9_L`~R7*Qv<^z63Ng822M6m5J#T8}xZRWYyAC-%X0}?m2C;H0xy&gT_zX*8y7)`4CA}nE~E18ozJkQ3UhQ>wX-dD>P(UjRT^3&d1Qs zW|rLVrx1a8Myc63tqp7E9Y^-PC=Icsnbkta;kV_B5KVgSO}q!_2`7I;b;;bE!fR?YOmoR(oi$&w zqt9rNw6!!97L-#g+6H7^k52uWm^o+71{&rFOd*ybN0rt}0xoxh2<5*2QnRy5kz=<0Ft1Nj5Z%wRO8HGK%<&BaAs6Aq5f!teIy){)<1sIjy&(k7Na zMpFR8uBgE3QP1~WQ(K-+47GyMZ*cT#OX_0x8Uk4zzv!Mb=Q;7oh<7YbZ&x)1a`AD{|nB=cg8qe!?$X4j`gS`a0Yy;=yUCCo(5$;Dy0c z{>95$1TS$C=`q}_ zVKN-H#q)d7E)5A*-BFGoy|1N<1c`*y7#4&byr^^zKnQPR4&XkY~fH! zTA=U6dW4-gGj{1E`(um398p%(73vrm{N^(LMGV?ILKjby z{1uG!58wQHp&`*|pL2bUk0nzwL)udDPIi7?+j}*Xu`OngTR$%uv&BtnxR!lrmMhKi zL%xn*9VC_E94W?!7kCRusmOgkS+n!I9mn-Zu=~k8zM~8&$Y>>SQ5!d$G@IW6#T$N7 z&1bz0h$i^$8vgvVeOLldLA5_iKS#nDFkB%jJ4#S>x4Djp(vbxzm)okHCEa&ah6@#m z7c3X9K<1k31_AbA#mVV~R87;+_+q*uJC9_1r3#1>hp4;1ebS}D<$L9A#Yhkoi2_D# zn?pT*ZFD0x`1gnL%lCjcFT!a9t?|KqAxr_hNEVRa=P2Ycv)**pd-_XE0jODx)A;9o zS6GG5k5+zv0a39dSWYNgiWZ$%ve&(Yf$eV##46cjZ{6Swoe>M&y}(1#QFXSQuW+hU zTB~_SkyM&QUKS*X{ib~_*Y0xOq3$i_N5NyQqzA$8CuhQV`R#+M(9F6d2D$P SEpmXoz}Vojex;sM?0*68at84L literal 0 HcmV?d00001 diff --git a/app/images/vehicles/flash.png b/app/images/vehicles/flash.png new file mode 100644 index 0000000000000000000000000000000000000000..0f6248256384c56cf6e1d7fbfcc5283c086123b9 GIT binary patch literal 429 zcmV;e0aE^nP)=SXGFv}GDoZc&Nd75Ezlb z$frQej(m;XEVc3oyi)TQUjhptx!zE!N)fptjsJ?kHtzTV&6B7iCzvxcfr8Rh4!{+f z+$%K_s39kqlgw|TMkawxWhnOeNgy3V)N&R$k?+GrKu~L@ryy?Fpau=(ydA^Ha+i@$ zfwo1U?NeY;8B_Bya^3{e+Yn`~lSiOW0{svIJrd}J5a^OXH-tco1X>{k>LgGPAy6TK zN(g}x36w$z7!ol50)IkaO}VB2fV99fu5VmU#k3zEmzLM>lCqAE(`C!}HS+T*rWPnG zcaksOlrv@3RiJDsHc=jx3*}hZqZQak?p%37<0RgJv%slxh%G2&c);T2-%^gS3RZy+ XqD!5zvq-Ev00000NkvXXu0mjfY=5x# literal 0 HcmV?d00001 diff --git a/app/images/vehicles/galaxy.png b/app/images/vehicles/galaxy.png new file mode 100644 index 0000000000000000000000000000000000000000..5bfabfe2781b7ee1f332609d62622c4e696146fd GIT binary patch literal 466 zcmV;@0WJQCP)gk+C+!1f{j zI}fZ8GmVBb^gb4!i@G=N^OOA1YkP8IZVM`!H&%{Yswm|Ar&?5Hm z@;D#{#2c}TF)euR05x3HY?;1_bp=ZM7OWGK70oBeDSbWAPiWq^xgcpI2sCW33FoZ& zyoyEXlnG3$5EwIoaTNjq69}ph@R@-BZ|=Ybu69T)n7{)2{H#m!w1gVoAs`X{G*wAc9CLEsU^BT7SYq1RF_}+JLYhk&w<} zCr`LTE|cBG(N&2LF1j=C-8u7gXIV!n<(yN_e+PJn&H^w|zcD#AfT#Y3cPfCkijMXv z0K6Cg?-T&(7=UyV04YNmQdyJ}bwU>JV6JS`n0`9}Dc%C=H-P`-~0&c*WcsuAf9DTBL+Ag&YSpdjV|al6fGyMe!PA zpQ0Ol@^^)t6lwzUac%B`8WpbT4-^oz?*YWL_K17K(YFuceP^eg~+ z7YWd}0O%(HaD|78z8+Wr45%&xFDrnC=nL{i*P^-wKpizc`7~?*RuMb$cUaty=-A2z zU>3707n&N<<5At39nt}xx{wv!8IM;NV_Dk0e@F*LMsbDanl%0l>1a}3#qfBsy$c?o vLde`Aq{WMnyaM?o`esz5-D?&9<=gQO{>L1T$ck|!00000NkvXXu0mjfbr8`( literal 0 HcmV?d00001 diff --git a/app/images/vehicles/javelin.png b/app/images/vehicles/javelin.png new file mode 100644 index 0000000000000000000000000000000000000000..ba0068c451a3bd2d35e7bcdbcd22dff051c77c73 GIT binary patch literal 361 zcmV-v0ha!WP)EeL@P&ylk*8*`4nmA1zP=I6+GJx@OXzhR>yLbJpl&2h5yDu}euqCl3O z1ra_#QwPid;=lh1gDJFd022_K5C{dBoFP5Z09AsY5(n@A@fWDYAAy*go(`~rTFe9G zb3^%-^b7$Q_(IPp@BrcoNKt?cU{Np?s*WZO*a5Zd9}u%a@hzyFB$8ux(8>WyNDctu z$w2G_#HDDVp+xJ-=pP!UC_9qt05)jr2o?eiv~&O?5`GA^Bo58jMNs-StsSrshXeef z^i*0qAQ5U&JCcu)L!gM(4q!yf_aMv(#s8u7XxD7i0VH7nEb@L?oF`vt00000NkvXX Hu0mjffQpT% literal 0 HcmV?d00001 diff --git a/app/images/vehicles/liberator.png b/app/images/vehicles/liberator.png new file mode 100644 index 0000000000000000000000000000000000000000..1d07c52bb5854bb77793807ea6c77d2243b7787e GIT binary patch literal 505 zcmVvY)L(%gce8uBPE;V;S^uY(hyLazC`9UtR7-QCNV*LeyG;vN;a6C(W1#{9S z1aj;b22$S95h#0Xw>;nX5a?q}-mc3!0&_$`Xf{Q@vY)L(%gce8uBPE;V;S^uY(hyLazC`9UtR7-QCNV*LeyG;vN;a6C(W1#{9S z1aj;b22$S95h#0Xw>;nX5a?q}-mc3!0&_$`Xf{Q@Y5QZ(R4e4XBh?V&OK?F&f5Hw31r*!fW=UBEPgDJ(67 zAOwGae?Y;kv+y2{%j7P5x8lG{lG%4>pS^?0?vzq$>OW;;Zjn+G_)_pX1vns$EC5EB z;|yTH0QN{Tf@cO}9>BmAKwbSr8lP8!?}~XwP(3QZH0ezM(6PaMWy9kEyhR1rQokdu zf`bESZASrkaRBff7N9B=<reWrTUvFaZ4t0Cu4ufW9q2e9YtJ0B45$02FFE19&2> zvqj6uIRlvY0nl+w>alzeIpqT|X9KVh0I)(DviuM^KY*+aKw$y^GlBr}HUK3b0KMCq zFjm9ZIDnE3z}lE1`qkW$PK2>j(w)!iTeSg5k}gQ^0>B~ZQCL9d8UnyOYR%dLOWchu_`(1=bVUE+g;XR56`I7KDznm5<01AHr@F)5P XQq5YvHwIC?00000NkvXXu0mjfXj%A7 literal 0 HcmV?d00001 diff --git a/app/images/vehicles/mosquito.png b/app/images/vehicles/mosquito.png new file mode 100644 index 0000000000000000000000000000000000000000..0f6248256384c56cf6e1d7fbfcc5283c086123b9 GIT binary patch literal 429 zcmV;e0aE^nP)=SXGFv}GDoZc&Nd75Ezlb z$frQej(m;XEVc3oyi)TQUjhptx!zE!N)fptjsJ?kHtzTV&6B7iCzvxcfr8Rh4!{+f z+$%K_s39kqlgw|TMkawxWhnOeNgy3V)N&R$k?+GrKu~L@ryy?Fpau=(ydA^Ha+i@$ zfwo1U?NeY;8B_Bya^3{e+Yn`~lSiOW0{svIJrd}J5a^OXH-tco1X>{k>LgGPAy6TK zN(g}x36w$z7!ol50)IkaO}VB2fV99fu5VmU#k3zEmzLM>lCqAE(`C!}HS+T*rWPnG zcaksOlrv@3RiJDsHc=jx3*}hZqZQak?p%37<0RgJv%slxh%G2&c);T2-%^gS3RZy+ XqD!5zvq-Ev00000NkvXXu0mjfY=5x# literal 0 HcmV?d00001 diff --git a/app/images/vehicles/prowler.png b/app/images/vehicles/prowler.png new file mode 100644 index 0000000000000000000000000000000000000000..475e3100458b5e16e63841c6a96c9b937e08d4de GIT binary patch literal 554 zcmV+_0@eMAP)JEEEUnpLPbOaf{-F95(~i~xFE5! zuogx14J@?~MQ}ZX|H9#r3lk=ZI`A>OXJ_Vonam8kmg6{95o!dMNvyvjkRY~DFH}R| z3dvQyQb~b%Vv>k6R*48|#in3G5G_^U!1*Z|VuzuUK0A6Pt1B?TI>!wNs1no>Xvg#` z2E|_x0Vy_knph@gh$%r3bm|EBh*r>m1pBDh(WZ|Hv>>MHHi0{iPe)*k4Zbf(Be@iq zF7g+Q{W}5G?v}X5^_oe6Vb*b**u%CwCSpVrm;jB~IgbSdhD`{hSkg~{8!xLs5=rbF z0(Q9sl91pt;7+%j{1LI3DS=UR`XCnBIj;_)Fxs56!Osx$n-<96KFd%$pPpw<$-5OIr*n0oeLQb3h2#s!ktITF}-mMEvd z7k+6tM}jb(z2p%XF(nW!AP_BZ6o+&Knu#0Yjd&G=fE07%vi&=8M|2P#-4Byyylim1 s;E5%FVsY8tBhdadlX#0D+y8EXZ)sj^(t#&-J^%m!07*qoM6N<$g1?C6X8-^I literal 0 HcmV?d00001 diff --git a/app/images/vehicles/reaver.png b/app/images/vehicles/reaver.png new file mode 100644 index 0000000000000000000000000000000000000000..847f8c5f895ebb5088816f278e18502d08731f66 GIT binary patch literal 467 zcmV;^0WAKBP)DB`hz<&h;2?sX)R4(0R;`GB0G(ZgfaqRVaS-QrRlk9MzJY^Kmtv(9zk}8x z1(!M*e}Uh@O9-`|$rbDi4;S9v-TU07m&6Lg(9&cWT!IT;Yt3W*Q6P%1eh3@^R|vSw z855`{2t1h-_y7e^1rNXjZBqhcTzMpL2o5L^#i$8^f)G&QiT3{s9JAP}5LjosZ$hBn zMWB|Zz!sQ`}B={sUH&_BMWN-wEpu`tbApV2_b0zjkO~3{|xCNbGybU`X z<8uxz0sDs{L5yx;>j+GN3RmHCYm4HclM}^dPW4mcDx52^&NrZg$o08xeHj1%002ov JPDHLkV1l!~!6pCz literal 0 HcmV?d00001 diff --git a/app/images/vehicles/scythe.png b/app/images/vehicles/scythe.png new file mode 100644 index 0000000000000000000000000000000000000000..1e0148bac31aa505f439d6c1eed00467ded663a0 GIT binary patch literal 535 zcmV+y0_gpTP)Bo3vi7IVp7}$paA;D99O0Q$Dl6^9$EtM&%o>xb7}!> zW3QK}c1D3+(0&GBo8*Zx_ey{Z5|jmSi$nYay3qhwVs38$6C7R!@Ul*Tu43p}@1H^U z2q6RL+5j}t|Hd|ys$_;2u=pcrhlu-BF_v1x27p84NO4y91oQ!V2VH{h2tZC!TpNJz zexk7+jmfhJz-^4aqQxfrKu-aXXaFP%fSw6J9yApa90_f81YkNR?Njtkg|@uuR?GtW z9sv5P;yQ;>#REXa^vKa5G`)js$?R3qVN#2uNWOU`EzC1t_@y6eR#m zdngW27J5nniY@@50)XE8W|DRa5W3z8xz!}^9`HaC+iN7w`BFO1060*L3G ZoWHd@<+ff4@1+0$002ovPDHLkV1jE++;0E? literal 0 HcmV?d00001 diff --git a/app/images/vehicles/sunderer.png b/app/images/vehicles/sunderer.png new file mode 100644 index 0000000000000000000000000000000000000000..644c78362680428d4ee89a91e1fef7b1b3692399 GIT binary patch literal 511 zcmVAq(STZ{SP5(q@&HyU30Q0;cxinFiC6_6!QLVe(xeO6MM(la zfrSwalQZxS9ApP~Gm}Edzy~sOF5j#>%Wg_anMz2SC>ls7t(UZUo_Ha?4XCxVzY^#X z8#K8ypw_;Ku4{oCH1FY3j|~*jTuEHJ6i`JkiJ?3^hKTLD5~$&Soyz!QikzCQfIz{n zOg?muIBEe8Mfwp9CODRP{N5ir$Fsc$&cXf|5bt0Rv99x+oC39r2QUFuCQxPbI?i)- z3OroSXYdG`OrXi;bzDD=L>z&U;U?=4AE0B18T|{m5D19XF9cS|wH7!5Ef9hN@d*lS zUdQP?X9DW^zGXWMnLxY0If)pb| zl$f!#8RnLVX|Ofl_8y25C`CN^N(O=8s0GRn1j;Vnhy)%k>f>ZhWQf~6GFaOnR=E5y z5o|}Q$&-OKi*qh8PVh=Tb}gVKD#XPBDsh`DegHaUOnYKVlLG(%002ovPDHLkV1l{U B(9i$? literal 0 HcmV?d00001 diff --git a/app/images/vehicles/valkyrie.png b/app/images/vehicles/valkyrie.png new file mode 100644 index 0000000000000000000000000000000000000000..4a64a150af32ffbf9dbe5ca77c07292dffcc6c9d GIT binary patch literal 555 zcmV+`0@VG9P)9UKZRLfve2=+rv6>tG8~LEQww#V(d2MNk|BU37AAa8s$^ z=3;TMinHLXwZ#A6KRC{`CN)X%;N#Nt<-ISL%jFEyG!2IYe!)>bO?s8fLs4RLM&EU`~<5~i3u0@4&sYq5>;*%k;b zx&y8g-*?3!A_e4cPX$Es3@ET@LJ^S11;zxt5XkU(*P95`8Iy5aAjqY)%x$=r4PBw_0^hCStdpQ=MW_G(002ovPDHLkV1iJz=ac{d literal 0 HcmV?d00001 diff --git a/app/images/vehicles/vanguard.png b/app/images/vehicles/vanguard.png new file mode 100644 index 0000000000000000000000000000000000000000..1d1f1c24b191382bb4eb75079d583bb8d65c950c GIT binary patch literal 488 zcmVP)nuoK`Lb+8P4wECePo0+TERZWMtpu!Y~Oi@A(* zf$Llf_-IsN0u{u3(*mc&n<8*X6p48>^f51DjSIXI7diss<{t44YKS|;fHfK*W+6xB z_p%Uh!s)}wrNBr-U}S0s_($l*2)N2eXk-V*8Uo`i1eS>)qIrO|%mpkwX`nQB*>7k} z;F9=^5D=^qPsB5^#^PK++ [ { rel: "preconnect", - href: "https://fonts.gstatic.com", - crossOrigin: "anonymous", - }, - { - rel: "preconnect", - href: "ttps://fonts.googleapis.com", - crossOrigin: "anonymous", + href: "https://fonts.bunny.net", }, { rel: "stylesheet", - href: "https://fonts.googleapis.com/css2?family=Unbounded:wght@700&display=swap", + href: "https://fonts.bunny.net/css?family=unbounded:700", }, ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []), @@ -43,7 +37,6 @@ export default function App() { - ); diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 24db3ce..c57cdbd 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,10 +1,10 @@ -import { json, type V2_MetaFunction } from "@remix-run/cloudflare"; +import { json, type MetaFunction } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; -import { Footer } from "~/components/footer"; -import { WorldContainer } from "~/components/index-world-container"; -import { outer } from "~/components/index.css"; -import { fetchMetagameWorlds } from "~/utils/metagame"; -import { fetchPopulationWorlds } from "~/utils/population"; +import { Footer } from "../components/footer"; +import { WorldContainer } from "../components/index-world-container"; +import { outer } from "../components/index.css"; +import { fetchMetagameWorlds } from "../utils/metagame"; +import { fetchPopulationWorlds } from "../utils/population"; export const loader = async () => { const [metagame, population] = await Promise.all([ @@ -15,7 +15,7 @@ export const loader = async () => { return json({ metagame: metagame.sort((a, b) => a.id - b.id), population }); }; -export const meta: V2_MetaFunction = () => { +export const meta: MetaFunction = () => { return [ { title: "PS2.LIVE" }, { diff --git a/app/routes/about.tsx b/app/routes/about.tsx index 1b0f3d7..5bcd385 100644 --- a/app/routes/about.tsx +++ b/app/routes/about.tsx @@ -1,4 +1,4 @@ -import { Footer } from "~/components/footer"; +import { Footer } from "../components/footer"; import { header, item, @@ -8,7 +8,7 @@ import { link, love, outer, -} from "~/components/about.css"; +} from "../components/about.css"; export default function About() { return ( diff --git a/app/routes/debug.components.tsx b/app/routes/debug.components.tsx index 5d7b186..3bb0a83 100644 --- a/app/routes/debug.components.tsx +++ b/app/routes/debug.components.tsx @@ -1,7 +1,8 @@ import { useState } from "react"; -import { FactionBar } from "~/components/faction-bar"; -import { FactionPie } from "~/components/faction-pie"; -import type { Population } from "~/utils/saerro"; +import { FactionBar } from "../components/faction-bar"; +import { FactionPie } from "../components/faction-pie"; +import type { Population } from "../utils/saerro"; +import { FactionBarSxS } from "../components/faction-bar-sxs"; export default function DebugComponents() { const [population, setPopulation] = useState({ @@ -66,6 +67,10 @@ export default function DebugComponents() { value={innerColor} onChange={(e) => setInnerColor(e.target.value)} /> +

Vertical Side-by-Side Bar Chart

+
+ +
); diff --git a/app/routes/worlds.$id.tsx b/app/routes/worlds.$id.tsx index 6476747..97996c4 100644 --- a/app/routes/worlds.$id.tsx +++ b/app/routes/worlds.$id.tsx @@ -1,32 +1,33 @@ -import type { LoaderArgs, V2_MetaFunction } from "@remix-run/cloudflare"; -import { json } from "@remix-run/cloudflare"; +import type { MetaFunction } from "@remix-run/node"; +import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; -import { Footer } from "~/components/footer"; -import type { MetagameWorld } from "~/utils/metagame"; -import { fetchSingleMetagameWorld } from "~/utils/metagame"; -import type { WorldResponse, Zone } from "~/utils/saerro"; +import { Footer } from "../components/footer"; +import type { MetagameWorld } from "../utils/metagame"; +import { fetchSingleMetagameWorld } from "../utils/metagame"; +import type { WorldResponse, Zone } from "../utils/saerro"; import { allClasses, allVehicles, totalPopulation, worldQuery, -} from "~/utils/saerro"; +} from "../utils/saerro"; import { pascalCaseToTitleCase, toTitleCase, worlds, zones, -} from "~/utils/strings"; -import * as styles from "~/components/world.css"; -import { c } from "~/utils/classes"; -import { FactionBar } from "~/components/faction-bar"; -import { popImage } from "~/components/index-world.css"; -import vsLogo from "~/images/vs-100.png"; -import ncLogo from "~/images/nc-100.png"; -import trLogo from "~/images/tr-100.png"; -import { FactionPie } from "~/components/faction-pie"; -import { AlertTimer } from "~/components/alert-timer"; -import { contPrioritySort } from "~/utils/sorting"; +} from "../utils/strings"; +import * as styles from "../components/world.css"; +import { c } from "../utils/classes"; +import { FactionBar } from "../components/faction-bar"; +import { popImage } from "../components/index-world.css"; +import vsLogo from "../images/vs-100.png"; +import ncLogo from "../images/nc-100.png"; +import trLogo from "../images/tr-100.png"; +import { FactionPie } from "../components/faction-pie"; +import { AlertTimer } from "../components/alert-timer"; +import { contPrioritySort, zonePopulationSort } from "../utils/sorting"; +import { WorldZoneContainer } from "../components/world-zone-container"; type LoaderData = { saerro: WorldResponse; @@ -34,7 +35,7 @@ type LoaderData = { id: string; }; -export async function loader({ params }: LoaderArgs) { +export async function loader({ params }) { const [saerro, metagame] = await Promise.all([ worldQuery(params.id as string), fetchSingleMetagameWorld(params.id as string), @@ -42,7 +43,7 @@ export async function loader({ params }: LoaderArgs) { return json({ saerro, metagame, id: params.id } as LoaderData); } -export const meta: V2_MetaFunction = ({ data }) => { +export const meta: MetaFunction = ({ data }) => { const { saerro, id } = data as LoaderData; const date = new Date(); const worldInfo = worlds[String(id || "default")]; @@ -168,10 +169,13 @@ export default function World() {
-

Continents

- {world.zones.all.map((zone) => ( - - ))} +

WARZONES

+
+ {world.zones.all.sort(zonePopulationSort).map((zone) => ( + + // + ))} +
@@ -183,8 +187,8 @@ export default function World() { const ZoneInfo = ({ zone }: { zone: Zone }) => { const zoneInfo = zones[String(zone.id)]; return ( -
-

{zoneInfo.name}

+
+

{zoneInfo.name.toUpperCase()}

{totalPopulation(zone.population)} players ({zone.population.vs} VS,{" "} {zone.population.nc} NC, {zone.population.tr} TR) diff --git a/app/utils/class-icons.ts b/app/utils/class-icons.ts new file mode 100644 index 0000000..345014f --- /dev/null +++ b/app/utils/class-icons.ts @@ -0,0 +1,17 @@ +import combatMedic from "../images/icon_medic.png"; +import engineer from "../images/icon_engi.png"; +import heavyAssault from "../images/icon_heavy.png"; +import infiltrator from "../images/icon_infil.png"; +import lightAssault from "../images/icon_light.png"; +import max from "../images/icon_max.png"; + +export { combatMedic, engineer, heavyAssault, infiltrator, lightAssault, max }; + +export const classIconMap = { + combatMedic, + engineer, + heavyAssault, + infiltrator, + lightAssault, + max, +}; diff --git a/app/utils/saerro.ts b/app/utils/saerro.ts index 6760946..210d90d 100644 --- a/app/utils/saerro.ts +++ b/app/utils/saerro.ts @@ -1,11 +1,6 @@ export const saerroFetch = async (query: string): Promise => { const response = await fetch( - `https://saerro.ps2.live/graphql?query=${query}`, - { - cf: { - cacheTtl: 60, - }, - } + `https://saerro.ps2.live/graphql?query=${query}` ); const json: { data: T } = await response.json(); return json.data; @@ -22,10 +17,10 @@ export type Zone = { id: string; name: string; population: Population; - vehicles?: Record & { + vehicles?: Record<(typeof allVehicles)[number], Population> & { total: number; }; - classes?: Record; + classes?: Record<(typeof allClasses)[number], Population>; }; export type World = { @@ -83,6 +78,7 @@ export const worldQuery = async (worldID: string): Promise => { zones { all { id + name classes { ${allClasses.map((cls) => `${cls} { total nc tr vs }`).join(" ")} } diff --git a/app/utils/sorting.ts b/app/utils/sorting.ts index 89d2318..0849152 100644 --- a/app/utils/sorting.ts +++ b/app/utils/sorting.ts @@ -1,4 +1,5 @@ import type { MetagameWorld } from "./metagame"; +import { totalPopulation, type Zone } from "./saerro"; export const contPrioritySort = ( a: MetagameWorld["zones"][number], @@ -41,3 +42,24 @@ export const contPrioritySort = ( return 0; }; + +export const zonePopulationSort = (a: Zone, b: Zone): number => { + const total = ({ nc, vs, tr }: { nc: number; vs: number; tr: number }) => + nc + vs + tr; + const ap = total(a.population); + const bp = total(b.population); + + if (ap < bp) { + return 1; + } + + if (ap > bp) { + return -1; + } + + if (ap === bp) { + return a.id < b.id ? 1 : -1; + } + + return 0; +}; diff --git a/app/utils/theme.ts b/app/utils/theme.ts new file mode 100644 index 0000000..a0fd6d0 --- /dev/null +++ b/app/utils/theme.ts @@ -0,0 +1,8 @@ +export const trFaction = "#d30101"; +export const ncFaction = "#1564cc"; +export const vsFaction = "#991cba"; + +export const background100 = "#222"; +export const background200 = "#444"; + +export const edge = "#4d4d4d"; diff --git a/app/utils/vehicle-icons.ts b/app/utils/vehicle-icons.ts new file mode 100644 index 0000000..c89cf47 --- /dev/null +++ b/app/utils/vehicle-icons.ts @@ -0,0 +1,60 @@ +import ant from "../images/vehicles/ant.png"; +import chimera from "../images/vehicles/chimera.png"; +import corsair from "../images/vehicles/corsair.png"; +import dervish from "../images/vehicles/dervish.png"; +import flash from "../images/vehicles/flash.png"; +import galaxy from "../images/vehicles/galaxy.png"; +import harasser from "../images/vehicles/harasser.png"; +import javelin from "../images/vehicles/javelin.png"; +import liberator from "../images/vehicles/liberator.png"; +import lightning from "../images/vehicles/lightning.png"; +import magrider from "../images/vehicles/magrider.png"; +import mosquito from "../images/vehicles/mosquito.png"; +import prowler from "../images/vehicles/prowler.png"; +import reaver from "../images/vehicles/reaver.png"; +import scythe from "../images/vehicles/scythe.png"; +import sunderer from "../images/vehicles/sunderer.png"; +import valkyrie from "../images/vehicles/valkyrie.png"; +import vanguard from "../images/vehicles/vanguard.png"; + +export { + ant, + chimera, + corsair, + dervish, + flash, + galaxy, + harasser, + javelin, + liberator, + lightning, + magrider, + mosquito, + prowler, + reaver, + scythe, + sunderer, + valkyrie, + vanguard, +}; + +export const vehicleIconMap = { + ant, + chimera, + corsair, + dervish, + flash, + galaxy, + harasser, + javelin, + liberator, + lightning, + magrider, + mosquito, + prowler, + reaver, + scythe, + sunderer, + valkyrie, + vanguard, +}; diff --git a/build/index.js b/build/index.js new file mode 100644 index 0000000..ffe2fa0 --- /dev/null +++ b/build/index.js @@ -0,0 +1,1904 @@ +var __defProp = Object.defineProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: !0 }); +}; + +// node_modules/@remix-run/dev/dist/config/defaults/entry.server.node.tsx +var entry_server_node_exports = {}; +__export(entry_server_node_exports, { + default: () => handleRequest +}); +import { PassThrough } from "node:stream"; +import { createReadableStreamFromReadable } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import * as isbotModule from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; +import { jsxDEV } from "react/jsx-dev-runtime"; +var ABORT_DELAY = 5e3; +function handleRequest(request, responseStatusCode, responseHeaders, remixContext, loadContext) { + return isBotRequest(request.headers.get("user-agent")) || remixContext.isSpaMode ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ) : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext + ); +} +function isBotRequest(userAgent) { + return userAgent ? "isbot" in isbotModule && typeof isbotModule.isbot == "function" ? isbotModule.isbot(userAgent) : "default" in isbotModule && typeof isbotModule.default == "function" ? isbotModule.default(userAgent) : !1 : !1; +} +function handleBotRequest(request, responseStatusCode, responseHeaders, remixContext) { + return new Promise((resolve, reject) => { + let shellRendered = !1, { pipe, abort } = renderToPipeableStream( + /* @__PURE__ */ jsxDEV( + RemixServer, + { + context: remixContext, + url: request.url, + abortDelay: ABORT_DELAY + }, + void 0, + !1, + { + fileName: "node_modules/@remix-run/dev/dist/config/defaults/entry.server.node.tsx", + lineNumber: 66, + columnNumber: 7 + }, + this + ), + { + onAllReady() { + shellRendered = !0; + let body = new PassThrough(), stream = createReadableStreamFromReadable(body); + responseHeaders.set("Content-Type", "text/html"), resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode + }) + ), pipe(body); + }, + onShellError(error) { + reject(error); + }, + onError(error) { + responseStatusCode = 500, shellRendered && console.error(error); + } + } + ); + setTimeout(abort, ABORT_DELAY); + }); +} +function handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext) { + return new Promise((resolve, reject) => { + let shellRendered = !1, { pipe, abort } = renderToPipeableStream( + /* @__PURE__ */ jsxDEV( + RemixServer, + { + context: remixContext, + url: request.url, + abortDelay: ABORT_DELAY + }, + void 0, + !1, + { + fileName: "node_modules/@remix-run/dev/dist/config/defaults/entry.server.node.tsx", + lineNumber: 116, + columnNumber: 7 + }, + this + ), + { + onShellReady() { + shellRendered = !0; + let body = new PassThrough(), stream = createReadableStreamFromReadable(body); + responseHeaders.set("Content-Type", "text/html"), resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode + }) + ), pipe(body); + }, + onShellError(error) { + reject(error); + }, + onError(error) { + responseStatusCode = 500, shellRendered && console.error(error); + } + } + ); + setTimeout(abort, ABORT_DELAY); + }); +} + +// app/root.tsx +var root_exports = {}; +__export(root_exports, { + default: () => App, + links: () => links +}); + +// css-bundle-plugin-ns:@remix-run/css-bundle +var cssBundleHref = "/build/css-bundle-RYSASN3C.css"; + +// app/root.tsx +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration +} from "@remix-run/react"; + +// app/root.css.ts +var root = "root_root__o19f1u0"; + +// app/root.tsx +import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime"; +var links = () => [ + { + rel: "preconnect", + href: "https://fonts.gstatic.com", + crossOrigin: "anonymous" + }, + { + rel: "preconnect", + href: "ttps://fonts.googleapis.com", + crossOrigin: "anonymous" + }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Unbounded:wght@700&display=swap" + }, + ...cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : [] +]; +function App() { + return /* @__PURE__ */ jsxDEV2("html", { lang: "en", children: [ + /* @__PURE__ */ jsxDEV2("head", { children: [ + /* @__PURE__ */ jsxDEV2("meta", { charSet: "utf-8" }, void 0, !1, { + fileName: "app/root.tsx", + lineNumber: 37, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV2("meta", { name: "viewport", content: "width=device-width,initial-scale=1" }, void 0, !1, { + fileName: "app/root.tsx", + lineNumber: 38, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV2(Meta, {}, void 0, !1, { + fileName: "app/root.tsx", + lineNumber: 39, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV2(Links, {}, void 0, !1, { + fileName: "app/root.tsx", + lineNumber: 40, + columnNumber: 9 + }, this) + ] }, void 0, !0, { + fileName: "app/root.tsx", + lineNumber: 36, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV2("body", { className: root, children: [ + /* @__PURE__ */ jsxDEV2(Outlet, {}, void 0, !1, { + fileName: "app/root.tsx", + lineNumber: 43, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV2(ScrollRestoration, {}, void 0, !1, { + fileName: "app/root.tsx", + lineNumber: 44, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV2(Scripts, {}, void 0, !1, { + fileName: "app/root.tsx", + lineNumber: 45, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV2(LiveReload, {}, void 0, !1, { + fileName: "app/root.tsx", + lineNumber: 46, + columnNumber: 9 + }, this) + ] }, void 0, !0, { + fileName: "app/root.tsx", + lineNumber: 42, + columnNumber: 7 + }, this) + ] }, void 0, !0, { + fileName: "app/root.tsx", + lineNumber: 35, + columnNumber: 5 + }, this); +} + +// app/routes/debug.components.tsx +var debug_components_exports = {}; +__export(debug_components_exports, { + default: () => DebugComponents +}); +import { useState } from "react"; + +// app/components/faction-bar.tsx +import { useMemo } from "react"; + +// app/components/faction-bar.css.ts +var bar = "faction-bar_bar__prieg10", tinyBar = "faction-bar_tinyBar__prieg11", left = "faction-bar_left__prieg12", center = "faction-bar_center__prieg13", right = "faction-bar_right__prieg14"; + +// app/components/faction-bar.tsx +import { Fragment, jsxDEV as jsxDEV3 } from "react/jsx-dev-runtime"; +var FactionBar = ({ + population: { vs, nc, tr }, + tiny +}) => { + let { vsPercent, ncPercent, trPercent } = useMemo(() => { + let total = nc + vs + tr; + return { + vsPercent: Math.round(vs / total * 100) || 0, + ncPercent: Math.round(nc / total * 100) || 0, + trPercent: Math.round(tr / total * 100) || 0 + }; + }, [vs, nc, tr]); + return /* @__PURE__ */ jsxDEV3("div", { className: tiny ? tinyBar : bar, children: [ + /* @__PURE__ */ jsxDEV3("div", { className: left, style: { flexGrow: vs + 1 }, children: tiny ? /* @__PURE__ */ jsxDEV3(Fragment, { children: "\xA0" }, void 0, !1, { + fileName: "app/components/faction-bar.tsx", + lineNumber: 23, + columnNumber: 17 + }, this) : `${vsPercent}%` }, void 0, !1, { + fileName: "app/components/faction-bar.tsx", + lineNumber: 22, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV3("div", { className: center, style: { flexGrow: nc + 1 }, children: tiny ? /* @__PURE__ */ jsxDEV3(Fragment, { children: "\xA0" }, void 0, !1, { + fileName: "app/components/faction-bar.tsx", + lineNumber: 26, + columnNumber: 17 + }, this) : `${ncPercent}%` }, void 0, !1, { + fileName: "app/components/faction-bar.tsx", + lineNumber: 25, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV3("div", { className: right, style: { flexGrow: tr + 1 }, children: tiny ? /* @__PURE__ */ jsxDEV3(Fragment, { children: "\xA0" }, void 0, !1, { + fileName: "app/components/faction-bar.tsx", + lineNumber: 29, + columnNumber: 17 + }, this) : `${trPercent}%` }, void 0, !1, { + fileName: "app/components/faction-bar.tsx", + lineNumber: 28, + columnNumber: 7 + }, this) + ] }, void 0, !0, { + fileName: "app/components/faction-bar.tsx", + lineNumber: 21, + columnNumber: 5 + }, this); +}; + +// app/components/faction-pie.css.ts +var pieRoot = "faction-pie_pieRoot__15tar860"; + +// app/components/faction-pie.tsx +import { jsxDEV as jsxDEV4 } from "react/jsx-dev-runtime"; +var FactionPie = ({ + population: population3, + innerMargin, + innerBackground, + size +}) => { + let { nc, tr, vs } = population3, total = nc + tr + vs, trPct = tr / total * 100, vsPct = vs / total * 100; + return /* @__PURE__ */ jsxDEV4( + "div", + { + className: pieRoot, + style: { + fontSize: size || "1em", + backgroundImage: `conic-gradient( + #d30101 0% ${trPct}%, + #991cba ${trPct}% ${trPct + vsPct}%, + #1564cc ${trPct + vsPct}% 100% + )`, + "--inner-margin": innerMargin ? `${innerMargin}px` : "0", + "--inner-bg": innerBackground || "none" + }, + children: "\xA0" + }, + void 0, + !1, + { + fileName: "app/components/faction-pie.tsx", + lineNumber: 22, + columnNumber: 5 + }, + this + ); +}; + +// app/routes/debug.components.tsx +import { jsxDEV as jsxDEV5 } from "react/jsx-dev-runtime"; +function DebugComponents() { + let [population3, setPopulation] = useState({ + nc: 33, + tr: 33, + vs: 33 + }), [innerMargin, setInnerMargin] = useState(10), [innerColor, setInnerColor] = useState("black"); + return /* @__PURE__ */ jsxDEV5("div", { children: [ + /* @__PURE__ */ jsxDEV5("h1", { children: "Debug Components" }, void 0, !1, { + fileName: "app/routes/debug.components.tsx", + lineNumber: 17, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV5("h2", { children: "Faction Viz" }, void 0, !1, { + fileName: "app/routes/debug.components.tsx", + lineNumber: 18, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV5("div", { children: [ + "NC", + " ", + /* @__PURE__ */ jsxDEV5( + "input", + { + type: "number", + value: population3.nc, + onChange: (e) => setPopulation((p) => ({ ...p, nc: Number(e.target.value) })) + }, + void 0, + !1, + { + fileName: "app/routes/debug.components.tsx", + lineNumber: 21, + columnNumber: 9 + }, + this + ), + " ", + "|| TR", + " ", + /* @__PURE__ */ jsxDEV5( + "input", + { + type: "number", + value: population3.tr, + onChange: (e) => setPopulation((p) => ({ ...p, tr: Number(e.target.value) })) + }, + void 0, + !1, + { + fileName: "app/routes/debug.components.tsx", + lineNumber: 29, + columnNumber: 9 + }, + this + ), + " ", + "|| VS", + " ", + /* @__PURE__ */ jsxDEV5( + "input", + { + type: "number", + value: population3.vs, + onChange: (e) => setPopulation((p) => ({ ...p, vs: Number(e.target.value) })) + }, + void 0, + !1, + { + fileName: "app/routes/debug.components.tsx", + lineNumber: 37, + columnNumber: 9 + }, + this + ) + ] }, void 0, !0, { + fileName: "app/routes/debug.components.tsx", + lineNumber: 19, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV5("div", { children: [ + /* @__PURE__ */ jsxDEV5("h3", { children: "Horizontal Stacked Bar Chart" }, void 0, !1, { + fileName: "app/routes/debug.components.tsx", + lineNumber: 46, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV5(FactionBar, { population: population3 }, void 0, !1, { + fileName: "app/routes/debug.components.tsx", + lineNumber: 47, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV5("h3", { children: "Pie Chart" }, void 0, !1, { + fileName: "app/routes/debug.components.tsx", + lineNumber: 48, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV5("div", { style: { fontSize: "5rem" }, children: [ + /* @__PURE__ */ jsxDEV5(FactionPie, { population: population3 }, void 0, !1, { + fileName: "app/routes/debug.components.tsx", + lineNumber: 50, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV5( + FactionPie, + { + population: population3, + innerBackground: innerColor, + innerMargin + }, + void 0, + !1, + { + fileName: "app/routes/debug.components.tsx", + lineNumber: 51, + columnNumber: 11 + }, + this + ) + ] }, void 0, !0, { + fileName: "app/routes/debug.components.tsx", + lineNumber: 49, + columnNumber: 9 + }, this), + "Inner margin", + " ", + /* @__PURE__ */ jsxDEV5( + "input", + { + type: "number", + value: innerMargin, + onChange: (e) => setInnerMargin(Number(e.target.value)) + }, + void 0, + !1, + { + fileName: "app/routes/debug.components.tsx", + lineNumber: 58, + columnNumber: 9 + }, + this + ), + "Inner color", + " ", + /* @__PURE__ */ jsxDEV5( + "input", + { + type: "color", + value: innerColor, + onChange: (e) => setInnerColor(e.target.value) + }, + void 0, + !1, + { + fileName: "app/routes/debug.components.tsx", + lineNumber: 64, + columnNumber: 9 + }, + this + ) + ] }, void 0, !0, { + fileName: "app/routes/debug.components.tsx", + lineNumber: 45, + columnNumber: 7 + }, this) + ] }, void 0, !0, { + fileName: "app/routes/debug.components.tsx", + lineNumber: 16, + columnNumber: 5 + }, this); +} + +// app/routes/worlds.$id.tsx +var worlds_id_exports = {}; +__export(worlds_id_exports, { + default: () => World, + loader: () => loader, + meta: () => meta +}); +import { json } from "@remix-run/cloudflare"; +import { useLoaderData } from "@remix-run/react"; + +// app/components/footer.tsx +import { Link } from "@remix-run/react"; + +// app/components/footer.css.ts +var root2 = "footer_root__1a7dndp0", background = "footer_background__1a7dndp1", logo = "footer_logo__1a7dndp2"; +var logoLive = "footer_logoLive__1a7dndp4", logoDot = "footer_logoDot__1a7dndp5", lowerLogo = "footer_lowerLogo__1a7dndp6", link = "footer_link__1a7dndp7"; + +// app/components/footer.tsx +import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime"; +var Footer = ({ isMainPage }) => /* @__PURE__ */ jsxDEV6("footer", { children: /* @__PURE__ */ jsxDEV6("div", { className: root2, children: [ + /* @__PURE__ */ jsxDEV6("div", { className: background }, void 0, !1, { + fileName: "app/components/footer.tsx", + lineNumber: 7, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV6("div", { className: logo, children: [ + "PS2", + /* @__PURE__ */ jsxDEV6("div", { className: logoDot }, void 0, !1, { + fileName: "app/components/footer.tsx", + lineNumber: 10, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV6("span", { className: logoLive, children: "LIVE" }, void 0, !1, { + fileName: "app/components/footer.tsx", + lineNumber: 11, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV6("div", { className: lowerLogo, children: [ + /* @__PURE__ */ jsxDEV6("div", { children: isMainPage ? /* @__PURE__ */ jsxDEV6(Link, { className: link, to: "/about", children: "more stuff \xBB" }, void 0, !1, { + fileName: "app/components/footer.tsx", + lineNumber: 15, + columnNumber: 11 + }, this) : /* @__PURE__ */ jsxDEV6(Link, { className: link, to: "/", children: "less stuff \xBB" }, void 0, !1, { + fileName: "app/components/footer.tsx", + lineNumber: 19, + columnNumber: 11 + }, this) }, void 0, !1, { + fileName: "app/components/footer.tsx", + lineNumber: 13, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV6("div", { children: [ + "\xA9 ", + (/* @__PURE__ */ new Date()).getFullYear() + ] }, void 0, !0, { + fileName: "app/components/footer.tsx", + lineNumber: 24, + columnNumber: 11 + }, this) + ] }, void 0, !0, { + fileName: "app/components/footer.tsx", + lineNumber: 12, + columnNumber: 9 + }, this) + ] }, void 0, !0, { + fileName: "app/components/footer.tsx", + lineNumber: 8, + columnNumber: 7 + }, this) +] }, void 0, !0, { + fileName: "app/components/footer.tsx", + lineNumber: 6, + columnNumber: 5 +}, this) }, void 0, !1, { + fileName: "app/components/footer.tsx", + lineNumber: 5, + columnNumber: 1 +}, this); + +// app/utils/metagame.ts +var fetchMetagameWorlds = async () => await (await fetch("https://metagame.ps2.live/all")).json(), fetchSingleMetagameWorld = async (id) => await (await fetch(`https://metagame.ps2.live/${id}`)).json(); + +// app/utils/saerro.ts +var saerroFetch = async (query) => (await (await fetch( + `https://saerro.ps2.live/graphql?query=${query}`, + { + cf: { + cacheTtl: 60 + } + } +)).json()).data, allVehicles = [ + "flash", + "sunderer", + "lightning", + "scythe", + "vanguard", + "prowler", + "reaver", + "mosquito", + "galaxy", + "valkyrie", + "liberator", + "ant", + "harasser", + "dervish", + "chimera", + "javelin", + "corsair", + "magrider" +], allClasses = [ + "infiltrator", + "lightAssault", + "combatMedic", + "engineer", + "heavyAssault", + "max" +], worldQuery = async (worldID) => { + let query = `{ + world(by: {id: ${Number(worldID)}}) { + id + population { + nc + tr + vs + } + zones { + all { + id + classes { + ${allClasses.map((cls) => `${cls} { total nc tr vs }`).join(" ")} + } + vehicles { + total + ${allVehicles.map((vehicle) => `${vehicle} { total nc tr vs }`).join(" ")} + } + population { + nc + tr + vs + } + } + } + } + }`; + return await saerroFetch(query); +}, totalPopulation = ({ nc, vs, tr }) => nc + vs + tr; + +// app/utils/strings.ts +var toTitleCase = (str) => str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()), pascalCaseToTitleCase = (str) => toTitleCase(str.replace(/([A-Z])/g, " $1")), snakeCaseToTitleCase = (str) => toTitleCase(str.replace(/_/g, " ")), humanTimeAgo = (ms, full) => { + let millis = Math.floor(ms % 1e3), seconds = Math.floor(ms / 1e3), minutes = Math.floor(seconds / 60), hours = Math.floor(minutes / 60); + return hours > 0 ? full ? `${hours}h ${minutes % 60}m ${seconds % 60}s` : `${hours}h` : minutes > 0 ? full ? `${minutes}m ${seconds % 60}s` : `${minutes}m` : seconds > 0 ? `${seconds}s` : `${millis}ms`; +}, worlds = { + 1: { + name: "Connery", + timeZone: "America/Los_Angeles", + locale: "en-US", + location: "US-W", + platform: "PC" + }, + 10: { + name: "Miller", + timeZone: "UTC", + locale: "en-GB", + location: "EU", + platform: "PC" + }, + 13: { + name: "Cobalt", + timeZone: "UTC", + locale: "en-GB", + location: "EU", + platform: "PC" + }, + 17: { + name: "Emerald", + timeZone: "America/New_York", + locale: "en-US", + location: "US-E", + platform: "PC" + }, + 19: { + name: "Jaeger", + timeZone: "America/New_York", + locale: "en-US", + location: "US-E", + platform: "PC" + }, + 40: { + name: "SolTech", + timeZone: "Asia/Tokyo", + locale: "en-GB", + location: "JP", + platform: "PC" + }, + 1e3: { + name: "Genudine", + timeZone: "America/New_York", + locale: "en-US", + location: "US-E", + platform: "PS4" + }, + 2e3: { + name: "Ceres", + timeZone: "UTC", + locale: "en-GB", + location: "EU", + platform: "PS4" + }, + default: { + name: "Unknown", + timeZone: "UTC", + locale: "en-US", + location: "???", + platform: "???" + } +}, zones = { + 2: { + name: "Indar", + colors: ["#edb96b", "#964c2f"] + }, + 4: { + name: "Hossin", + colors: ["#47570d", "#7b9c05"] + }, + 6: { + name: "Amerish", + colors: ["#87a12a", "#5f634f"] + }, + 8: { + name: "Esamir", + colors: ["#d5f3f5", "#a1c7e6"] + }, + 344: { + name: "Oshur", + colors: ["#00c2bf", "#174185"] + }, + default: { + name: "Unknown", + colors: ["#111111", "#cccccc"] + } +}; + +// app/components/world.css.ts +var headerFont = "world_headerFont__cpwhdm0", header = "world_header__cpwhdm1", headerName = "world_headerName__cpwhdm2", headerSub = "world_headerSub__cpwhdm3", outer = "world_outer__cpwhdm4", population = "world_population__cpwhdm5", populationHead = "world_populationHead__cpwhdm6", popNumbers = "world_popNumbers__cpwhdm7", popItem = "world_popItem__cpwhdm8", totalPop = "world_totalPop__cpwhdm9", headerConts = "world_headerConts__cpwhdma"; +var cont = "world_cont__cpwhdmc", contSub = "world_contSub__cpwhdmd"; + +// app/utils/classes.ts +var c = (...args) => args.filter((x) => !!x).join(" "); + +// app/components/index-world.css.ts +var container = "index-world_container__1e3z7iu0", header2 = "index-world_header__1e3z7iu1", headerName2 = "index-world_headerName__1e3z7iu2", headerDetailsLink = "index-world_headerDetailsLink__1e3z7iu3", headerMarkers = "index-world_headerMarkers__1e3z7iu4"; +var details = "index-world_details__1e3z7iu6", population2 = "index-world_population__1e3z7iu7", popFaction = "index-world_popFaction__1e3z7iu8", popImage = "index-world_popImage__1e3z7iu9", totalPop2 = "index-world_totalPop__1e3z7iua", continent = "index-world_continent__1e3z7iub", contBars = "index-world_contBars__1e3z7iuc", contBarTitle = "index-world_contBarTitle__1e3z7iud", barSeparator = "index-world_barSeparator__1e3z7iue", contCircle = "index-world_contCircle__1e3z7iuf", contName = "index-world_contName__1e3z7iug", jaegerConts = "index-world_jaegerConts__1e3z7iuh"; +var nextCont = "index-world_nextCont__1e3z7ium", nextContText = "index-world_nextContText__1e3z7iun", oopsies = "index-world_oopsies__1e3z7iuo", oopsiesSpin = "index-world_oopsiesSpin__1e3z7iuq"; + +// app/images/vs-100.png +var vs_100_default = "/build/_assets/vs-100-5TO3XQQ2.png"; + +// app/images/nc-100.png +var nc_100_default = "/build/_assets/nc-100-22JVQEND.png"; + +// app/images/tr-100.png +var tr_100_default = "/build/_assets/tr-100-MTGS5ODT.png"; + +// app/components/alert-timer.tsx +import { useEffect, useState as useState2 } from "react"; + +// app/components/alert-timer.css.ts +var alertDot = "alert-timer_alertDot__eh4xj51", timer = "alert-timer_timer__eh4xj52"; + +// app/components/alert-timer.tsx +import { Fragment as Fragment2, jsxDEV as jsxDEV7 } from "react/jsx-dev-runtime"; +var endTime = (alert) => { + let alertDurationMins = 90; + switch (alert.alert_type) { + case "air": + alertDurationMins = 30; + break; + case "sudden_death": + case "max": + alertDurationMins = 15; + break; + default: + break; + } + return new Date(alert.start_time).getTime() + alertDurationMins * 60 * 1e3; +}, timeLeftString = (alert) => { + if (alert) { + let time = endTime(alert) - Date.now(); + if (time < 2e3) + return /* @__PURE__ */ jsxDEV7(Fragment2, { children: "JUST ENDED" }, void 0, !1, { + fileName: "app/components/alert-timer.tsx", + lineNumber: 30, + columnNumber: 14 + }, this); + let speed = time < 1e3 * 60 * 15 ? "1s" : "4s"; + return /* @__PURE__ */ jsxDEV7(Fragment2, { children: [ + humanTimeAgo(time, !0).toUpperCase(), + " LEFT", + " ", + /* @__PURE__ */ jsxDEV7( + "div", + { + className: alertDot, + style: { "--speed": speed } + }, + void 0, + !1, + { + fileName: "app/components/alert-timer.tsx", + lineNumber: 38, + columnNumber: 9 + }, + this + ) + ] }, void 0, !0, { + fileName: "app/components/alert-timer.tsx", + lineNumber: 36, + columnNumber: 7 + }, this); + } else + return /* @__PURE__ */ jsxDEV7(Fragment2, {}, void 0, !1, { + fileName: "app/components/alert-timer.tsx", + lineNumber: 45, + columnNumber: 12 + }, this); +}, AlertTimer = ({ + alert +}) => { + let [timeLeft, setTimeLeft] = useState2(timeLeftString(alert)); + return useEffect(() => { + if (alert) { + let interval = setInterval(() => { + setTimeLeft(timeLeftString(alert)); + }, 1e3); + return () => clearInterval(interval); + } + }, [alert]), /* @__PURE__ */ jsxDEV7("div", { className: timer, children: timeLeft }, void 0, !1, { + fileName: "app/components/alert-timer.tsx", + lineNumber: 65, + columnNumber: 10 + }, this); +}; + +// app/utils/sorting.ts +var contPrioritySort = (a, b) => a.locked && !b.locked ? 1 : !a.locked && b.locked ? -1 : a.alert && b.alert ? Date.parse(a.alert.start_time) - Date.parse(b.alert.start_time) : a.alert ? -1 : b.alert ? 1 : a.locked_since && b.locked_since ? Date.parse(a.locked_since) - Date.parse(b.locked_since) : a.locked_since ? -1 : b.locked_since ? 1 : 0; + +// app/routes/worlds.$id.tsx +import { Fragment as Fragment3, jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime"; +async function loader({ params }) { + let [saerro, metagame] = await Promise.all( + [ + worldQuery(params.id), + fetchSingleMetagameWorld(params.id) + ] + ); + return json({ saerro, metagame, id: params.id }); +} +var meta = ({ data }) => { + let { saerro, id } = data, date = /* @__PURE__ */ new Date(), worldInfo = worlds[String(id || "default")], datetimeHumanFriendly = date.toLocaleString(worldInfo.locale, { + timeZone: worldInfo.timeZone, + dateStyle: "medium", + timeStyle: "short" + }); + return [ + { + title: `${worldInfo.name || "Unknown world"} | PlanetSide 2 Live Population Stats` + }, + { + name: "description", + content: `${worldInfo.name} currently has ${totalPopulation( + saerro.world.population + )} players online as of ${datetimeHumanFriendly} ${worldInfo.name} time. VS: ${saerro.world.population.vs}, NC: ${saerro.world.population.nc}, TR: ${saerro.world.population.tr} -- See more detailed stats on ps2.live.` + } + ]; +}; +function World() { + let { + saerro: { world }, + id, + metagame + } = useLoaderData(), worldInfo = worlds[String(id || "default")], nextZoneID = metagame.zones.length !== 0 ? metagame.zones.sort( + (a, b) => new Date(a.locked_since ?? Date.now()).getTime() - new Date(b.locked_since ?? Date.now()).getTime() + )[0].id : 0; + return /* @__PURE__ */ jsxDEV8(Fragment3, { children: [ + /* @__PURE__ */ jsxDEV8("div", { className: outer, children: /* @__PURE__ */ jsxDEV8("div", { children: [ + /* @__PURE__ */ jsxDEV8("div", { className: header, children: [ + /* @__PURE__ */ jsxDEV8("div", { className: c(headerName, headerFont), children: [ + /* @__PURE__ */ jsxDEV8("div", { children: worldInfo.name.toUpperCase() }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 98, + columnNumber: 15 + }, this), + /* @__PURE__ */ jsxDEV8("div", { className: headerSub, children: [ + "[", + worldInfo.location, + "] [", + worldInfo.platform, + "]" + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 99, + columnNumber: 15 + }, this) + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 97, + columnNumber: 13 + }, this), + /* @__PURE__ */ jsxDEV8("div", { className: populationHead, children: [ + /* @__PURE__ */ jsxDEV8("div", { className: headerFont, children: [ + /* @__PURE__ */ jsxDEV8("div", { className: totalPop, children: totalPopulation(world.population).toLocaleString() }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 105, + columnNumber: 17 + }, this), + "PLAYERS" + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 104, + columnNumber: 15 + }, this), + /* @__PURE__ */ jsxDEV8("div", { className: population, children: [ + /* @__PURE__ */ jsxDEV8("div", { className: popNumbers, children: [ + /* @__PURE__ */ jsxDEV8( + "div", + { + className: popItem, + style: { flex: world.population.vs + 1 }, + children: [ + /* @__PURE__ */ jsxDEV8("img", { className: popImage, src: vs_100_default, alt: "VS" }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 116, + columnNumber: 21 + }, this), + " ", + world.population.vs + ] + }, + void 0, + !0, + { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 112, + columnNumber: 19 + }, + this + ), + /* @__PURE__ */ jsxDEV8( + "div", + { + className: popItem, + style: { flex: world.population.nc + 1 }, + children: [ + /* @__PURE__ */ jsxDEV8("img", { className: popImage, src: nc_100_default, alt: "NC" }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 123, + columnNumber: 21 + }, this), + " ", + world.population.nc + ] + }, + void 0, + !0, + { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 119, + columnNumber: 19 + }, + this + ), + /* @__PURE__ */ jsxDEV8( + "div", + { + className: popItem, + style: { flex: world.population.tr + 1 }, + children: [ + /* @__PURE__ */ jsxDEV8("img", { className: popImage, src: tr_100_default, alt: "TR" }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 130, + columnNumber: 21 + }, this), + " ", + world.population.tr + ] + }, + void 0, + !0, + { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 126, + columnNumber: 19 + }, + this + ) + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 111, + columnNumber: 17 + }, this), + /* @__PURE__ */ jsxDEV8(FactionBar, { population: world.population }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 134, + columnNumber: 17 + }, this) + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 110, + columnNumber: 15 + }, this) + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 103, + columnNumber: 13 + }, this), + /* @__PURE__ */ jsxDEV8("div", { className: headerConts, children: [ + /* @__PURE__ */ jsxDEV8("div", { className: headerSub, children: "CONTINENT CONTROL" }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 138, + columnNumber: 15 + }, this), + metagame.zones.sort(contPrioritySort).map((zone, idx) => { + let zoneInfo = zones[String(zone.id)]; + return /* @__PURE__ */ jsxDEV8("div", { className: cont, children: [ + /* @__PURE__ */ jsxDEV8("div", { style: { flex: 0 }, children: zoneInfo.name.toUpperCase() }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 143, + columnNumber: 21 + }, this), + /* @__PURE__ */ jsxDEV8("div", { style: { flex: 1 }, children: /* @__PURE__ */ jsxDEV8( + FactionPie, + { + size: "4rem", + population: zone.alert?.percentages ?? zone.territory, + innerBackground: `linear-gradient(45deg, ${zoneInfo.colors[0]}, ${zoneInfo.colors[1]})`, + innerMargin: 10 + }, + void 0, + !1, + { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 145, + columnNumber: 23 + }, + this + ) }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 144, + columnNumber: 21 + }, this), + /* @__PURE__ */ jsxDEV8("div", { className: contSub, children: zone.alert ? /* @__PURE__ */ jsxDEV8(AlertTimer, { alert: zone.alert }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 154, + columnNumber: 23 + }, this) : zone.locked ? nextZoneID == zone.id ? /* @__PURE__ */ jsxDEV8(Fragment3, { children: "NEXT UP \xBB" }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 157, + columnNumber: 23 + }, this) : /* @__PURE__ */ jsxDEV8(Fragment3, { children: "LOCKED" }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 159, + columnNumber: 23 + }, this) : /* @__PURE__ */ jsxDEV8(Fragment3, { children: "UNLOCKED" }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 162, + columnNumber: 23 + }, this) }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 152, + columnNumber: 21 + }, this) + ] }, idx, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 142, + columnNumber: 19 + }, this); + }) + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 137, + columnNumber: 13 + }, this) + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 96, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV8("div", { children: [ + /* @__PURE__ */ jsxDEV8("h2", { children: "Continents" }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 171, + columnNumber: 13 + }, this), + world.zones.all.map( + (zone) => /* @__PURE__ */ jsxDEV8(ZoneInfo, { zone }, zone.id, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 173, + columnNumber: 13 + }, this) + ) + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 170, + columnNumber: 11 + }, this) + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 95, + columnNumber: 9 + }, this) }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 94, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV8(Footer, { isMainPage: !0 }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 178, + columnNumber: 7 + }, this) + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 93, + columnNumber: 5 + }, this); +} +var ZoneInfo = ({ zone }) => { + let zoneInfo = zones[String(zone.id)]; + return /* @__PURE__ */ jsxDEV8("section", { children: [ + /* @__PURE__ */ jsxDEV8("h3", { children: zoneInfo.name }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 187, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV8("p", { children: [ + totalPopulation(zone.population), + " players (", + zone.population.vs, + " VS,", + " ", + zone.population.nc, + " NC, ", + zone.population.tr, + " TR)" + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 188, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV8("p", { children: /* @__PURE__ */ jsxDEV8("ul", { children: allClasses.map( + (cls, idx) => /* @__PURE__ */ jsxDEV8("li", { children: [ + /* @__PURE__ */ jsxDEV8("b", { children: pascalCaseToTitleCase(cls) }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 196, + columnNumber: 15 + }, this), + ": ", + zone.classes?.[cls].total, + " ", + "total, ", + zone.classes?.[cls].vs, + " VS, ", + zone.classes?.[cls].nc, + " NC,", + " ", + zone.classes?.[cls].tr, + " TR" + ] }, idx, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 195, + columnNumber: 11 + }, this) + ) }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 193, + columnNumber: 9 + }, this) }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 192, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV8("p", { children: [ + totalPopulation(zone.vehicles), + " vehicles...", + /* @__PURE__ */ jsxDEV8("ul", { children: allVehicles.map( + (vehicle, idx) => /* @__PURE__ */ jsxDEV8("li", { children: [ + /* @__PURE__ */ jsxDEV8("b", { children: toTitleCase(vehicle) }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 208, + columnNumber: 15 + }, this), + ":", + " ", + totalPopulation(zone.vehicles?.[vehicle]), + " total,", + " ", + zone.vehicles?.[vehicle].vs, + " VS, ", + zone.vehicles?.[vehicle].nc, + " ", + "NC, ", + zone.vehicles?.[vehicle].tr, + " TR" + ] }, idx, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 207, + columnNumber: 11 + }, this) + ) }, void 0, !1, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 205, + columnNumber: 9 + }, this) + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 203, + columnNumber: 7 + }, this) + ] }, void 0, !0, { + fileName: "app/routes/worlds.$id.tsx", + lineNumber: 186, + columnNumber: 5 + }, this); +}; + +// app/routes/_index.tsx +var index_exports = {}; +__export(index_exports, { + default: () => Index, + loader: () => loader2, + meta: () => meta2 +}); +import { json as json2 } from "@remix-run/cloudflare"; +import { useLoaderData as useLoaderData2 } from "@remix-run/react"; + +// app/components/index-world.tsx +import { Link as Link2 } from "@remix-run/react"; +import { Fragment as Fragment4, jsxDEV as jsxDEV9 } from "react/jsx-dev-runtime"; +var IndexWorld = ({ metagame, population: population3 }) => { + let worldId = metagame.id, { platform, location, name } = worlds[String(worldId || "default")]; + if (metagame.zones.length === 0) + return /* @__PURE__ */ jsxDEV9(BrokenWorld, { worldId }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 29, + columnNumber: 12 + }, this); + let nextZone = metagame.zones.sort( + (a, b) => new Date(a.locked_since ?? Date.now()).getTime() - new Date(b.locked_since ?? Date.now()).getTime() + )[0], nextZoneStrings = zones[nextZone.id]; + return /* @__PURE__ */ jsxDEV9("div", { className: container, children: [ + /* @__PURE__ */ jsxDEV9(Link2, { to: `/worlds/${worldId}`, className: header2, children: [ + /* @__PURE__ */ jsxDEV9("div", { className: headerName2, children: name }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 42, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV9("div", { className: headerMarkers, children: [ + "[", + location, + "] [", + platform, + "]", + " " + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 43, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV9("div", { className: headerDetailsLink, children: "DETAILS \u21E8" }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 46, + columnNumber: 9 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 41, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV9("div", { className: details, children: [ + /* @__PURE__ */ jsxDEV9("div", { className: population2, children: [ + /* @__PURE__ */ jsxDEV9("div", { className: totalPop2, children: population3.factions.vs + population3.factions.nc + population3.factions.tr }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 50, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV9("div", { className: popFaction, children: [ + /* @__PURE__ */ jsxDEV9("img", { className: popImage, src: vs_100_default, alt: "VS" }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 56, + columnNumber: 13 + }, this), + " ", + population3.factions.vs + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 55, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV9("div", { className: popFaction, children: [ + /* @__PURE__ */ jsxDEV9("img", { className: popImage, src: nc_100_default, alt: "NC" }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 60, + columnNumber: 13 + }, this), + " ", + population3.factions.nc + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 59, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV9("div", { className: popFaction, children: [ + /* @__PURE__ */ jsxDEV9("img", { className: popImage, src: tr_100_default, alt: "TR" }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 64, + columnNumber: 13 + }, this), + " ", + population3.factions.tr + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 63, + columnNumber: 11 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 49, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV9(FactionBar, { population: population3.factions }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 68, + columnNumber: 9 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 48, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV9("div", { className: c(worldId === 19 && jaegerConts), children: [ + metagame.zones.filter((zone) => !zone.locked).sort((a, b) => a.alert && !b.alert ? -1 : b.alert && !a.alert ? 1 : 0).map((zone) => worldId !== 19 ? /* @__PURE__ */ jsxDEV9(Continent, { zone }, zone.id, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 78, + columnNumber: 11 + }, this) : /* @__PURE__ */ jsxDEV9(JaegerContinent, { zone }, zone.id, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 80, + columnNumber: 11 + }, this)), + worldId !== 19 && /* @__PURE__ */ jsxDEV9("div", { className: nextCont, children: [ + /* @__PURE__ */ jsxDEV9("div", { className: nextContText, children: "Next continent \xBB" }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 85, + columnNumber: 13 + }, this), + " ", + /* @__PURE__ */ jsxDEV9("div", { className: contName, children: [ + /* @__PURE__ */ jsxDEV9( + "div", + { + className: contCircle, + style: { + "--upper-color": nextZoneStrings.colors[0], + "--lower-color": nextZoneStrings.colors[1] + } + }, + void 0, + !1, + { + fileName: "app/components/index-world.tsx", + lineNumber: 87, + columnNumber: 15 + }, + this + ), + /* @__PURE__ */ jsxDEV9("div", { children: nextZoneStrings.name }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 96, + columnNumber: 15 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 86, + columnNumber: 13 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 84, + columnNumber: 9 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 70, + columnNumber: 7 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 40, + columnNumber: 5 + }, this); +}, JaegerContinent = ({ zone }) => { + let { + name, + colors: [upper, lower] + } = zones[zone.id]; + return /* @__PURE__ */ jsxDEV9("div", { className: contName, children: [ + /* @__PURE__ */ jsxDEV9( + "div", + { + className: contCircle, + style: { + "--upper-color": upper, + "--lower-color": lower + } + }, + void 0, + !1, + { + fileName: "app/components/index-world.tsx", + lineNumber: 112, + columnNumber: 7 + }, + this + ), + /* @__PURE__ */ jsxDEV9("div", { children: name }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 121, + columnNumber: 7 + }, this) + ] }, zone.id, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 111, + columnNumber: 5 + }, this); +}, Continent = ({ zone }) => { + let { + name, + colors: [upper, lower] + } = zones[zone.id]; + return /* @__PURE__ */ jsxDEV9("div", { className: c(continent), children: [ + /* @__PURE__ */ jsxDEV9("div", { className: contName, children: [ + /* @__PURE__ */ jsxDEV9( + "div", + { + className: contCircle, + style: { + "--upper-color": upper, + "--lower-color": lower + } + }, + void 0, + !1, + { + fileName: "app/components/index-world.tsx", + lineNumber: 135, + columnNumber: 9 + }, + this + ), + /* @__PURE__ */ jsxDEV9("div", { children: name }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 144, + columnNumber: 9 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 134, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV9("div", { className: contBars, children: [ + /* @__PURE__ */ jsxDEV9("div", { children: [ + /* @__PURE__ */ jsxDEV9("div", { className: contBarTitle, children: "TERRITORY CONTROL" }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 148, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV9(FactionBar, { population: zone.territory }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 149, + columnNumber: 11 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 147, + columnNumber: 9 + }, this), + zone.alert && /* @__PURE__ */ jsxDEV9(Fragment4, { children: [ + /* @__PURE__ */ jsxDEV9("div", { className: barSeparator }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 153, + columnNumber: 13 + }, this), + /* @__PURE__ */ jsxDEV9("div", { children: [ + /* @__PURE__ */ jsxDEV9("div", { className: contBarTitle, children: [ + /* @__PURE__ */ jsxDEV9("div", { children: [ + snakeCaseToTitleCase(zone.alert.alert_type).toUpperCase(), + " ", + "ALERT PROGRESS" + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 156, + columnNumber: 17 + }, this), + " ", + /* @__PURE__ */ jsxDEV9("div", { children: [ + /* @__PURE__ */ jsxDEV9(AlertTimer, { alert: zone.alert }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 161, + columnNumber: 19 + }, this), + " " + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 160, + columnNumber: 17 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 155, + columnNumber: 15 + }, this), + /* @__PURE__ */ jsxDEV9(FactionBar, { population: zone.alert.percentages }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 164, + columnNumber: 15 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 154, + columnNumber: 13 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 152, + columnNumber: 9 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 146, + columnNumber: 7 + }, this) + ] }, zone.id, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 133, + columnNumber: 5 + }, this); +}, BrokenWorld = ({ worldId }) => { + let { platform, location, name } = worlds[String(worldId || "default")]; + return /* @__PURE__ */ jsxDEV9("div", { className: container, children: [ + /* @__PURE__ */ jsxDEV9(Link2, { to: `/worlds/${worldId}`, className: header2, children: [ + /* @__PURE__ */ jsxDEV9("div", { className: headerName2, children: name }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 179, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV9("div", { className: headerMarkers, children: [ + "[", + location, + "] [", + platform, + "]", + " " + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 180, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV9("div", { className: headerDetailsLink, children: "DETAILS \u21E8" }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 183, + columnNumber: 9 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 178, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV9("div", { className: details, children: /* @__PURE__ */ jsxDEV9("div", { className: oopsies, children: [ + "Daybreak made an oopsie.", + /* @__PURE__ */ jsxDEV9("br", {}, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 188, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV9("div", { className: oopsiesSpin, children: "\u{1F642}" }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 189, + columnNumber: 11 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 186, + columnNumber: 9 + }, this) }, void 0, !1, { + fileName: "app/components/index-world.tsx", + lineNumber: 185, + columnNumber: 7 + }, this) + ] }, void 0, !0, { + fileName: "app/components/index-world.tsx", + lineNumber: 177, + columnNumber: 5 + }, this); +}; + +// app/components/index-world-container.css.ts +var container2 = "index-world-container_container__1ib388g0"; + +// app/components/index-world-container.tsx +import { jsxDEV as jsxDEV10 } from "react/jsx-dev-runtime"; +var WorldContainer = ({ + metagame, + population: population3 +}) => /* @__PURE__ */ jsxDEV10("div", { className: container2, children: metagame.map( + (world) => /* @__PURE__ */ jsxDEV10( + IndexWorld, + { + metagame: world, + population: population3.find((p) => p.id === world.id) + }, + world.id, + !1, + { + fileName: "app/components/index-world-container.tsx", + lineNumber: 15, + columnNumber: 3 + }, + this + ) +) }, void 0, !1, { + fileName: "app/components/index-world-container.tsx", + lineNumber: 13, + columnNumber: 1 +}, this); + +// app/components/index.css.ts +var outer2 = "components_outer__8hh0bp0"; + +// app/utils/population.ts +var fetchPopulationWorlds = async () => (await (await fetch("https://agg.ps2.live/population/all")).json()).map(({ id, average, factions }) => ({ id, average, factions })); + +// app/routes/_index.tsx +import { jsxDEV as jsxDEV11 } from "react/jsx-dev-runtime"; +var loader2 = async () => { + let [metagame, population3] = await Promise.all( + [ + fetchMetagameWorlds(), + fetchPopulationWorlds() + ] + ); + return json2({ metagame: metagame.sort((a, b) => a.id - b.id), population: population3 }); +}, meta2 = () => [ + { title: "PS2.LIVE" }, + { + name: "description", + content: "PlanetSide 2 Live Population Stats" + } +]; +function Index() { + let data = useLoaderData2(); + return /* @__PURE__ */ jsxDEV11("div", { children: [ + /* @__PURE__ */ jsxDEV11("div", { className: outer2, children: /* @__PURE__ */ jsxDEV11(WorldContainer, { metagame: data.metagame, population: data.population }, void 0, !1, { + fileName: "app/routes/_index.tsx", + lineNumber: 33, + columnNumber: 9 + }, this) }, void 0, !1, { + fileName: "app/routes/_index.tsx", + lineNumber: 32, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV11(Footer, { isMainPage: !0 }, void 0, !1, { + fileName: "app/routes/_index.tsx", + lineNumber: 35, + columnNumber: 7 + }, this) + ] }, void 0, !0, { + fileName: "app/routes/_index.tsx", + lineNumber: 31, + columnNumber: 5 + }, this); +} + +// app/routes/about.tsx +var about_exports = {}; +__export(about_exports, { + default: () => About +}); + +// app/components/about.css.ts +var header3 = "about_header__wg0hcp0", outer3 = "about_outer__wg0hcp1", link2 = "about_link__wg0hcp2", itemContainer = "about_itemContainer__wg0hcp3", item = "about_item__wg0hcp4", itemLink = "about_itemLink__wg0hcp5", itemGithubLink = "about_itemGithubLink__wg0hcp6", love = "about_love__wg0hcp7"; + +// app/routes/about.tsx +import { jsxDEV as jsxDEV12 } from "react/jsx-dev-runtime"; +function About() { + return /* @__PURE__ */ jsxDEV12("div", { children: [ + /* @__PURE__ */ jsxDEV12("div", { className: outer3, children: [ + /* @__PURE__ */ jsxDEV12("div", { children: [ + /* @__PURE__ */ jsxDEV12("p", { className: header3, children: [ + /* @__PURE__ */ jsxDEV12("b", { children: "PS2.LIVE" }, void 0, !1, { + fileName: "app/routes/about.tsx", + lineNumber: 19, + columnNumber: 13 + }, this), + " is a network of services that report on the ongoing war on Auraxis." + ] }, void 0, !0, { + fileName: "app/routes/about.tsx", + lineNumber: 18, + columnNumber: 11 + }, this), + /* @__PURE__ */ jsxDEV12("p", { style: { fontStyle: "italic" }, children: [ + "hat tips:", + " ", + /* @__PURE__ */ jsxDEV12("a", { className: link2, href: "https://ps2.fisu.pw/", children: "fisu" }, void 0, !1, { + fileName: "app/routes/about.tsx", + lineNumber: 24, + columnNumber: 13 + }, this), + ",", + " ", + /* @__PURE__ */ jsxDEV12("a", { className: link2, href: "https://wt.honu.pw/", children: "honu & varunda" }, void 0, !1, { + fileName: "app/routes/about.tsx", + lineNumber: 28, + columnNumber: 13 + }, this), + ",", + " ", + /* @__PURE__ */ jsxDEV12("a", { className: link2, href: "https://voidwell.com/", children: "Voidwell & Lampjaw" }, void 0, !1, { + fileName: "app/routes/about.tsx", + lineNumber: 32, + columnNumber: 13 + }, this), + ",", + " ", + /* @__PURE__ */ jsxDEV12("a", { className: link2, href: "https://census.lithafalcon.cc/", children: "Sanctuary & Falcon" }, void 0, !1, { + fileName: "app/routes/about.tsx", + lineNumber: 36, + columnNumber: 13 + }, this), + ",", + " ", + /* @__PURE__ */ jsxDEV12("a", { className: link2, href: "https://ps2alerts.com/", children: "PS2Alerts team" }, void 0, !1, { + fileName: "app/routes/about.tsx", + lineNumber: 40, + columnNumber: 13 + }, this), + ",", + " ", + /* @__PURE__ */ jsxDEV12("a", { className: link2, href: "https://discord.gg/yVzGEg3RKV", children: "PS2devs Discord" }, void 0, !1, { + fileName: "app/routes/about.tsx", + lineNumber: 44, + columnNumber: 13 + }, this), + ", Daybreak Census Team \u{1F496}" + ] }, void 0, !0, { + fileName: "app/routes/about.tsx", + lineNumber: 22, + columnNumber: 11 + }, this) + ] }, void 0, !0, { + fileName: "app/routes/about.tsx", + lineNumber: 17, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV12("div", { children: /* @__PURE__ */ jsxDEV12("ul", { className: itemContainer, children: [ + { + name: "Saerro", + url: "https://saerro.ps2.live", + github: "https://github.com/genudine/saerro", + description: "Population GraphQL API focussing on deep granularity." + }, + { + name: "Metagame API", + url: "https://metagame.ps2.live", + github: "https://github.com/genudine/metagame", + description: "World states, contininent locks, alerts, etc." + }, + { + name: "Population API", + url: "https://agg.ps2.live/population", + github: "https://github.com/genudine/agg-population", + description: "Population as seen by many services, averaged." + }, + { + name: "Census Playground", + url: "https://try.ps2.live", + github: "https://github.com/genudine/try.ps2.live", + description: "Explore and share the Census API." + }, + { + name: "ps2.live", + url: "https://ps2.live", + github: "https://github.com/genudine/ps2.live", + description: "This website. It's pretty cool." + }, + { + name: "Medkit", + url: "https://github.com/genudine/medkit2", + github: "https://github.com/genudine/medkit2", + description: "PS2 Discord bot for population/continents in channel names." + } + ].map( + ({ name, url, github, description }, i) => /* @__PURE__ */ jsxDEV12("li", { className: item, children: [ + /* @__PURE__ */ jsxDEV12("a", { href: url, className: itemLink, children: name }, void 0, !1, { + fileName: "app/routes/about.tsx", + lineNumber: 93, + columnNumber: 17 + }, this), + /* @__PURE__ */ jsxDEV12("div", { children: [ + description, + " " + ] }, void 0, !0, { + fileName: "app/routes/about.tsx", + lineNumber: 96, + columnNumber: 17 + }, this), + /* @__PURE__ */ jsxDEV12("a", { href: github, className: itemGithubLink, children: "github" }, void 0, !1, { + fileName: "app/routes/about.tsx", + lineNumber: 97, + columnNumber: 17 + }, this) + ] }, i, !0, { + fileName: "app/routes/about.tsx", + lineNumber: 92, + columnNumber: 13 + }, this) + ) }, void 0, !1, { + fileName: "app/routes/about.tsx", + lineNumber: 51, + columnNumber: 11 + }, this) }, void 0, !1, { + fileName: "app/routes/about.tsx", + lineNumber: 50, + columnNumber: 9 + }, this), + /* @__PURE__ */ jsxDEV12("p", { className: love, children: "Built with \u{1F496} by Doll" }, void 0, !1, { + fileName: "app/routes/about.tsx", + lineNumber: 104, + columnNumber: 9 + }, this) + ] }, void 0, !0, { + fileName: "app/routes/about.tsx", + lineNumber: 16, + columnNumber: 7 + }, this), + /* @__PURE__ */ jsxDEV12(Footer, {}, void 0, !1, { + fileName: "app/routes/about.tsx", + lineNumber: 106, + columnNumber: 7 + }, this) + ] }, void 0, !0, { + fileName: "app/routes/about.tsx", + lineNumber: 15, + columnNumber: 5 + }, this); +} + +// server-assets-manifest:@remix-run/dev/assets-manifest +var assets_manifest_default = { entry: { module: "/build/entry.client-ZTY5EYA2.js", imports: ["/build/_shared/chunk-O4BRYNJ4.js", "/build/_shared/chunk-Q3VAUTNA.js", "/build/_shared/chunk-U4FRFQSK.js", "/build/_shared/chunk-D6I44YFN.js", "/build/_shared/chunk-UWV35TSL.js", "/build/_shared/chunk-XGOTYLZ5.js", "/build/_shared/chunk-7M6SC7J5.js", "/build/_shared/chunk-PNG5AS42.js"] }, routes: { root: { id: "root", parentId: void 0, path: "", index: void 0, caseSensitive: void 0, module: "/build/root-LK675L7D.js", imports: void 0, hasAction: !1, hasLoader: !1, hasClientAction: !1, hasClientLoader: !1, hasErrorBoundary: !1 }, "routes/_index": { id: "routes/_index", parentId: "root", path: void 0, index: !0, caseSensitive: void 0, module: "/build/routes/_index-QVFHENLU.js", imports: ["/build/_shared/chunk-RPE3GBUS.js", "/build/_shared/chunk-UFMODTTP.js", "/build/_shared/chunk-AKBCVZVU.js"], hasAction: !1, hasLoader: !0, hasClientAction: !1, hasClientLoader: !1, hasErrorBoundary: !1 }, "routes/about": { id: "routes/about", parentId: "root", path: "about", index: void 0, caseSensitive: void 0, module: "/build/routes/about-NZRSS7UU.js", imports: ["/build/_shared/chunk-AKBCVZVU.js"], hasAction: !1, hasLoader: !1, hasClientAction: !1, hasClientLoader: !1, hasErrorBoundary: !1 }, "routes/debug.components": { id: "routes/debug.components", parentId: "root", path: "debug/components", index: void 0, caseSensitive: void 0, module: "/build/routes/debug.components-JC3GMEKW.js", imports: ["/build/_shared/chunk-YKZC3E7O.js", "/build/_shared/chunk-UFMODTTP.js"], hasAction: !1, hasLoader: !1, hasClientAction: !1, hasClientLoader: !1, hasErrorBoundary: !1 }, "routes/worlds.$id": { id: "routes/worlds.$id", parentId: "root", path: "worlds/:id", index: void 0, caseSensitive: void 0, module: "/build/routes/worlds.$id-S4Z76SWU.js", imports: ["/build/_shared/chunk-YKZC3E7O.js", "/build/_shared/chunk-RPE3GBUS.js", "/build/_shared/chunk-UFMODTTP.js", "/build/_shared/chunk-AKBCVZVU.js"], hasAction: !1, hasLoader: !0, hasClientAction: !1, hasClientLoader: !1, hasErrorBoundary: !1 } }, version: "918ff520", hmr: { runtime: "/build/_shared/chunk-D6I44YFN.js", timestamp: 1718514722630 }, url: "/build/manifest-918FF520.js" }; + +// server-entry-module:@remix-run/dev/server-build +var mode = "development", assetsBuildDirectory = "public/build", future = { v3_fetcherPersist: !1, v3_relativeSplatPath: !1, v3_throwAbortReason: !1, unstable_singleFetch: !1 }, publicPath = "/build/", entry = { module: entry_server_node_exports }, routes = { + root: { + id: "root", + parentId: void 0, + path: "", + index: void 0, + caseSensitive: void 0, + module: root_exports + }, + "routes/debug.components": { + id: "routes/debug.components", + parentId: "root", + path: "debug/components", + index: void 0, + caseSensitive: void 0, + module: debug_components_exports + }, + "routes/worlds.$id": { + id: "routes/worlds.$id", + parentId: "root", + path: "worlds/:id", + index: void 0, + caseSensitive: void 0, + module: worlds_id_exports + }, + "routes/_index": { + id: "routes/_index", + parentId: "root", + path: void 0, + index: !0, + caseSensitive: void 0, + module: index_exports + }, + "routes/about": { + id: "routes/about", + parentId: "root", + path: "about", + index: void 0, + caseSensitive: void 0, + module: about_exports + } +}; +export { + assets_manifest_default as assets, + assetsBuildDirectory, + entry, + future, + mode, + publicPath, + routes +}; +//# sourceMappingURL=index.js.map diff --git a/build/index.js.map b/build/index.js.map new file mode 100644 index 0000000..15a29a2 --- /dev/null +++ b/build/index.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../node_modules/@remix-run/dev/dist/config/defaults/entry.server.node.tsx", "../app/root.tsx", "css-bundle-plugin-ns:@remix-run/css-bundle", "../app/root.css.ts", "../app/routes/debug.components.tsx", "../app/components/faction-bar.tsx", "../app/components/faction-bar.css.ts", "../app/components/faction-pie.css.ts", "../app/components/faction-pie.tsx", "../app/routes/worlds.$id.tsx", "../app/components/footer.tsx", "../app/components/footer.css.ts", "../app/utils/metagame.ts", "../app/utils/saerro.ts", "../app/utils/strings.ts", "../app/components/world.css.ts", "../app/utils/classes.ts", "../app/components/index-world.css.ts", "../app/components/alert-timer.tsx", "../app/components/alert-timer.css.ts", "../app/utils/sorting.ts", "../app/routes/_index.tsx", "../app/components/index-world.tsx", "../app/components/index-world-container.css.ts", "../app/components/index-world-container.tsx", "../app/components/index.css.ts", "../app/utils/population.ts", "../app/routes/about.tsx", "../app/components/about.css.ts", "server-assets-manifest:@remix-run/dev/assets-manifest", "server-entry-module:@remix-run/dev/server-build"], + "sourcesContent": ["import { PassThrough } from \"node:stream\";\n\nimport type { AppLoadContext, EntryContext } from \"@remix-run/node\";\nimport { createReadableStreamFromReadable } from \"@remix-run/node\";\nimport { RemixServer } from \"@remix-run/react\";\nimport * as isbotModule from \"isbot\";\nimport { renderToPipeableStream } from \"react-dom/server\";\n\nconst ABORT_DELAY = 5_000;\n\nexport default function handleRequest(\n request: Request,\n responseStatusCode: number,\n responseHeaders: Headers,\n remixContext: EntryContext,\n loadContext: AppLoadContext\n) {\n let prohibitOutOfOrderStreaming =\n isBotRequest(request.headers.get(\"user-agent\")) || remixContext.isSpaMode;\n\n return prohibitOutOfOrderStreaming\n ? handleBotRequest(\n request,\n responseStatusCode,\n responseHeaders,\n remixContext\n )\n : handleBrowserRequest(\n request,\n responseStatusCode,\n responseHeaders,\n remixContext\n );\n}\n\n// We have some Remix apps in the wild already running with isbot@3 so we need\n// to maintain backwards compatibility even though we want new apps to use\n// isbot@4. That way, we can ship this as a minor Semver update to @remix-run/dev.\nfunction isBotRequest(userAgent: string | null) {\n if (!userAgent) {\n return false;\n }\n\n // isbot >= 3.8.0, >4\n if (\"isbot\" in isbotModule && typeof isbotModule.isbot === \"function\") {\n return isbotModule.isbot(userAgent);\n }\n\n // isbot < 3.8.0\n if (\"default\" in isbotModule && typeof isbotModule.default === \"function\") {\n return isbotModule.default(userAgent);\n }\n\n return false;\n}\n\nfunction handleBotRequest(\n request: Request,\n responseStatusCode: number,\n responseHeaders: Headers,\n remixContext: EntryContext\n) {\n return new Promise((resolve, reject) => {\n let shellRendered = false;\n const { pipe, abort } = renderToPipeableStream(\n ,\n {\n onAllReady() {\n shellRendered = true;\n const body = new PassThrough();\n const stream = createReadableStreamFromReadable(body);\n\n responseHeaders.set(\"Content-Type\", \"text/html\");\n\n resolve(\n new Response(stream, {\n headers: responseHeaders,\n status: responseStatusCode,\n })\n );\n\n pipe(body);\n },\n onShellError(error: unknown) {\n reject(error);\n },\n onError(error: unknown) {\n responseStatusCode = 500;\n // Log streaming rendering errors from inside the shell. Don't log\n // errors encountered during initial shell rendering since they'll\n // reject and get logged in handleDocumentRequest.\n if (shellRendered) {\n console.error(error);\n }\n },\n }\n );\n\n setTimeout(abort, ABORT_DELAY);\n });\n}\n\nfunction handleBrowserRequest(\n request: Request,\n responseStatusCode: number,\n responseHeaders: Headers,\n remixContext: EntryContext\n) {\n return new Promise((resolve, reject) => {\n let shellRendered = false;\n const { pipe, abort } = renderToPipeableStream(\n ,\n {\n onShellReady() {\n shellRendered = true;\n const body = new PassThrough();\n const stream = createReadableStreamFromReadable(body);\n\n responseHeaders.set(\"Content-Type\", \"text/html\");\n\n resolve(\n new Response(stream, {\n headers: responseHeaders,\n status: responseStatusCode,\n })\n );\n\n pipe(body);\n },\n onShellError(error: unknown) {\n reject(error);\n },\n onError(error: unknown) {\n responseStatusCode = 500;\n // Log streaming rendering errors from inside the shell. Don't log\n // errors encountered during initial shell rendering since they'll\n // reject and get logged in handleDocumentRequest.\n if (shellRendered) {\n console.error(error);\n }\n },\n }\n );\n\n setTimeout(abort, ABORT_DELAY);\n });\n}\n", "import type { LinksFunction } from \"@remix-run/node\";\nimport { cssBundleHref } from \"@remix-run/css-bundle\";\nimport {\n Links,\n LiveReload,\n Meta,\n Outlet,\n Scripts,\n ScrollRestoration } from\n\"@remix-run/react\";\nimport * as styles from \"./root.css\";\nimport \"./reset.css?__remix_sideEffect__\";\n\nexport const links: LinksFunction = () => [\n{\n rel: \"preconnect\",\n href: \"https://fonts.gstatic.com\",\n crossOrigin: \"anonymous\"\n},\n{\n rel: \"preconnect\",\n href: \"ttps://fonts.googleapis.com\",\n crossOrigin: \"anonymous\"\n},\n{\n rel: \"stylesheet\",\n href: \"https://fonts.googleapis.com/css2?family=Unbounded:wght@700&display=swap\"\n},\n\n...(cssBundleHref ? [{ rel: \"stylesheet\", href: cssBundleHref }] : [])];\n\n\nexport default function App() {\n return (\n \n \n \n \n \n \n \n \n \n \n \n \n \n );\n\n}", "export const cssBundleHref = \"/build/css-bundle-RYSASN3C.css\";", "export var root = 'root_root__o19f1u0';", "import { useState } from \"react\";\nimport { FactionBar } from \"~/components/faction-bar\";\nimport { FactionPie } from \"~/components/faction-pie\";\nimport type { Population } from \"~/utils/saerro\";\n\nexport default function DebugComponents() {\n const [population, setPopulation] = useState({\n nc: 33,\n tr: 33,\n vs: 33,\n });\n\n const [innerMargin, setInnerMargin] = useState(10);\n const [innerColor, setInnerColor] = useState(\"black\");\n return (\n

\n

Debug Components

\n

Faction Viz

\n
\n NC{\" \"}\n \n setPopulation((p) => ({ ...p, nc: Number(e.target.value) }))\n }\n />{\" \"}\n || TR{\" \"}\n \n setPopulation((p) => ({ ...p, tr: Number(e.target.value) }))\n }\n />{\" \"}\n || VS{\" \"}\n \n setPopulation((p) => ({ ...p, vs: Number(e.target.value) }))\n }\n />\n
\n
\n

Horizontal Stacked Bar Chart

\n \n

Pie Chart

\n
\n \n \n
\n Inner margin{\" \"}\n setInnerMargin(Number(e.target.value))}\n />\n Inner color{\" \"}\n setInnerColor(e.target.value)}\n />\n
\n
\n );\n}\n", "import { useMemo } from \"react\";\nimport type { Population } from \"~/utils/saerro\";\nimport * as styles from \"./faction-bar.css\";\n\nexport const FactionBar = ({\n population: { vs, nc, tr },\n tiny\n\n\n\n}: {population: Population;tiny?: boolean;}) => {\n const { vsPercent, ncPercent, trPercent } = useMemo(() => {\n const total = nc + vs + tr;\n return {\n vsPercent: Math.round(vs / total * 100) || 0,\n ncPercent: Math.round(nc / total * 100) || 0,\n trPercent: Math.round(tr / total * 100) || 0\n };\n }, [vs, nc, tr]);\n return (\n
\n
\n {tiny ? <>  : `${vsPercent}%`}\n
\n
\n {tiny ? <>  : `${ncPercent}%`}\n
\n
\n {tiny ? <>  : `${trPercent}%`}\n
\n
);\n\n};", "export var bar = 'faction-bar_bar__prieg10';\nexport var tinyBar = 'faction-bar_tinyBar__prieg11';\nexport var left = 'faction-bar_left__prieg12';\nexport var center = 'faction-bar_center__prieg13';\nexport var right = 'faction-bar_right__prieg14';", "export var pieRoot = 'faction-pie_pieRoot__15tar860';", "import type { Population } from \"~/utils/saerro\";\nimport { pieRoot } from \"./faction-pie.css\";\n\nexport const FactionPie = ({\n population,\n innerMargin,\n innerBackground,\n size\n\n\n\n\n\n}: {population: Population;innerMargin?: number;innerBackground?: string;size?: string;}) => {\n const { nc, tr, vs } = population;\n const total = nc + tr + vs;\n\n const trPct = tr / total * 100;\n const vsPct = vs / total * 100;\n\n return (\n \n\n\n  \n );\n\n};", "import type { LoaderArgs, V2_MetaFunction } from \"@remix-run/cloudflare\";\nimport { json } from \"@remix-run/cloudflare\";\nimport { useLoaderData } from \"@remix-run/react\";\nimport { Footer } from \"~/components/footer\";\nimport type { MetagameWorld } from \"~/utils/metagame\";\nimport { fetchSingleMetagameWorld } from \"~/utils/metagame\";\nimport type { WorldResponse, Zone } from \"~/utils/saerro\";\nimport {\n allClasses,\n allVehicles,\n totalPopulation,\n worldQuery } from\n\"~/utils/saerro\";\nimport {\n pascalCaseToTitleCase,\n toTitleCase,\n worlds,\n zones } from\n\"~/utils/strings\";\nimport * as styles from \"~/components/world.css\";\nimport { c } from \"~/utils/classes\";\nimport { FactionBar } from \"~/components/faction-bar\";\nimport { popImage } from \"~/components/index-world.css\";\nimport vsLogo from \"~/images/vs-100.png\";\nimport ncLogo from \"~/images/nc-100.png\";\nimport trLogo from \"~/images/tr-100.png\";\nimport { FactionPie } from \"~/components/faction-pie\";\nimport { AlertTimer } from \"~/components/alert-timer\";\nimport { contPrioritySort } from \"~/utils/sorting\";\n\ntype LoaderData = {\n saerro: WorldResponse;\n metagame: MetagameWorld;\n id: string;\n};\n\nexport async function loader({ params }: LoaderArgs) {\n const [saerro, metagame] = await Promise.all([\n worldQuery((params.id as string)),\n fetchSingleMetagameWorld((params.id as string))]\n );\n return json(({ saerro, metagame, id: params.id } as LoaderData));\n}\n\nexport const meta: V2_MetaFunction = ({ data }) => {\n const { saerro, id } = (data as LoaderData);\n const date = new Date();\n const worldInfo = worlds[String(id || \"default\")];\n const datetimeHumanFriendly = date.toLocaleString(worldInfo.locale, {\n timeZone: worldInfo.timeZone,\n dateStyle: \"medium\",\n timeStyle: \"short\"\n });\n return [\n {\n title: `${\n worldInfo.name || \"Unknown world\"} | PlanetSide 2 Live Population Stats`\n\n },\n {\n name: \"description\",\n content: `${worldInfo.name} currently has ${totalPopulation(\n saerro.world.population\n )} players online as of ${datetimeHumanFriendly} ${\n worldInfo.name} time. VS: ${\n saerro.world.population.vs}, NC: ${\n saerro.world.population.nc}, TR: ${\n\n saerro.world.population.tr} -- See more detailed stats on ps2.live.`\n\n }];\n\n};\n\nexport default function World() {\n const {\n saerro: { world },\n id,\n metagame\n } = useLoaderData();\n\n const worldInfo = worlds[String(id || \"default\")];\n const nextZoneID =\n metagame.zones.length !== 0 ?\n metagame.zones.sort(\n (a, b) =>\n new Date(a.locked_since ?? Date.now()).getTime() -\n new Date(b.locked_since ?? Date.now()).getTime()\n )[0].id :\n 0;\n\n return (\n <>\n
\n
\n
\n
\n
{worldInfo.name.toUpperCase()}
\n
\n [{worldInfo.location}] [{worldInfo.platform}]\n
\n
\n
\n
\n
\n {totalPopulation(world.population).toLocaleString()}\n
\n PLAYERS\n
\n
\n
\n \n\n \"VS\"{\" \"}\n {world.population.vs}\n
\n \n\n \"NC\"{\" \"}\n {world.population.nc}\n
\n \n\n \"TR\"{\" \"}\n {world.population.tr}\n
\n
\n \n
\n
\n
\n
CONTINENT CONTROL
\n {metagame.zones.sort(contPrioritySort).map((zone, idx) => {\n const zoneInfo = zones[String(zone.id)];\n return (\n
\n
{zoneInfo.name.toUpperCase()}
\n
\n \n\n
\n
\n {zone.alert ?\n :\n zone.locked ?\n nextZoneID == zone.id ?\n <>NEXT UP \u00BB :\n\n <>LOCKED :\n\n\n <>UNLOCKED}\n\n
\n
);\n\n })}\n
\n \n
\n

Continents

\n {world.zones.all.map((zone) =>\n \n )}\n
\n \n \n