diff --git a/css/menupages.css b/css/menupages.css
index 802dc40da..910997e76 100644
--- a/css/menupages.css
+++ b/css/menupages.css
@@ -116,10 +116,43 @@
height: 100%;
margin-left: 10%;
width: 99%;
+ overflow-y: scroll;
}
-.hacknet-node li {
+#hacknet-nodes-container li{
+ padding: 6px;
+ margin: 6px;
+}
+
+#hacknet-nodes-text,
+#hacknet-nodes-money {
+ width: 80%;
+ margin: 10px;
+ padding: 10px;
+}
+
+.hacknet-node {
+ margin: 6px;
+ padding: 6px;
+ border: 2px solid white;
+ -webkit-box-shadow:
+ inset 0 0 8px rgba(0,0,0,0.1),
+ 0 0 16px rgba(0,0,0,0.1);
+ -moz-box-shadow:
+ inset 0 0 8px rgba(0,0,0,0.1),
+ 0 0 16px rgba(0,0,0,0.1);
+ box-shadow:
+ inset 0 0 8px rgba(0,0,0,0.1),
+ 0 0 16px rgba(0,0,0,0.1);
+}
+
+.hacknet-node-button-div a {
+ display: block;
+}
+
+.hacknet-node-button-div:not(:last-child) {
+ border-bottom: none;
}
/* World */
diff --git a/css/styles.css b/css/styles.css
index 9975f581e..d8fbc6c5e 100644
--- a/css/styles.css
+++ b/css/styles.css
@@ -22,6 +22,7 @@ h2 {
}
ul {
+ padding: 6px;
list-style-type: none;
}
@@ -87,10 +88,14 @@ span {
border-left: 1px solid #333333;
}
+.a-link-button:hover {
+ background-color: #666;
+}
+
/* Make anchor tags ("a" elements) inactive (not clickable) */
.a-link-button-inactive {
text-decoration: none;
- background-color: #555;
+ background-color: #333;
color: #FFFFFF;
padding: 6px;
margin: 6px;
diff --git a/index.html b/index.html
index bc64ebd14..804bc32eb 100644
--- a/index.html
+++ b/index.html
@@ -73,6 +73,10 @@
+
+
+
+
@@ -140,8 +144,22 @@
+
Hacknet Nodes
+
+ 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.
+
+ Here, you can purchase a Hacknet Node, a specialized machine that can connect and contribute its
+ resources to the Hacknet network. 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.
+
+ 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.
+
+
Purchase Hacknet Node
+
diff --git a/src/Constants.js b/src/Constants.js
index e8ab15b27..451ed2d60 100644
--- a/src/Constants.js
+++ b/src/Constants.js
@@ -8,8 +8,18 @@ CONSTANTS = {
//How much reputation is needed to join a megacorporation's faction
CorpFactionRepRequirement: 250000,
- //Base cost for 1GB of RAM
- BaseCostFor1GBOfRam: 50000,
+ /* Base costs */
+ BaseCostFor1GBOfRam: 50000, //1 GB of RAM
+
+ BaseCostForHacknetNode: 1000,
+ BaseCostForHacknetNodeCore: 1000000,
+
+ /* Hacknet Node constants */
+ HacknetNodeMoneyGainPerLevel: 0.25,
+ HacknetNodePurchaseNextMult: 1.2; //Multiplier when purchasing an additional hacknet node
+ HacknetNodeUpgradeLevelMult: 1.08, //Multiplier for cost when upgrading level
+ HacknetNodeUpgradeRamMult: 1.2, //Multiplier for cost when upgrading RAM
+ HacknetNodeUpgradeCoreMult: 1.5, //Multiplier for cost when buying another core
/* Script related things */
//Time (ms) it takes to run one operation in Netscript.
diff --git a/src/HacknetNode.js b/src/HacknetNode.js
index 37349f1d0..2c38da252 100644
--- a/src/HacknetNode.js
+++ b/src/HacknetNode.js
@@ -1,7 +1,7 @@
/* HacknetNode.js */
function HacknetNode(name) {
this.level = 1;
- this.ram = 0; //GB
+ this.ram = 1; //GB
this.numCores = 1;
this.name = name;
@@ -14,17 +14,19 @@ function HacknetNode(name) {
HacknetNode.prototype.updateMoneyGainRate = function() {
//How much extra $/s is gained per level
- var gainPerLevel = 0.25;
+ var gainPerLevel = CONSTANTS.HacknetNodeMoneyGainPerLevel;
//Each CPU core doubles the speed. Every 1GB of ram adds 10% increase
- this.moneyGainRatePerSecond = (this.level * gainPerLevel) * Math.pow(1.1, ram) * this.numCores;
+ this.moneyGainRatePerSecond = (this.level * gainPerLevel) * Math.pow(1.1, this.ram-1) * this.numCores;
+ if (isNaN(this.moneyGainRatePerSecond)) {
+ throw new Error("Money gain rate calculated for Hacknet Node is NaN");
+ }
}
HacknetNode.prototype.calculateLevelUpgradeCost = function() {
//Upgrade cost = Base cost * multiplier ^ level
- var baseCost = 10000;
- var mult = 1.08;
- return baseCost * Math.pow(mult, this.level);
+ var mult = CONSTANTS.HacknetNodeUpgradeLevelMult;
+ return CONSTANTS.BaseCostForHacknetNode * Math.pow(mult, this.level);
}
HacknetNode.prototype.purchaseLevelUpgrade = function() {
@@ -32,6 +34,7 @@ HacknetNode.prototype.purchaseLevelUpgrade = function() {
if (cost > Player.money) {return;}
Player.loseMoney(cost);
++this.level;
+ this.updateMoneyGainRate();
}
HacknetNode.prototype.calculateRamUpgradeCost = function() {
@@ -40,8 +43,8 @@ HacknetNode.prototype.calculateRamUpgradeCost = function() {
//Calculate cost
//Base cost of RAM is 50k per 1GB...but lets have this increase by 10% for every time
//the RAM has been upgraded
- var cost = currentRam * CONSTANTS.BaseCostFor1GBOfRam;
- var mult = Math.pow(1.1, numUpgrades);
+ var cost = this.ram * CONSTANTS.BaseCostFor1GBOfRam;
+ var mult = Math.pow(CONSTANTS.HacknetNodeUpgradeRamMult, numUpgrades);
return cost * mult;
}
@@ -50,12 +53,13 @@ HacknetNode.prototype.purchaseRamUpgrade = function() {
if (cost > Player.money) {return;}
Player.loseMoney(cost);
this.ram *= 2; //Ram is always doubled
+ this.updateMoneyGainRate();
}
HacknetNode.prototype.calculateCoreUpgradeCost = function() {
- var coreBaseCost = 1000000;
- var mult = 1.5;
- return coreBaseCost * Math.pow(mult, this.numCores);
+ var coreBaseCost = CONSTANTS.BaseCostForHacknetNodeCore;
+ var mult = CONSTANTS.HacknetNodeUpgradeCoreMult;
+ return coreBaseCost * Math.pow(mult, this.numCores-1);
}
HacknetNode.prototype.purchaseCoreUpgrade = function() {
@@ -63,6 +67,7 @@ HacknetNode.prototype.purchaseCoreUpgrade = function() {
if (cost > Player.money) {return;}
Player.loseMoney(cost);
++this.numCores;
+ this.updateMoneyGainRate();
}
/* Saving and loading HackNets */
@@ -74,54 +79,199 @@ HacknetNode.fromJSON = function(value) {
return Generic_fromJSON(HacknetNode, value.data);
}
+Reviver.constructors.HacknetNode = HacknetNode;
+
+
+
+purchaseHacknet = function() {
+ var cost = getCostOfNextHacknetNode();
+ if (cost > Player.money) {
+ dialogBoxCreate("You cannot afford to purchase a Hacknet Node!");
+ return;
+ }
+
+ //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);
+
+ displayHacknetNodesContent();
+}
+
+getCostOfNextHacknetNode = function() {
+ //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);
+}
+
+//Creates Hacknet Node DOM elements when the page is opened
+displayHacknetNodesContent = function() {
+ //Update Hacknet Nodes button
+ var purchaseButton = document.getElementById("hacknet-nodes-purchase-button");
+ var newPurchaseButton = purchaseButton.cloneNode(true);
+ purchaseButton.parentNode.replaceChild(newPurchaseButton, purchaseButton);
+
+ newPurchaseButton.addEventListener("click", function() {
+ purchaseHacknet();
+ return false;
+ });
+
+ //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]);
+ }
+ updateHacknetNodesContent();
+}
+
+//Update information on all Hacknet Node DOM elements
+updateHacknetNodesContent = function() {
+ //Set purchase button to inactive if not enough money, and update its price display
+ var cost = getCostOfNextHacknetNode();
+ var purchaseButton = document.getElementById("hacknet-nodes-purchase-button");
+ purchaseButton.innerHTML = "Purchase Hacknet Node - $" + cost.toFixed(2);
+ if (cost > Player.money) {
+ purchaseButton.setAttribute("class", "a-link-button-inactive");
+ } else {
+ purchaseButton.setAttribute("class", "a-link-button");
+ }
+
+ //Update player's money
+ document.getElementById("hacknet-nodes-money").innerHTML = "Money: $" + Player.money.toFixed(2);
+
+ //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
createHacknetNodeDomElement = function(nodeObj) {
var nodeName = nodeObj.name;
-
- var list = document.getElementById("hacknet-nodes-list");
-
+
var listItem = document.createElement("li");
- item.setAttribute("class", "hacknet-node");
+ listItem.setAttribute("class", "hacknet-node");
var span = document.createElement("span");
- span.style.display = "inline-block";
+ span.style.display = "inline";
+
+ var buttonDiv = document.createElement("div");
+ buttonDiv.setAttribute("class", "hacknet-node-button-div");
//Text
var txt = document.createElement("p");
- txt.setAttribute("id", "hacknet-node-text-" + nodeName);
- txt.innerHTML = "Node name: " + nodeName + "
"
- "Production: " + nodeObj.totalMoneyGenerated +
- " ($" + nodeObj.moneyGainRatePerSecond + ")
" +
- "Level: " + nodeObj.level + "
" +
- "RAM: " + nodeObj.ram + "GB
" +
- "Cores: " + nodeObj.numCores;
+ //txt.setAttribute("id", "hacknet-node-text-" + nodeName);
+ txt.id = "hacknet-node-text-" + nodeName;
//Upgrade buttons
var upgradeLevelButton = document.createElement("a");
var upgradeRamButton = document.createElement("a");
var upgradeCoreButton = document.createElement("a");
- upgradeLevelButton.setAttribute("id", "hacknet-node-upgrade-level-" + nodeName);
+ //upgradeLevelButton.setAttribute("id", "hacknet-node-upgrade-level-" + nodeName);
+ upgradeLevelButton.id = "hacknet-node-upgrade-level-" + nodeName;
upgradeLevelButton.setAttribute("class", "a-link-button-inactive");
- upgradeRamButton.setAttribute("id", "hacknet-node-upgrade-ram-" + nodeName);
+ upgradeLevelButton.addEventListener("click", function() {
+ nodeObj.purchaseLevelUpgrade();
+ updateHacknetNodesContent();
+ return false;
+ });
+ //upgradeRamButton.setAttribute("id", "hacknet-node-upgrade-ram-" + nodeName);
+ upgradeRamButton.id = "hacknet-node-upgrade-ram-" + nodeName;
upgradeRamButton.setAttribute("class", "a-link-button-inactive");
- upgradeCoreButton.setAttribute("id", "hacknet-node-upgrade-core-" + nodeName);
+ upgradeRamButton.addEventListener("click", function() {
+ nodeObj.purchaseRamUpgrade();
+ updateHacknetNodesContent();
+ return false;
+ });
+ //upgradeCoreButton.setAttribute("id", "hacknet-node-upgrade-core-" + nodeName);
+ upgradeCoreButton.id = "hacknet-node-upgrade-core-" + nodeName;
upgradeCoreButton.setAttribute("class", "a-link-button-inactive");
-
- upgradeLevelButton.innerHTML = "Upgrade Hacknet Node Level";
- upgradeRamButton.innerHTML = "Upgrade Hacknet Node RAM";
- upgradeCoreButton.innerHTML = "Purchase additional CPU Core for Hacknet Node";
+ upgradeCoreButton.addEventListener("click", function() {
+ nodeObj.purchaseCoreUpgrade();
+ updateHacknetNodesContent();
+ return false;
+ });
+
+ //Put all the components together in the li element
+ span.appendChild(txt);
+ buttonDiv.appendChild(upgradeLevelButton);
+ buttonDiv.appendChild(upgradeRamButton);
+ buttonDiv.appendChild(upgradeCoreButton);
+ span.appendChild(buttonDiv);
+ listItem.appendChild(span);
- updateHacknetNodeDomElement(item, nodeObj);
+ document.getElementById("hacknet-nodes-list").appendChild(listItem);
- list.appendChild(item);
+ //Set the text and stuff inside the DOM element
+ updateHacknetNodeDomElement(nodeObj);
}
-updateHacknetNodeDomElement = function(li, nodeObj) {
+//Updates information on a single hacknet node DOM element
+updateHacknetNodeDomElement = function(nodeObj) {
var nodeName = nodeObj.name;
-}
-
-Reviver.constructors.HacknetNode = HacknetNode;
-
-purchaseHacknet = function() {
+ var txt = document.getElementById("hacknet-node-text-" + nodeName);
+ if (txt == null) {throw new Error("Cannot find text element");}
+ txt.innerHTML = "Node name: " + nodeName + "
" +
+ "Production: $" + nodeObj.totalMoneyGenerated.toFixed(2) +
+ " ($" + nodeObj.moneyGainRatePerSecond + " / second)
" +
+ "Level: " + nodeObj.level + "
" +
+ "RAM: " + nodeObj.ram + "GB
" +
+ "Cores: " + nodeObj.numCores;
+
+ var upgradeLevelButton = document.getElementById("hacknet-node-upgrade-level-" + nodeName);
+ if (upgradeLevelButton == null) {throw new Error("Cannot find upgrade level button element");}
+ var upgradeLevelCost = nodeObj.calculateLevelUpgradeCost();
+ upgradeLevelButton.innerHTML = "Upgrade Hacknet Node Level - $" + upgradeLevelCost.toFixed(2);
+ if (upgradeLevelCost > Player.money) {
+ upgradeLevelButton.setAttribute("class", "a-link-button-inactive");
+ } else {
+ upgradeLevelButton.setAttribute("class", "a-link-button");
+ }
+ var upgradeRamButton = document.getElementById("hacknet-node-upgrade-ram-" + nodeName);
+ if (upgradeRamButton == null) {throw new Error("Cannot find upgrade ram button element");}
+ var upgradeRamCost = nodeObj.calculateRamUpgradeCost();
+ upgradeRamButton.innerHTML = "Upgrade Hacknet Node RAM -$" + upgradeRamCost.toFixed(2);
+ if (upgradeRamCost > Player.money) {
+ upgradeRamButton.setAttribute("class", "a-link-button-inactive");
+ } else {
+ upgradeRamButton.setAttribute("class", "a-link-button");
+ }
+
+ var upgradeCoreButton = document.getElementById("hacknet-node-upgrade-core-" + nodeName);
+ if (upgradeCoreButton == null) {throw new Error("Cannot find upgrade cores button element");}
+ var upgradeCoreCost = nodeObj.calculateCoreUpgradeCost();
+ upgradeCoreButton.innerHTML = "Purchase additional CPU Core - $" + upgradeCoreCost.toFixed(2);
+ if (upgradeCoreCost > Player.money) {
+ upgradeCoreButton.setAttribute("class", "a-link-button-inactive");
+ } else {
+ upgradeCoreButton.setAttribute("class", "a-link-button");
+ }
}
+
+processAllHacknetNodeEarnings = function(numCycles) {
+ for (var i = 0; i < Player.hacknetNodes.length; ++i) {
+ processSingleHacknetNodeEarnings(numCycles, Player.hacknetNodes[i]);
+ }
+}
+
+processSingleHacknetNodeEarnings = function(numCycles, nodeObj) {
+ var cyclesPerSecond = 1000 / Engine._idleSpeed;
+ var earningPerCycle = nodeObj.moneyGainRatePerSecond / cyclesPerSecond;
+ if (isNaN(earningPerCycle)) {throw new Error("Calculated Earnings is not a number");}
+ var totalEarnings = numCycles * earningPerCycle;
+ nodeObj.totalMoneyGenerated += totalEarnings;
+ nodeObj.onlineTimeSeconds += (numCycles * (Engine._idleSpeed / 1000));
+ Player.gainMoney(totalEarnings);
+}
\ No newline at end of file
diff --git a/src/Terminal.js b/src/Terminal.js
index 37934bd54..1bbc36127 100644
--- a/src/Terminal.js
+++ b/src/Terminal.js
@@ -297,7 +297,7 @@ var Terminal = {
for (var i = 0; i < Player.getCurrentServer().runningScripts.length; i++) {
if (Player.getCurrentServer().runningScripts[i] == scriptName) {
killWorkerScript(scriptName, Player.getCurrentServer().ip);
- post("Killing " + scriptName + ". May take a few seconds");
+ post("Killing " + scriptName + ". May take up to a few minutes for the scripts to die...");
return;
}
}
diff --git a/src/engine.js b/src/engine.js
index 129592bcb..cb4ede5d1 100644
--- a/src/engine.js
+++ b/src/engine.js
@@ -8,6 +8,7 @@ var Engine = {
characterMainMenuButton: null,
scriptEditorMainMenuButton: null,
activeScriptsMainMenuButton: null,
+ hacknetNodesMainMenuButton: null,
worldMainMenuButton: null,
createProgramMainMenuButton: null,
factionsMainMenuButton: null,
@@ -43,6 +44,7 @@ var Engine = {
characterContent: null,
scriptEditorContent: null,
activeScriptsContent: null,
+ hacknetNodesContent: null,
worldContent: null,
createProgramContent: null,
factionsContent: null,
@@ -66,6 +68,7 @@ var Engine = {
CharacterInfo: "CharacterInfo",
ScriptEditor: "ScriptEditor",
ActiveScripts: "ActiveScripts",
+ HacknetNodes: "HacknetNodes",
World: "World",
CreateProgram: "CreateProgram",
Factions: "Factions",
@@ -202,6 +205,14 @@ var Engine = {
Engine.currentPage = Engine.Page.ActiveScripts;
},
+ loadHacknetNodesContent: function() {
+ Engine.hideAllContent();
+ Engine.Display.hacknetNodesContent.style.visibility = "visible";
+ displayHacknetNodesContent();
+
+ Engine.currentPage = Engine.Page.HacknetNodes;
+ },
+
loadWorldContent: function() {
Engine.hideAllContent();
Engine.Display.worldContent.style.visibility = "visible";
@@ -274,6 +285,7 @@ var Engine = {
Engine.Display.characterContent.style.visibility = "hidden";
Engine.Display.scriptEditorContent.style.visibility = "hidden";
Engine.Display.activeScriptsContent.style.visibility = "hidden";
+ Engine.Display.hacknetNodesContent.style.visibility = "hidden";
Engine.Display.worldContent.style.visibility = "hidden";
Engine.Display.createProgramContent.style.visibility = "hidden";
Engine.Display.factionsContent.style.visibility = "hidden";
@@ -310,6 +322,7 @@ var Engine = {
'Agility: ' + (Player.agility).toLocaleString() + '
' +
'Charisma: ' + (Player.charisma).toLocaleString() + '
' +
'Servers owned: ' + Player.purchasedServers.length + '
' +
+ 'Hacknet Nodes owned: ' + Player.hacknetNodes.length + '
' +
'Hacking experience: ' + (Player.hacking_exp.toFixed(4)).toLocaleString() + '
' +
'Strength experience: ' + (Player.strength_exp.toFixed(4)).toLocaleString() + '
' +
'Defense experience: ' + (Player.defense_exp.toFixed(4)).toLocaleString() + '
' +
@@ -585,6 +598,9 @@ var Engine = {
//Update the running time of all active scripts
updateOnlineScriptTimes(numCycles);
+
+ //Hacknet Nodes
+ processAllHacknetNodeEarnings(numCycles);
},
//Counters for the main event loop. Represent the number of game cycles are required
@@ -592,7 +608,7 @@ var Engine = {
Counters: {
autoSaveCounter: 300, //Autosave every minute
updateSkillLevelsCounter: 10, //Only update skill levels every 2 seconds. Might improve performance
- updateDisplays: 5, //Update displays such as Active Scripts display and character display
+ updateDisplays: 4, //Update displays such as Active Scripts display and character display
serverGrowth: 450, //Process server growth every minute and a half
checkFactionInvitations: 1500, //Check whether you qualify for any faction invitations every 5 minutes
},
@@ -623,9 +639,11 @@ var Engine = {
Engine.updateActiveScriptsItems();
} else if (Engine.currentPage == Engine.Page.CharacterInfo) {
Engine.displayCharacterInfo();
- }
+ } else if (Engine.currentPage == Engine.Page.HacknetNodes) {
+ updateHacknetNodesContent();
+ }
- Engine.Counters.updateDisplays = 5;
+ Engine.Counters.updateDisplays = 4;
}
if (Engine.Counters.serverGrowth <= 0) {
@@ -706,6 +724,12 @@ var Engine = {
return false;
});
+ Engine.Clickables.hacknetNodesMainMenuButton = document.getElementById("hacknet-nodes-menu-link");
+ Engine.Clickables.hacknetNodesMainMenuButton.addEventListener("click", function() {
+ Engine.loadHacknetNodesContent();
+ return false;
+ });
+
Engine.Clickables.worldMainMenuButton = document.getElementById("world-menu-link");
Engine.Clickables.worldMainMenuButton.addEventListener("click", function() {
Engine.loadWorldContent();
@@ -811,6 +835,9 @@ var Engine = {
Engine.Display.activeScriptsContent = document.getElementById("active-scripts-container");
Engine.Display.activeScriptsContent.style.visibility = "hidden";
+ Engine.Display.hacknetNodesContent = document.getElementById("hacknet-nodes-container");
+ Engine.Display.hacknetNodesContent.style.visibility = "hidden";
+
Engine.Display.worldContent = document.getElementById("world-container");
Engine.Display.worldContent.style.visibility = "hidden";
@@ -881,6 +908,9 @@ var Engine = {
Player.work(numCyclesOffline);
}
}
+
+ //Hacknet Nodes
+ processAllHacknetNodeEarnings(numCyclesOffline);
} else {
//No save found, start new game
console.log("Initializing new game");