Merge pull request #1045 from danielyxie/gang2

Gang rework
This commit is contained in:
hydroflame 2021-08-15 15:01:27 -04:00 committed by GitHub
commit 0f92890f0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 2302 additions and 2254 deletions

@ -1,5 +1,6 @@
@import "mixins";
@import "theme";
@import "styles";
/**
* Styling for all buttons
@ -16,6 +17,7 @@ button {
.a-link-button,
.std-button {
@extend .noselect;
text-decoration: none;
background-color: #555;
color: #fff;
@ -23,11 +25,6 @@ button {
margin: 5px;
border: 1px solid #333;
-moz-user-select: none;
-ms-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
&:hover {
background-color: #666;
}
@ -68,6 +65,7 @@ button {
.a-link-button-bought,
.std-button-bought {
@extend .noselect;
text-decoration: none;
background-color: #0a0;
color: #fff;

@ -1,5 +1,6 @@
@import "mixins";
@import "theme";
@import "styles";
/* Pop-up boxes */
.popup-box-container {
@ -23,6 +24,7 @@
width: 70%;
max-height: 80%;
overflow-y: auto;
z-index: 11; /* Sit on top of the container */
color: var(--my-font-color);
}
@ -87,6 +89,7 @@
.dialog-box-close-button {
@include borderRadius(12px);
@include boxShadow(1px 1px 3px #000);
@extend .noselect;
float: right;
color: #aaa;

@ -36,6 +36,10 @@ li {
list-style-type: none;
}
br {
@extend .noselect;
}
#entire-game-container {
background-color: transparent;
}

File diff suppressed because one or more lines are too long

@ -1,2 +1,2 @@
!function(n){function t(t){for(var e,i,f=t[0],c=t[1],l=t[2],p=0,s=[];p<f.length;p++)i=f[p],u[i]&&s.push(u[i][0]),u[i]=0;for(e in c)Object.prototype.hasOwnProperty.call(c,e)&&(n[e]=c[e]);for(a&&a(t);s.length;)s.shift()();return r.push.apply(r,l||[]),o()}function o(){for(var n,t=0;t<r.length;t++){for(var o=r[t],e=!0,f=1;f<o.length;f++){var c=o[f];0!==u[c]&&(e=!1)}e&&(r.splice(t--,1),n=i(i.s=o[0]))}return n}var e={},u={1:0},r=[];function i(t){if(e[t])return e[t].exports;var o=e[t]={i:t,l:!1,exports:{}};return n[t].call(o.exports,o,o.exports,i),o.l=!0,o.exports}i.m=n,i.c=e,i.d=function(n,t,o){i.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:o})},i.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},i.t=function(n,t){if(1&t&&(n=i(n)),8&t)return n;if(4&t&&"object"==typeof n&&n&&n.__esModule)return n;var o=Object.create(null);if(i.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:n}),2&t&&"string"!=typeof n)for(var e in n)i.d(o,e,function(t){return n[t]}.bind(null,e));return o},i.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return i.d(t,"a",t),t},i.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},i.p="";var f=window.webpackJsonp=window.webpackJsonp||[],c=f.push.bind(f);f.push=t,f=f.slice();for(var l=0;l<f.length;l++)t(f[l]);var a=c;r.push([853,0]),o()}({790:function(n,t,o){},792:function(n,t,o){},794:function(n,t,o){},796:function(n,t,o){},798:function(n,t,o){},800:function(n,t,o){},802:function(n,t,o){},804:function(n,t,o){},806:function(n,t,o){},808:function(n,t,o){},810:function(n,t,o){},812:function(n,t,o){},814:function(n,t,o){},816:function(n,t,o){},818:function(n,t,o){},820:function(n,t,o){},822:function(n,t,o){},824:function(n,t,o){},826:function(n,t,o){},828:function(n,t,o){},830:function(n,t,o){},832:function(n,t,o){},834:function(n,t,o){},836:function(n,t,o){},838:function(n,t,o){},840:function(n,t,o){},842:function(n,t,o){},844:function(n,t,o){},846:function(n,t,o){},848:function(n,t,o){},850:function(n,t,o){},853:function(n,t,o){"use strict";o.r(t);o(852),o(850),o(848),o(846),o(844),o(842),o(840),o(838),o(836),o(834),o(832),o(830),o(828),o(826),o(824),o(822),o(820),o(818),o(816),o(814),o(812),o(810),o(808),o(806),o(804),o(802),o(800),o(798),o(796),o(794),o(792),o(790)}});
!function(n){function t(t){for(var e,i,f=t[0],c=t[1],l=t[2],p=0,s=[];p<f.length;p++)i=f[p],u[i]&&s.push(u[i][0]),u[i]=0;for(e in c)Object.prototype.hasOwnProperty.call(c,e)&&(n[e]=c[e]);for(a&&a(t);s.length;)s.shift()();return r.push.apply(r,l||[]),o()}function o(){for(var n,t=0;t<r.length;t++){for(var o=r[t],e=!0,f=1;f<o.length;f++){var c=o[f];0!==u[c]&&(e=!1)}e&&(r.splice(t--,1),n=i(i.s=o[0]))}return n}var e={},u={1:0},r=[];function i(t){if(e[t])return e[t].exports;var o=e[t]={i:t,l:!1,exports:{}};return n[t].call(o.exports,o,o.exports,i),o.l=!0,o.exports}i.m=n,i.c=e,i.d=function(n,t,o){i.o(n,t)||Object.defineProperty(n,t,{enumerable:!0,get:o})},i.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},i.t=function(n,t){if(1&t&&(n=i(n)),8&t)return n;if(4&t&&"object"==typeof n&&n&&n.__esModule)return n;var o=Object.create(null);if(i.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:n}),2&t&&"string"!=typeof n)for(var e in n)i.d(o,e,function(t){return n[t]}.bind(null,e));return o},i.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return i.d(t,"a",t),t},i.o=function(n,t){return Object.prototype.hasOwnProperty.call(n,t)},i.p="";var f=window.webpackJsonp=window.webpackJsonp||[],c=f.push.bind(f);f.push=t,f=f.slice();for(var l=0;l<f.length;l++)t(f[l]);var a=c;r.push([857,0]),o()}({794:function(n,t,o){},796:function(n,t,o){},798:function(n,t,o){},800:function(n,t,o){},802:function(n,t,o){},804:function(n,t,o){},806:function(n,t,o){},808:function(n,t,o){},810:function(n,t,o){},812:function(n,t,o){},814:function(n,t,o){},816:function(n,t,o){},818:function(n,t,o){},820:function(n,t,o){},822:function(n,t,o){},824:function(n,t,o){},826:function(n,t,o){},828:function(n,t,o){},830:function(n,t,o){},832:function(n,t,o){},834:function(n,t,o){},836:function(n,t,o){},838:function(n,t,o){},840:function(n,t,o){},842:function(n,t,o){},844:function(n,t,o){},846:function(n,t,o){},848:function(n,t,o){},850:function(n,t,o){},852:function(n,t,o){},854:function(n,t,o){},857:function(n,t,o){"use strict";o.r(t);o(856),o(854),o(852),o(850),o(848),o(846),o(844),o(842),o(840),o(838),o(836),o(834),o(832),o(830),o(828),o(826),o(824),o(822),o(820),o(818),o(816),o(814),o(812),o(810),o(808),o(806),o(804),o(802),o(800),o(798),o(796),o(794)}});
//# sourceMappingURL=engineStyle.bundle.js.map

@ -1552,6 +1552,8 @@ button {
width: 70%;
max-height: 80%;
overflow-y: auto;
z-index: 11;
/* Sit on top of the container */
color: var(--my-font-color); }
.popup-box-button,

34
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

@ -271,6 +271,10 @@
<!-- React Component -->
</div>
<div id="gang-container" class="generic-menupage-container">
<!-- React Component -->
</div>
<!-- Log Box -->
<div id="log-box-container">
<div id="log-box-content">

