mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-10-23 02:03:14 +02:00
417 lines
14 KiB
TypeScript
417 lines
14 KiB
TypeScript
/**
|
|
* 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 { dialogBoxCreate } from "../ui/React/DialogBox";
|
|
import { Reviver, Generic_toJSON, Generic_fromJSON } from "../utils/JSONReviver";
|
|
|
|
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
|
|
import { getRandomInt } from "../utils/helpers/getRandomInt";
|
|
|
|
import { GangMemberUpgrade } from "./GangMemberUpgrade";
|
|
import { GangConstants } from "./data/Constants";
|
|
import { CONSTANTS } from "../Constants";
|
|
import { GangMemberTasks } from "./GangMemberTasks";
|
|
import { IAscensionResult } from "./IAscensionResult";
|
|
|
|
import { AllGangs } from "./AllGangs";
|
|
import { GangMember } from "./GangMember";
|
|
|
|
import { WorkerScript } from "../Netscript/WorkerScript";
|
|
import { IPlayer } from "../PersonObjects/IPlayer";
|
|
import { PowerMultiplier } from "./data/power";
|
|
import { IGang } from "./IGang";
|
|
|
|
export class Gang implements 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;
|
|
|
|
constructor(facName = "", hacking = false) {
|
|
this.facName = facName;
|
|
this.members = [];
|
|
this.wanted = 1;
|
|
this.respect = 1;
|
|
|
|
this.isHackingGang = hacking;
|
|
|
|
this.respectGainRate = 0;
|
|
this.wantedGainRate = 0;
|
|
this.moneyGainRate = 0;
|
|
|
|
// When processing gains, this stores the number of cycles until some
|
|
// limit is reached, and then calculates and applies the gains only at that limit
|
|
this.storedCycles = 0;
|
|
|
|
// Separate variable to keep track of cycles for Territry + Power gang, which
|
|
// happens on a slower "clock" than normal processing
|
|
this.storedTerritoryAndPowerCycles = 0;
|
|
|
|
this.territoryClashChance = 0;
|
|
this.territoryWarfareEngaged = false;
|
|
|
|
this.notifyMemberDeath = true;
|
|
}
|
|
|
|
getPower(): number {
|
|
return AllGangs[this.facName].power;
|
|
}
|
|
|
|
getTerritory(): number {
|
|
return AllGangs[this.facName].territory;
|
|
}
|
|
|
|
process(numCycles = 1, player: IPlayer): void {
|
|
const CyclesPerSecond = 1000 / CONSTANTS._idleSpeed;
|
|
|
|
if (isNaN(numCycles)) {
|
|
console.error(`NaN passed into Gang.process(): ${numCycles}`);
|
|
}
|
|
this.storedCycles += numCycles;
|
|
|
|
// Only process if there are at least 2 seconds, and at most 5 seconds
|
|
if (this.storedCycles < 2 * CyclesPerSecond) return;
|
|
const cycles = Math.min(this.storedCycles, 5 * CyclesPerSecond);
|
|
|
|
try {
|
|
this.processGains(cycles, player);
|
|
this.processExperienceGains(cycles);
|
|
this.processTerritoryAndPowerGains(cycles);
|
|
this.storedCycles -= cycles;
|
|
} catch (e: any) {
|
|
console.error(`Exception caught when processing Gang: ${e}`);
|
|
}
|
|
}
|
|
|
|
processGains(numCycles = 1, player: IPlayer): void {
|
|
// Get gains per cycle
|
|
let moneyGains = 0;
|
|
let respectGains = 0;
|
|
let wantedLevelGains = 0;
|
|
let justice = 0;
|
|
for (let i = 0; i < this.members.length; ++i) {
|
|
respectGains += this.members[i].calculateRespectGain(this);
|
|
moneyGains += this.members[i].calculateMoneyGain(this);
|
|
const wantedLevelGain = this.members[i].calculateWantedLevelGain(this);
|
|
wantedLevelGains += wantedLevelGain;
|
|
if (this.members[i].getTask().baseWanted < 0) justice++; // this member is lowering wanted.
|
|
}
|
|
this.respectGainRate = respectGains;
|
|
this.wantedGainRate = wantedLevelGains;
|
|
this.moneyGainRate = moneyGains;
|
|
const gain = respectGains * numCycles;
|
|
this.respect += gain;
|
|
// Faction reputation gains is respect gain divided by some constant
|
|
const fac = Factions[this.facName];
|
|
if (!(fac instanceof Faction)) {
|
|
dialogBoxCreate(
|
|
"ERROR: Could not get Faction associates with your gang. This is a bug, please report to game dev",
|
|
);
|
|
throw new Error("Could not find the faction associated with this gang.");
|
|
}
|
|
const favorMult = 1 + fac.favor / 100;
|
|
|
|
fac.playerReputation += (player.faction_rep_mult * gain * favorMult) / GangConstants.GangRespectToReputationRatio;
|
|
|
|
// Keep track of respect gained per member
|
|
for (let i = 0; i < this.members.length; ++i) {
|
|
this.members[i].recordEarnedRespect(numCycles, this);
|
|
}
|
|
if (!(this.wanted === 1 && wantedLevelGains < 0)) {
|
|
const oldWanted = this.wanted;
|
|
let newWanted = oldWanted + wantedLevelGains * numCycles;
|
|
newWanted = newWanted * (1 - justice * 0.001); // safeguard
|
|
// Prevent overflow
|
|
if (wantedLevelGains <= 0 && newWanted > oldWanted) newWanted = 1;
|
|
|
|
this.wanted = newWanted;
|
|
if (this.wanted < 1) this.wanted = 1;
|
|
}
|
|
player.gainMoney(moneyGains * numCycles, "gang");
|
|
}
|
|
|
|
processTerritoryAndPowerGains(numCycles = 1): void {
|
|
this.storedTerritoryAndPowerCycles += numCycles;
|
|
if (this.storedTerritoryAndPowerCycles < GangConstants.CyclesPerTerritoryAndPowerUpdate) return;
|
|
this.storedTerritoryAndPowerCycles -= GangConstants.CyclesPerTerritoryAndPowerUpdate;
|
|
|
|
// Process power first
|
|
const gangName = this.facName;
|
|
for (const name of Object.keys(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 powerMult = PowerMultiplier[name];
|
|
if (powerMult === undefined) throw new Error("Should not be undefined");
|
|
const additiveGain = 0.75 * gainRoll * AllGangs[name].territory * powerMult;
|
|
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
|
|
const gangs = GangConstants.Names.filter((g) => AllGangs[g].territory > 0 || g === gangName);
|
|
if (gangs.length > 1) {
|
|
for (let i = 0; i < gangs.length; ++i) {
|
|
const others = gangs.filter((e) => {
|
|
return e !== gangs[i];
|
|
});
|
|
const other = getRandomInt(0, others.length - 1);
|
|
|
|
const thisGang = gangs[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() + 0.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;
|
|
}
|
|
}
|
|
|
|
const total = Object.values(AllGangs)
|
|
.map((g) => g.territory)
|
|
.reduce((p, c) => p + c, 0);
|
|
console.log(total);
|
|
Object.values(AllGangs).forEach((g) => (g.territory /= total));
|
|
}
|
|
}
|
|
}
|
|
|
|
processExperienceGains(numCycles = 1): void {
|
|
for (let i = 0; i < this.members.length; ++i) {
|
|
this.members[i].gainExperience(numCycles);
|
|
this.members[i].updateSkillLevels();
|
|
}
|
|
}
|
|
|
|
clash(won = false): void {
|
|
// Determine if a gang member should die
|
|
let baseDeathChance = 0.01;
|
|
if (won) baseDeathChance /= 2;
|
|
// If the clash was lost, the player loses a small percentage of power
|
|
else AllGangs[this.facName].power *= 1 / 1.008;
|
|
|
|
// Deaths can only occur during X% of clashes
|
|
if (Math.random() < 0.65) return;
|
|
|
|
for (let i = this.members.length - 1; i >= 0; --i) {
|
|
const member = this.members[i];
|
|
|
|
// Only members assigned to Territory Warfare can die
|
|
if (member.task !== "Territory Warfare") continue;
|
|
|
|
// Chance to die is decreased based on defense
|
|
const modifiedDeathChance = baseDeathChance / Math.pow(member.def, 0.6);
|
|
if (Math.random() < modifiedDeathChance) {
|
|
this.killMember(member);
|
|
}
|
|
}
|
|
}
|
|
|
|
canRecruitMember(): boolean {
|
|
if (this.members.length >= GangConstants.MaximumGangMembers) return false;
|
|
return this.respect >= this.getRespectNeededToRecruitMember();
|
|
}
|
|
|
|
getRespectNeededToRecruitMember(): number {
|
|
// First N gang members are free (can be recruited at 0 respect)
|
|
const numFreeMembers = 3;
|
|
if (this.members.length < numFreeMembers) return 0;
|
|
|
|
const i = this.members.length - (numFreeMembers - 1);
|
|
return Math.pow(5, i);
|
|
}
|
|
|
|
recruitMember(name: string): boolean {
|
|
name = String(name);
|
|
if (name === "" || !this.canRecruitMember()) return false;
|
|
|
|
// Check for already-existing names
|
|
const sameNames = this.members.filter((m) => m.name === name);
|
|
if (sameNames.length >= 1) return false;
|
|
|
|
const member = new GangMember(name);
|
|
this.members.push(member);
|
|
return true;
|
|
}
|
|
|
|
// Money and Respect gains multiplied by this number (< 1)
|
|
getWantedPenalty(): number {
|
|
return this.respect / (this.respect + this.wanted);
|
|
}
|
|
|
|
//Calculates power GAIN, which is added onto the Gang's existing power
|
|
calculatePower(): number {
|
|
let memberTotal = 0;
|
|
for (let i = 0; i < this.members.length; ++i) {
|
|
if (!GangMemberTasks.hasOwnProperty(this.members[i].task) || this.members[i].task !== "Territory Warfare")
|
|
continue;
|
|
memberTotal += this.members[i].calculatePower();
|
|
}
|
|
return 0.015 * Math.max(0.002, this.getTerritory()) * memberTotal;
|
|
}
|
|
|
|
killMember(member: GangMember): void {
|
|
// Player loses a percentage of total respect, plus whatever respect that member has earned
|
|
const totalRespect = this.respect;
|
|
const lostRespect = 0.05 * totalRespect + member.earnedRespect;
|
|
this.respect = Math.max(0, totalRespect - lostRespect);
|
|
|
|
for (let i = 0; i < this.members.length; ++i) {
|
|
if (member.name === this.members[i].name) {
|
|
this.members.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Notify of death
|
|
if (this.notifyMemberDeath) {
|
|
dialogBoxCreate(`${member.name} was killed in a gang clash! You lost ${lostRespect} respect`);
|
|
}
|
|
}
|
|
|
|
ascendMember(member: GangMember, workerScript?: WorkerScript): IAscensionResult {
|
|
try {
|
|
const res = member.ascend();
|
|
this.respect = Math.max(1, this.respect - res.respect);
|
|
if (workerScript) {
|
|
workerScript.log("gang.ascendMember", () => `Ascended Gang member ${member.name}`);
|
|
}
|
|
return res;
|
|
} catch (e: any) {
|
|
if (workerScript == null) {
|
|
exceptionAlert(e);
|
|
}
|
|
throw e; // Re-throw, will be caught in the Netscript Function
|
|
}
|
|
}
|
|
|
|
// Cost of upgrade gets cheaper as gang increases in respect + power
|
|
getDiscount(): number {
|
|
const power = this.getPower();
|
|
const respect = this.respect;
|
|
|
|
const respectLinearFac = 5e6;
|
|
const powerLinearFac = 1e6;
|
|
const discount =
|
|
Math.pow(respect, 0.01) + respect / respectLinearFac + Math.pow(power, 0.01) + power / powerLinearFac - 1;
|
|
return Math.max(1, discount);
|
|
}
|
|
|
|
// Returns only valid tasks for this gang. Excludes 'Unassigned'
|
|
getAllTaskNames(): string[] {
|
|
return Object.keys(GangMemberTasks).filter((taskName: string) => {
|
|
const task = GangMemberTasks[taskName];
|
|
if (task == null) return false;
|
|
if (task.name === "Unassigned") return false;
|
|
// yes you need both checks
|
|
return this.isHackingGang === task.isHacking || !this.isHackingGang === task.isCombat;
|
|
});
|
|
}
|
|
|
|
getUpgradeCost(upg: GangMemberUpgrade | null): number {
|
|
if (upg == null) {
|
|
return Infinity;
|
|
}
|
|
return upg.cost / this.getDiscount();
|
|
}
|
|
|
|
/**
|
|
* Serialize the current object to a JSON save state.
|
|
*/
|
|
toJSON(): any {
|
|
return Generic_toJSON("Gang", this);
|
|
}
|
|
|
|
/**
|
|
* Initiatizes a Gang object from a JSON save state.
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
static fromJSON(value: any): Gang {
|
|
return Generic_fromJSON(Gang, value.data);
|
|
}
|
|
}
|
|
|
|
Reviver.constructors.Gang = Gang;
|