mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-12-02 04:23:48 +01:00
2f95d21503
There are a bunch of React components that update at the same rate that the game engine processes cycles. Rather than have each place that does so start its own timer to update that often, add a new react hook that triggers an update shortly after the engine completes a cycle.
474 lines
16 KiB
TypeScript
474 lines
16 KiB
TypeScript
import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions";
|
|
import { AugmentationName, ToastVariant } from "@enums";
|
|
import { initBitNodeMultipliers } from "./BitNode/BitNode";
|
|
import { initSourceFiles } from "./SourceFile/SourceFiles";
|
|
import { generateRandomContract } from "./CodingContractGenerator";
|
|
import { CONSTANTS } from "./Constants";
|
|
import { Factions } from "./Faction/Factions";
|
|
import { staneksGift } from "./CotMG/Helper";
|
|
import { processPassiveFactionRepGain, inviteToFaction } from "./Faction/FactionHelpers";
|
|
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
|
|
|
|
import {
|
|
getHackingWorkRepGain,
|
|
getFactionSecurityWorkRepGain,
|
|
getFactionFieldWorkRepGain,
|
|
} from "./PersonObjects/formulas/reputation";
|
|
import { hasHacknetServers, processHacknetEarnings } from "./Hacknet/HacknetHelpers";
|
|
import { iTutorialStart } from "./InteractiveTutorial";
|
|
import { checkForMessagesToSend } from "./Message/MessageHelpers";
|
|
import { loadAllRunningScripts, updateOnlineScriptTimes } from "./NetscriptWorker";
|
|
import { Player } from "@player";
|
|
import { saveObject, loadGame } from "./SaveObject";
|
|
import { initForeignServers } from "./Server/AllServers";
|
|
import { Settings } from "./Settings/Settings";
|
|
import { FormatsNeedToChange } from "./ui/formatNumber";
|
|
import { initSymbolToStockMap, processStockPrices } from "./StockMarket/StockMarket";
|
|
import { Terminal } from "./Terminal";
|
|
|
|
import { Money } from "./ui/React/Money";
|
|
import { Hashes } from "./ui/React/Hashes";
|
|
import { Reputation } from "./ui/React/Reputation";
|
|
|
|
import { AlertEvents } from "./ui/React/AlertManager";
|
|
import { exceptionAlert } from "./utils/helpers/exceptionAlert";
|
|
|
|
import { startExploits } from "./Exploits/loops";
|
|
import { calculateAchievements } from "./Achievements/Achievements";
|
|
|
|
import React from "react";
|
|
import ReactDOM from "react-dom";
|
|
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. */
|
|
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;
|
|
};
|
|
decrementAllCounters: (numCycles?: number) => void;
|
|
checkCounters: () => void;
|
|
load: (saveData: SaveData) => Promise<void>;
|
|
start: () => void;
|
|
} = {
|
|
// Time variables (milliseconds unix epoch time)
|
|
_lastUpdate: new Date().getTime(),
|
|
updateGame: function (numCycles = 1) {
|
|
const time = numCycles * CONSTANTS.MilliPerCycle;
|
|
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;
|
|
|
|
Terminal.process(numCycles);
|
|
|
|
Player.processWork(numCycles);
|
|
|
|
// Update stock prices
|
|
if (Player.hasWseAccount) {
|
|
processStockPrices(numCycles);
|
|
}
|
|
|
|
// Gang
|
|
if (Player.gang) Player.gang.process(numCycles);
|
|
|
|
// Staneks gift
|
|
staneksGift.process(numCycles);
|
|
|
|
// Corporation
|
|
if (Player.corporation) {
|
|
Player.corporation.storeCycles(numCycles);
|
|
Player.corporation.process();
|
|
}
|
|
|
|
// Bladeburner
|
|
if (Player.bladeburner) Player.bladeburner.storeCycles(numCycles);
|
|
|
|
// Sleeves
|
|
Player.sleeves.forEach((sleeve) => sleeve.process(numCycles));
|
|
|
|
// Update the running time of all active scripts
|
|
updateOnlineScriptTimes(numCycles);
|
|
|
|
// Hacknet Nodes
|
|
processHacknetEarnings(numCycles);
|
|
|
|
// 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();
|
|
},
|
|
|
|
/**
|
|
* 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
|
|
contractGeneration: 3000, // Generate Coding Contracts
|
|
achievementsCounter: 60, // Check if we have new achievements
|
|
},
|
|
|
|
decrementAllCounters: function (numCycles = 1) {
|
|
for (const [counterName, counter] of Object.entries(Engine.Counters)) {
|
|
if (counter === undefined) throw new Error("counter should not be undefined");
|
|
Engine.Counters[counterName] = counter - numCycles;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Checks if any counters are 0. If they are, executes whatever
|
|
* is necessary and then resets the counter
|
|
*/
|
|
checkCounters: function () {
|
|
if (Engine.Counters.checkFactionInvitations <= 0) {
|
|
const invitedFactions = Player.checkForFactionInvitations();
|
|
for (const invitedFaction of invitedFactions) {
|
|
inviteToFaction(invitedFaction);
|
|
}
|
|
Engine.Counters.checkFactionInvitations = 100;
|
|
}
|
|
|
|
if (Engine.Counters.passiveFactionGrowth <= 0) {
|
|
const adjustedCycles = Math.floor(5 - Engine.Counters.passiveFactionGrowth);
|
|
processPassiveFactionRepGain(adjustedCycles);
|
|
Engine.Counters.passiveFactionGrowth = 5;
|
|
}
|
|
|
|
if (Engine.Counters.messages <= 0) {
|
|
checkForMessagesToSend();
|
|
if (Player.hasAugmentation(AugmentationName.TheRedPill)) {
|
|
Engine.Counters.messages = 4500; // 15 minutes for Red pill message
|
|
} else {
|
|
Engine.Counters.messages = 150;
|
|
}
|
|
}
|
|
if (Engine.Counters.mechanicProcess <= 0) {
|
|
if (Player.bladeburner) {
|
|
try {
|
|
Player.bladeburner.process();
|
|
} catch (e) {
|
|
exceptionAlert("Exception caught in Bladeburner.process(): " + e);
|
|
}
|
|
}
|
|
Engine.Counters.mechanicProcess = 5;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
},
|
|
|
|
load: async function (saveData) {
|
|
startExploits();
|
|
setupUncaughtPromiseHandler();
|
|
// Source files must be initialized early because save-game translation in
|
|
// loadGame() needs them sometimes.
|
|
initSourceFiles();
|
|
// Load game from save or create new game
|
|
|
|
if (await loadGame(saveData)) {
|
|
FormatsNeedToChange.emit();
|
|
initBitNodeMultipliers();
|
|
if (Player.hasWseAccount) {
|
|
initSymbolToStockMap();
|
|
}
|
|
|
|
// Apply penalty for entropy accumulation
|
|
Player.applyEntropy(Player.entropy);
|
|
|
|
// 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;
|
|
}
|
|
const numCyclesOffline = Math.floor(timeOffline / CONSTANTS.MilliPerCycle);
|
|
|
|
// Calculate the number of chances for a contract the player had whilst offline
|
|
const contractChancesWhileOffline = Math.floor(timeOffline / (1000 * 60 * 10));
|
|
|
|
// 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();
|
|
}
|
|
|
|
let offlineReputation = 0;
|
|
const offlineHackingIncome =
|
|
(Player.moneySourceA.hacking / Player.playtimeSinceLastAug) * timeOffline * CONSTANTS.OfflineHackingIncome;
|
|
Player.gainMoney(offlineHackingIncome, "hacking");
|
|
// Process offline progress
|
|
|
|
loadAllRunningScripts(); // This also takes care of offline production for those scripts
|
|
|
|
if (Player.currentWork !== null) {
|
|
Player.focus = true;
|
|
Player.processWork(numCyclesOffline);
|
|
} else if (Player.bitNodeN !== 2) {
|
|
for (let i = 0; i < Player.factions.length; i++) {
|
|
const facName = Player.factions[i];
|
|
if (!Object.hasOwn(Factions, facName)) continue;
|
|
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);
|
|
// can be infinite, doesn't matter.
|
|
const reputationRate = Math.max(hRep, sRep, fRep) / Player.factions.length;
|
|
|
|
const rep = reputationRate * numCyclesOffline;
|
|
faction.playerReputation += rep;
|
|
offlineReputation += rep;
|
|
}
|
|
}
|
|
|
|
// Hacknet Nodes offline progress
|
|
const offlineProductionFromHacknetNodes = processHacknetEarnings(numCyclesOffline);
|
|
const hacknetProdInfo = hasHacknetServers() ? (
|
|
<>
|
|
<Hashes hashes={offlineProductionFromHacknetNodes} /> hashes
|
|
</>
|
|
) : (
|
|
<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);
|
|
|
|
// Corporation offline progress
|
|
if (Player.corporation) Player.corporation.storeCycles(numCyclesOffline);
|
|
|
|
// Bladeburner offline progress
|
|
if (Player.bladeburner) Player.bladeburner.storeCycles(numCyclesOffline);
|
|
|
|
Go.storeCycles(numCyclesOffline);
|
|
|
|
staneksGift.process(numCyclesOffline);
|
|
|
|
// Sleeves offline progress
|
|
Player.sleeves.forEach((sleeve) => sleeve.process(numCyclesOffline));
|
|
|
|
// Update total playtime
|
|
const time = numCyclesOffline * CONSTANTS.MilliPerCycle;
|
|
Player.totalPlaytime ??= 0;
|
|
Player.playtimeSinceLastAug ??= 0;
|
|
Player.playtimeSinceLastBitnode ??= 0;
|
|
|
|
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);
|
|
setTimeout(
|
|
() =>
|
|
AlertEvents.emit(
|
|
<>
|
|
<Typography>Offline for {timeOfflineString}. While you were offline:</Typography>
|
|
<ul>
|
|
<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>
|
|
</ul>
|
|
</>,
|
|
),
|
|
250,
|
|
);
|
|
} else {
|
|
// No save found, start new game
|
|
FormatsNeedToChange.emit();
|
|
initBitNodeMultipliers();
|
|
Engine.start(); // Run main game loop and Scripts loop
|
|
Player.init();
|
|
initForeignServers(Player.getHomeComputer());
|
|
Player.reapplyAllAugmentations();
|
|
|
|
// Start interactive tutorial
|
|
iTutorialStart();
|
|
}
|
|
},
|
|
|
|
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;
|
|
}
|
|
const offset = diff % CONSTANTS.MilliPerCycle;
|
|
|
|
// Divide this by cycle time to determine how many cycles have elapsed since last update
|
|
diff = Math.floor(diff / CONSTANTS.MilliPerCycle);
|
|
|
|
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();
|
|
});
|
|
}
|
|
}
|
|
window.setTimeout(Engine.start, CONSTANTS.MilliPerCycle - offset);
|
|
},
|
|
};
|
|
|
|
/** 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);
|
|
}
|
|
|
|
export { Engine };
|