Compare commits

...

5 Commits

11 changed files with 136 additions and 60 deletions

View File

@@ -4,19 +4,10 @@ import { AchievementList } from "./AchievementList";
import { achievements } from "./Achievements"; import { achievements } from "./Achievements";
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
import { Player } from "@player"; import { Player } from "@player";
import { makeStyles } from "tss-react/mui";
const useStyles = makeStyles()({
root: {
width: 50,
userSelect: "none",
},
});
export function AchievementsRoot(): JSX.Element { export function AchievementsRoot(): JSX.Element {
const { classes } = useStyles();
return ( return (
<div className={classes.root} style={{ width: "100%" }}> <div style={{ width: "100%" }}>
<Typography variant="h4">Achievements</Typography> <Typography variant="h4">Achievements</Typography>
<Box mx={2}> <Box mx={2}>
<Typography> <Typography>

View File

@@ -44,12 +44,18 @@ export function resolveTeamCasualties(action: TeamActionWithCasualties, team: Op
*/ */
const losses = const losses =
minCasualties <= maxCasualties ? team.getTeamCasualtiesRoll(minCasualties, maxCasualties) : minCasualties; minCasualties <= maxCasualties ? team.getTeamCasualtiesRoll(minCasualties, maxCasualties) : minCasualties;
team.teamSize -= losses; // Calculate the new teamSize in a temporary variable and call the setter team.teamSize ONCE.
if (team.teamSize < team.sleeveSize) { // Note that it's important to call the setter only once; otherwise, the team count of each operation won't be reset
team.killRandomSupportingSleeves(team.sleeveSize - team.teamSize); // correctly.
// For example, if _teamSize is 9 (1 team member + 8 support sleeves) and "losses" is 9, calling the setter with
// (team.teamSize - losses) will set teamCount of ops/blackOps to 0 while it should be 8.
let newTeamSize = team.teamSize - losses;
if (newTeamSize < team.sleeveSize) {
team.killRandomSupportingSleeves(team.sleeveSize - newTeamSize);
// If this happens, all team members died and some sleeves took damage. In this case, teamSize = sleeveSize. // If this happens, all team members died and some sleeves took damage. In this case, teamSize = sleeveSize.
team.teamSize = team.sleeveSize; newTeamSize = team.sleeveSize;
} }
team.teamSize = newTeamSize;
team.teamLost += losses; team.teamLost += losses;
return losses; return losses;

View File

@@ -72,7 +72,31 @@ export class Bladeburner implements OperationTeam {
skillPoints = 0; skillPoints = 0;
totalSkillPoints = 0; totalSkillPoints = 0;
teamSize = 0; /**
* Do NOT directly read and write this field. You must use the getter/setter.
* We use _teamSize instead of a private field #teamSize to reduce the complexity of saving/loading code.
*/
_teamSize = 0;
get teamSize() {
return this._teamSize;
}
set teamSize(value: number) {
// Ensure teamSize is a non-negative integer.
let newSize = value;
if (!Number.isInteger(newSize) || newSize < 0) {
newSize = 0;
}
// Early return if there is no change.
if (this._teamSize === newSize) {
return;
}
this._teamSize = newSize;
// Reduce teamCount of actions if it's greater than the team size.
for (const action of [...Object.values(this.operations), ...Object.values(BlackOperations)]) {
action.teamCount = Math.min(action.teamCount, this._teamSize);
}
}
get sleeveSize() { get sleeveSize() {
return Player.sleevesSupportingBladeburner().length; return Player.sleevesSupportingBladeburner().length;
} }
@@ -1488,6 +1512,10 @@ export class Bladeburner implements OperationTeam {
bladeburner.maxStamina = 1; bladeburner.maxStamina = 1;
bladeburner.calculateMaxStamina(); bladeburner.calculateMaxStamina();
} }
// "_teamSize" was "teamSize" in pre-v3 versions.
if ("teamSize" in value.data && Number.isFinite(value.data.teamSize)) {
bladeburner.teamSize = value.data.teamSize as number;
}
return bladeburner; return bladeburner;
} }
} }

View File

@@ -15,25 +15,26 @@ interface TeamSizeModalProps {
} }
export function TeamSizeModal({ bladeburner, action, open, onClose }: TeamSizeModalProps): React.ReactElement { export function TeamSizeModal({ bladeburner, action, open, onClose }: TeamSizeModalProps): React.ReactElement {
const [teamSize, setTeamSize] = useState<number | undefined>(); const [teamSize, setTeamSize] = useState(0);
function confirmTeamSize(event: React.FormEvent): void { function confirmTeamSize(event: React.FormEvent): void {
// Prevent reloading page when submitting form // Prevent reloading page when submitting form
event.preventDefault(); event.preventDefault();
if (teamSize === undefined) return; if (!Number.isInteger(teamSize) || teamSize < 0) {
const num = Math.round(teamSize); dialogBoxCreate("Invalid value entered for number of Team Members (must be a non-negative integer)");
if (isNaN(num) || num < 0) { return;
dialogBoxCreate("Invalid value entered for number of Team Members (must be numeric and non-negative)");
} else {
action.teamCount = num;
} }
action.teamCount = teamSize;
onClose(); onClose();
} }
function onTeamSize(event: React.ChangeEvent<HTMLInputElement>): void { function onTeamSize(event: React.ChangeEvent<HTMLInputElement>): void {
const x = parseFloat(event.target.value); const newTeamSize = Number(event.target.value);
if (x > bladeburner.teamSize) setTeamSize(bladeburner.teamSize); if (newTeamSize > bladeburner.teamSize) {
else setTeamSize(x); setTeamSize(bladeburner.teamSize);
} else {
setTeamSize(newTeamSize);
}
} }
return ( return (

View File

@@ -77,7 +77,7 @@ export function NetworkDisplayWrapper(): React.ReactElement {
useEffect(() => { useEffect(() => {
const clearSubscription = DarknetEvents.subscribe(() => updateDisplay()); const clearSubscription = DarknetEvents.subscribe(() => updateDisplay());
draggableBackground.current?.addEventListener("wheel", (e) => e.preventDefault()); draggableBackground.current?.addEventListener("wheel", (e) => e.preventDefault(), { passive: false });
scrollTo(DarknetState.netViewTopScroll, DarknetState.netViewLeftScroll); scrollTo(DarknetState.netViewTopScroll, DarknetState.netViewLeftScroll);
updateDisplay(); updateDisplay();

View File

@@ -4,37 +4,37 @@ let pidCounter = 1;
/** Find and return the next available PID for a script */ /** Find and return the next available PID for a script */
export function generateNextPid(): number { export function generateNextPid(): number {
let tempCounter = pidCounter; let pidCandidate = pidCounter;
// Cap the number of search iterations at some arbitrary value to avoid // Cap the number of search iterations at some arbitrary value to avoid
// infinite loops. We'll assume that players wont have 1mil+ running scripts // infinite loops. We'll assume that players won't have a million running scripts.
let found = false; for (let attemptCounter = 0; attemptCounter < 1e6; ++attemptCounter, ++pidCandidate) {
for (let i = 0; i < 1e6; ) { // ensure the candidate PID is a safe integer
if (!workerScripts.has(tempCounter + i)) { if (pidCandidate >= Number.MAX_SAFE_INTEGER) {
found = true; pidCandidate = 1;
tempCounter = tempCounter + i;
break;
} }
// ensure the PID is not in use
if (i === Number.MAX_SAFE_INTEGER - 1) { if (workerScripts.has(pidCandidate)) {
i = 1; continue;
} else {
++i;
} }
// found a PID that's not in use
pidCounter = pidCandidate + 1;
return pidCandidate;
} }
// ran out of attempts without finding an unused PID :-(
if (found) { return -1;
pidCounter = tempCounter + 1;
if (pidCounter >= Number.MAX_SAFE_INTEGER) {
pidCounter = 1;
}
return tempCounter;
} else {
return -1;
}
} }
/**
* Reset the PID counter to 1.
*
* Note that the list of recently finished scripts has to be
* cleared (`recentScripts.splice(0)`) when resetting the PID counter.
* Otherwise scripts which re-use a PID that is still in the
* list of recent scripts do not show up there when they finish
* (if the previous script with that PID is still in the list at that point),
* because the function `AddRecentScript` de-duplicates scripts by PID.
*/
export function resetPidCounter(): void { export function resetPidCounter(): void {
pidCounter = 1; pidCounter = 1;
} }

View File

@@ -5,6 +5,11 @@ import { clampNumber } from "../../utils/helpers/clampNumber";
* stat level. Stat-agnostic (same formula for every stat) * stat level. Stat-agnostic (same formula for every stat)
*/ */
export function calculateSkill(exp: number, mult = 1): number { export function calculateSkill(exp: number, mult = 1): number {
// Mult can be 0 in BN12 when the player has a very high SF12 level. In this case, the skill level will never change
// from its initial value (1 for most stats, except intelligence).
if (mult === 0) {
return 1;
}
const value = Math.floor(mult * (32 * Math.log(exp + 534.6) - 200)); const value = Math.floor(mult * (32 * Math.log(exp + 534.6) - 200));
return clampNumber(value, 1); return clampNumber(value, 1);
} }
@@ -12,7 +17,7 @@ export function calculateSkill(exp: number, mult = 1): number {
export function calculateExp(skill: number, mult = 1): number { export function calculateExp(skill: number, mult = 1): number {
const floorSkill = Math.floor(skill); const floorSkill = Math.floor(skill);
let value = Math.exp((skill / mult + 200) / 32) - 534.6; let value = Math.exp((skill / mult + 200) / 32) - 534.6;
if (skill === floorSkill && Number.isFinite(skill)) { if (skill === floorSkill && Number.isFinite(skill) && Number.isFinite(value)) {
// Check for floating point rounding issues that would cause the inverse // Check for floating point rounding issues that would cause the inverse
// operation to return the wrong result. // operation to return the wrong result.
let calcSkill = calculateSkill(value, mult); let calcSkill = calculateSkill(value, mult);

View File

@@ -190,6 +190,8 @@ export function prestigeAugmentation(): void {
} }
} }
// clear recent scripts
recentScripts.splice(0);
resetPidCounter(); resetPidCounter();
ProgramsSeen.clear(); ProgramsSeen.clear();
InvitationsSeen.clear(); InvitationsSeen.clear();

View File

@@ -157,7 +157,7 @@ const Engine = {
messages: 150, messages: 150,
mechanicProcess: 5, // Process Bladeburner mechanicProcess: 5, // Process Bladeburner
contractGeneration: 3000, // Generate Coding Contracts contractGeneration: 3000, // Generate Coding Contracts
achievementsCounter: 60, // Check if we have new achievements achievementsCounter: 5, // Check if we have new achievements
}, },
decrementAllCounters: function (numCycles = 1) { decrementAllCounters: function (numCycles = 1) {
@@ -215,7 +215,7 @@ const Engine = {
if (Engine.Counters.achievementsCounter <= 0) { if (Engine.Counters.achievementsCounter <= 0) {
calculateAchievements(); calculateAchievements();
Engine.Counters.achievementsCounter = 300; Engine.Counters.achievementsCounter = 5;
} }
// This **MUST** remain the last block in the function! // This **MUST** remain the last block in the function!

View File

@@ -1,5 +1,4 @@
import { Player, setPlayer } from "@player"; import { Player, setPlayer } from "@player";
import { FormatsNeedToChange } from "../../../src/ui/formatNumber";
import type { ActionIdFor } from "../../../src/Bladeburner/Types"; import type { ActionIdFor } from "../../../src/Bladeburner/Types";
import type { Bladeburner } from "../../../src/Bladeburner/Bladeburner"; import type { Bladeburner } from "../../../src/Bladeburner/Bladeburner";
import { BlackOperation } from "../../../src/Bladeburner/Actions/BlackOperation"; import { BlackOperation } from "../../../src/Bladeburner/Actions/BlackOperation";
@@ -9,6 +8,10 @@ import { SleeveSupportWork } from "../../../src/PersonObjects/Sleeve/Work/Sleeve
import { BladeburnerBlackOpName, BladeburnerContractName, BladeburnerOperationName } from "@enums"; import { BladeburnerBlackOpName, BladeburnerContractName, BladeburnerOperationName } from "@enums";
import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject"; import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
import { recalculateNumberOfOwnedSleeves } from "../../../src/PersonObjects/Sleeve/SleeveCovenantPurchases"; import { recalculateNumberOfOwnedSleeves } from "../../../src/PersonObjects/Sleeve/SleeveCovenantPurchases";
import { initGameEnvironment } from "../Utilities";
import { BlackOperations } from "../../../src/Bladeburner/data/BlackOperations";
initGameEnvironment();
/** /**
* You may want to use hook to help with debugging * You may want to use hook to help with debugging
@@ -27,11 +30,6 @@ describe("Bladeburner Team", () => {
let inst: Bladeburner; let inst: Bladeburner;
let action: BlackOperation | Operation; let action: BlackOperation | Operation;
beforeAll(() => {
/* Initialise Formatters. Dependency of Bladeburner */
FormatsNeedToChange.emit();
});
beforeEach(() => { beforeEach(() => {
setPlayer(new PlayerObject()); setPlayer(new PlayerObject());
Player.init(); Player.init();
@@ -133,6 +131,51 @@ describe("Bladeburner Team", () => {
}); });
}); });
describe("Check teamSize and teamCount", () => {
test("Failing actions", () => {
teamSize(10);
startAction(OP);
forceMaxCasualties();
for (const action of [...Object.values(inst.operations), ...Object.values(BlackOperations)]) {
action.teamCount = 10;
expect(action.teamCount).toStrictEqual(10);
}
actionFails();
expect(inst.teamSize).toStrictEqual(0);
for (const action of [...Object.values(inst.operations), ...Object.values(BlackOperations)]) {
expect(action.teamCount).toStrictEqual(0);
}
});
test("Sleeves", () => {
teamSize(1);
supportingSleeves(8);
expect(inst.teamSize).toStrictEqual(9);
startAction(OP);
forceMaxCasualties();
for (const action of [...Object.values(inst.operations), ...Object.values(BlackOperations)]) {
action.teamCount = 9;
expect(action.teamCount).toStrictEqual(9);
}
actionFails();
// The teamCount of all operations/black operations should be 8, not 0.
assertSleevesHaveBeenShocked();
expect(inst.teamSize).toStrictEqual(8);
for (const action of [...Object.values(inst.operations), ...Object.values(BlackOperations)]) {
expect(action.teamCount).toStrictEqual(8);
}
Player.sleeves[0].stopWork();
expect(inst.teamSize).toStrictEqual(7);
for (const action of [...Object.values(inst.operations), ...Object.values(BlackOperations)]) {
expect(action.teamCount).toStrictEqual(7);
}
Player.sleeves[0].startWork(new SleeveSupportWork());
expect(inst.teamSize).toStrictEqual(8);
for (const action of [...Object.values(inst.operations), ...Object.values(BlackOperations)]) {
expect(action.teamCount).toStrictEqual(7);
}
});
});
function teamSize(n: number) { function teamSize(n: number) {
inst.teamSize = n; inst.teamSize = n;
} }

View File

@@ -75,6 +75,7 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
"bladeburner": { "bladeburner": {
"ctor": "Bladeburner", "ctor": "Bladeburner",
"data": { "data": {
"_teamSize": 0,
"action": null, "action": null,
"actionTimeCurrent": 0, "actionTimeCurrent": 0,
"actionTimeOverflow": 0, "actionTimeOverflow": 0,
@@ -281,7 +282,6 @@ exports[`Check Save File Continuity PlayerSave continuity 1`] = `
"staminaBonus": 0, "staminaBonus": 0,
"storedCycles": 0, "storedCycles": 0,
"teamLost": 0, "teamLost": 0,
"teamSize": 0,
"totalSkillPoints": 666, "totalSkillPoints": 666,
}, },
}, },