@ -1865,11 +1865,6 @@ function initAugmentations() {
resetAugmentation(BladesSimulacrum);
}
try {
console.log([1, 0.96, 0.94, 0.93][SourceFileFlags[11]]);
} catch(err) {
console.log(err);
}
// Update costs based on how many have been purchased
mult = Math.pow(CONSTANTS.MultipleAugMultiplier * [1, 0.96, 0.94, 0.93][SourceFileFlags[11]], Player.queuedAugmentations.length);
for (var name in Augmentations) {

@ -7,7 +7,6 @@ import * as React from "react";
import { Augmentations } from "../../Augmentation/Augmentations";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { Player } from "../../Player";
import { IPlayerOwnedAugmentation } from "../../Augmentation/PlayerOwnedAugmentation";
import { AugmentationAccordion } from "../../ui/React/AugmentationAccordion";

@ -172,7 +172,7 @@ export class CodingContract {
async prompt(): Promise<CodingContractResult> {
const popupId = `coding-contract-prompt-popup-${this.fn}`;
return new Promise<CodingContractResult>((resolve) => {
const popup = new CodingContractPopup({
createPopup(popupId, CodingContractPopup, {
c: this,
popupId: popupId,
onClose: () => {
@ -188,7 +188,6 @@ export class CodingContract {
removePopup(popupId);
},
});
createPopup(popupId, CodingContractPopup, popup.props);
});
}

@ -8,6 +8,9 @@ import { IMap } from "./types";
export const CONSTANTS: IMap<any> = {
Version: "0.52.2",
// Speed (in ms) at which the main loop is updated
_idleSpeed: 200,
/** Max level for any skill, assuming no multipliers. Determined by max numerical value in javascript for experience
* and the skill level formula in Player.js. Note that all this means it that when experience hits MAX_INT, then
* the player will have this level assuming no multipliers. Multipliers can cause skills to go above this.

@ -5,3 +5,4 @@ export declare function getNextNeurofluxLevel(): number;
export declare function hasAugmentationPrereqs(aug: Augmentation): boolean;
export declare function purchaseAugmentationBoxCreate(aug: Augmentation, fac: Faction): void;
export declare function purchaseAugmentation(aug: Augmentation, fac: Faction, sing?: boolean): void;
export declare function displayFactionContent(factionName: string, initiallyOnAugmentationsPage: boolean=false);

@ -46,6 +46,7 @@ export function inviteToFaction(faction) {
}
export function joinFaction(faction) {
if(faction.isMember) return;
faction.isMember = true;
Player.factions.push(faction.name);
const factionInfo = faction.getInfo();

File diff suppressed because it is too large Load Diff

76
src/Gang/AllGangs.ts Normal file

@ -0,0 +1,76 @@
import { Reviver } from "../../utils/JSONReviver";
interface GangTerritory {
power: number;
territory: number;
}
export let AllGangs: {
[key: string]: GangTerritory;
} = {
"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(): void {
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: string): void {
AllGangs = JSON.parse(saveString, Reviver);
}

413
src/Gang/Gang.ts Normal file

@ -0,0 +1,413 @@
/**
* TODO
* Add police clashes
* balance point to keep them from running out of control
*/
import { Faction } from "../Faction/Faction";
import { Factions } from "../Faction/Factions";
import { numeralWrapper } from "../ui/numeralFormat";
import { dialogBoxCreate } from "../../utils/DialogBox";
import {
Reviver,
Generic_toJSON,
Generic_fromJSON,
} from "../../utils/JSONReviver";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { getRandomInt } from "../../utils/helpers/getRandomInt";
import { GangMemberUpgrade } from "./GangMemberUpgrade";
import { GangConstants } from "./data/Constants";
import { CONSTANTS } from "../Constants";
import { GangMemberTasks } from "./GangMemberTasks";
import { IAscensionResult } from "./IAscensionResult";
import { AllGangs } from "./AllGangs";
import { GangMember } from "./GangMember";
import { WorkerScript } from "../Netscript/WorkerScript";
import { IPlayer } from "../PersonObjects/IPlayer";
export class Gang {
facName: string;
members: GangMember[];
wanted: number;
respect: number;
isHackingGang: boolean;
respectGainRate: number;
wantedGainRate: number;
moneyGainRate: number;
storedCycles: number;
storedTerritoryAndPowerCycles: number;
territoryClashChance: number;
territoryWarfareEngaged: boolean;
notifyMemberDeath: boolean;
constructor(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;
}
getPower(): number {
return AllGangs[this.facName].power;
}
getTerritory(): number {
return AllGangs[this.facName].territory;
}
process(numCycles = 1, player: IPlayer): void {
const CyclesPerSecond = 1000 / CONSTANTS._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) {
console.error(`Exception caught when processing Gang: ${e}`);
}
}
processGains(numCycles = 1, player: IPlayer): void {
// Get gains per cycle
let moneyGains = 0;
let respectGains = 0;
let wantedLevelGains = 0;
let justice = 0;
for (let i = 0; i < this.members.length; ++i) {
respectGains += (this.members[i].calculateRespectGain(this));
moneyGains += (this.members[i].calculateMoneyGain(this));
const wantedLevelGain = this.members[i].calculateWantedLevelGain(this);
wantedLevelGains += wantedLevelGain;
if(wantedLevelGain < 0) justice++; // this member is lowering wanted.
}
this.respectGainRate = respectGains;
this.wantedGainRate = wantedLevelGains;
this.moneyGainRate = moneyGains;
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");
throw new Error('Could not find the faction associated with this gang.');
}
const favorMult = 1 + (fac.favor / 100);
fac.playerReputation += ((player.faction_rep_mult * gain * favorMult) / GangConstants.GangRespectToReputationRatio);
// Keep track of respect gained per member
for (let i = 0; i < this.members.length; ++i) {
this.members[i].recordEarnedRespect(numCycles, this);
}
if (!(this.wanted === 1 && wantedLevelGains < 0)) {
const oldWanted = this.wanted;
let newWanted = oldWanted + (wantedLevelGains * numCycles);
newWanted = newWanted * (1 - justice * 0.001); // safeguard
// Prevent overflow
if (wantedLevelGains <= 0 && newWanted > oldWanted) newWanted = 1;
this.wanted = newWanted;
if (this.wanted < 1) this.wanted = 1;
}
player.gainMoney(moneyGains * numCycles);
player.recordMoneySource(moneyGains * numCycles, "gang");
}
processTerritoryAndPowerGains(numCycles = 1): void {
this.storedTerritoryAndPowerCycles += numCycles;
if (this.storedTerritoryAndPowerCycles < GangConstants.CyclesPerTerritoryAndPowerUpdate) return;
this.storedTerritoryAndPowerCycles -= GangConstants.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 < GangConstants.Names.length; ++i) {
const others = GangConstants.Names.filter((e) => {
return e !== GangConstants.Names[i];
});
const other = getRandomInt(0, others.length - 1);
const thisGang = GangConstants.Names[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);
function calculateTerritoryGain(winGang: string, loseGang: string): number {
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;
}
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);
}
}
}
}
processExperienceGains(numCycles = 1): void {
for (let i = 0; i < this.members.length; ++i) {
this.members[i].gainExperience(numCycles);
this.members[i].updateSkillLevels();
}
}
clash(won = false): void {
// 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
else 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);
}
}
}
canRecruitMember(): boolean {
if (this.members.length >= GangConstants.MaximumGangMembers) return false;
return (this.respect >= this.getRespectNeededToRecruitMember());
}
getRespectNeededToRecruitMember(): number {
// 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.pow(5, i);
}
recruitMember(name: string): boolean {
name = String(name);
if (name === "" || !this.canRecruitMember()) return false;
// Check for already-existing names
const sameNames = this.members.filter((m) => m.name === name);
if (sameNames.length >= 1) return false;
const member = new GangMember(name);
this.members.push(member);
return true;
}
// Money and Respect gains multiplied by this number (< 1)
getWantedPenalty(): number {
return (this.respect) / (this.respect + this.wanted);
}
//Calculates power GAIN, which is added onto the Gang's existing power
calculatePower(): number {
let memberTotal = 0;
for (let i = 0; i < this.members.length; ++i) {
if (!GangMemberTasks.hasOwnProperty(this.members[i].task) ||
this.members[i].task !== "Territory Warfare") continue;
memberTotal += this.members[i].calculatePower();
}
return (0.015 * this.getTerritory() * memberTotal);
}
killMember(member: GangMember): void {
// Player loses a percentage of total respect, plus whatever respect that member has earned
const totalRespect = this.respect;
const lostRespect = (0.05 * totalRespect) + member.earnedRespect;
this.respect = Math.max(0, totalRespect - lostRespect);
for (let i = 0; i < this.members.length; ++i) {
if (member.name === this.members[i].name) {
this.members.splice(i, 1);
break;
}
}
// Notify of death
if (this.notifyMemberDeath) {
dialogBoxCreate(`${member.name} was killed in a gang clash! You lost ${lostRespect} respect`);
}
}
ascendMember(member: GangMember, workerScript?: WorkerScript): IAscensionResult {
try {
const res = member.ascend();
this.respect = Math.max(1, this.respect - res.respect);
if (workerScript) {
workerScript.log('ascend', `Ascended Gang member ${member.name}`);
}
return res;
} catch(e) {
if (workerScript == null) {
exceptionAlert(e);
}
throw e; // Re-throw, will be caught in the Netscript Function
}
}
// Cost of upgrade gets cheaper as gang increases in respect + power
getDiscount(): number {
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'
getAllTaskNames(): string[] {
return Object.keys(GangMemberTasks).filter((taskName: string) => {
const task = GangMemberTasks[taskName];
if (task == null) return false;
if (task.name === "Unassigned") return false;
// yes you need both checks
return this.isHackingGang === task.isHacking ||
!this.isHackingGang === task.isCombat;
});
}
getUpgradeCost(upg: GangMemberUpgrade | null): number {
if (upg == null) { return Infinity; }
return upg.cost/this.getDiscount();
}
/**
* Serialize the current object to a JSON save state.
*/
toJSON(): any {
return Generic_toJSON("Gang", this);
}
/**
* Initiatizes a Gang object from a JSON save state.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
static fromJSON(value: any): Gang {
return Generic_fromJSON(Gang, value.data);
}
}
Reviver.constructors.Gang = Gang;

320
src/Gang/GangMember.ts Normal file

@ -0,0 +1,320 @@
import { GangMemberTask } from "./GangMemberTask";
import { GangMemberTasks } from "./GangMemberTasks";
import { GangMemberUpgrade } from "./GangMemberUpgrade";
import { GangMemberUpgrades } from "./GangMemberUpgrades";
import { IAscensionResult } from "./IAscensionResult";
import { IPlayer } from "../PersonObjects/IPlayer";
import { GangConstants } from "./data/Constants";
import { AllGangs } from "./AllGangs";
import { IGang } from "./IGang";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver";
interface IMults {
hack: number;
str: number;
def: number;
dex: number;
agi: number;
cha: number;
}
export class GangMember {
name: string;
task = "Unassigned";
earnedRespect = 0;
hack = 1;
str = 1;
def = 1;
dex = 1;
agi = 1;
cha = 1;
hack_exp = 0;
str_exp = 0;
def_exp = 0;
dex_exp = 0;
agi_exp = 0;
cha_exp = 0;
hack_mult = 1;
str_mult = 1;
def_mult = 1;
dex_mult = 1;
agi_mult = 1;
cha_mult = 1;
hack_asc_points = 0;
str_asc_points = 0;
def_asc_points = 0;
dex_asc_points = 0;
agi_asc_points = 0;
cha_asc_points = 0;
upgrades: string[] = []; // Names of upgrades
augmentations: string[] = []; // Names of augmentations only
constructor(name = "") {
this.name = name;
}
calculateSkill(exp: number, mult = 1): number {
return Math.max(Math.floor(mult * (32 * Math.log(exp + 534.5) - 200)), 1);
}
calculateAscensionMult(points: number): number {
return Math.max(Math.pow(points/4000, 0.7), 1);
}
updateSkillLevels(): void {
this.hack = this.calculateSkill(this.hack_exp, this.hack_mult * this.calculateAscensionMult(this.hack_asc_points));
this.str = this.calculateSkill(this.str_exp, this.str_mult * this.calculateAscensionMult(this.str_asc_points));
this.def = this.calculateSkill(this.def_exp, this.def_mult * this.calculateAscensionMult(this.def_asc_points));
this.dex = this.calculateSkill(this.dex_exp, this.dex_mult * this.calculateAscensionMult(this.dex_asc_points));
this.agi = this.calculateSkill(this.agi_exp, this.agi_mult * this.calculateAscensionMult(this.agi_asc_points));
this.cha = this.calculateSkill(this.cha_exp, this.cha_mult * this.calculateAscensionMult(this.cha_asc_points));
}
calculatePower(): number {
return (this.hack +
this.str +
this.def +
this.dex +
this.agi +
this.cha) / 95;
}
assignToTask(taskName: string): boolean {
if (!GangMemberTasks.hasOwnProperty(taskName)) {
this.task = "Unassigned";
return false;
}
this.task = taskName;
return true;
}
unassignFromTask(): void {
this.task = "Unassigned";
}
getTask(): GangMemberTask {
// TODO(hydroflame): transfer that to a save file migration function
// Backwards compatibility
if ((this.task as any) instanceof GangMemberTask) {
this.task = (this.task as any).name;
}
if (GangMemberTasks.hasOwnProperty(this.task)) {
return GangMemberTasks[this.task];
}
return GangMemberTasks["Unassigned"];
}
calculateRespectGain(gang: IGang): number {
const task = this.getTask();
if (task.baseRespect === 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 -= (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;
const respectMult = gang.getWantedPenalty();
return 11 * task.baseRespect * statWeight * territoryMult * respectMult;
}
calculateWantedLevelGain(gang: IGang): number {
const task = this.getTask();
if (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;
}
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);
}
calculateMoneyGain(gang: IGang): number {
const task = this.getTask();
if (task.baseMoney === 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.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;
const respectMult = gang.getWantedPenalty();
return 5 * task.baseMoney * statWeight * territoryMult * respectMult;
}
expMult(): IMults {
return {
hack: (this.hack_mult-1)/4+1,
str: (this.str_mult-1)/4+1,
def: (this.def_mult-1)/4+1,
dex: (this.dex_mult-1)/4+1,
agi: (this.agi_mult-1)/4+1,
cha: (this.cha_mult-1)/4+1,
};
}
gainExperience(numCycles = 1): void {
const task = this.getTask();
if (task === GangMemberTasks["Unassigned"]) return;
const difficultyMult = Math.pow(task.difficulty, 0.9);
const difficultyPerCycles = difficultyMult * numCycles;
const weightDivisor = 1500;
const expMult = this.expMult();
this.hack_exp += (task.hackWeight / weightDivisor) * difficultyPerCycles * expMult.hack;
this.str_exp += (task.strWeight / weightDivisor) * difficultyPerCycles * expMult.str;
this.def_exp += (task.defWeight / weightDivisor) * difficultyPerCycles * expMult.def;
this.dex_exp += (task.dexWeight / weightDivisor) * difficultyPerCycles * expMult.dex;
this.agi_exp += (task.agiWeight / weightDivisor) * difficultyPerCycles * expMult.agi;
this.cha_exp += (task.chaWeight / weightDivisor) * difficultyPerCycles * expMult.cha;
}
recordEarnedRespect(numCycles = 1, gang: IGang): void {
this.earnedRespect += (this.calculateRespectGain(gang) * numCycles);
}
getGainedAscensionPoints(): IMults {
return {
hack: Math.max(this.hack_exp - 1000, 0),
str: Math.max(this.str_exp - 1000, 0),
def: Math.max(this.def_exp - 1000, 0),
dex: Math.max(this.dex_exp - 1000, 0),
agi: Math.max(this.agi_exp - 1000, 0),
cha: Math.max(this.cha_exp - 1000, 0),
}
}
canAscend(): boolean {
const points = this.getGainedAscensionPoints();
return points.hack > 0 ||
points.str > 0 ||
points.def > 0 ||
points.dex > 0 ||
points.agi > 0 ||
points.cha > 0;
}
getAscensionResults(): IMults {
const points = this.getGainedAscensionPoints();
return {
hack: this.calculateAscensionMult(this.hack_asc_points+points.hack)/this.calculateAscensionMult(this.hack_asc_points),
str: this.calculateAscensionMult(this.str_asc_points+points.str)/this.calculateAscensionMult(this.str_asc_points),
def: this.calculateAscensionMult(this.def_asc_points+points.def)/this.calculateAscensionMult(this.def_asc_points),
dex: this.calculateAscensionMult(this.dex_asc_points+points.dex)/this.calculateAscensionMult(this.dex_asc_points),
agi: this.calculateAscensionMult(this.agi_asc_points+points.agi)/this.calculateAscensionMult(this.agi_asc_points),
cha: this.calculateAscensionMult(this.cha_asc_points+points.cha)/this.calculateAscensionMult(this.cha_asc_points),
}
}
ascend(): IAscensionResult {
const res = this.getAscensionResults();
const points = this.getGainedAscensionPoints();
this.hack_asc_points += points.hack;
this.str_asc_points += points.str;
this.def_asc_points += points.def;
this.dex_asc_points += points.dex;
this.agi_asc_points += points.agi;
this.cha_asc_points += points.cha;
// 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) {
const aug = GangMemberUpgrades[this.augmentations[i]];
this.applyUpgrade(aug);
}
// 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: res.hack,
str: res.str,
def: res.def,
dex: res.dex,
agi: res.agi,
cha: res.cha,
};
}
applyUpgrade(upg: GangMemberUpgrade): void {
if (upg.mults.str != null) this.str_mult *= upg.mults.str;
if (upg.mults.def != null) this.def_mult *= upg.mults.def;
if (upg.mults.dex != null) this.dex_mult *= upg.mults.dex;
if (upg.mults.agi != null) this.agi_mult *= upg.mults.agi;
if (upg.mults.cha != null) this.cha_mult *= upg.mults.cha;
if (upg.mults.hack != null) this.hack_mult *= upg.mults.hack;
}
buyUpgrade(upg: GangMemberUpgrade, player: IPlayer, gang: IGang): boolean {
// Prevent purchasing of already-owned upgrades
if (this.augmentations.includes(upg.name) ||
this.upgrades.includes(upg.name)) return false;
if (player.money.lt(gang.getUpgradeCost(upg))) return false;
player.loseMoney(gang.getUpgradeCost(upg));
if (upg.type === "g") {
this.augmentations.push(upg.name);
} else {
this.upgrades.push(upg.name);
}
this.applyUpgrade(upg);
return true;
}
/**
* Serialize the current object to a JSON save state.
*/
toJSON(): any {
return Generic_toJSON("GangMember", this);
}
/**
* Initiatizes a GangMember object from a JSON save state.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
static fromJSON(value: any): GangMember {
return Generic_fromJSON(GangMember, value.data);
}
}
Reviver.constructors.GangMember = GangMember;

@ -0,0 +1,62 @@
import { ITaskParams, ITerritory } from "./ITaskParams";
export class GangMemberTask {
name: string;
desc: string;
isHacking: boolean;
isCombat: boolean;
baseRespect: number;
baseWanted: number;
baseMoney: number;
hackWeight: number;
strWeight: number;
defWeight: number;
dexWeight: number;
agiWeight: number;
chaWeight: number;
difficulty: number;
territory: ITerritory;
// Defines tasks that Gang Members can work on
constructor(name: string, desc: string, isHacking: boolean, isCombat: boolean, params: ITaskParams) {
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};
}
}

@ -0,0 +1,12 @@
import { gangMemberTasksMetadata } from "./data/tasks";
import { GangMemberTask } from "./GangMemberTask";
export const GangMemberTasks: {
[key: string]: GangMemberTask;
} = {};
(function() {
gangMemberTasksMetadata.forEach((e) => {
GangMemberTasks[e.name] = new GangMemberTask(e.name, e.desc, e.isHacking, e.isCombat, e.params);
});
})();

@ -0,0 +1,66 @@
import { IMults, UpgradeType } from "./data/upgrades";
import { numeralWrapper } from "../ui/numeralFormat";
export class GangMemberUpgrade {
name: string;
cost: number;
type: UpgradeType;
desc: string;
mults: IMults;
constructor(name = "", cost = 0, type: UpgradeType = UpgradeType.Weapon, mults: IMults = {}) {
this.name = name;
this.cost = cost;
this.type = type;
this.mults = mults;
this.desc = this.createDescription();
}
createDescription(): string {
const lines = ["Effects:"];
if (this.mults.str != null) {
lines.push(`+${numeralWrapper.formatPercentage(this.mults.str-1, 0)} strength skill`);
lines.push(`+${numeralWrapper.formatPercentage((this.mults.str-1)/4, 2)} strength exp`);
}
if (this.mults.def != null) {
lines.push(`+${numeralWrapper.formatPercentage(this.mults.def-1, 0)} defense skill`);
lines.push(`+${numeralWrapper.formatPercentage((this.mults.def-1)/4, 2)} defense exp`);
}
if (this.mults.dex != null) {
lines.push(`+${numeralWrapper.formatPercentage(this.mults.dex-1, 0)} dexterity skill`);
lines.push(`+${numeralWrapper.formatPercentage((this.mults.dex-1)/4, 2)} dexterity exp`);
}
if (this.mults.agi != null) {
lines.push(`+${numeralWrapper.formatPercentage(this.mults.agi-1, 0)} agility skill`);
lines.push(`+${numeralWrapper.formatPercentage((this.mults.agi-1)/4, 2)} agility exp`);
}
if (this.mults.cha != null) {
lines.push(`+${numeralWrapper.formatPercentage(this.mults.cha-1, 0)} charisma skill`);
lines.push(`+${numeralWrapper.formatPercentage((this.mults.cha-1)/4, 2)} charisma exp`);
}
if (this.mults.hack != null) {
lines.push(`+${numeralWrapper.formatPercentage(this.mults.hack-1, 0)} hacking skill`);
lines.push(`+${numeralWrapper.formatPercentage((this.mults.hack-1)/4, 2)} hacking exp`);
}
return lines.join("<br>");
}
// User friendly version of type.
getType(): string {
switch (this.type) {
case UpgradeType.Weapon:
return "Weapon";
case UpgradeType.Armor:
return "Armor";
case UpgradeType.Vehicle:
return "Vehicle";
case UpgradeType.Rootkit:
return "Rootkit";
case UpgradeType.Augmentation:
return "Augmentation";
default:
return "";
}
}
}

@ -0,0 +1,12 @@
import { gangMemberUpgradesMetadata } from "./data/upgrades";
import { GangMemberUpgrade } from "./GangMemberUpgrade";
export const GangMemberUpgrades: {
[key: string]: GangMemberUpgrade;
} = {};
(function() {
gangMemberUpgradesMetadata.forEach((e) => {
GangMemberUpgrades[e.name] = new GangMemberUpgrade(e.name, e.cost, e.upgType, e.mults);
});
})();

32
src/Gang/Helpers.tsx Normal file

@ -0,0 +1,32 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { IPlayer } from "../PersonObjects/IPlayer";
import { IEngine } from "../IEngine";
import { Root } from "./ui/Root";
import { Gang } from "./Gang";
import { Page, routing } from ".././ui/navigationTracking";
let gangContainer: HTMLElement;
(function() {
function set(): void {
const c = document.getElementById("gang-container");
if(c === null) throw new Error("Could not find element 'gang-container'");
gangContainer = c;
document.removeEventListener("DOMContentLoaded", set);
}
document.addEventListener("DOMContentLoaded", set);
})();
export function displayGangContent(engine: IEngine, gang: Gang, player: IPlayer): void {
if (!routing.isOn(Page.Gang)) {
return;
}
ReactDOM.render(<Root
engine={engine}
gang={gang}
player={player} />, gangContainer);
}

@ -0,0 +1,9 @@
export interface IAscensionResult {
respect: number;
hack: number;
str: number;
def: number;
dex: number;
agi: number;
cha: number;
}

44
src/Gang/IGang.ts Normal file

@ -0,0 +1,44 @@
import { GangMemberUpgrade } from "./GangMemberUpgrade";
import { GangMember } from "./GangMember";
import { WorkerScript } from "../Netscript/WorkerScript";
import { IPlayer } from "../PersonObjects/IPlayer";
export interface IGang {
facName: string;
members: GangMember[];
wanted: number;
respect: number;
isHackingGang: boolean;
respectGainRate: number;
wantedGainRate: number;
moneyGainRate: number;
storedCycles: number;
storedTerritoryAndPowerCycles: number;
territoryClashChance: number;
territoryWarfareEngaged: boolean;
notifyMemberDeath: boolean;
getPower(): number;
getTerritory(): number;
process(numCycles: number, player: IPlayer): void;
processGains(numCycles: number, player: IPlayer): void;
processTerritoryAndPowerGains(numCycles: number): void;
processExperienceGains(numCycles: number): void;
clash(won: boolean): void;
canRecruitMember(): boolean;
getRespectNeededToRecruitMember(): number;
recruitMember(name: string): boolean;
getWantedPenalty(): number;
calculatePower(): number;
killMember(member: GangMember): void;
ascendMember(member: GangMember, workerScript: WorkerScript): void;
getDiscount(): number;
getAllTaskNames(): string[];
getUpgradeCost(upg: GangMemberUpgrade): number;
}

20
src/Gang/ITaskParams.ts Normal file

@ -0,0 +1,20 @@
export interface ITerritory {
money: number;
respect: number;
wanted: number;
}
export interface ITaskParams {
baseRespect?: number;
baseWanted?: number;
baseMoney?: number;
hackWeight?: number;
strWeight?: number;
defWeight?: number;
dexWeight?: number;
agiWeight?: number;
chaWeight?: number;
difficulty?: number;
territory?: ITerritory;
}

@ -0,0 +1,24 @@
export const GangConstants: {
GangRespectToReputationRatio: number;
MaximumGangMembers: number;
CyclesPerTerritoryAndPowerUpdate: number;
AscensionMultiplierRatio: number;
Names: string[];
} = {
// Respect is divided by this to get rep gain
GangRespectToReputationRatio: 25,
MaximumGangMembers: 12,
CyclesPerTerritoryAndPowerUpdate: 100,
// Portion of upgrade multiplier that is kept after ascending
AscensionMultiplierRatio: .15,
// Names of possible Gangs
Names: [
"Slum Snakes",
"Tetrads",
"The Syndicate",
"The Dark Army",
"Speakers for the Dead",
"NiteSec",
"The Black Hand",
],
};

@ -1,3 +1,4 @@
import { ITaskParams } from "../ITaskParams";
/* tslint:disable:max-line-length */
/**
@ -29,7 +30,7 @@ export interface IGangMemberTaskMetadata {
* An object containing weighting parameters for the task. These parameters are used for
* various calculations (respect gain, wanted gain, etc.)
*/
params?: any;
params: ITaskParams;
}
/**
@ -254,7 +255,7 @@ export const gangMemberTasksMetadata: IGangMemberTaskMetadata[] = [
name: "Train Combat",
params: {
strWeight: 25, defWeight: 25, dexWeight: 25, agiWeight: 25,
difficulty: 5,
difficulty: 100,
},
},
{
@ -262,7 +263,7 @@ export const gangMemberTasksMetadata: IGangMemberTaskMetadata[] = [
isCombat: true,
isHacking: true,
name: "Train Hacking",
params: {hackWeight: 100, difficulty: 8},
params: {hackWeight: 100, difficulty: 45},
},
{
desc: "Assign this gang member to train their charisma",
@ -281,4 +282,4 @@ export const gangMemberTasksMetadata: IGangMemberTaskMetadata[] = [
difficulty: 5,
},
},
];
];

@ -1,12 +1,29 @@
export interface IMults {
hack?: number;
str?: number;
def?: number;
dex?: number;
agi?: number;
cha?: number;
}
export enum UpgradeType {
Weapon = "w",
Armor = "a",
Vehicle = "v",
Rootkit = "r",
Augmentation = "g",
}
/**
* Defines the parameters that can be used to initialize and describe a GangMemberUpgrade
* (defined in Gang.js)
*/
export interface IGangMemberUpgradeMetadata {
cost: number;
mults: any;
mults: IMults;
name: string;
upgType: string;
upgType: UpgradeType;
}
/**
@ -18,192 +35,192 @@ export const gangMemberUpgradesMetadata: IGangMemberUpgradeMetadata[] = [
cost: 1e6,
mults: {str: 1.04, def: 1.04},
name: "Baseball Bat",
upgType: "w",
upgType: UpgradeType.Weapon,
},
{
cost: 12e6,
mults: {str: 1.08, def: 1.08, dex: 1.08},
name: "Katana",
upgType: "w",
upgType: UpgradeType.Weapon,
},
{
cost: 25e6,
mults: {str: 1.1, def: 1.1, dex: 1.1, agi: 1.1},
name: "Glock 18C",
upgType: "w",
upgType: UpgradeType.Weapon,
},
{
cost: 50e6,
mults: {str: 1.12, def: 1.1, agi: 1.1},
name: "P90C",
upgType: "w",
upgType: UpgradeType.Weapon,
},
{
cost: 60e6,
mults: {str: 1.2, def: 1.15},
name: "Steyr AUG",
upgType: "w",
upgType: UpgradeType.Weapon,
},
{
cost: 100e6,
mults: {str: 1.25, def: 1.2},
name: "AK-47",
upgType: "w",
upgType: UpgradeType.Weapon,
},
{
cost: 150e6,
mults: {str: 1.3, def: 1.25},
name: "M15A10 Assault Rifle",
upgType: "w",
upgType: UpgradeType.Weapon,
},
{
cost: 225e6,
mults: {str: 1.3, dex: 1.25, agi: 1.3},
name: "AWM Sniper Rifle",
upgType: "w",
upgType: UpgradeType.Weapon,
},
{
cost: 2e6,
mults: {def: 1.04},
name: "Bulletproof Vest",
upgType: "a",
upgType: UpgradeType.Armor,
},
{
cost: 5e6,
mults: {def: 1.08},
name: "Full Body Armor",
upgType: "a",
upgType: UpgradeType.Armor,
},
{
cost: 25e6,
mults: {def: 1.15, agi: 1.15},
name: "Liquid Body Armor",
upgType: "a",
upgType: UpgradeType.Armor,
},
{
cost: 40e6,
mults: {def: 1.2},
name: "Graphene Plating Armor",
upgType: "a",
upgType: UpgradeType.Armor,
},
{
cost: 3e6,
mults: {agi: 1.04, cha: 1.04},
name: "Ford Flex V20",
upgType: "v",
upgType: UpgradeType.Vehicle,
},
{
cost: 9e6,
mults: {agi: 1.08, cha: 1.08},
name: "ATX1070 Superbike",
upgType: "v",
upgType: UpgradeType.Vehicle,
},
{
cost: 18e6,
mults: {agi: 1.12, cha: 1.12},
name: "Mercedes-Benz S9001",
upgType: "v",
upgType: UpgradeType.Vehicle,
},
{
cost: 30e6,
mults: {agi: 1.16, cha: 1.16},
name: "White Ferrari",
upgType: "v",
upgType: UpgradeType.Vehicle,
},
{
cost: 5e6,
mults: {hack: 1.05},
name: "NUKE Rootkit",
upgType: "r",
upgType: UpgradeType.Rootkit,
},
{
cost: 25e6,
mults: {hack: 1.1},
name: "Soulstealer Rootkit",
upgType: "r",
upgType: UpgradeType.Rootkit,
},
{
cost: 75e6,
mults: {hack: 1.15},
name: "Demon Rootkit",
upgType: "r",
upgType: UpgradeType.Rootkit,
},
{
cost: 40e6,
mults: {hack: 1.12},
name: "Hmap Node",
upgType: "r",
upgType: UpgradeType.Rootkit,
},
{
cost: 75e6,
mults: {hack: 1.15},
name: "Jack the Ripper",
upgType: "r",
upgType: UpgradeType.Rootkit,
},
{
cost: 10e9,
mults: {str: 1.3, dex: 1.3},
name: "Bionic Arms",
upgType: "g",
upgType: UpgradeType.Augmentation,
},
{
cost: 10e9,
mults: {agi: 1.6},
name: "Bionic Legs",
upgType: "g",
upgType: UpgradeType.Augmentation,
},
{
cost: 15e9,
mults: {str: 1.15, def: 1.15, dex: 1.15, agi: 1.15},
name: "Bionic Spine",
upgType: "g",
upgType: UpgradeType.Augmentation,
},
{
cost: 20e9,
mults: {str: 1.4, def: 1.4},
name: "BrachiBlades",
upgType: "g",
upgType: UpgradeType.Augmentation,
},
{
cost: 12e9,
mults: {str: 1.2, def: 1.2},
name: "Nanofiber Weave",
upgType: "g",
upgType: UpgradeType.Augmentation,
},
{
cost: 25e9,
mults: {str: 1.5, agi: 1.5},
name: "Synthetic Heart",
upgType: "g",
upgType: UpgradeType.Augmentation,
},
{
cost: 15e9,
mults: {str: 1.3, def: 1.3},
name: "Synfibril Muscle",
upgType: "g",
upgType: UpgradeType.Augmentation,
},
{
cost: 5e9,
mults: {hack: 1.05},
name: "BitWire",
upgType: "g",
upgType: UpgradeType.Augmentation,
},
{
cost: 10e9,
mults: {hack: 1.15},
name: "Neuralstimulator",
upgType: "g",
upgType: UpgradeType.Augmentation,
},
{
cost: 7.5e9,
mults: {hack: 1.1},
name: "DataJack",
upgType: "g",
upgType: UpgradeType.Augmentation,
},
{
cost: 50e9,
mults: {str: 1.7, def: 1.7},
name: "Graphene Bone Lacings",
upgType: "g",
upgType: UpgradeType.Augmentation,
},
];

@ -0,0 +1,70 @@
/**
* React Component for the content of the popup before the player confirms the
* ascension of a gang member.
*/
import React, { useState, useEffect } from "react";
import { Gang } from "../Gang";
import { GangMember } from "../GangMember";
import { numeralWrapper } from "../../ui/numeralFormat";
import { removePopup } from "../../ui/React/createPopup";
import { dialogBoxCreate } from "../../../utils/DialogBox";
interface IProps {
member: GangMember;
gang: Gang;
popupId: string;
onAscend: () => void;
}
export function AscensionPopup(props: IProps): React.ReactElement {
const setRerender = useState(false)[1];
useEffect(() => {
const id = setInterval(() => setRerender(old => !old), 1000);
return () => clearInterval(id);
}, []);
function confirm(): void {
props.onAscend();
const res = props.gang.ascendMember(props.member);
dialogBoxCreate(<p>
You ascended {props.member.name}!<br />
<br />
Your gang lost {numeralWrapper.formatRespect(res.respect)} respect.<br />
<br />
{props.member.name} gained the following stat multipliers for ascending:<br />
Hacking: x{numeralWrapper.format(res.hack, '0.000')}<br />
Strength: x{numeralWrapper.format(res.str, '0.000')}<br />
Defense: x{numeralWrapper.format(res.def, '0.000')}<br />
Dexterity: x{numeralWrapper.format(res.dex, '0.000')}<br />
Agility: x{numeralWrapper.format(res.agi, '0.000')}<br />
Charisma: x{numeralWrapper.format(res.cha, '0.000')}<br />
</p>);
removePopup(props.popupId);
}
function cancel(): void {
removePopup(props.popupId);
}
const ascendBenefits = props.member.getAscensionResults();
return (<>
<pre>
Are you sure you want to ascend this member? They will lose all of<br />
their non-Augmentation upgrades and their stats will reset back to 1.<br />
<br />
Furthermore, your gang will lose {numeralWrapper.formatRespect(props.member.earnedRespect)} respect<br />
<br />
In return, they will gain the following permanent boost to stat multipliers:<br />
Hacking: x{numeralWrapper.format(ascendBenefits.hack, '0.000')}<br />
Strength: x{numeralWrapper.format(ascendBenefits.str, '0.000')}<br />
Defense: x{numeralWrapper.format(ascendBenefits.def, '0.000')}<br />
Dexterity: x{numeralWrapper.format(ascendBenefits.dex, '0.000')}<br />
Agility: x{numeralWrapper.format(ascendBenefits.agi, '0.000')}<br />
Charisma: x{numeralWrapper.format(ascendBenefits.cha, '0.000')}<br />
</pre>
<button className="std-button" onClick={confirm}>Ascend</button>
<button className="std-button" onClick={cancel}>Cancel</button>
</>);
}

