bitburner-src/src/engine.tsx

474 lines
16 KiB
TypeScript
Raw Normal View History

2021-09-25 20:42:57 +02:00
import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions";
import { AugmentationName, ToastVariant } from "@enums";
import { initBitNodeMultipliers } from "./BitNode/BitNode";
2022-10-01 21:03:47 +02:00
import { initSourceFiles } from "./SourceFile/SourceFiles";
import { generateRandomContract } from "./CodingContractGenerator";
import { CONSTANTS } from "./Constants";
2023-06-26 04:53:35 +02:00
import { Factions } from "./Faction/Factions";
2021-09-25 23:21:50 +02:00
import { staneksGift } from "./CotMG/Helper";
2021-09-09 05:47:34 +02:00
import { processPassiveFactionRepGain, inviteToFaction } from "./Faction/FactionHelpers";
2021-09-20 05:29:02 +02:00
import { Router } from "./ui/GameRoot";
import "./utils/Protections"; // Side-effect: Protect against certain unrecoverable errors
import "./PersonObjects/Player/PlayerObject"; // For side-effect of creating Player
2021-09-20 05:29:02 +02:00
2021-09-05 01:09:30 +02:00
import {
getHackingWorkRepGain,
getFactionSecurityWorkRepGain,
getFactionFieldWorkRepGain,
} from "./PersonObjects/formulas/reputation";
2021-09-09 09:17:01 +02:00
import { hasHacknetServers, processHacknetEarnings } from "./Hacknet/HacknetHelpers";
import { iTutorialStart } from "./InteractiveTutorial";
import { checkForMessagesToSend } from "./Message/MessageHelpers";
2021-09-09 05:47:34 +02:00
import { loadAllRunningScripts, updateOnlineScriptTimes } from "./NetscriptWorker";
import { Player } from "@player";
import { saveObject, loadGame } from "./SaveObject";
2021-09-18 01:43:08 +02:00
import { initForeignServers } from "./Server/AllServers";
import { Settings } from "./Settings/Settings";
import { FormatsNeedToChange } from "./ui/formatNumber";
2021-09-17 08:31:19 +02:00
import { initSymbolToStockMap, processStockPrices } from "./StockMarket/StockMarket";
2021-09-16 08:52:45 +02:00
import { Terminal } from "./Terminal";
import { Money } from "./ui/React/Money";
import { Hashes } from "./ui/React/Hashes";
import { Reputation } from "./ui/React/Reputation";
2021-10-01 07:00:50 +02:00
import { AlertEvents } from "./ui/React/AlertManager";
2021-09-25 20:42:57 +02:00
import { exceptionAlert } from "./utils/helpers/exceptionAlert";
2021-09-20 00:04:12 +02:00
import { startExploits } from "./Exploits/loops";
import { calculateAchievements } from "./Achievements/Achievements";
import React from "react";
import ReactDOM from "react-dom";
2021-12-20 19:57:07 +01:00
import { setupUncaughtPromiseHandler } from "./UncaughtPromiseHandler";
import { Button, Typography } from "@mui/material";
import { SnackbarEvents } from "./ui/React/Snackbar";
import { SaveData } from "./types";
import { Go } from "./Go/Go";
import { EventEmitter } from "./utils/EventEmitter";
// Only show warning if the time diff is greater than this value.
const thresholdOfTimeDiffForShowingWarningAboutSystemClock = CONSTANTS.MillisecondsPerFiveMinutes;
function showWarningAboutSystemClock(timeDiff: number) {
AlertEvents.emit(
`Warning: The system clock moved backward: ${convertTimeMsToTimeElapsedString(Math.abs(timeDiff))}.`,
);
}
export const GameCycleEvents = new EventEmitter<[]>();
/** Game engine. Handles the main game loop. */
2021-09-25 00:29:25 +02:00
const Engine: {
_lastUpdate: number;
updateGame: (numCycles?: number) => void;
Counters: {
[key: string]: number | undefined;
autoSaveCounter: number;
updateSkillLevelsCounter: number;
updateDisplays: number;
updateDisplaysLong: number;
updateActiveScriptsDisplay: number;
createProgramNotifications: number;
augmentationsNotifications: number;
checkFactionInvitations: number;
passiveFactionGrowth: number;
messages: number;
mechanicProcess: number;
contractGeneration: number;
achievementsCounter: number;
2021-09-25 00:29:25 +02:00
};
decrementAllCounters: (numCycles?: number) => void;
checkCounters: () => void;
load: (saveData: SaveData) => Promise<void>;
2021-09-25 00:29:25 +02:00
start: () => void;
} = {
2021-09-05 01:09:30 +02:00
// Time variables (milliseconds unix epoch time)
_lastUpdate: new Date().getTime(),
updateGame: function (numCycles = 1) {
2023-02-21 15:44:18 +01:00
const time = numCycles * CONSTANTS.MilliPerCycle;
2021-09-05 01:09:30 +02:00
if (Player.totalPlaytime == null) {
Player.totalPlaytime = 0;
}
if (Player.playtimeSinceLastAug == null) {
Player.playtimeSinceLastAug = 0;
}
if (Player.playtimeSinceLastBitnode == null) {
Player.playtimeSinceLastBitnode = 0;
}
Player.totalPlaytime += time;
Player.playtimeSinceLastAug += time;
Player.playtimeSinceLastBitnode += time;
2022-09-06 15:07:12 +02:00
Terminal.process(numCycles);
2022-07-14 23:43:08 +02:00
Player.processWork(numCycles);
2021-09-05 01:09:30 +02:00
// Update stock prices
if (Player.hasWseAccount) {
processStockPrices(numCycles);
}
// Gang
if (Player.gang) Player.gang.process(numCycles);
2021-09-25 23:21:50 +02:00
// Staneks gift
2022-09-18 03:09:15 +02:00
staneksGift.process(numCycles);
2021-09-25 23:21:50 +02:00
2021-09-05 01:09:30 +02:00
// Corporation
if (Player.corporation) {
Player.corporation.storeCycles(numCycles);
Player.corporation.process();
}
// Bladeburner
if (Player.bladeburner) Player.bladeburner.storeCycles(numCycles);
2021-09-05 01:09:30 +02:00
// Sleeves
Player.sleeves.forEach((sleeve) => sleeve.process(numCycles));
2021-09-05 01:09:30 +02:00
// Update the running time of all active scripts
updateOnlineScriptTimes(numCycles);
// Hacknet Nodes
2022-09-06 15:07:12 +02:00
processHacknetEarnings(numCycles);
2024-04-24 02:40:59 +02:00
// Counters
Engine.decrementAllCounters(numCycles);
// This **MUST** be the last call in the function, because checkCounters()
// can invoke the autosave, so any work done after here risks not getting saved!
Engine.checkCounters();
2021-09-05 01:09:30 +02:00
},
/**
* Counters for the main event loop. Represent the number of game cycles that
* are required for something to happen. These counters are in game cycles,
* which is once every 200ms
*/
Counters: {
autoSaveCounter: 300,
updateSkillLevelsCounter: 10,
updateDisplays: 3,
updateDisplaysLong: 15,
updateActiveScriptsDisplay: 5,
createProgramNotifications: 10,
augmentationsNotifications: 10,
checkFactionInvitations: 100,
passiveFactionGrowth: 5,
messages: 150,
mechanicProcess: 5, // Process Bladeburner
2021-09-05 01:09:30 +02:00
contractGeneration: 3000, // Generate Coding Contracts
2022-01-08 20:58:34 +01:00
achievementsCounter: 60, // Check if we have new achievements
2021-09-05 01:09:30 +02:00
},
decrementAllCounters: function (numCycles = 1) {
for (const [counterName, counter] of Object.entries(Engine.Counters)) {
2021-09-25 00:29:25 +02:00
if (counter === undefined) throw new Error("counter should not be undefined");
Engine.Counters[counterName] = counter - numCycles;
2021-09-05 01:09:30 +02:00
}
},
/**
* Checks if any counters are 0. If they are, executes whatever
* is necessary and then resets the counter
*/
checkCounters: function () {
2021-09-11 23:23:56 +02:00
if (Engine.Counters.checkFactionInvitations <= 0) {
const invitedFactions = Player.checkForFactionInvitations();
for (const invitedFaction of invitedFactions) {
inviteToFaction(invitedFaction);
2021-09-11 23:23:56 +02:00
}
Engine.Counters.checkFactionInvitations = 100;
}
2021-09-05 01:09:30 +02:00
if (Engine.Counters.passiveFactionGrowth <= 0) {
2021-09-25 07:26:03 +02:00
const adjustedCycles = Math.floor(5 - Engine.Counters.passiveFactionGrowth);
2021-09-05 01:09:30 +02:00
processPassiveFactionRepGain(adjustedCycles);
Engine.Counters.passiveFactionGrowth = 5;
}
2021-09-05 01:09:30 +02:00
if (Engine.Counters.messages <= 0) {
checkForMessagesToSend();
if (Player.hasAugmentation(AugmentationName.TheRedPill)) {
2021-09-05 01:09:30 +02:00
Engine.Counters.messages = 4500; // 15 minutes for Red pill message
} else {
Engine.Counters.messages = 150;
}
}
if (Engine.Counters.mechanicProcess <= 0) {
if (Player.bladeburner) {
2021-09-05 01:09:30 +02:00
try {
2022-09-06 15:07:12 +02:00
Player.bladeburner.process();
2021-09-05 01:09:30 +02:00
} catch (e) {
exceptionAlert("Exception caught in Bladeburner.process(): " + e);
}
2021-09-05 01:09:30 +02:00
}
Engine.Counters.mechanicProcess = 5;
}
2021-09-05 01:09:30 +02:00
if (Engine.Counters.contractGeneration <= 0) {
// X% chance of a contract being generated
if (Math.random() <= 0.25) {
generateRandomContract();
}
Engine.Counters.contractGeneration = 3000;
}
if (Engine.Counters.achievementsCounter <= 0) {
calculateAchievements();
Engine.Counters.achievementsCounter = 300;
}
2024-04-24 02:40:59 +02:00
// This **MUST** remain the last block in the function!
// Otherwise, any work done after this point won't be saved!
// Due to the way most of these counters are reset, that would probably be
// OK, but it's much simpler to reason about if we can assume that the
// entire function has been performed after a save.
if (Engine.Counters.autoSaveCounter <= 0) {
if (Settings.AutosaveInterval == null) {
Settings.AutosaveInterval = 60;
}
if (Settings.AutosaveInterval === 0) {
warnAutosaveDisabled();
Engine.Counters.autoSaveCounter = 60 * 5; // Let's check back in a bit
} else {
Engine.Counters.autoSaveCounter = Settings.AutosaveInterval * 5;
saveObject.saveGame(!Settings.SuppressSavedGameToast);
}
}
2021-09-05 01:09:30 +02:00
},
load: async function (saveData) {
startExploits();
2021-12-20 19:57:07 +01:00
setupUncaughtPromiseHandler();
// Source files must be initialized early because save-game translation in
// loadGame() needs them sometimes.
initSourceFiles();
2021-09-05 01:09:30 +02:00
// Load game from save or create new game
2022-07-19 19:09:56 +02:00
if (await loadGame(saveData)) {
FormatsNeedToChange.emit();
initBitNodeMultipliers();
2021-09-05 01:09:30 +02:00
if (Player.hasWseAccount) {
initSymbolToStockMap();
}
2022-03-19 19:15:31 +01:00
// Apply penalty for entropy accumulation
2022-03-29 20:09:17 +02:00
Player.applyEntropy(Player.entropy);
2022-03-19 19:15:31 +01:00
2021-09-05 01:09:30 +02:00
// Calculate the number of cycles have elapsed while offline
Engine._lastUpdate = new Date().getTime();
const lastUpdate = Player.lastUpdate;
let timeOffline = Engine._lastUpdate - lastUpdate;
if (timeOffline < 0) {
if (Math.abs(timeOffline) > thresholdOfTimeDiffForShowingWarningAboutSystemClock) {
const timeDiff = timeOffline;
setTimeout(() => {
showWarningAboutSystemClock(timeDiff);
}, 250);
}
timeOffline = 0;
}
2023-02-21 15:44:18 +01:00
const numCyclesOffline = Math.floor(timeOffline / CONSTANTS.MilliPerCycle);
2021-09-05 01:09:30 +02:00
2022-09-25 12:37:20 +02:00
// Calculate the number of chances for a contract the player had whilst offline
const contractChancesWhileOffline = Math.floor(timeOffline / (1000 * 60 * 10));
2021-11-12 01:56:09 +01:00
// Generate coding contracts
let numContracts = 0;
if (contractChancesWhileOffline > 100) {
numContracts += Math.floor(contractChancesWhileOffline * 0.25);
}
if (contractChancesWhileOffline > 0 && contractChancesWhileOffline <= 100) {
for (let i = 0; i < contractChancesWhileOffline; ++i) {
if (Math.random() <= 0.25) {
numContracts++;
}
}
}
for (let i = 0; i < numContracts; i++) {
generateRandomContract();
}
2021-11-12 01:56:09 +01:00
2021-09-05 01:09:30 +02:00
let offlineReputation = 0;
const offlineHackingIncome =
(Player.moneySourceA.hacking / Player.playtimeSinceLastAug) * timeOffline * CONSTANTS.OfflineHackingIncome;
Player.gainMoney(offlineHackingIncome, "hacking");
2021-09-05 01:09:30 +02:00
// Process offline progress
2022-07-26 21:09:11 +02:00
2022-08-28 02:56:12 +02:00
loadAllRunningScripts(); // This also takes care of offline production for those scripts
2022-07-26 21:09:11 +02:00
2022-07-07 08:00:23 +02:00
if (Player.currentWork !== null) {
Player.focus = true;
2022-07-14 23:43:08 +02:00
Player.processWork(numCyclesOffline);
} else if (Player.bitNodeN !== 2) {
2021-09-05 01:09:30 +02:00
for (let i = 0; i < Player.factions.length; i++) {
const facName = Player.factions[i];
if (!Object.hasOwn(Factions, facName)) continue;
2021-09-05 01:09:30 +02:00
const faction = Factions[facName];
if (!faction.isMember) continue;
// No rep for special factions.
const info = faction.getInfo();
if (!info.offersWork()) continue;
// No rep for gangs.
if (Player.getGangName() === facName) continue;
const hRep = getHackingWorkRepGain(Player, faction.favor);
const sRep = getFactionSecurityWorkRepGain(Player, faction.favor);
const fRep = getFactionFieldWorkRepGain(Player, faction.favor);
2021-09-05 01:09:30 +02:00
// can be infinite, doesn't matter.
2021-09-09 05:47:34 +02:00
const reputationRate = Math.max(hRep, sRep, fRep) / Player.factions.length;
2021-09-05 01:09:30 +02:00
const rep = reputationRate * numCyclesOffline;
faction.playerReputation += rep;
offlineReputation += rep;
2017-07-22 00:54:55 +02:00
}
2021-09-05 01:09:30 +02:00
}
// Hacknet Nodes offline progress
2022-09-06 15:07:12 +02:00
const offlineProductionFromHacknetNodes = processHacknetEarnings(numCyclesOffline);
const hacknetProdInfo = hasHacknetServers() ? (
2021-10-01 19:08:37 +02:00
<>
<Hashes hashes={offlineProductionFromHacknetNodes} /> hashes
</>
2021-09-05 01:09:30 +02:00
) : (
<Money money={offlineProductionFromHacknetNodes} />
);
// Passive faction rep gain offline
processPassiveFactionRepGain(numCyclesOffline);
// Stock Market offline progress
if (Player.hasWseAccount) {
processStockPrices(numCyclesOffline);
}
// Gang progress for BitNode 2
if (Player.gang) Player.gang.process(numCyclesOffline);
2021-09-05 01:09:30 +02:00
// Corporation offline progress
if (Player.corporation) Player.corporation.storeCycles(numCyclesOffline);
2021-09-05 01:09:30 +02:00
// Bladeburner offline progress
if (Player.bladeburner) Player.bladeburner.storeCycles(numCyclesOffline);
2021-09-05 01:09:30 +02:00
Go.storeCycles(numCyclesOffline);
2022-09-18 03:09:15 +02:00
staneksGift.process(numCyclesOffline);
2021-10-08 09:16:51 +02:00
2021-09-05 01:09:30 +02:00
// Sleeves offline progress
Player.sleeves.forEach((sleeve) => sleeve.process(numCyclesOffline));
2021-09-05 01:09:30 +02:00
// Update total playtime
2023-02-21 15:44:18 +01:00
const time = numCyclesOffline * CONSTANTS.MilliPerCycle;
Player.totalPlaytime ??= 0;
Player.playtimeSinceLastAug ??= 0;
Player.playtimeSinceLastBitnode ??= 0;
2021-09-05 01:09:30 +02:00
Player.totalPlaytime += time;
Player.playtimeSinceLastAug += time;
Player.playtimeSinceLastBitnode += time;
Player.lastUpdate = Engine._lastUpdate;
Engine.start(); // Run main game loop and Scripts loop
const timeOfflineString = convertTimeMsToTimeElapsedString(time);
2021-10-01 07:00:50 +02:00
setTimeout(
() =>
AlertEvents.emit(
<>
2022-03-21 02:26:10 +01:00
<Typography>Offline for {timeOfflineString}. While you were offline:</Typography>
2022-03-06 03:04:43 +01:00
<ul>
2022-03-21 02:26:10 +01:00
<li>
<Typography>
Your scripts generated <Money money={offlineHackingIncome} />
</Typography>
</li>
<li>
<Typography>Your Hacknet Nodes generated {hacknetProdInfo}</Typography>
</li>
<li>
<Typography>
You gained <Reputation reputation={offlineReputation} /> reputation divided amongst your factions
</Typography>
</li>
2022-03-06 03:04:43 +01:00
</ul>
2021-10-01 07:00:50 +02:00
</>,
),
250,
2021-09-05 01:09:30 +02:00
);
} else {
// No save found, start new game
FormatsNeedToChange.emit();
initBitNodeMultipliers();
2021-09-05 01:09:30 +02:00
Engine.start(); // Run main game loop and Scripts loop
Player.init();
initForeignServers(Player.getHomeComputer());
Player.reapplyAllAugmentations();
2021-09-05 01:09:30 +02:00
// Start interactive tutorial
iTutorialStart();
}
},
2021-09-18 01:43:08 +02:00
start: function () {
// Get time difference
const _thisUpdate = new Date().getTime();
let diff = _thisUpdate - Engine._lastUpdate;
if (diff < 0) {
if (Math.abs(diff) > thresholdOfTimeDiffForShowingWarningAboutSystemClock) {
showWarningAboutSystemClock(diff);
}
diff = 0;
Engine._lastUpdate = _thisUpdate;
Player.lastUpdate = _thisUpdate;
}
2023-02-21 15:44:18 +01:00
const offset = diff % CONSTANTS.MilliPerCycle;
2021-09-17 01:42:55 +02:00
2021-09-18 01:43:08 +02:00
// Divide this by cycle time to determine how many cycles have elapsed since last update
2023-02-21 15:44:18 +01:00
diff = Math.floor(diff / CONSTANTS.MilliPerCycle);
2021-09-18 01:43:08 +02:00
if (diff > 0) {
// Update the game engine by the calculated number of cycles
Engine._lastUpdate = _thisUpdate - offset;
Player.lastUpdate = _thisUpdate - offset;
Engine.updateGame(diff);
if (GameCycleEvents.hasSubscibers()) {
ReactDOM.unstable_batchedUpdates(() => {
GameCycleEvents.emit();
});
}
2021-09-16 23:30:47 +02:00
}
window.setTimeout(Engine.start, CONSTANTS.MilliPerCycle - offset);
2021-09-05 01:09:30 +02:00
},
};
/** Shows a toast warning that lets the player know that auto-saves are disabled, with an button to re-enable them. */
function warnAutosaveDisabled(): void {
// If the player has suppressed those warnings let's just exit right away.
if (Settings.SuppressAutosaveDisabledWarnings) return;
// We don't want this warning to show up on certain pages.
// When in recovery or importing we want to keep autosave disabled.
if (Router.hidingMessages()) return;
const warningToast = (
<>
Auto-saves are <strong>disabled</strong>!
<Button
sx={{ ml: 1 }}
color="warning"
size="small"
onClick={() => {
// We reset the value to a default
Settings.AutosaveInterval = 60;
}}
>
Enable
</Button>
</>
);
SnackbarEvents.emit(warningToast, ToastVariant.WARNING, 5000);
}
2021-09-21 22:49:38 +02:00
export { Engine };