Map Planner (#1145)
* Initial * Add images * Tweaks * Proper sized bg image and weapons * Stage bg picker * Outlined weapon images for planner * First version * Lint * Add alt and title to weapon
252
app/components/Planner.tsx
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
import type { TDImageAsset } from "@tldraw/tldraw";
|
||||
import {
|
||||
Tldraw,
|
||||
ColorStyle,
|
||||
type TldrawApp,
|
||||
TDShapeType,
|
||||
TDAssetType,
|
||||
} from "@tldraw/tldraw";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { useForceRefreshOnMount } from "~/hooks/useForceRefresh";
|
||||
import { useTranslation } from "~/hooks/useTranslation";
|
||||
import type { MainWeaponId, ModeShort, StageId } from "~/modules/in-game-lists";
|
||||
import { stageIds } from "~/modules/in-game-lists";
|
||||
import { mainWeaponIds } from "~/modules/in-game-lists";
|
||||
import { modesShort } from "~/modules/in-game-lists/modes";
|
||||
import {
|
||||
mainWeaponImageUrl,
|
||||
modeImageUrl,
|
||||
outlinedMainWeaponImageUrl,
|
||||
stageMinimapImageUrlWithEnding,
|
||||
TLDRAW_URL,
|
||||
} from "~/utils/urls";
|
||||
import { Button } from "./Button";
|
||||
import { Image } from "./Image";
|
||||
import { nanoid } from "nanoid";
|
||||
import randomInt from "just-random-integer";
|
||||
import type { LanguageCode } from "~/modules/i18n";
|
||||
|
||||
export default function Planner() {
|
||||
const { t } = useTranslation(["common", "weapons"]);
|
||||
const { i18n } = useTranslation();
|
||||
const appRef = React.useRef<TldrawApp>();
|
||||
const app = appRef.current!;
|
||||
|
||||
useForceRefreshOnMount();
|
||||
|
||||
const handleMount = React.useCallback(
|
||||
(mountedApp: TldrawApp) => {
|
||||
appRef.current = mountedApp;
|
||||
mountedApp.setSetting(
|
||||
"language",
|
||||
ourLanguageToTldrawLanguage(i18n.language)
|
||||
);
|
||||
mountedApp.style({ color: ColorStyle.Red });
|
||||
},
|
||||
[i18n]
|
||||
);
|
||||
|
||||
const handleAddImage = React.useCallback(
|
||||
({
|
||||
src,
|
||||
size,
|
||||
isLocked,
|
||||
point,
|
||||
cb,
|
||||
}: {
|
||||
src: string;
|
||||
size: number[];
|
||||
isLocked: boolean;
|
||||
point: number[];
|
||||
cb?: () => void;
|
||||
}) => {
|
||||
if (!app) return;
|
||||
|
||||
const asset: TDImageAsset = {
|
||||
id: src,
|
||||
type: TDAssetType.Image,
|
||||
fileName: "img",
|
||||
src,
|
||||
size,
|
||||
};
|
||||
|
||||
// tldraw creator:
|
||||
// "So image shapes in tldraw work like this: we add an asset to the app.assets table, then we reference that asset in the shape object itself.
|
||||
// This lets us have multiple copies of an image on the canvas without having all of those take up memory individually"
|
||||
|
||||
app.insertContent({
|
||||
assets: [asset],
|
||||
shapes: [],
|
||||
});
|
||||
|
||||
app.createShapes({
|
||||
id: nanoid(),
|
||||
type: TDShapeType.Image,
|
||||
assetId: src,
|
||||
size,
|
||||
isAspectRatioLocked: true,
|
||||
isLocked,
|
||||
point,
|
||||
});
|
||||
cb?.();
|
||||
},
|
||||
[app]
|
||||
);
|
||||
|
||||
const handleAddWeapon = React.useCallback(
|
||||
(weaponId: MainWeaponId) => {
|
||||
handleAddImage({
|
||||
src: `${outlinedMainWeaponImageUrl(weaponId)}.png`,
|
||||
size: [45, 45],
|
||||
isLocked: false,
|
||||
point: [randomInt(250, 1000), randomInt(250, 750)],
|
||||
cb: () => app.selectTool("select"),
|
||||
});
|
||||
},
|
||||
[app, handleAddImage]
|
||||
);
|
||||
|
||||
const handleAddBackgroundImage = React.useCallback(
|
||||
({ stageId, modeShort }: { stageId: StageId; modeShort: ModeShort }) => {
|
||||
app.resetDocument();
|
||||
handleAddImage({
|
||||
src: stageMinimapImageUrlWithEnding({ stageId, modeShort }),
|
||||
size: [1600, 900],
|
||||
isLocked: true,
|
||||
point: [65, 20],
|
||||
});
|
||||
},
|
||||
[app, handleAddImage]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StageBackgroundSelector onAddBackground={handleAddBackgroundImage} />
|
||||
<div className="plans__weapons-section">
|
||||
{mainWeaponIds.map((weaponId) => {
|
||||
return (
|
||||
<Button
|
||||
key={weaponId}
|
||||
variant="minimal"
|
||||
onClick={() => handleAddWeapon(weaponId)}
|
||||
>
|
||||
<Image
|
||||
alt={t(`weapons:MAIN_${weaponId}`)}
|
||||
title={t(`weapons:MAIN_${weaponId}`)}
|
||||
path={mainWeaponImageUrl(weaponId)}
|
||||
width={36}
|
||||
height={36}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="plans__powered-by">
|
||||
<a href={TLDRAW_URL} target="_blank" rel="noreferrer">
|
||||
{t("common:plans.poweredBy", { name: "tldraw" })}
|
||||
</a>
|
||||
</div>
|
||||
<Tldraw showMultiplayerMenu={false} onMount={handleMount} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function StageBackgroundSelector({
|
||||
onAddBackground,
|
||||
}: {
|
||||
onAddBackground: ({
|
||||
stageId,
|
||||
modeShort,
|
||||
}: {
|
||||
stageId: StageId;
|
||||
modeShort: ModeShort;
|
||||
}) => void;
|
||||
}) {
|
||||
const { t } = useTranslation(["game-misc", "common"]);
|
||||
const [stageId, setStageId] = React.useState<StageId>(stageIds[0]);
|
||||
const [selectedMode, setSelectedMode] = React.useState<ModeShort>("SZ");
|
||||
|
||||
return (
|
||||
<div className="plans__top-section">
|
||||
<select
|
||||
className="w-max"
|
||||
value={stageId}
|
||||
onChange={(e) => setStageId(Number(e.target.value) as StageId)}
|
||||
aria-label="Select stage"
|
||||
>
|
||||
{stageIds.map((stageId) => {
|
||||
return (
|
||||
<option value={stageId} key={stageId}>
|
||||
{t(`game-misc:STAGE_${stageId}`)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<div className="plans__mode-buttons">
|
||||
{modesShort.map((mode) => {
|
||||
const selected = mode === selectedMode;
|
||||
return (
|
||||
<button
|
||||
key={mode}
|
||||
className={clsx("plans__mode-button", "outline-theme", {
|
||||
selected,
|
||||
})}
|
||||
onClick={() => setSelectedMode(mode)}
|
||||
type="button"
|
||||
title={t(`game-misc:MODE_LONG_${mode}`)}
|
||||
aria-pressed={selected}
|
||||
>
|
||||
<Image
|
||||
className={clsx("plans__mode-img", {
|
||||
selected,
|
||||
})}
|
||||
alt={t(`game-misc:MODE_LONG_${mode}`)}
|
||||
path={modeImageUrl(mode)}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
tiny
|
||||
onClick={() => onAddBackground({ modeShort: selectedMode, stageId })}
|
||||
className="w-max"
|
||||
>
|
||||
{t("common:actions.setBg")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// when adding new language check from Tldraw codebase what is the matching
|
||||
// language in TRANSLATIONS constant, or default to english if none found
|
||||
const ourLanguageToTldrawLanguageMap: Record<LanguageCode, string> = {
|
||||
"es-US": "es",
|
||||
"es-ES": "es",
|
||||
ko: "ko-kr",
|
||||
nl: "en",
|
||||
zh: "zh-ch",
|
||||
// map to itself
|
||||
da: "da",
|
||||
de: "de",
|
||||
en: "en",
|
||||
fr: "fr",
|
||||
it: "it",
|
||||
ja: "ja",
|
||||
ru: "ru",
|
||||
};
|
||||
function ourLanguageToTldrawLanguage(ourLanguageUserSelected: string) {
|
||||
for (const [ourLanguage, tldrawLanguage] of Object.entries(
|
||||
ourLanguageToTldrawLanguageMap
|
||||
)) {
|
||||
if (ourLanguage === ourLanguageUserSelected) {
|
||||
return tldrawLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`No tldraw language found for: ${ourLanguageUserSelected}`);
|
||||
return "en";
|
||||
}
|
||||
|
|
@ -11,6 +11,11 @@
|
|||
"url": "object-damage-calculator",
|
||||
"prefetch": true
|
||||
},
|
||||
{
|
||||
"name": "plans",
|
||||
"url": "plans",
|
||||
"prefetch": false
|
||||
},
|
||||
{ "name": "calendar", "url": "calendar", "prefetch": false },
|
||||
{ "name": "maps", "url": "maps", "prefetch": false },
|
||||
{ "name": "badges", "url": "badges", "prefetch": false },
|
||||
|
|
|
|||
9
app/hooks/useForceRefresh.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import * as React from "react";
|
||||
|
||||
export function useForceRefreshOnMount() {
|
||||
const [, setOne] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
setOne(1);
|
||||
}, []);
|
||||
}
|
||||
|
|
@ -51,6 +51,8 @@ export const languages = [
|
|||
},
|
||||
] as const;
|
||||
|
||||
export type LanguageCode = typeof languages[number]["code"];
|
||||
|
||||
export const config = {
|
||||
supportedLngs: languages.map((lang) => lang.code),
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export { i18nLoader } from "./loader";
|
||||
export { i18Instance } from "./loader.server";
|
||||
export { i18next, i18nCookie } from "./i18next.server";
|
||||
export { DEFAULT_LANGUAGE, languages } from "./config";
|
||||
export { DEFAULT_LANGUAGE, languages, type LanguageCode } from "./config";
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
GITHUB_CONTRIBUTORS_URL,
|
||||
LEAN_TWITTER,
|
||||
SENDOU_TWITTER_URL,
|
||||
TWIG_TWITTER,
|
||||
UBERU_TWITTER,
|
||||
} from "~/utils/urls";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
|
@ -24,6 +25,8 @@ export const handle: SendouRouteHandle = {
|
|||
i18n: "contributions",
|
||||
};
|
||||
|
||||
const PROGRAMMERS = ["DoubleCookies", "ElementUser", "remmycat"] as const;
|
||||
|
||||
const TRANSLATORS: Array<{
|
||||
translators: Array<string>;
|
||||
language: typeof languages[number]["code"];
|
||||
|
|
@ -83,6 +86,7 @@ export default function ContributionsPage() {
|
|||
</p>
|
||||
<ul className="mt-2">
|
||||
<li>
|
||||
{PROGRAMMERS.join(", ")} -{" "}
|
||||
<a href={GITHUB_CONTRIBUTORS_URL} target="_blank" rel="noreferrer">
|
||||
{t("contributions:code")}
|
||||
</a>
|
||||
|
|
@ -105,6 +109,12 @@ export default function ContributionsPage() {
|
|||
</a>{" "}
|
||||
- {t("contributions:uberu")}
|
||||
</li>
|
||||
<li>
|
||||
<a href={TWIG_TWITTER} target="_blank" rel="noreferrer">
|
||||
Twig
|
||||
</a>{" "}
|
||||
- {t("contributions:twig")}
|
||||
</li>
|
||||
{TRANSLATORS.map(({ translators, language }) => (
|
||||
<li key={language}>
|
||||
{translators.join(", ")} - {t("contributions:translation")} (
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
mapsPage,
|
||||
navIconUrl,
|
||||
objectDamageCalculatorPage,
|
||||
PLANNER_URL,
|
||||
plusSuggestionPage,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
|
|
@ -108,6 +109,12 @@ export default function Index() {
|
|||
description={t("front:maps.description")}
|
||||
to={mapsPage()}
|
||||
/>
|
||||
<FeatureCard
|
||||
navItem="plans"
|
||||
title={t("common:pages.plans")}
|
||||
description={t("front:plans.description")}
|
||||
to={PLANNER_URL}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Main>
|
||||
|
|
|
|||
22
app/routes/plans.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { lazy, Suspense } from "react";
|
||||
import type { LinksFunction } from "@remix-run/node";
|
||||
import styles from "~/styles/plans.css";
|
||||
import type { SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons"],
|
||||
};
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
const Planner = lazy(() => import("~/components/Planner"));
|
||||
|
||||
export default function MapPlannerPage() {
|
||||
return (
|
||||
<Suspense fallback={<div className="plans__placeholder" />}>
|
||||
<Planner />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
|
@ -266,7 +266,7 @@ select {
|
|||
font-size: var(--fonts-sm);
|
||||
font-weight: 500;
|
||||
padding-block: 3.5px;
|
||||
padding-inline: var(--s-3) var(--s-7);
|
||||
padding-inline: var(--s-3) var(--s-8);
|
||||
}
|
||||
|
||||
/* Temporary solution for issue: https://github.com/Sendouc/sendou.ink/issues/1141 */
|
||||
|
|
|
|||
92
app/styles/plans.css
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
div {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/** Hacky selector to remove the question mark popup on bottom left */
|
||||
button[data-state="closed"][aria-haspopup="dialog"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* stylelint-disable selector-id-pattern */
|
||||
#TD-MenuPanel {
|
||||
display: none;
|
||||
}
|
||||
/* stylelint-enable selector-id-pattern */
|
||||
|
||||
.layout__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tl-canvas {
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
.plans__placeholder {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.plans__top-section {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: 3%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--s-3);
|
||||
border: 1px solid var(--theme-very-transparent);
|
||||
border-radius: 0 0 var(--rounded) var(--rounded);
|
||||
background-color: var(--bg-darker);
|
||||
gap: var(--s-4);
|
||||
transform: translate(-50%, -42%);
|
||||
}
|
||||
|
||||
.plans__mode-buttons {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.plans__mode-button {
|
||||
padding: 0;
|
||||
padding: var(--s-1-5);
|
||||
border: none;
|
||||
border-radius: var(--rounded-full);
|
||||
background-color: transparent;
|
||||
color: var(--theme);
|
||||
opacity: 1 !important;
|
||||
outline: initial;
|
||||
}
|
||||
|
||||
.plans__mode-button.selected {
|
||||
background-color: var(--bg-mode-active);
|
||||
}
|
||||
|
||||
.plans__mode-img:not(.selected) {
|
||||
filter: var(--inactive-image-filter);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.plans__weapons-section {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
top: 15%;
|
||||
display: flex;
|
||||
width: 130px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: var(--s-2);
|
||||
border: 1px solid var(--theme-very-transparent);
|
||||
border-radius: 0 var(--rounded) var(--rounded) 0;
|
||||
background: var(--bg-darker);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.plans__powered-by {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
right: 0.5%;
|
||||
bottom: 0.5%;
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xxxs);
|
||||
}
|
||||
|
|
@ -12,8 +12,8 @@ import type {
|
|||
AbilityWithUnknown,
|
||||
MainWeaponId,
|
||||
SpecialWeaponId,
|
||||
StageId,
|
||||
SubWeaponId,
|
||||
StageId,
|
||||
} from "~/modules/in-game-lists/types";
|
||||
import type navItems from "~/components/layout/nav-items.json";
|
||||
import { type AuthErrorCode } from "~/modules/auth";
|
||||
|
|
@ -27,9 +27,11 @@ export const SENDOU_INK_PATREON_URL = "https://patreon.com/sendou";
|
|||
export const SENDOU_INK_GITHUB_URL = "https://github.com/Sendouc/sendou.ink";
|
||||
export const GITHUB_CONTRIBUTORS_URL =
|
||||
"https://github.com/Sendouc/sendou.ink/graphs/contributors";
|
||||
export const TLDRAW_URL = "https://www.tldraw.com/";
|
||||
export const BORZOIC_TWITTER = "https://twitter.com/borzoic_";
|
||||
export const LEAN_TWITTER = "https://twitter.com/LeanYoshi";
|
||||
export const UBERU_TWITTER = "https://twitter.com/uberu5";
|
||||
export const TWIG_TWITTER = "https://twitter.com/TwigTheBluePik";
|
||||
export const ipLabsMaps = (pool: string) =>
|
||||
`https://maps.iplabs.ink/?3&pool=${pool}`;
|
||||
|
||||
|
|
@ -44,6 +46,7 @@ export const BUILDS_PAGE = "/builds";
|
|||
export const CALENDAR_PAGE = "/calendar";
|
||||
export const STOP_IMPERSONATING_URL = "/auth/impersonate/stop";
|
||||
export const SEED_URL = "/seed";
|
||||
export const PLANNER_URL = "/plans";
|
||||
|
||||
export const COMMON_PREVIEW_IMAGE = "/img/layout/common-preview.png";
|
||||
export const ERROR_GIRL_IMAGE_PATH = `/img/layout/error-girl`;
|
||||
|
|
@ -125,6 +128,8 @@ export const weaponCategoryUrl = (
|
|||
) => `/img/weapon-categories/${category}`;
|
||||
export const mainWeaponImageUrl = (mainWeaponSplId: MainWeaponId) =>
|
||||
`/img/main-weapons/${mainWeaponSplId}`;
|
||||
export const outlinedMainWeaponImageUrl = (mainWeaponSplId: MainWeaponId) =>
|
||||
`/img/main-weapons-outlined/${mainWeaponSplId}`;
|
||||
export const subWeaponImageUrl = (subWeaponSplId: SubWeaponId) =>
|
||||
`/img/sub-weapons/${subWeaponSplId}`;
|
||||
export const specialWeaponImageUrl = (specialWeaponSplId: SpecialWeaponId) =>
|
||||
|
|
@ -133,6 +138,13 @@ export const abilityImageUrl = (ability: AbilityWithUnknown) =>
|
|||
`/img/abilities/${ability}`;
|
||||
export const modeImageUrl = (mode: ModeShort) => `/img/modes/${mode}`;
|
||||
export const stageImageUrl = (stageId: StageId) => `/img/stages/${stageId}`;
|
||||
export const stageMinimapImageUrlWithEnding = ({
|
||||
stageId,
|
||||
modeShort,
|
||||
}: {
|
||||
stageId: StageId;
|
||||
modeShort: ModeShort;
|
||||
}) => `/img/stage-minimaps/${stageId}-${modeShort}.jpeg`;
|
||||
|
||||
export function resolveBaseUrl(url: string) {
|
||||
return new URL(url).host;
|
||||
|
|
|
|||
2357
package-lock.json
generated
|
|
@ -19,6 +19,7 @@
|
|||
"check-translation-jsons": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/check-translation-jsons.ts",
|
||||
"check-translation-jsons:no-write": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/check-translation-jsons.ts --no-write",
|
||||
"replace-img-names": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/replace-img-names.ts",
|
||||
"replace-weapon-names": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/replace-weapon-names.ts",
|
||||
"remove-bad-custom-urls": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/remove-bad-custom-urls.ts",
|
||||
"lint:ts": "eslint . --ext .ts,.tsx",
|
||||
"lint:styles": "stylelint \"app/styles/**/*.css\"",
|
||||
|
|
@ -36,6 +37,7 @@
|
|||
"@remix-run/node": "^1.7.4",
|
||||
"@remix-run/react": "^1.7.4",
|
||||
"@remix-run/serve": "^1.7.4",
|
||||
"@tldraw/tldraw": "^1.26.2",
|
||||
"better-sqlite3": "^7.6.2",
|
||||
"clsx": "^1.2.1",
|
||||
"countries-list": "^2.6.1",
|
||||
|
|
@ -48,8 +50,10 @@
|
|||
"i18next-http-backend": "^1.4.4",
|
||||
"just-capitalize": "^3.1.1",
|
||||
"just-clone": "^6.1.1",
|
||||
"just-random-integer": "^4.1.1",
|
||||
"just-shuffle": "^4.1.1",
|
||||
"markdown-to-jsx": "^7.1.7",
|
||||
"nanoid": "^4.0.0",
|
||||
"node-cron": "3.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
|
|
|||
BIN
public/img/layout/plans.avif
Normal file
BIN
public/img/layout/plans.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/img/main-weapons-outlined/0.avif
Normal file
BIN
public/img/main-weapons-outlined/0.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
public/img/main-weapons-outlined/10.avif
Normal file
BIN
public/img/main-weapons-outlined/10.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
public/img/main-weapons-outlined/1000.avif
Normal file
BIN
public/img/main-weapons-outlined/1000.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
public/img/main-weapons-outlined/1010.avif
Normal file
BIN
public/img/main-weapons-outlined/1010.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
public/img/main-weapons-outlined/1020.avif
Normal file
BIN
public/img/main-weapons-outlined/1020.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/img/main-weapons-outlined/1030.avif
Normal file
BIN
public/img/main-weapons-outlined/1030.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public/img/main-weapons-outlined/1100.avif
Normal file
BIN
public/img/main-weapons-outlined/1100.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
public/img/main-weapons-outlined/1110.avif
Normal file
BIN
public/img/main-weapons-outlined/1110.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/img/main-weapons-outlined/20.avif
Normal file
BIN
public/img/main-weapons-outlined/20.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
public/img/main-weapons-outlined/200.avif
Normal file
BIN
public/img/main-weapons-outlined/200.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
public/img/main-weapons-outlined/2000.avif
Normal file
BIN
public/img/main-weapons-outlined/2000.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/img/main-weapons-outlined/2010.avif
Normal file
BIN
public/img/main-weapons-outlined/2010.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/img/main-weapons-outlined/2020.avif
Normal file
BIN
public/img/main-weapons-outlined/2020.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/img/main-weapons-outlined/2030.avif
Normal file
BIN
public/img/main-weapons-outlined/2030.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/img/main-weapons-outlined/2040.avif
Normal file
BIN
public/img/main-weapons-outlined/2040.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/img/main-weapons-outlined/2050.avif
Normal file
BIN
public/img/main-weapons-outlined/2050.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
public/img/main-weapons-outlined/2060.avif
Normal file
BIN
public/img/main-weapons-outlined/2060.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
public/img/main-weapons-outlined/210.avif
Normal file
BIN
public/img/main-weapons-outlined/210.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/img/main-weapons-outlined/220.avif
Normal file
BIN
public/img/main-weapons-outlined/220.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
public/img/main-weapons-outlined/230.avif
Normal file
BIN
public/img/main-weapons-outlined/230.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
public/img/main-weapons-outlined/240.avif
Normal file
BIN
public/img/main-weapons-outlined/240.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
public/img/main-weapons-outlined/250.avif
Normal file
BIN
public/img/main-weapons-outlined/250.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
public/img/main-weapons-outlined/30.avif
Normal file
BIN
public/img/main-weapons-outlined/30.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
public/img/main-weapons-outlined/300.avif
Normal file
BIN
public/img/main-weapons-outlined/300.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/img/main-weapons-outlined/3000.avif
Normal file
BIN
public/img/main-weapons-outlined/3000.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
public/img/main-weapons-outlined/3010.avif
Normal file
BIN
public/img/main-weapons-outlined/3010.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
public/img/main-weapons-outlined/3020.avif
Normal file
BIN
public/img/main-weapons-outlined/3020.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
public/img/main-weapons-outlined/3030.avif
Normal file
BIN
public/img/main-weapons-outlined/3030.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
public/img/main-weapons-outlined/3040.avif
Normal file
BIN
public/img/main-weapons-outlined/3040.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
public/img/main-weapons-outlined/310.avif
Normal file
BIN
public/img/main-weapons-outlined/310.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
public/img/main-weapons-outlined/40.avif
Normal file
BIN
public/img/main-weapons-outlined/40.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
public/img/main-weapons-outlined/400.avif
Normal file
BIN
public/img/main-weapons-outlined/400.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public/img/main-weapons-outlined/4000.avif
Normal file
BIN
public/img/main-weapons-outlined/4000.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
public/img/main-weapons-outlined/4010.avif
Normal file
BIN
public/img/main-weapons-outlined/4010.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
public/img/main-weapons-outlined/4020.avif
Normal file
BIN
public/img/main-weapons-outlined/4020.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/img/main-weapons-outlined/4030.avif
Normal file
BIN
public/img/main-weapons-outlined/4030.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
public/img/main-weapons-outlined/4040.avif
Normal file
BIN
public/img/main-weapons-outlined/4040.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/img/main-weapons-outlined/45.avif
Normal file
BIN
public/img/main-weapons-outlined/45.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
public/img/main-weapons-outlined/50.avif
Normal file
BIN
public/img/main-weapons-outlined/50.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public/img/main-weapons-outlined/5000.avif
Normal file
BIN
public/img/main-weapons-outlined/5000.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
public/img/main-weapons-outlined/5010.avif
Normal file
BIN
public/img/main-weapons-outlined/5010.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
public/img/main-weapons-outlined/5020.avif
Normal file
BIN
public/img/main-weapons-outlined/5020.png
Normal file
|
After Width: | Height: | Size: 10 KiB |