mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-30 03:23:48 +01:00
1541 lines
57 KiB
JavaScript
1541 lines
57 KiB
JavaScript
import { CONSTANTS } from "./Constants";
|
|
import { Engine } from "./engine";
|
|
import { displayFactionContent } from "./Faction/FactionHelpers";
|
|
import { Player } from "./Player";
|
|
|
|
import { dialogBoxCreate } from "../utils/DialogBox";
|
|
import { formatNumber } from "../utils/StringHelperFunctions";
|
|
import { Reputation } from "./ui/React/Reputation";
|
|
|
|
import { addOffset } from "../utils/helpers/addOffset";
|
|
import { getRandomInt } from "../utils/helpers/getRandomInt";
|
|
import { isString } from "../utils/helpers/isString";
|
|
|
|
import { clearEventListeners } from "../utils/uiHelpers/clearEventListeners";
|
|
|
|
// For some reason `jsplumb` needs to be imported exactly like this,
|
|
// lowercase p, and later in the code used as `jsPlumb` uppercase P. wtf.
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
import jsplumb from "jsplumb";
|
|
|
|
import React from "react";
|
|
import ReactDOM from "react-dom";
|
|
|
|
|
|
let inMission = false; // Flag to denote whether a mission is running
|
|
let currMission = null;
|
|
function setInMission(bool, mission) {
|
|
inMission = bool;
|
|
if (bool) {
|
|
currMission = mission;
|
|
} else {
|
|
currMission = null;
|
|
}
|
|
}
|
|
|
|
// Keyboard shortcuts
|
|
$(document).keydown(function(e) {
|
|
if (inMission && currMission && currMission.selectedNode.length != 0) {
|
|
switch (e.keyCode) {
|
|
case 65: // a for Attack
|
|
currMission.actionButtons[0].click();
|
|
break;
|
|
case 83: // s for Scan
|
|
currMission.actionButtons[1].click();
|
|
break;
|
|
case 87: // w for Weaken
|
|
currMission.actionButtons[2].click();
|
|
break;
|
|
case 70: // f for Fortify
|
|
currMission.actionButtons[3].click();
|
|
break;
|
|
case 82: // r for Overflow
|
|
currMission.actionButtons[4].click();
|
|
break;
|
|
case 68: // d for Detach connection
|
|
currMission.actionButtons[5].click();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
let NodeTypes = {
|
|
Core: "CPU Core Node", // All actions available
|
|
Firewall: "Firewall Node", // No actions available
|
|
Database: "Database Node", // No actions available
|
|
Spam: "Spam Node", // No actions Available
|
|
Transfer: "Transfer Node", // Can Weaken, Scan, Fortify and Overflow
|
|
Shield: "Shield Node", // Can Fortify
|
|
}
|
|
|
|
let NodeActions = {
|
|
Attack: "Attacking", // Damaged based on attack stat + hacking level + opp def
|
|
Scan: "Scanning", // -Def for target, affected by attack and hacking level
|
|
Weaken: "Weakening", // -Attack for target, affected by attack and hacking level
|
|
Fortify: "Fortifying", // +Defense for Node, affected by hacking level
|
|
Overflow: "Overflowing", // +Attack but -Defense for Node, affected by hacking level
|
|
}
|
|
|
|
function Node(type, stats) {
|
|
this.type = type;
|
|
this.atk = stats.atk ? stats.atk : 0;
|
|
this.def = stats.def ? stats.def : 0;
|
|
this.hp = stats.hp ? stats.hp : 0;
|
|
this.maxhp = this.hp;
|
|
this.plyrCtrl = false;
|
|
this.enmyCtrl = false;
|
|
this.pos = [0, 0]; // x, y
|
|
this.el = null; // Holds the Node's DOM element
|
|
this.action = null;
|
|
this.targetedCount = 0; // Count of how many connections this node is the target of
|
|
|
|
/**
|
|
* Holds the JsPlumb Connection object for this Node, where this Node is the Source (since each Node
|
|
* can only have 1 outgoing Connection)
|
|
*/
|
|
this.conn = null;
|
|
}
|
|
|
|
Node.prototype.setPosition = function(x, y) {
|
|
this.pos = [x, y];
|
|
}
|
|
|
|
Node.prototype.setControlledByPlayer = function() {
|
|
this.plyrCtrl = true;
|
|
this.enmyCtrl = false;
|
|
if (this.el) {
|
|
this.el.classList.remove("hack-mission-enemy-node");
|
|
this.el.classList.add("hack-mission-player-node");
|
|
}
|
|
}
|
|
|
|
Node.prototype.setControlledByEnemy = function() {
|
|
this.plyrCtrl = false;
|
|
this.enmyCtrl = true;
|
|
if (this.el) {
|
|
this.el.classList.remove("hack-mission-player-node");
|
|
this.el.classList.add("hack-mission-enemy-node");
|
|
}
|
|
}
|
|
|
|
// Sets this node to be the active node
|
|
Node.prototype.select = function(actionButtons) {
|
|
if (this.enmyCtrl) {return;}
|
|
this.el.classList.add("hack-mission-player-node-active");
|
|
|
|
// Make all buttons inactive
|
|
for (var i = 0; i < actionButtons.length; ++i) {
|
|
actionButtons[i].classList.remove("a-link-button");
|
|
actionButtons[i].classList.add("a-link-button-inactive");
|
|
}
|
|
|
|
switch(this.type) {
|
|
case NodeTypes.Core:
|
|
// All buttons active
|
|
for (var i = 0; i < actionButtons.length; ++i) {
|
|
actionButtons[i].classList.remove("a-link-button-inactive");
|
|
actionButtons[i].classList.add("a-link-button");
|
|
}
|
|
break;
|
|
case NodeTypes.Transfer:
|
|
actionButtons[1].classList.remove("a-link-button-inactive");
|
|
actionButtons[1].classList.add("a-link-button");
|
|
actionButtons[2].classList.remove("a-link-button-inactive");
|
|
actionButtons[2].classList.add("a-link-button");
|
|
actionButtons[3].classList.remove("a-link-button-inactive");
|
|
actionButtons[3].classList.add("a-link-button");
|
|
actionButtons[4].classList.remove("a-link-button-inactive");
|
|
actionButtons[4].classList.add("a-link-button");
|
|
actionButtons[5].classList.remove("a-link-button-inactive");
|
|
actionButtons[5].classList.add("a-link-button");
|
|
break;
|
|
case NodeTypes.Shield:
|
|
case NodeTypes.Firewall:
|
|
actionButtons[3].classList.remove("a-link-button-inactive");
|
|
actionButtons[3].classList.add("a-link-button");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
Node.prototype.deselect = function(actionButtons) {
|
|
this.el.classList.remove("hack-mission-player-node-active");
|
|
for (var i = 0; i < actionButtons.length; ++i) {
|
|
actionButtons[i].classList.remove("a-link-button");
|
|
actionButtons[i].classList.add("a-link-button-inactive");
|
|
}
|
|
}
|
|
|
|
|
|
Node.prototype.untarget = function() {
|
|
if (this.targetedCount === 0) {
|
|
console.warn(`Node ${this.el.id} is being 'untargeted' when it has no target count`);
|
|
return;
|
|
}
|
|
--this.targetedCount;
|
|
}
|
|
|
|
/**
|
|
* Hacking mission instance
|
|
* @param rep {number} How much reputation the player has for the faction
|
|
* @param fac {Faction} Faction for which this mission is being conducted
|
|
*/
|
|
function HackingMission(rep, fac) {
|
|
this.faction = fac;
|
|
|
|
this.started = false;
|
|
this.time = 180000; // 5 minutes to start, milliseconds
|
|
|
|
this.playerCores = [];
|
|
this.playerNodes = []; // Non-core nodes
|
|
this.playerAtk = 0;
|
|
this.playerDef = 0;
|
|
|
|
this.enemyCores = [];
|
|
this.enemyDatabases = [];
|
|
this.enemyNodes = []; // Non-core nodes
|
|
this.enemyAtk = 0;
|
|
this.enemyDef = 0;
|
|
|
|
this.miscNodes = [];
|
|
|
|
this.selectedNode = []; // Which of the player's nodes are currently selected
|
|
|
|
this.actionButtons = []; // DOM buttons for actions
|
|
|
|
this.availablePositions = [];
|
|
for (var r = 0; r < 8; ++r) {
|
|
for (var c = 0; c < 8; ++c) {
|
|
this.availablePositions.push([r, c]);
|
|
}
|
|
}
|
|
|
|
this.map = [];
|
|
for (var i = 0; i < 8; ++i) {
|
|
this.map.push([null, null, null, null, null, null, null, null]);
|
|
}
|
|
|
|
this.jsplumbinstance = null;
|
|
|
|
this.difficulty = rep / CONSTANTS.HackingMissionRepToDiffConversion + 1;
|
|
this.reward = 250 + (rep / CONSTANTS.HackingMissionRepToRewardConversion);
|
|
}
|
|
|
|
HackingMission.prototype.init = function() {
|
|
// Create Header DOM
|
|
this.createPageDom();
|
|
|
|
// Create player starting nodes
|
|
var home = Player.getHomeComputer()
|
|
for (var i = 0; i < home.cpuCores; ++i) {
|
|
var stats = {
|
|
atk: (Player.hacking_skill / 7.5) + 30,
|
|
def: (Player.hacking_skill / 20),
|
|
hp: (Player.hacking_skill / 4),
|
|
};
|
|
this.playerCores.push(new Node(NodeTypes.Core, stats));
|
|
this.playerCores[i].setControlledByPlayer();
|
|
this.setNodePosition(this.playerCores[i], i, 0);
|
|
this.removeAvailablePosition(i, 0);
|
|
}
|
|
|
|
// Randomly generate enemy nodes (CPU and Firewall) based on difficulty
|
|
var numNodes = Math.min(8, Math.max(1, Math.round(this.difficulty / 4)));
|
|
var numFirewalls = Math.min(20,
|
|
getRandomInt(Math.round(this.difficulty/3), Math.round(this.difficulty/3) + 1));
|
|
var numDatabases = Math.min(10, getRandomInt(1, Math.round(this.difficulty / 3) + 1));
|
|
var totalNodes = numNodes + numFirewalls + numDatabases;
|
|
var xlimit = 7 - Math.floor(totalNodes / 8);
|
|
var randMult = addOffset(0.8 + (this.difficulty / 5), 10);
|
|
for (var i = 0; i < numNodes; ++i) {
|
|
var stats = {
|
|
atk: randMult * getRandomInt(80, 86),
|
|
def: randMult * getRandomInt(5, 10),
|
|
hp: randMult * getRandomInt(210, 230),
|
|
}
|
|
this.enemyCores.push(new Node(NodeTypes.Core, stats));
|
|
this.enemyCores[i].setControlledByEnemy();
|
|
this.setNodeRandomPosition(this.enemyCores[i], xlimit);
|
|
}
|
|
for (var i = 0; i < numFirewalls; ++i) {
|
|
var stats = {
|
|
atk: 0,
|
|
def: randMult * getRandomInt(10, 20),
|
|
hp: randMult * getRandomInt(275, 300),
|
|
}
|
|
this.enemyNodes.push(new Node(NodeTypes.Firewall, stats));
|
|
this.enemyNodes[i].setControlledByEnemy();
|
|
this.setNodeRandomPosition(this.enemyNodes[i], xlimit);
|
|
}
|
|
for (var i = 0; i < numDatabases; ++i) {
|
|
var stats = {
|
|
atk: 0,
|
|
def: randMult * getRandomInt(30, 55),
|
|
hp: randMult * getRandomInt(210, 275),
|
|
}
|
|
var node = new Node(NodeTypes.Database, stats);
|
|
node.setControlledByEnemy();
|
|
this.setNodeRandomPosition(node, xlimit);
|
|
this.enemyDatabases.push(node);
|
|
}
|
|
this.calculateDefenses();
|
|
this.calculateAttacks();
|
|
this.createMap();
|
|
}
|
|
|
|
HackingMission.prototype.createPageDom = function() {
|
|
var container = document.getElementById("mission-container");
|
|
|
|
var favorMult = 1 + (this.faction.favor / 100);
|
|
var gain = this.reward * Player.faction_rep_mult * favorMult;
|
|
var headerText = document.createElement("p");
|
|
ReactDOM.render(<>You are about to start a hacking mission! You will gain {Reputation(gain)} faction reputation with {this.faction.name} if you win. Click the 'Start' button to begin.</>, headerText);
|
|
headerText.style.display = "block";
|
|
headerText.classList.add("hack-mission-header-element");
|
|
headerText.style.width = "80%";
|
|
|
|
var inGameGuideBtn = document.createElement("a");
|
|
inGameGuideBtn.innerText = "How to Play";
|
|
inGameGuideBtn.classList.add("a-link-button");
|
|
inGameGuideBtn.style.display = "inline-block";
|
|
inGameGuideBtn.classList.add("hack-mission-header-element");
|
|
inGameGuideBtn.addEventListener("click", function() {
|
|
dialogBoxCreate(CONSTANTS.HackingMissionHowToPlay);
|
|
return false;
|
|
});
|
|
|
|
// Start button will get replaced with forfeit when game is started
|
|
var startBtn = document.createElement("a");
|
|
startBtn.innerHTML = "Start";
|
|
startBtn.setAttribute("id", "hack-mission-start-btn");
|
|
startBtn.classList.add("a-link-button");
|
|
startBtn.classList.add("hack-mission-header-element");
|
|
startBtn.style.display = "inline-block";
|
|
startBtn.addEventListener("click", () => {
|
|
this.start();
|
|
return false;
|
|
});
|
|
|
|
var forfeitMission = document.createElement("a");
|
|
forfeitMission.innerHTML = "Forfeit Mission (Exit)";
|
|
forfeitMission.classList.add("a-link-button");
|
|
forfeitMission.classList.add("hack-mission-header-element");
|
|
forfeitMission.style.display = "inline-block";
|
|
forfeitMission.addEventListener("click", () => {
|
|
this.finishMission(false);
|
|
return false;
|
|
});
|
|
|
|
var timer = document.createElement("p");
|
|
timer.setAttribute("id", "hacking-mission-timer");
|
|
timer.style.display = "inline-block";
|
|
timer.style.margin = "6px";
|
|
|
|
// Create Action Buttons (Attack/Scan/Weaken/ etc...)
|
|
var actionsContainer = document.createElement("span");
|
|
actionsContainer.style.display = "block";
|
|
actionsContainer.classList.add("hack-mission-action-buttons-container");
|
|
for (var i = 0; i < 6; ++i) {
|
|
this.actionButtons.push(document.createElement("a"));
|
|
this.actionButtons[i].style.display = "inline-block";
|
|
this.actionButtons[i].classList.add("a-link-button-inactive"); // Disabled at start
|
|
this.actionButtons[i].classList.add("tooltip"); // Disabled at start
|
|
this.actionButtons[i].classList.add("hack-mission-header-element");
|
|
actionsContainer.appendChild(this.actionButtons[i]);
|
|
}
|
|
this.actionButtons[0].innerText = "Attack(a)";
|
|
var atkTooltip = document.createElement("span");
|
|
atkTooltip.classList.add("tooltiptexthigh");
|
|
atkTooltip.innerText = "Lowers the targeted node's HP. The effectiveness of this depends on " +
|
|
"this node's Attack level, your hacking level, and the opponent's defense level.";
|
|
this.actionButtons[0].appendChild(atkTooltip);
|
|
this.actionButtons[1].innerText = "Scan(s)";
|
|
var scanTooltip = document.createElement("span");
|
|
scanTooltip.classList.add("tooltiptexthigh");
|
|
scanTooltip.innerText = "Lowers the targeted node's defense. The effectiveness of this depends on " +
|
|
"this node's Attack level, your hacking level, and the opponent's defense level.";
|
|
this.actionButtons[1].appendChild(scanTooltip);
|
|
this.actionButtons[2].innerText = "Weaken(w)";
|
|
var WeakenTooltip = document.createElement("span");
|
|
WeakenTooltip.classList.add("tooltiptexthigh");
|
|
WeakenTooltip.innerText = "Lowers the targeted node's attack. The effectiveness of this depends on " +
|
|
"this node's Attack level, your hacking level, and the opponent's defense level.";
|
|
this.actionButtons[2].appendChild(WeakenTooltip);
|
|
this.actionButtons[3].innerText = "Fortify(f)";
|
|
var fortifyTooltip = document.createElement("span");
|
|
fortifyTooltip.classList.add("tooltiptexthigh");
|
|
fortifyTooltip.innerText = "Raises this node's Defense level. The effectiveness of this depends on " +
|
|
"your hacking level";
|
|
this.actionButtons[3].appendChild(fortifyTooltip);
|
|
this.actionButtons[4].innerText = "Overflow(r)";
|
|
var overflowTooltip = document.createElement("span");
|
|
overflowTooltip.classList.add("tooltiptexthigh");
|
|
overflowTooltip.innerText = "Raises this node's Attack level but lowers its Defense level. The effectiveness " +
|
|
"of this depends on your hacking level.";
|
|
this.actionButtons[4].appendChild(overflowTooltip);
|
|
this.actionButtons[5].innerText = "Drop Connection(d)";
|
|
var dropconnTooltip = document.createElement("span");
|
|
dropconnTooltip.classList.add("tooltiptexthigh");
|
|
dropconnTooltip.innerText = "Removes this Node's current connection to some target Node, if it has one. This can " +
|
|
"also be done by simply clicking the white connection line.";
|
|
this.actionButtons[5].appendChild(dropconnTooltip);
|
|
|
|
// Player/enemy defense displays will be in action container
|
|
var playerStats = document.createElement("p");
|
|
var enemyStats = document.createElement("p");
|
|
playerStats.style.display = "inline-block";
|
|
enemyStats.style.display = "inline-block";
|
|
playerStats.style.color = "#00ccff";
|
|
enemyStats.style.color = "red";
|
|
playerStats.style.margin = "4px";
|
|
enemyStats.style.margin = "4px";
|
|
playerStats.setAttribute("id", "hacking-mission-player-stats");
|
|
enemyStats.setAttribute("id", "hacking-mission-enemy-stats");
|
|
actionsContainer.appendChild(playerStats);
|
|
actionsContainer.appendChild(enemyStats);
|
|
|
|
// Set Action Button event listeners
|
|
this.actionButtons[0].addEventListener("click", () => {
|
|
if (!(this.selectedNode.length > 0)) {
|
|
console.error("Pressing Action button without selected node");
|
|
return;
|
|
}
|
|
if (this.selectedNode[0].type !== NodeTypes.Core) {return;}
|
|
this.setActionButtonsActive(this.selectedNode[0].type);
|
|
this.setActionButton(NodeActions.Attack, false); // Set attack button inactive
|
|
this.selectedNode.forEach(function(node){
|
|
node.action = NodeActions.Attack;
|
|
});
|
|
});
|
|
|
|
this.actionButtons[1].addEventListener("click", () => {
|
|
if (!(this.selectedNode.length > 0)) {
|
|
console.error("Pressing Action button without selected node");
|
|
return;
|
|
}
|
|
var nodeType = this.selectedNode[0].type; // In a multiselect, every Node will have the same type
|
|
if (nodeType !== NodeTypes.Core && nodeType !== NodeTypes.Transfer) {return;}
|
|
this.setActionButtonsActive(nodeType);
|
|
this.setActionButton(NodeActions.Scan, false); // Set scan button inactive
|
|
this.selectedNode.forEach(function(node){
|
|
node.action = NodeActions.Scan;
|
|
});
|
|
});
|
|
|
|
this.actionButtons[2].addEventListener("click", () => {
|
|
if (!(this.selectedNode.length > 0)) {
|
|
console.error("Pressing Action button without selected node");
|
|
return;
|
|
}
|
|
var nodeType = this.selectedNode[0].type; // In a multiselect, every Node will have the same type
|
|
if (nodeType !== NodeTypes.Core && nodeType !== NodeTypes.Transfer) {return;}
|
|
this.setActionButtonsActive(nodeType);
|
|
this.setActionButton(NodeActions.Weaken, false); // Set Weaken button inactive
|
|
this.selectedNode.forEach(function(node){
|
|
node.action = NodeActions.Weaken;
|
|
});
|
|
});
|
|
|
|
this.actionButtons[3].addEventListener("click", () => {
|
|
if (!(this.selectedNode.length > 0)) {
|
|
console.error("Pressing Action button without selected node");
|
|
return;
|
|
}
|
|
this.setActionButtonsActive(this.selectedNode[0].type);
|
|
this.setActionButton(NodeActions.Fortify, false); // Set Fortify button inactive
|
|
this.selectedNode.forEach(function(node){
|
|
node.action = NodeActions.Fortify;
|
|
});
|
|
});
|
|
|
|
this.actionButtons[4].addEventListener("click", () => {
|
|
if (!(this.selectedNode.length > 0)) {
|
|
console.error("Pressing Action button without selected node");
|
|
return;
|
|
}
|
|
var nodeType = this.selectedNode[0].type;
|
|
if (nodeType !== NodeTypes.Core && nodeType !== NodeTypes.Transfer) {return;}
|
|
this.setActionButtonsActive(nodeType);
|
|
this.setActionButton(NodeActions.Overflow, false); // Set Overflow button inactive
|
|
this.selectedNode.forEach(function(node){
|
|
node.action = NodeActions.Overflow;
|
|
});
|
|
});
|
|
|
|
this.actionButtons[5].addEventListener("click", () => {
|
|
if (!(this.selectedNode.length > 0)) {
|
|
console.error("Pressing Action button without selected node");
|
|
return;
|
|
}
|
|
this.selectedNode.forEach(function(node){
|
|
if (node.conn) {
|
|
var endpoints = node.conn.endpoints;
|
|
endpoints[0].detachFrom(endpoints[1]);
|
|
}
|
|
node.action = NodeActions.Fortify;
|
|
});
|
|
});
|
|
|
|
var timeDisplay = document.createElement("p");
|
|
|
|
container.appendChild(headerText);
|
|
container.appendChild(inGameGuideBtn);
|
|
container.appendChild(startBtn);
|
|
container.appendChild(forfeitMission);
|
|
container.appendChild(timer);
|
|
container.appendChild(actionsContainer);
|
|
container.appendChild(timeDisplay);
|
|
}
|
|
|
|
HackingMission.prototype.setActionButtonsInactive = function() {
|
|
for (var i = 0; i < this.actionButtons.length; ++i) {
|
|
this.actionButtons[i].classList.remove("a-link-button");
|
|
this.actionButtons[i].classList.add("a-link-button-inactive");
|
|
}
|
|
}
|
|
|
|
HackingMission.prototype.setActionButtonsActive = function(nodeType=null) {
|
|
for (var i = 0; i < this.actionButtons.length; ++i) {
|
|
this.actionButtons[i].classList.add("a-link-button");
|
|
this.actionButtons[i].classList.remove("a-link-button-inactive");
|
|
}
|
|
|
|
/**
|
|
* For Transfer, FireWall and Shield Nodes, certain buttons should always be disabled
|
|
* 0 = Attack, 1 = Scan, 2 = Weaken, 3 = Fortify, 4 = overflow, 5 = Drop conn
|
|
*/
|
|
if (nodeType) {
|
|
switch (nodeType) {
|
|
case NodeTypes.Firewall:
|
|
case NodeTypes.Shield:
|
|
this.actionButtons[0].classList.remove("a-link-button");
|
|
this.actionButtons[0].classList.add("a-link-button-inactive");
|
|
this.actionButtons[1].classList.remove("a-link-button");
|
|
this.actionButtons[1].classList.add("a-link-button-inactive");
|
|
this.actionButtons[2].classList.remove("a-link-button");
|
|
this.actionButtons[2].classList.add("a-link-button-inactive");
|
|
this.actionButtons[4].classList.remove("a-link-button");
|
|
this.actionButtons[4].classList.add("a-link-button-inactive");
|
|
this.actionButtons[5].classList.remove("a-link-button");
|
|
this.actionButtons[5].classList.add("a-link-button-inactive");
|
|
break;
|
|
case NodeTypes.Transfer:
|
|
this.actionButtons[0].classList.remove("a-link-button");
|
|
this.actionButtons[0].classList.add("a-link-button-inactive");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// True for active, false for inactive
|
|
HackingMission.prototype.setActionButton = function(i, active=true) {
|
|
if (isString(i)) {
|
|
switch (i) {
|
|
case NodeActions.Attack:
|
|
i = 0;
|
|
break;
|
|
case NodeActions.Scan:
|
|
i = 1;
|
|
break;
|
|
case NodeActions.Weaken:
|
|
i = 2;
|
|
break;
|
|
case NodeActions.Fortify:
|
|
i = 3;
|
|
break;
|
|
case NodeActions.Overflow:
|
|
default:
|
|
i = 4;
|
|
break;
|
|
}
|
|
}
|
|
if (active) {
|
|
this.actionButtons[i].classList.remove("a-link-button-inactive");
|
|
this.actionButtons[i].classList.add("a-link-button");
|
|
} else {
|
|
this.actionButtons[i].classList.remove("a-link-button");
|
|
this.actionButtons[i].classList.add("a-link-button-inactive");
|
|
}
|
|
|
|
}
|
|
|
|
HackingMission.prototype.calculateAttacks = function() {
|
|
var total = 0;
|
|
for (var i = 0; i < this.playerCores.length; ++i) {
|
|
total += this.playerCores[i].atk;
|
|
}
|
|
for (var i = 0; i < this.playerNodes.length; ++i) {
|
|
total += this.playerNodes[i].atk;
|
|
}
|
|
this.playerAtk = total;
|
|
document.getElementById("hacking-mission-player-stats").innerHTML =
|
|
"Player Attack: " + formatNumber(this.playerAtk, 1) + "<br>" +
|
|
"Player Defense: " + formatNumber(this.playerDef, 1);
|
|
total = 0;
|
|
for (var i = 0; i < this.enemyCores.length; ++i) {
|
|
total += this.enemyCores[i].atk;
|
|
}
|
|
for (var i = 0; i < this.enemyDatabases.length; ++i) {
|
|
total += this.enemyDatabases[i].atk;
|
|
}
|
|
for (var i = 0; i < this.enemyNodes.length; ++i) {
|
|
total += this.enemyNodes[i].atk;
|
|
}
|
|
this.enemyAtk = total;
|
|
document.getElementById("hacking-mission-enemy-stats").innerHTML =
|
|
"Enemy Attack: " + formatNumber(this.enemyAtk, 1) + "<br>" +
|
|
"Enemy Defense: " + formatNumber(this.enemyDef, 1);
|
|
}
|
|
|
|
HackingMission.prototype.calculateDefenses = function() {
|
|
var total = 0;
|
|
for (var i = 0; i < this.playerCores.length; ++i) {
|
|
total += this.playerCores[i].def;
|
|
}
|
|
for (var i = 0; i < this.playerNodes.length; ++i) {
|
|
total += this.playerNodes[i].def;
|
|
}
|
|
this.playerDef = total;
|
|
document.getElementById("hacking-mission-player-stats").innerHTML =
|
|
"Player Attack: " + formatNumber(this.playerAtk, 1) + "<br>" +
|
|
"Player Defense: " + formatNumber(this.playerDef, 1);
|
|
total = 0;
|
|
for (var i = 0; i < this.enemyCores.length; ++i) {
|
|
total += this.enemyCores[i].def;
|
|
}
|
|
for (var i = 0; i < this.enemyDatabases.length; ++i) {
|
|
total += this.enemyDatabases[i].def;
|
|
}
|
|
for (var i = 0; i < this.enemyNodes.length; ++i) {
|
|
total += this.enemyNodes[i].def;
|
|
}
|
|
this.enemyDef = total;
|
|
document.getElementById("hacking-mission-enemy-stats").innerHTML =
|
|
"Enemy Attack: " + formatNumber(this.enemyAtk, 1) + "<br>" +
|
|
"Enemy Defense: " + formatNumber(this.enemyDef, 1);
|
|
}
|
|
|
|
HackingMission.prototype.removeAvailablePosition = function(x, y) {
|
|
for (var i = 0; i < this.availablePositions.length; ++i) {
|
|
if (this.availablePositions[i][0] === x &&
|
|
this.availablePositions[i][1] === y) {
|
|
this.availablePositions.splice(i, 1);
|
|
return;
|
|
}
|
|
}
|
|
console.warn(`removeAvailablePosition() did not remove ${x}, ${y}`);
|
|
}
|
|
|
|
HackingMission.prototype.setNodePosition = function(nodeObj, x, y) {
|
|
if (!(nodeObj instanceof Node)) {
|
|
console.warn("Non-Node object passed into setNodePOsition");
|
|
return;
|
|
}
|
|
if (isNaN(x) || isNaN(y)) {
|
|
console.error(`Invalid values (${x}, ${y}) passed as (x, y) for setNodePosition`);
|
|
return;
|
|
}
|
|
nodeObj.pos = [x, y];
|
|
this.map[x][y] = nodeObj;
|
|
}
|
|
|
|
HackingMission.prototype.setNodeRandomPosition = function(nodeObj, xlimit=0) {
|
|
var i = getRandomInt(0, this.availablePositions.length - 1);
|
|
if (this.availablePositions[i][1] < xlimit) {
|
|
// Recurse if not within limit
|
|
return this.setNodeRandomPosition(nodeObj, xlimit);
|
|
}
|
|
var pos = this.availablePositions.splice(i, 1);
|
|
pos = pos[0];
|
|
this.setNodePosition(nodeObj, pos[0], pos[1]);
|
|
}
|
|
|
|
HackingMission.prototype.createMap = function() {
|
|
// Use a grid
|
|
var map = document.createElement("div");
|
|
map.classList.add("hack-mission-grid");
|
|
map.setAttribute("id", "hacking-mission-map");
|
|
document.getElementById("mission-container").appendChild(map);
|
|
|
|
// Create random Nodes for every space in the map that
|
|
// hasn't been filled yet. The stats of each Node will be based on
|
|
// the player/enemy attack
|
|
var averageAttack = (this.playerAtk + this.enemyAtk) / 2;
|
|
for (var x = 0; x < 8; ++x) {
|
|
for (var y = 0; y < 8; ++y) {
|
|
if (!(this.map[x][y] instanceof Node)) {
|
|
var node, type = getRandomInt(0, 2);
|
|
var randMult = addOffset(0.85 + (this.difficulty / 2), 15);
|
|
switch (type) {
|
|
case 0: // Spam
|
|
var stats = {
|
|
atk: 0,
|
|
def: averageAttack * 1.1 + getRandomInt(15, 45),
|
|
hp: randMult * getRandomInt(200, 225),
|
|
}
|
|
node = new Node(NodeTypes.Spam, stats);
|
|
break;
|
|
case 1: // Transfer
|
|
var stats = {
|
|
atk: 0,
|
|
def: averageAttack * 1.1 + getRandomInt(15, 45),
|
|
hp: randMult * getRandomInt(250, 275),
|
|
}
|
|
node = new Node(NodeTypes.Transfer, stats);
|
|
break;
|
|
case 2: // Shield
|
|
default:
|
|
var stats = {
|
|
atk: 0,
|
|
def: averageAttack * 1.1 + getRandomInt(30, 70),
|
|
hp: randMult * getRandomInt(300, 320),
|
|
}
|
|
node = new Node(NodeTypes.Shield, stats);
|
|
break;
|
|
}
|
|
this.setNodePosition(node, x, y);
|
|
this.removeAvailablePosition(x, y);
|
|
this.miscNodes.push(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create DOM elements in order
|
|
for (var r = 0; r < 8; ++r) {
|
|
for (var c = 0; c < 8; ++c) {
|
|
this.createNodeDomElement(this.map[r][c]);
|
|
}
|
|
}
|
|
|
|
// Configure all Player CPUS
|
|
for (var i = 0; i < this.playerCores.length; ++i) {
|
|
this.configurePlayerNodeElement(this.playerCores[i].el);
|
|
}
|
|
}
|
|
|
|
HackingMission.prototype.createNodeDomElement = function(nodeObj) {
|
|
var nodeDiv = document.createElement("a"), txtEl = document.createElement('p');
|
|
nodeObj.el = nodeDiv;
|
|
|
|
// Set the node element's id based on its coordinates
|
|
var id = "hacking-mission-node-" + nodeObj.pos[0] + "-" + nodeObj.pos[1];
|
|
nodeDiv.setAttribute("id", id);
|
|
txtEl.setAttribute("id", id + "-txt");
|
|
|
|
// Set node classes for owner
|
|
nodeDiv.classList.add("hack-mission-node");
|
|
if (nodeObj.plyrCtrl) {
|
|
nodeDiv.classList.add("hack-mission-player-node");
|
|
} else if (nodeObj.enmyCtrl) {
|
|
nodeDiv.classList.add("hack-mission-enemy-node");
|
|
}
|
|
|
|
// Set node classes based on type
|
|
var txt;
|
|
switch (nodeObj.type) {
|
|
case NodeTypes.Core:
|
|
txt = "CPU Core<br>" + "HP: " +
|
|
formatNumber(nodeObj.hp, 1);
|
|
nodeDiv.classList.add("hack-mission-cpu-node");
|
|
break;
|
|
case NodeTypes.Firewall:
|
|
txt = "Firewall<br>" + "HP: " +
|
|
formatNumber(nodeObj.hp, 1);
|
|
nodeDiv.classList.add("hack-mission-firewall-node");
|
|
break;
|
|
case NodeTypes.Database:
|
|
txt = "Database<br>" + "HP: " +
|
|
formatNumber(nodeObj.hp, 1);
|
|
nodeDiv.classList.add("hack-mission-database-node");
|
|
break;
|
|
case NodeTypes.Spam:
|
|
txt = "Spam<br>" + "HP: " +
|
|
formatNumber(nodeObj.hp, 1);
|
|
nodeDiv.classList.add("hack-mission-spam-node");
|
|
break;
|
|
case NodeTypes.Transfer:
|
|
txt = "Transfer<br>" + "HP: " +
|
|
formatNumber(nodeObj.hp, 1);
|
|
nodeDiv.classList.add("hack-mission-transfer-node");
|
|
break;
|
|
case NodeTypes.Shield:
|
|
default:
|
|
txt = "Shield<br>" + "HP: " +
|
|
formatNumber(nodeObj.hp, 1);
|
|
nodeDiv.classList.add("hack-mission-shield-node");
|
|
break;
|
|
}
|
|
|
|
txt += "<br>Atk: " + formatNumber(nodeObj.atk, 1) +
|
|
"<br>Def: " + formatNumber(nodeObj.def, 1);
|
|
txtEl.innerHTML = txt;
|
|
|
|
nodeDiv.appendChild(txtEl);
|
|
document.getElementById("hacking-mission-map").appendChild(nodeDiv);
|
|
}
|
|
|
|
HackingMission.prototype.updateNodeDomElement = function(nodeObj) {
|
|
if (nodeObj.el == null) {
|
|
console.error("Calling updateNodeDomElement on a Node without an element");
|
|
return;
|
|
}
|
|
|
|
let id = "hacking-mission-node-" + nodeObj.pos[0] + "-" + nodeObj.pos[1];
|
|
let txtEl = document.getElementById(id + "-txt");
|
|
|
|
// Set node classes based on type
|
|
let txt;
|
|
switch (nodeObj.type) {
|
|
case NodeTypes.Core:
|
|
txt = "CPU Core<br>" + "HP: " +
|
|
formatNumber(nodeObj.hp, 1);
|
|
break;
|
|
case NodeTypes.Firewall:
|
|
txt = "Firewall<br>" + "HP: " +
|
|
formatNumber(nodeObj.hp, 1);
|
|
break;
|
|
case NodeTypes.Database:
|
|
txt = "Database<br>" + "HP: " +
|
|
formatNumber(nodeObj.hp, 1);
|
|
break;
|
|
case NodeTypes.Spam:
|
|
txt = "Spam<br>" + "HP: " +
|
|
formatNumber(nodeObj.hp, 1);
|
|
break;
|
|
case NodeTypes.Transfer:
|
|
txt = "Transfer<br>" + "HP: " +
|
|
formatNumber(nodeObj.hp, 1);
|
|
break;
|
|
case NodeTypes.Shield:
|
|
default:
|
|
txt = "Shield<br>" + "HP: " +
|
|
formatNumber(nodeObj.hp, 1);
|
|
break;
|
|
}
|
|
|
|
txt += "<br>Atk: " + formatNumber(nodeObj.atk, 1) +
|
|
"<br>Def: " + formatNumber(nodeObj.def, 1);
|
|
if (nodeObj.action) {
|
|
txt += "<br>" + nodeObj.action;
|
|
}
|
|
txtEl.innerHTML = txt;
|
|
}
|
|
|
|
/**
|
|
* Gets a Node DOM elements corresponding Node object using its
|
|
* element id. Function accepts either the DOM element object or the ID as
|
|
* an argument
|
|
*/
|
|
HackingMission.prototype.getNodeFromElement = function(el) {
|
|
var id;
|
|
if (isString(el)) {
|
|
id = el;
|
|
} else {
|
|
id = el.id;
|
|
}
|
|
id = id.replace("hacking-mission-node-", "");
|
|
var res = id.split('-');
|
|
if (res.length != 2) {
|
|
console.error("Parsing hacking mission node id. could not find coordinates");
|
|
return null;
|
|
}
|
|
var x = res[0], y = res[1];
|
|
if (isNaN(x) || isNaN(y) || x >= 8 || y >= 8 || x < 0 || y < 0) {
|
|
console.error(`Unexpected values (${x}, ${y}) for (x, y)`);
|
|
return null;
|
|
}
|
|
return this.map[x][y];
|
|
}
|
|
|
|
function selectNode(hackMissionInst, el) {
|
|
var nodeObj = hackMissionInst.getNodeFromElement(el);
|
|
if (nodeObj == null) {console.error("Failed getting Node object");}
|
|
if (!nodeObj.plyrCtrl) {return;}
|
|
|
|
clearAllSelectedNodes(hackMissionInst);
|
|
nodeObj.select(hackMissionInst.actionButtons);
|
|
hackMissionInst.selectedNode.push(nodeObj);
|
|
}
|
|
|
|
function multiselectNode(hackMissionInst, el) {
|
|
var nodeObj = hackMissionInst.getNodeFromElement(el);
|
|
if (nodeObj == null) {console.error("Failed getting Node Object in multiselectNode()");}
|
|
if (!nodeObj.plyrCtrl) {return;}
|
|
|
|
clearAllSelectedNodes(hackMissionInst);
|
|
var type = nodeObj.type;
|
|
if (type === NodeTypes.Core) {
|
|
hackMissionInst.playerCores.forEach(function(node) {
|
|
node.select(hackMissionInst.actionButtons);
|
|
hackMissionInst.selectedNode.push(node);
|
|
});
|
|
} else {
|
|
hackMissionInst.playerNodes.forEach(function(node) {
|
|
if (node.type === type) {
|
|
node.select(hackMissionInst.actionButtons);
|
|
hackMissionInst.selectedNode.push(node);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function clearAllSelectedNodes(hackMissionInst) {
|
|
if (hackMissionInst.selectedNode.length > 0) {
|
|
hackMissionInst.selectedNode.forEach(function(node){
|
|
node.deselect(hackMissionInst.actionButtons);
|
|
});
|
|
hackMissionInst.selectedNode.length = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configures a DOM element representing a player-owned node to
|
|
* be selectable and actionable.
|
|
* Note: Does NOT change its css class. This is handled by Node.setControlledBy...
|
|
*/
|
|
HackingMission.prototype.configurePlayerNodeElement = function(el) {
|
|
var nodeObj = this.getNodeFromElement(el);
|
|
if (nodeObj == null) {console.error("Failed getting Node object");}
|
|
|
|
// Add event listener
|
|
const selectNodeWrapper = () => {
|
|
selectNode(this, el);
|
|
}
|
|
el.addEventListener("click", selectNodeWrapper);
|
|
|
|
const multiselectNodeWrapper = () => {
|
|
multiselectNode(this, el);
|
|
}
|
|
el.addEventListener("dblclick", multiselectNodeWrapper);
|
|
|
|
|
|
if (el.firstChild) {
|
|
el.firstChild.addEventListener("click", selectNodeWrapper);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Configures a DOM element representing an enemy-node by removing
|
|
* any event listeners
|
|
*/
|
|
HackingMission.prototype.configureEnemyNodeElement = function(el) {
|
|
// Deselect node if it was the selected node
|
|
var nodeObj = this.getNodeFromElement(el);
|
|
for (var i = 0; i < this.selectedNode.length; ++i) {
|
|
if (this.selectedNode[i] == nodeObj) {
|
|
nodeObj.deselect(this.actionButtons);
|
|
this.selectedNode.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns bool indicating whether a node is reachable by player
|
|
* by checking if any of the adjacent nodes are owned by the player
|
|
*/
|
|
HackingMission.prototype.nodeReachable = function(node) {
|
|
var x = node.pos[0], y = node.pos[1];
|
|
if (x > 0 && this.map[x-1][y].plyrCtrl) {return true;}
|
|
if (x < 7 && this.map[x+1][y].plyrCtrl) {return true;}
|
|
if (y > 0 && this.map[x][y-1].plyrCtrl) {return true;}
|
|
if (y < 7 && this.map[x][y+1].plyrCtrl) {return true;}
|
|
return false;
|
|
}
|
|
|
|
HackingMission.prototype.nodeReachableByEnemy = function(node) {
|
|
if (node == null) {return false;}
|
|
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() {
|
|
this.started = true;
|
|
this.initJsPlumb();
|
|
var startBtn = clearEventListeners("hack-mission-start-btn");
|
|
startBtn.classList.remove("a-link-button");
|
|
startBtn.classList.add("a-link-button-inactive");
|
|
}
|
|
|
|
HackingMission.prototype.initJsPlumb = function() {
|
|
var instance = jsPlumb.getInstance({
|
|
DragOptions:{cursor:"pointer", zIndex:2000},
|
|
PaintStyle: {
|
|
gradient: { stops: [
|
|
[ 0, "#FFFFFF" ],
|
|
[ 1, "#FFFFFF" ],
|
|
] },
|
|
stroke: "#FFFFFF",
|
|
strokeWidth: 8,
|
|
},
|
|
});
|
|
|
|
this.jsplumbinstance = instance;
|
|
|
|
// All player cores are sources
|
|
for (var i = 0; i < this.playerCores.length; ++i) {
|
|
instance.makeSource(this.playerCores[i].el, {
|
|
deleteEndpointsOnEmpty:true,
|
|
maxConnections:1,
|
|
anchor:"Continuous",
|
|
connector:"Flowchart",
|
|
});
|
|
}
|
|
|
|
// Everything else is a target
|
|
for (var i = 0; i < this.enemyCores.length; ++i) {
|
|
instance.makeTarget(this.enemyCores[i].el, {
|
|
maxConnections:-1,
|
|
anchor:"Continuous",
|
|
connector:"Flowchart",
|
|
});
|
|
}
|
|
for (var i = 0; i < this.enemyDatabases.length; ++i) {
|
|
instance.makeTarget(this.enemyDatabases[i].el, {
|
|
maxConnections:-1,
|
|
anchor:"Continuous",
|
|
connector:["Flowchart"],
|
|
});
|
|
}
|
|
for (var i = 0; i < this.enemyNodes.length; ++i) {
|
|
instance.makeTarget(this.enemyNodes[i].el, {
|
|
maxConnections:-1,
|
|
anchor:"Continuous",
|
|
connector:"Flowchart",
|
|
});
|
|
}
|
|
for (var i = 0; i < this.miscNodes.length; ++i) {
|
|
instance.makeTarget(this.miscNodes[i].el, {
|
|
maxConnections:-1,
|
|
anchor:"Continuous",
|
|
connector:"Flowchart",
|
|
});
|
|
}
|
|
|
|
// Clicking a connection drops it
|
|
instance.bind("click", (conn) => {
|
|
// Cannot drop enemy's connections
|
|
const sourceNode = this.getNodeFromElement(conn.source);
|
|
if (sourceNode.enmyCtrl) { return; }
|
|
|
|
var endpoints = conn.endpoints;
|
|
endpoints[0].detachFrom(endpoints[1]);
|
|
});
|
|
|
|
// Connection events
|
|
instance.bind("connection", (info) => {
|
|
var targetNode = this.getNodeFromElement(info.target);
|
|
|
|
// Do not detach for enemy nodes
|
|
var thisNode = this.getNodeFromElement(info.source);
|
|
if (thisNode.enmyCtrl) {return;}
|
|
|
|
// If the node is not reachable, drop the connection
|
|
if (!this.nodeReachable(targetNode)) {
|
|
info.sourceEndpoint.detachFrom(info.targetEndpoint);
|
|
return;
|
|
}
|
|
|
|
var sourceNode = this.getNodeFromElement(info.source);
|
|
sourceNode.conn = info.connection;
|
|
var targetNode = this.getNodeFromElement(info.target);
|
|
++targetNode.targetedCount;
|
|
});
|
|
|
|
// Detach Connection events
|
|
instance.bind("connectionDetached", (info) => {
|
|
var sourceNode = this.getNodeFromElement(info.source);
|
|
sourceNode.conn = null;
|
|
var targetNode = this.getNodeFromElement(info.target);
|
|
targetNode.untarget();
|
|
});
|
|
|
|
}
|
|
|
|
// Drops all connections where the specified node is the source
|
|
HackingMission.prototype.dropAllConnectionsFromNode = function(node) {
|
|
var allConns = this.jsplumbinstance.getAllConnections();
|
|
for (var i = allConns.length-1; i >= 0; --i) {
|
|
if (allConns[i].source == node.el) {
|
|
allConns[i].endpoints[0].detachFrom(allConns[i].endpoints[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Drops all connections where the specified node is the target
|
|
HackingMission.prototype.dropAllConnectionsToNode = function(node) {
|
|
var allConns = this.jsplumbinstance.getAllConnections();
|
|
for (var i = allConns.length-1; i >= 0; --i) {
|
|
if (allConns[i].target == node.el) {
|
|
allConns[i].endpoints[0].detachFrom(allConns[i].endpoints[1]);
|
|
}
|
|
}
|
|
node.beingTargeted = false;
|
|
}
|
|
|
|
var storedCycles = 0;
|
|
HackingMission.prototype.process = function(numCycles=1) {
|
|
if (!this.started) {return;}
|
|
storedCycles += numCycles;
|
|
if (storedCycles < 2) {return;} // Only process every 3 cycles minimum
|
|
|
|
var res = false;
|
|
// Process actions of all player nodes
|
|
this.playerCores.forEach((node) => {
|
|
res |= this.processNode(node, storedCycles);
|
|
});
|
|
|
|
this.playerNodes.forEach((node) => {
|
|
if (node.type === NodeTypes.Transfer ||
|
|
node.type === NodeTypes.Shield ||
|
|
node.type === NodeTypes.Firewall) {
|
|
res |= this.processNode(node, storedCycles);
|
|
}
|
|
});
|
|
|
|
// Process actions of all enemy nodes
|
|
this.enemyCores.forEach((node) => {
|
|
this.enemyAISelectAction(node);
|
|
res |= this.processNode(node, storedCycles);
|
|
});
|
|
|
|
this.enemyNodes.forEach((node) => {
|
|
if (node.type === NodeTypes.Transfer ||
|
|
node.type === NodeTypes.Shield ||
|
|
node.type === NodeTypes.Firewall) {
|
|
this.enemyAISelectAction(node);
|
|
res |= this.processNode(node, storedCycles);
|
|
}
|
|
});
|
|
|
|
// The hp of enemy databases increases slowly
|
|
this.enemyDatabases.forEach((node) => {
|
|
node.maxhp += (0.1 * storedCycles);
|
|
node.hp += (0.1 * storedCycles);
|
|
});
|
|
|
|
if (res) {
|
|
this.calculateAttacks();
|
|
this.calculateDefenses();
|
|
}
|
|
|
|
// Win if all enemy databases are conquered
|
|
if (this.enemyDatabases.length === 0) {
|
|
this.finishMission(true);
|
|
return;
|
|
}
|
|
|
|
// Lose if all your cores are gone
|
|
if (this.playerCores.length === 0) {
|
|
this.finishMission(false);
|
|
return;
|
|
}
|
|
|
|
// Defense/hp of misc nodes increases slowly over time
|
|
this.miscNodes.forEach((node) => {
|
|
node.def += (0.1 * storedCycles);
|
|
node.maxhp += (0.05 * storedCycles);
|
|
node.hp += (0.1 * storedCycles);
|
|
if (node.hp > node.maxhp) {node.hp = node.maxhp;}
|
|
this.updateNodeDomElement(node);
|
|
});
|
|
|
|
// Update timer and check if player lost
|
|
this.time -= (storedCycles * Engine._idleSpeed);
|
|
if (this.time <= 0) {
|
|
this.finishMission(false);
|
|
return;
|
|
}
|
|
this.updateTimer();
|
|
|
|
storedCycles = 0;
|
|
}
|
|
|
|
// Returns a bool representing whether defenses need to be re-calculated
|
|
HackingMission.prototype.processNode = function(nodeObj, numCycles=1) {
|
|
if (nodeObj.action == null) {
|
|
return;
|
|
}
|
|
|
|
var targetNode = null, def, atk;
|
|
if (nodeObj.conn) {
|
|
if (nodeObj.conn.target != null) {
|
|
targetNode = this.getNodeFromElement(nodeObj.conn.target);
|
|
} else {
|
|
targetNode = this.getNodeFromElement(nodeObj.conn.targetId);
|
|
}
|
|
|
|
if (targetNode == null) {
|
|
// Player is in the middle of dragging the connection,
|
|
// so the target node is null. Do nothing here
|
|
} else if (targetNode.plyrCtrl) {
|
|
def = this.playerDef;
|
|
atk = this.enemyAtk;
|
|
} else if (targetNode.enmyCtrl) {
|
|
def = this.enemyDef;
|
|
atk = this.playerAtk;
|
|
} else { // Misc Node
|
|
def = targetNode.def;
|
|
nodeObj.plyrCtrl ? atk = this.playerAtk : atk = this.enemyAtk;
|
|
}
|
|
}
|
|
|
|
// Calculations are per second, so divide everything by 5
|
|
var calcStats = false, plyr = nodeObj.plyrCtrl;
|
|
var enmyHacking = this.difficulty * CONSTANTS.HackingMissionDifficultyToHacking;
|
|
switch(nodeObj.action) {
|
|
case NodeActions.Attack:
|
|
if (targetNode == null) {break;}
|
|
if (nodeObj.conn == null) {break;}
|
|
var dmg = this.calculateAttackDamage(atk, def, plyr ? Player.hacking_skill : enmyHacking);
|
|
targetNode.hp -= (dmg/5 * numCycles);
|
|
break;
|
|
case NodeActions.Scan:
|
|
if (targetNode == null) {break;}
|
|
if (nodeObj.conn == null) {break;}
|
|
var eff = this.calculateScanEffect(atk, def, plyr ? Player.hacking_skill : enmyHacking);
|
|
targetNode.def -= (eff/5 * numCycles);
|
|
calcStats = true;
|
|
break;
|
|
case NodeActions.Weaken:
|
|
if (targetNode == null) {break;}
|
|
if (nodeObj.conn == null) {break;}
|
|
var eff = this.calculateWeakenEffect(atk, def, plyr ? Player.hacking_skill : enmyHacking);
|
|
targetNode.atk -= (eff/5 * numCycles);
|
|
calcStats = true;
|
|
break;
|
|
case NodeActions.Fortify:
|
|
var eff = this.calculateFortifyEffect(Player.hacking_skill);
|
|
nodeObj.def += (eff/5 * numCycles);
|
|
calcStats = true;
|
|
break;
|
|
case NodeActions.Overflow:
|
|
var eff = this.calculateOverflowEffect(Player.hacking_skill);
|
|
if (nodeObj.def < eff) {break;}
|
|
nodeObj.def -= (eff/5 * numCycles);
|
|
nodeObj.atk += (eff/5 * numCycles);
|
|
calcStats = true;
|
|
break;
|
|
default:
|
|
console.error(`Invalid Node Action: ${nodeObj.action}`);
|
|
break;
|
|
}
|
|
|
|
// Stats can't go below 0
|
|
if (nodeObj.atk < 0) {nodeObj.atk = 0;}
|
|
if (nodeObj.def < 0) {nodeObj.def = 0;}
|
|
if (targetNode && targetNode.atk < 0) {targetNode.atk = 0;}
|
|
if (targetNode && targetNode.def < 0) {targetNode.def = 0;}
|
|
|
|
// Conquering a node
|
|
if (targetNode && targetNode.hp <= 0) {
|
|
var conqueredByPlayer = nodeObj.plyrCtrl;
|
|
targetNode.hp = targetNode.maxhp;
|
|
targetNode.action = null;
|
|
targetNode.conn = null;
|
|
if (this.selectedNode == targetNode) {
|
|
targetNode.deselect(this.actionButtons);
|
|
}
|
|
|
|
// The conquered node has its stats reduced
|
|
targetNode.atk /= 2;
|
|
targetNode.def /= 3.5;
|
|
|
|
// Flag for whether the target node was a misc node
|
|
var isMiscNode = !targetNode.plyrCtrl && !targetNode.enmyCtrl;
|
|
|
|
// Remove all connections from Node
|
|
this.dropAllConnectionsToNode(targetNode);
|
|
this.dropAllConnectionsFromNode(targetNode);
|
|
|
|
// Changes the css class and turn the node into a JsPlumb Source/Target
|
|
if (conqueredByPlayer) {
|
|
targetNode.setControlledByPlayer();
|
|
this.jsplumbinstance.unmakeTarget(targetNode.el);
|
|
this.jsplumbinstance.makeSource(targetNode.el, {
|
|
deleteEndpointsOnEmpty:true,
|
|
maxConnections:1,
|
|
anchor:"Continuous",
|
|
connector:"Flowchart",
|
|
});
|
|
} else {
|
|
targetNode.setControlledByEnemy();
|
|
nodeObj.conn = null; // Clear connection
|
|
this.jsplumbinstance.unmakeSource(targetNode.el);
|
|
this.jsplumbinstance.makeTarget(targetNode.el, {
|
|
maxConnections:-1,
|
|
anchor:"Continuous",
|
|
connector:["Flowchart"],
|
|
});
|
|
}
|
|
|
|
calcStats = true;
|
|
|
|
// Helper function to swap nodes between the respective enemyNodes/playerNodes arrays
|
|
function swapNodes(orig, dest, targetNode) {
|
|
for (var i = 0; i < orig.length; ++i) {
|
|
if (orig[i] == targetNode) {
|
|
var node = orig.splice(i, 1);
|
|
node = node[0];
|
|
dest.push(node);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
switch(targetNode.type) {
|
|
case NodeTypes.Core:
|
|
if (conqueredByPlayer) {
|
|
swapNodes(this.enemyCores, this.playerCores, targetNode);
|
|
this.configurePlayerNodeElement(targetNode.el);
|
|
} else {
|
|
swapNodes(this.playerCores, this.enemyCores, targetNode);
|
|
this.configureEnemyNodeElement(targetNode.el);
|
|
}
|
|
break;
|
|
case NodeTypes.Firewall:
|
|
if (conqueredByPlayer) {
|
|
swapNodes(this.enemyNodes, this.playerNodes, targetNode);
|
|
} else {
|
|
swapNodes(this.playerNodes, this.enemyNodes, targetNode);
|
|
this.configureEnemyNodeElement(targetNode.el);
|
|
}
|
|
break;
|
|
case NodeTypes.Database:
|
|
if (conqueredByPlayer) {
|
|
swapNodes(this.enemyDatabases, this.playerNodes, targetNode);
|
|
} else {
|
|
swapNodes(this.playerNodes, this.enemyDatabases, targetNode);
|
|
}
|
|
break;
|
|
case NodeTypes.Spam:
|
|
if (conqueredByPlayer) {
|
|
swapNodes(isMiscNode ? this.miscNodes : this.enemyNodes, this.playerNodes, targetNode);
|
|
// Conquering spam node increases time limit
|
|
this.time += CONSTANTS.HackingMissionSpamTimeIncrease;
|
|
} else {
|
|
swapNodes(isMiscNode ? this.miscNodes : this.playerNodes, this.enemyNodes, targetNode);
|
|
}
|
|
|
|
break;
|
|
case NodeTypes.Transfer:
|
|
// Conquering a Transfer node increases the attack of all cores by some percentages
|
|
if (conqueredByPlayer) {
|
|
swapNodes(isMiscNode ? this.miscNodes : this.enemyNodes, this.playerNodes, targetNode);
|
|
this.playerCores.forEach(function(node) {
|
|
node.atk *= CONSTANTS.HackingMissionTransferAttackIncrease;
|
|
});
|
|
this.configurePlayerNodeElement(targetNode.el);
|
|
} else {
|
|
swapNodes(isMiscNode ? this.miscNodes : this.playerNodes, this.enemyNodes, targetNode);
|
|
this.enemyCores.forEach(function(node) {
|
|
node.atk *= CONSTANTS.HackingMissionTransferAttackIncrease;
|
|
});
|
|
this.configureEnemyNodeElement(targetNode.el);
|
|
}
|
|
break;
|
|
case NodeTypes.Shield:
|
|
if (conqueredByPlayer) {
|
|
swapNodes(isMiscNode ? this.miscNodes : this.enemyNodes, this.playerNodes, targetNode);
|
|
this.configurePlayerNodeElement(targetNode.el);
|
|
} else {
|
|
swapNodes(isMiscNode ? this.miscNodes : this.playerNodes, this.enemyNodes, targetNode);
|
|
this.configureEnemyNodeElement(targetNode.el);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// If a misc node was conquered, the defense for all misc nodes increases by some fixed amount
|
|
if (isMiscNode) { //&& conqueredByPlayer) {
|
|
this.miscNodes.forEach((node) => {
|
|
if (node.targetedCount === 0) {
|
|
node.def *= CONSTANTS.HackingMissionMiscDefenseIncrease;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Update node DOMs
|
|
this.updateNodeDomElement(nodeObj);
|
|
if (targetNode) {this.updateNodeDomElement(targetNode);}
|
|
return calcStats;
|
|
}
|
|
|
|
// Enemy "AI" for CPU Core and 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) {
|
|
// Randomly pick a player node and attack it if its reachable
|
|
var rand = getRandomInt(0, this.playerNodes.length-1);
|
|
var node;
|
|
if (this.playerNodes.length === 0) {
|
|
node = null;
|
|
} else {
|
|
node = this.playerNodes[rand];
|
|
}
|
|
if (this.nodeReachableByEnemy(node)) {
|
|
// Create connection
|
|
nodeObj.conn = this.jsplumbinstance.connect({
|
|
source:nodeObj.el,
|
|
target:node.el,
|
|
});
|
|
++node.targetedCount;
|
|
} else {
|
|
// Randomly pick a player core and attack it if its reachable
|
|
rand = getRandomInt(0, this.playerCores.length-1);
|
|
if (this.playerCores.length === 0) {
|
|
return; // No Misc Nodes, no player Nodes, no Player cores. Player lost
|
|
} else {
|
|
node = this.playerCores[rand];
|
|
}
|
|
|
|
if (this.nodeReachableByEnemy(node)) {
|
|
// Create connection
|
|
nodeObj.conn = this.jsplumbinstance.connect({
|
|
source:nodeObj.el,
|
|
target:node.el,
|
|
});
|
|
++node.targetedCount;
|
|
}
|
|
}
|
|
} else {
|
|
// Randomly pick a misc node and attack it if its reachable
|
|
var rand = getRandomInt(0, this.miscNodes.length-1);
|
|
var node = this.miscNodes[rand];
|
|
if (this.nodeReachableByEnemy(node)) {
|
|
nodeObj.conn = this.jsplumbinstance.connect({
|
|
source:nodeObj.el,
|
|
target:node.el,
|
|
});
|
|
++node.targetedCount;
|
|
}
|
|
}
|
|
|
|
// If no connection was made, set the Core to Fortify
|
|
nodeObj.action = NodeActions.Fortify;
|
|
} else {
|
|
// If this node has a selected target
|
|
var targetNode;
|
|
if (nodeObj.conn.target) {
|
|
targetNode = this.getNodeFromElement(nodeObj.conn.target);
|
|
} else {
|
|
targetNode = this.getNodeFromElement(nodeObj.conn.targetId);
|
|
}
|
|
if (targetNode == null) {
|
|
console.error("Error getting Target node Object in enemyAISelectAction()");
|
|
}
|
|
|
|
if (targetNode.def > this.enemyAtk + 15) {
|
|
if (nodeObj.def < 50) {
|
|
nodeObj.action = NodeActions.Fortify;
|
|
} else {
|
|
nodeObj.action = NodeActions.Overflow;
|
|
}
|
|
} else if (Math.abs(targetNode.def - this.enemyAtk) <= 15) {
|
|
nodeObj.action = NodeActions.Scan;
|
|
} else {
|
|
nodeObj.action = NodeActions.Attack;
|
|
}
|
|
}
|
|
break;
|
|
case NodeTypes.Transfer:
|
|
// Switch between fortifying and overflowing as necessary
|
|
if (nodeObj.def < 125) {
|
|
nodeObj.action = NodeActions.Fortify;
|
|
} else {
|
|
nodeObj.action = NodeActions.Overflow;
|
|
}
|
|
break;
|
|
case NodeTypes.Firewall:
|
|
case NodeTypes.Shield:
|
|
nodeObj.action = NodeActions.Fortify;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
var hackEffWeightSelf = 130; // Weight for Node actions on self
|
|
var hackEffWeightTarget = 25; // Weight for Node Actions against Target
|
|
var hackEffWeightAttack = 80; // Weight for Attack action
|
|
|
|
// Returns damage per cycle based on stats
|
|
HackingMission.prototype.calculateAttackDamage = function(atk, def, hacking = 0) {
|
|
return Math.max(0.55 * (atk + (hacking / hackEffWeightAttack) - def), 1);
|
|
}
|
|
|
|
HackingMission.prototype.calculateScanEffect = function(atk, def, hacking=0) {
|
|
return Math.max(0.6 * ((atk) + hacking / hackEffWeightTarget - (def * 0.95)), 2);
|
|
}
|
|
|
|
HackingMission.prototype.calculateWeakenEffect = function(atk, def, hacking=0) {
|
|
return Math.max((atk) + hacking / hackEffWeightTarget - (def * 0.95), 2);
|
|
}
|
|
|
|
HackingMission.prototype.calculateFortifyEffect = function(hacking=0) {
|
|
return 0.9 * hacking / hackEffWeightSelf;
|
|
}
|
|
|
|
HackingMission.prototype.calculateOverflowEffect = function(hacking=0) {
|
|
return 0.95 * hacking / hackEffWeightSelf;
|
|
}
|
|
|
|
// Updates timer display
|
|
HackingMission.prototype.updateTimer = function() {
|
|
var timer = document.getElementById("hacking-mission-timer");
|
|
|
|
// Convert time remaining to a string of the form mm:ss
|
|
var seconds = Math.round(this.time / 1000);
|
|
var minutes = Math.trunc(seconds / 60);
|
|
seconds %= 60;
|
|
var str = ("0" + minutes).slice(-2) + ":" + ("0" + seconds).slice(-2);
|
|
timer.innerText = "Time left: " + str;
|
|
}
|
|
|
|
// The 'win' argument is a bool for whether or not the player won
|
|
HackingMission.prototype.finishMission = function(win) {
|
|
inMission = false;
|
|
currMission = null;
|
|
|
|
if (win) {
|
|
var favorMult = 1 + (this.faction.favor / 100);
|
|
var gain = this.reward * Player.faction_rep_mult * favorMult;
|
|
dialogBoxCreate(<>Mission won! You earned {Reputation(gain)} reputation with {this.faction.name}</>);
|
|
Player.gainIntelligenceExp(this.difficulty * CONSTANTS.IntelligenceHackingMissionBaseExpGain);
|
|
this.faction.playerReputation += gain;
|
|
} else {
|
|
dialogBoxCreate("Mission lost/forfeited! You did not gain any faction reputation.");
|
|
}
|
|
|
|
// Clear mission container
|
|
var container = document.getElementById("mission-container");
|
|
while(container.firstChild) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
|
|
// Return to Faction page
|
|
document.getElementById("mainmenu-container").style.visibility = "visible";
|
|
document.getElementById("character-overview-wrapper").style.visibility = "visible";
|
|
Engine.loadFactionContent();
|
|
displayFactionContent(this.faction.name);
|
|
}
|
|
|
|
export {HackingMission, inMission, setInMission, currMission};
|