mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-07-27 22:13:56 -05:00
Fix RP + new Tacticooler calculation
This commit is contained in:
parent
bbe57802b6
commit
d8b5dbade6
|
|
@ -68,12 +68,14 @@ export function useAnalyzeBuild() {
|
|||
effects,
|
||||
ldeIntensity,
|
||||
});
|
||||
const hasTacticooler = effects.includes("TACTICOOLER");
|
||||
const analyzed = buildStats({
|
||||
abilityPoints,
|
||||
weaponSplId: mainWeaponId,
|
||||
mainOnlyAbilities: build
|
||||
.map((row) => row[0])
|
||||
.filter(filterMainOnlyAbilities),
|
||||
hasTacticooler,
|
||||
});
|
||||
|
||||
const buildAbilityPoints2 = buildToAbilityPoints(build2);
|
||||
|
|
@ -88,6 +90,7 @@ export function useAnalyzeBuild() {
|
|||
mainOnlyAbilities: build2
|
||||
.map((row) => row[0])
|
||||
.filter(filterMainOnlyAbilities),
|
||||
hasTacticooler,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -134,10 +134,7 @@ export interface Stat<T = number> {
|
|||
modifiedBy: Ability | Array<Ability>;
|
||||
}
|
||||
|
||||
export type AbilityPoints = Map<
|
||||
AbilityWithUnknown,
|
||||
{ ap: number; apBeforeTacticooler: number }
|
||||
>;
|
||||
export type AbilityPoints = Map<AbilityWithUnknown, number>;
|
||||
|
||||
export type AbilityChunks = Map<AbilityWithUnknown, number>;
|
||||
|
||||
|
|
@ -148,6 +145,7 @@ export interface StatFunctionInput {
|
|||
specialWeaponParams: SpecialWeaponParams;
|
||||
abilityPoints: AbilityPoints;
|
||||
mainOnlyAbilities: Array<Ability>;
|
||||
hasTacticooler: boolean;
|
||||
}
|
||||
|
||||
export type InkConsumeType = typeof INK_CONSUME_TYPES[number];
|
||||
|
|
|
|||
|
|
@ -4,11 +4,6 @@ import { applySpecialEffects } from "./specialEffects";
|
|||
|
||||
const ApplySpecialEffects = suite("applySpecialEffects()");
|
||||
|
||||
const valueToAps = (value: number) => ({
|
||||
ap: value,
|
||||
apBeforeTacticooler: value,
|
||||
});
|
||||
|
||||
ApplySpecialEffects("Adds an effect to empty build", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["CB"],
|
||||
|
|
@ -17,7 +12,7 @@ ApplySpecialEffects("Adds an effect to empty build", () => {
|
|||
});
|
||||
|
||||
assert.equal(aps.size, 6);
|
||||
assert.equal(aps.get("ISM")?.ap, 10);
|
||||
assert.equal(aps.get("ISM"), 10);
|
||||
});
|
||||
|
||||
ApplySpecialEffects(
|
||||
|
|
@ -25,33 +20,33 @@ ApplySpecialEffects(
|
|||
() => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["CB"],
|
||||
abilityPoints: new Map([["SPU", valueToAps(10)]]),
|
||||
abilityPoints: new Map([["SPU", 10]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.size, 7);
|
||||
assert.equal(aps.get("SPU")?.ap, 10);
|
||||
assert.equal(aps.get("SPU"), 10);
|
||||
}
|
||||
);
|
||||
|
||||
ApplySpecialEffects("Does not boost ability beyond 57", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["CB"],
|
||||
abilityPoints: new Map([["ISM", valueToAps(57)]]),
|
||||
abilityPoints: new Map([["ISM", 57]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("ISM")?.ap, 57);
|
||||
assert.equal(aps.get("ISM"), 57);
|
||||
});
|
||||
|
||||
ApplySpecialEffects("Tacticooler doesn't boost swim speed beyond 29", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["TACTICOOLER"],
|
||||
abilityPoints: new Map([["SSU", valueToAps(28)]]),
|
||||
abilityPoints: new Map([["SSU", 28]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("SSU")?.ap, 29);
|
||||
assert.equal(aps.get("SSU"), 29);
|
||||
});
|
||||
|
||||
ApplySpecialEffects(
|
||||
|
|
@ -59,53 +54,42 @@ ApplySpecialEffects(
|
|||
() => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["TACTICOOLER"],
|
||||
abilityPoints: new Map([["SSU", valueToAps(30)]]),
|
||||
abilityPoints: new Map([["SSU", 30]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("SSU")?.ap, 30);
|
||||
assert.equal(aps.get("SSU"), 30);
|
||||
}
|
||||
);
|
||||
|
||||
ApplySpecialEffects("Tacticooler remembers AP before it was applied", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["TACTICOOLER"],
|
||||
abilityPoints: new Map([["QR", valueToAps(10)]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("QR")?.ap, 57);
|
||||
assert.equal(aps.get("QR")?.apBeforeTacticooler, 10);
|
||||
});
|
||||
|
||||
ApplySpecialEffects("Applies many effects", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["DR", "CB"],
|
||||
abilityPoints: new Map([["SSU", valueToAps(1)]]),
|
||||
abilityPoints: new Map([["SSU", 1]]),
|
||||
ldeIntensity: 0,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("SSU")?.ap, 21);
|
||||
assert.equal(aps.get("SSU"), 21);
|
||||
});
|
||||
|
||||
ApplySpecialEffects("Applies LDE", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["LDE"],
|
||||
abilityPoints: new Map([["ISM", valueToAps(1)]]),
|
||||
abilityPoints: new Map([["ISM", 1]]),
|
||||
ldeIntensity: 1,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("ISM")?.ap, 2);
|
||||
assert.equal(aps.get("ISM"), 2);
|
||||
});
|
||||
|
||||
ApplySpecialEffects("Applies LDE (intensity != aps given)", () => {
|
||||
const aps = applySpecialEffects({
|
||||
effects: ["LDE"],
|
||||
abilityPoints: new Map([["ISM", valueToAps(1)]]),
|
||||
abilityPoints: new Map([["ISM", 1]]),
|
||||
ldeIntensity: 15,
|
||||
});
|
||||
|
||||
assert.equal(aps.get("ISM")?.ap, 18);
|
||||
assert.equal(aps.get("ISM"), 18);
|
||||
});
|
||||
|
||||
ApplySpecialEffects.run();
|
||||
|
|
|
|||
|
|
@ -147,17 +147,13 @@ export function applySpecialEffects({
|
|||
|
||||
for (const value of valuesArr) {
|
||||
const boostsBeyond = "boostsBeyond" in value ? value.boostsBeyond : true;
|
||||
const currentAP = result.get(value.type)?.ap ?? 0;
|
||||
const currentAP = result.get(value.type) ?? 0;
|
||||
const newAPUnlimited = boostsBeyond
|
||||
? currentAP + value.ap
|
||||
: Math.max(currentAP, value.ap);
|
||||
const newAP = Math.min(newAPUnlimited, MAX_AP);
|
||||
|
||||
result.set(value.type, {
|
||||
ap: newAP,
|
||||
apBeforeTacticooler:
|
||||
effectObj.type === "TACTICOOLER" ? currentAP : newAP,
|
||||
});
|
||||
result.set(value.type, newAP);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ AnalyzeBuild("Every main weapon has damage", () => {
|
|||
for (const weaponSplId of mainWeaponIds) {
|
||||
const analyzed = buildStats({
|
||||
weaponSplId,
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
const hasDamage =
|
||||
|
|
@ -38,11 +39,13 @@ AnalyzeBuild("Every main weapon has damage", () => {
|
|||
AnalyzeBuild("Ninja Squid decreases swim speed", () => {
|
||||
const analyzed = buildStats({
|
||||
weaponSplId: 0,
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
const analyzedWithNS = buildStats({
|
||||
weaponSplId: 0,
|
||||
mainOnlyAbilities: ["NS"],
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
|
|
@ -50,4 +53,34 @@ AnalyzeBuild("Ninja Squid decreases swim speed", () => {
|
|||
);
|
||||
});
|
||||
|
||||
AnalyzeBuild("Tacticooler / RP calculated correctly", () => {
|
||||
const fullQR = buildStats({
|
||||
weaponSplId: 0,
|
||||
abilityPoints: new Map([["QR", 57]]),
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
const tacticooler = buildStats({
|
||||
weaponSplId: 0,
|
||||
abilityPoints: new Map([["QR", 57]]),
|
||||
hasTacticooler: true,
|
||||
});
|
||||
|
||||
assert.ok(
|
||||
fullQR.stats.quickRespawnTime.value ===
|
||||
tacticooler.stats.quickRespawnTime.value,
|
||||
"Base QR should be same whether 57AP of QR or Tacticooler"
|
||||
);
|
||||
assert.ok(
|
||||
fullQR.stats.quickRespawnTimeSplattedByRP.value >
|
||||
tacticooler.stats.quickRespawnTimeSplattedByRP.value,
|
||||
"Tacticooler splatted by RP should respawn faster than 57AP of QR"
|
||||
);
|
||||
assert.ok(
|
||||
tacticooler.stats.quickRespawnTime.value <
|
||||
tacticooler.stats.quickRespawnTimeSplattedByRP.value,
|
||||
"Tacticooler should respawn faster than Tacticooler splatted by RP"
|
||||
);
|
||||
});
|
||||
|
||||
AnalyzeBuild.run();
|
||||
|
|
|
|||
|
|
@ -32,10 +32,12 @@ export function buildStats({
|
|||
weaponSplId,
|
||||
abilityPoints = new Map(),
|
||||
mainOnlyAbilities = [],
|
||||
hasTacticooler,
|
||||
}: {
|
||||
weaponSplId: MainWeaponId;
|
||||
abilityPoints?: AbilityPoints;
|
||||
mainOnlyAbilities?: Array<Ability>;
|
||||
hasTacticooler: boolean;
|
||||
}): AnalyzedBuild {
|
||||
const mainWeaponParams = weaponParams().mainWeapons[weaponSplId];
|
||||
invariant(mainWeaponParams, `Weapon with splId ${weaponSplId} not found`);
|
||||
|
|
@ -61,6 +63,7 @@ export function buildStats({
|
|||
specialWeaponParams,
|
||||
abilityPoints,
|
||||
mainOnlyAbilities,
|
||||
hasTacticooler,
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
@ -196,7 +199,6 @@ function specialLost(
|
|||
abilityPoints: apFromMap({
|
||||
abilityPoints: abilityPoints,
|
||||
ability: SPECIAL_SAVED_AFTER_DEATH_ABILITY,
|
||||
key: hasRespawnPunisher ? "apBeforeTacticooler" : "ap",
|
||||
}),
|
||||
key: "SpecialGaugeRt_Restart",
|
||||
weapon: mainWeaponParams,
|
||||
|
|
@ -593,6 +595,14 @@ function swimSpeedHoldingRainmaker(
|
|||
};
|
||||
}
|
||||
|
||||
const qrApAfterRespawnPunish = ({
|
||||
ap,
|
||||
hasTacticooler,
|
||||
}: {
|
||||
ap: number;
|
||||
hasTacticooler: boolean;
|
||||
}) => (hasTacticooler ? ap : Math.ceil(ap * 0.15));
|
||||
|
||||
const RESPAWN_CHASE_FRAME = 150;
|
||||
const OWN_RESPAWN_PUNISHER_EXTRA_RESPAWN_FRAMES = 68;
|
||||
const ENEMY_RESPAWN_PUNISHER_EXTRA_RESPAWN_FRAMES = 45;
|
||||
|
|
@ -604,21 +614,25 @@ function respawnTime(
|
|||
const QUICK_RESPAWN_TIME_ABILITY = "QR";
|
||||
const hasRespawnPunisher = args.mainOnlyAbilities.includes("RP");
|
||||
|
||||
const ap = apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: QUICK_RESPAWN_TIME_ABILITY,
|
||||
});
|
||||
const abilityPoints =
|
||||
splattedByRP || hasRespawnPunisher
|
||||
? qrApAfterRespawnPunish({
|
||||
ap,
|
||||
hasTacticooler: args.hasTacticooler,
|
||||
})
|
||||
: ap;
|
||||
|
||||
const chase = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: QUICK_RESPAWN_TIME_ABILITY,
|
||||
key: hasRespawnPunisher ? "apBeforeTacticooler" : "ap",
|
||||
}),
|
||||
abilityPoints,
|
||||
key: "Dying_ChaseFrm",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
const around = abilityPointsToEffects({
|
||||
abilityPoints: apFromMap({
|
||||
abilityPoints: args.abilityPoints,
|
||||
ability: QUICK_RESPAWN_TIME_ABILITY,
|
||||
key: hasRespawnPunisher ? "apBeforeTacticooler" : "ap",
|
||||
}),
|
||||
abilityPoints,
|
||||
key: "Dying_AroundFrm",
|
||||
weapon: args.mainWeaponParams,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ BuildToAbilityPoints("Calculates ability points", () => {
|
|||
EMPTY_ROW,
|
||||
]);
|
||||
|
||||
assert.equal(aps.get("SS")?.ap, 13);
|
||||
assert.equal(aps.get("RSU")?.ap, 6);
|
||||
assert.equal(aps.get("UNKNOWN")?.ap, 38);
|
||||
assert.equal(aps.get("SS"), 13);
|
||||
assert.equal(aps.get("RSU"), 6);
|
||||
assert.equal(aps.get("UNKNOWN"), 38);
|
||||
});
|
||||
|
||||
BuildToAbilityPoints("Handles ability doubler", () => {
|
||||
|
|
@ -31,7 +31,7 @@ BuildToAbilityPoints("Handles ability doubler", () => {
|
|||
EMPTY_ROW,
|
||||
]);
|
||||
|
||||
assert.equal(aps.get("SS")?.ap, 6);
|
||||
assert.equal(aps.get("SS"), 6);
|
||||
});
|
||||
|
||||
BuildToAbilityPoints("Does not calculate AP for main only abilities", () => {
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ export function buildToAbilityPoints(build: BuildAbilitiesTupleWithUnknown) {
|
|||
|
||||
const aps = i === 0 ? 10 : 3;
|
||||
const apsDoubled = aps * (abilityDoublerActive ? 2 : 1);
|
||||
const newAp = (result.get(ability)?.ap ?? 0) + apsDoubled;
|
||||
const newAp = (result.get(ability) ?? 0) + apsDoubled;
|
||||
|
||||
result.set(ability, { ap: newAp, apBeforeTacticooler: newAp });
|
||||
result.set(ability, newAp);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,13 +57,11 @@ function isStackableAbility(ability: AbilityWithUnknown): ability is Ability {
|
|||
export function apFromMap({
|
||||
abilityPoints,
|
||||
ability,
|
||||
key = "ap",
|
||||
}: {
|
||||
abilityPoints: AbilityPoints;
|
||||
ability: Ability;
|
||||
key?: "ap" | "apBeforeTacticooler";
|
||||
}) {
|
||||
return abilityPoints.get(ability)?.[key] ?? 0;
|
||||
return abilityPoints.get(ability) ?? 0;
|
||||
}
|
||||
|
||||
function abilityValues({
|
||||
|
|
|
|||
|
|
@ -697,7 +697,7 @@ export default function BuildAnalyzerPage() {
|
|||
>
|
||||
{/** Hack the :has ;) */}
|
||||
{(["ISM", "ISS"] as const).some(
|
||||
(ability) => (abilityPoints.get(ability)?.ap ?? 0) > 0
|
||||
(ability) => (abilityPoints.get(ability) ?? 0) > 0
|
||||
) ? (
|
||||
<div className="analyzer__stat-card-highlighted" />
|
||||
) : null}
|
||||
|
|
@ -855,6 +855,7 @@ export default function BuildAnalyzerPage() {
|
|||
);
|
||||
}
|
||||
|
||||
// xxx: no main abilities?
|
||||
function APCompare({
|
||||
abilityPoints,
|
||||
abilityPoints2,
|
||||
|
|
@ -867,13 +868,13 @@ function APCompare({
|
|||
return (
|
||||
<div className="analyzer__ap-compare">
|
||||
{([...abilitiesShort, "UNKNOWN"] as const).map((ability) => {
|
||||
const ap = abilityPoints.get(ability)?.ap ?? 0;
|
||||
const ap2 = abilityPoints2.get(ability)?.ap ?? 0;
|
||||
const ap = abilityPoints.get(ability) ?? 0;
|
||||
const ap2 = abilityPoints2.get(ability) ?? 0;
|
||||
|
||||
if (!ap && !ap2) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<React.Fragment key={ability}>
|
||||
<div
|
||||
className={clsx("justify-self-end", {
|
||||
invisible: !ap,
|
||||
|
|
@ -898,7 +899,7 @@ function APCompare({
|
|||
{ap2}
|
||||
{t("analyzer:abilityPoints.short")}
|
||||
</div>
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -1076,7 +1077,7 @@ function StatCard({
|
|||
// you have Ninja Squid and stack swim speed
|
||||
// -> we still want to show the build value
|
||||
return [stat[0].modifiedBy].flat().some((ability) => {
|
||||
const hasStackable = (abilityPoints.get(ability)?.ap ?? 0) > 0;
|
||||
const hasStackable = (abilityPoints.get(ability) ?? 0) > 0;
|
||||
const hasEffect =
|
||||
baseValue !== stat[0].value && baseValue !== stat[1].value;
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export function useObjectDamage() {
|
|||
const isMultiShot = validatedMultiShotFromSearchParams(searchParams);
|
||||
const analyzed = buildStats({
|
||||
weaponSplId: mainWeaponId,
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
const damageType = validatedDamageTypeFromSearchParams({
|
||||
|
|
@ -62,8 +63,8 @@ export function useObjectDamage() {
|
|||
damagesToReceivers: damageType
|
||||
? calculateDamage({
|
||||
abilityPoints: new Map([
|
||||
["BRU", { ap: abilityPoints, apBeforeTacticooler: abilityPoints }],
|
||||
["SPU", { ap: abilityPoints, apBeforeTacticooler: abilityPoints }],
|
||||
["BRU", abilityPoints],
|
||||
["SPU", abilityPoints],
|
||||
]),
|
||||
analyzed,
|
||||
mainWeaponId,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ function calculate({
|
|||
}) {
|
||||
const analyzed = buildStats({
|
||||
weaponSplId: mainWeaponId,
|
||||
hasTacticooler: false,
|
||||
});
|
||||
|
||||
return calculateDamage({
|
||||
|
|
@ -35,7 +36,7 @@ const CalculateDamage = suite("calculateDamage()");
|
|||
CalculateDamage("BRU increases Splash Wall hitpoints", () => {
|
||||
const withoutBRU = calculate({});
|
||||
const withBRU = calculate({
|
||||
abilityPoints: new Map([["BRU", { ap: 10, apBeforeTacticooler: 10 }]]),
|
||||
abilityPoints: new Map([["BRU", 10]]),
|
||||
});
|
||||
|
||||
const hpWithoutBRU = withoutBRU.find(
|
||||
|
|
@ -51,7 +52,7 @@ CalculateDamage("BRU increases Splash Wall hitpoints", () => {
|
|||
CalculateDamage("SPU increases Big Bubbler hitpoints", () => {
|
||||
const withoutSPU = calculate({});
|
||||
const withSPU = calculate({
|
||||
abilityPoints: new Map([["SPU", { ap: 10, apBeforeTacticooler: 10 }]]),
|
||||
abilityPoints: new Map([["SPU", 10]]),
|
||||
});
|
||||
|
||||
const hpWithoutSPU = withoutSPU.find(
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user