converted everything to ts

This commit is contained in:
Olivier Gagnon 2021-06-16 18:38:29 -04:00
parent 43d0fcb9f9
commit 07cca48a17
13 changed files with 585 additions and 501 deletions

@ -8,6 +8,9 @@ import { IMap } from "./types";
export const CONSTANTS: IMap<any> = { export const CONSTANTS: IMap<any> = {
Version: "0.52.2", 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 /** 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 * 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. * the player will have this level assuming no multipliers. Multipliers can cause skills to go above this.

@ -1,468 +0,0 @@
/**
* TODO
* Add police clashes
* balance point to keep them from running out of control
*/
import { Engine } from "./engine";
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 { createElement } from "../utils/uiHelpers/createElement";
import { removeElement } from "../utils/uiHelpers/removeElement";
import { GangMemberUpgrades } from "./Gang/GangMemberUpgrades";
import { GangConstants } from "./Gang/data/Constants";
import { GangMemberTasks } from "./Gang/GangMemberTasks";
import { AllGangs } from "./Gang/AllGangs";
import { Root } from "./Gang/ui/Root";
import { GangMember } from "./Gang/GangMember";
import React from "react";
import ReactDOM from "react-dom";
/**
* @param facName {string} Name of corresponding faction
* @param hacking {bollean} Whether or not its a hacking gang
*/
export function Gang(facName, hacking=false) {
this.facName = facName;
this.members = [];
this.wanted = 1;
this.respect = 1;
this.isHackingGang = hacking;
this.respectGainRate = 0;
this.wantedGainRate = 0;
this.moneyGainRate = 0;
// When processing gains, this stores the number of cycles until some
// limit is reached, and then calculates and applies the gains only at that limit
this.storedCycles = 0;
// Separate variable to keep track of cycles for Territry + Power gang, which
// happens on a slower "clock" than normal processing
this.storedTerritoryAndPowerCycles = 0;
this.territoryClashChance = 0;
this.territoryWarfareEngaged = false;
this.notifyMemberDeath = true;
}
Gang.prototype.getPower = function() {
return AllGangs[this.facName].power;
}
Gang.prototype.getTerritory = function() {
return AllGangs[this.facName].territory;
}
Gang.prototype.process = function(numCycles=1, player) {
const CyclesPerSecond = 1000 / Engine._idleSpeed;
if (isNaN(numCycles)) {
console.error(`NaN passed into Gang.process(): ${numCycles}`);
}
this.storedCycles += numCycles;
// Only process if there are at least 2 seconds, and at most 5 seconds
if (this.storedCycles < 2 * CyclesPerSecond) { return; }
const cycles = Math.min(this.storedCycles, 5 * CyclesPerSecond);
try {
this.processGains(cycles, player);
this.processExperienceGains(cycles);
this.processTerritoryAndPowerGains(cycles);
this.storedCycles -= cycles;
} catch(e) {
exceptionAlert(`Exception caught when processing Gang: ${e}`);
}
}
Gang.prototype.processGains = function(numCycles=1, player) {
// Get gains per cycle
let moneyGains = 0, respectGains = 0, 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;
if (typeof respectGains === "number") {
const gain = respectGains * numCycles;
this.respect += gain;
// Faction reputation gains is respect gain divided by some constant
const fac = Factions[this.facName];
if (!(fac instanceof Faction)) {
dialogBoxCreate("ERROR: Could not get Faction associates with your gang. This is a bug, please report to game dev");
} else {
let 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);
}
} else {
console.warn("respectGains calculated to be NaN");
}
if (typeof wantedLevelGains === "number") {
if (this.wanted === 1 && wantedLevelGains < 0) {
// At minimum wanted, do nothing
} else {
const oldWanted = this.wanted;
let newWanted = oldWanted + (wantedLevelGains * numCycles);
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;}
}
} else {
console.warn("ERROR: wantedLevelGains is NaN");
}
if (typeof moneyGains === "number") {
player.gainMoney(moneyGains * numCycles);
player.recordMoneySource(moneyGains * numCycles, "gang");
} else {
console.warn("ERROR: respectGains is NaN");
}
}
Gang.prototype.processTerritoryAndPowerGains = function(numCycles=1) {
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);
if (Math.random() < thisChance) {
if (AllGangs[otherGang].territory <= 0) {
return;
}
const territoryGain = calculateTerritoryGain(thisGang, otherGang);
AllGangs[thisGang].territory += territoryGain;
AllGangs[otherGang].territory -= territoryGain;
if (thisGang === gangName) {
this.clash(true); // Player won
AllGangs[otherGang].power *= (1 / 1.01);
} else if (otherGang === gangName) {
this.clash(false); // Player lost
} else {
AllGangs[otherGang].power *= (1 / 1.01);
}
} else {
if (AllGangs[thisGang].territory <= 0) {
return;
}
const territoryGain = calculateTerritoryGain(otherGang, thisGang);
AllGangs[thisGang].territory -= territoryGain;
AllGangs[otherGang].territory += territoryGain;
if (thisGang === gangName) {
this.clash(false); // Player lost
} else if (otherGang === gangName) {
this.clash(true); // Player won
AllGangs[thisGang].power *= (1 / 1.01);
} else {
AllGangs[thisGang].power *= (1 / 1.01);
}
}
}
}
Gang.prototype.canRecruitMember = function() {
if (this.members.length >= GangConstants.MaximumGangMembers) { return false; }
return (this.respect >= this.getRespectNeededToRecruitMember());
}
Gang.prototype.getRespectNeededToRecruitMember = function() {
// First N gang members are free (can be recruited at 0 respect)
const numFreeMembers = 3;
if (this.members.length < numFreeMembers) { return 0; }
const i = this.members.length - (numFreeMembers - 1);
return Math.round(0.9 * Math.pow(i, 3) + Math.pow(i, 2));
}
Gang.prototype.recruitMember = function(name) {
name = String(name);
if (name === "" || !this.canRecruitMember()) { return false; }
// Check for already-existing names
let sameNames = this.members.filter((m) => {
return m.name === name;
});
if (sameNames.length >= 1) { return false; }
let member = new GangMember(name);
this.members.push(member);
return true;
}
// Money and Respect gains multiplied by this number (< 1)
Gang.prototype.getWantedPenalty = function() {
return (this.respect) / (this.respect + this.wanted);
}
Gang.prototype.processExperienceGains = function(numCycles=1) {
for (var i = 0; i < this.members.length; ++i) {
this.members[i].gainExperience(numCycles);
this.members[i].updateSkillLevels();
}
}
//Calculates power GAIN, which is added onto the Gang's existing power
Gang.prototype.calculatePower = function() {
var memberTotal = 0;
for (var i = 0; i < this.members.length; ++i) {
if (GangMemberTasks.hasOwnProperty(this.members[i].task) && this.members[i].task == "Territory Warfare") {
const gain = this.members[i].calculatePower();
memberTotal += gain;
}
}
return (0.015 * this.getTerritory() * memberTotal);
}
Gang.prototype.clash = function(won=false) {
// Determine if a gang member should die
let baseDeathChance = 0.01;
if (won) { baseDeathChance /= 2; }
// If the clash was lost, the player loses a small percentage of power
if (!won) {
AllGangs[this.facName].power *= (1 / 1.008);
}
// Deaths can only occur during X% of clashes
if (Math.random() < 0.65) { return; }
for (let i = this.members.length - 1; i >= 0; --i) {
const member = this.members[i];
// Only members assigned to Territory Warfare can die
if (member.task !== "Territory Warfare") { continue; }
// Chance to die is decreased based on defense
const modifiedDeathChance = baseDeathChance / Math.pow(member.def, 0.6);
if (Math.random() < modifiedDeathChance) {
this.killMember(member);
}
}
}
Gang.prototype.killMember = function(memberObj) {
// Player loses a percentage of total respect, plus whatever respect that member has earned
const totalRespect = this.respect;
const lostRespect = (0.05 * totalRespect) + memberObj.earnedRespect;
this.respect = Math.max(0, totalRespect - lostRespect);
for (let i = 0; i < this.members.length; ++i) {
if (memberObj.name === this.members[i].name) {
this.members.splice(i, 1);
break;
}
}
// Notify of death
if (this.notifyMemberDeath) {
dialogBoxCreate(`${memberObj.name} was killed in a gang clash! You lost ${lostRespect} respect`);
}
}
Gang.prototype.ascendMember = function(memberObj, workerScript) {
try {
/**
* res is an object with the following format:
* {
* respect: Amount of respect to deduct
* hack/str/def/dex/agi/cha: Ascension multipliers gained for each stat
* }
*/
const res = memberObj.ascend();
this.respect = Math.max(1, this.respect - res.respect);
if (workerScript == null) {
dialogBoxCreate([`You ascended ${memberObj.name}!`,
"",
`Your gang lost ${numeralWrapper.formatRespect(res.respect)} respect`,
"",
`${memberObj.name} gained the following stat multipliers for ascending:`,
`Hacking: ${numeralWrapper.formatPercentage(res.hack, 3)}`,
`Strength: ${numeralWrapper.formatPercentage(res.str, 3)}`,
`Defense: ${numeralWrapper.formatPercentage(res.def, 3)}`,
`Dexterity: ${numeralWrapper.formatPercentage(res.dex, 3)}`,
`Agility: ${numeralWrapper.formatPercentage(res.agi, 3)}`,
`Charisma: ${numeralWrapper.formatPercentage(res.cha, 3)}`].join("<br>"));
} else {
workerScript.log(`Ascended Gang member ${memberObj.name}`);
}
return res;
} catch(e) {
if (workerScript == null) {
exceptionAlert(e);
} else {
throw e; // Re-throw, will be caught in the Netscript Function
}
}
}
// Cost of upgrade gets cheaper as gang increases in respect + power
Gang.prototype.getDiscount = function() {
const power = this.getPower();
const respect = this.respect;
const respectLinearFac = 5e6;
const powerLinearFac = 1e6;
const discount = Math.pow(respect, 0.01) + respect / respectLinearFac + Math.pow(power, 0.01) + power / powerLinearFac - 1;
return Math.max(1, discount);
}
// Returns only valid tasks for this gang. Excludes 'Unassigned'
Gang.prototype.getAllTaskNames = function() {
let tasks = [];
const allTasks = Object.keys(GangMemberTasks);
if (this.isHackingGang) {
tasks = allTasks.filter((e) => {
let task = GangMemberTasks[e];
if (task == null) { return false; }
if (e === "Unassigned") { return false; }
return task.isHacking;
});
} else {
tasks = allTasks.filter((e) => {
let task = GangMemberTasks[e];
if (task == null) { return false; }
if (e === "Unassigned") { return false; }
return task.isCombat;
});
}
return tasks;
}
Gang.prototype.getUpgradeCost = function(upgName) {
if (GangMemberUpgrades[upgName] == null) { return Infinity; }
return GangMemberUpgrades[upgName].getCost(this);
}
Gang.prototype.toJSON = function() {
return Generic_toJSON("Gang", this);
}
Gang.fromJSON = function(value) {
return Generic_fromJSON(Gang, value.data);
}
Reviver.constructors.Gang = Gang;
function calculateTerritoryGain(winGang, loseGang) {
const powerBonus = Math.max(1, 1+Math.log(AllGangs[winGang].power/AllGangs[loseGang].power)/Math.log(50));
const gains = Math.min(AllGangs[loseGang].territory, powerBonus*0.0001*(Math.random()+.5))
return gains;
}
// Gang UI Dom Elements
const UIElems = {
gangContentCreated: false,
gangContainer: null,
}
Gang.prototype.displayGangContent = function(player) {
if (!UIElems.gangContentCreated || UIElems.gangContainer == null) {
UIElems.gangContentCreated = true;
// Create gang container
UIElems.gangContainer = createElement("div", {
id:"gang-container", class:"generic-menupage-container",
});
ReactDOM.render(<Root engine={Engine} gang={this} player={player} />, UIElems.gangContainer);
document.getElementById("entire-game-container").appendChild(UIElems.gangContainer);
}
UIElems.gangContainer.style.display = "block";
}
Gang.prototype.clearUI = function() {
if (UIElems.gangContainer instanceof Element) { removeElement(UIElems.gangContainer); }
for (const prop in UIElems) {
UIElems[prop] = null;
}
UIElems.gangContentCreated = false;
}

