diff --git a/css/buttons.scss b/css/buttons.scss index b854356d6..a22844a87 100644 --- a/css/buttons.scss +++ b/css/buttons.scss @@ -38,6 +38,7 @@ button { } .a-link-button-inactive, +.std-button-disabled, .std-button:disabled { text-decoration: none; background-color: #333; diff --git a/css/characteroverview.scss b/css/characteroverview.scss index 0fabea40f..2e0279be2 100644 --- a/css/characteroverview.scss +++ b/css/characteroverview.scss @@ -2,7 +2,7 @@ @import "theme"; /** - * Styling for the Character Overview Panel (top-right) + * Styling for the Character Overview Panel (top-right panel) */ #character-overview-wrapper { diff --git a/css/hacknetnodes.scss b/css/hacknetnodes.scss new file mode 100644 index 000000000..cf21c6a72 --- /dev/null +++ b/css/hacknetnodes.scss @@ -0,0 +1,75 @@ +@import "mixins"; +@import "theme"; + +/** + * Styling for the Hacknet Nodes UI Page + */ + +#hacknet-nodes-container { + position: fixed; + padding: 10px; +} + +.hacknet-general-info { + margin: 10px; + width: 70vw; +} + +#hacknet-nodes-container li { + float: left; + overflow: hidden; + white-space: nowrap; + + &.hacknet-node { + $boxShadowArgs: inset 0 0 8px rgba(0, 0, 0, 0.1), 0 0 16px rgba(0, 0, 0, 0.1); + @include boxShadow($boxShadowArgs); + + margin: 6px; + padding: 7px; + width: 35vw; + border: 2px solid var(--my-highlight-color); + } +} + +#hacknet-nodes-list { + list-style: none; + width: 82vw; +} + +#hacknet-nodes-money { + margin: 10px; + float: left; +} + +#hacknet-nodes-money-multipliers-div { + display: inline-block; + width: 70vw; +} + +#hacknet-nodes-multipliers { + float: right; +} + +#hacknet-nodes-purchase-button { + display: inline-block; +} + +.hacknet-node-container { + display: inline-table; + + .row { + display: table-row; + height: 30px; + + p { + display: table-cell; + } + } + + .upgradable-info { + display: inline-block; + margin: 0 4px; /* Don't want the vertical margin/padding, just left & right */ + padding: 0 4px; + width: $defaultFontSize * 4; + } +} diff --git a/css/menupages.scss b/css/menupages.scss index e67847fa8..0c8e55033 100644 --- a/css/menupages.scss +++ b/css/menupages.scss @@ -138,81 +138,6 @@ } } -/* Hacknet Nodes */ -#hacknet-nodes-container { - position: fixed; - padding: 10px; -} - -#hacknet-nodes-text, -#hacknet-nodes-container li { - margin: 10px; - padding: 10px; -} - -#hacknet-nodes-container li { - float: left; - overflow: hidden; - white-space: nowrap; - - &.hacknet-node { - $boxShadowArgs: inset 0 0 8px rgba(0, 0, 0, 0.1), 0 0 16px rgba(0, 0, 0, 0.1); - @include boxShadow($boxShadowArgs); - - margin: 6px; - padding: 7px; - width: 35vw; - border: 2px solid var(--my-highlight-color); - } -} - -#hacknet-nodes-list { - list-style: none; - width: 82vw; -} - -#hacknet-nodes-money { - margin: 10px; - float: left; -} - -#hacknet-nodes-money-multipliers-div { - display: inline-block; - width: 70vw; -} - -#hacknet-nodes-multipliers { - float: right; -} - -#hacknet-nodes-purchase-button { - display: inline-block; -} - -.hacknet-node-container { - display: inline-table; - - .row { - display: table-row; - height: 30px; - - p { - display: table-cell; - } - } - - .upgradable-info { - display: inline-block; - margin: 0 4px; /* Don't want the vertical margin/padding, just left & right */ - padding: 0 4px; - width: $defaultFontSize * 4; - } -} - -.menu-page-text { - width: 70vw; -} - /* World */ #world-container { position: fixed; diff --git a/src/Augmentation/AugmentationHelpers.js b/src/Augmentation/AugmentationHelpers.js index c6c99debc..9be26fea2 100644 --- a/src/Augmentation/AugmentationHelpers.js +++ b/src/Augmentation/AugmentationHelpers.js @@ -2069,7 +2069,7 @@ function installAugmentations(cbScript=null) { } var runningScriptObj = new RunningScript(script, []); //No args runningScriptObj.threads = 1; //Only 1 thread - home.runningScripts.push(runningScriptObj); + home.runScript(runningScriptObj, Player); addWorkerScript(runningScriptObj, home); } } diff --git a/src/BitNode/BitNode.ts b/src/BitNode/BitNode.ts index 8888e8e7c..2d3568d70 100644 --- a/src/BitNode/BitNode.ts +++ b/src/BitNode/BitNode.ts @@ -173,7 +173,10 @@ export function initBitNodes() { "Level 3: Ability to use limit/stop orders in other BitNodes

" + "This Source-File also increases your hacking growth multipliers by: " + "
Level 1: 12%
Level 2: 18%
Level 3: 21%"); - BitNodes["BitNode9"] = new BitNode(9, "Do Androids Dream?", "COMING SOON"); + BitNodes["BitNode9"] = new BitNode(9, "Hacktocracy", "Hacknet Unleashed", + "When Fulcrum Technologies released their open-source Linux distro Chapeau, it quickly " + + "became the OS of choice for the underground hacking community. Chapeau became especially notorious for " + + "powering the Hacknet, "); BitNodes["BitNode10"] = new BitNode(10, "Digital Carbon", "Your body is not who you are", "In 2084, VitaLife unveiled to the world the Persona Core, a technology that allowed people " + "to digitize their consciousness. Their consciousness could then be transferred into Synthoids " + @@ -245,9 +248,9 @@ export function initBitNodeMultipliers(p: IPlayer) { } switch (p.bitNodeN) { - case 1: //Source Genesis (every multiplier is 1) + case 1: // Source Genesis (every multiplier is 1) break; - case 2: //Rise of the Underworld + case 2: // Rise of the Underworld BitNodeMultipliers.HackingLevelMultiplier = 0.8; BitNodeMultipliers.ServerGrowthRate = 0.8; BitNodeMultipliers.ServerMaxMoney = 0.2; @@ -257,7 +260,7 @@ export function initBitNodeMultipliers(p: IPlayer) { BitNodeMultipliers.FactionWorkRepGain = 0.5; BitNodeMultipliers.FactionPassiveRepGain = 0; break; - case 3: //Corporatocracy + case 3: // Corporatocracy BitNodeMultipliers.HackingLevelMultiplier = 0.8; BitNodeMultipliers.RepToDonateToFaction = 0.5; BitNodeMultipliers.AugmentationRepCost = 3; @@ -272,7 +275,7 @@ export function initBitNodeMultipliers(p: IPlayer) { BitNodeMultipliers.HomeComputerRamCost = 1.5; BitNodeMultipliers.PurchasedServerCost = 2; break; - case 4: //The Singularity + case 4: // The Singularity BitNodeMultipliers.ServerMaxMoney = 0.15; BitNodeMultipliers.ServerStartingMoney = 0.75; BitNodeMultipliers.ScriptHackMoney = 0.2; @@ -286,7 +289,7 @@ export function initBitNodeMultipliers(p: IPlayer) { BitNodeMultipliers.CrimeExpGain = 0.5; BitNodeMultipliers.FactionWorkRepGain = 0.75; break; - case 5: //Artificial intelligence + case 5: // Artificial intelligence BitNodeMultipliers.ServerMaxMoney = 2; BitNodeMultipliers.ServerStartingSecurity = 2; BitNodeMultipliers.ServerStartingMoney = 0.5; @@ -299,7 +302,7 @@ export function initBitNodeMultipliers(p: IPlayer) { BitNodeMultipliers.HackExpGain = 0.5; BitNodeMultipliers.CorporationValuation = 0.5; break; - case 6: //Bladeburner + case 6: // Bladeburner BitNodeMultipliers.HackingLevelMultiplier = 0.35; BitNodeMultipliers.ServerMaxMoney = 0.4; BitNodeMultipliers.ServerStartingMoney = 0.5; @@ -314,7 +317,7 @@ export function initBitNodeMultipliers(p: IPlayer) { BitNodeMultipliers.HackExpGain = 0.25; BitNodeMultipliers.DaedalusAugsRequirement = 1.166; // Results in 35 Augs needed break; - case 7: //Bladeburner 2079 + case 7: // Bladeburner 2079 BitNodeMultipliers.BladeburnerRank = 0.6; BitNodeMultipliers.BladeburnerSkillCost = 2; BitNodeMultipliers.AugmentationMoneyCost = 3; @@ -334,7 +337,7 @@ export function initBitNodeMultipliers(p: IPlayer) { BitNodeMultipliers.FourSigmaMarketDataApiCost = 2; BitNodeMultipliers.DaedalusAugsRequirement = 1.166; // Results in 35 Augs needed break; - case 8: //Ghost of Wall Street + case 8: // Ghost of Wall Street BitNodeMultipliers.ScriptHackMoney = 0; BitNodeMultipliers.ManualHackMoney = 0; BitNodeMultipliers.CompanyWorkMoney = 0; @@ -345,6 +348,19 @@ export function initBitNodeMultipliers(p: IPlayer) { BitNodeMultipliers.CorporationValuation = 0; BitNodeMultipliers.CodingContractMoney = 0; break; + case 9: // Hacktocracy + BitNodeMultipliers.HackingLevelMultiplier = 0.3; + BitNodeMultipliers.StrengthLevelMultiplier = 0.45; + BitNodeMultipliers.DefenseLevelMultiplier = 0.45; + BitNodeMultipliers.DexterityLevelMultiplier = 0.45; + BitNodeMultipliers.AgilityLevelMultiplier = 0.45; + BitNodeMultipliers.CharismaLevelMultiplier = 0.45; + BitNodeMultipliers.PurchasedServerLimit = 0; + BitNodeMultipliers.HomeComputerRamCost = 3; + BitNodeMultipliers.CrimeMoney = 0.5; + BitNodeMultipliers.ScriptHackMoney = 0.1; + BitNodeMultipliers.HackExpGain = 0.1; + break; case 10: // Digital Carbon BitNodeMultipliers.HackingLevelMultiplier = 0.2; BitNodeMultipliers.StrengthLevelMultiplier = 0.4; diff --git a/src/BitNode/BitNodeMultipliers.ts b/src/BitNode/BitNodeMultipliers.ts index e6d66062c..0a9334d59 100644 --- a/src/BitNode/BitNodeMultipliers.ts +++ b/src/BitNode/BitNodeMultipliers.ts @@ -120,7 +120,8 @@ interface IBitNodeMultipliers { HackingLevelMultiplier: number; /** - * Influences how much money each Hacknet node can generate. + * Influences how much money is produced by Hacknet Nodes. + * Influeces the hash rate of Hacknet Servers (unlocked in BitNode-9) */ HacknetNodeMoney: number; diff --git a/src/Constants.ts b/src/Constants.ts index a34343427..cb56cb3d2 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -1,3 +1,8 @@ +/** + * Generic Game Constants + * + * Constants for specific mechanics or features will NOT be here. + */ import {IMap} from "./types"; export let CONSTANTS: IMap = { @@ -17,24 +22,9 @@ export let CONSTANTS: IMap = { /* Base costs */ BaseCostFor1GBOfRamHome: 32000, BaseCostFor1GBOfRamServer: 55000, //1 GB of RAM - BaseCostFor1GBOfRamHacknetNode: 30000, TravelCost: 200e3, - BaseCostForHacknetNode: 1000, - BaseCostForHacknetNodeCore: 500000, - - /* Hacknet Node constants */ - HacknetNodeMoneyGainPerLevel: 1.6, - HacknetNodePurchaseNextMult: 1.85, //Multiplier when purchasing an additional hacknet node - HacknetNodeUpgradeLevelMult: 1.04, //Multiplier for cost when upgrading level - HacknetNodeUpgradeRamMult: 1.28, //Multiplier for cost when upgrading RAM - HacknetNodeUpgradeCoreMult: 1.48, //Multiplier for cost when buying another core - - HacknetNodeMaxLevel: 200, - HacknetNodeMaxRam: 64, - HacknetNodeMaxCores: 16, - /* Faction and Company favor */ BaseFavorToDonate: 150, DonateMoneyToRepDivisor: 1e6, diff --git a/src/Hacknet/HacknetHelpers.jsx b/src/Hacknet/HacknetHelpers.jsx new file mode 100644 index 000000000..38246f75a --- /dev/null +++ b/src/Hacknet/HacknetHelpers.jsx @@ -0,0 +1,431 @@ +import { HacknetNode, + BaseCostForHacknetNode, + HacknetNodePurchaseNextMult, + HacknetNodeMaxLevel, + HacknetNodeMaxRam, + HacknetNodeMaxCores } from "./HacknetNode"; +import { HacknetServer, + BaseCostForHacknetServer, + HacknetServerPurchaseMult, + HacknetServerMaxLevel, + HacknetServerMaxRam, + HacknetServerMaxCores, + HacknetServerMaxCache, + MaxNumberHacknetServers } from "./HacknetServer"; +import { HashManager } from "./HashManager"; +import { HashUpgrades } from "./HashUpgrades"; + +import { generateRandomContractOnHome } from "../CodingContractGenerator"; +import { iTutorialSteps, iTutorialNextStep, + ITutorial} from "../InteractiveTutorial"; +import { Player } from "../Player"; +import { AddToAllServers } from "../Server/AllServers"; +import { GetServerByHostname } from "../Server/ServerHelpers"; +import { SourceFileFlags } from "../SourceFile/SourceFileFlags"; +import { Page, routing } from "../ui/navigationTracking"; + +import {getElementById} from "../../utils/uiHelpers/getElementById"; + +import React from "react"; +import ReactDOM from "react-dom"; +import { HacknetRoot } from "./ui/Root"; + +let hacknetNodesDiv; +function hacknetNodesInit() { + hacknetNodesDiv = document.getElementById("hacknet-nodes-container"); +} + +document.addEventListener("DOMContentLoaded", hacknetNodesInit, false); + +// Returns a boolean indicating whether the player has Hacknet Servers +// (the upgraded form of Hacknet Nodes) +export function hasHacknetServers() { + return (Player.bitNodeN === 9 || SourceFileFlags[9] > 0); +} + +export function purchaseHacknet() { + /* INTERACTIVE TUTORIAL */ + if (ITutorial.isRunning) { + if (ITutorial.currStep === iTutorialSteps.HacknetNodesIntroduction) { + iTutorialNextStep(); + } else { + return; + } + } + + /* END INTERACTIVE TUTORIAL */ + + if (hasHacknetServers()) { + const cost = getCostOfNextHacknetServer(); + if (isNaN(cost)) { + throw new Error(`Calculated cost of purchasing HacknetServer is NaN`) + } + + if (!Player.canAfford(cost)) { return -1; } + + // Auto generate a hostname for this Server + const numOwned = Player.hacknetNodes.length; + const name = `hacknet-node-${numOwned}`; + const server = new HacknetServer({ + adminRights: true, + hostname: name, + player: Player, + }); + + Player.loseMoney(cost); + Player.hacknetNodes.push(server); + + // Configure the HacknetServer to actually act as a Server + AddToAllServers(server); + const homeComputer = Player.getHomeComputer(); + homeComputer.serversOnNetwork.push(server.ip); + server.serversOnNetwork.push(homeComputer.ip); + + return numOwned; + } else { + const cost = getCostOfNextHacknetNode(); + if (isNaN(cost)) { + throw new Error(`Calculated cost of purchasing HacknetNode is NaN`); + } + + if (!Player.canAfford(cost)) { return -1; } + + // Auto generate a name for the Node + const numOwned = Player.hacknetNodes.length; + const name = "hacknet-node-" + numOwned; + const node = new HacknetNode(name); + node.updateMoneyGainRate(Player); + + Player.loseMoney(cost); + Player.hacknetNodes.push(node); + + return numOwned; + } +} + +export function hasMaxNumberHacknetServers() { + return hasHacknetServers() && Player.hacknetNodes.length >= MaxNumberHacknetServers; +} + +export function getCostOfNextHacknetNode() { + // Cost increases exponentially based on how many you own + const numOwned = Player.hacknetNodes.length; + const mult = HacknetNodePurchaseNextMult; + + return BaseCostForHacknetNode * Math.pow(mult, numOwned) * Player.hacknet_node_purchase_cost_mult; +} + +export function getCostOfNextHacknetServer() { + const numOwned = Player.hacknetNodes.length; + const mult = HacknetServerPurchaseMult; + + if (numOwned > MaxNumberHacknetServers) { return Infinity; } + + return BaseCostForHacknetServer * Math.pow(mult, numOwned) * Player.hacknet_node_purchase_cost_mult; +} + +//Calculate the maximum number of times the Player can afford to upgrade a Hacknet Node +export function getMaxNumberLevelUpgrades(nodeObj, maxLevel) { + if (maxLevel == null) { + throw new Error(`getMaxNumberLevelUpgrades() called without maxLevel arg`); + } + + if (Player.money.lt(nodeObj.calculateLevelUpgradeCost(1, Player))) { + return 0; + } + + let min = 1; + let max = maxLevel - 1; + let levelsToMax = maxLevel - nodeObj.level; + if (Player.money.gt(nodeObj.calculateLevelUpgradeCost(levelsToMax, Player))) { + return levelsToMax; + } + + while (min <= max) { + var curr = (min + max) / 2 | 0; + if (curr !== maxLevel && + Player.money.gt(nodeObj.calculateLevelUpgradeCost(curr, Player)) && + Player.money.lt(nodeObj.calculateLevelUpgradeCost(curr + 1, Player))) { + return Math.min(levelsToMax, curr); + } else if (Player.money.lt(nodeObj.calculateLevelUpgradeCost(curr, Player))) { + max = curr - 1; + } else if (Player.money.gt(nodeObj.calculateLevelUpgradeCost(curr, Player))) { + min = curr + 1; + } else { + return Math.min(levelsToMax, curr); + } + } + return 0; +} + +export function getMaxNumberRamUpgrades(nodeObj, maxLevel) { + if (maxLevel == null) { + throw new Error(`getMaxNumberRamUpgrades() called without maxLevel arg`); + } + + if (Player.money.lt(nodeObj.calculateRamUpgradeCost(1, Player))) { + return 0; + } + + let levelsToMax; + if (nodeObj instanceof HacknetServer) { + levelsToMax = Math.round(Math.log2(maxLevel / nodeObj.maxRam)); + } else { + levelsToMax = Math.round(Math.log2(maxLevel / nodeObj.ram)); + } + if (Player.money.gt(nodeObj.calculateRamUpgradeCost(levelsToMax, Player))) { + return levelsToMax; + } + + //We'll just loop until we find the max + for (let i = levelsToMax-1; i >= 0; --i) { + if (Player.money.gt(nodeObj.calculateRamUpgradeCost(i, Player))) { + return i; + } + } + return 0; +} + +export function getMaxNumberCoreUpgrades(nodeObj, maxLevel) { + if (maxLevel == null) { + throw new Error(`getMaxNumberCoreUpgrades() called without maxLevel arg`); + } + + if (Player.money.lt(nodeObj.calculateCoreUpgradeCost(1, Player))) { + return 0; + } + + let min = 1; + let max = maxLevel - 1; + const levelsToMax = maxLevel - nodeObj.cores; + if (Player.money.gt(nodeObj.calculateCoreUpgradeCost(levelsToMax, Player))) { + return levelsToMax; + } + + //Use a binary search to find the max possible number of upgrades + while (min <= max) { + let curr = (min + max) / 2 | 0; + if (curr != maxLevel && + Player.money.gt(nodeObj.calculateCoreUpgradeCost(curr, Player)) && + Player.money.lt(nodeObj.calculateCoreUpgradeCost(curr + 1, Player))) { + return Math.min(levelsToMax, curr); + } else if (Player.money.lt(nodeObj.calculateCoreUpgradeCost(curr, Player))) { + max = curr - 1; + } else if (Player.money.gt(nodeObj.calculateCoreUpgradeCost(curr, Player))) { + min = curr + 1; + } else { + return Math.min(levelsToMax, curr); + } + } + + return 0; +} + +export function getMaxNumberCacheUpgrades(nodeObj, maxLevel) { + if (maxLevel == null) { + throw new Error(`getMaxNumberCacheUpgrades() called without maxLevel arg`); + } + + if (!Player.canAfford(nodeObj.calculateCacheUpgradeCost(1))) { + return 0; + } + + let min = 1; + let max = maxLevel - 1; + const levelsToMax = maxLevel - nodeObj.cache; + if (Player.canAfford(nodeObj.calculateCacheUpgradeCost(levelsToMax))) { + return levelsToMax; + } + + // Use a binary search to find the max possible number of upgrades + while (min <= max) { + let curr = (min + max) / 2 | 0; + if (curr != maxLevel && + Player.canAfford(nodeObj.calculateCacheUpgradeCost(curr)) && + !Player.canAfford(nodeObj.calculateCacheUpgradeCost(curr + 1))) { + return Math.min(levelsToMax, curr); + } else if (!Player.canAfford(nodeObj.calculateCacheUpgradeCost(curr))) { + max = curr -1 ; + } else if (Player.canAfford(nodeObj.calculateCacheUpgradeCost(curr))) { + min = curr + 1; + } else { + return Math.min(levelsToMax, curr); + } + } + + return 0; +} + +// Initial construction of Hacknet Nodes UI +export function renderHacknetNodesUI() { + if (!routing.isOn(Page.HacknetNodes)) { return; } + + ReactDOM.render(, hacknetNodesDiv); +} + +export function clearHacknetNodesUI() { + if (hacknetNodesDiv instanceof HTMLElement) { + ReactDOM.unmountComponentAtNode(hacknetNodesDiv); + } + + hacknetNodesDiv.style.display = "none"; +} + +export function processHacknetEarnings(numCycles) { + // Determine if player has Hacknet Nodes or Hacknet Servers, then + // call the appropriate function + if (Player.hacknetNodes.length === 0) { return 0; } + if (hasHacknetServers()) { + return processAllHacknetServerEarnings(); + } else if (Player.hacknetNodes[0] instanceof HacknetNode) { + return processAllHacknetNodeEarnings(); + } else { + return 0; + } +} + +function processAllHacknetNodeEarnings(numCycles) { + let total = 0; + for (let i = 0; i < Player.hacknetNodes.length; ++i) { + total += processSingleHacknetNodeEarnings(numCycles, Player.hacknetNodes[i]); + } + + return total; +} + +function processSingleHacknetNodeEarnings(numCycles, nodeObj) { + const totalEarnings = nodeObj.process(numCycles); + Player.gainMoney(totalEarnings); + Player.recordMoneySource(totalEarnings, "hacknetnode"); + + return totalEarnings; +} + +function processAllHacknetServerEarnings(numCycles) { + if (!(Player.hashManager instanceof HashManager)) { + throw new Error(`Player does not have a HashManager (should be in 'hashManager' prop)`) + } + + let hashes = 0; + for (let i = 0; i < Player.hacknetNodes.length; ++i) { + hashes += Player.hacknetNodes[i].process(numCycles); + } + + Player.hashManager.storeHashes(hashes); + + return hashes; +} + +export function getHacknetNode(name) { + for (var i = 0; i < Player.hacknetNodes.length; ++i) { + if (Player.hacknetNodes[i].name == name) { + return Player.hacknetNodes[i]; + } + } + + return null; +} + +export function purchaseHashUpgrade(upgName, upgTarget) { + if (!(Player.hashManager instanceof HashManager)) { + console.error(`Player does not have a HashManager`); + return false; + } + + // HashManager handles the transaction. This just needs to actually implement + // the effects of the upgrade + if (Player.hashManager.upgrade(upgName)) { + const upg = HashUpgrades[upgName]; + + switch (upgName) { + case "Sell for Money": { + Player.gainMoney(upg.value); + break; + } + case "Sell for Corporation Funds": { + // This will throw if player doesn't have a corporation + try { + Player.corporation.funds = Player.corporation.funds.plus(upg.value); + } catch(e) { + Player.hashManager.refundUpgrade(upgName); + return false; + } + break; + } + case "Reduce Minimum Security": { + try { + const target = GetServerByHostname(upgTarget); + if (target == null) { + console.error(`Invalid target specified in purchaseHashUpgrade(): ${upgTarget}`); + return false; + } + + target.changeMinimumSecurity(upg.value, true); + } catch(e) { + Player.hashManager.refundUpgrade(upgName); + return false; + } + break; + } + case "Increase Maximum Money": { + try { + const target = GetServerByHostname(upgTarget); + if (target == null) { + console.error(`Invalid target specified in purchaseHashUpgrade(): ${upgTarget}`); + return false; + } + + target.changeMaximumMoney(upg.value, true); + } catch(e) { + Player.hashManager.refundUpgrade(upgName); + return false; + } + break; + } + case "Improve Studying": { + // Multiplier handled by HashManager + break; + } + case "Improve Gym Training": { + // Multiplier handled by HashManager + break; + } + case "Exchange for Corporation Research": { + // This will throw if player doesn't have a corporation + try { + for (const division of Player.corporation.divisions) { + division.sciResearch.qty += upg.value; + } + } catch(e) { + Player.hashManager.refundUpgrade(upgName); + return false; + } + break; + } + case "Exchange for Bladeburner Rank": { + // This will throw if player doesn't have a corporation + try { + for (const division of Player.corporation.divisions) { + division.sciResearch.qty += upg.value; + } + } catch(e) { + Player.hashManager.refundUpgrade(upgName); + return false; + } + break; + } + case "Generate Coding Contract": { + generateRandomContractOnHome(); + break; + } + default: + console.warn(`Unrecognized upgrade name ${upgName}. Upgrade has no effect`) + return false; + } + + console.log("Hash Upgrade successfully purchased"); + return true; + } + + return false; +} diff --git a/src/Hacknet/HacknetNode.ts b/src/Hacknet/HacknetNode.ts new file mode 100644 index 000000000..51b9b2025 --- /dev/null +++ b/src/Hacknet/HacknetNode.ts @@ -0,0 +1,285 @@ +/** + * Hacknet Node Class + * + * Hacknet Nodes are specialized machines that passively earn the player money over time. + * They can be upgraded to increase their production + */ +import { IHacknetNode } from "./IHacknetNode"; + +import { CONSTANTS } from "../Constants"; + +import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; +import { IPlayer } from "../PersonObjects/IPlayer"; + +import { dialogBoxCreate } from "../../utils/DialogBox"; +import { Generic_fromJSON, + Generic_toJSON, + Reviver } from "../../utils/JSONReviver"; + +// Constants for Hacknet Node production +export const HacknetNodeMoneyGainPerLevel: number = 1.6; // Base production per level + +// Constants for Hacknet Node purchase/upgrade costs +export const BaseCostForHacknetNode: number = 1000; +export const BaseCostFor1GBOfRamHacknetNode: number = 30e3; +export const BaseCostForHacknetNodeCore: number = 500e3; +export const HacknetNodePurchaseNextMult: number = 1.85; // Multiplier when purchasing an additional hacknet node +export const HacknetNodeUpgradeLevelMult: number = 1.04; // Multiplier for cost when upgrading level +export const HacknetNodeUpgradeRamMult: number = 1.28; // Multiplier for cost when upgrading RAM +export const HacknetNodeUpgradeCoreMult: number = 1.48; // Multiplier for cost when buying another core + +// Constants for max upgrade levels for Hacknet Nodes +export const HacknetNodeMaxLevel: number = 200; +export const HacknetNodeMaxRam: number = 64; +export const HacknetNodeMaxCores: number = 16; + +export class HacknetNode implements IHacknetNode { + /** + * Initiatizes a HacknetNode object from a JSON save state. + */ + static fromJSON(value: any): HacknetNode { + return Generic_fromJSON(HacknetNode, value.data); + } + + // Node's number of cores + cores: number = 1; + + // Node's Level + level: number = 1; + + // Node's production per second + moneyGainRatePerSecond: number = 0; + + // Identifier for Node. Includes the full "name" (hacknet-node-N) + name: string; + + // How long this Node has existed, in seconds + onlineTimeSeconds: number = 0; + + // Node's RAM (GB) + ram: number = 1; + + // Total money earned by this Node + totalMoneyGenerated: number = 0; + + constructor(name: string="") { + this.name = name; + } + + // Get the cost to upgrade this Node's number of cores + calculateCoreUpgradeCost(levels: number=1, p: IPlayer): number { + const sanitizedLevels = Math.round(levels); + if (isNaN(sanitizedLevels) || sanitizedLevels < 1) { + return 0; + } + + if (this.cores >= HacknetNodeMaxCores) { + return Infinity; + } + + const coreBaseCost = BaseCostForHacknetNodeCore; + const mult = HacknetNodeUpgradeCoreMult; + let totalCost = 0; + let currentCores = this.cores; + for (let i = 0; i < sanitizedLevels; ++i) { + totalCost += (coreBaseCost * Math.pow(mult, currentCores-1)); + ++currentCores; + } + + totalCost *= p.hacknet_node_core_cost_mult; + + return totalCost; + } + + // Get the cost to upgrade this Node's level + calculateLevelUpgradeCost(levels: number=1, p: IPlayer): number { + const sanitizedLevels = Math.round(levels); + if (isNaN(sanitizedLevels) || sanitizedLevels < 1) { + return 0; + } + + if (this.level >= HacknetNodeMaxLevel) { + return Infinity; + } + + const mult = HacknetNodeUpgradeLevelMult; + let totalMultiplier = 0; + let currLevel = this.level; + for (let i = 0; i < sanitizedLevels; ++i) { + totalMultiplier += Math.pow(mult, currLevel); + ++currLevel; + } + + return BaseCostForHacknetNode / 2 * totalMultiplier * p.hacknet_node_level_cost_mult; + } + + // Get the cost to upgrade this Node's RAM + calculateRamUpgradeCost(levels: number=1, p: IPlayer): number { + const sanitizedLevels = Math.round(levels); + if (isNaN(sanitizedLevels) || sanitizedLevels < 1) { + return 0; + } + + if (this.ram >= HacknetNodeMaxRam) { + return Infinity; + } + + let totalCost = 0; + let numUpgrades = Math.round(Math.log2(this.ram)); + let currentRam = this.ram; + + for (let i = 0; i < sanitizedLevels; ++i) { + let baseCost = currentRam * BaseCostFor1GBOfRamHacknetNode; + let mult = Math.pow(HacknetNodeUpgradeRamMult, numUpgrades); + + totalCost += (baseCost * mult); + + currentRam *= 2; + ++numUpgrades; + } + + totalCost *= p.hacknet_node_ram_cost_mult; + + return totalCost; + } + + // Process this Hacknet Node in the game loop. + // Returns the amount of money generated + process(numCycles: number=1): number { + const seconds = numCycles * CONSTANTS.MilliPerCycle / 1000; + let gain = this.moneyGainRatePerSecond * seconds; + if (isNaN(gain)) { + console.error(`Hacknet Node ${this.name} calculated earnings of NaN`); + gain = 0; + } + + this.totalMoneyGenerated += gain; + this.onlineTimeSeconds += seconds; + + return gain; + } + + // Upgrade this Node's number of cores, if possible + // Returns a boolean indicating whether new cores were successfully bought + purchaseCoreUpgrade(levels: number=1, p: IPlayer): boolean { + const sanitizedLevels = Math.round(levels); + const cost = this.calculateCoreUpgradeCost(sanitizedLevels, p); + if (isNaN(cost) || sanitizedLevels < 0) { + return false; + } + + // Fail if we're already at max + if (this.cores >= HacknetNodeMaxCores) { + return false; + } + + // If the specified number of upgrades would exceed the max Cores, calculate + // the max possible number of upgrades and use that + if (this.cores + sanitizedLevels > HacknetNodeMaxCores) { + const diff = Math.max(0, HacknetNodeMaxCores - this.cores); + return this.purchaseCoreUpgrade(diff, p); + } + + if (!p.canAfford(cost)) { + return false; + } + + p.loseMoney(cost); + this.cores = Math.round(this.cores + sanitizedLevels); // Just in case of floating point imprecision + this.updateMoneyGainRate(p); + + return true; + } + + // Upgrade this Node's level, if possible + // Returns a boolean indicating whether the level was successfully updated + purchaseLevelUpgrade(levels: number=1, p: IPlayer): boolean { + const sanitizedLevels = Math.round(levels); + const cost = this.calculateLevelUpgradeCost(sanitizedLevels, p); + if (isNaN(cost) || sanitizedLevels < 0) { + return false; + } + + // If we're at max level, return false + if (this.level >= HacknetNodeMaxLevel) { + return false; + } + + // If the number of specified upgrades would exceed the max level, calculate + // the maximum number of upgrades and use that + if (this.level + sanitizedLevels > HacknetNodeMaxLevel) { + var diff = Math.max(0, HacknetNodeMaxLevel - this.level); + return this.purchaseLevelUpgrade(diff, p); + } + + if (!p.canAfford(cost)) { + return false; + } + + p.loseMoney(cost); + this.level = Math.round(this.level + sanitizedLevels); // Just in case of floating point imprecision + this.updateMoneyGainRate(p); + + return true; + } + + // Upgrade this Node's RAM, if possible + // Returns a boolean indicating whether the RAM was successfully upgraded + purchaseRamUpgrade(levels: number=1, p: IPlayer): boolean { + const sanitizedLevels = Math.round(levels); + const cost = this.calculateRamUpgradeCost(sanitizedLevels, p); + if (isNaN(cost) || sanitizedLevels < 0) { + return false; + } + + // Fail if we're already at max + if (this.ram >= HacknetNodeMaxRam) { + return false; + } + + // If the number of specified upgrades would exceed the max RAM, calculate the + // max possible number of upgrades and use that + if (this.ram * Math.pow(2, sanitizedLevels) > HacknetNodeMaxRam) { + var diff = Math.max(0, Math.log2(Math.round(HacknetNodeMaxRam / this.ram))); + return this.purchaseRamUpgrade(diff, p); + } + + if (!p.canAfford(cost)) { + return false; + } + + p.loseMoney(cost); + for (let i = 0; i < sanitizedLevels; ++i) { + this.ram *= 2; // Ram is always doubled + } + this.ram = Math.round(this.ram); // Handle any floating point precision issues + this.updateMoneyGainRate(p); + + return true; + } + + // Re-calculate this Node's production and update the moneyGainRatePerSecond prop + updateMoneyGainRate(p: IPlayer): void { + //How much extra $/s is gained per level + var gainPerLevel = HacknetNodeMoneyGainPerLevel; + + this.moneyGainRatePerSecond = (this.level * gainPerLevel) * + Math.pow(1.035, this.ram - 1) * + ((this.cores + 5) / 6) * + p.hacknet_node_money_mult * + BitNodeMultipliers.HacknetNodeMoney; + if (isNaN(this.moneyGainRatePerSecond)) { + this.moneyGainRatePerSecond = 0; + dialogBoxCreate("Error in calculating Hacknet Node production. Please report to game developer", false); + } + } + + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): any { + return Generic_toJSON("HacknetNode", this); + } +} + +Reviver.constructors.HacknetNode = HacknetNode; diff --git a/src/Hacknet/HacknetServer.ts b/src/Hacknet/HacknetServer.ts new file mode 100644 index 000000000..15022cd1b --- /dev/null +++ b/src/Hacknet/HacknetServer.ts @@ -0,0 +1,348 @@ +/** + * Hacknet Servers - Reworked Hacknet Node mechanic for BitNode-9 + */ +import { CONSTANTS } from "../Constants"; + +import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; +import { IHacknetNode } from "../Hacknet/IHacknetNode"; +import { IPlayer } from "../PersonObjects/IPlayer"; +import { BaseServer } from "../Server/BaseServer"; +import { RunningScript } from "../Script/RunningScript"; + +import { dialogBoxCreate } from "../../utils/DialogBox"; +import { createRandomIp } from "../../utils/IPAddress"; + +import { Generic_fromJSON, + Generic_toJSON, + Reviver } from "../../utils/JSONReviver"; + +// Constants for Hacknet Server stats/production +export const HacknetServerHashesPerLevel: number = 0.001; + +// Constants for Hacknet Server purchase/upgrade costs +export const BaseCostForHacknetServer: number = 10e3; +export const BaseCostFor1GBHacknetServerRam: number = 200e3; +export const BaseCostForHacknetServerCore: number = 1e6; +export const BaseCostForHacknetServerCache: number = 10e6; + +export const HacknetServerPurchaseMult: number = 3.2; // Multiplier for puchasing an additional Hacknet Server +export const HacknetServerUpgradeLevelMult: number = 1.1; // Multiplier for cost when upgrading level +export const HacknetServerUpgradeRamMult: number = 1.4; // Multiplier for cost when upgrading RAM +export const HacknetServerUpgradeCoreMult: number = 1.55; // Multiplier for cost when buying another core +export const HacknetServerUpgradeCacheMult: number = 1.85; // Multiplier for cost when upgrading cache + +export const MaxNumberHacknetServers: number = 25; // Max number of Hacknet Servers you can own + +// Constants for max upgrade levels for Hacknet Server +export const HacknetServerMaxLevel: number = 300; +export const HacknetServerMaxRam: number = 8192; +export const HacknetServerMaxCores: number = 128; +export const HacknetServerMaxCache: number = 15; // Max cache level. So max capacity is 2 ^ 12 + +interface IConstructorParams { + adminRights?: boolean; + hostname: string; + ip?: string; + isConnectedTo?: boolean; + maxRam?: number; + organizationName?: string; + player?: IPlayer; +} + +export class HacknetServer extends BaseServer implements IHacknetNode { + // Initializes a HacknetServer Object from a JSON save state + static fromJSON(value: any): HacknetServer { + return Generic_fromJSON(HacknetServer, value.data); + } + + // Cache level. Affects hash Capacity + cache: number = 1; + + // Number of cores. Improves hash production + cores: number = 1; + + // Number of hashes that can be stored by this Hacknet Server + hashCapacity: number = 0; + + // Hashes produced per second + hashRate: number = 0; + + // Similar to Node level. Improves hash production + level: number = 1; + + // How long this HacknetServer has existed, in seconds + onlineTimeSeconds: number = 0; + + // Total number of hashes earned by this + totalHashesGenerated: number = 0; + + constructor(params: IConstructorParams={ hostname: "", ip: createRandomIp() }) { + super(params); + + this.maxRam = 1; + this.updateHashCapacity(); + + if (params.player) { + this.updateHashRate(params.player); + } + } + + calculateCacheUpgradeCost(levels: number): number { + const sanitizedLevels = Math.round(levels); + if (isNaN(sanitizedLevels) || sanitizedLevels < 1) { + return 0; + } + + if (this.cache >= HacknetServerMaxCache) { + return Infinity; + } + + const mult = HacknetServerUpgradeCacheMult; + let totalCost = 0; + let currentCache = this.cache; + for (let i = 0; i < sanitizedLevels; ++i) { + totalCost += Math.pow(mult, currentCache - 1); + ++currentCache; + } + totalCost *= BaseCostForHacknetServerCache; + + return totalCost; + } + + calculateCoreUpgradeCost(levels: number, p: IPlayer): number { + const sanitizedLevels = Math.round(levels); + if (isNaN(sanitizedLevels) || sanitizedLevels < 1) { + return 0; + } + + if (this.cores >= HacknetServerMaxCores) { + return Infinity; + } + + const mult = HacknetServerUpgradeCoreMult; + let totalCost = 0; + let currentCores = this.cores; + for (let i = 0; i < sanitizedLevels; ++i) { + totalCost += Math.pow(mult, currentCores-1); + ++currentCores; + } + totalCost *= BaseCostForHacknetServerCore; + totalCost *= p.hacknet_node_core_cost_mult; + + return totalCost; + } + + calculateLevelUpgradeCost(levels: number, p: IPlayer): number { + const sanitizedLevels = Math.round(levels); + if (isNaN(sanitizedLevels) || sanitizedLevels < 1) { + return 0; + } + + if (this.level >= HacknetServerMaxLevel) { + return Infinity; + } + + const mult = HacknetServerUpgradeLevelMult; + let totalMultiplier = 0; + let currLevel = this.level; + for (let i = 0; i < sanitizedLevels; ++i) { + totalMultiplier += Math.pow(mult, currLevel); + ++currLevel; + } + + return 10 * BaseCostForHacknetServer * totalMultiplier * p.hacknet_node_level_cost_mult; + } + + calculateRamUpgradeCost(levels: number, p: IPlayer): number { + const sanitizedLevels = Math.round(levels); + if (isNaN(sanitizedLevels) || sanitizedLevels < 1) { + return 0; + } + + if (this.maxRam >= HacknetServerMaxRam) { + return Infinity; + } + + let totalCost = 0; + let numUpgrades = Math.round(Math.log2(this.maxRam)); + let currentRam = this.maxRam; + for (let i = 0; i < sanitizedLevels; ++i) { + let baseCost = currentRam * BaseCostFor1GBHacknetServerRam; + let mult = Math.pow(HacknetServerUpgradeRamMult, numUpgrades); + + totalCost += (baseCost * mult); + + currentRam *= 2; + ++numUpgrades; + } + totalCost *= p.hacknet_node_ram_cost_mult; + + return totalCost; + } + + // Process this Hacknet Server in the game loop. + // Returns the number of hashes generated + process(numCycles: number=1): number { + const seconds = numCycles * CONSTANTS.MilliPerCycle / 1000; + + return this.hashRate * seconds; + } + + // Returns a boolean indicating whether the cache was successfully upgraded + purchaseCacheUpgrade(levels: number, p: IPlayer): boolean { + const sanitizedLevels = Math.round(levels); + const cost = this.calculateCacheUpgradeCost(levels); + if (isNaN(cost) || cost <= 0 || sanitizedLevels <= 0) { + return false; + } + + if (this.cache >= HacknetServerMaxCache) { + return false; + } + + // If the specified number of upgrades would exceed the max, try to purchase + // the maximum possible + if (this.cache + levels > HacknetServerMaxCache) { + const diff = Math.max(0, HacknetServerMaxCache - this.cache); + return this.purchaseCacheUpgrade(diff, p); + } + + if (!p.canAfford(cost)) { + return false; + } + + p.loseMoney(cost); + this.cache = Math.round(this.cache + sanitizedLevels); + this.updateHashCapacity(); + + return true; + } + + // Returns a boolean indicating whether the number of cores was successfully upgraded + purchaseCoreUpgrade(levels: number, p: IPlayer): boolean { + const sanitizedLevels = Math.round(levels); + const cost = this.calculateCoreUpgradeCost(sanitizedLevels, p); + if (isNaN(cost) || cost <= 0 || sanitizedLevels <= 0) { + return false; + } + + if (this.cores >= HacknetServerMaxCores) { + return false; + } + + // If the specified number of upgrades would exceed the max, try to purchase + // the maximum possible + if (this.cores + sanitizedLevels > HacknetServerMaxCores) { + const diff = Math.max(0, HacknetServerMaxCores - this.cores); + return this.purchaseCoreUpgrade(diff, p); + } + + if (!p.canAfford(cost)) { + return false; + } + + p.loseMoney(cost); + this.cores = Math.round(this.cores + sanitizedLevels); + this.updateHashRate(p); + + return true; + } + + // Returns a boolean indicating whether the level was successfully upgraded + purchaseLevelUpgrade(levels: number, p: IPlayer): boolean { + const sanitizedLevels = Math.round(levels); + const cost = this.calculateLevelUpgradeCost(sanitizedLevels, p); + if (isNaN(cost) || cost <= 0 || sanitizedLevels <= 0) { + return false; + } + + if (this.level >= HacknetServerMaxLevel) { + return false; + } + + // If the specified number of upgrades would exceed the max, try to purchase the + // maximum possible + if (this.level + sanitizedLevels > HacknetServerMaxLevel) { + const diff = Math.max(0, HacknetServerMaxLevel - this.level); + return this.purchaseLevelUpgrade(diff, p); + } + + if (!p.canAfford(cost)) { + return false; + } + + p.loseMoney(cost); + this.level = Math.round(this.level + sanitizedLevels); + this.updateHashRate(p); + + return true; + } + + // Returns a boolean indicating whether the RAM was successfully upgraded + purchaseRamUpgrade(levels: number, p: IPlayer): boolean { + const sanitizedLevels = Math.round(levels); + const cost = this.calculateRamUpgradeCost(sanitizedLevels, p); + if(isNaN(cost) || cost <= 0 || sanitizedLevels <= 0) { + return false; + } + + if (this.maxRam >= HacknetServerMaxRam) { + return false; + } + + // If the specified number of upgrades would exceed the max, try to purchase + // just the maximum possible + if (this.maxRam * Math.pow(2, sanitizedLevels) > HacknetServerMaxRam) { + const diff = Math.max(0, Math.log2(Math.round(HacknetServerMaxRam / this.maxRam))); + return this.purchaseRamUpgrade(diff, p); + } + + if (!p.canAfford(cost)) { + return false; + } + + p.loseMoney(cost); + for (let i = 0; i < sanitizedLevels; ++i) { + this.maxRam *= 2; + } + this.maxRam = Math.round(this.maxRam); + + return true; + } + + /** + * Whenever a script is run, we must update this server's hash rate + */ + runScript(script: RunningScript, p?: IPlayer): void { + super.runScript(script); + if (p) { + this.updateHashRate(p); + } + } + + updateHashCapacity(): void { + this.hashCapacity = 16 * Math.pow(2, this.cache); + } + + updateHashRate(p: IPlayer): void { + const baseGain = HacknetServerHashesPerLevel * this.level; + const coreMultiplier = Math.pow(1.1, this.cores - 1); + const ramRatio = (1 - this.ramUsed / this.maxRam); + + const hashRate = baseGain * coreMultiplier * ramRatio; + + this.hashRate = hashRate * p.hacknet_node_money_mult * BitNodeMultipliers.HacknetNodeMoney; + + if (isNaN(this.hashRate)) { + this.hashRate = 0; + dialogBoxCreate(`Error calculating Hacknet Server hash production. This is a bug. Please report to game dev`, false); + } + } + + // Serialize the current object to a JSON save state + toJSON(): any { + return Generic_toJSON("HacknetServer", this); + } +} + +Reviver.constructors.HacknetServer = HacknetServer; diff --git a/src/Hacknet/HashManager.ts b/src/Hacknet/HashManager.ts new file mode 100644 index 000000000..4aa9fa5f8 --- /dev/null +++ b/src/Hacknet/HashManager.ts @@ -0,0 +1,151 @@ +/** + * This is a central class for storing and managing the player's hashes, + * which are generated by Hacknet Servers + * + * It is also used to keep track of what upgrades the player has bought with + * his hashes, and contains method for grabbing the data/multipliers from + * those upgrades + */ +import { HacknetServer } from "./HacknetServer"; +import { HashUpgrades } from "./HashUpgrades"; + +import { IMap } from "../types"; +import { IPlayer } from "../PersonObjects/IPlayer"; +import { Generic_fromJSON, + Generic_toJSON, + Reviver } from "../../utils/JSONReviver"; + +export class HashManager { + // Initiatizes a HashManager object from a JSON save state. + static fromJSON(value: any): HashManager { + return Generic_fromJSON(HashManager, value.data); + } + + // Max number of hashes this can hold. Equal to the sum of capacities of + // all Hacknet Servers + capacity: number = 0; + + // Number of hashes currently in storage + hashes: number = 0; + + // Map of Hash Upgrade Name -> levels in that upgrade + upgrades: IMap = {}; + + constructor() { + for (const name in HashUpgrades) { + this.upgrades[name] = 0; + } + } + + /** + * Generic helper function for getting a multiplier from a HashUpgrade + */ + getMult(upgName: string): number { + const upg = HashUpgrades[upgName]; + const currLevel = this.upgrades[upgName]; + if (upg == null || currLevel == null) { + console.error(`Could not find Hash Study upgrade`); + return 1; + } + + return 1 + ((upg.value * currLevel) / 100); + } + + /** + * One of the Hash upgrades improves studying. This returns that multiplier + */ + getStudyMult(): number { + const upgName = "Improve Studying"; + + return this.getMult(upgName); + } + + /** + * One of the Hash upgrades improves gym training. This returns that multiplier + */ + getTrainingMult(): number { + const upgName = "Improve Gym Training"; + + return this.getMult(upgName); + } + + /** + * Get the cost (in hashes) of an upgrade + */ + getUpgradeCost(upgName: string): number { + const upg = HashUpgrades[upgName]; + const currLevel = this.upgrades[upgName]; + if (upg == null || currLevel == null) { + console.error(`Invalid Upgrade Name given to HashManager.getUpgradeCost(): ${upgName}`); + return Infinity; + } + + return upg.getCost(currLevel); + } + + storeHashes(numHashes: number): void { + this.hashes += numHashes; + this.hashes = Math.min(this.hashes, this.capacity); + } + + /** + * Reverts an upgrade and refunds the hashes used to buy it + */ + refundUpgrade(upgName: string): void { + const upg = HashUpgrades[upgName]; + const currLevel = this.upgrades[upgName]; + if (upg == null || currLevel == null || currLevel === 0) { + console.error(`Invalid Upgrade Name given to HashManager.upgrade(): ${upgName}`); + return; + } + + // Reduce the level first, so we get the right cost + --this.upgrades[upgName]; + const cost = upg.getCost(currLevel); + this.hashes += cost; + } + + updateCapacity(p: IPlayer): void { + if (p.hacknetNodes.length <= 0) { this.capacity = 0; } + if (!(p.hacknetNodes[0] instanceof HacknetServer)) { this.capacity = 0; } + + let total: number = 0; + for (let i = 0; i < p.hacknetNodes.length; ++i) { + const hacknetServer = (p.hacknetNodes[i]); + total += hacknetServer.hashCapacity; + } + + this.capacity = total; + } + + /** + * Returns boolean indicating whether or not the upgrade was successfully purchased + * Note that this does NOT actually implement the effect + */ + upgrade(upgName: string): boolean { + const upg = HashUpgrades[upgName]; + const currLevel = this.upgrades[upgName]; + if (upg == null || currLevel == null) { + console.error(`Invalid Upgrade Name given to HashManager.upgrade(): ${upgName}`); + return false; + } + + const cost = upg.getCost(currLevel); + + if (this.hashes < cost) { + return false; + } + + this.hashes -= cost; + ++this.upgrades[upgName]; + + return true; + } + + //Serialize the current object to a JSON save state. + toJSON(): any { + return Generic_toJSON("HashManager", this); + } +} + +Reviver.constructors.HashManager = HashManager; diff --git a/src/Hacknet/HashUpgrade.ts b/src/Hacknet/HashUpgrade.ts new file mode 100644 index 000000000..9f76e995b --- /dev/null +++ b/src/Hacknet/HashUpgrade.ts @@ -0,0 +1,48 @@ +/** + * Object representing an upgrade that can be purchased with hashes + */ +export interface IConstructorParams { + costPerLevel: number; + desc: string; + hasTargetServer?: boolean; + name: string; + value: number; +} + +export class HashUpgrade { + /** + * Base cost for this upgrade. Every time the upgrade is purchased, + * its cost increases by this same amount (so its 1x, 2x, 3x, 4x, etc.) + */ + costPerLevel: number = 0; + + /** + * Description of what the upgrade does + */ + desc: string = ""; + + /** + * Boolean indicating that this upgrade's effect affects a single server, + * the "target" server + */ + hasTargetServer: boolean = false; + + // Name of upgrade + name: string = ""; + + // Generic value used to indicate the potency/amount of this upgrade's effect + // The meaning varies between different upgrades + value: number = 0; + + constructor(p: IConstructorParams) { + this.costPerLevel = p.costPerLevel; + this.desc = p.desc; + this.hasTargetServer = p.hasTargetServer ? p.hasTargetServer : false; + this.name = p.name; + this.value = p.value; + } + + getCost(levels: number): number { + return Math.round((levels + 1) * this.costPerLevel); + } +} diff --git a/src/Hacknet/HashUpgrades.ts b/src/Hacknet/HashUpgrades.ts new file mode 100644 index 000000000..0a89e2525 --- /dev/null +++ b/src/Hacknet/HashUpgrades.ts @@ -0,0 +1,18 @@ +/** + * Map of all Hash Upgrades + * Key = Hash name, Value = HashUpgrade object + */ +import { HashUpgrade, + IConstructorParams } from "./HashUpgrade"; +import { HashUpgradesMetadata } from "./data/HashUpgradesMetadata"; +import { IMap } from "../types"; + +export const HashUpgrades: IMap = {}; + +function createHashUpgrade(p: IConstructorParams) { + HashUpgrades[p.name] = new HashUpgrade(p); +} + +for (const metadata of HashUpgradesMetadata) { + createHashUpgrade(metadata); +} diff --git a/src/Hacknet/IHacknetNode.ts b/src/Hacknet/IHacknetNode.ts new file mode 100644 index 000000000..32ec81f53 --- /dev/null +++ b/src/Hacknet/IHacknetNode.ts @@ -0,0 +1,16 @@ +// Interface for a Hacknet Node. Implemented by both a basic Hacknet Node, +// and the upgraded Hacknet Server in BitNode-9 +import { IPlayer } from "../PersonObjects/IPlayer"; + +export interface IHacknetNode { + cores: number; + level: number; + onlineTimeSeconds: number; + + calculateCoreUpgradeCost: (levels: number, p: IPlayer) => number; + calculateLevelUpgradeCost: (levels: number, p: IPlayer) => number; + calculateRamUpgradeCost: (levels: number, p: IPlayer) => number; + purchaseCoreUpgrade: (levels: number, p: IPlayer) => boolean; + purchaseLevelUpgrade: (levels: number, p: IPlayer) => boolean; + purchaseRamUpgrade: (levels: number, p: IPlayer) => boolean; +} diff --git a/src/Hacknet/data/HashUpgradesMetadata.ts b/src/Hacknet/data/HashUpgradesMetadata.ts new file mode 100644 index 000000000..d1904ec35 --- /dev/null +++ b/src/Hacknet/data/HashUpgradesMetadata.ts @@ -0,0 +1,64 @@ +// Metadata used to construct all Hash Upgrades +import { IConstructorParams } from "../HashUpgrade"; + +export const HashUpgradesMetadata: IConstructorParams[] = [ + { + costPerLevel: 2, + desc: "Sell hashes for $1m", + name: "Sell for Money", + value: 1e6, + }, + { + costPerLevel: 100, + desc: "Sell hashes for $1b in Corporation funds", + name: "Sell for Corporation Funds", + value: 1e9, + }, + { + costPerLevel: 100, + desc: "Use hashes to decrease the minimum security of a single server by 5%. " + + "Note that a server's minimum security cannot go below 1.", + hasTargetServer: true, + name: "Reduce Minimum Security", + value: 0.95, + }, + { + costPerLevel: 100, + desc: "Use hashes to increase the maximum amount of money on a single server by 5%", + hasTargetServer: true, + name: "Increase Maximum Money", + value: 1.05, + }, + { + costPerLevel: 100, + desc: "Use hashes to improve the experience earned when studying at a university. " + + "This effect persists until you install Augmentations", + name: "Improve Studying", + value: 20, // Improves studying by value% + }, + { + costPerLevel: 100, + desc: "Use hashes to improve the experience earned when training at the gym. This effect " + + "persists until you install Augmentations", + name: "Improve Gym Training", + value: 20, // Improves training by value% + }, + { + costPerLevel: 250, + desc: "Exchange hashes for 1k Scientific Research in all of your Corporation's Industries", + name: "Exchange for Corporation Research", + value: 1000, + }, + { + costPerLevel: 250, + desc: "Exchange hashes for 100 Bladeburner Rank", + name: "Exchange for Bladeburner Rank", + value: 100, + }, + { + costPerLevel: 200, + desc: "Generate a random Coding Contract on your home computer", + name: "Generate Coding Contract", + value: 1, + }, +] diff --git a/src/Hacknet/ui/GeneralInfo.jsx b/src/Hacknet/ui/GeneralInfo.jsx new file mode 100644 index 000000000..3e4b7cd1d --- /dev/null +++ b/src/Hacknet/ui/GeneralInfo.jsx @@ -0,0 +1,56 @@ +/** + * React Component for the Hacknet Node UI + * + * Displays general information about Hacknet Nodes + */ +import React from "react"; + +import { hasHacknetServers } from "../HacknetHelpers"; + +export class GeneralInfo extends React.Component { + getSecondParagraph() { + if (hasHacknetServers()) { + return `Here, you can purchase a Hacknet Server, an upgraded version of the Hacknet Node. ` + + `Hacknet Servers will perform computations and operations on the network, earning ` + + `you hashes. Hashes can be spent on a variety of different upgrades.`; + } else { + return `Here, you can purchase a Hacknet Node, a specialized machine that can connect ` + + `and contribute its resources to the Hacknet networ. This allows you to take ` + + `a small percentage of profits from hacks performed on the network. Essentially, ` + + `you are renting out your Node's computing power.`; + } + } + + getThirdParagraph() { + if (hasHacknetServers()) { + return `Hacknet Servers can also be used as servers to run scripts. However, running scripts ` + + `on a server will reduce its hash rate (hashes generated per second). A Hacknet Server's hash ` + + `rate will be reduced by the percentage of RAM that is being used by that Server to run ` + + `scripts.` + } else { + return `Each Hacknet Node you purchase will passively earn you money. Each Hacknet Node ` + + `can be upgraded in order to increase its computing power and thereby increase ` + + `the profit you earn from it.`; + } + } + + render() { + return ( +
+

+ The Hacknet is a global, decentralized network of machines. It is used by + hackers all around the world to anonymously share computing power and + perform distributed cyberattacks without the fear of being traced. +

+
+

+ {this.getSecondParagraph()} +

+
+

+ {this.getThirdParagraph()} +

+
+ ) + } +} diff --git a/src/Hacknet/ui/HacknetNode.jsx b/src/Hacknet/ui/HacknetNode.jsx new file mode 100644 index 000000000..6ba40d09a --- /dev/null +++ b/src/Hacknet/ui/HacknetNode.jsx @@ -0,0 +1,153 @@ +/** + * React Component for the Hacknet Node UI. + * This Component displays the panel for a single Hacknet Node + */ +import React from "react"; + +import { HacknetNodeMaxLevel, + HacknetNodeMaxRam, + HacknetNodeMaxCores } from "../HacknetNode"; +import { getMaxNumberLevelUpgrades, + getMaxNumberRamUpgrades, + getMaxNumberCoreUpgrades } from "../HacknetHelpers"; + +import { Player } from "../../Player"; + +import { numeralWrapper } from "../../ui/numeralFormat"; + +export class HacknetNode extends React.Component { + render() { + const node = this.props.node; + const purchaseMult = this.props.purchaseMultiplier; + const recalculate = this.props.recalculate; + + // Upgrade Level Button + let upgradeLevelText, upgradeLevelClass; + if (node.level >= HacknetNodeMaxLevel) { + upgradeLevelText = "MAX LEVEL"; + upgradeLevelClass = "std-button-disabled"; + } else { + let multiplier = 0; + if (purchaseMult === "MAX") { + multiplier = getMaxNumberLevelUpgrades(node, HacknetNodeMaxLevel); + } else { + const levelsToMax = HacknetNodeMaxLevel - node.level; + multiplier = Math.min(levelsToMax, purchaseMult); + } + + const upgradeLevelCost = node.calculateLevelUpgradeCost(multiplier, Player); + upgradeLevelText = `Upgrade x${multiplier} - ${numeralWrapper.formatMoney(upgradeLevelCost)}`; + if (Player.money.lt(upgradeLevelCost)) { + upgradeLevelClass = "std-button-disabled"; + } else { + upgradeLevelClass = "std-button"; + } + } + const upgradeLevelOnClick = () => { + let numUpgrades = purchaseMult; + if (purchaseMult === "MAX") { + numUpgrades = getMaxNumberLevelUpgrades(node, HacknetNodeMaxLevel); + } + node.purchaseLevelUpgrade(numUpgrades, Player); + recalculate(); + return false; + } + + let upgradeRamText, upgradeRamClass; + if (node.ram >= HacknetNodeMaxRam) { + upgradeRamText = "MAX RAM"; + upgradeRamClass = "std-button-disabled"; + } else { + let multiplier = 0; + if (purchaseMult === "MAX") { + multiplier = getMaxNumberRamUpgrades(node, HacknetNodeMaxRam); + } else { + const levelsToMax = Math.round(Math.log2(HacknetNodeMaxRam / node.ram)); + multiplier = Math.min(levelsToMax, purchaseMult); + } + + const upgradeRamCost = node.calculateRamUpgradeCost(multiplier, Player); + upgradeRamText = `Upgrade x${multiplier} - ${numeralWrapper.formatMoney(upgradeRamCost)}`; + if (Player.money.lt(upgradeRamCost)) { + upgradeRamClass = "std-button-disabled"; + } else { + upgradeRamClass = "std-button"; + } + } + const upgradeRamOnClick = () => { + let numUpgrades = purchaseMult; + if (purchaseMult === "MAX") { + numUpgrades = getMaxNumberRamUpgrades(node, HacknetNodeMaxRam); + } + node.purchaseRamUpgrade(numUpgrades, Player); + recalculate(); + return false; + } + + let upgradeCoresText, upgradeCoresClass; + if (node.cores >= HacknetNodeMaxCores) { + upgradeCoresText = "MAX CORES"; + upgradeCoresClass = "std-button-disabled"; + } else { + let multiplier = 0; + if (purchaseMult === "MAX") { + multiplier = getMaxNumberCoreUpgrades(node, HacknetNodeMaxCores); + } else { + const levelsToMax = HacknetNodeMaxCores - node.cores; + multiplier = Math.min(levelsToMax, purchaseMult); + } + + const upgradeCoreCost = node.calculateCoreUpgradeCost(multiplier, Player); + upgradeCoresText = `Upgrade x${multiplier} - ${numeralWrapper.formatMoney(upgradeCoreCost)}`; + if (Player.money.lt(upgradeCoreCost)) { + upgradeCoresClass = "std-button-disabled"; + } else { + upgradeCoresClass = "std-button"; + } + } + const upgradeCoresOnClick = () => { + let numUpgrades = purchaseMult; + if (purchaseMult === "MAX") { + numUpgrades = getMaxNumberCoreUpgrades(node, HacknetNodeMaxCores); + } + node.purchaseCoreUpgrade(numUpgrades, Player); + recalculate(); + return false; + } + + return ( +
  • +
    +
    +

    Node name:

    + {node.name} +
    +
    +

    Production:

    + + {numeralWrapper.formatMoney(node.totalMoneyGenerated)} ({numeralWrapper.formatMoney(node.moneyGainRatePerSecond)} / sec) + +
    +
    +

    Level:

    {node.level} + +
    +
    +

    RAM:

    {node.ram}GB + +
    +
    +

    Cores:

    {node.cores} + +
    +
    +
  • + ) + } +} diff --git a/src/Hacknet/ui/HacknetServer.jsx b/src/Hacknet/ui/HacknetServer.jsx new file mode 100644 index 000000000..5e69dc387 --- /dev/null +++ b/src/Hacknet/ui/HacknetServer.jsx @@ -0,0 +1,200 @@ +/** + * React Component for the Hacknet Node UI. + * This Component displays the panel for a single Hacknet Node + */ +import React from "react"; + +import { HacknetServerMaxLevel, + HacknetServerMaxRam, + HacknetServerMaxCores, + HacknetServerMaxCache } from "../HacknetServer"; +import { getMaxNumberLevelUpgrades, + getMaxNumberRamUpgrades, + getMaxNumberCoreUpgrades, + getMaxNumberCacheUpgrades } from "../HacknetHelpers"; + +import { Player } from "../../Player"; + +import { numeralWrapper } from "../../ui/numeralFormat"; + +export class HacknetServer extends React.Component { + render() { + const node = this.props.node; + const purchaseMult = this.props.purchaseMultiplier; + const recalculate = this.props.recalculate; + + // Upgrade Level Button + let upgradeLevelText, upgradeLevelClass; + if (node.level >= HacknetServerMaxLevel) { + upgradeLevelText = "MAX LEVEL"; + upgradeLevelClass = "std-button-disabled"; + } else { + let multiplier = 0; + if (purchaseMult === "MAX") { + multiplier = getMaxNumberLevelUpgrades(node, HacknetServerMaxLevel); + } else { + const levelsToMax = HacknetServerMaxLevel - node.level; + multiplier = Math.min(levelsToMax, purchaseMult); + } + + const upgradeLevelCost = node.calculateLevelUpgradeCost(multiplier, Player); + upgradeLevelText = `Upgrade x${multiplier} - ${numeralWrapper.formatMoney(upgradeLevelCost)}`; + if (Player.money.lt(upgradeLevelCost)) { + upgradeLevelClass = "std-button-disabled"; + } else { + upgradeLevelClass = "std-button"; + } + } + const upgradeLevelOnClick = () => { + let numUpgrades = purchaseMult; + if (purchaseMult === "MAX") { + numUpgrades = getMaxNumberLevelUpgrades(node, HacknetServerMaxLevel); + } + node.purchaseLevelUpgrade(numUpgrades, Player); + recalculate(); + return false; + } + + // Upgrade RAM Button + let upgradeRamText, upgradeRamClass; + if (node.maxRam >= HacknetServerMaxRam) { + upgradeRamText = "MAX RAM"; + upgradeRamClass = "std-button-disabled"; + } else { + let multiplier = 0; + if (purchaseMult === "MAX") { + multiplier = getMaxNumberRamUpgrades(node, HacknetServerMaxRam); + } else { + const levelsToMax = Math.round(Math.log2(HacknetServerMaxRam / node.maxRam)); + multiplier = Math.min(levelsToMax, purchaseMult); + } + + const upgradeRamCost = node.calculateRamUpgradeCost(multiplier, Player); + upgradeRamText = `Upgrade x${multiplier} - ${numeralWrapper.formatMoney(upgradeRamCost)}`; + if (Player.money.lt(upgradeRamCost)) { + upgradeRamClass = "std-button-disabled"; + } else { + upgradeRamClass = "std-button"; + } + } + const upgradeRamOnClick = () => { + let numUpgrades = purchaseMult; + if (purchaseMult === "MAX") { + numUpgrades = getMaxNumberRamUpgrades(node, HacknetServerMaxRam); + } + node.purchaseRamUpgrade(numUpgrades, Player); + recalculate(); + return false; + } + + // Upgrade Cores Button + let upgradeCoresText, upgradeCoresClass; + if (node.cores >= HacknetServerMaxCores) { + upgradeCoresText = "MAX CORES"; + upgradeCoresClass = "std-button-disabled"; + } else { + let multiplier = 0; + if (purchaseMult === "MAX") { + multiplier = getMaxNumberCoreUpgrades(node, HacknetServerMaxCores); + } else { + const levelsToMax = HacknetServerMaxCores - node.cores; + multiplier = Math.min(levelsToMax, purchaseMult); + } + + const upgradeCoreCost = node.calculateCoreUpgradeCost(multiplier, Player); + upgradeCoresText = `Upgrade x${multiplier} - ${numeralWrapper.formatMoney(upgradeCoreCost)}`; + if (Player.money.lt(upgradeCoreCost)) { + upgradeCoresClass = "std-button-disabled"; + } else { + upgradeCoresClass = "std-button"; + } + } + const upgradeCoresOnClick = () => { + let numUpgrades = purchaseMult; + if (purchaseMult === "MAX") { + numUpgrades = getMaxNumberCoreUpgrades(node, HacknetServerMaxCores); + } + node.purchaseCoreUpgrade(numUpgrades, Player); + recalculate(); + return false; + } + + // Upgrade Cache button + let upgradeCacheText, upgradeCacheClass; + if (node.cache >= HacknetServerMaxCache) { + upgradeCacheText = "MAX CACHE"; + upgradeCacheClass = "std-button-disabled"; + } else { + let multiplier = 0; + if (purchaseMult === "MAX") { + multiplier = getMaxNumberCacheUpgrades(node, HacknetServerMaxCache); + } else { + const levelsToMax = HacknetServerMaxCache - node.cache; + multiplier = Math.min(levelsToMax, purchaseMult); + } + + const upgradeCacheCost = node.calculateCacheUpgradeCost(multiplier); + upgradeCacheText = `Upgrade x${multiplier} - ${numeralWrapper.formatMoney(upgradeCacheCost)}`; + if (Player.money.lt(upgradeCacheCost)) { + upgradeCacheClass = "std-button-disabled"; + } else { + upgradeCacheClass = "std-button"; + } + } + const upgradeCacheOnClick = () => { + let numUpgrades = purchaseMult; + if (purchaseMult === "MAX") { + numUpgrades = getMaxNumberCacheUpgrades(node, HacknetServerMaxCache); + } + node.purchaseCacheUpgrade(numUpgrades, Player); + recalculate(); + Player.hashManager.updateCapacity(Player); + return false; + } + + return ( +
  • +
    +
    +

    Node name:

    + {node.hostname} +
    +
    +

    Production:

    + + {numeralWrapper.formatBigNumber(node.totalHashesGenerated)} ({numeralWrapper.formatBigNumber(node.hashRate)} / sec) + +
    +
    +

    Hash Capacity:

    + {node.hashCapacity} +
    +
    +

    Level:

    {node.level} + +
    +
    +

    RAM:

    {node.maxRam}GB + +
    +
    +

    Cores:

    {node.cores} + +
    +
    +

    Cache Level:

    {node.cache} + +
    +
    +
  • + ) + } +} diff --git a/src/Hacknet/ui/HashUpgradePopup.jsx b/src/Hacknet/ui/HashUpgradePopup.jsx new file mode 100644 index 000000000..720f73a51 --- /dev/null +++ b/src/Hacknet/ui/HashUpgradePopup.jsx @@ -0,0 +1,137 @@ +/** + * Create the pop-up for purchasing upgrades with hashes + */ +import React from "react"; + +import { purchaseHashUpgrade } from "../HacknetHelpers"; +import { HashManager } from "../HashManager"; +import { HashUpgrades } from "../HashUpgrades"; + +import { Player } from "../../Player"; +import { AllServers } from "../../Server/AllServers"; +import { Server } from "../../Server/Server"; + +import { numeralWrapper } from "../../ui/numeralFormat"; + +import { removePopup } from "../../ui/React/createPopup"; +import { ServerDropdown, + ServerType } from "../../ui/React/ServerDropdown" + +import { dialogBoxCreate } from "../../../utils/DialogBox"; + +class HashUpgrade extends React.Component { + constructor(props) { + super(props); + + this.state = { + selectedServer: "foodnstuff", + } + + this.changeTargetServer = this.changeTargetServer.bind(this); + this.purchase = this.purchase.bind(this, this.props.hashManager, this.props.upg); + } + + changeTargetServer(e) { + this.setState({ + selectedServer: e.target.value + }); + } + + purchase(hashManager, upg) { + const canPurchase = hashManager.hashes >= hashManager.getUpgradeCost(upg.name); + if (canPurchase) { + const res = purchaseHashUpgrade(upg.name, this.state.selectedServer); + if (res) { + this.props.rerender(); + } else { + dialogBoxCreate("Failed to purchase upgrade. This may be because you do not have enough hashes, " + + "or because you do not have access to the feature this upgrade affects."); + } + } + } + + render() { + const hashManager = this.props.hashManager; + const upg = this.props.upg; + const cost = hashManager.getUpgradeCost(upg.name); + + // Purchase button + const canPurchase = hashManager.hashes >= cost; + const btnClass = canPurchase ? "std-button" : "std-button-disabled"; + + // We'll reuse a Bladeburner css class + return ( +
    +

    {upg.name}

    +

    Cost: {numeralWrapper.format(cost, "0.000a")}

    +

    {upg.desc}

    + + { + upg.hasTargetServer && + + } +
    + ) + } +} + +export class HashUpgradePopup extends React.Component { + constructor(props) { + super(props); + + this.closePopup = this.closePopup.bind(this); + + this.state = { + totalHashes: Player.hashManager.hashes, + } + } + + componentDidMount() { + this.interval = setInterval(() => this.tick(), 1e3); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + closePopup() { + removePopup(this.props.popupId); + } + + tick() { + this.setState({ + totalHashes: Player.hashManager.hashes, + }) + } + + render() { + const rerender = this.props.rerender; + + const hashManager = Player.hashManager; + if (!(hashManager instanceof HashManager)) { + throw new Error(`Player does not have a HashManager)`); + } + + const upgradeElems = Object.keys(HashUpgrades).map((upgName) => { + const upg = HashUpgrades[upgName]; + return + }); + + return ( +
    + +

    Spend your hashes on a variety of different upgrades

    +

    Hashes: {numeralWrapper.formatBigNumber(this.state.totalHashes)}

    + {upgradeElems} +
    + ) + } +} diff --git a/src/Hacknet/ui/MultiplierButtons.jsx b/src/Hacknet/ui/MultiplierButtons.jsx new file mode 100644 index 000000000..3fe12477c --- /dev/null +++ b/src/Hacknet/ui/MultiplierButtons.jsx @@ -0,0 +1,41 @@ +/** + * React Component for the Multiplier buttons on the Hacknet page. + * These buttons let the player control how many Nodes/Upgrades they're + * purchasing when using the UI (x1, x5, x10, MAX) + */ +import React from "react"; + +import { PurchaseMultipliers } from "./Root"; + +function MultiplierButton(props) { + return ( + + ) +} + +export function MultiplierButtons(props) { + if (props.purchaseMultiplier == null) { + throw new Error(`MultiplierButtons constructed without required props`); + } + + const mults = ["x1", "x5", "x10", "MAX"]; + const onClicks = props.onClicks; + const buttons = []; + for (let i = 0; i < mults.length; ++i) { + const mult = mults[i]; + const btnProps = { + className: props.purchaseMultiplier === PurchaseMultipliers[mult] ? "std-button-disabled" : "std-button", + key: mult, + onClick: onClicks[i], + text: mult, + } + + buttons.push() + } + + return ( + + {buttons} + + ) +} diff --git a/src/Hacknet/ui/PlayerInfo.jsx b/src/Hacknet/ui/PlayerInfo.jsx new file mode 100644 index 000000000..03a6e31fb --- /dev/null +++ b/src/Hacknet/ui/PlayerInfo.jsx @@ -0,0 +1,51 @@ +/** + * React Component for displaying Player info and stats on the Hacknet Node UI. + * This includes: + * - Player's money + * - Player's production from Hacknet Nodes + */ +import React from "react"; + +import { hasHacknetServers } from "../HacknetHelpers"; +import { Player } from "../../Player"; +import { numeralWrapper } from "../../ui/numeralFormat"; + +export function PlayerInfo(props) { + const hasServers = hasHacknetServers(); + + let prod; + if (hasServers) { + prod = numeralWrapper.format(props.totalProduction, "0.000a") + " hashes / sec"; + } else { + prod = numeralWrapper.formatMoney(props.totalProduction) + " / sec"; + } + + let hashInfo; + if (hasServers) { + hashInfo = numeralWrapper.format(Player.hashManager.hashes, "0.000a") + " / " + + numeralWrapper.format(Player.hashManager.capacity, "0.000a"); + } + + return ( +

    + Money: + {numeralWrapper.formatMoney(Player.money.toNumber())}
    + + { + hasServers && + Hashes: + } + { + hasServers && + {hashInfo} + } + { + hasServers && +
    + } + + Total Hacknet Node Production: + {prod} +

    + ) +} diff --git a/src/Hacknet/ui/PurchaseButton.jsx b/src/Hacknet/ui/PurchaseButton.jsx new file mode 100644 index 000000000..eac9b233d --- /dev/null +++ b/src/Hacknet/ui/PurchaseButton.jsx @@ -0,0 +1,40 @@ +/** + * React Component for the button that is used to purchase new Hacknet Nodes + */ +import React from "react"; + +import { hasHacknetServers, + hasMaxNumberHacknetServers } from "../HacknetHelpers"; +import { Player } from "../../Player"; +import { numeralWrapper } from "../../ui/numeralFormat"; + +export function PurchaseButton(props) { + if (props.multiplier == null || props.onClick == null) { + throw new Error(`PurchaseButton constructed without required props`); + } + + const cost = props.cost; + let className = Player.canAfford(cost) ? "std-button" : "std-button-disabled"; + let text; + let style = null; + if (hasHacknetServers()) { + if (hasMaxNumberHacknetServers()) { + className = "std-button-disabled"; + text = "Hacknet Server limit reached"; + style = {color: "red"}; + } else { + text = `Purchase Hacknet Server - ${numeralWrapper.formatMoney(cost)}`; + } + } else { + text = `Purchase Hacknet Node - ${numeralWrapper.formatMoney(cost)}`; + } + + return ( + + + ) +} diff --git a/src/Hacknet/ui/Root.jsx b/src/Hacknet/ui/Root.jsx new file mode 100644 index 000000000..3e1a95633 --- /dev/null +++ b/src/Hacknet/ui/Root.jsx @@ -0,0 +1,144 @@ +/** + * Root React Component for the Hacknet Node UI + */ +import React from "react"; + +import { GeneralInfo } from "./GeneralInfo"; +import { HacknetNode } from "./HacknetNode"; +import { HacknetServer } from "./HacknetServer"; +import { HashUpgradePopup } from "./HashUpgradePopup"; +import { MultiplierButtons } from "./MultiplierButtons"; +import { PlayerInfo } from "./PlayerInfo"; +import { PurchaseButton } from "./PurchaseButton"; + +import { getCostOfNextHacknetNode, + getCostOfNextHacknetServer, + hasHacknetServers, + purchaseHacknet } from "../HacknetHelpers"; + +import { Player } from "../../Player"; + +import { createPopup } from "../../ui/React/createPopup"; + +export const PurchaseMultipliers = Object.freeze({ + "x1": 1, + "x5": 5, + "x10": 10, + "MAX": "MAX", +}); + +export class HacknetRoot extends React.Component { + constructor(props) { + super(props); + + this.state = { + purchaseMultiplier: PurchaseMultipliers.x1, + totalProduction: 0, // Total production ($ / s) of Hacknet Nodes + } + + this.createHashUpgradesPopup = this.createHashUpgradesPopup.bind(this); + } + + componentDidMount() { + this.recalculateTotalProduction(); + } + + createHashUpgradesPopup() { + const id = "hacknet-server-hash-upgrades-popup"; + createPopup(id, HashUpgradePopup, { popupId: id, rerender: this.createHashUpgradesPopup }); + } + + recalculateTotalProduction() { + let total = 0; + for (let i = 0; i < Player.hacknetNodes.length; ++i) { + if (hasHacknetServers()) { + total += Player.hacknetNodes[i].hashRate; + } else { + total += Player.hacknetNodes[i].moneyGainRatePerSecond; + } + } + + this.setState({ + totalProduction: total, + }); + } + + setPurchaseMultiplier(mult) { + this.setState({ + purchaseMultiplier: mult, + }); + } + + render() { + // Cost to purchase a new Hacknet Node + let purchaseCost; + if (hasHacknetServers()) { + purchaseCost = getCostOfNextHacknetServer(); + } else { + purchaseCost = getCostOfNextHacknetNode(); + } + + // onClick event handler for purchase button + const purchaseOnClick = () => { + if (purchaseHacknet() >= 0) { + this.recalculateTotalProduction(); + Player.hashManager.updateCapacity(Player); + } + } + + // onClick event handlers for purchase multiplier buttons + const purchaseMultiplierOnClicks = [ + this.setPurchaseMultiplier.bind(this, PurchaseMultipliers.x1), + this.setPurchaseMultiplier.bind(this, PurchaseMultipliers.x5), + this.setPurchaseMultiplier.bind(this, PurchaseMultipliers.x10), + this.setPurchaseMultiplier.bind(this, PurchaseMultipliers.MAX), + ]; + + // HacknetNode components + const nodes = Player.hacknetNodes.map((node) => { + if (hasHacknetServers()) { + return ( + + ) + } else { + return ( + + ) + } + }); + + return ( +
    +

    Hacknet Nodes

    + + + + +
    +
    + + +
    + + { + hasHacknetServers() && + + } + +
      {nodes}
    +
    + ) + } +} diff --git a/src/HacknetNode.js b/src/HacknetNode.js deleted file mode 100644 index 9421b9346..000000000 --- a/src/HacknetNode.js +++ /dev/null @@ -1,694 +0,0 @@ -import { BitNodeMultipliers } from "./BitNode/BitNodeMultipliers"; -import { CONSTANTS } from "./Constants"; -import { Engine } from "./engine"; -import {iTutorialSteps, iTutorialNextStep, - ITutorial} from "./InteractiveTutorial"; -import {Player} from "./Player"; -import {Page, routing} from "./ui/navigationTracking"; -import { numeralWrapper } from "./ui/numeralFormat"; - -import {dialogBoxCreate} from "../utils/DialogBox"; -import {clearEventListeners} from "../utils/uiHelpers/clearEventListeners"; -import {Reviver, Generic_toJSON, - Generic_fromJSON} from "../utils/JSONReviver"; -import {createElement} from "../utils/uiHelpers/createElement"; -import {getElementById} from "../utils/uiHelpers/getElementById"; - -// Stores total money gain rate from all of the player's Hacknet Nodes -let TotalHacknetNodeProduction = 0; - -/** - * Overwrites the inner text of the specified HTML element if it is different from what currently exists. - * @param {string} elementId The HTML ID to find the first instance of. - * @param {string} text The inner text that should be set. - */ -function updateText(elementId, text) { - var el = getElementById(elementId); - if (el.innerText != text) { - el.innerText = text; - } -}; - -/* HacknetNode.js */ -function hacknetNodesInit() { - var performMapping = function(x) { - getElementById("hacknet-nodes-" + x.id + "-multiplier") - .addEventListener("click", function() { - hacknetNodePurchaseMultiplier = x.multiplier; - updateHacknetNodesMultiplierButtons(); - updateHacknetNodesContent(); - return false; - }); - }; - - var mappings = [ - { id: "1x", multiplier: 1 }, - { id: "5x", multiplier: 5 }, - { id: "10x", multiplier: 10 }, - { id: "max", multiplier: 0 } - ]; - for (var elem of mappings) { - // Encapsulate in a function so that the appropriate scope is kept in the click handler. - performMapping(elem); - } -} - -document.addEventListener("DOMContentLoaded", hacknetNodesInit, false); - -function HacknetNode(name) { - this.level = 1; - this.ram = 1; //GB - this.cores = 1; - - this.name = name; - - this.totalMoneyGenerated = 0; - this.onlineTimeSeconds = 0; - - this.moneyGainRatePerSecond = 0; -} - -HacknetNode.prototype.updateMoneyGainRate = function() { - //How much extra $/s is gained per level - var gainPerLevel = CONSTANTS.HacknetNodeMoneyGainPerLevel; - - this.moneyGainRatePerSecond = (this.level * gainPerLevel) * - Math.pow(1.035, this.ram-1) * - ((this.cores + 5) / 6) * - Player.hacknet_node_money_mult * - BitNodeMultipliers.HacknetNodeMoney; - if (isNaN(this.moneyGainRatePerSecond)) { - this.moneyGainRatePerSecond = 0; - dialogBoxCreate("Error in calculating Hacknet Node production. Please report to game developer"); - } - - updateTotalHacknetProduction(); -} - -HacknetNode.prototype.calculateLevelUpgradeCost = function(levels=1) { - levels = Math.round(levels); - if (isNaN(levels) || levels < 1) { - return 0; - } - - if (this.level >= CONSTANTS.HacknetNodeMaxLevel) { - return Infinity; - } - - var mult = CONSTANTS.HacknetNodeUpgradeLevelMult; - var totalMultiplier = 0; //Summed - var currLevel = this.level; - for (var i = 0; i < levels; ++i) { - totalMultiplier += Math.pow(mult, currLevel); - ++currLevel; - } - - return CONSTANTS.BaseCostForHacknetNode / 2 * totalMultiplier * Player.hacknet_node_level_cost_mult; -} - -HacknetNode.prototype.purchaseLevelUpgrade = function(levels=1) { - levels = Math.round(levels); - var cost = this.calculateLevelUpgradeCost(levels); - if (isNaN(cost) || levels < 0) { - return false; - } - - //If we're at max level, return false - if (this.level >= CONSTANTS.HacknetNodeMaxLevel) { - return false; - } - - //If the number of specified upgrades would exceed the max level, calculate - //the maximum number of upgrades and use that - if (this.level + levels > CONSTANTS.HacknetNodeMaxLevel) { - var diff = Math.max(0, CONSTANTS.HacknetNodeMaxLevel - this.level); - return this.purchaseLevelUpgrade(diff); - } - - if (Player.money.lt(cost)) { - return false; - } - - Player.loseMoney(cost); - this.level = Math.round(this.level + levels); //Just in case of floating point imprecision - this.updateMoneyGainRate(); - return true; -} - -HacknetNode.prototype.calculateRamUpgradeCost = function(levels=1) { - levels = Math.round(levels); - if (isNaN(levels) || levels < 1) { - return 0; - } - - if (this.ram >= CONSTANTS.HacknetNodeMaxRam) { - return Infinity; - } - - let totalCost = 0; - let numUpgrades = Math.round(Math.log2(this.ram)); - let currentRam = this.ram; - - for (let i = 0; i < levels; ++i) { - let baseCost = currentRam * CONSTANTS.BaseCostFor1GBOfRamHacknetNode; - let mult = Math.pow(CONSTANTS.HacknetNodeUpgradeRamMult, numUpgrades); - - totalCost += (baseCost * mult); - - currentRam *= 2; - ++numUpgrades; - } - - totalCost *= Player.hacknet_node_ram_cost_mult - return totalCost; -} - -HacknetNode.prototype.purchaseRamUpgrade = function(levels=1) { - levels = Math.round(levels); - var cost = this.calculateRamUpgradeCost(levels); - if (isNaN(cost) || levels < 0) { - return false; - } - - // Fail if we're already at max - if (this.ram >= CONSTANTS.HacknetNodeMaxRam) { - return false; - } - - //If the number of specified upgrades would exceed the max RAM, calculate the - //max possible number of upgrades and use that - if (this.ram * Math.pow(2, levels) > CONSTANTS.HacknetNodeMaxRam) { - var diff = Math.max(0, Math.log2(Math.round(CONSTANTS.HacknetNodeMaxRam / this.ram))); - return this.purchaseRamUpgrade(diff); - } - - if (Player.money.lt(cost)) { - return false; - } - - Player.loseMoney(cost); - for (let i = 0; i < levels; ++i) { - this.ram *= 2; //Ram is always doubled - } - this.ram = Math.round(this.ram); //Handle any floating point precision issues - this.updateMoneyGainRate(); - return true; -} - -HacknetNode.prototype.calculateCoreUpgradeCost = function(levels=1) { - levels = Math.round(levels); - if (isNaN(levels) || levels < 1) { - return 0; - } - - if (this.cores >= CONSTANTS.HacknetNodeMaxCores) { - return Infinity; - } - - const coreBaseCost = CONSTANTS.BaseCostForHacknetNodeCore; - const mult = CONSTANTS.HacknetNodeUpgradeCoreMult; - let totalCost = 0; - let currentCores = this.cores; - for (let i = 0; i < levels; ++i) { - totalCost += (coreBaseCost * Math.pow(mult, currentCores-1)); - ++currentCores; - } - - totalCost *= Player.hacknet_node_core_cost_mult; - - return totalCost; -} - -HacknetNode.prototype.purchaseCoreUpgrade = function(levels=1) { - levels = Math.round(levels); - var cost = this.calculateCoreUpgradeCost(levels); - if (isNaN(cost) || levels < 0) { - return false; - } - - //Fail if we're already at max - if (this.cores >= CONSTANTS.HacknetNodeMaxCores) { - return false; - } - - //If the specified number of upgrades would exceed the max Cores, calculate - //the max possible number of upgrades and use that - if (this.cores + levels > CONSTANTS.HacknetNodeMaxCores) { - var diff = Math.max(0, CONSTANTS.HacknetNodeMaxCores - this.cores); - return this.purchaseCoreUpgrade(diff); - } - - if (Player.money.lt(cost)) { - return false; - } - - Player.loseMoney(cost); - this.cores = Math.round(this.cores + levels); //Just in case of floating point imprecision - this.updateMoneyGainRate(); - return true; -} - -/* Saving and loading HackNets */ -HacknetNode.prototype.toJSON = function() { - return Generic_toJSON("HacknetNode", this); -} - -HacknetNode.fromJSON = function(value) { - return Generic_fromJSON(HacknetNode, value.data); -} - -Reviver.constructors.HacknetNode = HacknetNode; - -function purchaseHacknet() { - /* INTERACTIVE TUTORIAL */ - if (ITutorial.isRunning) { - if (ITutorial.currStep === iTutorialSteps.HacknetNodesIntroduction) { - iTutorialNextStep(); - } else { - return; - } - } - - /* END INTERACTIVE TUTORIAL */ - - var cost = getCostOfNextHacknetNode(); - if (isNaN(cost)) { - throw new Error("Cost is NaN"); - } - - if (Player.money.lt(cost)) { - //dialogBoxCreate("You cannot afford to purchase a Hacknet Node!"); - return -1; - } - - //Auto generate a name for the node for now...TODO - var numOwned = Player.hacknetNodes.length; - var name = "hacknet-node-" + numOwned; - var node = new HacknetNode(name); - node.updateMoneyGainRate(); - - Player.loseMoney(cost); - Player.hacknetNodes.push(node); - - if (routing.isOn(Page.HacknetNodes)) { - displayHacknetNodesContent(); - } - updateTotalHacknetProduction(); - return numOwned; -} - -//Calculates the total production from all HacknetNodes -function updateTotalHacknetProduction() { - var total = 0; - for (var i = 0; i < Player.hacknetNodes.length; ++i) { - total += Player.hacknetNodes[i].moneyGainRatePerSecond; - } - TotalHacknetNodeProduction = total; -} - -function getCostOfNextHacknetNode() { - //Cost increases exponentially based on how many you own - var numOwned = Player.hacknetNodes.length; - var mult = CONSTANTS.HacknetNodePurchaseNextMult; - return CONSTANTS.BaseCostForHacknetNode * Math.pow(mult, numOwned) * Player.hacknet_node_purchase_cost_mult; -} - -var hacknetNodePurchaseMultiplier = 1; -function updateHacknetNodesMultiplierButtons() { - var mult1x = document.getElementById("hacknet-nodes-1x-multiplier"); - var mult5x = document.getElementById("hacknet-nodes-5x-multiplier"); - var mult10x = document.getElementById("hacknet-nodes-10x-multiplier"); - var multMax = document.getElementById("hacknet-nodes-max-multiplier"); - mult1x.setAttribute("class", "a-link-button"); - mult5x.setAttribute("class", "a-link-button"); - mult10x.setAttribute("class", "a-link-button"); - multMax.setAttribute("class", "a-link-button"); - if (Player.hacknetNodes.length == 0) { - mult1x.setAttribute("class", "a-link-button-inactive"); - mult5x.setAttribute("class", "a-link-button-inactive"); - mult10x.setAttribute("class", "a-link-button-inactive"); - multMax.setAttribute("class", "a-link-button-inactive"); - } else if (hacknetNodePurchaseMultiplier == 1) { - mult1x.setAttribute("class", "a-link-button-inactive"); - } else if (hacknetNodePurchaseMultiplier == 5) { - mult5x.setAttribute("class", "a-link-button-inactive"); - } else if (hacknetNodePurchaseMultiplier == 10) { - mult10x.setAttribute("class", "a-link-button-inactive"); - } else { - multMax.setAttribute("class", "a-link-button-inactive"); - } -} - -//Calculate the maximum number of times the Player can afford to upgrade a Hacknet Node -function getMaxNumberLevelUpgrades(nodeObj) { - if (Player.money.lt(nodeObj.calculateLevelUpgradeCost(1))) { - return 0; - } - - var min = 1; - var max = CONSTANTS.HacknetNodeMaxLevel - 1; - var levelsToMax = CONSTANTS.HacknetNodeMaxLevel - nodeObj.level; - if (Player.money.gt(nodeObj.calculateLevelUpgradeCost(levelsToMax))) { - return levelsToMax; - } - - while (min <= max) { - var curr = (min + max) / 2 | 0; - if (curr != CONSTANTS.HacknetNodeMaxLevel && - Player.money.gt(nodeObj.calculateLevelUpgradeCost(curr)) && - Player.money.lt(nodeObj.calculateLevelUpgradeCost(curr + 1))) { - return Math.min(levelsToMax, curr); - } else if (Player.money.lt(nodeObj.calculateLevelUpgradeCost(curr))) { - max = curr - 1; - } else if (Player.money.gt(nodeObj.calculateLevelUpgradeCost(curr))) { - min = curr + 1; - } else { - return Math.min(levelsToMax, curr); - } - } - return 0; -} - -function getMaxNumberRamUpgrades(nodeObj) { - if (Player.money.lt(nodeObj.calculateRamUpgradeCost(1))) { - return 0; - } - - const levelsToMax = Math.round(Math.log2(CONSTANTS.HacknetNodeMaxRam / nodeObj.ram)); - if (Player.money.gt(nodeObj.calculateRamUpgradeCost(levelsToMax))) { - return levelsToMax; - } - - //We'll just loop until we find the max - for (let i = levelsToMax-1; i >= 0; --i) { - if (Player.money.gt(nodeObj.calculateRamUpgradeCost(i))) { - return i; - } - } - return 0; -} - -function getMaxNumberCoreUpgrades(nodeObj) { - if (Player.money.lt(nodeObj.calculateCoreUpgradeCost(1))) { - return 0; - } - - var min = 1; - var max = CONSTANTS.HacknetNodeMaxCores - 1; - const levelsToMax = CONSTANTS.HacknetNodeMaxCores - nodeObj.cores; - if (Player.money.gt(nodeObj.calculateCoreUpgradeCost(levelsToMax))) { - return levelsToMax; - } - - //Use a binary search to find the max possible number of upgrades - while (min <= max) { - let curr = (min + max) / 2 | 0; - if (curr != CONSTANTS.HacknetNodeMaxCores && - Player.money.gt(nodeObj.calculateCoreUpgradeCost(curr)) && - Player.money.lt(nodeObj.calculateCoreUpgradeCost(curr + 1))) { - return Math.min(levelsToMax, curr); - } else if (Player.money.lt(nodeObj.calculateCoreUpgradeCost(curr))) { - max = curr - 1; - } else if (Player.money.gt(nodeObj.calculateCoreUpgradeCost(curr))) { - min = curr + 1; - } else { - return Math.min(levelsToMax, curr); - } - } - return 0; -} - -//Creates Hacknet Node DOM elements when the page is opened -function displayHacknetNodesContent() { - //Update Hacknet Nodes button - var newPurchaseButton = clearEventListeners("hacknet-nodes-purchase-button"); - - newPurchaseButton.addEventListener("click", function() { - purchaseHacknet(); - return false; - }); - - //Handle Purchase multiplier buttons - updateHacknetNodesMultiplierButtons(); - - //Remove all old hacknet Node DOM elements - var hacknetNodesList = document.getElementById("hacknet-nodes-list"); - while (hacknetNodesList.firstChild) { - hacknetNodesList.removeChild(hacknetNodesList.firstChild); - } - - //Then re-create them - for (var i = 0; i < Player.hacknetNodes.length; ++i) { - createHacknetNodeDomElement(Player.hacknetNodes[i]); - } - - updateTotalHacknetProduction(); - updateHacknetNodesContent(); -} - -//Update information on all Hacknet Node DOM elements -function updateHacknetNodesContent() { - //Set purchase button to inactive if not enough money, and update its price display - var cost = getCostOfNextHacknetNode(); - var purchaseButton = getElementById("hacknet-nodes-purchase-button"); - var formattedCost = numeralWrapper.formatMoney(cost); - - updateText("hacknet-nodes-purchase-button", `Purchase Hacknet Node - ${formattedCost}`); - - if (Player.money.lt(cost)) { - purchaseButton.setAttribute("class", "a-link-button-inactive"); - } else { - purchaseButton.setAttribute("class", "a-link-button"); - } - - //Update player's money - updateText("hacknet-nodes-player-money", numeralWrapper.formatMoney(Player.money.toNumber())); - updateText("hacknet-nodes-total-production", numeralWrapper.formatMoney(TotalHacknetNodeProduction) + " / sec"); - - //Update information in each owned hacknet node - for (var i = 0; i < Player.hacknetNodes.length; ++i) { - updateHacknetNodeDomElement(Player.hacknetNodes[i]); - } -} - -//Creates a single Hacknet Node DOM element -function createHacknetNodeDomElement(nodeObj) { - var nodeName = nodeObj.name; - - var nodeLevelContainer = createElement("div", { - class: "hacknet-node-level-container row", - innerHTML: "

    Level:

    " - }); - - var nodeRamContainer = createElement("div", { - class: "hacknet-node-ram-container row", - innerHTML: "

    RAM:

    " - }); - - var nodeCoresContainer = createElement("div", { - class: "hacknet-node-cores-container row", - innerHTML: "

    Cores:

    " - }) - var containingDiv = createElement("div", { - class: "hacknet-node-container", - innerHTML: "
    " + - "

    Node name:

    " + - "" + - "
    " + - "
    " + - "

    Production:

    " + - "" + - "" + - "
    " - }); - containingDiv.appendChild(nodeLevelContainer); - containingDiv.appendChild(nodeRamContainer); - containingDiv.appendChild(nodeCoresContainer); - - var listItem = createElement("li", { - class: "hacknet-node" - }); - listItem.appendChild(containingDiv); - - //Upgrade buttons - nodeLevelContainer.appendChild(createElement("a", { - id: "hacknet-node-upgrade-level-" + nodeName, - class: "a-link-button-inactive", - clickListener: function() { - let numUpgrades = hacknetNodePurchaseMultiplier; - if (hacknetNodePurchaseMultiplier == 0) { - numUpgrades = getMaxNumberLevelUpgrades(nodeObj); - } - nodeObj.purchaseLevelUpgrade(numUpgrades); - updateHacknetNodesContent(); - return false; - } - })); - - nodeRamContainer.appendChild(createElement("a", { - id: "hacknet-node-upgrade-ram-" + nodeName, - class: "a-link-button-inactive", - clickListener: function() { - let numUpgrades = hacknetNodePurchaseMultiplier; - if (hacknetNodePurchaseMultiplier == 0) { - numUpgrades = getMaxNumberRamUpgrades(nodeObj); - } - nodeObj.purchaseRamUpgrade(numUpgrades); - updateHacknetNodesContent(); - return false; - } - })); - - nodeCoresContainer.appendChild(createElement("a", { - id: "hacknet-node-upgrade-core-" + nodeName, - class: "a-link-button-inactive", - clickListener: function() { - let numUpgrades = hacknetNodePurchaseMultiplier; - if (hacknetNodePurchaseMultiplier == 0) { - numUpgrades = getMaxNumberCoreUpgrades(nodeObj); - } - nodeObj.purchaseCoreUpgrade(numUpgrades); - updateHacknetNodesContent(); - return false; - } - })); - - document.getElementById("hacknet-nodes-list").appendChild(listItem); - - //Set the text and stuff inside the DOM element - updateHacknetNodeDomElement(nodeObj); -} - -//Updates information on a single hacknet node DOM element -function updateHacknetNodeDomElement(nodeObj) { - var nodeName = nodeObj.name; - - updateText("hacknet-node-name-" + nodeName, nodeName); - updateText("hacknet-node-total-production-" + nodeName, numeralWrapper.formatMoney(nodeObj.totalMoneyGenerated)); - updateText("hacknet-node-production-rate-" + nodeName, "(" + numeralWrapper.formatMoney(nodeObj.moneyGainRatePerSecond) + " / sec)"); - updateText("hacknet-node-level-" + nodeName, nodeObj.level); - updateText("hacknet-node-ram-" + nodeName, nodeObj.ram + "GB"); - updateText("hacknet-node-cores-" + nodeName, nodeObj.cores); - - //Upgrade level - var upgradeLevelButton = getElementById("hacknet-node-upgrade-level-" + nodeName); - - if (nodeObj.level >= CONSTANTS.HacknetNodeMaxLevel) { - updateText("hacknet-node-upgrade-level-" + nodeName, "MAX LEVEL"); - upgradeLevelButton.setAttribute("class", "a-link-button-inactive"); - } else { - let multiplier = 0; - if (hacknetNodePurchaseMultiplier == 0) { - //Max - multiplier = getMaxNumberLevelUpgrades(nodeObj); - } else { - var levelsToMax = CONSTANTS.HacknetNodeMaxLevel - nodeObj.level; - multiplier = Math.min(levelsToMax, hacknetNodePurchaseMultiplier); - } - - var upgradeLevelCost = nodeObj.calculateLevelUpgradeCost(multiplier); - updateText("hacknet-node-upgrade-level-" + nodeName, "Upgrade x" + multiplier + " - " + numeralWrapper.formatMoney(upgradeLevelCost)) - if (Player.money.lt(upgradeLevelCost)) { - upgradeLevelButton.setAttribute("class", "a-link-button-inactive"); - } else { - upgradeLevelButton.setAttribute("class", "a-link-button"); - } - } - - //Upgrade RAM - var upgradeRamButton = getElementById("hacknet-node-upgrade-ram-" + nodeName); - - if (nodeObj.ram >= CONSTANTS.HacknetNodeMaxRam) { - updateText("hacknet-node-upgrade-ram-" + nodeName, "MAX RAM"); - upgradeRamButton.setAttribute("class", "a-link-button-inactive"); - } else { - let multiplier = 0; - if (hacknetNodePurchaseMultiplier == 0) { - multiplier = getMaxNumberRamUpgrades(nodeObj); - } else { - var levelsToMax = Math.round(Math.log2(CONSTANTS.HacknetNodeMaxRam / nodeObj.ram)); - multiplier = Math.min(levelsToMax, hacknetNodePurchaseMultiplier); - } - - var upgradeRamCost = nodeObj.calculateRamUpgradeCost(multiplier); - updateText("hacknet-node-upgrade-ram-" + nodeName, "Upgrade x" + multiplier + " - " + numeralWrapper.formatMoney(upgradeRamCost)); - if (Player.money.lt(upgradeRamCost)) { - upgradeRamButton.setAttribute("class", "a-link-button-inactive"); - } else { - upgradeRamButton.setAttribute("class", "a-link-button"); - } - } - - //Upgrade Cores - var upgradeCoreButton = getElementById("hacknet-node-upgrade-core-" + nodeName); - - if (nodeObj.cores >= CONSTANTS.HacknetNodeMaxCores) { - updateText("hacknet-node-upgrade-core-" + nodeName, "MAX CORES"); - upgradeCoreButton.setAttribute("class", "a-link-button-inactive"); - } else { - let multiplier = 0; - if (hacknetNodePurchaseMultiplier == 0) { - multiplier = getMaxNumberCoreUpgrades(nodeObj); - } else { - var levelsToMax = CONSTANTS.HacknetNodeMaxCores - nodeObj.cores; - multiplier = Math.min(levelsToMax, hacknetNodePurchaseMultiplier); - } - var upgradeCoreCost = nodeObj.calculateCoreUpgradeCost(multiplier); - updateText("hacknet-node-upgrade-core-" + nodeName, "Upgrade x" + multiplier + " - " + numeralWrapper.formatMoney(upgradeCoreCost)); - if (Player.money.lt(upgradeCoreCost)) { - upgradeCoreButton.setAttribute("class", "a-link-button-inactive"); - } else { - upgradeCoreButton.setAttribute("class", "a-link-button"); - } - } -} - - -function processAllHacknetNodeEarnings(numCycles) { - var total = 0; - for (var i = 0; i < Player.hacknetNodes.length; ++i) { - total += processSingleHacknetNodeEarnings(numCycles, Player.hacknetNodes[i]); - } - - return total; -} - -function processSingleHacknetNodeEarnings(numCycles, nodeObj) { - var cyclesPerSecond = 1000 / Engine._idleSpeed; - var earningPerCycle = nodeObj.moneyGainRatePerSecond / cyclesPerSecond; - if (isNaN(earningPerCycle)) { - console.error("Hacknet Node '" + nodeObj.name + "' Calculated earnings is NaN"); - earningPerCycle = 0; - } - - var totalEarnings = numCycles * earningPerCycle; - nodeObj.totalMoneyGenerated += totalEarnings; - nodeObj.onlineTimeSeconds += (numCycles * (Engine._idleSpeed / 1000)); - Player.gainMoney(totalEarnings); - Player.recordMoneySource(totalEarnings, "hacknetnode"); - return totalEarnings; -} - -function getHacknetNode(name) { - for (var i = 0; i < Player.hacknetNodes.length; ++i) { - if (Player.hacknetNodes[i].name == name) { - return Player.hacknetNodes[i]; - } - } - - return null; -} - -export { - HacknetNode, - displayHacknetNodesContent, - getCostOfNextHacknetNode, - getHacknetNode, - getMaxNumberLevelUpgrades, - hacknetNodesInit, - processAllHacknetNodeEarnings, - purchaseHacknet, - updateHacknetNodesContent, - updateHacknetNodesMultiplierButtons, - updateTotalHacknetProduction -}; diff --git a/src/NetscriptEnvironment.js b/src/NetscriptEnvironment.js index dcdacbb77..480e4b45f 100644 --- a/src/NetscriptEnvironment.js +++ b/src/NetscriptEnvironment.js @@ -1,4 +1,3 @@ -import {HacknetNode} from "./HacknetNode"; import {NetscriptFunctions} from "./NetscriptFunctions"; import {NetscriptPort} from "./NetscriptPort"; @@ -56,37 +55,6 @@ Environment.prototype = { } }, - setArrayElement: function(name, idx, value) { - if (!(idx instanceof Array)) { - throw new Error("idx parameter is not an Array"); - } - var scope = this.lookup(name); - if (!scope && this.parent) { - throw new Error("Undefined variable " + name); - } - var arr = (scope || this).vars[name]; - if (!(arr.constructor === Array || arr instanceof Array)) { - throw new Error("Variable is not an array: " + name); - } - var res = arr; - for (var iterator = 0; iterator < idx.length-1; ++iterator) { - var i = idx[iterator]; - if (!(res instanceof Array) || i >= res.length) { - throw new Error("Out-of-bounds array access"); - } - res = res[i]; - } - - //Cant assign to ports or HacknetNodes - if (res[idx[idx.length-1]] instanceof HacknetNode) { - throw new Error("Cannot assign a Hacknet Node handle to a new value"); - } - if (res[idx[idx.length-1]] instanceof NetscriptPort) { - throw new Error("Cannot assign a Netscript Port handle to a new value"); - } - return res[idx[idx.length-1]] = value; - }, - //Creates (or overwrites) a variable in the current scope def: function(name, value) { return this.vars[name] = value; diff --git a/src/NetscriptEvaluator.js b/src/NetscriptEvaluator.js index c301f2b76..97e030ab4 100644 --- a/src/NetscriptEvaluator.js +++ b/src/NetscriptEvaluator.js @@ -196,7 +196,7 @@ export function runScriptFromScript(server, scriptname, args, workerScript, thre } var runningScriptObj = new RunningScript(script, args); runningScriptObj.threads = threads; - server.runningScripts.push(runningScriptObj); //Push onto runningScripts + server.runScript(runningScriptObj, Player); // Push onto runningScripts addWorkerScript(runningScriptObj, server); return Promise.resolve(true); } diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index 669bce884..c9380c790 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -30,7 +30,7 @@ import { joinFaction, purchaseAugmentation } from "./Faction/FactionHelpers"; import { FactionWorkType } from "./Faction/FactionWorkTypeEnum"; import { getCostOfNextHacknetNode, - purchaseHacknet } from "./HacknetNode"; + purchaseHacknet } from "./Hacknet/HacknetNode"; import {Locations} from "./Locations"; import { Message } from "./Message/Message"; import { Messages } from "./Message/MessageHelpers"; @@ -278,27 +278,27 @@ function NetscriptFunctions(workerScript) { }, upgradeLevel : function(i, n) { var node = getHacknetNode(i); - return node.purchaseLevelUpgrade(n); + return node.purchaseLevelUpgrade(n, Player); }, upgradeRam : function(i, n) { var node = getHacknetNode(i); - return node.purchaseRamUpgrade(n); + return node.purchaseRamUpgrade(n, Player); }, upgradeCore : function(i, n) { var node = getHacknetNode(i); - return node.purchaseCoreUpgrade(n); + return node.purchaseCoreUpgrade(n, Player); }, getLevelUpgradeCost : function(i, n) { var node = getHacknetNode(i); - return node.calculateLevelUpgradeCost(n); + return node.calculateLevelUpgradeCost(n, Player); }, getRamUpgradeCost : function(i, n) { var node = getHacknetNode(i); - return node.calculateRamUpgradeCost(n); + return node.calculateRamUpgradeCost(n, Player); }, getCoreUpgradeCost : function(i, n) { var node = getHacknetNode(i); - return node.calculateCoreUpgradeCost(n); + return node.calculateCoreUpgradeCost(n, Player); } }, sprintf : sprintf, diff --git a/src/PersonObjects/IPlayer.ts b/src/PersonObjects/IPlayer.ts index c41247a2e..c3406d3c7 100644 --- a/src/PersonObjects/IPlayer.ts +++ b/src/PersonObjects/IPlayer.ts @@ -9,6 +9,8 @@ import { Sleeve } from "./Sleeve/Sleeve"; import { IMap } from "../types"; import { IPlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation"; +import { HacknetNode } from "../Hacknet/HacknetNode"; +import { HacknetServer } from "../Hacknet/HacknetServer"; import { IPlayerOwnedSourceFile } from "../SourceFile/PlayerOwnedSourceFile"; import { MoneySourceTracker } from "../utils/MoneySourceTracker"; @@ -22,7 +24,7 @@ export interface IPlayer { corporation: any; currentServer: string; factions: string[]; - hacknetNodes: any[]; + hacknetNodes: (HacknetNode | HacknetServer)[]; hasWseAccount: boolean; jobs: IMap; karma: number; diff --git a/src/Player.js b/src/Player.js index 01903d82c..c7ee6502e 100644 --- a/src/Player.js +++ b/src/Player.js @@ -21,6 +21,7 @@ import { Faction } from "./Faction/Faction"; import { Factions } from "./Faction/Factions"; import { displayFactionContent } from "./Faction/FactionHelpers"; import {Gang, resetGangs} from "./Gang"; +import { HashManager } from "./Hacknet/HashManager"; import {Locations} from "./Locations"; import {hasBn11SF, hasWallStreetSF,hasAISF} from "./NetscriptFunctions"; import { Sleeve } from "./PersonObjects/Sleeve/Sleeve"; @@ -111,10 +112,13 @@ function PlayerObject() { // Company at which player is CURRENTLY working (only valid when the player is actively working) this.companyName = ""; // Name of Company. Must match a key value in Companies map - //Servers + // Servers this.currentServer = ""; //IP address of Server currently being accessed through terminal this.purchasedServers = []; //IP Addresses of purchased servers + + // Hacknet Nodes/Servers this.hacknetNodes = []; + this.hashManager = new HashManager(); //Factions this.factions = []; //Names of all factions player has joined @@ -1391,45 +1395,46 @@ PlayerObject.prototype.startClass = function(costMult, expMult, className) { //Find cost and exp gain per game cycle var cost = 0; var hackExp = 0, strExp = 0, defExp = 0, dexExp = 0, agiExp = 0, chaExp = 0; + const hashManager = this.hashManager; switch (className) { case CONSTANTS.ClassStudyComputerScience: - hackExp = baseStudyComputerScienceExp * expMult / gameCPS; + hackExp = baseStudyComputerScienceExp * expMult / gameCPS * hashManager.getStudyMult(); break; case CONSTANTS.ClassDataStructures: cost = CONSTANTS.ClassDataStructuresBaseCost * costMult / gameCPS; - hackExp = baseDataStructuresExp * expMult / gameCPS; + hackExp = baseDataStructuresExp * expMult / gameCPS * hashManager.getStudyMult(); break; case CONSTANTS.ClassNetworks: cost = CONSTANTS.ClassNetworksBaseCost * costMult / gameCPS; - hackExp = baseNetworksExp * expMult / gameCPS; + hackExp = baseNetworksExp * expMult / gameCPS * hashManager.getStudyMult(); break; case CONSTANTS.ClassAlgorithms: cost = CONSTANTS.ClassAlgorithmsBaseCost * costMult / gameCPS; - hackExp = baseAlgorithmsExp * expMult / gameCPS; + hackExp = baseAlgorithmsExp * expMult / gameCPS * hashManager.getStudyMult(); break; case CONSTANTS.ClassManagement: cost = CONSTANTS.ClassManagementBaseCost * costMult / gameCPS; - chaExp = baseManagementExp * expMult / gameCPS; + chaExp = baseManagementExp * expMult / gameCPS * hashManager.getStudyMult(); break; case CONSTANTS.ClassLeadership: cost = CONSTANTS.ClassLeadershipBaseCost * costMult / gameCPS; - chaExp = baseLeadershipExp * expMult / gameCPS; + chaExp = baseLeadershipExp * expMult / gameCPS * hashManager.getStudyMult(); break; case CONSTANTS.ClassGymStrength: cost = CONSTANTS.ClassGymBaseCost * costMult / gameCPS; - strExp = baseGymExp * expMult / gameCPS; + strExp = baseGymExp * expMult / gameCPS * hashManager.getTrainingMult(); break; case CONSTANTS.ClassGymDefense: cost = CONSTANTS.ClassGymBaseCost * costMult / gameCPS; - defExp = baseGymExp * expMult / gameCPS; + defExp = baseGymExp * expMult / gameCPS * hashManager.getTrainingMult(); break; case CONSTANTS.ClassGymDexterity: cost = CONSTANTS.ClassGymBaseCost * costMult / gameCPS; - dexExp = baseGymExp * expMult / gameCPS; + dexExp = baseGymExp * expMult / gameCPS * hashManager.getTrainingMult(); break; case CONSTANTS.ClassGymAgility: cost = CONSTANTS.ClassGymBaseCost * costMult / gameCPS; - agiExp = baseGymExp * expMult / gameCPS; + agiExp = baseGymExp * expMult / gameCPS * hashManager.getTrainingMult(); break; default: throw new Error("ERR: Invalid/unrecognized class name"); diff --git a/src/RedPill.js b/src/RedPill.js index 43a913c90..5420e43c5 100644 --- a/src/RedPill.js +++ b/src/RedPill.js @@ -213,9 +213,7 @@ function loadBitVerse(destroyedBitNodeNum, flume=false) { var elemId = "bitnode-" + i.toString(); var elem = clearEventListeners(elemId); if (elem == null) {return;} - if (i === 1 || i === 2 || i === 3 || i === 4 || i === 5 || - i === 6 || i === 7 || i === 8 || i === 10 || i === 11 || - i === 12) { + if (i >= 1 && i <= 12) { elem.addEventListener("click", function() { var bitNodeKey = "BitNode" + i; var bitNode = BitNodes[bitNodeKey]; diff --git a/src/SaveObject.js b/src/SaveObject.js index 5edc37780..d382765c9 100755 --- a/src/SaveObject.js +++ b/src/SaveObject.js @@ -10,7 +10,8 @@ import { processPassiveFactionRepGain } from "./Faction/FactionHelpers"; import { loadFconf } from "./Fconf/Fconf"; import { FconfSettings } from "./Fconf/FconfSettings"; import {loadAllGangs, AllGangs} from "./Gang"; -import {processAllHacknetNodeEarnings} from "./HacknetNode"; +import { hasHacknetServers, + processHacknetEarnings } from "./Hacknet/HacknetHelpers"; import { loadMessages, initMessages, Messages } from "./Message/MessageHelpers"; import {Player, loadPlayer} from "./Player"; import { loadAllRunningScripts } from "./Script/ScriptHelpers"; @@ -490,7 +491,10 @@ function loadImportedGame(saveObj, saveString) { } //Hacknet Nodes offline progress - var offlineProductionFromHacknetNodes = processAllHacknetNodeEarnings(numCyclesOffline); + var offlineProductionFromHacknetNodes = processHacknetEarnings(numCyclesOffline); + const hacknetProdInfo = hasHacknetServers() ? + `${numeralWrapper.format(offlineProductionFromHacknetNodes, "0.000a")} hashes` : + `${numeralWrapper.formatMoney(offlineProductionFromHacknetNodes)}`; //Passive faction rep gain offline processPassiveFactionRepGain(numCyclesOffline); @@ -515,8 +519,8 @@ function loadImportedGame(saveObj, saveString) { const timeOfflineString = convertTimeMsToTimeElapsedString(time); dialogBoxCreate(`Offline for ${timeOfflineString}. While you were offline, your scripts ` + "generated " + - numeralWrapper.formatMoney(offlineProductionFromScripts) + " and your Hacknet Nodes generated " + - numeralWrapper.formatMoney(offlineProductionFromHacknetNodes) + ""); + numeralWrapper.formatMoney(offlineProductionFromScripts) + "
    " + + "and your Hacknet Nodes generated " + hacknetProdInfo + ""); return true; } diff --git a/src/Server/BaseServer.ts b/src/Server/BaseServer.ts new file mode 100644 index 000000000..b09ff9534 --- /dev/null +++ b/src/Server/BaseServer.ts @@ -0,0 +1,206 @@ +/** + * Abstract Base Class for any Server object + */ +import { CodingContract } from "../CodingContracts"; +import { Message } from "../Message/Message"; +import { RunningScript } from "../Script/RunningScript"; +import { Script } from "../Script/Script"; +import { TextFile } from "../TextFile"; + +import { isScriptFilename } from "../Script/ScriptHelpersTS"; + +import { createRandomIp } from "../../utils/IPAddress"; + +interface IConstructorParams { + adminRights?: boolean; + hostname: string; + ip?: string; + isConnectedTo?: boolean; + maxRam?: number; + organizationName?: string; +} + +export abstract class BaseServer { + // Coding Contract files on this server + contracts: CodingContract[] = []; + + // How many CPU cores this server has. Maximum of 8. + // Currently, this only affects hacking missions + cpuCores: number = 1; + + // Flag indicating whether the FTP port is open + ftpPortOpen: boolean = false; + + // Flag indicating whether player has admin/root access to this server + hasAdminRights: boolean = false; + + // Hostname. Must be unique + hostname: string = ""; + + // Flag indicating whether HTTP Port is open + httpPortOpen: boolean = false; + + // IP Address. Must be unique + ip: string = ""; + + // Flag indicating whether player is curently connected to this server + isConnectedTo: boolean = false; + + // RAM (GB) available on this server + maxRam: number = 0; + + // Message files AND Literature files on this Server + // For Literature files, this array contains only the filename (string) + // For Messages, it contains the actual Message object + // TODO Separate literature files into its own property + messages: (Message | string)[] = []; + + // Name of company/faction/etc. that this server belongs to. + // Optional, not applicable to all Servers + organizationName: string = ""; + + // Programs on this servers. Contains only the names of the programs + programs: string[] = []; + + // RAM (GB) used. i.e. unavailable RAM + ramUsed: number = 0; + + // RunningScript files on this server + runningScripts: RunningScript[] = []; + + // Script files on this Server + scripts: Script[] = []; + + // Contains the IP Addresses of all servers that are immediately + // reachable from this one + serversOnNetwork: string[] = []; + + // Flag indicating whether SMTP Port is open + smtpPortOpen: boolean = false; + + // Flag indicating whether SQL Port is open + sqlPortOpen: boolean = false; + + // Flag indicating whether the SSH Port is open + sshPortOpen: boolean = false; + + // Text files on this server + textFiles: TextFile[] = []; + + constructor(params: IConstructorParams={ hostname: "", ip: createRandomIp() }) { + this.ip = params.ip ? params.ip : createRandomIp(); + + this.hostname = params.hostname; + this.organizationName = params.organizationName != null ? params.organizationName : ""; + this.isConnectedTo = params.isConnectedTo != null ? params.isConnectedTo : false; + + //Access information + this.hasAdminRights = params.adminRights != null ? params.adminRights : false; + } + + addContract(contract: CodingContract) { + this.contracts.push(contract); + } + + getContract(contractName: string): CodingContract | null { + for (const contract of this.contracts) { + if (contract.fn === contractName) { + return contract; + } + } + return null; + } + + // Given the name of the script, returns the corresponding + // script object on the server (if it exists) + getScript(scriptName: string): Script | null { + for (let i = 0; i < this.scripts.length; i++) { + if (this.scripts[i].filename === scriptName) { + return this.scripts[i]; + } + } + + return null; + } + + removeContract(contract: CodingContract) { + if (contract instanceof CodingContract) { + this.contracts = this.contracts.filter((c) => { + return c.fn !== contract.fn; + }); + } else { + this.contracts = this.contracts.filter((c) => { + return c.fn !== contract; + }); + } + } + + /** + * Called when a script is run on this server. + * All this function does is add a RunningScript object to the + * `runningScripts` array. It does NOT check whether the script actually can + * be run. + */ + runScript(script: RunningScript): void { + this.runningScripts.push(script); + } + + setMaxRam(ram: number): void { + this.maxRam = ram; + } + + /** + * Write to a script file + * Overwrites existing files. Creates new files if the script does not eixst + */ + writeToScriptFile(fn: string, code: string) { + var ret = {success: false, overwritten: false}; + if (!isScriptFilename(fn)) { return ret; } + + //Check if the script already exists, and overwrite it if it does + for (let i = 0; i < this.scripts.length; ++i) { + if (fn === this.scripts[i].filename) { + let script = this.scripts[i]; + script.code = code; + script.updateRamUsage(); + script.module = ""; + ret.overwritten = true; + ret.success = true; + return ret; + } + } + + //Otherwise, create a new script + const newScript = new Script(); + newScript.filename = fn; + newScript.code = code; + newScript.updateRamUsage(); + newScript.server = this.ip; + this.scripts.push(newScript); + ret.success = true; + return ret; + } + + // Write to a text file + // Overwrites existing files. Creates new files if the text file does not exist + writeToTextFile(fn: string, txt: string) { + var ret = { success: false, overwritten: false }; + if (!fn.endsWith("txt")) { return ret; } + + //Check if the text file already exists, and overwrite if it does + for (let i = 0; i < this.textFiles.length; ++i) { + if (this.textFiles[i].fn === fn) { + ret.overwritten = true; + this.textFiles[i].text = txt; + ret.success = true; + return ret; + } + } + + //Otherwise create a new text file + var newFile = new TextFile(fn, txt); + this.textFiles.push(newFile); + ret.success = true; + return ret; + } +} diff --git a/src/Server/Server.ts b/src/Server/Server.ts index bc3056d14..f11398c37 100644 --- a/src/Server/Server.ts +++ b/src/Server/Server.ts @@ -1,16 +1,12 @@ -// Class representing a single generic Server +// Class representing a single hackable Server +import { BaseServer } from "./BaseServer"; // TODO This import is a circular import. Try to fix it in the future import { GetServerByHostname } from "./ServerHelpers"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; -import { CodingContract } from "../CodingContracts"; -import { Message } from "../Message/Message"; -import { RunningScript } from "../Script/RunningScript"; -import { Script } from "../Script/Script"; -import { isScriptFilename } from "../Script/ScriptHelpersTS"; -import { TextFile } from "../TextFile"; +import { createRandomString } from "../utils/createRandomString"; import { createRandomIp } from "../../utils/IPAddress"; import { Generic_fromJSON, Generic_toJSON, @@ -31,7 +27,7 @@ interface IConstructorParams { serverGrowth?: number; } -export class Server { +export class Server extends BaseServer { // Initializes a Server Object from a JSON save state static fromJSON(value: any): Server { return Generic_fromJSON(Server, value.data); @@ -41,47 +37,13 @@ export class Server { // (i.e. security level when the server was created) baseDifficulty: number = 1; - // Coding Contract files on this server - contracts: CodingContract[] = []; - - // How many CPU cores this server has. Maximum of 8. - // Currently, this only affects hacking missions - cpuCores: number = 1; - - // Flag indicating whether the FTP port is open - ftpPortOpen: boolean = false; - // Server Security Level hackDifficulty: number = 1; - // Flag indicating whether player has admin/root access to this server - hasAdminRights: boolean = false; - - // Hostname. Must be unique - hostname: string = ""; - - // Flag indicating whether HTTP Port is open - httpPortOpen: boolean = false; - - // IP Address. Must be unique - ip: string = ""; - - // Flag indicating whether player is curently connected to this server - isConnectedTo: boolean = false; - // Flag indicating whether this server has been manually hacked (ie. // hacked through Terminal) by the player manuallyHacked: boolean = false; - // RAM (GB) available on this server - maxRam: number = 0; - - // Message files AND Literature files on this Server - // For Literature files, this array contains only the filename (string) - // For Messages, it contains the actual Message object - // TODO Separate literature files into its own property - messages: (Message | string)[] = []; - // Minimum server security level that this server can be weakened to minDifficulty: number = 1; @@ -97,67 +59,35 @@ export class Server { // How many ports are currently opened on the server openPortCount: number = 0; - // Name of company/faction/etc. that this server belongs to. - // Optional, not applicable to all Servers - organizationName: string = ""; - - // Programs on this servers. Contains only the names of the programs - programs: string[] = []; - // Flag indicating wehther this is a purchased server purchasedByPlayer: boolean = false; - // RAM (GB) used. i.e. unavailable RAM - ramUsed: number = 0; - // Hacking level required to hack this server requiredHackingSkill: number = 1; - // RunningScript files on this server - runningScripts: RunningScript[] = []; - - // Script files on this Server - scripts: Script[] = []; - // Parameter that affects how effectively this server's money can // be increased using the grow() Netscript function serverGrowth: number = 1; - // Contains the IP Addresses of all servers that are immediately - // reachable from this one - serversOnNetwork: string[] = []; - - // Flag indicating whether SMTP Port is open - smtpPortOpen: boolean = false; - - // Flag indicating whether SQL Port is open - sqlPortOpen: boolean = false; - - // Flag indicating whether the SSH Port is open - sshPortOpen: boolean = false; - - // Text files on this server - textFiles: TextFile[] = []; - constructor(params: IConstructorParams={hostname: "", ip: createRandomIp() }) { - /* Properties */ - //Connection information - this.ip = params.ip ? params.ip : createRandomIp(); + super(params); - var hostname = params.hostname; - var i = 0; - var suffix = ""; - while (GetServerByHostname(hostname+suffix) != null) { - //Server already exists - suffix = "-" + i; - ++i; + // "hacknet-node-X" hostnames are reserved for Hacknet Servers + if (this.hostname.startsWith("hacknet-node-")) { + this.hostname = createRandomString(10); + } + + // Validate hostname by ensuring there are no repeats + if (GetServerByHostname(this.hostname) != null) { + // Use a for loop to ensure that we don't get suck in an infinite loop somehow + let hostname: string = this.hostname; + for (let i = 0; i < 200; ++i) { + hostname = `${this.hostname}-${i}`; + if (GetServerByHostname(hostname) == null) { break; } + } + this.hostname = hostname; } - this.hostname = hostname + suffix; - this.organizationName = params.organizationName != null ? params.organizationName : ""; - this.isConnectedTo = params.isConnectedTo != null ? params.isConnectedTo : false; - //Access information - this.hasAdminRights = params.adminRights != null ? params.adminRights : false; this.purchasedByPlayer = params.purchasedByPlayer != null ? params.purchasedByPlayer : false; //RAM, CPU speed and Scripts @@ -178,23 +108,9 @@ export class Server { this.numOpenPortsRequired = params.numOpenPortsRequired != null ? params.numOpenPortsRequired : 5; }; - setMaxRam(ram: number): void { - this.maxRam = ram; - } - - // Given the name of the script, returns the corresponding - // script object on the server (if it exists) - getScript(scriptName: string): Script | null { - for (let i = 0; i < this.scripts.length; i++) { - if (this.scripts[i].filename === scriptName) { - return this.scripts[i]; - } - } - - return null; - } - - // Ensures that the server's difficulty (server security) doesn't get too high + /** + * Ensures that the server's difficulty (server security) doesn't get too high + */ capDifficulty(): void { if (this.hackDifficulty < this.minDifficulty) {this.hackDifficulty = this.minDifficulty;} if (this.hackDifficulty < 1) {this.hackDifficulty = 1;} @@ -204,97 +120,54 @@ export class Server { if (this.hackDifficulty > 1000000) {this.hackDifficulty = 1000000;} } - // Strengthens a server's security level (difficulty) by the specified amount + /** + * Change this server's minimum security + * @param n - Value by which to increase/decrease the server's minimum security + * @param perc - Whether it should be changed by a percentage, or a flat value + */ + changeMinimumSecurity(n: number, perc: boolean=false): void { + if (perc) { + this.minDifficulty *= n; + } else { + this.minDifficulty += n; + } + + // Server security cannot go below 1 + this.minDifficulty = Math.max(1, this.minDifficulty); + } + + /** + * Strengthens a server's security level (difficulty) by the specified amount + */ fortify(amt: number): void { this.hackDifficulty += amt; this.capDifficulty(); } - // Lowers the server's security level (difficulty) by the specified amount) + /** + * Change this server's maximum money + * @param n - Value by which to change the server's maximum money + * @param perc - Whether it should be changed by a percentage, or a flat value + */ + changeMaximumMoney(n: number, perc: boolean=false): void { + if (perc) { + this.moneyMax *= n; + } else { + this.moneyMax += n; + } + } + + /** + * Lowers the server's security level (difficulty) by the specified amount) + */ weaken(amt: number): void { this.hackDifficulty -= (amt * BitNodeMultipliers.ServerWeakenRate); this.capDifficulty(); } - // Write to a script file - // Overwrites existing files. Creates new files if the script does not eixst - writeToScriptFile(fn: string, code: string) { - var ret = {success: false, overwritten: false}; - if (!isScriptFilename(fn)) { return ret; } - - //Check if the script already exists, and overwrite it if it does - for (let i = 0; i < this.scripts.length; ++i) { - if (fn === this.scripts[i].filename) { - let script = this.scripts[i]; - script.code = code; - script.updateRamUsage(); - script.module = ""; - ret.overwritten = true; - ret.success = true; - return ret; - } - } - - //Otherwise, create a new script - const newScript = new Script(); - newScript.filename = fn; - newScript.code = code; - newScript.updateRamUsage(); - newScript.server = this.ip; - this.scripts.push(newScript); - ret.success = true; - return ret; - } - - // Write to a text file - // Overwrites existing files. Creates new files if the text file does not exist - writeToTextFile(fn: string, txt: string) { - var ret = { success: false, overwritten: false }; - if (!fn.endsWith("txt")) { return ret; } - - //Check if the text file already exists, and overwrite if it does - for (let i = 0; i < this.textFiles.length; ++i) { - if (this.textFiles[i].fn === fn) { - ret.overwritten = true; - this.textFiles[i].text = txt; - ret.success = true; - return ret; - } - } - - //Otherwise create a new text file - var newFile = new TextFile(fn, txt); - this.textFiles.push(newFile); - ret.success = true; - return ret; - } - - addContract(contract: CodingContract) { - this.contracts.push(contract); - } - - removeContract(contract: CodingContract) { - if (contract instanceof CodingContract) { - this.contracts = this.contracts.filter((c) => { - return c.fn !== contract.fn; - }); - } else { - this.contracts = this.contracts.filter((c) => { - return c.fn !== contract; - }); - } - } - - getContract(contractName: string) { - for (const contract of this.contracts) { - if (contract.fn === contractName) { - return contract; - } - } - return null; - } - - // Serialize the current object to a JSON save state + /** + * Serialize the current object to a JSON save state + */ toJSON(): any { return Generic_toJSON("Server", this); } diff --git a/src/Terminal.js b/src/Terminal.js index d2744b2c9..08595bc5a 100644 --- a/src/Terminal.js +++ b/src/Terminal.js @@ -19,6 +19,7 @@ import {calculateHackingChance, calculateHackingTime, calculateGrowTime, calculateWeakenTime} from "./Hacking"; +import { HacknetServer } from "./Hacknet/HacknetServer"; import {TerminalHelpText, HelpTexts} from "./HelpText"; import {iTutorialNextStep, iTutorialSteps, ITutorial} from "./InteractiveTutorial"; @@ -760,18 +761,19 @@ let Terminal = { finishAnalyze: function(cancelled = false) { if (cancelled == false) { let currServ = Player.getCurrentServer(); + const isHacknet = currServ instanceof HacknetServer; post(currServ.hostname + ": "); post("Organization name: " + currServ.organizationName); var rootAccess = ""; if (currServ.hasAdminRights) {rootAccess = "YES";} else {rootAccess = "NO";} post("Root Access: " + rootAccess); - post("Required hacking skill: " + currServ.requiredHackingSkill); + if (!isHacknet) { post("Required hacking skill: " + currServ.requiredHackingSkill); } post("Server security level: " + numeralWrapper.format(currServ.hackDifficulty, '0.000a')); post("Chance to hack: " + numeralWrapper.format(calculateHackingChance(currServ), '0.00%')); post("Time to hack: " + numeralWrapper.format(calculateHackingTime(currServ), '0.000') + " seconds"); post("Total money available on server: " + numeralWrapper.format(currServ.moneyAvailable, '$0,0.00')); - post("Required number of open ports for NUKE: " + currServ.numOpenPortsRequired); + if (!isHacknet) { post("Required number of open ports for NUKE: " + currServ.numOpenPortsRequired); } if (currServ.sshPortOpen) { post("SSH port: Open") @@ -2240,7 +2242,7 @@ let Terminal = { post("May take a few seconds to start up the process..."); var runningScriptObj = new RunningScript(script, args); runningScriptObj.threads = numThreads; - server.runningScripts.push(runningScriptObj); + server.runScript(runningScriptObj, Player); addWorkerScript(runningScriptObj, server); return; diff --git a/src/engine.jsx b/src/engine.jsx index 87d8fffe4..b1e5064e2 100644 --- a/src/engine.jsx +++ b/src/engine.jsx @@ -30,8 +30,10 @@ import { FconfSettings } from "./Fconf/FconfSetti import {displayLocationContent, initLocationButtons} from "./Location"; import {Locations} from "./Locations"; -import {displayHacknetNodesContent, processAllHacknetNodeEarnings, - updateHacknetNodesContent} from "./HacknetNode"; +import { hasHacknetServers, + renderHacknetNodesUI, + clearHacknetNodesUI, + processHacknetEarnings } from "./Hacknet/HacknetHelpers"; import {iTutorialStart} from "./InteractiveTutorial"; import {initLiterature} from "./Literature"; import { checkForMessagesToSend, initMessages } from "./Message/MessageHelpers"; @@ -109,6 +111,7 @@ import "../css/mainmenu.scss"; import "../css/characteroverview.scss"; import "../css/terminal.scss"; import "../css/scripteditor.scss"; +import "../css/hacknetnodes.scss"; import "../css/menupages.scss"; import "../css/redpill.scss"; import "../css/stockmarket.scss"; @@ -294,8 +297,8 @@ const Engine = { loadHacknetNodesContent: function() { Engine.hideAllContent(); Engine.Display.hacknetNodesContent.style.display = "block"; - displayHacknetNodesContent(); routing.navigateTo(Page.HacknetNodes); + renderHacknetNodesUI(); MainMenuLinks.HacknetNodes.classList.add("active"); }, @@ -506,7 +509,7 @@ const Engine = { Engine.Display.characterContent.style.display = "none"; Engine.Display.scriptEditorContent.style.display = "none"; Engine.Display.activeScriptsContent.style.display = "none"; - Engine.Display.hacknetNodesContent.style.display = "none"; + clearHacknetNodesUI(); Engine.Display.worldContent.style.display = "none"; Engine.Display.createProgramContent.style.display = "none"; Engine.Display.factionsContent.style.display = "none"; @@ -870,7 +873,7 @@ const Engine = { updateOnlineScriptTimes(numCycles); //Hacknet Nodes - processAllHacknetNodeEarnings(numCycles); + processHacknetEarnings(numCycles); }, //Counters for the main event loop. Represent the number of game cycles are required @@ -932,7 +935,7 @@ const Engine = { if (Engine.Counters.updateDisplays <= 0) { Engine.displayCharacterOverviewInfo(); if (routing.isOn(Page.HacknetNodes)) { - updateHacknetNodesContent(); + renderHacknetNodesUI(); } else if (routing.isOn(Page.CreateProgram)) { displayCreateProgramContent(); } else if (routing.isOn(Page.Sleeves)) { @@ -1183,7 +1186,10 @@ const Engine = { } //Hacknet Nodes offline progress - var offlineProductionFromHacknetNodes = processAllHacknetNodeEarnings(numCyclesOffline); + var offlineProductionFromHacknetNodes = processHacknetEarnings(numCyclesOffline); + const hacknetProdInfo = hasHacknetServers() ? + `${numeralWrapper.format(offlineProductionFromHacknetNodes, "0.000a")} hashes` : + `${numeralWrapper.formatMoney(offlineProductionFromHacknetNodes)}`; //Passive faction rep gain offline processPassiveFactionRepGain(numCyclesOffline); @@ -1237,8 +1243,8 @@ const Engine = { const timeOfflineString = convertTimeMsToTimeElapsedString(time); dialogBoxCreate(`Offline for ${timeOfflineString}. While you were offline, your scripts ` + "generated " + - numeralWrapper.formatMoney(offlineProductionFromScripts) + " and your Hacknet Nodes generated " + - numeralWrapper.formatMoney(offlineProductionFromHacknetNodes) + ""); + numeralWrapper.formatMoney(offlineProductionFromScripts) + "
    " + + "and your Hacknet Nodes generated " + hacknetProdInfo + ""); //Close main menu accordions for loaded game var visibleMenuTabs = [terminal, createScript, activeScripts, stats, hacknetnodes, city, tutorial, options, dev]; diff --git a/src/index.html b/src/index.html index d07c437f7..885c2dce4 100644 --- a/src/index.html +++ b/src/index.html @@ -203,35 +203,7 @@ if (htmlWebpackPlugin.options.googleAnalytics.trackingId) { %>
    -

    Hacknet Nodes

    - - Purchase Hacknet Node -
    -
    -

    - Money:
    - Total Hacknet Node Production: -

    - - x1 - x5 - x10 - MAX - -
    -
      -
    +
    diff --git a/src/ui/React/ServerDropdown.jsx b/src/ui/React/ServerDropdown.jsx new file mode 100644 index 000000000..81c25d3b6 --- /dev/null +++ b/src/ui/React/ServerDropdown.jsx @@ -0,0 +1,65 @@ +/** + * Creates a dropdown (select HTML element) with server hostnames as options + * + * Configurable to only contain certain types of servers + */ +import React from "react"; +import { AllServers } from "../../Server/AllServers"; + +import { HacknetServer } from "../../Hacknet/HacknetServer"; + +// TODO make this an enum when this gets converted to TypeScript +export const ServerType = { + All: 0, + Foreign: 1, // Hackable, non-owned servers + Owned: 2, // Home Computer, Purchased Servers, and Hacknet Servers + Purchased: 3, // Everything from Owned except home computer +} + +export class ServerDropdown extends React.Component { + /** + * Checks if the server should be shown in the dropdown menu, based on the + * 'serverType' property + */ + isValidServer(s) { + const type = this.props.serverType; + switch (type) { + case ServerType.All: + return true; + case ServerType.Foreign: + return (s.hostname !== "home" && !s.purchasedByPlayer); + case ServerType.Owned: + return s.purchasedByPlayer || (s instanceof HacknetServer) || s.hostname === "home"; + case ServerType.Purchased: + return s.purchasedByPlayer || (s instanceof HacknetServer); + default: + console.warn(`Invalid ServerType specified for ServerDropdown component: ${type}`); + return false; + } + } + + /** + * Given a Server object, creates a Option element + */ + renderOption(s) { + return ( + + ) + } + + render() { + const servers = []; + for (const serverName in AllServers) { + const server = AllServers[serverName]; + if (this.isValidServer(server)) { + servers.push(this.renderOption(server)); + } + } + + return ( + + ) + } +} diff --git a/src/ui/React/createPopup.ts b/src/ui/React/createPopup.ts new file mode 100644 index 000000000..2974549fb --- /dev/null +++ b/src/ui/React/createPopup.ts @@ -0,0 +1,56 @@ +/** + * Create a pop-up dialog box using React. + * + * Calling this function with the same ID and React Root Component will trigger a re-render + * + * @param id The (hopefully) unique identifier for the popup container + * @param rootComponent Root React Component + */ +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +import { createElement } from "../../../utils/uiHelpers/createElement"; +import { removeElementById } from "../../../utils/uiHelpers/removeElementById"; + +type ReactComponent = new(...args: any[]) => React.Component; + +export function createPopup(id: string, rootComponent: ReactComponent, props: object): HTMLElement { + let container = document.getElementById(id); + let content = document.getElementById(`${id}-content`); + if (container == null || content == null) { + container = createElement("div", { + class: "popup-box-container", + display: "flex", + id: id, + }); + + content = createElement("div", { + class: "popup-box-content", + id: `${id}-content`, + }); + + container.appendChild(content); + + try { + document.getElementById("entire-game-container")!.appendChild(container); + } catch(e) { + console.error(`Exception caught when creating popup: ${e}`); + } + } + + ReactDOM.render(React.createElement(rootComponent, props), content); + + return container; +} + +/** + * Closes a popup created with the createPopup() function above + */ +export function removePopup(id: string): void { + let content = document.getElementById(`${id}-content`); + if (content == null) { return; } + + ReactDOM.unmountComponentAtNode(content); + + removeElementById(id); +} diff --git a/src/utils/createRandomString.ts b/src/utils/createRandomString.ts new file mode 100644 index 000000000..d963e8a41 --- /dev/null +++ b/src/utils/createRandomString.ts @@ -0,0 +1,12 @@ +// Function that generates a random gibberish string of length n +const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + +export function createRandomString(n: number): string { + let str: string = ""; + + for (let i = 0; i < n; ++i) { + str += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return str; +}