mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-24 03:55:49 -05:00
Merge branch 'master' into update-translation
This commit is contained in:
@@ -4,7 +4,7 @@ import Fuse from "fuse.js";
|
||||
import clsx from "clsx";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import type { GearType, UserWithPlusTier } from "~/db/types";
|
||||
import { useUsers } from "~/hooks/swr";
|
||||
import { useAllEventsWithMapPools, useUsers } from "~/hooks/swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
clothesGearIds,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "~/modules/in-game-lists";
|
||||
import { gearImageUrl, mainWeaponImageUrl } from "~/utils/urls";
|
||||
import { Image } from "./Image";
|
||||
import { type SerializedMapPoolEvent } from "~/routes/calendar/map-pool-events";
|
||||
|
||||
const MAX_RESULTS_SHOWN = 6;
|
||||
|
||||
@@ -36,6 +37,7 @@ interface ComboboxProps<T> {
|
||||
initialValue?: ComboboxOption<T>;
|
||||
clearsInputOnFocus?: boolean;
|
||||
onChange?: (selectedOption?: ComboboxOption<T>) => void;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export function Combobox<T extends Record<string, string | null | number>>({
|
||||
@@ -49,11 +51,16 @@ export function Combobox<T extends Record<string, string | null | number>>({
|
||||
className,
|
||||
id,
|
||||
isLoading = false,
|
||||
fullWidth = false,
|
||||
}: ComboboxProps<T>) {
|
||||
const [selectedOption, setSelectedOption] =
|
||||
React.useState<Unpacked<typeof options>>();
|
||||
const [lastSelectedOption, setLastSelectedOption] =
|
||||
React.useState<Unpacked<typeof options>>();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selectedOption, setSelectedOption] = React.useState<
|
||||
Unpacked<typeof options> | undefined
|
||||
>(initialValue);
|
||||
const [lastSelectedOption, setLastSelectedOption] = React.useState<
|
||||
Unpacked<typeof options> | undefined
|
||||
>(initialValue);
|
||||
const [query, setQuery] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -75,73 +82,85 @@ export function Combobox<T extends Record<string, string | null | number>>({
|
||||
|
||||
const noMatches = filteredOptions.length === 0;
|
||||
|
||||
const displayValue = (option: Unpacked<typeof options>) => {
|
||||
return option?.label ?? "";
|
||||
};
|
||||
|
||||
return (
|
||||
<HeadlessCombobox
|
||||
value={selectedOption}
|
||||
onChange={(selected) => {
|
||||
onChange?.(selected);
|
||||
setSelectedOption(selected);
|
||||
setLastSelectedOption(selected);
|
||||
}}
|
||||
name={inputName}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<HeadlessCombobox.Input
|
||||
onFocus={() => {
|
||||
if (clearsInputOnFocus) {
|
||||
setSelectedOption(undefined);
|
||||
}
|
||||
<div className="combobox-wrapper">
|
||||
<HeadlessCombobox
|
||||
value={selectedOption}
|
||||
onChange={(selected) => {
|
||||
onChange?.(selected);
|
||||
setSelectedOption(selected);
|
||||
setLastSelectedOption(selected);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!selectedOption && clearsInputOnFocus) {
|
||||
setSelectedOption(lastSelectedOption);
|
||||
}
|
||||
}}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={isLoading ? "Loading..." : placeholder}
|
||||
className={clsx("combobox-input", className)}
|
||||
displayValue={(option) =>
|
||||
(option as unknown as Unpacked<typeof options>)?.label ?? ""
|
||||
}
|
||||
data-cy={`${inputName}-combobox-input`}
|
||||
id={id}
|
||||
required={required}
|
||||
/>
|
||||
<HeadlessCombobox.Options
|
||||
className={clsx("combobox-options", {
|
||||
empty: noMatches,
|
||||
hidden: !query,
|
||||
})}
|
||||
name={inputName}
|
||||
disabled={!selectedOption && isLoading}
|
||||
>
|
||||
{noMatches ? (
|
||||
<div className="combobox-no-matches">
|
||||
No matches found <span className="combobox-emoji">🤔</span>
|
||||
</div>
|
||||
) : (
|
||||
filteredOptions.map((option) => (
|
||||
<HeadlessCombobox.Option
|
||||
key={option.value}
|
||||
value={option}
|
||||
as={React.Fragment}
|
||||
>
|
||||
{({ active }) => (
|
||||
<li className={clsx("combobox-item", { active })}>
|
||||
{option.imgPath && (
|
||||
<Image
|
||||
alt=""
|
||||
path={option.imgPath}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
{option.label}
|
||||
</li>
|
||||
)}
|
||||
</HeadlessCombobox.Option>
|
||||
))
|
||||
)}
|
||||
</HeadlessCombobox.Options>
|
||||
</HeadlessCombobox>
|
||||
<HeadlessCombobox.Input
|
||||
onFocus={() => {
|
||||
if (clearsInputOnFocus) {
|
||||
setSelectedOption(undefined);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!selectedOption && clearsInputOnFocus) {
|
||||
setSelectedOption(lastSelectedOption);
|
||||
}
|
||||
}}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={isLoading ? t("actions.loading") : placeholder}
|
||||
className={clsx("combobox-input", className, {
|
||||
fullWidth,
|
||||
})}
|
||||
// To make SSR prefill work in an uncontrolled component
|
||||
defaultValue={initialValue ? displayValue(initialValue) : undefined}
|
||||
displayValue={displayValue}
|
||||
data-cy={`${inputName}-combobox-input`}
|
||||
id={id}
|
||||
required={required}
|
||||
/>
|
||||
<HeadlessCombobox.Options
|
||||
className={clsx("combobox-options", {
|
||||
empty: noMatches,
|
||||
fullWidth,
|
||||
hidden: !query,
|
||||
})}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="combobox-no-matches">{t("actions.loading")}</div>
|
||||
) : noMatches ? (
|
||||
<div className="combobox-no-matches">
|
||||
{t("forms.errors.noSearchMatches")}{" "}
|
||||
<span className="combobox-emoji">🤔</span>
|
||||
</div>
|
||||
) : (
|
||||
filteredOptions.map((option) => (
|
||||
<HeadlessCombobox.Option
|
||||
key={option.value}
|
||||
value={option}
|
||||
as={React.Fragment}
|
||||
>
|
||||
{({ active }) => (
|
||||
<li className={clsx("combobox-item", { active })}>
|
||||
{option.imgPath && (
|
||||
<Image
|
||||
alt=""
|
||||
path={option.imgPath}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
{option.label}
|
||||
</li>
|
||||
)}
|
||||
</HeadlessCombobox.Option>
|
||||
))
|
||||
)}
|
||||
</HeadlessCombobox.Options>
|
||||
</HeadlessCombobox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,6 +176,7 @@ export function UserCombobox({
|
||||
ComboboxProps<Pick<UserWithPlusTier, "discordId" | "plusTier">>,
|
||||
"inputName" | "onChange" | "className" | "id" | "required"
|
||||
> & { userIdsToOmit?: Set<number>; initialUserId?: number }) {
|
||||
const { t } = useTranslation();
|
||||
const { users, isLoading, isError } = useUsers();
|
||||
|
||||
const options = React.useMemo(() => {
|
||||
@@ -181,9 +201,7 @@ export function UserCombobox({
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="text-sm text-error">
|
||||
Something went wrong. Try reloading the page.
|
||||
</div>
|
||||
<div className="text-sm text-error">{t("errors.genericReload")}</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,7 +209,7 @@ export function UserCombobox({
|
||||
<Combobox
|
||||
inputName={inputName}
|
||||
options={options}
|
||||
placeholder="Sendou#0043"
|
||||
placeholder="Sendou#4059"
|
||||
isLoading={isLoading}
|
||||
initialValue={initialValue}
|
||||
onChange={onChange}
|
||||
@@ -288,3 +306,71 @@ export function GearCombobox({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const mapPoolEventToOption = (
|
||||
e: SerializedMapPoolEvent
|
||||
): ComboboxOption<Pick<SerializedMapPoolEvent, "serializedMapPool">> => ({
|
||||
serializedMapPool: e.serializedMapPool,
|
||||
label: e.name,
|
||||
value: e.id.toString(),
|
||||
});
|
||||
|
||||
type MapPoolEventsComboboxProps = Pick<
|
||||
ComboboxProps<Pick<SerializedMapPoolEvent, "serializedMapPool">>,
|
||||
"inputName" | "className" | "id" | "required"
|
||||
> & {
|
||||
initialEvent?: SerializedMapPoolEvent;
|
||||
onChange: (event?: SerializedMapPoolEvent) => void;
|
||||
};
|
||||
|
||||
export function MapPoolEventsCombobox({
|
||||
id,
|
||||
required,
|
||||
className,
|
||||
inputName,
|
||||
onChange,
|
||||
initialEvent,
|
||||
}: MapPoolEventsComboboxProps) {
|
||||
const { t } = useTranslation();
|
||||
const { events, isLoading, isError } = useAllEventsWithMapPools();
|
||||
|
||||
const options = React.useMemo(
|
||||
() => (events ? events.map(mapPoolEventToOption) : []),
|
||||
[events]
|
||||
);
|
||||
|
||||
// this is important so that we don't trigger the reset to the initialEvent every time
|
||||
const initialOption = React.useMemo(
|
||||
() => initialEvent && mapPoolEventToOption(initialEvent),
|
||||
[initialEvent]
|
||||
);
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="text-sm text-error">{t("errors.genericReload")}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
inputName={inputName}
|
||||
options={isLoading && initialOption ? [initialOption] : options}
|
||||
placeholder={t("actions.search")}
|
||||
initialValue={initialOption}
|
||||
onChange={(e) => {
|
||||
onChange(
|
||||
e && {
|
||||
id: parseInt(e.value, 10),
|
||||
name: e.label,
|
||||
serializedMapPool: e.serializedMapPool,
|
||||
}
|
||||
);
|
||||
}}
|
||||
className={className}
|
||||
id={id}
|
||||
required={required}
|
||||
isLoading={isLoading}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
406
app/components/MapPoolSelector.tsx
Normal file
406
app/components/MapPoolSelector.tsx
Normal file
@@ -0,0 +1,406 @@
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Image } from "~/components/Image";
|
||||
import {
|
||||
type ModeShort,
|
||||
modesShort,
|
||||
type StageId,
|
||||
} from "~/modules/in-game-lists";
|
||||
import { modes, stageIds } from "~/modules/in-game-lists";
|
||||
import { MapPool } from "~/modules/map-pool-serializer";
|
||||
import { modeImageUrl, stageImageUrl } from "~/utils/urls";
|
||||
import { Button } from "~/components/Button";
|
||||
import { split, startsWith } from "~/utils/strings";
|
||||
import { CrossIcon } from "./icons/Cross";
|
||||
import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft";
|
||||
import * as React from "react";
|
||||
import type { CalendarEvent } from "~/db/types";
|
||||
import type { SerializedMapPoolEvent } from "~/routes/calendar/map-pool-events";
|
||||
import { assertType } from "~/utils/types";
|
||||
import { MapPoolEventsCombobox } from "./Combobox";
|
||||
|
||||
export type MapPoolSelectorProps = {
|
||||
mapPool: MapPool;
|
||||
handleRemoval?: () => void;
|
||||
handleMapPoolChange: (
|
||||
mapPool: MapPool,
|
||||
event?: Pick<CalendarEvent, "id" | "name">
|
||||
) => void;
|
||||
className?: string;
|
||||
recentEvents?: SerializedMapPoolEvent[];
|
||||
initialEvent?: Pick<CalendarEvent, "id" | "name">;
|
||||
};
|
||||
|
||||
export function MapPoolSelector({
|
||||
mapPool,
|
||||
handleMapPoolChange,
|
||||
handleRemoval,
|
||||
className,
|
||||
recentEvents,
|
||||
initialEvent,
|
||||
}: MapPoolSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [template, setTemplate] = React.useState<MapPoolTemplateValue>(
|
||||
initialEvent ? "event" : detectTemplate(mapPool)
|
||||
);
|
||||
|
||||
const [initialSerializedEvent, setInitialSerializedEvent] = React.useState(
|
||||
(): SerializedMapPoolEvent | undefined =>
|
||||
initialEvent && {
|
||||
...initialEvent,
|
||||
serializedMapPool: mapPool.serialized,
|
||||
}
|
||||
);
|
||||
|
||||
const handleStageModesChange = (newMapPool: MapPool) => {
|
||||
setTemplate(detectTemplate(newMapPool));
|
||||
handleMapPoolChange(newMapPool);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
setTemplate("none");
|
||||
handleMapPoolChange(MapPool.EMPTY);
|
||||
};
|
||||
|
||||
const handleTemplateChange = (template: MapPoolTemplateValue) => {
|
||||
setTemplate(template);
|
||||
|
||||
if (template === "none") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (template === "event") {
|
||||
// If the user selected the "event" option, the _initial_ event passed via
|
||||
// props is likely not the current state and should not be prefilled
|
||||
// anymore.
|
||||
setInitialSerializedEvent(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (startsWith(template, "preset:")) {
|
||||
const [, presetId] = split(template, ":");
|
||||
|
||||
handleMapPoolChange(MapPool[presetId]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (startsWith(template, "recent-event:")) {
|
||||
const [, eventId] = split(template, ":");
|
||||
|
||||
const event = recentEvents?.find((e) => e.id.toString() === eventId);
|
||||
|
||||
if (event) {
|
||||
handleMapPoolChange(new MapPool(event.serializedMapPool), event);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
assertType<never, typeof template>();
|
||||
};
|
||||
|
||||
return (
|
||||
<fieldset className={className}>
|
||||
<legend>{t("maps.mapPool")}</legend>
|
||||
<div className="stack horizontal sm justify-end">
|
||||
{handleRemoval && (
|
||||
<Button variant="minimal" onClick={handleRemoval}>
|
||||
{t("actions.remove")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="minimal-destructive"
|
||||
disabled={mapPool.isEmpty()}
|
||||
onClick={handleClear}
|
||||
>
|
||||
{t("actions.clear")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="stack md">
|
||||
<div className="maps__template-selection">
|
||||
<MapPoolTemplateSelect
|
||||
value={template}
|
||||
handleChange={handleTemplateChange}
|
||||
recentEvents={recentEvents}
|
||||
/>
|
||||
{template === "event" && (
|
||||
<TemplateEventSelection
|
||||
initialEvent={initialSerializedEvent}
|
||||
handleEventChange={handleMapPoolChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<MapPoolStages
|
||||
mapPool={mapPool}
|
||||
handleMapPoolChange={handleStageModesChange}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
export type MapPoolStagesProps = {
|
||||
mapPool: MapPool;
|
||||
handleMapPoolChange?: (newMapPool: MapPool) => void;
|
||||
};
|
||||
|
||||
export function MapPoolStages({
|
||||
mapPool,
|
||||
handleMapPoolChange,
|
||||
}: MapPoolStagesProps) {
|
||||
const { t } = useTranslation(["game-misc", "common"]);
|
||||
|
||||
const isPresentational = !handleMapPoolChange;
|
||||
|
||||
const stageRowIsVisible = (stageId: StageId) => {
|
||||
if (!isPresentational) return true;
|
||||
|
||||
return mapPool.hasStage(stageId);
|
||||
};
|
||||
|
||||
const handleModeChange = ({
|
||||
mode,
|
||||
stageId,
|
||||
}: {
|
||||
mode: ModeShort;
|
||||
stageId: StageId;
|
||||
}) => {
|
||||
const newMapPool = mapPool.parsed[mode].includes(stageId)
|
||||
? new MapPool({
|
||||
...mapPool.parsed,
|
||||
[mode]: mapPool.parsed[mode].filter((id) => id !== stageId),
|
||||
})
|
||||
: new MapPool({
|
||||
...mapPool.parsed,
|
||||
[mode]: [...mapPool.parsed[mode], stageId],
|
||||
});
|
||||
|
||||
handleMapPoolChange?.(newMapPool);
|
||||
};
|
||||
|
||||
const handleStageClear = (stageId: StageId) => {
|
||||
const newMapPool = new MapPool({
|
||||
TW: mapPool.parsed.TW.filter((id) => id !== stageId),
|
||||
SZ: mapPool.parsed.SZ.filter((id) => id !== stageId),
|
||||
TC: mapPool.parsed.TC.filter((id) => id !== stageId),
|
||||
RM: mapPool.parsed.RM.filter((id) => id !== stageId),
|
||||
CB: mapPool.parsed.CB.filter((id) => id !== stageId),
|
||||
});
|
||||
|
||||
handleMapPoolChange?.(newMapPool);
|
||||
};
|
||||
|
||||
const handleStageAdd = (stageId: StageId) => {
|
||||
const newMapPool = new MapPool({
|
||||
TW: [...mapPool.parsed.TW, stageId],
|
||||
SZ: [...mapPool.parsed.SZ, stageId],
|
||||
TC: [...mapPool.parsed.TC, stageId],
|
||||
RM: [...mapPool.parsed.RM, stageId],
|
||||
CB: [...mapPool.parsed.CB, stageId],
|
||||
});
|
||||
|
||||
handleMapPoolChange?.(newMapPool);
|
||||
};
|
||||
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<div className="stack md">
|
||||
{stageIds.filter(stageRowIsVisible).map((stageId) => (
|
||||
<div key={stageId} className="maps__stage-row">
|
||||
<Image
|
||||
className="maps__stage-image"
|
||||
alt=""
|
||||
path={stageImageUrl(stageId)}
|
||||
width={80}
|
||||
height={45}
|
||||
/>
|
||||
<div
|
||||
className="maps__stage-name-row"
|
||||
role="group"
|
||||
aria-labelledby={`${id}-stage-name-${stageId}`}
|
||||
>
|
||||
<div id={`${id}-stage-name-${stageId}`}>
|
||||
{t(`game-misc:STAGE_${stageId}`)}
|
||||
</div>
|
||||
<div className="maps__mode-buttons-container">
|
||||
{modes.map((mode) => {
|
||||
const selected = mapPool.parsed[mode.short].includes(stageId);
|
||||
|
||||
if (isPresentational && !selected) return null;
|
||||
if (isPresentational && selected) {
|
||||
return (
|
||||
<Image
|
||||
key={mode.short}
|
||||
className={clsx("maps__mode", {
|
||||
selected,
|
||||
})}
|
||||
title={t(`game-misc:MODE_LONG_${mode.short}`)}
|
||||
alt={t(`game-misc:MODE_LONG_${mode.short}`)}
|
||||
path={modeImageUrl(mode.short)}
|
||||
width={33}
|
||||
height={33}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={mode.short}
|
||||
className={clsx("maps__mode-button", "outline-theme", {
|
||||
selected,
|
||||
})}
|
||||
onClick={() =>
|
||||
handleModeChange?.({ mode: mode.short, stageId })
|
||||
}
|
||||
type="button"
|
||||
title={t(`game-misc:MODE_LONG_${mode.short}`)}
|
||||
aria-describedby={`${id}-stage-name-${stageId}`}
|
||||
aria-pressed={selected}
|
||||
>
|
||||
<Image
|
||||
className={clsx("maps__mode", {
|
||||
selected,
|
||||
})}
|
||||
alt={t(`game-misc:MODE_LONG_${mode.short}`)}
|
||||
path={modeImageUrl(mode.short)}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{!isPresentational &&
|
||||
(mapPool.hasStage(stageId) ? (
|
||||
<Button
|
||||
key="clear"
|
||||
onClick={() => handleStageClear(stageId)}
|
||||
icon={<CrossIcon />}
|
||||
variant="minimal"
|
||||
aria-label={t("common:actions.remove")}
|
||||
title={t("common:actions.remove")}
|
||||
tiny
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
key="select-all"
|
||||
onClick={() => handleStageAdd(stageId)}
|
||||
icon={<ArrowLongLeftIcon />}
|
||||
variant="minimal"
|
||||
aria-label={t("common:actions.selectAll")}
|
||||
title={t("common:actions.selectAll")}
|
||||
tiny
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type MapModePresetId = "ANARCHY" | "ALL" | ModeShort;
|
||||
|
||||
const presetIds: MapModePresetId[] = ["ANARCHY", "ALL", ...modesShort];
|
||||
|
||||
type MapPoolTemplateValue =
|
||||
| "none"
|
||||
| `preset:${MapModePresetId}`
|
||||
| `recent-event:${string}`
|
||||
| "event";
|
||||
|
||||
function detectTemplate(mapPool: MapPool): MapPoolTemplateValue {
|
||||
for (const presetId of presetIds) {
|
||||
if (MapPool[presetId].serialized === mapPool.serialized) {
|
||||
return `preset:${presetId}`;
|
||||
}
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
type MapPoolTemplateSelectProps = {
|
||||
value: MapPoolTemplateValue;
|
||||
handleChange: (newValue: MapPoolTemplateValue) => void;
|
||||
recentEvents?: Pick<CalendarEvent, "id" | "name">[];
|
||||
};
|
||||
|
||||
function MapPoolTemplateSelect({
|
||||
handleChange,
|
||||
value,
|
||||
recentEvents,
|
||||
}: MapPoolTemplateSelectProps) {
|
||||
const { t } = useTranslation(["game-misc", "common"]);
|
||||
|
||||
return (
|
||||
<label className="stack sm">
|
||||
{t("common:maps.template")}
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
handleChange(e.currentTarget.value as MapPoolTemplateValue);
|
||||
}}
|
||||
>
|
||||
<option value="none">{t("common:maps.template.none")}</option>
|
||||
<option value="event">{t("common:maps.template.event")}</option>
|
||||
<optgroup label={t("common:maps.template.presets")}>
|
||||
{(["ANARCHY", "ALL"] as const).map((presetId) => (
|
||||
<option key={presetId} value={`preset:${presetId}`}>
|
||||
{t(`common:maps.template.preset.${presetId}`)}
|
||||
</option>
|
||||
))}
|
||||
{modes.map((mode) => (
|
||||
<option key={mode.short} value={`preset:${mode.short}`}>
|
||||
{t(`common:maps.template.preset.onlyMode`, {
|
||||
modeName: t(`game-misc:MODE_LONG_${mode.short}`),
|
||||
})}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
{recentEvents && recentEvents.length > 0 && (
|
||||
<optgroup label={t("common:maps.template.yourRecentEvents")}>
|
||||
{recentEvents.map((event) => (
|
||||
<option key={event.id} value={`recent-event:${event.id}`}>
|
||||
{event.name}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
type TemplateEventSelectionProps = {
|
||||
handleEventChange: (
|
||||
mapPool: MapPool,
|
||||
event?: Pick<CalendarEvent, "id" | "name">
|
||||
) => void;
|
||||
initialEvent?: SerializedMapPoolEvent;
|
||||
};
|
||||
function TemplateEventSelection({
|
||||
handleEventChange,
|
||||
initialEvent,
|
||||
}: TemplateEventSelectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<label className="stack sm">
|
||||
{t("maps.template.event")}
|
||||
<MapPoolEventsCombobox
|
||||
id={id}
|
||||
inputName={id}
|
||||
onChange={(e) => {
|
||||
if (e) {
|
||||
handleEventChange(new MapPool(e.serializedMapPool), {
|
||||
id: e.id,
|
||||
name: e.name,
|
||||
});
|
||||
}
|
||||
}}
|
||||
initialEvent={initialEvent}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
18
app/components/icons/ArrowLongLeft.tsx
Normal file
18
app/components/icons/ArrowLongLeft.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
export function ArrowLongLeftIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={2}
|
||||
stroke="currentColor"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6.75 15.75L3 12m0 0l3.75-3.75M3 12h18"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -43,6 +43,7 @@ export function Menu({
|
||||
onClick={closeMenu}
|
||||
data-cy={`menu-link-${navItem.name}`}
|
||||
tabIndex={!expanded ? -1 : undefined}
|
||||
prefetch={navItem.prefetch ? "render" : undefined}
|
||||
>
|
||||
<Image
|
||||
className="layout__menu__link__icon"
|
||||
|
||||
@@ -68,7 +68,7 @@ export const Layout = React.memo(function Layout({
|
||||
</header>
|
||||
<Menu expanded={menuOpen} closeMenu={() => setMenuOpen(false)} />
|
||||
{activeNavItem && (
|
||||
<div className="layout__page-title-header">
|
||||
<h1 className="layout__page-title-header">
|
||||
<Image
|
||||
path={navIconUrl(activeNavItem.name)}
|
||||
width={28}
|
||||
@@ -76,7 +76,7 @@ export const Layout = React.memo(function Layout({
|
||||
alt=""
|
||||
/>
|
||||
{t(`pages.${activeNavItem.name}` as any)}
|
||||
</div>
|
||||
</h1>
|
||||
)}
|
||||
{children}
|
||||
<Footer patrons={patrons} />
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
[
|
||||
{
|
||||
"name": "admin",
|
||||
"url": "admin"
|
||||
"url": "admin",
|
||||
"prefetch": false
|
||||
},
|
||||
{ "name": "builds", "url": "builds" },
|
||||
{ "name": "analyzer", "url": "analyzer" },
|
||||
{ "name": "object-damage-calculator", "url": "object-damage-calculator" },
|
||||
{ "name": "calendar", "url": "calendar" },
|
||||
{ "name": "maps", "url": "maps" },
|
||||
{ "name": "badges", "url": "badges" },
|
||||
{ "name": "builds", "url": "builds", "prefetch": true },
|
||||
{ "name": "analyzer", "url": "analyzer", "prefetch": true },
|
||||
{
|
||||
"name": "object-damage-calculator",
|
||||
"url": "object-damage-calculator",
|
||||
"prefetch": true
|
||||
},
|
||||
{ "name": "calendar", "url": "calendar", "prefetch": false },
|
||||
{ "name": "maps", "url": "maps", "prefetch": false },
|
||||
{ "name": "badges", "url": "badges", "prefetch": false },
|
||||
{
|
||||
"name": "plus",
|
||||
"url": "plus/suggestions"
|
||||
"url": "plus/suggestions",
|
||||
"prefetch": false
|
||||
}
|
||||
]
|
||||
|
||||
18
app/db/models/calendar/findAllEventsWithMapPools.sql
Normal file
18
app/db/models/calendar/findAllEventsWithMapPools.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
select
|
||||
"CalendarEvent"."id",
|
||||
"CalendarEvent"."name",
|
||||
json_group_array(
|
||||
json_object(
|
||||
'stageId',
|
||||
"MapPoolMap"."stageId",
|
||||
'mode',
|
||||
"MapPoolMap"."mode"
|
||||
)
|
||||
) as "mapPool"
|
||||
from
|
||||
"CalendarEvent"
|
||||
join "MapPoolMap" on "CalendarEvent"."id" = "MapPoolMap"."calendarEventId"
|
||||
group by
|
||||
"CalendarEvent"."id"
|
||||
order by
|
||||
"CalendarEvent"."id" desc
|
||||
21
app/db/models/calendar/findRecentMapPoolsByAuthorId.sql
Normal file
21
app/db/models/calendar/findRecentMapPoolsByAuthorId.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
select
|
||||
"CalendarEvent"."id",
|
||||
"CalendarEvent"."name",
|
||||
json_group_array(
|
||||
json_object(
|
||||
'stageId',
|
||||
"MapPoolMap"."stageId",
|
||||
'mode',
|
||||
"MapPoolMap"."mode"
|
||||
)
|
||||
) as "mapPool"
|
||||
from
|
||||
"CalendarEvent"
|
||||
join "MapPoolMap" on "CalendarEvent"."id" = "MapPoolMap"."calendarEventId"
|
||||
where
|
||||
"CalendarEvent"."authorId" = @authorId
|
||||
group by
|
||||
"CalendarEvent"."id"
|
||||
order by
|
||||
"CalendarEvent"."id" desc
|
||||
limit 5
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
CalendarEventResultPlayer,
|
||||
MapPoolMap,
|
||||
} from "../../types";
|
||||
import { mapPoolListToMapPoolObject } from "~/modules/map-list-generator";
|
||||
import { MapPool } from "~/modules/map-pool-serializer";
|
||||
|
||||
import createSql from "./create.sql";
|
||||
import updateSql from "./update.sql";
|
||||
@@ -36,6 +36,8 @@ import upcomingEventsSql from "./upcomingEvents.sql";
|
||||
import createMapPoolMapSql from "./createMapPoolMap.sql";
|
||||
import deleteMapPoolMapsSql from "./deleteMapPoolMaps.sql";
|
||||
import findMapPoolByEventIdSql from "./findMapPoolByEventId.sql";
|
||||
import findRecentMapPoolsByAuthorIdSql from "./findRecentMapPoolsByAuthorId.sql";
|
||||
import findAllEventsWithMapPoolsSql from "./findAllEventsWithMapPools.sql";
|
||||
|
||||
const createStm = sql.prepare(createSql);
|
||||
const updateStm = sql.prepare(updateSql);
|
||||
@@ -449,7 +451,7 @@ export function findMapPoolByEventId(calendarEventId: CalendarEvent["id"]) {
|
||||
|
||||
if (rows.length === 0) return;
|
||||
|
||||
return mapPoolListToMapPoolObject(rows);
|
||||
return MapPool.parse(rows);
|
||||
}
|
||||
|
||||
const eventsToReportStm = sql.prepare(eventsToReportSql);
|
||||
@@ -467,3 +469,37 @@ export function eventsToReport(authorId?: CalendarEvent["authorId"]) {
|
||||
}) as Array<Pick<CalendarEvent, "id" | "name">>
|
||||
).map((row) => ({ id: row.id, name: row.name }));
|
||||
}
|
||||
|
||||
const findRecentMapPoolsByAuthorIdStm = sql.prepare(
|
||||
findRecentMapPoolsByAuthorIdSql
|
||||
);
|
||||
export function findRecentMapPoolsByAuthorId(
|
||||
authorId: CalendarEvent["authorId"]
|
||||
) {
|
||||
return (
|
||||
findRecentMapPoolsByAuthorIdStm.all({ authorId }) as Array<
|
||||
Pick<CalendarEvent, "id" | "name"> & {
|
||||
mapPool: string;
|
||||
}
|
||||
>
|
||||
).map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
serializedMapPool: MapPool.serialize(JSON.parse(row.mapPool)),
|
||||
}));
|
||||
}
|
||||
|
||||
const findAllEventsWithMapPoolsStm = sql.prepare(findAllEventsWithMapPoolsSql);
|
||||
export function findAllEventsWithMapPools() {
|
||||
return (
|
||||
findAllEventsWithMapPoolsStm.all() as Array<
|
||||
Pick<CalendarEvent, "id" | "name"> & {
|
||||
mapPool: string;
|
||||
}
|
||||
>
|
||||
).map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
serializedMapPool: MapPool.serialize(JSON.parse(row.mapPool)),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
import type { EventsWithMapPoolsLoaderData } from "~/routes/calendar/map-pool-events";
|
||||
import type { UsersLoaderData } from "~/routes/users";
|
||||
|
||||
const ALL_USERS_ROUTE = "/users?_data=routes%2Fusers";
|
||||
import {
|
||||
GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE,
|
||||
GET_ALL_USERS_ROUTE,
|
||||
} from "~/utils/urls";
|
||||
|
||||
const fetcher = async (url: string) => {
|
||||
const res = await fetch(url);
|
||||
@@ -10,7 +13,7 @@ const fetcher = async (url: string) => {
|
||||
|
||||
export function useUsers() {
|
||||
const { data, error } = useSWRImmutable<UsersLoaderData>(
|
||||
ALL_USERS_ROUTE,
|
||||
GET_ALL_USERS_ROUTE,
|
||||
fetcher
|
||||
);
|
||||
|
||||
@@ -20,3 +23,16 @@ export function useUsers() {
|
||||
isError: error,
|
||||
};
|
||||
}
|
||||
|
||||
export function useAllEventsWithMapPools() {
|
||||
const { data, error } = useSWRImmutable<EventsWithMapPoolsLoaderData>(
|
||||
GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE,
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
events: data?.events,
|
||||
isLoading: !error && !data,
|
||||
isError: error,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export const DAMAGE_RECEIVERS = [
|
||||
export const DAMAGE_TYPE = [
|
||||
"NORMAL_MIN",
|
||||
"NORMAL_MAX",
|
||||
"NORMAL_MAX_FULL_CHARGE", // Hydra Splatling goes from 32 to 40 dmg when fully charged
|
||||
"DIRECT",
|
||||
"FULL_CHARGE",
|
||||
"MAX_CHARGE",
|
||||
@@ -38,6 +39,7 @@ export const damageTypeToWeaponType: Record<
|
||||
> = {
|
||||
NORMAL_MIN: "MAIN",
|
||||
NORMAL_MAX: "MAIN",
|
||||
NORMAL_MAX_FULL_CHARGE: "MAIN",
|
||||
DIRECT: "MAIN",
|
||||
FULL_CHARGE: "MAIN",
|
||||
MAX_CHARGE: "MAIN",
|
||||
|
||||
@@ -80,6 +80,8 @@ const shotsToPopRM: Array<
|
||||
[2010, "FULL_CHARGE", 4, 3],
|
||||
// E-liter 4K
|
||||
[2030, "TAP_SHOT", 13, 12],
|
||||
// Hydra Splatling
|
||||
[4020, "NORMAL_MAX", 32, 29],
|
||||
];
|
||||
|
||||
CalculateDamage(
|
||||
|
||||
@@ -341,6 +341,7 @@ const damageTypeToParamsKey: Record<
|
||||
> = {
|
||||
NORMAL_MIN: "DamageParam_ValueMin",
|
||||
NORMAL_MAX: "DamageParam_ValueMax",
|
||||
NORMAL_MAX_FULL_CHARGE: "DamageParam_ValueFullChargeMax",
|
||||
DIRECT: "DamageParam_ValueDirect",
|
||||
DISTANCE: "BlastParam_DistanceDamage",
|
||||
FULL_CHARGE: "DamageParam_ValueFullCharge",
|
||||
@@ -398,7 +399,7 @@ function shotsToSplat({
|
||||
type: DamageType;
|
||||
isTripleShooter: boolean;
|
||||
}) {
|
||||
if (type !== "NORMAL_MAX") return;
|
||||
if (type !== "NORMAL_MAX" && type !== "NORMAL_MAX_FULL_CHARGE") return;
|
||||
|
||||
const multiplier = isTripleShooter ? 3 : 1;
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface MainWeaponParams {
|
||||
MoveSpeedFullCharge?: number;
|
||||
DamageParam_ValueMax?: number;
|
||||
DamageParam_ValueMin?: number;
|
||||
DamageParam_ValueFullChargeMax?: number;
|
||||
DamageParam_ValueDirect?: number;
|
||||
Jump_DegSwerve?: number;
|
||||
Stand_DegSwerve?: number;
|
||||
|
||||
@@ -73,17 +73,17 @@ function validatedAbilityPointsFromSearchParams(searchParams: URLSearchParams) {
|
||||
);
|
||||
}
|
||||
|
||||
export const damageTypePriorityList = [
|
||||
export const damageTypePriorityList: Array<DamageType> = [
|
||||
"DIRECT",
|
||||
"FULL_CHARGE",
|
||||
"MAX_CHARGE",
|
||||
"NORMAL_MAX",
|
||||
"NORMAL_MAX_FULL_CHARGE",
|
||||
"NORMAL_MIN",
|
||||
"TAP_SHOT",
|
||||
"DISTANCE",
|
||||
"BOMB_DIRECT",
|
||||
"BOMB_NORMAL",
|
||||
] as const;
|
||||
];
|
||||
function validatedDamageTypeFromSearchParams({
|
||||
searchParams,
|
||||
analyzed,
|
||||
|
||||
@@ -375,7 +375,7 @@
|
||||
"InkConsume_WeaponSwingParam": 0.18
|
||||
},
|
||||
"1030": {
|
||||
"SpecialPoint": 200,
|
||||
"SpecialPoint": 210,
|
||||
"subWeaponId": 10,
|
||||
"specialWeaponId": 4,
|
||||
"InkConsume_WeaponVerticalSwingParam": 0.12,
|
||||
@@ -513,7 +513,7 @@
|
||||
"InkConsumeSlosher": 0.06
|
||||
},
|
||||
"3020": {
|
||||
"SpecialPoint": 200,
|
||||
"SpecialPoint": 210,
|
||||
"subWeaponId": 5,
|
||||
"specialWeaponId": 6,
|
||||
"MoveSpeed": 0.07,
|
||||
@@ -597,6 +597,7 @@
|
||||
"MoveSpeed_Charge": 0.04,
|
||||
"DamageParam_ValueMax": 320,
|
||||
"DamageParam_ValueMin": 160,
|
||||
"DamageParam_ValueFullChargeMax": 400,
|
||||
"Jump_DegSwerve": 6,
|
||||
"Stand_DegSwerve": 3,
|
||||
"InkRecoverStop": 40,
|
||||
@@ -748,7 +749,7 @@
|
||||
"InkConsumeFullCharge_ChargeParam": 0.085
|
||||
},
|
||||
"7020": {
|
||||
"SpecialPoint": 200,
|
||||
"SpecialPoint": 210,
|
||||
"subWeaponId": 6,
|
||||
"specialWeaponId": 4,
|
||||
"WeaponSpeedType": "Fast",
|
||||
@@ -881,7 +882,7 @@
|
||||
},
|
||||
"SubInkSaveLv": 1,
|
||||
"InkConsume": 0.6,
|
||||
"InkRecoverStop": 70,
|
||||
"InkRecoverStop": 85,
|
||||
"DistanceDamage_BlastParamArray": [
|
||||
[
|
||||
{
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
export const modes = [
|
||||
{
|
||||
short: "TW",
|
||||
long: "Turf War",
|
||||
},
|
||||
{
|
||||
short: "SZ",
|
||||
long: "Splat Zones",
|
||||
},
|
||||
{
|
||||
short: "TC",
|
||||
long: "Tower Control",
|
||||
},
|
||||
{
|
||||
short: "RM",
|
||||
long: "Rainmaker",
|
||||
},
|
||||
{
|
||||
short: "CB",
|
||||
long: "Clam Blitz",
|
||||
},
|
||||
{ short: "TW" },
|
||||
{ short: "SZ" },
|
||||
{ short: "TC" },
|
||||
{ short: "RM" },
|
||||
{ short: "CB" },
|
||||
] as const;
|
||||
|
||||
export const modesShort = modes.map((mode) => mode.short);
|
||||
|
||||
@@ -6,14 +6,13 @@ import type {
|
||||
ModeWithStage,
|
||||
StageId,
|
||||
} from "~/modules/in-game-lists";
|
||||
import type { MapPool } from "~/modules/map-pool-serializer";
|
||||
import clone from "just-clone";
|
||||
import type { MapPool, MapPoolObject } from "~/modules/map-pool-serializer";
|
||||
|
||||
const BACKLOG = 2;
|
||||
|
||||
export type Popularity = Map<ModeShort, Map<StageId, number>>;
|
||||
|
||||
type MapBucket = Map<number, MapPool>;
|
||||
type MapBucket = Map<number, MapPoolObject>;
|
||||
|
||||
/**
|
||||
* @param mapPool Map pool to work with as dictionary
|
||||
@@ -79,7 +78,7 @@ function addAndReturnMap(
|
||||
TC: [],
|
||||
RM: [],
|
||||
CB: [],
|
||||
} as MapPool);
|
||||
} as MapPoolObject);
|
||||
}
|
||||
|
||||
/* prettier-ignore */
|
||||
@@ -97,7 +96,7 @@ function getMapPopular(
|
||||
): StageId {
|
||||
const popularity_map_pool = new Map();
|
||||
for (const [stageId, votes] of popularity.get(mode)!.entries()) {
|
||||
if (mapPool[mode].includes(stageId)) {
|
||||
if (mapPool.parsed[mode].includes(stageId)) {
|
||||
popularity_map_pool.set(stageId, votes);
|
||||
}
|
||||
}
|
||||
@@ -150,7 +149,7 @@ function getMap(
|
||||
mapHistory: StageId[]
|
||||
) {
|
||||
if (!buckets.size) {
|
||||
buckets.set(0, clone(mapPool));
|
||||
buckets.set(0, mapPool.getClonedObject());
|
||||
}
|
||||
|
||||
for (let bucketNum = 0; bucketNum < buckets.size; bucketNum++) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { MapPoolMap } from "~/db/types";
|
||||
import type { ModeShort } from "../in-game-lists";
|
||||
import type { MapPool } from "../map-pool-serializer";
|
||||
import type { MapPool, MapPoolObject } from "../map-pool-serializer";
|
||||
|
||||
export function mapPoolToNonEmptyModes(mapPool: MapPool) {
|
||||
const result: ModeShort[] = [];
|
||||
|
||||
for (const [key, stages] of Object.entries(mapPool)) {
|
||||
for (const [key, stages] of Object.entries(mapPool.parsed)) {
|
||||
if (stages.length === 0) continue;
|
||||
|
||||
result.push(key as ModeShort);
|
||||
@@ -17,7 +17,7 @@ export function mapPoolToNonEmptyModes(mapPool: MapPool) {
|
||||
export function mapPoolListToMapPoolObject(
|
||||
mapPoolList: Array<Pick<MapPoolMap, "stageId" | "mode">>
|
||||
) {
|
||||
const result: MapPool = {
|
||||
const result: MapPoolObject = {
|
||||
TW: [],
|
||||
SZ: [],
|
||||
TC: [],
|
||||
|
||||
@@ -1,6 +1,2 @@
|
||||
export {
|
||||
mapPoolToSerializedString,
|
||||
serializedStringToMapPool,
|
||||
} from "./serializer";
|
||||
|
||||
export type { MapPool } from "./types";
|
||||
export { MapPool } from "./map-pool";
|
||||
export type { MapPoolObject } from "./types";
|
||||
|
||||
136
app/modules/map-pool-serializer/map-pool.ts
Normal file
136
app/modules/map-pool-serializer/map-pool.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import {
|
||||
mapPoolToSerializedString,
|
||||
serializedStringToMapPool,
|
||||
} from "./serializer";
|
||||
import type { ReadonlyMapPoolObject, MapPoolObject } from "./types";
|
||||
import clone from "just-clone";
|
||||
import type { MapPoolMap } from "~/db/types";
|
||||
import { mapPoolListToMapPoolObject } from "~/modules/map-list-generator";
|
||||
import {
|
||||
type ModeShort,
|
||||
type StageId,
|
||||
stageIds,
|
||||
} from "~/modules/in-game-lists";
|
||||
|
||||
type DbMapPoolList = Array<Pick<MapPoolMap, "stageId" | "mode">>;
|
||||
|
||||
export class MapPool {
|
||||
private source: string | ReadonlyMapPoolObject;
|
||||
private asSerialized?: string;
|
||||
private asObject?: ReadonlyMapPoolObject;
|
||||
|
||||
constructor(init: ReadonlyMapPoolObject | string | DbMapPoolList) {
|
||||
this.source = Array.isArray(init) ? mapPoolListToMapPoolObject(init) : init;
|
||||
}
|
||||
|
||||
static serialize(init: ReadonlyMapPoolObject | string | DbMapPoolList) {
|
||||
return new MapPool(init).serialized;
|
||||
}
|
||||
|
||||
static parse(init: MapPoolObject | string | DbMapPoolList) {
|
||||
return new MapPool(init).parsed;
|
||||
}
|
||||
|
||||
static toDbList(init: MapPoolObject | string | DbMapPoolList) {
|
||||
return new MapPool(init).dbList;
|
||||
}
|
||||
|
||||
get serialized(): string {
|
||||
if (this.asSerialized !== undefined) {
|
||||
return this.asSerialized;
|
||||
}
|
||||
|
||||
return (this.asSerialized =
|
||||
typeof this.source === "string"
|
||||
? this.source
|
||||
: mapPoolToSerializedString(this.source));
|
||||
}
|
||||
|
||||
get parsed(): ReadonlyMapPoolObject {
|
||||
if (this.asObject !== undefined) {
|
||||
return this.asObject;
|
||||
}
|
||||
|
||||
return (this.asObject =
|
||||
typeof this.source === "string"
|
||||
? serializedStringToMapPool(this.source)
|
||||
: this.source);
|
||||
}
|
||||
|
||||
get dbList(): DbMapPoolList {
|
||||
return Object.entries(this.parsed).flatMap(([mode, stages]) =>
|
||||
stages.flatMap((stageId) => ({ mode: mode as ModeShort, stageId }))
|
||||
);
|
||||
}
|
||||
|
||||
hasMode(mode: ModeShort): boolean {
|
||||
return this.parsed[mode].length > 0;
|
||||
}
|
||||
|
||||
hasStage(stageId: StageId): boolean {
|
||||
return Object.values(this.parsed).some((stages) =>
|
||||
stages.includes(stageId)
|
||||
);
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return Object.values(this.parsed).every((stages) => stages.length === 0);
|
||||
}
|
||||
|
||||
getClonedObject(): MapPoolObject {
|
||||
return clone(this.parsed) as MapPoolObject;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.serialized;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.parsed;
|
||||
}
|
||||
|
||||
static EMPTY = new MapPool({
|
||||
SZ: [],
|
||||
TC: [],
|
||||
CB: [],
|
||||
RM: [],
|
||||
TW: [],
|
||||
});
|
||||
|
||||
static ALL = new MapPool({
|
||||
SZ: [...stageIds],
|
||||
TC: [...stageIds],
|
||||
CB: [...stageIds],
|
||||
RM: [...stageIds],
|
||||
TW: [...stageIds],
|
||||
});
|
||||
|
||||
static ANARCHY = new MapPool({
|
||||
SZ: [...stageIds],
|
||||
TC: [...stageIds],
|
||||
CB: [...stageIds],
|
||||
RM: [...stageIds],
|
||||
TW: [],
|
||||
});
|
||||
|
||||
static SZ = new MapPool({
|
||||
...MapPool.EMPTY.parsed,
|
||||
SZ: [...stageIds],
|
||||
});
|
||||
static TC = new MapPool({
|
||||
...MapPool.EMPTY.parsed,
|
||||
TC: [...stageIds],
|
||||
});
|
||||
static CB = new MapPool({
|
||||
...MapPool.EMPTY.parsed,
|
||||
CB: [...stageIds],
|
||||
});
|
||||
static RM = new MapPool({
|
||||
...MapPool.EMPTY.parsed,
|
||||
RM: [...stageIds],
|
||||
});
|
||||
static TW = new MapPool({
|
||||
...MapPool.EMPTY.parsed,
|
||||
TW: [...stageIds],
|
||||
});
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
mapPoolToSerializedString,
|
||||
serializedStringToMapPool,
|
||||
} from "./serializer";
|
||||
import type { MapPool } from "./types";
|
||||
import type { MapPoolObject } from "./types";
|
||||
|
||||
const Serializer = suite("Map pool serializer");
|
||||
|
||||
@@ -24,7 +24,7 @@ Serializer("Ignores invalid mode key", () => {
|
||||
});
|
||||
|
||||
Serializer("Matching serialization with IPLMapGen2", () => {
|
||||
const testMapPool: MapPool = {
|
||||
const testMapPool: MapPoolObject = {
|
||||
TW: [0, 3, 4, 7, 8],
|
||||
SZ: [0, 1, 3, 8, 10],
|
||||
TC: [1, 2, 5, 8, 9],
|
||||
@@ -36,7 +36,7 @@ Serializer("Matching serialization with IPLMapGen2", () => {
|
||||
});
|
||||
|
||||
Serializer("Omits key if mode has no maps", () => {
|
||||
const testPoolWithoutTw: MapPool = {
|
||||
const testPoolWithoutTw: MapPoolObject = {
|
||||
CB: [1, 2],
|
||||
RM: [1, 8],
|
||||
TC: [8, 4],
|
||||
@@ -50,7 +50,7 @@ Serializer("Omits key if mode has no maps", () => {
|
||||
});
|
||||
|
||||
Serializer("Returns empty string if no maps", () => {
|
||||
const testPoolWithoutTw: MapPool = {
|
||||
const testPoolWithoutTw: MapPoolObject = {
|
||||
CB: [],
|
||||
RM: [],
|
||||
TC: [],
|
||||
@@ -64,7 +64,7 @@ Serializer("Returns empty string if no maps", () => {
|
||||
});
|
||||
|
||||
Serializer("Value of two modes is the same with same maps", () => {
|
||||
const testPoolWithDuplicateMaps: MapPool = {
|
||||
const testPoolWithDuplicateMaps: MapPoolObject = {
|
||||
CB: [1, 2],
|
||||
RM: [1, 2],
|
||||
TC: [],
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import invariant from "tiny-invariant";
|
||||
import { modesShort, type StageId, stageIds } from "../in-game-lists";
|
||||
import type { MapPool } from "./types";
|
||||
import type { MapPoolObject, ReadonlyMapPoolObject } from "./types";
|
||||
|
||||
export function mapPoolToSerializedString(mapPool: MapPool): string {
|
||||
export function mapPoolToSerializedString(
|
||||
mapPool: ReadonlyMapPoolObject
|
||||
): string {
|
||||
const serializedModes = [];
|
||||
|
||||
for (const mode of modesShort) {
|
||||
@@ -15,7 +17,7 @@ export function mapPoolToSerializedString(mapPool: MapPool): string {
|
||||
return serializedModes.join(";").toLowerCase();
|
||||
}
|
||||
|
||||
function stageIdsToBinary(input: StageId[]) {
|
||||
function stageIdsToBinary(input: readonly StageId[]) {
|
||||
let result = "1";
|
||||
|
||||
for (const stageId of stageIds) {
|
||||
@@ -33,8 +35,10 @@ function binaryToHex(binary: string) {
|
||||
return parseInt(binary, 2).toString(16);
|
||||
}
|
||||
|
||||
export function serializedStringToMapPool(serialized: string) {
|
||||
const result: MapPool = {
|
||||
export function serializedStringToMapPool(
|
||||
serialized: string
|
||||
): ReadonlyMapPoolObject {
|
||||
const result: MapPoolObject = {
|
||||
SZ: [],
|
||||
CB: [],
|
||||
RM: [],
|
||||
@@ -58,7 +62,7 @@ export function serializedStringToMapPool(serialized: string) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function binaryToStageIds(binary: string): StageId[] {
|
||||
function binaryToStageIds(binary: string): readonly StageId[] {
|
||||
const result: StageId[] = [];
|
||||
|
||||
// first 1 is padding
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ModeShort } from "../in-game-lists";
|
||||
import type { StageId } from "../in-game-lists";
|
||||
|
||||
export type MapPool = Record<ModeShort, StageId[]>;
|
||||
export type MapPoolObject = Record<ModeShort, StageId[]>;
|
||||
export type ReadonlyMapPoolObject = Readonly<
|
||||
Record<ModeShort, readonly StageId[]>
|
||||
>;
|
||||
|
||||
@@ -4,6 +4,8 @@ import { isTheme } from "./provider";
|
||||
import type { Theme } from "./provider";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
const TEN_YEARS_IN_SECONDS = 315_360_000;
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
invariant(process.env["SESSION_SECRET"], "SESSION_SECRET is required");
|
||||
}
|
||||
@@ -12,11 +14,12 @@ const sessionSecret = process.env["SESSION_SECRET"] ?? "secret";
|
||||
const themeStorage = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: "theme",
|
||||
secure: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
secrets: [sessionSecret],
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
maxAge: TEN_YEARS_IN_SECONDS,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type LinksFunction, type MetaFunction } from "@remix-run/node";
|
||||
import { Link } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AbilitiesSelector } from "~/components/AbilitiesSelector";
|
||||
@@ -37,9 +38,14 @@ import styles from "~/styles/analyzer.css";
|
||||
import { damageTypeTranslationString } from "~/utils/i18next";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { specialWeaponImageUrl, subWeaponImageUrl } from "~/utils/urls";
|
||||
import {
|
||||
navIconUrl,
|
||||
objectDamageCalculatorPage,
|
||||
specialWeaponImageUrl,
|
||||
subWeaponImageUrl,
|
||||
} from "~/utils/urls";
|
||||
|
||||
export const CURRENT_PATCH = "1.1";
|
||||
export const CURRENT_PATCH = "1.2";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return {
|
||||
@@ -69,6 +75,8 @@ export default function BuildAnalyzerPage() {
|
||||
effects,
|
||||
} = useAnalyzeBuild();
|
||||
|
||||
const objectShredderSelected = build[2][0] === "OS";
|
||||
|
||||
const mainWeaponCategoryItems = [
|
||||
analyzed.stats.shotSpreadAir && (
|
||||
<StatCard
|
||||
@@ -564,6 +572,20 @@ export default function BuildAnalyzerPage() {
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
</StatCategory>
|
||||
{objectShredderSelected && (
|
||||
<Link
|
||||
className="analyzer__noticeable-link"
|
||||
to={objectDamageCalculatorPage(mainWeaponId)}
|
||||
>
|
||||
<Image
|
||||
path={navIconUrl("object-damage-calculator")}
|
||||
width={24}
|
||||
height={24}
|
||||
alt=""
|
||||
/>
|
||||
{t("analyzer:objCalcAd")}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Main>
|
||||
|
||||
@@ -33,14 +33,15 @@ import { discordFullName, makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
calendarEditPage,
|
||||
calendarReportWinnersPage,
|
||||
mapsPage,
|
||||
navIconUrl,
|
||||
readonlyMapsPage,
|
||||
resolveBaseUrl,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
import { actualNumber, id } from "~/utils/zod";
|
||||
import { MapPoolSelector } from "../components/MapPoolSelector";
|
||||
import { MapPoolStages } from "~/components/MapPoolSelector";
|
||||
import { Tags } from "../components/Tags";
|
||||
import { MapPool } from "~/modules/map-pool-serializer";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [
|
||||
@@ -244,10 +245,10 @@ function MapPoolInfo() {
|
||||
return (
|
||||
<Section title="Map pool">
|
||||
<div className="event__map-pool-section">
|
||||
<MapPoolSelector mapPool={data.mapPool} />
|
||||
<MapPoolStages mapPool={new MapPool(data.mapPool)} />
|
||||
<LinkButton
|
||||
className="event__create-map-list-link"
|
||||
to={mapsPage(data.event.eventId)}
|
||||
to={readonlyMapsPage(data.event.eventId)}
|
||||
variant="outlined"
|
||||
tiny
|
||||
>
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Image } from "~/components/Image";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists";
|
||||
import { modes, stageIds } from "~/modules/in-game-lists";
|
||||
import { type MapPool } from "~/modules/map-pool-serializer";
|
||||
import { modeImageUrl, stageImageUrl } from "~/utils/urls";
|
||||
|
||||
export function MapPoolSelector({
|
||||
mapPool,
|
||||
handleMapPoolChange,
|
||||
}: {
|
||||
mapPool: MapPool;
|
||||
handleMapPoolChange?: ({
|
||||
mode,
|
||||
stageId,
|
||||
}: {
|
||||
mode: ModeShort;
|
||||
stageId: StageId;
|
||||
}) => void;
|
||||
}) {
|
||||
const { t } = useTranslation(["game-misc", "calendar"]);
|
||||
|
||||
const isPresentational = !handleMapPoolChange;
|
||||
|
||||
const stageRowIsVisible = (stageId: StageId) => {
|
||||
if (!isPresentational) return true;
|
||||
|
||||
return modes.some((mode) => mapPool[mode.short].includes(stageId));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="stack md">
|
||||
{stageIds.filter(stageRowIsVisible).map((stageId) => (
|
||||
<div key={stageId} className="maps__stage-row">
|
||||
<Image
|
||||
className="maps__stage-image"
|
||||
alt=""
|
||||
path={stageImageUrl(stageId)}
|
||||
width={80}
|
||||
height={45}
|
||||
/>
|
||||
<div className="maps__stage-name-row">
|
||||
<div>{t(`game-misc:STAGE_${stageId}`)}</div>
|
||||
<div className="maps__mode-buttons-container">
|
||||
{modes.map((mode) => {
|
||||
const selected = (mapPool[mode.short] as StageId[]).includes(
|
||||
stageId
|
||||
);
|
||||
|
||||
if (isPresentational && !selected) return null;
|
||||
if (isPresentational && selected) {
|
||||
return (
|
||||
<Image
|
||||
key={mode.short}
|
||||
className={clsx("maps__mode", {
|
||||
selected,
|
||||
})}
|
||||
alt={mode.long}
|
||||
path={modeImageUrl(mode.short)}
|
||||
width={33}
|
||||
height={33}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={mode.short}
|
||||
className={clsx("maps__mode-button", "outline-theme", {
|
||||
selected,
|
||||
})}
|
||||
onClick={() =>
|
||||
handleMapPoolChange?.({ mode: mode.short, stageId })
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Image
|
||||
className={clsx("maps__mode", {
|
||||
selected,
|
||||
})}
|
||||
alt={mode.long}
|
||||
path={modeImageUrl(mode.short)}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
app/routes/calendar/map-pool-events.ts
Normal file
12
app/routes/calendar/map-pool-events.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { json, type SerializeFrom } from "@remix-run/node";
|
||||
import { findAllEventsWithMapPools } from "~/db/models/calendar/queries.server";
|
||||
|
||||
export const loader = () => {
|
||||
return json({
|
||||
events: findAllEventsWithMapPools(),
|
||||
});
|
||||
};
|
||||
|
||||
export type EventsWithMapPoolsLoaderData = SerializeFrom<typeof loader>;
|
||||
export type SerializedMapPoolEvent =
|
||||
EventsWithMapPoolsLoaderData["events"][number];
|
||||
@@ -20,19 +20,13 @@ import { TrashIcon } from "~/components/icons/Trash";
|
||||
import { Input } from "~/components/Input";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import { Toggle } from "~/components/Toggle";
|
||||
import { CALENDAR_EVENT } from "~/constants";
|
||||
import { db } from "~/db";
|
||||
import type { Badge as BadgeType, CalendarEventTag } from "~/db/types";
|
||||
import { useIsMounted } from "~/hooks/useIsMounted";
|
||||
import { requireUser } from "~/modules/auth";
|
||||
import { i18next } from "~/modules/i18n";
|
||||
import type { ModeShort, StageId } from "~/modules/in-game-lists";
|
||||
import {
|
||||
mapPoolToSerializedString,
|
||||
serializedStringToMapPool,
|
||||
type MapPool,
|
||||
} from "~/modules/map-pool-serializer";
|
||||
import { MapPool } from "~/modules/map-pool-serializer";
|
||||
import { canEditCalendarEvent } from "~/permissions";
|
||||
import calendarNewStyles from "~/styles/calendar-new.css";
|
||||
import mapsStyles from "~/styles/maps.css";
|
||||
@@ -58,7 +52,7 @@ import {
|
||||
safeJSONParse,
|
||||
toArray,
|
||||
} from "~/utils/zod";
|
||||
import { MapPoolSelector } from "./components/MapPoolSelector";
|
||||
import { MapPoolSelector } from "~/components/MapPoolSelector";
|
||||
import { Tags } from "./components/Tags";
|
||||
|
||||
const MIN_DATE = new Date(Date.UTC(2015, 4, 28));
|
||||
@@ -152,10 +146,7 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
const deserializedMaps = (() => {
|
||||
if (!data.pool) return;
|
||||
|
||||
const mapPool = serializedStringToMapPool(data.pool);
|
||||
return Object.entries(mapPool).flatMap(([mode, stages]) =>
|
||||
stages.flatMap((stageId) => ({ mode: mode as ModeShort, stageId }))
|
||||
);
|
||||
return MapPool.toDbList(data.pool);
|
||||
})();
|
||||
|
||||
if (data.eventToEditId) {
|
||||
@@ -201,6 +192,9 @@ export const loader = async ({ request }: LoaderArgs) => {
|
||||
|
||||
return json({
|
||||
managedBadges: db.badges.managedByUserId(user.id),
|
||||
recentEventsWithMapPools: db.calendarEvents.findRecentMapPoolsByAuthorId(
|
||||
user.id
|
||||
),
|
||||
eventToEdit: canEditEvent
|
||||
? {
|
||||
...eventToEdit,
|
||||
@@ -526,63 +520,43 @@ function BadgesAdder() {
|
||||
);
|
||||
}
|
||||
|
||||
const DEFAULT_MAP_POOL = {
|
||||
SZ: [],
|
||||
TC: [],
|
||||
CB: [],
|
||||
RM: [],
|
||||
TW: [],
|
||||
};
|
||||
function MapPoolSection() {
|
||||
const { t } = useTranslation(["game-misc", "calendar"]);
|
||||
const { t } = useTranslation(["game-misc", "common"]);
|
||||
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const { eventToEdit, recentEventsWithMapPools } =
|
||||
useLoaderData<typeof loader>();
|
||||
const [mapPool, setMapPool] = React.useState<MapPool>(
|
||||
data.eventToEdit?.mapPool ?? DEFAULT_MAP_POOL
|
||||
eventToEdit?.mapPool ? new MapPool(eventToEdit.mapPool) : MapPool.EMPTY
|
||||
);
|
||||
const [includeMapPool, setIncludeMapPool] = React.useState(
|
||||
Boolean(data.eventToEdit?.mapPool)
|
||||
Boolean(eventToEdit?.mapPool)
|
||||
);
|
||||
|
||||
const handleMapPoolChange = ({
|
||||
mode,
|
||||
stageId,
|
||||
}: {
|
||||
mode: ModeShort;
|
||||
stageId: StageId;
|
||||
}) => {
|
||||
const newMapPool = mapPool[mode].includes(stageId)
|
||||
? {
|
||||
...mapPool,
|
||||
[mode]: mapPool[mode].filter((id) => id !== stageId),
|
||||
}
|
||||
: {
|
||||
...mapPool,
|
||||
[mode]: [...mapPool[mode], stageId],
|
||||
};
|
||||
const id = React.useId();
|
||||
|
||||
setMapPool(newMapPool);
|
||||
};
|
||||
return includeMapPool ? (
|
||||
<>
|
||||
<input type="hidden" name="pool" value={mapPool.serialized} />
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{includeMapPool && (
|
||||
<input
|
||||
type="hidden"
|
||||
name="pool"
|
||||
value={mapPoolToSerializedString(mapPool)}
|
||||
/>
|
||||
)}
|
||||
<Label>{t("calendar:forms.mapPool")}</Label>
|
||||
<div className="stack md">
|
||||
<Toggle checked={includeMapPool} setChecked={setIncludeMapPool} tiny />
|
||||
{includeMapPool && (
|
||||
<MapPoolSelector
|
||||
mapPool={mapPool}
|
||||
handleMapPoolChange={handleMapPoolChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<MapPoolSelector
|
||||
className="w-full"
|
||||
mapPool={mapPool}
|
||||
handleRemoval={() => setIncludeMapPool(false)}
|
||||
handleMapPoolChange={setMapPool}
|
||||
recentEvents={recentEventsWithMapPools}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<label htmlFor={id}>{t("common:maps.mapPool")}</label>
|
||||
<Button
|
||||
id={id}
|
||||
variant="outlined"
|
||||
tiny
|
||||
onClick={() => setIncludeMapPool(true)}
|
||||
>
|
||||
{t("common:actions.add")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
CALENDAR_PAGE,
|
||||
mapsPage,
|
||||
navIconUrl,
|
||||
OBJECT_DAMAGE_CALCULATOR,
|
||||
objectDamageCalculatorPage,
|
||||
plusSuggestionPage,
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
@@ -36,7 +36,7 @@ export const links: LinksFunction = () => {
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons", "builds"],
|
||||
i18n: ["weapons", "builds", "front"],
|
||||
};
|
||||
|
||||
export const loader = async () => {
|
||||
@@ -80,7 +80,7 @@ export default function Index() {
|
||||
navItem="object-damage-calculator"
|
||||
title={t("common:pages.object-damage-calculator")}
|
||||
description={t("front:object-damage-calculator.description")}
|
||||
to={OBJECT_DAMAGE_CALCULATOR}
|
||||
to={objectDamageCalculatorPage()}
|
||||
/>
|
||||
<FeatureCard
|
||||
navItem="plus"
|
||||
|
||||
@@ -7,48 +7,40 @@ import type {
|
||||
import type { ShouldReloadFunction } from "@remix-run/react";
|
||||
import { Link } from "@remix-run/react";
|
||||
import { useLoaderData, useSearchParams } from "@remix-run/react";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCopyToClipboard } from "react-use";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Button } from "~/components/Button";
|
||||
import { Image } from "~/components/Image";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import { Toggle } from "~/components/Toggle";
|
||||
import { db } from "~/db";
|
||||
import { i18next } from "~/modules/i18n";
|
||||
import {
|
||||
modes,
|
||||
stageIds,
|
||||
type ModeShort,
|
||||
type ModeWithStage,
|
||||
type StageId,
|
||||
} from "~/modules/in-game-lists";
|
||||
import { stageIds, type ModeWithStage } from "~/modules/in-game-lists";
|
||||
import {
|
||||
generateMapList,
|
||||
mapPoolToNonEmptyModes,
|
||||
modesOrder,
|
||||
} from "~/modules/map-list-generator";
|
||||
import {
|
||||
mapPoolToSerializedString,
|
||||
serializedStringToMapPool,
|
||||
} from "~/modules/map-pool-serializer";
|
||||
import type { MapPool } from "~/modules/map-pool-serializer/types";
|
||||
import { MapPool } from "~/modules/map-pool-serializer";
|
||||
import styles from "~/styles/maps.css";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
calendarEventPage,
|
||||
ipLabsMaps,
|
||||
modeImageUrl,
|
||||
stageImageUrl,
|
||||
} from "~/utils/urls";
|
||||
import { calendarEventPage, ipLabsMaps } from "~/utils/urls";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
import { MapPoolSelector, MapPoolStages } from "~/components/MapPoolSelector";
|
||||
import { EditIcon } from "~/components/icons/Edit";
|
||||
import { getUser } from "~/modules/auth";
|
||||
import type { CalendarEvent } from "~/db/types";
|
||||
|
||||
const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2;
|
||||
|
||||
export const unstable_shouldReload: ShouldReloadFunction = () => false;
|
||||
export const unstable_shouldReload: ShouldReloadFunction = ({ url }) => {
|
||||
const searchParams = new URL(url).searchParams;
|
||||
// Only let loader reload data if we're not currently editing the map pool
|
||||
// and persisting it in the search params.
|
||||
return searchParams.has("readonly");
|
||||
};
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
@@ -70,6 +62,7 @@ export const handle: SendouRouteHandle = {
|
||||
};
|
||||
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
const user = await getUser(request);
|
||||
const url = new URL(request.url);
|
||||
const calendarEventId = url.searchParams.get("eventId");
|
||||
const t = await i18next.getFixedT(request);
|
||||
@@ -88,40 +81,54 @@ export const loader = async ({ request }: LoaderArgs) => {
|
||||
mapPool: event
|
||||
? db.calendarEvents.findMapPoolByEventId(event.eventId)
|
||||
: null,
|
||||
recentEventsWithMapPools: user
|
||||
? db.calendarEvents.findRecentMapPoolsByAuthorId(user.id)
|
||||
: undefined,
|
||||
title: makeTitle([t("pages.maps")]),
|
||||
};
|
||||
};
|
||||
|
||||
const DEFAULT_MAP_POOL = {
|
||||
SZ: [...stageIds],
|
||||
TC: [...stageIds],
|
||||
CB: [...stageIds],
|
||||
RM: [...stageIds],
|
||||
TW: [],
|
||||
};
|
||||
|
||||
export default function MapListPage() {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { mapPool, handleMapPoolChange } = useSearchParamMapPool();
|
||||
const { mapPool, handleMapPoolChange, readonly, switchToEditMode } =
|
||||
useSearchParamPersistedMapPool();
|
||||
|
||||
return (
|
||||
<Main className="maps__container stack lg">
|
||||
{data.calendarEvent && !searchParams.has("pool") && (
|
||||
<div className="maps__pool-info">
|
||||
{t("common:maps.mapPool")}:{" "}
|
||||
<Link to={calendarEventPage(data.calendarEvent.id)}>
|
||||
{data.calendarEvent.name}
|
||||
</Link>
|
||||
{searchParams.has("readonly") && data.calendarEvent && (
|
||||
<div className="maps__pool-meta">
|
||||
<div className="maps__pool-info">
|
||||
{t("common:maps.mapPool")}:{" "}
|
||||
{
|
||||
<Link to={calendarEventPage(data.calendarEvent.id)}>
|
||||
{data.calendarEvent.name}
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={switchToEditMode}
|
||||
tiny
|
||||
icon={<EditIcon />}
|
||||
>
|
||||
{t("common:actions.edit")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<MapPoolSelector
|
||||
mapPool={mapPool}
|
||||
handleMapPoolChange={handleMapPoolChange}
|
||||
/>
|
||||
{readonly ? (
|
||||
<MapPoolStages mapPool={mapPool} />
|
||||
) : (
|
||||
<MapPoolSelector
|
||||
mapPool={mapPool}
|
||||
handleMapPoolChange={handleMapPoolChange}
|
||||
recentEvents={data.recentEventsWithMapPools}
|
||||
initialEvent={data.calendarEvent}
|
||||
/>
|
||||
)}
|
||||
<a
|
||||
href={ipLabsMaps(mapPoolToSerializedString(mapPool))}
|
||||
href={ipLabsMaps(mapPool.serialized)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="maps__tournament-map-list-link"
|
||||
@@ -133,110 +140,54 @@ export default function MapListPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function useSearchParamMapPool() {
|
||||
function useSearchParamPersistedMapPool() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const mapPool = (() => {
|
||||
const [mapPool, setMapPool] = React.useState(() => {
|
||||
if (searchParams.has("pool")) {
|
||||
return serializedStringToMapPool(searchParams.get("pool")!);
|
||||
return new MapPool(searchParams.get("pool")!);
|
||||
}
|
||||
|
||||
if (data?.mapPool) {
|
||||
return data.mapPool;
|
||||
if (data.mapPool) {
|
||||
return new MapPool(data.mapPool);
|
||||
}
|
||||
|
||||
return DEFAULT_MAP_POOL;
|
||||
})();
|
||||
|
||||
const handleMapPoolChange = ({
|
||||
mode,
|
||||
stageId,
|
||||
}: {
|
||||
mode: ModeShort;
|
||||
stageId: StageId;
|
||||
}) => {
|
||||
const newMapPool = mapPool[mode].includes(stageId)
|
||||
? {
|
||||
...mapPool,
|
||||
[mode]: mapPool[mode].filter((id) => id !== stageId),
|
||||
}
|
||||
: {
|
||||
...mapPool,
|
||||
[mode]: [...mapPool[mode], stageId],
|
||||
};
|
||||
return MapPool.ANARCHY;
|
||||
});
|
||||
|
||||
const handleMapPoolChange = (
|
||||
newMapPool: MapPool,
|
||||
event?: Pick<CalendarEvent, "id" | "name">
|
||||
) => {
|
||||
setMapPool(newMapPool);
|
||||
setSearchParams(
|
||||
{
|
||||
pool: mapPoolToSerializedString(newMapPool),
|
||||
},
|
||||
event
|
||||
? { eventId: event.id.toString() }
|
||||
: {
|
||||
pool: newMapPool.serialized,
|
||||
},
|
||||
{ replace: true, state: { scroll: false } }
|
||||
);
|
||||
};
|
||||
|
||||
const switchToEditMode = () => {
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.delete("readonly");
|
||||
setSearchParams(newSearchParams, {
|
||||
replace: false,
|
||||
state: { scroll: false },
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
mapPool,
|
||||
readonly: searchParams.has("readonly"),
|
||||
handleMapPoolChange,
|
||||
switchToEditMode,
|
||||
};
|
||||
}
|
||||
|
||||
function MapPoolSelector({
|
||||
mapPool,
|
||||
handleMapPoolChange,
|
||||
}: {
|
||||
mapPool: MapPool;
|
||||
handleMapPoolChange: (args: { mode: ModeShort; stageId: StageId }) => void;
|
||||
}) {
|
||||
const { t } = useTranslation(["game-misc"]);
|
||||
|
||||
return (
|
||||
<div className="stack md">
|
||||
{stageIds.map((stageId) => (
|
||||
<div key={stageId} className="maps__stage-row">
|
||||
<Image
|
||||
className="maps__stage-image"
|
||||
alt=""
|
||||
path={stageImageUrl(stageId)}
|
||||
width={80}
|
||||
height={45}
|
||||
/>
|
||||
<div className="maps__stage-name-row">
|
||||
<div>{t(`game-misc:STAGE_${stageId}`)}</div>
|
||||
<div className="maps__mode-buttons-container">
|
||||
{modes.map((mode) => {
|
||||
const selected = mapPool[mode.short].includes(stageId);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={mode.short}
|
||||
className={clsx("maps__mode-button", "outline-theme", {
|
||||
selected,
|
||||
})}
|
||||
onClick={() =>
|
||||
handleMapPoolChange({ mode: mode.short, stageId })
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Image
|
||||
className={clsx("maps__mode", {
|
||||
selected,
|
||||
})}
|
||||
alt={mode.long}
|
||||
path={modeImageUrl(mode.short)}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MapListCreator({ mapPool }: { mapPool: MapPool }) {
|
||||
const { t } = useTranslation(["game-misc", "common"]);
|
||||
const [mapList, setMapList] = React.useState<ModeWithStage[]>();
|
||||
@@ -258,13 +209,16 @@ function MapListCreator({ mapPool }: { mapPool: MapPool }) {
|
||||
setMapList(list);
|
||||
};
|
||||
|
||||
const disabled =
|
||||
mapPool.isEmpty() || (szEveryOther && !mapPool.hasMode("SZ"));
|
||||
|
||||
return (
|
||||
<div className="maps__map-list-creator">
|
||||
<div className="maps__toggle-container">
|
||||
<Label>{t("common:maps.halfSz")}</Label>
|
||||
<Toggle checked={szEveryOther} setChecked={setSzEveryOther} tiny />
|
||||
</div>
|
||||
<Button onClick={handleCreateMaplist}>
|
||||
<Button onClick={handleCreateMaplist} disabled={disabled}>
|
||||
{t("common:maps.createMapList")}
|
||||
</Button>
|
||||
{mapList && (
|
||||
@@ -272,7 +226,12 @@ function MapListCreator({ mapPool }: { mapPool: MapPool }) {
|
||||
<ol className="maps__map-list">
|
||||
{mapList.map(({ mode, stageId }, i) => (
|
||||
<li key={i}>
|
||||
{t(`game-misc:MODE_SHORT_${mode}`)}{" "}
|
||||
<abbr
|
||||
className="maps__mode-abbr"
|
||||
title={t(`game-misc:MODE_LONG_${mode}`)}
|
||||
>
|
||||
{t(`game-misc:MODE_SHORT_${mode}`)}
|
||||
</abbr>{" "}
|
||||
{t(`game-misc:STAGE_${stageId}`)}
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -55,6 +55,19 @@
|
||||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
||||
.analyzer__noticeable-link {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--theme-transparent);
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--semi-bold);
|
||||
gap: var(--s-2);
|
||||
padding-block: var(--s-1);
|
||||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
||||
.analyzer__ap-text {
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xxs);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.calendar-new__container {
|
||||
max-width: 32rem;
|
||||
max-width: 38rem;
|
||||
}
|
||||
|
||||
.calendar-new__select {
|
||||
|
||||
@@ -243,7 +243,8 @@ article {
|
||||
|
||||
select {
|
||||
all: unset;
|
||||
width: 90%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--rounded);
|
||||
background: var(--select-background, var(--bg-lighter));
|
||||
@@ -492,10 +493,18 @@ dialog::backdrop {
|
||||
}
|
||||
}
|
||||
|
||||
.combobox-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.combobox-input {
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.combobox-input.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.combobox-options {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
@@ -509,6 +518,10 @@ dialog::backdrop {
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.combobox-options.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.combobox-options.empty {
|
||||
padding-block: var(--s-1-5);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
.maps__container {
|
||||
max-width: 32rem;
|
||||
max-width: 38rem;
|
||||
}
|
||||
|
||||
.maps__pool-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.maps__pool-info {
|
||||
font-size: var(--fonts-xxs);
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.maps__pool-info a {
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
@@ -49,29 +59,27 @@
|
||||
|
||||
.maps__mode-button {
|
||||
padding: 0;
|
||||
padding: var(--s-1-5);
|
||||
border: none;
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-darker);
|
||||
padding: var(--s-1);
|
||||
border: 2px solid var(--bg-darker);
|
||||
border-radius: var(--rounded-full);
|
||||
background-color: transparent;
|
||||
color: var(--theme);
|
||||
opacity: 1 !important;
|
||||
outline: initial;
|
||||
}
|
||||
|
||||
.maps__mode-button.selected {
|
||||
background-color: var(--theme-very-transparent);
|
||||
border: 2px solid transparent;
|
||||
background-color: var(--bg-mode-active);
|
||||
}
|
||||
|
||||
.maps__stage-image {
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.maps__mode {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.maps__mode.selected {
|
||||
filter: unset;
|
||||
.maps__mode:not(.selected) {
|
||||
filter: var(--inactive-image-filter);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.maps__map-list-creator {
|
||||
@@ -96,3 +104,21 @@
|
||||
font-weight: var(--semi-bold);
|
||||
margin-block-start: var(--s-4);
|
||||
}
|
||||
|
||||
.maps__mode-abbr {
|
||||
color: var(--text-lighter);
|
||||
font-weight: var(--bold);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.maps__template-selection {
|
||||
display: grid;
|
||||
gap: var(--s-2);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
.maps__template-selection {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,10 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ html {
|
||||
--bg-darker-transparent: hsla(202deg 90% 90% / 65%);
|
||||
--bg-ability: rgb(3 6 7);
|
||||
--bg-badge: #000;
|
||||
--bg-mode-active: hsl(255deg 66.7% 50% / 40%);
|
||||
--abilities-button-bg: hsl(237deg 32% 30%);
|
||||
--badge-text: rgb(255 255 255 / 95%);
|
||||
--border: hsl(237deg 100% 86%);
|
||||
@@ -31,6 +32,7 @@ html {
|
||||
--theme-semi-transparent-vibrant: hsl(255deg 100% 81% / 75%);
|
||||
--theme-secondary: hsl(85deg 66.7% 55.3%);
|
||||
--rounded: 16px;
|
||||
--rounded-full: 200px;
|
||||
--rounded-sm: 10px;
|
||||
--fonts-xl: 1.5rem;
|
||||
--fonts-lg: 1.2rem;
|
||||
@@ -73,6 +75,7 @@ html {
|
||||
--s-96: 2rem;
|
||||
--sparse: 0.4px;
|
||||
--label-margin: var(--s-1);
|
||||
--inactive-image-filter: grayscale(100%) brightness(30%);
|
||||
}
|
||||
|
||||
html.dark {
|
||||
@@ -84,6 +87,7 @@ html.dark {
|
||||
--bg-darker-transparent: hsla(237.3deg 42.3% 26.6% / 90%);
|
||||
--bg-ability: rgb(17 19 43);
|
||||
--bg-badge: #000;
|
||||
--bg-mode-active: var(--theme-transparent);
|
||||
--abilities-button-bg: hsl(237.3deg 42.3% 26.6%);
|
||||
--border: hsl(237.3deg 42.3% 45.6%);
|
||||
--button-text: rgb(0 0 0 / 85%);
|
||||
@@ -104,6 +108,7 @@ html.dark {
|
||||
--theme-transparent-vibrant: hsl(255deg 78% 65% / 54%);
|
||||
--theme-semi-transparent-vibrant: hsl(255deg 78% 65% / 75%);
|
||||
--theme-secondary: hsl(85deg 66.7% 55.3%);
|
||||
--inactive-image-filter: grayscale(100%) brightness(130%);
|
||||
}
|
||||
|
||||
html.dark .light-mode-only {
|
||||
|
||||
@@ -30,3 +30,30 @@ export function semiRandomId() {
|
||||
|
||||
export const rawSensToString = (sens: number) =>
|
||||
`${sens > 0 ? "+" : ""}${sens / 10}`;
|
||||
|
||||
type WithStart<
|
||||
S extends string,
|
||||
Start extends string
|
||||
> = S extends `${Start}${infer Rest}` ? `${Start}${Rest}` : never;
|
||||
|
||||
export function startsWith<S extends string, Start extends string>(
|
||||
str: S,
|
||||
start: Start
|
||||
): str is WithStart<S, Start> {
|
||||
return str.startsWith(start);
|
||||
}
|
||||
|
||||
type Split<S extends string, Sep extends string> = string extends S
|
||||
? string[]
|
||||
: S extends ""
|
||||
? []
|
||||
: S extends `${infer T}${Sep}${infer U}`
|
||||
? [T, ...Split<U, Sep>]
|
||||
: [S];
|
||||
|
||||
export function split<S extends string, Sep extends string>(
|
||||
str: S,
|
||||
seperator: Sep
|
||||
) {
|
||||
return str.split(seperator) as Split<S, Sep>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import slugify from "slugify";
|
||||
import type { Badge, GearType, MapPoolMap, User } from "~/db/types";
|
||||
import type {
|
||||
Badge,
|
||||
CalendarEvent,
|
||||
GearType,
|
||||
MapPoolMap,
|
||||
User,
|
||||
} from "~/db/types";
|
||||
import type { ModeShort, weaponCategories } from "~/modules/in-game-lists";
|
||||
import type {
|
||||
Ability,
|
||||
@@ -35,7 +41,6 @@ export const CONTRIBUTIONS_PAGE = "/contributions";
|
||||
export const BADGES_PAGE = "/badges";
|
||||
export const BUILDS_PAGE = "/builds";
|
||||
export const CALENDAR_PAGE = "/calendar";
|
||||
export const OBJECT_DAMAGE_CALCULATOR = "/object-damage-calculator";
|
||||
export const STOP_IMPERSONATING_URL = "/auth/impersonate/stop";
|
||||
export const SEED_URL = "/seed";
|
||||
|
||||
@@ -43,6 +48,9 @@ export const COMMON_PREVIEW_IMAGE = "/img/layout/common-preview.png";
|
||||
export const ERROR_GIRL_IMAGE_PATH = `/img/layout/error-girl`;
|
||||
export const LOGO_PATH = `/img/layout/logo`;
|
||||
|
||||
export const GET_ALL_USERS_ROUTE = "/users";
|
||||
export const GET_ALL_EVENTS_WITH_MAP_POOLS_ROUTE = "/calendar/map-pool-events";
|
||||
|
||||
interface UserLinkArgs {
|
||||
discordId: User["discordId"];
|
||||
customUrl?: User["customUrl"];
|
||||
@@ -75,6 +83,8 @@ export const calendarReportWinnersPage = (eventId: number) =>
|
||||
`/calendar/${eventId}/report-winners`;
|
||||
export const mapsPage = (eventId?: MapPoolMap["calendarEventId"]) =>
|
||||
`/maps${eventId ? `?eventId=${eventId}` : ""}`;
|
||||
export const readonlyMapsPage = (eventId: CalendarEvent["id"]) =>
|
||||
`/maps?readonly&eventId=${eventId}`;
|
||||
export const articlePage = (slug: string) => `/a/${slug}`;
|
||||
export const analyzerPage = (args?: {
|
||||
weaponId: MainWeaponId;
|
||||
@@ -87,6 +97,10 @@ export const analyzerPage = (args?: {
|
||||
)}`
|
||||
: ""
|
||||
}`;
|
||||
export const objectDamageCalculatorPage = (weaponId?: MainWeaponId) =>
|
||||
`/object-damage-calculator${
|
||||
typeof weaponId === "number" ? `?weapon=${weaponId}` : ""
|
||||
}`;
|
||||
|
||||
export const badgeUrl = ({
|
||||
code,
|
||||
|
||||
@@ -34,17 +34,32 @@
|
||||
"actions.delete": "Löschen",
|
||||
"actions.loadMore": "Mehr laden",
|
||||
"actions.close": "Schließen",
|
||||
"actions.loading": "Lädt...",
|
||||
"actions.clear": "Leeren",
|
||||
"actions.selectAll": "Alle auswählen",
|
||||
"actions.search": "Suchen",
|
||||
|
||||
"maps.createMapList": "Arenen-Liste erstellen",
|
||||
"maps.halfSz": "50% Herrschaft",
|
||||
"maps.mapPool": "Arenen-Pool",
|
||||
"maps.tournamentMaplist": "Arenen-Liste für Turnier erstellen (maps.iplabs.ink)",
|
||||
"maps.template": "Vorlage",
|
||||
"maps.template.none": "Keine",
|
||||
"maps.template.event": "Event",
|
||||
"maps.template.presets": "Voreinstellungen",
|
||||
"maps.template.yourRecentEvents": "Deine Events",
|
||||
"maps.template.preset.ANARCHY": "Anarchie-Modi",
|
||||
"maps.template.preset.ALL": "Alle Modi",
|
||||
"maps.template.preset.onlyMode": "Nur {{modeName}}",
|
||||
|
||||
"results": "Ergebnisse",
|
||||
|
||||
"forms.name": "Name",
|
||||
"forms.description": "Beschreibung",
|
||||
"forms.errors.title": "Diese Fehler müssen behoben werden",
|
||||
"forms.errors.noSearchMatches": "Keine Suchergebnisse",
|
||||
|
||||
"errors.genericReload": "Etwas ist schiefgegangen. Versuche die Seite neu zu laden.",
|
||||
|
||||
"tag.name.BADGE": "Abzeichen-Preise",
|
||||
"tag.name.SPECIAL": "Spezielle Regeln",
|
||||
|
||||
@@ -15,5 +15,10 @@
|
||||
"MODE_SHORT_SZ": "HS",
|
||||
"MODE_SHORT_TC": "TK",
|
||||
"MODE_SHORT_RM": "OG",
|
||||
"MODE_SHORT_CB": "MC"
|
||||
"MODE_SHORT_CB": "MC",
|
||||
"MODE_LONG_TW": "Revierkampf",
|
||||
"MODE_LONG_SZ": "Herrschaft",
|
||||
"MODE_LONG_TC": "Turmkommando",
|
||||
"MODE_LONG_RM": "Operation Goldfisch",
|
||||
"MODE_LONG_CB": "Muschelchaos"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"attribute.weight.Fast": "Light",
|
||||
"attribute.weight.Slow": "Heavy",
|
||||
"attribute.weight.Normal": "Normal",
|
||||
"objCalcAd": "For info on Object Shredder check out Object DMG Calc",
|
||||
"stat.category.main": "Main weapon",
|
||||
"stat.category.sub": "Sub weapon",
|
||||
"stat.category.special": "Special weapon",
|
||||
@@ -79,6 +80,7 @@
|
||||
"damage.toSplat": "{{count}} hits to splat",
|
||||
"damage.NORMAL_MIN": "Minimum",
|
||||
"damage.NORMAL_MAX": "Maximum",
|
||||
"damage.NORMAL_MAX_FULL_CHARGE": "Maximum (Fully charged)",
|
||||
"damage.DIRECT": "Direct",
|
||||
"damage.FULL_CHARGE": "Fully charged shot",
|
||||
"damage.MAX_CHARGE": "Maximum partial charge",
|
||||
|
||||
@@ -36,17 +36,32 @@
|
||||
"actions.loadMore": "Load more",
|
||||
"actions.copyToClipboard": "Copy to clipboard",
|
||||
"actions.close": "Close",
|
||||
"actions.loading": "Loading...",
|
||||
"actions.clear": "Clear",
|
||||
"actions.selectAll": "Select All",
|
||||
"actions.search": "Search",
|
||||
|
||||
"maps.createMapList": "Create map list",
|
||||
"maps.halfSz": "50% SZ",
|
||||
"maps.mapPool": "Map pool",
|
||||
"maps.tournamentMaplist": "Create tournament map list (maps.iplabs.ink)",
|
||||
"maps.template": "Template",
|
||||
"maps.template.none": "None",
|
||||
"maps.template.event": "Event",
|
||||
"maps.template.presets": "Presets",
|
||||
"maps.template.yourRecentEvents": "Recent Events",
|
||||
"maps.template.preset.ANARCHY": "Anarchy Modes",
|
||||
"maps.template.preset.ALL": "All Modes",
|
||||
"maps.template.preset.onlyMode": "Only {{modeName}}",
|
||||
|
||||
"results": "Results",
|
||||
|
||||
"forms.name": "Name",
|
||||
"forms.description": "Description",
|
||||
"forms.errors.title": "Following errors need to be fixed",
|
||||
"forms.errors.noSearchMatches": "No matches found",
|
||||
|
||||
"errors.genericReload": "Something went wrong. Try reloading the page.",
|
||||
|
||||
"tag.name.BADGE": "Badge prizes",
|
||||
"tag.name.SPECIAL": "Special rules",
|
||||
|
||||
@@ -15,5 +15,10 @@
|
||||
"MODE_SHORT_SZ": "SZ",
|
||||
"MODE_SHORT_TC": "TC",
|
||||
"MODE_SHORT_RM": "RM",
|
||||
"MODE_SHORT_CB": "CB"
|
||||
"MODE_SHORT_CB": "CB",
|
||||
"MODE_LONG_TW": "Turf War",
|
||||
"MODE_LONG_SZ": "Splat Zones",
|
||||
"MODE_LONG_TC": "Tower Control",
|
||||
"MODE_LONG_RM": "Rainmaker",
|
||||
"MODE_LONG_CB": "Clam Blitz"
|
||||
}
|
||||
|
||||
103
public/locales/ru/analyzer.json
Normal file
103
public/locales/ru/analyzer.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"attribute.weight": "Весовая категория:",
|
||||
"attribute.weight.Fast": "Лёгкая",
|
||||
"attribute.weight.Slow": "Тяжелая",
|
||||
"attribute.weight.Normal": "Средняя",
|
||||
"stat.category.main": "Основное оружие",
|
||||
"stat.category.sub": "Запасное оружие",
|
||||
"stat.category.special": "Особое оружие",
|
||||
"stat.category.subDef": "Защита от запасного оружия",
|
||||
"stat.category.actionsPerInkTank": "Действия за полный балон краски",
|
||||
"stat.category.damage": "Урон",
|
||||
"stat.category.movement": "Передвижение",
|
||||
"stat.category.misc": "Прочее",
|
||||
"stat.canopyHp": "Прочность зонта",
|
||||
"stat.fullChargeSeconds": "Время до полного заряда",
|
||||
"stat.maxChargeHoldSeconds": "Максимальное время удержания заряда в чернилах",
|
||||
"stat.specialPoints": "Очки для использования",
|
||||
"stat.specialLost": "Потеря заряда спешала после плюха",
|
||||
"stat.whiteInk": "Время до начала восстановления краски после использования",
|
||||
"stat.squidFormInkRecoverySeconds": "Время до полного восстановления баллона (кальмар)",
|
||||
"stat.quickRespawnTime": "Быстрое время воскрешения",
|
||||
"stat.superJumpTimeGround": "Период уязвимости суперпрыжка (в кадрах)",
|
||||
"stat.superJumpTimeTotal": "Суммарное время суперпрыжка",
|
||||
"stat.jumpShotSpread": "Разброс выстрелов в градусах (в прыжке)",
|
||||
"stat.groundShotSpread": "Разброс выстрелов в градусах (на земле)",
|
||||
"stat.squidSurgeChargeFrames": "Заряд Кальмарного Рывка (в кадрах)",
|
||||
"stat.swimSpeed": "Скорость плавания (единицы расстояния за кадр)",
|
||||
"stat.runSpeed": "Скорость бега (единицы расстояния за кадр)",
|
||||
"stat.runSpeedInEnemyInk": "Скорость бега во вражеских чернилах",
|
||||
"stat.shootingRunSpeed": "Скорость бега во время стрельбы",
|
||||
"stat.shootingRunSpeedCharging": "Скорость бега во время заряда",
|
||||
"stat.shootingRunSpeedFullCharge": "Скорость бега с полным зарядом",
|
||||
"stat.framesBeforeTakingDamageInEnemyInk": "Время до получения урона от нахождения во вражеских чернилах (в кадрах)",
|
||||
"stat.damageTakenInEnemyInkPerSecond": "Урон от нахождения во вражеских чернилах в секунду",
|
||||
"stat.enemyInkDamageLimit": "Максимальный урон от нахождения во вражеских чернилах",
|
||||
"stat.markedTime": "Время отслеживания: {{weapon}}",
|
||||
"stat.movementReduction": "Замедление передвижения: {{weapon}}",
|
||||
"stat.damage": "Урон: {{weapon}}",
|
||||
"stat.bombHdamage": "Тяжёлый урон от бомб",
|
||||
"stat.bombLdamage": "Лёгкий урон от бомб",
|
||||
"stat.consumption.NORMAL": "Выстрелы",
|
||||
"stat.consumption.SWING": "Взмахи",
|
||||
"stat.consumption.SLOSH": "Выплёскивания",
|
||||
"stat.consumption.VERTICAL_SWING": "Вертикальные взмахи",
|
||||
"stat.consumption.HORIZONTAL_SWING": "Горизонтальные взмахи",
|
||||
"stat.consumption.TAP_SHOT": "Тап шоты",
|
||||
"stat.consumption.FULL_CHARGE": "Полностью заряженные выстрелы",
|
||||
"stat.consumption.SPLATLING_CHARGE": "Полные заряды",
|
||||
"stat.consumption.SHIELD_LAUNCH": "Запуски щита",
|
||||
"stat.consumption.DUALIE_ROLL": "Перекаты",
|
||||
"stat.sub.velocity": "Скорость (определяет дальнобойность)",
|
||||
"stat.sub.firstPhaseDuration": "Длительность первой фазы",
|
||||
"stat.sub.secondPhaseDuration": "Длительность второй фазы",
|
||||
"stat.sub.markingTimeInSeconds": "Длительность маркировки",
|
||||
"stat.sub.markingRadius": "Радиус маркировки",
|
||||
"stat.sub.explosionRadius": "Радиус взрыва",
|
||||
"stat.sub.hp": "Прочность",
|
||||
"stat.sub.qsjBoost": "Ускорение суперпрыжка",
|
||||
"stat.special.duration": "Продолжительность: {{weapon}}",
|
||||
"stat.special.duration.inkStormExplanation": "Увеличивает прокрашиваемое расстояние, но не сам прокрас.",
|
||||
"stat.special.damageDistance": "Расстояние урона: {{weapon}}",
|
||||
"stat.special.paintRadius": "Радиус покраса: {{weapon}}",
|
||||
"stat.special.shieldHp": "Прочность щита: {{weapon}}",
|
||||
"stat.special.deviceHp": "Прочность устройства: {{weapon}}",
|
||||
"stat.special.inkConsumptionHook": "Потребление чернил тросом: {{weapon}}",
|
||||
"stat.special.inkConsumptionPerSecond": "Потребление чернил в секунду: {{weapon}}",
|
||||
"stat.special.reticleRadius": "Радиус прицела: {{weapon}}",
|
||||
"stat.special.throwDistance": "Расстояние броска: {{weapon}}",
|
||||
"stat.special.autoChargeRate": "Скорость автоматического заряда: {{weapon}}",
|
||||
"stat.special.maxRadius": "Максимальный радиус волны: {{weapon}}",
|
||||
"stat.special.maxRadius.explanation": "Скорость передвижения волны не меняется вместе с её радиусом.",
|
||||
"stat.special.radiusRange": "Радиус тяги: {{weapon}}",
|
||||
"stat.special.powerUpDuration": "Длительность эффекта напитка: {{weapon}}",
|
||||
"damage.header.type": "Тип",
|
||||
"damage.header.damage": "Урон",
|
||||
"damage.header.distance": "Дальнобойность",
|
||||
"damage.toSplat": "{{count}} выстрел(ов) для плюха",
|
||||
"damage.NORMAL_MIN": "Минимум",
|
||||
"damage.NORMAL_MAX": "Максимум",
|
||||
"damage.DIRECT": "Прямое попадание",
|
||||
"damage.FULL_CHARGE": "Полностью заряженный выстрел",
|
||||
"damage.MAX_CHARGE": "Максимальный частичный заряд",
|
||||
"damage.TAP_SHOT": "Тап шот",
|
||||
"damage.DISTANCE": "Разрыв",
|
||||
"suffix.seconds": "сек",
|
||||
"suffix.hp": "hp",
|
||||
"suffix.specialPointsShort": "очк.",
|
||||
"base": "Изначально",
|
||||
"value": "Значение",
|
||||
"build": "Сборка",
|
||||
"patch": "Патч:",
|
||||
"abilityPoints": "Свойства (AP)",
|
||||
"abilityPoints.short": "AP",
|
||||
"consumptionExplanation": "Данная таблица показывает, сколько действий можно совершить после 0-{{maxSubsToUse}}кратного использования запасного оружия. С полным баллоном запасное оружие можно использовать {{maxSubsToUse}} раз(а) подряд.",
|
||||
"trackingSubDefExplanation": "В расчётах продолжительности маркировки Маркера Движения, Мины и Углострела используется противник с 0AP Стойкости Запаса.",
|
||||
"distanceInline": "Расстояние: {{value}}",
|
||||
"damageShort": "Урон",
|
||||
"hitsToDestroyLong": "Атак для уничтожения",
|
||||
"hitsToDestroyShort": "Атак",
|
||||
"labels.amountOf": "Количество: ",
|
||||
"labels.damageType": "Тип урона",
|
||||
"labels.weapon": "Оружие"
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
"participatedCount": "{{count}} команд приняло участие",
|
||||
"members": "Участники",
|
||||
"results": "Результаты",
|
||||
"createMapList": "Создать список карт",
|
||||
|
||||
"forms.dates": "Даты",
|
||||
"forms.bracketUrl": "URL сетки",
|
||||
@@ -17,6 +18,7 @@
|
||||
"forms.tags.info": "Тег \"Призовые значки\" добавляется автоматически, если это возможно",
|
||||
"forms.badges": "Призовые значки",
|
||||
"forms.badges.placeholder": "Выберите призовой значок",
|
||||
"forms.mapPool": "Список карт",
|
||||
|
||||
"forms.participantCount": "Количество участников",
|
||||
"forms.reportResultsHeader": "Указать результаты для {{eventName}}",
|
||||
|
||||
@@ -3,14 +3,23 @@
|
||||
"pages.badges": "Значки",
|
||||
"pages.plus": "Plus Server",
|
||||
"pages.contributors": "Помощники",
|
||||
"pages.s2": "Splatoon 2",
|
||||
"pages.calendar": "Календарь",
|
||||
"pages.faq": "FAQ",
|
||||
"pages.builds": "Сборки",
|
||||
"pages.analyzer": "Анализатор сборок",
|
||||
"pages.maps": "Списки карт",
|
||||
"pages.object-damage": "Калькулятор урона",
|
||||
|
||||
"header.profile": "Профиль",
|
||||
"header.logout": "Выйти",
|
||||
"header.login": "Войти",
|
||||
|
||||
"auth.errors.aborted": "Вход отменён",
|
||||
"auth.errors.failed": "Ошибка входа",
|
||||
"auth.errors.discordPermissions": "Для вашего профиля на sendou.ink странице нужет доступ к вашему имени, аватару и привязанным аккаунтам соц. сетей в Discord.",
|
||||
"auth.errors.unknown": "Вход через Discord не удался по неизвестной причине. Если это продолжается — обратитесь за помощью.",
|
||||
|
||||
"footer.github.subtitle": "Исходный код",
|
||||
"footer.twitter.subtitle": "Обновления",
|
||||
"footer.discord.subtitle": "Помощь и обратная связь",
|
||||
@@ -24,6 +33,14 @@
|
||||
"actions.add": "Добавить",
|
||||
"actions.remove": "Убрать",
|
||||
"actions.delete": "Удалить",
|
||||
"actions.loadMore": "Загрузить ещё",
|
||||
"actions.copyToClipboard": "Скопировать в буфер обмена",
|
||||
"actions.close": "Закрыть",
|
||||
|
||||
"maps.createMapList": "Создать список карт",
|
||||
"maps.halfSz": "50% Зон",
|
||||
"maps.mapPool": "Пул карт",
|
||||
"maps.tournamentMaplist": "Создать список карт для турнира (maps.iplabs.ink)",
|
||||
|
||||
"results": "Результаты",
|
||||
|
||||
@@ -39,5 +56,17 @@
|
||||
"tag.name.LOW": "Ограничение по скиллу",
|
||||
"tag.name.COUNT": "Лимит участников",
|
||||
"tag.name.LAN": "LAN",
|
||||
"tag.name.QUALIFIER": "Квалификационный"
|
||||
"tag.name.QUALIFIER": "Квалификационный",
|
||||
|
||||
"weapon.category.SHOOTERS": "Каплестрелы",
|
||||
"weapon.category.BLASTERS": "Бластеры",
|
||||
"weapon.category.ROLLERS": "Валики",
|
||||
"weapon.category.BRUSHES": "Кисти",
|
||||
"weapon.category.CHARGERS": "Нагнетатели",
|
||||
"weapon.category.SLOSHERS": "Ведроганы",
|
||||
"weapon.category.SPLATLINGS": "Брызгометы",
|
||||
"weapon.category.DUALIES": "Спуртометы",
|
||||
"weapon.category.BRELLAS": "Зонтганы",
|
||||
"weapon.category.STRINGERS": "Тетиваторы",
|
||||
"weapon.category.SPLATANAS": "Сплат-катаны"
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
"code": "Помощники, внесшие изменения в код",
|
||||
"lean": "Помощь с исследованием внутренностей Splatoon и создатель бота Lanista",
|
||||
"borzoic": "Создатель рисунка на главной странице, а также значков и иконок",
|
||||
"uberu": "Нарисовал Судокотика, держащего эмодзи-сердце"
|
||||
"uberu": "Нарисовал Судокотика, держащего эмодзи-сердце",
|
||||
"translation": "Перевод"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"websiteSubtitle": "Соревновательный Splatoon-хаб",
|
||||
"buildsGoTo": "Изучайте сборки всех оружий в Splatoon 3 от лучших (и не только) игроков",
|
||||
"calendarGoTo": "Посмотреть все прошлые и предстоящие события на странице календаря",
|
||||
"moreFeatures": "Больше возможностей",
|
||||
"plus.description": "Посмотреть историю голосования Plus Server (и не только)",
|
||||
"badges.description": "Список всех значков, которые можно заработать для своего профиля",
|
||||
"analyzer.description": "Узнайте, что на самом деле делают ваши сборки",
|
||||
"maps.description": "Сделайте свой список на основе выбранного пула карт",
|
||||
"recentWinners": "Недавние победители",
|
||||
"upcomingEvents": "Предстоящие события",
|
||||
"articleBy": "от {{author}}"
|
||||
|
||||
@@ -10,5 +10,10 @@
|
||||
"STAGE_8": "Академия «Лепота»",
|
||||
"STAGE_9": "Осетровые верфи",
|
||||
"STAGE_10": "«Горбуша-Маркет»",
|
||||
"STAGE_11": "Луна-парк «Язь»"
|
||||
"STAGE_11": "Луна-парк «Язь»",
|
||||
"MODE_SHORT_TW": "Терфы:",
|
||||
"MODE_SHORT_SZ": "Зоны:",
|
||||
"MODE_SHORT_TC": "Башни:",
|
||||
"MODE_SHORT_RM": "Карп:",
|
||||
"MODE_SHORT_CB": "Клэмы:"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
{
|
||||
"customUrl": "Пользовательский URL",
|
||||
"ign": "Внутриигровое имя",
|
||||
"ign.short": "Ник",
|
||||
"country": "Страна",
|
||||
"bio": "О себе",
|
||||
"stickSens": "Чувствительность стика",
|
||||
"motionSens": "Чувствительность наклона",
|
||||
"motion": "Наклон",
|
||||
"stick": "Стик",
|
||||
"sens": "Чувствительность",
|
||||
|
||||
"results.title": "Результаты",
|
||||
"results.placing": "Место",
|
||||
"results.team": "Команда",
|
||||
"results.tournament": "Турнир",
|
||||
"results.participants": "Участники",
|
||||
"results.date": "Дата",
|
||||
"results.mates": "Напарники"
|
||||
"results.mates": "Напарники",
|
||||
"results.highlights": "Избранное",
|
||||
"results.nonHighlights": "Другие результаты",
|
||||
"results.highlights.choose": "Выберите избранное",
|
||||
"results.highlights.explanation": "Выберите ваш избранный результат",
|
||||
|
||||
"forms.errors.invalidCustomUrl.numbers": "Пользовательский URL не может содержать только цифры",
|
||||
"forms.errors.invalidCustomUrl.strangeCharacter": "Пользовательский URL не может содержать особые символы",
|
||||
"forms.errors.invalidCustomUrl.duplicate": "Кто-то уже использует этот пользовательский URL",
|
||||
"forms.errors.invalidSens": "Чувствительность наклона не может быть указана, если не указана чувствительность стика"
|
||||
}
|
||||
|
||||
@@ -172,6 +172,10 @@ function parametersToMainWeaponResult(
|
||||
DamageParam_ValueDirect,
|
||||
BlastParam_DistanceDamage: params["BlastParam"]?.["DistanceDamage"],
|
||||
DamageParam_ValueFullCharge: params["DamageParam"]?.["ValueFullCharge"],
|
||||
DamageParam_ValueFullChargeMax:
|
||||
params["DamageParam"]?.["ValueFullChargeMax"] !== DamageParam_ValueMax()
|
||||
? params["DamageParam"]?.["ValueFullChargeMax"]
|
||||
: undefined,
|
||||
DamageParam_ValueMaxCharge: params["DamageParam"]?.["ValueMaxCharge"],
|
||||
DamageParam_ValueMinCharge: params["DamageParam"]?.["ValueMinCharge"],
|
||||
CanopyHP: params["spl__BulletShelterCanopyParam"]?.["CanopyHP"],
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
|
||||
### 🟡 analyzer.json
|
||||
|
||||
**94/105**
|
||||
**94/107**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- objCalcAd
|
||||
- stat.specialLostSplattedByRP
|
||||
- stat.quickRespawnTimeSplattedByRP
|
||||
- damage.NORMAL_MAX_FULL_CHARGE
|
||||
- distanceInline
|
||||
- damageShort
|
||||
- hitsToDestroyLong
|
||||
@@ -37,12 +39,26 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**60/61**
|
||||
**60/75**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- pages.object-damage-calculator
|
||||
- actions.loading
|
||||
- actions.clear
|
||||
- actions.selectAll
|
||||
- actions.search
|
||||
- maps.template
|
||||
- maps.template.none
|
||||
- maps.template.event
|
||||
- maps.template.presets
|
||||
- maps.template.yourRecentEvents
|
||||
- maps.template.preset.ANARCHY
|
||||
- maps.template.preset.ALL
|
||||
- maps.template.preset.onlyMode
|
||||
- forms.errors.noSearchMatches
|
||||
- errors.genericReload
|
||||
|
||||
</details>
|
||||
|
||||
@@ -65,9 +81,20 @@
|
||||
|
||||
</details>
|
||||
|
||||
### 🟢 game-misc.json
|
||||
### 🟡 game-misc.json
|
||||
|
||||
**17/17**
|
||||
**17/22**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- MODE_LONG_TW
|
||||
- MODE_LONG_SZ
|
||||
- MODE_LONG_TC
|
||||
- MODE_LONG_RM
|
||||
- MODE_LONG_CB
|
||||
|
||||
</details>
|
||||
|
||||
### 🟢 user.json
|
||||
|
||||
@@ -79,13 +106,15 @@
|
||||
|
||||
### 🟡 analyzer.json
|
||||
|
||||
**94/105**
|
||||
**94/107**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- objCalcAd
|
||||
- stat.specialLostSplattedByRP
|
||||
- stat.quickRespawnTimeSplattedByRP
|
||||
- damage.NORMAL_MAX_FULL_CHARGE
|
||||
- distanceInline
|
||||
- damageShort
|
||||
- hitsToDestroyLong
|
||||
@@ -112,7 +141,7 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**59/61**
|
||||
**73/75**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -143,7 +172,7 @@
|
||||
|
||||
### 🟢 game-misc.json
|
||||
|
||||
**17/17**
|
||||
**22/22**
|
||||
|
||||
### 🟢 user.json
|
||||
|
||||
@@ -155,16 +184,18 @@
|
||||
|
||||
### 🟡 analyzer.json
|
||||
|
||||
**91/105**
|
||||
**91/107**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- objCalcAd
|
||||
- stat.specialLostSplattedByRP
|
||||
- stat.quickRespawnTimeSplattedByRP
|
||||
- stat.shootingRunSpeed
|
||||
- stat.shootingRunSpeedCharging
|
||||
- stat.shootingRunSpeedFullCharge
|
||||
- damage.NORMAL_MAX_FULL_CHARGE
|
||||
- distanceInline
|
||||
- damageShort
|
||||
- hitsToDestroyLong
|
||||
@@ -199,7 +230,7 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**48/61**
|
||||
**48/75**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -213,10 +244,24 @@
|
||||
- auth.errors.unknown
|
||||
- actions.copyToClipboard
|
||||
- actions.close
|
||||
- actions.loading
|
||||
- actions.clear
|
||||
- actions.selectAll
|
||||
- actions.search
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
- maps.tournamentMaplist
|
||||
- maps.template
|
||||
- maps.template.none
|
||||
- maps.template.event
|
||||
- maps.template.presets
|
||||
- maps.template.yourRecentEvents
|
||||
- maps.template.preset.ANARCHY
|
||||
- maps.template.preset.ALL
|
||||
- maps.template.preset.onlyMode
|
||||
- forms.errors.noSearchMatches
|
||||
- errors.genericReload
|
||||
|
||||
</details>
|
||||
|
||||
@@ -242,7 +287,7 @@
|
||||
|
||||
### 🟡 game-misc.json
|
||||
|
||||
**12/17**
|
||||
**12/22**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -252,6 +297,11 @@
|
||||
- MODE_SHORT_TC
|
||||
- MODE_SHORT_RM
|
||||
- MODE_SHORT_CB
|
||||
- MODE_LONG_TW
|
||||
- MODE_LONG_SZ
|
||||
- MODE_LONG_TC
|
||||
- MODE_LONG_RM
|
||||
- MODE_LONG_CB
|
||||
|
||||
</details>
|
||||
|
||||
@@ -289,16 +339,18 @@
|
||||
|
||||
### 🟡 analyzer.json
|
||||
|
||||
**91/105**
|
||||
**91/107**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- objCalcAd
|
||||
- stat.specialLostSplattedByRP
|
||||
- stat.quickRespawnTimeSplattedByRP
|
||||
- stat.shootingRunSpeed
|
||||
- stat.shootingRunSpeedCharging
|
||||
- stat.shootingRunSpeedFullCharge
|
||||
- damage.NORMAL_MAX_FULL_CHARGE
|
||||
- distanceInline
|
||||
- damageShort
|
||||
- hitsToDestroyLong
|
||||
@@ -333,7 +385,7 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**46/61**
|
||||
**46/75**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -349,10 +401,24 @@
|
||||
- actions.loadMore
|
||||
- actions.copyToClipboard
|
||||
- actions.close
|
||||
- actions.loading
|
||||
- actions.clear
|
||||
- actions.selectAll
|
||||
- actions.search
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
- maps.tournamentMaplist
|
||||
- maps.template
|
||||
- maps.template.none
|
||||
- maps.template.event
|
||||
- maps.template.presets
|
||||
- maps.template.yourRecentEvents
|
||||
- maps.template.preset.ANARCHY
|
||||
- maps.template.preset.ALL
|
||||
- maps.template.preset.onlyMode
|
||||
- forms.errors.noSearchMatches
|
||||
- errors.genericReload
|
||||
|
||||
</details>
|
||||
|
||||
@@ -388,7 +454,7 @@
|
||||
|
||||
### 🟡 game-misc.json
|
||||
|
||||
**12/17**
|
||||
**12/22**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -398,6 +464,11 @@
|
||||
- MODE_SHORT_TC
|
||||
- MODE_SHORT_RM
|
||||
- MODE_SHORT_CB
|
||||
- MODE_LONG_TW
|
||||
- MODE_LONG_SZ
|
||||
- MODE_LONG_TC
|
||||
- MODE_LONG_RM
|
||||
- MODE_LONG_CB
|
||||
|
||||
</details>
|
||||
|
||||
@@ -435,7 +506,7 @@
|
||||
|
||||
### 🔴 analyzer.json
|
||||
|
||||
**0/105**
|
||||
**0/107**
|
||||
|
||||
### 🔴 badges.json
|
||||
|
||||
@@ -451,7 +522,7 @@
|
||||
|
||||
### 🔴 common.json
|
||||
|
||||
**0/61**
|
||||
**0/75**
|
||||
|
||||
### 🔴 contributions.json
|
||||
|
||||
@@ -467,7 +538,7 @@
|
||||
|
||||
### 🟡 game-misc.json
|
||||
|
||||
**12/17**
|
||||
**12/22**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -477,6 +548,11 @@
|
||||
- MODE_SHORT_TC
|
||||
- MODE_SHORT_RM
|
||||
- MODE_SHORT_CB
|
||||
- MODE_LONG_TW
|
||||
- MODE_LONG_SZ
|
||||
- MODE_LONG_TC
|
||||
- MODE_LONG_RM
|
||||
- MODE_LONG_CB
|
||||
|
||||
</details>
|
||||
|
||||
@@ -494,7 +570,7 @@
|
||||
|
||||
### 🔴 analyzer.json
|
||||
|
||||
**0/105**
|
||||
**0/107**
|
||||
|
||||
### 🟢 badges.json
|
||||
|
||||
@@ -518,7 +594,7 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**35/61**
|
||||
**35/75**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -534,10 +610,24 @@
|
||||
- actions.loadMore
|
||||
- actions.copyToClipboard
|
||||
- actions.close
|
||||
- actions.loading
|
||||
- actions.clear
|
||||
- actions.selectAll
|
||||
- actions.search
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
- maps.tournamentMaplist
|
||||
- maps.template
|
||||
- maps.template.none
|
||||
- maps.template.event
|
||||
- maps.template.presets
|
||||
- maps.template.yourRecentEvents
|
||||
- maps.template.preset.ANARCHY
|
||||
- maps.template.preset.ALL
|
||||
- maps.template.preset.onlyMode
|
||||
- forms.errors.noSearchMatches
|
||||
- errors.genericReload
|
||||
- weapon.category.SHOOTERS
|
||||
- weapon.category.BLASTERS
|
||||
- weapon.category.ROLLERS
|
||||
@@ -583,7 +673,7 @@
|
||||
|
||||
### 🟡 game-misc.json
|
||||
|
||||
**12/17**
|
||||
**12/22**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -593,6 +683,11 @@
|
||||
- MODE_SHORT_TC
|
||||
- MODE_SHORT_RM
|
||||
- MODE_SHORT_CB
|
||||
- MODE_LONG_TW
|
||||
- MODE_LONG_SZ
|
||||
- MODE_LONG_TC
|
||||
- MODE_LONG_RM
|
||||
- MODE_LONG_CB
|
||||
|
||||
</details>
|
||||
|
||||
@@ -630,13 +725,15 @@
|
||||
|
||||
### 🟡 analyzer.json
|
||||
|
||||
**94/105**
|
||||
**94/107**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- objCalcAd
|
||||
- stat.specialLostSplattedByRP
|
||||
- stat.quickRespawnTimeSplattedByRP
|
||||
- damage.NORMAL_MAX_FULL_CHARGE
|
||||
- distanceInline
|
||||
- damageShort
|
||||
- hitsToDestroyLong
|
||||
@@ -671,7 +768,7 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**48/61**
|
||||
**48/75**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -685,10 +782,24 @@
|
||||
- auth.errors.unknown
|
||||
- actions.copyToClipboard
|
||||
- actions.close
|
||||
- actions.loading
|
||||
- actions.clear
|
||||
- actions.selectAll
|
||||
- actions.search
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
- maps.tournamentMaplist
|
||||
- maps.template
|
||||
- maps.template.none
|
||||
- maps.template.event
|
||||
- maps.template.presets
|
||||
- maps.template.yourRecentEvents
|
||||
- maps.template.preset.ANARCHY
|
||||
- maps.template.preset.ALL
|
||||
- maps.template.preset.onlyMode
|
||||
- forms.errors.noSearchMatches
|
||||
- errors.genericReload
|
||||
|
||||
</details>
|
||||
|
||||
@@ -714,7 +825,7 @@
|
||||
|
||||
### 🟡 game-misc.json
|
||||
|
||||
**12/17**
|
||||
**12/22**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -724,6 +835,11 @@
|
||||
- MODE_SHORT_TC
|
||||
- MODE_SHORT_RM
|
||||
- MODE_SHORT_CB
|
||||
- MODE_LONG_TW
|
||||
- MODE_LONG_SZ
|
||||
- MODE_LONG_TC
|
||||
- MODE_LONG_RM
|
||||
- MODE_LONG_CB
|
||||
|
||||
</details>
|
||||
|
||||
@@ -747,9 +863,21 @@
|
||||
|
||||
## /ru (🟡 In progress)
|
||||
|
||||
### 🔴 analyzer.json
|
||||
### 🟡 analyzer.json
|
||||
|
||||
**0/105**
|
||||
**101/107**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- objCalcAd
|
||||
- stat.specialLostSplattedByRP
|
||||
- stat.quickRespawnTimeSplattedByRP
|
||||
- damage.NORMAL_MAX_FULL_CHARGE
|
||||
- dmgHtdExplanation
|
||||
- noDmgData
|
||||
|
||||
</details>
|
||||
|
||||
### 🟢 badges.json
|
||||
|
||||
@@ -759,64 +887,38 @@
|
||||
|
||||
**11/11**
|
||||
|
||||
### 🟡 calendar.json
|
||||
### 🟢 calendar.json
|
||||
|
||||
**44/46**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- createMapList
|
||||
- forms.mapPool
|
||||
|
||||
</details>
|
||||
**46/46**
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**35/61**
|
||||
**60/75**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- pages.s2
|
||||
- pages.analyzer
|
||||
- pages.maps
|
||||
- pages.object-damage-calculator
|
||||
- auth.errors.aborted
|
||||
- auth.errors.failed
|
||||
- auth.errors.discordPermissions
|
||||
- auth.errors.unknown
|
||||
- actions.loadMore
|
||||
- actions.copyToClipboard
|
||||
- actions.close
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
- maps.tournamentMaplist
|
||||
- weapon.category.SHOOTERS
|
||||
- weapon.category.BLASTERS
|
||||
- weapon.category.ROLLERS
|
||||
- weapon.category.BRUSHES
|
||||
- weapon.category.CHARGERS
|
||||
- weapon.category.SLOSHERS
|
||||
- weapon.category.SPLATLINGS
|
||||
- weapon.category.DUALIES
|
||||
- weapon.category.BRELLAS
|
||||
- weapon.category.STRINGERS
|
||||
- weapon.category.SPLATANAS
|
||||
- actions.loading
|
||||
- actions.clear
|
||||
- actions.selectAll
|
||||
- actions.search
|
||||
- maps.template
|
||||
- maps.template.none
|
||||
- maps.template.event
|
||||
- maps.template.presets
|
||||
- maps.template.yourRecentEvents
|
||||
- maps.template.preset.ANARCHY
|
||||
- maps.template.preset.ALL
|
||||
- maps.template.preset.onlyMode
|
||||
- forms.errors.noSearchMatches
|
||||
- errors.genericReload
|
||||
|
||||
</details>
|
||||
|
||||
### 🟡 contributions.json
|
||||
### 🟢 contributions.json
|
||||
|
||||
**5/6**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- translation
|
||||
|
||||
</details>
|
||||
**6/6**
|
||||
|
||||
### 🟢 faq.json
|
||||
|
||||
@@ -824,60 +926,33 @@
|
||||
|
||||
### 🟡 front.json
|
||||
|
||||
**8/12**
|
||||
**11/12**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- buildsGoTo
|
||||
- analyzer.description
|
||||
- maps.description
|
||||
- object-damage-calculator.description
|
||||
|
||||
</details>
|
||||
|
||||
### 🟡 game-misc.json
|
||||
|
||||
**12/17**
|
||||
**17/22**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- MODE_SHORT_TW
|
||||
- MODE_SHORT_SZ
|
||||
- MODE_SHORT_TC
|
||||
- MODE_SHORT_RM
|
||||
- MODE_SHORT_CB
|
||||
- MODE_LONG_TW
|
||||
- MODE_LONG_SZ
|
||||
- MODE_LONG_TC
|
||||
- MODE_LONG_RM
|
||||
- MODE_LONG_CB
|
||||
|
||||
</details>
|
||||
|
||||
### 🟡 user.json
|
||||
### 🟢 user.json
|
||||
|
||||
**7/25**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- customUrl
|
||||
- ign
|
||||
- ign.short
|
||||
- stickSens
|
||||
- motionSens
|
||||
- motion
|
||||
- stick
|
||||
- sens
|
||||
- results.title
|
||||
- results.participants
|
||||
- results.highlights
|
||||
- results.nonHighlights
|
||||
- results.highlights.choose
|
||||
- results.highlights.explanation
|
||||
- forms.errors.invalidCustomUrl.numbers
|
||||
- forms.errors.invalidCustomUrl.strangeCharacter
|
||||
- forms.errors.invalidCustomUrl.duplicate
|
||||
- forms.errors.invalidSens
|
||||
|
||||
</details>
|
||||
**25/25**
|
||||
|
||||
---
|
||||
|
||||
@@ -885,13 +960,15 @@
|
||||
|
||||
### 🟡 analyzer.json
|
||||
|
||||
**94/105**
|
||||
**94/107**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- objCalcAd
|
||||
- stat.specialLostSplattedByRP
|
||||
- stat.quickRespawnTimeSplattedByRP
|
||||
- damage.NORMAL_MAX_FULL_CHARGE
|
||||
- distanceInline
|
||||
- damageShort
|
||||
- hitsToDestroyLong
|
||||
@@ -926,7 +1003,7 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**35/61**
|
||||
**35/75**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -942,10 +1019,24 @@
|
||||
- actions.loadMore
|
||||
- actions.copyToClipboard
|
||||
- actions.close
|
||||
- actions.loading
|
||||
- actions.clear
|
||||
- actions.selectAll
|
||||
- actions.search
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
- maps.tournamentMaplist
|
||||
- maps.template
|
||||
- maps.template.none
|
||||
- maps.template.event
|
||||
- maps.template.presets
|
||||
- maps.template.yourRecentEvents
|
||||
- maps.template.preset.ANARCHY
|
||||
- maps.template.preset.ALL
|
||||
- maps.template.preset.onlyMode
|
||||
- forms.errors.noSearchMatches
|
||||
- errors.genericReload
|
||||
- weapon.category.SHOOTERS
|
||||
- weapon.category.BLASTERS
|
||||
- weapon.category.ROLLERS
|
||||
@@ -991,7 +1082,7 @@
|
||||
|
||||
### 🟡 game-misc.json
|
||||
|
||||
**12/17**
|
||||
**12/22**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -1001,6 +1092,11 @@
|
||||
- MODE_SHORT_TC
|
||||
- MODE_SHORT_RM
|
||||
- MODE_SHORT_CB
|
||||
- MODE_LONG_TW
|
||||
- MODE_LONG_SZ
|
||||
- MODE_LONG_TC
|
||||
- MODE_LONG_RM
|
||||
- MODE_LONG_CB
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user