/** * TODO * Add police clashes * balance point to keep them from running out of control */ import { gangMemberTasksMetadata } from "./data/gangmembertasks"; import { gangMemberUpgradesMetadata } from "./data/gangmemberupgrades"; import { Engine } from "./engine"; import { Faction } from "./Faction/Faction"; import { Factions } from "./Faction/Factions"; import { displayFactionContent } from "./Faction/FactionHelpers"; import { Page, routing } from "./ui/navigationTracking"; import { numeralWrapper } from "./ui/numeralFormat"; import { dialogBoxCreate } from "../utils/DialogBox"; import { Reviver, Generic_toJSON, Generic_fromJSON, } from "../utils/JSONReviver"; import { formatNumber, convertTimeMsToTimeElapsedString, } from "../utils/StringHelperFunctions"; import { exceptionAlert } from "../utils/helpers/exceptionAlert"; import { getRandomInt } from "../utils/helpers/getRandomInt"; import { KEY } from "../utils/helpers/keyCodes"; import { createAccordionElement } from "../utils/uiHelpers/createAccordionElement"; import { createElement } from "../utils/uiHelpers/createElement"; import { createPopup } from "../utils/uiHelpers/createPopup"; import { removeChildrenFromElement } from "../utils/uiHelpers/removeChildrenFromElement"; import { removeElement } from "../utils/uiHelpers/removeElement"; import { removeElementById } from "../utils/uiHelpers/removeElementById"; import { StatsTable } from "./ui/React/StatsTable"; import { Money } from "./ui/React/Money"; import { MoneyRate } from "./ui/React/MoneyRate"; import { Reputation } from "./ui/React/Reputation"; import React from "react"; import ReactDOM from "react-dom"; import { renderToStaticMarkup } from "react-dom/server" // Constants const GangRespectToReputationRatio = 5; // Respect is divided by this to get rep gain const MaximumGangMembers = 30; const CyclesPerTerritoryAndPowerUpdate = 100; const AscensionMultiplierRatio = 15 / 100; // Portion of upgrade multiplier that is kept after ascending // Switch between territory and management screen with 1 and 2 $(document).keydown(function(event) { if (routing.isOn(Page.Gang) && event.altKey) { if (UIElems.gangMemberFilter != null && UIElems.gangMemberFilter === document.activeElement) {return;} if (event.keyCode === KEY["1"]) { if(UIElems.gangTerritorySubpage.style.display === "block") { UIElems.managementButton.click(); } } else if (event.keyCode === KEY["2"]) { if (UIElems.gangManagementSubpage.style.display === "block") { UIElems.territoryButton.click(); } } } }); // Delete upgrade box when clicking outside $(document).mousedown(function(event) { var contentId = "gang-member-upgrade-popup-box-content"; if (UIElems.gangMemberUpgradeBoxOpened) { if ( $(event.target).closest("#" + contentId).get(0) == null ) { //Delete the box removeElement(UIElems.gangMemberUpgradeBox); UIElems.gangMemberUpgradeBox = null; UIElems.gangMemberUpgradeBoxContent = null; UIElems.gangMemberUpgradeBoxOpened = false; UIElems.gangMemberUpgradeBoxElements = null; } } }); const GangNames = [ "Slum Snakes", "Tetrads", "The Syndicate", "The Dark Army", "Speakers for the Dead", "NiteSec", "The Black Hand", ]; export let AllGangs = { "Slum Snakes" : { power: 1, territory: 1/7, }, "Tetrads" : { power: 1, territory: 1/7, }, "The Syndicate" : { power: 1, territory: 1/7, }, "The Dark Army" : { power: 1, territory: 1/7, }, "Speakers for the Dead" : { power: 1, territory: 1/7, }, "NiteSec" : { power: 1, territory: 1/7, }, "The Black Hand" : { power: 1, territory: 1/7, }, } export function resetGangs() { AllGangs = { "Slum Snakes" : { power: 1, territory: 1/7, }, "Tetrads" : { power: 1, territory: 1/7, }, "The Syndicate" : { power: 1, territory: 1/7, }, "The Dark Army" : { power: 1, territory: 1/7, }, "Speakers for the Dead" : { power: 1, territory: 1/7, }, "NiteSec" : { power: 1, territory: 1/7, }, "The Black Hand" : { power: 1, territory: 1/7, }, } } export function loadAllGangs(saveString) { AllGangs = JSON.parse(saveString, Reviver); } /** * @param facName {string} Name of corresponding faction * @param hacking {bollean} Whether or not its a hacking gang */ export function Gang(facName, hacking=false) { this.facName = facName; this.members = []; this.wanted = 1; this.respect = 1; this.isHackingGang = hacking; this.respectGainRate = 0; this.wantedGainRate = 0; this.moneyGainRate = 0; // When processing gains, this stores the number of cycles until some // limit is reached, and then calculates and applies the gains only at that limit this.storedCycles = 0; // Separate variable to keep track of cycles for Territry + Power gang, which // happens on a slower "clock" than normal processing this.storedTerritoryAndPowerCycles = 0; this.territoryClashChance = 0; this.territoryWarfareEngaged = false; this.notifyMemberDeath = true; } Gang.prototype.getPower = function() { return AllGangs[this.facName].power; } Gang.prototype.getTerritory = function() { return AllGangs[this.facName].territory; } Gang.prototype.process = function(numCycles=1, player) { const CyclesPerSecond = 1000 / Engine._idleSpeed; if (isNaN(numCycles)) { console.error(`NaN passed into Gang.process(): ${numCycles}`); } this.storedCycles += numCycles; // Only process if there are at least 2 seconds, and at most 5 seconds if (this.storedCycles < 2 * CyclesPerSecond) { return; } const cycles = Math.min(this.storedCycles, 5 * CyclesPerSecond); try { this.processGains(cycles, player); this.processExperienceGains(cycles); this.processTerritoryAndPowerGains(cycles); this.storedCycles -= cycles; } catch(e) { exceptionAlert(`Exception caught when processing Gang: ${e}`); } } Gang.prototype.processGains = function(numCycles=1, player) { // Get gains per cycle var moneyGains = 0, respectGains = 0, wantedLevelGains = 0; for (var i = 0; i < this.members.length; ++i) { respectGains += (this.members[i].calculateRespectGain(this)); wantedLevelGains += (this.members[i].calculateWantedLevelGain(this)); moneyGains += (this.members[i].calculateMoneyGain(this)); } this.respectGainRate = respectGains; this.wantedGainRate = wantedLevelGains; this.moneyGainRate = moneyGains; if (typeof respectGains === "number") { const gain = respectGains * numCycles; this.respect += gain; // Faction reputation gains is respect gain divided by some constant const fac = Factions[this.facName]; if (!(fac instanceof Faction)) { dialogBoxCreate("ERROR: Could not get Faction associates with your gang. This is a bug, please report to game dev"); } else { var favorMult = 1 + (fac.favor / 100); fac.playerReputation += ((player.faction_rep_mult * gain * favorMult) / GangRespectToReputationRatio); } // Keep track of respect gained per member for (let i = 0; i < this.members.length; ++i) { this.members[i].recordEarnedRespect(numCycles, this); } } else { console.warn("respectGains calculated to be NaN"); } if (typeof wantedLevelGains === "number") { if (this.wanted === 1 && wantedLevelGains < 0) { // At minimum wanted, do nothing } else { const oldWanted = this.wanted; let newWanted = oldWanted + (wantedLevelGains * numCycles); // Prevent overflow if (wantedLevelGains <= 0 && newWanted > oldWanted) { newWanted = 1; } this.wanted = newWanted; if (this.wanted < 1) {this.wanted = 1;} } } else { console.warn("ERROR: wantedLevelGains is NaN"); } if (typeof moneyGains === "number") { player.gainMoney(moneyGains * numCycles); player.recordMoneySource(moneyGains * numCycles, "gang"); } else { console.warn("ERROR: respectGains is NaN"); } } function calculateTerritoryGain(winGang, loseGang) { const powerBonus = Math.max(1, 1+Math.log(AllGangs[winGang].power/AllGangs[loseGang].power)/Math.log(50)); const gains = Math.min(AllGangs[loseGang].territory, powerBonus*0.0001*(Math.random()+.5)) return gains; } Gang.prototype.processTerritoryAndPowerGains = function(numCycles=1) { this.storedTerritoryAndPowerCycles += numCycles; if (this.storedTerritoryAndPowerCycles < CyclesPerTerritoryAndPowerUpdate) { return; } this.storedTerritoryAndPowerCycles -= CyclesPerTerritoryAndPowerUpdate; // Process power first const gangName = this.facName; for (const name in AllGangs) { if (AllGangs.hasOwnProperty(name)) { if (name == gangName) { AllGangs[name].power += this.calculatePower(); } else { // All NPC gangs get random power gains const gainRoll = Math.random(); if (gainRoll < 0.5) { // Multiplicative gain (50% chance) // This is capped per cycle, to prevent it from getting out of control const multiplicativeGain = AllGangs[name].power * 0.005; AllGangs[name].power += Math.min(0.85, multiplicativeGain); } else { // Additive gain (50% chance) const additiveGain = 0.75 * gainRoll * AllGangs[name].territory; AllGangs[name].power += (additiveGain); } } } } // Determine if territory should be processed if (this.territoryWarfareEngaged) { this.territoryClashChance = 1; } else if (this.territoryClashChance > 0) { // Engagement turned off, but still a positive clash chance. So there's // still a chance of clashing but it slowly goes down over time this.territoryClashChance = Math.max(0, this.territoryClashChance - 0.01); } // Then process territory for (let i = 0; i < GangNames.length; ++i) { const others = GangNames.filter((e) => { return e !== GangNames[i]; }); const other = getRandomInt(0, others.length - 1); const thisGang = GangNames[i]; const otherGang = others[other]; // If either of the gangs involved in this clash is the player, determine // whether to skip or process it using the clash chance if (thisGang === gangName || otherGang === gangName) { if (!(Math.random() < this.territoryClashChance)) { continue; } } const thisPwr = AllGangs[thisGang].power; const otherPwr = AllGangs[otherGang].power; const thisChance = thisPwr / (thisPwr + otherPwr); if (Math.random() < thisChance) { if (AllGangs[otherGang].territory <= 0) { return; } const territoryGain = calculateTerritoryGain(thisGang, otherGang); AllGangs[thisGang].territory += territoryGain; AllGangs[otherGang].territory -= territoryGain; if (thisGang === gangName) { this.clash(true); // Player won AllGangs[otherGang].power *= (1 / 1.01); } else if (otherGang === gangName) { this.clash(false); // Player lost } else { AllGangs[otherGang].power *= (1 / 1.01); } } else { if (AllGangs[thisGang].territory <= 0) { return; } const territoryGain = calculateTerritoryGain(otherGang, thisGang); AllGangs[thisGang].territory -= territoryGain; AllGangs[otherGang].territory += territoryGain; if (thisGang === gangName) { this.clash(false); // Player lost } else if (otherGang === gangName) { this.clash(true); // Player won AllGangs[thisGang].power *= (1 / 1.01); } else { AllGangs[thisGang].power *= (1 / 1.01); } } } } Gang.prototype.canRecruitMember = function() { if (this.members.length >= MaximumGangMembers) { return false; } return (this.respect >= this.getRespectNeededToRecruitMember()); } Gang.prototype.getRespectNeededToRecruitMember = function() { // First N gang members are free (can be recruited at 0 respect) const numFreeMembers = 3; if (this.members.length < numFreeMembers) { return 0; } const i = this.members.length - (numFreeMembers - 1); return Math.round(0.9 * Math.pow(i, 3) + Math.pow(i, 2)); } Gang.prototype.recruitMember = function(name) { name = String(name); if (name === "" || !this.canRecruitMember()) { return false; } // Check for already-existing names let sameNames = this.members.filter((m) => { return m.name === name; }); if (sameNames.length >= 1) { return false; } let member = new GangMember(name); this.members.push(member); if (routing.isOn(Page.Gang)) { this.createGangMemberDisplayElement(member); this.updateGangContent(); } return true; } // Money and Respect gains multiplied by this number (< 1) Gang.prototype.getWantedPenalty = function() { return (this.respect) / (this.respect + this.wanted); } Gang.prototype.processExperienceGains = function(numCycles=1) { for (var i = 0; i < this.members.length; ++i) { this.members[i].gainExperience(numCycles); this.members[i].updateSkillLevels(); } } //Calculates power GAIN, which is added onto the Gang's existing power Gang.prototype.calculatePower = function() { var memberTotal = 0; for (var i = 0; i < this.members.length; ++i) { if (GangMemberTasks.hasOwnProperty(this.members[i].task) && this.members[i].task == "Territory Warfare") { const gain = this.members[i].calculatePower(); memberTotal += gain; } } return (0.015 * this.getTerritory() * memberTotal); } Gang.prototype.clash = function(won=false) { // Determine if a gang member should die let baseDeathChance = 0.01; if (won) { baseDeathChance /= 2; } // If the clash was lost, the player loses a small percentage of power if (!won) { AllGangs[this.facName].power *= (1 / 1.008); } // Deaths can only occur during X% of clashes if (Math.random() < 0.65) { return; } for (let i = this.members.length - 1; i >= 0; --i) { const member = this.members[i]; // Only members assigned to Territory Warfare can die if (member.task !== "Territory Warfare") { continue; } // Chance to die is decreased based on defense const modifiedDeathChance = baseDeathChance / Math.pow(member.def, 0.6); if (Math.random() < modifiedDeathChance) { this.killMember(member); } } } Gang.prototype.killMember = function(memberObj) { // Player loses a percentage of total respect, plus whatever respect that member has earned const totalRespect = this.respect; const lostRespect = (0.05 * totalRespect) + memberObj.earnedRespect; this.respect = Math.max(0, totalRespect - lostRespect); for (let i = 0; i < this.members.length; ++i) { if (memberObj.name === this.members[i].name) { this.members.splice(i, 1); break; } } // Notify of death if (this.notifyMemberDeath) { dialogBoxCreate(`${memberObj.name} was killed in a gang clash! You lost ${lostRespect} respect`); } // Update UI if (routing.isOn(Page.Gang)) { this.displayGangMemberList(); } } Gang.prototype.ascendMember = function(memberObj, workerScript) { try { /** * res is an object with the following format: * { * respect: Amount of respect to deduct * hack/str/def/dex/agi/cha: Ascension multipliers gained for each stat * } */ const res = memberObj.ascend(); this.respect = Math.max(1, this.respect - res.respect); if (workerScript == null) { dialogBoxCreate([`You ascended ${memberObj.name}!`, "", `Your gang lost ${numeralWrapper.formatRespect(res.respect)} respect`, "", `${memberObj.name} gained the following stat multipliers for ascending:`, `Hacking: ${numeralWrapper.formatPercentage(res.hack, 3)}`, `Strength: ${numeralWrapper.formatPercentage(res.str, 3)}`, `Defense: ${numeralWrapper.formatPercentage(res.def, 3)}`, `Dexterity: ${numeralWrapper.formatPercentage(res.dex, 3)}`, `Agility: ${numeralWrapper.formatPercentage(res.agi, 3)}`, `Charisma: ${numeralWrapper.formatPercentage(res.cha, 3)}`].join("
")); } else { workerScript.log(`Ascended Gang member ${memberObj.name}`); } if (routing.isOn(Page.Gang)) { this.displayGangMemberList(); } return res; } catch(e) { if (workerScript == null) { exceptionAlert(e); } else { throw e; // Re-throw, will be caught in the Netscript Function } } } // Cost of upgrade gets cheaper as gang increases in respect + power Gang.prototype.getDiscount = function() { const power = this.getPower(); const respect = this.respect; const respectLinearFac = 5e6; const powerLinearFac = 1e6; const discount = Math.pow(respect, 0.01) + respect / respectLinearFac + Math.pow(power, 0.01) + power / powerLinearFac - 1; return Math.max(1, discount); } // Returns only valid tasks for this gang. Excludes 'Unassigned' Gang.prototype.getAllTaskNames = function() { let tasks = []; const allTasks = Object.keys(GangMemberTasks); if (this.isHackingGang) { tasks = allTasks.filter((e) => { let task = GangMemberTasks[e]; if (task == null) { return false; } if (e === "Unassigned") { return false; } return task.isHacking; }); } else { tasks = allTasks.filter((e) => { let task = GangMemberTasks[e]; if (task == null) { return false; } if (e === "Unassigned") { return false; } return task.isCombat; }); } return tasks; } Gang.prototype.getAllUpgradeNames = function() { return Object.keys(GangMemberUpgrades); } Gang.prototype.getUpgradeCost = function(upgName) { if (GangMemberUpgrades[upgName] == null) { return Infinity; } return GangMemberUpgrades[upgName].getCost(this); } // Returns a player-friendly string stating the type of the specified upgrade Gang.prototype.getUpgradeType = function(upgName) { const upg = GangMemberUpgrades[upgName]; if (upg == null) { return ""; } switch (upg.type) { case "w": return "Weapon"; case "a": return "Armor"; case "v": return "Vehicle"; case "r": return "Rootkit"; case "g": return "Augmentation"; default: return ""; } } Gang.prototype.toJSON = function() { return Generic_toJSON("Gang", this); } Gang.fromJSON = function(value) { return Generic_fromJSON(Gang, value.data); } Reviver.constructors.Gang = Gang; function GangMember(name) { this.name = name; this.task = "Unassigned"; this.earnedRespect = 0; this.hack = 1; this.str = 1; this.def = 1; this.dex = 1; this.agi = 1; this.cha = 1; this.hack_exp = 0; this.str_exp = 0; this.def_exp = 0; this.dex_exp = 0; this.agi_exp = 0; this.cha_exp = 0; this.hack_mult = 1; this.str_mult = 1; this.def_mult = 1; this.dex_mult = 1; this.agi_mult = 1; this.cha_mult = 1; this.hack_asc_mult = 1; this.str_asc_mult = 1; this.def_asc_mult = 1; this.dex_asc_mult = 1; this.agi_asc_mult = 1; this.cha_asc_mult = 1; this.upgrades = []; // Names of upgrades this.augmentations = []; // Names of augmentations only } // Same skill calculation formula as Player GangMember.prototype.calculateSkill = function(exp, mult=1) { return Math.max(Math.floor(mult * (32 * Math.log(exp + 534.5) - 200)), 1); } GangMember.prototype.updateSkillLevels = function() { this.hack = this.calculateSkill(this.hack_exp, this.hack_mult * this.hack_asc_mult); this.str = this.calculateSkill(this.str_exp, this.str_mult * this.str_asc_mult); this.def = this.calculateSkill(this.def_exp, this.def_mult * this.def_asc_mult); this.dex = this.calculateSkill(this.dex_exp, this.dex_mult * this.dex_asc_mult); this.agi = this.calculateSkill(this.agi_exp, this.agi_mult * this.agi_asc_mult); this.cha = this.calculateSkill(this.cha_exp, this.cha_mult * this.cha_asc_mult); } GangMember.prototype.calculatePower = function() { return (this.hack + this.str + this.def + this.dex + this.agi + this.cha) / 95; } GangMember.prototype.assignToTask = function(taskName) { if (GangMemberTasks.hasOwnProperty(taskName)) { this.task = taskName; return true; } else { this.task = "Unassigned"; return false; } } GangMember.prototype.unassignFromTask = function() { this.task = "Unassigned"; } GangMember.prototype.getTask = function() { // Backwards compatibility if (this.task instanceof GangMemberTask) { this.task = this.task.name; } if (GangMemberTasks.hasOwnProperty(this.task)) { return GangMemberTasks[this.task]; } return GangMemberTasks["Unassigned"]; } // Gains are per cycle GangMember.prototype.calculateRespectGain = function(gang) { const task = this.getTask(); if (task == null || !(task instanceof GangMemberTask) || task.baseRespect === 0) {return 0;} var statWeight = (task.hackWeight/100) * this.hack + (task.strWeight/100) * this.str + (task.defWeight/100) * this.def + (task.dexWeight/100) * this.dex + (task.agiWeight/100) * this.agi + (task.chaWeight/100) * this.cha; statWeight -= (4 * task.difficulty); if (statWeight <= 0) { return 0; } const territoryMult = Math.pow(AllGangs[gang.facName].territory * 100, task.territory.respect) / 100; if (isNaN(territoryMult) || territoryMult <= 0) { return 0; } var respectMult = gang.getWantedPenalty(); return 11 * task.baseRespect * statWeight * territoryMult * respectMult; } GangMember.prototype.calculateWantedLevelGain = function(gang) { const task = this.getTask(); if (task == null || !(task instanceof GangMemberTask) || task.baseWanted === 0) { return 0; } let statWeight = (task.hackWeight / 100) * this.hack + (task.strWeight / 100) * this.str + (task.defWeight / 100) * this.def + (task.dexWeight / 100) * this.dex + (task.agiWeight / 100) * this.agi + (task.chaWeight / 100) * this.cha; statWeight -= (3.5 * task.difficulty); if (statWeight <= 0) { return 0; } const territoryMult = Math.pow(AllGangs[gang.facName].territory * 100, task.territory.wanted) / 100; if (isNaN(territoryMult) || territoryMult <= 0) { return 0; } if (task.baseWanted < 0) { return 0.4 * task.baseWanted * statWeight * territoryMult; } else { const calc = 7 * task.baseWanted / (Math.pow(3 * statWeight * territoryMult, 0.8)); // Put an arbitrary cap on this to prevent wanted level from rising too fast if the // denominator is very small. Might want to rethink formula later return Math.min(100, calc); } } GangMember.prototype.calculateMoneyGain = function(gang) { const task = this.getTask(); if (task == null || !(task instanceof GangMemberTask) || task.baseMoney === 0) {return 0;} var statWeight = (task.hackWeight/100) * this.hack + (task.strWeight/100) * this.str + (task.defWeight/100) * this.def + (task.dexWeight/100) * this.dex + (task.agiWeight/100) * this.agi + (task.chaWeight/100) * this.cha; statWeight -= (3.2 * task.difficulty); if (statWeight <= 0) { return 0; } const territoryMult = Math.pow(AllGangs[gang.facName].territory * 100, task.territory.money) / 100; if (isNaN(territoryMult) || territoryMult <= 0) { return 0; } var respectMult = gang.getWantedPenalty(); return 5 * task.baseMoney * statWeight * territoryMult * respectMult; } GangMember.prototype.gainExperience = function(numCycles=1) { const task = this.getTask(); if (task == null || !(task instanceof GangMemberTask) || task === GangMemberTasks["Unassigned"]) {return;} const difficultyMult = Math.pow(task.difficulty, 0.9); const difficultyPerCycles = difficultyMult * numCycles; const weightDivisor = 1500; this.hack_exp += (task.hackWeight / weightDivisor) * difficultyPerCycles; this.str_exp += (task.strWeight / weightDivisor) * difficultyPerCycles; this.def_exp += (task.defWeight / weightDivisor) * difficultyPerCycles; this.dex_exp += (task.dexWeight / weightDivisor) * difficultyPerCycles; this.agi_exp += (task.agiWeight / weightDivisor) * difficultyPerCycles; this.cha_exp += (task.chaWeight / weightDivisor) * difficultyPerCycles; } GangMember.prototype.recordEarnedRespect = function(numCycles=1, gang) { this.earnedRespect += (this.calculateRespectGain(gang) * numCycles); } GangMember.prototype.ascend = function() { const res = this.getAscensionResults(); const hackAscMult = res.hack; const strAscMult = res.str; const defAscMult = res.def; const dexAscMult = res.dex; const agiAscMult = res.agi; const chaAscMult = res.cha; this.hack_asc_mult += hackAscMult; this.str_asc_mult += strAscMult; this.def_asc_mult += defAscMult; this.dex_asc_mult += dexAscMult; this.agi_asc_mult += agiAscMult; this.cha_asc_mult += chaAscMult; // Remove upgrades. Then re-calculate multipliers and stats this.upgrades.length = 0; this.hack_mult = 1; this.str_mult = 1; this.def_mult = 1; this.dex_mult = 1; this.agi_mult = 1; this.cha_mult = 1; for (let i = 0; i < this.augmentations.length; ++i) { let aug = GangMemberUpgrades[this.augmentations[i]]; aug.apply(this); } // Clear exp and recalculate stats this.hack_exp = 0; this.str_exp = 0; this.def_exp = 0; this.dex_exp = 0; this.agi_exp = 0; this.cha_exp = 0; this.updateSkillLevels(); const respectToDeduct = this.earnedRespect; this.earnedRespect = 0; return { respect: respectToDeduct, hack: hackAscMult, str: strAscMult, def: defAscMult, dex: dexAscMult, agi: agiAscMult, cha: chaAscMult, }; } GangMember.prototype.getAscensionEfficiency = function() { function formula(mult) { return 1/(1+Math.log(mult)/Math.log(20)); } return { hack: formula(this.hack_asc_mult), str: formula(this.str_asc_mult), def: formula(this.def_asc_mult), dex: formula(this.dex_asc_mult), agi: formula(this.agi_asc_mult), cha: formula(this.cha_asc_mult), }; } // Returns the multipliers that would be gained from ascension GangMember.prototype.getAscensionResults = function() { /** * Calculate ascension bonus to stat multipliers. * This is based on the current number of multipliers from Non-Augmentation upgrades * + Ascension Bonus = N% of current bonus from Augmentations */ let hack = 1; let str = 1; let def = 1; let dex = 1; let agi = 1; let cha = 1; for (let i = 0; i < this.upgrades.length; ++i) { let upg = GangMemberUpgrades[this.upgrades[i]]; if (upg.mults.hack != null) { hack *= upg.mults.hack; } if (upg.mults.str != null) { str *= upg.mults.str; } if (upg.mults.def != null) { def *= upg.mults.def; } if (upg.mults.dex != null) { dex *= upg.mults.dex; } if (upg.mults.agi != null) { agi *= upg.mults.agi; } if (upg.mults.cha != null) { cha *= upg.mults.cha; } } // Subtract 1 because we're only interested in the actual "bonus" part const eff = this.getAscensionEfficiency(); return { hack: (Math.max(0, hack - 1) * AscensionMultiplierRatio * eff.hack), str: (Math.max(0, str - 1) * AscensionMultiplierRatio * eff.str), def: (Math.max(0, def - 1) * AscensionMultiplierRatio * eff.def), dex: (Math.max(0, dex - 1) * AscensionMultiplierRatio * eff.dex), agi: (Math.max(0, agi - 1) * AscensionMultiplierRatio * eff.agi), cha: (Math.max(0, cha - 1) * AscensionMultiplierRatio * eff.cha), } } GangMember.prototype.buyUpgrade = function(upg, player, gang) { if (typeof upg === 'string') { upg = GangMemberUpgrades[upg]; } if (!(upg instanceof GangMemberUpgrade)) { return false; } // Prevent purchasing of already-owned upgrades if (this.augmentations.includes(upg.name) || this.upgrades.includes(upg.name)) { return false; } if (player.money.lt(upg.getCost(gang))) { return false; } player.loseMoney(upg.getCost(gang)); if (upg.type === "g") { this.augmentations.push(upg.name); } else { this.upgrades.push(upg.name); } upg.apply(this); if (routing.isOn(Page.Gang) && UIElems.gangMemberUpgradeBoxOpened) { var initFilterValue = UIElems.gangMemberUpgradeBoxFilter.value.toString(); gang.createGangMemberUpgradeBox(player, initFilterValue); } return true; } GangMember.prototype.toJSON = function() { return Generic_toJSON("GangMember", this); } GangMember.fromJSON = function(value) { return Generic_fromJSON(GangMember, value.data); } Reviver.constructors.GangMember = GangMember; // Defines tasks that Gang Members can work on function GangMemberTask(name="", desc="", isHacking=false, isCombat=false, params={baseRespect: 0, baseWanted: 0, baseMoney: 0, hackWeight: 0, strWeight: 0, defWeight: 0, dexWeight: 0, agiWeight: 0, chaWeight: 0, difficulty: 0}) { this.name = name; this.desc = desc; // Flags that describe whether this Task is applicable for Hacking/Combat gangs this.isHacking = isHacking; this.isCombat = isCombat; // Base gain rates for respect/wanted/money this.baseRespect = params.baseRespect ? params.baseRespect : 0; this.baseWanted = params.baseWanted ? params.baseWanted : 0; this.baseMoney = params.baseMoney ? params.baseMoney : 0; // Weighting for the effect that each stat has on the tasks effectiveness. // Weights must add up to 100 this.hackWeight = params.hackWeight ? params.hackWeight : 0; this.strWeight = params.strWeight ? params.strWeight : 0; this.defWeight = params.defWeight ? params.defWeight : 0; this.dexWeight = params.dexWeight ? params.dexWeight : 0; this.agiWeight = params.agiWeight ? params.agiWeight : 0; this.chaWeight = params.chaWeight ? params.chaWeight : 0; if (Math.round(this.hackWeight + this.strWeight + this.defWeight + this.dexWeight + this.agiWeight + this.chaWeight) != 100) { console.error(`GangMemberTask ${this.name} weights do not add up to 100`); } // 1 - 100 this.difficulty = params.difficulty ? params.difficulty : 1; // Territory Factors. Exponential factors that dictate how territory affects gains // Formula: Territory Mutiplier = (Territory * 100) ^ factor / 100 // So factor should be > 1 if something should scale exponentially with territory // and should be < 1 if it should have diminshing returns this.territory = params.territory ? params.territory : {money: 1, respect: 1, wanted: 1}; } GangMemberTask.prototype.toJSON = function() { return Generic_toJSON("GangMemberTask", this); } GangMemberTask.fromJSON = function(value) { return Generic_fromJSON(GangMemberTask, value.data); } Reviver.constructors.GangMemberTask = GangMemberTask; export const GangMemberTasks = {}; function addGangMemberTask(name, desc, isHacking, isCombat, params) { GangMemberTasks[name] = new GangMemberTask(name, desc, isHacking, isCombat, params); } gangMemberTasksMetadata.forEach((e) => { addGangMemberTask(e.name, e.desc, e.isHacking, e.isCombat, e.params); }); function GangMemberUpgrade(name="", cost=0, type="w", mults={}) { this.name = name; this.cost = cost; this.type = type; //w = weapon, a = armor, v = vehicle, r = rootkit, g = Aug this.mults = mults; this.createDescription(); } GangMemberUpgrade.prototype.getCost = function(gang) { const discount = gang.getDiscount(); return this.cost / discount; } GangMemberUpgrade.prototype.createDescription = function() { const lines = ["Increases:"]; if (this.mults.str != null) { lines.push(`* Strength by ${Math.round((this.mults.str - 1) * 100)}%`); } if (this.mults.def != null) { lines.push(`* Defense by ${Math.round((this.mults.def - 1) * 100)}%`); } if (this.mults.dex != null) { lines.push(`* Dexterity by ${Math.round((this.mults.dex - 1) * 100)}%`); } if (this.mults.agi != null) { lines.push(`* Agility by ${Math.round((this.mults.agi - 1) * 100)}%`); } if (this.mults.cha != null) { lines.push(`* Charisma by ${Math.round((this.mults.cha - 1) * 100)}%`); } if (this.mults.hack != null) { lines.push(`* Hacking by ${Math.round((this.mults.hack - 1) * 100)}%`); } this.desc = lines.join("
"); } // Passes in a GangMember object GangMemberUpgrade.prototype.apply = function(member) { if (this.mults.str != null) { member.str_mult *= this.mults.str; } if (this.mults.def != null) { member.def_mult *= this.mults.def; } if (this.mults.dex != null) { member.dex_mult *= this.mults.dex; } if (this.mults.agi != null) { member.agi_mult *= this.mults.agi; } if (this.mults.cha != null) { member.cha_mult *= this.mults.cha; } if (this.mults.hack != null) { member.hack_mult *= this.mults.hack; } return; } GangMemberUpgrade.prototype.toJSON = function() { return Generic_toJSON("GangMemberUpgrade", this); } GangMemberUpgrade.fromJSON = function(value) { return Generic_fromJSON(GangMemberUpgrade, value.data); } Reviver.constructors.GangMemberUpgrade = GangMemberUpgrade; // Initialize Gang Member Upgrades export const GangMemberUpgrades = {} function addGangMemberUpgrade(name, cost, type, mults) { GangMemberUpgrades[name] = new GangMemberUpgrade(name, cost, type, mults); } gangMemberUpgradesMetadata.forEach((e) => { addGangMemberUpgrade(e.name, e.cost, e.upgType, e.mults); }); // Create a pop-up box that lets player purchase upgrades Gang.prototype.createGangMemberUpgradeBox = function(player, initialFilter="") { const boxId = "gang-member-upgrade-popup-box"; if (UIElems.gangMemberUpgradeBoxOpened) { // Already opened, refreshing if (UIElems.gangMemberUpgradeBoxElements == null || UIElems.gangMemberUpgradeBox == null || UIElems.gangMemberUpgradeBoxContent == null) { console.error("Refreshing Gang member upgrade box throws error because required elements are null"); return; } for (var i = 2; i < UIElems.gangMemberUpgradeBoxElements.length; ++i) { removeElement(UIElems.gangMemberUpgradeBoxElements[i]); } UIElems.gangMemberUpgradeBoxElements = [UIElems.gangMemberUpgradeBoxFilter, UIElems.gangMemberUpgradeBoxDiscount]; var filter = UIElems.gangMemberUpgradeBoxFilter.value.toString(); for (var i = 0; i < this.members.length; ++i) { if (this.members[i].name.indexOf(filter) > -1 || this.members[i].task.indexOf(filter) > -1) { var newPanel = this.members[i].createGangMemberUpgradePanel(this, player); UIElems.gangMemberUpgradeBoxContent.appendChild(newPanel); UIElems.gangMemberUpgradeBoxElements.push(newPanel); } } } else { // New popup UIElems.gangMemberUpgradeBoxFilter = createElement("input", { type:"text", placeholder:"Filter gang members", class: "text-input", value:initialFilter, onkeyup:() => { var filterValue = UIElems.gangMemberUpgradeBoxFilter.value.toString(); this.createGangMemberUpgradeBox(player, filterValue); }, }); UIElems.gangMemberUpgradeBoxDiscount = createElement("p", { innerText: "Discount: -" + numeralWrapper.formatPercentage(1 - 1 / this.getDiscount()), marginLeft: "6px", tooltip: "You get a discount on equipment and upgrades based on your gang's " + "respect and power. More respect and power leads to more discounts.", }); UIElems.gangMemberUpgradeBoxElements = [UIElems.gangMemberUpgradeBoxFilter, UIElems.gangMemberUpgradeBoxDiscount]; var filter = UIElems.gangMemberUpgradeBoxFilter.value.toString(); for (var i = 0; i < this.members.length; ++i) { if (this.members[i].name.indexOf(filter) > -1 || this.members[i].task.indexOf(filter) > -1) { UIElems.gangMemberUpgradeBoxElements.push(this.members[i].createGangMemberUpgradePanel(this, player)); } } UIElems.gangMemberUpgradeBox = createPopup(boxId, UIElems.gangMemberUpgradeBoxElements); UIElems.gangMemberUpgradeBoxContent = document.getElementById(boxId + "-content"); UIElems.gangMemberUpgradeBoxOpened = true; } } // Create upgrade panels for each individual Gang Member GangMember.prototype.createGangMemberUpgradePanel = function(gangObj, player) { var container = createElement("div", { border:"1px solid white", }); var header = createElement("h1", { innerText: this.name + " (" + this.task + ")", }); container.appendChild(header); var text = createElement("pre", { fontSize:"14px", display: "inline-block", width:"20%", innerText: "Hack: " + this.hack + " (x" + formatNumber(this.hack_mult * this.hack_asc_mult, 2) + ")\n" + "Str: " + this.str + " (x" + formatNumber(this.str_mult * this.str_asc_mult, 2) + ")\n" + "Def: " + this.def + " (x" + formatNumber(this.def_mult * this.def_asc_mult, 2) + ")\n" + "Dex: " + this.dex + " (x" + formatNumber(this.dex_mult * this.dex_asc_mult, 2) + ")\n" + "Agi: " + this.agi + " (x" + formatNumber(this.agi_mult * this.agi_asc_mult, 2) + ")\n" + "Cha: " + this.cha + " (x" + formatNumber(this.cha_mult * this.cha_asc_mult, 2) + ")\n", }); // Already purchased upgrades const ownedUpgradesElements = []; function pushOwnedUpgrade(upgName) { const upg = GangMemberUpgrades[upgName]; if (upg == null) { console.error(`Could not find GangMemberUpgrade object for name ${upgName}`); return; } ownedUpgradesElements.push(createElement("div", { class: "gang-owned-upgrade", innerText: upgName, tooltip: upg.desc, })); } for (const upgName of this.upgrades) { pushOwnedUpgrade(upgName); } for (const upgName of this.augmentations) { pushOwnedUpgrade(upgName); } var ownedUpgrades = createElement("div", { class: "gang-owned-upgrades-div", innerText: "Purchased Upgrades:", }); for (const elem of ownedUpgradesElements) { ownedUpgrades.appendChild(elem); } container.appendChild(text); container.appendChild(ownedUpgrades); container.appendChild(createElement("br", {})); // Upgrade buttons. Only show upgrades that can be afforded const weaponUpgrades = []; const armorUpgrades = []; const vehicleUpgrades = []; const rootkitUpgrades = []; const augUpgrades = []; for (let upgName in GangMemberUpgrades) { if (GangMemberUpgrades.hasOwnProperty(upgName)) { let upg = GangMemberUpgrades[upgName]; if (player.money.lt(upg.getCost(gangObj))) { continue; } if (this.upgrades.includes(upgName) || this.augmentations.includes(upgName)) { continue; } switch (upg.type) { case "w": weaponUpgrades.push(upg); break; case "a": armorUpgrades.push(upg); break; case "v": vehicleUpgrades.push(upg); break; case "r": rootkitUpgrades.push(upg); break; case "g": augUpgrades.push(upg); break; default: console.error(`ERROR: Invalid Gang Member Upgrade Type: ${upg.type}`); } } } // Create separate columns for each upgrade type const weaponDiv = createElement("div", {width: "20%", display: "inline-block"}); const armorDiv = createElement("div", {width: "20%", display: "inline-block"}); const vehicleDiv = createElement("div", {width: "20%", display: "inline-block"}); const rootkitDiv = createElement("div", {width: "20%", display: "inline-block"}); const augDiv = createElement("div", {width: "20%", display: "inline-block"}); // Add a title/labe for each column weaponDiv.appendChild(createElement("h2", {innerText: "Weapons"})); armorDiv.appendChild(createElement("h2", {innerText: "Armor"})); vehicleDiv.appendChild(createElement("h2", {innerText: "Vehicles"})); rootkitDiv.appendChild(createElement("h2", {innerText: "Rootkits"})); augDiv.appendChild(createElement("h2", {innerText: "Augmentations"})); // Add buttons to purchase each upgrade const upgrades = [weaponUpgrades, armorUpgrades, vehicleUpgrades, rootkitUpgrades, augUpgrades]; const divs = [weaponDiv, armorDiv, vehicleDiv, rootkitDiv, augDiv]; for (let i = 0; i < upgrades.length; ++i) { let upgradeArray = upgrades[i]; let div = divs[i]; for (let j = 0; j < upgradeArray.length; ++j) { let upg = upgradeArray[j]; (function (upg, div, memberObj, i, gang) { let createElementParams = { innerHTML: `${upg.name} - ${renderToStaticMarkup(Money(upg.getCost(gang)))}`, class: "a-link-button", margin:"2px", padding:"2px", display:"block", fontSize:"11px", clickListener:() => { memberObj.buyUpgrade(upg, player, gangObj); return false; }, } // For the last two divs, tooltip should be on the left if (i >= 3) { createElementParams.tooltipleft = upg.desc; } else { createElementParams.tooltip = upg.desc; } div.appendChild(createElement("a", createElementParams)); })(upg, div, this, i, gangObj); } } container.appendChild(weaponDiv); container.appendChild(armorDiv); container.appendChild(vehicleDiv); container.appendChild(rootkitDiv); container.appendChild(augDiv); return container; } // Gang UI Dom Elements const UIElems = { // Main elems gangContentCreated: false, gangContainer: null, managementButton: null, territoryButton: null, // Subpages gangManagementSubpage: null, gangTerritorySubpage: null, // Gang Management Subpage Elements gangDesc: null, gangInfo: null, gangRecruitMemberButton: null, gangRecruitRequirementText: null, gangExpandAllButton: null, gangCollapseAllButton: null, gangMemberFilter: null, gangManageEquipmentButton: null, gangMemberList: null, gangMemberPanels: {}, // Gang Equipment Upgrade Elements gangMemberUpgradeBoxOpened: false, gangMemberUpgradeBox: null, gangMemberUpgradeBoxContent: null, gangMemberUpgradeBoxFilter: null, gangMemberUpgradeBoxDiscount: null, gangMemberUpgradeBoxElements: null, // Gang Territory Elements gangTerritoryDescText: null, gangTerritoryWarfareCheckbox: null, gangTerritoryWarfareCheckboxLabel: null, gangTerritoryWarfareClashChance: null, gangTerritoryDeathNotifyCheckbox: null, gangTerritoryDeathNotifyCheckboxLabel: null, gangTerritoryInfoText: null, } Gang.prototype.displayGangContent = function(player) { if (!UIElems.gangContentCreated || UIElems.gangContainer == null) { UIElems.gangContentCreated = true; // Create gang container UIElems.gangContainer = createElement("div", { id:"gang-container", class:"generic-menupage-container", }); // Get variables var facName = this.facName; // Back button UIElems.gangContainer.appendChild(createElement("a", { class:"a-link-button", display:"inline-block", innerText:"Back", clickListener:() => { Engine.loadFactionContent(); displayFactionContent(facName); return false; }, })); // Buttons to switch between panels UIElems.managementButton = createElement("a", { id:"gang-management-subpage-button", class:"a-link-button-inactive", display:"inline-block", innerHTML: "Gang Management (Alt+1)", clickListener:() => { UIElems.gangManagementSubpage.style.display = "block"; UIElems.gangTerritorySubpage.style.display = "none"; UIElems.managementButton.classList.toggle("a-link-button-inactive"); UIElems.managementButton.classList.toggle("a-link-button"); UIElems.territoryButton.classList.toggle("a-link-button-inactive"); UIElems.territoryButton.classList.toggle("a-link-button"); this.updateGangContent(); return false; }, }) UIElems.territoryButton = createElement("a", { id:"gang-territory-subpage-button", class:"a-link-button", display:"inline-block", innerHTML:"Gang Territory (Alt+2)", clickListener:() => { UIElems.gangManagementSubpage.style.display = "none"; UIElems.gangTerritorySubpage.style.display = "block"; UIElems.managementButton.classList.toggle("a-link-button-inactive"); UIElems.managementButton.classList.toggle("a-link-button"); UIElems.territoryButton.classList.toggle("a-link-button-inactive"); UIElems.territoryButton.classList.toggle("a-link-button"); this.updateGangContent(); return false; }, }); UIElems.gangContainer.appendChild(UIElems.managementButton); UIElems.gangContainer.appendChild(UIElems.territoryButton); // Subpage for managing gang members UIElems.gangManagementSubpage = createElement("div", { display:"block", id:"gang-management-subpage", }); var lowerWantedTask = ""; if (this.isHackingGang) { lowerWantedTask = "Ethical Hacking"; } else { lowerWantedTask = "Vigilante Justice"; } UIElems.gangDesc = createElement("p", {width:"70%", innerHTML: "This page is used to manage your gang members and get an overview of your " + "gang's stats.