463
src/Gang/Gang.ts Normal file

@ -0,0 +1,463 @@
/**
* 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 { 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: string = "", hacking: boolean = 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: number = 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: number = 1, player: IPlayer): void {
// Get gains per cycle
let moneyGains = 0, respectGains = 0, 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;
if (typeof respectGains === "number") {
const gain = respectGains * numCycles;
this.respect += gain;
// Faction reputation gains is respect gain divided by some constant
const fac = Factions[this.facName];
if (!(fac instanceof Faction)) {
dialogBoxCreate("ERROR: Could not get Faction associates with your gang. This is a bug, please report to game dev");
} else {
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);
}
} else {
console.warn("respectGains calculated to be NaN");
}
if (typeof wantedLevelGains === "number") {
if (this.wanted === 1 && wantedLevelGains < 0) {
// At minimum wanted, do nothing
} else {
const oldWanted = this.wanted;
let newWanted = oldWanted + (wantedLevelGains * numCycles);
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;}
}
} else {
console.warn("ERROR: wantedLevelGains is NaN");
}
if (typeof moneyGains === "number") {
player.gainMoney(moneyGains * numCycles);
player.recordMoneySource(moneyGains * numCycles, "gang");
} else {
console.warn("ERROR: respectGains is NaN");
}
}
processTerritoryAndPowerGains(numCycles: number = 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: number = 1): void {
for (let i = 0; i < this.members.length; ++i) {
this.members[i].gainExperience(numCycles);
this.members[i].updateSkillLevels();
}
}
clash(won: boolean = 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
if (!won) {
AllGangs[this.facName].power *= (1 / 1.008);
}
// Deaths can only occur during X% of clashes
if (Math.random() < 0.65) { return; }
for (let i = this.members.length - 1; i >= 0; --i) {
const member = this.members[i];
// Only members assigned to Territory Warfare can die
if (member.task !== "Territory Warfare") { continue; }
// Chance to die is decreased based on defense
const modifiedDeathChance = baseDeathChance / Math.pow(member.def, 0.6);
if (Math.random() < modifiedDeathChance) {
this.killMember(member);
}
}
}
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.round(0.9 * Math.pow(i, 3) + Math.pow(i, 2));
}
recruitMember(name: string): boolean {
name = String(name);
if (name === "" || !this.canRecruitMember()) { return false; }
// Check for already-existing names
const sameNames = this.members.filter((m) => {
return 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") {
const gain = this.members[i].calculatePower();
memberTotal += gain;
}
}
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): void {
try {
// res is an object with the following format:
// {
// respect: Amount of respect to deduct
// hack/str/def/dex/agi/cha: Ascension multipliers gained for each stat
// }
const res = member.ascend();
this.respect = Math.max(1, this.respect - res.respect);
if (workerScript == null) {
dialogBoxCreate([`You ascended ${member.name}!`,
"",
`Your gang lost ${numeralWrapper.formatRespect(res.respect)} respect`,
"",
`${member.name} gained the following stat multipliers for ascending:`,
`Hacking: ${numeralWrapper.formatPercentage(res.hack, 3)}`,
`Strength: ${numeralWrapper.formatPercentage(res.str, 3)}`,
`Defense: ${numeralWrapper.formatPercentage(res.def, 3)}`,
`Dexterity: ${numeralWrapper.formatPercentage(res.dex, 3)}`,
`Agility: ${numeralWrapper.formatPercentage(res.agi, 3)}`,
`Charisma: ${numeralWrapper.formatPercentage(res.cha, 3)}`].join("<br>"));
} else {
workerScript.log('ascend', `Ascended Gang member ${member.name}`);
}
return res;
} catch(e) {
if (workerScript == null) {
exceptionAlert(e);
} else {
throw e; // Re-throw, will be caught in the Netscript Function
}
}
}
// Cost of upgrade gets cheaper as gang increases in respect + power
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[] {
let tasks = [];
const allTasks = Object.keys(GangMemberTasks);
if (this.isHackingGang) {
tasks = allTasks.filter((e) => {
const task = GangMemberTasks[e];
if (task == null) { return false; }
if (e === "Unassigned") { return false; }
return task.isHacking;
});
} else {
tasks = allTasks.filter((e) => {
const task = GangMemberTasks[e];
if (task == null) { return false; }
if (e === "Unassigned") { return false; }
return task.isCombat;
});
}
return tasks;
}
getUpgradeCost(upg: GangMemberUpgrade): 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;

@ -5,6 +5,7 @@ import { GangMemberUpgrades } from "./GangMemberUpgrades";
import { IPlayer } from "../PersonObjects/IPlayer"; import { IPlayer } from "../PersonObjects/IPlayer";
import { GangConstants } from "./data/Constants"; import { GangConstants } from "./data/Constants";
import { AllGangs } from "./AllGangs"; import { AllGangs } from "./AllGangs";
import { IGang } from "./IGang";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver"; import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver";
export class GangMember { export class GangMember {
@ -92,7 +93,7 @@ export class GangMember {
return GangMemberTasks["Unassigned"]; return GangMemberTasks["Unassigned"];
} }
calculateRespectGain(gang: any): number { calculateRespectGain(gang: IGang): number {
const task = this.getTask(); const task = this.getTask();
if (task.baseRespect === 0) return 0; if (task.baseRespect === 0) return 0;
let statWeight = (task.hackWeight/100) * this.hack + let statWeight = (task.hackWeight/100) * this.hack +
@ -109,7 +110,7 @@ export class GangMember {
return 11 * task.baseRespect * statWeight * territoryMult * respectMult; return 11 * task.baseRespect * statWeight * territoryMult * respectMult;
} }
calculateWantedLevelGain(gang: any): number { calculateWantedLevelGain(gang: IGang): number {
const task = this.getTask(); const task = this.getTask();
if (task.baseWanted === 0) return 0; if (task.baseWanted === 0) return 0;
let statWeight = (task.hackWeight / 100) * this.hack + let statWeight = (task.hackWeight / 100) * this.hack +
@ -133,7 +134,7 @@ export class GangMember {
} }
} }
calculateMoneyGain(gang: any): number { calculateMoneyGain(gang: IGang): number {
const task = this.getTask(); const task = this.getTask();
if (task.baseMoney === 0) return 0; if (task.baseMoney === 0) return 0;
let statWeight = (task.hackWeight/100) * this.hack + let statWeight = (task.hackWeight/100) * this.hack +
@ -164,7 +165,7 @@ export class GangMember {
this.cha_exp += (task.chaWeight / weightDivisor) * difficultyPerCycles; this.cha_exp += (task.chaWeight / weightDivisor) * difficultyPerCycles;
} }
recordEarnedRespect(numCycles = 1, gang: any): void { recordEarnedRespect(numCycles = 1, gang: IGang): void {
this.earnedRespect += (this.calculateRespectGain(gang) * numCycles); this.earnedRespect += (this.calculateRespectGain(gang) * numCycles);
} }
@ -239,7 +240,7 @@ export class GangMember {
this.cha_mult = 1; this.cha_mult = 1;
for (let i = 0; i < this.augmentations.length; ++i) { for (let i = 0; i < this.augmentations.length; ++i) {
const aug = GangMemberUpgrades[this.augmentations[i]]; const aug = GangMemberUpgrades[this.augmentations[i]];
aug.apply(this); this.applyUpgrade(aug);
} }
// Clear exp and recalculate stats // Clear exp and recalculate stats
@ -264,7 +265,16 @@ export class GangMember {
}; };
} }
buyUpgrade(upg: GangMemberUpgrade, player: IPlayer, gang: any): boolean { 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 {
if (typeof upg === 'string') { if (typeof upg === 'string') {
upg = GangMemberUpgrades[upg]; upg = GangMemberUpgrades[upg];
} }
@ -276,14 +286,14 @@ export class GangMember {
return false; return false;
} }
if (player.money.lt(upg.getCost(gang))) { return false; } if (player.money.lt(gang.getUpgradeCost(upg))) { return false; }
player.loseMoney(upg.getCost(gang)); player.loseMoney(gang.getUpgradeCost(upg));
if (upg.type === "g") { if (upg.type === "g") {
this.augmentations.push(upg.name); this.augmentations.push(upg.name);
} else { } else {
this.upgrades.push(upg.name); this.upgrades.push(upg.name);
} }
upg.apply(this); this.applyUpgrade(upg);
return true; return true;
} }

@ -17,10 +17,6 @@ export class GangMemberUpgrade {
this.desc = this.createDescription(); this.desc = this.createDescription();
} }
getCost(gang: any): number {
return this.cost / gang.getDiscount();
}
createDescription(): string { createDescription(): string {
const lines = ["Increases:"]; const lines = ["Increases:"];
if (this.mults.str != null) { if (this.mults.str != null) {
@ -44,16 +40,6 @@ export class GangMemberUpgrade {
return lines.join("<br>"); return lines.join("<br>");
} }
// Passes in a GangMember object
apply(member: any): void {
if (this.mults.str != null) { member.str_mult *= this.mults.str; }
if (this.mults.def != null) { member.def_mult *= this.mults.def; }
if (this.mults.dex != null) { member.dex_mult *= this.mults.dex; }
if (this.mults.agi != null) { member.agi_mult *= this.mults.agi; }
if (this.mults.cha != null) { member.cha_mult *= this.mults.cha; }
if (this.mults.hack != null) { member.hack_mult *= this.mults.hack; }
}
// User friendly version of type. // User friendly version of type.
getType(): string { getType(): string {
switch (this.type) { switch (this.type) {

40
src/Gang/Helpers.tsx Normal file

@ -0,0 +1,40 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import { createElement } from "../../utils/uiHelpers/createElement";
import { IPlayer } from "../PersonObjects/IPlayer";
import { IEngine } from "../IEngine";
import { Root } from "./ui/Root";
import { Gang } from "./Gang";
// Gang UI Dom Elements
const UIElems: {
gangContentCreated: boolean;
gangContainer: HTMLElement | null;
} = {
gangContentCreated: false,
gangContainer: null,
}
export function displayGangContent(engine: IEngine, gang: Gang, player: IPlayer): void {
if (!UIElems.gangContentCreated || UIElems.gangContainer == null) {
UIElems.gangContentCreated = true;
// Create gang container
UIElems.gangContainer = createElement("div", {
id:"gang-container", class:"generic-menupage-container",
});
ReactDOM.render(<Root engine={engine} gang={gang} player={player} />, UIElems.gangContainer);
const container = document.getElementById("entire-game-container");
if(!container) throw new Error('entire-game-container was null');
container.appendChild(UIElems.gangContainer);
}
if(UIElems.gangContainer) UIElems.gangContainer.style.display = "block";
}
export function clearGangUI(): void {
if (UIElems.gangContainer instanceof Element) ReactDOM.unmountComponentAtNode(UIElems.gangContainer);
UIElems.gangContainer = null;
UIElems.gangContentCreated = false;
}

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;
}

@ -10,7 +10,7 @@ export const GangConstants: {
MaximumGangMembers: 30, MaximumGangMembers: 30,
CyclesPerTerritoryAndPowerUpdate: 100, CyclesPerTerritoryAndPowerUpdate: 100,
// Portion of upgrade multiplier that is kept after ascending // Portion of upgrade multiplier that is kept after ascending
AscensionMultiplierRatio: 15, AscensionMultiplierRatio: .15,
// Names of possible Gangs // Names of possible Gangs
Names: [ Names: [
"Slum Snakes", "Slum Snakes",

@ -25,7 +25,7 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
for (const upgName in GangMemberUpgrades) { for (const upgName in GangMemberUpgrades) {
if (GangMemberUpgrades.hasOwnProperty(upgName)) { if (GangMemberUpgrades.hasOwnProperty(upgName)) {
const upg = GangMemberUpgrades[upgName]; const upg = GangMemberUpgrades[upgName];
if (props.player.money.lt(upg.getCost(props.gang))) continue; if (props.player.money.lt(props.gang.getUpgradeCost(upg))) continue;
if (props.member.upgrades.includes(upgName) || props.member.augmentations.includes(upgName)) continue; if (props.member.upgrades.includes(upgName) || props.member.augmentations.includes(upgName)) continue;
switch (upg.type) { switch (upg.type) {
case "w": case "w":
@ -63,7 +63,7 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement {
setRerender(old => !old); setRerender(old => !old);
} }
return (<a key={upg.name} className="a-link-button tooltip" style={{margin:"2px", padding:"2px", display:"block", fontSize:"11px"}} onClick={onClick}> return (<a key={upg.name} className="a-link-button tooltip" style={{margin:"2px", padding:"2px", display:"block", fontSize:"11px"}} onClick={onClick}>
{upg.name} - {Money(upg.getCost(props.gang))} {upg.name} - {Money(props.gang.getUpgradeCost(upg))}
<span className={left?"tooltiptextleft":"tooltiptext"} dangerouslySetInnerHTML={{__html: upg.desc}} /> <span className={left?"tooltiptextleft":"tooltiptext"} dangerouslySetInnerHTML={{__html: upg.desc}} />
</a>); </a>);
} }

@ -29,7 +29,7 @@ import {
calculateWeakenTime, calculateWeakenTime,
} from "./Hacking"; } from "./Hacking";
import { calculateServerGrowth } from "./Server/formulas/grow"; import { calculateServerGrowth } from "./Server/formulas/grow";
import { Gang } from "./Gang"; import { Gang } from "./Gang/Gang";
import { AllGangs } from "./Gang/AllGangs"; import { AllGangs } from "./Gang/AllGangs";
import { GangMemberTasks } from "./Gang/GangMemberTasks"; import { GangMemberTasks } from "./Gang/GangMemberTasks";
import { GangMemberUpgrades } from "./Gang/GangMemberUpgrades"; import { GangMemberUpgrades } from "./Gang/GangMemberUpgrades";
@ -3746,7 +3746,9 @@ function NetscriptFunctions(workerScript) {
getEquipmentCost: function(equipName) { getEquipmentCost: function(equipName) {
updateDynamicRam("getEquipmentCost", getRamCost("gang", "getEquipmentCost")); updateDynamicRam("getEquipmentCost", getRamCost("gang", "getEquipmentCost"));
checkGangApiAccess("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) { getEquipmentType: function(equipName) {
updateDynamicRam("getEquipmentType", getRamCost("gang", "getEquipmentType")); updateDynamicRam("getEquipmentType", getRamCost("gang", "getEquipmentType"));
@ -3768,7 +3770,9 @@ function NetscriptFunctions(workerScript) {
updateDynamicRam("purchaseEquipment", getRamCost("gang", "purchaseEquipment")); updateDynamicRam("purchaseEquipment", getRamCost("gang", "purchaseEquipment"));
checkGangApiAccess("purchaseEquipment"); checkGangApiAccess("purchaseEquipment");
const member = getGangMember("purchaseEquipment", memberName); 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) { if (res) {
workerScript.log("purchaseEquipment", `Purchased '${equipName}' for Gang member '${memberName}'`); workerScript.log("purchaseEquipment", `Purchased '${equipName}' for Gang member '${memberName}'`);
} else { } else {

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

@ -14,6 +14,7 @@ import { Engine } from "./engine";
import { Faction } from "./Faction/Faction"; import { Faction } from "./Faction/Faction";
import { Factions, initFactions } from "./Faction/Factions"; import { Factions, initFactions } from "./Faction/Factions";
import { joinFaction } from "./Faction/FactionHelpers"; import { joinFaction } from "./Faction/FactionHelpers";
import { clearGangUI } from "./Gang/Helpers";
import { updateHashManagerCapacity } from "./Hacknet/HacknetHelpers"; import { updateHashManagerCapacity } from "./Hacknet/HacknetHelpers";
import { initMessages } from "./Message/MessageHelpers"; import { initMessages } from "./Message/MessageHelpers";
import { prestigeWorkerScripts } from "./NetscriptWorker"; import { prestigeWorkerScripts } from "./NetscriptWorker";
@ -333,7 +334,7 @@ function prestigeSourceFile(flume) {
deleteStockMarket(); deleteStockMarket();
} }
if (Player.inGang()) { Player.gang.clearUI(); } if (Player.inGang()) clearGangUI();
Player.gang = null; Player.gang = null;
Player.corporation = null; resetIndustryResearchTrees(); Player.corporation = null; resetIndustryResearchTrees();
Player.bladeburner = null; Player.bladeburner = null;

@ -32,6 +32,7 @@ import {
processPassiveFactionRepGain, processPassiveFactionRepGain,
inviteToFaction, inviteToFaction,
} from "./Faction/FactionHelpers"; } from "./Faction/FactionHelpers";
import { displayGangContent, clearGangUI } from "./Gang/Helpers";
import { displayInfiltrationContent } from "./Infiltration/Helper"; import { displayInfiltrationContent } from "./Infiltration/Helper";
import { import {
getHackingWorkRepGain, getHackingWorkRepGain,
@ -439,7 +440,7 @@ const Engine = {
loadGangContent: function() { loadGangContent: function() {
Engine.hideAllContent(); Engine.hideAllContent();
if (document.getElementById("gang-container") || Player.inGang()) { if (document.getElementById("gang-container") || Player.inGang()) {
Player.gang.displayGangContent(Player); displayGangContent(this, Player.gang, Player);
routing.navigateTo(Page.Gang); routing.navigateTo(Page.Gang);
} else { } else {
Engine.loadTerminalContent(); Engine.loadTerminalContent();
@ -534,7 +535,7 @@ const Engine = {
} }
if (Player.inGang()) { if (Player.inGang()) {
Player.gang.clearUI(); clearGangUI();
} }
if (Player.corporation instanceof Corporation) { if (Player.corporation instanceof Corporation) {
Player.corporation.clearUI(); Player.corporation.clearUI();