Merge remote-tracking branch 'origin/rewrite' into rewrite

This commit is contained in:
DoubleCookies
2022-10-27 10:42:57 +03:00
27 changed files with 657 additions and 146 deletions

View File

@@ -5,6 +5,7 @@
},
{ "name": "builds", "url": "builds" },
{ "name": "analyzer", "url": "analyzer" },
{ "name": "object-damage-calculator", "url": "object-damage-calculator" },
{ "name": "calendar", "url": "calendar" },
{ "name": "maps", "url": "maps" },
{ "name": "badges", "url": "badges" },

View File

@@ -1,21 +1,20 @@
import type { DamageType } from "./types";
import type objectDamages from "./object-dmg.json";
export const MAX_LDE_INTENSITY = 21;
export const MAX_AP = 57;
export const DAMAGE_RECEIVERS = [
"Bomb_TorpedoBullet", // Torpedo
"Chariot", // Crab Tank
"Gachihoko_Barrier", // Rainmaker Shield
"GreatBarrier_Barrier", // Big Bubbler Shield
"GreatBarrier_WeakPoint", // Big Bubbler Weak Point
// "InkRail", // InkRail
"NiceBall_Armor", // Booyah Bomb Armor
"ShockSonar", // Wave Breaker
// "Sponge_Versus", // Sponge
"GreatBarrier_Barrier", // Big Bubbler Shield
"GreatBarrier_WeakPoint", // Big Bubbler Weak Point
"Gachihoko_Barrier", // Rainmaker Shield
"Wsb_Flag", // Squid Beakon
"Wsb_Shield", // Splash Wall
"Wsb_Sprinkler", // Sprinkler
"Bomb_TorpedoBullet", // Torpedo
"BulletUmbrellaCanopyCompact", // Undercover Brella Canopy
"BulletUmbrellaCanopyNormal", // Splat Brella Canopy
"BulletUmbrellaCanopyWide", // Tenta Brella Canopy
@@ -24,6 +23,7 @@ export const DAMAGE_RECEIVERS = [
export const DAMAGE_TYPE = [
"NORMAL_MIN",
"NORMAL_MAX",
"NORMAL_MAX_FULL_CHARGE", // Hydra Splatling goes from 32 to 40 dmg when fully charged
"DIRECT",
"FULL_CHARGE",
"MAX_CHARGE",
@@ -39,6 +39,7 @@ export const damageTypeToWeaponType: Record<
> = {
NORMAL_MIN: "MAIN",
NORMAL_MAX: "MAIN",
NORMAL_MAX_FULL_CHARGE: "MAIN",
DIRECT: "MAIN",
FULL_CHARGE: "MAIN",
MAX_CHARGE: "MAIN",
@@ -47,3 +48,107 @@ export const damageTypeToWeaponType: Record<
BOMB_NORMAL: "SUB",
BOMB_DIRECT: "SUB",
};
export const objectDamageJsonKeyPriority: Record<
keyof typeof objectDamages,
Array<DamageType> | null
> = {
Blaster_BlasterMiddle: null,
Blaster_BlasterShort: null,
Blaster_KillOneShot: ["DIRECT"],
Blaster: null,
BlowerExhale_BombCore: null,
BlowerInhale: null,
BombFlower: null,
Bomb_CurlingBullet: null,
Bomb_DirectHit: ["BOMB_DIRECT"],
Bomb_Fizzy: null,
Bomb_Suction: null,
Bomb_TorpedoBullet: null,
Bomb_TorpedoSplashBurst: null,
Bomb_Trap: null,
Bomb: null,
BrushCore: null,
BrushSplash: null,
CannonMissile: null,
ChargerFull_Light: ["FULL_CHARGE"],
ChargerFull_Long: ["FULL_CHARGE"],
ChargerFull: ["FULL_CHARGE"],
Charger_Light: ["MAX_CHARGE", "TAP_SHOT"],
Charger_Long: ["MAX_CHARGE", "TAP_SHOT"],
Charger: ["MAX_CHARGE", "TAP_SHOT"],
Chariot_Body: null,
Chariot_Cannon: null,
Default: null,
EnemyFlyingHohei_BombCore: null,
GachihokoTimeUpBurst: null,
Gachihoko_BombCore: null,
Gachihoko_Bullet: null,
Geyser: null,
GoldenIkuraAttack: null,
InkStormRain: null,
InkStorm: null,
Jetpack_BombCore: null,
Jetpack_Bullet: null,
Jetpack_Coop: null,
Jetpack_Jet: null,
Maneuver_Short: null,
Maneuver: null,
MicroLaser: null,
MissionSalmonBuddy: null,
MovePainter_Burst: null,
MovePainter_Direct: null,
MultiMissile_BombCore: null,
MultiMissile_Bullet: null,
NiceBall: null,
ObjectEffect_Up: null,
RollerCore: null,
RollerSplash_Compact: null,
RollerSplash_Heavy: null,
RollerSplash_Hunter: null,
RollerSplash: null,
Saber_ChargeShot: null,
Saber_ChargeSlash: null,
Saber_Shot: null,
Saber_Slash: null,
SakerocketBullet: null,
ShelterCanopy_Compact: null,
ShelterCanopy_Wide: null,
ShelterCanopy: null,
ShelterShot_Compact: null,
ShelterShot_Wide: null,
ShelterShot: null,
Shield: null,
ShockSonar_Wave: null,
Shooter_Blaze: ["NORMAL_MAX", "NORMAL_MIN"],
Shooter_Expert: ["NORMAL_MAX", "NORMAL_MIN"],
Shooter_First: ["NORMAL_MAX", "NORMAL_MIN"],
Shooter_FlashRepeat: ["NORMAL_MAX", "NORMAL_MIN"],
Shooter_Flash: ["NORMAL_MAX", "NORMAL_MIN"],
Shooter_Gravity: ["NORMAL_MAX", "NORMAL_MIN"],
Shooter_Heavy: ["NORMAL_MAX", "NORMAL_MIN"],
Shooter_Long: ["NORMAL_MAX", "NORMAL_MIN"],
Shooter_Precision: ["NORMAL_MAX", "NORMAL_MIN"],
Shooter_Short: ["NORMAL_MAX", "NORMAL_MIN"],
Shooter_TripleMiddle: ["NORMAL_MAX", "NORMAL_MIN"],
Shooter_TripleQuick: ["NORMAL_MAX", "NORMAL_MIN"],
Shooter: null,
Skewer_Body: null,
Skewer: null,
Slosher_Bathtub: null,
Slosher_Bear: null,
Slosher_WashtubBombCore: null,
Slosher_Washtub: null,
Slosher: null,
Spinner: null,
Sprinkler: null,
Stringer_Short: null,
Stringer: null,
SuperHook: null,
SuperLanding: null,
TripleTornado: null,
UltraShot: null,
UltraStamp_Swing: null,
UltraStamp_Throw_BombCore: null,
UltraStamp_Throw: null,
};

View File

@@ -0,0 +1,114 @@
import { suite } from "uvu";
import * as assert from "uvu/assert";
import type { MainWeaponId } from "../in-game-lists";
import { calculateDamage } from "./objectDamage";
import { buildStats } from "./stats";
import type { AbilityPoints, DamageType } from "./types";
function calculate({
mainWeaponId = 10,
abilityPoints = new Map(),
damageType = "NORMAL_MAX",
}: {
mainWeaponId?: MainWeaponId;
abilityPoints?: AbilityPoints;
damageType?: DamageType;
}) {
const analyzed = buildStats({
weaponSplId: mainWeaponId,
});
return calculateDamage({
abilityPoints,
analyzed,
mainWeaponId,
damageType,
});
}
const CalculateDamage = suite("calculateDamage()");
CalculateDamage("BRU increases Splash Wall hitpoints", () => {
const withoutBRU = calculate({});
const withBRU = calculate({
abilityPoints: new Map([["BRU", { ap: 10, apBeforeTacticooler: 10 }]]),
});
const hpWithoutBRU = withoutBRU.find(
(d) => d.receiver === "Wsb_Shield"
)?.hitPoints;
const hpWithBRU = withBRU.find((d) => d.receiver === "Wsb_Shield")?.hitPoints;
assert.ok(typeof hpWithoutBRU === "number");
assert.ok(typeof hpWithBRU === "number");
assert.ok(hpWithoutBRU < hpWithBRU);
});
CalculateDamage("SPU increases Big Bubbler hitpoints", () => {
const withoutSPU = calculate({});
const withSPU = calculate({
abilityPoints: new Map([["SPU", { ap: 10, apBeforeTacticooler: 10 }]]),
});
const hpWithoutSPU = withoutSPU.find(
(d) => d.receiver === "GreatBarrier_Barrier"
)?.hitPoints;
const hpWithSPU = withSPU.find(
(d) => d.receiver === "GreatBarrier_Barrier"
)?.hitPoints;
assert.ok(typeof hpWithoutSPU === "number");
assert.ok(typeof hpWithSPU === "number");
assert.ok(hpWithoutSPU < hpWithSPU);
});
const shotsToPopRM: Array<
[
weaponId: MainWeaponId,
damageType: DamageType,
shotsToPop: number,
shotsToPopOS: number
]
> = [
// Splattershot
[40, "NORMAL_MAX", 28, 26],
// Range Blaster
[220, "DIRECT", 5, 4],
// .96 Gal
[80, "NORMAL_MAX", 17, 15],
// Splat Charger
[2010, "FULL_CHARGE", 4, 3],
// E-liter 4K
[2030, "TAP_SHOT", 13, 12],
];
CalculateDamage(
"Calculates matching HTD Rainmaker shield to in-game tests",
() => {
for (const [
mainWeaponId,
damageType,
shotsToPop,
shotsToPopOS,
] of shotsToPopRM) {
const damages = calculate({ mainWeaponId, damageType });
const damageVsRM = damages.find(
(d) => d.receiver === "Gachihoko_Barrier"
)!;
assert.equal(
damageVsRM.damages.find((d) => !d.objectShredder)!.hitsToDestroy,
shotsToPop,
`Shots to pop wrong for weapon id: ${mainWeaponId}`
);
assert.equal(
damageVsRM.damages.find((d) => d.objectShredder)!.hitsToDestroy,
shotsToPopOS,
`Shots to pop wrong with OS for weapon id: ${mainWeaponId}`
);
}
}
);
CalculateDamage.run();

View File

@@ -10,33 +10,14 @@ import type {
SpecialWeaponId,
SubWeaponId,
} from "../in-game-lists";
import { damageTypeToWeaponType, DAMAGE_RECEIVERS } from "./constants";
import {
damageTypeToWeaponType,
DAMAGE_RECEIVERS,
objectDamageJsonKeyPriority,
} from "./constants";
import { roundToNDecimalPlaces } from "~/utils/number";
import { objectHitPoints } from "./objectHitPoints";
/** Keys to check in the json. Lower index takes priority over higher. If key is omitted means any key with valid weapon id is okay. One json key can only map to one DamageType. */
const objectDamageJsonKeyPriority: Partial<
Record<DamageType, Array<keyof typeof objectDamages>>
> = {
// NORMAL_MIN: [],
// NORMAL_MAX: [],
DIRECT: ["Blaster_KillOneShot"],
FULL_CHARGE: [],
MAX_CHARGE: [],
TAP_SHOT: [],
// DISTANCE: [],
// BOMB_NORMAL: [],
BOMB_DIRECT: ["Bomb_DirectHit"],
};
const commonObjectDamageJsonKeys = () =>
Object.keys(objectDamages).filter(
(key) =>
!Object.values(objectDamageJsonKeyPriority)
.flat()
.includes(key as any)
) as Array<keyof typeof objectDamages>;
export function damageTypeToMultipliers({
type,
weapon,
@@ -56,10 +37,7 @@ export function damageTypeToMultipliers({
id: SpecialWeaponId;
};
}) {
const keysToCheck =
objectDamageJsonKeyPriority[type] ?? commonObjectDamageJsonKeys();
for (const key of keysToCheck) {
for (const key of jsonKeysToCeck(type)) {
const objectDamagesObj = objectDamages[key];
let ok = false;
@@ -77,13 +55,38 @@ export function damageTypeToMultipliers({
}
if (ok) {
console.log(`for ${type} used ${key ?? "FALLBACK"}`);
return objectDamagesObj.rates;
}
}
return null;
}
const objectDamageJsonKeyPriorityEntries = Object.entries(
objectDamageJsonKeyPriority
);
// for example blaster belongs to both Blaster_KillOneShot
// and Blaster categories so it needs to be specified
// which damage type uses which
function jsonKeysToCeck(type: DamageType) {
const result: Array<keyof typeof objectDamages> = [];
for (const [key, value] of objectDamageJsonKeyPriorityEntries) {
if (value?.includes(type)) {
result.push(key as keyof typeof objectDamages);
}
}
if (result.length) return result;
for (const [key, value] of objectDamageJsonKeyPriorityEntries) {
if (!value) {
result.push(key as keyof typeof objectDamages);
}
}
return result;
}
export function multipliersToRecordWithFallbacks(
multipliers: ReturnType<typeof damageTypeToMultipliers>

View File

@@ -75,7 +75,8 @@ export function buildStats({
},
stats: {
specialPoint: specialPoint(input),
specialSavedAfterDeath: specialSavedAfterDeath(input),
specialLost: specialLost(input),
specialLostSplattedByRP: specialLost(input, true),
fullInkTankOptions: fullInkTankOptions(input),
damages: damages(input),
mainWeaponWhiteInkSeconds:
@@ -97,7 +98,8 @@ export function buildStats({
enemyInkDamageLimit: enemyInkDamageLimit(input),
framesBeforeTakingDamageInEnemyInk:
framesBeforeTakingDamageInEnemyInk(input),
quickRespawnTime: quickRespawnTime(input),
quickRespawnTime: respawnTime(input),
quickRespawnTimeSplattedByRP: respawnTime(input, true),
superJumpTimeGroundFrames: superJumpTimeGroundFrames(input),
superJumpTimeTotal: superJumpTimeTotal(input),
shotSpreadAir: shotSpreadAir(input),
@@ -165,11 +167,11 @@ function specialPoint({
}
const OWN_RESPAWN_PUNISHER_EXTRA_SPECIAL_LOST = 0.225;
function specialSavedAfterDeath({
abilityPoints,
mainWeaponParams,
mainOnlyAbilities,
}: StatFunctionInput): AnalyzedBuild["stats"]["specialPoint"] {
const ENEMY_RESPAWN_PUNISHER_EXTRA_SPECIAL_LOST = 0.15;
function specialLost(
{ abilityPoints, mainWeaponParams, mainOnlyAbilities }: StatFunctionInput,
splattedByRP = false
): AnalyzedBuild["stats"]["specialPoint"] {
const SPECIAL_SAVED_AFTER_DEATH_ABILITY = "SS";
const hasRespawnPunisher = mainOnlyAbilities.includes("RP");
const extraSpecialLost = hasRespawnPunisher
@@ -189,9 +191,17 @@ function specialSavedAfterDeath({
weapon: mainWeaponParams,
});
const splattedByExtraPenalty = splattedByRP
? ENEMY_RESPAWN_PUNISHER_EXTRA_SPECIAL_LOST
: 0;
return {
baseValue: specialSavedAfterDeathForDisplay(baseEffect),
value: specialSavedAfterDeathForDisplay(effect - extraSpecialLost),
baseValue: specialSavedAfterDeathForDisplay(
baseEffect - splattedByExtraPenalty
),
value: specialSavedAfterDeathForDisplay(
effect - splattedByExtraPenalty - extraSpecialLost
),
modifiedBy: [SPECIAL_SAVED_AFTER_DEATH_ABILITY, "RP"],
};
}
@@ -331,6 +341,7 @@ const damageTypeToParamsKey: Record<
> = {
NORMAL_MIN: "DamageParam_ValueMin",
NORMAL_MAX: "DamageParam_ValueMax",
NORMAL_MAX_FULL_CHARGE: "DamageParam_ValueFullChargeMax",
DIRECT: "DamageParam_ValueDirect",
DISTANCE: "BlastParam_DistanceDamage",
FULL_CHARGE: "DamageParam_ValueFullCharge",
@@ -388,7 +399,7 @@ function shotsToSplat({
type: DamageType;
isTripleShooter: boolean;
}) {
if (type !== "NORMAL_MAX") return;
if (type !== "NORMAL_MAX" && type !== "NORMAL_MAX_FULL_CHARGE") return;
const multiplier = isTripleShooter ? 3 : 1;
@@ -516,9 +527,11 @@ function swimSpeed(
const RESPAWN_CHASE_FRAME = 150;
const OWN_RESPAWN_PUNISHER_EXTRA_RESPAWN_FRAMES = 68;
const ENEMY_RESPAWN_PUNISHER_EXTRA_RESPAWN_FRAMES = 45;
const SPLATOON_3_FASTER_RESPAWN = 60;
function quickRespawnTime(
args: StatFunctionInput
function respawnTime(
args: StatFunctionInput,
splattedByRP = false
): AnalyzedBuild["stats"]["quickRespawnTime"] {
const QUICK_RESPAWN_TIME_ABILITY = "QR";
const hasRespawnPunisher = args.mainOnlyAbilities.includes("RP");
@@ -542,14 +555,19 @@ function quickRespawnTime(
weapon: args.mainWeaponParams,
});
const extraFrames = hasRespawnPunisher
const ownRPExtraFrames = hasRespawnPunisher
? OWN_RESPAWN_PUNISHER_EXTRA_RESPAWN_FRAMES
: 0;
const splattedByExtraFrames = splattedByRP
? ENEMY_RESPAWN_PUNISHER_EXTRA_RESPAWN_FRAMES
: 0;
return {
baseValue: framesToSeconds(
RESPAWN_CHASE_FRAME +
chase.baseEffect +
splattedByExtraFrames +
around.baseEffect -
SPLATOON_3_FASTER_RESPAWN
),
@@ -557,7 +575,8 @@ function quickRespawnTime(
RESPAWN_CHASE_FRAME +
chase.effect +
around.effect +
extraFrames -
splattedByExtraFrames +
ownRPExtraFrames -
SPLATOON_3_FASTER_RESPAWN
),
modifiedBy: [QUICK_RESPAWN_TIME_ABILITY, "RP"],

View File

@@ -33,6 +33,7 @@ export interface MainWeaponParams {
MoveSpeedFullCharge?: number;
DamageParam_ValueMax?: number;
DamageParam_ValueMin?: number;
DamageParam_ValueFullChargeMax?: number;
DamageParam_ValueDirect?: number;
Jump_DegSwerve?: number;
Stand_DegSwerve?: number;
@@ -185,8 +186,8 @@ export interface AnalyzedBuild {
};
stats: {
specialPoint: Stat;
/** % of special charge saved when dying */
specialSavedAfterDeath: Stat;
specialLost: Stat;
specialLostSplattedByRP: Stat;
mainWeaponWhiteInkSeconds?: number;
subWeaponWhiteInkSeconds: number;
fullInkTankOptions: Array<FullInkTankOption & { id: string }>;
@@ -202,6 +203,7 @@ export interface AnalyzedBuild {
damageTakenInEnemyInkPerSecond: Stat;
enemyInkDamageLimit: Stat;
quickRespawnTime: Stat;
quickRespawnTimeSplattedByRP: Stat;
superJumpTimeGroundFrames: Stat;
superJumpTimeTotal: Stat;
shotSpreadAir?: Stat;

View File

@@ -1,5 +1,4 @@
import { useSearchParams } from "@remix-run/react";
import invariant from "tiny-invariant";
import { type MainWeaponId } from "../in-game-lists";
import { calculateDamage } from "./objectDamage";
import { buildStats } from "./stats";
@@ -37,7 +36,7 @@ export function useObjectDamage() {
{
weapon: String(newMainWeaponId),
[ABILITY_POINTS_SP_KEY]: String(newAbilityPoints),
[DAMAGE_TYPE_SP_KEY]: newDamageType,
[DAMAGE_TYPE_SP_KEY]: newDamageType ?? "",
},
{ replace: true, state: { scroll: false } }
);
@@ -47,15 +46,17 @@ export function useObjectDamage() {
mainWeaponId,
subWeaponId: analyzed.weapon.subWeaponSplId,
handleChange,
damagesToReceivers: calculateDamage({
abilityPoints: new Map([
["BRU", { ap: abilityPoints, apBeforeTacticooler: abilityPoints }],
["SPU", { ap: abilityPoints, apBeforeTacticooler: abilityPoints }],
]),
analyzed,
mainWeaponId,
damageType,
}),
damagesToReceivers: damageType
? calculateDamage({
abilityPoints: new Map([
["BRU", { ap: abilityPoints, apBeforeTacticooler: abilityPoints }],
["SPU", { ap: abilityPoints, apBeforeTacticooler: abilityPoints }],
]),
analyzed,
mainWeaponId,
damageType,
})
: null,
abilityPoints: String(abilityPoints),
damageType,
allDamageTypes: Array.from(
@@ -99,7 +100,6 @@ function validatedDamageTypeFromSearchParams({
const fallbackFound = damageTypePriorityList.find((type) =>
analyzed.stats.damages.some((d) => d.type === type)
);
invariant(fallbackFound);
return fallbackFound;
}

View File

@@ -375,7 +375,7 @@
"InkConsume_WeaponSwingParam": 0.18
},
"1030": {
"SpecialPoint": 200,
"SpecialPoint": 210,
"subWeaponId": 10,
"specialWeaponId": 4,
"InkConsume_WeaponVerticalSwingParam": 0.12,
@@ -513,7 +513,7 @@
"InkConsumeSlosher": 0.06
},
"3020": {
"SpecialPoint": 200,
"SpecialPoint": 210,
"subWeaponId": 5,
"specialWeaponId": 6,
"MoveSpeed": 0.07,
@@ -597,6 +597,7 @@
"MoveSpeed_Charge": 0.04,
"DamageParam_ValueMax": 320,
"DamageParam_ValueMin": 160,
"DamageParam_ValueFullChargeMax": 400,
"Jump_DegSwerve": 6,
"Stand_DegSwerve": 3,
"InkRecoverStop": 40,
@@ -748,7 +749,7 @@
"InkConsumeFullCharge_ChargeParam": 0.085
},
"7020": {
"SpecialPoint": 200,
"SpecialPoint": 210,
"subWeaponId": 6,
"specialWeaponId": 4,
"WeaponSpeedType": "Fast",
@@ -881,7 +882,7 @@
},
"SubInkSaveLv": 1,
"InkConsume": 0.6,
"InkRecoverStop": 70,
"InkRecoverStop": 85,
"DistanceDamage_BlastParamArray": [
[
{

View File

@@ -1,4 +1,5 @@
import { type LinksFunction, type MetaFunction } from "@remix-run/node";
import { Link } from "@remix-run/react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { AbilitiesSelector } from "~/components/AbilitiesSelector";
@@ -37,9 +38,14 @@ import styles from "~/styles/analyzer.css";
import { damageTypeTranslationString } from "~/utils/i18next";
import { type SendouRouteHandle } from "~/utils/remix";
import { makeTitle } from "~/utils/strings";
import { specialWeaponImageUrl, subWeaponImageUrl } from "~/utils/urls";
import {
navIconUrl,
objectDamageCalculatorPage,
specialWeaponImageUrl,
subWeaponImageUrl,
} from "~/utils/urls";
export const CURRENT_PATCH = "1.1";
export const CURRENT_PATCH = "1.2";
export const meta: MetaFunction = () => {
return {
@@ -69,6 +75,8 @@ export default function BuildAnalyzerPage() {
effects,
} = useAnalyzeBuild();
const objectShredderSelected = build[2][0] === "OS";
const mainWeaponCategoryItems = [
analyzed.stats.shotSpreadAir && (
<StatCard
@@ -245,10 +253,15 @@ export default function BuildAnalyzerPage() {
suffix={t("analyzer:suffix.specialPointsShort")}
/>
<StatCard
stat={analyzed.stats.specialSavedAfterDeath}
stat={analyzed.stats.specialLost}
title={t("analyzer:stat.specialLost")}
suffix="%"
/>
<StatCard
stat={analyzed.stats.specialLostSplattedByRP}
title={t("analyzer:stat.specialLostSplattedByRP")}
suffix="%"
/>
{analyzed.stats.specialDurationInSeconds && (
<StatCard
stat={analyzed.stats.specialDurationInSeconds}
@@ -544,6 +557,11 @@ export default function BuildAnalyzerPage() {
title={t("analyzer:stat.quickRespawnTime")}
suffix={t("analyzer:suffix.seconds")}
/>
<StatCard
stat={analyzed.stats.quickRespawnTimeSplattedByRP}
title={t("analyzer:stat.quickRespawnTimeSplattedByRP")}
suffix={t("analyzer:suffix.seconds")}
/>
<StatCard
stat={analyzed.stats.superJumpTimeGroundFrames}
title={t("analyzer:stat.superJumpTimeGround")}
@@ -554,6 +572,20 @@ export default function BuildAnalyzerPage() {
suffix={t("analyzer:suffix.seconds")}
/>
</StatCategory>
{objectShredderSelected && (
<Link
className="analyzer__noticeable-link"
to={objectDamageCalculatorPage(mainWeaponId)}
>
<Image
path={navIconUrl("object-damage-calculator")}
width={24}
height={24}
alt=""
/>
{t("analyzer:objCalcAd")}
</Link>
)}
</div>
</div>
</Main>
@@ -834,7 +866,7 @@ function DamageTable({
return (
<tr key={val.id}>
<td>{typeRowName}</td>
<td>{t(typeRowName)}</td>
<td>
{damage}{" "}
{val.shotsToSplat && (

View File

@@ -22,6 +22,7 @@ import {
CALENDAR_PAGE,
mapsPage,
navIconUrl,
objectDamageCalculatorPage,
plusSuggestionPage,
userPage,
} from "~/utils/urls";
@@ -75,6 +76,12 @@ export default function Index() {
description={t("front:analyzer.description")}
to={analyzerPage()}
/>
<FeatureCard
navItem="object-damage-calculator"
title={t("common:pages.object-damage-calculator")}
description={t("front:object-damage-calculator.description")}
to={objectDamageCalculatorPage()}
/>
<FeatureCard
navItem="plus"
title={t("common:pages.plus")}

View File

@@ -31,12 +31,15 @@ import { Ability } from "~/components/Ability";
import { damageTypeTranslationString } from "~/utils/i18next";
import { useSetTitle } from "~/hooks/useSetTitle";
export const CURRENT_PATCH = "1.2";
export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: styles }];
};
export const handle: SendouRouteHandle = {
i18n: ["weapons", "analyzer"],
navItemName: "object-damage-calculator",
};
export default function ObjectDamagePage() {
@@ -51,10 +54,6 @@ export default function ObjectDamagePage() {
allDamageTypes,
} = useObjectDamage();
if (process.env.NODE_ENV !== "development") {
return <Main>WIP :)</Main>;
}
return (
<Main className="stack lg">
<div className="object-damage__controls">
@@ -74,7 +73,7 @@ export default function ObjectDamagePage() {
clearsInputOnFocus
/>
</div>
<div>
<div className={clsx({ invisible: !damagesToReceivers })}>
<Label htmlFor="damage">{t("analyzer:labels.damageType")}</Label>
<DamageTypesSelect
handleChange={handleChange}
@@ -90,6 +89,7 @@ export default function ObjectDamagePage() {
<Ability ability="SPU" size="TINY" />
</Label>
<select
className="object-damage__select"
id="ap"
value={abilityPoints}
onChange={(e) =>
@@ -105,10 +105,22 @@ export default function ObjectDamagePage() {
</select>
</div>
</div>
<DamageReceiversGrid
subWeaponId={subWeaponId}
damagesToReceivers={damagesToReceivers}
/>
{damagesToReceivers ? (
<DamageReceiversGrid
subWeaponId={subWeaponId}
damagesToReceivers={damagesToReceivers}
/>
) : (
<div>{t("analyzer:noDmgData")}</div>
)}
<div className="object-damage__bottom-container">
<div className="text-lighter text-xs">
{t("analyzer:dmgHtdExplanation")}
</div>
<div className="object-damage__patch">
{t("analyzer:patch")} {CURRENT_PATCH}
</div>
</div>
</Main>
);
}
@@ -126,6 +138,7 @@ function DamageTypesSelect({
return (
<select
className="object-damage__select"
id="damage"
value={damageType}
onChange={(e) =>
@@ -167,12 +180,14 @@ const damageReceiverImages: Record<DamageReceiver, string> = {
function DamageReceiversGrid({
subWeaponId,
damagesToReceivers,
}: Pick<
ReturnType<typeof useObjectDamage>,
"damagesToReceivers" | "subWeaponId"
>) {
}: {
subWeaponId: ReturnType<typeof useObjectDamage>["subWeaponId"];
damagesToReceivers: NonNullable<
ReturnType<typeof useObjectDamage>["damagesToReceivers"]
>;
}) {
const { t } = useTranslation(["weapons", "analyzer", "common"]);
useSetTitle(t("common:pages.object-damage"));
useSetTitle(t("common:pages.object-damage-calculator"));
return (
<div

View File

@@ -55,6 +55,19 @@
padding-inline: var(--s-2);
}
.analyzer__noticeable-link {
display: flex;
width: 100%;
align-items: center;
border-radius: var(--rounded);
background-color: var(--theme-transparent);
font-size: var(--fonts-xxs);
font-weight: var(--semi-bold);
gap: var(--s-2);
padding-block: var(--s-1);
padding-inline: var(--s-2);
}
.analyzer__ap-text {
color: var(--text-lighter);
font-size: var(--fonts-xxs);

View File

@@ -1,6 +1,8 @@
.object-damage__controls {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: var(--s-4);
}
.object-damage__ap-label {
@@ -13,6 +15,7 @@
display: grid;
width: 100%;
column-gap: var(--s-6);
overflow-x: auto;
place-items: center;
row-gap: var(--s-3);
}
@@ -31,7 +34,9 @@
font-size: var(--fonts-xs);
font-weight: var(--semi-bold);
padding-block: var(--s-2);
padding-inline: var(--s-2);
text-align: center;
white-space: nowrap;
}
.object-damage__distance {
@@ -56,6 +61,11 @@
display: grid;
column-gap: var(--s-2);
grid-template-columns: 1fr 1fr;
padding-inline: var(--s-2);
}
.object-damage__select {
width: initial;
}
.object-damage__abbr {
@@ -71,3 +81,20 @@
letter-spacing: 0.5px;
margin-block-start: var(--s-2);
}
.object-damage__bottom-container {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
}
.object-damage__patch {
border-radius: var(--rounded);
background-color: var(--theme-transparent);
color: var(--text-lighter);
font-size: var(--fonts-xxxs);
font-weight: var(--bold);
padding-block: var(--s-0-5);
padding-inline: var(--s-1);
}

View File

@@ -1,3 +1,7 @@
.text-lg {
font-size: var(--fonts-lg);
}
.text-sm {
font-size: var(--fonts-sm);
}
@@ -6,8 +10,8 @@
font-size: var(--fonts-xs);
}
.text-lg {
font-size: var(--fonts-lg);
.text-xxs {
font-size: var(--fonts-xxs);
}
.text-center {

View File

@@ -86,6 +86,10 @@ export const analyzerPage = (args?: {
)}`
: ""
}`;
export const objectDamageCalculatorPage = (weaponId?: MainWeaponId) =>
`/object-damage-calculator${
typeof weaponId === "number" ? `?weapon=${weaponId}` : ""
}`;
export const badgeUrl = ({
code,

View File

@@ -0,0 +1,13 @@
---
title: InkTV Announces Its Return
date: 2022-10-24
author: Riczi
---
In a [Twitter post](https://twitter.com/InkTVSplat/status/1584590544205930496) released on Monday, October 24th, 2022, InkTV announced that it would be returning to the Splatoon competitive scene. This platform is dedicated highlighting the players and teams involved in competitive Splatoon. They were heavily involved in developing tournaments, podcast feeds, and player interviews in an effort to grow the community.
InkTV was retired in early 2020, just as the Covid-19 pandemic began to escalate. Now with tournaments being held again and the incredibly successful launch of a new game in Splatoon 3, InkTV is ready to make its return.
The announcement video features some of Splatoon's most prominent community members, including caster NineWholeGrains and professional players ProChara and KyochaN. In the footage they talk about what InkTV did in the past and how it helped them grow and develop as individuals. Everyone was then surprised with the return announcement.
Be sure to follow InkTV on social media so you can be part of the ever-growing Splatoon community. Check out the video if you haven't already so you can get excited for what InkTV will have in store.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -26,7 +26,7 @@
"forms.team.add": "Tilføj hold",
"forms.team.remove": "Fjern hold",
"forms.team.name": "Holdnavn",
"forms.team.placing": "Placerer",
"forms.team.placing": "Placering",
"forms.team.player.header": "Spiller {{number}}",
"forms.team.player.add": "Tilføj spiller",

View File

@@ -14,6 +14,11 @@
"header.logout": "Log ud",
"header.login": "Log ind",
"auth.errors.aborted": "logindforsøg afbrudt",
"auth.errors.failed": "Loginforsøg fejlet",
"auth.errors.discordPermissions": "Før at du kan oprette en profil på Sendou.ink, skal Sendou.ink have adgang til din Discordprofils navn, brugerbillede og sociale forbindelser (de sociale medier, som du har tilknyttet din discordprofil).",
"auth.errors.unknown": "Loginforsøget via discord fejlede grundet en ukendt årsag. Hvis dette bliver ved med at ske, så spørg om hjælp (på engelsk) på discord-serveren https://discord.gg/sendou via #helpdesk-chatten.",
"footer.github.subtitle": "Kildekode",
"footer.twitter.subtitle": "Opdateringer",
"footer.discord.subtitle": "Hjælp og feedback",
@@ -29,6 +34,7 @@
"actions.delete": "Slet",
"actions.loadMore": "Indlæs flere",
"actions.copyToClipboard": "Kopiér til udklipsholderen",
"actions.close": "Luk",
"maps.createMapList": "Lav bane-liste",
"maps.halfSz": "50% SZ",

View File

@@ -10,12 +10,17 @@
"stick": "Styrepind",
"sens": "Følsomhed",
"results.placing": "Rang",
"results.title": "Resultater",
"results.placing": "Placering",
"results.team": "Hold",
"results.tournament": "Turnering",
"results.participants": "Deltagere",
"results.date": "Dato",
"results.mates": "Holdkammerater",
"results.highlights": "Højdepunkter",
"results.nonHighlights": "Andre resultater",
"results.highlights.choose": "Vælg højdepunkter",
"results.highlights.explanation": "Vælg de resultater, som du vil fremhæve",
"forms.errors.invalidCustomUrl.numbers": "Brugerdefineret URL må ikke kun indeholde numre",
"forms.errors.invalidCustomUrl.strangeCharacter": "Brugerdefineret URL må ikke indeholde specialtegn (Gælder også æ, ø og å)",

View File

@@ -3,6 +3,7 @@
"attribute.weight.Fast": "Light",
"attribute.weight.Slow": "Heavy",
"attribute.weight.Normal": "Normal",
"objCalcAd": "For info on Object Shredder check out Object DMG Calc",
"stat.category.main": "Main weapon",
"stat.category.sub": "Sub weapon",
"stat.category.special": "Special weapon",
@@ -16,9 +17,11 @@
"stat.maxChargeHoldSeconds": "Max charge hold time",
"stat.specialPoints": "Points to special",
"stat.specialLost": "Special lost when splatted",
"stat.specialLostSplattedByRP": "Special lost when splatted by RP user",
"stat.whiteInk": "No ink recovery time after usage",
"stat.squidFormInkRecoverySeconds": "Ink tank full recovery time (squid form)",
"stat.quickRespawnTime": "Quick respawn time",
"stat.quickRespawnTimeSplattedByRP": "Quick Respawn time (splatted by RP user)",
"stat.superJumpTimeGround": "Super jump vulnerable frames",
"stat.superJumpTimeTotal": "Super jump time (total)",
"stat.jumpShotSpread": "Shot spread in degrees (jumping)",
@@ -77,6 +80,7 @@
"damage.toSplat": "{{count}} hits to splat",
"damage.NORMAL_MIN": "Minimum",
"damage.NORMAL_MAX": "Maximum",
"damage.NORMAL_MAX_FULL_CHARGE": "Maximum (Fully charged)",
"damage.DIRECT": "Direct",
"damage.FULL_CHARGE": "Fully charged shot",
"damage.MAX_CHARGE": "Maximum partial charge",
@@ -99,5 +103,7 @@
"hitsToDestroyShort": "HTD",
"labels.amountOf": "Amount of",
"labels.damageType": "Damage type",
"labels.weapon": "Weapon"
"labels.weapon": "Weapon",
"dmgHtdExplanation": "DMG = Damage • HTD = Hits to destroy",
"noDmgData": "No damage data yet for this weapon. Check back later!"
}

View File

@@ -9,7 +9,7 @@
"pages.builds": "Builds",
"pages.analyzer": "Build Analyzer",
"pages.maps": "Map Lists",
"pages.object-damage": "Object Damage Calculator",
"pages.object-damage-calculator": "Object DMG Calc",
"header.profile": "Profile",
"header.logout": "Log out",

View File

@@ -7,6 +7,7 @@
"badges.description": "List of all the badges you can earn for your profile",
"analyzer.description": "Find out what your builds actually do",
"maps.description": "Turn pool of maps into a list to play on",
"object-damage-calculator.description": "Calculate damage dealt to different objects",
"recentWinners": "Recent winners",
"upcomingEvents": "Upcoming events",
"articleBy": "by {{author}}"

View File

@@ -172,6 +172,10 @@ function parametersToMainWeaponResult(
DamageParam_ValueDirect,
BlastParam_DistanceDamage: params["BlastParam"]?.["DistanceDamage"],
DamageParam_ValueFullCharge: params["DamageParam"]?.["ValueFullCharge"],
DamageParam_ValueFullChargeMax:
params["DamageParam"]?.["ValueFullChargeMax"] !== DamageParam_ValueMax()
? params["DamageParam"]?.["ValueFullChargeMax"]
: undefined,
DamageParam_ValueMaxCharge: params["DamageParam"]?.["ValueMaxCharge"],
DamageParam_ValueMinCharge: params["DamageParam"]?.["ValueMinCharge"],
CanopyHP: params["spl__BulletShelterCanopyParam"]?.["CanopyHP"],

View File

@@ -3,7 +3,7 @@ import "dotenv/config";
import { sql } from "~/db/sql";
sql
.prepare(`update "User" set "customUrl" = NULL where "customUrl" like '%/%'`)
.prepare(`update "User" set "customUrl" = NULL where "customUrl" like '%#%'`)
.run();
console.log("Done");

View File

@@ -2,9 +2,28 @@
## /da (🟡 In progress)
### 🟢 analyzer.json
### 🟡 analyzer.json
**94/94**
**94/107**
<details>
<summary>Missing</summary>
- objCalcAd
- stat.specialLostSplattedByRP
- stat.quickRespawnTimeSplattedByRP
- damage.NORMAL_MAX_FULL_CHARGE
- distanceInline
- damageShort
- hitsToDestroyLong
- hitsToDestroyShort
- labels.amountOf
- labels.damageType
- labels.weapon
- dmgHtdExplanation
- noDmgData
</details>
### 🟢 badges.json
@@ -20,16 +39,12 @@
### 🟡 common.json
**55/60**
**60/61**
<details>
<summary>Missing</summary>
- auth.errors.aborted
- auth.errors.failed
- auth.errors.discordPermissions
- auth.errors.unknown
- actions.close
- pages.object-damage-calculator
</details>
@@ -41,36 +56,51 @@
**6/6**
### 🟢 front.json
### 🟡 front.json
**11/11**
**11/12**
<details>
<summary>Missing</summary>
- object-damage-calculator.description
</details>
### 🟢 game-misc.json
**17/17**
### 🟡 user.json
### 🟢 user.json
**20/25**
<details>
<summary>Missing</summary>
- results.title
- results.highlights
- results.nonHighlights
- results.highlights.choose
- results.highlights.explanation
</details>
**25/25**
---
## /de (🟡 In progress)
### 🟢 analyzer.json
### 🟡 analyzer.json
**94/94**
**94/107**
<details>
<summary>Missing</summary>
- objCalcAd
- stat.specialLostSplattedByRP
- stat.quickRespawnTimeSplattedByRP
- damage.NORMAL_MAX_FULL_CHARGE
- distanceInline
- damageShort
- hitsToDestroyLong
- hitsToDestroyShort
- labels.amountOf
- labels.damageType
- labels.weapon
- dmgHtdExplanation
- noDmgData
</details>
### 🟢 badges.json
@@ -86,11 +116,12 @@
### 🟡 common.json
**59/60**
**59/61**
<details>
<summary>Missing</summary>
- pages.object-damage-calculator
- actions.copyToClipboard
</details>
@@ -103,9 +134,16 @@
**6/6**
### 🟢 front.json
### 🟡 front.json
**11/11**
**11/12**
<details>
<summary>Missing</summary>
- object-damage-calculator.description
</details>
### 🟢 game-misc.json
@@ -121,14 +159,27 @@
### 🟡 analyzer.json
**91/94**
**91/107**
<details>
<summary>Missing</summary>
- objCalcAd
- stat.specialLostSplattedByRP
- stat.quickRespawnTimeSplattedByRP
- stat.shootingRunSpeed
- stat.shootingRunSpeedCharging
- stat.shootingRunSpeedFullCharge
- damage.NORMAL_MAX_FULL_CHARGE
- distanceInline
- damageShort
- hitsToDestroyLong
- hitsToDestroyShort
- labels.amountOf
- labels.damageType
- labels.weapon
- dmgHtdExplanation
- noDmgData
</details>
@@ -154,13 +205,14 @@
### 🟡 common.json
**48/60**
**48/61**
<details>
<summary>Missing</summary>
- pages.s2
- pages.maps
- pages.object-damage-calculator
- auth.errors.aborted
- auth.errors.failed
- auth.errors.discordPermissions
@@ -184,12 +236,13 @@
### 🟡 front.json
**10/11**
**10/12**
<details>
<summary>Missing</summary>
- maps.description
- object-damage-calculator.description
</details>
@@ -242,14 +295,27 @@
### 🟡 analyzer.json
**91/94**
**91/107**
<details>
<summary>Missing</summary>
- objCalcAd
- stat.specialLostSplattedByRP
- stat.quickRespawnTimeSplattedByRP
- stat.shootingRunSpeed
- stat.shootingRunSpeedCharging
- stat.shootingRunSpeedFullCharge
- damage.NORMAL_MAX_FULL_CHARGE
- distanceInline
- damageShort
- hitsToDestroyLong
- hitsToDestroyShort
- labels.amountOf
- labels.damageType
- labels.weapon
- dmgHtdExplanation
- noDmgData
</details>
@@ -275,7 +341,7 @@
### 🟡 common.json
**46/60**
**46/61**
<details>
<summary>Missing</summary>
@@ -283,6 +349,7 @@
- pages.s2
- pages.analyzer
- pages.maps
- pages.object-damage-calculator
- auth.errors.aborted
- auth.errors.failed
- auth.errors.discordPermissions
@@ -315,7 +382,7 @@
### 🟡 front.json
**8/11**
**8/12**
<details>
<summary>Missing</summary>
@@ -323,6 +390,7 @@
- buildsGoTo
- analyzer.description
- maps.description
- object-damage-calculator.description
</details>
@@ -375,7 +443,7 @@
### 🔴 analyzer.json
**0/94**
**0/107**
### 🔴 badges.json
@@ -391,7 +459,7 @@
### 🔴 common.json
**0/60**
**0/61**
### 🔴 contributions.json
@@ -403,7 +471,7 @@
### 🔴 front.json
**0/11**
**0/12**
### 🟡 game-misc.json
@@ -430,19 +498,22 @@
### 🟡 analyzer.json
**12/94**
**12/107**
<details>
<summary>Missing</summary>
- objCalcAd
- stat.canopyHp
- stat.fullChargeSeconds
- stat.maxChargeHoldSeconds
- stat.specialPoints
- stat.specialLost
- stat.specialLostSplattedByRP
- stat.whiteInk
- stat.squidFormInkRecoverySeconds
- stat.quickRespawnTime
- stat.quickRespawnTimeSplattedByRP
- stat.superJumpTimeGround
- stat.superJumpTimeTotal
- stat.jumpShotSpread
@@ -501,6 +572,7 @@
- damage.toSplat
- damage.NORMAL_MIN
- damage.NORMAL_MAX
- damage.NORMAL_MAX_FULL_CHARGE
- damage.DIRECT
- damage.FULL_CHARGE
- damage.MAX_CHARGE
@@ -517,6 +589,15 @@
- abilityPoints.short
- consumptionExplanation
- trackingSubDefExplanation
- distanceInline
- damageShort
- hitsToDestroyLong
- hitsToDestroyShort
- labels.amountOf
- labels.damageType
- labels.weapon
- dmgHtdExplanation
- noDmgData
</details>
@@ -542,7 +623,7 @@
### 🟡 common.json
**46/60**
**46/61**
<details>
<summary>Missing</summary>
@@ -550,6 +631,7 @@
- pages.s2
- pages.analyzer
- pages.maps
- pages.object-damage-calculator
- auth.errors.aborted
- auth.errors.failed
- auth.errors.discordPermissions
@@ -581,7 +663,7 @@
### 🟡 front.json
**8/11**
**8/12**
<details>
<summary>Missing</summary>
@@ -589,6 +671,7 @@
- buildsGoTo
- analyzer.description
- maps.description
- object-damage-calculator.description
</details>
@@ -641,7 +724,7 @@
### 🔴 analyzer.json
**0/94**
**0/107**
### 🟢 badges.json
@@ -665,7 +748,7 @@
### 🟡 common.json
**35/60**
**35/61**
<details>
<summary>Missing</summary>
@@ -673,6 +756,7 @@
- pages.s2
- pages.analyzer
- pages.maps
- pages.object-damage-calculator
- auth.errors.aborted
- auth.errors.failed
- auth.errors.discordPermissions
@@ -715,7 +799,7 @@
### 🟡 front.json
**8/11**
**8/12**
<details>
<summary>Missing</summary>
@@ -723,6 +807,7 @@
- buildsGoTo
- analyzer.description
- maps.description
- object-damage-calculator.description
</details>
@@ -773,9 +858,28 @@
## /nl (🟡 In progress)
### 🟢 analyzer.json
### 🟡 analyzer.json
**94/94**
**94/107**
<details>
<summary>Missing</summary>
- objCalcAd
- stat.specialLostSplattedByRP
- stat.quickRespawnTimeSplattedByRP
- damage.NORMAL_MAX_FULL_CHARGE
- distanceInline
- damageShort
- hitsToDestroyLong
- hitsToDestroyShort
- labels.amountOf
- labels.damageType
- labels.weapon
- dmgHtdExplanation
- noDmgData
</details>
### 🟢 badges.json
@@ -799,13 +903,14 @@
### 🟡 common.json
**48/60**
**48/61**
<details>
<summary>Missing</summary>
- pages.analyzer
- pages.maps
- pages.object-damage-calculator
- auth.errors.aborted
- auth.errors.failed
- auth.errors.discordPermissions
@@ -829,12 +934,13 @@
### 🟡 front.json
**10/11**
**10/12**
<details>
<summary>Missing</summary>
- maps.description
- object-damage-calculator.description
</details>
@@ -875,7 +981,7 @@
### 🔴 analyzer.json
**0/94**
**0/107**
### 🟢 badges.json
@@ -899,7 +1005,7 @@
### 🟡 common.json
**35/60**
**35/61**
<details>
<summary>Missing</summary>
@@ -907,6 +1013,7 @@
- pages.s2
- pages.analyzer
- pages.maps
- pages.object-damage-calculator
- auth.errors.aborted
- auth.errors.failed
- auth.errors.discordPermissions
@@ -949,7 +1056,7 @@
### 🟡 front.json
**8/11**
**8/12**
<details>
<summary>Missing</summary>
@@ -957,6 +1064,7 @@
- buildsGoTo
- analyzer.description
- maps.description
- object-damage-calculator.description
</details>
@@ -1007,9 +1115,28 @@
## /zh (🟡 In progress)
### 🟢 analyzer.json
### 🟡 analyzer.json
**94/94**
**94/107**
<details>
<summary>Missing</summary>
- objCalcAd
- stat.specialLostSplattedByRP
- stat.quickRespawnTimeSplattedByRP
- damage.NORMAL_MAX_FULL_CHARGE
- distanceInline
- damageShort
- hitsToDestroyLong
- hitsToDestroyShort
- labels.amountOf
- labels.damageType
- labels.weapon
- dmgHtdExplanation
- noDmgData
</details>
### 🟢 badges.json
@@ -1033,7 +1160,7 @@
### 🟡 common.json
**35/60**
**35/61**
<details>
<summary>Missing</summary>
@@ -1041,6 +1168,7 @@
- pages.s2
- pages.analyzer
- pages.maps
- pages.object-damage-calculator
- auth.errors.aborted
- auth.errors.failed
- auth.errors.discordPermissions
@@ -1083,7 +1211,7 @@
### 🟡 front.json
**8/11**
**8/12**
<details>
<summary>Missing</summary>
@@ -1091,6 +1219,7 @@
- buildsGoTo
- analyzer.description
- maps.description
- object-damage-calculator.description
</details>