" + "If a gang member is not earning much money or respect, the task that you " + "have assigned to that member might be too difficult. Consider training that " + "member's stats or choosing an easier task. The tasks closer to the " + "top of the dropdown list are generally easier. Alternatively, the gang member's " + "low production might be due to the fact that your wanted level is too high. " + "Consider assigning a few members to the '" + lowerWantedTask + "' " + "task to lower your wanted level.

" + "Installing Augmentations does NOT reset your progress with your Gang. " + "Furthermore, after installing Augmentations, you will " + "automatically be a member of whatever Faction you created your gang with.

" + "You can also manage your gang programmatically through Netscript using the Gang API", }); UIElems.gangManagementSubpage.appendChild(UIElems.gangDesc); UIElems.gangInfo = createElement("p", {id:"gang-info", width:"70%"}); UIElems.gangManagementSubpage.appendChild(UIElems.gangInfo); UIElems.gangRecruitMemberButton = createElement("a", { id: "gang-management-recruit-member-btn", class:"a-link-button-inactive", innerHTML:"Recruit Gang Member", display:"inline-block", margin:"10px", clickListener:() => { const popupId = "recruit-gang-member-popup"; let yesBtn; const txt = createElement("p", { innerText:"Please enter a name for your new Gang member:", }); const br = createElement("br"); const nameInput = createElement("input", { onkeyup: (e) => { if (e.keyCode === KEY.ENTER) { yesBtn.click(); } }, placeholder: "Name must be unique", type: "text", class:"text-input", }); yesBtn = createElement("a", { class: "std-button", clickListener: () => { let name = nameInput.value; if (name === "") { dialogBoxCreate("You must enter a name for your Gang member!"); return false; } if (!this.canRecruitMember()) { dialogBoxCreate("You cannot recruit another Gang member!"); return false; } // At this point, the only way this can fail is if you already // have a gang member with the same name if (!this.recruitMember(name)) { dialogBoxCreate("You already have a gang member with this name!"); return false; } removeElementById(popupId); return false; }, innerText: "Recruit Gang Member", }); const noBtn = createElement("a", { class: "std-button", clickListener: () => { removeElementById(popupId); return false; }, innerText: "Cancel", }); createPopup(popupId, [txt, br, nameInput, yesBtn, noBtn]); nameInput.focus(); }, }); UIElems.gangManagementSubpage.appendChild(UIElems.gangRecruitMemberButton); // Text for how much reputation is required for recruiting next memberList UIElems.gangRecruitRequirementText = createElement("p", { color:"red", id: "gang-recruit-requirement-text", margin: "10px", }); UIElems.gangManagementSubpage.appendChild(UIElems.gangRecruitRequirementText); // Gang Member List management buttons (Expand/Collapse All, select a single member) UIElems.gangManagementSubpage.appendChild(createElement("br", {})); UIElems.gangExpandAllButton = createElement("a", { class:"a-link-button", display:"inline-block", innerHTML:"Expand All", clickListener:() => { var allHeaders = UIElems.gangManagementSubpage.getElementsByClassName("accordion-header"); for (var i = 0; i < allHeaders.length; ++i) { var hdr = allHeaders[i]; if (!hdr.classList.contains("active")) { hdr.click(); } } return false; }, }); UIElems.gangCollapseAllButton = createElement("a", { class:"a-link-button", display:"inline-block", innerHTML:"Collapse All", clickListener:() => { var allHeaders = UIElems.gangManagementSubpage.getElementsByClassName("accordion-header"); for (var i = 0; i < allHeaders.length; ++i) { var hdr = allHeaders[i]; if (hdr.classList.contains("active")) { hdr.click(); } } return false; }, }); UIElems.gangMemberFilter = createElement("input", { type:"text", placeholder:"Filter gang members", margin:"5px", padding:"5px", class:"text-input", onkeyup:() => { this.displayGangMemberList(); }, }); UIElems.gangManageEquipmentButton = createElement("a", { class:"a-link-button", display:"inline-block", innerHTML:"Manage Equipment", clickListener: () => { this.createGangMemberUpgradeBox(player); }, }); UIElems.gangManagementSubpage.appendChild(UIElems.gangExpandAllButton); UIElems.gangManagementSubpage.appendChild(UIElems.gangCollapseAllButton); UIElems.gangManagementSubpage.appendChild(UIElems.gangMemberFilter); UIElems.gangManagementSubpage.appendChild(UIElems.gangManageEquipmentButton); // Gang Member list UIElems.gangMemberList = createElement("ul", {id:"gang-member-list"}); this.displayGangMemberList(); UIElems.gangManagementSubpage.appendChild(UIElems.gangMemberList); // Subpage for seeing gang territory information UIElems.gangTerritorySubpage = createElement("div", { id:"gang-territory-subpage", display:"none", }); // Info text for territory page UIElems.gangTerritoryDescText = createElement("p", { width:"70%", innerHTML: "This page shows how much territory your Gang controls. This statistic is listed as a percentage, " + "which represents how much of the total territory you control.

