sendou.ink/app/modules/analyzer/abilityChunksCalc.ts
William Lam 94ddd3031b
Ability chunk calculation now properly takes Ability Doubler into account (#1186)
* Issue 1180: ability chunk section now properly ignores Ability Doubler in its calculations
- Build Analyzer also no longer shows Ability Doubler as an Ability Chunk entry, nor does it show the Ability Chunks accordion if Ability Doubler is the only selected ability in the build

* Prettier formatting

* Now handles edge case for subabilities on Clothing gear with Ability Doubler
2022-12-07 19:07:29 +02:00

107 lines
3.9 KiB
TypeScript

import { abilities } from "../in-game-lists";
import type {
AbilityWithUnknown,
BuildAbilitiesTupleWithUnknown,
} from "../in-game-lists/types";
import type { AbilityChunks } from "./types";
// Reference for Ability Chunks numbers: https://splatoonwiki.org/wiki/Ability_chunk#Splatoon_3
const MAIN_REQUIRED_ABILITY_CHUNKS_COUNT = 45;
const PRIMARY_SLOT_ONLY_REQUIRED_ABILITY_CHUNKS_COUNT = 15;
const SUB_REQUIRED_ABILITY_CHUNKS_COUNT = 10;
// Ability Doubler: cost of adding a non-duplicate secondary ability only costs 3 ability chunks
// Reference: https://splatoonwiki.org/wiki/Splatfest_Tee#Splatoon_3_2
const SUB_WITH_ABILITY_DOUBLER_REQUIRED_ABILITY_CHUNKS_COUNT = 3;
export const ABILITIES_WITHOUT_CHUNKS = new Set(["UNKNOWN", "AD"]);
// From a given build, create a map of <Ability, number>, then return it as an Array after sorting by value, descending.
// The data structure describes the number of Ability chunks required for any given build.
export function getAbilityChunksMapAsArray(
build: BuildAbilitiesTupleWithUnknown
) {
const abilityChunksMap: AbilityChunks = new Map<AbilityWithUnknown, number>();
updateAbilityChunksMap(abilityChunksMap, build);
// Sort by value (number, descending) first, then sort by name (string, ascending)
return Array.from(abilityChunksMap).sort(
(a, b) => b[1] - a[1] || a[0].localeCompare(b[0])
);
}
function updateAbilityChunksMap(
abilityChunksMap: AbilityChunks,
build: BuildAbilitiesTupleWithUnknown
) {
let buildIndex = 0;
for (const gear of build) {
let hasAbilityDoubler = false;
// Handles the incremental amount of ability chunks required for the same ability for 1 piece of gear
const abilityChunksMapForGear = new Map<AbilityWithUnknown, number>();
for (const [index, selectedAbility] of gear.entries()) {
if (!selectedAbility) continue;
if (ABILITIES_WITHOUT_CHUNKS.has(selectedAbility)) {
// Detect the presence of Ability Doubler in the Clothing gear slot
if (selectedAbility === "AD" && buildIndex === 1) {
hasAbilityDoubler = true;
}
continue;
}
// Ability is in main slot
if (index === 0) {
const primarySlotOnlyAbilityRef = abilities.filter(
(a) =>
a.name === selectedAbility && a.abilityChunkTypesRequired.length > 0
);
// Extra processing is required for Main abilities that are primary slot-only abilities,
// as they are comprised of 3 stackable ability chunks at a lower ability chunk count than usual.
if (primarySlotOnlyAbilityRef.length === 1) {
const primaryAbility = primarySlotOnlyAbilityRef[0];
if (!primaryAbility) continue;
for (const ability of primaryAbility.abilityChunkTypesRequired) {
abilityChunksMap.set(
ability,
(abilityChunksMap.get(ability) ?? 0) +
PRIMARY_SLOT_ONLY_REQUIRED_ABILITY_CHUNKS_COUNT
);
}
} else {
abilityChunksMap.set(
selectedAbility,
(abilityChunksMap.get(selectedAbility) ?? 0) +
MAIN_REQUIRED_ABILITY_CHUNKS_COUNT
);
}
}
// Ability is in a sub slot
else {
// 1 Ability in sub slot = 10 chunks, 2 abilities = 20 chunks, 3 abilities = 30 chunks
// Also handle the edge case for when the piece of gear has Ability Doubler (3/6/9 chunks for 1/2/3 of the same ability)
abilityChunksMapForGear.set(
selectedAbility,
(abilityChunksMapForGear.get(selectedAbility) ?? 0) +
(hasAbilityDoubler
? SUB_WITH_ABILITY_DOUBLER_REQUIRED_ABILITY_CHUNKS_COUNT
: SUB_REQUIRED_ABILITY_CHUNKS_COUNT)
);
abilityChunksMap.set(
selectedAbility,
(abilityChunksMap.get(selectedAbility) ?? 0) +
(abilityChunksMapForGear.get(selectedAbility) ?? 0)
);
}
}
buildIndex += 1;
}
}