Merge branch 'rewrite' into rewrite
3
.gitignore
vendored
|
|
@ -19,5 +19,4 @@ dump
|
|||
/scripts/output/*
|
||||
!/scripts/output/.gitkeep
|
||||
|
||||
/scripts/dicts/*.json
|
||||
/scripts/dicts/langs/*.json
|
||||
/scripts/dicts/**/*.json
|
||||
12
README.md
|
|
@ -8,22 +8,18 @@ Prerequisites: [nvm](https://github.com/nvm-sh/nvm)
|
|||
|
||||
There is a sequence of commands you need to run:
|
||||
|
||||
1. `nvm use` to switch to the correct Node version. If you have problems with nvm you can also install the latest LTS version of Node.js from [their website](https://nodejs.org/en/).
|
||||
1. `nvm use` to switch to the correct Node version. If you don't have the correct Node.js version yet it will prompt you to install it via the `nvm install` command. If you have problems with nvm you can also install the latest LTS version of Node.js from [their website](https://nodejs.org/en/).
|
||||
2. `npm i` to install the dependencies.
|
||||
3. Make a copy of `.env.example` that's called `.env`. See below for note about environment variables.
|
||||
3. Make a copy of `.env.example` that's called `.env`. Filling additional values is not necessary unless you want to use real Discord auth or develop Lohi bot.
|
||||
4. `npm run migrate up` to set up the database tables.
|
||||
5. `npm run seed` to fill database with test data.
|
||||
6. `npm run dev` to run the project in development mode.
|
||||
5. `npm run dev` to run the project in development mode.
|
||||
6. Navigate to `http://localhost:5800/admin`. There press the seed button to fill the DB with test data. You can also impersonate any user (Sendou#0043 = admin).
|
||||
|
||||
And if you want to run the E2E tests:
|
||||
|
||||
6. Make a copy of the `db.sqlite3` file created by migration and name it `db-cypress.sqlite3`.
|
||||
7. `npm run dev:cypress` and `npm run cy:open` can be used to run the E2E tests.
|
||||
|
||||
#### Environment variables
|
||||
|
||||
You don't need to fill the missing values from `.env.example` to get started. Instead of using real auth via Discord you can "impersonate" the admin (=Sendou#0043) or any other use in the /admin page once the project has started up. `LOHI_TOKEN` is only needed for bot + sendou.ink interoperability.
|
||||
|
||||
## Lohi
|
||||
|
||||
TODO: instructions on how to develop Lohi locally
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export function AbilitiesSelector({
|
|||
onChange(abilitiesClone);
|
||||
};
|
||||
const onButtonClick = (ability: typeof abilities[number]) => {
|
||||
onChange(addAbility({ oldAbilites: selectedAbilities, ability }));
|
||||
onChange(addAbility({ oldAbilities: selectedAbilities, ability }));
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -77,14 +77,14 @@ export function AbilitiesSelector({
|
|||
}
|
||||
|
||||
function addAbility({
|
||||
oldAbilites,
|
||||
oldAbilities,
|
||||
ability,
|
||||
}: {
|
||||
oldAbilites: BuildAbilitiesTupleWithUnknown;
|
||||
oldAbilities: BuildAbilitiesTupleWithUnknown;
|
||||
ability: typeof abilities[number];
|
||||
}): BuildAbilitiesTupleWithUnknown {
|
||||
const abilitiesClone = JSON.parse(
|
||||
JSON.stringify(oldAbilites)
|
||||
JSON.stringify(oldAbilities)
|
||||
) as BuildAbilitiesTupleWithUnknown;
|
||||
|
||||
for (const [i, row] of abilitiesClone.entries()) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type {
|
|||
} from "~/modules/in-game-lists";
|
||||
import type { BuildAbilitiesTuple } from "~/modules/in-game-lists/types";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { gearImageUrl, modeImageUrl, weaponImageUrl } from "~/utils/urls";
|
||||
import { gearImageUrl, modeImageUrl, mainWeaponImageUrl } from "~/utils/urls";
|
||||
import { Ability } from "./Ability";
|
||||
import { Button, LinkButton } from "./Button";
|
||||
import { FormWithConfirm } from "./FormWithConfirm";
|
||||
|
|
@ -128,7 +128,7 @@ export function BuildCard({
|
|||
{weapons.map((weaponSplId) => (
|
||||
<div key={weaponSplId} className="build__weapon">
|
||||
<Image
|
||||
path={weaponImageUrl(weaponSplId)}
|
||||
path={mainWeaponImageUrl(weaponSplId)}
|
||||
alt={t(`weapons:${weaponSplId}` as any)}
|
||||
title={t(`weapons:${weaponSplId}` as any)}
|
||||
height={36}
|
||||
|
|
@ -138,7 +138,7 @@ export function BuildCard({
|
|||
))}
|
||||
{weapons.length === 1 && (
|
||||
<div className="build__weapon-text">
|
||||
{t(`weapons:${weapons[0]!}` as any)}
|
||||
{t(`weapons:MAIN_${weapons[0]!}` as any)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ import {
|
|||
clothesGearIds,
|
||||
headGearIds,
|
||||
shoesGearIds,
|
||||
weaponIds,
|
||||
mainWeaponIds,
|
||||
} from "~/modules/in-game-lists";
|
||||
import { gearImageUrl, weaponImageUrl } from "~/utils/urls";
|
||||
import { gearImageUrl, mainWeaponImageUrl } from "~/utils/urls";
|
||||
import { Image } from "./Image";
|
||||
|
||||
const MAX_RESULTS_SHOWN = 6;
|
||||
|
|
@ -33,6 +33,7 @@ interface ComboboxProps<T> {
|
|||
isLoading?: boolean;
|
||||
required?: boolean;
|
||||
initialValue?: ComboboxOption<T>;
|
||||
clearsInputOnFocus?: boolean;
|
||||
onChange?: (selectedOption?: ComboboxOption<T>) => void;
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ export function Combobox<T extends Record<string, string | null | number>>({
|
|||
inputName,
|
||||
placeholder,
|
||||
initialValue,
|
||||
clearsInputOnFocus = false,
|
||||
onChange,
|
||||
required,
|
||||
className,
|
||||
|
|
@ -49,10 +51,13 @@ export function Combobox<T extends Record<string, string | null | number>>({
|
|||
}: ComboboxProps<T>) {
|
||||
const [selectedOption, setSelectedOption] =
|
||||
React.useState<Unpacked<typeof options>>();
|
||||
const [lastSelectedOption, setLastSelectedOption] =
|
||||
React.useState<Unpacked<typeof options>>();
|
||||
const [query, setQuery] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
setSelectedOption(initialValue);
|
||||
setLastSelectedOption(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
const filteredOptions = (() => {
|
||||
|
|
@ -75,16 +80,27 @@ export function Combobox<T extends Record<string, string | null | number>>({
|
|||
onChange={(selected) => {
|
||||
onChange?.(selected);
|
||||
setSelectedOption(selected);
|
||||
setLastSelectedOption(selected);
|
||||
}}
|
||||
name={inputName}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<HeadlessCombobox.Input
|
||||
onFocus={() => {
|
||||
if (clearsInputOnFocus) {
|
||||
setSelectedOption(undefined);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!selectedOption && clearsInputOnFocus) {
|
||||
setSelectedOption(lastSelectedOption);
|
||||
}
|
||||
}}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={isLoading ? "Loading..." : placeholder}
|
||||
className={clsx("combobox-input", className)}
|
||||
displayValue={(option) =>
|
||||
(option as Unpacked<typeof options>)?.label ?? ""
|
||||
(option as unknown as Unpacked<typeof options>)?.label ?? ""
|
||||
}
|
||||
data-cy={`${inputName}-combobox-input`}
|
||||
id={id}
|
||||
|
|
@ -192,28 +208,39 @@ export function WeaponCombobox({
|
|||
inputName,
|
||||
onChange,
|
||||
initialWeaponId,
|
||||
clearsInputOnFocus,
|
||||
}: Pick<
|
||||
ComboboxProps<ComboboxBaseOption>,
|
||||
"inputName" | "onChange" | "className" | "id" | "required"
|
||||
> & { initialWeaponId?: typeof weaponIds[number] }) {
|
||||
| "inputName"
|
||||
| "onChange"
|
||||
| "className"
|
||||
| "id"
|
||||
| "required"
|
||||
| "clearsInputOnFocus"
|
||||
> & { initialWeaponId?: typeof mainWeaponIds[number] }) {
|
||||
const { t } = useTranslation("weapons");
|
||||
|
||||
const idToWeapon = (id: typeof weaponIds[number]) => ({
|
||||
const idToWeapon = (id: typeof mainWeaponIds[number]) => ({
|
||||
value: String(id),
|
||||
label: t(`${id}`),
|
||||
imgPath: weaponImageUrl(id),
|
||||
label: t(`MAIN_${id}`),
|
||||
imgPath: mainWeaponImageUrl(id),
|
||||
});
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
inputName={inputName}
|
||||
options={weaponIds.map(idToWeapon)}
|
||||
initialValue={initialWeaponId ? idToWeapon(initialWeaponId) : undefined}
|
||||
placeholder={t(`${weaponIds[0]}`)}
|
||||
options={mainWeaponIds.map(idToWeapon)}
|
||||
initialValue={
|
||||
typeof initialWeaponId === "number"
|
||||
? idToWeapon(initialWeaponId)
|
||||
: undefined
|
||||
}
|
||||
placeholder={t(`MAIN_${mainWeaponIds[0]}`)}
|
||||
onChange={onChange}
|
||||
className={className}
|
||||
id={id}
|
||||
required={required}
|
||||
clearsInputOnFocus={clearsInputOnFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ type LabelProps = Pick<
|
|||
max: number;
|
||||
};
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Label({
|
||||
|
|
@ -19,9 +20,10 @@ export function Label({
|
|||
required,
|
||||
children,
|
||||
htmlFor,
|
||||
className,
|
||||
}: LabelProps) {
|
||||
return (
|
||||
<div className="label__container">
|
||||
<div className={clsx("label__container", className)}>
|
||||
<label htmlFor={htmlFor}>
|
||||
{children} {required && <span className="text-error">*</span>}
|
||||
</label>
|
||||
|
|
|
|||
22
app/components/Toggle.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { Switch } from "@headlessui/react";
|
||||
import clsx from "clsx";
|
||||
|
||||
export function Toggle({
|
||||
checked,
|
||||
setChecked,
|
||||
tiny,
|
||||
}: {
|
||||
checked: boolean;
|
||||
setChecked: (checked: boolean) => void;
|
||||
tiny?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Switch
|
||||
checked={checked}
|
||||
onChange={setChecked}
|
||||
className={clsx("toggle", { checked, tiny })}
|
||||
>
|
||||
<span className={clsx("toggle-dot", { checked, tiny })} />
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import allTags from "~/routes/calendar/tags.json";
|
||||
import type { CalendarEventTag } from "./db/types";
|
||||
import type { BuildAbilitiesTupleWithUnknown } from "./modules/in-game-lists";
|
||||
|
||||
export const TWEET_LENGTH_MAX_LENGTH = 280;
|
||||
export const DISCORD_MESSAGE_MAX_LENGTH = 2000;
|
||||
|
|
@ -14,7 +15,7 @@ export const CALENDAR_EVENT = {
|
|||
NAME_MAX_LENGTH: 100,
|
||||
DESCRIPTION_MAX_LENGTH: DISCORD_MESSAGE_MAX_LENGTH,
|
||||
DISCORD_INVITE_CODE_MAX_LENGTH: 50,
|
||||
BRACKET_URL_MAX_LENGTH: 100,
|
||||
BRACKET_URL_MAX_LENGTH: 200,
|
||||
MAX_AMOUNT_OF_DATES: 5,
|
||||
TAGS: Object.keys(allTags) as Array<CalendarEventTag>,
|
||||
};
|
||||
|
|
@ -35,6 +36,12 @@ export const BUILD = {
|
|||
MAX_COUNT: 250,
|
||||
} as const;
|
||||
|
||||
export const EMPTY_BUILD: BuildAbilitiesTupleWithUnknown = [
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
];
|
||||
|
||||
export const PLUS_TIERS = [1, 2, 3];
|
||||
|
||||
export const PLUS_UPVOTE = 1;
|
||||
|
|
|
|||
|
|
@ -24,5 +24,5 @@ set
|
|||
"discordDiscriminator" = excluded."discordDiscriminator",
|
||||
"discordAvatar" = excluded."discordAvatar",
|
||||
"twitch" = excluded."twitch",
|
||||
"twitch" = excluded."twitch",
|
||||
"youtubeId" = excluded."youtubeId" returning *
|
||||
"twitter" = excluded."twitter",
|
||||
"youtubeId" = excluded."youtubeId" returning *
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
headGearIds,
|
||||
modesShort,
|
||||
shoesGearIds,
|
||||
weaponIds,
|
||||
mainWeaponIds,
|
||||
} from "~/modules/in-game-lists";
|
||||
import {
|
||||
lastCompletedVoting,
|
||||
|
|
@ -503,7 +503,7 @@ function adminBuilds() {
|
|||
const randomOrderHeadGear = shuffle(headGearIds.slice());
|
||||
const randomOrderClothesGear = shuffle(clothesGearIds.slice());
|
||||
const randomOrderShoesGear = shuffle(shoesGearIds.slice());
|
||||
const randomOrderWeaponIds = shuffle(weaponIds.slice());
|
||||
const randomOrderWeaponIds = shuffle(mainWeaponIds.slice());
|
||||
|
||||
const randomAbility = (legalTypes: AbilityType[]) => {
|
||||
const randomOrderAbilities = shuffle([...abilities]);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Ability } from "~/modules/in-game-lists";
|
||||
import type { Ability, MainWeaponId } from "~/modules/in-game-lists";
|
||||
import type allTags from "../routes/calendar/tags.json";
|
||||
|
||||
export interface User {
|
||||
|
|
@ -133,7 +133,7 @@ export interface Build {
|
|||
|
||||
export interface BuildWeapon {
|
||||
buildId: number;
|
||||
weaponSplId: number;
|
||||
weaponSplId: MainWeaponId;
|
||||
}
|
||||
|
||||
export type GearType = "HEAD" | "CLOTHES" | "SHOES";
|
||||
|
|
|
|||
49
app/modules/analyzer/ability-values.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"ConsumeRt_Main": [0.55, 0.775, 1.0],
|
||||
"ConsumeRt_Sub_Lv0": [0.8, 0.9, 1.0],
|
||||
"ConsumeRt_Sub_Lv1": [0.7, 0.85, 1.0],
|
||||
"ConsumeRt_Sub_Lv2": [0.65, 0.825, 1.0],
|
||||
"ConsumeRt_Sub_Lv3": [0.6, 0.8, 1.0],
|
||||
"DamageRt_BombH": [0.5, 0.75, 1.0],
|
||||
"DamageRt_BombL": [0.6, 0.8, 1.0],
|
||||
"DamageRt_LineMarker": [0.5, 0.75, 1.0],
|
||||
"DamageRt_Shield": [0.5, 0.75, 1.0],
|
||||
"DamageRt_Sprinkler": [0.5, 0.75, 1.0],
|
||||
"Dying_AroundFrm": [30.0, 60.0, 90.0],
|
||||
"Dying_ChaseFrm": [90.0, 180.0, 270.0],
|
||||
"IncreaseRt_Special": [1.3, 1.15, 1.0],
|
||||
"InkRecoverFrm_Std": [220.0, 410.0, 600.0],
|
||||
"InkRecoverFrm_Stealth": [117.0, 148.5, 180.0],
|
||||
"MarkingTimeRt": [0.1, 0.43, 1.0],
|
||||
"MarkingTimeRt_Trap": [0.1, 0.55, 1.0],
|
||||
"MoveDownRt_PoisonMist": [0.5, 0.75, 1.0],
|
||||
"MoveVelRt_Shot": [1.25, 1.125, 1.0],
|
||||
"MoveVel_Human": [0.144, 0.12, 0.096],
|
||||
"MoveVel_Human_Fast": [0.144, 0.124, 0.104],
|
||||
"MoveVel_Human_Slow": [0.144, 0.116, 0.088],
|
||||
"MoveVel_Stealth": [0.24, 0.216, 0.192],
|
||||
"MoveVel_Stealth_Fast": [0.24, 0.2208, 0.2016],
|
||||
"MoveVel_Stealth_Slow": [0.24, 0.216, 0.1728],
|
||||
"OpInk_ArmorHP": [39.0, 26.0, 0.0],
|
||||
"OpInk_DamageLmt": [0.2, 0.3, 0.4],
|
||||
"OpInk_DamagePerFrame": [0.0015, 0.0022, 0.003],
|
||||
"OpInk_JumpVel": [0.11, 0.098, 0.08],
|
||||
"OpInk_MoveVel": [0.0768, 0.0557, 0.024],
|
||||
"OpInk_MoveVel_Shot": [0.042, 0.033, 0.012],
|
||||
"OpInk_MoveVel_ShotK": [1.0, 0.75, 0.5],
|
||||
"Overwrite_ConsumeRt_Main": [-1.0, -1.0, -1.0],
|
||||
"Overwrite_MoveVelRt_Shot": [-1.0, -1.0, -1.0],
|
||||
"Somersault_MoveVelKd": [1.0, 0.925, 0.85],
|
||||
"SpecialGaugeRt_Restart": [1.0, 0.8, 0.5],
|
||||
"SuperJump_ChargeFrm": [20.0, 35.0, 80.0],
|
||||
"SuperJump_MoveFrm": [96.6, 132.3, 138.0],
|
||||
"WallJumpChargeFrm": [0.0, 0.0, 0.0],
|
||||
"ReduceJumpSwerveRate": [1.0, 0.75, 0.0],
|
||||
"SpawnSpeedZSpecUp": [0.0, 0.0, 0.0],
|
||||
"PeriodFirst": [0.0, 0.0, 0.0],
|
||||
"PeriodSecond": [0.0, 0.0, 0.0],
|
||||
"MarkingFrameSubSpec": [0.0, 0.0, 0.0],
|
||||
"SensorRadius": [0.0, 0.0, 0.0],
|
||||
"ExplosionRadius": [0.0, 0.0, 0.0],
|
||||
"MaxHP": [0.0, 0.0, 0.0]
|
||||
}
|
||||
2
app/modules/analyzer/constants.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const MAX_LDE_INTENSITY = 21;
|
||||
export const MAX_AP = 57;
|
||||
14
app/modules/analyzer/index.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export type {
|
||||
DistanceDamage,
|
||||
MainWeaponParams,
|
||||
SubWeaponParams,
|
||||
Stat,
|
||||
AnalyzedBuild,
|
||||
SpecialEffectType,
|
||||
} from "./types";
|
||||
|
||||
export { useAnalyzeBuild } from "./useAnalyzeBuild";
|
||||
|
||||
export { MAX_LDE_INTENSITY } from "./constants";
|
||||
|
||||
export { lastDitchEffortIntensityToAp } from "./specialEffects";
|
||||
95
app/modules/analyzer/specialEffects.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { applySpecialEffects } from "./specialEffects";
|
||||
|
||||
const ApplySpecialEffects = suite("applySpecialEffects()");
|
||||
|
||||
ApplySpecialEffects("Adds an effect to empty build", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["CB"],
|
||||
abilityPoints: new Map(),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.size, 6);
|
||||
assert.equal(aps.get("ISM"), 10);
|
||||
});
|
||||
|
||||
ApplySpecialEffects(
|
||||
"Adds an effect to build while keeping existing abilities untouched",
|
||||
() => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["CB"],
|
||||
abilityPoints: new Map([["SPU", 10]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.size, 7);
|
||||
assert.equal(aps.get("SPU"), 10);
|
||||
}
|
||||
);
|
||||
|
||||
ApplySpecialEffects("Does not boost ability beyond 57", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["CB"],
|
||||
abilityPoints: new Map([["ISM", 57]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("ISM"), 57);
|
||||
});
|
||||
|
||||
ApplySpecialEffects("Tacticooler doesn't boost swim speed beyond 29", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["TACTICOOLER"],
|
||||
abilityPoints: new Map([["SSU", 28]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("SSU"), 29);
|
||||
});
|
||||
|
||||
ApplySpecialEffects(
|
||||
"Tacticooler limit swim speed at 29 if more in build",
|
||||
() => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["TACTICOOLER"],
|
||||
abilityPoints: new Map([["SSU", 30]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("SSU"), 30);
|
||||
}
|
||||
);
|
||||
|
||||
ApplySpecialEffects("Applies many effects", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["DR", "CB"],
|
||||
abilityPoints: new Map([["SSU", 1]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("SSU"), 21);
|
||||
});
|
||||
|
||||
ApplySpecialEffects("Applies LDE", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["LDE"],
|
||||
abilityPoints: new Map([["ISM", 1]]),
|
||||
ldeIntensity: 1,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("ISM"), 2);
|
||||
});
|
||||
|
||||
ApplySpecialEffects("Applies LDE (intensity != aps given)", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["LDE"],
|
||||
abilityPoints: new Map([["ISM", 1]]),
|
||||
ldeIntensity: 15,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("ISM"), 18);
|
||||
});
|
||||
|
||||
ApplySpecialEffects.run();
|
||||
174
app/modules/analyzer/specialEffects.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import { MAX_AP } from "./constants";
|
||||
import type { AbilityPoints } from "./types";
|
||||
|
||||
export const SPECIAL_EFFECTS = [
|
||||
{
|
||||
type: "DR",
|
||||
values: [
|
||||
{
|
||||
type: "SSU",
|
||||
ap: 10,
|
||||
},
|
||||
{
|
||||
type: "RSU",
|
||||
ap: 10,
|
||||
},
|
||||
{
|
||||
type: "RES",
|
||||
ap: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "OG",
|
||||
values: [
|
||||
{
|
||||
type: "SSU",
|
||||
ap: 30,
|
||||
},
|
||||
{
|
||||
type: "RSU",
|
||||
ap: 30,
|
||||
},
|
||||
{
|
||||
type: "RES",
|
||||
ap: 30,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "LDE",
|
||||
values: lastDitchEffortValues,
|
||||
},
|
||||
{
|
||||
type: "CB",
|
||||
values: [
|
||||
{
|
||||
type: "ISM",
|
||||
ap: 10,
|
||||
},
|
||||
{
|
||||
type: "ISS",
|
||||
ap: 10,
|
||||
},
|
||||
{
|
||||
type: "IRU",
|
||||
ap: 10,
|
||||
},
|
||||
{
|
||||
type: "RSU",
|
||||
ap: 10,
|
||||
},
|
||||
{
|
||||
type: "SSU",
|
||||
ap: 10,
|
||||
},
|
||||
{
|
||||
type: "SCU",
|
||||
ap: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "TACTICOOLER",
|
||||
values: [
|
||||
{
|
||||
type: "SSU",
|
||||
ap: 29,
|
||||
boostsBeyond: false,
|
||||
},
|
||||
{
|
||||
type: "RSU",
|
||||
ap: 29,
|
||||
boostsBeyond: false,
|
||||
},
|
||||
{
|
||||
type: "RES",
|
||||
ap: MAX_AP,
|
||||
},
|
||||
{
|
||||
type: "QR",
|
||||
ap: MAX_AP,
|
||||
},
|
||||
{
|
||||
type: "QSJ",
|
||||
ap: MAX_AP,
|
||||
},
|
||||
{
|
||||
type: "SS",
|
||||
ap: MAX_AP,
|
||||
},
|
||||
{
|
||||
type: "IA",
|
||||
ap: MAX_AP,
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function lastDitchEffortIntensityToAp(intensity: number) {
|
||||
return Math.floor((24 / 21) * intensity);
|
||||
}
|
||||
|
||||
function lastDitchEffortValues(intensity: number) {
|
||||
const ap = lastDitchEffortIntensityToAp(intensity);
|
||||
|
||||
return [
|
||||
{
|
||||
type: "ISM",
|
||||
ap,
|
||||
},
|
||||
{
|
||||
type: "ISS",
|
||||
ap,
|
||||
},
|
||||
{
|
||||
type: "IRU",
|
||||
ap,
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
|
||||
export function applySpecialEffects({
|
||||
abilityPoints,
|
||||
effects,
|
||||
ldeIntensity,
|
||||
}: {
|
||||
abilityPoints: AbilityPoints;
|
||||
effects: Array<typeof SPECIAL_EFFECTS[number]["type"]>;
|
||||
ldeIntensity: number;
|
||||
}): AbilityPoints {
|
||||
const result: AbilityPoints = new Map(abilityPoints);
|
||||
|
||||
for (const effectObj of SPECIAL_EFFECTS) {
|
||||
if (!effects.includes(effectObj.type)) continue;
|
||||
|
||||
const valuesArr = effectObjToValuesArr({ effectObj, ldeIntensity });
|
||||
|
||||
for (const value of valuesArr) {
|
||||
const boostsBeyond = "boostsBeyond" in value ? value.boostsBeyond : true;
|
||||
const currentAP = result.get(value.type) ?? 0;
|
||||
const newAP = boostsBeyond
|
||||
? currentAP + value.ap
|
||||
: Math.max(currentAP, value.ap);
|
||||
|
||||
result.set(value.type, Math.min(newAP, MAX_AP));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function effectObjToValuesArr({
|
||||
effectObj,
|
||||
ldeIntensity,
|
||||
}: {
|
||||
effectObj: typeof SPECIAL_EFFECTS[number];
|
||||
ldeIntensity: number;
|
||||
}) {
|
||||
if (typeof effectObj.values === "function") {
|
||||
return effectObj.values(ldeIntensity);
|
||||
}
|
||||
|
||||
return effectObj.values;
|
||||
}
|
||||
827
app/modules/analyzer/stats.ts
Normal file
|
|
@ -0,0 +1,827 @@
|
|||
import type { Ability, MainWeaponId } from "~/modules/in-game-lists";
|
||||
import { ANGLE_SHOOTER_ID } from "~/modules/in-game-lists";
|
||||
import { INK_MINE_ID, POINT_SENSOR_ID } from "~/modules/in-game-lists";
|
||||
import type {
|
||||
AbilityPoints,
|
||||
AnalyzedBuild,
|
||||
DamageType,
|
||||
InkConsumeType,
|
||||
MainWeaponParams,
|
||||
StatFunctionInput,
|
||||
SubWeaponParams,
|
||||
} from "./types";
|
||||
import { DAMAGE_TYPE } from "./types";
|
||||
import { INK_CONSUME_TYPES } from "./types";
|
||||
import invariant from "tiny-invariant";
|
||||
import {
|
||||
abilityPointsToEffects,
|
||||
apFromMap,
|
||||
hasEffect,
|
||||
weaponParams,
|
||||
} from "./utils";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { semiRandomId } from "~/utils/strings";
|
||||
import { roundToTwoDecimalPlaces } from "~/utils/number";
|
||||
|
||||
export function buildStats({
|
||||
abilityPoints,
|
||||
weaponSplId,
|
||||
mainOnlyAbilities,
|
||||
}: {
|
||||
abilityPoints: AbilityPoints;
|
||||
weaponSplId: MainWeaponId;
|
||||
mainOnlyAbilities: Array<Ability>;
|
||||
}): AnalyzedBuild {
|
||||
const mainWeaponParams = weaponParams().mainWeapons[weaponSplId];
|
||||
invariant(mainWeaponParams, `Weapon with splId ${weaponSplId} not found`);
|
||||
|
||||
const subWeaponParams =
|
||||
weaponParams().subWeapons[mainWeaponParams.subWeaponId];
|
||||
invariant(
|
||||
subWeaponParams,
|
||||
`Sub weapon with splId ${mainWeaponParams.subWeaponId} not found`
|
||||
);
|
||||
|
||||
const input: StatFunctionInput = {
|
||||
mainWeaponParams,
|
||||
subWeaponParams,
|
||||
abilityPoints,
|
||||
mainOnlyAbilities,
|
||||
};
|
||||
|
||||
return {
|
||||
weapon: {
|
||||
subWeaponSplId: mainWeaponParams.subWeaponId,
|
||||
specialWeaponSplId: mainWeaponParams.specialWeaponId,
|
||||
brellaCanopyHp: mainWeaponParams.CanopyHP,
|
||||
fullChargeSeconds: mainWeaponParams.ChargeFrameFullCharge
|
||||
? framesToSeconds(mainWeaponParams.ChargeFrameFullCharge)
|
||||
: undefined,
|
||||
maxChargeHoldSeconds: mainWeaponParams.KeepChargeFullFrame
|
||||
? framesToSeconds(mainWeaponParams.KeepChargeFullFrame)
|
||||
: undefined,
|
||||
speedType: mainWeaponParams.WeaponSpeedType ?? "Normal",
|
||||
isTripleShooter: Boolean(mainWeaponParams.TripleShotSpanFrame),
|
||||
},
|
||||
stats: {
|
||||
specialPoint: specialPoint(input),
|
||||
specialSavedAfterDeath: specialSavedAfterDeath(input),
|
||||
fullInkTankOptions: fullInkTankOptions(input),
|
||||
damages: damages(input),
|
||||
mainWeaponWhiteInkSeconds:
|
||||
typeof mainWeaponParams.InkRecoverStop === "number"
|
||||
? framesToSeconds(mainWeaponParams.InkRecoverStop)
|
||||
: undefined,
|
||||
subWeaponWhiteInkSeconds: framesToSeconds(subWeaponParams.InkRecoverStop),
|
||||
squidFormInkRecoverySeconds: squidFormInkRecoverySeconds(input),
|
||||
runSpeed: runSpeed(input),
|
||||
// shootingRunSpeed: shootingRunSpeed(input),
|
||||
swimSpeed: swimSpeed(input),
|
||||
runSpeedInEnemyInk: runSpeedInEnemyInk(input),
|
||||
damageTakenInEnemyInkPerSecond: damageTakenInEnemyInkPerSecond(input),
|
||||
enemyInkDamageLimit: enemyInkDamageLimit(input),
|
||||
framesBeforeTakingDamageInEnemyInk:
|
||||
framesBeforeTakingDamageInEnemyInk(input),
|
||||
quickRespawnTime: quickRespawnTime(input),
|
||||
superJumpTimeGroundFrames: superJumpTimeGroundFrames(input),
|
||||
superJumpTimeTotal: superJumpTimeTotal(input),
|
||||
subDefPointSensorMarkedTimeInSeconds:
|
||||
subDefPointSensorMarkedTimeInSeconds(input),
|
||||
subDefInkMineMarkedTimeInSeconds: subDefInkMineMarkedTimeInSeconds(input),
|
||||
subDefAngleShooterMarkedTimeInSeconds:
|
||||
subDefAngleShooterMarkedTimeInSeconds(input),
|
||||
subDefToxicMistMovementReduction: subDefToxicMistMovementReduction(input),
|
||||
subDefAngleShooterDamage: subDefAngleShooterDamage(input),
|
||||
subDefSplashWallDamagePercentage: subDefSplashWallDamagePercentage(input),
|
||||
subDefSprinklerDamagePercentage: subDefSprinklerDamagePercentage(input),
|
||||
...subStats(input),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function specialPoint({
|
||||
abilityPoints,
|
||||
mainWeaponParams,
|
||||
}: StatFunctionInput): AnalyzedBuild["stats"]["specialPoint"] {
|
||||
const SPECIAL_POINT_ABILITY = "SCU";
|
||||
|
||||
const { effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: abilityPoints,
|
||||
ability: SPECIAL_POINT_ABILITY,
|
||||
}),
|
||||
key: "IncreaseRt_Special",
|
||||
weapon: mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: mainWeaponParams.SpecialPoint,
|
||||
modifiedBy: SPECIAL_POINT_ABILITY,
|
||||
value: Math.ceil(mainWeaponParams.SpecialPoint / effect),
|
||||
};
|
||||
}
|
||||
|
||||
function specialSavedAfterDeath({
|
||||
abilityPoints,
|
||||
mainWeaponParams,
|
||||
}: StatFunctionInput): AnalyzedBuild["stats"]["specialPoint"] {
|
||||
const SPECIAL_SAVED_AFTER_DEATH_ABILITY = "SS";
|
||||
const specialSavedAfterDeathForDisplay = (effect: number) =>
|
||||
Number(((1.0 - effect) * 100).toFixed(2));
|
||||
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: abilityPoints,
|
||||
ability: SPECIAL_SAVED_AFTER_DEATH_ABILITY,
|
||||
}),
|
||||
key: "SpecialGaugeRt_Restart",
|
||||
weapon: mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: specialSavedAfterDeathForDisplay(baseEffect),
|
||||
value: specialSavedAfterDeathForDisplay(effect),
|
||||
modifiedBy: SPECIAL_SAVED_AFTER_DEATH_ABILITY,
|
||||
};
|
||||
}
|
||||
|
||||
function fullInkTankOptions(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["fullInkTankOptions"] {
|
||||
const result: AnalyzedBuild["stats"]["fullInkTankOptions"] = [];
|
||||
|
||||
const { inkConsume: subWeaponInkConsume, maxSubsFromFullInkTank } =
|
||||
subWeaponConsume(args);
|
||||
|
||||
for (
|
||||
let subsFromFullInkTank = 0;
|
||||
subsFromFullInkTank <= maxSubsFromFullInkTank;
|
||||
subsFromFullInkTank++
|
||||
) {
|
||||
for (const type of INK_CONSUME_TYPES) {
|
||||
const mainWeaponInkConsume = mainWeaponInkConsumeByType({
|
||||
type,
|
||||
...args,
|
||||
});
|
||||
|
||||
if (typeof mainWeaponInkConsume !== "number") continue;
|
||||
|
||||
result.push({
|
||||
id: semiRandomId(),
|
||||
subsUsed: subsFromFullInkTank,
|
||||
type,
|
||||
value: effectToRounded(
|
||||
(1 - subWeaponInkConsume * subsFromFullInkTank) / mainWeaponInkConsume
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function effectToRounded(effect: number) {
|
||||
return Number(effect.toFixed(2));
|
||||
}
|
||||
|
||||
function subWeaponConsume({
|
||||
mainWeaponParams,
|
||||
subWeaponParams,
|
||||
abilityPoints,
|
||||
}: StatFunctionInput) {
|
||||
const { effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints,
|
||||
ability: "ISS",
|
||||
}),
|
||||
// xxx: placeholder fallback before prod
|
||||
key: `ConsumeRt_Sub_Lv${subWeaponParams.SubInkSaveLv ?? 0}`,
|
||||
weapon: mainWeaponParams,
|
||||
});
|
||||
|
||||
// xxx: placeholder fallback before prod
|
||||
const inkConsume = subWeaponParams.InkConsume ?? 0.6;
|
||||
|
||||
const inkConsumeAfterISS = inkConsume * effect;
|
||||
|
||||
return {
|
||||
inkConsume: inkConsumeAfterISS,
|
||||
maxSubsFromFullInkTank: Math.floor(1 / inkConsumeAfterISS),
|
||||
};
|
||||
}
|
||||
|
||||
function mainWeaponInkConsumeByType({
|
||||
mainWeaponParams,
|
||||
abilityPoints,
|
||||
type,
|
||||
}: {
|
||||
type: InkConsumeType;
|
||||
} & StatFunctionInput) {
|
||||
const { effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints,
|
||||
ability: "ISM",
|
||||
}),
|
||||
key: "ConsumeRt_Main",
|
||||
weapon: mainWeaponParams,
|
||||
});
|
||||
|
||||
// these keys are always mutually exclusive i.e. even if inkConsumeTypeToParamsKeys() returns many keys
|
||||
// then weapon params of this weapon should only have one defined
|
||||
for (const key of inkConsumeTypeToParamsKeys(type)) {
|
||||
const value = mainWeaponParams[key];
|
||||
|
||||
if (typeof value === "number") {
|
||||
return value * effect;
|
||||
}
|
||||
}
|
||||
|
||||
// not all weapons have all ink consume types
|
||||
// i.e. blaster does not (hopefully) perform dualie dodge rolls
|
||||
return;
|
||||
}
|
||||
|
||||
function inkConsumeTypeToParamsKeys(
|
||||
type: InkConsumeType
|
||||
): Array<keyof MainWeaponParams> {
|
||||
switch (type) {
|
||||
case "NORMAL":
|
||||
return ["InkConsume", "InkConsume_WeaponShelterShotgunParam"];
|
||||
case "SWING":
|
||||
return ["InkConsume_SwingParam", "InkConsume_WeaponSwingParam"];
|
||||
case "SLOSH":
|
||||
return ["InkConsumeSlosher"];
|
||||
case "TAP_SHOT":
|
||||
return ["InkConsumeMinCharge"];
|
||||
case "FULL_CHARGE":
|
||||
return ["InkConsumeFullCharge", "InkConsumeFullCharge_ChargeParam"];
|
||||
case "SPLATLING_CHARGE":
|
||||
return ["InkConsumeFullChargeSplatling"];
|
||||
case "HORIZONTAL_SWING":
|
||||
return ["InkConsume_WeaponWideSwingParam"];
|
||||
case "VERTICAL_SWING":
|
||||
return ["InkConsume_WeaponVerticalSwingParam"];
|
||||
case "DUALIE_ROLL":
|
||||
return ["InkConsume_SideStepParam"];
|
||||
case "SHIELD_LAUNCH":
|
||||
return ["InkConsumeUmbrella_WeaponShelterCanopyParam"];
|
||||
default: {
|
||||
assertUnreachable(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const damageTypeToParamsKey: Record<
|
||||
DamageType,
|
||||
keyof MainWeaponParams | keyof SubWeaponParams
|
||||
> = {
|
||||
NORMAL_MIN: "DamageParam_ValueMin",
|
||||
NORMAL_MAX: "DamageParam_ValueMax",
|
||||
DIRECT: "DamageParam_ValueDirect",
|
||||
DISTANCE: "BlastParam_DistanceDamage",
|
||||
FULL_CHARGE: "DamageParam_ValueFullCharge",
|
||||
MAX_CHARGE: "DamageParam_ValueMaxCharge",
|
||||
TAP_SHOT: "DamageParam_ValueMinCharge",
|
||||
BOMB_NORMAL: "DistanceDamage",
|
||||
BOMB_DIRECT: "DirectDamage",
|
||||
};
|
||||
|
||||
function damages(args: StatFunctionInput): AnalyzedBuild["stats"]["damages"] {
|
||||
const result: AnalyzedBuild["stats"]["damages"] = [];
|
||||
|
||||
for (const type of DAMAGE_TYPE) {
|
||||
const key = damageTypeToParamsKey[type];
|
||||
const value =
|
||||
args.mainWeaponParams[key as keyof MainWeaponParams] ??
|
||||
args.subWeaponParams[key as keyof SubWeaponParams];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (const subValue of value) {
|
||||
result.push({
|
||||
type,
|
||||
value: subValue.Damage / 10,
|
||||
distance: subValue.Distance,
|
||||
id: semiRandomId(),
|
||||
});
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value !== "number") continue;
|
||||
|
||||
result.push({
|
||||
id: semiRandomId(),
|
||||
type,
|
||||
value: value / 10,
|
||||
shotsToSplat: shotsToSplat({
|
||||
value,
|
||||
type,
|
||||
isTripleShooter: Boolean(args.mainWeaponParams.TripleShotSpanFrame),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function shotsToSplat({
|
||||
value,
|
||||
type,
|
||||
isTripleShooter,
|
||||
}: {
|
||||
value: number;
|
||||
type: DamageType;
|
||||
isTripleShooter: boolean;
|
||||
}) {
|
||||
if (type !== "NORMAL_MAX") return;
|
||||
|
||||
const multiplier = isTripleShooter ? 3 : 1;
|
||||
|
||||
return Math.ceil(1000 / (value * multiplier));
|
||||
}
|
||||
|
||||
const framesToSeconds = (frames: number) =>
|
||||
effectToRounded(Math.ceil(frames) / 60);
|
||||
function squidFormInkRecoverySeconds(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["squidFormInkRecoverySeconds"] {
|
||||
const SQUID_FORM_INK_RECOVERY_SECONDS_ABILITY = "IRU";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: SQUID_FORM_INK_RECOVERY_SECONDS_ABILITY,
|
||||
}),
|
||||
key: "InkRecoverFrm_Stealth",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: framesToSeconds(baseEffect),
|
||||
value: framesToSeconds(effect),
|
||||
modifiedBy: SQUID_FORM_INK_RECOVERY_SECONDS_ABILITY,
|
||||
};
|
||||
}
|
||||
|
||||
function runSpeed(args: StatFunctionInput): AnalyzedBuild["stats"]["runSpeed"] {
|
||||
const key =
|
||||
args.mainWeaponParams.WeaponSpeedType === "Fast"
|
||||
? "_Fast"
|
||||
: args.mainWeaponParams.WeaponSpeedType === "Slow"
|
||||
? "_Slow"
|
||||
: "";
|
||||
const RUN_SPEED_ABILITY = "RSU";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: RUN_SPEED_ABILITY,
|
||||
}),
|
||||
key: `MoveVel_Human${key}`,
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: effectToRounded(baseEffect * 10),
|
||||
value: effectToRounded(effect * 10),
|
||||
modifiedBy: RUN_SPEED_ABILITY,
|
||||
};
|
||||
}
|
||||
|
||||
function runSpeedInEnemyInk(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["runSpeedInEnemyInk"] {
|
||||
const RUN_SPEED_IN_ENEMY_INK_ABILITY = "RES";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: RUN_SPEED_IN_ENEMY_INK_ABILITY,
|
||||
}),
|
||||
key: "OpInk_MoveVel",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: effectToRounded(baseEffect * 10),
|
||||
value: effectToRounded(effect * 10),
|
||||
modifiedBy: RUN_SPEED_IN_ENEMY_INK_ABILITY,
|
||||
};
|
||||
}
|
||||
|
||||
function swimSpeed(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["swimSpeed"] {
|
||||
const key =
|
||||
args.mainWeaponParams.WeaponSpeedType === "Fast"
|
||||
? "_Fast"
|
||||
: args.mainWeaponParams.WeaponSpeedType === "Slow"
|
||||
? "_Slow"
|
||||
: "";
|
||||
const SWIM_SPEED_ABILITY = "SSU";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: SWIM_SPEED_ABILITY,
|
||||
}),
|
||||
key: `MoveVel_Stealth${key}`,
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
const ninjaSquidMultiplier = args.mainOnlyAbilities.includes("NS") ? 0.9 : 1;
|
||||
|
||||
return {
|
||||
baseValue: effectToRounded(baseEffect * 10),
|
||||
value: effectToRounded(effect * 10 * ninjaSquidMultiplier),
|
||||
modifiedBy: SWIM_SPEED_ABILITY,
|
||||
};
|
||||
}
|
||||
|
||||
const RESPAWN_CHASE_FRAME = 150;
|
||||
function quickRespawnTime(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["quickRespawnTime"] {
|
||||
const QUICK_RESPAWN_TIME_ABILITY = "QR";
|
||||
|
||||
const chase = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: QUICK_RESPAWN_TIME_ABILITY,
|
||||
}),
|
||||
key: "Dying_ChaseFrm",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
const around = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: QUICK_RESPAWN_TIME_ABILITY,
|
||||
}),
|
||||
key: "Dying_AroundFrm",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: framesToSeconds(
|
||||
RESPAWN_CHASE_FRAME + chase.baseEffect + around.baseEffect
|
||||
),
|
||||
value: framesToSeconds(RESPAWN_CHASE_FRAME + chase.effect + around.effect),
|
||||
modifiedBy: QUICK_RESPAWN_TIME_ABILITY,
|
||||
};
|
||||
}
|
||||
|
||||
function superJumpTimeGroundFrames(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["superJumpTimeGroundFrames"] {
|
||||
const SUPER_JUMP_TIME_GROUND_ABILITY = "QSJ";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: SUPER_JUMP_TIME_GROUND_ABILITY,
|
||||
}),
|
||||
key: "SuperJump_ChargeFrm",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: Math.ceil(baseEffect),
|
||||
value: Math.ceil(effect),
|
||||
modifiedBy: SUPER_JUMP_TIME_GROUND_ABILITY,
|
||||
};
|
||||
}
|
||||
|
||||
function superJumpTimeTotal(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["superJumpTimeTotal"] {
|
||||
const SUPER_JUMP_TIME_TOTAL_ABILITY = "QSJ";
|
||||
|
||||
const charge = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: SUPER_JUMP_TIME_TOTAL_ABILITY,
|
||||
}),
|
||||
key: "SuperJump_ChargeFrm",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
const move = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: SUPER_JUMP_TIME_TOTAL_ABILITY,
|
||||
}),
|
||||
key: "SuperJump_MoveFrm",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: framesToSeconds(
|
||||
Math.ceil(charge.baseEffect) + Math.ceil(move.baseEffect)
|
||||
),
|
||||
value: framesToSeconds(Math.ceil(charge.effect) + Math.ceil(move.effect)),
|
||||
modifiedBy: SUPER_JUMP_TIME_TOTAL_ABILITY,
|
||||
};
|
||||
}
|
||||
|
||||
function damageTakenInEnemyInkPerSecond(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["damageTakenInEnemyInkPerSecond"] {
|
||||
const DAMAGE_TAKEN_IN_ENEMY_INK_ABILITY = "RES";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: DAMAGE_TAKEN_IN_ENEMY_INK_ABILITY,
|
||||
}),
|
||||
key: "OpInk_DamagePerFrame",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: effectToDamage(baseEffect) * 60,
|
||||
value: effectToDamage(effect) * 60,
|
||||
modifiedBy: DAMAGE_TAKEN_IN_ENEMY_INK_ABILITY,
|
||||
};
|
||||
}
|
||||
|
||||
function enemyInkDamageLimit(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["enemyInkDamageLimit"] {
|
||||
const ENEMY_INK_DAMAGE_LIMIT_ABILITY = "RES";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: ENEMY_INK_DAMAGE_LIMIT_ABILITY,
|
||||
}),
|
||||
key: "OpInk_DamageLmt",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: effectToDamage(baseEffect),
|
||||
value: effectToDamage(effect),
|
||||
modifiedBy: ENEMY_INK_DAMAGE_LIMIT_ABILITY,
|
||||
};
|
||||
}
|
||||
|
||||
function effectToDamage(effect: number) {
|
||||
// not sure where the 0.05 is coming from. Old analyzer had it as well so assuming it's correct.
|
||||
return Number((effect * 100 - 0.05).toFixed(1));
|
||||
}
|
||||
|
||||
function framesBeforeTakingDamageInEnemyInk(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["framesBeforeTakingDamageInEnemyInk"] {
|
||||
const FRAMES_BEFORE_TAKING_DAMAGE_IN_ENEMY_INK_ABILITY = "RES";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: FRAMES_BEFORE_TAKING_DAMAGE_IN_ENEMY_INK_ABILITY,
|
||||
}),
|
||||
key: "OpInk_ArmorHP",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: Math.ceil(baseEffect),
|
||||
value: Math.ceil(effect),
|
||||
modifiedBy: FRAMES_BEFORE_TAKING_DAMAGE_IN_ENEMY_INK_ABILITY,
|
||||
};
|
||||
}
|
||||
|
||||
const SUB_WEAPON_STATS = [
|
||||
{
|
||||
analyzedBuildKey: "subVelocity",
|
||||
abilityValuesKey: "SpawnSpeedZSpecUp",
|
||||
type: "NO_CHANGE",
|
||||
},
|
||||
{
|
||||
analyzedBuildKey: "subFirstPhaseDuration",
|
||||
abilityValuesKey: "PeriodFirst",
|
||||
type: "TIME",
|
||||
},
|
||||
{
|
||||
analyzedBuildKey: "subSecondPhaseDuration",
|
||||
abilityValuesKey: "PeriodSecond",
|
||||
type: "TIME",
|
||||
},
|
||||
{
|
||||
analyzedBuildKey: "subMarkingTimeInSeconds",
|
||||
abilityValuesKey: "MarkingFrameSubSpec",
|
||||
type: "TIME",
|
||||
},
|
||||
{
|
||||
analyzedBuildKey: "subMarkingRadius",
|
||||
abilityValuesKey: "SensorRadius",
|
||||
type: "NO_CHANGE",
|
||||
},
|
||||
{
|
||||
analyzedBuildKey: "subExplosionRadius",
|
||||
abilityValuesKey: "ExplosionRadius",
|
||||
type: "NO_CHANGE",
|
||||
},
|
||||
{ analyzedBuildKey: "subHp", abilityValuesKey: "MaxHP", type: "HP" },
|
||||
] as const;
|
||||
function subStats(args: StatFunctionInput) {
|
||||
const result: Partial<AnalyzedBuild["stats"]> = {};
|
||||
const SUB_STATS_KEY = "BRU";
|
||||
|
||||
for (const { analyzedBuildKey, abilityValuesKey, type } of SUB_WEAPON_STATS) {
|
||||
if (!hasEffect({ key: abilityValuesKey, weapon: args.subWeaponParams })) {
|
||||
continue;
|
||||
}
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: SUB_STATS_KEY,
|
||||
}),
|
||||
key: abilityValuesKey,
|
||||
weapon: args.subWeaponParams,
|
||||
});
|
||||
|
||||
const toValue = (effect: number) => {
|
||||
switch (type) {
|
||||
case "NO_CHANGE":
|
||||
return roundToTwoDecimalPlaces(effect);
|
||||
case "HP":
|
||||
return roundToTwoDecimalPlaces(effect / 10);
|
||||
case "TIME":
|
||||
return framesToSeconds(effect);
|
||||
default:
|
||||
assertUnreachable(type);
|
||||
}
|
||||
};
|
||||
|
||||
result[analyzedBuildKey] = {
|
||||
baseValue: toValue(baseEffect),
|
||||
modifiedBy: SUB_STATS_KEY,
|
||||
value: toValue(effect),
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function subDefPointSensorMarkedTimeInSeconds(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["subDefPointSensorMarkedTimeInSeconds"] {
|
||||
const SUB_DEF_POINT_SENSOR_MARKED_TIME_IN_SECONDS_KEY = "SRU";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: SUB_DEF_POINT_SENSOR_MARKED_TIME_IN_SECONDS_KEY,
|
||||
}),
|
||||
key: "MarkingTimeRt",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
const pointSensorParams = weaponParams().subWeapons[POINT_SENSOR_ID];
|
||||
|
||||
const { baseEffect: markingTimeEffect } = abilityPointsToEffects({
|
||||
abilityPoints: 0,
|
||||
key: "MarkingFrameSubSpec",
|
||||
weapon: pointSensorParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: framesToSeconds(markingTimeEffect * baseEffect),
|
||||
modifiedBy: SUB_DEF_POINT_SENSOR_MARKED_TIME_IN_SECONDS_KEY,
|
||||
value: framesToSeconds(markingTimeEffect * effect),
|
||||
};
|
||||
}
|
||||
|
||||
function subDefInkMineMarkedTimeInSeconds(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["subDefInkMineMarkedTimeInSeconds"] {
|
||||
const SUB_DEF_INK_MINE_MARKED_TIME_IN_SECONDS_KEY = "SRU";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: SUB_DEF_INK_MINE_MARKED_TIME_IN_SECONDS_KEY,
|
||||
}),
|
||||
key: "MarkingTimeRt_Trap",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
const inkMineParams = weaponParams().subWeapons[INK_MINE_ID];
|
||||
|
||||
const { baseEffect: markingTimeEffect } = abilityPointsToEffects({
|
||||
abilityPoints: 0,
|
||||
key: "MarkingFrameSubSpec",
|
||||
weapon: inkMineParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: framesToSeconds(markingTimeEffect * baseEffect),
|
||||
modifiedBy: SUB_DEF_INK_MINE_MARKED_TIME_IN_SECONDS_KEY,
|
||||
value: framesToSeconds(markingTimeEffect * effect),
|
||||
};
|
||||
}
|
||||
|
||||
function subDefAngleShooterMarkedTimeInSeconds(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["subDefAngleShooterMarkedTimeInSeconds"] {
|
||||
const SUB_DEF_ANGLE_SHOOTER_MARKED_TIME_IN_SECONDS_KEY = "SRU";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: SUB_DEF_ANGLE_SHOOTER_MARKED_TIME_IN_SECONDS_KEY,
|
||||
}),
|
||||
key: "MarkingTimeRt",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
const angleShooterParams = weaponParams().subWeapons[ANGLE_SHOOTER_ID];
|
||||
|
||||
const { baseEffect: markingTimeEffect } = abilityPointsToEffects({
|
||||
abilityPoints: 0,
|
||||
key: "MarkingFrameSubSpec",
|
||||
weapon: angleShooterParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: framesToSeconds(markingTimeEffect * baseEffect),
|
||||
modifiedBy: SUB_DEF_ANGLE_SHOOTER_MARKED_TIME_IN_SECONDS_KEY,
|
||||
value: framesToSeconds(markingTimeEffect * effect),
|
||||
};
|
||||
}
|
||||
|
||||
function subDefToxicMistMovementReduction(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["subDefToxicMistMovementReduction"] {
|
||||
const SUB_DEF_TOXIC_MIST_MOVEMENT_REDUCTION_KEY = "SRU";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: SUB_DEF_TOXIC_MIST_MOVEMENT_REDUCTION_KEY,
|
||||
}),
|
||||
key: "MoveDownRt_PoisonMist",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect * 100),
|
||||
value: roundToTwoDecimalPlaces(effect * 100),
|
||||
modifiedBy: SUB_DEF_TOXIC_MIST_MOVEMENT_REDUCTION_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
function subDefAngleShooterDamage(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["subDefAngleShooterDamage"] {
|
||||
const SUB_DEF_ANGLE_SHOOTER_DAMAGE_KEY = "SRU";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: SUB_DEF_ANGLE_SHOOTER_DAMAGE_KEY,
|
||||
}),
|
||||
key: "DamageRt_LineMarker",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
const angleShooterDirectDamage =
|
||||
weaponParams().subWeapons[ANGLE_SHOOTER_ID].DirectDamage;
|
||||
invariant(angleShooterDirectDamage);
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(
|
||||
(angleShooterDirectDamage * baseEffect) / 10
|
||||
),
|
||||
value: roundToTwoDecimalPlaces((angleShooterDirectDamage * effect) / 10),
|
||||
modifiedBy: SUB_DEF_ANGLE_SHOOTER_DAMAGE_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
function subDefSplashWallDamagePercentage(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["subDefSplashWallDamagePercentage"] {
|
||||
const SUB_DEF_SPLASH_WALL_DAMAGE_PERCENTAGE_KEY = "SRU";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: SUB_DEF_SPLASH_WALL_DAMAGE_PERCENTAGE_KEY,
|
||||
}),
|
||||
key: "DamageRt_Shield",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect * 100),
|
||||
value: roundToTwoDecimalPlaces(effect * 100),
|
||||
modifiedBy: SUB_DEF_SPLASH_WALL_DAMAGE_PERCENTAGE_KEY,
|
||||
};
|
||||
}
|
||||
|
||||
function subDefSprinklerDamagePercentage(
|
||||
args: StatFunctionInput
|
||||
): AnalyzedBuild["stats"]["subDefSprinklerDamagePercentage"] {
|
||||
const SUB_DEF_SPRINKLER_DAMAGE_PERCENTAGE_KEY = "SRU";
|
||||
const { baseEffect, effect } = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: SUB_DEF_SPRINKLER_DAMAGE_PERCENTAGE_KEY,
|
||||
}),
|
||||
key: "DamageRt_Sprinkler",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
||||
return {
|
||||
baseValue: roundToTwoDecimalPlaces(baseEffect * 100),
|
||||
value: roundToTwoDecimalPlaces(effect * 100),
|
||||
modifiedBy: SUB_DEF_SPRINKLER_DAMAGE_PERCENTAGE_KEY,
|
||||
};
|
||||
}
|
||||
217
app/modules/analyzer/types.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import type {
|
||||
Ability,
|
||||
MainWeaponId,
|
||||
SpecialWeaponId,
|
||||
SubWeaponId,
|
||||
} from "~/modules/in-game-lists";
|
||||
import type { SPECIAL_EFFECTS } from "./specialEffects";
|
||||
import type abilityValues from "./ability-values.json";
|
||||
|
||||
export interface MainWeaponParams {
|
||||
subWeaponId: SubWeaponId;
|
||||
specialWeaponId: SpecialWeaponId;
|
||||
/** Replacing default values of the ability json for this specific weapon */
|
||||
overwrites?: Record<string, Partial<Record<"High" | "Mid" | "Low", number>>>;
|
||||
SpecialPoint: number;
|
||||
/** Weapon's weight class. "Light/Heavy weapon" */
|
||||
WeaponSpeedType?: "Slow" | "Fast";
|
||||
/** Total frames it takes the weapon to shoot out three times */
|
||||
TripleShotSpanFrame?: number;
|
||||
/** Amount of frames charge can be held */
|
||||
KeepChargeFullFrame?: number;
|
||||
/** Amount of frames full charge takes */
|
||||
ChargeFrameFullCharge?: number;
|
||||
DamageParam_ValueMax?: number;
|
||||
DamageParam_ValueMin?: number;
|
||||
DamageParam_ValueDirect?: number;
|
||||
/** Damage caused by charger's full charged shot */
|
||||
DamageParam_ValueFullCharge?: number;
|
||||
/** Max damage caused by charger's charged shot before fully charged */
|
||||
DamageParam_ValueMaxCharge?: number;
|
||||
/** Charger tap shot damage */
|
||||
DamageParam_ValueMinCharge?: number;
|
||||
BlastParam_DistanceDamage?: Array<DistanceDamage>;
|
||||
// DamageParam_ReduceStartFrame?: number;
|
||||
// DamageParam_ReduceEndFrame?: number;
|
||||
/** Brella shield HP */
|
||||
CanopyHP?: number;
|
||||
/** Amount of frames white ink (=no ink recovery during this time) takes */
|
||||
InkRecoverStop?: number;
|
||||
/** How much ink one shot consumes? InkConsume = 0.5 means 2 shots per full tank */
|
||||
InkConsume?: number;
|
||||
/** How much ink one slosh of slosher consumes? */
|
||||
InkConsumeSlosher?: number;
|
||||
/** How much ink one fully charged shot consumes? */
|
||||
InkConsumeFullCharge?: number;
|
||||
/** How much ink one tap shot consumes? */
|
||||
InkConsumeMinCharge?: number;
|
||||
/** How much ink one full charge of splatling consumes? */
|
||||
InkConsumeFullChargeSplatling?: number;
|
||||
/** How much ink one swing of brush consumes? */
|
||||
InkConsume_WeaponSwingParam?: number;
|
||||
/** How much ink one vertical swing of roller consumes? */
|
||||
InkConsume_WeaponVerticalSwingParam?: number;
|
||||
/** How much ink one horizontal swing of roller consumes? */
|
||||
InkConsume_WeaponWideSwingParam?: number;
|
||||
/** How much ink one swing of splatana consumes? */
|
||||
InkConsume_SwingParam?: number;
|
||||
/** How much ink brella shield launch consumes? */
|
||||
InkConsumeUmbrella_WeaponShelterCanopyParam?: number;
|
||||
/** How much ink one brella shot consumes? */
|
||||
InkConsume_WeaponShelterShotgunParam?: number;
|
||||
/** How much ink a dualie dodge roll consumes? */
|
||||
InkConsume_SideStepParam?: number;
|
||||
/** How much ink a fully charger Splatana shot consumes? */
|
||||
InkConsumeFullCharge_ChargeParam?: number;
|
||||
//InkConsumeMidCharge_ChargeParam?: number;
|
||||
// SpeedInkConsumeMax_WeaponRollParam?: number;
|
||||
// SpeedInkConsumeMin_WeaponRollParam?: number;
|
||||
}
|
||||
|
||||
export interface DistanceDamage {
|
||||
Damage: number;
|
||||
Distance: number;
|
||||
}
|
||||
|
||||
export interface SubWeaponParams {
|
||||
overwrites?: Record<string, Partial<Record<"High" | "Mid" | "Low", number>>>;
|
||||
SubInkSaveLv: 0 | 1 | 2 | 3;
|
||||
/** How much ink one usage of the sub consumes */
|
||||
InkConsume: number;
|
||||
/** Amount of frames white ink (=no ink recovery during this time) takes */
|
||||
InkRecoverStop: number;
|
||||
/** Damage dealt at different radiuses */
|
||||
DistanceDamage?: Array<DistanceDamage>;
|
||||
/** Damage dealt by explosion at different radiuses (curling bomb charged all the way) */
|
||||
DistanceDamage_BlastParamMaxCharge?: Array<DistanceDamage>;
|
||||
/** Damage dealt by explosion at different radiuses (curling bomb not charged) */
|
||||
DistanceDamage_BlastParamMinCharge?: Array<DistanceDamage>;
|
||||
/** Damage dealt by explosion at different radiuses (fizzy bomb bounces) */
|
||||
DistanceDamage_BlastParamArray?: Array<DistanceDamage>;
|
||||
/** Damage dealt by explosion at different radiuses (torpedo explosion air to ground) */
|
||||
DistanceDamage_BlastParamChase?: Array<DistanceDamage>;
|
||||
/** Damage dealt by explosion at different radiuses (rolling torpedo) */
|
||||
DistanceDamage_SplashBlastParam?: Array<DistanceDamage>;
|
||||
/** Damage dealt by direct hit */
|
||||
DirectDamage?: number;
|
||||
}
|
||||
|
||||
export type ParamsJson = {
|
||||
mainWeapons: Record<MainWeaponId, MainWeaponParams>;
|
||||
subWeapons: Record<SubWeaponId, SubWeaponParams>;
|
||||
};
|
||||
|
||||
export interface Stat {
|
||||
value: number;
|
||||
baseValue: number;
|
||||
modifiedBy: Ability;
|
||||
}
|
||||
|
||||
export type AbilityPoints = Map<Ability, number>;
|
||||
|
||||
export interface StatFunctionInput {
|
||||
mainWeaponParams: MainWeaponParams;
|
||||
subWeaponParams: SubWeaponParams;
|
||||
abilityPoints: AbilityPoints;
|
||||
mainOnlyAbilities: Array<Ability>;
|
||||
}
|
||||
|
||||
export type InkConsumeType = typeof INK_CONSUME_TYPES[number];
|
||||
|
||||
export const INK_CONSUME_TYPES = [
|
||||
"NORMAL",
|
||||
"SWING",
|
||||
"SLOSH",
|
||||
"VERTICAL_SWING",
|
||||
"HORIZONTAL_SWING",
|
||||
"TAP_SHOT",
|
||||
"FULL_CHARGE",
|
||||
"SPLATLING_CHARGE",
|
||||
"SHIELD_LAUNCH",
|
||||
"DUALIE_ROLL",
|
||||
] as const;
|
||||
|
||||
export interface FullInkTankOption {
|
||||
subsUsed: number;
|
||||
value: number;
|
||||
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 interface Damage {
|
||||
value: number;
|
||||
type: DamageType;
|
||||
distance?: number;
|
||||
shotsToSplat?: number;
|
||||
}
|
||||
|
||||
export interface AnalyzedBuild {
|
||||
weapon: {
|
||||
subWeaponSplId: SubWeaponId;
|
||||
specialWeaponSplId: SpecialWeaponId;
|
||||
brellaCanopyHp?: number;
|
||||
maxChargeHoldSeconds?: number;
|
||||
fullChargeSeconds?: number;
|
||||
speedType: NonNullable<MainWeaponParams["WeaponSpeedType"]> | "Normal";
|
||||
isTripleShooter: boolean;
|
||||
};
|
||||
stats: {
|
||||
specialPoint: Stat;
|
||||
/** % of special charge saved when dying */
|
||||
specialSavedAfterDeath: Stat;
|
||||
mainWeaponWhiteInkSeconds?: number;
|
||||
subWeaponWhiteInkSeconds: number;
|
||||
fullInkTankOptions: Array<FullInkTankOption & { id: string }>;
|
||||
damages: Array<Damage & { id: string }>;
|
||||
squidFormInkRecoverySeconds: Stat;
|
||||
runSpeed: Stat;
|
||||
// shootingRunSpeed: Stat;
|
||||
swimSpeed: Stat;
|
||||
runSpeedInEnemyInk: Stat;
|
||||
framesBeforeTakingDamageInEnemyInk: Stat;
|
||||
damageTakenInEnemyInkPerSecond: Stat;
|
||||
enemyInkDamageLimit: Stat;
|
||||
quickRespawnTime: Stat;
|
||||
superJumpTimeGroundFrames: Stat;
|
||||
superJumpTimeTotal: Stat;
|
||||
|
||||
subDefPointSensorMarkedTimeInSeconds: Stat;
|
||||
subDefInkMineMarkedTimeInSeconds: Stat;
|
||||
subDefAngleShooterMarkedTimeInSeconds: Stat;
|
||||
subDefToxicMistMovementReduction: Stat;
|
||||
subDefAngleShooterDamage: Stat;
|
||||
subDefSplashWallDamagePercentage: Stat;
|
||||
subDefSprinklerDamagePercentage: Stat;
|
||||
// subDefBombDamageLight: Stat;
|
||||
// subDefBombDamageHeavy: Stat;
|
||||
// subDefAngleShooterDamage: Stat;
|
||||
// subDefSplashWallDamage: Stat;
|
||||
// subDefSprinklerDamage: Stat;
|
||||
// subDefToxicMistMoveReduction: Stat;
|
||||
|
||||
subVelocity?: Stat;
|
||||
subFirstPhaseDuration?: Stat;
|
||||
subSecondPhaseDuration?: Stat;
|
||||
subMarkingTimeInSeconds?: Stat;
|
||||
subMarkingRadius?: Stat;
|
||||
subExplosionRadius?: Stat;
|
||||
subHp?: Stat;
|
||||
};
|
||||
}
|
||||
|
||||
export type SpecialEffectType = typeof SPECIAL_EFFECTS[number]["type"];
|
||||
|
||||
export type AbilityValuesKeys = keyof typeof abilityValues;
|
||||
201
app/modules/analyzer/useAnalyzeBuild.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
import { useSearchParams } from "@remix-run/react";
|
||||
import { EMPTY_BUILD } from "~/constants";
|
||||
import {
|
||||
type BuildAbilitiesTupleWithUnknown,
|
||||
type MainWeaponId,
|
||||
mainWeaponIds,
|
||||
abilities,
|
||||
isAbility,
|
||||
} from "../in-game-lists";
|
||||
import type {
|
||||
Ability,
|
||||
AbilityType,
|
||||
AbilityWithUnknown,
|
||||
} from "../in-game-lists/types";
|
||||
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";
|
||||
|
||||
const UNKNOWN_SHORT = "U";
|
||||
|
||||
export function useAnalyzeBuild() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const mainWeaponId = validatedWeaponIdFromSearchParams(searchParams);
|
||||
const build = validatedBuildFromSearchParams(searchParams);
|
||||
const ldeIntensity = validatedLdeIntensityFromSearchParams(searchParams);
|
||||
const effects = validatedEffectsFromSearchParams({ searchParams, build });
|
||||
|
||||
const handleChange = ({
|
||||
newMainWeaponId = mainWeaponId,
|
||||
newBuild = build,
|
||||
newLdeIntensity = ldeIntensity,
|
||||
newEffects = effects,
|
||||
}: {
|
||||
newMainWeaponId?: MainWeaponId;
|
||||
newBuild?: BuildAbilitiesTupleWithUnknown;
|
||||
newLdeIntensity?: number;
|
||||
newEffects?: Array<SpecialEffectType>;
|
||||
}) => {
|
||||
setSearchParams({
|
||||
weapon: String(newMainWeaponId),
|
||||
build: serializeBuild(newBuild),
|
||||
lde: String(newLdeIntensity),
|
||||
effect: newEffects,
|
||||
});
|
||||
};
|
||||
|
||||
const buildsAbilityPoints = buildToAbilityPoints(build);
|
||||
|
||||
const abilityPoints = applySpecialEffects({
|
||||
abilityPoints: buildsAbilityPoints,
|
||||
effects,
|
||||
ldeIntensity,
|
||||
});
|
||||
|
||||
const analyzed = buildStats({
|
||||
abilityPoints,
|
||||
weaponSplId: mainWeaponId,
|
||||
mainOnlyAbilities: build
|
||||
.map((row) => row[0])
|
||||
.filter((ability): ability is Ability => {
|
||||
const abilityObj = abilities.find((a) => a.name === ability);
|
||||
return Boolean(abilityObj && abilityObj.type !== "STACKABLE");
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
build,
|
||||
mainWeaponId,
|
||||
handleChange,
|
||||
analyzed,
|
||||
abilityPoints,
|
||||
effects,
|
||||
ldeIntensity,
|
||||
};
|
||||
}
|
||||
|
||||
function serializeBuild(build: BuildAbilitiesTupleWithUnknown) {
|
||||
return build
|
||||
.flat()
|
||||
.map((ability) => (ability === "UNKNOWN" ? UNKNOWN_SHORT : ability))
|
||||
.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 mainWeaponIds[0];
|
||||
}
|
||||
|
||||
function validatedBuildFromSearchParams(
|
||||
searchParams: URLSearchParams
|
||||
): BuildAbilitiesTupleWithUnknown {
|
||||
const abilitiesArr = searchParams.get("build")
|
||||
? searchParams.get("build")?.split(",")
|
||||
: null;
|
||||
|
||||
if (!abilitiesArr) return EMPTY_BUILD;
|
||||
|
||||
try {
|
||||
return [
|
||||
[
|
||||
validateAbility(["STACKABLE", "HEAD_MAIN_ONLY"], abilitiesArr[0]),
|
||||
validateAbility(["STACKABLE"], abilitiesArr[1]),
|
||||
validateAbility(["STACKABLE"], abilitiesArr[2]),
|
||||
validateAbility(["STACKABLE"], abilitiesArr[3]),
|
||||
],
|
||||
[
|
||||
validateAbility(["STACKABLE", "CLOTHES_MAIN_ONLY"], abilitiesArr[4]),
|
||||
validateAbility(["STACKABLE"], abilitiesArr[5]),
|
||||
validateAbility(["STACKABLE"], abilitiesArr[6]),
|
||||
validateAbility(["STACKABLE"], abilitiesArr[7]),
|
||||
],
|
||||
[
|
||||
validateAbility(["STACKABLE", "SHOES_MAIN_ONLY"], abilitiesArr[8]),
|
||||
validateAbility(["STACKABLE"], abilitiesArr[9]),
|
||||
validateAbility(["STACKABLE"], abilitiesArr[10]),
|
||||
validateAbility(["STACKABLE"], abilitiesArr[11]),
|
||||
],
|
||||
];
|
||||
} catch (err) {
|
||||
return EMPTY_BUILD;
|
||||
}
|
||||
}
|
||||
|
||||
function validateAbility(
|
||||
legalTypes: Array<AbilityType>,
|
||||
ability?: string
|
||||
): AbilityWithUnknown {
|
||||
if (!ability) throw new Error("Ability missing");
|
||||
if (ability === UNKNOWN_SHORT) return "UNKNOWN";
|
||||
|
||||
const abilityObj = abilities.find(
|
||||
(a) => a.name === ability && legalTypes.includes(a.type)
|
||||
);
|
||||
if (abilityObj) return abilityObj.name;
|
||||
|
||||
throw new Error("Invalid ability");
|
||||
}
|
||||
|
||||
function validatedLdeIntensityFromSearchParams(searchParams: URLSearchParams) {
|
||||
const ldeIntensity = searchParams.get("lde")
|
||||
? Number(searchParams.get("lde"))
|
||||
: null;
|
||||
|
||||
if (
|
||||
!ldeIntensity ||
|
||||
!Number.isInteger(ldeIntensity) ||
|
||||
ldeIntensity < 0 ||
|
||||
ldeIntensity > MAX_LDE_INTENSITY
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ldeIntensity;
|
||||
}
|
||||
|
||||
function validatedEffectsFromSearchParams({
|
||||
searchParams,
|
||||
build,
|
||||
}: {
|
||||
searchParams: URLSearchParams;
|
||||
build: BuildAbilitiesTupleWithUnknown;
|
||||
}) {
|
||||
const result: Array<SpecialEffectType> = [];
|
||||
|
||||
const effects = searchParams.getAll("effect");
|
||||
const effectsNoDuplicates = [...new Set(effects)];
|
||||
const abilities = build.flat();
|
||||
|
||||
for (const effect of effectsNoDuplicates) {
|
||||
const effectObj = SPECIAL_EFFECTS.find((e) => e.type === effect);
|
||||
if (!effectObj) continue;
|
||||
|
||||
// e.g. even if OG effect is active in state
|
||||
// it can't be on unless build has OG
|
||||
if (isAbility(effect) && !abilities.includes(effect)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(effect as SpecialEffectType);
|
||||
}
|
||||
|
||||
// lde is a special case in that it's always
|
||||
// considered active when in the build
|
||||
if (abilities.includes("LDE") && !result.includes("LDE")) {
|
||||
result.push("LDE");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
52
app/modules/analyzer/utils.test.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import type { AbilityWithUnknown } from "../in-game-lists/types";
|
||||
import { buildToAbilityPoints } from "./utils";
|
||||
|
||||
const BuildToAbilityPoints = suite("buildToAbilityPoints()");
|
||||
|
||||
const EMPTY_ROW: [
|
||||
AbilityWithUnknown,
|
||||
AbilityWithUnknown,
|
||||
AbilityWithUnknown,
|
||||
AbilityWithUnknown
|
||||
] = ["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"];
|
||||
|
||||
BuildToAbilityPoints("Empty build leads to empty AP map", () => {
|
||||
const aps = buildToAbilityPoints([EMPTY_ROW, EMPTY_ROW, EMPTY_ROW]);
|
||||
|
||||
assert.equal(aps.size, 0);
|
||||
});
|
||||
|
||||
BuildToAbilityPoints("Calculates ability points", () => {
|
||||
const aps = buildToAbilityPoints([
|
||||
["SS", "SS", "RSU", "RSU"],
|
||||
EMPTY_ROW,
|
||||
EMPTY_ROW,
|
||||
]);
|
||||
|
||||
assert.equal(aps.get("SS"), 13);
|
||||
assert.equal(aps.get("RSU"), 6);
|
||||
});
|
||||
|
||||
BuildToAbilityPoints("Handles ability doubler", () => {
|
||||
const aps = buildToAbilityPoints([
|
||||
EMPTY_ROW,
|
||||
["AD", "SS", "UNKNOWN", "UNKNOWN"],
|
||||
EMPTY_ROW,
|
||||
]);
|
||||
|
||||
assert.equal(aps.get("SS"), 6);
|
||||
});
|
||||
|
||||
BuildToAbilityPoints("Does not calculate AP for main only abilities", () => {
|
||||
const aps = buildToAbilityPoints([
|
||||
["LDE", "SS", "RSU", "RSU"],
|
||||
EMPTY_ROW,
|
||||
EMPTY_ROW,
|
||||
]);
|
||||
|
||||
assert.not.ok(aps.has("LDE"));
|
||||
});
|
||||
|
||||
BuildToAbilityPoints.run();
|
||||
149
app/modules/analyzer/utils.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import type { Ability, BuildAbilitiesTupleWithUnknown } from "../in-game-lists";
|
||||
import { abilities } from "../in-game-lists";
|
||||
import weaponParamsJson from "./weapon-params.json";
|
||||
import abilityValuesJson from "./ability-values.json";
|
||||
import type {
|
||||
AbilityPoints,
|
||||
MainWeaponParams,
|
||||
ParamsJson,
|
||||
SubWeaponParams,
|
||||
} from "./types";
|
||||
import invariant from "tiny-invariant";
|
||||
import type { AbilityWithUnknown } from "../in-game-lists/types";
|
||||
|
||||
export function weaponParams(): ParamsJson {
|
||||
// @ts-expect-error can be removed when Lean updates the json
|
||||
return weaponParamsJson as ParamsJson;
|
||||
}
|
||||
|
||||
export function buildToAbilityPoints(build: BuildAbilitiesTupleWithUnknown) {
|
||||
const result: AbilityPoints = new Map();
|
||||
|
||||
for (const abilityRow of build) {
|
||||
let abilityDoublerActive = false;
|
||||
for (const [i, ability] of abilityRow.entries()) {
|
||||
if (ability === "AD") {
|
||||
abilityDoublerActive = true;
|
||||
}
|
||||
if (!isStackableAbility(ability)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const aps = i === 0 ? 10 : 3;
|
||||
const apsDoubled = aps * (abilityDoublerActive ? 2 : 1);
|
||||
|
||||
result.set(ability, (result.get(ability) ?? 0) + apsDoubled);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function isStackableAbility(ability: AbilityWithUnknown): ability is Ability {
|
||||
if (ability === "UNKNOWN") return false;
|
||||
const abilityObj = abilities.find((a) => a.name === ability);
|
||||
invariant(abilityObj);
|
||||
|
||||
return abilityObj.type === "STACKABLE";
|
||||
}
|
||||
|
||||
export function apFromMap({
|
||||
abilityPoints,
|
||||
ability,
|
||||
}: {
|
||||
abilityPoints: AbilityPoints;
|
||||
ability: Ability;
|
||||
}) {
|
||||
return abilityPoints.get(ability) ?? 0;
|
||||
}
|
||||
|
||||
function abilityValues({
|
||||
key,
|
||||
weapon,
|
||||
}: {
|
||||
key: keyof typeof abilityValuesJson;
|
||||
weapon: MainWeaponParams | SubWeaponParams;
|
||||
}): [number, number, number] {
|
||||
const overwrites = weapon.overwrites?.[key];
|
||||
|
||||
const [High, Mid, Low] = abilityValuesJson[key];
|
||||
invariant(typeof High === "number");
|
||||
invariant(typeof Mid === "number");
|
||||
invariant(typeof Low === "number");
|
||||
|
||||
return [
|
||||
overwrites?.High ?? High,
|
||||
overwrites?.Mid ?? Mid,
|
||||
overwrites?.Low ?? Low,
|
||||
];
|
||||
}
|
||||
|
||||
function calculateAbilityPointToPercent(ap: number) {
|
||||
return Math.min(3.3 * ap - 0.027 * Math.pow(ap, 2), 100);
|
||||
}
|
||||
|
||||
function getSlope(high: number, mid: number, low: number) {
|
||||
if (mid === low) {
|
||||
return 0;
|
||||
}
|
||||
return (mid - low) / (high - low);
|
||||
}
|
||||
|
||||
function lerpN(p: number, s: number) {
|
||||
if (s.toFixed(3) === "0.500") {
|
||||
return p;
|
||||
}
|
||||
if (p === 0.0) {
|
||||
return p;
|
||||
}
|
||||
if (p === 1.0) {
|
||||
return p;
|
||||
}
|
||||
|
||||
return Math.pow(Math.E, -1 * ((Math.log(p) * Math.log(s)) / Math.log(2)));
|
||||
}
|
||||
|
||||
function abilityPointsToEffect({
|
||||
key,
|
||||
abilityPoints,
|
||||
weapon,
|
||||
}: {
|
||||
key: keyof typeof abilityValuesJson;
|
||||
abilityPoints: number;
|
||||
weapon: MainWeaponParams | SubWeaponParams;
|
||||
}) {
|
||||
const [high, mid, low] = abilityValues({ key, weapon });
|
||||
|
||||
const slope = getSlope(high, mid, low);
|
||||
const percentage = calculateAbilityPointToPercent(abilityPoints) / 100.0;
|
||||
const result = low + (high - low) * lerpN(slope, percentage);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function abilityPointsToEffects({
|
||||
key,
|
||||
abilityPoints,
|
||||
weapon,
|
||||
}: {
|
||||
key: keyof typeof abilityValuesJson;
|
||||
abilityPoints: number;
|
||||
weapon: MainWeaponParams | SubWeaponParams;
|
||||
}) {
|
||||
return {
|
||||
baseEffect: abilityPointsToEffect({ key, abilityPoints: 0, weapon }),
|
||||
effect: abilityPointsToEffect({ key, abilityPoints, weapon }),
|
||||
};
|
||||
}
|
||||
|
||||
export function hasEffect({
|
||||
key,
|
||||
weapon,
|
||||
}: {
|
||||
key: keyof typeof abilityValuesJson;
|
||||
weapon: MainWeaponParams | SubWeaponParams;
|
||||
}) {
|
||||
const [high, mid, low] = abilityValues({ key, weapon });
|
||||
|
||||
return high !== mid || mid !== low;
|
||||
}
|
||||
966
app/modules/analyzer/weapon-params.json
Normal file
|
|
@ -0,0 +1,966 @@
|
|||
{
|
||||
"mainWeapons": {
|
||||
"0": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 6,
|
||||
"specialWeaponId": 11,
|
||||
"WeaponSpeedType": "Fast",
|
||||
"DamageParam_ValueMax": 380,
|
||||
"DamageParam_ValueMin": 190,
|
||||
"InkRecoverStop": 15,
|
||||
"InkConsume": 0.008
|
||||
},
|
||||
"10": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 0,
|
||||
"specialWeaponId": 2,
|
||||
"WeaponSpeedType": "Fast",
|
||||
"DamageParam_ValueMax": 280,
|
||||
"DamageParam_ValueMin": 140,
|
||||
"InkRecoverStop": 15,
|
||||
"InkConsume": 0.0043
|
||||
},
|
||||
"20": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 2,
|
||||
"specialWeaponId": 12,
|
||||
"WeaponSpeedType": "Fast",
|
||||
"DamageParam_ValueMax": 280,
|
||||
"DamageParam_ValueMin": 140,
|
||||
"InkConsume": 0.008
|
||||
},
|
||||
"30": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 5,
|
||||
"specialWeaponId": 13,
|
||||
"WeaponSpeedType": "Fast",
|
||||
"DamageParam_ValueMax": 240,
|
||||
"DamageParam_ValueMin": 120,
|
||||
"InkRecoverStop": 15,
|
||||
"InkConsume": 0.0055
|
||||
},
|
||||
"40": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 1,
|
||||
"specialWeaponId": 1,
|
||||
"DamageParam_ValueMax": 360,
|
||||
"DamageParam_ValueMin": 180,
|
||||
"InkConsume": 0.0092
|
||||
},
|
||||
"45": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 1,
|
||||
"specialWeaponId": 1,
|
||||
"DamageParam_ValueMax": 360,
|
||||
"DamageParam_ValueMin": 180,
|
||||
"InkConsume": 0.0092
|
||||
},
|
||||
"50": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 4,
|
||||
"specialWeaponId": 9,
|
||||
"DamageParam_ValueMax": 520,
|
||||
"DamageParam_ValueMin": 300,
|
||||
"InkConsume": 0.013
|
||||
},
|
||||
"60": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 1,
|
||||
"specialWeaponId": 15,
|
||||
"WeaponSpeedType": "Fast",
|
||||
"DamageParam_ValueMax": 280,
|
||||
"DamageParam_ValueMin": 140,
|
||||
"InkConsume": 0.008
|
||||
},
|
||||
"70": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 12,
|
||||
"specialWeaponId": 12,
|
||||
"overwrites": {
|
||||
"ConsumeRt_Main": {
|
||||
"High": 0.5,
|
||||
"Mid": 0.7
|
||||
},
|
||||
"MoveVelRt_Shot": {
|
||||
"High": -1,
|
||||
"Mid": -1
|
||||
}
|
||||
},
|
||||
"DamageParam_ValueMax": 420,
|
||||
"DamageParam_ValueMin": 210,
|
||||
"InkConsume": 0.02
|
||||
},
|
||||
"80": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 3,
|
||||
"specialWeaponId": 8,
|
||||
"DamageParam_ValueMax": 620,
|
||||
"DamageParam_ValueMin": 350,
|
||||
"InkConsume": 0.025
|
||||
},
|
||||
"90": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 12,
|
||||
"specialWeaponId": 8,
|
||||
"DamageParam_ValueMax": 320,
|
||||
"DamageParam_ValueMin": 160,
|
||||
"InkConsume": 0.016
|
||||
},
|
||||
"200": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 0,
|
||||
"specialWeaponId": 3,
|
||||
"overwrites": {
|
||||
"ConsumeRt_Main": {
|
||||
"High": 0.5,
|
||||
"Mid": 0.7
|
||||
},
|
||||
"ReduceJumpSwerveRate": {
|
||||
"Mid": 0.5
|
||||
}
|
||||
},
|
||||
"WeaponSpeedType": "Fast",
|
||||
"DamageParam_ValueDirect": 1250,
|
||||
"BlastParam_DistanceDamage": [
|
||||
{
|
||||
"Damage": 700,
|
||||
"Distance": 1
|
||||
},
|
||||
{
|
||||
"Damage": 500,
|
||||
"Distance": 3.57
|
||||
}
|
||||
],
|
||||
"InkRecoverStop": 55,
|
||||
"InkConsume": 0.075
|
||||
},
|
||||
"210": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 7,
|
||||
"specialWeaponId": 2,
|
||||
"overwrites": {
|
||||
"ReduceJumpSwerveRate": {
|
||||
"Mid": 0.5
|
||||
}
|
||||
},
|
||||
"DamageParam_ValueDirect": 1250,
|
||||
"BlastParam_DistanceDamage": [
|
||||
{
|
||||
"Damage": 700,
|
||||
"Distance": 0.94
|
||||
},
|
||||
{
|
||||
"Damage": 500,
|
||||
"Distance": 3.3
|
||||
}
|
||||
],
|
||||
"InkRecoverStop": 60,
|
||||
"InkConsume": 0.1
|
||||
},
|
||||
"220": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 1,
|
||||
"specialWeaponId": 7,
|
||||
"overwrites": {
|
||||
"ReduceJumpSwerveRate": {
|
||||
"Mid": 0.5
|
||||
}
|
||||
},
|
||||
"DamageParam_ValueDirect": 1250,
|
||||
"BlastParam_DistanceDamage": [
|
||||
{
|
||||
"Damage": 700,
|
||||
"Distance": 1
|
||||
},
|
||||
{
|
||||
"Damage": 500,
|
||||
"Distance": 3.5
|
||||
}
|
||||
],
|
||||
"InkRecoverStop": 70,
|
||||
"InkConsume": 0.11
|
||||
},
|
||||
"230": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 0,
|
||||
"specialWeaponId": 1,
|
||||
"overwrites": {
|
||||
"ReduceJumpSwerveRate": {
|
||||
"Mid": 0.5
|
||||
}
|
||||
},
|
||||
"WeaponSpeedType": "Fast",
|
||||
"DamageParam_ValueDirect": 600,
|
||||
"BlastParam_DistanceDamage": [
|
||||
{
|
||||
"Damage": 300,
|
||||
"Distance": 1
|
||||
},
|
||||
{
|
||||
"Damage": 300,
|
||||
"Distance": 4
|
||||
}
|
||||
],
|
||||
"InkRecoverStop": 40,
|
||||
"InkConsume": 0.04
|
||||
},
|
||||
"240": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 10,
|
||||
"specialWeaponId": 14,
|
||||
"overwrites": {
|
||||
"ReduceJumpSwerveRate": {
|
||||
"Mid": 0.5
|
||||
}
|
||||
},
|
||||
"DamageParam_ValueDirect": 850,
|
||||
"BlastParam_DistanceDamage": [
|
||||
{
|
||||
"Damage": 350,
|
||||
"Distance": 0.94
|
||||
},
|
||||
{
|
||||
"Damage": 350,
|
||||
"Distance": 3.3
|
||||
}
|
||||
],
|
||||
"InkRecoverStop": 50,
|
||||
"InkConsume": 0.07
|
||||
},
|
||||
"250": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 11,
|
||||
"specialWeaponId": 8,
|
||||
"overwrites": {
|
||||
"ReduceJumpSwerveRate": {
|
||||
"Mid": 0.5
|
||||
}
|
||||
},
|
||||
"DamageParam_ValueDirect": 850,
|
||||
"BlastParam_DistanceDamage": [
|
||||
{
|
||||
"Damage": 350,
|
||||
"Distance": 0.94
|
||||
},
|
||||
{
|
||||
"Damage": 350,
|
||||
"Distance": 3.3
|
||||
}
|
||||
],
|
||||
"InkRecoverStop": 50,
|
||||
"InkConsume": 0.08
|
||||
},
|
||||
"300": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 6,
|
||||
"specialWeaponId": 12,
|
||||
"TripleShotSpanFrame": 8,
|
||||
"DamageParam_ValueMax": 290,
|
||||
"DamageParam_ValueMin": 145,
|
||||
"InkRecoverStop": 25,
|
||||
"InkConsume": 0.0115
|
||||
},
|
||||
"310": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 9,
|
||||
"specialWeaponId": 15,
|
||||
"overwrites": {
|
||||
"ConsumeRt_Main": {
|
||||
"High": 0.5,
|
||||
"Mid": 0.7
|
||||
},
|
||||
"MoveVelRt_Shot": {
|
||||
"High": -1,
|
||||
"Mid": -1
|
||||
}
|
||||
},
|
||||
"TripleShotSpanFrame": 20,
|
||||
"DamageParam_ValueMax": 410,
|
||||
"DamageParam_ValueMin": 205,
|
||||
"InkRecoverStop": 25,
|
||||
"InkConsume": 0.0225
|
||||
},
|
||||
"400": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 4,
|
||||
"specialWeaponId": 1,
|
||||
"DamageParam_ValueMax": 380,
|
||||
"DamageParam_ValueMin": 190,
|
||||
"InkConsume": 0.022
|
||||
},
|
||||
"1000": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 7,
|
||||
"specialWeaponId": 3,
|
||||
"WeaponSpeedType": "Fast",
|
||||
"InkConsume_WeaponSwingParam": 0.0396
|
||||
},
|
||||
"1010": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 6,
|
||||
"specialWeaponId": 2,
|
||||
"InkConsume_WeaponSwingParam": 0.085
|
||||
},
|
||||
"1020": {
|
||||
"SpecialPoint": 190,
|
||||
"subWeaponId": 3,
|
||||
"specialWeaponId": 15,
|
||||
"overwrites": {
|
||||
"ConsumeRt_Main": {
|
||||
"High": 0.5,
|
||||
"Mid": 0.7
|
||||
}
|
||||
},
|
||||
"WeaponSpeedType": "Slow",
|
||||
"InkConsume_WeaponSwingParam": 0.18
|
||||
},
|
||||
"1030": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 10,
|
||||
"specialWeaponId": 4,
|
||||
"InkConsume_WeaponVerticalSwingParam": 0.12,
|
||||
"InkConsume_WeaponWideSwingParam": 0.08
|
||||
},
|
||||
"1100": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 0,
|
||||
"specialWeaponId": 9,
|
||||
"WeaponSpeedType": "Fast",
|
||||
"InkConsume_WeaponSwingParam": 0.02
|
||||
},
|
||||
"1110": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 1,
|
||||
"specialWeaponId": 3,
|
||||
"InkConsume_WeaponSwingParam": 0.032
|
||||
},
|
||||
"2000": {
|
||||
"SpecialPoint": 190,
|
||||
"subWeaponId": 9,
|
||||
"specialWeaponId": 2,
|
||||
"DamageParam_ValueFullCharge": 1400,
|
||||
"DamageParam_ValueMaxCharge": 800,
|
||||
"DamageParam_ValueMinCharge": 400,
|
||||
"ChargeFrameFullCharge": 45,
|
||||
"KeepChargeFullFrame": 75,
|
||||
"InkConsumeFullCharge": 0.105,
|
||||
"InkConsumeMinCharge": 0.018667
|
||||
},
|
||||
"2010": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 0,
|
||||
"specialWeaponId": 8,
|
||||
"DamageParam_ValueFullCharge": 1600,
|
||||
"DamageParam_ValueMaxCharge": 800,
|
||||
"DamageParam_ValueMinCharge": 400,
|
||||
"KeepChargeFullFrame": 75,
|
||||
"InkConsumeFullCharge": 0.18,
|
||||
"InkConsumeMinCharge": 0.0225
|
||||
},
|
||||
"2020": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 0,
|
||||
"specialWeaponId": 8,
|
||||
"DamageParam_ValueFullCharge": 1600,
|
||||
"DamageParam_ValueMaxCharge": 800,
|
||||
"DamageParam_ValueMinCharge": 400,
|
||||
"InkConsumeFullCharge": 0.18,
|
||||
"InkConsumeMinCharge": 0.0225
|
||||
},
|
||||
"2030": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 10,
|
||||
"specialWeaponId": 7,
|
||||
"overwrites": {
|
||||
"ConsumeRt_Main": {
|
||||
"High": 0.5,
|
||||
"Mid": 0.7
|
||||
}
|
||||
},
|
||||
"WeaponSpeedType": "Slow",
|
||||
"DamageParam_ValueFullCharge": 1800,
|
||||
"DamageParam_ValueMaxCharge": 800,
|
||||
"DamageParam_ValueMinCharge": 400,
|
||||
"ChargeFrameFullCharge": 92,
|
||||
"KeepChargeFullFrame": 75,
|
||||
"InkConsumeFullCharge": 0.25,
|
||||
"InkConsumeMinCharge": 0.0225
|
||||
},
|
||||
"2040": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 10,
|
||||
"specialWeaponId": 7,
|
||||
"overwrites": {
|
||||
"ConsumeRt_Main": {
|
||||
"High": 0.5,
|
||||
"Mid": 0.7
|
||||
}
|
||||
},
|
||||
"WeaponSpeedType": "Slow",
|
||||
"DamageParam_ValueFullCharge": 1800,
|
||||
"DamageParam_ValueMaxCharge": 800,
|
||||
"DamageParam_ValueMinCharge": 400,
|
||||
"ChargeFrameFullCharge": 92,
|
||||
"InkConsumeFullCharge": 0.25,
|
||||
"InkConsumeMinCharge": 0.0225
|
||||
},
|
||||
"2050": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 7,
|
||||
"specialWeaponId": 9,
|
||||
"WeaponSpeedType": "Fast",
|
||||
"DamageParam_ValueFullCharge": 850,
|
||||
"DamageParam_ValueMaxCharge": 850,
|
||||
"DamageParam_ValueMinCharge": 300,
|
||||
"ChargeFrameFullCharge": 20,
|
||||
"InkConsumeFullCharge": 0.084,
|
||||
"InkConsumeMinCharge": 0.0336
|
||||
},
|
||||
"2060": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 13,
|
||||
"specialWeaponId": 4,
|
||||
"DamageParam_ValueFullCharge": 1800,
|
||||
"DamageParam_ValueMaxCharge": 1300,
|
||||
"DamageParam_ValueMinCharge": 400,
|
||||
"ChargeFrameFullCharge": 75,
|
||||
"KeepChargeFullFrame": 300,
|
||||
"InkConsumeFullCharge": 0.15,
|
||||
"InkConsumeMinCharge": 0.02
|
||||
},
|
||||
"3000": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 0,
|
||||
"specialWeaponId": 14,
|
||||
"InkRecoverStop": 40,
|
||||
"InkConsumeSlosher": 0.07
|
||||
},
|
||||
"3010": {
|
||||
"SpecialPoint": 190,
|
||||
"subWeaponId": 11,
|
||||
"specialWeaponId": 10,
|
||||
"WeaponSpeedType": "Fast",
|
||||
"InkRecoverStop": 35,
|
||||
"InkConsumeSlosher": 0.06
|
||||
},
|
||||
"3020": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 5,
|
||||
"specialWeaponId": 6,
|
||||
"InkRecoverStop": 42,
|
||||
"InkConsumeSlosher": 0.084
|
||||
},
|
||||
"3030": {
|
||||
"SpecialPoint": 190,
|
||||
"subWeaponId": 3,
|
||||
"specialWeaponId": 5,
|
||||
"InkRecoverStop": 40,
|
||||
"InkConsumeSlosher": 0.08
|
||||
},
|
||||
"3040": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 9,
|
||||
"specialWeaponId": 5,
|
||||
"WeaponSpeedType": "Slow",
|
||||
"InkRecoverStop": 70,
|
||||
"InkConsumeSlosher": 0.117
|
||||
},
|
||||
"4000": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 2,
|
||||
"specialWeaponId": 11,
|
||||
"overwrites": {
|
||||
"MoveVelRt_Shot": {
|
||||
"High": 1.4,
|
||||
"Low": 1,
|
||||
"Mid": 1.2
|
||||
}
|
||||
},
|
||||
"DamageParam_ValueMax": 320,
|
||||
"DamageParam_ValueMin": 160,
|
||||
"InkRecoverStop": 30,
|
||||
"InkConsumeFullChargeSplatling": 0.1725
|
||||
},
|
||||
"4010": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 3,
|
||||
"specialWeaponId": 7,
|
||||
"overwrites": {
|
||||
"MoveVelRt_Shot": {
|
||||
"High": 1.35,
|
||||
"Low": 1,
|
||||
"Mid": 1.175
|
||||
}
|
||||
},
|
||||
"DamageParam_ValueMax": 300,
|
||||
"DamageParam_ValueMin": 150,
|
||||
"InkRecoverStop": 40,
|
||||
"InkConsumeFullChargeSplatling": 0.225
|
||||
},
|
||||
"4020": {
|
||||
"SpecialPoint": 190,
|
||||
"subWeaponId": 7,
|
||||
"specialWeaponId": 6,
|
||||
"overwrites": {
|
||||
"ConsumeRt_Main": {
|
||||
"High": 0.5,
|
||||
"Low": 1,
|
||||
"Mid": 0.7
|
||||
},
|
||||
"MoveVelRt_Shot": {
|
||||
"High": 1.35,
|
||||
"Low": 1,
|
||||
"Mid": 1.175
|
||||
}
|
||||
},
|
||||
"WeaponSpeedType": "Slow",
|
||||
"DamageParam_ValueMax": 320,
|
||||
"DamageParam_ValueMin": 160,
|
||||
"InkRecoverStop": 40,
|
||||
"InkConsumeFullChargeSplatling": 0.35
|
||||
},
|
||||
"4030": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 5,
|
||||
"specialWeaponId": 10,
|
||||
"overwrites": {
|
||||
"MoveVelRt_Shot": {
|
||||
"High": 1.25,
|
||||
"Low": 1,
|
||||
"Mid": 1.125
|
||||
}
|
||||
},
|
||||
"DamageParam_ValueMax": 280,
|
||||
"DamageParam_ValueMin": 140,
|
||||
"InkRecoverStop": 40,
|
||||
"InkConsumeFullChargeSplatling": 0.25
|
||||
},
|
||||
"4040": {
|
||||
"SpecialPoint": 190,
|
||||
"subWeaponId": 9,
|
||||
"specialWeaponId": 5,
|
||||
"overwrites": {
|
||||
"MoveVelRt_Shot": {
|
||||
"High": 1.3,
|
||||
"Low": 1,
|
||||
"Mid": 1.15
|
||||
}
|
||||
},
|
||||
"DamageParam_ValueMax": 320,
|
||||
"DamageParam_ValueMin": 160,
|
||||
"KeepChargeFullFrame": 200,
|
||||
"InkRecoverStop": 40,
|
||||
"InkConsumeFullChargeSplatling": 0.15
|
||||
},
|
||||
"5000": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 8,
|
||||
"specialWeaponId": 15,
|
||||
"WeaponSpeedType": "Fast",
|
||||
"DamageParam_ValueMax": 360,
|
||||
"DamageParam_ValueMin": 180,
|
||||
"InkConsume": 0.00663,
|
||||
"InkConsume_SideStepParam": 0.05
|
||||
},
|
||||
"5010": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 1,
|
||||
"specialWeaponId": 12,
|
||||
"DamageParam_ValueMax": 300,
|
||||
"DamageParam_ValueMin": 150,
|
||||
"InkConsume": 0.0072,
|
||||
"InkConsume_SideStepParam": 0.07
|
||||
},
|
||||
"5020": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 4,
|
||||
"specialWeaponId": 6,
|
||||
"DamageParam_ValueMax": 360,
|
||||
"DamageParam_ValueMin": 180,
|
||||
"InkConsume": 0.014,
|
||||
"InkConsume_SideStepParam": 0.08
|
||||
},
|
||||
"5030": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 0,
|
||||
"specialWeaponId": 7,
|
||||
"DamageParam_ValueMax": 280,
|
||||
"DamageParam_ValueMin": 140,
|
||||
"InkConsume": 0.012,
|
||||
"InkConsume_SideStepParam": 0.08
|
||||
},
|
||||
"5040": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 7,
|
||||
"specialWeaponId": 13,
|
||||
"DamageParam_ValueMax": 280,
|
||||
"DamageParam_ValueMin": 140,
|
||||
"InkConsume": 0.008,
|
||||
"InkConsume_SideStepParam": 0.03
|
||||
},
|
||||
"6000": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 3,
|
||||
"specialWeaponId": 14,
|
||||
"DamageParam_ValueMax": 810,
|
||||
"InkConsumeUmbrella_WeaponShelterCanopyParam": 0.3
|
||||
},
|
||||
"6010": {
|
||||
"SpecialPoint": 190,
|
||||
"subWeaponId": 8,
|
||||
"specialWeaponId": 8,
|
||||
"overwrites": {
|
||||
"ConsumeRt_Main": {
|
||||
"High": 0.5,
|
||||
"Mid": 0.7
|
||||
}
|
||||
},
|
||||
"WeaponSpeedType": "Slow",
|
||||
"DamageParam_ValueMax": 1190,
|
||||
"CanopyHP": 7000,
|
||||
"InkConsumeUmbrella_WeaponShelterCanopyParam": 0.3,
|
||||
"InkConsume_WeaponShelterShotgunParam": 0.11
|
||||
},
|
||||
"6020": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 10,
|
||||
"specialWeaponId": 13,
|
||||
"WeaponSpeedType": "Fast",
|
||||
"DamageParam_ValueMax": 400,
|
||||
"CanopyHP": 2000,
|
||||
"InkConsume_WeaponShelterShotgunParam": 0.04
|
||||
},
|
||||
"7010": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 11,
|
||||
"specialWeaponId": 9,
|
||||
"ChargeFrameFullCharge": 72,
|
||||
"InkConsumeFullCharge_ChargeParam": 0.085
|
||||
},
|
||||
"7020": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 6,
|
||||
"specialWeaponId": 4,
|
||||
"WeaponSpeedType": "Fast",
|
||||
"ChargeFrameFullCharge": 34,
|
||||
"KeepChargeFullFrame": 75,
|
||||
"InkConsumeFullCharge_ChargeParam": 0.065
|
||||
},
|
||||
"8000": {
|
||||
"SpecialPoint": 200,
|
||||
"subWeaponId": 2,
|
||||
"specialWeaponId": 3,
|
||||
"InkConsume_SwingParam": 0.04,
|
||||
"InkConsumeFullCharge_ChargeParam": 0.09
|
||||
},
|
||||
"8010": {
|
||||
"SpecialPoint": 180,
|
||||
"subWeaponId": 13,
|
||||
"specialWeaponId": 11,
|
||||
"WeaponSpeedType": "Fast",
|
||||
"InkConsume_SwingParam": 0.03,
|
||||
"InkConsumeFullCharge_ChargeParam": 0.06
|
||||
}
|
||||
},
|
||||
"subWeapons": {
|
||||
"0": {
|
||||
"overwrites": {
|
||||
"SpawnSpeedZSpecUp": {
|
||||
"High": 1.68,
|
||||
"Low": 1.12,
|
||||
"Mid": 1.4
|
||||
}
|
||||
},
|
||||
"InkRecoverStop": 60,
|
||||
"DistanceDamage": [
|
||||
{
|
||||
"Damage": 1800,
|
||||
"Distance": 3.6
|
||||
},
|
||||
{
|
||||
"Damage": 300,
|
||||
"Distance": 7
|
||||
}
|
||||
]
|
||||
},
|
||||
"1": {
|
||||
"overwrites": {
|
||||
"SpawnSpeedZSpecUp": {
|
||||
"High": 1.68,
|
||||
"Low": 1.12,
|
||||
"Mid": 1.4
|
||||
}
|
||||
},
|
||||
"InkRecoverStop": 60,
|
||||
"DistanceDamage": [
|
||||
{
|
||||
"Damage": 1800,
|
||||
"Distance": 4.6
|
||||
},
|
||||
{
|
||||
"Damage": 300,
|
||||
"Distance": 8
|
||||
}
|
||||
]
|
||||
},
|
||||
"2": {
|
||||
"overwrites": {
|
||||
"SpawnSpeedZSpecUp": {
|
||||
"High": 1.68,
|
||||
"Low": 1.12,
|
||||
"Mid": 1.4
|
||||
}
|
||||
},
|
||||
"SubInkSaveLv": 0,
|
||||
"InkConsume": 0.4,
|
||||
"InkRecoverStop": 50,
|
||||
"DistanceDamage": [
|
||||
{
|
||||
"Damage": 350,
|
||||
"Distance": 2.8
|
||||
},
|
||||
{
|
||||
"Damage": 250,
|
||||
"Distance": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
"3": {
|
||||
"overwrites": {
|
||||
"PeriodFirst": {
|
||||
"High": 600,
|
||||
"Low": 300,
|
||||
"Mid": 450
|
||||
},
|
||||
"PeriodSecond": {
|
||||
"High": 1020,
|
||||
"Low": 900,
|
||||
"Mid": 960
|
||||
}
|
||||
},
|
||||
"SubInkSaveLv": 3,
|
||||
"InkConsume": 0.6,
|
||||
"InkRecoverStop": 60
|
||||
},
|
||||
"4": {
|
||||
"overwrites": {
|
||||
"MaxHP": {
|
||||
"High": 15000,
|
||||
"Low": 8000,
|
||||
"Mid": 11500
|
||||
}
|
||||
},
|
||||
"InkConsume": 0.6,
|
||||
"InkRecoverStop": 85
|
||||
},
|
||||
"5": {
|
||||
"overwrites": {
|
||||
"SpawnSpeedZSpecUp": {
|
||||
"High": 1.84,
|
||||
"Low": 1.36,
|
||||
"Mid": 1.6
|
||||
}
|
||||
},
|
||||
"SubInkSaveLv": 1,
|
||||
"InkConsume": 0.6,
|
||||
"InkRecoverStop": 70,
|
||||
"DistanceDamage_BlastParamArray": [
|
||||
[
|
||||
{
|
||||
"Damage": 500,
|
||||
"Distance": 1.6
|
||||
},
|
||||
{
|
||||
"Damage": 350,
|
||||
"Distance": 3.8
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"Damage": 500,
|
||||
"Distance": 1.95
|
||||
},
|
||||
{
|
||||
"Damage": 350,
|
||||
"Distance": 4.5
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"Damage": 500,
|
||||
"Distance": 2.6
|
||||
},
|
||||
{
|
||||
"Damage": 350,
|
||||
"Distance": 5.45
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"6": {
|
||||
"overwrites": {
|
||||
"SpawnSpeedZSpecUp": {
|
||||
"High": 0.52,
|
||||
"Low": 0.4,
|
||||
"Mid": 0.46
|
||||
}
|
||||
},
|
||||
"InkRecoverStop": 70,
|
||||
"DistanceDamage_BlastParamMaxCharge": [
|
||||
{
|
||||
"Damage": 1800,
|
||||
"Distance": 4.6
|
||||
},
|
||||
{
|
||||
"Damage": 300,
|
||||
"Distance": 8
|
||||
}
|
||||
],
|
||||
"DistanceDamage_BlastParamMinCharge": [
|
||||
{
|
||||
"Damage": 1800,
|
||||
"Distance": 1.6
|
||||
},
|
||||
{
|
||||
"Damage": 300,
|
||||
"Distance": 5
|
||||
}
|
||||
],
|
||||
"DirectDamage": 200
|
||||
},
|
||||
"7": {
|
||||
"overwrites": {
|
||||
"SpawnSpeedZSpecUp": {
|
||||
"High": 1.68,
|
||||
"Low": 1.12,
|
||||
"Mid": 1.4
|
||||
}
|
||||
},
|
||||
"SubInkSaveLv": 1,
|
||||
"InkConsume": 0.55,
|
||||
"InkRecoverStop": 85,
|
||||
"DistanceDamage": [
|
||||
{
|
||||
"Damage": 1800,
|
||||
"Distance": 2.85
|
||||
},
|
||||
{
|
||||
"Damage": 300,
|
||||
"Distance": 6.5
|
||||
}
|
||||
]
|
||||
},
|
||||
"8": {
|
||||
"overwrites": {},
|
||||
"SubInkSaveLv": 3,
|
||||
"InkConsume": 0.75,
|
||||
"InkRecoverStop": 0
|
||||
},
|
||||
"9": {
|
||||
"overwrites": {
|
||||
"SpawnSpeedZSpecUp": {
|
||||
"High": 1.87,
|
||||
"Low": 1.38,
|
||||
"Mid": 1.64
|
||||
},
|
||||
"MarkingFrameSubSpec": {
|
||||
"High": 960,
|
||||
"Low": 480,
|
||||
"Mid": 720
|
||||
}
|
||||
},
|
||||
"SubInkSaveLv": 1,
|
||||
"InkConsume": 0.45,
|
||||
"InkRecoverStop": 75
|
||||
},
|
||||
"10": {
|
||||
"overwrites": {
|
||||
"MarkingFrameSubSpec": {
|
||||
"High": 600,
|
||||
"Low": 300,
|
||||
"Mid": 450
|
||||
},
|
||||
"SensorRadius": {
|
||||
"High": 4,
|
||||
"Low": 3,
|
||||
"Mid": 3.5
|
||||
},
|
||||
"ExplosionRadius": {
|
||||
"High": 11,
|
||||
"Low": 8,
|
||||
"Mid": 9.5
|
||||
}
|
||||
},
|
||||
"SubInkSaveLv": 3,
|
||||
"InkConsume": 0.6,
|
||||
"InkRecoverStop": 0,
|
||||
"DistanceDamage": [
|
||||
{
|
||||
"Damage": 450,
|
||||
"Distance": 3.6
|
||||
},
|
||||
{
|
||||
"Damage": 350,
|
||||
"Distance": 8
|
||||
}
|
||||
]
|
||||
},
|
||||
"11": {
|
||||
"overwrites": {
|
||||
"SpawnSpeedZSpecUp": {
|
||||
"High": 1.68,
|
||||
"Low": 1.12,
|
||||
"Mid": 1.4
|
||||
}
|
||||
},
|
||||
"SubInkSaveLv": 1,
|
||||
"InkConsume": 0.6,
|
||||
"InkRecoverStop": 75
|
||||
},
|
||||
"12": {
|
||||
"overwrites": {
|
||||
"SpawnSpeedZSpecUp": {
|
||||
"High": 5.4,
|
||||
"Low": 5,
|
||||
"Mid": 5.2
|
||||
},
|
||||
"MarkingFrameSubSpec": {
|
||||
"High": 600,
|
||||
"Low": 300,
|
||||
"Mid": 450
|
||||
}
|
||||
},
|
||||
"SubInkSaveLv": 0,
|
||||
"InkConsume": 0.4,
|
||||
"InkRecoverStop": 50,
|
||||
"DirectDamage": 300
|
||||
},
|
||||
"13": {
|
||||
"overwrites": {
|
||||
"SpawnSpeedZSpecUp": {
|
||||
"High": 1.84,
|
||||
"Low": 1.36,
|
||||
"Mid": 1.6
|
||||
}
|
||||
},
|
||||
"InkConsume": 0.65,
|
||||
"InkRecoverStop": 88,
|
||||
"DistanceDamage_BlastParamChase": [
|
||||
{
|
||||
"Damage": 600,
|
||||
"Distance": 2.6
|
||||
},
|
||||
{
|
||||
"Damage": 350,
|
||||
"Distance": 6
|
||||
}
|
||||
],
|
||||
"DistanceDamage_SplashBlastParam": [
|
||||
{
|
||||
"Damage": 120,
|
||||
"Distance": 2.6
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
export const DEFAULT_LANGUAGE = "en";
|
||||
|
||||
export const languages = [
|
||||
{
|
||||
code: "da",
|
||||
name: "Dansk",
|
||||
},
|
||||
{
|
||||
code: "de",
|
||||
name: "Deutsch",
|
||||
|
|
|
|||
|
|
@ -1,23 +1,29 @@
|
|||
export const headGearIds = [
|
||||
1, 1000, 1002, 1003, 1005, 1012, 1020, 1021, 1028, 1036, 2008, 3001, 3003,
|
||||
3008, 3009, 3011, 3012, 3016, 3021, 3025, 3026, 3027, 4004, 4008, 4015, 4016,
|
||||
4017, 5001, 5004, 5007, 5008, 6001, 6003, 6004, 7007, 7012, 7013, 7014, 8005,
|
||||
8008, 8011, 8014, 8016, 9003, 9007, 9009,
|
||||
1, 1000, 1002, 1003, 1005, 1012, 1020, 1021, 1028, 1036, 2008, 3001, 3002,
|
||||
3003, 3008, 3009, 3011, 3012, 3016, 3021, 3022, 3023, 3024, 3025, 3026, 3027,
|
||||
3029, 4004, 4006, 4008, 4015, 4016, 4017, 4019, 5000, 5001, 5004, 5007, 5008,
|
||||
6001, 6003, 6004, 7007, 7012, 7013, 7014, 8005, 8008, 8011, 8014, 8015, 8016,
|
||||
9003, 9007, 9009, 21010, 25000, 25001, 25002, 25003, 25004, 25005, 25006,
|
||||
25007, 25008, 25009, 25010, 25016, 25017, 27000, 27004, 27109, 27306, 28000,
|
||||
] as const;
|
||||
|
||||
export const clothesGearIds = [
|
||||
1001, 1004, 1005, 1006, 1013, 1014, 1015, 1016, 1018, 1019, 1020, 1021, 1035,
|
||||
1062, 1066, 1069, 1070, 1071, 1072, 1073, 1074, 1075, 1076, 1077, 1082, 1083,
|
||||
1084, 1085, 1088, 1091, 2004, 3000, 3001, 3006, 3008, 3009, 4004, 4005, 4009,
|
||||
4010, 5000, 5001, 5006, 5014, 5015, 5019, 5023, 5045, 5046, 5047, 5048, 5049,
|
||||
5050, 5051, 5054, 6000, 6008, 7001, 7010, 7016, 7017, 7018, 8000, 8003, 8005,
|
||||
8012, 8017, 8018, 8019, 8020, 8024, 8025, 8030, 8031, 8033, 8034, 8036, 8040,
|
||||
9010, 9011, 10006, 10012,
|
||||
1000, 1001, 1004, 1005, 1006, 1013, 1014, 1015, 1016, 1018, 1019, 1020, 1021,
|
||||
1035, 1062, 1063, 1066, 1067, 1069, 1070, 1071, 1072, 1073, 1074, 1075, 1076,
|
||||
1077, 1082, 1083, 1084, 1085, 1088, 1090, 1091, 2004, 3000, 3001, 3004, 3006,
|
||||
3008, 3009, 4004, 4005, 4009, 4010, 5000, 5001, 5006, 5014, 5015, 5019, 5023,
|
||||
5045, 5046, 5047, 5048, 5049, 5050, 5051, 5054, 6000, 6008, 7001, 7010, 7016,
|
||||
7017, 7018, 8000, 8003, 8005, 8012, 8017, 8018, 8019, 8020, 8024, 8025, 8030,
|
||||
8031, 8033, 8034, 8036, 8040, 9010, 9011, 9012, 9013, 10006, 10012, 10014,
|
||||
25000, 25001, 25002, 25003, 25004, 25005, 25006, 25007, 25008, 25009, 25010,
|
||||
25014, 25015, 25017, 26000, 27000, 27004, 27306,
|
||||
] as const;
|
||||
|
||||
export const shoesGearIds = [
|
||||
1000, 1008, 1009, 1021, 1022, 1024, 2000, 2001, 2004, 2005, 2016, 2017, 2018,
|
||||
2042, 2043, 2045, 3001, 3004, 3013, 3020, 3022, 3023, 3024, 3025, 4000, 4001,
|
||||
4007, 4008, 4009, 4011, 4012, 4015, 4016, 4017, 4021, 4022, 5000, 5001, 6000,
|
||||
6001, 6006, 6007, 6012, 6021, 7002, 8010, 8013, 8014,
|
||||
1000, 1008, 1009, 1021, 1022, 1023, 1024, 2000, 2001, 2003, 2004, 2005, 2016,
|
||||
2017, 2018, 2042, 2043, 2045, 3000, 3001, 3004, 3013, 3020, 3022, 3023, 3024,
|
||||
3025, 3026, 4000, 4001, 4007, 4008, 4009, 4011, 4012, 4014, 4015, 4016, 4017,
|
||||
4021, 4022, 5000, 5001, 6000, 6001, 6006, 6007, 6012, 6020, 6021, 6023, 6025,
|
||||
7002, 8010, 8013, 8014, 25000, 25001, 25002, 25003, 25004, 25005, 25006,
|
||||
25007, 25008, 25009, 25010, 25014, 25015, 27000, 27004, 27306,
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,35 @@
|
|||
export { stages } from "./stages";
|
||||
export { modes, modesShort } from "./modes";
|
||||
export { weaponIds } from "./weapon-ids";
|
||||
export {
|
||||
mainWeaponIds,
|
||||
subWeaponIds,
|
||||
specialWeaponIds,
|
||||
SPLAT_BOMB_ID,
|
||||
SUCTION_BOMB_ID,
|
||||
BURST_BOMB_ID,
|
||||
SPRINKLER_ID,
|
||||
SPLASH_WALL_ID,
|
||||
FIZZY_BOMB_ID,
|
||||
CURLING_BOMB_ID,
|
||||
AUTO_BOMB_ID,
|
||||
SQUID_BEAKON_ID,
|
||||
POINT_SENSOR_ID,
|
||||
INK_MINE_ID,
|
||||
TOXIC_MIST_ID,
|
||||
ANGLE_SHOOTER_ID,
|
||||
TORPEDO_ID,
|
||||
} from "./weapon-ids";
|
||||
export { headGearIds, clothesGearIds, shoesGearIds } from "./gear-ids";
|
||||
export { abilitiesShort, abilities } from "./abilities";
|
||||
export type { Ability, AbilityType, ModeShort, Stage } from "./types";
|
||||
export type {
|
||||
Ability,
|
||||
AbilityType,
|
||||
ModeShort,
|
||||
Stage,
|
||||
BuildAbilitiesTuple,
|
||||
BuildAbilitiesTupleWithUnknown,
|
||||
MainWeaponId,
|
||||
SubWeaponId,
|
||||
SpecialWeaponId,
|
||||
} from "./types";
|
||||
export { isAbility } from "./utils";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import type { abilities } from "./abilities";
|
||||
import type { modes } from "./modes";
|
||||
import type { stages } from "./stages";
|
||||
import type {
|
||||
subWeaponIds,
|
||||
mainWeaponIds,
|
||||
specialWeaponIds,
|
||||
} from "./weapon-ids";
|
||||
|
||||
export type ModeShort = typeof modes[number]["short"];
|
||||
|
||||
|
|
@ -10,6 +15,10 @@ export type Ability = typeof abilities[number]["name"];
|
|||
export type AbilityWithUnknown = typeof abilities[number]["name"] | "UNKNOWN";
|
||||
export type AbilityType = typeof abilities[number]["type"];
|
||||
|
||||
export type MainWeaponId = typeof mainWeaponIds[number];
|
||||
export type SubWeaponId = typeof subWeaponIds[number];
|
||||
export type SpecialWeaponId = typeof specialWeaponIds[number];
|
||||
|
||||
export type BuildAbilitiesTuple = [
|
||||
head: [main: Ability, s1: Ability, s2: Ability, s3: Ability],
|
||||
clothes: [main: Ability, s1: Ability, s2: Ability, s3: Ability],
|
||||
|
|
|
|||
6
app/modules/in-game-lists/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { abilities } from "./abilities";
|
||||
import type { Ability } from "./types";
|
||||
|
||||
export function isAbility(value: string): value is Ability {
|
||||
return Boolean(abilities.some((a) => a.name === value));
|
||||
}
|
||||
|
|
@ -1,6 +1,42 @@
|
|||
export const weaponIds = [
|
||||
0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 200, 210, 220, 230, 240, 250, 300, 310,
|
||||
400, 1000, 1010, 1020, 1030, 1100, 1110, 2000, 2010, 2020, 2030, 2040, 2050,
|
||||
2060, 3000, 3010, 3020, 3030, 3040, 4000, 4010, 4020, 4030, 4040, 5000, 5010,
|
||||
5020, 5030, 5040, 6000, 6010, 6020, 7010, 8000, 8010,
|
||||
export const mainWeaponIds = [
|
||||
0, 10, 20, 30, 40, 45, 50, 60, 70, 80, 90, 200, 210, 220, 230, 240, 250, 300,
|
||||
310, 400, 1000, 1010, 1020, 1030, 1100, 1110, 2000, 2010, 2020, 2030, 2040,
|
||||
2050, 2060, 3000, 3010, 3020, 3030, 3040, 4000, 4010, 4020, 4030, 4040, 5000,
|
||||
5010, 5020, 5030, 5040, 6000, 6010, 6020, 7010, 7020, 8000, 8010,
|
||||
] as const;
|
||||
|
||||
export const SPLAT_BOMB_ID = 0;
|
||||
export const SUCTION_BOMB_ID = 1;
|
||||
export const BURST_BOMB_ID = 2;
|
||||
export const SPRINKLER_ID = 3;
|
||||
export const SPLASH_WALL_ID = 4;
|
||||
export const FIZZY_BOMB_ID = 5;
|
||||
export const CURLING_BOMB_ID = 6;
|
||||
export const AUTO_BOMB_ID = 7;
|
||||
export const SQUID_BEAKON_ID = 8;
|
||||
export const POINT_SENSOR_ID = 9;
|
||||
export const INK_MINE_ID = 10;
|
||||
export const TOXIC_MIST_ID = 11;
|
||||
export const ANGLE_SHOOTER_ID = 12;
|
||||
export const TORPEDO_ID = 13;
|
||||
|
||||
export const subWeaponIds = [
|
||||
SPLAT_BOMB_ID,
|
||||
SUCTION_BOMB_ID,
|
||||
BURST_BOMB_ID,
|
||||
SPRINKLER_ID,
|
||||
SPLASH_WALL_ID,
|
||||
FIZZY_BOMB_ID,
|
||||
CURLING_BOMB_ID,
|
||||
AUTO_BOMB_ID,
|
||||
SQUID_BEAKON_ID,
|
||||
POINT_SENSOR_ID,
|
||||
INK_MINE_ID,
|
||||
TOXIC_MIST_ID,
|
||||
ANGLE_SHOOTER_ID,
|
||||
TORPEDO_ID,
|
||||
] as const;
|
||||
|
||||
export const specialWeaponIds = [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { useChangeLanguage } from "remix-i18next";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Theme, ThemeHead, useTheme, ThemeProvider } from "./modules/theme";
|
||||
import { getThemeSession } from "./modules/theme/session.server";
|
||||
import { COMMON_PREVIEW_IMAGE } from "./utils/urls";
|
||||
|
||||
export const unstable_shouldReload: ShouldReloadFunction = () => false;
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ export const meta: MetaFunction = () => ({
|
|||
title: "sendou.ink",
|
||||
viewport: "width=device-width,initial-scale=1",
|
||||
"theme-color": "#8263de",
|
||||
"og:image": COMMON_PREVIEW_IMAGE,
|
||||
});
|
||||
|
||||
export interface RootLoaderData {
|
||||
|
|
|
|||
672
app/routes/analyzer.tsx
Normal file
|
|
@ -0,0 +1,672 @@
|
|||
import { type MetaFunction, type LinksFunction } from "@remix-run/node";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AbilitiesSelector } from "~/components/AbilitiesSelector";
|
||||
import { Ability } from "~/components/Ability";
|
||||
import { WeaponCombobox } from "~/components/Combobox";
|
||||
import { Image } from "~/components/Image";
|
||||
import { Main } from "~/components/Main";
|
||||
import { Toggle } from "~/components/Toggle";
|
||||
import { ADMIN_DISCORD_ID } from "~/constants";
|
||||
import { useSetTitle } from "~/hooks/useSetTitle";
|
||||
import type { AnalyzedBuild, Stat } from "~/modules/analyzer";
|
||||
import { MAX_LDE_INTENSITY } from "~/modules/analyzer";
|
||||
import { useAnalyzeBuild } from "~/modules/analyzer";
|
||||
import {
|
||||
lastDitchEffortIntensityToAp,
|
||||
SPECIAL_EFFECTS,
|
||||
} from "~/modules/analyzer/specialEffects";
|
||||
import type {
|
||||
AbilityPoints,
|
||||
SpecialEffectType,
|
||||
} from "~/modules/analyzer/types";
|
||||
import { useUser } from "~/modules/auth";
|
||||
import type { BuildAbilitiesTupleWithUnknown } from "~/modules/in-game-lists";
|
||||
import {
|
||||
SPLASH_WALL_ID,
|
||||
SPRINKLER_ID,
|
||||
TOXIC_MIST_ID,
|
||||
} from "~/modules/in-game-lists";
|
||||
import { ANGLE_SHOOTER_ID } from "~/modules/in-game-lists";
|
||||
import { INK_MINE_ID, POINT_SENSOR_ID } from "~/modules/in-game-lists";
|
||||
import {
|
||||
abilities,
|
||||
isAbility,
|
||||
type MainWeaponId,
|
||||
type SubWeaponId,
|
||||
} from "~/modules/in-game-lists";
|
||||
import styles from "~/styles/analyzer.css";
|
||||
import { makeTitle } from "~/utils/strings";
|
||||
import { specialWeaponImageUrl, subWeaponImageUrl } from "~/utils/urls";
|
||||
|
||||
export const CURRENT_PATCH = "1.1";
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return {
|
||||
title: makeTitle("Build Analyzer"),
|
||||
};
|
||||
};
|
||||
|
||||
export const links: LinksFunction = () => {
|
||||
return [{ rel: "stylesheet", href: styles }];
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
i18n: ["weapons", "analyzer"],
|
||||
};
|
||||
|
||||
const canViewInProduction = (discordId?: string) => {
|
||||
const LEAN_ID = "86905636402495488";
|
||||
const SENDOU_ID = ADMIN_DISCORD_ID;
|
||||
|
||||
return discordId === LEAN_ID || discordId === SENDOU_ID;
|
||||
};
|
||||
|
||||
export default function BuildAnalyzerPage() {
|
||||
const user = useUser();
|
||||
const { t } = useTranslation(["analyzer", "common", "weapons"]);
|
||||
useSetTitle(t("common:pages.buildAnalyzer"));
|
||||
const {
|
||||
build,
|
||||
mainWeaponId,
|
||||
handleChange,
|
||||
analyzed,
|
||||
abilityPoints,
|
||||
ldeIntensity,
|
||||
effects,
|
||||
} = useAnalyzeBuild();
|
||||
|
||||
if (
|
||||
process.env.NODE_ENV === "production" &&
|
||||
!canViewInProduction(user?.discordId)
|
||||
) {
|
||||
return <Main>Coming soon :)</Main>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Main>
|
||||
<div className="analyzer__container">
|
||||
<div className="analyzer__left-column">
|
||||
<div className="stack sm items-center w-full">
|
||||
<div className="w-full">
|
||||
<WeaponCombobox
|
||||
inputName="weapon"
|
||||
initialWeaponId={mainWeaponId}
|
||||
onChange={(opt) =>
|
||||
opt &&
|
||||
handleChange({
|
||||
newMainWeaponId: Number(opt.value) as MainWeaponId,
|
||||
})
|
||||
}
|
||||
className="w-full-important"
|
||||
clearsInputOnFocus
|
||||
/>
|
||||
</div>
|
||||
<WeaponInfoBadges analyzed={analyzed} />
|
||||
</div>
|
||||
<div className="stack md items-center">
|
||||
<AbilitiesSelector
|
||||
selectedAbilities={build}
|
||||
onChange={(newBuild) => handleChange({ newBuild })}
|
||||
/>
|
||||
<EffectsSelector
|
||||
build={build}
|
||||
ldeIntensity={ldeIntensity}
|
||||
handleLdeIntensityChange={(newLdeIntensity) =>
|
||||
handleChange({ newLdeIntensity })
|
||||
}
|
||||
handleAddEffect={(newEffect) =>
|
||||
handleChange({ newEffects: [...effects, newEffect] })
|
||||
}
|
||||
handleRemoveEffect={(effectToRemove) =>
|
||||
handleChange({
|
||||
newEffects: effects.filter((e) => e !== effectToRemove),
|
||||
})
|
||||
}
|
||||
effects={effects}
|
||||
/>
|
||||
<AbilityPointsDetails abilityPoints={abilityPoints} />
|
||||
</div>
|
||||
<div className="analyzer__patch">
|
||||
{t("analyzer:patch")} {CURRENT_PATCH}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stack md">
|
||||
<StatCategory title={t("analyzer:stat.category.main")}>
|
||||
{typeof analyzed.stats.mainWeaponWhiteInkSeconds === "number" && (
|
||||
<StatCard
|
||||
stat={analyzed.stats.mainWeaponWhiteInkSeconds}
|
||||
title={t("analyzer:stat.whiteInk")}
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
)}
|
||||
{typeof analyzed.weapon.brellaCanopyHp === "number" && (
|
||||
<StatCard
|
||||
stat={analyzed.weapon.brellaCanopyHp}
|
||||
title={t("analyzer:stat.canopyHp")}
|
||||
suffix={t("analyzer:suffix.hp")}
|
||||
/>
|
||||
)}
|
||||
{typeof analyzed.weapon.fullChargeSeconds === "number" && (
|
||||
<StatCard
|
||||
stat={analyzed.weapon.fullChargeSeconds}
|
||||
title={t("analyzer:stat.fullChargeSeconds")}
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
)}
|
||||
{typeof analyzed.weapon.maxChargeHoldSeconds === "number" && (
|
||||
<StatCard
|
||||
stat={analyzed.weapon.maxChargeHoldSeconds}
|
||||
title={t("analyzer:stat.maxChargeHoldSeconds")}
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
)}
|
||||
</StatCategory>
|
||||
|
||||
<StatCategory title={t("analyzer:stat.category.sub")}>
|
||||
<StatCard
|
||||
stat={analyzed.stats.subWeaponWhiteInkSeconds}
|
||||
title={t("analyzer:stat.whiteInk")}
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
{analyzed.stats.subVelocity && (
|
||||
<StatCard
|
||||
stat={analyzed.stats.subVelocity}
|
||||
title={t("analyzer:stat.sub.velocity")}
|
||||
/>
|
||||
)}
|
||||
{analyzed.stats.subFirstPhaseDuration && (
|
||||
<StatCard
|
||||
stat={analyzed.stats.subFirstPhaseDuration}
|
||||
title={t("analyzer:stat.sub.firstPhaseDuration")}
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
)}
|
||||
{analyzed.stats.subSecondPhaseDuration && (
|
||||
<StatCard
|
||||
stat={analyzed.stats.subSecondPhaseDuration}
|
||||
title={t("analyzer:stat.sub.secondPhaseDuration")}
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
)}
|
||||
{analyzed.stats.subMarkingTimeInSeconds && (
|
||||
<StatCard
|
||||
stat={analyzed.stats.subMarkingTimeInSeconds}
|
||||
title={t("analyzer:stat.sub.markingTimeInSeconds")}
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
)}
|
||||
{analyzed.stats.subMarkingRadius && (
|
||||
<StatCard
|
||||
stat={analyzed.stats.subMarkingRadius}
|
||||
title={t("analyzer:stat.sub.markingRadius")}
|
||||
/>
|
||||
)}
|
||||
{analyzed.stats.subExplosionRadius && (
|
||||
<StatCard
|
||||
stat={analyzed.stats.subExplosionRadius}
|
||||
title={t("analyzer:stat.sub.explosionRadius")}
|
||||
/>
|
||||
)}
|
||||
{analyzed.stats.subHp && (
|
||||
<StatCard
|
||||
stat={analyzed.stats.subHp}
|
||||
title={t("analyzer:stat.sub.hp")}
|
||||
suffix={t("analyzer:suffix.hp")}
|
||||
/>
|
||||
)}
|
||||
</StatCategory>
|
||||
|
||||
<StatCategory title={t("analyzer:stat.category.special")}>
|
||||
<StatCard
|
||||
stat={analyzed.stats.specialPoint}
|
||||
title={t("analyzer:stat.specialPoints")}
|
||||
suffix={t("analyzer:suffix.specialPointsShort")}
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.specialSavedAfterDeath}
|
||||
title={t("analyzer:stat.specialLost")}
|
||||
suffix="%"
|
||||
/>
|
||||
</StatCategory>
|
||||
<StatCategory title={t("analyzer:stat.category.subDef")}>
|
||||
<StatCard
|
||||
stat={analyzed.stats.subDefAngleShooterDamage}
|
||||
title={t("analyzer:stat.damage", {
|
||||
weapon: t(`weapons:SUB_${ANGLE_SHOOTER_ID}`),
|
||||
})}
|
||||
suffix={t("analyzer:suffix.hp")}
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.subDefSplashWallDamagePercentage}
|
||||
title={t("analyzer:stat.damage", {
|
||||
weapon: t(`weapons:SUB_${SPLASH_WALL_ID}`),
|
||||
})}
|
||||
suffix="%"
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.subDefSprinklerDamagePercentage}
|
||||
title={t("analyzer:stat.damage", {
|
||||
weapon: t(`weapons:SUB_${SPRINKLER_ID}`),
|
||||
})}
|
||||
suffix="%"
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.subDefToxicMistMovementReduction}
|
||||
title={t("analyzer:stat.movementReduction", {
|
||||
weapon: t(`weapons:SUB_${TOXIC_MIST_ID}`),
|
||||
})}
|
||||
suffix="%"
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.subDefPointSensorMarkedTimeInSeconds}
|
||||
title={t("analyzer:stat.markedTime", {
|
||||
weapon: t(`weapons:SUB_${POINT_SENSOR_ID}`),
|
||||
})}
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.subDefInkMineMarkedTimeInSeconds}
|
||||
title={t("analyzer:stat.markedTime", {
|
||||
weapon: t(`weapons:SUB_${INK_MINE_ID}`),
|
||||
})}
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.subDefAngleShooterMarkedTimeInSeconds}
|
||||
title={t("analyzer:stat.markedTime", {
|
||||
weapon: t(`weapons:SUB_${ANGLE_SHOOTER_ID}`),
|
||||
})}
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
<div className="analyzer__stat-category-explanation">
|
||||
{t("analyzer:trackingSubDefExplanation")}
|
||||
</div>
|
||||
</StatCategory>
|
||||
|
||||
{analyzed.stats.damages.length > 0 && (
|
||||
<StatCategory
|
||||
title={t("analyzer:stat.category.damage")}
|
||||
containerClassName="analyzer__table-container"
|
||||
>
|
||||
<DamageTable
|
||||
values={analyzed.stats.damages}
|
||||
isTripleShooter={analyzed.weapon.isTripleShooter}
|
||||
subWeaponId={analyzed.weapon.subWeaponSplId}
|
||||
/>
|
||||
</StatCategory>
|
||||
)}
|
||||
|
||||
{analyzed.stats.fullInkTankOptions.length > 0 && (
|
||||
<StatCategory
|
||||
title={t("analyzer:stat.category.actionsPerInkTank")}
|
||||
containerClassName="analyzer__table-container"
|
||||
>
|
||||
<ConsumptionTable
|
||||
options={analyzed.stats.fullInkTankOptions}
|
||||
subWeaponId={analyzed.weapon.subWeaponSplId}
|
||||
/>
|
||||
</StatCategory>
|
||||
)}
|
||||
|
||||
<StatCategory title={t("analyzer:stat.category.movement")}>
|
||||
<StatCard
|
||||
stat={analyzed.stats.swimSpeed}
|
||||
title={t("analyzer:stat.swimSpeed")}
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.runSpeed}
|
||||
title={t("analyzer:stat.runSpeed")}
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.runSpeedInEnemyInk}
|
||||
title={t("analyzer:stat.runSpeedInEnemyInk")}
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.framesBeforeTakingDamageInEnemyInk}
|
||||
title={t("analyzer:stat.framesBeforeTakingDamageInEnemyInk")}
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.damageTakenInEnemyInkPerSecond}
|
||||
title={t("analyzer:stat.damageTakenInEnemyInkPerSecond")}
|
||||
suffix={t("analyzer:suffix.hp")}
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.enemyInkDamageLimit}
|
||||
title={t("analyzer:stat.enemyInkDamageLimit")}
|
||||
suffix={t("analyzer:suffix.hp")}
|
||||
/>
|
||||
</StatCategory>
|
||||
|
||||
<StatCategory title={t("analyzer:stat.category.misc")}>
|
||||
<StatCard
|
||||
stat={analyzed.stats.squidFormInkRecoverySeconds}
|
||||
title={t("analyzer:stat.squidFormInkRecoverySeconds")}
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.quickRespawnTime}
|
||||
title={t("analyzer:stat.quickRespawnTime")}
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.superJumpTimeGroundFrames}
|
||||
title={t("analyzer:stat.superJumpTimeGround")}
|
||||
/>
|
||||
<StatCard
|
||||
stat={analyzed.stats.superJumpTimeTotal}
|
||||
title={t("analyzer:stat.superJumpTimeTotal")}
|
||||
suffix={t("analyzer:suffix.seconds")}
|
||||
/>
|
||||
</StatCategory>
|
||||
</div>
|
||||
</div>
|
||||
</Main>
|
||||
);
|
||||
}
|
||||
|
||||
function WeaponInfoBadges({ analyzed }: { analyzed: AnalyzedBuild }) {
|
||||
const { t } = useTranslation(["weapons", "analyzer"]);
|
||||
|
||||
return (
|
||||
<div className="analyzer__weapon-info-badges">
|
||||
<div className="analyzer__weapon-info-badge">
|
||||
<Image
|
||||
path={subWeaponImageUrl(analyzed.weapon.subWeaponSplId)}
|
||||
width={20}
|
||||
height={20}
|
||||
alt={t(`weapons:SUB_${analyzed.weapon.subWeaponSplId}`)}
|
||||
/>
|
||||
{t(`weapons:SUB_${analyzed.weapon.subWeaponSplId}`)}
|
||||
</div>
|
||||
<div className="analyzer__weapon-info-badge">
|
||||
<Image
|
||||
path={specialWeaponImageUrl(analyzed.weapon.specialWeaponSplId)}
|
||||
width={20}
|
||||
height={20}
|
||||
alt={t(`weapons:SPECIAL_${analyzed.weapon.specialWeaponSplId}`)}
|
||||
/>
|
||||
{t(`weapons:SPECIAL_${analyzed.weapon.specialWeaponSplId}`)}
|
||||
</div>
|
||||
<div className="analyzer__weapon-info-badge">
|
||||
{t("analyzer:attribute.weight")}{" "}
|
||||
{t(`analyzer:attribute.weight.${analyzed.weapon.speedType}`)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EffectsSelector({
|
||||
build,
|
||||
effects,
|
||||
ldeIntensity,
|
||||
handleLdeIntensityChange,
|
||||
handleAddEffect,
|
||||
handleRemoveEffect,
|
||||
}: {
|
||||
build: BuildAbilitiesTupleWithUnknown;
|
||||
effects: Array<SpecialEffectType>;
|
||||
ldeIntensity: number;
|
||||
handleLdeIntensityChange: (newLdeIntensity: number) => void;
|
||||
handleAddEffect: (effect: SpecialEffectType) => void;
|
||||
handleRemoveEffect: (effect: SpecialEffectType) => void;
|
||||
}) {
|
||||
const { t } = useTranslation(["weapons", "analyzer"]);
|
||||
|
||||
const effectsToShow = SPECIAL_EFFECTS.filter(
|
||||
(effect) => !isAbility(effect.type) || build.flat().includes(effect.type)
|
||||
).reverse(); // reverse to show Tacticooler first as it always shows
|
||||
|
||||
return (
|
||||
<div className="analyzer__effects-selector">
|
||||
{effectsToShow.map((effect) => {
|
||||
return (
|
||||
<React.Fragment key={effect.type}>
|
||||
<div>
|
||||
{isAbility(effect.type) ? (
|
||||
<Ability ability={effect.type} size="SUB" />
|
||||
) : (
|
||||
<Image
|
||||
path={specialWeaponImageUrl(15)}
|
||||
alt={t("weapons:SPECIAL_15")}
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{effect.type === "LDE" ? (
|
||||
<select
|
||||
value={ldeIntensity}
|
||||
onChange={(e) =>
|
||||
handleLdeIntensityChange(Number(e.target.value))
|
||||
}
|
||||
className="analyzer__lde-intensity-select"
|
||||
>
|
||||
{new Array(MAX_LDE_INTENSITY + 1).fill(null).map((_, i) => {
|
||||
const percentage = ((i / MAX_LDE_INTENSITY) * 100)
|
||||
.toFixed(2)
|
||||
.replace(".00", "");
|
||||
|
||||
return (
|
||||
<option key={i} value={i}>
|
||||
{percentage}% (+{lastDitchEffortIntensityToAp(i)}{" "}
|
||||
{t("analyzer:abilityPoints.short")})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
) : (
|
||||
<Toggle
|
||||
checked={effects.includes(effect.type)}
|
||||
setChecked={(checked) =>
|
||||
checked
|
||||
? handleAddEffect(effect.type)
|
||||
: handleRemoveEffect(effect.type)
|
||||
}
|
||||
tiny
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AbilityPointsDetails({
|
||||
abilityPoints,
|
||||
}: {
|
||||
abilityPoints: AbilityPoints;
|
||||
}) {
|
||||
const { t } = useTranslation("analyzer");
|
||||
|
||||
return (
|
||||
<details className="w-full">
|
||||
<summary className="analyzer__ap-summary">{t("abilityPoints")}</summary>
|
||||
<div className="stack sm horizontal flex-wrap mt-4">
|
||||
{abilities
|
||||
.filter((a) => (abilityPoints.get(a.name) ?? 0) > 0)
|
||||
.sort((a, b) => {
|
||||
return abilityPoints.get(b.name)! - abilityPoints.get(a.name)!;
|
||||
})
|
||||
.map((a) => (
|
||||
<div key={a.name} className="stack items-center">
|
||||
<Ability ability={a.name} size="TINY" />
|
||||
<div className="analyzer__ap-text">
|
||||
{abilityPoints.get(a.name)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCategory({
|
||||
title,
|
||||
children,
|
||||
containerClassName = "analyzer__stat-collection",
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
containerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<details>
|
||||
<summary className="analyzer__summary">{title}</summary>
|
||||
<div className={containerClassName}>{children}</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
title,
|
||||
stat,
|
||||
suffix,
|
||||
}: {
|
||||
title: string;
|
||||
stat: Stat | number;
|
||||
suffix?: string;
|
||||
}) {
|
||||
const { t } = useTranslation("analyzer");
|
||||
const baseValue = typeof stat === "number" ? stat : stat.baseValue;
|
||||
|
||||
return (
|
||||
<div key={title} className="analyzer__stat-card">
|
||||
<div>
|
||||
<h3 className="analyzer__stat-card__title">{title}</h3>
|
||||
<div className="analyzer__stat-card-values">
|
||||
<div className="analyzer__stat-card__value">
|
||||
<h4 className="analyzer__stat-card__value__title">
|
||||
{typeof stat === "number" ? t("value") : t("base")}
|
||||
</h4>{" "}
|
||||
<div className="analyzer__stat-card__value__number">
|
||||
{baseValue}
|
||||
{suffix}
|
||||
</div>
|
||||
</div>
|
||||
{typeof stat !== "number" && stat.value !== stat.baseValue && (
|
||||
<div className="analyzer__stat-card__value">
|
||||
<h4 className="analyzer__stat-card__value__title">
|
||||
{t("build")}
|
||||
</h4>{" "}
|
||||
<div className="analyzer__stat-card__value__number">
|
||||
{stat.value}
|
||||
{suffix}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{typeof stat !== "number" && (
|
||||
<div className="stack items-center">
|
||||
<Ability ability={stat.modifiedBy} size="TINY" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DamageTable({
|
||||
values,
|
||||
isTripleShooter,
|
||||
subWeaponId,
|
||||
}: {
|
||||
values: AnalyzedBuild["stats"]["damages"];
|
||||
isTripleShooter: AnalyzedBuild["weapon"]["isTripleShooter"];
|
||||
subWeaponId: SubWeaponId;
|
||||
}) {
|
||||
const { t } = useTranslation(["weapons", "analyzer"]);
|
||||
|
||||
const showDistanceColumn = values.some((val) => val.distance);
|
||||
|
||||
return (
|
||||
<>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("analyzer:damage.header.type")}</th>
|
||||
<th>{t("analyzer:damage.header.damage")}</th>
|
||||
{showDistanceColumn && (
|
||||
<th>{t("analyzer:damage.header.distance")}</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{values.map((val) => {
|
||||
const damage = isTripleShooter
|
||||
? `${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"}`);
|
||||
|
||||
return (
|
||||
<tr key={val.id}>
|
||||
<td>{typeRowName}</td>
|
||||
<td>
|
||||
{damage}{" "}
|
||||
{val.shotsToSplat && (
|
||||
<span className="analyzer__shots-to-splat">
|
||||
{t("analyzer:damage.toSplat", {
|
||||
count: val.shotsToSplat,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
{showDistanceColumn && <td>{val.distance}</td>}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ConsumptionTable({
|
||||
options,
|
||||
subWeaponId,
|
||||
}: {
|
||||
options: AnalyzedBuild["stats"]["fullInkTankOptions"];
|
||||
subWeaponId: SubWeaponId;
|
||||
}) {
|
||||
const { t } = useTranslation(["analyzer", "weapons"]);
|
||||
const maxSubsToUse = Math.max(...options.map((opt) => opt.subsUsed));
|
||||
const types = Array.from(new Set(options.map((opt) => opt.type)));
|
||||
|
||||
return (
|
||||
<>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t(`weapons:SUB_${subWeaponId}`)}</th>
|
||||
{types.map((type) => (
|
||||
<th key={type}>{t(`analyzer:stat.consumption.${type}`)}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{new Array(maxSubsToUse + 1).fill(null).map((_, subsUsed) => {
|
||||
return (
|
||||
<tr key={subsUsed} className="bg-darker-important">
|
||||
<td>×{subsUsed}</td>
|
||||
{options
|
||||
.filter((opt) => opt.subsUsed === subsUsed)
|
||||
.map((opt) => {
|
||||
return <td key={opt.id}>{opt.value}</td>;
|
||||
})}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="analyzer__consumption-table-explanation">
|
||||
{t("analyzer:consumptionExplanation", { maxSubsToUse })}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import { assertUnreachable } from "~/utils/types";
|
|||
import { db } from "~/db";
|
||||
import type { User } from "~/db/types";
|
||||
import { badgePage } from "~/utils/urls";
|
||||
import { Label } from "~/components/Label";
|
||||
|
||||
const editBadgeActionSchema = z.union([
|
||||
z.object({
|
||||
|
|
@ -121,6 +122,22 @@ function Managers({ data }: { data: BadgeDetailsLoaderData }) {
|
|||
<div className="stack md">
|
||||
<div className="stack sm">
|
||||
<h3 className="badges-edit__small-header">Managers</h3>
|
||||
<div className="text-center my-4">
|
||||
<Label className="stack vertical items-center">Add new manager</Label>
|
||||
<UserCombobox
|
||||
className="mx-auto"
|
||||
inputName="new-manager"
|
||||
onChange={(user) => {
|
||||
if (!user) return;
|
||||
|
||||
setManagers([
|
||||
...managers,
|
||||
{ discordFullName: user.label, id: Number(user.value) },
|
||||
]);
|
||||
}}
|
||||
userIdsToOmit={userIdsToOmitFromCombobox}
|
||||
/>
|
||||
</div>
|
||||
<ul className="badges-edit__users-list">
|
||||
{managers.map((manager) => (
|
||||
<li key={manager.id} data-cy="manager">
|
||||
|
|
@ -137,21 +154,6 @@ function Managers({ data }: { data: BadgeDetailsLoaderData }) {
|
|||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="text-center">
|
||||
<UserCombobox
|
||||
className="mx-auto"
|
||||
inputName="new-manager"
|
||||
onChange={(user) => {
|
||||
if (!user) return;
|
||||
|
||||
setManagers([
|
||||
...managers,
|
||||
{ discordFullName: user.label, id: Number(user.value) },
|
||||
]);
|
||||
}}
|
||||
userIdsToOmit={userIdsToOmitFromCombobox}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
|
|
@ -198,32 +200,8 @@ function Owners({ data }: { data: BadgeDetailsLoaderData }) {
|
|||
<div className="stack md">
|
||||
<div className="stack sm">
|
||||
<h3 className="badges-edit__small-header">Owners</h3>
|
||||
<ul className="badges-edit__users-list">
|
||||
{owners.map((owner) => (
|
||||
<li key={owner.id}>
|
||||
{owner.discordFullName}
|
||||
<input
|
||||
className="badges-edit__number-input"
|
||||
data-cy="owner-count-input"
|
||||
id="number"
|
||||
type="number"
|
||||
value={owner.count}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={(e) =>
|
||||
setOwners(
|
||||
owners.map((o) =>
|
||||
o.id === owner.id
|
||||
? { ...o, count: Number(e.target.value) }
|
||||
: o
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="text-center">
|
||||
<div className="text-center my-4">
|
||||
<Label className="stack items-center">Add new owner</Label>
|
||||
<UserCombobox
|
||||
className="mx-auto"
|
||||
inputName="new-owner"
|
||||
|
|
@ -243,6 +221,31 @@ function Owners({ data }: { data: BadgeDetailsLoaderData }) {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="badges-edit__users-list">
|
||||
{owners.map((owner) => (
|
||||
<li key={owner.id}>
|
||||
{owner.discordFullName}
|
||||
<input
|
||||
className="badges-edit__number-input"
|
||||
data-cy="owner-count-input"
|
||||
id="number"
|
||||
type="number"
|
||||
value={owner.count}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={(e) =>
|
||||
setOwners(
|
||||
owners.map((o) =>
|
||||
o.id === owner.id
|
||||
? { ...o, count: Number(e.target.value) }
|
||||
: o
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{ownerDifferences.length > 0 ? (
|
||||
<ul className="badges-edit__differences">
|
||||
{ownerDifferences.map((o) => (
|
||||
|
|
|
|||
|
|
@ -402,6 +402,7 @@ function Players({
|
|||
variant="minimal"
|
||||
onClick={() => handlePlayerInputTypeChange(i)}
|
||||
data-cy="change-input-type-button"
|
||||
className="outline-theme"
|
||||
>
|
||||
{asPlainInput
|
||||
? t("forms.team.player.addAsUser")
|
||||
|
|
|
|||
|
|
@ -330,83 +330,74 @@ function EventsList({
|
|||
</div>
|
||||
</div>
|
||||
<div className="stack md">
|
||||
{events.map((calendarEvent, i) => {
|
||||
{events.map((calendarEvent) => {
|
||||
return (
|
||||
<React.Fragment key={calendarEvent.eventDateId}>
|
||||
<section className="calendar__event main stack md">
|
||||
<div className="stack sm">
|
||||
<div
|
||||
className={clsx(
|
||||
"calendar__event__top-info-container",
|
||||
{
|
||||
"mt-4": i === 0,
|
||||
}
|
||||
)}
|
||||
<section
|
||||
key={calendarEvent.eventDateId}
|
||||
className="calendar__event main stack md"
|
||||
>
|
||||
<div className="stack sm">
|
||||
<div className="calendar__event__top-info-container">
|
||||
<time
|
||||
dateTime={databaseTimestampToDate(
|
||||
calendarEvent.startTime
|
||||
).toISOString()}
|
||||
className="calendar__event__time"
|
||||
>
|
||||
<time
|
||||
dateTime={databaseTimestampToDate(
|
||||
calendarEvent.startTime
|
||||
).toISOString()}
|
||||
className="calendar__event__time"
|
||||
>
|
||||
{databaseTimestampToDate(
|
||||
calendarEvent.startTime
|
||||
).toLocaleTimeString(i18n.language, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})}
|
||||
</time>
|
||||
<div className="calendar__event__author">
|
||||
From {discordFullName(calendarEvent)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stack xs">
|
||||
<Link
|
||||
to={String(calendarEvent.eventId)}
|
||||
data-cy="event-page-link"
|
||||
>
|
||||
<h2 className="calendar__event__title">
|
||||
{calendarEvent.name}{" "}
|
||||
{calendarEvent.nthAppearance > 1 ? (
|
||||
<span className="calendar__event__day">
|
||||
{t("day", {
|
||||
number: calendarEvent.nthAppearance,
|
||||
})}
|
||||
</span>
|
||||
) : null}
|
||||
</h2>
|
||||
</Link>
|
||||
<Tags
|
||||
tags={calendarEvent.tags}
|
||||
badges={calendarEvent.badgePrizes}
|
||||
/>
|
||||
{databaseTimestampToDate(
|
||||
calendarEvent.startTime
|
||||
).toLocaleTimeString(i18n.language, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})}
|
||||
</time>
|
||||
<div className="calendar__event__author">
|
||||
From {discordFullName(calendarEvent)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="calendar__event__bottom-info-container">
|
||||
{calendarEvent.discordUrl ? (
|
||||
<LinkButton
|
||||
to={calendarEvent.discordUrl}
|
||||
variant="outlined"
|
||||
tiny
|
||||
isExternal
|
||||
>
|
||||
Discord
|
||||
</LinkButton>
|
||||
) : null}
|
||||
<div className="stack xs">
|
||||
<Link
|
||||
to={String(calendarEvent.eventId)}
|
||||
data-cy="event-page-link"
|
||||
>
|
||||
<h2 className="calendar__event__title">
|
||||
{calendarEvent.name}{" "}
|
||||
{calendarEvent.nthAppearance > 1 ? (
|
||||
<span className="calendar__event__day">
|
||||
{t("day", {
|
||||
number: calendarEvent.nthAppearance,
|
||||
})}
|
||||
</span>
|
||||
) : null}
|
||||
</h2>
|
||||
</Link>
|
||||
<Tags
|
||||
tags={calendarEvent.tags}
|
||||
badges={calendarEvent.badgePrizes}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="calendar__event__bottom-info-container">
|
||||
{calendarEvent.discordUrl ? (
|
||||
<LinkButton
|
||||
to={calendarEvent.bracketUrl}
|
||||
to={calendarEvent.discordUrl}
|
||||
variant="outlined"
|
||||
tiny
|
||||
isExternal
|
||||
>
|
||||
{resolveBaseUrl(calendarEvent.bracketUrl)}
|
||||
Discord
|
||||
</LinkButton>
|
||||
</div>
|
||||
</section>
|
||||
{i < events.length - 1 ? (
|
||||
<hr className="calendar__event__divider" />
|
||||
) : null}
|
||||
</React.Fragment>
|
||||
) : null}
|
||||
<LinkButton
|
||||
to={calendarEvent.bracketUrl}
|
||||
variant="outlined"
|
||||
tiny
|
||||
isExternal
|
||||
>
|
||||
{resolveBaseUrl(calendarEvent.bracketUrl)}
|
||||
</LinkButton>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { GearCombobox, WeaponCombobox } from "~/components/Combobox";
|
|||
import { Image } from "~/components/Image";
|
||||
import { Label } from "~/components/Label";
|
||||
import { Main } from "~/components/Main";
|
||||
import { BUILD } from "~/constants";
|
||||
import { BUILD, EMPTY_BUILD } from "~/constants";
|
||||
import { db } from "~/db";
|
||||
import type { GearType } from "~/db/types";
|
||||
import { requireUser } from "~/modules/auth";
|
||||
|
|
@ -23,11 +23,12 @@ import {
|
|||
headGearIds,
|
||||
modesShort,
|
||||
shoesGearIds,
|
||||
weaponIds,
|
||||
mainWeaponIds,
|
||||
} from "~/modules/in-game-lists";
|
||||
import type {
|
||||
BuildAbilitiesTuple,
|
||||
BuildAbilitiesTupleWithUnknown,
|
||||
MainWeaponId,
|
||||
} from "~/modules/in-game-lists/types";
|
||||
import { parseRequestFormData } from "~/utils/remix";
|
||||
import { modeImageUrl, userBuildsPage } from "~/utils/urls";
|
||||
|
|
@ -67,7 +68,7 @@ const newBuildActionSchema = z.object({
|
|||
z
|
||||
.number()
|
||||
.refine((val) =>
|
||||
weaponIds.includes(val as typeof weaponIds[number])
|
||||
mainWeaponIds.includes(val as typeof mainWeaponIds[number])
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -141,7 +142,7 @@ export const action: ActionFunction = async ({ request }) => {
|
|||
clothesGearSplId: data["CLOTHES[value]"],
|
||||
shoesGearSplId: data["SHOES[value]"],
|
||||
modes: modesShort.filter((mode) => data[mode]),
|
||||
weaponSplIds: data["weapon[value]"],
|
||||
weaponSplIds: data["weapon[value]"] as Array<MainWeaponId>,
|
||||
ownerId: user.id,
|
||||
};
|
||||
if (data.buildToEditId) {
|
||||
|
|
@ -367,11 +368,7 @@ function Abilities() {
|
|||
const { buildToEdit } = useLoaderData<typeof loader>();
|
||||
const [abilities, setAbilities] =
|
||||
React.useState<BuildAbilitiesTupleWithUnknown>(
|
||||
buildToEdit?.abilities ?? [
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
["UNKNOWN", "UNKNOWN", "UNKNOWN", "UNKNOWN"],
|
||||
]
|
||||
buildToEdit?.abilities ?? EMPTY_BUILD
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
156
app/styles/analyzer.css
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
.analyzer__container {
|
||||
display: grid;
|
||||
gap: var(--s-10);
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
|
||||
.analyzer__left-column {
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
display: flex;
|
||||
height: max-content;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--s-8);
|
||||
}
|
||||
|
||||
.analyzer__weapon-info-badges {
|
||||
display: grid;
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--semi-bold);
|
||||
gap: var(--s-2);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.analyzer__weapon-info-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--theme-transparent);
|
||||
border-radius: var(--rounded);
|
||||
gap: var(--s-1);
|
||||
padding-block: var(--s-1);
|
||||
padding-inline: var(--s-1-5);
|
||||
}
|
||||
|
||||
.analyzer__effects-selector {
|
||||
display: grid;
|
||||
gap: var(--s-2);
|
||||
grid-template-columns: 1fr 2.5fr;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.analyzer__lde-intensity-select {
|
||||
font-size: var(--fonts-xxs);
|
||||
}
|
||||
|
||||
.analyzer__ap-summary {
|
||||
width: 100%;
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--semi-bold);
|
||||
padding-block: var(--s-1);
|
||||
padding-inline: var(--s-2);
|
||||
}
|
||||
|
||||
.analyzer__ap-text {
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
||||
.analyzer__summary {
|
||||
background-color: var(--bg-lighter);
|
||||
border-radius: var(--rounded);
|
||||
font-size: var(--fonts-md);
|
||||
font-weight: var(--bold);
|
||||
padding-block: var(--s-2);
|
||||
padding-inline: var(--s-3);
|
||||
}
|
||||
|
||||
.analyzer__stat-collection {
|
||||
display: grid;
|
||||
gap: var(--s-2);
|
||||
grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
|
||||
margin-block-start: var(--s-4);
|
||||
}
|
||||
|
||||
.analyzer__stat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: var(--s-2);
|
||||
background-color: var(--bg-darker);
|
||||
border-radius: var(--rounded);
|
||||
gap: var(--s-4);
|
||||
}
|
||||
|
||||
.analyzer__stat-card__title {
|
||||
height: 2.75rem;
|
||||
font-size: var(--fonts-xs);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.analyzer__stat-card-values {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
gap: var(--s-1);
|
||||
}
|
||||
|
||||
.analyzer__stat-card__value {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.analyzer__stat-card__value__title {
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xxs);
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.analyzer__stat-card__value__number {
|
||||
font-size: var(--fonts-md);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
|
||||
.analyzer__table-container {
|
||||
width: 100%;
|
||||
padding: var(--s-3);
|
||||
background-color: var(--bg-darker);
|
||||
border-radius: var(--rounded);
|
||||
margin-block-start: var(--s-4);
|
||||
padding-block: var(--s-2);
|
||||
}
|
||||
|
||||
.analyzer__shots-to-splat {
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xxxs);
|
||||
margin-inline-start: var(--s-4);
|
||||
}
|
||||
|
||||
.analyzer__consumption-table-explanation {
|
||||
margin-top: var(--s-2);
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xxs);
|
||||
}
|
||||
|
||||
.analyzer__stat-category-explanation {
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xxs);
|
||||
grid-column: 1 / 4;
|
||||
}
|
||||
|
||||
.analyzer__patch {
|
||||
background-color: var(--theme-transparent);
|
||||
border-radius: var(--rounded);
|
||||
color: var(--text-lighter);
|
||||
font-size: var(--fonts-xxxs);
|
||||
font-weight: var(--bold);
|
||||
padding-block: var(--s-0-5);
|
||||
padding-inline: var(--s-1);
|
||||
}
|
||||
|
|
@ -103,9 +103,14 @@
|
|||
.calendar__event {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: var(--s-4);
|
||||
font-size: var(--fonts-lg);
|
||||
}
|
||||
|
||||
.calendar__event + .calendar__event {
|
||||
border-top: 2px solid var(--divider);
|
||||
}
|
||||
|
||||
.calendar__event:last-child {
|
||||
padding-block-end: var(--s-4);
|
||||
}
|
||||
|
|
@ -152,11 +157,6 @@
|
|||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.calendar__event__divider {
|
||||
width: 50rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.calendar__no-events {
|
||||
color: var(--text-lighter);
|
||||
padding-block: var(--s-20);
|
||||
|
|
|
|||
|
|
@ -302,7 +302,6 @@ abbr[title] {
|
|||
}
|
||||
|
||||
dialog {
|
||||
overflow: visible;
|
||||
width: min(90%, 24rem);
|
||||
border: 0;
|
||||
margin: auto;
|
||||
|
|
@ -333,6 +332,54 @@ dialog::backdrop {
|
|||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
all: unset;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: var(--s-11);
|
||||
height: var(--s-6);
|
||||
align-items: center;
|
||||
background-color: var(--theme-transparent);
|
||||
border-radius: var(--rounded);
|
||||
}
|
||||
|
||||
.toggle.tiny {
|
||||
width: var(--s-6);
|
||||
height: var(--s-3);
|
||||
}
|
||||
|
||||
.toggle.checked {
|
||||
background-color: var(--theme);
|
||||
}
|
||||
|
||||
.toggle:active {
|
||||
transform: initial;
|
||||
}
|
||||
|
||||
.toggle-dot {
|
||||
display: inline-block;
|
||||
width: var(--s-4);
|
||||
height: var(--s-4);
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
transform: translateX(var(--s-1));
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-dot.tiny {
|
||||
width: var(--s-3);
|
||||
height: var(--s-3);
|
||||
transform: translateX(-0.2rem);
|
||||
}
|
||||
|
||||
.toggle-dot.checked {
|
||||
transform: translateX(var(--s-6));
|
||||
}
|
||||
|
||||
.toggle-dot.checked.tiny {
|
||||
transform: translateX(var(--s-4));
|
||||
}
|
||||
|
||||
.button-text-paragraph {
|
||||
display: flex;
|
||||
gap: var(--s-1);
|
||||
|
|
|
|||
|
|
@ -226,6 +226,10 @@
|
|||
gap: var(--s-2);
|
||||
}
|
||||
|
||||
.layout__user-popover > button:focus {
|
||||
outline: 2px solid var(--theme);
|
||||
}
|
||||
|
||||
.layout__footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@
|
|||
color: var(--theme-success);
|
||||
}
|
||||
|
||||
.bg-darker-important {
|
||||
background-color: var(--bg-darker) !important;
|
||||
}
|
||||
|
||||
.font-semi-bold {
|
||||
font-weight: var(--semi-bold);
|
||||
}
|
||||
|
|
@ -38,6 +42,10 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.w-full-important {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.w-24 {
|
||||
width: var(--s-24);
|
||||
}
|
||||
|
|
@ -74,6 +82,10 @@
|
|||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.my-4 {
|
||||
margin-block: var(--s-4);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -113,3 +125,7 @@
|
|||
.all-unset {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
.outline-theme:focus {
|
||||
outline: 2px solid var(--theme);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ html {
|
|||
--text: rgb(0 0 0 / 95%);
|
||||
--black-text: rgb(0 0 0 / 95%);
|
||||
--text-lighter: rgb(75 75 75 / 95%);
|
||||
--divider: #635dab;
|
||||
--theme-error: rgb(199 13 6);
|
||||
--theme-error-transparent: rgba(199 13 6 / 75%);
|
||||
--theme-warning: #c9c900;
|
||||
|
|
|
|||
|
|
@ -17,3 +17,7 @@ export function placementString(placement: number) {
|
|||
|
||||
return `${placement}th`;
|
||||
}
|
||||
|
||||
export function semiRandomId() {
|
||||
return String(Math.random());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import type { Badge, GearType } from "~/db/types";
|
||||
import type { ModeShort } from "~/modules/in-game-lists";
|
||||
import type { AbilityWithUnknown } from "~/modules/in-game-lists/types";
|
||||
import type {
|
||||
AbilityWithUnknown,
|
||||
MainWeaponId,
|
||||
SpecialWeaponId,
|
||||
SubWeaponId,
|
||||
} from "~/modules/in-game-lists/types";
|
||||
|
||||
export const PLUS_SERVER_DISCORD_URL = "https://discord.gg/FW4dKrY";
|
||||
export const SENDOU_INK_DISCORD_URL = "https://discord.gg/sendou";
|
||||
|
|
@ -23,6 +28,8 @@ export const CALENDAR_PAGE = "/calendar";
|
|||
export const STOP_IMPERSONATING_URL = "/auth/impersonate/stop";
|
||||
export const SEED_URL = "/seed";
|
||||
|
||||
export const COMMON_PREVIEW_IMAGE = "/img/layout/common-preview.png";
|
||||
|
||||
export const userPage = (discordId: string) => `/u/${discordId}`;
|
||||
export const userBuildsPage = (discordId: string) =>
|
||||
`${userPage(discordId)}/builds`;
|
||||
|
|
@ -51,8 +58,12 @@ export const articlePreviewUrl = (slug: string) =>
|
|||
export const navIconUrl = (navItem: string) => `/img/layout/${navItem}`;
|
||||
export const gearImageUrl = (gearType: GearType, gearSplId: number) =>
|
||||
`/img/gear/${gearType.toLowerCase()}/${gearSplId}`;
|
||||
export const weaponImageUrl = (weaponSplId: number) =>
|
||||
`/img/weapons/${weaponSplId}`;
|
||||
export const mainWeaponImageUrl = (mainWeaponSplId: MainWeaponId) =>
|
||||
`/img/main-weapons/${mainWeaponSplId}`;
|
||||
export const subWeaponImageUrl = (subWeaponSplId: SubWeaponId) =>
|
||||
`/img/sub-weapons/${subWeaponSplId}`;
|
||||
export const specialWeaponImageUrl = (specialWeaponSplId: SpecialWeaponId) =>
|
||||
`/img/special-weapons/${specialWeaponSplId}`;
|
||||
export const abilityImageUrl = (ability: AbilityWithUnknown) =>
|
||||
`/img/abilities/${ability}`;
|
||||
export const modeImageUrl = (mode: ModeShort) => `/img/modes/${mode}`;
|
||||
|
|
|
|||
15
content/articles/splatoon-3-breaks-sales-record.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
title: Splatoon 3 Breaks Sales Records in Japan
|
||||
date: 2022-09-12
|
||||
author: Riczi
|
||||
---
|
||||
|
||||
Nintendo is known for developing some of the biggest video game franchises in the world. From Pokémon to Mario to Legend of Zelda, Nintendo has created some of the most well-known and memorable games and characters in history. You would think that one of these franchises hold the record for most sales on release of a new game. You would be wrong.
|
||||
|
||||
Splatoon 3 released in September of 2022 and it is shattering sales records in Japan. In just three days, the game sold nearly three and a half million copies in Japan alone. This crushed the previous record held by Animal Crossing: New Horizons which sold 2.6 million copies when it released at the beginning of Covid-19.
|
||||
|
||||
The Splatoon franchise has been a fan-favorite in Japan for years. The first game released on the Wii U back in 2015, with Splatoon 2 debuting on the Nintendo Switch just a couple of years after. Despite huge success in their home country, Nintendo hasn't seen Splatoon take off in other countries quite as well. The arrival of Splatoon 3 may cause an increase in popularity in western countries where the game series hasn't quite reached yet.
|
||||
|
||||
Splatoon has a thriving competitive scene that is welcoming new players into its ranks. Top players in the United States and Europe such as ProChara and ThatSrb2DUDE create content with the hope that players in their respective regions will enter the Splatlands with the same amount of hype being generated in Japan.
|
||||
|
||||
If you're interested in watching or even participating in a Splatoon tournament, check out the Sendou.ink calendar. It contains all of the upcoming events for Splatoon 3 with information on where to sign up and how to follow along live.
|
||||
366
package-lock.json
generated
|
|
@ -7,7 +7,7 @@
|
|||
"name": "sendou.ink",
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^7.5.0",
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@headlessui/react": "^1.7.1",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@remix-run/node": "^1.7.0",
|
||||
"@remix-run/react": "^1.7.0",
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
"better-sqlite3": "^7.6.2",
|
||||
"clsx": "^1.2.1",
|
||||
"countries-list": "^2.6.1",
|
||||
"date-fns": "^2.29.2",
|
||||
"date-fns": "^2.29.3",
|
||||
"fuse.js": "^6.6.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"i18next": "^21.9.1",
|
||||
|
|
@ -29,30 +29,30 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-flip-toolkit": "^7.0.16",
|
||||
"react-i18next": "^11.18.5",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-popper": "^2.3.0",
|
||||
"remix-auth": "^3.2.2",
|
||||
"remix-auth": "^3.3.0",
|
||||
"remix-auth-oauth2": "^1.3.0",
|
||||
"remix-i18next": "^4.1.1",
|
||||
"swr": "^1.3.0",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"zod": "^3.18.0"
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^1.7.0",
|
||||
"@remix-run/eslint-config": "^1.7.0",
|
||||
"@types/better-sqlite3": "^7.6.0",
|
||||
"@types/i18next-fs-backend": "^1.1.2",
|
||||
"@types/node-cron": "^3.0.3",
|
||||
"@types/react": "^18.0.18",
|
||||
"@types/node-cron": "^3.0.4",
|
||||
"@types/react": "^18.0.20",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.36.1",
|
||||
"@typescript-eslint/parser": "^5.36.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.37.0",
|
||||
"@typescript-eslint/parser": "^5.37.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^10.7.0",
|
||||
"cypress": "^10.8.0",
|
||||
"dotenv": "^16.0.2",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-plugin-react": "^7.31.5",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"ley": "^0.7.1",
|
||||
"prettier": "^2.7.1",
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"tsm": "^2.2.2",
|
||||
"typescript": "^4.8.2",
|
||||
"typescript": "^4.8.3",
|
||||
"uvu": "^0.5.6"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -2199,9 +2199,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.1.tgz",
|
||||
"integrity": "sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz",
|
||||
"integrity": "sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
|
|
@ -2275,9 +2275,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@headlessui/react": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.6.6.tgz",
|
||||
"integrity": "sha512-MFJtmj9Xh/hhBMhLccGbBoSk+sk61BlP6sJe4uQcVMtXZhCgGqd2GyIQzzmsdPdTEWGSF434CBi8mnhR6um46Q==",
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.1.tgz",
|
||||
"integrity": "sha512-vnRlB71kvEr3y26Lm1WpCiMML8n5JcJ7jK5+vaF0hGTZFArW206j61meVemXkTOuGLhmyWe6yj3OETzsxHoryQ==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -2988,9 +2988,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node-cron": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.3.tgz",
|
||||
"integrity": "sha512-FPzux/llEiCe5mPn3TvLEORcF2pRXvH5cugtJCJf+UrkwQ7pYfb4wn9J/sxJ8QkT/sw9BjWSi9uur5Vh1OuAZQ==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.4.tgz",
|
||||
"integrity": "sha512-A2H+uz5ry4hohYjRe5mQSE/8Dx/HGw4WZ728JxhKUZ7z8CMvRuG2tpbzGHRGQCuQzz5aCNB1iXzPZYHd4BPHvw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/normalize-package-data": {
|
||||
|
|
@ -3012,9 +3012,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.18.tgz",
|
||||
"integrity": "sha512-6hI08umYs6NaiHFEEGioXnxJ+oEhY3eRz8VCUaudZmGdtvPviCJB8mgaMxaDWAdPSYd4eFavrPk2QIolwbLYrg==",
|
||||
"version": "18.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.20.tgz",
|
||||
"integrity": "sha512-MWul1teSPxujEHVwZl4a5HxQ9vVNsjTchVA+xRqv/VYGCuKGAU6UhfrTdF5aBefwD1BHUD8i/zq+O/vyCm/FrA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
|
|
@ -3075,14 +3075,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.1.tgz",
|
||||
"integrity": "sha512-iC40UK8q1tMepSDwiLbTbMXKDxzNy+4TfPWgIL661Ym0sD42vRcQU93IsZIrmi+x292DBr60UI/gSwfdVYexCA==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.37.0.tgz",
|
||||
"integrity": "sha512-Fde6W0IafXktz1UlnhGkrrmnnGpAo1kyX7dnyHHVrmwJOn72Oqm3eYtddrpOwwel2W8PAK9F3pIL5S+lfoM0og==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "5.36.1",
|
||||
"@typescript-eslint/type-utils": "5.36.1",
|
||||
"@typescript-eslint/utils": "5.36.1",
|
||||
"@typescript-eslint/scope-manager": "5.37.0",
|
||||
"@typescript-eslint/type-utils": "5.37.0",
|
||||
"@typescript-eslint/utils": "5.37.0",
|
||||
"debug": "^4.3.4",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"ignore": "^5.2.0",
|
||||
|
|
@ -3131,14 +3131,14 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.36.1.tgz",
|
||||
"integrity": "sha512-/IsgNGOkBi7CuDfUbwt1eOqUXF9WGVBW9dwEe1pi+L32XrTsZIgmDFIi2RxjzsvB/8i+MIf5JIoTEH8LOZ368A==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.37.0.tgz",
|
||||
"integrity": "sha512-01VzI/ipYKuaG5PkE5+qyJ6m02fVALmMPY3Qq5BHflDx3y4VobbLdHQkSMg9VPRS4KdNt4oYTMaomFoHonBGAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "5.36.1",
|
||||
"@typescript-eslint/types": "5.36.1",
|
||||
"@typescript-eslint/typescript-estree": "5.36.1",
|
||||
"@typescript-eslint/scope-manager": "5.37.0",
|
||||
"@typescript-eslint/types": "5.37.0",
|
||||
"@typescript-eslint/typescript-estree": "5.37.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -3181,13 +3181,13 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.36.1.tgz",
|
||||
"integrity": "sha512-pGC2SH3/tXdu9IH3ItoqciD3f3RRGCh7hb9zPdN2Drsr341zgd6VbhP5OHQO/reUqihNltfPpMpTNihFMarP2w==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.37.0.tgz",
|
||||
"integrity": "sha512-F67MqrmSXGd/eZnujjtkPgBQzgespu/iCZ+54Ok9X5tALb9L2v3G+QBSoWkXG0p3lcTJsL+iXz5eLUEdSiJU9Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.36.1",
|
||||
"@typescript-eslint/visitor-keys": "5.36.1"
|
||||
"@typescript-eslint/types": "5.37.0",
|
||||
"@typescript-eslint/visitor-keys": "5.37.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
|
|
@ -3198,13 +3198,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.36.1.tgz",
|
||||
"integrity": "sha512-xfZhfmoQT6m3lmlqDvDzv9TiCYdw22cdj06xY0obSznBsT///GK5IEZQdGliXpAOaRL34o8phEvXzEo/VJx13Q==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.37.0.tgz",
|
||||
"integrity": "sha512-BSx/O0Z0SXOF5tY0bNTBcDEKz2Ec20GVYvq/H/XNKiUorUFilH7NPbFUuiiyzWaSdN3PA8JV0OvYx0gH/5aFAQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "5.36.1",
|
||||
"@typescript-eslint/utils": "5.36.1",
|
||||
"@typescript-eslint/typescript-estree": "5.37.0",
|
||||
"@typescript-eslint/utils": "5.37.0",
|
||||
"debug": "^4.3.4",
|
||||
"tsutils": "^3.21.0"
|
||||
},
|
||||
|
|
@ -3248,9 +3248,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.36.1.tgz",
|
||||
"integrity": "sha512-jd93ShpsIk1KgBTx9E+hCSEuLCUFwi9V/urhjOWnOaksGZFbTOxAT47OH2d4NLJnLhkVD+wDbB48BuaycZPLBg==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.37.0.tgz",
|
||||
"integrity": "sha512-3frIJiTa5+tCb2iqR/bf7XwU20lnU05r/sgPJnRpwvfZaqCJBrl8Q/mw9vr3NrNdB/XtVyMA0eppRMMBqdJ1bA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
|
|
@ -3261,13 +3261,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.36.1.tgz",
|
||||
"integrity": "sha512-ih7V52zvHdiX6WcPjsOdmADhYMDN15SylWRZrT2OMy80wzKbc79n8wFW0xpWpU0x3VpBz/oDgTm2xwDAnFTl+g==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.37.0.tgz",
|
||||
"integrity": "sha512-JkFoFIt/cx59iqEDSgIGnQpCTRv96MQnXCYvJi7QhBC24uyuzbD8wVbajMB1b9x4I0octYFJ3OwjAwNqk1AjDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.36.1",
|
||||
"@typescript-eslint/visitor-keys": "5.36.1",
|
||||
"@typescript-eslint/types": "5.37.0",
|
||||
"@typescript-eslint/visitor-keys": "5.37.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
|
|
@ -3331,15 +3331,15 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.36.1.tgz",
|
||||
"integrity": "sha512-lNj4FtTiXm5c+u0pUehozaUWhh7UYKnwryku0nxJlYUEWetyG92uw2pr+2Iy4M/u0ONMKzfrx7AsGBTCzORmIg==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.37.0.tgz",
|
||||
"integrity": "sha512-jUEJoQrWbZhmikbcWSMDuUSxEE7ID2W/QCV/uz10WtQqfOuKZUqFGjqLJ+qhDd17rjgp+QJPqTdPIBWwoob2NQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"@typescript-eslint/scope-manager": "5.36.1",
|
||||
"@typescript-eslint/types": "5.36.1",
|
||||
"@typescript-eslint/typescript-estree": "5.36.1",
|
||||
"@typescript-eslint/scope-manager": "5.37.0",
|
||||
"@typescript-eslint/types": "5.37.0",
|
||||
"@typescript-eslint/typescript-estree": "5.37.0",
|
||||
"eslint-scope": "^5.1.1",
|
||||
"eslint-utils": "^3.0.0"
|
||||
},
|
||||
|
|
@ -3355,12 +3355,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.36.1.tgz",
|
||||
"integrity": "sha512-ojB9aRyRFzVMN3b5joSYni6FAS10BBSCAfKJhjJAV08t/a95aM6tAhz+O1jF+EtgxktuSO3wJysp2R+Def/IWQ==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.37.0.tgz",
|
||||
"integrity": "sha512-Hp7rT4cENBPIzMwrlehLW/28EVCOcE9U1Z1BQTc8EA8v5qpr7GRGuG+U58V5tTY48zvUOA3KHvw3rA8tY9fbdA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.36.1",
|
||||
"@typescript-eslint/types": "5.37.0",
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -5158,9 +5158,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/cypress": {
|
||||
"version": "10.7.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.7.0.tgz",
|
||||
"integrity": "sha512-gTFvjrUoBnqPPOu9Vl5SBHuFlzx/Wxg/ZXIz2H4lzoOLFelKeF7mbwYUOzgzgF0oieU2WhJAestQdkgwJMMTvQ==",
|
||||
"version": "10.8.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.8.0.tgz",
|
||||
"integrity": "sha512-QVse0dnLm018hgti2enKMVZR9qbIO488YGX06nH5j3Dg1isL38DwrBtyrax02CANU6y8F4EJUuyW6HJKw1jsFA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
|
|
@ -5183,7 +5183,7 @@
|
|||
"dayjs": "^1.10.4",
|
||||
"debug": "^4.3.2",
|
||||
"enquirer": "^2.3.6",
|
||||
"eventemitter2": "^6.4.3",
|
||||
"eventemitter2": "6.4.7",
|
||||
"execa": "4.1.0",
|
||||
"executable": "^4.1.1",
|
||||
"extract-zip": "2.0.1",
|
||||
|
|
@ -5378,9 +5378,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.29.2",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.2.tgz",
|
||||
"integrity": "sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA==",
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
|
|
@ -6232,12 +6232,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.0.tgz",
|
||||
"integrity": "sha512-pBG/XOn0MsJcKcTRLr27S5HpzQo4kLr+HjLQIyK4EiCsijDl/TB+h5uEuJU6bQ8Edvwz1XWOjpaP2qgnXGpTcA==",
|
||||
"version": "8.23.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.1.tgz",
|
||||
"integrity": "sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint/eslintrc": "^1.3.1",
|
||||
"@eslint/eslintrc": "^1.3.2",
|
||||
"@humanwhocodes/config-array": "^0.10.4",
|
||||
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
|
|
@ -6256,7 +6256,6 @@
|
|||
"fast-deep-equal": "^3.1.3",
|
||||
"file-entry-cache": "^6.0.1",
|
||||
"find-up": "^5.0.0",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"glob-parent": "^6.0.1",
|
||||
"globals": "^13.15.0",
|
||||
"globby": "^11.1.0",
|
||||
|
|
@ -6265,6 +6264,7 @@
|
|||
"import-fresh": "^3.0.0",
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"js-sdsl": "^4.1.4",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"levn": "^0.4.1",
|
||||
|
|
@ -6719,9 +6719,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react": {
|
||||
"version": "7.31.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.5.tgz",
|
||||
"integrity": "sha512-y7472VAcqns17rsQUk6tQCnqBi+boYjGdYarX022719+wGd1T4U1fOYJ2T2Trd3Od2q5M92e42zJ2uZOGmWamA==",
|
||||
"version": "7.31.8",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz",
|
||||
"integrity": "sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.5",
|
||||
|
|
@ -7259,9 +7259,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eventemitter2": {
|
||||
"version": "6.4.5",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.5.tgz",
|
||||
"integrity": "sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw==",
|
||||
"version": "6.4.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz",
|
||||
"integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/executable": {
|
||||
|
|
@ -9360,6 +9360,12 @@
|
|||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/js-sdsl": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz",
|
||||
"integrity": "sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
@ -12373,9 +12379,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "11.18.5",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.5.tgz",
|
||||
"integrity": "sha512-cKcyuuzIv0YUZ4l9WORflVNuhISPAqQShOAsxwFyYuJoCA7HlLmHm7XnvO6hfAGmGpDNRhJHoBX8hG49Cb2xZQ==",
|
||||
"version": "11.18.6",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz",
|
||||
"integrity": "sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.14.5",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
|
|
@ -12786,9 +12792,9 @@
|
|||
"integrity": "sha512-agFFS3RzrLXJl5LY5xg/xYyXvUuVAnkhgKO7RaO9J1Ssth6yvbO+PIiV67V59MB5NCdAK2flvGvNT4mdKVniFA=="
|
||||
},
|
||||
"node_modules/remix-auth": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.2.2.tgz",
|
||||
"integrity": "sha512-VtzkfxeXbnXilupRTZkP40aik4vFSdwwRT96mbq0UBDMqHVRfQ7h9Y51HFrTufHJZEfAdkCopedMVvm0vQYKag==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.3.0.tgz",
|
||||
"integrity": "sha512-sLY5s6DQF4OJp03HM/dIEw3tLfSOIowerS2QDTuvEVXoEA3IHVmk7QkvOKosWdVPbcnlRfwF6PbeBLKgo6vqsg==",
|
||||
"dependencies": {
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
|
|
@ -14800,9 +14806,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
|
||||
"integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
|
||||
"version": "4.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz",
|
||||
"integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
|
@ -15668,9 +15674,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.18.0",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.18.0.tgz",
|
||||
"integrity": "sha512-gwTm8RfUCe8l9rDwN5r2A17DkAa8Ez4Yl4yXqc5VqeGaXaJahzYYXbTwvhroZi0SNBqTwh/bKm2N0mpCzuw4bA==",
|
||||
"version": "3.19.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.19.1.tgz",
|
||||
"integrity": "sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
|
@ -17182,9 +17188,9 @@
|
|||
}
|
||||
},
|
||||
"@eslint/eslintrc": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.1.tgz",
|
||||
"integrity": "sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz",
|
||||
"integrity": "sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ajv": "^6.12.4",
|
||||
|
|
@ -17236,9 +17242,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@headlessui/react": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.6.6.tgz",
|
||||
"integrity": "sha512-MFJtmj9Xh/hhBMhLccGbBoSk+sk61BlP6sJe4uQcVMtXZhCgGqd2GyIQzzmsdPdTEWGSF434CBi8mnhR6um46Q==",
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.1.tgz",
|
||||
"integrity": "sha512-vnRlB71kvEr3y26Lm1WpCiMML8n5JcJ7jK5+vaF0hGTZFArW206j61meVemXkTOuGLhmyWe6yj3OETzsxHoryQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
|
|
@ -17820,9 +17826,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@types/node-cron": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.3.tgz",
|
||||
"integrity": "sha512-FPzux/llEiCe5mPn3TvLEORcF2pRXvH5cugtJCJf+UrkwQ7pYfb4wn9J/sxJ8QkT/sw9BjWSi9uur5Vh1OuAZQ==",
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.4.tgz",
|
||||
"integrity": "sha512-A2H+uz5ry4hohYjRe5mQSE/8Dx/HGw4WZ728JxhKUZ7z8CMvRuG2tpbzGHRGQCuQzz5aCNB1iXzPZYHd4BPHvw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/normalize-package-data": {
|
||||
|
|
@ -17844,9 +17850,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "18.0.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.18.tgz",
|
||||
"integrity": "sha512-6hI08umYs6NaiHFEEGioXnxJ+oEhY3eRz8VCUaudZmGdtvPviCJB8mgaMxaDWAdPSYd4eFavrPk2QIolwbLYrg==",
|
||||
"version": "18.0.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.20.tgz",
|
||||
"integrity": "sha512-MWul1teSPxujEHVwZl4a5HxQ9vVNsjTchVA+xRqv/VYGCuKGAU6UhfrTdF5aBefwD1BHUD8i/zq+O/vyCm/FrA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
|
|
@ -17907,14 +17913,14 @@
|
|||
}
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.1.tgz",
|
||||
"integrity": "sha512-iC40UK8q1tMepSDwiLbTbMXKDxzNy+4TfPWgIL661Ym0sD42vRcQU93IsZIrmi+x292DBr60UI/gSwfdVYexCA==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.37.0.tgz",
|
||||
"integrity": "sha512-Fde6W0IafXktz1UlnhGkrrmnnGpAo1kyX7dnyHHVrmwJOn72Oqm3eYtddrpOwwel2W8PAK9F3pIL5S+lfoM0og==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "5.36.1",
|
||||
"@typescript-eslint/type-utils": "5.36.1",
|
||||
"@typescript-eslint/utils": "5.36.1",
|
||||
"@typescript-eslint/scope-manager": "5.37.0",
|
||||
"@typescript-eslint/type-utils": "5.37.0",
|
||||
"@typescript-eslint/utils": "5.37.0",
|
||||
"debug": "^4.3.4",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"ignore": "^5.2.0",
|
||||
|
|
@ -17941,14 +17947,14 @@
|
|||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.36.1.tgz",
|
||||
"integrity": "sha512-/IsgNGOkBi7CuDfUbwt1eOqUXF9WGVBW9dwEe1pi+L32XrTsZIgmDFIi2RxjzsvB/8i+MIf5JIoTEH8LOZ368A==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.37.0.tgz",
|
||||
"integrity": "sha512-01VzI/ipYKuaG5PkE5+qyJ6m02fVALmMPY3Qq5BHflDx3y4VobbLdHQkSMg9VPRS4KdNt4oYTMaomFoHonBGAw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "5.36.1",
|
||||
"@typescript-eslint/types": "5.36.1",
|
||||
"@typescript-eslint/typescript-estree": "5.36.1",
|
||||
"@typescript-eslint/scope-manager": "5.37.0",
|
||||
"@typescript-eslint/types": "5.37.0",
|
||||
"@typescript-eslint/typescript-estree": "5.37.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -17970,23 +17976,23 @@
|
|||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.36.1.tgz",
|
||||
"integrity": "sha512-pGC2SH3/tXdu9IH3ItoqciD3f3RRGCh7hb9zPdN2Drsr341zgd6VbhP5OHQO/reUqihNltfPpMpTNihFMarP2w==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.37.0.tgz",
|
||||
"integrity": "sha512-F67MqrmSXGd/eZnujjtkPgBQzgespu/iCZ+54Ok9X5tALb9L2v3G+QBSoWkXG0p3lcTJsL+iXz5eLUEdSiJU9Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "5.36.1",
|
||||
"@typescript-eslint/visitor-keys": "5.36.1"
|
||||
"@typescript-eslint/types": "5.37.0",
|
||||
"@typescript-eslint/visitor-keys": "5.37.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/type-utils": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.36.1.tgz",
|
||||
"integrity": "sha512-xfZhfmoQT6m3lmlqDvDzv9TiCYdw22cdj06xY0obSznBsT///GK5IEZQdGliXpAOaRL34o8phEvXzEo/VJx13Q==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.37.0.tgz",
|
||||
"integrity": "sha512-BSx/O0Z0SXOF5tY0bNTBcDEKz2Ec20GVYvq/H/XNKiUorUFilH7NPbFUuiiyzWaSdN3PA8JV0OvYx0gH/5aFAQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/typescript-estree": "5.36.1",
|
||||
"@typescript-eslint/utils": "5.36.1",
|
||||
"@typescript-eslint/typescript-estree": "5.37.0",
|
||||
"@typescript-eslint/utils": "5.37.0",
|
||||
"debug": "^4.3.4",
|
||||
"tsutils": "^3.21.0"
|
||||
},
|
||||
|
|
@ -18009,19 +18015,19 @@
|
|||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.36.1.tgz",
|
||||
"integrity": "sha512-jd93ShpsIk1KgBTx9E+hCSEuLCUFwi9V/urhjOWnOaksGZFbTOxAT47OH2d4NLJnLhkVD+wDbB48BuaycZPLBg==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.37.0.tgz",
|
||||
"integrity": "sha512-3frIJiTa5+tCb2iqR/bf7XwU20lnU05r/sgPJnRpwvfZaqCJBrl8Q/mw9vr3NrNdB/XtVyMA0eppRMMBqdJ1bA==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.36.1.tgz",
|
||||
"integrity": "sha512-ih7V52zvHdiX6WcPjsOdmADhYMDN15SylWRZrT2OMy80wzKbc79n8wFW0xpWpU0x3VpBz/oDgTm2xwDAnFTl+g==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.37.0.tgz",
|
||||
"integrity": "sha512-JkFoFIt/cx59iqEDSgIGnQpCTRv96MQnXCYvJi7QhBC24uyuzbD8wVbajMB1b9x4I0octYFJ3OwjAwNqk1AjDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "5.36.1",
|
||||
"@typescript-eslint/visitor-keys": "5.36.1",
|
||||
"@typescript-eslint/types": "5.37.0",
|
||||
"@typescript-eslint/visitor-keys": "5.37.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
|
|
@ -18061,26 +18067,26 @@
|
|||
}
|
||||
},
|
||||
"@typescript-eslint/utils": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.36.1.tgz",
|
||||
"integrity": "sha512-lNj4FtTiXm5c+u0pUehozaUWhh7UYKnwryku0nxJlYUEWetyG92uw2pr+2Iy4M/u0ONMKzfrx7AsGBTCzORmIg==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.37.0.tgz",
|
||||
"integrity": "sha512-jUEJoQrWbZhmikbcWSMDuUSxEE7ID2W/QCV/uz10WtQqfOuKZUqFGjqLJ+qhDd17rjgp+QJPqTdPIBWwoob2NQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"@typescript-eslint/scope-manager": "5.36.1",
|
||||
"@typescript-eslint/types": "5.36.1",
|
||||
"@typescript-eslint/typescript-estree": "5.36.1",
|
||||
"@typescript-eslint/scope-manager": "5.37.0",
|
||||
"@typescript-eslint/types": "5.37.0",
|
||||
"@typescript-eslint/typescript-estree": "5.37.0",
|
||||
"eslint-scope": "^5.1.1",
|
||||
"eslint-utils": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "5.36.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.36.1.tgz",
|
||||
"integrity": "sha512-ojB9aRyRFzVMN3b5joSYni6FAS10BBSCAfKJhjJAV08t/a95aM6tAhz+O1jF+EtgxktuSO3wJysp2R+Def/IWQ==",
|
||||
"version": "5.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.37.0.tgz",
|
||||
"integrity": "sha512-Hp7rT4cENBPIzMwrlehLW/28EVCOcE9U1Z1BQTc8EA8v5qpr7GRGuG+U58V5tTY48zvUOA3KHvw3rA8tY9fbdA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "5.36.1",
|
||||
"@typescript-eslint/types": "5.37.0",
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -19417,9 +19423,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"cypress": {
|
||||
"version": "10.7.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.7.0.tgz",
|
||||
"integrity": "sha512-gTFvjrUoBnqPPOu9Vl5SBHuFlzx/Wxg/ZXIz2H4lzoOLFelKeF7mbwYUOzgzgF0oieU2WhJAestQdkgwJMMTvQ==",
|
||||
"version": "10.8.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.8.0.tgz",
|
||||
"integrity": "sha512-QVse0dnLm018hgti2enKMVZR9qbIO488YGX06nH5j3Dg1isL38DwrBtyrax02CANU6y8F4EJUuyW6HJKw1jsFA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@cypress/request": "^2.88.10",
|
||||
|
|
@ -19441,7 +19447,7 @@
|
|||
"dayjs": "^1.10.4",
|
||||
"debug": "^4.3.2",
|
||||
"enquirer": "^2.3.6",
|
||||
"eventemitter2": "^6.4.3",
|
||||
"eventemitter2": "6.4.7",
|
||||
"execa": "4.1.0",
|
||||
"executable": "^4.1.1",
|
||||
"extract-zip": "2.0.1",
|
||||
|
|
@ -19588,9 +19594,9 @@
|
|||
"integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og=="
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "2.29.2",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.2.tgz",
|
||||
"integrity": "sha512-0VNbwmWJDS/G3ySwFSJA3ayhbURMTJLtwM2DTxf9CWondCnh6DTNlO9JgRSq6ibf4eD0lfMJNBxUdEAHHix+bA=="
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.11.2",
|
||||
|
|
@ -20137,12 +20143,12 @@
|
|||
"dev": true
|
||||
},
|
||||
"eslint": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.0.tgz",
|
||||
"integrity": "sha512-pBG/XOn0MsJcKcTRLr27S5HpzQo4kLr+HjLQIyK4EiCsijDl/TB+h5uEuJU6bQ8Edvwz1XWOjpaP2qgnXGpTcA==",
|
||||
"version": "8.23.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.1.tgz",
|
||||
"integrity": "sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@eslint/eslintrc": "^1.3.1",
|
||||
"@eslint/eslintrc": "^1.3.2",
|
||||
"@humanwhocodes/config-array": "^0.10.4",
|
||||
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
|
|
@ -20161,7 +20167,6 @@
|
|||
"fast-deep-equal": "^3.1.3",
|
||||
"file-entry-cache": "^6.0.1",
|
||||
"find-up": "^5.0.0",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"glob-parent": "^6.0.1",
|
||||
"globals": "^13.15.0",
|
||||
"globby": "^11.1.0",
|
||||
|
|
@ -20170,6 +20175,7 @@
|
|||
"import-fresh": "^3.0.0",
|
||||
"imurmurhash": "^0.1.4",
|
||||
"is-glob": "^4.0.0",
|
||||
"js-sdsl": "^4.1.4",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"levn": "^0.4.1",
|
||||
|
|
@ -20639,9 +20645,9 @@
|
|||
}
|
||||
},
|
||||
"eslint-plugin-react": {
|
||||
"version": "7.31.5",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.5.tgz",
|
||||
"integrity": "sha512-y7472VAcqns17rsQUk6tQCnqBi+boYjGdYarX022719+wGd1T4U1fOYJ2T2Trd3Od2q5M92e42zJ2uZOGmWamA==",
|
||||
"version": "7.31.8",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz",
|
||||
"integrity": "sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-includes": "^3.1.5",
|
||||
|
|
@ -20887,9 +20893,9 @@
|
|||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
|
||||
},
|
||||
"eventemitter2": {
|
||||
"version": "6.4.5",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.5.tgz",
|
||||
"integrity": "sha512-bXE7Dyc1i6oQElDG0jMRZJrRAn9QR2xyyFGmBdZleNmyQX0FqGYmhZIrIrpPfm/w//LTo4tVQGOGQcGCb5q9uw==",
|
||||
"version": "6.4.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz",
|
||||
"integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==",
|
||||
"dev": true
|
||||
},
|
||||
"executable": {
|
||||
|
|
@ -22464,6 +22470,12 @@
|
|||
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=",
|
||||
"dev": true
|
||||
},
|
||||
"js-sdsl": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz",
|
||||
"integrity": "sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==",
|
||||
"dev": true
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
@ -24633,9 +24645,9 @@
|
|||
}
|
||||
},
|
||||
"react-i18next": {
|
||||
"version": "11.18.5",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.5.tgz",
|
||||
"integrity": "sha512-cKcyuuzIv0YUZ4l9WORflVNuhISPAqQShOAsxwFyYuJoCA7HlLmHm7XnvO6hfAGmGpDNRhJHoBX8hG49Cb2xZQ==",
|
||||
"version": "11.18.6",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz",
|
||||
"integrity": "sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.14.5",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
|
|
@ -24951,9 +24963,9 @@
|
|||
"integrity": "sha512-agFFS3RzrLXJl5LY5xg/xYyXvUuVAnkhgKO7RaO9J1Ssth6yvbO+PIiV67V59MB5NCdAK2flvGvNT4mdKVniFA=="
|
||||
},
|
||||
"remix-auth": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.2.2.tgz",
|
||||
"integrity": "sha512-VtzkfxeXbnXilupRTZkP40aik4vFSdwwRT96mbq0UBDMqHVRfQ7h9Y51HFrTufHJZEfAdkCopedMVvm0vQYKag==",
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/remix-auth/-/remix-auth-3.3.0.tgz",
|
||||
"integrity": "sha512-sLY5s6DQF4OJp03HM/dIEw3tLfSOIowerS2QDTuvEVXoEA3IHVmk7QkvOKosWdVPbcnlRfwF6PbeBLKgo6vqsg==",
|
||||
"requires": {
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
|
|
@ -26495,9 +26507,9 @@
|
|||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.8.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.2.tgz",
|
||||
"integrity": "sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==",
|
||||
"version": "4.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz",
|
||||
"integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==",
|
||||
"dev": true
|
||||
},
|
||||
"unbox-primitive": {
|
||||
|
|
@ -27131,9 +27143,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"zod": {
|
||||
"version": "3.18.0",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.18.0.tgz",
|
||||
"integrity": "sha512-gwTm8RfUCe8l9rDwN5r2A17DkAa8Ez4Yl4yXqc5VqeGaXaJahzYYXbTwvhroZi0SNBqTwh/bKm2N0mpCzuw4bA=="
|
||||
"version": "3.19.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.19.1.tgz",
|
||||
"integrity": "sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA=="
|
||||
},
|
||||
"zwitch": {
|
||||
"version": "2.0.2",
|
||||
|
|
|
|||
27
package.json
|
|
@ -14,6 +14,7 @@
|
|||
"add-badge": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/add-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-analyzer-json": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/create-analyzer-json.ts",
|
||||
"replace-img-names": "node --experimental-specifier-resolution=node --loader ts-node/esm -r tsconfig-paths/register scripts/replace-img-names.ts",
|
||||
"lint:ts": "eslint . --ext .ts,.tsx",
|
||||
"lint:styles": "stylelint \"app/styles/**/*.css\"",
|
||||
|
|
@ -28,7 +29,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^7.5.0",
|
||||
"@headlessui/react": "^1.6.6",
|
||||
"@headlessui/react": "^1.7.1",
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@remix-run/node": "^1.7.0",
|
||||
"@remix-run/react": "^1.7.0",
|
||||
|
|
@ -36,7 +37,7 @@
|
|||
"better-sqlite3": "^7.6.2",
|
||||
"clsx": "^1.2.1",
|
||||
"countries-list": "^2.6.1",
|
||||
"date-fns": "^2.29.2",
|
||||
"date-fns": "^2.29.3",
|
||||
"fuse.js": "^6.6.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"i18next": "^21.9.1",
|
||||
|
|
@ -50,30 +51,30 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-flip-toolkit": "^7.0.16",
|
||||
"react-i18next": "^11.18.5",
|
||||
"react-i18next": "^11.18.6",
|
||||
"react-popper": "^2.3.0",
|
||||
"remix-auth": "^3.2.2",
|
||||
"remix-auth": "^3.3.0",
|
||||
"remix-auth-oauth2": "^1.3.0",
|
||||
"remix-i18next": "^4.1.1",
|
||||
"swr": "^1.3.0",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
"zod": "^3.18.0"
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^1.7.0",
|
||||
"@remix-run/eslint-config": "^1.7.0",
|
||||
"@types/better-sqlite3": "^7.6.0",
|
||||
"@types/i18next-fs-backend": "^1.1.2",
|
||||
"@types/node-cron": "^3.0.3",
|
||||
"@types/react": "^18.0.18",
|
||||
"@types/node-cron": "^3.0.4",
|
||||
"@types/react": "^18.0.20",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@typescript-eslint/eslint-plugin": "^5.36.1",
|
||||
"@typescript-eslint/parser": "^5.36.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.37.0",
|
||||
"@typescript-eslint/parser": "^5.37.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^10.7.0",
|
||||
"cypress": "^10.8.0",
|
||||
"dotenv": "^16.0.2",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-plugin-react": "^7.31.5",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"ley": "^0.7.1",
|
||||
"prettier": "^2.7.1",
|
||||
|
|
@ -85,7 +86,7 @@
|
|||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.0",
|
||||
"tsm": "^2.2.2",
|
||||
"typescript": "^4.8.2",
|
||||
"typescript": "^4.8.3",
|
||||
"uvu": "^0.5.6"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
|||
BIN
public/img/gear/clothes/1000.avif
Normal file
BIN
public/img/gear/clothes/1000.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
BIN
public/img/gear/clothes/10014.avif
Normal file
BIN
public/img/gear/clothes/10014.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 12 KiB |
BIN
public/img/gear/clothes/1063.avif
Normal file
BIN
public/img/gear/clothes/1063.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB |
BIN
public/img/gear/clothes/1067.avif
Normal file
BIN
public/img/gear/clothes/1067.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.7 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 11 KiB |
BIN
public/img/gear/clothes/1090.avif
Normal file
BIN
public/img/gear/clothes/1090.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/img/gear/clothes/25000.avif
Normal file
BIN
public/img/gear/clothes/25000.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/img/gear/clothes/25001.avif
Normal file
BIN
public/img/gear/clothes/25001.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/img/gear/clothes/25002.avif
Normal file
BIN
public/img/gear/clothes/25002.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/img/gear/clothes/25003.avif
Normal file
BIN
public/img/gear/clothes/25003.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/img/gear/clothes/25004.avif
Normal file
BIN
public/img/gear/clothes/25004.png
Normal file
|
After Width: | Height: | Size: 18 KiB |