" + "Every ~20 seconds, your gang has a chance to 'clash' with other gangs. Your chance " + "to win a clash depends on your gang's power, which is listed in the display below. " + "Your gang's power slowly accumulates over time. The accumulation rate is determined by the stats " + "of all Gang members you have assigned to the 'Territory Warfare' task. Gang members that are not " + "assigned to this task do not contribute to your gang's power. Your gang also loses a small amount " + "of power whenever you lose a clash

" + "NOTE: Gang members assigned to 'Territory Warfare' can be killed during clashes. This can happen regardless of whether you win " + "or lose the clash. A gang member being killed results in both respect and power loss for your gang.

" + "The amount of territory you have affects all aspects of your Gang members' production, including " + "money, respect, and wanted level. It is very beneficial to have high territory control.

", }); UIElems.gangTerritorySubpage.appendChild(UIElems.gangTerritoryDescText); // Checkbox for Engaging in Territory Warfare UIElems.gangTerritoryWarfareCheckbox = createElement("input", { display: "inline-block", id: "gang-management-territory-warfare-checkbox", changeListener: () => { this.territoryWarfareEngaged = UIElems.gangTerritoryWarfareCheckbox.checked; }, margin: "2px", type: "checkbox", }); UIElems.gangTerritoryWarfareCheckbox.checked = this.territoryWarfareEngaged; UIElems.gangTerritoryWarfareCheckboxLabel = createElement("label", { color: "white", display: "inline-block", for: "gang-management-territory-warfare-checkbox", innerText: "Engage in Territory Warfare", tooltip: "Engaging in Territory Warfare sets your clash chance to 100%. " + "Disengaging will cause your clash chance to gradually decrease until " + "it reaches 0%", }); UIElems.gangTerritorySubpage.appendChild(UIElems.gangTerritoryWarfareCheckbox); UIElems.gangTerritorySubpage.appendChild(UIElems.gangTerritoryWarfareCheckboxLabel); // Territory Clash chance UIElems.gangTerritorySubpage.appendChild(createElement("br")); UIElems.gangTerritoryWarfareClashChance = createElement("p", {display: "inline-block"}); UIElems.gangTerritorySubpage.appendChild(UIElems.gangTerritoryWarfareClashChance); UIElems.gangTerritorySubpage.appendChild(createElement("div", { class: "help-tip", display: "inline-block", innerText: "?", clickListener: () => { dialogBoxCreate("This percentage represents the chance you have of 'clashing' with " + "with another gang. If you do not wish to gain/lose territory, " + "then keep this percentage at 0% by not engaging in territory " + "warfare.") }, })); // Checkbox for whether player wants to be notified of gang member death UIElems.gangTerritoryDeathNotifyCheckbox = createElement("input", { display: "inline-block", id: "gang-management-notify-member-death-checkbox", changeListener: () => { this.notifyMemberDeath = UIElems.gangTerritoryDeathNotifyCheckbox.checked; }, margin: "2px", type: "checkbox", }); UIElems.gangTerritoryDeathNotifyCheckbox.checked = this.notifyMemberDeath; UIElems.gangTerritoryDeathNotifyCheckboxLabel = createElement("label", { color: "white", display: "inline-block", for: "gang-management-notify-member-death-checkbox", innerText: "Notify about Gang Member Deaths", tooltip: "If this is enabled, then you will receive a pop-up notifying you " + "whenever one of your Gang Members dies in a territory clash.", }); UIElems.gangTerritorySubpage.appendChild(createElement("br")); UIElems.gangTerritorySubpage.appendChild(UIElems.gangTerritoryDeathNotifyCheckbox); UIElems.gangTerritorySubpage.appendChild(UIElems.gangTerritoryDeathNotifyCheckboxLabel); // Territory info (percentages of territory owned for each gang) UIElems.gangTerritorySubpage.appendChild(createElement("br")); var territoryBorder = createElement("fieldset", { display:"block", margin: "6px", width:"50%", }); UIElems.gangTerritoryInfoText = createElement("p"); territoryBorder.appendChild(UIElems.gangTerritoryInfoText); UIElems.gangTerritorySubpage.appendChild(territoryBorder); UIElems.gangContainer.appendChild(UIElems.gangTerritorySubpage); UIElems.gangContainer.appendChild(UIElems.gangManagementSubpage); document.getElementById("entire-game-container").appendChild(UIElems.gangContainer); } UIElems.gangContainer.style.display = "block"; this.updateGangContent(); } Gang.prototype.displayGangMemberList = function() { removeChildrenFromElement(UIElems.gangMemberList); UIElems.gangMemberPanels = {}; const members = this.members; const filter = UIElems.gangMemberFilter.value.toString(); for (var i = 0; i < members.length; ++i) { if (members[i].name.indexOf(filter) > -1 || members[i].task.indexOf(filter) > -1) { this.createGangMemberDisplayElement(members[i]); } } } Gang.prototype.updateGangContent = function() { if (!UIElems.gangContentCreated) { return; } if (UIElems.gangMemberUpgradeBoxOpened) { UIElems.gangMemberUpgradeBoxDiscount.childNodes[0].nodeValue = "Discount: -" + numeralWrapper.formatPercentage(1 - 1 / this.getDiscount()); } if (UIElems.gangTerritorySubpage.style.display === "block") { // Territory Warfare Clash Chance UIElems.gangTerritoryWarfareClashChance.innerText = `Territory Clash Chance: ${numeralWrapper.formatPercentage(this.territoryClashChance, 3)}`; // Engaged in Territory Warfare checkbox UIElems.gangTerritoryWarfareCheckbox.checked = this.territoryWarfareEngaged; // Update territory information UIElems.gangTerritoryInfoText.innerHTML = ""; const playerPower = AllGangs[this.facName].power; let gangNames = Object.keys(AllGangs).filter(g => g != this.facName); gangNames.unshift(this.facName); for (const gangname of gangNames) { if (AllGangs.hasOwnProperty(gangname)) { const gangTerritoryInfo = AllGangs[gangname]; let territory = gangTerritoryInfo.territory * 100; //Fix some rounding issues graphically let displayNumber; if (territory <= 0) { displayNumber = formatNumber(0, 2); } else if (territory >= 100) { displayNumber = formatNumber(100, 2); } else { displayNumber = formatNumber(territory, 2); } if (gangname === this.facName) { let newHTML = `${gangname}
Power: ${formatNumber(gangTerritoryInfo.power, 6)}
`; newHTML += `Territory: ${displayNumber}%

