diff --git a/src/Constants.ts b/src/Constants.ts index 1f2fb3b2e..401458e95 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -8,6 +8,9 @@ import { IMap } from "./types"; export const CONSTANTS: IMap = { 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. diff --git a/src/Gang.jsx b/src/Gang.jsx deleted file mode 100644 index a4da530ed..000000000 --- a/src/Gang.jsx +++ /dev/null @@ -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("
")); - } 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(, 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; -} diff --git a/src/Gang/Gang.ts b/src/Gang/Gang.ts new file mode 100644 index 000000000..d0267b8d0 --- /dev/null +++ b/src/Gang/Gang.ts @@ -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("
")); + } 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; diff --git a/src/Gang/GangMember.ts b/src/Gang/GangMember.ts index 84a06914c..78ebfde8c 100644 --- a/src/Gang/GangMember.ts +++ b/src/Gang/GangMember.ts @@ -5,6 +5,7 @@ import { GangMemberUpgrades } from "./GangMemberUpgrades"; 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"; export class GangMember { @@ -92,7 +93,7 @@ export class GangMember { return GangMemberTasks["Unassigned"]; } - calculateRespectGain(gang: any): number { + calculateRespectGain(gang: IGang): number { const task = this.getTask(); if (task.baseRespect === 0) return 0; let statWeight = (task.hackWeight/100) * this.hack + @@ -109,7 +110,7 @@ export class GangMember { return 11 * task.baseRespect * statWeight * territoryMult * respectMult; } - calculateWantedLevelGain(gang: any): number { + calculateWantedLevelGain(gang: IGang): number { const task = this.getTask(); if (task.baseWanted === 0) return 0; 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(); if (task.baseMoney === 0) return 0; let statWeight = (task.hackWeight/100) * this.hack + @@ -164,7 +165,7 @@ export class GangMember { 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); } @@ -239,7 +240,7 @@ export class GangMember { this.cha_mult = 1; for (let i = 0; i < this.augmentations.length; ++i) { const aug = GangMemberUpgrades[this.augmentations[i]]; - aug.apply(this); + this.applyUpgrade(aug); } // 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') { upg = GangMemberUpgrades[upg]; } @@ -276,14 +286,14 @@ export class GangMember { return false; } - if (player.money.lt(upg.getCost(gang))) { return false; } - player.loseMoney(upg.getCost(gang)); + 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); } - upg.apply(this); + this.applyUpgrade(upg); return true; } diff --git a/src/Gang/GangMemberUpgrade.ts b/src/Gang/GangMemberUpgrade.ts index 29fc90af6..7cfd6c291 100644 --- a/src/Gang/GangMemberUpgrade.ts +++ b/src/Gang/GangMemberUpgrade.ts @@ -17,10 +17,6 @@ export class GangMemberUpgrade { this.desc = this.createDescription(); } - getCost(gang: any): number { - return this.cost / gang.getDiscount(); - } - createDescription(): string { const lines = ["Increases:"]; if (this.mults.str != null) { @@ -44,16 +40,6 @@ export class GangMemberUpgrade { return lines.join("
"); } - // 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. getType(): string { switch (this.type) { diff --git a/src/Gang/Helpers.tsx b/src/Gang/Helpers.tsx new file mode 100644 index 000000000..bf7de1029 --- /dev/null +++ b/src/Gang/Helpers.tsx @@ -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(, 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; +} diff --git a/src/Gang/IGang.ts b/src/Gang/IGang.ts new file mode 100644 index 000000000..f1a40f76d --- /dev/null +++ b/src/Gang/IGang.ts @@ -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; +} \ No newline at end of file diff --git a/src/Gang/data/Constants.ts b/src/Gang/data/Constants.ts index e4ab78b09..5975de58b 100644 --- a/src/Gang/data/Constants.ts +++ b/src/Gang/data/Constants.ts @@ -10,7 +10,7 @@ export const GangConstants: { MaximumGangMembers: 30, CyclesPerTerritoryAndPowerUpdate: 100, // Portion of upgrade multiplier that is kept after ascending - AscensionMultiplierRatio: 15, + AscensionMultiplierRatio: .15, // Names of possible Gangs Names: [ "Slum Snakes", diff --git a/src/Gang/ui/GangMemberUpgradePopup.tsx b/src/Gang/ui/GangMemberUpgradePopup.tsx index e9d19d074..999605d6e 100644 --- a/src/Gang/ui/GangMemberUpgradePopup.tsx +++ b/src/Gang/ui/GangMemberUpgradePopup.tsx @@ -25,7 +25,7 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement { for (const upgName in GangMemberUpgrades) { if (GangMemberUpgrades.hasOwnProperty(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; switch (upg.type) { case "w": @@ -63,7 +63,7 @@ function GangMemberUpgradePanel(props: IPanelProps): React.ReactElement { setRerender(old => !old); } return ( - {upg.name} - {Money(upg.getCost(props.gang))} + {upg.name} - {Money(props.gang.getUpgradeCost(upg))} ); } diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index e20b9f191..5cb34f5e9 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -29,7 +29,7 @@ import { calculateWeakenTime, } from "./Hacking"; import { calculateServerGrowth } from "./Server/formulas/grow"; -import { Gang } from "./Gang"; +import { Gang } from "./Gang/Gang"; import { AllGangs } from "./Gang/AllGangs"; import { GangMemberTasks } from "./Gang/GangMemberTasks"; import { GangMemberUpgrades } from "./Gang/GangMemberUpgrades"; @@ -3746,7 +3746,9 @@ function NetscriptFunctions(workerScript) { 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")); @@ -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 { diff --git a/src/PersonObjects/Player/PlayerObjectGangMethods.js b/src/PersonObjects/Player/PlayerObjectGangMethods.js index 1d071066f..82399d79e 100644 --- a/src/PersonObjects/Player/PlayerObjectGangMethods.js +++ b/src/PersonObjects/Player/PlayerObjectGangMethods.js @@ -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"; diff --git a/src/Prestige.js b/src/Prestige.js index a851b3022..d21bbe182 100755 --- a/src/Prestige.js +++ b/src/Prestige.js @@ -14,6 +14,7 @@ import { Engine } from "./engine"; import { Faction } from "./Faction/Faction"; import { Factions, initFactions } from "./Faction/Factions"; import { joinFaction } from "./Faction/FactionHelpers"; +import { clearGangUI } from "./Gang/Helpers"; import { updateHashManagerCapacity } from "./Hacknet/HacknetHelpers"; import { initMessages } from "./Message/MessageHelpers"; import { prestigeWorkerScripts } from "./NetscriptWorker"; @@ -333,7 +334,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; diff --git a/src/engine.jsx b/src/engine.jsx index c0cb90a1e..ab6181941 100644 --- a/src/engine.jsx +++ b/src/engine.jsx @@ -32,6 +32,7 @@ import { processPassiveFactionRepGain, inviteToFaction, } from "./Faction/FactionHelpers"; +import { displayGangContent, clearGangUI } from "./Gang/Helpers"; import { displayInfiltrationContent } from "./Infiltration/Helper"; import { getHackingWorkRepGain, @@ -439,7 +440,7 @@ const Engine = { loadGangContent: function() { Engine.hideAllContent(); if (document.getElementById("gang-container") || Player.inGang()) { - Player.gang.displayGangContent(Player); + displayGangContent(this, Player.gang, Player); routing.navigateTo(Page.Gang); } else { Engine.loadTerminalContent(); @@ -534,7 +535,7 @@ const Engine = { } if (Player.inGang()) { - Player.gang.clearUI(); + clearGangUI(); } if (Player.corporation instanceof Corporation) { Player.corporation.clearUI();