29
src/Gang/ui/BonusTime.tsx Normal file

@ -0,0 +1,29 @@
/**
* React Component for displaying the bonus time remaining.
*/
import * as React from "react";
import { Gang } from "../Gang";
import { CONSTANTS } from "../../Constants";
import { convertTimeMsToTimeElapsedString } from "../../../utils/StringHelperFunctions";
interface IProps {
gang: Gang;
}
export function BonusTime(props: IProps): React.ReactElement {
const CyclerPerSecond = 1000 / CONSTANTS._idleSpeed;
if (props.gang.storedCycles / CyclerPerSecond*1000 <= 5000) return (<></>);
const bonusMillis = props.gang.storedCycles / CyclerPerSecond * 1000;
return (<>
<p className="tooltip" style={{display: "inline-block"}}>
Bonus time: {convertTimeMsToTimeElapsedString(bonusMillis)}
<span className="tooltiptext noselect">
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.
</span>
</p>
<br />
</>);
}

@ -0,0 +1,22 @@
/**
* React Component for a gang member on the management subpage.
*/
import React from "react";
import { Gang } from "../Gang";
import { GangMember } from "../GangMember";
import { Accordion } from "../../ui/React/Accordion";
import { GangMemberAccordionContent } from "./GangMemberAccordionContent";
interface IProps {
gang: Gang;
member: GangMember;
}
export function GangMemberAccordion(props: IProps): React.ReactElement {
return <Accordion
panelInitiallyOpened={true}
headerContent={<>{props.member.name}</>}
panelContent={<GangMemberAccordionContent
gang={props.gang}
member={props.member} />} />
}

