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", "name": "bitburner",
"version": "1.6.4", "version": "1.7.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bitburner", "name": "bitburner",
"version": "1.6.4", "version": "1.7.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "SEE LICENSE IN license.txt", "license": "SEE LICENSE IN license.txt",
"dependencies": { "dependencies": {

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

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

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

@ -3,6 +3,8 @@ import { City } from "./City";
import { Skill } from "./Skill"; import { Skill } from "./Skill";
import { IAction } from "./IAction"; import { IAction } from "./IAction";
import { IPlayer } from "../PersonObjects/IPlayer"; import { IPlayer } from "../PersonObjects/IPlayer";
import { IPerson } from "../PersonObjects/IPerson";
import { ITaskTracker } from "../PersonObjects/ITaskTracker";
import { IRouter } from "../ui/Router"; import { IRouter } from "../ui/Router";
import { WorkerScript } from "../Netscript/WorkerScript"; import { WorkerScript } from "../Netscript/WorkerScript";
@ -70,13 +72,8 @@ export interface IBladeburner {
getGeneralActionNamesNetscriptFn(): string[]; getGeneralActionNamesNetscriptFn(): string[];
getSkillNamesNetscriptFn(): string[]; getSkillNamesNetscriptFn(): string[];
startActionNetscriptFn(player: IPlayer, type: string, name: string, workerScript: WorkerScript): boolean; startActionNetscriptFn(player: IPlayer, type: string, name: string, workerScript: WorkerScript): boolean;
getActionTimeNetscriptFn(player: IPlayer, type: string, name: string, workerScript: WorkerScript): number; getActionTimeNetscriptFn(person: IPerson, type: string, name: string): number | string;
getActionEstimatedSuccessChanceNetscriptFn( getActionEstimatedSuccessChanceNetscriptFn(person: IPerson, type: string, name: string): [number, number] | string;
player: IPlayer,
type: string,
name: string,
workerScript: WorkerScript,
): [number, number];
getActionCountRemainingNetscriptFn(type: string, name: string, workerScript: WorkerScript): number; getActionCountRemainingNetscriptFn(type: string, name: string, workerScript: WorkerScript): number;
getSkillLevelNetscriptFn(skillName: string, workerScript: WorkerScript): number; getSkillLevelNetscriptFn(skillName: string, workerScript: WorkerScript): number;
getSkillUpgradeCostNetscriptFn(skillName: string, workerScript: WorkerScript): number; getSkillUpgradeCostNetscriptFn(skillName: string, workerScript: WorkerScript): number;
@ -95,20 +92,22 @@ export interface IBladeburner {
triggerMigration(sourceCityName: string): void; triggerMigration(sourceCityName: string): void;
triggerPotentialMigration(sourceCityName: string, chance: number): void; triggerPotentialMigration(sourceCityName: string, chance: number): void;
randomEvent(): void; randomEvent(): void;
gainActionStats(player: IPlayer, action: IAction, success: boolean): void;
getDiplomacyEffectiveness(player: IPlayer): number; getDiplomacyEffectiveness(player: IPlayer): number;
getRecruitmentSuccessChance(player: IPlayer): number; getRecruitmentSuccessChance(player: IPerson): number;
getRecruitmentTime(player: IPlayer): number; getRecruitmentTime(player: IPerson): number;
resetSkillMultipliers(): void; resetSkillMultipliers(): void;
updateSkillMultipliers(): void; updateSkillMultipliers(): void;
completeOperation(success: boolean): void; completeOperation(success: boolean, player: IPlayer): void;
getActionObject(actionId: IActionIdentifier): IAction | null; getActionObject(actionId: IActionIdentifier): IAction | null;
completeContract(success: boolean): void; completeContract(success: boolean, actionIdent: IActionIdentifier): void;
completeAction(router: IRouter, player: IPlayer): void; completeAction(player: IPlayer, person: IPerson, actionIdent: IActionIdentifier, isPlayer?: boolean): ITaskTracker;
infiltrateSynthoidCommunities(p: IPlayer): void;
changeRank(player: IPlayer, change: number): void; changeRank(player: IPlayer, change: number): void;
processAction(router: IRouter, player: IPlayer, seconds: number): void; processAction(router: IRouter, player: IPlayer, seconds: number): void;
calculateStaminaGainPerSecond(player: IPlayer): number; calculateStaminaGainPerSecond(player: IPlayer): number;
calculateMaxStamina(player: IPlayer): void; calculateMaxStamina(player: IPlayer): void;
create(): void; create(): void;
process(router: IRouter, player: IPlayer): 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 = const isActive =
props.bladeburner.action.type === ActionTypes["BlackOperation"] && props.bladeburner.action.type === ActionTypes["BlackOperation"] &&
props.action.name === props.bladeburner.action.name; 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 hasReqdRank = props.bladeburner.rank >= props.action.reqdRank;
const computedActionTimeCurrent = Math.min( const computedActionTimeCurrent = Math.min(
props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, 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.actionTimeCurrent + props.bladeburner.actionTimeOverflow,
props.bladeburner.actionTimeToComplete, 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]; const actionData = Contracts[props.action.name];
if (actionData === undefined) { if (actionData === undefined) {

@ -33,7 +33,7 @@ export function OperationElem(props: IProps): React.ReactElement {
props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow,
props.bladeburner.actionTimeToComplete, 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]; const actionData = Operations[props.action.name];
if (actionData === undefined) { if (actionData === undefined) {

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

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

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

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

@ -125,7 +125,14 @@ export function NetscriptBladeburner(player: IPlayer, workerScript: WorkerScript
const bladeburner = player.bladeburner; const bladeburner = player.bladeburner;
if (bladeburner === null) throw new Error("Should not be called without Bladeburner"); if (bladeburner === null) throw new Error("Should not be called without Bladeburner");
try { 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) { } catch (e: any) {
throw ctx.makeRuntimeErrorMsg(e); throw ctx.makeRuntimeErrorMsg(e);
} }
@ -139,7 +146,14 @@ export function NetscriptBladeburner(player: IPlayer, workerScript: WorkerScript
const bladeburner = player.bladeburner; const bladeburner = player.bladeburner;
if (bladeburner === null) throw new Error("Should not be called without Bladeburner"); if (bladeburner === null) throw new Error("Should not be called without Bladeburner");
try { 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) { } catch (e: any) {
throw ctx.makeRuntimeErrorMsg(e); throw ctx.makeRuntimeErrorMsg(e);
} }

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

@ -310,5 +310,36 @@ export function NetscriptSleeve(player: IPlayer): InternalAPI<ISleeve> {
return player.sleeves[sleeveNumber].tryBuyAugmentation(player, aug); 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 { HacknetServer } from "../Hacknet/HacknetServer";
import { ISkillProgress } from "./formulas/skill"; import { ISkillProgress } from "./formulas/skill";
import { PlayerAchievement } from "../Achievements/Achievements"; import { PlayerAchievement } from "../Achievements/Achievements";
import { IPerson } from "./IPerson";
import { WorkType, ClassType, CrimeType } from "../utils/WorkType"; import { WorkType, ClassType, CrimeType } from "../utils/WorkType";
export interface IPlayer { export interface IPlayer extends IPerson {
// Class members
augmentations: IPlayerOwnedAugmentation[];
bitNodeN: number; bitNodeN: number;
city: CityName; city: CityName;
companyName: string; companyName: string;
@ -186,13 +185,6 @@ export interface IPlayer {
canAccessGang(): boolean; canAccessGang(): boolean;
canAccessGrafting(): boolean; canAccessGrafting(): boolean;
canAfford(cost: number): 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; gainMoney(money: number, source: string): void;
getCurrentServer(): BaseServer; getCurrentServer(): BaseServer;
getGangFaction(): Faction; getGangFaction(): Faction;
@ -215,7 +207,6 @@ export interface IPlayer {
process(router: IRouter, numCycles?: number): void; process(router: IRouter, numCycles?: number): void;
reapplyAllAugmentations(resetMultipliers?: boolean): void; reapplyAllAugmentations(resetMultipliers?: boolean): void;
reapplyAllSourceFiles(): void; reapplyAllSourceFiles(): void;
regenerateHp(amt: number): void;
setMoney(amt: number): void; setMoney(amt: number): void;
singularityStopWork(): string; singularityStopWork(): string;
startBladeburner(p: any): void; startBladeburner(p: any): void;
@ -242,12 +233,9 @@ export interface IPlayer {
startGang(facName: string, isHacking: boolean): void; startGang(facName: string, isHacking: boolean): void;
startWork(companyName: string): void; startWork(companyName: string): void;
startWorkPartTime(companyName: string): void; startWorkPartTime(companyName: string): void;
takeDamage(amt: number): boolean;
travel(to: CityName): boolean; travel(to: CityName): boolean;
giveExploit(exploit: Exploit): void; giveExploit(exploit: Exploit): void;
giveAchievement(achievementId: string): void; giveAchievement(achievementId: string): void;
queryStatFromString(str: string): number;
getIntelligenceBonus(weight: number): number;
getCasinoWinnings(): number; getCasinoWinnings(): number;
quitJob(company: string, sing?: boolean): void; quitJob(company: string, sing?: boolean): void;
hasJob(): boolean; hasJob(): boolean;
@ -268,7 +256,6 @@ export interface IPlayer {
resetMultipliers(): void; resetMultipliers(): void;
prestigeAugmentation(): void; prestigeAugmentation(): void;
prestigeSourceFile(): void; prestigeSourceFile(): void;
calculateSkill(exp: number, mult?: number): number;
calculateSkillProgress(exp: number, mult?: number): ISkillProgress; calculateSkillProgress(exp: number, mult?: number): ISkillProgress;
resetWorkStatus(generalType?: WorkType, group?: string, workType?: string): void; resetWorkStatus(generalType?: WorkType, group?: string, workType?: string): void;
getWorkHackExpGain(): number; 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 { Augmentation } from "../Augmentation/Augmentation";
import { IPlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation"; import { IPlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
@ -6,32 +6,10 @@ import { CityName } from "../Locations/data/CityNames";
import { CONSTANTS } from "../Constants"; import { CONSTANTS } from "../Constants";
import { calculateSkill } from "./formulas/skill"; import { calculateSkill } from "./formulas/skill";
import { calculateIntelligenceBonus } from "./formulas/intelligence"; import { calculateIntelligenceBonus } from "./formulas/intelligence";
import { IPerson } from "./IPerson";
// Interface that defines a generic object used to track experience/money // Base class representing a person-like object
// earnings for tasks export abstract class Person implements IPerson {
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 {
/** /**
* Stats * Stats
*/ */
@ -41,7 +19,7 @@ export abstract class Person {
dexterity = 1; dexterity = 1;
agility = 1; agility = 1;
charisma = 1; charisma = 1;
intelligence = 1; intelligence = 0;
hp = 10; hp = 10;
max_hp = 10; max_hp = 10;
@ -97,24 +75,28 @@ export abstract class Person {
bladeburner_analysis_mult = 1; bladeburner_analysis_mult = 1;
bladeburner_success_chance_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
*/ */
augmentations: IPlayerOwnedAugmentation[] = []; augmentations: IPlayerOwnedAugmentation[] = [];
queuedAugmentations: IPlayerOwnedAugmentation[] = [];
/** /**
* City that the person is in * City that the person is in
*/ */
city: CityName = CityName.Sector12; 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 * 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_stamina_gain_mult = 1;
this.bladeburner_analysis_mult = 1; this.bladeburner_analysis_mult = 1;
this.bladeburner_success_chance_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 { getIntelligenceBonus(weight: number): number {
return calculateIntelligenceBonus(this.intelligence, weight); 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 { PlayerAchievement } from "../../Achievements/Achievements";
import { cyrb53 } from "../../utils/StringHelperFunctions"; import { cyrb53 } from "../../utils/StringHelperFunctions";
import { getRandomInt } from "../../utils/helpers/getRandomInt"; import { getRandomInt } from "../../utils/helpers/getRandomInt";
import { ITaskTracker } from "../ITaskTracker";
import { CONSTANTS } from "../../Constants"; import { CONSTANTS } from "../../Constants";
import { WorkType, ClassType, CrimeType, PlayerFactionWorkType } from "../../utils/WorkType"; import { WorkType, ClassType, CrimeType, PlayerFactionWorkType } from "../../utils/WorkType";
@ -203,6 +204,7 @@ export class PlayerObject implements IPlayer {
gainAgilityExp: (exp: number) => void; gainAgilityExp: (exp: number) => void;
gainCharismaExp: (exp: number) => void; gainCharismaExp: (exp: number) => void;
gainIntelligenceExp: (exp: number) => void; gainIntelligenceExp: (exp: number) => void;
gainStats: (retValue: ITaskTracker) => void;
gainMoney: (money: number, source: string) => void; gainMoney: (money: number, source: string) => void;
getCurrentServer: () => BaseServer; getCurrentServer: () => BaseServer;
getGangFaction: () => Faction; getGangFaction: () => Faction;
@ -524,6 +526,7 @@ export class PlayerObject implements IPlayer {
this.gainAgilityExp = generalMethods.gainAgilityExp; this.gainAgilityExp = generalMethods.gainAgilityExp;
this.gainCharismaExp = generalMethods.gainCharismaExp; this.gainCharismaExp = generalMethods.gainCharismaExp;
this.gainIntelligenceExp = generalMethods.gainIntelligenceExp; this.gainIntelligenceExp = generalMethods.gainIntelligenceExp;
this.gainStats = generalMethods.gainStats;
this.queryStatFromString = generalMethods.queryStatFromString; this.queryStatFromString = generalMethods.queryStatFromString;
this.resetWorkStatus = generalMethods.resetWorkStatus; this.resetWorkStatus = generalMethods.resetWorkStatus;
this.processWorkEarnings = generalMethods.processWorkEarnings; this.processWorkEarnings = generalMethods.processWorkEarnings;
@ -632,6 +635,10 @@ export class PlayerObject implements IPlayer {
this.applyEntropy = augmentationMethods.applyEntropy; this.applyEntropy = augmentationMethods.applyEntropy;
} }
whoAmI(): string {
return "Player";
}
/** /**
* Serialize the current object to a JSON save state. * 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 { calculateClassEarnings } from "../formulas/work";
import { achievements } from "../../Achievements/Achievements"; import { achievements } from "../../Achievements/Achievements";
import { FactionNames } from "../../Faction/data/FactionNames"; import { FactionNames } from "../../Faction/data/FactionNames";
import { ITaskTracker } from "../ITaskTracker";
import { IPerson } from "../IPerson";
import { Player } from "../../Player";
import { graftingIntBonus } from "../Grafting/GraftingHelpers"; import { graftingIntBonus } from "../Grafting/GraftingHelpers";
import { WorkType, PlayerFactionWorkType, ClassType, CrimeType } from "../../utils/WorkType"; 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 //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); return calculateSkillF(exp, mult);
} }
@ -379,7 +382,7 @@ export function recordMoneySource(this: PlayerObject, amt: number, source: strin
this.moneySourceB.record(amt, source); this.moneySourceB.record(amt, source);
} }
export function gainHackingExp(this: IPlayer, exp: number): void { export function gainHackingExp(this: IPerson, exp: number): void {
if (isNaN(exp)) { if (isNaN(exp)) {
console.error("ERR: NaN passed into Player.gainHackingExp()"); console.error("ERR: NaN passed into Player.gainHackingExp()");
return; return;
@ -392,7 +395,7 @@ export function gainHackingExp(this: IPlayer, exp: number): void {
this.hacking = calculateSkillF(this.hacking_exp, this.hacking_mult * BitNodeMultipliers.HackingLevelMultiplier); 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)) { if (isNaN(exp)) {
console.error("ERR: NaN passed into Player.gainStrengthExp()"); console.error("ERR: NaN passed into Player.gainStrengthExp()");
return; return;
@ -405,7 +408,7 @@ export function gainStrengthExp(this: IPlayer, exp: number): void {
this.strength = calculateSkillF(this.strength_exp, this.strength_mult * BitNodeMultipliers.StrengthLevelMultiplier); 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)) { if (isNaN(exp)) {
console.error("ERR: NaN passed into player.gainDefenseExp()"); console.error("ERR: NaN passed into player.gainDefenseExp()");
return; return;
@ -421,7 +424,7 @@ export function gainDefenseExp(this: IPlayer, exp: number): void {
this.hp = Math.round(this.max_hp * ratio); 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)) { if (isNaN(exp)) {
console.error("ERR: NaN passed into Player.gainDexterityExp()"); console.error("ERR: NaN passed into Player.gainDexterityExp()");
return; 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)) { if (isNaN(exp)) {
console.error("ERR: NaN passed into Player.gainAgilityExp()"); console.error("ERR: NaN passed into Player.gainAgilityExp()");
return; return;
@ -450,7 +453,7 @@ export function gainAgilityExp(this: IPlayer, exp: number): void {
this.agility = calculateSkillF(this.agility_exp, this.agility_mult * BitNodeMultipliers.AgilityLevelMultiplier); 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)) { if (isNaN(exp)) {
console.error("ERR: NaN passed into Player.gainCharismaExp()"); console.error("ERR: NaN passed into Player.gainCharismaExp()");
return; return;
@ -463,17 +466,27 @@ export function gainCharismaExp(this: IPlayer, exp: number): void {
this.charisma = calculateSkillF(this.charisma_exp, this.charisma_mult * BitNodeMultipliers.CharismaLevelMultiplier); 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)) { if (isNaN(exp)) {
console.error("ERROR: NaN passed into Player.gainIntelligenceExp()"); console.error("ERROR: NaN passed into Player.gainIntelligenceExp()");
return; return;
} }
if (this.sourceFileLvl(5) > 0 || this.intelligence > 0) { if (Player.sourceFileLvl(5) > 0 || this.intelligence > 0) {
this.intelligence_exp += exp; 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 //Given a string expression like "str" or "strength", returns the given stat
export function queryStatFromString(this: IPlayer, str: string): number { export function queryStatFromString(this: IPlayer, str: string): number {
const tempStr = str.toLowerCase(); 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") { if (typeof amt !== "number") {
console.warn(`Player.regenerateHp() called without a numeric argument: ${amt}`); console.warn(`Player.regenerateHp() called without a numeric argument: ${amt}`);
return; return;

@ -9,7 +9,8 @@
import { SleeveTaskType } from "./SleeveTaskTypesEnum"; import { SleeveTaskType } from "./SleeveTaskTypesEnum";
import { IPlayer } from "../IPlayer"; import { IPlayer } from "../IPlayer";
import { Person, ITaskTracker, createTaskTracker } from "../Person"; import { Person } from "../Person";
import { ITaskTracker, createTaskTracker } from "../ITaskTracker";
import { Augmentation } from "../../Augmentation/Augmentation"; import { Augmentation } from "../../Augmentation/Augmentation";
@ -33,6 +34,9 @@ import { CityName } from "../../Locations/data/CityNames";
import { LocationName } from "../../Locations/data/LocationNames"; import { LocationName } from "../../Locations/data/LocationNames";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver"; 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 { export class Sleeve extends Person {
/** /**
@ -58,6 +62,7 @@ export class Sleeve extends Person {
* Faction/Company Work: Name of Faction/Company * Faction/Company Work: Name of Faction/Company
* Crime: Money earned if successful * Crime: Money earned if successful
* Class/Gym: Name of university/gym * Class/Gym: Name of university/gym
* Bladeburner: success chance
*/ */
currentTaskLocation = ""; currentTaskLocation = "";
@ -101,6 +106,16 @@ export class Sleeve extends Person {
*/ */
gymStatType = ""; 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 * Keeps track of events/notifications for this sleeve
*/ */
@ -151,7 +166,7 @@ export class Sleeve extends Person {
if (this.currentTask !== SleeveTaskType.Idle) { if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p); this.finishTask(p);
} else { } else {
this.resetTaskStatus(); this.resetTaskStatus(p);
} }
this.gainRatesForTask.hack = crime.hacking_exp * this.hacking_exp_mult * BitNodeMultipliers.CrimeExpGain; 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.dex = crime.dexterity_exp * this.dexterity_exp_mult * BitNodeMultipliers.CrimeExpGain;
this.gainRatesForTask.agi = crime.agility_exp * this.agility_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.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.gainRatesForTask.money = crime.money * this.crime_money_mult * BitNodeMultipliers.CrimeMoney;
this.currentTaskLocation = String(this.gainRatesForTask.money); 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); const crime: Crime | undefined = Object.values(Crimes).find((crime) => crime.name === this.crimeType);
if (!crime) { if (!crime) {
console.error(`Invalid data stored in sleeve.crimeType: ${this.crimeType}`); console.error(`Invalid data stored in sleeve.crimeType: ${this.crimeType}`);
this.resetTaskStatus(); this.resetTaskStatus(p);
return retValue; return retValue;
} }
if (Math.random() < crime.successRate(this)) { if (Math.random() < crime.successRate(this)) {
@ -206,11 +222,60 @@ export class Sleeve extends Person {
this.currentTaskTime = 0; this.currentTaskTime = 0;
return retValue; return retValue;
} }
} else { } else if (this.currentTask === SleeveTaskType.Bladeburner) {
// For other crimes... I dont think anything else needs to be done 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; return retValue;
} }
@ -260,50 +325,56 @@ export class Sleeve extends Person {
const pDexExp = exp.dex * multFac; const pDexExp = exp.dex * multFac;
const pAgiExp = exp.agi * multFac; const pAgiExp = exp.agi * multFac;
const pChaExp = exp.cha * multFac; const pChaExp = exp.cha * multFac;
const pIntExp = exp.int * multFac;
// Experience is gained by both this sleeve and player // Experience is gained by both this sleeve and player
if (pHackExp > 0) { if (pHackExp > 0) {
this.hacking_exp += pHackExp; this.gainHackingExp(pHackExp);
p.gainHackingExp(pHackExp); p.gainHackingExp(pHackExp);
this.earningsForPlayer.hack += pHackExp; this.earningsForPlayer.hack += pHackExp;
this.earningsForTask.hack += pHackExp; this.earningsForTask.hack += pHackExp;
} }
if (pStrExp > 0) { if (pStrExp > 0) {
this.strength_exp += pStrExp; this.gainStrengthExp(pStrExp);
p.gainStrengthExp(pStrExp); p.gainStrengthExp(pStrExp);
this.earningsForPlayer.str += pStrExp; this.earningsForPlayer.str += pStrExp;
this.earningsForTask.str += pStrExp; this.earningsForTask.str += pStrExp;
} }
if (pDefExp > 0) { if (pDefExp > 0) {
this.defense_exp += pDefExp; this.gainDefenseExp(pDefExp);
p.gainDefenseExp(pDefExp); p.gainDefenseExp(pDefExp);
this.earningsForPlayer.def += pDefExp; this.earningsForPlayer.def += pDefExp;
this.earningsForTask.def += pDefExp; this.earningsForTask.def += pDefExp;
} }
if (pDexExp > 0) { if (pDexExp > 0) {
this.dexterity_exp += pDexExp; this.gainDexterityExp(pDexExp);
p.gainDexterityExp(pDexExp); p.gainDexterityExp(pDexExp);
this.earningsForPlayer.dex += pDexExp; this.earningsForPlayer.dex += pDexExp;
this.earningsForTask.dex += pDexExp; this.earningsForTask.dex += pDexExp;
} }
if (pAgiExp > 0) { if (pAgiExp > 0) {
this.agility_exp += pAgiExp; this.gainAgilityExp(pAgiExp);
p.gainAgilityExp(pAgiExp); p.gainAgilityExp(pAgiExp);
this.earningsForPlayer.agi += pAgiExp; this.earningsForPlayer.agi += pAgiExp;
this.earningsForTask.agi += pAgiExp; this.earningsForTask.agi += pAgiExp;
} }
if (pChaExp > 0) { if (pChaExp > 0) {
this.charisma_exp += pChaExp; this.gainCharismaExp(pChaExp);
p.gainCharismaExp(pChaExp); p.gainCharismaExp(pChaExp);
this.earningsForPlayer.cha += pChaExp; this.earningsForPlayer.cha += pChaExp;
this.earningsForTask.cha += pChaExp; this.earningsForTask.cha += pChaExp;
} }
if (pIntExp > 0) {
this.gainIntelligenceExp(pIntExp);
p.gainIntelligenceExp(pIntExp);
}
// Record earnings for other sleeves // Record earnings for other sleeves
this.earningsForSleeves.hack += pHackExp * (this.sync / 100); this.earningsForSleeves.hack += pHackExp * (this.sync / 100);
this.earningsForSleeves.str += pStrExp * (this.sync / 100); this.earningsForSleeves.str += pStrExp * (this.sync / 100);
@ -320,7 +391,8 @@ export class Sleeve extends Person {
dex: pDexExp * (this.sync / 100), dex: pDexExp * (this.sync / 100),
agi: pAgiExp * (this.sync / 100), agi: pAgiExp * (this.sync / 100),
cha: pChaExp * (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; this.charisma_exp = 0;
// Reset task-related stuff // Reset task-related stuff
this.resetTaskStatus(); this.resetTaskStatus(p);
this.earningsForSleeves = createTaskTracker(); this.earningsForSleeves = createTaskTracker();
this.earningsForPlayer = createTaskTracker(); this.earningsForPlayer = createTaskTracker();
this.shockRecovery(p); this.shockRecovery(p);
@ -523,7 +595,7 @@ export class Sleeve extends Person {
// for, we need to reset the sleeve's task // for, we need to reset the sleeve's task
if (p.gang) { if (p.gang) {
if (fac.name === p.gang.facName) { if (fac.name === p.gang.facName) {
this.resetTaskStatus(); this.resetTaskStatus(p);
} }
} }
@ -545,18 +617,18 @@ export class Sleeve extends Person {
} }
case SleeveTaskType.Recovery: case SleeveTaskType.Recovery:
this.shock = Math.min(100, this.shock + 0.0002 * cyclesUsed); this.shock = Math.min(100, this.shock + 0.0002 * cyclesUsed);
if (this.shock >= 100) this.resetTaskStatus(); if (this.shock >= 100) this.resetTaskStatus(p);
break; break;
case SleeveTaskType.Synchro: case SleeveTaskType.Synchro:
this.sync = Math.min(100, this.sync + p.getIntelligenceBonus(0.5) * 0.0002 * cyclesUsed); 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; break;
default: default:
break; break;
} }
if (this.currentTaskMaxTime !== 0 && this.currentTaskTime >= this.currentTaskMaxTime) { 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); retValue = this.finishTask(p);
} else { } else {
this.finishTask(p); this.finishTask(p);
@ -573,7 +645,16 @@ export class Sleeve extends Person {
/** /**
* Resets all parameters used to keep information about the current task * 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.earningsForTask = createTaskTracker();
this.gainRatesForTask = createTaskTracker(); this.gainRatesForTask = createTaskTracker();
this.currentTask = SleeveTaskType.Idle; this.currentTask = SleeveTaskType.Idle;
@ -584,13 +665,15 @@ export class Sleeve extends Person {
this.currentTaskLocation = ""; this.currentTaskLocation = "";
this.gymStatType = ""; this.gymStatType = "";
this.className = ""; this.className = "";
this.bbAction = "";
this.bbContract = "------";
} }
shockRecovery(p: IPlayer): boolean { shockRecovery(p: IPlayer): boolean {
if (this.currentTask !== SleeveTaskType.Idle) { if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p); this.finishTask(p);
} else { } else {
this.resetTaskStatus(); this.resetTaskStatus(p);
} }
this.currentTask = SleeveTaskType.Recovery; this.currentTask = SleeveTaskType.Recovery;
@ -601,7 +684,7 @@ export class Sleeve extends Person {
if (this.currentTask !== SleeveTaskType.Idle) { if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p); this.finishTask(p);
} else { } else {
this.resetTaskStatus(); this.resetTaskStatus(p);
} }
this.currentTask = SleeveTaskType.Synchro; this.currentTask = SleeveTaskType.Synchro;
@ -615,7 +698,7 @@ export class Sleeve extends Person {
if (this.currentTask !== SleeveTaskType.Idle) { if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p); this.finishTask(p);
} else { } else {
this.resetTaskStatus(); this.resetTaskStatus(p);
} }
// Set exp/money multipliers based on which university. // Set exp/money multipliers based on which university.
@ -809,7 +892,7 @@ export class Sleeve extends Person {
if (this.currentTask !== SleeveTaskType.Idle) { if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p); this.finishTask(p);
} else { } else {
this.resetTaskStatus(); this.resetTaskStatus(p);
} }
const company: Company | null = Companies[companyName]; const company: Company | null = Companies[companyName];
@ -875,7 +958,7 @@ export class Sleeve extends Person {
if (this.currentTask !== SleeveTaskType.Idle) { if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p); this.finishTask(p);
} else { } else {
this.resetTaskStatus(); this.resetTaskStatus(p);
} }
const factionInfo = faction.getInfo(); const factionInfo = faction.getInfo();
@ -926,7 +1009,7 @@ export class Sleeve extends Person {
if (this.currentTask !== SleeveTaskType.Idle) { if (this.currentTask !== SleeveTaskType.Idle) {
this.finishTask(p); this.finishTask(p);
} else { } else {
this.resetTaskStatus(); this.resetTaskStatus(p);
} }
// Set exp/money multipliers based on which university. // Set exp/money multipliers based on which university.
@ -994,6 +1077,162 @@ export class Sleeve extends Person {
return true; 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. * Serialize the current object to a JSON save state.
*/ */

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

@ -30,7 +30,7 @@ export function SleeveElem(props: IProps): React.ReactElement {
const [abc, setABC] = useState(["------", "------", "------"]); const [abc, setABC] = useState(["------", "------", "------"]);
function setTask(): void { function setTask(): void {
props.sleeve.resetTaskStatus(); // sets to idle props.sleeve.resetTaskStatus(player); // sets to idle
switch (abc[0]) { switch (abc[0]) {
case "------": case "------":
break; break;
@ -49,6 +49,9 @@ export function SleeveElem(props: IProps): React.ReactElement {
case "Workout at Gym": case "Workout at Gym":
props.sleeve.workoutAtGym(player, abc[2], abc[1]); props.sleeve.workoutAtGym(player, abc[2], abc[1]);
break; break;
case "Perform Bladeburner Actions":
props.sleeve.bladeburner(player, abc[1], abc[2]);
break;
case "Shock Recovery": case "Shock Recovery":
props.sleeve.shockRecovery(player); props.sleeve.shockRecovery(player);
break; break;
@ -106,6 +109,20 @@ export function SleeveElem(props: IProps): React.ReactElement {
case SleeveTaskType.Gym: case SleeveTaskType.Gym:
desc = <>This sleeve is currently working out at {props.sleeve.currentTaskLocation}.</>; desc = <>This sleeve is currently working out at {props.sleeve.currentTaskLocation}.</>;
break; 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: case SleeveTaskType.Recovery:
desc = ( desc = (
<> <>
@ -168,7 +185,9 @@ export function SleeveElem(props: IProps): React.ReactElement {
</Button> </Button>
<Typography>{desc}</Typography> <Typography>{desc}</Typography>
<Typography> <Typography>
{props.sleeve.currentTask === SleeveTaskType.Crime && ( {(props.sleeve.currentTask === SleeveTaskType.Crime ||
props.sleeve.currentTask === SleeveTaskType.Bladeburner) &&
props.sleeve.currentTaskMaxTime > 0 && (
<ProgressBar <ProgressBar
variant="determinate" variant="determinate"
value={(props.sleeve.currentTaskTime / props.sleeve.currentTaskMaxTime) * 100} 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 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 { interface IProps {
sleeve: Sleeve; sleeve: Sleeve;
player: IPlayer; 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: { const tasks: {
[key: string]: undefined | ((player: IPlayer, sleeve: Sleeve) => ITaskDetails); [key: string]: undefined | ((player: IPlayer, sleeve: Sleeve) => ITaskDetails);
["------"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; ["------"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails;
@ -92,6 +121,7 @@ const tasks: {
["Commit Crime"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; ["Commit Crime"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails;
["Take University Course"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; ["Take University Course"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails;
["Workout at Gym"]: (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; ["Shock Recovery"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails;
["Synchronize"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; ["Synchronize"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails;
} = { } = {
@ -170,6 +200,18 @@ const tasks: {
return { first: gymSelectorOptions, second: () => gyms }; 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 => { "Shock Recovery": (): ITaskDetails => {
return { first: ["------"], second: () => ["------"] }; return { first: ["------"], second: () => ["------"] };
}, },
@ -186,6 +228,7 @@ const canDo: {
["Commit Crime"]: (player: IPlayer, sleeve: Sleeve) => boolean; ["Commit Crime"]: (player: IPlayer, sleeve: Sleeve) => boolean;
["Take University Course"]: (player: IPlayer, sleeve: Sleeve) => boolean; ["Take University Course"]: (player: IPlayer, sleeve: Sleeve) => boolean;
["Workout at Gym"]: (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; ["Shock Recovery"]: (player: IPlayer, sleeve: Sleeve) => boolean;
["Synchronize"]: (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), [CityName.Aevum, CityName.Sector12, CityName.Volhaven].includes(sleeve.city),
"Workout at Gym": (player: IPlayer, sleeve: Sleeve) => "Workout at Gym": (player: IPlayer, sleeve: Sleeve) =>
[CityName.Aevum, CityName.Sector12, CityName.Volhaven].includes(sleeve.city), [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, "Shock Recovery": (player: IPlayer, sleeve: Sleeve) => sleeve.shock < 100,
Synchronize: (player: IPlayer, sleeve: Sleeve) => sleeve.sync < 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]; return ["Take University Course", sleeve.className, sleeve.currentTaskLocation];
case SleeveTaskType.Gym: case SleeveTaskType.Gym:
return ["Workout at Gym", sleeve.gymStatType, sleeve.currentTaskLocation]; return ["Workout at Gym", sleeve.gymStatType, sleeve.currentTaskLocation];
case SleeveTaskType.Bladeburner:
return ["Perform Bladeburner Actions", sleeve.bbAction, sleeve.bbContract];
case SleeveTaskType.Recovery: case SleeveTaskType.Recovery:
return ["Shock Recovery", "------", "------"]; return ["Shock Recovery", "------", "------"];
case SleeveTaskType.Synchro: case SleeveTaskType.Synchro:

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

@ -3789,6 +3789,20 @@ export interface Sleeve {
* @returns True if the aug was purchased and installed on the sleeve, false otherwise. * @returns True if the aug was purchased and installed on the sleeve, false otherwise.
*/ */
purchaseSleeveAug(sleeveNumber: number, augName: string): boolean; 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); 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 { export {
convertTimeMsToTimeElapsedString, convertTimeMsToTimeElapsedString,
longestCommonStart, longestCommonStart,
@ -124,4 +135,6 @@ export {
formatNumber, formatNumber,
generateRandomString, generateRandomString,
cyrb53, cyrb53,
capitalizeFirstLetter,
capitalizeEachWord,
}; };