bitburner-src/src/Gang/Gang.ts

426 lines
16 KiB
TypeScript
Raw Normal View History

2021-06-17 00:38:29 +02:00
/**
* 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";
2021-06-17 00:38:29 +02:00
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) {
2021-06-17 00:38:29 +02:00
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 {
2021-06-17 00:38:29 +02:00
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
2021-06-18 00:59:45 +02:00
if (this.storedCycles < 2 * CyclesPerSecond) return;
2021-06-17 00:38:29 +02:00
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 {
2021-06-17 00:38:29 +02:00
// Get gains per cycle
2021-06-18 00:59:45 +02:00
let moneyGains = 0;
let respectGains = 0;
let wantedLevelGains = 0;
2021-06-17 00:38:29 +02:00
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;
2021-06-18 00:59:45 +02:00
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.');
2021-06-17 00:38:29 +02:00
}
2021-06-18 00:59:45 +02:00
const favorMult = 1 + (fac.favor / 100);
fac.playerReputation += ((player.faction_rep_mult * gain * favorMult) / GangConstants.GangRespectToReputationRatio);
2021-06-17 00:38:29 +02:00
2021-06-18 00:59:45 +02:00
// Keep track of respect gained per member
for (let i = 0; i < this.members.length; ++i) {
this.members[i].recordEarnedRespect(numCycles, this);
2021-06-17 00:38:29 +02:00
}
2021-06-18 00:59:45 +02:00
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;
2021-06-17 00:38:29 +02:00
}
2021-06-18 00:59:45 +02:00
player.gainMoney(moneyGains * numCycles);
player.recordMoneySource(moneyGains * numCycles, "gang");
2021-06-17 00:38:29 +02:00
}
processTerritoryAndPowerGains(numCycles = 1): void {
2021-06-17 00:38:29 +02:00
this.storedTerritoryAndPowerCycles += numCycles;
2021-06-18 00:59:45 +02:00
if (this.storedTerritoryAndPowerCycles < GangConstants.CyclesPerTerritoryAndPowerUpdate) return;
2021-06-17 00:38:29 +02:00
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) {
2021-06-18 00:59:45 +02:00
if (!(Math.random() < this.territoryClashChance)) continue;
2021-06-17 00:38:29 +02:00
}
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) {
2021-06-18 00:59:45 +02:00
if (AllGangs[otherGang].territory <= 0) return;
2021-06-17 00:38:29 +02:00
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 {
2021-06-18 00:59:45 +02:00
if (AllGangs[thisGang].territory <= 0) return;
2021-06-17 00:38:29 +02:00
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 {
2021-06-17 00:38:29 +02:00
for (let i = 0; i < this.members.length; ++i) {
this.members[i].gainExperience(numCycles);
this.members[i].updateSkillLevels();
}
}
clash(won = false): void {
2021-06-17 00:38:29 +02:00
// Determine if a gang member should die
let baseDeathChance = 0.01;
2021-06-18 00:59:45 +02:00
if (won) baseDeathChance /= 2;
2021-06-17 00:38:29 +02:00
// If the clash was lost, the player loses a small percentage of power
2021-06-18 00:59:45 +02:00
else AllGangs[this.facName].power *= (1 / 1.008);
2021-06-17 00:38:29 +02:00
// Deaths can only occur during X% of clashes
2021-06-18 00:59:45 +02:00
if (Math.random() < 0.65) return;
2021-06-17 00:38:29 +02:00
for (let i = this.members.length - 1; i >= 0; --i) {
const member = this.members[i];
// Only members assigned to Territory Warfare can die
2021-06-18 00:59:45 +02:00
if (member.task !== "Territory Warfare") continue;
2021-06-17 00:38:29 +02:00
// 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 {
2021-06-18 00:59:45 +02:00
if (this.members.length >= GangConstants.MaximumGangMembers) return false;
2021-06-17 00:38:29 +02:00
return (this.respect >= this.getRespectNeededToRecruitMember());
}
getRespectNeededToRecruitMember(): number {
// First N gang members are free (can be recruited at 0 respect)
const numFreeMembers = 3;
2021-06-18 00:59:45 +02:00
if (this.members.length < numFreeMembers) return 0;
2021-06-17 00:38:29 +02:00
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);
2021-06-18 00:59:45 +02:00
if (name === "" || !this.canRecruitMember()) return false;
2021-06-17 00:38:29 +02:00
// Check for already-existing names
2021-06-18 00:59:45 +02:00
const sameNames = this.members.filter((m) => m.name === name);
if (sameNames.length >= 1) return false;
2021-06-17 00:38:29 +02:00
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) {
2021-06-18 00:59:45 +02:00
if (!GangMemberTasks.hasOwnProperty(this.members[i].task) ||
this.members[i].task !== "Territory Warfare") continue;
memberTotal += this.members[i].calculatePower();
2021-06-17 00:38:29 +02:00
}
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 {
2021-06-17 00:38:29 +02:00
try {
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);
}
throw e; // Re-throw, will be caught in the Netscript Function
2021-06-17 00:38:29 +02:00
}
}
// 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;
2021-06-18 00:59:45 +02:00
const discount = Math.pow(respect, 0.01) +
respect / respectLinearFac +
Math.pow(power, 0.01) +
power / powerLinearFac - 1;
2021-06-17 00:38:29 +02:00
return Math.max(1, discount);
}
// Returns only valid tasks for this gang. Excludes 'Unassigned'
getAllTaskNames(): string[] {
2021-06-18 00:59:45 +02:00
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;
});
2021-06-17 00:38:29 +02:00
}
2021-06-18 00:59:45 +02:00
getUpgradeCost(upg: GangMemberUpgrade | null): number {
2021-06-17 00:38:29 +02:00
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;