@ -0,0 +1,36 @@
/**
* React Component for the content of the accordion of gang members on the
* management subpage.
*/
import React, { useState } from "react";
import { GangMemberStats } from "./GangMemberStats";
import { TaskSelector } from "./TaskSelector";
import { TaskDescription } from "./TaskDescription";
import { Gang } from "../Gang";
import { GangMember } from "../GangMember";
interface IProps {
gang: Gang;
member: GangMember;
}
export function GangMemberAccordionContent(props: IProps): React.ReactElement {
const setRerender = useState(false)[1];
return (<>
<div className={"gang-member-info-div tooltip"}>
<GangMemberStats
onAscend={()=>setRerender(old => !old)}
gang={props.gang}
member={props.member} />
</div>
<div className={"gang-member-info-div"}>
<TaskSelector
onTaskChange={()=>setRerender(old => !old)}
gang={props.gang}
member={props.member} />
</div>
<div className={"gang-member-info-div"}>
<TaskDescription member={props.member} />
</div>
</>);
}

@ -0,0 +1,58 @@
/**
* React Component for the list of gang members on the management subpage.
*/
import React, { useState } from "react";
import { GangMemberUpgradePopup } from "./GangMemberUpgradePopup";
import { GangMemberAccordion } from "./GangMemberAccordion";
import { createPopup } from "../../ui/React/createPopup";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { Gang } from "../Gang";
import { GangMember } from "../GangMember";
import { RecruitButton } from "./RecruitButton";
interface IProps {
gang: Gang;
player: IPlayer;
}
export function GangMemberList(props: IProps): React.ReactElement {
const [filter, setFilter] = useState("");
const setRerender = useState(false)[1];
function openUpgradePopup(): void {
const popupId = `gang-upgrade-popup`;
createPopup(popupId, GangMemberUpgradePopup, {
gang: props.gang,
player: props.player,
popupId: popupId,
});
}
function onFilterChange(event: React.ChangeEvent<HTMLInputElement>): void {
setFilter(event.target.value);
}
const members = props.gang.members.filter((member: GangMember) => member.name.indexOf(filter) > -1 || member.task.indexOf(filter) > -1);
return (<>
<RecruitButton
onRecruit={() => setRerender(old => !old)}
gang={props.gang} />
<br />
<input
className="text-input noselect"
placeholder="Filter gang member"
style={{margin: "5px", padding: "5px"}}
value={filter}
onChange={onFilterChange} />
<a
className="a-link-button"
style={{display: 'inline-block'}}
onClick={openUpgradePopup}>Manage Equipment</a>
<ul>
{members.map((member: GangMember) => <li key={member.name}>
<GangMemberAccordion gang={props.gang} member={member} />
</li>)}
</ul>
</>);
}

@ -0,0 +1,77 @@
/**
* React Component for the first part of a gang member details.
* Contains skills and exp.
*/
import React from "react";
import { dialogBoxCreate } from "../../../utils/DialogBox";
import { formatNumber } from "../../../utils/StringHelperFunctions";
import { numeralWrapper } from "../../ui/numeralFormat";
import { createPopup } from "../../ui/React/createPopup";
import { Gang } from "../Gang";
import { GangMember } from "../GangMember";
import { AscensionPopup } from "./AscensionPopup";
interface IProps {
member: GangMember;
gang: Gang;
onAscend: () => void;
}
export function GangMemberStats(props: IProps): React.ReactElement {
function ascend(): void {
const popupId = `gang-management-ascend-member ${props.member.name}`;
createPopup(popupId, AscensionPopup, {
member: props.member,
gang: props.gang,
popupId: popupId,
onAscend: props.onAscend,
});
}
function openAscensionHelp(): void {
dialogBoxCreate(<>
Ascending a Gang Member resets the member's progress and stats in
exchange for a permanent boost to their stat multipliers.
<br /><br />
The additional stat multiplier that the Gang Member gains upon
ascension is based on the amount of exp they have.
<br /><br />
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.
</>);
}
const asc = {
hack: props.member.calculateAscensionMult(props.member.hack_asc_points),
str: props.member.calculateAscensionMult(props.member.str_asc_points),
def: props.member.calculateAscensionMult(props.member.def_asc_points),
dex: props.member.calculateAscensionMult(props.member.dex_asc_points),
agi: props.member.calculateAscensionMult(props.member.agi_asc_points),
cha: props.member.calculateAscensionMult(props.member.cha_asc_points),
};
return (<>
<span className="tooltiptext smallfont">
Hk: x{numeralWrapper.formatMultiplier(props.member.hack_mult * asc.hack)}(x{numeralWrapper.formatMultiplier(props.member.hack_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.hack)} Asc)<br />
St: x{numeralWrapper.formatMultiplier(props.member.str_mult * asc.str)}(x{numeralWrapper.formatMultiplier(props.member.str_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.str)} Asc)<br />
Df: x{numeralWrapper.formatMultiplier(props.member.def_mult * asc.def)}(x{numeralWrapper.formatMultiplier(props.member.def_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.def)} Asc)<br />
Dx: x{numeralWrapper.formatMultiplier(props.member.dex_mult * asc.dex)}(x{numeralWrapper.formatMultiplier(props.member.dex_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.dex)} Asc)<br />
Ag: x{numeralWrapper.formatMultiplier(props.member.agi_mult * asc.agi)}(x{numeralWrapper.formatMultiplier(props.member.agi_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.agi)} Asc)<br />
Ch: x{numeralWrapper.formatMultiplier(props.member.cha_mult * asc.cha)}(x{numeralWrapper.formatMultiplier(props.member.cha_mult)} Eq, x{numeralWrapper.formatMultiplier(asc.cha)} Asc)
</span>
<pre>
Hacking: {formatNumber(props.member.hack, 0)} ({numeralWrapper.formatExp(props.member.hack_exp)} exp)<br />
Strength: {formatNumber(props.member.str, 0)} ({numeralWrapper.formatExp(props.member.str_exp)} exp)<br />
Defense: {formatNumber(props.member.def, 0)} ({numeralWrapper.formatExp(props.member.def_exp)} exp)<br />
Dexterity: {formatNumber(props.member.dex, 0)} ({numeralWrapper.formatExp(props.member.dex_exp)} exp)<br />
Agility: {formatNumber(props.member.agi, 0)} ({numeralWrapper.formatExp(props.member.agi_exp)} exp)<br />
Charisma: {formatNumber(props.member.cha, 0)} ({numeralWrapper.formatExp(props.member.cha_exp)} exp)<br />
</pre>
<br />
{ props.member.canAscend() && <>
<button className="accordion-button noselect" onClick={ascend}>Ascend</button>
<div className="help-tip noselect" style={{marginTop: "5px"}} onClick={openAscensionHelp}>?</div>
</>}
</>);
}

