diff --git a/src/Infiltration/SaveLoadInfiltration.ts b/src/Infiltration/SaveLoadInfiltration.ts
new file mode 100644
index 000000000..888813989
--- /dev/null
+++ b/src/Infiltration/SaveLoadInfiltration.ts
@@ -0,0 +1,26 @@
+import { assertObject } from "../utils/TypeAssertion";
+import { InfiltrationState, InfiltrationStateDefault } from "./formulas/game";
+
+export function loadInfiltrations(saveString: unknown): void {
+ if (saveString == null || typeof saveString !== "string" || saveString === "") {
+ Object.assign(InfiltrationState, InfiltrationStateDefault);
+ return;
+ }
+ try {
+ const parsedData: unknown = JSON.parse(saveString);
+ assertObject(parsedData);
+ const { floors, lastChangeTimestamp } = parsedData;
+ if (typeof floors !== "number") {
+ throw new Error("Invalid parsedData.floors");
+ }
+ if (typeof lastChangeTimestamp !== "number") {
+ throw new Error("Invalid parsedData.lastChangeTimestamp");
+ }
+ InfiltrationState.floors = floors;
+ InfiltrationState.lastChangeTimestamp = lastChangeTimestamp;
+ } catch (error) {
+ console.error(error);
+ console.error("Invalid recent infiltrations:", saveString);
+ Object.assign(InfiltrationState, InfiltrationStateDefault);
+ }
+}
diff --git a/src/Infiltration/formulas/game.ts b/src/Infiltration/formulas/game.ts
index de01b2855..b1d97d6d1 100644
--- a/src/Infiltration/formulas/game.ts
+++ b/src/Infiltration/formulas/game.ts
@@ -1,11 +1,45 @@
import { Player } from "@player";
-import { calculateSkill } from "../../PersonObjects/formulas/skill";
+import { clampNumber } from "../../utils/helpers/clampNumber";
+
+export const MaxDifficultyForInfiltration = 3.5;
+// This value is typically denoted "lambda," and is the instantaneous rate of decay.
+const DecayRate = -2e-5;
+// This is the scalar for how much each floor completed affects the rewards for infiltration.
+const MarketDemandFactor = 1e-3;
+
+export const InfiltrationStateDefault = {
+ lastChangeTimestamp: 0,
+ floors: 0,
+};
+// Tracks an exponential moving average of number of successful infiltrations performed,
+// which decays back to 0. This state is only updated after a successful infil.
+export const InfiltrationState = { ...InfiltrationStateDefault };
+
+function calculateCurrentInfilFloors(timestamp: number): number {
+ return InfiltrationState.floors * Math.exp(DecayRate * (timestamp - InfiltrationState.lastChangeTimestamp));
+}
+
+// Calculates the infiltration reward multiplier based on how many and how recent other infiltrations were completed.
+// Each infiltration completed reduces the demand for corporate espionage data for a little while, thus affecting the
+// market demand.
+export function calculateMarketDemandMultiplier(timestamp: number, clamp = true): number {
+ const floors = calculateCurrentInfilFloors(timestamp);
+ // A parabola is chosen because it is easy to analyze and tune. The constant
+ // is a tuning factor, which primarily adjusts what the optimum rate of
+ // auto-infil is, and thus how good auto-infil is. The optimum
+ // marketDemandMultiplier will be 2/3 regardless of this constant.
+ const marketDemandMultiplier = 1 - MarketDemandFactor * floors * floors;
+
+ return clampNumber(marketDemandMultiplier, clamp ? 0 : marketDemandMultiplier, 1);
+}
+
+export function decreaseMarketDemandMultiplier(timestamp: number, floors: number) {
+ InfiltrationState.floors = calculateCurrentInfilFloors(timestamp) + floors;
+ InfiltrationState.lastChangeTimestamp = timestamp;
+}
function calculateRawDiff(stats: number, startingDifficulty: number): number {
- const difficulty = startingDifficulty - Math.pow(stats, 0.9) / 250 - Player.skills.intelligence / 1600;
- if (difficulty < 0) return 0;
- if (difficulty > 3) return 3;
- return difficulty;
+ return clampNumber(startingDifficulty - Math.pow(stats, 0.9) / 250 - Player.skills.intelligence / 1600, 0);
}
export function calculateDifficulty(startingSecurityLevel: number): number {
@@ -19,12 +53,5 @@ export function calculateDifficulty(startingSecurityLevel: number): number {
}
export function calculateReward(startingSecurityLevel: number): number {
- const xpMult = 10 * 60 * 15;
- const total =
- calculateSkill(Player.mults.strength_exp * xpMult, Player.mults.strength) +
- calculateSkill(Player.mults.defense_exp * xpMult, Player.mults.defense) +
- calculateSkill(Player.mults.agility_exp * xpMult, Player.mults.agility) +
- calculateSkill(Player.mults.dexterity_exp * xpMult, Player.mults.dexterity) +
- calculateSkill(Player.mults.charisma_exp * xpMult, Player.mults.charisma);
- return calculateRawDiff(total, startingSecurityLevel);
+ return clampNumber(calculateRawDiff(465, startingSecurityLevel), 0, 3);
}
diff --git a/src/Infiltration/formulas/victory.ts b/src/Infiltration/formulas/victory.ts
index 065b6eeeb..d452d3d8d 100644
--- a/src/Infiltration/formulas/victory.ts
+++ b/src/Infiltration/formulas/victory.ts
@@ -3,17 +3,21 @@ import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
import { LocationsMetadata } from "../../Locations/data/LocationsMetadata";
import { AugmentationName } from "@enums";
import { Faction } from "../../Faction/Faction";
+import { calculateMarketDemandMultiplier } from "./game";
export function calculateSellInformationCashReward(
reward: number,
maxLevel: number,
startingSecurityLevel: number,
+ timeStamp: number,
): number {
const levelBonus = maxLevel * Math.pow(1.01, maxLevel);
+ const marketRateMultiplier = calculateMarketDemandMultiplier(timeStamp);
return (
Math.pow(reward + 1, 2) *
Math.pow(startingSecurityLevel, 3) *
+ marketRateMultiplier *
3e3 *
levelBonus *
(Player.hasAugmentation(AugmentationName.WKSharmonizer, true) ? 1.5 : 1) *
@@ -25,27 +29,58 @@ export function calculateTradeInformationRepReward(
reward: number,
maxLevel: number,
startingSecurityLevel: number,
+ timeStamp: number,
): number {
- const levelBonus = maxLevel * Math.pow(1.01, maxLevel);
+ const levelBonus = maxLevel * Math.pow(1.005, maxLevel);
+ const marketRateMultiplier = calculateMarketDemandMultiplier(timeStamp);
+ let balanceMultiplier;
+ if (startingSecurityLevel < 4) {
+ balanceMultiplier = 0.45;
+ } else if (startingSecurityLevel < 5) {
+ balanceMultiplier = 0.4;
+ } else if (startingSecurityLevel < 7) {
+ balanceMultiplier = 0.35;
+ } else if (startingSecurityLevel < 12) {
+ balanceMultiplier = 0.3;
+ } else if (startingSecurityLevel < 14) {
+ balanceMultiplier = 0.26;
+ } else if (startingSecurityLevel < 15) {
+ balanceMultiplier = 0.25;
+ } else {
+ balanceMultiplier = 0.2;
+ }
return (
Math.pow(reward + 1, 1.1) *
- Math.pow(startingSecurityLevel, 1.2) *
+ Math.pow(startingSecurityLevel, 1.1) *
+ balanceMultiplier *
+ marketRateMultiplier *
30 *
levelBonus *
- (Player.hasAugmentation(AugmentationName.WKSharmonizer, true) ? 1.5 : 1) *
+ (Player.hasAugmentation(AugmentationName.WKSharmonizer, true) ? 1.2 : 1) *
currentNodeMults.InfiltrationRep
);
}
-export function calculateInfiltratorsRepReward(faction: Faction, startingSecurityLevel: number): number {
+export function calculateInfiltratorsRepReward(
+ faction: Faction,
+ maxLevel: number,
+ startingSecurityLevel: number,
+ timeStamp: number,
+): number {
const maxStartingSecurityLevel = LocationsMetadata.reduce((acc, data): number => {
const startingSecurityLevel = data.infiltrationData?.startingSecurityLevel || 0;
return acc > startingSecurityLevel ? acc : startingSecurityLevel;
}, 0);
const baseRepGain = (startingSecurityLevel / maxStartingSecurityLevel) * 5000;
+ const balanceMultiplier = 0.8 + 0.05 * (maxLevel - 5);
+ const marketRateMultiplier = calculateMarketDemandMultiplier(timeStamp);
return (
- baseRepGain * (Player.hasAugmentation(AugmentationName.WKSharmonizer, true) ? 2 : 1) * (1 + faction.favor / 100)
+ baseRepGain *
+ balanceMultiplier *
+ marketRateMultiplier *
+ (Player.hasAugmentation(AugmentationName.WKSharmonizer, true) ? 2 : 1) *
+ (1 + faction.favor / 100)
);
}
diff --git a/src/Infiltration/ui/BackwardGame.tsx b/src/Infiltration/ui/BackwardGame.tsx
index 8c7ab5dc6..c82cbefba 100644
--- a/src/Infiltration/ui/BackwardGame.tsx
+++ b/src/Infiltration/ui/BackwardGame.tsx
@@ -21,12 +21,12 @@ const difficulties: {
Trivial: Difficulty;
Normal: Difficulty;
Hard: Difficulty;
- Impossible: Difficulty;
+ Brutal: Difficulty;
} = {
Trivial: { timer: 16000, min: 3, max: 4 },
Normal: { timer: 12500, min: 2, max: 3 },
Hard: { timer: 15000, min: 3, max: 4 },
- Impossible: { timer: 8000, min: 4, max: 4 },
+ Brutal: { timer: 8000, min: 4, max: 4 },
};
export function BackwardGame(props: IMinigameProps): React.ReactElement {
diff --git a/src/Infiltration/ui/BracketGame.tsx b/src/Infiltration/ui/BracketGame.tsx
index 0617af5ed..a42cb79a3 100644
--- a/src/Infiltration/ui/BracketGame.tsx
+++ b/src/Infiltration/ui/BracketGame.tsx
@@ -21,12 +21,12 @@ const difficulties: {
Trivial: Difficulty;
Normal: Difficulty;
Hard: Difficulty;
- Impossible: Difficulty;
+ Brutal: Difficulty;
} = {
Trivial: { timer: 8000, min: 2, max: 3 },
Normal: { timer: 6000, min: 4, max: 5 },
Hard: { timer: 4000, min: 4, max: 6 },
- Impossible: { timer: 2500, min: 7, max: 7 },
+ Brutal: { timer: 2500, min: 7, max: 7 },
};
function generateLeftSide(difficulty: Difficulty): string {
diff --git a/src/Infiltration/ui/BribeGame.tsx b/src/Infiltration/ui/BribeGame.tsx
index 6d2617a2d..3c85dd034 100644
--- a/src/Infiltration/ui/BribeGame.tsx
+++ b/src/Infiltration/ui/BribeGame.tsx
@@ -21,12 +21,12 @@ const difficulties: {
Trivial: Difficulty;
Normal: Difficulty;
Hard: Difficulty;
- Impossible: Difficulty;
+ Brutal: Difficulty;
} = {
Trivial: { timer: 12000, size: 6 },
Normal: { timer: 9000, size: 8 },
Hard: { timer: 5000, size: 9 },
- Impossible: { timer: 2500, size: 12 },
+ Brutal: { timer: 2500, size: 12 },
};
export function BribeGame(props: IMinigameProps): React.ReactElement {
diff --git a/src/Infiltration/ui/CheatCodeGame.tsx b/src/Infiltration/ui/CheatCodeGame.tsx
index 5d86f4132..15a71fb1a 100644
--- a/src/Infiltration/ui/CheatCodeGame.tsx
+++ b/src/Infiltration/ui/CheatCodeGame.tsx
@@ -20,12 +20,12 @@ const difficulties: {
Trivial: Difficulty;
Normal: Difficulty;
Hard: Difficulty;
- Impossible: Difficulty;
+ Brutal: Difficulty;
} = {
Trivial: { timer: 13000, min: 6, max: 8 },
Normal: { timer: 7000, min: 7, max: 8 },
Hard: { timer: 5000, min: 8, max: 9 },
- Impossible: { timer: 3000, min: 9, max: 10 },
+ Brutal: { timer: 3000, min: 9, max: 10 },
};
export function CheatCodeGame(props: IMinigameProps): React.ReactElement {
diff --git a/src/Infiltration/ui/Cyberpunk2077Game.tsx b/src/Infiltration/ui/Cyberpunk2077Game.tsx
index 0ef0bc6a8..7e5dbac5e 100644
--- a/src/Infiltration/ui/Cyberpunk2077Game.tsx
+++ b/src/Infiltration/ui/Cyberpunk2077Game.tsx
@@ -28,12 +28,12 @@ const difficulties: {
Trivial: Difficulty;
Normal: Difficulty;
Hard: Difficulty;
- Impossible: Difficulty;
+ Brutal: Difficulty;
} = {
Trivial: { timer: 12500, width: 3, height: 3, symbols: 6 },
Normal: { timer: 15000, width: 4, height: 4, symbols: 7 },
Hard: { timer: 12500, width: 5, height: 5, symbols: 8 },
- Impossible: { timer: 10000, width: 6, height: 6, symbols: 9 },
+ Brutal: { timer: 10000, width: 6, height: 6, symbols: 9 },
};
export function Cyberpunk2077Game(props: IMinigameProps): React.ReactElement {
diff --git a/src/Infiltration/ui/Difficulty.ts b/src/Infiltration/ui/Difficulty.ts
index 5a64a896c..1a284e416 100644
--- a/src/Infiltration/ui/Difficulty.ts
+++ b/src/Infiltration/ui/Difficulty.ts
@@ -4,7 +4,7 @@ interface DifficultySettings {
Trivial: DifficultySetting;
Normal: DifficultySetting;
Hard: DifficultySetting;
- Impossible: DifficultySetting;
+ Brutal: DifficultySetting;
}
// I could use `any` to simply some of this but I also want to take advantage
@@ -24,6 +24,6 @@ export function interpolate(settings: DifficultySettings, n: number, out: Diffic
if (n < 0) return lerpD(settings.Trivial, settings.Trivial, 0);
if (n >= 0 && n < 1) return lerpD(settings.Trivial, settings.Normal, n);
if (n >= 1 && n < 2) return lerpD(settings.Normal, settings.Hard, n - 1);
- if (n >= 2 && n < 3) return lerpD(settings.Hard, settings.Impossible, n - 2);
- return lerpD(settings.Impossible, settings.Impossible, 0);
+ if (n >= 2 && n < 3) return lerpD(settings.Hard, settings.Brutal, n - 2);
+ return lerpD(settings.Brutal, settings.Brutal, 0);
}
diff --git a/src/Infiltration/ui/Game.tsx b/src/Infiltration/ui/Game.tsx
index d53612793..da238feda 100644
--- a/src/Infiltration/ui/Game.tsx
+++ b/src/Infiltration/ui/Game.tsx
@@ -18,12 +18,12 @@ import { calculateDamageAfterFailingInfiltration } from "../utils";
import { SnackbarEvents } from "../../ui/React/Snackbar";
import { PlayerEventType, PlayerEvents } from "../../PersonObjects/Player/PlayerEvents";
import { dialogBoxCreate } from "../../ui/React/DialogBox";
+import { calculateReward, MaxDifficultyForInfiltration } from "../formulas/game";
type GameProps = {
- StartingDifficulty: number;
- Difficulty: number;
- Reward: number;
- MaxLevel: number;
+ startingSecurityLevel: number;
+ difficulty: number;
+ maxLevel: number;
};
enum Stage {
@@ -44,7 +44,7 @@ const minigames = [
WireCuttingGame,
];
-export function Game(props: GameProps): React.ReactElement {
+export function Game({ startingSecurityLevel, difficulty, maxLevel }: GameProps): React.ReactElement {
const [level, setLevel] = useState(1);
const [stage, setStage] = useState(Stage.Countdown);
const [results, setResults] = useState("");
@@ -52,6 +52,9 @@ export function Game(props: GameProps): React.ReactElement {
lastGames: [-1, -1],
id: Math.floor(Math.random() * minigames.length),
});
+ // Base for when rewards are calculated, which is the start of the game window
+ const [timestamp, __] = useState(Date.now());
+ const reward = calculateReward(startingSecurityLevel);
const setupNextGame = useCallback(() => {
const nextGameId = () => {
@@ -80,21 +83,21 @@ export function Game(props: GameProps): React.ReactElement {
const onSuccess = useCallback(() => {
pushResult(true);
- if (level === props.MaxLevel) {
+ if (level === maxLevel) {
setStage(Stage.Sell);
} else {
setStage(Stage.Countdown);
setLevel(level + 1);
}
setupNextGame();
- }, [level, props.MaxLevel, setupNextGame]);
+ }, [level, maxLevel, setupNextGame]);
const onFailure = useCallback(
- (options?: { automated: boolean }) => {
+ (options?: { automated?: boolean; impossible?: boolean }) => {
setStage(Stage.Countdown);
pushResult(false);
Player.receiveRumor(FactionName.ShadowsOfAnarchy);
- let damage = calculateDamageAfterFailingInfiltration(props.StartingDifficulty);
+ let damage = calculateDamageAfterFailingInfiltration(startingSecurityLevel);
// Kill the player immediately if they use automation, so it's clear they're not meant to
if (options?.automated) {
damage = Player.hp.current;
@@ -106,13 +109,23 @@ export function Game(props: GameProps): React.ReactElement {
);
}, 500);
}
+ if (options?.impossible) {
+ damage = Player.hp.current;
+ setTimeout(() => {
+ SnackbarEvents.emit(
+ "You were discovered immediately. That location is far too secure for your current skill level.",
+ ToastVariant.ERROR,
+ 5000,
+ );
+ }, 500);
+ }
if (Player.takeDamage(damage)) {
Router.toPage(Page.City);
return;
}
setupNextGame();
},
- [props.StartingDifficulty, setupNextGame],
+ [startingSecurityLevel, setupNextGame],
);
function cancel(): void {
@@ -127,18 +140,17 @@ export function Game(props: GameProps): React.ReactElement {
break;
case Stage.Minigame: {
const MiniGame = minigames[gameIds.id];
- stageComponent = (
-
- );
+ stageComponent = ;
break;
}
case Stage.Sell:
stageComponent = (
);
break;
@@ -161,9 +173,17 @@ export function Game(props: GameProps): React.ReactElement {
cancel();
dialogBoxCreate("Infiltration was cancelled because you were hospitalized");
});
+
return clearSubscription;
}, []);
+ useEffect(() => {
+ // Immediately fail if the difficulty is higher than the max value.
+ if (difficulty >= MaxDifficultyForInfiltration) {
+ onFailure({ impossible: true });
+ }
+ });
+
return (
@@ -173,7 +193,7 @@ export function Game(props: GameProps): React.ReactElement {
)}
- Level {level} / {props.MaxLevel}
+ Level {level} / {maxLevel}
diff --git a/src/Infiltration/ui/InfiltrationRoot.tsx b/src/Infiltration/ui/InfiltrationRoot.tsx
index 1ff5010d5..eb8ef56ec 100644
--- a/src/Infiltration/ui/InfiltrationRoot.tsx
+++ b/src/Infiltration/ui/InfiltrationRoot.tsx
@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { Location } from "../../Locations/Location";
import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router";
-import { calculateDifficulty, calculateReward } from "../formulas/game";
+import { calculateDifficulty } from "../formulas/game";
import { Game } from "./Game";
import { Intro } from "./Intro";
import { dialogBoxCreate } from "../../ui/React/DialogBox";
@@ -30,7 +30,6 @@ export function InfiltrationRoot(props: IProps): React.ReactElement {
const startingSecurityLevel = props.location.infiltrationData.startingSecurityLevel;
const difficulty = calculateDifficulty(startingSecurityLevel);
- const reward = calculateReward(startingSecurityLevel);
function cancel(): void {
Router.toPage(Page.City);
@@ -40,18 +39,16 @@ export function InfiltrationRoot(props: IProps): React.ReactElement {
{start ? (
) : (
setStart(true)}
cancel={cancel}
/>
diff --git a/src/Infiltration/ui/Intro.tsx b/src/Infiltration/ui/Intro.tsx
index 71322950d..e4eb4921e 100644
--- a/src/Infiltration/ui/Intro.tsx
+++ b/src/Infiltration/ui/Intro.tsx
@@ -1,9 +1,8 @@
-import { Report } from "@mui/icons-material";
-import { Box, Button, Container, Paper, Tooltip, Typography } from "@mui/material";
+import { Box, Button, Container, Paper, Typography } from "@mui/material";
import React from "react";
-import { Location } from "../../Locations/Location";
+import type { Location } from "../../Locations/Location";
import { Settings } from "../../Settings/Settings";
-import { formatHp, formatMoney, formatNumberNoSuffix, formatReputation } from "../../ui/formatNumber";
+import { formatHp, formatMoney, formatNumberNoSuffix, formatPercent, formatReputation } from "../../ui/formatNumber";
import { Player } from "@player";
import { calculateDamageAfterFailingInfiltration } from "../utils";
import {
@@ -13,13 +12,14 @@ import {
} from "../formulas/victory";
import { Factions } from "../../Faction/Factions";
import { FactionName } from "../../Faction/Enums";
+import { calculateMarketDemandMultiplier, calculateReward, MaxDifficultyForInfiltration } from "../formulas/game";
+import { useRerender } from "../../ui/React/hooks";
interface IProps {
- Location: Location;
- StartingDifficulty: number;
- Difficulty: number;
- MaxLevel: number;
- Reward: number;
+ location: Location;
+ startingSecurityLevel: number;
+ difficulty: number;
+ maxLevel: number;
start: () => void;
cancel: () => void;
}
@@ -42,46 +42,81 @@ function arrowPart(color: string, length: number): JSX.Element {
}
function coloredArrow(difficulty: number): JSX.Element {
- if (difficulty === 0) {
+ const cappedDifficulty = Math.min(difficulty, MaxDifficultyForInfiltration);
+ if (cappedDifficulty === 0) {
return (
{">"}
- {" ".repeat(38)}
+ {" ".repeat(51)}
);
} else {
return (
<>
- {arrowPart(Settings.theme.primary, difficulty * 13)}
- {arrowPart(Settings.theme.warning, (difficulty - 1) * 13)}
- {arrowPart(Settings.theme.error, (difficulty - 2) * 13)}
+ {arrowPart(Settings.theme.primary, cappedDifficulty * 13)}
+ {arrowPart(Settings.theme.warning, (cappedDifficulty - 1) * 13)}
+ {arrowPart(Settings.theme.warning, (cappedDifficulty - 2) * 13)}
+ {arrowPart(Settings.theme.error, (cappedDifficulty - 3) * 26)}
>
);
}
}
-export function Intro(props: IProps): React.ReactElement {
- const repGain = calculateTradeInformationRepReward(props.Reward, props.MaxLevel, props.StartingDifficulty);
- const moneyGain = calculateSellInformationCashReward(props.Reward, props.MaxLevel, props.StartingDifficulty);
- const soaRepGain = calculateInfiltratorsRepReward(Factions[FactionName.ShadowsOfAnarchy], props.StartingDifficulty);
+export function Intro({
+ location,
+ startingSecurityLevel,
+ difficulty,
+ maxLevel,
+ start,
+ cancel,
+}: IProps): React.ReactElement {
+ useRerender(1000);
+
+ const timestampNow = Date.now();
+
+ const reward = calculateReward(startingSecurityLevel);
+ const repGain = calculateTradeInformationRepReward(reward, maxLevel, startingSecurityLevel, timestampNow);
+ const moneyGain = calculateSellInformationCashReward(reward, maxLevel, startingSecurityLevel, timestampNow);
+ const soaRepGain = calculateInfiltratorsRepReward(
+ Factions[FactionName.ShadowsOfAnarchy],
+ maxLevel,
+ startingSecurityLevel,
+ timestampNow,
+ );
+ const marketRateMultiplier = calculateMarketDemandMultiplier(timestampNow, false);
+
+ let warningMessage;
+ if (difficulty >= MaxDifficultyForInfiltration) {
+ warningMessage = (
+
+ This location is too secure for your current abilities. You cannot infiltrate it.
+
+ );
+ } else if (difficulty >= 1.5) {
+ warningMessage = (
+ 2 ? Settings.theme.error : Settings.theme.warning} textAlign="center">
+ This location is too heavily guarded for your current stats. You should train more or find an easier location.
+
+ );
+ }
return (
- Infiltrating {props.Location.name}
+ Infiltrating {location.name}
HP: {`${formatHp(Player.hp.current)} / ${formatHp(Player.hp.max)}`}
- Lose {formatHp(calculateDamageAfterFailingInfiltration(props.StartingDifficulty))} HP for each failure
+ Lose {formatHp(calculateDamageAfterFailingInfiltration(startingSecurityLevel))} HP for each failure
Maximum clearance level:
- {props.MaxLevel}
+ {maxLevel}
@@ -95,6 +130,12 @@ export function Intro(props: IProps): React.ReactElement {
{Player.factions.includes(FactionName.ShadowsOfAnarchy) && (
SoA reputation: {formatReputation(soaRepGain)}
)}
+
+ Market demand:{" "}
+ {marketRateMultiplier >= 0
+ ? formatPercent(marketRateMultiplier, marketRateMultiplier !== 100 ? 3 : 0)
+ : `0% (${formatPercent(marketRateMultiplier)})`}
+
@@ -102,38 +143,28 @@ export function Intro(props: IProps): React.ReactElement {
variant="h6"
sx={{
color:
- props.Difficulty > 2
- ? Settings.theme.error
- : props.Difficulty > 1
- ? Settings.theme.warning
- : Settings.theme.primary,
+ difficulty > 2 ? Settings.theme.error : difficulty > 1 ? Settings.theme.warning : Settings.theme.primary,
display: "flex",
alignItems: "center",
}}
>
Difficulty:
- {formatNumberNoSuffix(props.Difficulty * 33.3333)} / 100
- {props.Difficulty > 1.5 && (
-
- This location is too heavily guarded for your current stats. It is recommended that you try training
- or finding an easier location.
-
- }
- >
-
-
- )}
+ {formatNumberNoSuffix(difficulty * (100 / MaxDifficultyForInfiltration))} / 100
+ [{coloredArrow(difficulty)}]
+ {`▲ ▲ ▲ ▲ ▲`}
+ {` Trivial Normal Hard Brutal Impossible`}
- [{coloredArrow(props.Difficulty)}]
- {`▲ ▲ ▲ ▲`}
- {` Trivial Normal Hard Impossible`}
+ {warningMessage && (
+ <>
+
+ {warningMessage}
+ >
+ )}
@@ -163,8 +194,10 @@ export function Intro(props: IProps): React.ReactElement {
-
-
+
+
diff --git a/src/Infiltration/ui/MinesweeperGame.tsx b/src/Infiltration/ui/MinesweeperGame.tsx
index 983ed816c..1efc00ade 100644
--- a/src/Infiltration/ui/MinesweeperGame.tsx
+++ b/src/Infiltration/ui/MinesweeperGame.tsx
@@ -24,12 +24,12 @@ const difficulties: {
Trivial: Difficulty;
Normal: Difficulty;
Hard: Difficulty;
- Impossible: Difficulty;
+ Brutal: Difficulty;
} = {
Trivial: { timer: 15000, width: 3, height: 3, mines: 4 },
Normal: { timer: 15000, width: 4, height: 4, mines: 7 },
Hard: { timer: 15000, width: 5, height: 5, mines: 11 },
- Impossible: { timer: 15000, width: 6, height: 6, mines: 15 },
+ Brutal: { timer: 15000, width: 6, height: 6, mines: 15 },
};
export function MinesweeperGame(props: IMinigameProps): React.ReactElement {
diff --git a/src/Infiltration/ui/SlashGame.tsx b/src/Infiltration/ui/SlashGame.tsx
index 9b6ef9041..a0e131c1a 100644
--- a/src/Infiltration/ui/SlashGame.tsx
+++ b/src/Infiltration/ui/SlashGame.tsx
@@ -17,12 +17,12 @@ const difficulties: {
Trivial: Difficulty;
Normal: Difficulty;
Hard: Difficulty;
- Impossible: Difficulty;
+ Brutal: Difficulty;
} = {
Trivial: { window: 800 },
Normal: { window: 500 },
Hard: { window: 350 },
- Impossible: { window: 250 },
+ Brutal: { window: 250 },
};
export function SlashGame({ difficulty, onSuccess, onFailure }: IMinigameProps): React.ReactElement {
diff --git a/src/Infiltration/ui/Victory.tsx b/src/Infiltration/ui/Victory.tsx
index 4e2389905..8e746d691 100644
--- a/src/Infiltration/ui/Victory.tsx
+++ b/src/Infiltration/ui/Victory.tsx
@@ -18,12 +18,14 @@ import {
} from "../formulas/victory";
import { getEnumHelper } from "../../utils/EnumHelper";
import { isFactionWork } from "../../Work/FactionWork";
+import { decreaseMarketDemandMultiplier } from "../formulas/game";
interface IProps {
- StartingDifficulty: number;
- Difficulty: number;
- Reward: number;
- MaxLevel: number;
+ startingSecurityLevel: number;
+ difficulty: number;
+ reward: number;
+ timestamp: number;
+ maxLevel: number;
}
// Use a module-scope variable to save the faction choice.
@@ -42,13 +44,29 @@ export function Victory(props: IProps): React.ReactElement {
function quitInfiltration(): void {
handleInfiltrators();
+ decreaseMarketDemandMultiplier(props.timestamp, props.maxLevel);
Router.toPage(Page.City);
}
const soa = Factions[FactionName.ShadowsOfAnarchy];
- const repGain = calculateTradeInformationRepReward(props.Reward, props.MaxLevel, props.StartingDifficulty);
- const moneyGain = calculateSellInformationCashReward(props.Reward, props.MaxLevel, props.StartingDifficulty);
- const infiltrationRepGain = calculateInfiltratorsRepReward(soa, props.StartingDifficulty);
+ const repGain = calculateTradeInformationRepReward(
+ props.reward,
+ props.maxLevel,
+ props.startingSecurityLevel,
+ props.timestamp,
+ );
+ const moneyGain = calculateSellInformationCashReward(
+ props.reward,
+ props.maxLevel,
+ props.startingSecurityLevel,
+ props.timestamp,
+ );
+ const infiltrationRepGain = calculateInfiltratorsRepReward(
+ soa,
+ props.maxLevel,
+ props.startingSecurityLevel,
+ props.timestamp,
+ );
const isMemberOfInfiltrators = Player.factions.includes(FactionName.ShadowsOfAnarchy);
diff --git a/src/Infiltration/ui/WireCuttingGame.tsx b/src/Infiltration/ui/WireCuttingGame.tsx
index 7ab31c98b..f729db5f8 100644
--- a/src/Infiltration/ui/WireCuttingGame.tsx
+++ b/src/Infiltration/ui/WireCuttingGame.tsx
@@ -23,12 +23,12 @@ const difficulties: {
Trivial: Difficulty;
Normal: Difficulty;
Hard: Difficulty;
- Impossible: Difficulty;
+ Brutal: Difficulty;
} = {
Trivial: { timer: 9000, wiresmin: 4, wiresmax: 4, rules: 2 },
Normal: { timer: 7000, wiresmin: 6, wiresmax: 6, rules: 2 },
Hard: { timer: 5000, wiresmin: 8, wiresmax: 8, rules: 3 },
- Impossible: { timer: 4000, wiresmin: 9, wiresmax: 9, rules: 4 },
+ Brutal: { timer: 4000, wiresmin: 9, wiresmax: 9, rules: 4 },
};
const colors = ["red", "#FFC107", "blue", "white"];
diff --git a/src/NetscriptFunctions/Infiltration.ts b/src/NetscriptFunctions/Infiltration.ts
index 1681b4bbc..928dc5699 100644
--- a/src/NetscriptFunctions/Infiltration.ts
+++ b/src/NetscriptFunctions/Infiltration.ts
@@ -40,6 +40,9 @@ export function NetscriptInfiltration(): InternalAPI {
}
const startingSecurityLevel = location.infiltrationData.startingSecurityLevel;
const difficulty = calculateDifficulty(startingSecurityLevel);
+ // This is supposed to calculate the constant reward, without market demand.
+ // We simulate this by using a time far in the future.
+ const timestamp = Date.now() + 1e20;
const reward = calculateReward(startingSecurityLevel);
const maxLevel = location.infiltrationData.maxClearanceLevel;
return {
@@ -48,9 +51,14 @@ export function NetscriptInfiltration(): InternalAPI {
name: location.name,
},
reward: {
- tradeRep: calculateTradeInformationRepReward(reward, maxLevel, startingSecurityLevel),
- sellCash: calculateSellInformationCashReward(reward, maxLevel, startingSecurityLevel),
- SoARep: calculateInfiltratorsRepReward(Factions[FactionName.ShadowsOfAnarchy], startingSecurityLevel),
+ tradeRep: calculateTradeInformationRepReward(reward, maxLevel, startingSecurityLevel, timestamp),
+ sellCash: calculateSellInformationCashReward(reward, maxLevel, startingSecurityLevel, timestamp),
+ SoARep: calculateInfiltratorsRepReward(
+ Factions[FactionName.ShadowsOfAnarchy],
+ maxLevel,
+ startingSecurityLevel,
+ timestamp,
+ ),
},
difficulty: difficulty,
maxClearanceLevel: location.infiltrationData.maxClearanceLevel,
diff --git a/src/SaveObject.ts b/src/SaveObject.ts
index 789272b11..4960f0883 100644
--- a/src/SaveObject.ts
+++ b/src/SaveObject.ts
@@ -30,6 +30,8 @@ import { isObject, assertObject } from "./utils/TypeAssertion";
import { evaluateVersionCompatibility } from "./utils/SaveDataMigrationUtils";
import { Reviver } from "./utils/GenericReviver";
import { giveExportBonus } from "./ExportBonus";
+import { loadInfiltrations } from "./Infiltration/SaveLoadInfiltration";
+import { InfiltrationState } from "./Infiltration/formulas/game";
/* SaveObject.js
* Defines the object used to save/load games
@@ -83,6 +85,7 @@ export type BitburnerSaveObjectType = {
LastExportBonus?: string;
StaneksGiftSave: string;
GoSave: unknown; // "loadGo" function can process unknown data
+ InfiltrationsSave: unknown;
};
type ParsedSaveData = {
@@ -171,6 +174,7 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
LastExportBonus = "0";
StaneksGiftSave = "";
GoSave = "";
+ InfiltrationsSave = "";
async getSaveData(forceExcludeRunningScripts = false): Promise {
this.PlayerSave = JSON.stringify(Player);
@@ -191,6 +195,7 @@ class BitburnerSaveObject implements BitburnerSaveObjectType {
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
this.StaneksGiftSave = JSON.stringify(staneksGift);
this.GoSave = JSON.stringify(getGoSave());
+ this.InfiltrationsSave = JSON.stringify(InfiltrationState);
if (Player.gang) this.AllGangsSave = JSON.stringify(AllGangs);
@@ -429,6 +434,7 @@ async function loadGame(saveData: SaveData): Promise {
loadCompanies(saveObj.CompaniesSave);
loadFactions(saveObj.FactionsSave, Player);
loadGo(saveObj.GoSave);
+ loadInfiltrations(saveObj.InfiltrationsSave);
try {
loadAliases(saveObj.AliasesSave);
} catch (e) {
diff --git a/src/utils/TypeAssertion.ts b/src/utils/TypeAssertion.ts
index 19c9e5f5f..55a3b7dcb 100644
--- a/src/utils/TypeAssertion.ts
+++ b/src/utils/TypeAssertion.ts
@@ -72,3 +72,20 @@ export function assertArray(v: unknown): asserts v is unknown[] {
throw new TypeAssertionError(`The value is not an array. Its type is ${type}.`, type);
}
}
+
+export function assertNumberArray(unknownData: unknown, assertFinite = false): asserts unknownData is number[] {
+ assertArray(unknownData);
+ for (const value of unknownData) {
+ if (assertFinite) {
+ if (!Number.isFinite(value)) {
+ console.error("The array contains a value that is not a finite number. Array:", unknownData);
+ throw new Error(`${value} is not a number.`);
+ }
+ } else {
+ if (typeof value !== "number") {
+ console.error("The array contains a value that is not a number. Array:", unknownData);
+ throw new Error(`${value} is not a number.`);
+ }
+ }
+ }
+}