Merge pull request #3680 from danielyxie/sleeve-blade

SLEEVE: Can now perform bladeburner actions
This commit is contained in:
hydroflame 2022-05-20 18:23:00 -04:00 committed by GitHub
commit 8d2041389e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 795 additions and 303 deletions

4
package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "bitburner",
"version": "1.6.4",
"version": "1.7.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "bitburner",
"version": "1.6.4",
"version": "1.7.0",
"hasInstallScript": true,
"license": "SEE LICENSE IN license.txt",
"dependencies": {

@ -5,6 +5,7 @@ import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver"
import { BladeburnerConstants } from "./data/Constants";
import { IBladeburner } from "./IBladeburner";
import { IAction, ISuccessChanceParams } from "./IAction";
import { IPerson } from "../PersonObjects/IPerson";
class StatsMultiplier {
[key: string]: number;
@ -152,8 +153,8 @@ export class Action implements IAction {
* Tests for success. Should be called when an action has completed
* @param inst {Bladeburner} - Bladeburner instance
*/
attempt(inst: IBladeburner): boolean {
return Math.random() < this.getSuccessChance(inst);
attempt(inst: IBladeburner, person: IPerson): boolean {
return Math.random() < this.getSuccessChance(inst, person);
}
// To be implemented by subtypes
@ -161,13 +162,13 @@ export class Action implements IAction {
return 1;
}
getActionTime(inst: IBladeburner): number {
getActionTime(inst: IBladeburner, person: IPerson): number {
const difficulty = this.getDifficulty();
let baseTime = difficulty / BladeburnerConstants.DifficultyToTimeFactor;
const skillFac = inst.skillMultipliers.actionTime; // Always < 1
const effAgility = Player.agility * inst.skillMultipliers.effAgi;
const effDexterity = Player.dexterity * inst.skillMultipliers.effDex;
const effAgility = person.agility * inst.skillMultipliers.effAgi;
const effDexterity = person.dexterity * inst.skillMultipliers.effDex;
const statFac =
0.5 *
(Math.pow(effAgility, BladeburnerConstants.EffAgiExponentialFactor) +
@ -211,12 +212,12 @@ export class Action implements IAction {
return 1;
}
getEstSuccessChance(inst: IBladeburner): [number, number] {
getEstSuccessChance(inst: IBladeburner, person: IPerson): [number, number] {
function clamp(x: number): number {
return Math.max(0, Math.min(x, 1));
}
const est = this.getSuccessChance(inst, { est: true });
const real = this.getSuccessChance(inst);
const est = this.getSuccessChance(inst, person, { est: true });
const real = this.getSuccessChance(inst, person);
const diff = Math.abs(real - est);
let low = real - diff;
let high = real + diff;
@ -232,7 +233,7 @@ export class Action implements IAction {
* @params - options:
* est (bool): Get success chance estimate instead of real success chance
*/
getSuccessChance(inst: IBladeburner, params: ISuccessChanceParams = { est: false }): number {
getSuccessChance(inst: IBladeburner, person: IPerson, params: ISuccessChanceParams = { est: false }): number {
if (inst == null) {
throw new Error("Invalid Bladeburner instance passed into Action.getSuccessChance");
}
@ -240,7 +241,7 @@ export class Action implements IAction {
let competence = 0;
for (const stat of Object.keys(this.weights)) {
if (this.weights.hasOwnProperty(stat)) {
const playerStatLvl = Player.queryStatFromString(stat);
const playerStatLvl = person.queryStatFromString(stat);
const key = "eff" + stat.charAt(0).toUpperCase() + stat.slice(1);
let effMultiplier = inst.skillMultipliers[key];
if (effMultiplier == null) {

@ -15,6 +15,8 @@ import { Skill } from "./Skill";
import { City } from "./City";
import { IAction } from "./IAction";
import { IPlayer } from "../PersonObjects/IPlayer";
import { createTaskTracker, ITaskTracker } from "../PersonObjects/ITaskTracker";
import { IPerson } from "../PersonObjects/IPerson";
import { IRouter, Page } from "../ui/Router";
import { ConsoleHelpText } from "./data/Help";
import { exceptionAlert } from "../utils/helpers/exceptionAlert";
@ -52,6 +54,7 @@ export class Bladeburner implements IBladeburner {
totalSkillPoints = 0;
teamSize = 0;
sleeveSize = 0;
teamLost = 0;
hpLost = 0;
@ -158,7 +161,7 @@ export class Bladeburner implements IBladeburner {
return { isAvailable: true, action };
}
startAction(player: IPlayer, actionId: IActionIdentifier): void {
startAction(person: IPerson, actionId: IActionIdentifier): void {
if (actionId == null) return;
this.action = actionId;
this.actionTimeCurrent = 0;
@ -175,7 +178,7 @@ export class Bladeburner implements IBladeburner {
if (action.count < 1) {
return this.resetAction();
}
this.actionTimeToComplete = action.getActionTime(this);
this.actionTimeToComplete = action.getActionTime(this, person);
} catch (e: any) {
exceptionAlert(e);
}
@ -192,7 +195,7 @@ export class Bladeburner implements IBladeburner {
if (actionId.name === "Raid" && this.getCurrentCity().comms === 0) {
return this.resetAction();
}
this.actionTimeToComplete = action.getActionTime(this);
this.actionTimeToComplete = action.getActionTime(this, person);
} catch (e: any) {
exceptionAlert(e);
}
@ -210,14 +213,14 @@ export class Bladeburner implements IBladeburner {
if (testBlackOp.action === undefined) {
throw new Error("action should not be null");
}
this.actionTimeToComplete = testBlackOp.action.getActionTime(this);
this.actionTimeToComplete = testBlackOp.action.getActionTime(this, person);
} catch (e: any) {
exceptionAlert(e);
}
break;
}
case ActionTypes["Recruitment"]:
this.actionTimeToComplete = this.getRecruitmentTime(player);
this.actionTimeToComplete = this.getRecruitmentTime(person);
break;
case ActionTypes["Training"]:
case ActionTypes["FieldAnalysis"]:
@ -996,11 +999,11 @@ export class Bladeburner implements IBladeburner {
}
/**
* Process stat gains from Contracts, Operations, and Black Operations
* Return stat to be gained from Contracts, Operations, and Black Operations
* @param action(Action obj) - Derived action class
* @param success(bool) - Whether action was successful
*/
gainActionStats(player: IPlayer, action: IAction, success: boolean): void {
getActionStats(action: IAction, success: boolean): ITaskTracker {
const difficulty = action.getDifficulty();
/**
@ -1017,34 +1020,48 @@ export class Bladeburner implements IBladeburner {
const unweightedGain = time * BladeburnerConstants.BaseStatGain * successMult * difficultyMult;
const unweightedIntGain = time * BladeburnerConstants.BaseIntGain * successMult * difficultyMult;
const skillMult = this.skillMultipliers.expGain;
player.gainHackingExp(unweightedGain * action.weights.hack * player.hacking_exp_mult * skillMult);
player.gainStrengthExp(unweightedGain * action.weights.str * player.strength_exp_mult * skillMult);
player.gainDefenseExp(unweightedGain * action.weights.def * player.defense_exp_mult * skillMult);
player.gainDexterityExp(unweightedGain * action.weights.dex * player.dexterity_exp_mult * skillMult);
player.gainAgilityExp(unweightedGain * action.weights.agi * player.agility_exp_mult * skillMult);
player.gainCharismaExp(unweightedGain * action.weights.cha * player.charisma_exp_mult * skillMult);
player.gainIntelligenceExp(unweightedIntGain * action.weights.int * skillMult);
return {
hack: unweightedGain * action.weights.hack * skillMult,
str: unweightedGain * action.weights.str * skillMult,
def: unweightedGain * action.weights.def * skillMult,
dex: unweightedGain * action.weights.dex * skillMult,
agi: unweightedGain * action.weights.agi * skillMult,
cha: unweightedGain * action.weights.cha * skillMult,
int: unweightedIntGain * action.weights.int * skillMult,
money: 0,
};
}
getDiplomacyEffectiveness(player: IPlayer): number {
getDiplomacyEffectiveness(person: IPerson): number {
// Returns a decimal by which the city's chaos level should be multiplied (e.g. 0.98)
const CharismaLinearFactor = 1e3;
const CharismaExponentialFactor = 0.045;
const charismaEff = Math.pow(player.charisma, CharismaExponentialFactor) + player.charisma / CharismaLinearFactor;
const charismaEff = Math.pow(person.charisma, CharismaExponentialFactor) + person.charisma / CharismaLinearFactor;
return (100 - charismaEff) / 100;
}
getRecruitmentSuccessChance(player: IPlayer): number {
return Math.pow(player.charisma, 0.45) / (this.teamSize + 1);
getRecruitmentSuccessChance(person: IPerson): number {
return Math.pow(person.charisma, 0.45) / (this.teamSize - this.sleeveSize + 1);
}
getRecruitmentTime(player: IPlayer): number {
const effCharisma = player.charisma * this.skillMultipliers.effCha;
getRecruitmentTime(person: IPerson): number {
const effCharisma = person.charisma * this.skillMultipliers.effCha;
const charismaFactor = Math.pow(effCharisma, 0.81) + effCharisma / 90;
return Math.max(10, Math.round(BladeburnerConstants.BaseRecruitmentTimeNeeded - charismaFactor));
}
sleeveSupport(joining: boolean): void {
if (joining) {
this.sleeveSize += 1;
this.teamSize += 1;
} else {
this.sleeveSize -= 1;
this.teamSize -= 1;
}
}
resetSkillMultipliers(): void {
this.skillMultipliers = {
successChanceAll: 1,
@ -1096,7 +1113,7 @@ export class Bladeburner implements IBladeburner {
}
}
completeOperation(success: boolean): void {
completeOperation(success: boolean, player: IPlayer): void {
if (this.action.type !== ActionTypes.Operation) {
throw new Error("completeOperation() called even though current action is not an Operation");
}
@ -1116,6 +1133,15 @@ export class Bladeburner implements IBladeburner {
}
const losses = getRandomInt(0, max);
this.teamSize -= losses;
if (this.teamSize < this.sleeveSize) {
const sup = player.sleeves.filter((x) => x.bbAction == "Support main sleeve");
for (let i = 0; i > this.teamSize - this.sleeveSize; i--) {
const r = Math.floor(Math.random() * sup.length);
sup[r].takeDamage(sup[r].max_hp);
sup.splice(r, 1);
}
this.teamSize += this.sleeveSize;
}
this.teamLost += losses;
if (this.logging.ops && losses > 0) {
this.log("Lost " + formatNumber(losses, 0) + " team members during this " + action.name);
@ -1213,13 +1239,13 @@ export class Bladeburner implements IBladeburner {
}
}
completeContract(success: boolean): void {
if (this.action.type !== ActionTypes.Contract) {
completeContract(success: boolean, actionIdent: IActionIdentifier): void {
if (actionIdent.type !== ActionTypes.Contract) {
throw new Error("completeContract() called even though current action is not a Contract");
}
const city = this.getCurrentCity();
if (success) {
switch (this.action.name) {
switch (actionIdent.name) {
case "Tracking":
// Increase estimate accuracy by a relatively small amount
city.improvePopulationEstimateByCount(getRandomInt(100, 1e3));
@ -1233,20 +1259,21 @@ export class Bladeburner implements IBladeburner {
city.changeChaosByCount(0.04);
break;
default:
throw new Error("Invalid Action name in completeContract: " + this.action.name);
throw new Error("Invalid Action name in completeContract: " + actionIdent.name);
}
}
}
completeAction(router: IRouter, player: IPlayer): void {
switch (this.action.type) {
completeAction(player: IPlayer, person: IPerson, actionIdent: IActionIdentifier, isPlayer = true): ITaskTracker {
let retValue = createTaskTracker();
switch (actionIdent.type) {
case ActionTypes["Contract"]:
case ActionTypes["Operation"]: {
try {
const isOperation = this.action.type === ActionTypes["Operation"];
const action = this.getActionObject(this.action);
const isOperation = actionIdent.type === ActionTypes["Operation"];
const action = this.getActionObject(actionIdent);
if (action == null) {
throw new Error("Failed to get Contract/Operation Object for: " + this.action.name);
throw new Error("Failed to get Contract/Operation Object for: " + actionIdent.name);
}
const difficulty = action.getDifficulty();
const difficultyMultiplier =
@ -1254,15 +1281,17 @@ export class Bladeburner implements IBladeburner {
difficulty / BladeburnerConstants.DiffMultLinearFactor;
const rewardMultiplier = Math.pow(action.rewardFac, action.level - 1);
if (isPlayer) {
// Stamina loss is based on difficulty
this.stamina -= BladeburnerConstants.BaseStaminaLoss * difficultyMultiplier;
if (this.stamina < 0) {
this.stamina = 0;
}
}
// Process Contract/Operation success/failure
if (action.attempt(this)) {
this.gainActionStats(player, action, true);
if (action.attempt(this, person)) {
retValue = this.getActionStats(action, true);
++action.successes;
--action.count;
@ -1270,7 +1299,7 @@ export class Bladeburner implements IBladeburner {
let moneyGain = 0;
if (!isOperation) {
moneyGain = BladeburnerConstants.ContractBaseMoneyGain * rewardMultiplier * this.skillMultipliers.money;
player.gainMoney(moneyGain, "bladeburner");
retValue.money = moneyGain;
}
if (isOperation) {
@ -1280,11 +1309,18 @@ export class Bladeburner implements IBladeburner {
}
if (action.rankGain) {
const gain = addOffset(action.rankGain * rewardMultiplier * BitNodeMultipliers.BladeburnerRank, 10);
this.changeRank(player, gain);
this.changeRank(person, gain);
if (isOperation && this.logging.ops) {
this.log(action.name + " successfully completed! Gained " + formatNumber(gain, 3) + " rank");
this.log(
`${person.whoAmI()}: ` +
action.name +
" successfully completed! Gained " +
formatNumber(gain, 3) +
" rank",
);
} else if (!isOperation && this.logging.contracts) {
this.log(
`${person.whoAmI()}: ` +
action.name +
" contract successfully completed! Gained " +
formatNumber(gain, 3) +
@ -1293,22 +1329,22 @@ export class Bladeburner implements IBladeburner {
);
}
}
isOperation ? this.completeOperation(true) : this.completeContract(true);
isOperation ? this.completeOperation(true, player) : this.completeContract(true, actionIdent);
} else {
this.gainActionStats(player, action, false);
retValue = this.getActionStats(action, false);
++action.failures;
let loss = 0,
damage = 0;
if (action.rankLoss) {
loss = addOffset(action.rankLoss * rewardMultiplier, 10);
this.changeRank(player, -1 * loss);
this.changeRank(person, -1 * loss);
}
if (action.hpLoss) {
damage = action.hpLoss * difficultyMultiplier;
damage = Math.ceil(addOffset(damage, 10));
this.hpLost += damage;
const cost = calculateHospitalizationCost(player, damage);
if (player.takeDamage(damage)) {
if (person.takeDamage(damage)) {
++this.numHosp;
this.moneyLost += cost;
}
@ -1321,16 +1357,15 @@ export class Bladeburner implements IBladeburner {
logLossText += "Took " + formatNumber(damage, 0) + " damage.";
}
if (isOperation && this.logging.ops) {
this.log(action.name + " failed! " + logLossText);
this.log(`${person.whoAmI()}: ` + action.name + " failed! " + logLossText);
} else if (!isOperation && this.logging.contracts) {
this.log(action.name + " contract failed! " + logLossText);
this.log(`${person.whoAmI()}: ` + action.name + " contract failed! " + logLossText);
}
isOperation ? this.completeOperation(false) : this.completeContract(false);
isOperation ? this.completeOperation(false, player) : this.completeContract(false, actionIdent);
}
if (action.autoLevel) {
action.level = action.maxLevel;
} // Autolevel
this.startAction(player, this.action); // Repeat action
} catch (e: any) {
exceptionAlert(e);
}
@ -1339,9 +1374,9 @@ export class Bladeburner implements IBladeburner {
case ActionTypes["BlackOp"]:
case ActionTypes["BlackOperation"]: {
try {
const action = this.getActionObject(this.action);
const action = this.getActionObject(actionIdent);
if (action == null || !(action instanceof BlackOperation)) {
throw new Error("Failed to get BlackOperation Object for: " + this.action.name);
throw new Error("Failed to get BlackOperation Object for: " + actionIdent.name);
}
const difficulty = action.getDifficulty();
const difficultyMultiplier =
@ -1358,39 +1393,35 @@ export class Bladeburner implements IBladeburner {
const teamCount = action.teamCount;
let teamLossMax;
if (action.attempt(this)) {
this.gainActionStats(player, action, true);
if (action.attempt(this, person)) {
retValue = this.getActionStats(action, true);
action.count = 0;
this.blackops[action.name] = true;
let rankGain = 0;
if (action.rankGain) {
rankGain = addOffset(action.rankGain * BitNodeMultipliers.BladeburnerRank, 10);
this.changeRank(player, rankGain);
this.changeRank(person, rankGain);
}
teamLossMax = Math.ceil(teamCount / 2);
// Operation Daedalus
if (action.name === BlackOperationNames.OperationDaedalus) {
this.resetAction();
return router.toBitVerse(false, false);
}
if (this.logging.blackops) {
this.log(action.name + " successful! Gained " + formatNumber(rankGain, 1) + " rank");
this.log(
`${person.whoAmI()}: ` + action.name + " successful! Gained " + formatNumber(rankGain, 1) + " rank",
);
}
} else {
this.gainActionStats(player, action, false);
retValue = this.getActionStats(action, false);
let rankLoss = 0;
let damage = 0;
if (action.rankLoss) {
rankLoss = addOffset(action.rankLoss, 10);
this.changeRank(player, -1 * rankLoss);
this.changeRank(person, -1 * rankLoss);
}
if (action.hpLoss) {
damage = action.hpLoss * difficultyMultiplier;
damage = Math.ceil(addOffset(damage, 10));
const cost = calculateHospitalizationCost(player, damage);
if (player.takeDamage(damage)) {
if (person.takeDamage(damage)) {
++this.numHosp;
this.moneyLost += cost;
}
@ -1399,6 +1430,7 @@ export class Bladeburner implements IBladeburner {
if (this.logging.blackops) {
this.log(
`${person.whoAmI()}: ` +
action.name +
" failed! Lost " +
formatNumber(rankLoss, 1) +
@ -1415,9 +1447,18 @@ export class Bladeburner implements IBladeburner {
if (teamCount >= 1) {
const losses = getRandomInt(1, teamLossMax);
this.teamSize -= losses;
if (this.teamSize < this.sleeveSize) {
const sup = player.sleeves.filter((x) => x.bbAction == "Support main sleeve");
for (let i = 0; i > this.teamSize - this.sleeveSize; i--) {
const r = Math.floor(Math.random() * sup.length);
sup[r].takeDamage(sup[r].max_hp);
sup.splice(r, 1);
}
this.teamSize += this.sleeveSize;
}
this.teamLost += losses;
if (this.logging.blackops) {
this.log("You lost " + formatNumber(losses, 0) + " team members during " + action.name);
this.log(`${person.whoAmI()}: You lost ${formatNumber(losses, 0)} team members during ${action.name}`);
}
}
} catch (e: any) {
@ -1427,18 +1468,19 @@ export class Bladeburner implements IBladeburner {
}
case ActionTypes["Training"]: {
this.stamina -= 0.5 * BladeburnerConstants.BaseStaminaLoss;
const strExpGain = 30 * player.strength_exp_mult,
defExpGain = 30 * player.defense_exp_mult,
dexExpGain = 30 * player.dexterity_exp_mult,
agiExpGain = 30 * player.agility_exp_mult,
const strExpGain = 30 * person.strength_exp_mult,
defExpGain = 30 * person.defense_exp_mult,
dexExpGain = 30 * person.dexterity_exp_mult,
agiExpGain = 30 * person.agility_exp_mult,
staminaGain = 0.04 * this.skillMultipliers.stamina;
player.gainStrengthExp(strExpGain);
player.gainDefenseExp(defExpGain);
player.gainDexterityExp(dexExpGain);
player.gainAgilityExp(agiExpGain);
retValue.str = strExpGain;
retValue.def = defExpGain;
retValue.dex = dexExpGain;
retValue.agi = agiExpGain;
this.staminaBonus += staminaGain;
if (this.logging.general) {
this.log(
`${person.whoAmI()}: ` +
"Training completed. Gained: " +
formatNumber(strExpGain, 1) +
" str exp, " +
@ -1452,80 +1494,89 @@ export class Bladeburner implements IBladeburner {
" max stamina",
);
}
this.startAction(player, this.action); // Repeat action
break;
}
case ActionTypes["FieldAnalysis"]:
case ActionTypes["Field Analysis"]: {
// Does not use stamina. Effectiveness depends on hacking, int, and cha
let eff =
0.04 * Math.pow(player.hacking, 0.3) +
0.04 * Math.pow(player.intelligence, 0.9) +
0.02 * Math.pow(player.charisma, 0.3);
eff *= player.bladeburner_analysis_mult;
0.04 * Math.pow(person.hacking, 0.3) +
0.04 * Math.pow(person.intelligence, 0.9) +
0.02 * Math.pow(person.charisma, 0.3);
eff *= person.bladeburner_analysis_mult;
if (isNaN(eff) || eff < 0) {
throw new Error("Field Analysis Effectiveness calculated to be NaN or negative");
}
const hackingExpGain = 20 * player.hacking_exp_mult;
const charismaExpGain = 20 * player.charisma_exp_mult;
const hackingExpGain = 20 * person.hacking_exp_mult;
const charismaExpGain = 20 * person.charisma_exp_mult;
const rankGain = 0.1 * BitNodeMultipliers.BladeburnerRank;
player.gainHackingExp(hackingExpGain);
player.gainIntelligenceExp(BladeburnerConstants.BaseIntGain);
player.gainCharismaExp(charismaExpGain);
this.changeRank(player, rankGain);
retValue.hack = hackingExpGain;
retValue.cha = charismaExpGain;
retValue.int = BladeburnerConstants.BaseIntGain;
this.changeRank(person, rankGain);
this.getCurrentCity().improvePopulationEstimateByPercentage(eff * this.skillMultipliers.successChanceEstimate);
if (this.logging.general) {
this.log(
`${person.whoAmI()}: ` +
`Field analysis completed. Gained ${formatNumber(rankGain, 2)} rank, ` +
`${formatNumber(hackingExpGain, 1)} hacking exp, and ` +
`${formatNumber(charismaExpGain, 1)} charisma exp`,
);
}
this.startAction(player, this.action); // Repeat action
break;
}
case ActionTypes["Recruitment"]: {
const successChance = this.getRecruitmentSuccessChance(player);
const successChance = this.getRecruitmentSuccessChance(person);
const recruitTime = this.getRecruitmentTime(person) * 1000;
if (Math.random() < successChance) {
const expGain = 2 * BladeburnerConstants.BaseStatGain * this.actionTimeToComplete;
player.gainCharismaExp(expGain);
const expGain = 2 * BladeburnerConstants.BaseStatGain * recruitTime;
retValue.cha = expGain;
++this.teamSize;
if (this.logging.general) {
this.log("Successfully recruited a team member! Gained " + formatNumber(expGain, 1) + " charisma exp");
this.log(
`${person.whoAmI()}: ` +
"Successfully recruited a team member! Gained " +
formatNumber(expGain, 1) +
" charisma exp",
);
}
} else {
const expGain = BladeburnerConstants.BaseStatGain * this.actionTimeToComplete;
player.gainCharismaExp(expGain);
const expGain = BladeburnerConstants.BaseStatGain * recruitTime;
retValue.cha = expGain;
if (this.logging.general) {
this.log("Failed to recruit a team member. Gained " + formatNumber(expGain, 1) + " charisma exp");
this.log(
`${person.whoAmI()}: ` +
"Failed to recruit a team member. Gained " +
formatNumber(expGain, 1) +
" charisma exp",
);
}
}
this.startAction(player, this.action); // Repeat action
break;
}
case ActionTypes["Diplomacy"]: {
const eff = this.getDiplomacyEffectiveness(player);
const eff = this.getDiplomacyEffectiveness(person);
this.getCurrentCity().chaos *= eff;
if (this.getCurrentCity().chaos < 0) {
this.getCurrentCity().chaos = 0;
}
if (this.logging.general) {
this.log(
`Diplomacy completed. Chaos levels in the current city fell by ${numeralWrapper.formatPercentage(1 - eff)}`,
`${person.whoAmI()}: Diplomacy completed. Chaos levels in the current city fell by ${numeralWrapper.formatPercentage(
1 - eff,
)}`,
);
}
this.startAction(player, this.action); // Repeat Action
break;
}
case ActionTypes["Hyperbolic Regeneration Chamber"]: {
player.regenerateHp(BladeburnerConstants.HrcHpGain);
person.regenerateHp(BladeburnerConstants.HrcHpGain);
const staminaGain = this.maxStamina * (BladeburnerConstants.HrcStaminaGain / 100);
this.stamina = Math.min(this.maxStamina, this.stamina + staminaGain);
this.startAction(player, this.action);
if (this.logging.general) {
this.log(
`Rested in Hyperbolic Regeneration Chamber. Restored ${
`${person.whoAmI()}: Rested in Hyperbolic Regeneration Chamber. Restored ${
BladeburnerConstants.HrcHpGain
} HP and gained ${numeralWrapper.formatStamina(staminaGain)} stamina`,
);
@ -1544,24 +1595,37 @@ export class Bladeburner implements IBladeburner {
this.operations[operation].count += (60 * 3 * growthF()) / BladeburnerConstants.ActionCountGrowthPeriod;
}
if (this.logging.general) {
this.log(`Incited violence in the synthoid communities.`);
this.log(`${person.whoAmI()}: Incited violence in the synthoid communities.`);
}
for (const cityName of Object.keys(this.cities)) {
const city = this.cities[cityName];
city.chaos += 10;
city.chaos += city.chaos / (Math.log(city.chaos) / Math.log(10));
}
this.startAction(player, this.action);
break;
}
default:
console.error(`Bladeburner.completeAction() called for invalid action: ${this.action.type}`);
console.error(`Bladeburner.completeAction() called for invalid action: ${actionIdent.type}`);
break;
}
return retValue;
}
infiltrateSynthoidCommunities(p: IPlayer): void {
const infilSleeves = p.sleeves.filter((s) => s.bbAction === "Infiltrate synthoids").length;
const amt = Math.pow(infilSleeves, -0.5) / 2;
for (const contract of Object.keys(this.contracts)) {
this.contracts[contract].count += amt;
}
for (const operation of Object.keys(this.operations)) {
this.operations[operation].count += amt;
}
if (this.logging.general) {
this.log(`Sleeve: Infiltrate the synthoid communities.`);
}
}
changeRank(player: IPlayer, change: number): void {
changeRank(person: IPerson, change: number): void {
if (isNaN(change)) {
throw new Error("NaN passed into Bladeburner.changeRank()");
}
@ -1582,7 +1646,7 @@ export class Bladeburner implements IBladeburner {
if (bladeburnerFac.isMember) {
const favorBonus = 1 + bladeburnerFac.favor / 100;
bladeburnerFac.playerReputation +=
BladeburnerConstants.RankToFactionRepFactor * change * player.faction_rep_mult * favorBonus;
BladeburnerConstants.RankToFactionRepFactor * change * person.faction_rep_mult * favorBonus;
}
}
@ -1613,7 +1677,19 @@ export class Bladeburner implements IBladeburner {
this.actionTimeOverflow = 0;
if (this.actionTimeCurrent >= this.actionTimeToComplete) {
this.actionTimeOverflow = this.actionTimeCurrent - this.actionTimeToComplete;
return this.completeAction(router, player);
const retValue = this.completeAction(player, player, this.action);
player.gainMoney(retValue.money, "bladeburner");
player.gainStats(retValue);
// Operation Daedalus
const action = this.getActionObject(this.action);
if (action == null) {
throw new Error("Failed to get BlackOperation Object for: " + this.action.name);
} else if (action.name === BlackOperationNames.OperationDaedalus && this.blackops[action.name]) {
this.resetAction();
router.toBitVerse(false, false);
} else if (this.action.type != ActionTypes["BlackOperation"] && this.action.type != ActionTypes["BlackOp"]) {
this.startAction(player, this.action); // Repeat action
}
}
}
@ -2092,67 +2168,53 @@ export class Bladeburner implements IBladeburner {
}
}
getActionTimeNetscriptFn(player: IPlayer, type: string, name: string, workerScript: WorkerScript): number {
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
getActionTimeNetscriptFn(person: IPerson, type: string, name: string): number | string {
const actionId = this.getActionIdFromTypeAndName(type, name);
if (actionId == null) {
workerScript.log("bladeburner.getActionTime", () => errorLogText);
return -1;
return "bladeburner.getActionTime";
}
const actionObj = this.getActionObject(actionId);
if (actionObj == null) {
workerScript.log("bladeburner.getActionTime", () => errorLogText);
return -1;
return "bladeburner.getActionTime";
}
switch (actionId.type) {
case ActionTypes["Contract"]:
case ActionTypes["Operation"]:
case ActionTypes["BlackOp"]:
case ActionTypes["BlackOperation"]:
return actionObj.getActionTime(this) * 1000;
return actionObj.getActionTime(this, person) * 1000;
case ActionTypes["Training"]:
case ActionTypes["Field Analysis"]:
case ActionTypes["FieldAnalysis"]:
return 30000;
case ActionTypes["Recruitment"]:
return this.getRecruitmentTime(player) * 1000;
return this.getRecruitmentTime(person) * 1000;
case ActionTypes["Diplomacy"]:
case ActionTypes["Hyperbolic Regeneration Chamber"]:
case ActionTypes["Incite Violence"]:
return 60000;
default:
workerScript.log("bladeburner.getActionTime", () => errorLogText);
return -1;
return "bladeburner.getActionTime";
}
}
getActionEstimatedSuccessChanceNetscriptFn(
player: IPlayer,
type: string,
name: string,
workerScript: WorkerScript,
): [number, number] {
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
getActionEstimatedSuccessChanceNetscriptFn(person: IPerson, type: string, name: string): [number, number] | string {
const actionId = this.getActionIdFromTypeAndName(type, name);
if (actionId == null) {
workerScript.log("bladeburner.getActionEstimatedSuccessChance", () => errorLogText);
return [-1, -1];
return "bladeburner.getActionEstimatedSuccessChance";
}
const actionObj = this.getActionObject(actionId);
if (actionObj == null) {
workerScript.log("bladeburner.getActionEstimatedSuccessChance", () => errorLogText);
return [-1, -1];
return "bladeburner.getActionEstimatedSuccessChance";
}
switch (actionId.type) {
case ActionTypes["Contract"]:
case ActionTypes["Operation"]:
case ActionTypes["BlackOp"]:
case ActionTypes["BlackOperation"]:
return actionObj.getEstSuccessChance(this);
return actionObj.getEstSuccessChance(this, person);
case ActionTypes["Training"]:
case ActionTypes["Field Analysis"]:
case ActionTypes["FieldAnalysis"]:
@ -2161,12 +2223,11 @@ export class Bladeburner implements IBladeburner {
case ActionTypes["Incite Violence"]:
return [1, 1];
case ActionTypes["Recruitment"]: {
const recChance = this.getRecruitmentSuccessChance(player);
const recChance = this.getRecruitmentSuccessChance(person);
return [recChance, recChance];
}
default:
workerScript.log("bladeburner.getActionEstimatedSuccessChance", () => errorLogText);
return [-1, -1];
return "bladeburner.getActionEstimatedSuccessChance";
}
}

@ -1,3 +1,4 @@
import { IPerson } from "../PersonObjects/IPerson";
import { IBladeburner } from "./IBladeburner";
interface IStatsMultiplier {
@ -55,15 +56,15 @@ export interface IAction {
teamCount: number;
getDifficulty(): number;
attempt(inst: IBladeburner): boolean;
attempt(inst: IBladeburner, person: IPerson): boolean;
getActionTimePenalty(): number;
getActionTime(inst: IBladeburner): number;
getActionTime(inst: IBladeburner, person: IPerson): number;
getTeamSuccessBonus(inst: IBladeburner): number;
getActionTypeSkillSuccessBonus(inst: IBladeburner): number;
getChaosCompetencePenalty(inst: IBladeburner, params: ISuccessChanceParams): number;
getChaosDifficultyBonus(inst: IBladeburner): number;
getEstSuccessChance(inst: IBladeburner): [number, number];
getSuccessChance(inst: IBladeburner, params: ISuccessChanceParams): number;
getEstSuccessChance(inst: IBladeburner, person: IPerson): [number, number];
getSuccessChance(inst: IBladeburner, person: IPerson, params: ISuccessChanceParams): number;
getSuccessesNeededForNextLevel(baseSuccessesPerLevel: number): number;
setMaxLevel(baseSuccessesPerLevel: number): void;
toJSON(): any;

@ -3,6 +3,8 @@ import { City } from "./City";
import { Skill } from "./Skill";
import { IAction } from "./IAction";
import { IPlayer } from "../PersonObjects/IPlayer";
import { IPerson } from "../PersonObjects/IPerson";
import { ITaskTracker } from "../PersonObjects/ITaskTracker";
import { IRouter } from "../ui/Router";
import { WorkerScript } from "../Netscript/WorkerScript";
@ -70,13 +72,8 @@ export interface IBladeburner {
getGeneralActionNamesNetscriptFn(): string[];
getSkillNamesNetscriptFn(): string[];
startActionNetscriptFn(player: IPlayer, type: string, name: string, workerScript: WorkerScript): boolean;
getActionTimeNetscriptFn(player: IPlayer, type: string, name: string, workerScript: WorkerScript): number;
getActionEstimatedSuccessChanceNetscriptFn(
player: IPlayer,
type: string,
name: string,
workerScript: WorkerScript,
): [number, number];
getActionTimeNetscriptFn(person: IPerson, type: string, name: string): number | string;
getActionEstimatedSuccessChanceNetscriptFn(person: IPerson, type: string, name: string): [number, number] | string;
getActionCountRemainingNetscriptFn(type: string, name: string, workerScript: WorkerScript): number;
getSkillLevelNetscriptFn(skillName: string, workerScript: WorkerScript): number;
getSkillUpgradeCostNetscriptFn(skillName: string, workerScript: WorkerScript): number;
@ -95,20 +92,22 @@ export interface IBladeburner {
triggerMigration(sourceCityName: string): void;
triggerPotentialMigration(sourceCityName: string, chance: number): void;
randomEvent(): void;
gainActionStats(player: IPlayer, action: IAction, success: boolean): void;
getDiplomacyEffectiveness(player: IPlayer): number;
getRecruitmentSuccessChance(player: IPlayer): number;
getRecruitmentTime(player: IPlayer): number;
getRecruitmentSuccessChance(player: IPerson): number;
getRecruitmentTime(player: IPerson): number;
resetSkillMultipliers(): void;
updateSkillMultipliers(): void;
completeOperation(success: boolean): void;
completeOperation(success: boolean, player: IPlayer): void;
getActionObject(actionId: IActionIdentifier): IAction | null;
completeContract(success: boolean): void;
completeAction(router: IRouter, player: IPlayer): void;
completeContract(success: boolean, actionIdent: IActionIdentifier): void;
completeAction(player: IPlayer, person: IPerson, actionIdent: IActionIdentifier, isPlayer?: boolean): ITaskTracker;
infiltrateSynthoidCommunities(p: IPlayer): void;
changeRank(player: IPlayer, change: number): void;
processAction(router: IRouter, player: IPlayer, seconds: number): void;
calculateStaminaGainPerSecond(player: IPlayer): number;
calculateMaxStamina(player: IPlayer): void;
create(): void;
process(router: IRouter, player: IPlayer): void;
getActionStats(action: IAction, success: boolean): ITaskTracker;
sleeveSupport(joining: boolean): void;
}

@ -37,7 +37,7 @@ export function BlackOpElem(props: IProps): React.ReactElement {
const isActive =
props.bladeburner.action.type === ActionTypes["BlackOperation"] &&
props.action.name === props.bladeburner.action.name;
const actionTime = props.action.getActionTime(props.bladeburner);
const actionTime = props.action.getActionTime(props.bladeburner, props.player);
const hasReqdRank = props.bladeburner.rank >= props.action.reqdRank;
const computedActionTimeCurrent = Math.min(
props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow,

@ -32,7 +32,7 @@ export function ContractElem(props: IProps): React.ReactElement {
props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow,
props.bladeburner.actionTimeToComplete,
);
const actionTime = props.action.getActionTime(props.bladeburner);
const actionTime = props.action.getActionTime(props.bladeburner, props.player);
const actionData = Contracts[props.action.name];
if (actionData === undefined) {

@ -33,7 +33,7 @@ export function OperationElem(props: IProps): React.ReactElement {
props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow,
props.bladeburner.actionTimeToComplete,
);
const actionTime = props.action.getActionTime(props.bladeburner);
const actionTime = props.action.getActionTime(props.bladeburner, props.player);
const actionData = Operations[props.action.name];
if (actionData === undefined) {

@ -4,6 +4,7 @@ import { StealthIcon } from "./StealthIcon";
import { KillIcon } from "./KillIcon";
import { IAction } from "../IAction";
import { IBladeburner } from "../IBladeburner";
import { Player } from "../../Player";
interface IProps {
bladeburner: IBladeburner;
@ -11,7 +12,7 @@ interface IProps {
}
export function SuccessChance(props: IProps): React.ReactElement {
const estimatedSuccessChance = props.action.getEstSuccessChance(props.bladeburner);
const estimatedSuccessChance = props.action.getEstSuccessChance(props.bladeburner, Player);
let chance = <></>;
if (estimatedSuccessChance[0] === estimatedSuccessChance[1]) {

@ -1,6 +1,6 @@
import { CONSTANTS } from "../Constants";
import { IPlayer } from "../PersonObjects/IPlayer";
import { IPlayerOrSleeve } from "../PersonObjects/IPlayerOrSleeve";
import { IPerson } from "../PersonObjects/IPerson";
import { IRouter } from "../ui/Router";
import { WorkerScript } from "../Netscript/WorkerScript";
import { CrimeType } from "../utils/WorkType";
@ -117,7 +117,7 @@ export class Crime {
return this.time;
}
successRate(p: IPlayerOrSleeve): number {
successRate(p: IPerson): number {
let chance: number =
this.hacking_success_weight * p.hacking +
this.strength_success_weight * p.strength +

@ -1,5 +1,5 @@
import { CONSTANTS } from "../Constants";
import { IPlayer } from "../PersonObjects/IPlayer";
import { CONSTANTS } from "../Constants";
export function getHospitalizationCost(p: IPlayer): number {
if (p.money < 0) {

@ -284,6 +284,7 @@ const sleeve: IMap<any> = {
getSleeveAugmentations: RamCostConstants.ScriptSleeveBaseRamCost,
getSleevePurchasableAugs: RamCostConstants.ScriptSleeveBaseRamCost,
purchaseSleeveAug: RamCostConstants.ScriptSleeveBaseRamCost,
setToBladeburnerAction: RamCostConstants.ScriptSleeveBaseRamCost,
};
// Stanek API

@ -125,7 +125,14 @@ export function NetscriptBladeburner(player: IPlayer, workerScript: WorkerScript
const bladeburner = player.bladeburner;
if (bladeburner === null) throw new Error("Should not be called without Bladeburner");
try {
return bladeburner.getActionTimeNetscriptFn(player, type, name, workerScript);
const time = bladeburner.getActionTimeNetscriptFn(player, type, name);
if (typeof time === "string") {
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
ctx.log(() => errorLogText);
return -1;
} else {
return time;
}
} catch (e: any) {
throw ctx.makeRuntimeErrorMsg(e);
}
@ -139,7 +146,14 @@ export function NetscriptBladeburner(player: IPlayer, workerScript: WorkerScript
const bladeburner = player.bladeburner;
if (bladeburner === null) throw new Error("Should not be called without Bladeburner");
try {
return bladeburner.getActionEstimatedSuccessChanceNetscriptFn(player, type, name, workerScript);
const chance = bladeburner.getActionEstimatedSuccessChanceNetscriptFn(player, type, name);
if (typeof chance === "string") {
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
ctx.log(() => errorLogText);
return [-1, -1];
} else {
return chance;
}
} catch (e: any) {
throw ctx.makeRuntimeErrorMsg(e);
}

@ -29,7 +29,7 @@ import { Router } from "../ui/GameRoot";
import { SpecialServers } from "../Server/data/SpecialServers";
import { Page } from "../ui/Router";
import { Locations } from "../Locations/Locations";
import { GetServer, AddToAllServers, createUniqueRandomIp } from "../Server/AllServers";
import { GetServer } from "../Server/AllServers";
import { Programs } from "../Programs/Programs";
import { numeralWrapper } from "../ui/numeralFormat";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
@ -39,7 +39,7 @@ import { Factions, factionExists } from "../Faction/Factions";
import { Faction } from "../Faction/Faction";
import { netscriptDelay } from "../NetscriptEvaluator";
import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions";
import { getServerOnNetwork, safetlyCreateUniqueServer } from "../Server/ServerHelpers";
import { getServerOnNetwork } from "../Server/ServerHelpers";
import { Terminal } from "../Terminal";
import { calculateHackingTime } from "../Hacking";
import { Server } from "../Server/Server";

@ -310,5 +310,36 @@ export function NetscriptSleeve(player: IPlayer): InternalAPI<ISleeve> {
return player.sleeves[sleeveNumber].tryBuyAugmentation(player, aug);
},
setToBladeburnerAction:
(ctx: NetscriptContext) =>
(_sleeveNumber: unknown, _action: unknown, _contract?: unknown): boolean => {
const sleeveNumber = ctx.helper.number("sleeveNumber", _sleeveNumber);
const action = ctx.helper.string("action", _action);
let contract: string;
if (typeof _contract === "undefined") {
contract = "------";
} else {
contract = ctx.helper.string("contract", _contract);
}
checkSleeveAPIAccess(ctx);
checkSleeveNumber(ctx, sleeveNumber);
// Cannot Take on Contracts if another sleeve is performing that action
if (action === "Take on contracts") {
for (let i = 0; i < player.sleeves.length; ++i) {
if (i === sleeveNumber) {
continue;
}
const other = player.sleeves[i];
if (other.currentTask === SleeveTaskType.Bladeburner && other.bbAction === action) {
throw ctx.helper.makeRuntimeErrorMsg(
`Sleeve ${sleeveNumber} cannot take of contracts because Sleeve ${i} is already performing that action.`,
);
}
}
}
return player.sleeves[sleeveNumber].bladeburner(player, action, contract);
},
};
}

@ -0,0 +1,66 @@
// Interface that represents either the player (PlayerObject) or
// a Sleeve. Used for functions that need to take in both.
import { IPlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation";
import { ITaskTracker } from "./ITaskTracker";
export interface IPerson {
// Stats
hacking: number;
strength: number;
defense: number;
dexterity: number;
agility: number;
charisma: number;
intelligence: number;
hp: number;
max_hp: number;
// Experience
hacking_exp: number;
strength_exp: number;
defense_exp: number;
dexterity_exp: number;
agility_exp: number;
charisma_exp: number;
intelligence_exp: number;
// Multipliers
hacking_exp_mult: number;
strength_exp_mult: number;
defense_exp_mult: number;
dexterity_exp_mult: number;
agility_exp_mult: number;
charisma_exp_mult: number;
hacking_mult: number;
strength_mult: number;
defense_mult: number;
dexterity_mult: number;
agility_mult: number;
charisma_mult: number;
company_rep_mult: number;
faction_rep_mult: number;
crime_money_mult: number;
crime_success_mult: number;
bladeburner_analysis_mult: number;
augmentations: IPlayerOwnedAugmentation[];
getIntelligenceBonus(weight: number): number;
gainHackingExp(exp: number): void;
gainStrengthExp(exp: number): void;
gainDefenseExp(exp: number): void;
gainDexterityExp(exp: number): void;
gainAgilityExp(exp: number): void;
gainCharismaExp(exp: number): void;
gainIntelligenceExp(exp: number): void;
gainStats(retValue: ITaskTracker): void;
calculateSkill(exp: number, mult?: number): number;
takeDamage(amt: number): boolean;
regenerateHp: (amt: number) => void;
queryStatFromString: (str: string) => number;
whoAmI: () => string;
}

@ -30,11 +30,10 @@ import { WorkerScript } from "../Netscript/WorkerScript";
import { HacknetServer } from "../Hacknet/HacknetServer";
import { ISkillProgress } from "./formulas/skill";
import { PlayerAchievement } from "../Achievements/Achievements";
import { IPerson } from "./IPerson";
import { WorkType, ClassType, CrimeType } from "../utils/WorkType";
export interface IPlayer {
// Class members
augmentations: IPlayerOwnedAugmentation[];
export interface IPlayer extends IPerson {
bitNodeN: number;
city: CityName;
companyName: string;
@ -186,13 +185,6 @@ export interface IPlayer {
canAccessGang(): boolean;
canAccessGrafting(): boolean;
canAfford(cost: number): boolean;
gainHackingExp(exp: number): void;
gainStrengthExp(exp: number): void;
gainDefenseExp(exp: number): void;
gainDexterityExp(exp: number): void;
gainAgilityExp(exp: number): void;
gainCharismaExp(exp: number): void;
gainIntelligenceExp(exp: number): void;
gainMoney(money: number, source: string): void;
getCurrentServer(): BaseServer;
getGangFaction(): Faction;
@ -215,7 +207,6 @@ export interface IPlayer {
process(router: IRouter, numCycles?: number): void;
reapplyAllAugmentations(resetMultipliers?: boolean): void;
reapplyAllSourceFiles(): void;
regenerateHp(amt: number): void;
setMoney(amt: number): void;
singularityStopWork(): string;
startBladeburner(p: any): void;
@ -242,12 +233,9 @@ export interface IPlayer {
startGang(facName: string, isHacking: boolean): void;
startWork(companyName: string): void;
startWorkPartTime(companyName: string): void;
takeDamage(amt: number): boolean;
travel(to: CityName): boolean;
giveExploit(exploit: Exploit): void;
giveAchievement(achievementId: string): void;
queryStatFromString(str: string): number;
getIntelligenceBonus(weight: number): number;
getCasinoWinnings(): number;
quitJob(company: string, sing?: boolean): void;
hasJob(): boolean;
@ -268,7 +256,6 @@ export interface IPlayer {
resetMultipliers(): void;
prestigeAugmentation(): void;
prestigeSourceFile(): void;
calculateSkill(exp: number, mult?: number): number;
calculateSkillProgress(exp: number, mult?: number): ISkillProgress;
resetWorkStatus(generalType?: WorkType, group?: string, workType?: string): void;
getWorkHackExpGain(): number;

@ -1,26 +0,0 @@
// Interface that represents either the player (PlayerObject) or
// a Sleeve. Used for functions that need to take in both.
export interface IPlayerOrSleeve {
// Stats
hacking: number;
strength: number;
defense: number;
dexterity: number;
agility: number;
charisma: number;
intelligence: number;
// Experience
hacking_exp: number;
strength_exp: number;
defense_exp: number;
dexterity_exp: number;
agility_exp: number;
charisma_exp: number;
// Multipliers
crime_success_mult: number;
getIntelligenceBonus(weight: number): number;
}

@ -0,0 +1,25 @@
// Interface that defines a generic object used to track experience/money
// earnings for tasks
export interface ITaskTracker {
hack: number;
str: number;
def: number;
dex: number;
agi: number;
cha: number;
int: number;
money: number;
}
export function createTaskTracker(): ITaskTracker {
return {
hack: 0,
str: 0,
def: 0,
dex: 0,
agi: 0,
cha: 0,
int: 0,
money: 0,
};
}

@ -1,4 +1,4 @@
// Base class representing a person-like object
import * as generalMethods from "./Player/PlayerObjectGeneralMethods";
import { Augmentation } from "../Augmentation/Augmentation";
import { IPlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
@ -6,32 +6,10 @@ import { CityName } from "../Locations/data/CityNames";
import { CONSTANTS } from "../Constants";
import { calculateSkill } from "./formulas/skill";
import { calculateIntelligenceBonus } from "./formulas/intelligence";
import { IPerson } from "./IPerson";
// Interface that defines a generic object used to track experience/money
// earnings for tasks
export interface ITaskTracker {
hack: number;
str: number;
def: number;
dex: number;
agi: number;
cha: number;
money: number;
}
export function createTaskTracker(): ITaskTracker {
return {
hack: 0,
str: 0,
def: 0,
dex: 0,
agi: 0,
cha: 0,
money: 0,
};
}
export abstract class Person {
// Base class representing a person-like object
export abstract class Person implements IPerson {
/**
* Stats
*/
@ -41,7 +19,7 @@ export abstract class Person {
dexterity = 1;
agility = 1;
charisma = 1;
intelligence = 1;
intelligence = 0;
hp = 10;
max_hp = 10;
@ -97,24 +75,28 @@ export abstract class Person {
bladeburner_analysis_mult = 1;
bladeburner_success_chance_mult = 1;
infiltration_base_rep_increase = 0;
infiltration_rep_mult = 1;
infiltration_trade_mult = 1;
infiltration_sell_mult = 1;
infiltration_timer_mult = 1;
infiltration_damage_reduction_mult = 1;
/**
* Augmentations
*/
augmentations: IPlayerOwnedAugmentation[] = [];
queuedAugmentations: IPlayerOwnedAugmentation[] = [];
/**
* City that the person is in
*/
city: CityName = CityName.Sector12;
gainHackingExp = generalMethods.gainHackingExp;
gainStrengthExp = generalMethods.gainStrengthExp;
gainDefenseExp = generalMethods.gainDefenseExp;
gainDexterityExp = generalMethods.gainDexterityExp;
gainAgilityExp = generalMethods.gainAgilityExp;
gainCharismaExp = generalMethods.gainCharismaExp;
gainIntelligenceExp = generalMethods.gainIntelligenceExp;
gainStats = generalMethods.gainStats;
calculateSkill = generalMethods.calculateSkill;
regenerateHp = generalMethods.regenerateHp;
queryStatFromString = generalMethods.queryStatFromString;
/**
* Updates this object's multipliers for the given augmentation
*/
@ -213,13 +195,6 @@ export abstract class Person {
this.bladeburner_stamina_gain_mult = 1;
this.bladeburner_analysis_mult = 1;
this.bladeburner_success_chance_mult = 1;
this.infiltration_base_rep_increase = 0;
this.infiltration_rep_mult = 1;
this.infiltration_trade_mult = 1;
this.infiltration_sell_mult = 1;
this.infiltration_timer_mult = 1;
this.infiltration_damage_reduction_mult = 1;
}
/**
@ -265,4 +240,8 @@ export abstract class Person {
getIntelligenceBonus(weight: number): number {
return calculateIntelligenceBonus(this.intelligence, weight);
}
abstract takeDamage(amt: number): boolean;
abstract whoAmI(): string;
}

@ -37,6 +37,7 @@ import { ISkillProgress } from "../formulas/skill";
import { PlayerAchievement } from "../../Achievements/Achievements";
import { cyrb53 } from "../../utils/StringHelperFunctions";
import { getRandomInt } from "../../utils/helpers/getRandomInt";
import { ITaskTracker } from "../ITaskTracker";
import { CONSTANTS } from "../../Constants";
import { WorkType, ClassType, CrimeType, PlayerFactionWorkType } from "../../utils/WorkType";
@ -203,6 +204,7 @@ export class PlayerObject implements IPlayer {
gainAgilityExp: (exp: number) => void;
gainCharismaExp: (exp: number) => void;
gainIntelligenceExp: (exp: number) => void;
gainStats: (retValue: ITaskTracker) => void;
gainMoney: (money: number, source: string) => void;
getCurrentServer: () => BaseServer;
getGangFaction: () => Faction;
@ -524,6 +526,7 @@ export class PlayerObject implements IPlayer {
this.gainAgilityExp = generalMethods.gainAgilityExp;
this.gainCharismaExp = generalMethods.gainCharismaExp;
this.gainIntelligenceExp = generalMethods.gainIntelligenceExp;
this.gainStats = generalMethods.gainStats;
this.queryStatFromString = generalMethods.queryStatFromString;
this.resetWorkStatus = generalMethods.resetWorkStatus;
this.processWorkEarnings = generalMethods.processWorkEarnings;
@ -632,6 +635,10 @@ export class PlayerObject implements IPlayer {
this.applyEntropy = augmentationMethods.applyEntropy;
}
whoAmI(): string {
return "Player";
}
/**
* Serialize the current object to a JSON save state.
*/

@ -64,6 +64,9 @@ import { SnackbarEvents, ToastVariant } from "../../ui/React/Snackbar";
import { calculateClassEarnings } from "../formulas/work";
import { achievements } from "../../Achievements/Achievements";
import { FactionNames } from "../../Faction/data/FactionNames";
import { ITaskTracker } from "../ITaskTracker";
import { IPerson } from "../IPerson";
import { Player } from "../../Player";
import { graftingIntBonus } from "../Grafting/GraftingHelpers";
import { WorkType, PlayerFactionWorkType, ClassType, CrimeType } from "../../utils/WorkType";
@ -228,7 +231,7 @@ export function receiveInvite(this: IPlayer, factionName: string): void {
}
//Calculates skill level based on experience. The same formula will be used for every skill
export function calculateSkill(this: IPlayer, exp: number, mult = 1): number {
export function calculateSkill(this: IPerson, exp: number, mult = 1): number {
return calculateSkillF(exp, mult);
}
@ -379,7 +382,7 @@ export function recordMoneySource(this: PlayerObject, amt: number, source: strin
this.moneySourceB.record(amt, source);
}
export function gainHackingExp(this: IPlayer, exp: number): void {
export function gainHackingExp(this: IPerson, exp: number): void {
if (isNaN(exp)) {
console.error("ERR: NaN passed into Player.gainHackingExp()");
return;
@ -392,7 +395,7 @@ export function gainHackingExp(this: IPlayer, exp: number): void {
this.hacking = calculateSkillF(this.hacking_exp, this.hacking_mult * BitNodeMultipliers.HackingLevelMultiplier);
}
export function gainStrengthExp(this: IPlayer, exp: number): void {
export function gainStrengthExp(this: IPerson, exp: number): void {
if (isNaN(exp)) {
console.error("ERR: NaN passed into Player.gainStrengthExp()");
return;
@ -405,7 +408,7 @@ export function gainStrengthExp(this: IPlayer, exp: number): void {
this.strength = calculateSkillF(this.strength_exp, this.strength_mult * BitNodeMultipliers.StrengthLevelMultiplier);
}
export function gainDefenseExp(this: IPlayer, exp: number): void {
export function gainDefenseExp(this: IPerson, exp: number): void {
if (isNaN(exp)) {
console.error("ERR: NaN passed into player.gainDefenseExp()");
return;
@ -421,7 +424,7 @@ export function gainDefenseExp(this: IPlayer, exp: number): void {
this.hp = Math.round(this.max_hp * ratio);
}
export function gainDexterityExp(this: IPlayer, exp: number): void {
export function gainDexterityExp(this: IPerson, exp: number): void {
if (isNaN(exp)) {
console.error("ERR: NaN passed into Player.gainDexterityExp()");
return;
@ -437,7 +440,7 @@ export function gainDexterityExp(this: IPlayer, exp: number): void {
);
}
export function gainAgilityExp(this: IPlayer, exp: number): void {
export function gainAgilityExp(this: IPerson, exp: number): void {
if (isNaN(exp)) {
console.error("ERR: NaN passed into Player.gainAgilityExp()");
return;
@ -450,7 +453,7 @@ export function gainAgilityExp(this: IPlayer, exp: number): void {
this.agility = calculateSkillF(this.agility_exp, this.agility_mult * BitNodeMultipliers.AgilityLevelMultiplier);
}
export function gainCharismaExp(this: IPlayer, exp: number): void {
export function gainCharismaExp(this: IPerson, exp: number): void {
if (isNaN(exp)) {
console.error("ERR: NaN passed into Player.gainCharismaExp()");
return;
@ -463,17 +466,27 @@ export function gainCharismaExp(this: IPlayer, exp: number): void {
this.charisma = calculateSkillF(this.charisma_exp, this.charisma_mult * BitNodeMultipliers.CharismaLevelMultiplier);
}
export function gainIntelligenceExp(this: IPlayer, exp: number): void {
export function gainIntelligenceExp(this: IPerson, exp: number): void {
if (isNaN(exp)) {
console.error("ERROR: NaN passed into Player.gainIntelligenceExp()");
return;
}
if (this.sourceFileLvl(5) > 0 || this.intelligence > 0) {
if (Player.sourceFileLvl(5) > 0 || this.intelligence > 0) {
this.intelligence_exp += exp;
this.intelligence = Math.floor(this.calculateSkill(this.intelligence_exp));
this.intelligence = Math.floor(this.calculateSkill(this.intelligence_exp, 1));
}
}
export function gainStats(this: IPerson, retValue: ITaskTracker): void {
this.gainHackingExp(retValue.hack * this.hacking_exp_mult);
this.gainStrengthExp(retValue.str * this.strength_exp_mult);
this.gainDefenseExp(retValue.def * this.defense_exp_mult);
this.gainDexterityExp(retValue.dex * this.dexterity_exp_mult);
this.gainAgilityExp(retValue.agi * this.agility_exp_mult);
this.gainCharismaExp(retValue.cha * this.charisma_exp_mult);
this.gainIntelligenceExp(retValue.int);
}
//Given a string expression like "str" or "strength", returns the given stat
export function queryStatFromString(this: IPlayer, str: string): number {
const tempStr = str.toLowerCase();
@ -1726,7 +1739,7 @@ export function takeDamage(this: IPlayer, amt: number): boolean {
}
}
export function regenerateHp(this: IPlayer, amt: number): void {
export function regenerateHp(this: IPerson, amt: number): void {
if (typeof amt !== "number") {
console.warn(`Player.regenerateHp() called without a numeric argument: ${amt}`);
return;

@ -9,7 +9,8 @@
import { SleeveTaskType } from "./SleeveTaskTypesEnum";
import { IPlayer } from "../IPlayer";
import { Person, ITaskTracker, createTaskTracker } from "../Person";
import { Person } from "../Person";
import { ITaskTracker, createTaskTracker } from "../ITaskTracker";
import { Augmentation } from "../../Augmentation/Augmentation";
@ -33,6 +34,9 @@ import { CityName } from "../../Locations/data/CityNames";
import { LocationName } from "../../Locations/data/LocationNames";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver";
import { BladeburnerConstants } from "../../Bladeburner/data/Constants";
import { numeralWrapper } from "../../ui/numeralFormat";
import { capitalizeFirstLetter, capitalizeEachWord } from "../../utils/StringHelperFunctions";
export class Sleeve extends Person {
/**
@ -58,6 +62,7 @@ export class Sleeve extends Person {
* Faction/Company Work: Name of Faction/Company
* Crime: Money earned if successful
* Class/Gym: Name of university/gym
* Bladeburner: success chance
*/
currentTaskLocation = "";
@ -101,6 +106,16 @@ export class Sleeve extends Person {
*/
gymStatType = "";
/**
* String that stores what stat the sleeve is training at the gym
*/
bbAction = "";
/**
* String that stores what stat the sleeve is training at the gym
*/
bbContract = "";
/**
* Keeps track of events/notifications for this sleeve
*/
@ -151,7 +166,7 @@ export class Sleeve extends Person {
if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p);
} else {
this.resetTaskStatus();
this.resetTaskStatus(p);
}
this.gainRatesForTask.hack = crime.hacking_exp * this.hacking_exp_mult * BitNodeMultipliers.CrimeExpGain;
@ -160,6 +175,7 @@ export class Sleeve extends Person {
this.gainRatesForTask.dex = crime.dexterity_exp * this.dexterity_exp_mult * BitNodeMultipliers.CrimeExpGain;
this.gainRatesForTask.agi = crime.agility_exp * this.agility_exp_mult * BitNodeMultipliers.CrimeExpGain;
this.gainRatesForTask.cha = crime.charisma_exp * this.charisma_exp_mult * BitNodeMultipliers.CrimeExpGain;
this.gainRatesForTask.int = crime.intelligence_exp;
this.gainRatesForTask.money = crime.money * this.crime_money_mult * BitNodeMultipliers.CrimeMoney;
this.currentTaskLocation = String(this.gainRatesForTask.money);
@ -182,7 +198,7 @@ export class Sleeve extends Person {
const crime: Crime | undefined = Object.values(Crimes).find((crime) => crime.name === this.crimeType);
if (!crime) {
console.error(`Invalid data stored in sleeve.crimeType: ${this.crimeType}`);
this.resetTaskStatus();
this.resetTaskStatus(p);
return retValue;
}
if (Math.random() < crime.successRate(this)) {
@ -206,11 +222,60 @@ export class Sleeve extends Person {
this.currentTaskTime = 0;
return retValue;
}
} else {
// For other crimes... I dont think anything else needs to be done
} else if (this.currentTask === SleeveTaskType.Bladeburner) {
if (this.currentTaskMaxTime === 0) {
this.currentTaskTime = 0;
return retValue;
}
// For bladeburner, all experience and money is gained at the end
const bb = p.bladeburner;
if (bb === null) {
const errorLogText = `bladeburner is null`;
console.error(`Function: sleeves.finishTask; Message: '${errorLogText}'`);
this.resetTaskStatus(p);
return retValue;
}
this.resetTaskStatus();
if (this.currentTaskTime >= this.currentTaskMaxTime) {
if (this.bbAction === "Infiltrate synthoids") {
bb.infiltrateSynthoidCommunities(p);
this.currentTaskTime = 0;
return retValue;
}
let type: string;
let name: string;
if (this.bbAction === "Take on contracts") {
type = "Contracts";
name = this.bbContract;
} else {
type = "General";
name = this.bbAction;
}
const actionIdent = bb.getActionIdFromTypeAndName(type, name);
if (actionIdent === null) {
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
console.error(`Function: sleeves.finishTask; Message: '${errorLogText}'`);
this.resetTaskStatus(p);
return retValue;
}
const action = bb.getActionObject(actionIdent);
if ((action?.count ?? 0) > 0) {
const bbRetValue = bb.completeAction(p, this, actionIdent, false);
if (bbRetValue) {
retValue = this.gainExperience(p, bbRetValue);
this.gainMoney(p, bbRetValue);
// Do not reset task to IDLE
this.currentTaskTime = 0;
return retValue;
}
}
}
}
this.resetTaskStatus(p);
return retValue;
}
@ -260,50 +325,56 @@ export class Sleeve extends Person {
const pDexExp = exp.dex * multFac;
const pAgiExp = exp.agi * multFac;
const pChaExp = exp.cha * multFac;
const pIntExp = exp.int * multFac;
// Experience is gained by both this sleeve and player
if (pHackExp > 0) {
this.hacking_exp += pHackExp;
this.gainHackingExp(pHackExp);
p.gainHackingExp(pHackExp);
this.earningsForPlayer.hack += pHackExp;
this.earningsForTask.hack += pHackExp;
}
if (pStrExp > 0) {
this.strength_exp += pStrExp;
this.gainStrengthExp(pStrExp);
p.gainStrengthExp(pStrExp);
this.earningsForPlayer.str += pStrExp;
this.earningsForTask.str += pStrExp;
}
if (pDefExp > 0) {
this.defense_exp += pDefExp;
this.gainDefenseExp(pDefExp);
p.gainDefenseExp(pDefExp);
this.earningsForPlayer.def += pDefExp;
this.earningsForTask.def += pDefExp;
}
if (pDexExp > 0) {
this.dexterity_exp += pDexExp;
this.gainDexterityExp(pDexExp);
p.gainDexterityExp(pDexExp);
this.earningsForPlayer.dex += pDexExp;
this.earningsForTask.dex += pDexExp;
}
if (pAgiExp > 0) {
this.agility_exp += pAgiExp;
this.gainAgilityExp(pAgiExp);
p.gainAgilityExp(pAgiExp);
this.earningsForPlayer.agi += pAgiExp;
this.earningsForTask.agi += pAgiExp;
}
if (pChaExp > 0) {
this.charisma_exp += pChaExp;
this.gainCharismaExp(pChaExp);
p.gainCharismaExp(pChaExp);
this.earningsForPlayer.cha += pChaExp;
this.earningsForTask.cha += pChaExp;
}
if (pIntExp > 0) {
this.gainIntelligenceExp(pIntExp);
p.gainIntelligenceExp(pIntExp);
}
// Record earnings for other sleeves
this.earningsForSleeves.hack += pHackExp * (this.sync / 100);
this.earningsForSleeves.str += pStrExp * (this.sync / 100);
@ -320,7 +391,8 @@ export class Sleeve extends Person {
dex: pDexExp * (this.sync / 100),
agi: pAgiExp * (this.sync / 100),
cha: pChaExp * (this.sync / 100),
money: 0,
int: pIntExp * (this.sync / 100),
money: exp.money,
};
}
@ -445,7 +517,7 @@ export class Sleeve extends Person {
this.charisma_exp = 0;
// Reset task-related stuff
this.resetTaskStatus();
this.resetTaskStatus(p);
this.earningsForSleeves = createTaskTracker();
this.earningsForPlayer = createTaskTracker();
this.shockRecovery(p);
@ -523,7 +595,7 @@ export class Sleeve extends Person {
// for, we need to reset the sleeve's task
if (p.gang) {
if (fac.name === p.gang.facName) {
this.resetTaskStatus();
this.resetTaskStatus(p);
}
}
@ -545,18 +617,18 @@ export class Sleeve extends Person {
}
case SleeveTaskType.Recovery:
this.shock = Math.min(100, this.shock + 0.0002 * cyclesUsed);
if (this.shock >= 100) this.resetTaskStatus();
if (this.shock >= 100) this.resetTaskStatus(p);
break;
case SleeveTaskType.Synchro:
this.sync = Math.min(100, this.sync + p.getIntelligenceBonus(0.5) * 0.0002 * cyclesUsed);
if (this.sync >= 100) this.resetTaskStatus();
if (this.sync >= 100) this.resetTaskStatus(p);
break;
default:
break;
}
if (this.currentTaskMaxTime !== 0 && this.currentTaskTime >= this.currentTaskMaxTime) {
if (this.currentTask === SleeveTaskType.Crime) {
if (this.currentTask === SleeveTaskType.Crime || this.currentTask === SleeveTaskType.Bladeburner) {
retValue = this.finishTask(p);
} else {
this.finishTask(p);
@ -573,7 +645,16 @@ export class Sleeve extends Person {
/**
* Resets all parameters used to keep information about the current task
*/
resetTaskStatus(): void {
resetTaskStatus(p: IPlayer): void {
if (this.bbAction == "Support main sleeve") {
p.bladeburner?.sleeveSupport(false);
}
if (this.currentTask == SleeveTaskType.Class) {
const retVal = createTaskTracker();
retVal.int = CONSTANTS.IntelligenceClassBaseExpGain * Math.round(this.currentTaskTime / 1000);
const r = this.gainExperience(p, retVal);
p.sleeves.filter((s) => s != this).forEach((s) => s.gainExperience(p, r, 1, true));
}
this.earningsForTask = createTaskTracker();
this.gainRatesForTask = createTaskTracker();
this.currentTask = SleeveTaskType.Idle;
@ -584,13 +665,15 @@ export class Sleeve extends Person {
this.currentTaskLocation = "";
this.gymStatType = "";
this.className = "";
this.bbAction = "";
this.bbContract = "------";
}
shockRecovery(p: IPlayer): boolean {
if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p);
} else {
this.resetTaskStatus();
this.resetTaskStatus(p);
}
this.currentTask = SleeveTaskType.Recovery;
@ -601,7 +684,7 @@ export class Sleeve extends Person {
if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p);
} else {
this.resetTaskStatus();
this.resetTaskStatus(p);
}
this.currentTask = SleeveTaskType.Synchro;
@ -615,7 +698,7 @@ export class Sleeve extends Person {
if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p);
} else {
this.resetTaskStatus();
this.resetTaskStatus(p);
}
// Set exp/money multipliers based on which university.
@ -809,7 +892,7 @@ export class Sleeve extends Person {
if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p);
} else {
this.resetTaskStatus();
this.resetTaskStatus(p);
}
const company: Company | null = Companies[companyName];
@ -875,7 +958,7 @@ export class Sleeve extends Person {
if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p);
} else {
this.resetTaskStatus();
this.resetTaskStatus(p);
}
const factionInfo = faction.getInfo();
@ -926,7 +1009,7 @@ export class Sleeve extends Person {
if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p);
} else {
this.resetTaskStatus();
this.resetTaskStatus(p);
}
// Set exp/money multipliers based on which university.
@ -994,6 +1077,162 @@ export class Sleeve extends Person {
return true;
}
/**
* Begin a bladeburner task
*/
bladeburner(p: IPlayer, action: string, contract: string): boolean {
if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p);
} else {
this.resetTaskStatus(p);
}
this.gainRatesForTask.hack = 0;
this.gainRatesForTask.str = 0;
this.gainRatesForTask.def = 0;
this.gainRatesForTask.dex = 0;
this.gainRatesForTask.agi = 0;
this.gainRatesForTask.cha = 0;
this.gainRatesForTask.money = 0;
this.currentTaskLocation = "";
let time = 0;
this.bbContract = "------";
switch (action) {
case "Field analysis":
time = this.getBladeburnerActionTime(p, "General", action);
this.gainRatesForTask.hack = 20 * this.hacking_exp_mult;
this.gainRatesForTask.cha = 20 * this.charisma_exp_mult;
break;
case "Recruitment":
time = this.getBladeburnerActionTime(p, "General", action);
this.gainRatesForTask.cha =
2 * BladeburnerConstants.BaseStatGain * (p.bladeburner?.getRecruitmentTime(this) ?? 0) * 1000;
this.currentTaskLocation = `(Success Rate: ${numeralWrapper.formatPercentage(
this.recruitmentSuccessChance(p),
)})`;
break;
case "Diplomacy":
time = this.getBladeburnerActionTime(p, "General", action);
break;
case "Infiltrate synthoids":
time = 60000;
this.currentTaskLocation = "This will generate additional contracts and operations";
break;
case "Support main sleeve":
p.bladeburner?.sleeveSupport(true);
time = 0;
break;
case "Take on contracts":
time = this.getBladeburnerActionTime(p, "Contracts", contract);
this.contractGainRates(p, "Contracts", contract);
this.currentTaskLocation = this.contractSuccessChance(p, "Contracts", contract);
this.bbContract = capitalizeEachWord(contract.toLowerCase());
break;
}
this.bbAction = capitalizeFirstLetter(action.toLowerCase());
this.currentTaskMaxTime = time;
this.currentTask = SleeveTaskType.Bladeburner;
return true;
}
recruitmentSuccessChance(p: IPlayer): number {
return Math.max(0, Math.min(1, p.bladeburner?.getRecruitmentSuccessChance(this) ?? 0));
}
contractSuccessChance(p: IPlayer, type: string, name: string): string {
const bb = p.bladeburner;
if (bb === null) {
const errorLogText = `bladeburner is null`;
console.error(`Function: sleeves.contractSuccessChance; Message: '${errorLogText}'`);
return "0%";
}
const chances = bb.getActionEstimatedSuccessChanceNetscriptFn(this, type, name);
if (typeof chances === "string") {
console.error(`Function: sleeves.contractSuccessChance; Message: '${chances}'`);
return "0%";
}
if (chances[0] >= 1) {
return "100%";
} else {
return `${numeralWrapper.formatPercentage(chances[0])} - ${numeralWrapper.formatPercentage(chances[1])}`;
}
}
contractGainRates(p: IPlayer, type: string, name: string): void {
const bb = p.bladeburner;
if (bb === null) {
const errorLogText = `bladeburner is null`;
console.error(`Function: sleeves.contractGainRates; Message: '${errorLogText}'`);
return;
}
const actionIdent = bb.getActionIdFromTypeAndName(type, name);
if (actionIdent === null) {
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
console.error(`Function: sleeves.contractGainRates; Message: '${errorLogText}'`);
this.resetTaskStatus(p);
return;
}
const action = bb.getActionObject(actionIdent);
if (action === null) {
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
console.error(`Function: sleeves.contractGainRates; Message: '${errorLogText}'`);
this.resetTaskStatus(p);
return;
}
const retValue = bb.getActionStats(action, true);
this.gainRatesForTask.hack = retValue.hack;
this.gainRatesForTask.str = retValue.str;
this.gainRatesForTask.def = retValue.def;
this.gainRatesForTask.dex = retValue.dex;
this.gainRatesForTask.agi = retValue.agi;
this.gainRatesForTask.cha = retValue.cha;
const rewardMultiplier = Math.pow(action.rewardFac, action.level - 1);
this.gainRatesForTask.money =
BladeburnerConstants.ContractBaseMoneyGain * rewardMultiplier * bb.skillMultipliers.money;
}
getBladeburnerActionTime(p: IPlayer, type: string, name: string): number {
//Maybe find workerscript and use original
const bb = p.bladeburner;
if (bb === null) {
const errorLogText = `bladeburner is null`;
console.error(`Function: sleeves.getBladeburnerActionTime; Message: '${errorLogText}'`);
return -1;
}
const time = bb.getActionTimeNetscriptFn(this, type, name);
if (typeof time === "string") {
const errorLogText = `Invalid action: type='${type}' name='${name}'`;
console.error(`Function: sleeves.getBladeburnerActionTime; Message: '${errorLogText}'`);
return -1;
} else {
return time;
}
}
takeDamage(amt: number): boolean {
if (typeof amt !== "number") {
console.warn(`Player.takeDamage() called without a numeric argument: ${amt}`);
return false;
}
this.hp -= amt;
if (this.hp <= 0) {
this.shock += 0.5;
this.hp = this.max_hp;
return true;
} else {
return false;
}
}
whoAmI(): string {
return "Sleeve";
}
/**
* Serialize the current object to a JSON save state.
*/

@ -9,6 +9,7 @@ export enum SleeveTaskType {
Crime,
Class,
Gym,
Bladeburner,
Recovery,
Synchro,
}

@ -30,7 +30,7 @@ export function SleeveElem(props: IProps): React.ReactElement {
const [abc, setABC] = useState(["------", "------", "------"]);
function setTask(): void {
props.sleeve.resetTaskStatus(); // sets to idle
props.sleeve.resetTaskStatus(player); // sets to idle
switch (abc[0]) {
case "------":
break;
@ -49,6 +49,9 @@ export function SleeveElem(props: IProps): React.ReactElement {
case "Workout at Gym":
props.sleeve.workoutAtGym(player, abc[2], abc[1]);
break;
case "Perform Bladeburner Actions":
props.sleeve.bladeburner(player, abc[1], abc[2]);
break;
case "Shock Recovery":
props.sleeve.shockRecovery(player);
break;
@ -106,6 +109,20 @@ export function SleeveElem(props: IProps): React.ReactElement {
case SleeveTaskType.Gym:
desc = <>This sleeve is currently working out at {props.sleeve.currentTaskLocation}.</>;
break;
case SleeveTaskType.Bladeburner: {
let message = "";
if (props.sleeve.bbContract !== "------") {
message = ` - ${props.sleeve.bbContract} (Success Rate: ${props.sleeve.currentTaskLocation})`;
} else if (props.sleeve.currentTaskLocation !== "") {
message = props.sleeve.currentTaskLocation;
}
desc = (
<>
This sleeve is currently attempting to {props.sleeve.bbAction}. {message}
</>
);
break;
}
case SleeveTaskType.Recovery:
desc = (
<>
@ -168,7 +185,9 @@ export function SleeveElem(props: IProps): React.ReactElement {
</Button>
<Typography>{desc}</Typography>
<Typography>
{props.sleeve.currentTask === SleeveTaskType.Crime && (
{(props.sleeve.currentTask === SleeveTaskType.Crime ||
props.sleeve.currentTask === SleeveTaskType.Bladeburner) &&
props.sleeve.currentTaskMaxTime > 0 && (
<ProgressBar
variant="determinate"
value={(props.sleeve.currentTaskTime / props.sleeve.currentTaskMaxTime) * 100}

@ -22,6 +22,15 @@ const universitySelectorOptions: string[] = [
const gymSelectorOptions: string[] = ["Train Strength", "Train Defense", "Train Dexterity", "Train Agility"];
const bladeburnerSelectorOptions: string[] = [
"Field analysis",
"Recruitment",
"Diplomacy",
"Infiltrate synthoids",
"Support main sleeve",
"Take on contracts",
];
interface IProps {
sleeve: Sleeve;
player: IPlayer;
@ -84,6 +93,26 @@ function possibleFactions(player: IPlayer, sleeve: Sleeve): string[] {
});
}
function possibleContracts(player: IPlayer, sleeve: Sleeve): string[] {
const bb = player.bladeburner;
if (bb === null) {
return ["------"];
}
let contracts = bb.getContractNamesNetscriptFn();
for (const otherSleeve of player.sleeves) {
if (sleeve === otherSleeve) {
continue;
}
if (otherSleeve.currentTask === SleeveTaskType.Bladeburner && otherSleeve.bbAction == "Take on contracts") {
contracts = contracts.filter((x) => x != otherSleeve.bbContract);
}
}
if (contracts.length === 0) {
return ["------"];
}
return contracts;
}
const tasks: {
[key: string]: undefined | ((player: IPlayer, sleeve: Sleeve) => ITaskDetails);
["------"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails;
@ -92,6 +121,7 @@ const tasks: {
["Commit Crime"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails;
["Take University Course"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails;
["Workout at Gym"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails;
["Perform Bladeburner Actions"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails;
["Shock Recovery"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails;
["Synchronize"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails;
} = {
@ -170,6 +200,18 @@ const tasks: {
return { first: gymSelectorOptions, second: () => gyms };
},
"Perform Bladeburner Actions": (player: IPlayer, sleeve: Sleeve): ITaskDetails => {
return {
first: bladeburnerSelectorOptions,
second: (s1: string) => {
if (s1 === "Take on contracts") {
return possibleContracts(player, sleeve);
} else {
return ["------"];
}
},
};
},
"Shock Recovery": (): ITaskDetails => {
return { first: ["------"], second: () => ["------"] };
},
@ -186,6 +228,7 @@ const canDo: {
["Commit Crime"]: (player: IPlayer, sleeve: Sleeve) => boolean;
["Take University Course"]: (player: IPlayer, sleeve: Sleeve) => boolean;
["Workout at Gym"]: (player: IPlayer, sleeve: Sleeve) => boolean;
["Perform Bladeburner Actions"]: (player: IPlayer, sleeve: Sleeve) => boolean;
["Shock Recovery"]: (player: IPlayer, sleeve: Sleeve) => boolean;
["Synchronize"]: (player: IPlayer, sleeve: Sleeve) => boolean;
} = {
@ -197,6 +240,7 @@ const canDo: {
[CityName.Aevum, CityName.Sector12, CityName.Volhaven].includes(sleeve.city),
"Workout at Gym": (player: IPlayer, sleeve: Sleeve) =>
[CityName.Aevum, CityName.Sector12, CityName.Volhaven].includes(sleeve.city),
"Perform Bladeburner Actions": (player: IPlayer, _: Sleeve) => player.inBladeburner(),
"Shock Recovery": (player: IPlayer, sleeve: Sleeve) => sleeve.shock < 100,
Synchronize: (player: IPlayer, sleeve: Sleeve) => sleeve.sync < 100,
};
@ -228,6 +272,8 @@ function getABC(sleeve: Sleeve): [string, string, string] {
return ["Take University Course", sleeve.className, sleeve.currentTaskLocation];
case SleeveTaskType.Gym:
return ["Workout at Gym", sleeve.gymStatType, sleeve.currentTaskLocation];
case SleeveTaskType.Bladeburner:
return ["Perform Bladeburner Actions", sleeve.bbAction, sleeve.bbContract];
case SleeveTaskType.Recovery:
return ["Shock Recovery", "------", "------"];
case SleeveTaskType.Synchro:

@ -26,7 +26,7 @@ export function TravelModal(props: IProps): React.ReactElement {
}
props.sleeve.city = city as CityName;
player.loseMoney(CONSTANTS.TravelCost, "sleeve");
props.sleeve.resetTaskStatus();
props.sleeve.resetTaskStatus(player);
props.rerender();
props.onClose();
}

@ -3789,6 +3789,20 @@ export interface Sleeve {
* @returns True if the aug was purchased and installed on the sleeve, false otherwise.
*/
purchaseSleeveAug(sleeveNumber: number, augName: string): boolean;
/**
* Set a sleeve to perform bladeburner actions.
* @remarks
* RAM cost: 4 GB
*
* Return a boolean indicating whether or not the sleeve started working out.
*
* @param sleeveNumber - Index of the sleeve to workout at the gym.
* @param action - Name of the action to be performed.
* @param contract - Name of the contract if applicable.
* @returns True if the sleeve started working out, false otherwise.
*/
setToBladeburnerAction(sleeveNumber: number, action: string, contract?: string): boolean;
}
/**

@ -117,6 +117,17 @@ function cyrb53(str: string, seed = 0): string {
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16);
}
function capitalizeFirstLetter(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}
function capitalizeEachWord(s: string): string {
return s
.split(" ")
.map((word) => capitalizeFirstLetter(word))
.join(" ");
}
export {
convertTimeMsToTimeElapsedString,
longestCommonStart,
@ -124,4 +135,6 @@ export {
formatNumber,
generateRandomString,
cyrb53,
capitalizeFirstLetter,
capitalizeEachWord,
};