`; UIElems.gangTerritoryInfoText.innerHTML += newHTML; } else { const clashVictoryChance = playerPower / (gangTerritoryInfo.power + playerPower); let newHTML = `${gangname}
Power: ${formatNumber(gangTerritoryInfo.power, 6)}
`; newHTML += `Territory: ${displayNumber}%
`; newHTML += `Chance to win clash with this gang: ${numeralWrapper.formatPercentage(clashVictoryChance, 3)}

`; UIElems.gangTerritoryInfoText.innerHTML += newHTML; } } } } else { // Update information for overall gang if (UIElems.gangInfo instanceof Element) { var faction = Factions[this.facName]; var rep; if (!(faction instanceof Faction)) { rep = "ERROR"; } else { rep = faction.playerReputation; } removeChildrenFromElement(UIElems.gangInfo); UIElems.gangInfo.appendChild(createElement("p", { // Respect display: "inline-block", innerText: "Respect: " + numeralWrapper.formatRespect(this.respect) + " (" + numeralWrapper.formatRespect(5*this.respectGainRate) + " / sec)", tooltip: "Represents the amount of respect your gang has from other gangs and criminal " + "organizations. Your respect affects the amount of money " + "your gang members will earn, and also determines how much " + "reputation you are earning with your gang's corresponding Faction.", })); UIElems.gangInfo.appendChild(createElement("br")); UIElems.gangInfo.appendChild(createElement("p", { // Wanted level display: "inline-block", innerText: "Wanted Level: " + numeralWrapper.formatWanted(this.wanted) + " (" + numeralWrapper.formatWanted(5*this.wantedGainRate) + " / sec)", tooltip: "Represents how much the gang is wanted by law enforcement. The higher " + "your gang's wanted level, the harder it will be for your gang members " + "to make money and earn respect. Note that the minimum wanted level is 1.", })); UIElems.gangInfo.appendChild(createElement("br")); var wantedPenalty = this.getWantedPenalty(); wantedPenalty = (1 - wantedPenalty) * 100; UIElems.gangInfo.appendChild(createElement("p", { // Wanted Level multiplier display: "inline-block", innerText: `Wanted Level Penalty: -${formatNumber(wantedPenalty, 2)}%`, tooltip: "Penalty for respect and money gain rates due to Wanted Level", })); UIElems.gangInfo.appendChild(createElement("br")); const d0 = createElement("div"); ReactDOM.render(

Money gain rate: {MoneyRate(5 * this.moneyGainRate)}

, d0); UIElems.gangInfo.appendChild(d0); UIElems.gangInfo.appendChild(createElement("br")); // Fix some rounding issues graphically var territoryMult = AllGangs[this.facName].territory * 100; let displayNumber; if (territoryMult <= 0) { displayNumber = formatNumber(0, 2); } else if (territoryMult >= 100) { displayNumber = formatNumber(100, 2); } else { displayNumber = formatNumber(territoryMult, 2); } UIElems.gangInfo.appendChild(createElement("p", { // Territory multiplier display: "inline-block", innerText: `Territory: ${formatNumber(displayNumber, 3)}%`, tooltip: "The percentage of total territory your Gang controls", })); UIElems.gangInfo.appendChild(createElement("br")); const d1 = createElement("div"); ReactDOM.render(

Faction reputation: {Reputation(rep)}

, d1); UIElems.gangInfo.appendChild(d1); UIElems.gangInfo.appendChild(createElement("br")); const CyclesPerSecond = 1000 / Engine._idleSpeed; if (this.storedCycles / CyclesPerSecond*1000 > 5000) { UIElems.gangInfo.appendChild(createElement("p", { // Stored Cycles innerText: `Bonus time: ${convertTimeMsToTimeElapsedString(this.storedCycles / CyclesPerSecond*1000)}`, display: "inline-block", tooltip: "You gain bonus time while offline or when the game is inactive (e.g. when the tab is throttled by the browser). " + "Bonus time makes the Gang mechanic progress faster, up to 5x the normal speed", })); UIElems.gangInfo.appendChild(createElement("br")); } } else { console.error("gang-info DOM element DNE"); } // Toggle the 'Recruit member button' if valid const numMembers = this.members.length; const respectCost = this.getRespectNeededToRecruitMember(); const btn = UIElems.gangRecruitMemberButton; if (numMembers >= MaximumGangMembers) { btn.className = "a-link-button-inactive"; UIElems.gangRecruitRequirementText.style.display = "inline-block"; UIElems.gangRecruitRequirementText.innerHTML = "You have reached the maximum amount of gang members"; } else if (this.canRecruitMember()) { btn.className = "a-link-button"; UIElems.gangRecruitRequirementText.style.display = "none"; } else { btn.className = "a-link-button-inactive"; UIElems.gangRecruitRequirementText.style.display = "inline-block"; UIElems.gangRecruitRequirementText.innerHTML = `${formatNumber(respectCost, 2)} respect needed to recruit next member`; } // Update information for each gang member for (let i = 0; i < this.members.length; ++i) { this.updateGangMemberDisplayElement(this.members[i]); } } } // Takes in a GangMember object Gang.prototype.createGangMemberDisplayElement = function(memberObj) { if (!UIElems.gangContentCreated) { return; } const name = memberObj.name; // Clear/Update the UIElems map to keep track of this gang member's panel UIElems.gangMemberPanels[name] = {}; // Create the accordion var accordion = createAccordionElement({ id: name + "gang-member", hdrText: name, }); const li = accordion[0]; const gangMemberDiv = accordion[2]; UIElems.gangMemberPanels[name]["panel"] = gangMemberDiv; // Gang member content divided into 3 panels: // Panel 1 - Shows member's stats & Ascension stuff const statsDiv = createElement("div", { class: "gang-member-info-div", id: name + "gang-member-stats", tooltipsmall: [`Hk: x${numeralWrapper.formatMultiplier(memberObj.hack_mult * memberObj.hack_asc_mult)}(x${numeralWrapper.formatMultiplier(memberObj.hack_mult)} Eq, x${numeralWrapper.formatMultiplier(memberObj.hack_asc_mult)} Asc)`, `St: x${numeralWrapper.formatMultiplier(memberObj.str_mult * memberObj.str_asc_mult)}(x${numeralWrapper.formatMultiplier(memberObj.str_mult)} Eq, x${numeralWrapper.formatMultiplier(memberObj.str_asc_mult)} Asc)`, `Df: x${numeralWrapper.formatMultiplier(memberObj.def_mult * memberObj.def_asc_mult)}(x${numeralWrapper.formatMultiplier(memberObj.def_mult)} Eq, x${numeralWrapper.formatMultiplier(memberObj.def_asc_mult)} Asc)`, `Dx: x${numeralWrapper.formatMultiplier(memberObj.dex_mult * memberObj.dex_asc_mult)}(x${numeralWrapper.formatMultiplier(memberObj.dex_mult)} Eq, x${numeralWrapper.formatMultiplier(memberObj.dex_asc_mult)} Asc)`, `Ag: x${numeralWrapper.formatMultiplier(memberObj.agi_mult * memberObj.agi_asc_mult)}(x${numeralWrapper.formatMultiplier(memberObj.agi_mult)} Eq, x${numeralWrapper.formatMultiplier(memberObj.agi_asc_mult)} Asc)`, `Ch: x${numeralWrapper.formatMultiplier(memberObj.cha_mult * memberObj.cha_asc_mult)}(x${numeralWrapper.formatMultiplier(memberObj.cha_mult)} Eq, x${numeralWrapper.formatMultiplier(memberObj.cha_asc_mult)} Asc)`].join("
"), }); UIElems.gangMemberPanels[name]["statsDiv"] = statsDiv; const statsP = createElement("pre", { display: "inline", id: name + "gang-member-stats-text", }); const brElement = createElement("br"); const ascendButton = createElement("button", { class: "accordion-button", innerText: "Ascend", clickListener: () => { const popupId = `gang-management-ascend-member ${memberObj.name}`; const ascendBenefits = memberObj.getAscensionResults(); const txt = createElement("pre", { innerText: ["Are you sure you want to ascend this member? They will lose all of", "their non-Augmentation upgrades and their stats will reset back to 1.", "", `Furthermore, your gang will lose ${numeralWrapper.formatRespect(memberObj.earnedRespect)} respect`, "", "In return, they will gain the following permanent boost to stat multipliers:\n", `Hacking: +${numeralWrapper.formatPercentage(ascendBenefits.hack)}`, `Strength: +${numeralWrapper.formatPercentage(ascendBenefits.str)}`, `Defense: +${numeralWrapper.formatPercentage(ascendBenefits.def)}`, `Dexterity: +${numeralWrapper.formatPercentage(ascendBenefits.dex)}`, `Agility: +${numeralWrapper.formatPercentage(ascendBenefits.agi)}`, `Charisma: +${numeralWrapper.formatPercentage(ascendBenefits.cha)}`].join("\n"), }); const confirmBtn = createElement("button", { class: "std-button", clickListener: () => { this.ascendMember(memberObj); this.updateGangMemberDisplayElement(memberObj); removeElementById(popupId); return false; }, innerText: "Ascend", }); const cancelBtn = createElement("button", { class: "std-button", clickListener: () => { removeElementById(popupId); return false; }, innerText: "Cancel", }); createPopup(popupId, [txt, confirmBtn, cancelBtn]); }, }); const ascendHelpTip = createElement("div", { class: "help-tip", clickListener: () => { dialogBoxCreate(["Ascending a Gang Member resets the member's progress and stats in exchange", "for a permanent boost to their stat multipliers.", "

The additional stat multiplier that the Gang Member gains upon ascension", "is based on the amount of multipliers the member has from non-Augmentation Equipment.", "

Upon ascension, the member will lose all of its non-Augmentation Equipment and your", "gang will lose respect equal to the total respect earned by the member."].join(" ")); }, innerText: "?", marginTop: "5px", }); statsDiv.appendChild(statsP); statsDiv.appendChild(brElement); statsDiv.appendChild(ascendButton); statsDiv.appendChild(ascendHelpTip); // Panel 2 - Task Selection & Info const taskDiv = createElement("div", { class:"gang-member-info-div", id: name + "gang-member-task", }); const taskSelector = createElement("select", { class: "dropdown", id: name + "gang-member-task-selector", }); // Get an array of the name of all tasks that are applicable for this Gang let tasks = this.getAllTaskNames(); tasks.unshift("---"); // Create selector for Gang member task for (var i = 0; i < tasks.length; ++i) { var option = document.createElement("option"); option.text = tasks[i]; taskSelector.add(option); } taskSelector.addEventListener("change", () => { var task = taskSelector.options[taskSelector.selectedIndex].text; memberObj.assignToTask(task); this.setGangMemberTaskDescription(memberObj, task); this.updateGangContent(); }); // Set initial task in selector if (GangMemberTasks.hasOwnProperty(memberObj.task)) { var taskName = memberObj.task; var taskIndex = 0; for (let i = 0; i < tasks.length; ++i) { if (taskName === tasks[i]) { taskIndex = i; break; } } taskSelector.selectedIndex = taskIndex; } var gainInfo = createElement("div", {id:name + "gang-member-gain-info"}); taskDiv.appendChild(taskSelector); taskDiv.appendChild(gainInfo); // Panel for Description of task var taskDescDiv = createElement("div", { class:"gang-member-info-div", id: name + "gang-member-task-desc", }); var taskDescP = createElement("p", { display:"inline", id: name + "gang-member-task-description", }); taskDescDiv.appendChild(taskDescP); gangMemberDiv.appendChild(statsDiv); gangMemberDiv.appendChild(taskDiv); gangMemberDiv.appendChild(taskDescDiv); UIElems.gangMemberList.appendChild(li); this.setGangMemberTaskDescription(memberObj, taskName); //Initialize description, TODO doesnt work rn this.updateGangMemberDisplayElement(memberObj); } Gang.prototype.updateGangMemberDisplayElement = function(memberObj) { if (!UIElems.gangContentCreated) { return; } var name = memberObj.name; // Update stats + exp var stats = document.getElementById(name + "gang-member-stats-text"); if (stats) { stats.innerText = [`Hacking: ${formatNumber(memberObj.hack, 0)} (${numeralWrapper.formatExp(memberObj.hack_exp)} exp)`, `Strength: ${formatNumber(memberObj.str, 0)} (${numeralWrapper.formatExp(memberObj.str_exp)} exp)`, `Defense: ${formatNumber(memberObj.def, 0)} (${numeralWrapper.formatExp(memberObj.def_exp)} exp)`, `Dexterity: ${formatNumber(memberObj.dex, 0)} (${numeralWrapper.formatExp(memberObj.dex_exp)} exp)`, `Agility: ${formatNumber(memberObj.agi, 0)} (${numeralWrapper.formatExp(memberObj.agi_exp)} exp)`, `Charisma: ${formatNumber(memberObj.cha, 0)} (${numeralWrapper.formatExp(memberObj.cha_exp)} exp)`].join("\n"); } // Update tooltip for stat multipliers const panel = UIElems.gangMemberPanels[name]; if (panel) { const statsDiv = panel["statsDiv"]; if (statsDiv) { statsDiv.firstChild.innerHTML = [`Hk: x${numeralWrapper.formatMultiplier(memberObj.hack_mult * memberObj.hack_asc_mult)}(x${numeralWrapper.formatMultiplier(memberObj.hack_mult)} Eq, x${numeralWrapper.formatMultiplier(memberObj.hack_asc_mult)} Asc)`, `St: x${numeralWrapper.formatMultiplier(memberObj.str_mult * memberObj.str_asc_mult)}(x${numeralWrapper.formatMultiplier(memberObj.str_mult)} Eq, x${numeralWrapper.formatMultiplier(memberObj.str_asc_mult)} Asc)`, `Df: x${numeralWrapper.formatMultiplier(memberObj.def_mult * memberObj.def_asc_mult)}(x${numeralWrapper.formatMultiplier(memberObj.def_mult)} Eq, x${numeralWrapper.formatMultiplier(memberObj.def_asc_mult)} Asc)`, `Dx: x${numeralWrapper.formatMultiplier(memberObj.dex_mult * memberObj.dex_asc_mult)}(x${numeralWrapper.formatMultiplier(memberObj.dex_mult)} Eq, x${numeralWrapper.formatMultiplier(memberObj.dex_asc_mult)} Asc)`, `Ag: x${numeralWrapper.formatMultiplier(memberObj.agi_mult * memberObj.agi_asc_mult)}(x${numeralWrapper.formatMultiplier(memberObj.agi_mult)} Eq, x${numeralWrapper.formatMultiplier(memberObj.agi_asc_mult)} Asc)`, `Ch: x${numeralWrapper.formatMultiplier(memberObj.cha_mult * memberObj.cha_asc_mult)}(x${numeralWrapper.formatMultiplier(memberObj.cha_mult)} Eq, x${numeralWrapper.formatMultiplier(memberObj.cha_asc_mult)} Asc)`].join("
"); } } // Update info about gang member's earnings/gains var gainInfo = document.getElementById(name + "gang-member-gain-info"); if (gainInfo) { const data = [ [`Money:`, MoneyRate(5*memberObj.calculateMoneyGain(this))], [`Respect:`, `${numeralWrapper.formatRespect(5*memberObj.calculateRespectGain(this))} / sec`], [`Wanted Level:`, `${numeralWrapper.formatWanted(5*memberObj.calculateWantedLevelGain(this))} / sec`], [`Total Respect:`, `${numeralWrapper.formatRespect(memberObj.earnedRespect)}`], ]; ReactDOM.render(StatsTable(data), gainInfo); } // Update selector to have the correct task const taskSelector = document.getElementById(name + "gang-member-task-selector"); if (taskSelector) { let tasks = this.getAllTaskNames(); tasks.unshift("---"); if (GangMemberTasks.hasOwnProperty(memberObj.task)) { const taskName = memberObj.task; let taskIndex = 0; for (let i = 0; i < tasks.length; ++i) { if (taskName === tasks[i]) { taskIndex = i; break; } } taskSelector.selectedIndex = taskIndex; } } } Gang.prototype.setGangMemberTaskDescription = function(memberObj, taskName) { const name = memberObj.name; const taskDesc = document.getElementById(name + "gang-member-task-description"); if (taskDesc) { var task = GangMemberTasks[taskName]; if (task == null) { task = GangMemberTasks["Unassigned"]; } var desc = task.desc; taskDesc.innerHTML = desc; } } Gang.prototype.clearUI = function() { if (UIElems.gangContainer instanceof Element) { removeElement(UIElems.gangContainer); } if (UIElems.gangMemberUpgradeBox instanceof Element) { removeElement(UIElems.gangMemberUpgradeBox); } for (const prop in UIElems) { UIElems[prop] = null; } UIElems.gangContentCreated = false; UIElems.gangMemberUpgradeBoxOpened = false; UIElems.gangMemberPanels = {}; }