This commit is contained in:
41666 2024-07-15 14:13:23 -04:00
parent a5bffa763e
commit 752041a375
70 changed files with 7484 additions and 7443 deletions

View file

@ -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<MetagameWorld["zones"][0]>["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) {

View file

@ -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",
});

View file

@ -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 (
<div className={styles.container}>
<Bar percent={vsPercent} color={vsFaction} />
<Bar percent={ncPercent} color={ncFaction} />
<Bar percent={trPercent} color={trFaction} />
</div>
);
};
const Bar = (props: { percent: number; color: string }) => (
<div
className={styles.bar}
style={{
backgroundImage: `linear-gradient( to top,
${props.color} 0% ${props.percent}% ,
${background200} ${props.percent + 0.1}% 100%
)`,
}}
>
&nbsp;
</div>
);

View file

@ -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,
});

View file

@ -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 = ({

View file

@ -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",

View file

@ -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,

View file

@ -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,

View file

@ -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 = {

View file

@ -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",
});

View file

@ -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 (
<section className={zoneContainer} title={props.zone.name}>
<div className={zoneHeader}>
<h3 className={headerFont}>{props.zone.name.toUpperCase()}</h3>
{/* TODO: metagame */}
</div>
<div>
<h4 className={headerFont}>CLASSES</h4>
<div style={{ display: "flex" }}>
<Classes classes={props.zone.classes} />
<div style={{ display: "flex", flex: "1 3 100%", flexWrap: "wrap" }}>
&nbsp;
</div>
<div style={{ display: "flex", flex: "1 3 100%", flexWrap: "wrap" }}>
&nbsp;
</div>
</div>
</div>
</section>
);
};
const Classes = (props: { classes: Zone["classes"] }) => {
if (props.classes === undefined) {
return null;
}
return (
<div className={classesContainer}>
{allClasses.map((name) => (
<ChartTile
key={name}
name={name}
pop={(props.classes as Record<string, Population>)[name]}
/>
))}
</div>
);
};
const ChartTile = (props: { name: string; pop: Population }) => (
<div className={chartTile}>
<div className={chartTileChart}>
<FactionBarSxS population={props.pop} />
<img src={(classIconMap as any)[props.name]} />
</div>
<div>
<div className={c(headerFont, chartTileTotal)}>
{totalPopulation(props.pop)}
</div>
<div>
<div className={chartTilePopLine}>
<img className={chartTilePopImage} src={vsLogo} alt="VS" />{" "}
{props.pop.vs}
</div>
<div className={chartTilePopLine}>
<img className={chartTilePopImage} src={ncLogo} alt="NC" />{" "}
{props.pop.nc}
</div>
<div className={chartTilePopLine}>
<img className={chartTilePopImage} src={trLogo} alt="TR" />{" "}
{props.pop.tr}
</div>
</div>
</div>
</div>
);

View file

@ -28,7 +28,6 @@ export const headerSub = style({
export const outer = style({
display: "flex",
justifyContent: "center",
minHeight: "100vh",
flexDirection: "column",
maxWidth: "1920px",

View file

@ -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,
<StrictMode>
<RemixBrowser />
</StrictMode>
);
});

View file

@ -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(
<RemixServer context={remixContext} url={request.url} />,
{
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,
});
}

BIN
app/images/icon_engi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

BIN
app/images/icon_heavy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

BIN
app/images/icon_infil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,008 B

BIN
app/images/icon_light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
app/images/icon_max.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

BIN
app/images/icon_medic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

BIN
app/images/vehicles/ant.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

View file

@ -1,4 +1,4 @@
import type { LinksFunction } from "@remix-run/cloudflare";
import type { LinksFunction } from "@remix-run/node";
import { cssBundleHref } from "@remix-run/css-bundle";
import {
Links,
@ -14,17 +14,11 @@ import "./reset.css";
export const links: LinksFunction = () => [
{
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() {
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);

View file

@ -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" },
{

View file

@ -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 (

View file

@ -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<Population>({
@ -66,6 +67,10 @@ export default function DebugComponents() {
value={innerColor}
onChange={(e) => setInnerColor(e.target.value)}
/>
<h3>Vertical Side-by-Side Bar Chart</h3>
<div style={{ fontSize: "5rem" }}>
<FactionBarSxS population={population}></FactionBarSxS>
</div>
</div>
</div>
);

View file

@ -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<typeof loader> = ({ data }) => {
export const meta: MetaFunction<typeof loader> = ({ 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() {
</div>
</div>
<div>
<h2>Continents</h2>
{world.zones.all.map((zone) => (
<ZoneInfo zone={zone} key={zone.id} />
))}
<h2 className={styles.headerFont}>WARZONES</h2>
<div>
{world.zones.all.sort(zonePopulationSort).map((zone) => (
<WorldZoneContainer key={zone.id} world={world} zone={zone} />
// <ZoneInfo key={zone.id} zone={zone} />
))}
</div>
</div>
</div>
</div>
@ -183,8 +187,8 @@ export default function World() {
const ZoneInfo = ({ zone }: { zone: Zone }) => {
const zoneInfo = zones[String(zone.id)];
return (
<section>
<h3>{zoneInfo.name}</h3>
<section className={styles.zone}>
<h3 className={styles.headerFont}>{zoneInfo.name.toUpperCase()}</h3>
<p>
{totalPopulation(zone.population)} players ({zone.population.vs} VS,{" "}
{zone.population.nc} NC, {zone.population.tr} TR)

17
app/utils/class-icons.ts Normal file
View file

@ -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,
};

View file

@ -1,11 +1,6 @@
export const saerroFetch = async <T>(query: string): Promise<T> => {
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<typeof allVehicles[number], Population> & {
vehicles?: Record<(typeof allVehicles)[number], Population> & {
total: number;
};
classes?: Record<typeof allClasses[number], Population>;
classes?: Record<(typeof allClasses)[number], Population>;
};
export type World = {
@ -83,6 +78,7 @@ export const worldQuery = async (worldID: string): Promise<WorldResponse> => {
zones {
all {
id
name
classes {
${allClasses.map((cls) => `${cls} { total nc tr vs }`).join(" ")}
}

View file

@ -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;
};

8
app/utils/theme.ts Normal file
View file

@ -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";

View file

@ -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,
};