@ -0,0 +1,149 @@
/**
* React Component for the popup that manages gang members upgrades
*/
import React, { useState, useEffect } from "react";
import { formatNumber } from "../../../utils/StringHelperFunctions";
import { numeralWrapper } from "../../ui/numeralFormat";
import { GangMemberUpgrades } from "../GangMemberUpgrades";
import { GangMemberUpgrade } from "../GangMemberUpgrade";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { Money } from "../../ui/React/Money";
import { removePopup } from "../../ui/React/createPopup";
import { GangMember } from "../GangMember";
import { Gang } from "../Gang";
import { UpgradeType } from "../data/upgrades";
interface IPanelProps {
member: GangMember;
gang: Gang;
player: IPlayer;
}
function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
const setRerender = useState(false)[1];
function filterUpgrades(list: string[], type: UpgradeType): GangMemberUpgrade[] {
return Object.keys(GangMemberUpgrades).filter((upgName: string) => {
const upg = GangMemberUpgrades[upgName];
if (props.player.money.lt(props.gang.getUpgradeCost(upg)))
return false;
if(upg.type !== type) return false;
if(list.includes(upgName)) return false;
return true;
}).map((upgName: string) => GangMemberUpgrades[upgName]);
}
const weaponUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Weapon);
const armorUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Armor);
const vehicleUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Vehicle);
const rootkitUpgrades = filterUpgrades(props.member.upgrades, UpgradeType.Rootkit);
const augUpgrades = filterUpgrades(props.member.augmentations, UpgradeType.Augmentation);
function purchasedUpgrade(upgName: string): React.ReactElement {
const upg = GangMemberUpgrades[upgName]
return (<div key={upgName} className="gang-owned-upgrade tooltip">
{upg.name}
<span className="tooltiptext" dangerouslySetInnerHTML={{__html: upg.desc}} />
</div>);
}
function upgradeButton(upg: GangMemberUpgrade, left = false): React.ReactElement {
function onClick(): void {
props.member.buyUpgrade(upg, props.player, props.gang);
setRerender(old => !old);
}
return (<a key={upg.name} className="a-link-button tooltip" style={{margin:"2px", padding:"2px", display:"block", fontSize:"11px"}} onClick={onClick}>
{upg.name} - {Money(props.gang.getUpgradeCost(upg))}
<span className={left?"tooltiptextleft":"tooltiptext"} dangerouslySetInnerHTML={{__html: upg.desc}} />
</a>);
}
const asc = {
hack: props.member.calculateAscensionMult(props.member.hack_asc_points),
str: props.member.calculateAscensionMult(props.member.str_asc_points),
def: props.member.calculateAscensionMult(props.member.def_asc_points),
dex: props.member.calculateAscensionMult(props.member.dex_asc_points),
agi: props.member.calculateAscensionMult(props.member.agi_asc_points),
cha: props.member.calculateAscensionMult(props.member.cha_asc_points),
};
return (<div style={{border: '1px solid white'}}>
<h1>{props.member.name}({props.member.task})</h1>
<pre style={{fontSize:"14px", display: "inline-block", width:"20%"}}>
Hack: {props.member.hack} (x{formatNumber(props.member.hack_mult * asc.hack, 2)})<br />
Str: {props.member.str} (x{formatNumber(props.member.str_mult * asc.str, 2)})<br />
Def: {props.member.def} (x{formatNumber(props.member.def_mult * asc.def, 2)})<br />
Dex: {props.member.dex} (x{formatNumber(props.member.dex_mult * asc.dex, 2)})<br />
Agi: {props.member.agi} (x{formatNumber(props.member.agi_mult * asc.agi, 2)})<br />
Cha: {props.member.cha} (x{formatNumber(props.member.cha_mult * asc.cha, 2)})
</pre>
<div className="gang-owned-upgrades-div noselect">
Purchased Upgrades: {props.member.upgrades.map((upg: string) => purchasedUpgrade(upg))}
{props.member.augmentations.map((upg: string) => purchasedUpgrade(upg))}
</div>
<div className="noselect" style={{width: "20%", display: "inline-block"}}>
<h2>Weapons</h2>
{weaponUpgrades.map(upg => upgradeButton(upg))}
</div>
<div className="noselect" style={{width: "20%", display: "inline-block"}}>
<h2>Armor</h2>
{armorUpgrades.map(upg => upgradeButton(upg))}
</div>
<div className="noselect" style={{width: "20%", display: "inline-block"}}>
<h2>Vehicles</h2>
{vehicleUpgrades.map(upg => upgradeButton(upg))}
</div>
<div className="noselect" style={{width: "20%", display: "inline-block"}}>
<h2>Rootkits</h2>
{rootkitUpgrades.map(upg => upgradeButton(upg, true))}
</div>
<div className="noselect" style={{width: "20%", display: "inline-block"}}>
<h2>Augmentations</h2>
{augUpgrades.map(upg => upgradeButton(upg, true))}
</div>
</div>);
}
interface IProps {
gang: Gang;
player: IPlayer;
popupId: string;
}
export function GangMemberUpgradePopup(props: IProps): React.ReactElement {
const setRerender = useState(false)[1];
const [filter, setFilter] = useState("");
function closePopup(this: Window, ev: KeyboardEvent): void {
if(ev.keyCode !== 27) return;
removePopup(props.popupId);
}
useEffect(() => {
window.addEventListener<'keydown'>('keydown', closePopup);
const id = setInterval(() => setRerender(old => !old), 1000);
return () => {
clearInterval(id);
window.removeEventListener<'keydown'>('keydown', closePopup);
}
}, []);
return (<>
<input
className="text-input noselect"
value={filter}
placeholder="Filter gang member"
onChange={event => setFilter(event.target.value)} />
<p className="tooltip" style={{marginLeft: '6px', display: 'inline-block'}}>
Discount: -{numeralWrapper.formatPercentage(1 - 1 / props.gang.getDiscount())}
<span className="tooltiptext noselect">
You get a discount on equipment and upgrades based on your
gang's respect and power. More respect and power leads to more
discounts.
</span>
</p>
{props.gang.members.map((member: GangMember) => <GangMemberUpgradePanel
key={member.name}
player={props.player}
gang={props.gang}
member={member} />)
}
</>);
}

72
src/Gang/ui/GangStats.tsx Normal file

@ -0,0 +1,72 @@
/**
* React Component for the stats related to the gang, like total respect and
* money per second.
*/
import React from "react";
import { Factions } from "../../Faction/Factions";
import { Gang } from "../Gang";
import { formatNumber } from "../../../utils/StringHelperFunctions";
import { numeralWrapper } from "../../ui/numeralFormat";
import { MoneyRate } from "../../ui/React/MoneyRate";
import { Reputation } from "../../ui/React/Reputation";
import { AllGangs } from "../AllGangs";
import { BonusTime } from "./BonusTime";
interface IProps {
gang: Gang;
}
export function GangStats(props: IProps): React.ReactElement {
const territoryMult = AllGangs[props.gang.facName].territory * 100;
let territoryStr;
if (territoryMult <= 0) {
territoryStr = formatNumber(0, 2);
} else if (territoryMult >= 100) {
territoryStr = formatNumber(100, 2);
} else {
territoryStr = formatNumber(territoryMult, 2);
}
return (<>
<p className="tooltip" style={{display: "inline-block"}}>
Respect: {numeralWrapper.formatRespect(props.gang.respect)} ({numeralWrapper.formatRespect(5*props.gang.respectGainRate)} / sec)
<span className="tooltiptext">
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.
</span>
</p>
<br />
<p className="tooltip" style={{display: "inline-block"}}>
Wanted Level: {numeralWrapper.formatWanted(props.gang.wanted)} ({numeralWrapper.formatWanted(5*props.gang.wantedGainRate)} / sec)
<span className="tooltiptext">
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.
</span>
</p>
<br />
<p className="tooltip" style={{display: "inline-block"}}>
Wanted Level Penalty: -{formatNumber((1 - props.gang.getWantedPenalty()) * 100, 2)}%
<span className="tooltiptext">
Penalty for respect and money gain rates due to Wanted Level
</span>
</p>
<br />
<div>
<p style={{display: "inline-block"}}>
Money gain rate: {MoneyRate(5 * props.gang.moneyGainRate)}
</p>
</div>
<br />
<p className="tooltip" style={{display: "inline-block"}}>
Territory: {territoryStr}%
<span className="tooltiptext">
The percentage of total territory your Gang controls
</span>
</p>
<br />
<p style={{display: "inline-block"}}>
Faction reputation: {Reputation(Factions[props.gang.facName].playerReputation)}
</p>
<br />
<BonusTime gang={props.gang} />
</>);
}

@ -0,0 +1,46 @@
/**
* React Component for the subpage that manages gang members, the main page.
*/
import React from "react";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { GangStats } from "./GangStats";
import { Gang } from "../Gang";
import { GangMemberList } from "./GangMemberList";
interface IProps {
gang: Gang;
player: IPlayer;
}
export function ManagementSubpage(props: IProps): React.ReactElement {
return (<div style={{display: 'block'}}>
<p className="noselect" style={{width: "70%"}}>
This page is used to manage your gang members and get an overview of
your gang's stats.
<br />
<br />
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 '{props.gang.isHackingGang?"Ethical Hacking":"Vigilante Justice"}'
task to lower your wanted level.
<br />
<br />
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.
<br />
<br />
You can also manage your gang programmatically through Netscript
using the Gang API
</p>
<br />
<GangStats gang={props.gang} />
<br />
<GangMemberList gang={props.gang} player={props.player} />
</div>);
}

@ -0,0 +1,50 @@
/**
* React Component for the recruitment button and text on the gang main page.
*/
import React from "react";
import { Gang } from "../Gang";
import { RecruitPopup } from "./RecruitPopup";
import { GangConstants } from "../data/Constants";
import { formatNumber } from "../../../utils/StringHelperFunctions";
import { createPopup } from "../../ui/React/createPopup";
interface IProps {
gang: Gang;
onRecruit: () => void;
}
export function RecruitButton(props: IProps): React.ReactElement {
if (props.gang.members.length >= GangConstants.MaximumGangMembers) {
return (<></>);
}
if (!props.gang.canRecruitMember()) {
const respect = props.gang.getRespectNeededToRecruitMember();
return (<>
<a className="a-link-button-inactive"
style={{display: 'inline-block', margin: '10px'}}>
Recruit Gang Member
</a>
<p style={{margin: '10px', color: 'red', display: 'inline-block'}}>
{formatNumber(respect, 2)} respect needed to recruit next member
</p>
</>);
}
function onClick(): void {
const popupId = "recruit-gang-member-popup";
createPopup(popupId, RecruitPopup, {
gang: props.gang,
popupId: popupId,
onRecruit: props.onRecruit,
});
}
return (<>
<a className="a-link-button"
onClick={onClick}
style={{display: 'inline-block', margin: '10px'}}>
Recruit Gang Member
</a>
</>);
}

