bitburner-src/src/engine.tsx

441 lines
15 KiB
TypeScript
Raw Normal View History

/**
2021-09-18 01:43:08 +02:00
* Game engine. Handles the main game loop.
*/
2021-09-25 20:42:57 +02:00
import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions";
import { Augmentations } from "./Augmentation/Augmentations";
2021-09-18 01:43:08 +02:00
import { initAugmentations } from "./Augmentation/AugmentationHelpers";
import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
2021-09-05 01:09:30 +02:00
import { initBitNodeMultipliers } from "./BitNode/BitNode";
import { Bladeburner } from "./Bladeburner/Bladeburner";
import { generateRandomContract } from "./CodingContractGenerator";
import { initCompanies } from "./Company/Companies";
import { Corporation } from "./Corporation/Corporation";
import { CONSTANTS } from "./Constants";
import { Factions, initFactions } 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 { SetupTextEditor } from "./ScriptEditor/ui/ScriptEditorRoot";
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, initMessages } 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 { ThemeEvents } from "./ui/React/Theme";
import { updateSourceFileFlags } from "./SourceFile/SourceFileFlags";
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 { Sleeve } from "./PersonObjects/Sleeve/Sleeve";
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 React from "react";
2021-12-20 19:57:07 +01:00
import { setupUncaughtPromiseHandler } from "./UncaughtPromiseHandler";
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;
};
decrementAllCounters: (numCycles?: number) => void;
checkCounters: () => void;
load: (saveString: string) => void;
start: () => void;
} = {
2021-09-05 01:09:30 +02:00
// Time variables (milliseconds unix epoch time)
_lastUpdate: new Date().getTime(),
2021-09-05 01:09:30 +02:00
updateGame: function (numCycles = 1) {
2021-09-18 01:43:08 +02:00
const time = numCycles * CONSTANTS._idleSpeed;
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;
2021-09-18 01:43:08 +02:00
Terminal.process(Router, Player, numCycles);
Player.process(Router, numCycles);
2021-09-05 01:09:30 +02:00
// Update stock prices
if (Player.hasWseAccount) {
processStockPrices(numCycles);
}
2021-09-05 01:09:30 +02:00
// Gang, if applicable
2021-09-25 00:29:25 +02:00
if (Player.inGang() && Player.gang !== null) {
2021-09-05 01:09:30 +02:00
Player.gang.process(numCycles, Player);
}
2021-09-25 23:21:50 +02:00
// Staneks gift
staneksGift.process(Player, numCycles);
2021-09-05 01:09:30 +02:00
// Corporation
if (Player.corporation instanceof Corporation) {
// Stores cycles in a "buffer". Processed separately using Engine Counters
Player.corporation.storeCycles(numCycles);
}
2021-09-05 01:09:30 +02:00
if (Player.bladeburner instanceof Bladeburner) {
Player.bladeburner.storeCycles(numCycles);
}
2021-09-05 01:09:30 +02:00
// Sleeves
for (let i = 0; i < Player.sleeves.length; ++i) {
if (Player.sleeves[i] instanceof Sleeve) {
const expForOtherSleeves = Player.sleeves[i].process(Player, numCycles);
2021-09-05 01:09:30 +02:00
// This sleeve earns experience for other sleeves
if (expForOtherSleeves == null) {
continue;
}
2021-09-05 01:09:30 +02:00
for (let j = 0; j < Player.sleeves.length; ++j) {
if (j === i) {
continue;
}
2021-09-09 05:47:34 +02:00
Player.sleeves[j].gainExperience(Player, expForOtherSleeves, numCycles, true);
}
2021-09-05 01:09:30 +02:00
}
}
2021-09-05 01:09:30 +02:00
// Counters
Engine.decrementAllCounters(numCycles);
Engine.checkCounters();
2021-09-05 01:09:30 +02:00
// Update the running time of all active scripts
updateOnlineScriptTimes(numCycles);
// Hacknet Nodes
2021-09-09 09:17:01 +02:00
processHacknetEarnings(Player, numCycles);
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, // Processes certain mechanics (Corporation, Bladeburner)
contractGeneration: 3000, // Generate Coding Contracts
},
decrementAllCounters: function (numCycles = 1) {
2021-09-25 00:29:25 +02:00
for (const counterName in Engine.Counters) {
const counter = Engine.Counters[counterName];
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 () {
if (Engine.Counters.autoSaveCounter <= 0) {
if (Settings.AutosaveInterval == null) {
Settings.AutosaveInterval = 60;
}
if (Settings.AutosaveInterval === 0) {
Engine.Counters.autoSaveCounter = Infinity;
} else {
Engine.Counters.autoSaveCounter = Settings.AutosaveInterval * 5;
2021-09-21 22:49:38 +02:00
saveObject.saveGame();
2021-09-05 01:09:30 +02:00
}
}
2021-09-11 23:23:56 +02:00
if (Engine.Counters.checkFactionInvitations <= 0) {
const invitedFactions = Player.checkForFactionInvitations();
if (invitedFactions.length > 0) {
const randFaction = invitedFactions[Math.floor(Math.random() * invitedFactions.length)];
inviteToFaction(randFaction);
}
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 (Augmentations[AugmentationNames.TheRedPill].owned) {
Engine.Counters.messages = 4500; // 15 minutes for Red pill message
} else {
Engine.Counters.messages = 150;
}
}
2021-10-06 07:59:41 +02:00
if (Player.corporation instanceof Corporation) {
Player.corporation.process(Player);
}
2021-09-05 01:09:30 +02:00
if (Engine.Counters.mechanicProcess <= 0) {
if (Player.bladeburner instanceof Bladeburner) {
try {
2021-09-18 01:43:08 +02:00
Player.bladeburner.process(Router, Player);
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;
}
},
load: function (saveString) {
startExploits();
2021-12-20 19:57:07 +01:00
setupUncaughtPromiseHandler();
2021-09-05 01:09:30 +02:00
// Load game from save or create new game
if (loadGame(saveString)) {
ThemeEvents.emit();
2021-09-05 01:09:30 +02:00
initBitNodeMultipliers(Player);
updateSourceFileFlags(Player);
initAugmentations(); // Also calls Player.reapplyAllAugmentations()
Player.reapplyAllSourceFiles();
if (Player.hasWseAccount) {
initSymbolToStockMap();
}
// Calculate the number of cycles have elapsed while offline
Engine._lastUpdate = new Date().getTime();
const lastUpdate = Player.lastUpdate;
const timeOffline = Engine._lastUpdate - lastUpdate;
2021-09-18 01:43:08 +02:00
const numCyclesOffline = Math.floor(timeOffline / CONSTANTS._idleSpeed);
2021-09-05 01:09:30 +02:00
2021-11-12 01:56:09 +01:00
// Generate coding contracts
2021-11-12 02:31:26 +01:00
// let numContracts = 0;
// if (numCyclesOffline < 3000 * 100) {
// // if we have less than 100 rolls, just roll them exactly.
// for (let i = 0; i < numCyclesOffline / 3000; i++) {
// if (Math.random() < 0.25) numContracts++;
// }
// } else {
// // just average it.
// numContracts = (numCyclesOffline / 3000) * 0.25;
// }
// console.log(`${numCyclesOffline} ${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;
2021-09-09 05:47:34 +02:00
const offlineHackingIncome = (Player.moneySourceA.hacking / Player.playtimeSinceLastAug) * timeOffline * 0.75;
Player.gainMoney(offlineHackingIncome, "hacking");
2021-09-05 01:09:30 +02:00
// Process offline progress
2022-01-05 01:09:34 +01:00
loadAllRunningScripts(Player); // This also takes care of offline production for those scripts
2021-09-05 01:09:30 +02:00
if (Player.isWorking) {
Player.focus = true;
if (Player.workType == CONSTANTS.WorkTypeFaction) {
Player.workForFaction(numCyclesOffline);
} else if (Player.workType == CONSTANTS.WorkTypeCreateProgram) {
Player.createProgramWork(numCyclesOffline);
} else if (Player.workType == CONSTANTS.WorkTypeStudyClass) {
Player.takeClass(numCyclesOffline);
} else if (Player.workType == CONSTANTS.WorkTypeCrime) {
Player.commitCrime(numCyclesOffline);
} else if (Player.workType == CONSTANTS.WorkTypeCompanyPartTime) {
Player.workPartTime(numCyclesOffline);
} else {
Player.work(numCyclesOffline);
}
2021-09-05 01:09:30 +02:00
} else {
for (let i = 0; i < Player.factions.length; i++) {
const facName = Player.factions[i];
if (!Factions.hasOwnProperty(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);
const sRep = getFactionSecurityWorkRepGain(Player, faction);
const fRep = getFactionFieldWorkRepGain(Player, faction);
// 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
2021-09-25 07:26:03 +02:00
const offlineProductionFromHacknetNodes = processHacknetEarnings(Player, numCyclesOffline);
2021-09-09 09:17:01 +02:00
const hacknetProdInfo = hasHacknetServers(Player) ? (
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
2021-09-25 00:29:25 +02:00
const gang = Player.gang;
if (Player.inGang() && gang !== null) {
gang.process(numCyclesOffline, Player);
2021-09-05 01:09:30 +02:00
}
// Corporation offline progress
if (Player.corporation instanceof Corporation) {
Player.corporation.storeCycles(numCyclesOffline);
}
// Bladeburner offline progress
if (Player.bladeburner instanceof Bladeburner) {
Player.bladeburner.storeCycles(numCyclesOffline);
}
2021-10-08 09:16:51 +02:00
staneksGift.process(Player, numCyclesOffline);
2021-09-05 01:09:30 +02:00
// Sleeves offline progress
for (let i = 0; i < Player.sleeves.length; ++i) {
if (Player.sleeves[i] instanceof Sleeve) {
2021-09-09 05:47:34 +02:00
const expForOtherSleeves = Player.sleeves[i].process(Player, numCyclesOffline);
2021-09-05 01:09:30 +02:00
// This sleeve earns experience for other sleeves
if (expForOtherSleeves == null) {
continue;
}
for (let j = 0; j < Player.sleeves.length; ++j) {
if (j === i) {
continue;
}
2021-09-09 05:47:34 +02:00
Player.sleeves[j].gainExperience(Player, expForOtherSleeves, numCyclesOffline, true);
2021-09-05 01:09:30 +02:00
}
}
}
// Update total playtime
2021-09-25 07:26:03 +02:00
const time = numCyclesOffline * CONSTANTS._idleSpeed;
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;
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(
<>
Offline for {timeOfflineString}. While you were offline, your scripts generated{" "}
<Money money={offlineHackingIncome} />, your Hacknet Nodes generated {hacknetProdInfo} and you gained{" "}
2021-10-01 19:08:37 +02:00
<Reputation reputation={offlineReputation} /> reputation divided amongst your factions.
2021-10-01 07:00:50 +02:00
</>,
),
250,
2021-09-05 01:09:30 +02:00
);
} else {
// No save found, start new game
initBitNodeMultipliers(Player);
Engine.start(); // Run main game loop and Scripts loop
Player.init();
initForeignServers(Player.getHomeComputer());
initCompanies();
initFactions();
initAugmentations();
initMessages();
updateSourceFileFlags(Player);
// Start interactive tutorial
iTutorialStart();
}
2021-10-05 03:06:55 +02:00
SetupTextEditor();
2021-09-05 01:09:30 +02:00
},
2021-09-18 01:43:08 +02:00
start: function () {
// Get time difference
const _thisUpdate = new Date().getTime();
let diff = _thisUpdate - Engine._lastUpdate;
const offset = diff % CONSTANTS._idleSpeed;
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
diff = Math.floor(diff / CONSTANTS._idleSpeed);
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);
2021-09-16 23:30:47 +02:00
}
2021-09-18 01:43:08 +02:00
window.requestAnimationFrame(Engine.start);
2021-09-05 01:09:30 +02:00
},
};
2021-09-21 22:49:38 +02:00
export { Engine };