Added Callback function to installAugmentations(). Implemented Source-File 5. Added player-defined functions to Netscript (not thoroughly tested). Began working on Hacking Mission Enemy 'AI'

This commit is contained in:
danielyxie 2017-09-29 10:02:33 -05:00
parent 1233d487d9
commit 26fe9eb519
11 changed files with 5230 additions and 4892 deletions

@ -1,16 +1,26 @@
/* css for Missions */ /* css for Missions */
/* Hacking missions */ /* Hacking missions */
#mission-container {
overflow:hidden;
}
.hack-mission-grid { .hack-mission-grid {
display: grid; display: grid;
grid-template-columns: 10% 10% 10% 10% 10% 10% 10% 10%; /*grid-template-columns: 11% 11% 11% 11% 11% 11% 11% 11%;*/
grid-template-rows: 8% 8% 8% 8% 8% 8% 8% 8%; grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
/*grid-template-rows: 10% 10% 10% 10% 10% 10% 10% 10%;*/
grid-template-rows: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
grid-gap: 2.5%; grid-gap: 2.5%;
height: 100%; height: 100%;
position:fixed; position:absolute;
width: 100%; width: 100%;
overflow-y:auto; overflow-y:auto;
padding-right: 10px;
}
.hack-mission-grid::-webkit-scrollbar {
display:none;
} }
.hack-mission-node { .hack-mission-node {

@ -2,6 +2,7 @@
.generic-fullscreen-container { .generic-fullscreen-container {
color: var(--my-font-color); color: var(--my-font-color);
width: 99%; width: 99%;
height: 100%;
} }
#work-in-progress-container { #work-in-progress-container {

9906
dist/bundle.js vendored

File diff suppressed because it is too large Load Diff

@ -1,8 +1,11 @@
import {BitNodeMultipliers} from "./BitNode.js"; import {BitNodeMultipliers} from "./BitNode.js";
import {CONSTANTS} from "./Constants.js"; import {CONSTANTS} from "./Constants.js";
import {Factions, getNextNeurofluxLevel} from "./Faction.js";
import {addWorkerScript} from "./NetscriptWorker.js";
import {Player} from "./Player.js"; import {Player} from "./Player.js";
import {prestigeAugmentation} from "./Prestige.js"; import {prestigeAugmentation} from "./Prestige.js";
import {Factions, getNextNeurofluxLevel} from "./Faction.js"; import {Script, RunningScript} from "./Script.js";
import {Server} from "./Server.js";
import {dialogBoxCreate} from "../utils/DialogBox.js"; import {dialogBoxCreate} from "../utils/DialogBox.js";
import {Reviver, Generic_toJSON, import {Reviver, Generic_toJSON,
Generic_fromJSON} from "../utils/JSONReviver.js"; Generic_fromJSON} from "../utils/JSONReviver.js";
@ -1910,7 +1913,7 @@ function PlayerOwnedAugmentation(name) {
this.level = 1; this.level = 1;
} }
function installAugmentations() { function installAugmentations(cbScript=null) {
if (Player.queuedAugmentations.length == 0) { if (Player.queuedAugmentations.length == 0) {
dialogBoxCreate("You have not purchased any Augmentations to install!"); dialogBoxCreate("You have not purchased any Augmentations to install!");
return false; return false;
@ -1930,6 +1933,25 @@ function installAugmentations() {
"to install the following Augmentations:<br>" + augmentationList + "to install the following Augmentations:<br>" + augmentationList +
"<br>You wake up in your home...you feel different..."); "<br>You wake up in your home...you feel different...");
prestigeAugmentation(); prestigeAugmentation();
//Run a script after prestiging
if (cbScript) {
var home = Player.getHomeComputer();
for (var i = 0; i < home.scripts.length; ++i) {
if (home.scripts[i].filename === cbScript) {
var script = home.scripts[i];
var ramUsage = script.ramUsage;
var ramAvailable = home.maxRam - home.ramUsed;
if (ramUsage > ramAvailable) {
return; //Not enough RAM
}
var runningScriptObj = new RunningScript(script, []); //No args
runningScriptObj.threads = 1; //Only 1 thread
home.runningScripts.push(runningScriptObj);
addWorkerScript(runningScriptObj, home);
}
}
}
} }
function augmentationExists(name) { function augmentationExists(name) {

@ -34,7 +34,6 @@ let CONSTANTS = {
CompanyReputationToFavorBase: 500, CompanyReputationToFavorBase: 500,
CompanyReputationToFavorMult: 1.02, CompanyReputationToFavorMult: 1.02,
/* Augmentation */ /* Augmentation */
//NeuroFlux Governor cost multiplier as you level up //NeuroFlux Governor cost multiplier as you level up
NeuroFluxGovernorLevelMult: 1.14, NeuroFluxGovernorLevelMult: 1.14,
@ -515,11 +514,12 @@ let CONSTANTS = {
"must be a string containing the hostname or IP of the target server. This function will always return true. <br><br>" + "must be a string containing the hostname or IP of the target server. This function will always return true. <br><br>" +
"<i>scp(script, [source], destination)</i><br>Copies a script or literature (.lit) file to another server. The first argument is a string with " + "<i>scp(script, [source], destination)</i><br>Copies a script or literature (.lit) file to another server. The first argument is a string with " +
"the filename of the script or literature file " + "the filename of the script or literature file " +
"to be copied. The next two arguments are strings containing the hostname/IPs of the source and target server. " + "to be copied, or an array of filenames to be copied. The next two arguments are strings containing the hostname/IPs of the source and target server. " +
"The source refers to the server from which the script/literature file will be copied, while the destination " + "The source refers to the server from which the script/literature file will be copied, while the destination " +
"refers to the server to which it will be copied. The source server argument is optional, and if ommitted the source " + "refers to the server to which it will be copied. The source server argument is optional, and if ommitted the source " +
"will be the current server (the server on which the script is running). Returns true if the script/literature file is " + "will be the current server (the server on which the script is running). Returns true if the script/literature file is " +
"successfully copied over and false otherwise. <br><br>" + "successfully copied over and false otherwise. If the first argument passed in is an array, then the function " +
"will return if at least one of the files in the array is successfully copied over.<br><br>" +
"Example: scp('hack-template.script', 'foodnstuff'); //Copies hack-template.script from the current server to foodnstuff<br>" + "Example: scp('hack-template.script', 'foodnstuff'); //Copies hack-template.script from the current server to foodnstuff<br>" +
"Example: scp('foo.lit', 'helios', 'home'); //Copies foo.lit from the helios server to the home computer<br><br>" + "Example: scp('foo.lit', 'helios', 'home'); //Copies foo.lit from the helios server to the home computer<br><br>" +
"<i>ls(hostname/ip)</i><br>Returns an array containing the names of all files on the specified server. The argument must be a " + "<i>ls(hostname/ip)</i><br>Returns an array containing the names of all files on the specified server. The argument must be a " +
@ -632,6 +632,9 @@ let CONSTANTS = {
"an empty string. The function will fail if the arguments passed in are invalid or if the player does not have enough money to purchase the specified server.<br><br>" + "an empty string. The function will fail if the arguments passed in are invalid or if the player does not have enough money to purchase the specified server.<br><br>" +
"<i>deleteServer(hostname)</i><br>Deletes one of the servers you've purchased with the specified hostname. The function will fail if " + "<i>deleteServer(hostname)</i><br>Deletes one of the servers you've purchased with the specified hostname. The function will fail if " +
"there are any scripts running on the specified server. Returns true if successful and false otherwise<br><br>" + "there are any scripts running on the specified server. Returns true if successful and false otherwise<br><br>" +
"<i>getPurchasedServers([hostname=true])</i><br>Returns an array with either the hostname or IPs of all of the servers you " +
"have purchased. It takes an optional parameter specifying whether the hostname or IP addresses will be returned. If this " +
"parameter is not specified, it is true by default and hostnames will be returned<br><br>" +
"<i>round(n)</i><br>Rounds the number n to the nearest integer. If the argument passed in is not a number, then the function will return 0.<br><br>" + "<i>round(n)</i><br>Rounds the number n to the nearest integer. If the argument passed in is not a number, then the function will return 0.<br><br>" +
"<i>write(port, data)</i><br>Writes data to a port. The first argument must be a number between 1 and 10 that specifies the port. The second " + "<i>write(port, data)</i><br>Writes data to a port. The first argument must be a number between 1 and 10 that specifies the port. The second " +
"argument defines the data to write to the port. If the second argument is not specified then it will write an empty string to the port.<br><br>" + "argument defines the data to write to the port. If the second argument is not specified then it will write an empty string to the port.<br><br>" +
@ -1008,6 +1011,11 @@ let CONSTANTS = {
"World Stock Exchange account and TIX API Access<br>", "World Stock Exchange account and TIX API Access<br>",
LatestUpdate: LatestUpdate:
"v0.29.2<br>" +
"-installAugmentations() Singularity Function now takes a callback script as an argument. This is a script " +
"that gets ran automatically after Augmentations are installed. The script is run with no arguments and only a single thread, " +
"and must be found on your home computer.<br>" +
"-Added functions to Netscript. See the link here for details<br>" +
"v0.29.1<br>" + "v0.29.1<br>" +
"-New gameplay feature that is currently in BETA: Hacking Missions. Hacking Missions is an active gameplay mechanic (its a minigame) " + "-New gameplay feature that is currently in BETA: Hacking Missions. Hacking Missions is an active gameplay mechanic (its a minigame) " +
"that is meant to be used to earn faction reputation. However, since this is currently in beta, hacking missions will NOT grant reputation " + "that is meant to be used to earn faction reputation. However, since this is currently in beta, hacking missions will NOT grant reputation " +

@ -820,6 +820,15 @@ HackingMission.prototype.nodeReachable = function(node) {
return false; return false;
} }
HackingMission.prototype.nodeReachableByEnemy = function(node) {
var x = node.pos[0], y = node.pos[1];
if (x > 0 && this.map[x-1][y].enmyCtrl) {return true;}
if (x < 7 && this.map[x+1][y].enmyCtrl) {return true;}
if (y > 0 && this.map[x][y-1].enmyCtrl) {return true;}
if (y < 7 && this.map[x][y+1].enmyCtrl) {return true;}
return false;
}
HackingMission.prototype.start = function() { HackingMission.prototype.start = function() {
this.started = true; this.started = true;
this.initJsPlumb(); this.initJsPlumb();
@ -1169,6 +1178,53 @@ HackingMission.prototype.processNode = function(nodeObj, numCycles=1) {
return calcStats; return calcStats;
} }
//Enemy "AI" for CPU Cor eand Transfer Nodes
HackingMission.prototype.enemyAISelectAction = function(nodeObj) {
if (nodeObj === null) {return;}
switch(nodeObj.type) {
case NodeTypes.Core:
//Select a single RANDOM target from miscNodes and player's Nodes
//If it is reachable, it will target it. If not, no target will
//be selected for now, and the next time process() gets called this will repeat
if (nodeObj.conn === null) {
if (this.miscNodes.length === 0) {
var rand = getRandomInt(0, this.playerNodes.length-1);
var node = this.playerNodes[rand];
if (this.nodeReachableByEnemy(node)) {
//TODO Create connection
} else {
rand = getRandomInt(0, this.playerCores.length-1);
node = this.playerCores[rand];
if (this.nodeReachableByEnemy(node)) {
//TODO Create connection
}
}
} else {
var rand = getRandomInt(0, this.miscNodes.length-1);
var node = this.miscNodes[rand];
if (this.nodeReachableByEnemy(node)) {
//TODO Create connection to this Node
}
}
}
//TODO Select action
break;
case NodeTypes.Transfer:
//Switch between fortifying and overflowing as necessary
if (nodeObj.def < 500) {
nodeObj.action = NodeActions.Fortify;
} else {
nodeObj.action = NodeActions.Overflow;
}
break;
default:
break;
}
}
var hackEffWeightSelf = 150; //Weight for Node actions on self var hackEffWeightSelf = 150; //Weight for Node actions on self
var hackEffWeightTarget = 25; //Weight for Node Actions against Target var hackEffWeightTarget = 25; //Weight for Node Actions against Target
var hackEffWeightAttack = 110; //Weight for Attack action var hackEffWeightAttack = 110; //Weight for Attack action

@ -15,7 +15,7 @@ Environment.prototype = {
//Create a "subscope", which is a new new "sub-environment" //Create a "subscope", which is a new new "sub-environment"
//The subscope is linked to this through its parent variable //The subscope is linked to this through its parent variable
extend: function() { extend: function() {
return new Environment(this); return new Environment(null, this);
}, },
//Finds the scope where the variable with the given name is defined //Finds the scope where the variable with the given name is defined

@ -8,6 +8,7 @@ import {Settings} from "./Settings.js";
import {Script, findRunningScript, import {Script, findRunningScript,
RunningScript} from "./Script.js"; RunningScript} from "./Script.js";
import {Node} from "../utils/acorn.js";
import {printArray} from "../utils/HelperFunctions.js"; import {printArray} from "../utils/HelperFunctions.js";
import {isValidIPAddress} from "../utils/IPAddress.js"; import {isValidIPAddress} from "../utils/IPAddress.js";
import {isString} from "../utils/StringHelperFunctions.js"; import {isString} from "../utils/StringHelperFunctions.js";
@ -19,7 +20,7 @@ import {isString} from "../utils/StringHelperFunctions
*/ */
function evaluate(exp, workerScript) { function evaluate(exp, workerScript) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var env = workerScript.env; var env = workerScript.env;
if (env.stopFlag) {return reject(workerScript);} if (env.stopFlag) {return reject(workerScript);}
if (exp == null) { if (exp == null) {
return reject(makeRuntimeRejectMsg(workerScript, "Error: NULL expression")); return reject(makeRuntimeRejectMsg(workerScript, "Error: NULL expression"));
@ -33,7 +34,10 @@ function evaluate(exp, workerScript) {
evaluateProgPromise.then(function(w) { evaluateProgPromise.then(function(w) {
resolve(workerScript); resolve(workerScript);
}, function(e) { }, function(e) {
if (isString(e)) { if (e.constructor === Array && e.length === 2 && e[0] === "RETURNSTATEMENT") {
//Returning from a Player-defined function
resolve(e[1]);
} else if (isString(e)) {
workerScript.errorMessage = e; workerScript.errorMessage = e;
reject(workerScript); reject(workerScript);
} else if (e instanceof WorkerScript) { } else if (e instanceof WorkerScript) {
@ -55,7 +59,7 @@ function evaluate(exp, workerScript) {
case "ExpressionStatement": case "ExpressionStatement":
var e = evaluate(exp.expression, workerScript); var e = evaluate(exp.expression, workerScript);
e.then(function(res) { e.then(function(res) {
resolve("expression done"); resolve(res);
}, function(e) { }, function(e) {
reject(e); reject(e);
}); });
@ -76,7 +80,32 @@ function evaluate(exp, workerScript) {
return evaluate(arg, workerScript); return evaluate(arg, workerScript);
}); });
Promise.all(argPromises).then(function(args) { Promise.all(argPromises).then(function(args) {
if (exp.callee.type == "MemberExpression"){ if (func instanceof Node) { //Player-defined function
//Create new Environment for the function
//Should be automatically garbage collected...
var funcEnv = env.extend();
//Define function arguments in this new environment
for (var i = 0; i < func.params.length; ++i) {
var arg;
if (i >= args.length) {
arg = null;
} else {
arg = args[i];
}
funcEnv.def(func.params[i].name, arg);
}
//Create a new WorkerScript for this function evaluation
var funcWorkerScript = new WorkerScript(workerScript.scriptRef);
funcWorkerScript.env = funcEnv;
evaluate(func.body, funcWorkerScript).then(function(res) {
resolve(res);
}).catch(function(e) {
reject(e);
});
} else if (exp.callee.type == "MemberExpression"){
evaluate(exp.callee.object, workerScript).then(function(object) { evaluate(exp.callee.object, workerScript).then(function(object) {
try { try {
var res = func.apply(object,args); var res = func.apply(object,args);
@ -125,7 +154,6 @@ function evaluate(exp, workerScript) {
} }
resolve(object[index]); resolve(object[index]);
}).catch(function(e) { }).catch(function(e) {
console.log("here");
reject(makeRuntimeRejectMsg(workerScript, "Invalid MemberExpression")); reject(makeRuntimeRejectMsg(workerScript, "Invalid MemberExpression"));
}); });
} else { } else {
@ -195,8 +223,14 @@ function evaluate(exp, workerScript) {
resolve(false); resolve(false);
break; break;
case "ReturnStatement": case "ReturnStatement":
var lineNum = getErrorLineNumber(exp, workerScript); console.log("Evaluating Return Statement");
reject(makeRuntimeRejectMsg(workerScript, "Return statements are not yet implemented in Netscript (line " + (lineNum+1) + ")")); //var lineNum = getErrorLineNumber(exp, workerScript);
//reject(makeRuntimeRejectMsg(workerScript, "Return statements are not yet implemented in Netscript (line " + (lineNum+1) + ")"));
evaluate(exp.argument, workerScript).then(function(res) {
reject(["RETURNSTATEMENT", res]);
}).catch(function(e) {
reject(e);
});
break; break;
case "BreakStatement": case "BreakStatement":
reject("BREAKSTATEMENT"); reject("BREAKSTATEMENT");
@ -206,7 +240,7 @@ function evaluate(exp, workerScript) {
break; break;
case "IfStatement": case "IfStatement":
evaluateIf(exp, workerScript).then(function(forLoopRes) { evaluateIf(exp, workerScript).then(function(forLoopRes) {
resolve("forLoopDone"); resolve(forLoopRes);
}).catch(function(e) { }).catch(function(e) {
reject(e); reject(e);
}); });
@ -217,7 +251,7 @@ function evaluate(exp, workerScript) {
break;e break;e
case "WhileStatement": case "WhileStatement":
evaluateWhile(exp, workerScript).then(function(forLoopRes) { evaluateWhile(exp, workerScript).then(function(forLoopRes) {
resolve("forLoopDone"); resolve(forLoopRes);
}).catch(function(e) { }).catch(function(e) {
if (e == "BREAKSTATEMENT" || if (e == "BREAKSTATEMENT" ||
(e instanceof WorkerScript && e.errorMessage == "BREAKSTATEMENT")) { (e instanceof WorkerScript && e.errorMessage == "BREAKSTATEMENT")) {
@ -241,6 +275,15 @@ function evaluate(exp, workerScript) {
} }
}); });
break; break;
case "FunctionDeclaration":
if (exp.id && exp.id.name) {
env.set(exp.id.name, exp);
resolve(true);
} else {
var lineNum = getErrorLineNumber(exp, workerScript);
reject(makeRuntimeRejectMsg(workerScript, "Invalid function declaration at line " + lineNum+1));
}
break;
default: default:
var lineNum = getErrorLineNumber(exp, workerScript); var lineNum = getErrorLineNumber(exp, workerScript);
reject(makeRuntimeRejectMsg(workerScript, "Unrecognized token: " + exp.type + " (line " + (lineNum+1) + "). This is currently unsupported in Netscript")); reject(makeRuntimeRejectMsg(workerScript, "Unrecognized token: " + exp.type + " (line " + (lineNum+1) + "). This is currently unsupported in Netscript"));

@ -12,7 +12,6 @@ import {parse} from "../utils/acorn.js";
import {dialogBoxCreate} from "../utils/DialogBox.js"; import {dialogBoxCreate} from "../utils/DialogBox.js";
import {compareArrays, printArray} from "../utils/HelperFunctions.js"; import {compareArrays, printArray} from "../utils/HelperFunctions.js";
function WorkerScript(runningScriptObj) { function WorkerScript(runningScriptObj) {
this.name = runningScriptObj.filename; this.name = runningScriptObj.filename;
this.running = false; this.running = false;
@ -93,7 +92,7 @@ function runScriptsLoop() {
if (workerScripts[i].running == false && workerScripts[i].env.stopFlag == false) { if (workerScripts[i].running == false && workerScripts[i].env.stopFlag == false) {
try { try {
var ast = parse(workerScripts[i].code); var ast = parse(workerScripts[i].code);
//console.log(ast); console.log(ast);
} catch (e) { } catch (e) {
console.log("Error parsing script: " + workerScripts[i].name); console.log("Error parsing script: " + workerScripts[i].name);
dialogBoxCreate("Syntax ERROR in " + workerScripts[i].name + ":<br>" + e); dialogBoxCreate("Syntax ERROR in " + workerScripts[i].name + ":<br>" + e);
@ -180,6 +179,14 @@ function addWorkerScript(runningScriptObj, server) {
} }
var ramUsage = runningScriptObj.scriptRef.ramUsage * threads var ramUsage = runningScriptObj.scriptRef.ramUsage * threads
* Math.pow(CONSTANTS.MultithreadingRAMCost, threads-1); * Math.pow(CONSTANTS.MultithreadingRAMCost, threads-1);
var ramAvailable = server.maxRam - server.ramUsed;
if (ramUsage > ramAvailable) {
dialogBoxCreate("Not enough RAM to run script " + runningScriptObj.filename + " with args " +
printArray(runningScriptObj.args) + ". This likely occurred because you re-loaded " +
"the game and the script's RAM usage increased (either because of an update to the game or " +
"your changes to the script.)");
return;
}
server.ramUsed += ramUsage; server.ramUsed += ramUsage;
//Create the WorkerScript //Create the WorkerScript

@ -33,7 +33,15 @@ function initSourceFiles() {
SourceFiles["SourceFile3"] = new SourceFile(3); SourceFiles["SourceFile3"] = new SourceFile(3);
SourceFiles["SourceFile4"] = new SourceFile(4, "This Source-File lets you access and use the Singularity Functions in every BitNode. Every " + SourceFiles["SourceFile4"] = new SourceFile(4, "This Source-File lets you access and use the Singularity Functions in every BitNode. Every " +
"level of this Source-File opens up more of the Singularity Functions you can use."); "level of this Source-File opens up more of the Singularity Functions you can use.");
SourceFiles["SourceFile5"] = new SourceFile(5); SourceFiles["SourceFile5"] = new SourceFile(5, "This Source-File grants a special new stat called Intelligence. Intelligence " +
"is unique because it is permanent and persistent (it never gets reset back to 1). However, " +
"gaining Intelligence experience is much slower than other stats, and it is also hidden (you won't " +
"know when you gain experience and how much). Higher Intelligence levels will boost your production " +
"for many actions in the game. In addition, this Source-File will unlock the getBitNodeMultipliers() " +
"Netscript function, and will raise all of your hacking-related multipliers by:<br><br> " +
"Level 1: 4%<br>" +
"Level 2: 6%<br>" +
"Level 3: 7%");
SourceFiles["SourceFile6"] = new SourceFile(6); SourceFiles["SourceFile6"] = new SourceFile(6);
SourceFiles["SourceFile7"] = new SourceFile(7); SourceFiles["SourceFile7"] = new SourceFile(7);
SourceFiles["SourceFile8"] = new SourceFile(8); SourceFiles["SourceFile8"] = new SourceFile(8);
@ -108,6 +116,17 @@ function applySourceFile(srcFile) {
case 4: //The Singularity case 4: //The Singularity
//No effects, just gives access to Singularity functions //No effects, just gives access to Singularity functions
break; break;
case 5: //Artificial Intelligence
var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) {
mult += (4 / (Math.pow(2, i)));
}
var incMult = 1 + (mult / 100);
Player.hacking_chance_mult *= incMult;
Player.hacking_speed_mult *= incMult;
Player.hacking_money_mult *= incMult;
Player.hacking_grow_mult *= incMult;
break;
case 11: //The Big Crash case 11: //The Big Crash
var mult = 0; var mult = 0;
for (var i = 0; i < srcFile.lvl; ++i) { for (var i = 0; i < srcFile.lvl; ++i) {

@ -484,6 +484,11 @@ let Engine = {
if (Player.sourceFiles.length !== 0) { if (Player.sourceFiles.length !== 0) {
bnText = "<br>Current BitNode: " + Player.bitNodeN; bnText = "<br>Current BitNode: " + Player.bitNodeN;
} }
var intText = "";
if (Player.intelligence > 0) {
intText = 'Intelligence: ' + (Player.intelligence).toLocaleString() + "<br><br><br>";
}
Engine.Display.characterInfo.innerHTML = Engine.Display.characterInfo.innerHTML =
('<b>General</b><br><br>' + ('<b>General</b><br><br>' +
'Current City: ' + Player.city + '<br><br>' + 'Current City: ' + Player.city + '<br><br>' +
@ -502,7 +507,8 @@ let Engine = {
'Agility: ' + (Player.agility).toLocaleString() + 'Agility: ' + (Player.agility).toLocaleString() +
" (" + numeral(Player.agility_exp).format('(0.000a)') + ' experience)<br>' + " (" + numeral(Player.agility_exp).format('(0.000a)') + ' experience)<br>' +
'Charisma: ' + (Player.charisma).toLocaleString() + 'Charisma: ' + (Player.charisma).toLocaleString() +
" (" + numeral(Player.charisma_exp).format('(0.000a)') + ' experience)<br><br><br>' + " (" + numeral(Player.charisma_exp).format('(0.000a)') + ' experience)<br>' +
intText +
'<b>Multipliers</b><br><br>' + '<b>Multipliers</b><br><br>' +
'Hacking Chance multiplier: ' + formatNumber(Player.hacking_chance_mult * 100, 2) + '%<br>' + 'Hacking Chance multiplier: ' + formatNumber(Player.hacking_chance_mult * 100, 2) + '%<br>' +
'Hacking Speed multiplier: ' + formatNumber(Player.hacking_speed_mult * 100, 2) + '%<br>' + 'Hacking Speed multiplier: ' + formatNumber(Player.hacking_speed_mult * 100, 2) + '%<br>' +