@ -0,0 +1,63 @@
/**
* React Component for the popup used to recruit new gang members.
*/
import React, { useState } from "react";
import { Gang } from "../Gang";
import { removePopup } from "../../ui/React/createPopup";
import { dialogBoxCreate } from "../../../utils/DialogBox";
interface IRecruitPopupProps {
gang: Gang;
popupId: string;
onRecruit: () => void;
}
export function RecruitPopup(props: IRecruitPopupProps): React.ReactElement {
const [name, setName] = useState("");
function recruit(): void {
if (name === "") {
dialogBoxCreate("You must enter a name for your Gang member!");
return;
}
if (!props.gang.canRecruitMember()) {
dialogBoxCreate("You cannot recruit another Gang member!");
return;
}
// At this point, the only way this can fail is if you already
// have a gang member with the same name
if (!props.gang.recruitMember(name)) {
dialogBoxCreate("You already have a gang member with this name!");
return;
}
props.onRecruit();
removePopup(props.popupId);
}
function cancel(): void {
removePopup(props.popupId);
}
function onKeyUp(event: React.KeyboardEvent<HTMLInputElement>): void {
if(event.keyCode === 13) recruit();
if(event.keyCode === 27) cancel();
}
function onChange(event: React.ChangeEvent<HTMLInputElement>): void {
setName(event.target.value);
}
return (<>
<p className="noselect">Enter a name for your new Gang member:</p><br />
<input autoFocus
onKeyUp={onKeyUp}
onChange={onChange}
className="text-input noselect"
type="text"
placeholder="unique name" />
<a className="std-button" onClick={recruit}>Recruit Gang Member</a>
<a className="std-button" onClick={cancel}>Cancel</a>
</>);
}

49
src/Gang/ui/Root.tsx Normal file

@ -0,0 +1,49 @@
/**
* React Component for all the gang stuff.
*/
import React, { useState, useEffect } from "react";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { ManagementSubpage } from "./ManagementSubpage";
import { TerritorySubpage } from "./TerritorySubpage";
import { IEngine } from "../../IEngine";
import { Gang } from "../Gang";
import { displayFactionContent } from "../../Faction/FactionHelpers";
interface IProps {
gang: Gang;
player: IPlayer;
engine: IEngine;
}
export function Root(props: IProps): React.ReactElement {
const [management, setManagement] = useState(true);
const setRerender = useState(false)[1];
useEffect(() => {
const id = setInterval(() => setRerender(old => !old), 1000);
return () => clearInterval(id);
}, []);
function back(): void {
props.engine.loadFactionContent();
displayFactionContent(props.gang.facName);
}
return (<>
<a className="a-link-button" style={{display: "inline-block"}}
onClick={back}>Back</a>
<a className={management?"a-link-button-inactive":"a-link-button"}
style={{display: "inline-block"}}
onClick={() => setManagement(true)}>
Gang Management
</a>
<a className={!management?"a-link-button-inactive":"a-link-button"}
style={{display: "inline-block"}}
onClick={() => setManagement(false)}>
Gang Territory
</a>
{management ?
<ManagementSubpage gang={props.gang} player={props.player} /> :
<TerritorySubpage gang={props.gang} />}
</>);
}

@ -0,0 +1,18 @@
/**
* React Component for left side of the gang member accordion, contains the
* description of the task that member is currently doing.
*/
import React from "react";
import { GangMemberTasks } from "../GangMemberTasks";
import { GangMember } from "../GangMember";
interface IProps {
member: GangMember;
}
export function TaskDescription(props: IProps): React.ReactElement {
const task = GangMemberTasks[props.member.task];
const desc = task ? task.desc: GangMemberTasks["Unassigned"].desc;
return (<p className="inline noselect" dangerouslySetInnerHTML={{__html: desc}} />);
}

@ -0,0 +1,47 @@
/**
* React Component for the middle part of the gang member accordion. Contains
* the task selector as well as some stats.
*/
import React, { useState } from "react";
import { numeralWrapper } from "../../ui/numeralFormat";
import { StatsTable } from "../../ui/React/StatsTable";
import { MoneyRate } from "../../ui/React/MoneyRate";
import { Gang } from "../Gang";
import { GangMember } from "../GangMember";
interface IProps {
member: GangMember;
gang: Gang;
onTaskChange: () => void;
}
export function TaskSelector(props: IProps): React.ReactElement {
const [currentTask, setCurrentTask] = useState(props.member.task);
function onChange(event: React.ChangeEvent<HTMLSelectElement>): void {
const task = event.target.value;
props.member.assignToTask(task);
setCurrentTask(task);
props.onTaskChange();
}
const tasks = props.gang.getAllTaskNames();
const data = [
[`Money:`, MoneyRate(5*props.member.calculateMoneyGain(props.gang))],
[`Respect:`, `${numeralWrapper.formatRespect(5*props.member.calculateRespectGain(props.gang))} / sec`],
[`Wanted Level:`, `${numeralWrapper.formatWanted(5*props.member.calculateWantedLevelGain(props.gang))} / sec`],
[`Total Respect:`, `${numeralWrapper.formatRespect(props.member.earnedRespect)}`],
];
return (<>
<select
onChange={onChange}
className="dropdown noselect"
value={currentTask}>
<option key={0} value={"---"}>---</option>
{tasks.map((task: string, i: number) => <option key={i+1} value={task}>{task}</option>)}
</select>
<div>{StatsTable(data, null)}</div>
</>);
}

@ -0,0 +1,130 @@
/**
* React Component for the territory subpage.
*/
import React from "react";
import { numeralWrapper } from "../../ui/numeralFormat";
import { dialogBoxCreate } from "../../../utils/DialogBox";
import { formatNumber } from "../../../utils/StringHelperFunctions";
import { AllGangs } from "../AllGangs";
import { Gang } from "../Gang";
interface IProps {
gang: Gang;
}
export function TerritorySubpage(props: IProps): React.ReactElement {
function openWarfareHelp(): void {
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.");
}
function formatTerritory(n: number): string {
const v = n * 100;
if (v <= 0) {
return formatNumber(0, 2);
} else if (v >= 100) {
return formatNumber(100, 2);
} else {
return formatNumber(v, 2);
}
}
const playerPower = AllGangs[props.gang.facName].power;
function otherGangTerritory(name: string): React.ReactElement {
const power = AllGangs[name].power
const clashVictoryChance = playerPower / (power + playerPower);
return (<span key={name}>
<u>{name}</u><br />
Power: {formatNumber(power, 6)}<br />
Territory: {formatTerritory(AllGangs[name].territory)}%<br />
Chance to win clash with this gang: {numeralWrapper.formatPercentage(clashVictoryChance, 3)}<br />
<br />
</span>);
}
const gangNames = Object.keys(AllGangs).filter(g => g != props.gang.facName);
return (<div style={{width: '70%'}}>
<p className="noselect">
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.
<br />
<br />
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.
<br />
<br />
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.
<br />
<br />
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.
<br />
<br />
</p>
<input
checked={props.gang.territoryWarfareEngaged}
id="warfare"
type="checkbox"
style={{display: "inline-block", margin: "2px"}}
onChange={(event)=> props.gang.territoryWarfareEngaged = event.target.checked}/>
<label
htmlFor="warfare"
className="tooltip noselect"
style={{color: "white", display: 'inline-block'}}>
Engage in Territory Warfare
<span className="tooltiptext" style={{display: "inline-block"}}>
Engaging in Territory Warfare sets your clash chance to 100%.
Disengaging will cause your clash chance to gradually decrease
until it reaches 0%.
</span>
</label>
<br />
<p style={{display: 'inline-block'}}>
Territory Clash Chance: {numeralWrapper.formatPercentage(props.gang.territoryClashChance, 3)}
</p>
<div
className="help-tip noselect"
style={{display: "inline-block"}}
onClick={openWarfareHelp}>?</div>
<br />
<input
checked={props.gang.notifyMemberDeath}
id="notify"
type="checkbox"
style={{display: "inline-block", margin: "2px"}}
onChange={(event)=> props.gang.notifyMemberDeath = event.target.checked}/>
<label htmlFor="warfare" className="tooltip noselect" style={{color: "white", display: 'inline-block'}}>
Notify about Gang Member Deaths
<span className="tooltiptext" style={{display: "inline-block"}}>
If this is enabled, then you will receive a pop-up notifying you
whenever one of your Gang Members dies in a territory clash.
</span>
</label>
<br />
<fieldset style={{display: "block", margin: "6px"}}>
<p>
<b><u>{props.gang.facName}</u></b><br />
Power: {formatNumber(AllGangs[props.gang.facName].power, 6)}<br />
Territory: {formatTerritory(AllGangs[props.gang.facName].territory)}%<br />
<br />
{gangNames.map(name => otherGangTerritory(name))}
</p>
</fieldset>
</div>);
}

