diff --git a/app/features/build-analyzer/analyzer-types.ts b/app/features/build-analyzer/analyzer-types.ts
index e5321d504..b2d04c3d2 100644
--- a/app/features/build-analyzer/analyzer-types.ts
+++ b/app/features/build-analyzer/analyzer-types.ts
@@ -258,6 +258,10 @@ export interface FullInkTankOption {
type: InkConsumeType;
}
+export type MainWeaponInkConsumptionStats = {
+ [K in InkConsumeType as `mainWeaponInkConsumptionPercentage_${K}`]?: Stat;
+};
+
export type DamageType = (typeof DAMAGE_TYPE)[number];
export type TenacityPlayerDeficit = (typeof TENACITY_PLAYER_DEFICITS)[number];
@@ -350,7 +354,7 @@ export interface AnalyzedBuild {
specialRadiusRangeMin?: Stat;
specialRadiusRangeMax?: Stat;
specialPowerUpDuration?: Stat;
- };
+ } & MainWeaponInkConsumptionStats;
}
export type SpecialEffectType = (typeof SPECIAL_EFFECTS)[number]["type"];
diff --git a/app/features/build-analyzer/core/stats.test.ts b/app/features/build-analyzer/core/stats.test.ts
index 8bd8b5fb0..5afb22dde 100644
--- a/app/features/build-analyzer/core/stats.test.ts
+++ b/app/features/build-analyzer/core/stats.test.ts
@@ -1,8 +1,10 @@
import { describe, expect, test } from "vitest";
import type { MainWeaponId } from "~/modules/in-game-lists/types";
import { mainWeaponIds } from "~/modules/in-game-lists/weapon-ids";
+import { roundToNDecimalPlaces } from "~/utils/number";
import { damageTypeToWeaponType } from "../analyzer-constants";
import { buildStats } from "./stats";
+import { mainWeaponParams } from "./utils";
describe("Analyze build", () => {
test("Every main weapon has damage", () => {
@@ -151,6 +153,57 @@ describe("Analyze build", () => {
[57, 57],
]);
+ test("Main weapon ink consumption % exists per ink consume type of the weapon", () => {
+ const analyzedCharger = buildStats({
+ weaponSplId: 2010,
+ hasTacticooler: false,
+ });
+
+ expect(
+ analyzedCharger.stats.mainWeaponInkConsumptionPercentage_TAP_SHOT,
+ ).toBeDefined();
+ expect(
+ analyzedCharger.stats.mainWeaponInkConsumptionPercentage_FULL_CHARGE,
+ ).toBeDefined();
+ expect(
+ analyzedCharger.stats.mainWeaponInkConsumptionPercentage_NORMAL,
+ ).toBeUndefined();
+ });
+
+ test("ISM decreases main weapon ink consumption %", () => {
+ const analyzed = buildStats({
+ weaponSplId: 0,
+ hasTacticooler: false,
+ });
+
+ const analyzedWithISM = buildStats({
+ weaponSplId: 0,
+ abilityPoints: new Map([["ISM", 20]]),
+ hasTacticooler: false,
+ });
+
+ const stat = analyzed.stats.mainWeaponInkConsumptionPercentage_NORMAL!;
+ const statWithISM =
+ analyzedWithISM.stats.mainWeaponInkConsumptionPercentage_NORMAL!;
+
+ expect(stat.value).toBe(stat.baseValue);
+ expect(statWithISM.baseValue).toBe(stat.baseValue);
+ expect(statWithISM.value).toBeLessThan(statWithISM.baseValue);
+ });
+
+ test("Accounts for Jr. big ink tank with main weapon ink consumption %", () => {
+ const analyzedJr = buildStats({
+ weaponSplId: 10,
+ hasTacticooler: false,
+ });
+
+ const jrParams = mainWeaponParams(10);
+
+ expect(
+ analyzedJr.stats.mainWeaponInkConsumptionPercentage_NORMAL?.baseValue,
+ ).toBe(roundToNDecimalPlaces((jrParams.InkConsume! * 100) / 1.1));
+ });
+
test("Sub Power Up Beakon AP boost matches Lean", () => {
for (const [subPowerAp, quickSuperJumpAp] of subPowerApToQuickSuperJumpAp) {
const analyzed = buildStats({
diff --git a/app/features/build-analyzer/core/stats.ts b/app/features/build-analyzer/core/stats.ts
index 53507c8c8..19b137db4 100644
--- a/app/features/build-analyzer/core/stats.ts
+++ b/app/features/build-analyzer/core/stats.ts
@@ -35,6 +35,7 @@ import type {
AnalyzedBuild,
DamageType,
InkConsumeType,
+ MainWeaponInkConsumptionStats,
MainWeaponParams,
SpecialWeaponParams,
StatFunctionInput,
@@ -123,6 +124,7 @@ export function buildStats({
subWeaponWhiteInkSeconds: framesToSeconds(subWeaponParams.InkRecoverStop),
subWeaponInkConsumptionPercentage:
subWeaponInkConsumptionPercentage(input),
+ ...mainWeaponInkConsumptionPercentages(input),
squidFormInkRecoverySeconds: squidFormInkRecoverySeconds(input),
humanoidFormInkRecoverySeconds: humanoidFormInkRecoverySeconds(input),
runSpeed: runSpeed(input),
@@ -358,6 +360,37 @@ function subWeaponConsume({
};
}
+function mainWeaponInkConsumptionPercentages(
+ args: StatFunctionInput,
+): MainWeaponInkConsumptionStats {
+ const result: MainWeaponInkConsumptionStats = {};
+
+ for (const type of INK_CONSUME_TYPES) {
+ const baseInkConsume = mainWeaponInkConsumeByType({
+ ...args,
+ abilityPoints: new Map(),
+ type,
+ });
+
+ if (typeof baseInkConsume !== "number") continue;
+
+ const inkConsume = mainWeaponInkConsumeByType({ ...args, type });
+ invariant(typeof inkConsume === "number");
+
+ result[`mainWeaponInkConsumptionPercentage_${type}`] = {
+ baseValue: roundToNDecimalPlaces(
+ (baseInkConsume * 100) / inkTankSize(args.weaponSplId),
+ ),
+ value: roundToNDecimalPlaces(
+ (inkConsume * 100) / inkTankSize(args.weaponSplId),
+ ),
+ modifiedBy: "ISM",
+ };
+ }
+
+ return result;
+}
+
function mainWeaponInkConsumeByType({
mainWeaponParams,
abilityPoints,
diff --git a/app/features/build-analyzer/routes/analyzer.tsx b/app/features/build-analyzer/routes/analyzer.tsx
index 9d34820dc..7a83bc2b2 100644
--- a/app/features/build-analyzer/routes/analyzer.tsx
+++ b/app/features/build-analyzer/routes/analyzer.tsx
@@ -75,6 +75,7 @@ import type {
Stat,
SubWeaponDamage,
} from "../analyzer-types";
+import { INK_CONSUME_TYPES } from "../analyzer-types";
import { PerInkTankGrid } from "../components/PerInkTankGrid";
import {
ABILITIES_WITHOUT_CHUNKS,
@@ -207,6 +208,17 @@ function BuildAnalyzerPage() {
/>
),
+ ...INK_CONSUME_TYPES.filter(
+ (type) => analyzed.stats[`mainWeaponInkConsumptionPercentage_${type}`],
+ ).map((type) => (
+
+ )),
typeof analyzed.stats.mainWeaponWhiteInkSeconds === "number" && (