import { CONSTANTS } from "./Constants"; 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) + "
" + "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) + "
" + "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) + "
" + "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) + "
" + "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
" + "HP: " + formatNumber(nodeObj.hp, 1); nodeDiv.classList.add("hack-mission-cpu-node"); break; case NodeTypes.Firewall: txt = "Firewall
" + "HP: " + formatNumber(nodeObj.hp, 1); nodeDiv.classList.add("hack-mission-firewall-node"); break; case NodeTypes.Database: txt = "Database
" + "HP: " + formatNumber(nodeObj.hp, 1); nodeDiv.classList.add("hack-mission-database-node"); break; case NodeTypes.Spam: txt = "Spam
" + "HP: " + formatNumber(nodeObj.hp, 1); nodeDiv.classList.add("hack-mission-spam-node"); break; case NodeTypes.Transfer: txt = "Transfer
" + "HP: " + formatNumber(nodeObj.hp, 1); nodeDiv.classList.add("hack-mission-transfer-node"); break; case NodeTypes.Shield: default: txt = "Shield
" + "HP: " + formatNumber(nodeObj.hp, 1); nodeDiv.classList.add("hack-mission-shield-node"); break; } txt += "
Atk: " + formatNumber(nodeObj.atk, 1) + "
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
" + "HP: " + formatNumber(nodeObj.hp, 1); break; case NodeTypes.Firewall: txt = "Firewall
" + "HP: " + formatNumber(nodeObj.hp, 1); break; case NodeTypes.Database: txt = "Database
" + "HP: " + formatNumber(nodeObj.hp, 1); break; case NodeTypes.Spam: txt = "Spam
" + "HP: " + formatNumber(nodeObj.hp, 1); break; case NodeTypes.Transfer: txt = "Transfer
" + "HP: " + formatNumber(nodeObj.hp, 1); break; case NodeTypes.Shield: default: txt = "Shield
" + "HP: " + formatNumber(nodeObj.hp, 1); break; } txt += "
Atk: " + formatNumber(nodeObj.atk, 1) + "
Def: " + formatNumber(nodeObj.def, 1); if (nodeObj.action) { txt += "
" + 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 * CONSTANTS._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(Math.pow(this.difficulty * CONSTANTS.IntelligenceHackingMissionBaseExpGain, 0.5)); this.faction.playerReputation += gain; } else { dialogBoxCreate("Mission lost/forfeited! You did not gain any faction reputation."); } }; export { HackingMission, inMission, setInMission, currMission };