mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-11 21:56:11 -05:00
Merge branch 'Sendouc:rewrite' into rewrite
This commit is contained in:
@@ -30,6 +30,7 @@ module.exports = {
|
||||
"@typescript-eslint/no-unsafe-argument": 0,
|
||||
"@typescript-eslint/no-non-null-assertion": 0,
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"react/prop-types": 0,
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
|
||||
58
app/components/Breadcrumbs.tsx
Normal file
58
app/components/Breadcrumbs.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Link, useMatches } from "@remix-run/react";
|
||||
import { useMemo, Fragment } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isDefined } from "~/utils/arrays";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
type Crumb = {
|
||||
path: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function useBreadcrumbs(): Crumb[] {
|
||||
const matches = useMatches();
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
matches
|
||||
.map((match) => {
|
||||
const handle = match.handle as undefined | SendouRouteHandle;
|
||||
const name = handle?.breadcrumb?.({ match, t });
|
||||
return name ? { path: match.pathname, name } : undefined;
|
||||
})
|
||||
.filter(isDefined),
|
||||
[matches, t]
|
||||
);
|
||||
}
|
||||
|
||||
export function Breadcrumbs() {
|
||||
const breadcrumbs = useBreadcrumbs();
|
||||
|
||||
const showBreadcrumbs = breadcrumbs.length > 0;
|
||||
|
||||
if (!showBreadcrumbs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="breadcrumbs">
|
||||
{breadcrumbs.map((crumb, i) => {
|
||||
const isLast = i === breadcrumbs.length - 1;
|
||||
|
||||
if (isLast) {
|
||||
return <div key={crumb.path}>{crumb.name}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={crumb.path}>
|
||||
<div>
|
||||
<Link to={crumb.path}>{crumb.name}</Link>
|
||||
</div>
|
||||
<div>/</div>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ type LabelProps = Pick<
|
||||
};
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
};
|
||||
|
||||
export function Label({
|
||||
@@ -21,10 +22,11 @@ export function Label({
|
||||
children,
|
||||
htmlFor,
|
||||
className,
|
||||
labelClassName,
|
||||
}: LabelProps) {
|
||||
return (
|
||||
<div className={clsx("label__container", className)}>
|
||||
<label htmlFor={htmlFor}>
|
||||
<label htmlFor={htmlFor} className={labelClassName}>
|
||||
{children} {required && <span className="text-error">*</span>}
|
||||
</label>
|
||||
{valueLimits ? (
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import { Link, useSearchParams } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useUser } from "~/modules/auth";
|
||||
import { LOG_IN_URL, LOG_OUT_URL, userPage } from "~/utils/urls";
|
||||
import { Avatar } from "../Avatar";
|
||||
import { Button } from "../Button";
|
||||
import { Dialog } from "../Dialog";
|
||||
import { DiscordIcon } from "../icons/Discord";
|
||||
import { LogOutIcon } from "../icons/LogOut";
|
||||
import { UserIcon } from "../icons/User";
|
||||
@@ -12,8 +13,9 @@ import { Popover } from "../Popover";
|
||||
export function UserItem() {
|
||||
const { t } = useTranslation();
|
||||
const user = useUser();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
if (user)
|
||||
if (user) {
|
||||
return (
|
||||
<Popover
|
||||
buttonChildren={
|
||||
@@ -45,16 +47,56 @@ export function UserItem() {
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const authError = searchParams.get("authError");
|
||||
const closeAuthErrorDialog = () => {
|
||||
const newSearchParams = new URLSearchParams(searchParams);
|
||||
newSearchParams.delete("authError");
|
||||
setSearchParams(newSearchParams);
|
||||
};
|
||||
|
||||
return (
|
||||
<form action={LOG_IN_URL} method="post" data-cy="log-in-form">
|
||||
<button
|
||||
type="submit"
|
||||
className="layout__log-in-button"
|
||||
data-cy="log-in-button"
|
||||
>
|
||||
<DiscordIcon /> {t("header.login")}
|
||||
</button>
|
||||
</form>
|
||||
<>
|
||||
<form action={LOG_IN_URL} method="post" data-cy="log-in-form">
|
||||
<button
|
||||
type="submit"
|
||||
className="layout__log-in-button"
|
||||
data-cy="log-in-button"
|
||||
>
|
||||
<DiscordIcon /> {t("header.login")}
|
||||
</button>
|
||||
</form>
|
||||
{authError != null && (
|
||||
<Dialog isOpen close={closeAuthErrorDialog}>
|
||||
<div className="stack md">
|
||||
<AuthenticationErrorHelp errorCode={authError} />
|
||||
<Button onClick={closeAuthErrorDialog}>{t("actions.close")}</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthenticationErrorHelp({ errorCode }: { errorCode: string }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
switch (errorCode) {
|
||||
case "aborted":
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-lg text-center">{t("auth.errors.aborted")}</h2>
|
||||
{t("auth.errors.discordPermissions")}
|
||||
</>
|
||||
);
|
||||
case "unknown":
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-lg text-center">{t("auth.errors.failed")}</h2>
|
||||
{t("auth.errors.unknown")}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Link, useLocation } from "@remix-run/react";
|
||||
import { Link, useMatches } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { RootLoaderData } from "~/root";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
import { LOGO_PATH, navIconUrl } from "~/utils/urls";
|
||||
import { Image } from "../Image";
|
||||
import { ColorModeToggle } from "./ColorModeToggle";
|
||||
@@ -12,6 +13,25 @@ import { Menu } from "./Menu";
|
||||
import navItems from "./nav-items.json";
|
||||
import { UserItem } from "./UserItem";
|
||||
|
||||
function useActiveNavItem() {
|
||||
const matches = useMatches();
|
||||
|
||||
return React.useMemo(() => {
|
||||
let activeItem: { name: string; url: string } | undefined = undefined;
|
||||
|
||||
for (const match of matches.reverse()) {
|
||||
const handle = match.handle as SendouRouteHandle | undefined;
|
||||
|
||||
if (handle?.navItemName) {
|
||||
activeItem = navItems.find(({ name }) => name === handle.navItemName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return activeItem;
|
||||
}, [matches]);
|
||||
}
|
||||
|
||||
export const Layout = React.memo(function Layout({
|
||||
children,
|
||||
patrons,
|
||||
@@ -22,12 +42,8 @@ export const Layout = React.memo(function Layout({
|
||||
isCatchBoundary?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const [menuOpen, setMenuOpen] = React.useState(false);
|
||||
|
||||
const currentPagesNavItem = navItems.find((navItem) =>
|
||||
location.pathname.includes(navItem.name)
|
||||
);
|
||||
const activeNavItem = useActiveNavItem();
|
||||
|
||||
return (
|
||||
<div className="layout__container">
|
||||
@@ -51,15 +67,15 @@ export const Layout = React.memo(function Layout({
|
||||
</div>
|
||||
</header>
|
||||
<Menu expanded={menuOpen} closeMenu={() => setMenuOpen(false)} />
|
||||
{currentPagesNavItem && (
|
||||
{activeNavItem && (
|
||||
<div className="layout__page-title-header">
|
||||
<Image
|
||||
path={navIconUrl(currentPagesNavItem.name)}
|
||||
path={navIconUrl(activeNavItem.name)}
|
||||
width={28}
|
||||
height={28}
|
||||
alt=""
|
||||
/>
|
||||
{t(`pages.${currentPagesNavItem.name}` as any)}
|
||||
{t(`pages.${activeNavItem.name}` as any)}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
|
||||
@@ -12,7 +12,16 @@ select
|
||||
"CalendarEventDate"
|
||||
where
|
||||
"eventId" = "CalendarEvent"."id"
|
||||
) as "startTime"
|
||||
) as "startTime",
|
||||
exists (
|
||||
select
|
||||
1
|
||||
from
|
||||
"UserResultHighlight"
|
||||
where
|
||||
"userId" = @userId and
|
||||
"teamId" = "CalendarEventResultTeam"."id"
|
||||
) as "isHighlight"
|
||||
from
|
||||
"CalendarEventResultPlayer"
|
||||
join "CalendarEventResultTeam" on "CalendarEventResultTeam"."id" = "CalendarEventResultPlayer"."teamId"
|
||||
|
||||
@@ -244,9 +244,11 @@ export function findResultsByUserId(userId: User["id"]) {
|
||||
placement: CalendarEventResultTeam["placement"];
|
||||
participantCount: CalendarEvent["participantCount"];
|
||||
startTime: CalendarEventDate["startTime"];
|
||||
isHighlight: number;
|
||||
}>
|
||||
).map((row) => ({
|
||||
...row,
|
||||
isHighlight: Boolean(row.isHighlight),
|
||||
mates: (
|
||||
findMatesByResultTeamIdStm.all({
|
||||
teamId: row.teamId,
|
||||
|
||||
10
app/db/models/users/addResultHighlight.sql
Normal file
10
app/db/models/users/addResultHighlight.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
insert into
|
||||
"UserResultHighlight" (
|
||||
"userId",
|
||||
"teamId"
|
||||
)
|
||||
values
|
||||
(
|
||||
@userId,
|
||||
@teamId
|
||||
)
|
||||
4
app/db/models/users/deleteAllResultHighlights.sql
Normal file
4
app/db/models/users/deleteAllResultHighlights.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
delete from
|
||||
"UserResultHighlight"
|
||||
where
|
||||
"userId" = @userId
|
||||
@@ -1,5 +1,9 @@
|
||||
import { sql } from "../../sql";
|
||||
import type { User, UserWithPlusTier } from "../../types";
|
||||
import type {
|
||||
CalendarEventResultTeam,
|
||||
User,
|
||||
UserWithPlusTier,
|
||||
} from "../../types";
|
||||
|
||||
import upsertSql from "./upsert.sql";
|
||||
import updateProfileSql from "./updateProfile.sql";
|
||||
@@ -12,6 +16,8 @@ import updateDiscordIdSql from "./updateDiscordId.sql";
|
||||
import findByIdentifierSql from "./findByIdentifier.sql";
|
||||
import findAllPlusMembersSql from "./findAllPlusMembers.sql";
|
||||
import findAllPatronsSql from "./findAllPatrons.sql";
|
||||
import addResultHighlightSql from "./addResultHighlight.sql";
|
||||
import deleteAllResultHighlightsSql from "./deleteAllResultHighlights.sql";
|
||||
|
||||
const upsertStm = sql.prepare(upsertSql);
|
||||
export function upsert(
|
||||
@@ -123,3 +129,18 @@ export type FindAllPatrons = Array<
|
||||
export function findAllPatrons() {
|
||||
return findAllPatronsStm.all() as FindAllPatrons;
|
||||
}
|
||||
|
||||
const deleteAllResultHighlightsStm = sql.prepare(deleteAllResultHighlightsSql);
|
||||
const addResultHighlightStm = sql.prepare(addResultHighlightSql);
|
||||
export type UpdateResultHighlightsArgs = {
|
||||
userId: User["id"];
|
||||
resultTeamIds: Array<CalendarEventResultTeam["id"]>;
|
||||
};
|
||||
export const updateResultHighlights = sql.transaction(
|
||||
({ userId, resultTeamIds }: UpdateResultHighlightsArgs) => {
|
||||
deleteAllResultHighlightsStm.run({ userId });
|
||||
for (const teamId of resultTeamIds) {
|
||||
addResultHighlightStm.run({ userId, teamId });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,2 +1,49 @@
|
||||
import type { DamageType } from "./types";
|
||||
|
||||
export const MAX_LDE_INTENSITY = 21;
|
||||
export const MAX_AP = 57;
|
||||
|
||||
export const DAMAGE_RECEIVERS = [
|
||||
"Bomb_TorpedoBullet", // Torpedo
|
||||
"Chariot", // Crab Tank
|
||||
"Gachihoko_Barrier", // Rainmaker Shield
|
||||
"GreatBarrier_Barrier", // Big Bubbler Shield
|
||||
"GreatBarrier_WeakPoint", // Big Bubbler Weak Point
|
||||
// "InkRail", // InkRail
|
||||
"NiceBall_Armor", // Booyah Bomb Armor
|
||||
"ShockSonar", // Wave Breaker
|
||||
// "Sponge_Versus", // Sponge
|
||||
"Wsb_Flag", // Squid Beakon
|
||||
"Wsb_Shield", // Splash Wall
|
||||
"Wsb_Sprinkler", // Sprinkler
|
||||
"BulletUmbrellaCanopyCompact", // Undercover Brella Canopy
|
||||
"BulletUmbrellaCanopyNormal", // Splat Brella Canopy
|
||||
"BulletUmbrellaCanopyWide", // Tenta Brella Canopy
|
||||
] as const;
|
||||
|
||||
export const DAMAGE_TYPE = [
|
||||
"NORMAL_MIN",
|
||||
"NORMAL_MAX",
|
||||
"DIRECT",
|
||||
"FULL_CHARGE",
|
||||
"MAX_CHARGE",
|
||||
"TAP_SHOT",
|
||||
"DISTANCE",
|
||||
"BOMB_NORMAL",
|
||||
"BOMB_DIRECT",
|
||||
] as const;
|
||||
|
||||
export const damageTypeToWeaponType: Record<
|
||||
DamageType,
|
||||
"MAIN" | "SUB" | "SPECIAL"
|
||||
> = {
|
||||
NORMAL_MIN: "MAIN",
|
||||
NORMAL_MAX: "MAIN",
|
||||
DIRECT: "MAIN",
|
||||
FULL_CHARGE: "MAIN",
|
||||
MAX_CHARGE: "MAIN",
|
||||
TAP_SHOT: "MAIN",
|
||||
DISTANCE: "MAIN",
|
||||
BOMB_NORMAL: "SUB",
|
||||
BOMB_DIRECT: "SUB",
|
||||
};
|
||||
|
||||
@@ -5,10 +5,17 @@ export type {
|
||||
Stat,
|
||||
AnalyzedBuild,
|
||||
SpecialEffectType,
|
||||
HitPoints,
|
||||
DamageReceiver,
|
||||
DamageType,
|
||||
} from "./types";
|
||||
|
||||
export { useAnalyzeBuild } from "./useAnalyzeBuild";
|
||||
|
||||
export { MAX_LDE_INTENSITY } from "./constants";
|
||||
export { useObjectDamage } from "./useObjectDamage";
|
||||
|
||||
export { MAX_LDE_INTENSITY, DAMAGE_RECEIVERS, DAMAGE_TYPE } from "./constants";
|
||||
|
||||
export { lastDitchEffortIntensityToAp } from "./specialEffects";
|
||||
|
||||
export { possibleApValues } from "./utils";
|
||||
|
||||
5579
app/modules/analyzer/object-dmg.json
Normal file
5579
app/modules/analyzer/object-dmg.json
Normal file
File diff suppressed because it is too large
Load Diff
174
app/modules/analyzer/objectDamage.ts
Normal file
174
app/modules/analyzer/objectDamage.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import type {
|
||||
AbilityPoints,
|
||||
AnalyzedBuild,
|
||||
DamageReceiver,
|
||||
DamageType,
|
||||
} from "./types";
|
||||
import objectDamages from "./object-dmg.json";
|
||||
import type {
|
||||
MainWeaponId,
|
||||
SpecialWeaponId,
|
||||
SubWeaponId,
|
||||
} from "../in-game-lists";
|
||||
import { damageTypeToWeaponType, DAMAGE_RECEIVERS } from "./constants";
|
||||
import { roundToNDecimalPlaces } from "~/utils/number";
|
||||
import { objectHitPoints } from "./objectHitPoints";
|
||||
|
||||
/** Keys to check in the json. Lower index takes priority over higher. If key is omitted means any key with valid weapon id is okay. One json key can only map to one DamageType. */
|
||||
const objectDamageJsonKeyPriority: Partial<
|
||||
Record<DamageType, Array<keyof typeof objectDamages>>
|
||||
> = {
|
||||
// NORMAL_MIN: [],
|
||||
// NORMAL_MAX: [],
|
||||
DIRECT: ["Blaster_KillOneShot"],
|
||||
FULL_CHARGE: [],
|
||||
MAX_CHARGE: [],
|
||||
TAP_SHOT: [],
|
||||
// DISTANCE: [],
|
||||
// BOMB_NORMAL: [],
|
||||
BOMB_DIRECT: ["Bomb_DirectHit"],
|
||||
};
|
||||
|
||||
const commonObjectDamageJsonKeys = () =>
|
||||
Object.keys(objectDamages).filter(
|
||||
(key) =>
|
||||
!Object.values(objectDamageJsonKeyPriority)
|
||||
.flat()
|
||||
.includes(key as any)
|
||||
) as Array<keyof typeof objectDamages>;
|
||||
|
||||
export function damageTypeToMultipliers({
|
||||
type,
|
||||
weapon,
|
||||
}: {
|
||||
type: DamageType;
|
||||
weapon:
|
||||
| {
|
||||
type: "MAIN";
|
||||
id: MainWeaponId;
|
||||
}
|
||||
| {
|
||||
type: "SUB";
|
||||
id: SubWeaponId;
|
||||
}
|
||||
| {
|
||||
type: "SPECIAL";
|
||||
id: SpecialWeaponId;
|
||||
};
|
||||
}) {
|
||||
const keysToCheck =
|
||||
objectDamageJsonKeyPriority[type] ?? commonObjectDamageJsonKeys();
|
||||
|
||||
for (const key of keysToCheck) {
|
||||
const objectDamagesObj = objectDamages[key];
|
||||
|
||||
let ok = false;
|
||||
|
||||
if (weapon.type === "MAIN") {
|
||||
ok = (objectDamagesObj.mainWeaponIds as MainWeaponId[]).includes(
|
||||
weapon.id
|
||||
);
|
||||
} else if (weapon.type === "SUB") {
|
||||
ok = (objectDamagesObj.subWeaponIds as SubWeaponId[]).includes(weapon.id);
|
||||
} else if (weapon.type === "SPECIAL") {
|
||||
ok = (objectDamagesObj.specialWeaponIds as SpecialWeaponId[]).includes(
|
||||
weapon.id
|
||||
);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
console.log(`for ${type} used ${key ?? "FALLBACK"}`);
|
||||
return objectDamagesObj.rates;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function multipliersToRecordWithFallbacks(
|
||||
multipliers: ReturnType<typeof damageTypeToMultipliers>
|
||||
) {
|
||||
return Object.fromEntries(
|
||||
DAMAGE_RECEIVERS.map((receiver) => [
|
||||
receiver,
|
||||
multipliers?.find((m) => m.target === receiver)?.rate ?? 1,
|
||||
])
|
||||
) as Record<DamageReceiver, number>;
|
||||
}
|
||||
|
||||
const objectShredderMultipliers = objectDamages.ObjectEffect_Up.rates;
|
||||
export function calculateDamage({
|
||||
analyzed,
|
||||
mainWeaponId,
|
||||
abilityPoints,
|
||||
damageType,
|
||||
}: {
|
||||
analyzed: AnalyzedBuild;
|
||||
mainWeaponId: MainWeaponId;
|
||||
abilityPoints: AbilityPoints;
|
||||
damageType: DamageType;
|
||||
}) {
|
||||
const filteredDamages = analyzed.stats.damages.filter(
|
||||
(d) => d.type === damageType
|
||||
);
|
||||
|
||||
const hitPoints = objectHitPoints(abilityPoints);
|
||||
const multipliers = Object.fromEntries(
|
||||
filteredDamages.map((damage) => {
|
||||
const weaponType = damageTypeToWeaponType[damage.type];
|
||||
const weaponId: any =
|
||||
weaponType === "MAIN"
|
||||
? mainWeaponId
|
||||
: weaponType === "SUB"
|
||||
? analyzed.weapon.subWeaponSplId
|
||||
: analyzed.weapon.specialWeaponSplId;
|
||||
|
||||
return [
|
||||
damage.type,
|
||||
multipliersToRecordWithFallbacks(
|
||||
damageTypeToMultipliers({
|
||||
type: damage.type,
|
||||
weapon: { type: weaponType, id: weaponId },
|
||||
})
|
||||
),
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
return DAMAGE_RECEIVERS.map((receiver) => {
|
||||
const damageReceiverHp = hitPoints[receiver];
|
||||
|
||||
return {
|
||||
receiver,
|
||||
hitPoints: damageReceiverHp,
|
||||
damages: filteredDamages
|
||||
.flatMap((damage) => [
|
||||
{ ...damage, objectShredder: false },
|
||||
{ ...damage, objectShredder: true },
|
||||
])
|
||||
.map((damage) => {
|
||||
const baseMultiplier = multipliers[damage.type]![receiver];
|
||||
const objectShredderMultiplier =
|
||||
objectShredderMultipliers.find((m) => m.target === receiver)
|
||||
?.rate ?? 1;
|
||||
const multiplier =
|
||||
baseMultiplier *
|
||||
(damage.objectShredder ? objectShredderMultiplier : 1);
|
||||
|
||||
const damagePerHit = roundToNDecimalPlaces(damage.value * multiplier);
|
||||
|
||||
const hitsToDestroy = Math.ceil(damageReceiverHp / damagePerHit);
|
||||
|
||||
return {
|
||||
value: damagePerHit,
|
||||
hitsToDestroy,
|
||||
multiplier: roundToNDecimalPlaces(multiplier, 2),
|
||||
type: damage.type,
|
||||
id: `${damage.id}-${String(damage.objectShredder)}`,
|
||||
distance: damage.distance,
|
||||
objectShredder: damage.objectShredder,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
62
app/modules/analyzer/objectHitPoints.ts
Normal file
62
app/modules/analyzer/objectHitPoints.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import invariant from "tiny-invariant";
|
||||
import { BIG_BUBBLER_ID, CRAB_TANK_ID, SPLASH_WALL_ID } from "../in-game-lists";
|
||||
import { specialDeviceHp, specialFieldHp, subStats } from "./stats";
|
||||
import type {
|
||||
AbilityPoints,
|
||||
HitPoints,
|
||||
SpecialWeaponParams,
|
||||
SubWeaponParams,
|
||||
} from "./types";
|
||||
import { hpDivided } from "./utils";
|
||||
import weaponParams from "./weapon-params.json";
|
||||
|
||||
const WAVE_BREAKER_HP = 400;
|
||||
const SPRINKER_HP = 100;
|
||||
const RAINMAKER_HP = 1000;
|
||||
const SPLAT_BRELLA_SHIELD_HP = 500;
|
||||
const BOOYAH_BOMB_ARMOR_HP = 470;
|
||||
const BEAKON_HP = 120;
|
||||
const TORPEDO_HP = 20;
|
||||
|
||||
export const objectHitPoints = (abilityPoints: AbilityPoints): HitPoints => {
|
||||
const Wsb_Shield = subStats({
|
||||
abilityPoints,
|
||||
subWeaponParams: weaponParams.subWeapons[SPLASH_WALL_ID] as SubWeaponParams,
|
||||
}).subHp?.value;
|
||||
const GreatBarrier_Barrier = specialFieldHp({
|
||||
abilityPoints,
|
||||
specialWeaponParams: weaponParams.specialWeapons[
|
||||
BIG_BUBBLER_ID
|
||||
] as SpecialWeaponParams,
|
||||
})?.value;
|
||||
const GreatBarrier_WeakPoint = specialDeviceHp({
|
||||
abilityPoints,
|
||||
specialWeaponParams: weaponParams.specialWeapons[
|
||||
BIG_BUBBLER_ID
|
||||
] as SpecialWeaponParams,
|
||||
})?.value;
|
||||
|
||||
invariant(Wsb_Shield);
|
||||
invariant(GreatBarrier_Barrier);
|
||||
invariant(GreatBarrier_WeakPoint);
|
||||
|
||||
return {
|
||||
BulletUmbrellaCanopyNormal: SPLAT_BRELLA_SHIELD_HP,
|
||||
BulletUmbrellaCanopyWide: hpDivided(
|
||||
weaponParams.mainWeapons[6010].CanopyHP
|
||||
),
|
||||
BulletUmbrellaCanopyCompact: hpDivided(
|
||||
weaponParams.mainWeapons[6020].CanopyHP
|
||||
),
|
||||
Wsb_Shield,
|
||||
Bomb_TorpedoBullet: TORPEDO_HP,
|
||||
Chariot: hpDivided(weaponParams.specialWeapons[CRAB_TANK_ID].ArmorHP),
|
||||
Gachihoko_Barrier: RAINMAKER_HP,
|
||||
GreatBarrier_Barrier,
|
||||
GreatBarrier_WeakPoint,
|
||||
NiceBall_Armor: BOOYAH_BOMB_ARMOR_HP, // ??
|
||||
ShockSonar: WAVE_BREAKER_HP,
|
||||
Wsb_Flag: BEAKON_HP,
|
||||
Wsb_Sprinkler: SPRINKER_HP,
|
||||
};
|
||||
};
|
||||
@@ -10,27 +10,28 @@ import type {
|
||||
StatFunctionInput,
|
||||
SubWeaponParams,
|
||||
} from "./types";
|
||||
import { DAMAGE_TYPE } from "./types";
|
||||
import { DAMAGE_TYPE } from "./constants";
|
||||
import { INK_CONSUME_TYPES } from "./types";
|
||||
import invariant from "tiny-invariant";
|
||||
import {
|
||||
abilityPointsToEffects,
|
||||
apFromMap,
|
||||
hasEffect,
|
||||
hpDivided,
|
||||
weaponParams,
|
||||
} from "./utils";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { semiRandomId } from "~/utils/strings";
|
||||
import { roundToTwoDecimalPlaces } from "~/utils/number";
|
||||
import { roundToNDecimalPlaces } from "~/utils/number";
|
||||
|
||||
export function buildStats({
|
||||
abilityPoints,
|
||||
weaponSplId,
|
||||
mainOnlyAbilities,
|
||||
abilityPoints = new Map(),
|
||||
mainOnlyAbilities = [],
|
||||
}: {
|
||||
abilityPoints: AbilityPoints;
|
||||
weaponSplId: MainWeaponId;
|
||||
mainOnlyAbilities: Array<Ability>;
|
||||
abilityPoints?: AbilityPoints;
|
||||
mainOnlyAbilities?: Array<Ability>;
|
||||
}): AnalyzedBuild {
|
||||
const mainWeaponParams = weaponParams().mainWeapons[weaponSplId];
|
||||
invariant(mainWeaponParams, `Weapon with splId ${weaponSplId} not found`);
|
||||
@@ -641,8 +642,8 @@ function shotSpreadAir(
|
||||
const reducedExtraSpread = extraSpread * (1 - effect);
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(jumpSpread),
|
||||
value: roundToTwoDecimalPlaces(reducedExtraSpread + groundSpread),
|
||||
baseValue: roundToNDecimalPlaces(jumpSpread),
|
||||
value: roundToNDecimalPlaces(reducedExtraSpread + groundSpread),
|
||||
modifiedBy: SHOT_SPREAD_AIR_ABILITY,
|
||||
};
|
||||
}
|
||||
@@ -765,7 +766,9 @@ const SUB_WEAPON_STATS = [
|
||||
},
|
||||
{ analyzedBuildKey: "subHp", abilityValuesKey: "MaxHP", type: "HP" },
|
||||
] as const;
|
||||
function subStats(args: StatFunctionInput) {
|
||||
export function subStats(
|
||||
args: Pick<StatFunctionInput, "subWeaponParams" | "abilityPoints">
|
||||
) {
|
||||
const result: Partial<AnalyzedBuild["stats"]> = {};
|
||||
const SUB_STATS_KEY = "BRU";
|
||||
|
||||
@@ -785,9 +788,9 @@ function subStats(args: StatFunctionInput) {
|
||||
const toValue = (effect: number) => {
|
||||
switch (type) {
|
||||
case "NO_CHANGE":
|
||||
return roundToTwoDecimalPlaces(effect);
|
||||
return roundToNDecimalPlaces(effect);
|
||||
case "HP":
|
||||
return roundToTwoDecimalPlaces(effect / 10);
|
||||
return roundToNDecimalPlaces(hpDivided(effect), 1);
|
||||
case "TIME":
|
||||
return framesToSeconds(effect);
|
||||
default:
|
||||
@@ -903,8 +906,8 @@ function subDefToxicMistMovementReduction(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect * 100),
|
||||
value: roundToTwoDecimalPlaces(effect * 100),
|
||||
baseValue: roundToNDecimalPlaces(baseEffect * 100),
|
||||
value: roundToNDecimalPlaces(effect * 100),
|
||||
modifiedBy: SUB_DEF_TOXIC_MIST_MOVEMENT_REDUCTION_KEY,
|
||||
};
|
||||
}
|
||||
@@ -927,10 +930,11 @@ function subDefAngleShooterDamage(
|
||||
invariant(angleShooterDirectDamage);
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(
|
||||
(angleShooterDirectDamage * baseEffect) / 10
|
||||
baseValue: roundToNDecimalPlaces(
|
||||
(angleShooterDirectDamage * baseEffect) / 10,
|
||||
1
|
||||
),
|
||||
value: roundToTwoDecimalPlaces((angleShooterDirectDamage * effect) / 10),
|
||||
value: roundToNDecimalPlaces((angleShooterDirectDamage * effect) / 10, 1),
|
||||
modifiedBy: SUB_DEF_ANGLE_SHOOTER_DAMAGE_KEY,
|
||||
};
|
||||
}
|
||||
@@ -949,8 +953,8 @@ function subDefSplashWallDamagePercentage(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect * 100),
|
||||
value: roundToTwoDecimalPlaces(effect * 100),
|
||||
baseValue: roundToNDecimalPlaces(baseEffect * 100),
|
||||
value: roundToNDecimalPlaces(effect * 100),
|
||||
modifiedBy: SUB_DEF_SPLASH_WALL_DAMAGE_PERCENTAGE_KEY,
|
||||
};
|
||||
}
|
||||
@@ -969,8 +973,8 @@ function subDefSprinklerDamagePercentage(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect * 100),
|
||||
value: roundToTwoDecimalPlaces(effect * 100),
|
||||
baseValue: roundToNDecimalPlaces(baseEffect * 100),
|
||||
value: roundToNDecimalPlaces(effect * 100),
|
||||
modifiedBy: SUB_DEF_SPRINKLER_DAMAGE_PERCENTAGE_KEY,
|
||||
};
|
||||
}
|
||||
@@ -989,8 +993,8 @@ function subDefBombDamageLightPercentage(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect * 100),
|
||||
value: roundToTwoDecimalPlaces(effect * 100),
|
||||
baseValue: roundToNDecimalPlaces(baseEffect * 100),
|
||||
value: roundToNDecimalPlaces(effect * 100),
|
||||
modifiedBy: SUB_DEF_BOMB_DAMAGE_LIGHT_PERCENTAGE_KEY,
|
||||
};
|
||||
}
|
||||
@@ -1009,8 +1013,8 @@ function subDefBombDamageHeavyPercentage(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect * 100),
|
||||
value: roundToTwoDecimalPlaces(effect * 100),
|
||||
baseValue: roundToNDecimalPlaces(baseEffect * 100),
|
||||
value: roundToNDecimalPlaces(effect * 100),
|
||||
modifiedBy: SUB_DEF_BOMB_DAMAGE_HEAVY_PERCENTAGE_KEY,
|
||||
};
|
||||
}
|
||||
@@ -1096,8 +1100,8 @@ function specialDamageDistance(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect),
|
||||
value: roundToTwoDecimalPlaces(effect),
|
||||
baseValue: roundToNDecimalPlaces(baseEffect),
|
||||
value: roundToNDecimalPlaces(effect),
|
||||
modifiedBy: SPECIAL_DAMAGE_DISTANCE_KEY,
|
||||
};
|
||||
}
|
||||
@@ -1120,14 +1124,14 @@ function specialPaintRadius(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect),
|
||||
value: roundToTwoDecimalPlaces(effect),
|
||||
baseValue: roundToNDecimalPlaces(baseEffect),
|
||||
value: roundToNDecimalPlaces(effect),
|
||||
modifiedBy: SPECIAL_PAINT_RADIUS_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
function specialFieldHp(
|
||||
args: StatFunctionInput
|
||||
export function specialFieldHp(
|
||||
args: Pick<StatFunctionInput, "specialWeaponParams" | "abilityPoints">
|
||||
): AnalyzedBuild["stats"]["specialFieldHp"] {
|
||||
if (!hasEffect({ key: "MaxFieldHP", weapon: args.specialWeaponParams })) {
|
||||
return;
|
||||
@@ -1150,8 +1154,8 @@ function specialFieldHp(
|
||||
};
|
||||
}
|
||||
|
||||
function specialDeviceHp(
|
||||
args: StatFunctionInput
|
||||
export function specialDeviceHp(
|
||||
args: Pick<StatFunctionInput, "specialWeaponParams" | "abilityPoints">
|
||||
): AnalyzedBuild["stats"]["specialDeviceHp"] {
|
||||
if (!hasEffect({ key: "MaxHP", weapon: args.specialWeaponParams })) {
|
||||
return;
|
||||
@@ -1168,8 +1172,8 @@ function specialDeviceHp(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: Math.round(baseEffect / 10),
|
||||
value: Math.round(effect / 10),
|
||||
baseValue: Math.round(hpDivided(baseEffect)),
|
||||
value: Math.round(hpDivided(effect)),
|
||||
modifiedBy: SPECIAL_DEVICE_HP_KEY,
|
||||
};
|
||||
}
|
||||
@@ -1197,8 +1201,8 @@ function specialHookInkConsumptionPercentage(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect * 100),
|
||||
value: roundToTwoDecimalPlaces(effect * 100),
|
||||
baseValue: roundToNDecimalPlaces(baseEffect * 100),
|
||||
value: roundToNDecimalPlaces(effect * 100),
|
||||
modifiedBy: SPECIAL_HOOK_INK_CONSUMPTION_PERCENTAGE_KEY,
|
||||
};
|
||||
}
|
||||
@@ -1226,8 +1230,8 @@ function specialInkConsumptionPerSecondPercentage(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect * 100),
|
||||
value: roundToTwoDecimalPlaces(effect * 100),
|
||||
baseValue: roundToNDecimalPlaces(baseEffect * 100),
|
||||
value: roundToNDecimalPlaces(effect * 100),
|
||||
modifiedBy: SPECIAL_INK_CONSUMPTION_PER_SECOND_PERCENTAGE_KEY,
|
||||
};
|
||||
}
|
||||
@@ -1255,8 +1259,8 @@ function specialReticleRadius(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect),
|
||||
value: roundToTwoDecimalPlaces(effect),
|
||||
baseValue: roundToNDecimalPlaces(baseEffect),
|
||||
value: roundToNDecimalPlaces(effect),
|
||||
modifiedBy: SPECIAL_RETICLE_RADIUS_KEY,
|
||||
};
|
||||
}
|
||||
@@ -1284,8 +1288,8 @@ function specialThrowDistance(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect),
|
||||
value: roundToTwoDecimalPlaces(effect),
|
||||
baseValue: roundToNDecimalPlaces(baseEffect),
|
||||
value: roundToNDecimalPlaces(effect),
|
||||
modifiedBy: SPECIAL_THROW_DISTANCE_KEY,
|
||||
};
|
||||
}
|
||||
@@ -1313,8 +1317,8 @@ function specialAutoChargeRate(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect * 100),
|
||||
value: roundToTwoDecimalPlaces(effect * 100),
|
||||
baseValue: roundToNDecimalPlaces(baseEffect * 100),
|
||||
value: roundToNDecimalPlaces(effect * 100),
|
||||
modifiedBy: SPECIAL_AUTO_CHARGE_RATE_KEY,
|
||||
};
|
||||
}
|
||||
@@ -1342,8 +1346,8 @@ function specialMaxRadius(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect),
|
||||
value: roundToTwoDecimalPlaces(effect),
|
||||
baseValue: roundToNDecimalPlaces(baseEffect),
|
||||
value: roundToNDecimalPlaces(effect),
|
||||
modifiedBy: SPECIAL_MAX_RADIUS_KEY,
|
||||
};
|
||||
}
|
||||
@@ -1384,12 +1388,12 @@ function specialRadiusRange(
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: `${roundToTwoDecimalPlaces(
|
||||
baseValue: `${roundToNDecimalPlaces(
|
||||
radiusMin.baseEffect
|
||||
)}-${roundToTwoDecimalPlaces(radiusMax.baseEffect)}`,
|
||||
value: `${roundToTwoDecimalPlaces(
|
||||
radiusMin.effect
|
||||
)}-${roundToTwoDecimalPlaces(radiusMax.effect)}`,
|
||||
)}-${roundToNDecimalPlaces(radiusMax.baseEffect)}`,
|
||||
value: `${roundToNDecimalPlaces(radiusMin.effect)}-${roundToNDecimalPlaces(
|
||||
radiusMax.effect
|
||||
)}`,
|
||||
modifiedBy: SPECIAL_RADIUS_RANGE_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
import type { SPECIAL_EFFECTS } from "./specialEffects";
|
||||
import type weaponParams from "./weapon-params.json";
|
||||
import type abilityValues from "./ability-values.json";
|
||||
import type { DAMAGE_RECEIVERS, DAMAGE_TYPE } from "./constants";
|
||||
|
||||
type Overwrites = Record<
|
||||
string,
|
||||
@@ -159,20 +160,12 @@ export interface FullInkTankOption {
|
||||
type: InkConsumeType;
|
||||
}
|
||||
|
||||
export const DAMAGE_TYPE = [
|
||||
"NORMAL_MIN",
|
||||
"NORMAL_MAX",
|
||||
"DIRECT",
|
||||
"FULL_CHARGE",
|
||||
"MAX_CHARGE",
|
||||
"TAP_SHOT",
|
||||
"DISTANCE",
|
||||
"BOMB_NORMAL",
|
||||
"BOMB_DIRECT",
|
||||
] as const;
|
||||
|
||||
export type DamageType = typeof DAMAGE_TYPE[number];
|
||||
|
||||
export type DamageReceiver = typeof DAMAGE_RECEIVERS[number];
|
||||
|
||||
export type HitPoints = Record<DamageReceiver, number>;
|
||||
|
||||
export interface Damage {
|
||||
value: number;
|
||||
type: DamageType;
|
||||
|
||||
@@ -3,10 +3,8 @@ import { EMPTY_BUILD } from "~/constants";
|
||||
import {
|
||||
type BuildAbilitiesTupleWithUnknown,
|
||||
type MainWeaponId,
|
||||
mainWeaponIds,
|
||||
abilities,
|
||||
isAbility,
|
||||
weaponCategories,
|
||||
} from "../in-game-lists";
|
||||
import type {
|
||||
Ability,
|
||||
@@ -17,7 +15,10 @@ import { MAX_LDE_INTENSITY } from "./constants";
|
||||
import { applySpecialEffects, SPECIAL_EFFECTS } from "./specialEffects";
|
||||
import { buildStats } from "./stats";
|
||||
import type { SpecialEffectType } from "./types";
|
||||
import { buildToAbilityPoints } from "./utils";
|
||||
import {
|
||||
buildToAbilityPoints,
|
||||
validatedWeaponIdFromSearchParams,
|
||||
} from "./utils";
|
||||
|
||||
const UNKNOWN_SHORT = "U";
|
||||
|
||||
@@ -88,20 +89,6 @@ function serializeBuild(build: BuildAbilitiesTupleWithUnknown) {
|
||||
.join(",");
|
||||
}
|
||||
|
||||
function validatedWeaponIdFromSearchParams(
|
||||
searchParams: URLSearchParams
|
||||
): MainWeaponId {
|
||||
const weaponId = searchParams.get("weapon")
|
||||
? Number(searchParams.get("weapon"))
|
||||
: null;
|
||||
|
||||
if (mainWeaponIds.includes(weaponId as any)) {
|
||||
return weaponId as MainWeaponId;
|
||||
}
|
||||
|
||||
return weaponCategories[0].weaponIds[0];
|
||||
}
|
||||
|
||||
function validatedBuildFromSearchParams(
|
||||
searchParams: URLSearchParams
|
||||
): BuildAbilitiesTupleWithUnknown {
|
||||
|
||||
105
app/modules/analyzer/useObjectDamage.ts
Normal file
105
app/modules/analyzer/useObjectDamage.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useSearchParams } from "@remix-run/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { type MainWeaponId } from "../in-game-lists";
|
||||
import { calculateDamage } from "./objectDamage";
|
||||
import { buildStats } from "./stats";
|
||||
import type { AnalyzedBuild, DamageType } from "./types";
|
||||
import { possibleApValues, validatedWeaponIdFromSearchParams } from "./utils";
|
||||
|
||||
const ABILITY_POINTS_SP_KEY = "ap";
|
||||
const DAMAGE_TYPE_SP_KEY = "dmg";
|
||||
|
||||
export function useObjectDamage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const mainWeaponId = validatedWeaponIdFromSearchParams(searchParams);
|
||||
const abilityPoints = validatedAbilityPointsFromSearchParams(searchParams);
|
||||
|
||||
const analyzed = buildStats({
|
||||
weaponSplId: mainWeaponId,
|
||||
});
|
||||
|
||||
const damageType = validatedDamageTypeFromSearchParams({
|
||||
searchParams,
|
||||
analyzed,
|
||||
});
|
||||
|
||||
const handleChange = ({
|
||||
newMainWeaponId = mainWeaponId,
|
||||
newAbilityPoints = abilityPoints,
|
||||
newDamageType = damageType,
|
||||
}: {
|
||||
newMainWeaponId?: MainWeaponId;
|
||||
newAbilityPoints?: number;
|
||||
newDamageType?: DamageType;
|
||||
}) => {
|
||||
setSearchParams(
|
||||
{
|
||||
weapon: String(newMainWeaponId),
|
||||
[ABILITY_POINTS_SP_KEY]: String(newAbilityPoints),
|
||||
[DAMAGE_TYPE_SP_KEY]: newDamageType,
|
||||
},
|
||||
{ replace: true, state: { scroll: false } }
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
mainWeaponId,
|
||||
subWeaponId: analyzed.weapon.subWeaponSplId,
|
||||
handleChange,
|
||||
damagesToReceivers: calculateDamage({
|
||||
abilityPoints: new Map([
|
||||
["BRU", { ap: abilityPoints, apBeforeTacticooler: abilityPoints }],
|
||||
["SPU", { ap: abilityPoints, apBeforeTacticooler: abilityPoints }],
|
||||
]),
|
||||
analyzed,
|
||||
mainWeaponId,
|
||||
damageType,
|
||||
}),
|
||||
abilityPoints: String(abilityPoints),
|
||||
damageType,
|
||||
allDamageTypes: Array.from(
|
||||
new Set(analyzed.stats.damages.map((d) => d.type))
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function validatedAbilityPointsFromSearchParams(searchParams: URLSearchParams) {
|
||||
const abilityPoints = Number(searchParams.get(ABILITY_POINTS_SP_KEY));
|
||||
|
||||
return (
|
||||
possibleApValues().find((possibleAp) => possibleAp === abilityPoints) ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
export const damageTypePriorityList = [
|
||||
"DIRECT",
|
||||
"FULL_CHARGE",
|
||||
"MAX_CHARGE",
|
||||
"NORMAL_MAX",
|
||||
"NORMAL_MIN",
|
||||
"TAP_SHOT",
|
||||
"DISTANCE",
|
||||
"BOMB_DIRECT",
|
||||
"BOMB_NORMAL",
|
||||
] as const;
|
||||
function validatedDamageTypeFromSearchParams({
|
||||
searchParams,
|
||||
analyzed,
|
||||
}: {
|
||||
searchParams: URLSearchParams;
|
||||
analyzed: AnalyzedBuild;
|
||||
}) {
|
||||
const damageType = searchParams.get(DAMAGE_TYPE_SP_KEY);
|
||||
|
||||
const found = analyzed.stats.damages.find((d) => d.type === damageType);
|
||||
|
||||
if (found) return found.type;
|
||||
|
||||
const fallbackFound = damageTypePriorityList.find((type) =>
|
||||
analyzed.stats.damages.some((d) => d.type === type)
|
||||
);
|
||||
invariant(fallbackFound);
|
||||
|
||||
return fallbackFound;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Ability, BuildAbilitiesTupleWithUnknown } from "../in-game-lists";
|
||||
import { mainWeaponIds, weaponCategories } from "../in-game-lists";
|
||||
import { abilities } from "../in-game-lists";
|
||||
import weaponParamsJson from "./weapon-params.json";
|
||||
import abilityValuesJson from "./ability-values.json";
|
||||
@@ -10,7 +11,7 @@ import type {
|
||||
SubWeaponParams,
|
||||
} from "./types";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { AbilityWithUnknown } from "../in-game-lists/types";
|
||||
import type { AbilityWithUnknown, MainWeaponId } from "../in-game-lists/types";
|
||||
|
||||
export function weaponParams(): ParamsJson {
|
||||
return weaponParamsJson as ParamsJson;
|
||||
@@ -150,3 +151,31 @@ export function hasEffect({
|
||||
|
||||
return high !== mid || mid !== low;
|
||||
}
|
||||
|
||||
export function validatedWeaponIdFromSearchParams(
|
||||
searchParams: URLSearchParams
|
||||
): MainWeaponId {
|
||||
const weaponId = searchParams.get("weapon")
|
||||
? Number(searchParams.get("weapon"))
|
||||
: null;
|
||||
|
||||
if (mainWeaponIds.includes(weaponId as any)) {
|
||||
return weaponId as MainWeaponId;
|
||||
}
|
||||
|
||||
return weaponCategories[0].weaponIds[0];
|
||||
}
|
||||
|
||||
export const hpDivided = (hp: number) => hp / 10;
|
||||
|
||||
export function possibleApValues() {
|
||||
const uniqueValues = new Set<number>();
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let j = 0; j < 10; j++) {
|
||||
uniqueValues.add(i * 10 + j * 3);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(uniqueValues).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
@@ -1246,6 +1246,7 @@
|
||||
}
|
||||
},
|
||||
"12": {
|
||||
"ArmorHP": 5000,
|
||||
"overwrites": {
|
||||
"SpecialDurationFrame": {
|
||||
"High": 660,
|
||||
|
||||
1
app/modules/auth/errors.ts
Normal file
1
app/modules/auth/errors.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type AuthErrorCode = "aborted" | "unknown";
|
||||
@@ -9,3 +9,5 @@ export {
|
||||
export { getUser, requireUser } from "./user.server";
|
||||
|
||||
export { useUser } from "./user";
|
||||
|
||||
export type { AuthErrorCode } from "./errors";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ActionFunction, LoaderFunction } from "@remix-run/node";
|
||||
import { redirect } from "@remix-run/node";
|
||||
import { canPerformAdminActions } from "~/permissions";
|
||||
import { ADMIN_PAGE } from "~/utils/urls";
|
||||
import { ADMIN_PAGE, authErrorUrl } from "~/utils/urls";
|
||||
import {
|
||||
authenticator,
|
||||
DISCORD_AUTH_KEY,
|
||||
@@ -11,11 +11,19 @@ import { authSessionStorage } from "./session.server";
|
||||
import { getUser } from "./user.server";
|
||||
|
||||
export const callbackLoader: LoaderFunction = async ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
if (url.searchParams.get("error") === "access_denied") {
|
||||
// The user denied the authentication request
|
||||
// This is part of the oauth2 protocol, but remix-auth-oauth2 doesn't do
|
||||
// nice error handling for this case.
|
||||
// https://www.oauth.com/oauth2-servers/server-side-apps/possible-errors/
|
||||
|
||||
return redirect(authErrorUrl("aborted"));
|
||||
}
|
||||
|
||||
await authenticator.authenticate(DISCORD_AUTH_KEY, request, {
|
||||
successRedirect: "/",
|
||||
// TODO: should include query param that displays an error banner explaining that log in went wrong
|
||||
// and where to get help for that
|
||||
failureRedirect: "/",
|
||||
failureRedirect: authErrorUrl("unknown"),
|
||||
});
|
||||
|
||||
throw new Response("Unknown authentication state", { status: 500 });
|
||||
|
||||
10
app/root.tsx
10
app/root.tsx
@@ -1,3 +1,4 @@
|
||||
import type { ErrorBoundaryComponent } from "@remix-run/node";
|
||||
import {
|
||||
json,
|
||||
type LinksFunction,
|
||||
@@ -32,6 +33,7 @@ import { Theme, ThemeHead, useTheme, ThemeProvider } from "./modules/theme";
|
||||
import { getThemeSession } from "./modules/theme/session.server";
|
||||
import { COMMON_PREVIEW_IMAGE } from "./utils/urls";
|
||||
import { ConditionalScrollRestoration } from "./components/ConditionalScrollRestoration";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
export const unstable_shouldReload: ShouldReloadFunction = ({ url }) => {
|
||||
// reload on language change so the selected language gets set into the cookie
|
||||
@@ -94,7 +96,7 @@ export const loader: LoaderFunction = async ({ request }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: "common",
|
||||
};
|
||||
|
||||
@@ -155,7 +157,9 @@ export function CatchBoundary() {
|
||||
);
|
||||
}
|
||||
|
||||
export function ErrorBoundary() {
|
||||
export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => {
|
||||
console.error(error);
|
||||
|
||||
return (
|
||||
<ThemeProvider specifiedTheme={Theme.DARK}>
|
||||
<Document>
|
||||
@@ -163,4 +167,4 @@ export function ErrorBoundary() {
|
||||
</Document>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,7 +18,11 @@ import { Main } from "~/components/Main";
|
||||
import { requireUser } from "~/modules/auth";
|
||||
import { getUser, isImpersonating } from "~/modules/auth/user.server";
|
||||
import { canPerformAdminActions } from "~/permissions";
|
||||
import { parseRequestFormData, validate } from "~/utils/remix";
|
||||
import {
|
||||
parseRequestFormData,
|
||||
type SendouRouteHandle,
|
||||
validate,
|
||||
} from "~/utils/remix";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { impersonateUrl, SEED_URL, STOP_IMPERSONATING_URL } from "~/utils/urls";
|
||||
import { db } from "~/db";
|
||||
@@ -69,6 +73,10 @@ export const loader: LoaderFunction = async ({ request }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
navItemName: "admin",
|
||||
};
|
||||
|
||||
export default function AdminPage() {
|
||||
return (
|
||||
<Main className="stack lg">
|
||||
|
||||
@@ -34,6 +34,8 @@ import {
|
||||
type SubWeaponId,
|
||||
} from "~/modules/in-game-lists";
|
||||
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";
|
||||
|
||||
@@ -49,8 +51,9 @@ export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons", "analyzer"],
|
||||
navItemName: "analyzer",
|
||||
};
|
||||
|
||||
export default function BuildAnalyzerPage() {
|
||||
@@ -824,9 +827,10 @@ function DamageTable({
|
||||
? `${val.value}+${val.value}+${val.value}`
|
||||
: val.value;
|
||||
|
||||
const typeRowName = val.type.startsWith("BOMB_")
|
||||
? t(`weapons:SUB_${subWeaponId}`)
|
||||
: t(`analyzer:damage.${val.type as "NORMAL_MIN"}`);
|
||||
const typeRowName = damageTypeTranslationString({
|
||||
damageType: val.type,
|
||||
subWeaponId,
|
||||
});
|
||||
|
||||
return (
|
||||
<tr key={val.id}>
|
||||
|
||||
@@ -9,6 +9,7 @@ import styles from "~/styles/badges.css";
|
||||
import { BORZOIC_TWITTER, FAQ_PAGE } from "~/utils/urls";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useAnimateListEntry } from "~/hooks/useAnimateListEntry";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
@@ -18,8 +19,9 @@ export interface BadgesLoaderData {
|
||||
badges: FindAll;
|
||||
}
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: "badges",
|
||||
navItemName: "badges",
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = () => {
|
||||
|
||||
@@ -1,50 +1,32 @@
|
||||
import { type LinksFunction } from "@remix-run/node";
|
||||
import { Link, Outlet, useMatches, useParams } from "@remix-run/react";
|
||||
import type * as React from "react";
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LinkButton } from "~/components/Button";
|
||||
import { Main } from "~/components/Main";
|
||||
import { useUser } from "~/modules/auth";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists";
|
||||
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
import styles from "~/styles/builds.css";
|
||||
import { atOrError } from "~/utils/arrays";
|
||||
import { BUILDS_PAGE, userNewBuildPage } from "~/utils/urls";
|
||||
import { userNewBuildPage } from "~/utils/urls";
|
||||
import { Breadcrumbs } from "~/components/Breadcrumbs";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons", "builds"],
|
||||
breadcrumb: ({ t }) => t("pages.builds"),
|
||||
navItemName: "builds",
|
||||
};
|
||||
|
||||
export default function BuildsLayoutPage() {
|
||||
const user = useUser();
|
||||
const matches = useMatches();
|
||||
const { t } = useTranslation(["weapons", "common", "builds"]);
|
||||
const params = useParams();
|
||||
|
||||
const weaponId: MainWeaponId | undefined = atOrError(matches, -1).data?.[
|
||||
"weaponId"
|
||||
];
|
||||
|
||||
return (
|
||||
<Main className="stack lg">
|
||||
<div className="builds__top-container">
|
||||
<nav className="builds__breadcrumbs">
|
||||
<SometimesLink isLink={Boolean(params["slug"])}>
|
||||
{t("common:pages.builds")}
|
||||
</SometimesLink>
|
||||
{typeof weaponId === "number" && (
|
||||
<>
|
||||
<div>/</div>
|
||||
<SometimesLink isLink={false}>
|
||||
{t(`weapons:MAIN_${weaponId}`)}
|
||||
</SometimesLink>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
<Breadcrumbs />
|
||||
{user && (
|
||||
<LinkButton to={userNewBuildPage(user)} tiny>
|
||||
{t("builds:addBuild")}
|
||||
@@ -55,17 +37,3 @@ export default function BuildsLayoutPage() {
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
|
||||
function SometimesLink({
|
||||
children,
|
||||
isLink,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
isLink: boolean;
|
||||
}) {
|
||||
if (isLink) {
|
||||
return <Link to={BUILDS_PAGE}>{children}</Link>;
|
||||
}
|
||||
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { BUILDS_PAGE_BATCH_SIZE, BUILDS_PAGE_MAX_BUILDS } from "~/constants";
|
||||
import { db } from "~/db";
|
||||
import { i18next } from "~/modules/i18n";
|
||||
import { weaponIdIsNotAlt } from "~/modules/in-game-lists";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { weaponNameSlugToId } from "~/utils/unslugify.server";
|
||||
|
||||
@@ -40,9 +41,12 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
BUILDS_PAGE_MAX_BUILDS
|
||||
);
|
||||
|
||||
const weaponName = t(`weapons:MAIN_${weaponId}`);
|
||||
|
||||
return {
|
||||
weaponId,
|
||||
title: makeTitle([t(`weapons:MAIN_${weaponId}`), t("common:pages.builds")]),
|
||||
weaponName,
|
||||
title: makeTitle([weaponName, t("common:pages.builds")]),
|
||||
builds: db.builds.buildsByWeaponId({
|
||||
weaponId,
|
||||
limit,
|
||||
@@ -51,6 +55,14 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
breadcrumb: ({ match }) => {
|
||||
const data = match.data as SerializeFrom<typeof loader> | null;
|
||||
|
||||
return data ? data.weaponName : "Unknown";
|
||||
},
|
||||
};
|
||||
|
||||
export default function WeaponsBuildsPage() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
const { t } = useTranslation(["common"]);
|
||||
|
||||
@@ -4,8 +4,9 @@ import { Image } from "~/components/Image";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists";
|
||||
import { weaponCategories, weaponIdIsNotAlt } from "~/modules/in-game-lists";
|
||||
import { mainWeaponImageUrl, mySlugify, weaponCategoryUrl } from "~/utils/urls";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: "weapons",
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
import calendarStyles from "~/styles/calendar-event.css";
|
||||
import mapsStyles from "~/styles/maps.css";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { notFoundIfFalsy } from "~/utils/remix";
|
||||
import { notFoundIfFalsy, type SendouRouteHandle } from "~/utils/remix";
|
||||
import { discordFullName, makeTitle } from "~/utils/strings";
|
||||
import {
|
||||
calendarEditPage,
|
||||
@@ -60,7 +60,7 @@ export const meta: MetaFunction = (args) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["calendar", "game-misc"],
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
notFoundIfFalsy,
|
||||
safeParseRequestFormData,
|
||||
validate,
|
||||
type SendouRouteHandle,
|
||||
} from "~/utils/remix";
|
||||
import { actualNumber, id, safeJSONParse, toArray } from "~/utils/zod";
|
||||
import * as React from "react";
|
||||
@@ -135,7 +136,7 @@ export const action: ActionFunction = async ({ request, params }) => {
|
||||
return redirect(calendarEventPage(parsedParams.id));
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: "calendar",
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { Unpacked } from "~/utils/types";
|
||||
import { calendarReportWinnersPage, resolveBaseUrl } from "~/utils/urls";
|
||||
import { actualNumber } from "~/utils/zod";
|
||||
import { Tags } from "./components/Tags";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
@@ -45,8 +46,9 @@ export const meta: MetaFunction = (args) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: "calendar",
|
||||
navItemName: "calendar",
|
||||
};
|
||||
|
||||
const loaderSearchParamsSchema = z.object({
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
badRequestIfFalsy,
|
||||
parseRequestFormData,
|
||||
validate,
|
||||
type SendouRouteHandle,
|
||||
} from "~/utils/remix";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { calendarEventPage } from "~/utils/urls";
|
||||
@@ -181,7 +182,7 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: "calendar",
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
SENDOU_TWITTER_URL,
|
||||
UBERU_TWITTER,
|
||||
} from "~/utils/urls";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return {
|
||||
@@ -18,7 +19,7 @@ export const meta: MetaFunction = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: "contributions",
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Main } from "~/components/Main";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
import styles from "~/styles/faq.css";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
const AMOUNT_OF_QUESTIONS = 3;
|
||||
|
||||
@@ -18,7 +19,7 @@ export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: "faq",
|
||||
};
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
userPage,
|
||||
} from "~/utils/urls";
|
||||
import { Tags } from "./calendar/components/Tags";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
const RECENT_ARTICLES_TO_SHOW = 3;
|
||||
|
||||
@@ -33,7 +34,7 @@ export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons", "builds"],
|
||||
};
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
modeImageUrl,
|
||||
stageImageUrl,
|
||||
} from "~/utils/urls";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
const AMOUNT_OF_MAPS_IN_MAP_LIST = stageIds.length * 2;
|
||||
|
||||
@@ -63,8 +64,9 @@ export const meta: MetaFunction = (args) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: "game-misc",
|
||||
navItemName: "maps",
|
||||
};
|
||||
|
||||
export const loader = async ({ request }: LoaderArgs) => {
|
||||
|
||||
259
app/routes/object-damage.tsx
Normal file
259
app/routes/object-damage.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import { WeaponCombobox } from "~/components/Combobox";
|
||||
import { Image } from "~/components/Image";
|
||||
import { Main } from "~/components/Main";
|
||||
import { possibleApValues, useObjectDamage } from "~/modules/analyzer";
|
||||
import {
|
||||
type MainWeaponId,
|
||||
BIG_BUBBLER_ID,
|
||||
BOOYAH_BOMB_ID,
|
||||
CRAB_TANK_ID,
|
||||
SPLASH_WALL_ID,
|
||||
SQUID_BEAKON_ID,
|
||||
TORPEDO_ID,
|
||||
WAVE_BREAKER_ID,
|
||||
SPRINKLER_ID,
|
||||
} from "~/modules/in-game-lists";
|
||||
import {
|
||||
mainWeaponImageUrl,
|
||||
modeImageUrl,
|
||||
specialWeaponImageUrl,
|
||||
subWeaponImageUrl,
|
||||
} from "~/utils/urls";
|
||||
import styles from "~/styles/object-damage.css";
|
||||
import type { LinksFunction } from "@remix-run/node";
|
||||
import type { SendouRouteHandle } from "~/utils/remix";
|
||||
import type { DamageReceiver, DamageType } from "~/modules/analyzer";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import clsx from "clsx";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Ability } from "~/components/Ability";
|
||||
import { damageTypeTranslationString } from "~/utils/i18next";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons", "analyzer"],
|
||||
};
|
||||
|
||||
export default function ObjectDamagePage() {
|
||||
const { t } = useTranslation(["analyzer"]);
|
||||
const {
|
||||
mainWeaponId,
|
||||
subWeaponId,
|
||||
handleChange,
|
||||
damagesToReceivers,
|
||||
abilityPoints,
|
||||
damageType,
|
||||
allDamageTypes,
|
||||
} = useObjectDamage();
|
||||
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
return <Main>WIP :)</Main>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Main className="stack lg">
|
||||
<div className="object-damage__controls">
|
||||
<div>
|
||||
<Label htmlFor="weapon">{t("analyzer:labels.weapon")}</Label>
|
||||
<WeaponCombobox
|
||||
id="weapon"
|
||||
inputName="weapon"
|
||||
initialWeaponId={mainWeaponId}
|
||||
onChange={(opt) =>
|
||||
opt &&
|
||||
handleChange({
|
||||
newMainWeaponId: Number(opt.value) as MainWeaponId,
|
||||
})
|
||||
}
|
||||
className="w-full-important"
|
||||
clearsInputOnFocus
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="damage">{t("analyzer:labels.damageType")}</Label>
|
||||
<DamageTypesSelect
|
||||
handleChange={handleChange}
|
||||
subWeaponId={subWeaponId}
|
||||
damageType={damageType}
|
||||
allDamageTypes={allDamageTypes}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="ap" labelClassName="object-damage__ap-label">
|
||||
{t("analyzer:labels.amountOf")}
|
||||
<Ability ability="BRU" size="TINY" />
|
||||
<Ability ability="SPU" size="TINY" />
|
||||
</Label>
|
||||
<select
|
||||
id="ap"
|
||||
value={abilityPoints}
|
||||
onChange={(e) =>
|
||||
handleChange({ newAbilityPoints: Number(e.target.value) })
|
||||
}
|
||||
>
|
||||
{possibleApValues().map((ap) => (
|
||||
<option key={ap} value={ap}>
|
||||
{ap}
|
||||
{t("analyzer:abilityPoints.short")}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<DamageReceiversGrid
|
||||
subWeaponId={subWeaponId}
|
||||
damagesToReceivers={damagesToReceivers}
|
||||
/>
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
|
||||
function DamageTypesSelect({
|
||||
allDamageTypes,
|
||||
handleChange,
|
||||
subWeaponId,
|
||||
damageType,
|
||||
}: Pick<
|
||||
ReturnType<typeof useObjectDamage>,
|
||||
"handleChange" | "subWeaponId" | "damageType" | "allDamageTypes"
|
||||
>) {
|
||||
const { t } = useTranslation(["analyzer"]);
|
||||
|
||||
return (
|
||||
<select
|
||||
id="damage"
|
||||
value={damageType}
|
||||
onChange={(e) =>
|
||||
handleChange({ newDamageType: e.target.value as DamageType })
|
||||
}
|
||||
>
|
||||
{allDamageTypes.map((damageType) => {
|
||||
return (
|
||||
<option key={damageType} value={damageType}>
|
||||
{t(
|
||||
damageTypeTranslationString({
|
||||
damageType,
|
||||
subWeaponId,
|
||||
})
|
||||
)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
const damageReceiverImages: Record<DamageReceiver, string> = {
|
||||
Bomb_TorpedoBullet: subWeaponImageUrl(TORPEDO_ID),
|
||||
Chariot: specialWeaponImageUrl(CRAB_TANK_ID),
|
||||
Gachihoko_Barrier: modeImageUrl("RM"),
|
||||
GreatBarrier_Barrier: specialWeaponImageUrl(BIG_BUBBLER_ID),
|
||||
GreatBarrier_WeakPoint: specialWeaponImageUrl(BIG_BUBBLER_ID),
|
||||
NiceBall_Armor: specialWeaponImageUrl(BOOYAH_BOMB_ID),
|
||||
ShockSonar: specialWeaponImageUrl(WAVE_BREAKER_ID),
|
||||
Wsb_Flag: subWeaponImageUrl(SQUID_BEAKON_ID),
|
||||
Wsb_Shield: subWeaponImageUrl(SPLASH_WALL_ID),
|
||||
Wsb_Sprinkler: subWeaponImageUrl(SPRINKLER_ID),
|
||||
BulletUmbrellaCanopyNormal: mainWeaponImageUrl(6000),
|
||||
BulletUmbrellaCanopyWide: mainWeaponImageUrl(6010),
|
||||
BulletUmbrellaCanopyCompact: mainWeaponImageUrl(6020),
|
||||
};
|
||||
|
||||
function DamageReceiversGrid({
|
||||
subWeaponId,
|
||||
damagesToReceivers,
|
||||
}: Pick<
|
||||
ReturnType<typeof useObjectDamage>,
|
||||
"damagesToReceivers" | "subWeaponId"
|
||||
>) {
|
||||
const { t } = useTranslation(["weapons", "analyzer", "common"]);
|
||||
useSetTitle(t("common:pages.object-damage"));
|
||||
|
||||
return (
|
||||
<div
|
||||
className="object-damage__grid"
|
||||
style={{
|
||||
gridTemplateColumns: gridTemplateColumnsValue(
|
||||
damagesToReceivers[0]?.damages.length ?? 0
|
||||
),
|
||||
}}
|
||||
>
|
||||
<div />
|
||||
<div />
|
||||
{damagesToReceivers[0]?.damages.map((damage) => (
|
||||
<div key={damage.id} className="object-damage__table-header">
|
||||
<div className="stack horizontal sm justify-center items-center">
|
||||
{t(
|
||||
damageTypeTranslationString({
|
||||
damageType: damage.type,
|
||||
subWeaponId: subWeaponId,
|
||||
})
|
||||
)}
|
||||
{damage.objectShredder && <Ability ability="OS" size="TINY" />}
|
||||
</div>
|
||||
<div
|
||||
className={clsx("object-damage__distance", {
|
||||
invisible: !damage.distance,
|
||||
})}
|
||||
>
|
||||
{t("analyzer:distanceInline", { value: damage.distance })}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{damagesToReceivers.map((damageToReceiver, i) => {
|
||||
return (
|
||||
<React.Fragment key={damageToReceiver.receiver}>
|
||||
<Image
|
||||
key={i}
|
||||
alt=""
|
||||
path={damageReceiverImages[damageToReceiver.receiver]}
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
<div className="object-damage__hp">
|
||||
{damageToReceiver.hitPoints}
|
||||
{t("analyzer:suffix.hp")}
|
||||
</div>
|
||||
{damageToReceiver.damages.map((damage) => {
|
||||
return (
|
||||
<div key={damage.id} className="object-damage__table-card">
|
||||
<div className="object-damage__table-card__results">
|
||||
<abbr
|
||||
className="object-damage__abbr"
|
||||
title={t("analyzer:stat.category.damage")}
|
||||
>
|
||||
{t("analyzer:damageShort")}
|
||||
</abbr>
|
||||
<div>{damage.value}</div>
|
||||
<abbr
|
||||
className="object-damage__abbr"
|
||||
title={t("analyzer:hitsToDestroyLong")}
|
||||
>
|
||||
{t("analyzer:hitsToDestroyShort")}
|
||||
</abbr>
|
||||
<div>{damage.hitsToDestroy}</div>
|
||||
</div>
|
||||
<div className="object-damage__multiplier">
|
||||
×{damage.multiplier}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function gridTemplateColumnsValue(dataColumnsCount: number) {
|
||||
return `max-content max-content ${new Array(dataColumnsCount)
|
||||
.fill(null)
|
||||
.map(() => `1fr`)
|
||||
.join(" ")}`;
|
||||
}
|
||||
@@ -3,11 +3,16 @@ import { Outlet } from "@remix-run/react";
|
||||
import { Main } from "~/components/Main";
|
||||
import { SubNav, SubNavLink } from "~/components/SubNav";
|
||||
import styles from "~/styles/plus.css";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
navItemName: "plus",
|
||||
};
|
||||
|
||||
export default function PlusPageLayout() {
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { lastCompletedVoting } from "~/modules/plus-server";
|
||||
import { db } from "~/db";
|
||||
import type { PlusVotingResultByMonthYear } from "~/db/models/plusVotes/queries.server";
|
||||
import type { PlusVotingResult } from "~/db/types";
|
||||
import { roundToTwoDecimalPlaces } from "~/utils/number";
|
||||
import { roundToNDecimalPlaces } from "~/utils/number";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import type { Unpacked } from "~/utils/types";
|
||||
import styles from "~/styles/plus-history.css";
|
||||
@@ -147,7 +147,7 @@ function Results({
|
||||
function databaseAvgToPercentage(score: number) {
|
||||
const scoreNormalized = score + 1;
|
||||
|
||||
return roundToTwoDecimalPlaces((scoreNormalized / 2) * 100);
|
||||
return roundToNDecimalPlaces((scoreNormalized / 2) * 100);
|
||||
}
|
||||
|
||||
function scoreForDisplaying(
|
||||
|
||||
@@ -15,7 +15,7 @@ import { db } from "~/db";
|
||||
import { useUser } from "~/modules/auth";
|
||||
import { i18next } from "~/modules/i18n";
|
||||
import { translatedCountry } from "~/utils/i18n.server";
|
||||
import { notFoundIfFalsy } from "~/utils/remix";
|
||||
import { notFoundIfFalsy, type SendouRouteHandle } from "~/utils/remix";
|
||||
import { discordFullName, makeTitle } from "~/utils/strings";
|
||||
import styles from "~/styles/u.css";
|
||||
import invariant from "tiny-invariant";
|
||||
@@ -39,7 +39,7 @@ export const meta: MetaFunction = ({ data }: { data: UserPageLoaderData }) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: "user",
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@ import { BUILD } from "~/constants";
|
||||
import { db } from "~/db";
|
||||
import { getUser, requireUser, useUser } from "~/modules/auth";
|
||||
import { atOrError } from "~/utils/arrays";
|
||||
import { notFoundIfFalsy, parseRequestFormData } from "~/utils/remix";
|
||||
import {
|
||||
notFoundIfFalsy,
|
||||
parseRequestFormData,
|
||||
type SendouRouteHandle,
|
||||
} from "~/utils/remix";
|
||||
import { userNewBuildPage } from "~/utils/urls";
|
||||
import { actualNumber, id } from "~/utils/zod";
|
||||
import { type UserPageLoaderData, userParamsSchema } from "../../u.$identifier";
|
||||
@@ -39,7 +43,7 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons", "builds"],
|
||||
};
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import type {
|
||||
BuildAbilitiesTupleWithUnknown,
|
||||
MainWeaponId,
|
||||
} from "~/modules/in-game-lists/types";
|
||||
import { parseRequestFormData } from "~/utils/remix";
|
||||
import { parseRequestFormData, type SendouRouteHandle } from "~/utils/remix";
|
||||
import { modeImageUrl, userBuildsPage } from "~/utils/urls";
|
||||
import {
|
||||
actualNumber,
|
||||
@@ -154,7 +154,7 @@ export const action: ActionFunction = async ({ request }) => {
|
||||
return redirect(userBuildsPage(user));
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: ["weapons", "builds", "gear"],
|
||||
};
|
||||
|
||||
|
||||
@@ -14,8 +14,9 @@ import type { Unpacked } from "~/utils/types";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { badgeExplanationText } from "../badges/$id";
|
||||
import type { UserPageLoaderData } from "../u.$identifier";
|
||||
import { type SendouRouteHandle } from "~/utils/remix";
|
||||
|
||||
export const handle = {
|
||||
export const handle: SendouRouteHandle = {
|
||||
i18n: "badges",
|
||||
};
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import { Link, useMatches } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import invariant from "tiny-invariant";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { Placement } from "~/components/Placement";
|
||||
import { Section } from "~/components/Section";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { discordFullName } from "~/utils/strings";
|
||||
import { calendarEventPage, userPage } from "~/utils/urls";
|
||||
import type { UserPageLoaderData } from "../u.$identifier";
|
||||
|
||||
export default function UserResultsPage() {
|
||||
const { t, i18n } = useTranslation("user");
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
const data = parentRoute.data as UserPageLoaderData;
|
||||
|
||||
return (
|
||||
<main className="main layout__main">
|
||||
<Section className="u__results-section">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("results.placing")}</th>
|
||||
<th>{t("results.team")}</th>
|
||||
<th>{t("results.tournament")}</th>
|
||||
<th>{t("results.participants")}</th>
|
||||
<th>{t("results.date")}</th>
|
||||
<th>{t("results.mates")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.results.map((result) => (
|
||||
<tr key={result.eventId}>
|
||||
<td className="pl-4">
|
||||
<Placement placement={result.placement} />
|
||||
</td>
|
||||
<td>{result.teamName}</td>
|
||||
<td>
|
||||
<Link to={calendarEventPage(result.eventId)}>
|
||||
{result.eventName}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{result.participantCount}</td>
|
||||
<td>
|
||||
{databaseTimestampToDate(result.startTime).toLocaleDateString(
|
||||
i18n.language,
|
||||
{
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
}
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<ul className="u__results-players">
|
||||
{result.mates.map((player) => (
|
||||
<li
|
||||
key={typeof player === "string" ? player : player.id}
|
||||
className="flex items-center"
|
||||
>
|
||||
{typeof player === "string" ? (
|
||||
player
|
||||
) : (
|
||||
<Link
|
||||
to={userPage(player)}
|
||||
className="stack horizontal xs items-center"
|
||||
>
|
||||
<Avatar user={player} size="xxs" />{" "}
|
||||
{discordFullName(player)}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
111
app/routes/u.$identifier/results/components/UserResultsTable.tsx
Normal file
111
app/routes/u.$identifier/results/components/UserResultsTable.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Avatar } from "~/components/Avatar";
|
||||
import { Placement } from "~/components/Placement";
|
||||
import { type UserPageLoaderData } from "~/routes/u.$identifier";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { discordFullName } from "~/utils/strings";
|
||||
import { calendarEventPage, userPage } from "~/utils/urls";
|
||||
|
||||
export type UserResultsTableProps = {
|
||||
results: UserPageLoaderData["results"];
|
||||
id: string;
|
||||
hasHighlightCheckboxes?: boolean;
|
||||
};
|
||||
|
||||
export const HIGHLIGHT_CHECKBOX_NAME = "highlightTeamIds";
|
||||
|
||||
export function UserResultsTable({
|
||||
results,
|
||||
id,
|
||||
hasHighlightCheckboxes,
|
||||
}: UserResultsTableProps) {
|
||||
const { t, i18n } = useTranslation("user");
|
||||
|
||||
const placementHeaderId = `${id}-th-placement`;
|
||||
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{hasHighlightCheckboxes && <th />}
|
||||
<th id={placementHeaderId}>{t("results.placing")}</th>
|
||||
<th>{t("results.team")}</th>
|
||||
<th>{t("results.tournament")}</th>
|
||||
<th>{t("results.participants")}</th>
|
||||
<th>{t("results.date")}</th>
|
||||
<th>{t("results.mates")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{results.map((result) => {
|
||||
// We are trying to construct a reasonable label for the checkbox
|
||||
// which shouldn't contain the whole information of the table row as
|
||||
// that can be also accessed when needed.
|
||||
// e.g. "20xx Placing 2nd", "Big House 10 Placing 20th"
|
||||
const placementCellId = `${id}-${result.teamId}-placement`;
|
||||
const nameCellId = `${id}-${result.teamId}-name`;
|
||||
const checkboxLabelIds = `${nameCellId} ${placementHeaderId} ${placementCellId}`;
|
||||
|
||||
return (
|
||||
<tr key={result.teamId}>
|
||||
{hasHighlightCheckboxes && (
|
||||
<td>
|
||||
<input
|
||||
value={result.teamId}
|
||||
aria-labelledby={checkboxLabelIds}
|
||||
name={HIGHLIGHT_CHECKBOX_NAME}
|
||||
type="checkbox"
|
||||
defaultChecked={result.isHighlight}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
<td className="pl-4" id={placementCellId}>
|
||||
<Placement placement={result.placement} />
|
||||
</td>
|
||||
<td>{result.teamName}</td>
|
||||
<td id={nameCellId}>
|
||||
<Link to={calendarEventPage(result.eventId)}>
|
||||
{result.eventName}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{result.participantCount}</td>
|
||||
<td>
|
||||
{databaseTimestampToDate(result.startTime).toLocaleDateString(
|
||||
i18n.language,
|
||||
{
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
}
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<ul className="u__results-players">
|
||||
{result.mates.map((player) => (
|
||||
<li
|
||||
key={typeof player === "string" ? player : player.id}
|
||||
className="flex items-center"
|
||||
>
|
||||
{typeof player === "string" ? (
|
||||
player
|
||||
) : (
|
||||
<Link
|
||||
to={userPage(player)}
|
||||
className="stack horizontal xs items-center"
|
||||
>
|
||||
<Avatar user={player} size="xxs" />
|
||||
{discordFullName(player)}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
79
app/routes/u.$identifier/results/highlights.tsx
Normal file
79
app/routes/u.$identifier/results/highlights.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { type ActionFunction, redirect } from "@remix-run/node";
|
||||
import { Form, useMatches, useTransition } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import invariant from "tiny-invariant";
|
||||
import { z } from "zod";
|
||||
import { Button } from "~/components/Button";
|
||||
import { FormErrors } from "~/components/FormErrors";
|
||||
import { Main } from "~/components/Main";
|
||||
import { db } from "~/db";
|
||||
import { requireUser } from "~/modules/auth";
|
||||
import { type UserPageLoaderData } from "~/routes/u.$identifier";
|
||||
import { normalizeFormFieldArray } from "~/utils/arrays";
|
||||
import { parseRequestFormData } from "~/utils/remix";
|
||||
import { userResultsPage } from "~/utils/urls";
|
||||
import {
|
||||
HIGHLIGHT_CHECKBOX_NAME,
|
||||
UserResultsTable,
|
||||
} from "./components/UserResultsTable";
|
||||
|
||||
const editHighlightsActionSchema = z.object({
|
||||
[HIGHLIGHT_CHECKBOX_NAME]: z.optional(
|
||||
z.union([z.array(z.string()), z.string()])
|
||||
),
|
||||
});
|
||||
|
||||
export const action: ActionFunction = async ({ request }) => {
|
||||
const user = await requireUser(request);
|
||||
const data = await parseRequestFormData({
|
||||
request,
|
||||
schema: editHighlightsActionSchema,
|
||||
});
|
||||
|
||||
const resultTeamIds = normalizeFormFieldArray(
|
||||
data[HIGHLIGHT_CHECKBOX_NAME]
|
||||
).map((id) => parseInt(id, 10));
|
||||
|
||||
db.users.updateResultHighlights({
|
||||
userId: user.id,
|
||||
resultTeamIds,
|
||||
});
|
||||
|
||||
return redirect(userResultsPage(user));
|
||||
};
|
||||
|
||||
export default function ResultHighlightsEditPage() {
|
||||
const { t } = useTranslation(["common", "user"]);
|
||||
const [, parentRoute] = useMatches();
|
||||
const transition = useTransition();
|
||||
|
||||
invariant(parentRoute);
|
||||
const userPageData = parentRoute.data as UserPageLoaderData;
|
||||
|
||||
return (
|
||||
<Main>
|
||||
<Form method="post" className="stack md items-start">
|
||||
<h2 className="text-start">{t("user:results.highlights.choose")}</h2>
|
||||
<div className="u__results-table-wrapper">
|
||||
<fieldset className="u__results-table-highlights">
|
||||
<legend>{t("user:results.highlights.explanation")}</legend>
|
||||
<UserResultsTable
|
||||
id="user-results-highlight-selection"
|
||||
results={userPageData.results}
|
||||
hasHighlightCheckboxes
|
||||
/>
|
||||
</fieldset>
|
||||
</div>
|
||||
<Button
|
||||
loadingText={t("common:actions.saving")}
|
||||
type="submit"
|
||||
loading={transition.state === "submitting"}
|
||||
data-cy="submit-button"
|
||||
>
|
||||
{t("common:actions.save")}
|
||||
</Button>
|
||||
<FormErrors namespace="user" />
|
||||
</Form>
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
68
app/routes/u.$identifier/results/index.tsx
Normal file
68
app/routes/u.$identifier/results/index.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useMatches } from "@remix-run/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import invariant from "tiny-invariant";
|
||||
import { LinkButton } from "~/components/Button";
|
||||
import { Main } from "~/components/Main";
|
||||
import { Section } from "~/components/Section";
|
||||
import { useUser } from "~/modules/auth";
|
||||
import { userResultsEditHighlightsPage } from "~/utils/urls";
|
||||
import type { UserPageLoaderData } from "../../u.$identifier";
|
||||
import { UserResultsTable } from "./components/UserResultsTable";
|
||||
|
||||
export default function UserResultsPage() {
|
||||
const { t } = useTranslation("user");
|
||||
const [, parentRoute] = useMatches();
|
||||
invariant(parentRoute);
|
||||
|
||||
const userPageData = parentRoute.data as UserPageLoaderData;
|
||||
const hasResults = userPageData.results.length > 0;
|
||||
|
||||
const nonHighlights = userPageData.results.filter((r) => !r.isHighlight);
|
||||
const hasNonHighlights = nonHighlights.length > 0;
|
||||
|
||||
const highlights = userPageData.results.filter((r) => r.isHighlight);
|
||||
const hasHighlights = highlights.length > 0;
|
||||
|
||||
const user = useUser();
|
||||
const isOwnResultsPage = user?.id === userPageData.id;
|
||||
|
||||
const showHighlightsSection =
|
||||
hasHighlights || (isOwnResultsPage && hasResults);
|
||||
|
||||
return (
|
||||
<Main className="stack lg">
|
||||
{showHighlightsSection && (
|
||||
<Section
|
||||
title={t("results.highlights")}
|
||||
className="u__results-table-wrapper u__results-table-highlights stack md items-center"
|
||||
>
|
||||
{hasHighlights && (
|
||||
<UserResultsTable
|
||||
id="user-results-highlight-table"
|
||||
results={highlights}
|
||||
/>
|
||||
)}
|
||||
{isOwnResultsPage && (
|
||||
<LinkButton
|
||||
variant="outlined"
|
||||
tiny
|
||||
to={userResultsEditHighlightsPage(userPageData)}
|
||||
>
|
||||
{t("results.highlights.choose")}
|
||||
</LinkButton>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
{hasNonHighlights && (
|
||||
<Section
|
||||
title={
|
||||
hasHighlights ? t("results.nonHighlights") : t("results.title")
|
||||
}
|
||||
className="u__results-table-wrapper"
|
||||
>
|
||||
<UserResultsTable id="user-results-table" results={nonHighlights} />
|
||||
</Section>
|
||||
)}
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
@@ -27,8 +27,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--theme-transparent);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--theme-transparent);
|
||||
gap: var(--s-1);
|
||||
padding-block: var(--s-1);
|
||||
padding-inline: var(--s-1-5);
|
||||
@@ -47,8 +47,8 @@
|
||||
|
||||
.analyzer__ap-summary {
|
||||
width: 100%;
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-lighter);
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--semi-bold);
|
||||
padding-block: var(--s-1);
|
||||
@@ -62,8 +62,8 @@
|
||||
}
|
||||
|
||||
.analyzer__summary {
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-lighter);
|
||||
font-size: var(--fonts-md);
|
||||
font-weight: var(--bold);
|
||||
padding-block: var(--s-2);
|
||||
@@ -82,8 +82,8 @@
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: var(--s-2);
|
||||
background-color: var(--bg-darker);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-darker);
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
@@ -151,8 +151,8 @@
|
||||
.analyzer__table-container {
|
||||
width: 100%;
|
||||
padding: var(--s-3);
|
||||
background-color: var(--bg-darker);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-darker);
|
||||
margin-block-start: var(--s-4);
|
||||
padding-block: var(--s-2);
|
||||
}
|
||||
@@ -176,8 +176,8 @@
|
||||
}
|
||||
|
||||
.analyzer__patch {
|
||||
background-color: var(--theme-transparent);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--theme-transparent);
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xxxs);
|
||||
font-weight: var(--bold);
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background-color: var(--bg-badge);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-badge);
|
||||
color: var(--badge-text);
|
||||
gap: var(--s-6);
|
||||
padding-block: var(--s-2);
|
||||
|
||||
@@ -4,19 +4,12 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.builds__breadcrumbs {
|
||||
display: flex;
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--bold);
|
||||
gap: var(--s-1);
|
||||
}
|
||||
|
||||
.builds__category {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--s-3);
|
||||
background-color: var(--bg-darker);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-darker);
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--semi-bold);
|
||||
gap: var(--s-4);
|
||||
@@ -44,6 +37,6 @@
|
||||
}
|
||||
|
||||
.builds__category__weapon__img {
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-lighter);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
.calendar-new__badges {
|
||||
width: max-content;
|
||||
padding: var(--s-2);
|
||||
background-color: var(--bg-badge);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-badge);
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: var(--s-1-5);
|
||||
background-color: var(--theme-very-transparent);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--theme-very-transparent);
|
||||
cursor: pointer;
|
||||
font-size: var(--fonts-xxxs);
|
||||
font-weight: var(--bold);
|
||||
|
||||
@@ -32,9 +32,9 @@ a {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2px solid var(--theme);
|
||||
border-radius: var(--rounded-sm);
|
||||
appearance: none;
|
||||
background: var(--theme);
|
||||
border-radius: var(--rounded-sm);
|
||||
color: var(--button-text);
|
||||
cursor: pointer;
|
||||
font-size: var(--fonts-sm);
|
||||
@@ -138,9 +138,9 @@ textarea:not(.plain) {
|
||||
height: 8rem;
|
||||
padding: var(--s-2-5) var(--s-3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--rounded);
|
||||
accent-color: var(--theme-secondary);
|
||||
background-color: transparent;
|
||||
border-radius: var(--rounded);
|
||||
color: var(--text);
|
||||
font-size: var(--fonts-sm);
|
||||
outline: none;
|
||||
@@ -168,9 +168,9 @@ input:not(.plain) {
|
||||
height: 1rem;
|
||||
padding: var(--s-4) var(--s-3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--rounded);
|
||||
accent-color: var(--theme-secondary);
|
||||
background-color: transparent;
|
||||
border-radius: var(--rounded);
|
||||
color: var(--text);
|
||||
font-size: var(--fonts-sm);
|
||||
outline: none;
|
||||
@@ -222,17 +222,17 @@ details summary {
|
||||
|
||||
fieldset {
|
||||
border: none;
|
||||
background-color: var(--bg-darker-transparent);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-darker-transparent);
|
||||
font-size: var(--fonts-sm);
|
||||
padding-block-end: var(--s-3);
|
||||
padding-inline: var(--s-3);
|
||||
}
|
||||
|
||||
legend {
|
||||
background-color: transparent;
|
||||
border-radius: 2px;
|
||||
border-radius: var(--rounded-sm);
|
||||
background-color: transparent;
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
@@ -245,13 +245,13 @@ select {
|
||||
all: unset;
|
||||
width: 90%;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--rounded);
|
||||
background: var(--select-background, var(--bg-lighter));
|
||||
|
||||
/* TODO: Get color from CSS var */
|
||||
background-image: url('data:image/svg+xml;utf8,<svg width="1rem" color="rgb(255 255 255 / 55%)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg>');
|
||||
background-position: center right var(--s-3);
|
||||
background-repeat: no-repeat;
|
||||
border-radius: var(--rounded);
|
||||
cursor: pointer;
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: 500;
|
||||
@@ -292,6 +292,10 @@ table > tbody > tr > td {
|
||||
padding-inline: var(--s-1);
|
||||
}
|
||||
|
||||
td > input[type="checkbox"] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: var(--theme-transparent);
|
||||
}
|
||||
@@ -307,9 +311,9 @@ abbr[title] {
|
||||
dialog {
|
||||
width: min(90%, 24rem);
|
||||
border: 0;
|
||||
border-radius: var(--rounded);
|
||||
margin: auto;
|
||||
background-color: var(--bg);
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
@@ -342,8 +346,9 @@ dialog::backdrop {
|
||||
width: var(--s-11);
|
||||
height: var(--s-6);
|
||||
align-items: center;
|
||||
background-color: var(--theme-transparent);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--theme-transparent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle.tiny {
|
||||
@@ -363,8 +368,8 @@ dialog::backdrop {
|
||||
display: inline-block;
|
||||
width: var(--s-4);
|
||||
height: var(--s-4);
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
transform: translateX(var(--s-1));
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
@@ -398,9 +403,9 @@ dialog::backdrop {
|
||||
.input-container {
|
||||
display: flex;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--rounded);
|
||||
accent-color: var(--theme-secondary);
|
||||
background-color: transparent;
|
||||
border-radius: var(--rounded);
|
||||
color: var(--text);
|
||||
font-size: var(--fonts-sm);
|
||||
outline: none;
|
||||
@@ -426,8 +431,8 @@ dialog::backdrop {
|
||||
|
||||
.input-addon {
|
||||
display: grid;
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded) 0 0 var(--rounded);
|
||||
background-color: var(--bg-lighter);
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
@@ -472,8 +477,8 @@ dialog::backdrop {
|
||||
z-index: 1;
|
||||
max-width: 20rem;
|
||||
padding: var(--s-2);
|
||||
background-color: var(--bg-darker-transparent);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-darker-transparent);
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--semi-bold);
|
||||
white-space: pre-wrap;
|
||||
@@ -495,9 +500,9 @@ dialog::backdrop {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: 12rem;
|
||||
border-radius: var(--rounded);
|
||||
margin-top: var(--s-2);
|
||||
background-color: var(--bg-darker);
|
||||
border-radius: var(--rounded);
|
||||
color: var(--text);
|
||||
font-size: var(--fonts-sm);
|
||||
padding-block: var(--s-3);
|
||||
@@ -544,8 +549,8 @@ dialog::backdrop {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--theme-info-transparent);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--theme-info-transparent);
|
||||
color: var(--text);
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--semi-bold);
|
||||
@@ -558,9 +563,9 @@ dialog::backdrop {
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-lighter);
|
||||
background-image: url("/svg/background-pattern.svg");
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.alert > svg {
|
||||
@@ -578,8 +583,8 @@ dialog::backdrop {
|
||||
|
||||
.section > div {
|
||||
padding: var(--s-2);
|
||||
background-color: var(--bg-darker);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-darker);
|
||||
}
|
||||
|
||||
.section > h2 {
|
||||
@@ -722,8 +727,8 @@ dialog::backdrop {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--s-2-5);
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-lighter);
|
||||
gap: var(--s-3);
|
||||
}
|
||||
|
||||
@@ -752,8 +757,8 @@ dialog::backdrop {
|
||||
|
||||
.build__weapon {
|
||||
padding: var(--s-0-5);
|
||||
background-color: var(--bg-darker-very-transparent);
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-darker-very-transparent);
|
||||
}
|
||||
|
||||
.build__weapon-text {
|
||||
@@ -780,8 +785,8 @@ dialog::backdrop {
|
||||
}
|
||||
|
||||
.build__gear {
|
||||
background-color: var(--bg-darker-very-transparent);
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-darker-very-transparent);
|
||||
}
|
||||
|
||||
.build__ability {
|
||||
@@ -789,11 +794,11 @@ dialog::backdrop {
|
||||
height: var(--ability-size);
|
||||
padding: 0;
|
||||
border: 2px solid var(--theme-transparent);
|
||||
border-radius: 50%;
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
background: var(--bg-ability);
|
||||
background-size: 100%;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 1px var(--bg-ability);
|
||||
transform: scale(1);
|
||||
transition: all 0.1s ease;
|
||||
@@ -860,10 +865,17 @@ dialog::backdrop {
|
||||
.ability-selector__ability-button {
|
||||
padding: var(--s-0-5);
|
||||
border-color: var(--abilities-button-bg);
|
||||
background-color: var(--abilities-button-bg);
|
||||
border-radius: 50%;
|
||||
background-color: var(--abilities-button-bg);
|
||||
}
|
||||
|
||||
.ability-selector__ability-button.is-dragging {
|
||||
box-shadow: 0 0 100px inset rgb(255 255 255 / 25%);
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
display: flex;
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--bold);
|
||||
gap: var(--s-1);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.faq__summary {
|
||||
padding: var(--s-3);
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-lighter);
|
||||
font-size: var(--fonts-lg);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--s-2);
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-lighter);
|
||||
color: var(--text);
|
||||
font-size: var(--fonts-sm);
|
||||
gap: var(--s-2);
|
||||
@@ -63,8 +63,8 @@
|
||||
min-width: 18rem;
|
||||
flex: 1 1 0px;
|
||||
padding: var(--s-2);
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-lighter);
|
||||
}
|
||||
|
||||
.front__calendar-peek-container > h2 {
|
||||
@@ -76,8 +76,8 @@
|
||||
|
||||
display: grid;
|
||||
flex: 1 1 0px;
|
||||
background-color: var(--bg-darker);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-darker);
|
||||
font-size: var(--fonts-sm);
|
||||
grid-template-areas: "name secondary" "content content";
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -150,8 +150,8 @@
|
||||
max-width: 12rem;
|
||||
flex: 1 1 0;
|
||||
padding: var(--s-2);
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-lighter);
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
height: var(--item-size);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-lighter);
|
||||
background-image: url("/svg/background-pattern.svg");
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.layout__avatar {
|
||||
@@ -44,8 +44,8 @@
|
||||
display: flex;
|
||||
width: max-content;
|
||||
align-items: center;
|
||||
background-color: var(--bg-darker);
|
||||
border-radius: 0 var(--rounded) var(--rounded) 0;
|
||||
background-color: var(--bg-darker);
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
gap: var(--s-1);
|
||||
@@ -66,8 +66,8 @@
|
||||
padding: 0.25rem;
|
||||
border: 2px solid;
|
||||
border-color: var(--theme-transparent-vibrant);
|
||||
background-color: transparent;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -102,8 +102,8 @@
|
||||
padding: 0.25rem;
|
||||
border: 2px solid;
|
||||
border-color: var(--theme-transparent-vibrant);
|
||||
background-color: transparent;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
gap: 2px;
|
||||
@@ -169,9 +169,9 @@
|
||||
.layout__menu__link__icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-lighter);
|
||||
background-image: url("/svg/background-pattern.svg");
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.layout__menu__links {
|
||||
@@ -213,8 +213,8 @@
|
||||
padding: 0.5rem;
|
||||
border: 2px solid;
|
||||
border-color: var(--bg-lighter);
|
||||
background-color: transparent;
|
||||
border-radius: var(--rounded);
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-size: var(--fonts-xs);
|
||||
@@ -276,8 +276,8 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--s-4);
|
||||
background-color: var(--theme-transparent-vibrant);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--theme-transparent-vibrant);
|
||||
cursor: pointer;
|
||||
font-size: var(--fonts-lg);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-lighter);
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
gap: var(--s-2);
|
||||
@@ -51,8 +51,8 @@
|
||||
padding: 0;
|
||||
padding: var(--s-1-5);
|
||||
border: none;
|
||||
background-color: var(--bg-darker);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-darker);
|
||||
color: var(--theme);
|
||||
opacity: 1 !important;
|
||||
outline: initial;
|
||||
@@ -71,7 +71,7 @@
|
||||
}
|
||||
|
||||
.maps__mode.selected {
|
||||
filter: grayscale(0%);
|
||||
filter: unset;
|
||||
}
|
||||
|
||||
.maps__map-list-creator {
|
||||
|
||||
73
app/styles/object-damage.css
Normal file
73
app/styles/object-damage.css
Normal file
@@ -0,0 +1,73 @@
|
||||
.object-damage__controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.object-damage__ap-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--s-1-5);
|
||||
}
|
||||
|
||||
.object-damage__grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
column-gap: var(--s-6);
|
||||
place-items: center;
|
||||
row-gap: var(--s-3);
|
||||
}
|
||||
|
||||
.object-damage__hp {
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.object-damage__table-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-darker);
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
padding-block: var(--s-2);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.object-damage__distance {
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xxs);
|
||||
margin-block-start: var(--s-1);
|
||||
}
|
||||
|
||||
.object-damage__table-card {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--s-2);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-lighter);
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.object-damage__table-card__results {
|
||||
display: grid;
|
||||
column-gap: var(--s-2);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.object-damage__abbr {
|
||||
color: var(--text-lighter);
|
||||
font-weight: var(--bold);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.object-damage__multiplier {
|
||||
color: var(--theme);
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--bold);
|
||||
letter-spacing: 0.5px;
|
||||
margin-block-start: var(--s-2);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
.plus-history__own-scores {
|
||||
width: max-content;
|
||||
padding: var(--s-2);
|
||||
border-radius: var(--rounded);
|
||||
margin: 0 auto;
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--semi-bold);
|
||||
list-style-type: none;
|
||||
@@ -53,8 +53,8 @@
|
||||
}
|
||||
|
||||
.plus-history__user-status {
|
||||
background-color: var(--theme);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--theme);
|
||||
color: var(--button-text);
|
||||
font-size: var(--fonts-xs);
|
||||
font-weight: var(--semi-bold);
|
||||
@@ -67,8 +67,8 @@
|
||||
}
|
||||
|
||||
.plus-history__suggestion-s {
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: 50%;
|
||||
background-color: var(--bg-lighter);
|
||||
color: var(--text);
|
||||
font-weight: var(--bold);
|
||||
margin-inline-end: var(--s-1);
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
}
|
||||
|
||||
.plus__comment {
|
||||
min-width: auto;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@@ -124,8 +126,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--theme-success-transparent);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--theme-success-transparent);
|
||||
color: var(--text);
|
||||
font-size: var(--fonts-sm);
|
||||
font-weight: var(--semi-bold);
|
||||
|
||||
@@ -96,8 +96,8 @@
|
||||
|
||||
.u__extra-info {
|
||||
padding: var(--s-1) var(--s-1-5);
|
||||
background-color: var(--bg-darker);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-darker);
|
||||
font-size: var(--fonts-xxs);
|
||||
}
|
||||
|
||||
@@ -111,8 +111,8 @@
|
||||
max-width: 24rem;
|
||||
align-items: center;
|
||||
padding: var(--s-2);
|
||||
background-color: var(--bg-badge);
|
||||
border-radius: var(--rounded);
|
||||
background-color: var(--bg-badge);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
@@ -147,10 +147,21 @@
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.u__results-section {
|
||||
.u__results-table-wrapper {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.u__results-table-highlights {
|
||||
border: var(--s-2) solid var(--bg-lighter);
|
||||
padding-inline: 0 !important;
|
||||
}
|
||||
|
||||
.u__results-table-highlights > legend {
|
||||
margin-inline-start: var(--s-2);
|
||||
padding-inline: var(--s-1);
|
||||
}
|
||||
|
||||
.u__results-players {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -34,3 +34,14 @@ export function joinListToNaturalString(arg: string[]) {
|
||||
|
||||
return last ? `${commaJoined} and ${last}` : commaJoined;
|
||||
}
|
||||
|
||||
export function normalizeFormFieldArray(
|
||||
value: undefined | null | string | string[]
|
||||
): string[] {
|
||||
return value == null ? [] : typeof value === "string" ? [value] : value;
|
||||
}
|
||||
|
||||
/** Can be used as a strongly typed array filter */
|
||||
export function isDefined<T>(value: T | undefined | null): value is T {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
14
app/utils/i18next.ts
Normal file
14
app/utils/i18next.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { DamageType } from "~/modules/analyzer";
|
||||
import type { SubWeaponId } from "~/modules/in-game-lists";
|
||||
|
||||
// TODO: type this correctly
|
||||
export const damageTypeTranslationString = ({
|
||||
damageType,
|
||||
subWeaponId,
|
||||
}: {
|
||||
damageType: DamageType;
|
||||
subWeaponId: SubWeaponId;
|
||||
}): any =>
|
||||
damageType.startsWith("BOMB_")
|
||||
? `weapons:SUB_${subWeaponId}`
|
||||
: `analyzer:damage.${damageType as "NORMAL_MIN"}`;
|
||||
@@ -1,3 +1,3 @@
|
||||
export function roundToTwoDecimalPlaces(num: number) {
|
||||
return Number((Math.round(num * 100) / 100).toFixed(2));
|
||||
export function roundToNDecimalPlaces(num: number, n = 2) {
|
||||
return Number((Math.round(num * 100) / 100).toFixed(n));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { type TFunction, type Namespace } from "react-i18next";
|
||||
import { type RouteMatch } from "@remix-run/react";
|
||||
import type navItems from "~/components/layout/nav-items.json";
|
||||
|
||||
export function notFoundIfFalsy<T>(value: T | null | undefined): T {
|
||||
if (!value) throw new Response(null, { status: 404 });
|
||||
@@ -89,3 +92,27 @@ export function validate(condition: any, status = 400): asserts condition {
|
||||
|
||||
throw new Response(null, { status });
|
||||
}
|
||||
|
||||
/**
|
||||
* Our custom type for route handles - the keys are defined by us or
|
||||
* libraries that parse them.
|
||||
*
|
||||
* Can be set per route using `export const handle: SendouRouteHandle = { };`
|
||||
* Can be accessed for all currently active routes via the `useMatches()` hook.
|
||||
*/
|
||||
export type SendouRouteHandle = {
|
||||
/** The i18n translation files used for this route, via remix-i18next */
|
||||
i18n?: Namespace;
|
||||
|
||||
/**
|
||||
* A function that returns the breadcrumb text that should be displayed in
|
||||
* the <Breadcrumb> component
|
||||
*/
|
||||
breadcrumb?: (args: {
|
||||
match: RouteMatch;
|
||||
t: TFunction<"common", undefined>;
|
||||
}) => string | undefined;
|
||||
|
||||
/** The name of a navItem that is active on this route. See nav-items.json */
|
||||
navItemName?: typeof navItems[number]["name"];
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
SubWeaponId,
|
||||
} from "~/modules/in-game-lists/types";
|
||||
import type navItems from "~/components/layout/nav-items.json";
|
||||
import { type AuthErrorCode } from "~/modules/auth";
|
||||
|
||||
export const SPLATOON_2_SENDOU_IN_URL = "https://spl2.sendou.ink";
|
||||
export const PLUS_SERVER_DISCORD_URL = "https://discord.gg/FW4dKrY";
|
||||
@@ -54,9 +55,13 @@ export const userBuildsPage = (user: UserLinkArgs) =>
|
||||
`${userPage(user)}/builds`;
|
||||
export const userResultsPage = (user: UserLinkArgs) =>
|
||||
`${userPage(user)}/results`;
|
||||
export const userResultsEditHighlightsPage = (user: UserLinkArgs) =>
|
||||
`${userResultsPage(user)}/highlights`;
|
||||
export const userNewBuildPage = (user: UserLinkArgs) =>
|
||||
`${userBuildsPage(user)}/new`;
|
||||
|
||||
export const authErrorUrl = (errorCode: AuthErrorCode) =>
|
||||
`/?authError=${errorCode}`;
|
||||
export const impersonateUrl = (idToLogInAs: number) =>
|
||||
`/auth/impersonate?id=${idToLogInAs}`;
|
||||
export const badgePage = (badgeId: number) => `${BADGES_PAGE}/${badgeId}`;
|
||||
|
||||
25
migrations/011-user-result-highlights.js
Normal file
25
migrations/011-user-result-highlights.js
Normal file
@@ -0,0 +1,25 @@
|
||||
module.exports.up = function (db) {
|
||||
db.prepare(
|
||||
`
|
||||
create table "UserResultHighlight" (
|
||||
"teamId" integer not null,
|
||||
"userId" integer not null,
|
||||
foreign key ("teamId") references "CalendarEventResultTeam"("id") on delete cascade,
|
||||
foreign key ("userId") references "User"("id") on delete cascade,
|
||||
unique("teamId", "userId") on conflict rollback
|
||||
) strict
|
||||
`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`create index user_result_highlight_user_id on "UserResultHighlight"("userId")`
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
`create index user_result_highlight_team_id on "UserResultHighlight"("teamId")`
|
||||
).run();
|
||||
};
|
||||
|
||||
module.exports.down = function (db) {
|
||||
db.prepare(`drop table "UserResultHighlight"`).run();
|
||||
};
|
||||
2050
package-lock.json
generated
2050
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
@@ -15,15 +15,17 @@
|
||||
"rename-badge": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/rename-badge.ts",
|
||||
"create-weapon-json": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/create-weapon-json.ts",
|
||||
"create-gear-json": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/create-gear-json.ts",
|
||||
"create-object-dmg-json": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/create-object-dmg-json.ts",
|
||||
"create-misc-json": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/create-misc-json.ts",
|
||||
"create-analyzer-json": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/create-analyzer-json.ts",
|
||||
"check-translation-jsons": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/check-translation-jsons.ts && npm run prettier:write",
|
||||
"check-translation-jsons": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/check-translation-jsons.ts && npm run prettier:write-translation-progress",
|
||||
"replace-img-names": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/replace-img-names.ts",
|
||||
"remove-bad-custom-urls": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/remove-bad-custom-urls.ts",
|
||||
"lint:ts": "eslint . --ext .ts,.tsx",
|
||||
"lint:styles": "stylelint \"app/styles/**/*.css\"",
|
||||
"prettier:check": "prettier --check . --loglevel warn",
|
||||
"prettier:write": "prettier --write . --loglevel warn",
|
||||
"prettier:write-translation-progress": "prettier --write translation-progress.md --loglevel warn",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test:unit": "uvu -r tsm -r tsconfig-paths/register -i cypress",
|
||||
"cy:open": "cypress open",
|
||||
@@ -32,12 +34,12 @@
|
||||
"cf": "npm run test:unit && npm run lint:styles -- --fix && npm run lint:ts -- --fix && npm run prettier:write && npm run typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^7.5.0",
|
||||
"@headlessui/react": "^1.7.2",
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@headlessui/react": "^1.7.3",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@remix-run/node": "^1.7.1",
|
||||
"@remix-run/react": "^1.7.1",
|
||||
"@remix-run/serve": "^1.7.1",
|
||||
"@remix-run/node": "^1.7.2",
|
||||
"@remix-run/react": "^1.7.2",
|
||||
"@remix-run/serve": "^1.7.2",
|
||||
"better-sqlite3": "^7.6.2",
|
||||
"clsx": "^1.2.1",
|
||||
"countries-list": "^2.6.1",
|
||||
@@ -55,7 +57,7 @@
|
||||
"node-cron": "3.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-flip-toolkit": "^7.0.16",
|
||||
"react-flip-toolkit": "^7.0.17",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-popper": "^2.3.0",
|
||||
"react-use": "^17.4.0",
|
||||
@@ -64,36 +66,36 @@
|
||||
"remix-i18next": "^4.1.1",
|
||||
"slugify": "^1.6.5",
|
||||
"swr": "^1.3.0",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"tiny-invariant": "^1.3.1",
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^1.7.1",
|
||||
"@remix-run/eslint-config": "^1.7.1",
|
||||
"@types/better-sqlite3": "^7.6.0",
|
||||
"@remix-run/dev": "^1.7.2",
|
||||
"@remix-run/eslint-config": "^1.7.2",
|
||||
"@types/better-sqlite3": "^7.6.2",
|
||||
"@types/i18next-fs-backend": "^1.1.2",
|
||||
"@types/node-cron": "^3.0.4",
|
||||
"@types/react": "^18.0.20",
|
||||
"@types/react": "^18.0.21",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
||||
"@typescript-eslint/parser": "^5.38.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^10.8.0",
|
||||
"dotenv": "^16.0.2",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"cypress": "^10.10.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"ley": "^0.7.1",
|
||||
"ley": "^0.8.1",
|
||||
"prettier": "^2.7.1",
|
||||
"stylelint": "^14.12.1",
|
||||
"stylelint-config-idiomatic-order": "^8.1.0",
|
||||
"stylelint": "^14.14.0",
|
||||
"stylelint-config-idiomatic-order": "^9.0.0",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-standard": "^28.0.0",
|
||||
"stylelint-config-standard": "^29.0.0",
|
||||
"stylelint-order": "^5.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"tsm": "^2.2.2",
|
||||
"typescript": "^4.8.3",
|
||||
"typescript": "^4.8.4",
|
||||
"uvu": "^0.5.6"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
BIN
public/badges/splatalittle.avif
Normal file
BIN
public/badges/splatalittle.avif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/badges/splatalittle.gif
Normal file
BIN
public/badges/splatalittle.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
BIN
public/badges/splatalittle.png
Normal file
BIN
public/badges/splatalittle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
@@ -29,7 +29,7 @@
|
||||
"stat.runSpeedInEnemyInk": "Løbehastighed i fjendtligt blæk",
|
||||
"stat.shootingRunSpeed": "Løbehastighed under skydning",
|
||||
"stat.shootingRunSpeedCharging": "Løbehastighed under opladning (af skud)",
|
||||
"stat.shootingRunSpeedFullCharge": "Løbehastig (ved fuld ladning af våben)",
|
||||
"stat.shootingRunSpeedFullCharge": "Løbehastighed (ved fuld ladning af våben)",
|
||||
"stat.framesBeforeTakingDamageInEnemyInk": "Mængde af tid i billeder/frames, før at du begynder at tage skade af at stå i fjendens blæk",
|
||||
"stat.damageTakenInEnemyInkPerSecond": "Skaden fra fjendtligt blæk per sekund",
|
||||
"stat.enemyInkDamageLimit": "Maksimal mængde skade man kan tage af fjendtligt blæk",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"participatedCount": "{{count}} hold deltog",
|
||||
"members": "Medlemmer",
|
||||
"results": "Resultater",
|
||||
"createMapList": "Lav bane-liste",
|
||||
|
||||
"forms.dates": "Datoer",
|
||||
"forms.bracketUrl": "Turneringsplans-URL",
|
||||
@@ -17,6 +18,7 @@
|
||||
"forms.tags.info": "\"Præmiemærker\" tag tilføjes automatisk, hvis den er anvendelig",
|
||||
"forms.badges": "Præmiemærker",
|
||||
"forms.badges.placeholder": "Vælg et premiemærke",
|
||||
"forms.mapPool": "Banepulje",
|
||||
|
||||
"forms.participantCount": "Antal deltagere",
|
||||
"forms.reportResultsHeader": "Viser resultater for {{eventName}}",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"pages.faq": "FAQ",
|
||||
"pages.builds": "Udrustningssæt",
|
||||
"pages.analyzer": "Udrustningsanalysator",
|
||||
"pages.maps": "Banelister",
|
||||
|
||||
"header.profile": "Profil",
|
||||
"header.logout": "Log ud",
|
||||
@@ -27,6 +28,12 @@
|
||||
"actions.remove": "Fjern",
|
||||
"actions.delete": "Slet",
|
||||
"actions.loadMore": "Indlæs flere",
|
||||
"actions.copyToClipboard": "Kopiér til udklipsholderen",
|
||||
|
||||
"maps.createMapList": "Lav bane-liste",
|
||||
"maps.halfSz": "50% SZ",
|
||||
"maps.mapPool": "Banepulje",
|
||||
"maps.tournamentMaplist": "Lav turneringsbanepuljer på (maps.iplabs.ink)",
|
||||
|
||||
"results": "Resultater",
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"plus.description": "Se plus-serverens valghistorik og mere til",
|
||||
"badges.description": "Liste af alle de premiemærker til din profil, som du kan gøre dig fortjent til",
|
||||
"analyzer.description": "Undersøg hvordan dine udrustinger giver dig af fordele i kamp",
|
||||
"maps.description": "Lav en liste af baner ud fra en pulje af baner",
|
||||
"recentWinners": "Se de seneste vindere",
|
||||
"upcomingEvents": "Kommende begivenheder",
|
||||
"articleBy": "Af {{author}}"
|
||||
|
||||
@@ -10,5 +10,10 @@
|
||||
"STAGE_8": "Inkblot Art Academy",
|
||||
"STAGE_9": "Sturgeon Shipyard",
|
||||
"STAGE_10": "MakoMart",
|
||||
"STAGE_11": "Wahoo World"
|
||||
"STAGE_11": "Wahoo World",
|
||||
"MODE_SHORT_TW": "TW",
|
||||
"MODE_SHORT_SZ": "SZ",
|
||||
"MODE_SHORT_TC": "TC",
|
||||
"MODE_SHORT_RM": "RM",
|
||||
"MODE_SHORT_CB": "CB"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
{
|
||||
"customUrl": "Brugerdefineret URL",
|
||||
"ign": "Splatoon 3 Brugernavn",
|
||||
"ign.short": "Splatnavn",
|
||||
"country": "Land",
|
||||
"bio": "Biografi",
|
||||
"stickSens": "Styrepindsfølsomhed",
|
||||
"motionSens": "Bevægelsesfølsomhed",
|
||||
"motion": "Bevægelse",
|
||||
"stick": "Styrepind",
|
||||
"sens": "Følsomhed",
|
||||
|
||||
"results.placing": "Rang",
|
||||
"results.team": "Hold",
|
||||
"results.tournament": "Turnering",
|
||||
"results.participants": "Deltagere",
|
||||
"results.date": "Dato",
|
||||
"results.mates": "Holdkammerater"
|
||||
"results.mates": "Holdkammerater",
|
||||
|
||||
"forms.errors.invalidCustomUrl.numbers": "Brugerdefineret URL må ikke kun indeholde numre",
|
||||
"forms.errors.invalidCustomUrl.strangeCharacter": "Brugerdefineret URL må ikke indeholde specialtegn (Gælder også æ, ø og å)",
|
||||
"forms.errors.invalidCustomUrl.duplicate": "Brugerdefineret URL er allerede i brug",
|
||||
"forms.errors.invalidSens": "Bevægelsesfølsomhed kan ikke indstilles før at Styrepindsfølsomheden er indstillet"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"inYourTimeZone": " Alle Zeiten sind konvertiert zur lokalen Zeitzone:",
|
||||
"addNew": "Turnier hinzufügen",
|
||||
"inYourTimeZone": "Alle Zeiten sind konvertiert zur lokalen Zeitzone:",
|
||||
"addNew": "Event hinzufügen",
|
||||
"noEvents": "Keine Events in dieser Woche",
|
||||
"reportResults": "Ergebnisse können eingetragen werden für:",
|
||||
"day": "Tag {{number}}",
|
||||
@@ -8,19 +8,21 @@
|
||||
"participatedCount": "{{count}} teilnehmende Teams",
|
||||
"members": "Mitglieder",
|
||||
"results": "Resultate",
|
||||
"createMapList": "Arenen-Liste erstellen",
|
||||
|
||||
"forms.dates": "Datum",
|
||||
"forms.bracketUrl": "Turnierbaum URL",
|
||||
"forms.discordInvite": "Discord server Einladung URL",
|
||||
"forms.bracketUrl": "Turnierbaum-URL",
|
||||
"forms.discordInvite": "Discord-Server Einladungs-URL",
|
||||
"forms.tags": "Tags",
|
||||
"forms.tags.placeholder": "Wähle einen Tag",
|
||||
"forms.tags.info": "\"Abzeichen-Preis\" tag wird automatisch hinzugefügt (falls anwendbar)",
|
||||
"forms.tags.info": "\"Abzeichen-Preis\"-Tag wird automatisch hinzugefügt (falls anwendbar)",
|
||||
"forms.badges": "Abzeichen-Preis",
|
||||
"forms.badges.placeholder": "Wähle ein Abzeichen für das Event",
|
||||
"forms.mapPool": "Arenen-Pool",
|
||||
|
||||
"forms.participantCount": "Anzahl Teilnehmer",
|
||||
"forms.reportResultsHeader": "Berichten der Ergebnisse von {{eventName}}",
|
||||
"forms.reportResultsInfo": "Die Anzahl der eintragbaren Resultate ist frei wählbar. Es kann nur das erste Team sein, die Top 3 oder mehr.",
|
||||
"forms.reportResultsInfo": "Die Anzahl der eintragbaren Ergebnisse ist frei wählbar. Es kann nur das erste Team sein, die Top 3 oder mehr.",
|
||||
"forms.team.add": "Team hinzufügen",
|
||||
"forms.team.remove": "Team löschen",
|
||||
"forms.team.name": "Name des Teams",
|
||||
|
||||
@@ -8,11 +8,17 @@
|
||||
"pages.faq": "FAQ",
|
||||
"pages.builds": "Ausrüstungen",
|
||||
"pages.analyzer": "Ausrüstungs-Analyse",
|
||||
"pages.maps": "Arenen-Listen",
|
||||
|
||||
"header.profile": "Profil",
|
||||
"header.logout": "Ausloggen",
|
||||
"header.login": "Einloggen",
|
||||
|
||||
"auth.errors.aborted": "Einloggen abgebrochen",
|
||||
"auth.errors.failed": "Einloggen fehlgeschlagen",
|
||||
"auth.errors.discordPermissions": "Für dein sendou.ink-Profil benötigt die Seite Zugriff auf den Namen, Avatar und verbundene Social-Media-Accounts in deinem Discord-Profil.",
|
||||
"auth.errors.unknown": "Das Einloggen via Discord ist aus unbekannten Gründen fehlgeschlagen. Falls dies wiederholt auftritt, kontaktiere uns bitte.",
|
||||
|
||||
"footer.github.subtitle": "Sourcecode",
|
||||
"footer.twitter.subtitle": "Updates",
|
||||
"footer.discord.subtitle": "Hilfe & Feedback",
|
||||
@@ -27,6 +33,12 @@
|
||||
"actions.remove": "Entfernen",
|
||||
"actions.delete": "Löschen",
|
||||
"actions.loadMore": "Mehr laden",
|
||||
"actions.close": "Schließen",
|
||||
|
||||
"maps.createMapList": "Arenen-Liste erstellen",
|
||||
"maps.halfSz": "50% Herrschaft",
|
||||
"maps.mapPool": "Arenen-Pool",
|
||||
"maps.tournamentMaplist": "Arenen-Liste für Turnier erstellen (maps.iplabs.ink)",
|
||||
|
||||
"results": "Ergebnisse",
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"plus.description": "Sieh vergangene Plus Server Abstimmungen und mehr",
|
||||
"badges.description": "Liste aller Abzeichen, die du für dein Profil verdienen kannst",
|
||||
"analyzer.description": "Analysiere, was deine Ausrüstungen wirklich bewirken",
|
||||
"maps.description": "Erstelle aus einem Arenen-Pool die Liste für dein Spiel",
|
||||
"recentWinners": "Aktuelle Gewinner",
|
||||
"upcomingEvents": "Bevorstehende Events",
|
||||
"articleBy": "von {{author}}"
|
||||
|
||||
@@ -10,5 +10,10 @@
|
||||
"STAGE_8": "Perlmutt-Akademie",
|
||||
"STAGE_9": "Störwerft",
|
||||
"STAGE_10": "Cetacea-Markt",
|
||||
"STAGE_11": "Flunder-Funpark"
|
||||
"STAGE_11": "Flunder-Funpark",
|
||||
"MODE_SHORT_TW": "RK",
|
||||
"MODE_SHORT_SZ": "HS",
|
||||
"MODE_SHORT_TC": "TK",
|
||||
"MODE_SHORT_RM": "OG",
|
||||
"MODE_SHORT_CB": "MC"
|
||||
}
|
||||
|
||||
@@ -10,12 +10,17 @@
|
||||
"stick": "Stick",
|
||||
"sens": "Empfindlichkeit",
|
||||
|
||||
"results.title": "Ergebnisse",
|
||||
"results.placing": "Platzierung",
|
||||
"results.team": "Team",
|
||||
"results.tournament": "Turnier",
|
||||
"results.participants": "Teilnehmer",
|
||||
"results.date": "Datum",
|
||||
"results.mates": "Mitspieler",
|
||||
"results.highlights": "Highlights",
|
||||
"results.nonHighlights": "Weitere Ergebnisse",
|
||||
"results.highlights.choose": "Highlights wählen",
|
||||
"results.highlights.explanation": "Wähle Ergebnisse, die du hervorheben möchtest",
|
||||
|
||||
"forms.errors.invalidCustomUrl.numbers": "Benutzerdefinierte URL kann nicht nur aus Zahlen bestehen",
|
||||
"forms.errors.invalidCustomUrl.strangeCharacter": "Benutzerdefinierte URL kann nicht aus speziellen Zeichen bestehen",
|
||||
|
||||
@@ -92,5 +92,12 @@
|
||||
"abilityPoints": "Ability points",
|
||||
"abilityPoints.short": "AP",
|
||||
"consumptionExplanation": "This chart shows the amount of actions left to perform with main weapon after using sub 0-{{maxSubsToUse}} times. Max amount of consecutive subs to use with full ink tank is {{maxSubsToUse}}.",
|
||||
"trackingSubDefExplanation": "Point Sensor, Ink Mine and Angle Shooter tracking times are calculated against an opponent with 0AP of Sub Power Up."
|
||||
"trackingSubDefExplanation": "Point Sensor, Ink Mine and Angle Shooter tracking times are calculated against an opponent with 0AP of Sub Power Up.",
|
||||
"distanceInline": "Distance: {{value}}",
|
||||
"damageShort": "DMG",
|
||||
"hitsToDestroyLong": "Hits to destroy",
|
||||
"hitsToDestroyShort": "HTD",
|
||||
"labels.amountOf": "Amount of",
|
||||
"labels.damageType": "Damage type",
|
||||
"labels.weapon": "Weapon"
|
||||
}
|
||||
|
||||
@@ -9,11 +9,17 @@
|
||||
"pages.builds": "Builds",
|
||||
"pages.analyzer": "Build Analyzer",
|
||||
"pages.maps": "Map Lists",
|
||||
"pages.object-damage": "Object Damage Calculator",
|
||||
|
||||
"header.profile": "Profile",
|
||||
"header.logout": "Log out",
|
||||
"header.login": "Log in",
|
||||
|
||||
"auth.errors.aborted": "Login Aborted",
|
||||
"auth.errors.failed": "Login Failed",
|
||||
"auth.errors.discordPermissions": "For your sendou.ink profile, the site needs access to your Discord profile's name, avatar and social connections.",
|
||||
"auth.errors.unknown": "The login via Discord failed for an unknown reason. If this keeps happening, please reach out for help.",
|
||||
|
||||
"footer.github.subtitle": "Source code",
|
||||
"footer.twitter.subtitle": "Updates",
|
||||
"footer.discord.subtitle": "Help & feedback",
|
||||
@@ -29,6 +35,7 @@
|
||||
"actions.delete": "Delete",
|
||||
"actions.loadMore": "Load more",
|
||||
"actions.copyToClipboard": "Copy to clipboard",
|
||||
"actions.close": "Close",
|
||||
|
||||
"maps.createMapList": "Create map list",
|
||||
"maps.halfSz": "50% SZ",
|
||||
|
||||
@@ -10,12 +10,17 @@
|
||||
"stick": "Stick",
|
||||
"sens": "Sens",
|
||||
|
||||
"results.title": "Results",
|
||||
"results.placing": "Placing",
|
||||
"results.team": "Team",
|
||||
"results.tournament": "Tournament",
|
||||
"results.participants": "Participants",
|
||||
"results.date": "Date",
|
||||
"results.mates": "Mates",
|
||||
"results.highlights": "Highlights",
|
||||
"results.nonHighlights": "Other Results",
|
||||
"results.highlights.choose": "Choose Highlights",
|
||||
"results.highlights.explanation": "Select the results you want to highlight",
|
||||
|
||||
"forms.errors.invalidCustomUrl.numbers": "Custom URL can't only contain numbers",
|
||||
"forms.errors.invalidCustomUrl.strangeCharacter": "Custom URL can't contain special characters",
|
||||
|
||||
@@ -314,7 +314,10 @@ function parametersToSpecialWeaponResult(params: any) {
|
||||
resultUnwrapped["SplashAroundPaintRadius"] = undefined;
|
||||
}
|
||||
|
||||
return { overwrites: resultUnwrapped };
|
||||
return {
|
||||
ArmorHP: params["WeaponSpChariotParam"]?.["ArmorHP"],
|
||||
overwrites: resultUnwrapped,
|
||||
};
|
||||
}
|
||||
|
||||
function unwrapSubSpecialSpecUpList(result: any) {
|
||||
|
||||
55
scripts/create-object-dmg-json.ts
Normal file
55
scripts/create-object-dmg-json.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
// 1) WeaponInfoMain.json inside dicts
|
||||
// 2) WeaponInfoSub.json inside dicts
|
||||
// 3) WeaponInfoSpecial.json inside dicts
|
||||
// 4) misc/spl__DamageRateInfoConfig.pp__CombinationDataTableData.json
|
||||
import params from "./dicts/spl__DamageRateInfoConfig.pp__CombinationDataTableData.json";
|
||||
import weapons from "./dicts/WeaponInfoMain.json";
|
||||
import subWeapons from "./dicts/WeaponInfoSub.json";
|
||||
import specialWeapons from "./dicts/WeaponInfoSpecial.json";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const OUTPUT_DIR_PATH = path.join(__dirname, "output");
|
||||
|
||||
const weaponParamsToWeaponIds = (
|
||||
params: typeof weapons | typeof subWeapons | typeof specialWeapons,
|
||||
key: string
|
||||
) => {
|
||||
return params
|
||||
.filter((param) => {
|
||||
return (
|
||||
param.DefaultDamageRateInfoRow === key ||
|
||||
param.ExtraDamageRateInfoRowSet?.some(
|
||||
(row) => row.DamageRateInfoRow === key
|
||||
)
|
||||
);
|
||||
})
|
||||
.map((weapon) => weapon.Id);
|
||||
};
|
||||
|
||||
const result = {};
|
||||
for (const cell of Object.values(params.CellList)) {
|
||||
if (!cell.DamageRate) continue;
|
||||
|
||||
if (!result[cell.RowKey]) {
|
||||
result[cell.RowKey] = {
|
||||
mainWeaponIds: weaponParamsToWeaponIds(weapons, cell.RowKey),
|
||||
subWeaponIds: weaponParamsToWeaponIds(subWeapons, cell.RowKey),
|
||||
specialWeaponIds: weaponParamsToWeaponIds(specialWeapons, cell.RowKey),
|
||||
rates: [],
|
||||
};
|
||||
}
|
||||
|
||||
result[cell.RowKey].rates.push({
|
||||
target: cell.ColumnKey,
|
||||
rate: cell.DamageRate,
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(OUTPUT_DIR_PATH, "object-dmg.json"),
|
||||
JSON.stringify(result, null, 2)
|
||||
);
|
||||
@@ -14,31 +14,22 @@
|
||||
|
||||
**11/11**
|
||||
|
||||
### 🟡 calendar.json
|
||||
### 🟢 calendar.json
|
||||
|
||||
**44/46**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- createMapList
|
||||
- forms.mapPool
|
||||
|
||||
</details>
|
||||
**46/46**
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**49/55**
|
||||
**55/60**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- pages.maps
|
||||
- actions.copyToClipboard
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
- maps.tournamentMaplist
|
||||
- auth.errors.aborted
|
||||
- auth.errors.failed
|
||||
- auth.errors.discordPermissions
|
||||
- auth.errors.unknown
|
||||
- actions.close
|
||||
|
||||
</details>
|
||||
|
||||
@@ -50,52 +41,26 @@
|
||||
|
||||
**6/6**
|
||||
|
||||
### 🟡 front.json
|
||||
### 🟢 front.json
|
||||
|
||||
**10/11**
|
||||
**11/11**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
### 🟢 game-misc.json
|
||||
|
||||
- maps.description
|
||||
|
||||
</details>
|
||||
|
||||
### 🟡 game-misc.json
|
||||
|
||||
**12/17**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- MODE_SHORT_TW
|
||||
- MODE_SHORT_SZ
|
||||
- MODE_SHORT_TC
|
||||
- MODE_SHORT_RM
|
||||
- MODE_SHORT_CB
|
||||
|
||||
</details>
|
||||
**17/17**
|
||||
|
||||
### 🟡 user.json
|
||||
|
||||
**7/20**
|
||||
**20/25**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- customUrl
|
||||
- ign
|
||||
- ign.short
|
||||
- stickSens
|
||||
- motionSens
|
||||
- motion
|
||||
- stick
|
||||
- sens
|
||||
- results.participants
|
||||
- forms.errors.invalidCustomUrl.numbers
|
||||
- forms.errors.invalidCustomUrl.strangeCharacter
|
||||
- forms.errors.invalidCustomUrl.duplicate
|
||||
- forms.errors.invalidSens
|
||||
- results.title
|
||||
- results.highlights
|
||||
- results.nonHighlights
|
||||
- results.highlights.choose
|
||||
- results.highlights.explanation
|
||||
|
||||
</details>
|
||||
|
||||
@@ -115,31 +80,18 @@
|
||||
|
||||
**11/11**
|
||||
|
||||
### 🟡 calendar.json
|
||||
### 🟢 calendar.json
|
||||
|
||||
**44/46**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- createMapList
|
||||
- forms.mapPool
|
||||
|
||||
</details>
|
||||
**46/46**
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**49/55**
|
||||
**59/60**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- pages.maps
|
||||
- actions.copyToClipboard
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
- maps.tournamentMaplist
|
||||
|
||||
</details>
|
||||
|
||||
@@ -151,35 +103,17 @@
|
||||
|
||||
**6/6**
|
||||
|
||||
### 🟡 front.json
|
||||
### 🟢 front.json
|
||||
|
||||
**10/11**
|
||||
**11/11**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
### 🟢 game-misc.json
|
||||
|
||||
- maps.description
|
||||
|
||||
</details>
|
||||
|
||||
### 🟡 game-misc.json
|
||||
|
||||
**12/17**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- MODE_SHORT_TW
|
||||
- MODE_SHORT_SZ
|
||||
- MODE_SHORT_TC
|
||||
- MODE_SHORT_RM
|
||||
- MODE_SHORT_CB
|
||||
|
||||
</details>
|
||||
**17/17**
|
||||
|
||||
### 🟢 user.json
|
||||
|
||||
**20/20**
|
||||
**25/25**
|
||||
|
||||
---
|
||||
|
||||
@@ -220,14 +154,19 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**48/55**
|
||||
**48/60**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- pages.s2
|
||||
- pages.maps
|
||||
- auth.errors.aborted
|
||||
- auth.errors.failed
|
||||
- auth.errors.discordPermissions
|
||||
- auth.errors.unknown
|
||||
- actions.copyToClipboard
|
||||
- actions.close
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
@@ -271,7 +210,7 @@
|
||||
|
||||
### 🟡 user.json
|
||||
|
||||
**7/20**
|
||||
**7/25**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -284,7 +223,12 @@
|
||||
- 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
|
||||
@@ -331,7 +275,7 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**46/55**
|
||||
**46/60**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -339,8 +283,13 @@
|
||||
- pages.s2
|
||||
- pages.analyzer
|
||||
- pages.maps
|
||||
- auth.errors.aborted
|
||||
- auth.errors.failed
|
||||
- auth.errors.discordPermissions
|
||||
- auth.errors.unknown
|
||||
- actions.loadMore
|
||||
- actions.copyToClipboard
|
||||
- actions.close
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
@@ -394,7 +343,7 @@
|
||||
|
||||
### 🟡 user.json
|
||||
|
||||
**7/20**
|
||||
**7/25**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -407,7 +356,12 @@
|
||||
- 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
|
||||
@@ -437,7 +391,7 @@
|
||||
|
||||
### 🔴 common.json
|
||||
|
||||
**0/55**
|
||||
**0/60**
|
||||
|
||||
### 🔴 contributions.json
|
||||
|
||||
@@ -468,7 +422,7 @@
|
||||
|
||||
### 🔴 user.json
|
||||
|
||||
**0/20**
|
||||
**0/25**
|
||||
|
||||
---
|
||||
|
||||
@@ -588,7 +542,7 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**46/55**
|
||||
**46/60**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -596,8 +550,13 @@
|
||||
- pages.s2
|
||||
- pages.analyzer
|
||||
- pages.maps
|
||||
- auth.errors.aborted
|
||||
- auth.errors.failed
|
||||
- auth.errors.discordPermissions
|
||||
- auth.errors.unknown
|
||||
- actions.loadMore
|
||||
- actions.copyToClipboard
|
||||
- actions.close
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
@@ -650,7 +609,7 @@
|
||||
|
||||
### 🟡 user.json
|
||||
|
||||
**7/20**
|
||||
**7/25**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -663,7 +622,12 @@
|
||||
- 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
|
||||
@@ -701,7 +665,7 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**35/55**
|
||||
**35/60**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -709,8 +673,13 @@
|
||||
- pages.s2
|
||||
- pages.analyzer
|
||||
- pages.maps
|
||||
- auth.errors.aborted
|
||||
- auth.errors.failed
|
||||
- auth.errors.discordPermissions
|
||||
- auth.errors.unknown
|
||||
- actions.loadMore
|
||||
- actions.copyToClipboard
|
||||
- actions.close
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
@@ -774,7 +743,7 @@
|
||||
|
||||
### 🟡 user.json
|
||||
|
||||
**7/20**
|
||||
**7/25**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -787,7 +756,12 @@
|
||||
- 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
|
||||
@@ -825,14 +799,19 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**48/55**
|
||||
**48/60**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- pages.analyzer
|
||||
- pages.maps
|
||||
- auth.errors.aborted
|
||||
- auth.errors.failed
|
||||
- auth.errors.discordPermissions
|
||||
- auth.errors.unknown
|
||||
- actions.copyToClipboard
|
||||
- actions.close
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
@@ -876,12 +855,17 @@
|
||||
|
||||
### 🟡 user.json
|
||||
|
||||
**19/20**
|
||||
**19/25**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
|
||||
- results.title
|
||||
- results.participants
|
||||
- results.highlights
|
||||
- results.nonHighlights
|
||||
- results.highlights.choose
|
||||
- results.highlights.explanation
|
||||
|
||||
</details>
|
||||
|
||||
@@ -915,7 +899,7 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**35/55**
|
||||
**35/60**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -923,8 +907,13 @@
|
||||
- pages.s2
|
||||
- pages.analyzer
|
||||
- pages.maps
|
||||
- auth.errors.aborted
|
||||
- auth.errors.failed
|
||||
- auth.errors.discordPermissions
|
||||
- auth.errors.unknown
|
||||
- actions.loadMore
|
||||
- actions.copyToClipboard
|
||||
- actions.close
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
@@ -988,7 +977,7 @@
|
||||
|
||||
### 🟡 user.json
|
||||
|
||||
**7/20**
|
||||
**7/25**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -1001,7 +990,12 @@
|
||||
- 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
|
||||
@@ -1039,7 +1033,7 @@
|
||||
|
||||
### 🟡 common.json
|
||||
|
||||
**35/55**
|
||||
**35/60**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -1047,8 +1041,13 @@
|
||||
- pages.s2
|
||||
- pages.analyzer
|
||||
- pages.maps
|
||||
- auth.errors.aborted
|
||||
- auth.errors.failed
|
||||
- auth.errors.discordPermissions
|
||||
- auth.errors.unknown
|
||||
- actions.loadMore
|
||||
- actions.copyToClipboard
|
||||
- actions.close
|
||||
- maps.createMapList
|
||||
- maps.halfSz
|
||||
- maps.mapPool
|
||||
@@ -1112,7 +1111,7 @@
|
||||
|
||||
### 🟡 user.json
|
||||
|
||||
**7/20**
|
||||
**7/25**
|
||||
|
||||
<details>
|
||||
<summary>Missing</summary>
|
||||
@@ -1125,7 +1124,12 @@
|
||||
- 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
|
||||
|
||||
Reference in New Issue
Block a user