mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-16 06:18:42 +02:00
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:
committed by
GitHub
parent
fdafa191ac
commit
dd128842af
26
src/Infiltration/SaveLoadInfiltration.ts
Normal file
26
src/Infiltration/SaveLoadInfiltration.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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: </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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user