diff --git a/package-lock.json b/package-lock.json index a313efae0..4f90b38cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bitburner", - "version": "1.6.4", + "version": "1.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bitburner", - "version": "1.6.4", + "version": "1.7.0", "hasInstallScript": true, "license": "SEE LICENSE IN license.txt", "dependencies": { diff --git a/src/Bladeburner/Action.tsx b/src/Bladeburner/Action.tsx index 73bb093e6..fbffa5151 100644 --- a/src/Bladeburner/Action.tsx +++ b/src/Bladeburner/Action.tsx @@ -5,6 +5,7 @@ import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver" import { BladeburnerConstants } from "./data/Constants"; import { IBladeburner } from "./IBladeburner"; import { IAction, ISuccessChanceParams } from "./IAction"; +import { IPerson } from "../PersonObjects/IPerson"; class StatsMultiplier { [key: string]: number; @@ -152,8 +153,8 @@ export class Action implements IAction { * Tests for success. Should be called when an action has completed * @param inst {Bladeburner} - Bladeburner instance */ - attempt(inst: IBladeburner): boolean { - return Math.random() < this.getSuccessChance(inst); + attempt(inst: IBladeburner, person: IPerson): boolean { + return Math.random() < this.getSuccessChance(inst, person); } // To be implemented by subtypes @@ -161,13 +162,13 @@ export class Action implements IAction { return 1; } - getActionTime(inst: IBladeburner): number { + getActionTime(inst: IBladeburner, person: IPerson): number { const difficulty = this.getDifficulty(); let baseTime = difficulty / BladeburnerConstants.DifficultyToTimeFactor; const skillFac = inst.skillMultipliers.actionTime; // Always < 1 - const effAgility = Player.agility * inst.skillMultipliers.effAgi; - const effDexterity = Player.dexterity * inst.skillMultipliers.effDex; + const effAgility = person.agility * inst.skillMultipliers.effAgi; + const effDexterity = person.dexterity * inst.skillMultipliers.effDex; const statFac = 0.5 * (Math.pow(effAgility, BladeburnerConstants.EffAgiExponentialFactor) + @@ -211,12 +212,12 @@ export class Action implements IAction { return 1; } - getEstSuccessChance(inst: IBladeburner): [number, number] { + getEstSuccessChance(inst: IBladeburner, person: IPerson): [number, number] { function clamp(x: number): number { return Math.max(0, Math.min(x, 1)); } - const est = this.getSuccessChance(inst, { est: true }); - const real = this.getSuccessChance(inst); + const est = this.getSuccessChance(inst, person, { est: true }); + const real = this.getSuccessChance(inst, person); const diff = Math.abs(real - est); let low = real - diff; let high = real + diff; @@ -232,7 +233,7 @@ export class Action implements IAction { * @params - options: * est (bool): Get success chance estimate instead of real success chance */ - getSuccessChance(inst: IBladeburner, params: ISuccessChanceParams = { est: false }): number { + getSuccessChance(inst: IBladeburner, person: IPerson, params: ISuccessChanceParams = { est: false }): number { if (inst == null) { throw new Error("Invalid Bladeburner instance passed into Action.getSuccessChance"); } @@ -240,7 +241,7 @@ export class Action implements IAction { let competence = 0; for (const stat of Object.keys(this.weights)) { if (this.weights.hasOwnProperty(stat)) { - const playerStatLvl = Player.queryStatFromString(stat); + const playerStatLvl = person.queryStatFromString(stat); const key = "eff" + stat.charAt(0).toUpperCase() + stat.slice(1); let effMultiplier = inst.skillMultipliers[key]; if (effMultiplier == null) { diff --git a/src/Bladeburner/Bladeburner.tsx b/src/Bladeburner/Bladeburner.tsx index 48e7f90e2..7a043ea35 100644 --- a/src/Bladeburner/Bladeburner.tsx +++ b/src/Bladeburner/Bladeburner.tsx @@ -15,6 +15,8 @@ import { Skill } from "./Skill"; import { City } from "./City"; import { IAction } from "./IAction"; import { IPlayer } from "../PersonObjects/IPlayer"; +import { createTaskTracker, ITaskTracker } from "../PersonObjects/ITaskTracker"; +import { IPerson } from "../PersonObjects/IPerson"; import { IRouter, Page } from "../ui/Router"; import { ConsoleHelpText } from "./data/Help"; import { exceptionAlert } from "../utils/helpers/exceptionAlert"; @@ -52,6 +54,7 @@ export class Bladeburner implements IBladeburner { totalSkillPoints = 0; teamSize = 0; + sleeveSize = 0; teamLost = 0; hpLost = 0; @@ -158,7 +161,7 @@ export class Bladeburner implements IBladeburner { return { isAvailable: true, action }; } - startAction(player: IPlayer, actionId: IActionIdentifier): void { + startAction(person: IPerson, actionId: IActionIdentifier): void { if (actionId == null) return; this.action = actionId; this.actionTimeCurrent = 0; @@ -175,7 +178,7 @@ export class Bladeburner implements IBladeburner { if (action.count < 1) { return this.resetAction(); } - this.actionTimeToComplete = action.getActionTime(this); + this.actionTimeToComplete = action.getActionTime(this, person); } catch (e: any) { exceptionAlert(e); } @@ -192,7 +195,7 @@ export class Bladeburner implements IBladeburner { if (actionId.name === "Raid" && this.getCurrentCity().comms === 0) { return this.resetAction(); } - this.actionTimeToComplete = action.getActionTime(this); + this.actionTimeToComplete = action.getActionTime(this, person); } catch (e: any) { exceptionAlert(e); } @@ -210,14 +213,14 @@ export class Bladeburner implements IBladeburner { if (testBlackOp.action === undefined) { throw new Error("action should not be null"); } - this.actionTimeToComplete = testBlackOp.action.getActionTime(this); + this.actionTimeToComplete = testBlackOp.action.getActionTime(this, person); } catch (e: any) { exceptionAlert(e); } break; } case ActionTypes["Recruitment"]: - this.actionTimeToComplete = this.getRecruitmentTime(player); + this.actionTimeToComplete = this.getRecruitmentTime(person); break; case ActionTypes["Training"]: case ActionTypes["FieldAnalysis"]: @@ -996,11 +999,11 @@ export class Bladeburner implements IBladeburner { } /** - * Process stat gains from Contracts, Operations, and Black Operations + * Return stat to be gained from Contracts, Operations, and Black Operations * @param action(Action obj) - Derived action class * @param success(bool) - Whether action was successful */ - gainActionStats(player: IPlayer, action: IAction, success: boolean): void { + getActionStats(action: IAction, success: boolean): ITaskTracker { const difficulty = action.getDifficulty(); /** @@ -1017,34 +1020,48 @@ export class Bladeburner implements IBladeburner { const unweightedGain = time * BladeburnerConstants.BaseStatGain * successMult * difficultyMult; const unweightedIntGain = time * BladeburnerConstants.BaseIntGain * successMult * difficultyMult; const skillMult = this.skillMultipliers.expGain; - player.gainHackingExp(unweightedGain * action.weights.hack * player.hacking_exp_mult * skillMult); - player.gainStrengthExp(unweightedGain * action.weights.str * player.strength_exp_mult * skillMult); - player.gainDefenseExp(unweightedGain * action.weights.def * player.defense_exp_mult * skillMult); - player.gainDexterityExp(unweightedGain * action.weights.dex * player.dexterity_exp_mult * skillMult); - player.gainAgilityExp(unweightedGain * action.weights.agi * player.agility_exp_mult * skillMult); - player.gainCharismaExp(unweightedGain * action.weights.cha * player.charisma_exp_mult * skillMult); - player.gainIntelligenceExp(unweightedIntGain * action.weights.int * skillMult); + + return { + hack: unweightedGain * action.weights.hack * skillMult, + str: unweightedGain * action.weights.str * skillMult, + def: unweightedGain * action.weights.def * skillMult, + dex: unweightedGain * action.weights.dex * skillMult, + agi: unweightedGain * action.weights.agi * skillMult, + cha: unweightedGain * action.weights.cha * skillMult, + int: unweightedIntGain * action.weights.int * skillMult, + money: 0, + }; } - getDiplomacyEffectiveness(player: IPlayer): number { + getDiplomacyEffectiveness(person: IPerson): number { // Returns a decimal by which the city's chaos level should be multiplied (e.g. 0.98) const CharismaLinearFactor = 1e3; const CharismaExponentialFactor = 0.045; - const charismaEff = Math.pow(player.charisma, CharismaExponentialFactor) + player.charisma / CharismaLinearFactor; + const charismaEff = Math.pow(person.charisma, CharismaExponentialFactor) + person.charisma / CharismaLinearFactor; return (100 - charismaEff) / 100; } - getRecruitmentSuccessChance(player: IPlayer): number { - return Math.pow(player.charisma, 0.45) / (this.teamSize + 1); + getRecruitmentSuccessChance(person: IPerson): number { + return Math.pow(person.charisma, 0.45) / (this.teamSize - this.sleeveSize + 1); } - getRecruitmentTime(player: IPlayer): number { - const effCharisma = player.charisma * this.skillMultipliers.effCha; + getRecruitmentTime(person: IPerson): number { + const effCharisma = person.charisma * this.skillMultipliers.effCha; const charismaFactor = Math.pow(effCharisma, 0.81) + effCharisma / 90; return Math.max(10, Math.round(BladeburnerConstants.BaseRecruitmentTimeNeeded - charismaFactor)); } + sleeveSupport(joining: boolean): void { + if (joining) { + this.sleeveSize += 1; + this.teamSize += 1; + } else { + this.sleeveSize -= 1; + this.teamSize -= 1; + } + } + resetSkillMultipliers(): void { this.skillMultipliers = { successChanceAll: 1, @@ -1096,7 +1113,7 @@ export class Bladeburner implements IBladeburner { } } - completeOperation(success: boolean): void { + completeOperation(success: boolean, player: IPlayer): void { if (this.action.type !== ActionTypes.Operation) { throw new Error("completeOperation() called even though current action is not an Operation"); } @@ -1116,6 +1133,15 @@ export class Bladeburner implements IBladeburner { } const losses = getRandomInt(0, max); this.teamSize -= losses; + if (this.teamSize < this.sleeveSize) { + const sup = player.sleeves.filter((x) => x.bbAction == "Support main sleeve"); + for (let i = 0; i > this.teamSize - this.sleeveSize; i--) { + const r = Math.floor(Math.random() * sup.length); + sup[r].takeDamage(sup[r].max_hp); + sup.splice(r, 1); + } + this.teamSize += this.sleeveSize; + } this.teamLost += losses; if (this.logging.ops && losses > 0) { this.log("Lost " + formatNumber(losses, 0) + " team members during this " + action.name); @@ -1213,13 +1239,13 @@ export class Bladeburner implements IBladeburner { } } - completeContract(success: boolean): void { - if (this.action.type !== ActionTypes.Contract) { + completeContract(success: boolean, actionIdent: IActionIdentifier): void { + if (actionIdent.type !== ActionTypes.Contract) { throw new Error("completeContract() called even though current action is not a Contract"); } const city = this.getCurrentCity(); if (success) { - switch (this.action.name) { + switch (actionIdent.name) { case "Tracking": // Increase estimate accuracy by a relatively small amount city.improvePopulationEstimateByCount(getRandomInt(100, 1e3)); @@ -1233,20 +1259,21 @@ export class Bladeburner implements IBladeburner { city.changeChaosByCount(0.04); break; default: - throw new Error("Invalid Action name in completeContract: " + this.action.name); + throw new Error("Invalid Action name in completeContract: " + actionIdent.name); } } } - completeAction(router: IRouter, player: IPlayer): void { - switch (this.action.type) { + completeAction(player: IPlayer, person: IPerson, actionIdent: IActionIdentifier, isPlayer = true): ITaskTracker { + let retValue = createTaskTracker(); + switch (actionIdent.type) { case ActionTypes["Contract"]: case ActionTypes["Operation"]: { try { - const isOperation = this.action.type === ActionTypes["Operation"]; - const action = this.getActionObject(this.action); + const isOperation = actionIdent.type === ActionTypes["Operation"]; + const action = this.getActionObject(actionIdent); if (action == null) { - throw new Error("Failed to get Contract/Operation Object for: " + this.action.name); + throw new Error("Failed to get Contract/Operation Object for: " + actionIdent.name); } const difficulty = action.getDifficulty(); const difficultyMultiplier = @@ -1254,15 +1281,17 @@ export class Bladeburner implements IBladeburner { difficulty / BladeburnerConstants.DiffMultLinearFactor; const rewardMultiplier = Math.pow(action.rewardFac, action.level - 1); - // Stamina loss is based on difficulty - this.stamina -= BladeburnerConstants.BaseStaminaLoss * difficultyMultiplier; - if (this.stamina < 0) { - this.stamina = 0; + if (isPlayer) { + // Stamina loss is based on difficulty + this.stamina -= BladeburnerConstants.BaseStaminaLoss * difficultyMultiplier; + if (this.stamina < 0) { + this.stamina = 0; + } } // Process Contract/Operation success/failure - if (action.attempt(this)) { - this.gainActionStats(player, action, true); + if (action.attempt(this, person)) { + retValue = this.getActionStats(action, true); ++action.successes; --action.count; @@ -1270,7 +1299,7 @@ export class Bladeburner implements IBladeburner { let moneyGain = 0; if (!isOperation) { moneyGain = BladeburnerConstants.ContractBaseMoneyGain * rewardMultiplier * this.skillMultipliers.money; - player.gainMoney(moneyGain, "bladeburner"); + retValue.money = moneyGain; } if (isOperation) { @@ -1280,12 +1309,19 @@ export class Bladeburner implements IBladeburner { } if (action.rankGain) { const gain = addOffset(action.rankGain * rewardMultiplier * BitNodeMultipliers.BladeburnerRank, 10); - this.changeRank(player, gain); + this.changeRank(person, gain); if (isOperation && this.logging.ops) { - this.log(action.name + " successfully completed! Gained " + formatNumber(gain, 3) + " rank"); + this.log( + `${person.whoAmI()}: ` + + action.name + + " successfully completed! Gained " + + formatNumber(gain, 3) + + " rank", + ); } else if (!isOperation && this.logging.contracts) { this.log( - action.name + + `${person.whoAmI()}: ` + + action.name + " contract successfully completed! Gained " + formatNumber(gain, 3) + " rank and " + @@ -1293,22 +1329,22 @@ export class Bladeburner implements IBladeburner { ); } } - isOperation ? this.completeOperation(true) : this.completeContract(true); + isOperation ? this.completeOperation(true, player) : this.completeContract(true, actionIdent); } else { - this.gainActionStats(player, action, false); + retValue = this.getActionStats(action, false); ++action.failures; let loss = 0, damage = 0; if (action.rankLoss) { loss = addOffset(action.rankLoss * rewardMultiplier, 10); - this.changeRank(player, -1 * loss); + this.changeRank(person, -1 * loss); } if (action.hpLoss) { damage = action.hpLoss * difficultyMultiplier; damage = Math.ceil(addOffset(damage, 10)); this.hpLost += damage; const cost = calculateHospitalizationCost(player, damage); - if (player.takeDamage(damage)) { + if (person.takeDamage(damage)) { ++this.numHosp; this.moneyLost += cost; } @@ -1321,16 +1357,15 @@ export class Bladeburner implements IBladeburner { logLossText += "Took " + formatNumber(damage, 0) + " damage."; } if (isOperation && this.logging.ops) { - this.log(action.name + " failed! " + logLossText); + this.log(`${person.whoAmI()}: ` + action.name + " failed! " + logLossText); } else if (!isOperation && this.logging.contracts) { - this.log(action.name + " contract failed! " + logLossText); + this.log(`${person.whoAmI()}: ` + action.name + " contract failed! " + logLossText); } - isOperation ? this.completeOperation(false) : this.completeContract(false); + isOperation ? this.completeOperation(false, player) : this.completeContract(false, actionIdent); } if (action.autoLevel) { action.level = action.maxLevel; } // Autolevel - this.startAction(player, this.action); // Repeat action } catch (e: any) { exceptionAlert(e); } @@ -1339,9 +1374,9 @@ export class Bladeburner implements IBladeburner { case ActionTypes["BlackOp"]: case ActionTypes["BlackOperation"]: { try { - const action = this.getActionObject(this.action); + const action = this.getActionObject(actionIdent); if (action == null || !(action instanceof BlackOperation)) { - throw new Error("Failed to get BlackOperation Object for: " + this.action.name); + throw new Error("Failed to get BlackOperation Object for: " + actionIdent.name); } const difficulty = action.getDifficulty(); const difficultyMultiplier = @@ -1358,39 +1393,35 @@ export class Bladeburner implements IBladeburner { const teamCount = action.teamCount; let teamLossMax; - if (action.attempt(this)) { - this.gainActionStats(player, action, true); + if (action.attempt(this, person)) { + retValue = this.getActionStats(action, true); action.count = 0; this.blackops[action.name] = true; let rankGain = 0; if (action.rankGain) { rankGain = addOffset(action.rankGain * BitNodeMultipliers.BladeburnerRank, 10); - this.changeRank(player, rankGain); + this.changeRank(person, rankGain); } teamLossMax = Math.ceil(teamCount / 2); - // Operation Daedalus - if (action.name === BlackOperationNames.OperationDaedalus) { - this.resetAction(); - return router.toBitVerse(false, false); - } - if (this.logging.blackops) { - this.log(action.name + " successful! Gained " + formatNumber(rankGain, 1) + " rank"); + this.log( + `${person.whoAmI()}: ` + action.name + " successful! Gained " + formatNumber(rankGain, 1) + " rank", + ); } } else { - this.gainActionStats(player, action, false); + retValue = this.getActionStats(action, false); let rankLoss = 0; let damage = 0; if (action.rankLoss) { rankLoss = addOffset(action.rankLoss, 10); - this.changeRank(player, -1 * rankLoss); + this.changeRank(person, -1 * rankLoss); } if (action.hpLoss) { damage = action.hpLoss * difficultyMultiplier; damage = Math.ceil(addOffset(damage, 10)); const cost = calculateHospitalizationCost(player, damage); - if (player.takeDamage(damage)) { + if (person.takeDamage(damage)) { ++this.numHosp; this.moneyLost += cost; } @@ -1399,7 +1430,8 @@ export class Bladeburner implements IBladeburner { if (this.logging.blackops) { this.log( - action.name + + `${person.whoAmI()}: ` + + action.name + " failed! Lost " + formatNumber(rankLoss, 1) + " rank and took " + @@ -1415,9 +1447,18 @@ export class Bladeburner implements IBladeburner { if (teamCount >= 1) { const losses = getRandomInt(1, teamLossMax); this.teamSize -= losses; + if (this.teamSize < this.sleeveSize) { + const sup = player.sleeves.filter((x) => x.bbAction == "Support main sleeve"); + for (let i = 0; i > this.teamSize - this.sleeveSize; i--) { + const r = Math.floor(Math.random() * sup.length); + sup[r].takeDamage(sup[r].max_hp); + sup.splice(r, 1); + } + this.teamSize += this.sleeveSize; + } this.teamLost += losses; if (this.logging.blackops) { - this.log("You lost " + formatNumber(losses, 0) + " team members during " + action.name); + this.log(`${person.whoAmI()}: You lost ${formatNumber(losses, 0)} team members during ${action.name}`); } } } catch (e: any) { @@ -1427,19 +1468,20 @@ export class Bladeburner implements IBladeburner { } case ActionTypes["Training"]: { this.stamina -= 0.5 * BladeburnerConstants.BaseStaminaLoss; - const strExpGain = 30 * player.strength_exp_mult, - defExpGain = 30 * player.defense_exp_mult, - dexExpGain = 30 * player.dexterity_exp_mult, - agiExpGain = 30 * player.agility_exp_mult, + const strExpGain = 30 * person.strength_exp_mult, + defExpGain = 30 * person.defense_exp_mult, + dexExpGain = 30 * person.dexterity_exp_mult, + agiExpGain = 30 * person.agility_exp_mult, staminaGain = 0.04 * this.skillMultipliers.stamina; - player.gainStrengthExp(strExpGain); - player.gainDefenseExp(defExpGain); - player.gainDexterityExp(dexExpGain); - player.gainAgilityExp(agiExpGain); + retValue.str = strExpGain; + retValue.def = defExpGain; + retValue.dex = dexExpGain; + retValue.agi = agiExpGain; this.staminaBonus += staminaGain; if (this.logging.general) { this.log( - "Training completed. Gained: " + + `${person.whoAmI()}: ` + + "Training completed. Gained: " + formatNumber(strExpGain, 1) + " str exp, " + formatNumber(defExpGain, 1) + @@ -1452,80 +1494,89 @@ export class Bladeburner implements IBladeburner { " max stamina", ); } - this.startAction(player, this.action); // Repeat action break; } case ActionTypes["FieldAnalysis"]: case ActionTypes["Field Analysis"]: { // Does not use stamina. Effectiveness depends on hacking, int, and cha let eff = - 0.04 * Math.pow(player.hacking, 0.3) + - 0.04 * Math.pow(player.intelligence, 0.9) + - 0.02 * Math.pow(player.charisma, 0.3); - eff *= player.bladeburner_analysis_mult; + 0.04 * Math.pow(person.hacking, 0.3) + + 0.04 * Math.pow(person.intelligence, 0.9) + + 0.02 * Math.pow(person.charisma, 0.3); + eff *= person.bladeburner_analysis_mult; if (isNaN(eff) || eff < 0) { throw new Error("Field Analysis Effectiveness calculated to be NaN or negative"); } - const hackingExpGain = 20 * player.hacking_exp_mult; - const charismaExpGain = 20 * player.charisma_exp_mult; + const hackingExpGain = 20 * person.hacking_exp_mult; + const charismaExpGain = 20 * person.charisma_exp_mult; const rankGain = 0.1 * BitNodeMultipliers.BladeburnerRank; - player.gainHackingExp(hackingExpGain); - player.gainIntelligenceExp(BladeburnerConstants.BaseIntGain); - player.gainCharismaExp(charismaExpGain); - this.changeRank(player, rankGain); + retValue.hack = hackingExpGain; + retValue.cha = charismaExpGain; + retValue.int = BladeburnerConstants.BaseIntGain; + this.changeRank(person, rankGain); this.getCurrentCity().improvePopulationEstimateByPercentage(eff * this.skillMultipliers.successChanceEstimate); if (this.logging.general) { this.log( - `Field analysis completed. Gained ${formatNumber(rankGain, 2)} rank, ` + + `${person.whoAmI()}: ` + + `Field analysis completed. Gained ${formatNumber(rankGain, 2)} rank, ` + `${formatNumber(hackingExpGain, 1)} hacking exp, and ` + `${formatNumber(charismaExpGain, 1)} charisma exp`, ); } - this.startAction(player, this.action); // Repeat action break; } case ActionTypes["Recruitment"]: { - const successChance = this.getRecruitmentSuccessChance(player); + const successChance = this.getRecruitmentSuccessChance(person); + const recruitTime = this.getRecruitmentTime(person) * 1000; if (Math.random() < successChance) { - const expGain = 2 * BladeburnerConstants.BaseStatGain * this.actionTimeToComplete; - player.gainCharismaExp(expGain); + const expGain = 2 * BladeburnerConstants.BaseStatGain * recruitTime; + retValue.cha = expGain; ++this.teamSize; if (this.logging.general) { - this.log("Successfully recruited a team member! Gained " + formatNumber(expGain, 1) + " charisma exp"); + this.log( + `${person.whoAmI()}: ` + + "Successfully recruited a team member! Gained " + + formatNumber(expGain, 1) + + " charisma exp", + ); } } else { - const expGain = BladeburnerConstants.BaseStatGain * this.actionTimeToComplete; - player.gainCharismaExp(expGain); + const expGain = BladeburnerConstants.BaseStatGain * recruitTime; + retValue.cha = expGain; if (this.logging.general) { - this.log("Failed to recruit a team member. Gained " + formatNumber(expGain, 1) + " charisma exp"); + this.log( + `${person.whoAmI()}: ` + + "Failed to recruit a team member. Gained " + + formatNumber(expGain, 1) + + " charisma exp", + ); } } - this.startAction(player, this.action); // Repeat action break; } case ActionTypes["Diplomacy"]: { - const eff = this.getDiplomacyEffectiveness(player); + const eff = this.getDiplomacyEffectiveness(person); this.getCurrentCity().chaos *= eff; if (this.getCurrentCity().chaos < 0) { this.getCurrentCity().chaos = 0; } if (this.logging.general) { this.log( - `Diplomacy completed. Chaos levels in the current city fell by ${numeralWrapper.formatPercentage(1 - eff)}`, + `${person.whoAmI()}: Diplomacy completed. Chaos levels in the current city fell by ${numeralWrapper.formatPercentage( + 1 - eff, + )}`, ); } - this.startAction(player, this.action); // Repeat Action break; } case ActionTypes["Hyperbolic Regeneration Chamber"]: { - player.regenerateHp(BladeburnerConstants.HrcHpGain); + person.regenerateHp(BladeburnerConstants.HrcHpGain); const staminaGain = this.maxStamina * (BladeburnerConstants.HrcStaminaGain / 100); this.stamina = Math.min(this.maxStamina, this.stamina + staminaGain); - this.startAction(player, this.action); if (this.logging.general) { this.log( - `Rested in Hyperbolic Regeneration Chamber. Restored ${ + `${person.whoAmI()}: Rested in Hyperbolic Regeneration Chamber. Restored ${ BladeburnerConstants.HrcHpGain } HP and gained ${numeralWrapper.formatStamina(staminaGain)} stamina`, ); @@ -1544,24 +1595,37 @@ export class Bladeburner implements IBladeburner { this.operations[operation].count += (60 * 3 * growthF()) / BladeburnerConstants.ActionCountGrowthPeriod; } if (this.logging.general) { - this.log(`Incited violence in the synthoid communities.`); + this.log(`${person.whoAmI()}: Incited violence in the synthoid communities.`); } for (const cityName of Object.keys(this.cities)) { const city = this.cities[cityName]; city.chaos += 10; city.chaos += city.chaos / (Math.log(city.chaos) / Math.log(10)); } - - this.startAction(player, this.action); break; } default: - console.error(`Bladeburner.completeAction() called for invalid action: ${this.action.type}`); + console.error(`Bladeburner.completeAction() called for invalid action: ${actionIdent.type}`); break; } + return retValue; + } + + infiltrateSynthoidCommunities(p: IPlayer): void { + const infilSleeves = p.sleeves.filter((s) => s.bbAction === "Infiltrate synthoids").length; + const amt = Math.pow(infilSleeves, -0.5) / 2; + for (const contract of Object.keys(this.contracts)) { + this.contracts[contract].count += amt; + } + for (const operation of Object.keys(this.operations)) { + this.operations[operation].count += amt; + } + if (this.logging.general) { + this.log(`Sleeve: Infiltrate the synthoid communities.`); + } } - changeRank(player: IPlayer, change: number): void { + changeRank(person: IPerson, change: number): void { if (isNaN(change)) { throw new Error("NaN passed into Bladeburner.changeRank()"); } @@ -1582,7 +1646,7 @@ export class Bladeburner implements IBladeburner { if (bladeburnerFac.isMember) { const favorBonus = 1 + bladeburnerFac.favor / 100; bladeburnerFac.playerReputation += - BladeburnerConstants.RankToFactionRepFactor * change * player.faction_rep_mult * favorBonus; + BladeburnerConstants.RankToFactionRepFactor * change * person.faction_rep_mult * favorBonus; } } @@ -1613,7 +1677,19 @@ export class Bladeburner implements IBladeburner { this.actionTimeOverflow = 0; if (this.actionTimeCurrent >= this.actionTimeToComplete) { this.actionTimeOverflow = this.actionTimeCurrent - this.actionTimeToComplete; - return this.completeAction(router, player); + const retValue = this.completeAction(player, player, this.action); + player.gainMoney(retValue.money, "bladeburner"); + player.gainStats(retValue); + // Operation Daedalus + const action = this.getActionObject(this.action); + if (action == null) { + throw new Error("Failed to get BlackOperation Object for: " + this.action.name); + } else if (action.name === BlackOperationNames.OperationDaedalus && this.blackops[action.name]) { + this.resetAction(); + router.toBitVerse(false, false); + } else if (this.action.type != ActionTypes["BlackOperation"] && this.action.type != ActionTypes["BlackOp"]) { + this.startAction(player, this.action); // Repeat action + } } } @@ -2092,67 +2168,53 @@ export class Bladeburner implements IBladeburner { } } - getActionTimeNetscriptFn(player: IPlayer, type: string, name: string, workerScript: WorkerScript): number { - const errorLogText = `Invalid action: type='${type}' name='${name}'`; + getActionTimeNetscriptFn(person: IPerson, type: string, name: string): number | string { const actionId = this.getActionIdFromTypeAndName(type, name); if (actionId == null) { - workerScript.log("bladeburner.getActionTime", () => errorLogText); - return -1; + return "bladeburner.getActionTime"; } const actionObj = this.getActionObject(actionId); if (actionObj == null) { - workerScript.log("bladeburner.getActionTime", () => errorLogText); - return -1; + return "bladeburner.getActionTime"; } - switch (actionId.type) { case ActionTypes["Contract"]: case ActionTypes["Operation"]: case ActionTypes["BlackOp"]: case ActionTypes["BlackOperation"]: - return actionObj.getActionTime(this) * 1000; + return actionObj.getActionTime(this, person) * 1000; case ActionTypes["Training"]: case ActionTypes["Field Analysis"]: case ActionTypes["FieldAnalysis"]: return 30000; case ActionTypes["Recruitment"]: - return this.getRecruitmentTime(player) * 1000; + return this.getRecruitmentTime(person) * 1000; case ActionTypes["Diplomacy"]: case ActionTypes["Hyperbolic Regeneration Chamber"]: case ActionTypes["Incite Violence"]: return 60000; default: - workerScript.log("bladeburner.getActionTime", () => errorLogText); - return -1; + return "bladeburner.getActionTime"; } } - getActionEstimatedSuccessChanceNetscriptFn( - player: IPlayer, - type: string, - name: string, - workerScript: WorkerScript, - ): [number, number] { - const errorLogText = `Invalid action: type='${type}' name='${name}'`; + getActionEstimatedSuccessChanceNetscriptFn(person: IPerson, type: string, name: string): [number, number] | string { const actionId = this.getActionIdFromTypeAndName(type, name); if (actionId == null) { - workerScript.log("bladeburner.getActionEstimatedSuccessChance", () => errorLogText); - return [-1, -1]; + return "bladeburner.getActionEstimatedSuccessChance"; } const actionObj = this.getActionObject(actionId); if (actionObj == null) { - workerScript.log("bladeburner.getActionEstimatedSuccessChance", () => errorLogText); - return [-1, -1]; + return "bladeburner.getActionEstimatedSuccessChance"; } - switch (actionId.type) { case ActionTypes["Contract"]: case ActionTypes["Operation"]: case ActionTypes["BlackOp"]: case ActionTypes["BlackOperation"]: - return actionObj.getEstSuccessChance(this); + return actionObj.getEstSuccessChance(this, person); case ActionTypes["Training"]: case ActionTypes["Field Analysis"]: case ActionTypes["FieldAnalysis"]: @@ -2161,12 +2223,11 @@ export class Bladeburner implements IBladeburner { case ActionTypes["Incite Violence"]: return [1, 1]; case ActionTypes["Recruitment"]: { - const recChance = this.getRecruitmentSuccessChance(player); + const recChance = this.getRecruitmentSuccessChance(person); return [recChance, recChance]; } default: - workerScript.log("bladeburner.getActionEstimatedSuccessChance", () => errorLogText); - return [-1, -1]; + return "bladeburner.getActionEstimatedSuccessChance"; } } diff --git a/src/Bladeburner/IAction.tsx b/src/Bladeburner/IAction.tsx index 274bae4c9..666ffbfe9 100644 --- a/src/Bladeburner/IAction.tsx +++ b/src/Bladeburner/IAction.tsx @@ -1,3 +1,4 @@ +import { IPerson } from "../PersonObjects/IPerson"; import { IBladeburner } from "./IBladeburner"; interface IStatsMultiplier { @@ -55,15 +56,15 @@ export interface IAction { teamCount: number; getDifficulty(): number; - attempt(inst: IBladeburner): boolean; + attempt(inst: IBladeburner, person: IPerson): boolean; getActionTimePenalty(): number; - getActionTime(inst: IBladeburner): number; + getActionTime(inst: IBladeburner, person: IPerson): number; getTeamSuccessBonus(inst: IBladeburner): number; getActionTypeSkillSuccessBonus(inst: IBladeburner): number; getChaosCompetencePenalty(inst: IBladeburner, params: ISuccessChanceParams): number; getChaosDifficultyBonus(inst: IBladeburner): number; - getEstSuccessChance(inst: IBladeburner): [number, number]; - getSuccessChance(inst: IBladeburner, params: ISuccessChanceParams): number; + getEstSuccessChance(inst: IBladeburner, person: IPerson): [number, number]; + getSuccessChance(inst: IBladeburner, person: IPerson, params: ISuccessChanceParams): number; getSuccessesNeededForNextLevel(baseSuccessesPerLevel: number): number; setMaxLevel(baseSuccessesPerLevel: number): void; toJSON(): any; diff --git a/src/Bladeburner/IBladeburner.ts b/src/Bladeburner/IBladeburner.ts index 840ed20f4..66f97e0f7 100644 --- a/src/Bladeburner/IBladeburner.ts +++ b/src/Bladeburner/IBladeburner.ts @@ -3,6 +3,8 @@ import { City } from "./City"; import { Skill } from "./Skill"; import { IAction } from "./IAction"; import { IPlayer } from "../PersonObjects/IPlayer"; +import { IPerson } from "../PersonObjects/IPerson"; +import { ITaskTracker } from "../PersonObjects/ITaskTracker"; import { IRouter } from "../ui/Router"; import { WorkerScript } from "../Netscript/WorkerScript"; @@ -70,13 +72,8 @@ export interface IBladeburner { getGeneralActionNamesNetscriptFn(): string[]; getSkillNamesNetscriptFn(): string[]; startActionNetscriptFn(player: IPlayer, type: string, name: string, workerScript: WorkerScript): boolean; - getActionTimeNetscriptFn(player: IPlayer, type: string, name: string, workerScript: WorkerScript): number; - getActionEstimatedSuccessChanceNetscriptFn( - player: IPlayer, - type: string, - name: string, - workerScript: WorkerScript, - ): [number, number]; + getActionTimeNetscriptFn(person: IPerson, type: string, name: string): number | string; + getActionEstimatedSuccessChanceNetscriptFn(person: IPerson, type: string, name: string): [number, number] | string; getActionCountRemainingNetscriptFn(type: string, name: string, workerScript: WorkerScript): number; getSkillLevelNetscriptFn(skillName: string, workerScript: WorkerScript): number; getSkillUpgradeCostNetscriptFn(skillName: string, workerScript: WorkerScript): number; @@ -95,20 +92,22 @@ export interface IBladeburner { triggerMigration(sourceCityName: string): void; triggerPotentialMigration(sourceCityName: string, chance: number): void; randomEvent(): void; - gainActionStats(player: IPlayer, action: IAction, success: boolean): void; getDiplomacyEffectiveness(player: IPlayer): number; - getRecruitmentSuccessChance(player: IPlayer): number; - getRecruitmentTime(player: IPlayer): number; + getRecruitmentSuccessChance(player: IPerson): number; + getRecruitmentTime(player: IPerson): number; resetSkillMultipliers(): void; updateSkillMultipliers(): void; - completeOperation(success: boolean): void; + completeOperation(success: boolean, player: IPlayer): void; getActionObject(actionId: IActionIdentifier): IAction | null; - completeContract(success: boolean): void; - completeAction(router: IRouter, player: IPlayer): void; + completeContract(success: boolean, actionIdent: IActionIdentifier): void; + completeAction(player: IPlayer, person: IPerson, actionIdent: IActionIdentifier, isPlayer?: boolean): ITaskTracker; + infiltrateSynthoidCommunities(p: IPlayer): void; changeRank(player: IPlayer, change: number): void; processAction(router: IRouter, player: IPlayer, seconds: number): void; calculateStaminaGainPerSecond(player: IPlayer): number; calculateMaxStamina(player: IPlayer): void; create(): void; process(router: IRouter, player: IPlayer): void; + getActionStats(action: IAction, success: boolean): ITaskTracker; + sleeveSupport(joining: boolean): void; } diff --git a/src/Bladeburner/ui/BlackOpElem.tsx b/src/Bladeburner/ui/BlackOpElem.tsx index ae406f14d..7c45836c3 100644 --- a/src/Bladeburner/ui/BlackOpElem.tsx +++ b/src/Bladeburner/ui/BlackOpElem.tsx @@ -37,7 +37,7 @@ export function BlackOpElem(props: IProps): React.ReactElement { const isActive = props.bladeburner.action.type === ActionTypes["BlackOperation"] && props.action.name === props.bladeburner.action.name; - const actionTime = props.action.getActionTime(props.bladeburner); + const actionTime = props.action.getActionTime(props.bladeburner, props.player); const hasReqdRank = props.bladeburner.rank >= props.action.reqdRank; const computedActionTimeCurrent = Math.min( props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, diff --git a/src/Bladeburner/ui/ContractElem.tsx b/src/Bladeburner/ui/ContractElem.tsx index fad0865df..5edf04842 100644 --- a/src/Bladeburner/ui/ContractElem.tsx +++ b/src/Bladeburner/ui/ContractElem.tsx @@ -32,7 +32,7 @@ export function ContractElem(props: IProps): React.ReactElement { props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, props.bladeburner.actionTimeToComplete, ); - const actionTime = props.action.getActionTime(props.bladeburner); + const actionTime = props.action.getActionTime(props.bladeburner, props.player); const actionData = Contracts[props.action.name]; if (actionData === undefined) { diff --git a/src/Bladeburner/ui/OperationElem.tsx b/src/Bladeburner/ui/OperationElem.tsx index 6e50fd547..e217e4225 100644 --- a/src/Bladeburner/ui/OperationElem.tsx +++ b/src/Bladeburner/ui/OperationElem.tsx @@ -33,7 +33,7 @@ export function OperationElem(props: IProps): React.ReactElement { props.bladeburner.actionTimeCurrent + props.bladeburner.actionTimeOverflow, props.bladeburner.actionTimeToComplete, ); - const actionTime = props.action.getActionTime(props.bladeburner); + const actionTime = props.action.getActionTime(props.bladeburner, props.player); const actionData = Operations[props.action.name]; if (actionData === undefined) { diff --git a/src/Bladeburner/ui/SuccessChance.tsx b/src/Bladeburner/ui/SuccessChance.tsx index 934b8338c..285a730d6 100644 --- a/src/Bladeburner/ui/SuccessChance.tsx +++ b/src/Bladeburner/ui/SuccessChance.tsx @@ -4,6 +4,7 @@ import { StealthIcon } from "./StealthIcon"; import { KillIcon } from "./KillIcon"; import { IAction } from "../IAction"; import { IBladeburner } from "../IBladeburner"; +import { Player } from "../../Player"; interface IProps { bladeburner: IBladeburner; @@ -11,7 +12,7 @@ interface IProps { } export function SuccessChance(props: IProps): React.ReactElement { - const estimatedSuccessChance = props.action.getEstSuccessChance(props.bladeburner); + const estimatedSuccessChance = props.action.getEstSuccessChance(props.bladeburner, Player); let chance = <>; if (estimatedSuccessChance[0] === estimatedSuccessChance[1]) { diff --git a/src/Crime/Crime.ts b/src/Crime/Crime.ts index c47a38f69..c7584ff05 100644 --- a/src/Crime/Crime.ts +++ b/src/Crime/Crime.ts @@ -1,6 +1,6 @@ import { CONSTANTS } from "../Constants"; import { IPlayer } from "../PersonObjects/IPlayer"; -import { IPlayerOrSleeve } from "../PersonObjects/IPlayerOrSleeve"; +import { IPerson } from "../PersonObjects/IPerson"; import { IRouter } from "../ui/Router"; import { WorkerScript } from "../Netscript/WorkerScript"; import { CrimeType } from "../utils/WorkType"; @@ -117,7 +117,7 @@ export class Crime { return this.time; } - successRate(p: IPlayerOrSleeve): number { + successRate(p: IPerson): number { let chance: number = this.hacking_success_weight * p.hacking + this.strength_success_weight * p.strength + diff --git a/src/Hospital/Hospital.ts b/src/Hospital/Hospital.ts index d811cae54..c392df19c 100644 --- a/src/Hospital/Hospital.ts +++ b/src/Hospital/Hospital.ts @@ -1,5 +1,5 @@ -import { CONSTANTS } from "../Constants"; import { IPlayer } from "../PersonObjects/IPlayer"; +import { CONSTANTS } from "../Constants"; export function getHospitalizationCost(p: IPlayer): number { if (p.money < 0) { diff --git a/src/Netscript/RamCostGenerator.ts b/src/Netscript/RamCostGenerator.ts index 3c55b635f..7f88d99c6 100644 --- a/src/Netscript/RamCostGenerator.ts +++ b/src/Netscript/RamCostGenerator.ts @@ -284,6 +284,7 @@ const sleeve: IMap = { getSleeveAugmentations: RamCostConstants.ScriptSleeveBaseRamCost, getSleevePurchasableAugs: RamCostConstants.ScriptSleeveBaseRamCost, purchaseSleeveAug: RamCostConstants.ScriptSleeveBaseRamCost, + setToBladeburnerAction: RamCostConstants.ScriptSleeveBaseRamCost, }; // Stanek API diff --git a/src/NetscriptFunctions/Bladeburner.ts b/src/NetscriptFunctions/Bladeburner.ts index f3a700e86..e87490fa7 100644 --- a/src/NetscriptFunctions/Bladeburner.ts +++ b/src/NetscriptFunctions/Bladeburner.ts @@ -125,7 +125,14 @@ export function NetscriptBladeburner(player: IPlayer, workerScript: WorkerScript const bladeburner = player.bladeburner; if (bladeburner === null) throw new Error("Should not be called without Bladeburner"); try { - return bladeburner.getActionTimeNetscriptFn(player, type, name, workerScript); + const time = bladeburner.getActionTimeNetscriptFn(player, type, name); + if (typeof time === "string") { + const errorLogText = `Invalid action: type='${type}' name='${name}'`; + ctx.log(() => errorLogText); + return -1; + } else { + return time; + } } catch (e: any) { throw ctx.makeRuntimeErrorMsg(e); } @@ -139,7 +146,14 @@ export function NetscriptBladeburner(player: IPlayer, workerScript: WorkerScript const bladeburner = player.bladeburner; if (bladeburner === null) throw new Error("Should not be called without Bladeburner"); try { - return bladeburner.getActionEstimatedSuccessChanceNetscriptFn(player, type, name, workerScript); + const chance = bladeburner.getActionEstimatedSuccessChanceNetscriptFn(player, type, name); + if (typeof chance === "string") { + const errorLogText = `Invalid action: type='${type}' name='${name}'`; + ctx.log(() => errorLogText); + return [-1, -1]; + } else { + return chance; + } } catch (e: any) { throw ctx.makeRuntimeErrorMsg(e); } diff --git a/src/NetscriptFunctions/Singularity.ts b/src/NetscriptFunctions/Singularity.ts index 0d8c04e77..8cf3ffc5a 100644 --- a/src/NetscriptFunctions/Singularity.ts +++ b/src/NetscriptFunctions/Singularity.ts @@ -29,7 +29,7 @@ import { Router } from "../ui/GameRoot"; import { SpecialServers } from "../Server/data/SpecialServers"; import { Page } from "../ui/Router"; import { Locations } from "../Locations/Locations"; -import { GetServer, AddToAllServers, createUniqueRandomIp } from "../Server/AllServers"; +import { GetServer } from "../Server/AllServers"; import { Programs } from "../Programs/Programs"; import { numeralWrapper } from "../ui/numeralFormat"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; @@ -39,7 +39,7 @@ import { Factions, factionExists } from "../Faction/Factions"; import { Faction } from "../Faction/Faction"; import { netscriptDelay } from "../NetscriptEvaluator"; import { convertTimeMsToTimeElapsedString } from "../utils/StringHelperFunctions"; -import { getServerOnNetwork, safetlyCreateUniqueServer } from "../Server/ServerHelpers"; +import { getServerOnNetwork } from "../Server/ServerHelpers"; import { Terminal } from "../Terminal"; import { calculateHackingTime } from "../Hacking"; import { Server } from "../Server/Server"; diff --git a/src/NetscriptFunctions/Sleeve.ts b/src/NetscriptFunctions/Sleeve.ts index e6beb8064..cca654cac 100644 --- a/src/NetscriptFunctions/Sleeve.ts +++ b/src/NetscriptFunctions/Sleeve.ts @@ -310,5 +310,36 @@ export function NetscriptSleeve(player: IPlayer): InternalAPI { 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); + }, }; } diff --git a/src/PersonObjects/IPerson.ts b/src/PersonObjects/IPerson.ts new file mode 100644 index 000000000..18b697de2 --- /dev/null +++ b/src/PersonObjects/IPerson.ts @@ -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; +} diff --git a/src/PersonObjects/IPlayer.ts b/src/PersonObjects/IPlayer.ts index 63a50ae9f..be62e8283 100644 --- a/src/PersonObjects/IPlayer.ts +++ b/src/PersonObjects/IPlayer.ts @@ -30,11 +30,10 @@ import { WorkerScript } from "../Netscript/WorkerScript"; import { HacknetServer } from "../Hacknet/HacknetServer"; import { ISkillProgress } from "./formulas/skill"; import { PlayerAchievement } from "../Achievements/Achievements"; +import { IPerson } from "./IPerson"; import { WorkType, ClassType, CrimeType } from "../utils/WorkType"; -export interface IPlayer { - // Class members - augmentations: IPlayerOwnedAugmentation[]; +export interface IPlayer extends IPerson { bitNodeN: number; city: CityName; companyName: string; @@ -186,13 +185,6 @@ export interface IPlayer { canAccessGang(): boolean; canAccessGrafting(): boolean; canAfford(cost: number): boolean; - gainHackingExp(exp: number): void; - gainStrengthExp(exp: number): void; - gainDefenseExp(exp: number): void; - gainDexterityExp(exp: number): void; - gainAgilityExp(exp: number): void; - gainCharismaExp(exp: number): void; - gainIntelligenceExp(exp: number): void; gainMoney(money: number, source: string): void; getCurrentServer(): BaseServer; getGangFaction(): Faction; @@ -215,7 +207,6 @@ export interface IPlayer { process(router: IRouter, numCycles?: number): void; reapplyAllAugmentations(resetMultipliers?: boolean): void; reapplyAllSourceFiles(): void; - regenerateHp(amt: number): void; setMoney(amt: number): void; singularityStopWork(): string; startBladeburner(p: any): void; @@ -242,12 +233,9 @@ export interface IPlayer { startGang(facName: string, isHacking: boolean): void; startWork(companyName: string): void; startWorkPartTime(companyName: string): void; - takeDamage(amt: number): boolean; travel(to: CityName): boolean; giveExploit(exploit: Exploit): void; giveAchievement(achievementId: string): void; - queryStatFromString(str: string): number; - getIntelligenceBonus(weight: number): number; getCasinoWinnings(): number; quitJob(company: string, sing?: boolean): void; hasJob(): boolean; @@ -268,7 +256,6 @@ export interface IPlayer { resetMultipliers(): void; prestigeAugmentation(): void; prestigeSourceFile(): void; - calculateSkill(exp: number, mult?: number): number; calculateSkillProgress(exp: number, mult?: number): ISkillProgress; resetWorkStatus(generalType?: WorkType, group?: string, workType?: string): void; getWorkHackExpGain(): number; diff --git a/src/PersonObjects/IPlayerOrSleeve.ts b/src/PersonObjects/IPlayerOrSleeve.ts deleted file mode 100644 index 366c31dde..000000000 --- a/src/PersonObjects/IPlayerOrSleeve.ts +++ /dev/null @@ -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; -} diff --git a/src/PersonObjects/ITaskTracker.ts b/src/PersonObjects/ITaskTracker.ts new file mode 100644 index 000000000..f6acc8799 --- /dev/null +++ b/src/PersonObjects/ITaskTracker.ts @@ -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, + }; +} diff --git a/src/PersonObjects/Person.ts b/src/PersonObjects/Person.ts index 9db8d5f17..8790a26b8 100644 --- a/src/PersonObjects/Person.ts +++ b/src/PersonObjects/Person.ts @@ -1,4 +1,4 @@ -// Base class representing a person-like object +import * as generalMethods from "./Player/PlayerObjectGeneralMethods"; import { Augmentation } from "../Augmentation/Augmentation"; import { IPlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; @@ -6,32 +6,10 @@ import { CityName } from "../Locations/data/CityNames"; import { CONSTANTS } from "../Constants"; import { calculateSkill } from "./formulas/skill"; import { calculateIntelligenceBonus } from "./formulas/intelligence"; +import { IPerson } from "./IPerson"; -// Interface that defines a generic object used to track experience/money -// earnings for tasks -export interface ITaskTracker { - hack: number; - str: number; - def: number; - dex: number; - agi: number; - cha: number; - money: number; -} - -export function createTaskTracker(): ITaskTracker { - return { - hack: 0, - str: 0, - def: 0, - dex: 0, - agi: 0, - cha: 0, - money: 0, - }; -} - -export abstract class Person { +// Base class representing a person-like object +export abstract class Person implements IPerson { /** * Stats */ @@ -41,7 +19,7 @@ export abstract class Person { dexterity = 1; agility = 1; charisma = 1; - intelligence = 1; + intelligence = 0; hp = 10; max_hp = 10; @@ -97,24 +75,28 @@ export abstract class Person { bladeburner_analysis_mult = 1; bladeburner_success_chance_mult = 1; - infiltration_base_rep_increase = 0; - infiltration_rep_mult = 1; - infiltration_trade_mult = 1; - infiltration_sell_mult = 1; - infiltration_timer_mult = 1; - infiltration_damage_reduction_mult = 1; - /** * Augmentations */ augmentations: IPlayerOwnedAugmentation[] = []; - queuedAugmentations: IPlayerOwnedAugmentation[] = []; /** * City that the person is in */ city: CityName = CityName.Sector12; + gainHackingExp = generalMethods.gainHackingExp; + gainStrengthExp = generalMethods.gainStrengthExp; + gainDefenseExp = generalMethods.gainDefenseExp; + gainDexterityExp = generalMethods.gainDexterityExp; + gainAgilityExp = generalMethods.gainAgilityExp; + gainCharismaExp = generalMethods.gainCharismaExp; + gainIntelligenceExp = generalMethods.gainIntelligenceExp; + gainStats = generalMethods.gainStats; + calculateSkill = generalMethods.calculateSkill; + regenerateHp = generalMethods.regenerateHp; + queryStatFromString = generalMethods.queryStatFromString; + /** * Updates this object's multipliers for the given augmentation */ @@ -213,13 +195,6 @@ export abstract class Person { this.bladeburner_stamina_gain_mult = 1; this.bladeburner_analysis_mult = 1; this.bladeburner_success_chance_mult = 1; - - this.infiltration_base_rep_increase = 0; - this.infiltration_rep_mult = 1; - this.infiltration_trade_mult = 1; - this.infiltration_sell_mult = 1; - this.infiltration_timer_mult = 1; - this.infiltration_damage_reduction_mult = 1; } /** @@ -265,4 +240,8 @@ export abstract class Person { getIntelligenceBonus(weight: number): number { return calculateIntelligenceBonus(this.intelligence, weight); } + + abstract takeDamage(amt: number): boolean; + + abstract whoAmI(): string; } diff --git a/src/PersonObjects/Player/PlayerObject.ts b/src/PersonObjects/Player/PlayerObject.ts index 7582ec133..aae9bdd61 100644 --- a/src/PersonObjects/Player/PlayerObject.ts +++ b/src/PersonObjects/Player/PlayerObject.ts @@ -37,6 +37,7 @@ import { ISkillProgress } from "../formulas/skill"; import { PlayerAchievement } from "../../Achievements/Achievements"; import { cyrb53 } from "../../utils/StringHelperFunctions"; import { getRandomInt } from "../../utils/helpers/getRandomInt"; +import { ITaskTracker } from "../ITaskTracker"; import { CONSTANTS } from "../../Constants"; import { WorkType, ClassType, CrimeType, PlayerFactionWorkType } from "../../utils/WorkType"; @@ -203,6 +204,7 @@ export class PlayerObject implements IPlayer { gainAgilityExp: (exp: number) => void; gainCharismaExp: (exp: number) => void; gainIntelligenceExp: (exp: number) => void; + gainStats: (retValue: ITaskTracker) => void; gainMoney: (money: number, source: string) => void; getCurrentServer: () => BaseServer; getGangFaction: () => Faction; @@ -524,6 +526,7 @@ export class PlayerObject implements IPlayer { this.gainAgilityExp = generalMethods.gainAgilityExp; this.gainCharismaExp = generalMethods.gainCharismaExp; this.gainIntelligenceExp = generalMethods.gainIntelligenceExp; + this.gainStats = generalMethods.gainStats; this.queryStatFromString = generalMethods.queryStatFromString; this.resetWorkStatus = generalMethods.resetWorkStatus; this.processWorkEarnings = generalMethods.processWorkEarnings; @@ -632,6 +635,10 @@ export class PlayerObject implements IPlayer { this.applyEntropy = augmentationMethods.applyEntropy; } + whoAmI(): string { + return "Player"; + } + /** * Serialize the current object to a JSON save state. */ diff --git a/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx b/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx index d7455cb35..b44ec6516 100644 --- a/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx +++ b/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx @@ -64,6 +64,9 @@ import { SnackbarEvents, ToastVariant } from "../../ui/React/Snackbar"; import { calculateClassEarnings } from "../formulas/work"; import { achievements } from "../../Achievements/Achievements"; import { FactionNames } from "../../Faction/data/FactionNames"; +import { ITaskTracker } from "../ITaskTracker"; +import { IPerson } from "../IPerson"; +import { Player } from "../../Player"; import { graftingIntBonus } from "../Grafting/GraftingHelpers"; import { WorkType, PlayerFactionWorkType, ClassType, CrimeType } from "../../utils/WorkType"; @@ -228,7 +231,7 @@ export function receiveInvite(this: IPlayer, factionName: string): void { } //Calculates skill level based on experience. The same formula will be used for every skill -export function calculateSkill(this: IPlayer, exp: number, mult = 1): number { +export function calculateSkill(this: IPerson, exp: number, mult = 1): number { return calculateSkillF(exp, mult); } @@ -379,7 +382,7 @@ export function recordMoneySource(this: PlayerObject, amt: number, source: strin this.moneySourceB.record(amt, source); } -export function gainHackingExp(this: IPlayer, exp: number): void { +export function gainHackingExp(this: IPerson, exp: number): void { if (isNaN(exp)) { console.error("ERR: NaN passed into Player.gainHackingExp()"); return; @@ -392,7 +395,7 @@ export function gainHackingExp(this: IPlayer, exp: number): void { this.hacking = calculateSkillF(this.hacking_exp, this.hacking_mult * BitNodeMultipliers.HackingLevelMultiplier); } -export function gainStrengthExp(this: IPlayer, exp: number): void { +export function gainStrengthExp(this: IPerson, exp: number): void { if (isNaN(exp)) { console.error("ERR: NaN passed into Player.gainStrengthExp()"); return; @@ -405,7 +408,7 @@ export function gainStrengthExp(this: IPlayer, exp: number): void { this.strength = calculateSkillF(this.strength_exp, this.strength_mult * BitNodeMultipliers.StrengthLevelMultiplier); } -export function gainDefenseExp(this: IPlayer, exp: number): void { +export function gainDefenseExp(this: IPerson, exp: number): void { if (isNaN(exp)) { console.error("ERR: NaN passed into player.gainDefenseExp()"); return; @@ -421,7 +424,7 @@ export function gainDefenseExp(this: IPlayer, exp: number): void { this.hp = Math.round(this.max_hp * ratio); } -export function gainDexterityExp(this: IPlayer, exp: number): void { +export function gainDexterityExp(this: IPerson, exp: number): void { if (isNaN(exp)) { console.error("ERR: NaN passed into Player.gainDexterityExp()"); return; @@ -437,7 +440,7 @@ export function gainDexterityExp(this: IPlayer, exp: number): void { ); } -export function gainAgilityExp(this: IPlayer, exp: number): void { +export function gainAgilityExp(this: IPerson, exp: number): void { if (isNaN(exp)) { console.error("ERR: NaN passed into Player.gainAgilityExp()"); return; @@ -450,7 +453,7 @@ export function gainAgilityExp(this: IPlayer, exp: number): void { this.agility = calculateSkillF(this.agility_exp, this.agility_mult * BitNodeMultipliers.AgilityLevelMultiplier); } -export function gainCharismaExp(this: IPlayer, exp: number): void { +export function gainCharismaExp(this: IPerson, exp: number): void { if (isNaN(exp)) { console.error("ERR: NaN passed into Player.gainCharismaExp()"); return; @@ -463,17 +466,27 @@ export function gainCharismaExp(this: IPlayer, exp: number): void { this.charisma = calculateSkillF(this.charisma_exp, this.charisma_mult * BitNodeMultipliers.CharismaLevelMultiplier); } -export function gainIntelligenceExp(this: IPlayer, exp: number): void { +export function gainIntelligenceExp(this: IPerson, exp: number): void { if (isNaN(exp)) { console.error("ERROR: NaN passed into Player.gainIntelligenceExp()"); return; } - if (this.sourceFileLvl(5) > 0 || this.intelligence > 0) { + if (Player.sourceFileLvl(5) > 0 || this.intelligence > 0) { this.intelligence_exp += exp; - this.intelligence = Math.floor(this.calculateSkill(this.intelligence_exp)); + this.intelligence = Math.floor(this.calculateSkill(this.intelligence_exp, 1)); } } +export function gainStats(this: IPerson, retValue: ITaskTracker): void { + this.gainHackingExp(retValue.hack * this.hacking_exp_mult); + this.gainStrengthExp(retValue.str * this.strength_exp_mult); + this.gainDefenseExp(retValue.def * this.defense_exp_mult); + this.gainDexterityExp(retValue.dex * this.dexterity_exp_mult); + this.gainAgilityExp(retValue.agi * this.agility_exp_mult); + this.gainCharismaExp(retValue.cha * this.charisma_exp_mult); + this.gainIntelligenceExp(retValue.int); +} + //Given a string expression like "str" or "strength", returns the given stat export function queryStatFromString(this: IPlayer, str: string): number { const tempStr = str.toLowerCase(); @@ -1726,7 +1739,7 @@ export function takeDamage(this: IPlayer, amt: number): boolean { } } -export function regenerateHp(this: IPlayer, amt: number): void { +export function regenerateHp(this: IPerson, amt: number): void { if (typeof amt !== "number") { console.warn(`Player.regenerateHp() called without a numeric argument: ${amt}`); return; diff --git a/src/PersonObjects/Sleeve/Sleeve.ts b/src/PersonObjects/Sleeve/Sleeve.ts index fff9b991d..9b49e6e22 100644 --- a/src/PersonObjects/Sleeve/Sleeve.ts +++ b/src/PersonObjects/Sleeve/Sleeve.ts @@ -9,7 +9,8 @@ import { SleeveTaskType } from "./SleeveTaskTypesEnum"; import { IPlayer } from "../IPlayer"; -import { Person, ITaskTracker, createTaskTracker } from "../Person"; +import { Person } from "../Person"; +import { ITaskTracker, createTaskTracker } from "../ITaskTracker"; import { Augmentation } from "../../Augmentation/Augmentation"; @@ -33,6 +34,9 @@ import { CityName } from "../../Locations/data/CityNames"; import { LocationName } from "../../Locations/data/LocationNames"; import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../utils/JSONReviver"; +import { BladeburnerConstants } from "../../Bladeburner/data/Constants"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { capitalizeFirstLetter, capitalizeEachWord } from "../../utils/StringHelperFunctions"; export class Sleeve extends Person { /** @@ -58,6 +62,7 @@ export class Sleeve extends Person { * Faction/Company Work: Name of Faction/Company * Crime: Money earned if successful * Class/Gym: Name of university/gym + * Bladeburner: success chance */ currentTaskLocation = ""; @@ -101,6 +106,16 @@ export class Sleeve extends Person { */ gymStatType = ""; + /** + * String that stores what stat the sleeve is training at the gym + */ + bbAction = ""; + + /** + * String that stores what stat the sleeve is training at the gym + */ + bbContract = ""; + /** * Keeps track of events/notifications for this sleeve */ @@ -151,7 +166,7 @@ export class Sleeve extends Person { if (this.currentTask !== SleeveTaskType.Idle) { this.finishTask(p); } else { - this.resetTaskStatus(); + this.resetTaskStatus(p); } this.gainRatesForTask.hack = crime.hacking_exp * this.hacking_exp_mult * BitNodeMultipliers.CrimeExpGain; @@ -160,6 +175,7 @@ export class Sleeve extends Person { this.gainRatesForTask.dex = crime.dexterity_exp * this.dexterity_exp_mult * BitNodeMultipliers.CrimeExpGain; this.gainRatesForTask.agi = crime.agility_exp * this.agility_exp_mult * BitNodeMultipliers.CrimeExpGain; this.gainRatesForTask.cha = crime.charisma_exp * this.charisma_exp_mult * BitNodeMultipliers.CrimeExpGain; + this.gainRatesForTask.int = crime.intelligence_exp; this.gainRatesForTask.money = crime.money * this.crime_money_mult * BitNodeMultipliers.CrimeMoney; this.currentTaskLocation = String(this.gainRatesForTask.money); @@ -182,7 +198,7 @@ export class Sleeve extends Person { const crime: Crime | undefined = Object.values(Crimes).find((crime) => crime.name === this.crimeType); if (!crime) { console.error(`Invalid data stored in sleeve.crimeType: ${this.crimeType}`); - this.resetTaskStatus(); + this.resetTaskStatus(p); return retValue; } if (Math.random() < crime.successRate(this)) { @@ -206,11 +222,60 @@ export class Sleeve extends Person { this.currentTaskTime = 0; return retValue; } - } else { - // For other crimes... I dont think anything else needs to be done + } else if (this.currentTask === SleeveTaskType.Bladeburner) { + if (this.currentTaskMaxTime === 0) { + this.currentTaskTime = 0; + return retValue; + } + // For bladeburner, all experience and money is gained at the end + const bb = p.bladeburner; + if (bb === null) { + const errorLogText = `bladeburner is null`; + console.error(`Function: sleeves.finishTask; Message: '${errorLogText}'`); + this.resetTaskStatus(p); + return retValue; + } + + 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(); + this.resetTaskStatus(p); return retValue; } @@ -260,50 +325,56 @@ export class Sleeve extends Person { const pDexExp = exp.dex * multFac; const pAgiExp = exp.agi * multFac; const pChaExp = exp.cha * multFac; + const pIntExp = exp.int * multFac; // Experience is gained by both this sleeve and player if (pHackExp > 0) { - this.hacking_exp += pHackExp; + this.gainHackingExp(pHackExp); p.gainHackingExp(pHackExp); this.earningsForPlayer.hack += pHackExp; this.earningsForTask.hack += pHackExp; } if (pStrExp > 0) { - this.strength_exp += pStrExp; + this.gainStrengthExp(pStrExp); p.gainStrengthExp(pStrExp); this.earningsForPlayer.str += pStrExp; this.earningsForTask.str += pStrExp; } if (pDefExp > 0) { - this.defense_exp += pDefExp; + this.gainDefenseExp(pDefExp); p.gainDefenseExp(pDefExp); this.earningsForPlayer.def += pDefExp; this.earningsForTask.def += pDefExp; } if (pDexExp > 0) { - this.dexterity_exp += pDexExp; + this.gainDexterityExp(pDexExp); p.gainDexterityExp(pDexExp); this.earningsForPlayer.dex += pDexExp; this.earningsForTask.dex += pDexExp; } if (pAgiExp > 0) { - this.agility_exp += pAgiExp; + this.gainAgilityExp(pAgiExp); p.gainAgilityExp(pAgiExp); this.earningsForPlayer.agi += pAgiExp; this.earningsForTask.agi += pAgiExp; } if (pChaExp > 0) { - this.charisma_exp += pChaExp; + this.gainCharismaExp(pChaExp); p.gainCharismaExp(pChaExp); this.earningsForPlayer.cha += pChaExp; this.earningsForTask.cha += pChaExp; } + if (pIntExp > 0) { + this.gainIntelligenceExp(pIntExp); + p.gainIntelligenceExp(pIntExp); + } + // Record earnings for other sleeves this.earningsForSleeves.hack += pHackExp * (this.sync / 100); this.earningsForSleeves.str += pStrExp * (this.sync / 100); @@ -320,7 +391,8 @@ export class Sleeve extends Person { dex: pDexExp * (this.sync / 100), agi: pAgiExp * (this.sync / 100), cha: pChaExp * (this.sync / 100), - money: 0, + int: pIntExp * (this.sync / 100), + money: exp.money, }; } @@ -445,7 +517,7 @@ export class Sleeve extends Person { this.charisma_exp = 0; // Reset task-related stuff - this.resetTaskStatus(); + this.resetTaskStatus(p); this.earningsForSleeves = createTaskTracker(); this.earningsForPlayer = createTaskTracker(); this.shockRecovery(p); @@ -523,7 +595,7 @@ export class Sleeve extends Person { // for, we need to reset the sleeve's task if (p.gang) { if (fac.name === p.gang.facName) { - this.resetTaskStatus(); + this.resetTaskStatus(p); } } @@ -545,18 +617,18 @@ export class Sleeve extends Person { } case SleeveTaskType.Recovery: this.shock = Math.min(100, this.shock + 0.0002 * cyclesUsed); - if (this.shock >= 100) this.resetTaskStatus(); + if (this.shock >= 100) this.resetTaskStatus(p); break; case SleeveTaskType.Synchro: this.sync = Math.min(100, this.sync + p.getIntelligenceBonus(0.5) * 0.0002 * cyclesUsed); - if (this.sync >= 100) this.resetTaskStatus(); + if (this.sync >= 100) this.resetTaskStatus(p); break; default: break; } if (this.currentTaskMaxTime !== 0 && this.currentTaskTime >= this.currentTaskMaxTime) { - if (this.currentTask === SleeveTaskType.Crime) { + if (this.currentTask === SleeveTaskType.Crime || this.currentTask === SleeveTaskType.Bladeburner) { retValue = this.finishTask(p); } else { this.finishTask(p); @@ -573,7 +645,16 @@ export class Sleeve extends Person { /** * Resets all parameters used to keep information about the current task */ - resetTaskStatus(): void { + resetTaskStatus(p: IPlayer): void { + if (this.bbAction == "Support main sleeve") { + p.bladeburner?.sleeveSupport(false); + } + if (this.currentTask == SleeveTaskType.Class) { + const retVal = createTaskTracker(); + retVal.int = CONSTANTS.IntelligenceClassBaseExpGain * Math.round(this.currentTaskTime / 1000); + const r = this.gainExperience(p, retVal); + p.sleeves.filter((s) => s != this).forEach((s) => s.gainExperience(p, r, 1, true)); + } this.earningsForTask = createTaskTracker(); this.gainRatesForTask = createTaskTracker(); this.currentTask = SleeveTaskType.Idle; @@ -584,13 +665,15 @@ export class Sleeve extends Person { this.currentTaskLocation = ""; this.gymStatType = ""; this.className = ""; + this.bbAction = ""; + this.bbContract = "------"; } shockRecovery(p: IPlayer): boolean { if (this.currentTask !== SleeveTaskType.Idle) { this.finishTask(p); } else { - this.resetTaskStatus(); + this.resetTaskStatus(p); } this.currentTask = SleeveTaskType.Recovery; @@ -601,7 +684,7 @@ export class Sleeve extends Person { if (this.currentTask !== SleeveTaskType.Idle) { this.finishTask(p); } else { - this.resetTaskStatus(); + this.resetTaskStatus(p); } this.currentTask = SleeveTaskType.Synchro; @@ -615,7 +698,7 @@ export class Sleeve extends Person { if (this.currentTask !== SleeveTaskType.Idle) { this.finishTask(p); } else { - this.resetTaskStatus(); + this.resetTaskStatus(p); } // Set exp/money multipliers based on which university. @@ -809,7 +892,7 @@ export class Sleeve extends Person { if (this.currentTask !== SleeveTaskType.Idle) { this.finishTask(p); } else { - this.resetTaskStatus(); + this.resetTaskStatus(p); } const company: Company | null = Companies[companyName]; @@ -875,7 +958,7 @@ export class Sleeve extends Person { if (this.currentTask !== SleeveTaskType.Idle) { this.finishTask(p); } else { - this.resetTaskStatus(); + this.resetTaskStatus(p); } const factionInfo = faction.getInfo(); @@ -926,7 +1009,7 @@ export class Sleeve extends Person { if (this.currentTask !== SleeveTaskType.Idle) { this.finishTask(p); } else { - this.resetTaskStatus(); + this.resetTaskStatus(p); } // Set exp/money multipliers based on which university. @@ -994,6 +1077,162 @@ export class Sleeve extends Person { return true; } + /** + * Begin a bladeburner task + */ + bladeburner(p: IPlayer, action: string, contract: string): boolean { + if (this.currentTask !== SleeveTaskType.Idle) { + this.finishTask(p); + } else { + this.resetTaskStatus(p); + } + + this.gainRatesForTask.hack = 0; + this.gainRatesForTask.str = 0; + this.gainRatesForTask.def = 0; + this.gainRatesForTask.dex = 0; + this.gainRatesForTask.agi = 0; + this.gainRatesForTask.cha = 0; + this.gainRatesForTask.money = 0; + this.currentTaskLocation = ""; + + let time = 0; + + this.bbContract = "------"; + switch (action) { + case "Field analysis": + time = this.getBladeburnerActionTime(p, "General", action); + this.gainRatesForTask.hack = 20 * this.hacking_exp_mult; + this.gainRatesForTask.cha = 20 * this.charisma_exp_mult; + break; + case "Recruitment": + time = this.getBladeburnerActionTime(p, "General", action); + this.gainRatesForTask.cha = + 2 * BladeburnerConstants.BaseStatGain * (p.bladeburner?.getRecruitmentTime(this) ?? 0) * 1000; + this.currentTaskLocation = `(Success Rate: ${numeralWrapper.formatPercentage( + this.recruitmentSuccessChance(p), + )})`; + break; + case "Diplomacy": + time = this.getBladeburnerActionTime(p, "General", action); + break; + case "Infiltrate synthoids": + time = 60000; + this.currentTaskLocation = "This will generate additional contracts and operations"; + break; + case "Support main sleeve": + p.bladeburner?.sleeveSupport(true); + time = 0; + break; + case "Take on contracts": + time = this.getBladeburnerActionTime(p, "Contracts", contract); + this.contractGainRates(p, "Contracts", contract); + this.currentTaskLocation = this.contractSuccessChance(p, "Contracts", contract); + this.bbContract = capitalizeEachWord(contract.toLowerCase()); + break; + } + + this.bbAction = capitalizeFirstLetter(action.toLowerCase()); + this.currentTaskMaxTime = time; + this.currentTask = SleeveTaskType.Bladeburner; + return true; + } + + recruitmentSuccessChance(p: IPlayer): number { + return Math.max(0, Math.min(1, p.bladeburner?.getRecruitmentSuccessChance(this) ?? 0)); + } + + contractSuccessChance(p: IPlayer, type: string, name: string): string { + const bb = p.bladeburner; + if (bb === null) { + const errorLogText = `bladeburner is null`; + console.error(`Function: sleeves.contractSuccessChance; Message: '${errorLogText}'`); + return "0%"; + } + const chances = bb.getActionEstimatedSuccessChanceNetscriptFn(this, type, name); + if (typeof chances === "string") { + console.error(`Function: sleeves.contractSuccessChance; Message: '${chances}'`); + return "0%"; + } + if (chances[0] >= 1) { + return "100%"; + } else { + return `${numeralWrapper.formatPercentage(chances[0])} - ${numeralWrapper.formatPercentage(chances[1])}`; + } + } + + contractGainRates(p: IPlayer, type: string, name: string): void { + const bb = p.bladeburner; + if (bb === null) { + const errorLogText = `bladeburner is null`; + console.error(`Function: sleeves.contractGainRates; Message: '${errorLogText}'`); + return; + } + const actionIdent = bb.getActionIdFromTypeAndName(type, name); + if (actionIdent === null) { + const errorLogText = `Invalid action: type='${type}' name='${name}'`; + console.error(`Function: sleeves.contractGainRates; Message: '${errorLogText}'`); + this.resetTaskStatus(p); + return; + } + const action = bb.getActionObject(actionIdent); + if (action === null) { + const errorLogText = `Invalid action: type='${type}' name='${name}'`; + console.error(`Function: sleeves.contractGainRates; Message: '${errorLogText}'`); + this.resetTaskStatus(p); + return; + } + const retValue = bb.getActionStats(action, true); + this.gainRatesForTask.hack = retValue.hack; + this.gainRatesForTask.str = retValue.str; + this.gainRatesForTask.def = retValue.def; + this.gainRatesForTask.dex = retValue.dex; + this.gainRatesForTask.agi = retValue.agi; + this.gainRatesForTask.cha = retValue.cha; + const rewardMultiplier = Math.pow(action.rewardFac, action.level - 1); + this.gainRatesForTask.money = + BladeburnerConstants.ContractBaseMoneyGain * rewardMultiplier * bb.skillMultipliers.money; + } + + getBladeburnerActionTime(p: IPlayer, type: string, name: string): number { + //Maybe find workerscript and use original + const bb = p.bladeburner; + if (bb === null) { + const errorLogText = `bladeburner is null`; + console.error(`Function: sleeves.getBladeburnerActionTime; Message: '${errorLogText}'`); + return -1; + } + + const time = bb.getActionTimeNetscriptFn(this, type, name); + if (typeof time === "string") { + const errorLogText = `Invalid action: type='${type}' name='${name}'`; + console.error(`Function: sleeves.getBladeburnerActionTime; Message: '${errorLogText}'`); + return -1; + } else { + return time; + } + } + + takeDamage(amt: number): boolean { + if (typeof amt !== "number") { + console.warn(`Player.takeDamage() called without a numeric argument: ${amt}`); + return false; + } + + this.hp -= amt; + if (this.hp <= 0) { + this.shock += 0.5; + this.hp = this.max_hp; + return true; + } else { + return false; + } + } + + whoAmI(): string { + return "Sleeve"; + } + /** * Serialize the current object to a JSON save state. */ diff --git a/src/PersonObjects/Sleeve/SleeveTaskTypesEnum.ts b/src/PersonObjects/Sleeve/SleeveTaskTypesEnum.ts index 61f216d03..68881383f 100644 --- a/src/PersonObjects/Sleeve/SleeveTaskTypesEnum.ts +++ b/src/PersonObjects/Sleeve/SleeveTaskTypesEnum.ts @@ -9,6 +9,7 @@ export enum SleeveTaskType { Crime, Class, Gym, + Bladeburner, Recovery, Synchro, } diff --git a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx index 82a98a706..cc87927d3 100644 --- a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx +++ b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx @@ -30,7 +30,7 @@ export function SleeveElem(props: IProps): React.ReactElement { const [abc, setABC] = useState(["------", "------", "------"]); function setTask(): void { - props.sleeve.resetTaskStatus(); // sets to idle + props.sleeve.resetTaskStatus(player); // sets to idle switch (abc[0]) { case "------": break; @@ -49,6 +49,9 @@ export function SleeveElem(props: IProps): React.ReactElement { case "Workout at Gym": props.sleeve.workoutAtGym(player, abc[2], abc[1]); break; + case "Perform Bladeburner Actions": + props.sleeve.bladeburner(player, abc[1], abc[2]); + break; case "Shock Recovery": props.sleeve.shockRecovery(player); break; @@ -106,6 +109,20 @@ export function SleeveElem(props: IProps): React.ReactElement { case SleeveTaskType.Gym: desc = <>This sleeve is currently working out at {props.sleeve.currentTaskLocation}.; break; + case SleeveTaskType.Bladeburner: { + let message = ""; + if (props.sleeve.bbContract !== "------") { + message = ` - ${props.sleeve.bbContract} (Success Rate: ${props.sleeve.currentTaskLocation})`; + } else if (props.sleeve.currentTaskLocation !== "") { + message = props.sleeve.currentTaskLocation; + } + desc = ( + <> + This sleeve is currently attempting to {props.sleeve.bbAction}. {message} + + ); + break; + } case SleeveTaskType.Recovery: desc = ( <> @@ -168,13 +185,15 @@ export function SleeveElem(props: IProps): React.ReactElement { {desc} - {props.sleeve.currentTask === SleeveTaskType.Crime && ( - - )} + {(props.sleeve.currentTask === SleeveTaskType.Crime || + props.sleeve.currentTask === SleeveTaskType.Bladeburner) && + props.sleeve.currentTaskMaxTime > 0 && ( + + )} diff --git a/src/PersonObjects/Sleeve/ui/TaskSelector.tsx b/src/PersonObjects/Sleeve/ui/TaskSelector.tsx index 0b105e349..0de75b9b0 100644 --- a/src/PersonObjects/Sleeve/ui/TaskSelector.tsx +++ b/src/PersonObjects/Sleeve/ui/TaskSelector.tsx @@ -22,6 +22,15 @@ const universitySelectorOptions: string[] = [ const gymSelectorOptions: string[] = ["Train Strength", "Train Defense", "Train Dexterity", "Train Agility"]; +const bladeburnerSelectorOptions: string[] = [ + "Field analysis", + "Recruitment", + "Diplomacy", + "Infiltrate synthoids", + "Support main sleeve", + "Take on contracts", +]; + interface IProps { sleeve: Sleeve; player: IPlayer; @@ -84,6 +93,26 @@ function possibleFactions(player: IPlayer, sleeve: Sleeve): string[] { }); } +function possibleContracts(player: IPlayer, sleeve: Sleeve): string[] { + const bb = player.bladeburner; + if (bb === null) { + return ["------"]; + } + let contracts = bb.getContractNamesNetscriptFn(); + for (const otherSleeve of player.sleeves) { + if (sleeve === otherSleeve) { + continue; + } + if (otherSleeve.currentTask === SleeveTaskType.Bladeburner && otherSleeve.bbAction == "Take on contracts") { + contracts = contracts.filter((x) => x != otherSleeve.bbContract); + } + } + if (contracts.length === 0) { + return ["------"]; + } + return contracts; +} + const tasks: { [key: string]: undefined | ((player: IPlayer, sleeve: Sleeve) => ITaskDetails); ["------"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; @@ -92,6 +121,7 @@ const tasks: { ["Commit Crime"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; ["Take University Course"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; ["Workout at Gym"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; + ["Perform Bladeburner Actions"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; ["Shock Recovery"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; ["Synchronize"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; } = { @@ -170,6 +200,18 @@ const tasks: { return { first: gymSelectorOptions, second: () => gyms }; }, + "Perform Bladeburner Actions": (player: IPlayer, sleeve: Sleeve): ITaskDetails => { + return { + first: bladeburnerSelectorOptions, + second: (s1: string) => { + if (s1 === "Take on contracts") { + return possibleContracts(player, sleeve); + } else { + return ["------"]; + } + }, + }; + }, "Shock Recovery": (): ITaskDetails => { return { first: ["------"], second: () => ["------"] }; }, @@ -186,6 +228,7 @@ const canDo: { ["Commit Crime"]: (player: IPlayer, sleeve: Sleeve) => boolean; ["Take University Course"]: (player: IPlayer, sleeve: Sleeve) => boolean; ["Workout at Gym"]: (player: IPlayer, sleeve: Sleeve) => boolean; + ["Perform Bladeburner Actions"]: (player: IPlayer, sleeve: Sleeve) => boolean; ["Shock Recovery"]: (player: IPlayer, sleeve: Sleeve) => boolean; ["Synchronize"]: (player: IPlayer, sleeve: Sleeve) => boolean; } = { @@ -197,6 +240,7 @@ const canDo: { [CityName.Aevum, CityName.Sector12, CityName.Volhaven].includes(sleeve.city), "Workout at Gym": (player: IPlayer, sleeve: Sleeve) => [CityName.Aevum, CityName.Sector12, CityName.Volhaven].includes(sleeve.city), + "Perform Bladeburner Actions": (player: IPlayer, _: Sleeve) => player.inBladeburner(), "Shock Recovery": (player: IPlayer, sleeve: Sleeve) => sleeve.shock < 100, Synchronize: (player: IPlayer, sleeve: Sleeve) => sleeve.sync < 100, }; @@ -228,6 +272,8 @@ function getABC(sleeve: Sleeve): [string, string, string] { return ["Take University Course", sleeve.className, sleeve.currentTaskLocation]; case SleeveTaskType.Gym: return ["Workout at Gym", sleeve.gymStatType, sleeve.currentTaskLocation]; + case SleeveTaskType.Bladeburner: + return ["Perform Bladeburner Actions", sleeve.bbAction, sleeve.bbContract]; case SleeveTaskType.Recovery: return ["Shock Recovery", "------", "------"]; case SleeveTaskType.Synchro: diff --git a/src/PersonObjects/Sleeve/ui/TravelModal.tsx b/src/PersonObjects/Sleeve/ui/TravelModal.tsx index 0883b1a8d..6bc4af1be 100644 --- a/src/PersonObjects/Sleeve/ui/TravelModal.tsx +++ b/src/PersonObjects/Sleeve/ui/TravelModal.tsx @@ -26,7 +26,7 @@ export function TravelModal(props: IProps): React.ReactElement { } props.sleeve.city = city as CityName; player.loseMoney(CONSTANTS.TravelCost, "sleeve"); - props.sleeve.resetTaskStatus(); + props.sleeve.resetTaskStatus(player); props.rerender(); props.onClose(); } diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts index dfc63eee6..6dfc47d9b 100644 --- a/src/ScriptEditor/NetscriptDefinitions.d.ts +++ b/src/ScriptEditor/NetscriptDefinitions.d.ts @@ -3789,6 +3789,20 @@ export interface Sleeve { * @returns True if the aug was purchased and installed on the sleeve, false otherwise. */ purchaseSleeveAug(sleeveNumber: number, augName: string): boolean; + + /** + * Set a sleeve to perform bladeburner actions. + * @remarks + * RAM cost: 4 GB + * + * Return a boolean indicating whether or not the sleeve started working out. + * + * @param sleeveNumber - Index of the sleeve to workout at the gym. + * @param action - Name of the action to be performed. + * @param contract - Name of the contract if applicable. + * @returns True if the sleeve started working out, false otherwise. + */ + setToBladeburnerAction(sleeveNumber: number, action: string, contract?: string): boolean; } /** diff --git a/src/utils/StringHelperFunctions.ts b/src/utils/StringHelperFunctions.ts index 58c685871..60bc09d6e 100644 --- a/src/utils/StringHelperFunctions.ts +++ b/src/utils/StringHelperFunctions.ts @@ -117,6 +117,17 @@ function cyrb53(str: string, seed = 0): string { return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16); } +function capitalizeFirstLetter(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +function capitalizeEachWord(s: string): string { + return s + .split(" ") + .map((word) => capitalizeFirstLetter(word)) + .join(" "); +} + export { convertTimeMsToTimeElapsedString, longestCommonStart, @@ -124,4 +135,6 @@ export { formatNumber, generateRandomString, cyrb53, + capitalizeFirstLetter, + capitalizeEachWord, };