INFIL: Add stat requirements; Add market consequences for spamming infiltration (#2210)

The primary parts are changing the way stats affect infiltration difficulty, to make rewards more intuitive and balanced, and adding a "market demand" mechanism, which kicks in when doing lots of infils quickly. With current parameters, market demand shouldn't affect manual play at all, and won't affect most auto-infil terribly (it depends how they're implemented).

This was a complex change, see PR #2210 for the full context
This commit is contained in:
Michael Ficocelli
2025-07-20 14:01:47 -04:00
committed by GitHub
parent fdafa191ac
commit dd128842af
19 changed files with 311 additions and 124 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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)
);
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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 = (
<MiniGame onSuccess={onSuccess} onFailure={onFailure} difficulty={props.Difficulty + level / 50} />
);
stageComponent = <MiniGame onSuccess={onSuccess} onFailure={onFailure} difficulty={difficulty + level / 50} />;
break;
}
case Stage.Sell:
stageComponent = (
<Victory
StartingDifficulty={props.StartingDifficulty}
Difficulty={props.Difficulty}
Reward={props.Reward}
MaxLevel={props.MaxLevel}
startingSecurityLevel={startingSecurityLevel}
difficulty={difficulty}
reward={reward}
timestamp={timestamp}
maxLevel={maxLevel}
/>
);
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 (
<Container>
<Paper sx={{ p: 1, mb: 1, display: "grid", justifyItems: "center", gap: 1 }}>
@@ -173,7 +193,7 @@ export function Game(props: GameProps): React.ReactElement {
</Button>
)}
<Typography variant="h5">
Level {level} / {props.MaxLevel}
Level {level} / {maxLevel}
</Typography>
<Progress />
</Paper>

View File

@@ -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 {
<div style={{ display: "flex", alignItems: "center", height: "calc(100vh - 16px)" }}>
{start ? (
<Game
StartingDifficulty={startingSecurityLevel}
Difficulty={difficulty}
Reward={reward}
MaxLevel={props.location.infiltrationData.maxClearanceLevel}
startingSecurityLevel={startingSecurityLevel}
difficulty={difficulty}
maxLevel={props.location.infiltrationData.maxClearanceLevel}
/>
) : (
<Intro
Location={props.location}
StartingDifficulty={startingSecurityLevel}
Difficulty={difficulty}
MaxLevel={props.location.infiltrationData.maxClearanceLevel}
Reward={reward}
location={props.location}
startingSecurityLevel={startingSecurityLevel}
difficulty={difficulty}
maxLevel={props.location.infiltrationData.maxClearanceLevel}
start={() => setStart(true)}
cancel={cancel}
/>

View File

@@ -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 (
<span style={{ color: "white" }}>
{">"}
{" ".repeat(38)}
{" ".repeat(51)}
</span>
);
} 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 = (
<Typography color={Settings.theme.error} textAlign="center">
This location is too secure for your current abilities. You cannot infiltrate it.
</Typography>
);
} else if (difficulty >= 1.5) {
warningMessage = (
<Typography color={difficulty > 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.
</Typography>
);
}
return (
<Container sx={{ alignItems: "center" }}>
<Paper sx={{ p: 1, mb: 1, display: "grid", justifyItems: "center" }}>
<Typography variant="h4">
Infiltrating <b>{props.Location.name}</b>
Infiltrating <b>{location.name}</b>
</Typography>
<Typography variant="h6">
<b>HP: {`${formatHp(Player.hp.current)} / ${formatHp(Player.hp.max)}`}</b>
</Typography>
<Typography variant="h6">
<b>Lose {formatHp(calculateDamageAfterFailingInfiltration(props.StartingDifficulty))} HP for each failure</b>
<b>Lose {formatHp(calculateDamageAfterFailingInfiltration(startingSecurityLevel))} HP for each failure</b>
</Typography>
<Typography variant="h6">
<b>Maximum clearance level: </b>
{props.MaxLevel}
{maxLevel}
</Typography>
<br />
@@ -95,6 +130,12 @@ export function Intro(props: IProps): React.ReactElement {
{Player.factions.includes(FactionName.ShadowsOfAnarchy) && (
<li>SoA reputation: {formatReputation(soaRepGain)}</li>
)}
<li>
Market demand:{" "}
{marketRateMultiplier >= 0
? formatPercent(marketRateMultiplier, marketRateMultiplier !== 100 ? 3 : 0)
: `0% (${formatPercent(marketRateMultiplier)})`}
</li>
</ul>
</Typography>
@@ -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",
}}
>
<b>Difficulty:&nbsp;</b>
{formatNumberNoSuffix(props.Difficulty * 33.3333)} / 100
{props.Difficulty > 1.5 && (
<Tooltip
title={
<Typography color="error">
This location is too heavily guarded for your current stats. It is recommended that you try training
or finding an easier location.
</Typography>
}
>
<Report sx={{ ml: 1 }} />
</Tooltip>
)}
{formatNumberNoSuffix(difficulty * (100 / MaxDifficultyForInfiltration))} / 100
</Typography>
<Typography sx={{ lineHeight: "1em", whiteSpace: "pre" }}>[{coloredArrow(difficulty)}]</Typography>
<Typography
sx={{ lineHeight: "1em", whiteSpace: "pre" }}
>{`▲ ▲ ▲ ▲ ▲`}</Typography>
<Typography
sx={{ lineHeight: "1em", whiteSpace: "pre" }}
>{` Trivial Normal Hard Brutal Impossible`}</Typography>
<Typography sx={{ lineHeight: "1em", whiteSpace: "pre" }}>[{coloredArrow(props.Difficulty)}]</Typography>
<Typography
sx={{ lineHeight: "1em", whiteSpace: "pre" }}
>{`▲ ▲ ▲ ▲`}</Typography>
<Typography
sx={{ lineHeight: "1em", whiteSpace: "pre" }}
>{` Trivial Normal Hard Impossible`}</Typography>
{warningMessage && (
<>
<br />
{warningMessage}
</>
)}
</Paper>
<Paper sx={{ p: 1, display: "grid", justifyItems: "center" }}>
@@ -163,8 +194,10 @@ export function Intro(props: IProps): React.ReactElement {
</ul>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", width: "100%" }}>
<Button onClick={props.start}>Start</Button>
<Button onClick={props.cancel}>Cancel</Button>
<Button onClick={start} disabled={difficulty >= MaxDifficultyForInfiltration}>
Start
</Button>
<Button onClick={cancel}>Cancel</Button>
</Box>
</Paper>
</Container>

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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"];

View File

@@ -40,6 +40,9 @@ export function NetscriptInfiltration(): InternalAPI<NetscriptInfiltation> {
}
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<NetscriptInfiltation> {
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,

View File

@@ -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<SaveData> {
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<boolean> {
loadCompanies(saveObj.CompaniesSave);
loadFactions(saveObj.FactionsSave, Player);
loadGo(saveObj.GoSave);
loadInfiltrations(saveObj.InfiltrationsSave);
try {
loadAliases(saveObj.AliasesSave);
} catch (e) {

View File

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