@ -100,7 +100,7 @@ export function WireCuttingGame(props: IMinigameProps): React.ReactElement {
return (<Grid container spacing={3}>
<GameTimer millis={timer} onExpire={props.onFailure} />
<Grid item xs={12}>
<h1 className={"noselect"}>Cut the wires with the following properties!</h1>
<h1 className={"noselect"}>Cut the wires with the following properties! (keyboard 1 to 9)</h1>
{questions.map((question, i) => <h3 key={i}>{question.toString()}</h3>)}
<pre>{(new Array(wires.length)).fill(0).map((_, i) => <span key={i}>&nbsp;{i+1}&nbsp;&nbsp;&nbsp;&nbsp;</span>)}</pre>
{(new Array(8)).fill(0).map((_, i) => <div key={i}>

@ -440,4 +440,4 @@ export const LocationsMetadata: IConstructorParams[] = [
name: LocationName.WorldStockExchange,
types: [LocationType.StockMarket],
},
];
];

@ -65,7 +65,7 @@ export class LocationCity extends React.Component<IProps, any> {
elems.push(<pre key={i}>{lineElems(lines[i])}</pre>)
}
return elems;
return <div className="noselect">{elems}</div>;
}
listCity(): React.ReactNode {

@ -158,7 +158,7 @@ export class GenericLocation extends React.Component<IProps, any> {
return (
<div>
<StdButton onClick={this.props.returnToCity} style={this.btnStyle} text={"Return to World"} />
<h1>
<h1 className="noselect">
{backdoorInstalled && !Settings.DisableTextEffects
? <CorruptableText content={this.props.loc.name}/>
: this.props.loc.name

@ -74,7 +74,7 @@ export class LocationRoot extends React.Component<IProps, IState> {
}
return (
<div>
<div className="noselect">
<h2>{this.state.city}</h2>
<LocationCity city={city} enterLocation={this.enterLocation} />
</div>
@ -120,7 +120,7 @@ export class LocationRoot extends React.Component<IProps, IState> {
p.loseMoney(cost);
p.travel(to);
dialogBoxCreate(`You are now in ${to}!`);
dialogBoxCreate(<span className="noselect">You are now in {to}!</span>);
// Dynamically update main menu
if (p.firstTimeTraveled === false) {

@ -44,7 +44,7 @@ export class TravelAgencyLocation extends React.Component<IProps, any> {
}
return (
<div>
<div className="noselect">
<p>
From here, you can travel to any other city! A ticket
costs {Money(CONSTANTS.TravelCost)}.

@ -29,12 +29,10 @@ import {
calculateWeakenTime,
} from "./Hacking";
import { calculateServerGrowth } from "./Server/formulas/grow";
import {
AllGangs,
GangMemberUpgrades,
GangMemberTasks,
Gang,
} from "./Gang";
import { Gang } from "./Gang/Gang";
import { AllGangs } from "./Gang/AllGangs";
import { GangMemberTasks } from "./Gang/GangMemberTasks";
import { GangMemberUpgrades } from "./Gang/GangMemberUpgrades";
import { Factions, factionExists } from "./Faction/Factions";
import { joinFaction, purchaseAugmentation } from "./Faction/FactionHelpers";
import { FactionWorkType } from "./Faction/FactionWorkTypeEnum";
@ -3743,17 +3741,21 @@ function NetscriptFunctions(workerScript) {
getEquipmentNames: function() {
updateDynamicRam("getEquipmentNames", getRamCost("gang", "getEquipmentNames"));
checkGangApiAccess("getEquipmentNames");
return Player.gang.getAllUpgradeNames();
return Object.keys(GangMemberUpgrades);
},
getEquipmentCost: function(equipName) {
updateDynamicRam("getEquipmentCost", getRamCost("gang", "getEquipmentCost"));
checkGangApiAccess("getEquipmentCost");
return Player.gang.getUpgradeCost(equipName);
const upg = GangMemberUpgrades[equipName];
if(upg === null) return Infinity;
return Player.gang.getUpgradeCost(upg);
},
getEquipmentType: function(equipName) {
updateDynamicRam("getEquipmentType", getRamCost("gang", "getEquipmentType"));
checkGangApiAccess("getEquipmentType");
return Player.gang.getUpgradeType(equipName);
const upg = GangMemberUpgrades[equipName];
if (upg == null) return "";
return upg.getType();
},
getEquipmentStats: function(equipName) {
updateDynamicRam("getEquipmentStats", getRamCost("gang", "getEquipmentStats"));
@ -3768,7 +3770,9 @@ function NetscriptFunctions(workerScript) {
updateDynamicRam("purchaseEquipment", getRamCost("gang", "purchaseEquipment"));
checkGangApiAccess("purchaseEquipment");
const member = getGangMember("purchaseEquipment", memberName);
const res = member.buyUpgrade(equipName, Player, Player.gang);
const equipment = GangMemberUpgrades[equipName];
if(!equipment) return false;
const res = member.buyUpgrade(equipment, Player, Player.gang);
if (res) {
workerScript.log("purchaseEquipment", `Purchased '${equipName}' for Gang member '${memberName}'`);
} else {
@ -3781,6 +3785,7 @@ function NetscriptFunctions(workerScript) {
updateDynamicRam("ascendMember", getRamCost("gang", "ascendMember"));
checkGangApiAccess("ascendMember");
const member = getGangMember("ascendMember", name);
if(!member.canAscend()) return;
return Player.gang.ascendMember(member, workerScript);
},
setTerritoryWarfare: function(engage) {

@ -1,5 +1,5 @@
import { Factions } from "../../Faction/Factions";
import { Gang } from "../../Gang";
import { Gang } from "../../Gang/Gang";
import { SourceFileFlags } from "../../SourceFile/SourceFileFlags";
import { BitNodeMultipliers } from "../../BitNode/BitNodeMultipliers";

@ -18,7 +18,7 @@ import { Engine } from "../../engine";
import { Faction } from "../../Faction/Faction";
import { Factions } from "../../Faction/Factions";
import { displayFactionContent } from "../../Faction/FactionHelpers";
import { resetGangs } from "../../Gang";
import { resetGangs } from "../../Gang/AllGangs";
import { hasHacknetServers } from "../../Hacknet/HacknetHelpers";
import { Cities } from "../../Locations/Cities";
import { Locations } from "../../Locations/Locations";

@ -13,6 +13,5 @@ export const BaseCostPerSleeve = 10e12;
export const PopupId = "covenant-sleeve-purchases-popup";
export function createSleevePurchasesFromCovenantPopup(p: IPlayer): void {
const removePopupFn = removePopup.bind(null, PopupId);
createPopup(PopupId, CovenantPurchasesRoot, { p: p, closeFn: removePopupFn });
createPopup(PopupId, CovenantPurchasesRoot, { p: p, closeFn: () => removePopup(PopupId) });
}

@ -2,7 +2,7 @@
* Root React component for the popup that lets player purchase Duplicate
* Sleeves and Sleeve-related upgrades from The Covenant
*/
import * as React from "react";
import React, { useState } from 'react';
import { CovenantSleeveUpgrades } from "./CovenantSleeveUpgrades";
@ -21,91 +21,73 @@ import { dialogBoxCreate } from "../../../../utils/DialogBox";
interface IProps {
closeFn: () => void;
p: IPlayer;
rerender: () => void;
}
interface IState {
update: number;
}
export class CovenantPurchasesRoot extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
update: 0,
}
this.rerender = this.rerender.bind(this);
}
export function CovenantPurchasesRoot(props: IProps): React.ReactElement {
const [update, setUpdate] = useState(0);
/**
* Get the cost to purchase a new Duplicate Sleeve
*/
purchaseCost(): number {
return (this.props.p.sleevesFromCovenant + 1) * BaseCostPerSleeve;
function purchaseCost(): number {
return (props.p.sleevesFromCovenant + 1) * BaseCostPerSleeve;
}
/**
* Force a rerender by just changing an arbitrary state value
*/
rerender(): void {
this.setState((state: IState) => ({
update: state.update + 1,
}));
function rerender(): void {
setUpdate(update + 1);
}
render(): React.ReactNode {
// Purchasing a new Duplicate Sleeve
let purchaseDisabled = false;
if (!this.props.p.canAfford(this.purchaseCost())) {
purchaseDisabled = true;
}
if (this.props.p.sleevesFromCovenant >= MaxSleevesFromCovenant) {
purchaseDisabled = true;
}
const purchaseOnClick = (): void => {
if (this.props.p.sleevesFromCovenant >= MaxSleevesFromCovenant) { return; }
if (this.props.p.canAfford(this.purchaseCost())) {
this.props.p.loseMoney(this.purchaseCost());
this.props.p.sleevesFromCovenant += 1;
this.props.p.sleeves.push(new Sleeve(this.props.p));
this.rerender();
} else {
dialogBoxCreate(`You cannot afford to purchase a Duplicate Sleeve`, false);
}
}
// Purchasing a new Duplicate Sleeve
let purchaseDisabled = false;
if (!props.p.canAfford(purchaseCost())) {
purchaseDisabled = true;
}
if (props.p.sleevesFromCovenant >= MaxSleevesFromCovenant) {
purchaseDisabled = true;
}
// Purchasing Upgrades for Sleeves
const upgradePanels = [];
for (let i = 0; i < this.props.p.sleeves.length; ++i) {
const sleeve = this.props.p.sleeves[i];
upgradePanels.push(
<CovenantSleeveUpgrades {...this.props} sleeve={sleeve} index={i} rerender={this.rerender} key={i} />,
)
function purchaseOnClick(): void {
if (props.p.sleevesFromCovenant >= MaxSleevesFromCovenant) return;
if (props.p.canAfford(purchaseCost())) {
props.p.loseMoney(purchaseCost());
props.p.sleevesFromCovenant += 1;
props.p.sleeves.push(new Sleeve(props.p));
rerender();
} else {
dialogBoxCreate(`You cannot afford to purchase a Duplicate Sleeve`, false);
}
}
return (
<div>
<PopupCloseButton popup={PopupId} text={"Close"} />
<p>
Would you like to purchase an additional Duplicate Sleeve from The Covenant
for {Money(this.purchaseCost())}?
</p>
<br />
<p>
These Duplicate Sleeves are permanent (they persist through BitNodes). You can
purchase a total of {MaxSleevesFromCovenant} from The Covenant.
</p>
<StdButton disabled={purchaseDisabled} onClick={purchaseOnClick} text={"Purchase"} />
<br /><br />
<p>
Here, you can also purchase upgrades for your Duplicate Sleeves. These upgrades
are also permanent, meaning they persist across BitNodes.
</p>
{upgradePanels}
</div>
// Purchasing Upgrades for Sleeves
const upgradePanels = [];
for (let i = 0; i < props.p.sleeves.length; ++i) {
const sleeve = props.p.sleeves[i];
upgradePanels.push(
<CovenantSleeveUpgrades {...props} sleeve={sleeve} index={i} rerender={rerender} key={i} />,
)
}
return (<div>
<PopupCloseButton popup={PopupId} text={"Close"} />
<p>
Would you like to purchase an additional Duplicate Sleeve from The Covenant
for {Money(purchaseCost())}?
</p>
<br />
<p>
These Duplicate Sleeves are permanent (they persist through BitNodes). You can
purchase a total of {MaxSleevesFromCovenant} from The Covenant.
</p>
<StdButton disabled={purchaseDisabled} onClick={purchaseOnClick} text={"Purchase"} />
<br /><br />
<p>
Here, you can also purchase upgrades for your Duplicate Sleeves. These upgrades
are also permanent, meaning they persist across BitNodes.
</p>
{upgradePanels}
</div>);
}

@ -333,7 +333,7 @@ function prestigeSourceFile(flume) {
deleteStockMarket();
}
if (Player.inGang()) { Player.gang.clearUI(); }
if (Player.inGang()) clearGangUI();
Player.gang = null;
Player.corporation = null; resetIndustryResearchTrees();
Player.bladeburner = null;

@ -10,7 +10,7 @@ import { Engine } from "./engine";
import { Factions, loadFactions } from "./Faction/Factions";
import { loadFconf } from "./Fconf/Fconf";
import { FconfSettings } from "./Fconf/FconfSettings";
import { loadAllGangs, AllGangs } from "./Gang";
import { loadAllGangs, AllGangs } from "./Gang/AllGangs";
import { loadMessages, initMessages, Messages } from "./Message/MessageHelpers";
import { Player, loadPlayer } from "./Player";
import { AllServers, loadAllServers } from "./Server/AllServers";

@ -32,6 +32,7 @@ import {
processPassiveFactionRepGain,
inviteToFaction,
} from "./Faction/FactionHelpers";
import { displayGangContent } from "./Gang/Helpers";
import { displayInfiltrationContent } from "./Infiltration/Helper";
import {
getHackingWorkRepGain,
@ -227,6 +228,7 @@ const Engine = {
tutorialContent: null,
infiltrationContent: null,
stockMarketContent: null,
gangContent: null,
locationContent: null,
workInProgressContent: null,
redPillContent: null,
@ -438,9 +440,10 @@ const Engine = {
loadGangContent: function() {
Engine.hideAllContent();
if (document.getElementById("gang-container") || Player.inGang()) {
Player.gang.displayGangContent(Player);
if (Player.inGang()) {
Engine.Display.gangContent.style.display = "block";
routing.navigateTo(Page.Gang);
displayGangContent(this, Player.gang, Player);
} else {
Engine.loadTerminalContent();
routing.navigateTo(Page.Terminal);
@ -523,6 +526,9 @@ const Engine = {
Engine.Display.locationContent.style.display = "none";
ReactDOM.unmountComponentAtNode(Engine.Display.locationContent);
Engine.Display.gangContent.style.display = "none";
ReactDOM.unmountComponentAtNode(Engine.Display.gangContent);
Engine.Display.workInProgressContent.style.display = "none";
Engine.Display.redPillContent.style.display = "none";
@ -533,9 +539,6 @@ const Engine = {
document.getElementById("gang-container").style.display = "none";
}
if (Player.inGang()) {
Player.gang.clearUI();
}
if (Player.corporation instanceof Corporation) {
Player.corporation.clearUI();
}
@ -569,6 +572,7 @@ const Engine = {
MainMenuLinks.Travel.classList.remove("active");
MainMenuLinks.Job.classList.remove("active");
MainMenuLinks.StockMarket.classList.remove("active");
MainMenuLinks.Gang.classList.remove("active");
MainMenuLinks.Bladeburner.classList.remove("active");
MainMenuLinks.Corporation.classList.remove("active");
MainMenuLinks.Gang.classList.remove("active");
@ -866,9 +870,7 @@ const Engine = {
}
if (Engine.Counters.updateDisplaysLong <= 0) {
if (routing.isOn(Page.Gang) && Player.inGang()) {
Player.gang.updateGangContent();
} else if (routing.isOn(Page.ScriptEditor)) {
if (routing.isOn(Page.ScriptEditor)) {
updateScriptEditorContent();
}
Engine.Counters.updateDisplaysLong = 15;
@ -1322,6 +1324,9 @@ const Engine = {
Engine.Display.stockMarketContent = document.getElementById("stock-market-container");
Engine.Display.stockMarketContent.style.display = "none";
Engine.Display.gangContent = document.getElementById("gang-container");
Engine.Display.gangContent.style.display = "none";
Engine.Display.missionContent = document.getElementById("mission-container");
Engine.Display.missionContent.style.display = "none";
@ -1450,6 +1455,7 @@ const Engine = {
MainMenuLinks.Gang.addEventListener("click", function() {
Engine.loadGangContent();
MainMenuLinks.Gang.classList.add('active');
return false;
});

@ -35,83 +35,83 @@ if (htmlWebpackPlugin.options.googleAnalytics.trackingId) { %>
<ul id="mainmenu" class="mainmenu noscrollbar">
<!-- Hacking dropdown -->
<li id="hacking-menu-header-li">
<button id="hacking-menu-header" class="mainmenu-accordion-header"> Hacking </button>
<button id="hacking-menu-header" class="mainmenu-accordion-header noselect"> Hacking </button>
</li>
<li id="terminal-tab" class="mainmenu-accordion-panel">
<li id="terminal-tab" class="mainmenu-accordion-panel noselect">
<button id="terminal-menu-link"> Terminal </button>
</li>
<li id="create-script-tab" class="mainmenu-accordion-panel">
<li id="create-script-tab" class="mainmenu-accordion-panel noselect">
<button id="create-script-menu-link"> Create Script </button>
</li>
<li id="active-scripts-tab" class="mainmenu-accordion-panel">
<li id="active-scripts-tab" class="mainmenu-accordion-panel noselect">
<button id="active-scripts-menu-link"> Active Scripts </button>
</li>
<li id="create-program-tab" class="mainmenu-accordion-panel">
<li id="create-program-tab" class="mainmenu-accordion-panel noselect">
<button id="create-program-menu-link"> Create Program </button>
<span id="create-program-notification" class="notification-off"> </span>
</li>
<!-- Character dropdown -->
<li id="character-menu-header-li">
<button id="character-menu-header" class="mainmenu-accordion-header"> Character </button>
<button id="character-menu-header" class="mainmenu-accordion-header noselect"> Character </button>
</li>
<li id="stats-tab" class="mainmenu-accordion-panel">
<li id="stats-tab" class="mainmenu-accordion-panel noselect">
<button id="stats-menu-link"> Stats </button>
</li>
<li id="factions-tab" class="mainmenu-accordion-panel">
<li id="factions-tab" class="mainmenu-accordion-panel noselect">
<button id="factions-menu-link"> Factions </button>
<span id="factions-notification" class="notification-off"> </span>
</li>
<li id="augmentations-tab" class="mainmenu-accordion-panel">
<li id="augmentations-tab" class="mainmenu-accordion-panel noselect">
<button id="augmentations-menu-link" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"> Augmentations </button>
<span id="augmentations-notification" class="notification-off"> </span>
</li>
<li id="hacknet-nodes-tab" class="mainmenu-accordion-panel">
<li id="hacknet-nodes-tab" class="mainmenu-accordion-panel noselect">
<button id="hacknet-nodes-menu-link"> Hacknet </button>
</li>
<li id="sleeves-tab" class="mainmenu-accordion-panel">
<li id="sleeves-tab" class="mainmenu-accordion-panel noselect">
<button id="sleeves-menu-link"> Sleeves </button>
</li>
<!-- World dropdown -->
<li id="world-menu-header-li">
<button id="world-menu-header" class="mainmenu-accordion-header">World</button>
<button id="world-menu-header" class="mainmenu-accordion-header noselect">World</button>
</li>
<li id="city-tab" class="mainmenu-accordion-panel">
<li id="city-tab" class="mainmenu-accordion-panel noselect">
<button id="city-menu-link"> City </button>
</li>
<li id="travel-tab" class="mainmenu-accordion-panel">
<li id="travel-tab" class="mainmenu-accordion-panel noselect">
<button id="travel-menu-link"> Travel </button>
</li>
<li id="job-tab" class="mainmenu-accordion-panel">
<li id="job-tab" class="mainmenu-accordion-panel noselect">
<button id="job-menu-link"> Job </button>
</li>
<li id="stock-market-tab" class="mainmenu-accordion-panel">
<li id="stock-market-tab" class="mainmenu-accordion-panel noselect">
<button id="stock-market-menu-link"> Stock Market </button>
</li>
<li id="bladeburner-tab" class="mainmenu-accordion-panel">
<li id="bladeburner-tab" class="mainmenu-accordion-panel noselect">
<button id="bladeburner-menu-link"> Bladeburner </button>
</li>
<li id="corporation-tab" class="mainmenu-accordion-panel">
<li id="corporation-tab" class="mainmenu-accordion-panel noselect">
<button id="corporation-menu-link"> Corp </button>
</li>
<li id="gang-tab" class="mainmenu-accordion-panel">
<li id="gang-tab" class="mainmenu-accordion-panel noselect">
<button id="gang-menu-link"> Gang </button>
</li>
<li id="help-menu-header-li">
<button id="help-menu-header" class="mainmenu-accordion-header"> Help </button>
<button id="help-menu-header" class="mainmenu-accordion-header noselect"> Help </button>
</li>
<li id="milestones-tab" class="mainmenu-accordion-panel">
<li id="milestones-tab" class="mainmenu-accordion-panel noselect">
<button id="milestones-menu-link"> Milestones </button>
</li>
<li id="tutorial-tab" class="mainmenu-accordion-panel">
<li id="tutorial-tab" class="mainmenu-accordion-panel noselect">
<button id="tutorial-menu-link"> Tutorial </button>
</li>
<li id="options-tab" class="mainmenu-accordion-panel">
<li id="options-tab" class="mainmenu-accordion-panel noselect">
<button id="options-menu-link"> Options </button>
</li>
<li id="dev-tab" class="mainmenu-accordion-panel">
<li id="dev-tab" class="mainmenu-accordion-panel noselect">
<button id="dev-menu-link"> Dev </button>
</li>
</ul>
@ -284,6 +284,10 @@ if (htmlWebpackPlugin.options.googleAnalytics.trackingId) { %>
<!-- React Component -->
</div>
<div id="gang-container" class="generic-menupage-container">
<!-- React Component -->
</div>
<!-- Log Box -->
<div id="log-box-container">
<div id="log-box-content">
@ -375,7 +379,7 @@ if (htmlWebpackPlugin.options.googleAnalytics.trackingId) { %>
<div id="character-overview-text">
<!-- ReactJS Component -->
</div>
<div class="character-quick-options">
<div class="character-quick-options noselect">
<button id="character-overview-save-button" class="character-overview-btn">Save Game</button>
<button id="character-overview-options-button" class="character-overview-btn">Options</button>
</div>

@ -23,27 +23,14 @@ export class Accordion extends React.Component<IProps, IState> {
this.handleHeaderClick = this.handleHeaderClick.bind(this);
this.state = {
panelOpened: props.panelInitiallyOpened ? true : false,
panelOpened: props.panelInitiallyOpened ? props.panelInitiallyOpened : false,
}
}
handleHeaderClick(e: React.MouseEvent<HTMLButtonElement>): void {
const elem = e.currentTarget;
elem.classList.toggle("active");
const panel: HTMLElement = elem.nextElementSibling as HTMLElement;
const active = elem.classList.contains("active");
if (active) {
panel.style.display = "block";
this.setState({
panelOpened: true,
});
} else {
panel.style.display = "none";
this.setState({
panelOpened: false,
});
}
handleHeaderClick(): void {
this.setState({
panelOpened: !this.state.panelOpened,
});
}
render(): React.ReactNode {
@ -52,6 +39,8 @@ export class Accordion extends React.Component<IProps, IState> {
className = this.props.headerClass;
}
if(this.state.panelOpened) className += " active"
return (
<>
<button className={className} onClick={this.handleHeaderClick}>
@ -84,8 +73,9 @@ class AccordionPanel extends React.Component<IPanelProps, any> {
className = this.props.panelClass;
}
return (
<div className={className}>
<div className={className} style={{display: this.props.opened ? "block" : "none"}}>
{this.props.panelContent}
</div>
)

@ -1,4 +1,4 @@
import * as React from "react";
import React, { useState } from 'react';
import { KEY } from "../../../utils/helpers/keyCodes";
import { CodingContract, CodingContractType, CodingContractTypes } from "../../CodingContracts";
@ -12,23 +12,14 @@ type IProps = {
onAttempt: (answer: string) => void;
}
type IState = {
answer: string;
}
export function CodingContractPopup(props: IProps): React.ReactElement {
const [answer, setAnswer] = useState("");
export class CodingContractPopup extends React.Component<IProps, IState>{
constructor(props: IProps) {
super(props);
this.state = { answer: ''};
this.setAnswer = this.setAnswer.bind(this);
this.onInputKeydown = this.onInputKeydown.bind(this);
function onChange(event: React.ChangeEvent<HTMLInputElement>): void {
setAnswer(event.target.value);
}
setAnswer(event: React.ChangeEvent<HTMLInputElement>): void {
this.setState({ answer: event.target.value });
}
onInputKeydown(event: React.KeyboardEvent<HTMLInputElement>): void {
function onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
// React just won't cooperate on this one.
// "React.KeyboardEvent<HTMLInputElement>" seems like the right type but
// whatever ...
@ -36,31 +27,41 @@ export class CodingContractPopup extends React.Component<IProps, IState>{
if (event.keyCode === KEY.ENTER && value !== "") {
event.preventDefault();
this.props.onAttempt(this.state.answer);
props.onAttempt(answer);
} else if (event.keyCode === KEY.ESC) {
event.preventDefault();
this.props.onClose();
props.onClose();
}
}
render(): React.ReactNode {
const contractType: CodingContractType = CodingContractTypes[this.props.c.type];
const description = [];
for (const [i, value] of contractType.desc(this.props.c.data).split('\n').entries())
description.push(<span key={i} dangerouslySetInnerHTML={{__html: value+'<br />'}}></span>);
return (
<div>
<CopyableText value={this.props.c.type} tag={ClickableTag.Tag_h1} />
<br/><br/>
<p>You are attempting to solve a Coding Contract. You have {this.props.c.getMaxNumTries() - this.props.c.tries} tries remaining, after which the contract will self-destruct.</p>
<br/>
<p>{description}</p>
<br/>
<input className="text-input" style={{ width:"50%",marginTop:"8px" }} autoFocus={true} placeholder="Enter Solution here" value={this.state.answer}
onChange={this.setAnswer} onKeyDown={this.onInputKeydown} />
<PopupCloseButton popup={this.props.popupId} onClose={() => this.props.onAttempt(this.state.answer)} text={"Solve"} />
<PopupCloseButton popup={this.props.popupId} onClose={this.props.onClose} text={"Close"} />
</div>
)
}
const contractType: CodingContractType = CodingContractTypes[props.c.type];
const description = [];
for (const [i, value] of contractType.desc(props.c.data).split('\n').entries())
description.push(<span key={i} dangerouslySetInnerHTML={{__html: value+'<br />'}}></span>);
return (
<div>
<CopyableText value={props.c.type} tag={ClickableTag.Tag_h1} />
<br/><br/>
<p>You are attempting to solve a Coding Contract. You have {props.c.getMaxNumTries() - props.c.tries} tries remaining, after which the contract will self-destruct.</p>
<br/>
<p>{description}</p>
<br/>
<input
className="text-input"
style={{ width:"50%",marginTop:"8px" }}
autoFocus={true}
placeholder="Enter Solution here"
value={answer}
onChange={onChange}
onKeyDown={onKeyDown} />
<PopupCloseButton
popup={props.popupId}
onClose={() => props.onAttempt(answer)}
text={"Solve"} />
<PopupCloseButton
popup={props.popupId}
onClose={props.onClose}
text={"Close"} />
</div>
)
}

@ -5,15 +5,13 @@
*/
import * as React from "react";
type ReactComponent = new(...args: any[]) => React.Component<any, any>
interface IProps {
content: ReactComponent;
interface IProps<T> {
content: (props: T) => React.ReactElement;
id: string;
props: any;
props: T;
}
export function Popup(props: IProps): React.ReactElement {
export function Popup<T>(props: IProps<T>): React.ReactElement {
return (
<div className={"popup-box-content"} id={`${props.id}-content`}>
{React.createElement(props.content, props.props)}

@ -14,8 +14,6 @@ import { Popup } from "./Popup";
import { createElement } from "../../../utils/uiHelpers/createElement";
import { removeElementById } from "../../../utils/uiHelpers/removeElementById";
type ReactComponent = new(...args: any[]) => React.Component<any, any>;
let gameContainer: HTMLElement;
function getGameContainer(): void {
@ -31,16 +29,26 @@ function getGameContainer(): void {
document.addEventListener("DOMContentLoaded", getGameContainer);
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function createPopup(id: string, rootComponent: ReactComponent, props: any): HTMLElement | null {
export function createPopup<T>(id: string, rootComponent: (props: T) => React.ReactElement, props: T): HTMLElement | null {
let container = document.getElementById(id);
if (container == null) {
function onClick(this: HTMLElement, event: MouseEvent): any {
if(!event.srcElement) return;
if(!(event.srcElement instanceof HTMLElement)) return;
const clickedId = (event.srcElement as HTMLElement).id;
if(clickedId !== id) return;
removePopup(id);
}
container = createElement("div", {
class: "popup-box-container",
display: "flex",
id: id,
backgroundColor: 'rgba(0,0,0,0.5)',
clickListener: onClick,
});
gameContainer.appendChild(container);
}
ReactDOM.render(<Popup content={rootComponent} id={id} props={props} />, container);
@ -53,9 +61,10 @@ export function createPopup(id: string, rootComponent: ReactComponent, props: an
*/
export function removePopup(id: string): void {
const content = document.getElementById(`${id}`);
if (content == null) { return; }
if (content == null) return;
ReactDOM.unmountComponentAtNode(content);
removeElementById(id);
removeElementById(`${id}-close`);
}

@ -62,6 +62,9 @@ interface ICreateElementStyleOptions {
visibility?: string;
whiteSpace?: string;
width?: string;
height?: string;
top?: string;
left?: string;
}
/**
@ -210,6 +213,15 @@ function setElementStyle(el: HTMLElement, params: ICreateElementStyleOptions): v
if (params.width !== undefined) {
el.style.width = params.width;
}
if (params.height !== undefined) {
el.style.height = params.height;
}
if (params.top !== undefined) {
el.style.top = params.top;
}
if (params.left !== undefined) {
el.style.left = params.left;
}
if (params.backgroundColor !== undefined) {
el.style.backgroundColor = params.backgroundColor;
}