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.`); + } + } + } +}