merge dev

This commit is contained in:
Olivier Gagnon 2024-06-12 09:32:44 -04:00
commit 1294f8f045
No known key found for this signature in database
GPG Key ID: 0018772EA86FA03F
134 changed files with 5123 additions and 4462 deletions

@ -9,7 +9,7 @@ Get the reputation gain of an action.
**Signature:** **Signature:**
```typescript ```typescript
getActionRepGain(type: string, name: string, level: number): number; getActionRepGain(type: string, name: string, level?: number): number;
``` ```
## Parameters ## Parameters
@ -18,7 +18,7 @@ getActionRepGain(type: string, name: string, level: number): number;
| --- | --- | --- | | --- | --- | --- |
| type | string | Type of action. | | type | string | Type of action. |
| name | string | Name of action. Must be an exact match. | | name | string | Name of action. Must be an exact match. |
| level | number | Optional number. Action level at which to calculate the gain. Will be the action's current level if not given. | | level | number | _(Optional)_ Optional number. Action level at which to calculate the gain. Will be the action's current level if not given. |
**Returns:** **Returns:**

@ -9,15 +9,15 @@ Get team size.
**Signature:** **Signature:**
```typescript ```typescript
getTeamSize(type: string, name: string): number; getTeamSize(type?: string, name?: string): number;
``` ```
## Parameters ## Parameters
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| type | string | Type of action. | | type | string | _(Optional)_ Type of action. |
| name | string | Name of action. Must be an exact match. | | name | string | _(Optional)_ Name of action. Must be an exact match. |
**Returns:** **Returns:**
@ -29,7 +29,7 @@ Number of Bladeburner team members that were assigned to the specified action.
RAM cost: 4 GB RAM cost: 4 GB
Returns the number of Bladeburner team members you have assigned to the specified action. Returns the number of available Bladeburner team members. You can also pass the type and name of an action to get the number of Bladeburner team members you have assigned to the specified action.
Setting a team is only applicable for Operations and BlackOps. This function will return 0 for other action types. Setting a team is only applicable for Operations and BlackOps. This function will return 0 for other action types.

@ -14,9 +14,11 @@ getGameState(): {
whiteScore: number; whiteScore: number;
blackScore: number; blackScore: number;
previousMove: [number, number] | null; previousMove: [number, number] | null;
komi: number;
bonusCycles: number;
}; };
``` ```
**Returns:** **Returns:**
{ currentPlayer: "White" \| "Black" \| "None"; whiteScore: number; blackScore: number; previousMove: \[number, number\] \| null; } { currentPlayer: "White" \| "Black" \| "None"; whiteScore: number; blackScore: number; previousMove: \[number, number\] \| null; komi: number; bonusCycles: number; }

@ -0,0 +1,21 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [Go](./bitburner.go.md) &gt; [getMoveHistory](./bitburner.go.getmovehistory.md)
## Go.getMoveHistory() method
Returns all the prior moves in the current game, as an array of simple board states.
For example, a single 5x5 prior move board might look like this:
\[<br/> "XX.O.",<br/> "X..OO",<br/> ".XO..",<br/> "XXO.\#",<br/> ".XO.\#",<br/> \]
**Signature:**
```typescript
getMoveHistory(): string[][];
```
**Returns:**
string\[\]\[\]

@ -26,9 +26,10 @@ export interface Go
| [getBoardState()](./bitburner.go.getboardstate.md) | <p>Retrieves a simplified version of the board state. "X" represents black pieces, "O" white, and "." empty points. "\#" are dead nodes that are not part of the subnet. (They are not territory nor open nodes.)</p><p>For example, a 5x5 board might look like this:</p><p>\[<br/> "XX.O.",<br/> "X..OO",<br/> ".XO..",<br/> "XXO.\#",<br/> ".XO.\#",<br/> \]</p><p>Each string represents a vertical column on the board, and each character in the string represents a point.</p><p>Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of index \[1\]\[0\].</p><p>Note that the \[0\]\[0\] point is shown on the bottom-left on the visual board (as is traditional), and each string represents a vertical column on the board. In other words, the printed example above can be understood to be rotated 90 degrees clockwise compared to the board UI as shown in the IPvGO subnet tab.</p> | | [getBoardState()](./bitburner.go.getboardstate.md) | <p>Retrieves a simplified version of the board state. "X" represents black pieces, "O" white, and "." empty points. "\#" are dead nodes that are not part of the subnet. (They are not territory nor open nodes.)</p><p>For example, a 5x5 board might look like this:</p><p>\[<br/> "XX.O.",<br/> "X..OO",<br/> ".XO..",<br/> "XXO.\#",<br/> ".XO.\#",<br/> \]</p><p>Each string represents a vertical column on the board, and each character in the string represents a point.</p><p>Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of index \[1\]\[0\].</p><p>Note that the \[0\]\[0\] point is shown on the bottom-left on the visual board (as is traditional), and each string represents a vertical column on the board. In other words, the printed example above can be understood to be rotated 90 degrees clockwise compared to the board UI as shown in the IPvGO subnet tab.</p> |
| [getCurrentPlayer()](./bitburner.go.getcurrentplayer.md) | Returns the color of the current player, or 'None' if the game is over. | | [getCurrentPlayer()](./bitburner.go.getcurrentplayer.md) | Returns the color of the current player, or 'None' if the game is over. |
| [getGameState()](./bitburner.go.getgamestate.md) | Gets the status of the current game. Shows the current player, current score, and the previous move coordinates. Previous move coordinates will be \[-1, -1\] for a pass, or if there are no prior moves. | | [getGameState()](./bitburner.go.getgamestate.md) | Gets the status of the current game. Shows the current player, current score, and the previous move coordinates. Previous move coordinates will be \[-1, -1\] for a pass, or if there are no prior moves. |
| [getMoveHistory()](./bitburner.go.getmovehistory.md) | <p>Returns all the prior moves in the current game, as an array of simple board states.</p><p>For example, a single 5x5 prior move board might look like this:</p><p>\[<br/> "XX.O.",<br/> "X..OO",<br/> ".XO..",<br/> "XXO.\#",<br/> ".XO.\#",<br/> \]</p> |
| [getOpponent()](./bitburner.go.getopponent.md) | Returns the name of the opponent faction in the current subnet. | | [getOpponent()](./bitburner.go.getopponent.md) | Returns the name of the opponent faction in the current subnet. |
| [makeMove(x, y)](./bitburner.go.makemove.md) | Make a move on the IPvGO subnet gameboard, and await the opponent's response. x:0 y:0 represents the bottom-left corner of the board in the UI. | | [makeMove(x, y)](./bitburner.go.makemove.md) | Make a move on the IPvGO subnet gameboard, and await the opponent's response. x:0 y:0 represents the bottom-left corner of the board in the UI. |
| [opponentNextTurn(logOpponentMove)](./bitburner.go.opponentnextturn.md) | Returns a promise that resolves with the success or failure state of your last move, and the AI's response, if applicable. x:0 y:0 represents the bottom-left corner of the board in the UI. | | [opponentNextTurn(logOpponentMove)](./bitburner.go.opponentnextturn.md) | Returns a promise that resolves with the success or failure state of your last move, and the AI's response, if applicable. x:0 y:0 represents the bottom-left corner of the board in the UI. |
| [passTurn()](./bitburner.go.passturn.md) | <p>Pass the player's turn rather than making a move, and await the opponent's response. This ends the game if the opponent passed on the previous turn, or if the opponent passes on their following turn.</p><p>This can also be used if you pick up the game in a state where the opponent needs to play next. For example: if BitBurner was closed while waiting for the opponent to make a move, you may need to call passTurn() to get them to play their move on game start.</p> | | [passTurn()](./bitburner.go.passturn.md) | <p>Pass the player's turn rather than making a move, and await the opponent's response. This ends the game if the opponent passed on the previous turn, or if the opponent passes on their following turn.</p><p>This can also be used if you pick up the game in a state where the opponent needs to play next. For example: if BitBurner was closed while waiting for the opponent to make a move, you may need to call passTurn() to get them to play their move on game start.</p> |
| [resetBoardState(opponent, boardSize)](./bitburner.go.resetboardstate.md) | <p>Gets new IPvGO subnet with the specified size owned by the listed faction, ready for the player to make a move. This will reset your win streak if the current game is not complete and you have already made moves.</p><p>Note that some factions will have a few routers on the subnet at this state.</p><p>opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????",</p> | | [resetBoardState(opponent, boardSize)](./bitburner.go.resetboardstate.md) | <p>Gets new IPvGO subnet with the specified size owned by the listed faction, ready for the player to make a move. This will reset your win streak if the current game is not complete and you have already made moves.</p><p>Note that some factions will have a few routers on the subnet at this state.</p><p>opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????" or "No AI",</p> |

@ -8,7 +8,7 @@ Gets new IPvGO subnet with the specified size owned by the listed faction, ready
Note that some factions will have a few routers on the subnet at this state. Note that some factions will have a few routers on the subnet at this state.
opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????", opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????" or "No AI",
**Signature:** **Signature:**

27
package-lock.json generated

@ -43,7 +43,8 @@
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"sprintf-js": "^1.1.3" "sprintf-js": "^1.1.3",
"tss-react": "^4.9.10"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.23.0", "@babel/core": "^7.23.0",
@ -16896,6 +16897,30 @@
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true "dev": true
}, },
"node_modules/tss-react": {
"version": "4.9.10",
"resolved": "https://registry.npmjs.org/tss-react/-/tss-react-4.9.10.tgz",
"integrity": "sha512-uQj+r8mOKy0tv+/GAIzViVG81w/WeTCOF7tjsDyNjlicnWbxtssYwTvVjWT4lhWh5FSznDRy6RFp0BDdoLbxyg==",
"dependencies": {
"@emotion/cache": "*",
"@emotion/serialize": "*",
"@emotion/utils": "*"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
"@emotion/server": "^11.4.0",
"@mui/material": "^5.0.0",
"react": "^16.8.0 || ^17.0.2 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/server": {
"optional": true
},
"@mui/material": {
"optional": true
}
}
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

@ -43,7 +43,8 @@
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"sprintf-js": "^1.1.3" "sprintf-js": "^1.1.3",
"tss-react": "^4.9.10"
}, },
"description": "A cyberpunk-themed incremental game", "description": "A cyberpunk-themed incremental game",
"devDependencies": { "devDependencies": {
@ -86,13 +87,13 @@
"prettier": "^2.8.8", "prettier": "^2.8.8",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"react-refresh": "^0.14.0", "react-refresh": "^0.14.0",
"rehype-mathjax": "^4.0.3",
"remark-math": "^5.1.1",
"style-loader": "^3.3.3", "style-loader": "^3.3.3",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"webpack": "^5.88.2", "webpack": "^5.88.2",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.2", "webpack-dev-server": "^4.15.2"
"remark-math": "^5.1.1",
"rehype-mathjax": "^4.0.3"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=14"
@ -118,7 +119,7 @@
"watch": "webpack --watch --mode production", "watch": "webpack --watch --mode production",
"watch:dev": "webpack --watch --mode development", "watch:dev": "webpack --watch --mode development",
"electron": "bash ./tools/package-electron.sh", "electron": "bash ./tools/package-electron.sh",
"electron:packager-all": "electron-packager .package bitburner --platform all --arch x64,armv7l,arm64,mips64el --out .build --overwrite --icon .package/icon.png", "electron:packager-all": "electron-packager .package bitburner --platform all --arch x64,armv7l,arm64,mips64el --out .build --overwrite --icon .package/icon.png --app-copyright \"Copyright (C) 2024 Bitburner\"",
"preversion": "npm install && npm run test", "preversion": "npm install && npm run test",
"version": "sh ./tools/build-release.sh && git add --all", "version": "sh ./tools/build-release.sh && git add --all",
"postversion": "git push -u origin dev && git push --tags", "postversion": "git push -u origin dev && git push --tags",

@ -19,7 +19,6 @@ import { HacknetNode } from "../Hacknet/HacknetNode";
import { HacknetServer } from "../Hacknet/HacknetServer"; import { HacknetServer } from "../Hacknet/HacknetServer";
import { Player } from "@player"; import { Player } from "@player";
import { GetAllServers, GetServer } from "../Server/AllServers"; import { GetAllServers, GetServer } from "../Server/AllServers";
import { SpecialServers } from "../Server/data/SpecialServers";
import { Server } from "../Server/Server"; import { Server } from "../Server/Server";
import { Router } from "../ui/GameRoot"; import { Router } from "../ui/GameRoot";
import { Page } from "../ui/Router"; import { Page } from "../ui/Router";
@ -30,7 +29,7 @@ import { workerScripts } from "../Netscript/WorkerScripts";
import { getRecordValues } from "../Types/Record"; import { getRecordValues } from "../Types/Record";
import { ServerConstants } from "../Server/data/Constants"; import { ServerConstants } from "../Server/data/Constants";
import { blackOpsArray } from "../Bladeburner/data/BlackOperations"; import { isBitNodeFinished } from "../BitNode/BitNodeUtils";
// Unable to correctly cast the JSON data into AchievementDataJson type otherwise... // Unable to correctly cast the JSON data into AchievementDataJson type otherwise...
const achievementData = (<AchievementDataJson>(<unknown>data)).achievements; const achievementData = (<AchievementDataJson>(<unknown>data)).achievements;
@ -61,15 +60,8 @@ export interface AchievementData {
Description: string; Description: string;
} }
function bitNodeFinishedState(): boolean { function canAccessBitNodeFeature(bitNode: number): boolean {
const wd = GetServer(SpecialServers.WorldDaemon); return Player.bitNodeN === bitNode || Player.sourceFileLvl(bitNode) > 0;
if (!(wd instanceof Server)) return false;
if (wd.backdoorInstalled) return true;
return Player.bladeburner !== null && Player.bladeburner.numBlackOpsComplete >= blackOpsArray.length;
}
function hasAccessToSF(bn: number): boolean {
return Player.bitNodeN === bn || Player.sourceFileLvl(bn) > 0;
} }
function knowsAboutBitverse(): boolean { function knowsAboutBitverse(): boolean {
@ -338,25 +330,25 @@ export const achievements: Record<string, Achievement> = {
GANG: { GANG: {
...achievementData.GANG, ...achievementData.GANG,
Icon: "GANG", Icon: "GANG",
Visible: () => hasAccessToSF(2), Visible: () => canAccessBitNodeFeature(2),
Condition: () => Player.gang !== null, Condition: () => Player.gang !== null,
}, },
FULL_GANG: { FULL_GANG: {
...achievementData.FULL_GANG, ...achievementData.FULL_GANG,
Icon: "GANGMAX", Icon: "GANGMAX",
Visible: () => hasAccessToSF(2), Visible: () => canAccessBitNodeFeature(2),
Condition: () => Player.gang !== null && Player.gang.members.length === GangConstants.MaximumGangMembers, Condition: () => Player.gang !== null && Player.gang.members.length === GangConstants.MaximumGangMembers,
}, },
GANG_TERRITORY: { GANG_TERRITORY: {
...achievementData.GANG_TERRITORY, ...achievementData.GANG_TERRITORY,
Icon: "GANG100%", Icon: "GANG100%",
Visible: () => hasAccessToSF(2), Visible: () => canAccessBitNodeFeature(2),
Condition: () => Player.gang !== null && AllGangs[Player.gang.facName].territory >= 0.999, Condition: () => Player.gang !== null && AllGangs[Player.gang.facName].territory >= 0.999,
}, },
GANG_MEMBER_POWER: { GANG_MEMBER_POWER: {
...achievementData.GANG_MEMBER_POWER, ...achievementData.GANG_MEMBER_POWER,
Icon: "GANG10000", Icon: "GANG10000",
Visible: () => hasAccessToSF(2), Visible: () => canAccessBitNodeFeature(2),
Condition: () => Condition: () =>
Player.gang !== null && Player.gang !== null &&
Player.gang.members.some( Player.gang.members.some(
@ -367,19 +359,19 @@ export const achievements: Record<string, Achievement> = {
CORPORATION: { CORPORATION: {
...achievementData.CORPORATION, ...achievementData.CORPORATION,
Icon: "CORP", Icon: "CORP",
Visible: () => hasAccessToSF(3), Visible: () => canAccessBitNodeFeature(3),
Condition: () => Player.corporation !== null, Condition: () => Player.corporation !== null,
}, },
CORPORATION_BRIBE: { CORPORATION_BRIBE: {
...achievementData.CORPORATION_BRIBE, ...achievementData.CORPORATION_BRIBE,
Icon: "CORPLOBBY", Icon: "CORPLOBBY",
Visible: () => hasAccessToSF(3), Visible: () => canAccessBitNodeFeature(3),
Condition: () => !!Player.corporation && Player.corporation.unlocks.has(CorpUnlockName.GovernmentPartnership), Condition: () => !!Player.corporation && Player.corporation.unlocks.has(CorpUnlockName.GovernmentPartnership),
}, },
CORPORATION_PROD_1000: { CORPORATION_PROD_1000: {
...achievementData.CORPORATION_PROD_1000, ...achievementData.CORPORATION_PROD_1000,
Icon: "CORP1000", Icon: "CORP1000",
Visible: () => hasAccessToSF(3), Visible: () => canAccessBitNodeFeature(3),
Condition: () => { Condition: () => {
if (!Player.corporation) return false; if (!Player.corporation) return false;
for (const division of Player.corporation.divisions.values()) { for (const division of Player.corporation.divisions.values()) {
@ -391,7 +383,7 @@ export const achievements: Record<string, Achievement> = {
CORPORATION_EMPLOYEE_3000: { CORPORATION_EMPLOYEE_3000: {
...achievementData.CORPORATION_EMPLOYEE_3000, ...achievementData.CORPORATION_EMPLOYEE_3000,
Icon: "CORPCITY", Icon: "CORPCITY",
Visible: () => hasAccessToSF(3), Visible: () => canAccessBitNodeFeature(3),
Condition: (): boolean => { Condition: (): boolean => {
if (!Player.corporation) return false; if (!Player.corporation) return false;
for (const division of Player.corporation.divisions.values()) { for (const division of Player.corporation.divisions.values()) {
@ -406,7 +398,7 @@ export const achievements: Record<string, Achievement> = {
Icon: "CORPRE", Icon: "CORPRE",
Name: "Own the land", Name: "Own the land",
Description: "Expand to the Real Estate division.", Description: "Expand to the Real Estate division.",
Visible: () => hasAccessToSF(3), Visible: () => canAccessBitNodeFeature(3),
Condition: () => { Condition: () => {
if (!Player.corporation) return false; if (!Player.corporation) return false;
for (const division of Player.corporation.divisions.values()) { for (const division of Player.corporation.divisions.values()) {
@ -418,26 +410,26 @@ export const achievements: Record<string, Achievement> = {
INTELLIGENCE_255: { INTELLIGENCE_255: {
...achievementData.INTELLIGENCE_255, ...achievementData.INTELLIGENCE_255,
Icon: "INT255", Icon: "INT255",
Visible: () => hasAccessToSF(5), Visible: () => canAccessBitNodeFeature(5),
Condition: () => Player.skills.intelligence >= 255, Condition: () => Player.skills.intelligence >= 255,
}, },
BLADEBURNER_DIVISION: { BLADEBURNER_DIVISION: {
...achievementData.BLADEBURNER_DIVISION, ...achievementData.BLADEBURNER_DIVISION,
Icon: "BLADE", Icon: "BLADE",
Visible: () => hasAccessToSF(6), Visible: () => canAccessBitNodeFeature(6),
Condition: () => Player.bladeburner !== null, Condition: () => Player.bladeburner !== null,
}, },
BLADEBURNER_OVERCLOCK: { BLADEBURNER_OVERCLOCK: {
...achievementData.BLADEBURNER_OVERCLOCK, ...achievementData.BLADEBURNER_OVERCLOCK,
Icon: "BLADEOVERCLOCK", Icon: "BLADEOVERCLOCK",
Visible: () => hasAccessToSF(6), Visible: () => canAccessBitNodeFeature(6),
Condition: () => Condition: () =>
Player.bladeburner?.getSkillLevel(BladeSkillName.overclock) === Skills[BladeSkillName.overclock].maxLvl, Player.bladeburner?.getSkillLevel(BladeSkillName.overclock) === Skills[BladeSkillName.overclock].maxLvl,
}, },
BLADEBURNER_UNSPENT_100000: { BLADEBURNER_UNSPENT_100000: {
...achievementData.BLADEBURNER_UNSPENT_100000, ...achievementData.BLADEBURNER_UNSPENT_100000,
Icon: "BLADE100K", Icon: "BLADE100K",
Visible: () => hasAccessToSF(6), Visible: () => canAccessBitNodeFeature(6),
Condition: () => Player.bladeburner !== null && Player.bladeburner.skillPoints >= 100000, Condition: () => Player.bladeburner !== null && Player.bladeburner.skillPoints >= 100000,
}, },
"4S": { "4S": {
@ -448,21 +440,21 @@ export const achievements: Record<string, Achievement> = {
FIRST_HACKNET_SERVER: { FIRST_HACKNET_SERVER: {
...achievementData.FIRST_HACKNET_SERVER, ...achievementData.FIRST_HACKNET_SERVER,
Icon: "HASHNET", Icon: "HASHNET",
Visible: () => hasAccessToSF(9), Visible: () => canAccessBitNodeFeature(9),
Condition: () => hasHacknetServers() && Player.hacknetNodes.length > 0, Condition: () => hasHacknetServers() && Player.hacknetNodes.length > 0,
AdditionalUnlock: [achievementData.FIRST_HACKNET_NODE.ID], AdditionalUnlock: [achievementData.FIRST_HACKNET_NODE.ID],
}, },
ALL_HACKNET_SERVER: { ALL_HACKNET_SERVER: {
...achievementData.ALL_HACKNET_SERVER, ...achievementData.ALL_HACKNET_SERVER,
Icon: "HASHNETALL", Icon: "HASHNETALL",
Visible: () => hasAccessToSF(9), Visible: () => canAccessBitNodeFeature(9),
Condition: () => hasHacknetServers() && Player.hacknetNodes.length === HacknetServerConstants.MaxServers, Condition: () => hasHacknetServers() && Player.hacknetNodes.length === HacknetServerConstants.MaxServers,
AdditionalUnlock: [achievementData["30_HACKNET_NODE"].ID], AdditionalUnlock: [achievementData["30_HACKNET_NODE"].ID],
}, },
MAX_HACKNET_SERVER: { MAX_HACKNET_SERVER: {
...achievementData.MAX_HACKNET_SERVER, ...achievementData.MAX_HACKNET_SERVER,
Icon: "HASHNETALL", Icon: "HASHNETALL",
Visible: () => hasAccessToSF(9), Visible: () => canAccessBitNodeFeature(9),
Condition: (): boolean => { Condition: (): boolean => {
if (!hasHacknetServers()) return false; if (!hasHacknetServers()) return false;
for (const h of Player.hacknetNodes) { for (const h of Player.hacknetNodes) {
@ -484,14 +476,14 @@ export const achievements: Record<string, Achievement> = {
HACKNET_SERVER_1B: { HACKNET_SERVER_1B: {
...achievementData.HACKNET_SERVER_1B, ...achievementData.HACKNET_SERVER_1B,
Icon: "HASHNETMONEY", Icon: "HASHNETMONEY",
Visible: () => hasAccessToSF(9), Visible: () => canAccessBitNodeFeature(9),
Condition: () => hasHacknetServers() && Player.moneySourceB.hacknet >= 1e9, Condition: () => hasHacknetServers() && Player.moneySourceB.hacknet >= 1e9,
AdditionalUnlock: [achievementData.HACKNET_NODE_10M.ID], AdditionalUnlock: [achievementData.HACKNET_NODE_10M.ID],
}, },
MAX_CACHE: { MAX_CACHE: {
...achievementData.MAX_CACHE, ...achievementData.MAX_CACHE,
Icon: "HASHNETCAP", Icon: "HASHNETCAP",
Visible: () => hasAccessToSF(9), Visible: () => canAccessBitNodeFeature(9),
Condition: () => Condition: () =>
hasHacknetServers() && hasHacknetServers() &&
Player.hashManager.hashes === Player.hashManager.capacity && Player.hashManager.hashes === Player.hashManager.capacity &&
@ -500,7 +492,7 @@ export const achievements: Record<string, Achievement> = {
SLEEVE_8: { SLEEVE_8: {
...achievementData.SLEEVE_8, ...achievementData.SLEEVE_8,
Icon: "SLEEVE8", Icon: "SLEEVE8",
Visible: () => hasAccessToSF(10), Visible: () => canAccessBitNodeFeature(10),
Condition: () => Player.sleeves.length === 8 && Player.sourceFileLvl(10) === 3, Condition: () => Player.sleeves.length === 8 && Player.sourceFileLvl(10) === 3,
}, },
INDECISIVE: { INDECISIVE: {
@ -523,7 +515,7 @@ export const achievements: Record<string, Achievement> = {
...achievementData.FAST_BN, ...achievementData.FAST_BN,
Icon: "2DAYS", Icon: "2DAYS",
Visible: knowsAboutBitverse, Visible: knowsAboutBitverse,
Condition: () => bitNodeFinishedState() && Player.playtimeSinceLastBitnode < 1000 * 60 * 60 * 24 * 2, Condition: () => isBitNodeFinished() && Player.playtimeSinceLastBitnode < 1000 * 60 * 60 * 24 * 2,
}, },
CHALLENGE_BN1: { CHALLENGE_BN1: {
...achievementData.CHALLENGE_BN1, ...achievementData.CHALLENGE_BN1,
@ -531,57 +523,57 @@ export const achievements: Record<string, Achievement> = {
Visible: knowsAboutBitverse, Visible: knowsAboutBitverse,
Condition: () => Condition: () =>
Player.bitNodeN === 1 && Player.bitNodeN === 1 &&
bitNodeFinishedState() && isBitNodeFinished() &&
Player.getHomeComputer().maxRam <= 128 && Player.getHomeComputer().maxRam <= 128 &&
Player.getHomeComputer().cpuCores === 1, Player.getHomeComputer().cpuCores === 1,
}, },
CHALLENGE_BN2: { CHALLENGE_BN2: {
...achievementData.CHALLENGE_BN2, ...achievementData.CHALLENGE_BN2,
Icon: "BN2+", Icon: "BN2+",
Visible: () => hasAccessToSF(2), Visible: () => canAccessBitNodeFeature(2),
Condition: () => Player.bitNodeN === 2 && bitNodeFinishedState() && Player.gang === null, Condition: () => Player.bitNodeN === 2 && isBitNodeFinished() && Player.gang === null,
}, },
CHALLENGE_BN3: { CHALLENGE_BN3: {
...achievementData.CHALLENGE_BN3, ...achievementData.CHALLENGE_BN3,
Icon: "BN3+", Icon: "BN3+",
Visible: () => hasAccessToSF(3), Visible: () => canAccessBitNodeFeature(3),
Condition: () => Player.bitNodeN === 3 && bitNodeFinishedState() && Player.corporation === null, Condition: () => Player.bitNodeN === 3 && isBitNodeFinished() && Player.corporation === null,
}, },
CHALLENGE_BN6: { CHALLENGE_BN6: {
...achievementData.CHALLENGE_BN6, ...achievementData.CHALLENGE_BN6,
Icon: "BN6+", Icon: "BN6+",
Visible: () => hasAccessToSF(6), Visible: () => canAccessBitNodeFeature(6),
Condition: () => Player.bitNodeN === 6 && bitNodeFinishedState() && Player.bladeburner === null, Condition: () => Player.bitNodeN === 6 && isBitNodeFinished() && Player.bladeburner === null,
}, },
CHALLENGE_BN7: { CHALLENGE_BN7: {
...achievementData.CHALLENGE_BN7, ...achievementData.CHALLENGE_BN7,
Icon: "BN7+", Icon: "BN7+",
Visible: () => hasAccessToSF(7), Visible: () => canAccessBitNodeFeature(7),
Condition: () => Player.bitNodeN === 7 && bitNodeFinishedState() && Player.bladeburner === null, Condition: () => Player.bitNodeN === 7 && isBitNodeFinished() && Player.bladeburner === null,
}, },
CHALLENGE_BN8: { CHALLENGE_BN8: {
...achievementData.CHALLENGE_BN8, ...achievementData.CHALLENGE_BN8,
Icon: "BN8+", Icon: "BN8+",
Visible: () => hasAccessToSF(8), Visible: () => canAccessBitNodeFeature(8),
Condition: () => Player.bitNodeN === 8 && bitNodeFinishedState() && !Player.has4SData && !Player.has4SDataTixApi, Condition: () => Player.bitNodeN === 8 && isBitNodeFinished() && !Player.has4SData && !Player.has4SDataTixApi,
}, },
CHALLENGE_BN9: { CHALLENGE_BN9: {
...achievementData.CHALLENGE_BN9, ...achievementData.CHALLENGE_BN9,
Icon: "BN9+", Icon: "BN9+",
Visible: () => hasAccessToSF(9), Visible: () => canAccessBitNodeFeature(9),
Condition: () => Condition: () =>
Player.bitNodeN === 9 && Player.bitNodeN === 9 &&
bitNodeFinishedState() && isBitNodeFinished() &&
Player.moneySourceB.hacknet === 0 && Player.moneySourceB.hacknet === 0 &&
Player.moneySourceB.hacknet_expenses === 0, Player.moneySourceB.hacknet_expenses === 0,
}, },
CHALLENGE_BN10: { CHALLENGE_BN10: {
...achievementData.CHALLENGE_BN10, ...achievementData.CHALLENGE_BN10,
Icon: "BN10+", Icon: "BN10+",
Visible: () => hasAccessToSF(10), Visible: () => canAccessBitNodeFeature(10),
Condition: () => Condition: () =>
Player.bitNodeN === 10 && Player.bitNodeN === 10 &&
bitNodeFinishedState() && isBitNodeFinished() &&
!Player.sleeves.some( !Player.sleeves.some(
(s) => (s) =>
s.augmentations.length > 0 || s.augmentations.length > 0 ||
@ -596,7 +588,7 @@ export const achievements: Record<string, Achievement> = {
CHALLENGE_BN12: { CHALLENGE_BN12: {
...achievementData.CHALLENGE_BN12, ...achievementData.CHALLENGE_BN12,
Icon: "BN12+", Icon: "BN12+",
Visible: () => hasAccessToSF(12), Visible: () => canAccessBitNodeFeature(12),
Condition: () => Player.sourceFileLvl(12) >= 50, Condition: () => Player.sourceFileLvl(12) >= 50,
}, },
BYPASS: { BYPASS: {
@ -657,10 +649,10 @@ export const achievements: Record<string, Achievement> = {
CHALLENGE_BN13: { CHALLENGE_BN13: {
...achievementData.CHALLENGE_BN13, ...achievementData.CHALLENGE_BN13,
Icon: "BN13+", Icon: "BN13+",
Visible: () => hasAccessToSF(13), Visible: () => canAccessBitNodeFeature(13),
Condition: () => Condition: () =>
Player.bitNodeN === 13 && Player.bitNodeN === 13 &&
bitNodeFinishedState() && isBitNodeFinished() &&
!Player.augmentations.some((a) => a.name === AugmentationName.StaneksGift1), !Player.augmentations.some((a) => a.name === AugmentationName.StaneksGift1),
}, },
DEVMENU: { DEVMENU: {

@ -1,26 +1,22 @@
import React from "react"; import React from "react";
import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import { AchievementList } from "./AchievementList"; import { AchievementList } from "./AchievementList";
import { achievements } from "./Achievements"; import { achievements } from "./Achievements";
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
import { Player } from "@player"; import { Player } from "@player";
import { makeStyles } from "tss-react/mui";
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles()((theme: Theme) => ({
createStyles({ root: {
root: { width: 50,
width: 50, padding: theme.spacing(2),
padding: theme.spacing(2), userSelect: "none",
userSelect: "none", },
}, }));
}),
);
export function AchievementsRoot(): JSX.Element { export function AchievementsRoot(): JSX.Element {
const classes = useStyles(); const { classes } = useStyles();
return ( return (
<div className={classes.root} style={{ width: "90%" }}> <div className={classes.root} style={{ width: "90%" }}>
<Typography variant="h4">Achievements</Typography> <Typography variant="h4">Achievements</Typography>

@ -1,5 +1,5 @@
import { PartialRecord, getRecordEntries } from "../Types/Record"; import { PartialRecord, getRecordEntries } from "../Types/Record";
import { clampNumber } from "../utils/helpers/clampNumber";
/** /**
* Bitnode multipliers influence the difficulty of different aspects of the game. * Bitnode multipliers influence the difficulty of different aspects of the game.
* Each Bitnode has a different theme/strategy to achieving the end goal, so these multipliers will can help drive the * Each Bitnode has a different theme/strategy to achieving the end goal, so these multipliers will can help drive the
@ -176,7 +176,7 @@ export class BitNodeMultipliers {
CorporationDivisions = 1; CorporationDivisions = 1;
constructor(a: PartialRecord<keyof BitNodeMultipliers, number> = {}) { constructor(a: PartialRecord<keyof BitNodeMultipliers, number> = {}) {
for (const [key, value] of getRecordEntries(a)) this[key] = value; for (const [key, value] of getRecordEntries(a)) this[key] = clampNumber(value);
} }
} }

@ -0,0 +1,11 @@
import { GetServer } from "../Server/AllServers";
import { Server } from "../Server/Server";
import { SpecialServers } from "../Server/data/SpecialServers";
export function isBitNodeFinished(): boolean {
const wd = GetServer(SpecialServers.WorldDaemon);
if (!(wd instanceof Server)) {
throw new Error("WorldDaemon is not a normal server. This is a bug. Please contact developers.");
}
return wd.backdoorInstalled;
}

@ -3,8 +3,7 @@ import { BitNodes } from "../BitNode";
import { PortalModal } from "./PortalModal"; import { PortalModal } from "./PortalModal";
import { CinematicText } from "../../ui/React/CinematicText"; import { CinematicText } from "../../ui/React/CinematicText";
import { Player } from "@player"; import { Player } from "@player";
import makeStyles from "@mui/styles/makeStyles"; import { makeStyles } from "tss-react/mui";
import createStyles from "@mui/styles/createStyles";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
@ -12,33 +11,31 @@ import { Settings } from "../../Settings/Settings";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { CompletedProgramName } from "@enums"; import { CompletedProgramName } from "@enums";
const useStyles = makeStyles(() => const useStyles = makeStyles()(() => ({
createStyles({ portal: {
portal: { cursor: "pointer",
cursor: "pointer", fontFamily: "inherit",
fontFamily: "inherit", fontSize: "1rem",
fontSize: "1rem", fontWeight: "bold",
fontWeight: "bold", lineHeight: 1,
lineHeight: 1, padding: 0,
padding: 0, "&:hover": {
"&:hover": { color: "#fff",
color: "#fff",
},
}, },
level0: { },
color: Settings.theme.bnlvl0, level0: {
}, color: Settings.theme.bnlvl0,
level1: { },
color: Settings.theme.bnlvl1, level1: {
}, color: Settings.theme.bnlvl1,
level2: { },
color: Settings.theme.bnlvl2, level2: {
}, color: Settings.theme.bnlvl2,
level3: { },
color: Settings.theme.bnlvl3, level3: {
}, color: Settings.theme.bnlvl3,
}), },
); }));
interface IPortalProps { interface IPortalProps {
n: number; n: number;
@ -48,7 +45,7 @@ interface IPortalProps {
} }
function BitNodePortal(props: IPortalProps): React.ReactElement { function BitNodePortal(props: IPortalProps): React.ReactElement {
const [portalOpen, setPortalOpen] = useState(false); const [portalOpen, setPortalOpen] = useState(false);
const classes = useStyles(); const { classes } = useStyles();
const bitNode = BitNodes[`BitNode${props.n}`]; const bitNode = BitNodes[`BitNode${props.n}`];
if (bitNode == null) { if (bitNode == null) {
return <>O</>; return <>O</>;

@ -17,7 +17,7 @@ import {
} from "@enums"; } from "@enums";
import { getKeyList } from "../utils/helpers/getKeyList"; import { getKeyList } from "../utils/helpers/getKeyList";
import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver"; import { constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver";
import { formatNumberNoSuffix } from "../ui/formatNumber"; import { formatHp, formatNumberNoSuffix, formatSleeveShock } from "../ui/formatNumber";
import { Skills } from "./data/Skills"; import { Skills } from "./data/Skills";
import { City } from "./City"; import { City } from "./City";
import { Player } from "@player"; import { Player } from "@player";
@ -46,6 +46,8 @@ import { clampInteger, clampNumber } from "../utils/helpers/clampNumber";
import { parseCommand } from "../Terminal/Parser"; import { parseCommand } from "../Terminal/Parser";
import { BlackOperations } from "./data/BlackOperations"; import { BlackOperations } from "./data/BlackOperations";
import { GeneralActions } from "./data/GeneralActions"; import { GeneralActions } from "./data/GeneralActions";
import { PlayerObject } from "../PersonObjects/Player/PlayerObject";
import { Sleeve } from "../PersonObjects/Sleeve/Sleeve";
export const BladeburnerPromise: PromisePair<number> = { promise: null, resolve: null }; export const BladeburnerPromise: PromisePair<number> = { promise: null, resolve: null };
@ -115,7 +117,6 @@ export class Bladeburner {
} }
calculateStaminaPenalty(): number { calculateStaminaPenalty(): number {
if (this.stamina === this.maxStamina) return 1;
return Math.min(1, this.stamina / (0.5 * this.maxStamina)); return Math.min(1, this.stamina / (0.5 * this.maxStamina));
} }
@ -854,6 +855,22 @@ export class Bladeburner {
} }
completeAction(person: Person, actionIdent: ActionIdentifier, isPlayer = true): WorkStats { completeAction(person: Person, actionIdent: ActionIdentifier, isPlayer = true): WorkStats {
const currentHp = person.hp.current;
const getExtraLogAfterTakingDamage = (damage: number) => {
let extraLog = "";
if (currentHp <= damage) {
if (person instanceof PlayerObject) {
extraLog += ` ${person.whoAmI()} was hospitalized. Current HP is ${formatHp(person.hp.current)}.`;
} else if (person instanceof Sleeve) {
extraLog += ` ${person.whoAmI()} was shocked. Current shock is ${formatSleeveShock(
person.shock,
)}. Current HP is ${formatHp(person.hp.current)}.`;
}
} else {
extraLog += ` HP reduced from ${formatHp(currentHp)} to ${formatHp(person.hp.current)}.`;
}
return extraLog;
};
let retValue = newWorkStats(); let retValue = newWorkStats();
const action = this.getActionObject(actionIdent); const action = this.getActionObject(actionIdent);
switch (action.type) { switch (action.type) {
@ -899,12 +916,12 @@ export class Bladeburner {
this.changeRank(person, gain); this.changeRank(person, gain);
if (isOperation && this.logging.ops) { if (isOperation && this.logging.ops) {
this.log( this.log(
`${person.whoAmI()}: ${action.name} successfully completed! Gained ${formatBigNumber(gain)} rank`, `${person.whoAmI()}: ${action.name} successfully completed! Gained ${formatBigNumber(gain)} rank.`,
); );
} else if (!isOperation && this.logging.contracts) { } else if (!isOperation && this.logging.contracts) {
this.log( this.log(
`${person.whoAmI()}: ${action.name} contract successfully completed! Gained ` + `${person.whoAmI()}: ${action.name} contract successfully completed! Gained ` +
`${formatBigNumber(gain)} rank and ${formatMoney(moneyGain)}`, `${formatBigNumber(gain)} rank and ${formatMoney(moneyGain)}.`,
); );
} }
} }
@ -930,15 +947,15 @@ export class Bladeburner {
} }
let logLossText = ""; let logLossText = "";
if (loss > 0) { if (loss > 0) {
logLossText += "Lost " + formatNumberNoSuffix(loss, 3) + " rank. "; logLossText += ` Lost ${formatNumberNoSuffix(loss, 3)} rank.`;
} }
if (damage > 0) { if (damage > 0) {
logLossText += "Took " + formatNumberNoSuffix(damage, 0) + " damage."; logLossText += ` Took ${formatNumberNoSuffix(damage, 0)} damage.${getExtraLogAfterTakingDamage(damage)}`;
} }
if (isOperation && this.logging.ops) { if (isOperation && this.logging.ops) {
this.log(`${person.whoAmI()}: ` + action.name + " failed! " + logLossText); this.log(`${person.whoAmI()}: ${action.name} failed!${logLossText}`);
} else if (!isOperation && this.logging.contracts) { } else if (!isOperation && this.logging.contracts) {
this.log(`${person.whoAmI()}: ` + action.name + " contract failed! " + logLossText); this.log(`${person.whoAmI()}: ${action.name} contract failed!${logLossText}`);
} }
isOperation ? this.completeOperation(false) : this.completeContract(false, action); isOperation ? this.completeOperation(false) : this.completeContract(false, action);
} }
@ -977,7 +994,9 @@ export class Bladeburner {
teamLossMax = Math.ceil(teamCount / 2); teamLossMax = Math.ceil(teamCount / 2);
if (this.logging.blackops) { if (this.logging.blackops) {
this.log(`${person.whoAmI()}: ${action.name} successful! Gained ${formatNumberNoSuffix(rankGain, 1)} rank`); this.log(
`${person.whoAmI()}: ${action.name} successful! Gained ${formatNumberNoSuffix(rankGain, 1)} rank.`,
);
} }
} else { } else {
retValue = this.getActionStats(action, person, false); retValue = this.getActionStats(action, person, false);
@ -1003,7 +1022,7 @@ export class Bladeburner {
`${person.whoAmI()}: ${action.name} failed! Lost ${formatNumberNoSuffix( `${person.whoAmI()}: ${action.name} failed! Lost ${formatNumberNoSuffix(
rankLoss, rankLoss,
1, 1,
)} rank and took ${formatNumberNoSuffix(damage, 0)} damage`, )} rank. Took ${formatNumberNoSuffix(damage, 0)} damage.${getExtraLogAfterTakingDamage(damage)}`,
); );
} }
} }
@ -1026,7 +1045,7 @@ export class Bladeburner {
this.teamLost += losses; this.teamLost += losses;
if (this.logging.blackops) { if (this.logging.blackops) {
this.log( this.log(
`${person.whoAmI()}: You lost ${formatNumberNoSuffix(losses, 0)} team members during ${action.name}`, `${person.whoAmI()}: You lost ${formatNumberNoSuffix(losses, 0)} team members during ${action.name}.`,
); );
} }
} }
@ -1059,7 +1078,7 @@ export class Bladeburner {
formatExp(agiExpGain) + formatExp(agiExpGain) +
" agi exp, " + " agi exp, " +
formatBigNumber(staminaGain) + formatBigNumber(staminaGain) +
" max stamina", " max stamina.",
); );
} }
break; break;
@ -1089,7 +1108,7 @@ export class Bladeburner {
`${person.whoAmI()}: ` + `${person.whoAmI()}: ` +
`Field analysis completed. Gained ${formatBigNumber(rankGain)} rank, ` + `Field analysis completed. Gained ${formatBigNumber(rankGain)} rank, ` +
`${formatExp(hackingExpGain)} hacking exp, and ` + `${formatExp(hackingExpGain)} hacking exp, and ` +
`${formatExp(charismaExpGain)} charisma exp`, `${formatExp(charismaExpGain)} charisma exp.`,
); );
} }
break; break;
@ -1105,7 +1124,7 @@ export class Bladeburner {
`${person.whoAmI()}: ` + `${person.whoAmI()}: ` +
"Successfully recruited a team member! Gained " + "Successfully recruited a team member! Gained " +
formatExp(expGain) + formatExp(expGain) +
" charisma exp", " charisma exp.",
); );
} }
} else { } else {
@ -1116,7 +1135,7 @@ export class Bladeburner {
`${person.whoAmI()}: ` + `${person.whoAmI()}: ` +
"Failed to recruit a team member. Gained " + "Failed to recruit a team member. Gained " +
formatExp(expGain) + formatExp(expGain) +
" charisma exp", " charisma exp.",
); );
} }
} }
@ -1132,7 +1151,7 @@ export class Bladeburner {
this.log( this.log(
`${person.whoAmI()}: Diplomacy completed. Chaos levels in the current city fell by ${formatPercent( `${person.whoAmI()}: Diplomacy completed. Chaos levels in the current city fell by ${formatPercent(
1 - eff, 1 - eff,
)}`, )}.`,
); );
} }
break; break;
@ -1140,14 +1159,22 @@ export class Bladeburner {
case BladeGeneralActionName.hyperbolicRegen: { case BladeGeneralActionName.hyperbolicRegen: {
person.regenerateHp(BladeburnerConstants.HrcHpGain); person.regenerateHp(BladeburnerConstants.HrcHpGain);
const currentStamina = this.stamina;
const staminaGain = this.maxStamina * (BladeburnerConstants.HrcStaminaGain / 100); const staminaGain = this.maxStamina * (BladeburnerConstants.HrcStaminaGain / 100);
this.stamina = Math.min(this.maxStamina, this.stamina + staminaGain); this.stamina = Math.min(this.maxStamina, this.stamina + staminaGain);
if (this.logging.general) { if (this.logging.general) {
this.log( let extraLog = "";
`${person.whoAmI()}: Rested in Hyperbolic Regeneration Chamber. Restored ${ if (Player.hp.current > currentHp) {
BladeburnerConstants.HrcHpGain extraLog += ` Restored ${formatHp(BladeburnerConstants.HrcHpGain)} HP. Current HP is ${formatHp(
} HP and gained ${formatStamina(staminaGain)} stamina`, Player.hp.current,
); )}.`;
}
if (this.stamina > currentStamina) {
extraLog += ` Restored ${formatStamina(staminaGain)} stamina. Current stamina is ${formatStamina(
this.stamina,
)}.`;
}
this.log(`${person.whoAmI()}: Rested in Hyperbolic Regeneration Chamber.${extraLog}`);
} }
break; break;
} }
@ -1258,13 +1285,16 @@ export class Bladeburner {
calculateMaxStamina(): void { calculateMaxStamina(): void {
const baseStamina = Math.pow(this.getEffectiveSkillLevel(Player, "agility"), 0.8); const baseStamina = Math.pow(this.getEffectiveSkillLevel(Player, "agility"), 0.8);
// Min value of maxStamina is an arbitrarily small positive value. It must not be 0 to avoid NaN stamina penalty.
const maxStamina = clampNumber( const maxStamina = clampNumber(
(baseStamina + this.staminaBonus) * (baseStamina + this.staminaBonus) *
this.getSkillMult(BladeMultName.stamina) * this.getSkillMult(BladeMultName.stamina) *
Player.mults.bladeburner_max_stamina, Player.mults.bladeburner_max_stamina,
0, 1e-9,
); );
if (this.maxStamina === maxStamina) return; if (this.maxStamina === maxStamina) {
return;
}
// If max stamina changed, adjust stamina accordingly // If max stamina changed, adjust stamina accordingly
const oldMax = this.maxStamina; const oldMax = this.maxStamina;
this.maxStamina = maxStamina; this.maxStamina = maxStamina;
@ -1431,6 +1461,16 @@ export class Bladeburner {
loadOperationsData(operationsData, bladeburner.operations); loadOperationsData(operationsData, bladeburner.operations);
// Regenerate skill multiplier data, which is not included in savedata // Regenerate skill multiplier data, which is not included in savedata
bladeburner.updateSkillMultipliers(); bladeburner.updateSkillMultipliers();
// If stamina or maxStamina is invalid, we set both of them to 1 and recalculate them.
if (
!Number.isFinite(bladeburner.stamina) ||
!Number.isFinite(bladeburner.maxStamina) ||
bladeburner.maxStamina === 0
) {
bladeburner.stamina = 1;
bladeburner.maxStamina = 1;
bladeburner.calculateMaxStamina();
}
return bladeburner; return bladeburner;
} }
} }

@ -0,0 +1,55 @@
import type { Bladeburner } from "../Bladeburner";
import type { Action } from "../Types";
import React from "react";
import { Box, Typography } from "@mui/material";
import { CopyableText } from "../../ui/React/CopyableText";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { StartButton } from "./StartButton";
import { StopButton } from "./StopButton";
import { TeamSizeButton } from "./TeamSizeButton";
import { formatNumberNoSuffix } from "../../ui/formatNumber";
import { BlackOperation, Operation } from "../Actions";
interface ActionHeaderProps {
bladeburner: Bladeburner;
action: Action;
rerender: () => void;
}
export function ActionHeader({ bladeburner, action, rerender }: ActionHeaderProps): React.ReactElement {
const isActive = action.name === bladeburner.action?.name;
const computedActionTimeCurrent = Math.min(
bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow,
bladeburner.actionTimeToComplete,
);
const allowTeam = action instanceof Operation || action instanceof BlackOperation;
if (isActive) {
return (
<>
<Box display="flex" flexDirection="row" alignItems="center">
<CopyableText value={action.name} />
<StopButton bladeburner={bladeburner} rerender={rerender} />
</Box>
<Typography>
(IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "}
{formatNumberNoSuffix(bladeburner.actionTimeToComplete, 0)})
</Typography>
<Typography>
{createProgressBarText({
progress: computedActionTimeCurrent / bladeburner.actionTimeToComplete,
})}
</Typography>
</>
);
}
return (
<Box display="flex" flexDirection="row" alignItems="center">
<CopyableText value={action.name} />
<StartButton bladeburner={bladeburner} action={action} rerender={rerender} />
{allowTeam && <TeamSizeButton bladeburner={bladeburner} action={action} />}
</Box>
);
}

@ -7,6 +7,7 @@ import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import { BladeburnerConstants } from "../data/Constants"; import { BladeburnerConstants } from "../data/Constants";
import { Contract } from "../Actions";
interface ActionLevelProps { interface ActionLevelProps {
action: LevelableAction; action: LevelableAction;
@ -18,6 +19,11 @@ interface ActionLevelProps {
export function ActionLevel({ action, isActive, bladeburner, rerender }: ActionLevelProps): React.ReactElement { export function ActionLevel({ action, isActive, bladeburner, rerender }: ActionLevelProps): React.ReactElement {
const canIncrease = action.level < action.maxLevel; const canIncrease = action.level < action.maxLevel;
const canDecrease = action.level > 1; const canDecrease = action.level > 1;
const successesNeededForNextLevel = action.getSuccessesNeededForNextLevel(
action instanceof Contract
? BladeburnerConstants.ContractSuccessesPerLevel
: BladeburnerConstants.OperationSuccessesPerLevel,
);
function increaseLevel(): void { function increaseLevel(): void {
if (!canIncrease) return; if (!canIncrease) return;
@ -36,21 +42,7 @@ export function ActionLevel({ action, isActive, bladeburner, rerender }: ActionL
return ( return (
<Box display="flex" flexDirection="row" alignItems="center"> <Box display="flex" flexDirection="row" alignItems="center">
<Box display="flex"> <Box display="flex">
<Tooltip <Tooltip title={<Typography>{successesNeededForNextLevel} successes needed for next level</Typography>}>
title={
action.constructor.name === "Contract" ? (
<Typography>
{action.getSuccessesNeededForNextLevel(BladeburnerConstants.ContractSuccessesPerLevel)} successes needed
for next level
</Typography>
) : (
<Typography>
{action.getSuccessesNeededForNextLevel(BladeburnerConstants.OperationSuccessesPerLevel)} successes
needed for next level
</Typography>
)
}
>
<Typography> <Typography>
Level: {action.level} / {action.maxLevel} Level: {action.level} / {action.maxLevel}
</Typography> </Typography>

@ -7,72 +7,41 @@ import { Paper, Typography } from "@mui/material";
import { Player } from "@player"; import { Player } from "@player";
import { formatNumberNoSuffix } from "../../ui/formatNumber"; import { formatNumberNoSuffix } from "../../ui/formatNumber";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { TeamSizeButton } from "./TeamSizeButton";
import { CopyableText } from "../../ui/React/CopyableText";
import { SuccessChance } from "./SuccessChance"; import { SuccessChance } from "./SuccessChance";
import { StartButton } from "./StartButton";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
import { ActionHeader } from "./ActionHeader";
interface BlackOpElemProps { interface BlackOpElemProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
blackOp: BlackOperation; action: BlackOperation;
} }
export function BlackOpElem({ bladeburner, blackOp }: BlackOpElemProps): React.ReactElement { export function BlackOpElem({ bladeburner, action }: BlackOpElemProps): React.ReactElement {
const rerender = useRerender(); const rerender = useRerender();
const isCompleted = bladeburner.numBlackOpsComplete > blackOp.n; const isCompleted = bladeburner.numBlackOpsComplete > action.n;
if (isCompleted) { if (isCompleted) {
return ( return (
<Paper sx={{ my: 1, p: 1 }}> <Paper sx={{ my: 1, p: 1 }}>
<Typography>{blackOp.name} (COMPLETED)</Typography> <Typography>{action.name} (COMPLETED)</Typography>
</Paper> </Paper>
); );
} }
const isActive = bladeburner.action?.name === blackOp.name; const actionTime = action.getActionTime(bladeburner, Player);
const actionTime = blackOp.getActionTime(bladeburner, Player); const hasRequiredRank = bladeburner.rank >= action.reqdRank;
const hasReqdRank = bladeburner.rank >= blackOp.reqdRank;
const computedActionTimeCurrent = Math.min(
bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow,
bladeburner.actionTimeToComplete,
);
return ( return (
<Paper sx={{ my: 1, p: 1 }}> <Paper sx={{ my: 1, p: 1 }}>
{isActive ? ( <ActionHeader bladeburner={bladeburner} action={action} rerender={rerender}></ActionHeader>
<>
<CopyableText value={blackOp.name} />
<Typography>
(IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "}
{formatNumberNoSuffix(bladeburner.actionTimeToComplete, 0)})
</Typography>
<Typography>
{createProgressBarText({
progress: computedActionTimeCurrent / bladeburner.actionTimeToComplete,
})}
</Typography>
</>
) : (
<>
<CopyableText value={blackOp.name} />
<StartButton bladeburner={bladeburner} action={blackOp} rerender={rerender} />
<TeamSizeButton action={blackOp} bladeburner={bladeburner} />
</>
)}
<br /> <br />
<Typography whiteSpace={"pre-wrap"}>{action.desc}</Typography>
<br /> <br />
<Typography whiteSpace={"pre-wrap"}>{blackOp.desc}</Typography> <Typography color={hasRequiredRank ? "primary" : "error"}>
<br /> Required Rank: {formatNumberNoSuffix(action.reqdRank, 0)}
<br />
<Typography color={hasReqdRank ? "primary" : "error"}>
Required Rank: {formatNumberNoSuffix(blackOp.reqdRank, 0)}
</Typography> </Typography>
<br /> <br />
<Typography> <Typography>
<SuccessChance action={blackOp} bladeburner={bladeburner} /> <SuccessChance action={action} bladeburner={bladeburner} />
<br /> <br />
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)} Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
</Typography> </Typography>

@ -1,6 +1,6 @@
import type { Bladeburner } from "../Bladeburner"; import type { Bladeburner } from "../Bladeburner";
import * as React from "react"; import React from "react";
import { Button, Typography } from "@mui/material"; import { Button, Typography } from "@mui/material";
import { FactionName } from "@enums"; import { FactionName } from "@enums";
import { BlackOpElem } from "./BlackOpElem"; import { BlackOpElem } from "./BlackOpElem";
@ -8,13 +8,25 @@ import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router"; import { Page } from "../../ui/Router";
import { CorruptableText } from "../../ui/React/CorruptableText"; import { CorruptableText } from "../../ui/React/CorruptableText";
import { blackOpsArray } from "../data/BlackOperations"; import { blackOpsArray } from "../data/BlackOperations";
import { GetServer } from "../../Server/AllServers";
import { SpecialServers } from "../../Server/data/SpecialServers";
import { Server } from "../../Server/Server";
interface BlackOpPageProps { interface BlackOpPageProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
} }
function finishBitNode() {
const wd = GetServer(SpecialServers.WorldDaemon);
if (!(wd instanceof Server)) {
throw new Error("WorldDaemon is not a normal server. This is a bug. Please contact developers.");
}
wd.backdoorInstalled = true;
Router.toPage(Page.BitVerse, { flume: false, quick: false });
}
export function BlackOpPage({ bladeburner }: BlackOpPageProps): React.ReactElement { export function BlackOpPage({ bladeburner }: BlackOpPageProps): React.ReactElement {
const blackOps = blackOpsArray.slice(0, bladeburner.numBlackOpsComplete + 1).reverse(); const blackOperations = blackOpsArray.slice(0, bladeburner.numBlackOpsComplete + 1).reverse();
return ( return (
<> <>
@ -33,13 +45,13 @@ export function BlackOpPage({ bladeburner }: BlackOpPageProps): React.ReactEleme
losses. losses.
</Typography> </Typography>
{bladeburner.numBlackOpsComplete >= blackOpsArray.length ? ( {bladeburner.numBlackOpsComplete >= blackOpsArray.length ? (
<Button sx={{ my: 1, p: 1 }} onClick={() => Router.toPage(Page.BitVerse, { flume: false, quick: false })}> <Button sx={{ my: 1, p: 1 }} onClick={finishBitNode}>
<CorruptableText content="Destroy w0rld_d34mon" spoiler={false}></CorruptableText> <CorruptableText content="Destroy w0r1d_d43m0n" spoiler={false}></CorruptableText>
</Button> </Button>
) : ( ) : (
<> <>
{blackOps.map((blackOp) => ( {blackOperations.map((blackOperation) => (
<BlackOpElem key={blackOp.name} bladeburner={bladeburner} blackOp={blackOp} /> <BlackOpElem key={blackOperation.name} bladeburner={bladeburner} action={blackOperation} />
))} ))}
</> </>
)} )}

@ -5,36 +5,33 @@ import { KEY } from "../../utils/helpers/keyCodes";
import { Box, List, ListItem, Paper, TextField, Typography } from "@mui/material"; import { Box, List, ListItem, Paper, TextField, Typography } from "@mui/material";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles"; import { makeStyles } from "tss-react/mui";
import createStyles from "@mui/styles/createStyles";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
interface ILineProps { interface ILineProps {
content: React.ReactNode; content: React.ReactNode;
} }
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles()((theme: Theme) => ({
createStyles({ textfield: {
textfield: { margin: theme.spacing(0),
margin: theme.spacing(0), width: "100%",
width: "100%", },
}, input: {
input: { backgroundColor: theme.colors.backgroundsecondary,
backgroundColor: theme.colors.backgroundsecondary, },
}, nopadding: {
nopadding: { padding: theme.spacing(0),
padding: theme.spacing(0), },
}, preformatted: {
preformatted: { whiteSpace: "pre-wrap",
whiteSpace: "pre-wrap", margin: theme.spacing(0),
margin: theme.spacing(0), },
}, list: {
list: { padding: theme.spacing(0),
padding: theme.spacing(0), height: "100%",
height: "100%", },
}, }));
}),
);
function Line(props: ILineProps): React.ReactElement { function Line(props: ILineProps): React.ReactElement {
return ( return (
@ -49,7 +46,7 @@ interface IProps {
} }
export function Console(props: IProps): React.ReactElement { export function Console(props: IProps): React.ReactElement {
const classes = useStyles(); const { classes } = useStyles();
const [command, setCommand] = useState(""); const [command, setCommand] = useState("");
const consoleInput = useRef<HTMLInputElement>(null); const consoleInput = useRef<HTMLInputElement>(null);
useRerender(1000); useRerender(1000);

@ -2,18 +2,15 @@ import type { Bladeburner } from "../Bladeburner";
import type { Contract } from "../Actions/Contract"; import type { Contract } from "../Actions/Contract";
import React from "react"; import React from "react";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { Player } from "@player"; import { Player } from "@player";
import { SuccessChance } from "./SuccessChance"; import { SuccessChance } from "./SuccessChance";
import { CopyableText } from "../../ui/React/CopyableText";
import { ActionLevel } from "./ActionLevel"; import { ActionLevel } from "./ActionLevel";
import { Autolevel } from "./Autolevel"; import { Autolevel } from "./Autolevel";
import { StartButton } from "./StartButton"; import { formatBigNumber } from "../../ui/formatNumber";
import { formatNumberNoSuffix, formatBigNumber } from "../../ui/formatNumber";
import { Paper, Typography } from "@mui/material"; import { Paper, Typography } from "@mui/material";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
import { getEnumHelper } from "../../utils/EnumHelper"; import { ActionHeader } from "./ActionHeader";
interface ContractElemProps { interface ContractElemProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
@ -22,41 +19,15 @@ interface ContractElemProps {
export function ContractElem({ bladeburner, action }: ContractElemProps): React.ReactElement { export function ContractElem({ bladeburner, action }: ContractElemProps): React.ReactElement {
const rerender = useRerender(); const rerender = useRerender();
// Temp special return
if (!getEnumHelper("BladeContractName").isMember(action.name)) return <></>;
const isActive = action.name === bladeburner.action?.name; const isActive = action.name === bladeburner.action?.name;
const computedActionTimeCurrent = Math.min(
bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow,
bladeburner.actionTimeToComplete,
);
const actionTime = action.getActionTime(bladeburner, Player); const actionTime = action.getActionTime(bladeburner, Player);
return ( return (
<Paper sx={{ my: 1, p: 1 }}> <Paper sx={{ my: 1, p: 1 }}>
{isActive ? ( <ActionHeader bladeburner={bladeburner} action={action} rerender={rerender}></ActionHeader>
<>
<CopyableText value={action.name} />
<Typography>
(IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "}
{formatNumberNoSuffix(bladeburner.actionTimeToComplete, 0)})
</Typography>
<Typography>
{createProgressBarText({
progress: computedActionTimeCurrent / bladeburner.actionTimeToComplete,
})}
</Typography>
</>
) : (
<>
<CopyableText value={action.name} />
<StartButton bladeburner={bladeburner} action={action} rerender={rerender} />
</>
)}
<br />
<br /> <br />
<ActionLevel action={action} bladeburner={bladeburner} isActive={isActive} rerender={rerender} /> <ActionLevel action={action} bladeburner={bladeburner} isActive={isActive} rerender={rerender} />
<br /> <br />
<br />
<Typography whiteSpace={"pre-wrap"}> <Typography whiteSpace={"pre-wrap"}>
{action.desc} {action.desc}
<br /> <br />

@ -2,15 +2,12 @@ import type { Bladeburner } from "../Bladeburner";
import type { GeneralAction } from "../Actions/GeneralAction"; import type { GeneralAction } from "../Actions/GeneralAction";
import React from "react"; import React from "react";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { formatNumberNoSuffix } from "../../ui/formatNumber"; import { formatNumberNoSuffix } from "../../ui/formatNumber";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { Player } from "@player"; import { Player } from "@player";
import { CopyableText } from "../../ui/React/CopyableText"; import { Paper, Typography } from "@mui/material";
import { StartButton } from "./StartButton";
import { StopButton } from "./StopButton";
import { Box, Paper, Typography } from "@mui/material";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
import { ActionHeader } from "./ActionHeader";
interface GeneralActionElemProps { interface GeneralActionElemProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
@ -19,44 +16,16 @@ interface GeneralActionElemProps {
export function GeneralActionElem({ bladeburner, action }: GeneralActionElemProps): React.ReactElement { export function GeneralActionElem({ bladeburner, action }: GeneralActionElemProps): React.ReactElement {
const rerender = useRerender(); const rerender = useRerender();
const isActive = action.name === bladeburner.action?.name;
const computedActionTimeCurrent = Math.min(
bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow,
bladeburner.actionTimeToComplete,
);
const actionTime = action.getActionTime(bladeburner, Player); const actionTime = action.getActionTime(bladeburner, Player);
const successChance = const successChance =
action.name === "Recruitment" ? Math.max(0, Math.min(bladeburner.getRecruitmentSuccessChance(Player), 1)) : -1; action.name === "Recruitment" ? Math.max(0, Math.min(bladeburner.getRecruitmentSuccessChance(Player), 1)) : -1;
return ( return (
<Paper sx={{ my: 1, p: 1 }}> <Paper sx={{ my: 1, p: 1 }}>
{isActive ? ( <ActionHeader bladeburner={bladeburner} action={action} rerender={rerender}></ActionHeader>
<>
<Box display="flex" flexDirection="row" alignItems="center">
<CopyableText value={action.name} />
<StopButton bladeburner={bladeburner} rerender={rerender} />
</Box>
<Typography>
(IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)} /{" "}
{formatNumberNoSuffix(bladeburner.actionTimeToComplete, 0)})
</Typography>
<Typography>
{createProgressBarText({
progress: computedActionTimeCurrent / bladeburner.actionTimeToComplete,
})}
</Typography>
</>
) : (
<Box display="flex" flexDirection="row" alignItems="center">
<CopyableText value={action.name} />
<StartButton bladeburner={bladeburner} action={action} rerender={rerender} />
</Box>
)}
<br />
<br /> <br />
<Typography>{action.desc}</Typography> <Typography>{action.desc}</Typography>
<br /> <br />
<br />
<Typography> <Typography>
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)} Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
{successChance !== -1 && ( {successChance !== -1 && (

@ -5,76 +5,47 @@ import React from "react";
import { Paper, Typography } from "@mui/material"; import { Paper, Typography } from "@mui/material";
import { Player } from "@player"; import { Player } from "@player";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions"; import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { SuccessChance } from "./SuccessChance"; import { SuccessChance } from "./SuccessChance";
import { ActionLevel } from "./ActionLevel"; import { ActionLevel } from "./ActionLevel";
import { Autolevel } from "./Autolevel"; import { Autolevel } from "./Autolevel";
import { StartButton } from "./StartButton"; import { formatBigNumber } from "../../ui/formatNumber";
import { TeamSizeButton } from "./TeamSizeButton";
import { CopyableText } from "../../ui/React/CopyableText";
import { formatNumberNoSuffix, formatBigNumber } from "../../ui/formatNumber";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
import { BladeActionType } from "@enums"; import { BladeActionType } from "@enums";
import { ActionHeader } from "./ActionHeader";
interface OperationElemProps { interface OperationElemProps {
bladeburner: Bladeburner; bladeburner: Bladeburner;
operation: Operation; action: Operation;
} }
export function OperationElem({ bladeburner, operation }: OperationElemProps): React.ReactElement { export function OperationElem({ bladeburner, action }: OperationElemProps): React.ReactElement {
const rerender = useRerender(); const rerender = useRerender();
const isActive = const isActive = bladeburner.action?.type === BladeActionType.operation && action.name === bladeburner.action?.name;
bladeburner.action?.type === BladeActionType.operation && operation.name === bladeburner.action?.name; const actionTime = action.getActionTime(bladeburner, Player);
const computedActionTimeCurrent = Math.min(
bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow,
bladeburner.actionTimeToComplete,
);
const actionTime = operation.getActionTime(bladeburner, Player);
return ( return (
<Paper sx={{ my: 1, p: 1 }}> <Paper sx={{ my: 1, p: 1 }}>
{isActive ? ( <ActionHeader bladeburner={bladeburner} action={action} rerender={rerender}></ActionHeader>
<>
<Typography>
<CopyableText value={operation.name} /> (IN PROGRESS - {formatNumberNoSuffix(computedActionTimeCurrent, 0)}{" "}
/ {formatNumberNoSuffix(bladeburner.actionTimeToComplete, 0)})
</Typography>
<Typography>
{createProgressBarText({
progress: computedActionTimeCurrent / bladeburner.actionTimeToComplete,
})}
</Typography>
</>
) : (
<>
<CopyableText value={operation.name} />
<StartButton bladeburner={bladeburner} action={operation} rerender={rerender} />
<TeamSizeButton action={operation} bladeburner={bladeburner} />
</>
)}
<br />
<br />
<ActionLevel action={operation} bladeburner={bladeburner} isActive={isActive} rerender={rerender} />
<br /> <br />
<ActionLevel action={action} bladeburner={bladeburner} isActive={isActive} rerender={rerender} />
<br /> <br />
<Typography whiteSpace={"pre-wrap"}> <Typography whiteSpace={"pre-wrap"}>
{operation.desc} {action.desc}
<br /> <br />
<br /> <br />
<SuccessChance action={operation} bladeburner={bladeburner} /> <SuccessChance action={action} bladeburner={bladeburner} />
<br /> <br />
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)} Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
<br /> <br />
Operations remaining: {formatBigNumber(Math.floor(operation.count))} Operations remaining: {formatBigNumber(Math.floor(action.count))}
<br /> <br />
Successes: {formatBigNumber(operation.successes)} Successes: {formatBigNumber(action.successes)}
<br /> <br />
Failures: {formatBigNumber(operation.failures)} Failures: {formatBigNumber(action.failures)}
</Typography> </Typography>
<br /> <br />
<Autolevel rerender={rerender} action={operation} /> <Autolevel rerender={rerender} action={action} />
</Paper> </Paper>
); );
} }

@ -1,6 +1,6 @@
import type { Bladeburner } from "../Bladeburner"; import type { Bladeburner } from "../Bladeburner";
import * as React from "react"; import React from "react";
import { OperationElem } from "./OperationElem"; import { OperationElem } from "./OperationElem";
import { Typography } from "@mui/material"; import { Typography } from "@mui/material";
@ -30,7 +30,7 @@ export function OperationPage({ bladeburner }: OperationPageProps): React.ReactE
difficult, but grant more rank and experience. difficult, but grant more rank and experience.
</Typography> </Typography>
{operations.map((operation) => ( {operations.map((operation) => (
<OperationElem key={operation.name} bladeburner={bladeburner} operation={operation} /> <OperationElem key={operation.name} bladeburner={bladeburner} action={operation} />
))} ))}
</> </>
); );

@ -20,7 +20,11 @@ export function StartButton({ bladeburner, action, rerender }: StartButtonProps)
} }
return ( return (
<ButtonWithTooltip disabledTooltip={disabledReason} onClick={onStart}> <ButtonWithTooltip
buttonProps={{ style: { marginLeft: "1rem" } }}
disabledTooltip={disabledReason}
onClick={onStart}
>
Start Start
</ButtonWithTooltip> </ButtonWithTooltip>
); );

@ -13,5 +13,9 @@ export function StopButton({ bladeburner, rerender }: StopButtonProps): React.Re
rerender(); rerender();
} }
return <Button onClick={onClick}>Stop</Button>; return (
<Button style={{ marginLeft: "1rem" }} onClick={onClick}>
Stop
</Button>
);
} }

@ -15,7 +15,7 @@ export function TeamSizeButton({ action, bladeburner }: TeamSizeButtonProps): Re
return ( return (
<> <>
<Button disabled={bladeburner.teamSize === 0} onClick={() => setOpen(true)}> <Button style={{ marginLeft: "1rem" }} disabled={bladeburner.teamSize === 0} onClick={() => setOpen(true)}>
Set Team Size (Curr Size: {formatNumberNoSuffix(action.teamCount, 0)}) Set Team Size (Curr Size: {formatNumberNoSuffix(action.teamCount, 0)})
</Button> </Button>
<TeamSizeModal open={open} onClose={() => setOpen(false)} action={action} bladeburner={bladeburner} /> <TeamSizeModal open={open} onClose={() => setOpen(false)} action={action} bladeburner={bladeburner} />

@ -1,8 +1,7 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { Card, Suit } from "./Card"; import { Card, Suit } from "./Card";
import makeStyles from "@mui/styles/makeStyles"; import { makeStyles } from "tss-react/mui";
import createStyles from "@mui/styles/createStyles";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
interface Props { interface Props {
@ -10,35 +9,33 @@ interface Props {
hidden?: boolean; hidden?: boolean;
} }
const useStyles = makeStyles(() => const useStyles = makeStyles()(() => ({
createStyles({ card: {
card: { padding: "10px",
padding: "10px", border: "solid 1px #808080",
border: "solid 1px #808080", backgroundColor: "white",
backgroundColor: "white", display: "inline-block",
display: "inline-block", borderRadius: "10px",
borderRadius: "10px", fontSize: "18.5px",
fontSize: "18.5px", textAlign: "center",
textAlign: "center", margin: "3px",
margin: "3px", fontWeight: "bold",
fontWeight: "bold", },
}, red: {
red: { color: "red",
color: "red", },
},
black: { black: {
color: "black", color: "black",
}, },
value: { value: {
fontSize: "20px", fontSize: "20px",
fontFamily: "sans-serif", fontFamily: "sans-serif",
}, },
}), }));
);
export const ReactCard: FC<Props> = ({ card, hidden }) => { export const ReactCard: FC<Props> = ({ card, hidden }) => {
const classes = useStyles(); const { classes } = useStyles();
let suit: React.ReactNode; let suit: React.ReactNode;
switch (card.suit) { switch (card.suit) {
case Suit.Clubs: case Suit.Clubs:

@ -24,7 +24,6 @@ export const CONSTANTS: {
IntelligenceCrimeBaseExpGain: number; IntelligenceCrimeBaseExpGain: number;
IntelligenceProgramBaseExpGain: number; IntelligenceProgramBaseExpGain: number;
IntelligenceGraftBaseExpGain: number; IntelligenceGraftBaseExpGain: number;
IntelligenceTerminalHackBaseExpGain: number;
IntelligenceSingFnBaseExpGain: number; IntelligenceSingFnBaseExpGain: number;
MillisecondsPer20Hours: number; MillisecondsPer20Hours: number;
GameCyclesPer20Hours: number; GameCyclesPer20Hours: number;
@ -101,7 +100,6 @@ export const CONSTANTS: {
IntelligenceCrimeBaseExpGain: 0.05, IntelligenceCrimeBaseExpGain: 0.05,
IntelligenceProgramBaseExpGain: 0.1, // Program required hack level divided by this to determine int exp gain IntelligenceProgramBaseExpGain: 0.1, // Program required hack level divided by this to determine int exp gain
IntelligenceGraftBaseExpGain: 0.05, IntelligenceGraftBaseExpGain: 0.05,
IntelligenceTerminalHackBaseExpGain: 200, // Hacking exp divided by this to determine int exp gain
IntelligenceSingFnBaseExpGain: 1.5, IntelligenceSingFnBaseExpGain: 1.5,
// Time-related constants // Time-related constants
@ -158,10 +156,48 @@ export const CONSTANTS: {
// Also update doc/source/changelog.rst // Also update doc/source/changelog.rst
LatestUpdate: ` LatestUpdate: `
## v2.6.2 dev - Last update 22 May 2024 ## v2.6.2 dev - Last update 4 June 2024
See 2.6.1 changelog at https://github.com/bitburner-official/bitburner-src/blob/v2.6.1/src/Documentation/doc/changelog.md See 2.6.1 changelog at https://github.com/bitburner-official/bitburner-src/blob/v2.6.1/src/Documentation/doc/changelog.md
No changes yet since 2.6.1 release ### CHANGES
- Hotfix (also backported to 2.6.1): Fixed an issue with invalid format on steam cloud save (@catloversg)
- Augmentations: Adjusted handling of augmentations that affect starting money or programs (@jjclark1982)
- Coding Contracts: Improved the performance of the All Valid Math Expressions contract checker (@yichizhng)
- Coding Contracts: Simplified the Shortest Path contract checker (@gmcew)
- Coding Contracts: Clarification on HammingCodes: Encoded Binary to Integer description (@gmcew)
- Faction: Fixed some edge cases around Favor overflow (@catloversg)
- Faction Invites: Code refactoring, all available invites are sent at once (@catloversg)
- Faction UI: show which skills are relevant for each type of Faction work (@gmcew)
- Font: Embedded the JetBrains Mono font as "JetBrainsMono" (@catloversg)
- Go: Support playing manually as white against your own scripts (@ficocelliguy)
- Go: Save a full game history to prevent repeat moves (@ficocelliguy)
- Infiltration: Updated Slash game text to be less confusing (@catloversg)
- Netscript API docs: Fixed some invalid usage issues + general type improvements (@catloversg, @ficocelliguy)
- Programs UI: Changed time elapsed display to time left (@TheAimMan)
- Servers: Game servers can now start with more than 1 core (@TheAimMan)
- Scripts: Relative imports should now work correctly (@Caldwell-74)
- Script Editor: Improved detection of possible infinite loops (@G4mingJon4s)
- Script Editor: should now remember cursor location when switching tabs or game pages (@catloversg)
- Skill XP: Fix an issue where in some cases, too much experience was needed to raise a skill from 1 to 2 (@catloversg)
- Terminal: Improved autocompletion code for mixed case strings (@yichizhng)
- Codebase: Partial migration away from outdated mui/styles (@Caldwell-74)
### SPOILER CHANGES
- Bladeburner: Added a button to stop the current action (@Kelenius)
- Bladeburner UI: Display Black Operations in the expected order (@catloversg)
- Corporation: Allow mass discarding products by selling for 0 (@gmcew)
- Grafting: Fixed a spacing issue (@Sphyxis)
- Grafting/Hacknet: Fixed an issue that could cause hacknet node production to be inaccurrate when combined with Grafting (@catloversg)
- Grafting: Fixed an issue that could cause inaccurate HP after Grafting (@catloversg)
- Hashnet: Clarified effect of hacknet multipliers in in documentation (@catloversg)
- Sleeve: Sleeve travel can no longer be performed if the player has insufficient funds (@gmcew)
- Sleeve: Added a missing availability check when installing augmentations on Sleeves (@yichizhng)
- Sleeve API: Fix an issue in ns.sleeve.setToBladeburnerAction that prevented setting sleeves to contract work (@Sphyxis)
### OTHER
- Nerf noodle bar
`, `,
}; };

@ -15,6 +15,7 @@ import { JSONMap, JSONSet } from "../Types/Jsonable";
import { PartialRecord, getRecordEntries, getRecordKeys, getRecordValues } from "../Types/Record"; import { PartialRecord, getRecordEntries, getRecordKeys, getRecordValues } from "../Types/Record";
import { Material } from "./Material"; import { Material } from "./Material";
import { getKeyList } from "../utils/helpers/getKeyList"; import { getKeyList } from "../utils/helpers/getKeyList";
import { calculateMarkupMultiplier } from "./helpers";
interface DivisionParams { interface DivisionParams {
name: string; name: string;
@ -582,26 +583,13 @@ export class Division {
} }
mat.uiMarketPrice = sCost; mat.uiMarketPrice = sCost;
// Calculate how much of the material sells (per second) const markupMultiplier = calculateMarkupMultiplier(sCost, mat.marketPrice, markupLimit);
let markup = 1;
if (sCost > mat.marketPrice) {
//Penalty if difference between sCost and bCost is greater than markup limit
if (sCost - mat.marketPrice > markupLimit) {
markup = Math.pow(markupLimit / (sCost - mat.marketPrice), 2);
}
} else if (sCost < mat.marketPrice) {
if (sCost <= 0) {
markup = 1e12; //Sell everything, essentially discard
} else {
//Lower prices than market increases sales
markup = mat.marketPrice / sCost;
}
}
// Calculate how much of the material sells (per second)
mat.maxSellPerCycle = mat.maxSellPerCycle =
(mat.quality + 0.001) * (mat.quality + 0.001) *
marketFactor * marketFactor *
markup * markupMultiplier *
businessFactor * businessFactor *
corporation.getSalesMult() * corporation.getSalesMult() *
advertisingFactor * advertisingFactor *
@ -920,26 +908,22 @@ export class Division {
sCost = sellPrice; sCost = sellPrice;
} }
product.uiMarketPrice[city] = sCost; product.uiMarketPrice[city] = sCost;
let markup = 1;
if (sCost > product.cityData[city].productionCost) { const markupMultiplier = calculateMarkupMultiplier(sCost, product.cityData[city].productionCost, markupLimit);
if (sCost - product.cityData[city].productionCost > markupLimit) {
markup = markupLimit / (sCost - product.cityData[city].productionCost);
}
}
product.maxSellAmount = product.maxSellAmount =
0.5 * 0.5 *
Math.pow(product.cityData[city].effectiveRating, 0.65) * Math.pow(product.cityData[city].effectiveRating, 0.65) *
marketFactor * marketFactor *
corporation.getSalesMult() * corporation.getSalesMult() *
Math.pow(markup, 2) * markupMultiplier *
businessFactor * businessFactor *
advertisingFactor * advertisingFactor *
this.getSalesMultiplier(); this.getSalesMultiplier();
sellAmt = Math.min(product.maxSellAmount, sellAmt); sellAmt = Math.min(product.maxSellAmount, sellAmt);
sellAmt = sellAmt * corpConstants.secondsPerMarketCycle * marketCycles; sellAmt = sellAmt * corpConstants.secondsPerMarketCycle * marketCycles;
sellAmt = Math.min(product.cityData[city].stored, sellAmt); //data[0] is qty sellAmt = Math.min(product.cityData[city].stored, sellAmt); //data[0] is qty
if (sellAmt && sCost) { if (sellAmt && sCost >= 0) {
product.cityData[city].stored -= sellAmt; //data[0] is qty product.cityData[city].stored -= sellAmt; //data[0] is qty
totalProfit += sellAmt * sCost; totalProfit += sellAmt * sCost;
product.cityData[city].actualSellAmount = sellAmt / (corpConstants.secondsPerMarketCycle * marketCycles); //data[2] is sell property product.cityData[city].actualSellAmount = sellAmt / (corpConstants.secondsPerMarketCycle * marketCycles); //data[2] is sell property

@ -88,3 +88,26 @@ export function issueNewSharesFailureReason(corp: Corporation, numShares: number
return ""; return "";
} }
export function calculateMarkupMultiplier(sellingPrice: number, marketPrice: number, markupLimit: number): number {
// Sanitize sellingPrice
if (!Number.isFinite(sellingPrice)) {
return 1;
}
let markupMultiplier = 1;
if (sellingPrice > marketPrice) {
// markupMultiplier is a penalty modifier if sellingPrice is greater than the sum of marketPrice and markupLimit.
if (sellingPrice > marketPrice + markupLimit) {
markupMultiplier = Math.pow(markupLimit / (sellingPrice - marketPrice), 2);
}
} else {
if (sellingPrice <= 0) {
// Discard
markupMultiplier = 1e12;
} else {
// markupMultiplier is a bonus modifier if sellingPrice is less than marketPrice.
markupMultiplier = marketPrice / sellingPrice;
}
}
return markupMultiplier;
}

@ -1,7 +1,7 @@
// React Component for displaying an Industry's warehouse information // React Component for displaying an Industry's warehouse information
// (right-side panel in the Industry UI) // (right-side panel in the Industry UI)
import React, { useState } from "react"; import React, { useState } from "react";
import { createStyles, makeStyles } from "@mui/styles"; import { makeStyles } from "tss-react/mui";
import { Box, Button, Paper, Tooltip, Typography } from "@mui/material"; import { Box, Button, Paper, Tooltip, Typography } from "@mui/material";
import * as corpConstants from "../data/Constants"; import * as corpConstants from "../data/Constants";
import { CityName, CorpUnlockName } from "@enums"; import { CityName, CorpUnlockName } from "@enums";
@ -32,16 +32,14 @@ interface WarehouseProps {
rerender: () => void; rerender: () => void;
} }
const useStyles = makeStyles(() => const useStyles = makeStyles()(() => ({
createStyles({ retainHeight: {
retainHeight: { minHeight: "3em",
minHeight: "3em", },
}, }));
}),
);
function WarehouseRoot(props: WarehouseProps): React.ReactElement { function WarehouseRoot(props: WarehouseProps): React.ReactElement {
const classes = useStyles(); const { classes } = useStyles();
const corp = useCorporation(); const corp = useCorporation();
const division = useDivision(); const division = useDivision();
const [smartSupplyOpen, setSmartSupplyOpen] = useState(false); const [smartSupplyOpen, setSmartSupplyOpen] = useState(false);

@ -2,19 +2,16 @@ import * as React from "react";
import { formatMoney } from "../../ui/formatNumber"; import { formatMoney } from "../../ui/formatNumber";
import { Corporation } from "../Corporation"; import { Corporation } from "../Corporation";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles"; import { makeStyles } from "tss-react/mui";
import createStyles from "@mui/styles/createStyles";
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles()((theme: Theme) => ({
createStyles({ unbuyable: {
unbuyable: { color: theme.palette.action.disabled,
color: theme.palette.action.disabled, },
}, money: {
money: { color: theme.colors.money,
color: theme.colors.money, },
}, }));
}),
);
interface IProps { interface IProps {
money: number; money: number;
@ -22,7 +19,7 @@ interface IProps {
} }
export function MoneyCost(props: IProps): React.ReactElement { export function MoneyCost(props: IProps): React.ReactElement {
const classes = useStyles(); const { classes } = useStyles();
if (!(props.corp.funds > props.money)) return <span className={classes.unbuyable}>{formatMoney(props.money)}</span>; if (!(props.corp.funds > props.money)) return <span className={classes.unbuyable}>{formatMoney(props.money)}</span>;
return <span className={classes.money}>{formatMoney(props.money)}</span>; return <span className={classes.money}>{formatMoney(props.money)}</span>;

@ -1,9 +1,9 @@
import * as React from "react"; import * as React from "react";
import makeStyles from "@mui/styles/makeStyles"; import { makeStyles } from "tss-react/mui";
import { TableCell as MuiTableCell, TableCellProps } from "@mui/material"; import { TableCell as MuiTableCell, TableCellProps } from "@mui/material";
const useStyles = makeStyles({ const useStyles = makeStyles()({
root: { root: {
border: "1px solid white", border: "1px solid white",
width: "5px", width: "5px",
@ -16,7 +16,7 @@ export const TableCell: React.FC<TableCellProps> = (props: TableCellProps) => {
<MuiTableCell <MuiTableCell
{...props} {...props}
classes={{ classes={{
root: useStyles().root, root: useStyles().classes.root,
...props.classes, ...props.classes,
}} }}
/> />

@ -2,7 +2,7 @@ import React, { useCallback } from "react";
import { Accordion, AccordionSummary, AccordionDetails, Button, ButtonGroup, Typography } from "@mui/material"; import { Accordion, AccordionSummary, AccordionDetails, Button, ButtonGroup, Typography } from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "tss-react/mui";
import { Player } from "@player"; import { Player } from "@player";
import { Sleeve } from "../../PersonObjects/Sleeve/Sleeve"; import { Sleeve } from "../../PersonObjects/Sleeve/Sleeve";
@ -11,7 +11,7 @@ import { MaxSleevesFromCovenant } from "../../PersonObjects/Sleeve/SleeveCovenan
// Update as additional BitNodes get implemented // Update as additional BitNodes get implemented
const validSFN = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]; const validSFN = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
const useStyles = makeStyles({ const useStyles = makeStyles()({
group: { group: {
display: "inline-flex", display: "inline-flex",
placeItems: "center", placeItems: "center",
@ -23,7 +23,7 @@ const useStyles = makeStyles({
}); });
export function SourceFilesDev({ parentRerender }: { parentRerender: () => void }): React.ReactElement { export function SourceFilesDev({ parentRerender }: { parentRerender: () => void }): React.ReactElement {
const classes = useStyles(); const { classes } = useStyles();
const setSF = useCallback( const setSF = useCallback(
(sfN: number, sfLvl: number) => () => { (sfN: number, sfLvl: number) => () => {

@ -63,12 +63,13 @@ $$MarketFactor = Max\left(0.1,{Demand\ast(100 - Competition)}\ast{0.01}\right)$$
- Division's research bonus: this is always 1. Currently there is not any research that increases the sales bonus. - Division's research bonus: this is always 1. Currently there is not any research that increases the sales bonus.
- `MarkupMultiplier`: initialize with 1. - `MarkupMultiplier`: initialize with 1.
- `SellingPrice` is the selling price that you set. - `SellingPrice` is the selling price that you set.
- With materials, if we set `SellingPrice` to 0, `MarkupMultiplier` is $10^{12}$ (check the formula below). Extremely high `MarkupMultiplier` means that we can sell all units, regardless of other factors. This is the fastest way to discard materials. - If we set `SellingPrice` to 0 or a negative number, `MarkupMultiplier` is $10^{12}$ (check the formula below). With an extremely high `MarkupMultiplier`, we can sell all units, regardless of other factors. This is the fastest way to discard stored units.
- If `(SellingPrice > MarketPrice + MarkupLimit)`: - If `(SellingPrice > MarketPrice)`:
$$MarkupMultiplier = \left(\frac{MarkupLimit}{SellingPrice - MarketPrice}\right)^{2}$$ - If `(SellingPrice > MarketPrice + MarkupLimit)`:
- If item is material and `SellingPrice` is less than `MarketPrice`: $$MarkupMultiplier = \left(\frac{MarkupLimit}{SellingPrice - MarketPrice}\right)^{2}$$
- If `(SellingPrice <= MarketPrice)`:
$$MarkupMultiplier = \begin{cases}\frac{MarketPrice}{SellingPrice}, & SellingPrice > 0 \land SellingPrice < MarketPrice \newline 10^{12}, & SellingPrice \leq 0 \end{cases}$$ $$MarkupMultiplier = \begin{cases}\frac{MarketPrice}{SellingPrice}, & SellingPrice > 0 \newline 10^{12}, & SellingPrice \leq 0 \end{cases}$$
## Optimal selling price ## Optimal selling price

@ -75,7 +75,7 @@ Calling the `grow` function in a script will also increase security level of the
These actions will make it harder for you to hack the [server](servers.md), and decrease the amount of money you can steal. These actions will make it harder for you to hack the [server](servers.md), and decrease the amount of money you can steal.
You can lower a [server](servers.md)'s security level in a script using the `weaken` function. You can lower a [server](servers.md)'s security level in a script using the `weaken` function.
This means that a [server](servers.md)'s security level will not fall below this value if you are trying to `weaken` it. Each server has a minimum security level. The [server](servers.md)'s security level will not fall below this value if you try to `weaken` it. You can get this value with the `getServerMinSecurityLevel` function.
## Backdoors ## Backdoors

File diff suppressed because it is too large Load Diff

@ -0,0 +1,659 @@
# Changelog - Legacy v1
## v1.6.3 - 2022-04-01 Few stanek fixes
Stanek Gift
- Has a minimum size of 2x3
- Active Fragment property 'avgCharge' renamed to 'highestCharge'
- Formula for fragment effect updated to make 561% more sense.
Now you can charge to your heart content.
- Logs for the 'chargeFragment' function updated.
Misc.
- Nerf noodle bar.
## v1.6.0 - 2022-03-29 Grafting
** Vitalife secret lab **
- A new mechanic called Augmentation Grafting has been added. Resleeving has been removed.
- Credit to @violet for her incredible work.
** Stanek **
- BREAKING: Many functions in the stanek API were renamed in order to avoid name collision with things like Map.prototype.get
** UI **
- Major update to Sleeve, Gang UI, and Create Program (@violet)
- re-add pre tags to support slash n in prompt (@jacktose)
- Tabelize linked output of 'ls' (@Master-Guy)
- Add the ability to filter open scripts (@phyzical)
- Add minHeight to editor tabs (@violet)
- Properly expand gang equipment cards to fill entire screen (@violet)
- Add shortcut to Faction augmentations page from FactionsRoot (@violet)
- Fix extra space on editor tabs (@violet)
- Present offline message as list (@DSteve595)
- add box showing remaining augments per faction (@jjayeon)
- Add tab switching support to vim mode (@JParisFerrer)
- Show current task on gang management screen (@zeddrak)
- Fix for ui of gang members current task when set via api (@phyzical)
- Don't hide irrelevant materials if their stock is not empty and hide irrelevant divisions from Export (@SagePtr)
- Fix regex to enable alpha transparency hex codes (8 digits) (@surdaft)
** API **
- Added dark web functions to ns api
- BREAKING: purchaseTor() should returns true if player already has Tor. (@DavidGrinberg, @waffleattack)
- Implement getBonusTime in Corporation API (@t-wolfeadam)
- Added functions to purchase TIX and WSI (@incubusnb)
- purchaseSleeveAug checks shock value (@incubusnb)
- Fix bug with hacknet api
- Fix spendHashes bug
- Added 0 cost of asleep() (@Master-Guy)
- Fix some misleading corporation errors (@TheRealMaxion)
- expose the inBladeburner on the player object (@phyzical)
- added ram charge for stanek width and height (@phyzical)
- Fix sufficient player money check to buy back shares. (@ChrissiQ)
- Fix Static Ram Circumventing for some NS functions (@CrafterKolyan)
- added CorporationSoftCap to NetscriptDefinitions (@phyzical)
- Added definition of autocomplete() 'data' argument. (@tigercat2000)
- Adding support for text/select options in Prompt command (@PhilipArmstead)
- Added the ability to exportGame via api (@phyzical)
** Arcade **
- Added an arcade to New Tokyo where you can play a 4 year old version of bitburner.
** Misc. **
- Add a warning triggered while auto-saves are off. (@MartinFournier)
- Log info for field analysis now displays actual rank gained. (@ApamNapat)
- Removed BladeburnerSkillCost from skill point cost description. (@ApamNapat)
- Fix handling for UpArrow in bladeburner console. (@dowinter)
- Add GitHub action to check PRs for generated files. (@MartinFournier)
- Cap Staneks gift at 25x25 to prevent crashes. (@waffleattack)
- Remove old & unused files from repository. (@MartinFournier)
- Factions on the factions screens are sorted by story progress / type. (@phyzical)
- Fix log manager not picking up new runs of scripts. (@phyzical)
- Added prettier to cicd.
- UI improvements (@phyzical)
- Documentation / Typos (@nanogyth, @Master-Guy, @incubusnb, @ApamNapat, @phyzical, @SagePtr)
- Give player code a copy of Division.upgrades instead of the live object (@Ornedan)
- Fix bug with small town achievement.
- Fix bug with purchaseSleeveAug (@phyzical)
- Check before unlocking corp upgrade (@gianfun)
- General codebase improvements. (@phyzical, @Master-Guy, @ApamNapat)
- Waiting on promises in NS1 no longer freezes the script. (@Master-Guy)
- Fix bug with missing ramcost for tFormat (@TheMas3212)
- Fix crash with new prompt
- Quick fix to prevent division by 0 in terminal (@Master-Guy)
- removed ip references (@phyzical, @Master-Guy)
- Terminal now supports 'ls -l'
- Fix negative number formatting (@Master-Guy)
- Fix unique ip generation (@InDieTasten)
- remove terminal command theme from docs (@phyzical)
- Fix 'Augmentations Left' with gang factions (@violet)
- Attempt to fix 'bladeburner.process()' early routing issue (@MartinFournier)
- work in progress augment fix (@phyzical)
- Fixes missing space in Smart Supply (@TheRealMaxion)
- Change license to Apache 2 with Commons Clause
- updated regex sanitization (@mbrannen)
- Sleeve fix for when faction isnt found (@phyzical)
- Fix editor "close" naming (@phyzical)
- Fix bug with sleeves where some factions would be listed as workable. (@phyzical)
- Fix research tree of product industries post-prestige (@pd)
- Added a check for exisiting industry type before expanding (@phyzical)
- fix hackAnalyzeThreads returning infinity (@chrisrabe)
- Make growthAnalyze more accurate (@dwRchyngqxs)
- Add 'Zoom -> Reset Zoom' command to Steam (@smolgumball)
- Add hasOwnProperty check to GetServer (@SagePtr)
- Speed up employee productivity calculation (@pd)
- Field Work and Security Work benefit from 'share' (@SagePtr)
- Nerf noodle bar.
## v1.5.0 - Steam Cloud integration
** Steam Cloud Saving **
- Added support for steam cloud saving (@MartinFournier)
** UI **
- background now matches game primary color (@violet)
- page title contains version (@MartinFourier)
- Major text editor improvements (@violet)
- Display bonus time on sleeve page (@MartinFourier)
- Several UI improvements (@violet, @smolgumball, @DrCuriosity, @phyzical)
- Fix aug display in alpha (@Dominik Winter)
- Fix display of corporation product equation (@SagePtr)
- Make Bitverse more accessible (@ChrissiQ)
- Make corporation warehouse more accessible (@ChrissiQ)
- Make tab style more consistent (@violet)
** Netscript **
- Fix bug with async.
- Add 'printf' ns function (@Ninetailed)
- Remove blob caching.
- Fix formulas access check (@Ornedan)
- Fix bug in exp calculation (@qcorradi)
- Fix NaN comparison (@qcorradi)
- Fix travelToCity with bad argument (@SlyCedix)
- Fix bug where augs could not be purchased via sing (@reacocard)
- Fix rounding error in donateToFaction (@Risenafis)
- Fix bug with weakenAnalyze (@rhobes)
- Prevent exploit with atExit (@Ornedan)
- Double 'share' power
** Corporations **
- Fix bugs with corp API (@pigalot)
- Add smart supply func to corp API (@pd)
** Misc. **
- The file API now allows GET and DELETE (@lordducky)
- Force achievement calculation on BN completion (@SagePtr)
- Cleanup in repository (@MartinFourier)
- Several improvements to the electron version (@MartinFourier)
- Fix bug with casino roulette (@jamie-mac)
- Terminal history persists in savefile (@MartinFourier)
- Fix tests (@jamie-mac)
- Fix crash with electron windows tracker (@smolgumball)
- Fix BN6/7 passive reputation gain (@BrianLDev)
- Fix Sleeve not resetting on install (@waffleattack)
- Sort joined factions (@jjayeon)
- Update documentation / typo (@lethern, @Meowdoleon, @JohnnyUrosevic, @JosephDavidTalbot,
@pd, @lethern, @lordducky, @zeddrak, @fearnlj01, @reasonablytall, @MatthewTh0,
@SagePtr, @manniL, @Jedimaster4559, @loganville, @Arrow2thekn33, @wdpk, @fwolfst,
@fschoenfeldt, @Waladil, @AdamTReineke, @citrusmunch, @factubsio, @ashtongreen,
@ChrissiQ, @DJ-Laser, @waffleattack, @ApamNapat, @CrafterKolyan, @DSteve595)
- Nerf noodle bar.
## v1.4.0 - 2022-01-18 Sharing is caring
** Computer sharing **
- A new mechanic has been added, it's is invoked by calling the new function 'share'.
This mechanic helps you farm reputation faster.
** gang **
- Installing augs means losing a little bit of ascension multipliers.
** Misc. **
- Prevent gang API from performing actions for the type of gang they are not. (@TheMas3212)
- Fix donation to gang faction. (@TheMas3212)
- Fix gang check crashing the game. (@TheMas3212)
- Make time compression more robust.
- Fix bug with scp.
- Add zoom to steam version. (@MartinFourier)
- Fix donateToFaction accepts donation of NaN. (@woody-lam-cwl)
- Show correct hash capacity gain on cache level upgrade tooltip. (@woody-lam-cwl)
- Fix tests (@woody-lam-cwl)
- Fix cache tooltip (@woody-lam-cwl)
- Added script to prettify save file for debugging (@MartinFourier)
- Update documentation / typos (@theit8514, @thadguidry, @tigercat2000, @SlyCedix, @Spacejoker, @KenJohansson,
@Ornedan, @JustAnOkapi, @violet, @philarmstead, @TheMas3212, @dcragusa, @XxKingsxX-Pinu,
@paiv, @smolgumball, @zeddrak, @stinky-lizard, @violet, @Feodoric, @daanflore,
@markusariliu, @mstruebing, @erplsf, @waffleattack, @Dexalt142, @AIT-OLPE, @deathly809, @BuckAMayzing,
@MartinFourier, @pigalot, @lethern)
- Fix BN3+ achievement (@SagePtr)
- Fix reputation carry over bug (@TheMas3212)
- Add button to exit infiltrations (@TheMas3212)
- Add dev menu achievement check (@TheMas3212)
- Add 'host' config for electron server (@MartinFourier)
- Suppress save toast only works for autosave (@MartinFourier)
- Fix some achievements not triggering with 'backdoor' (@SagePtr)
- Update Neuroflux Governor description.
- Fix bug with electron server.
- Fix bug with corporation employee assignment function (@Ornedan)
- Add detailed information to terminal 'mem' command (@MartinFourier)
- Add savestamp to savefile (@MartinFourier)
- Dev menu can apply export bonus (@MartinFourier)
- Icarus message no longer applies on top of itself (@Feodoric)
- purchase augment via API can no longer buy Neuroflux when it shouldn't (@Feodoric)
- Syntax highlighter should be smarter (@neuralsim)
- Fix some miscalculation when calculating money stolen (@zeddrak)
- Fix max cache achievement working with 0 cache (@MartinFourier)
- Add achievements in the game, not just steam (@MartinFourier)
- Overflow hash converts to money automatically (@MartinFourier)
- Make mathjax load locally (@MartinFourier)
- Make favor calculation more efficient (@kittycat2002)
- Fix some scripts crashing the game on startup (@MartinFourier)
- Toasts will appear above tail window (@MartinFourier)
- Fix issue that can cause terminal actions to start on one server and end on another (@MartinFourier)
- Fix 'fileExists' not correctly matching file names (@TheMas3212)
- Refactor some code to be more efficient (@TheMas3212)
- Fix exp gain for terminal grow and weaken (@violet)
- Refactor script death code to reject waiting promises instead of resolving (@Ornedan)
- HP recalculates on defense exp gain (@TheMas3212)
- Fix log for ascendMember (@TheMas3212)
- Netscript ports clear on reset (@TheMas3212)
- Fix bug related to company (@TheMas3212)
- Fix bug where corporation handbook would not be correctly added (@TheMas3212)
- Servers in hash upgrades are sorted alpha (@MartinFourier)
- Fix very old save not properly migrating augmentation renamed in 0.56 (@MartinFourier)
- Add font height and line height in theme settings (@MartinFourier)
- Fix crash when quitting job (@MartinFourier)
- Added save file validation system (@TheMas3212)
- React and ReactDOM are now global objects (@pigalot)
- 'nano' supports globs (@smolgumball)
- Character overview can be dragged (@MartinFourier)
- Job page updates in real time (@violet)
- Company favor gain uses the same calculation as faction, this is just performance
the value didn't change (@violet)
- ns2 files work with more import options (@theit8514)
- Allow autocomplete for partial executables (@violet)
- Add support for contract completion (@violet)
- 'ls' link are clickable (@smolgumball)
- Prevent steam from opening external LOCAL files (@MartinFourier)
- Fix a bug with autocomplete (@Feodoric)
- Optimise achievement checks (@Feodoric)
- Hacknet server achievements grant associated hacknet node achievement (@Feodoric)
- Fix display bug with hacknet (@Feodoric)
- 'analyze' now says if the server is backdoored (@deathly809)
- Add option to exclude running script from save (@MartinFourier)
- Game now catches more errors and redirects to recovery page (@MartinFourier)
- Fix bug with autocomplete (@violet)
- Add tooltip to unfocus work (@violet)
- Add detailst overview (@MartinFourier)
- Fix focus bug (@deathly809)
- Fix some NaN handling (@deathly809)
- Added 'mv' ns function (@deathly809)
- Add focus argument to some singularity functions (@violet)
- Fix some functions not disabling log correctly (@deathly809)
- General UI improvements (@violet)
- Handle steamworks errors gravefully (@MartinFourier)
- Fix some react component not unmounting correctly (@MartinFourier)
- 'help' autocompletes (@violet)
- No longer push all achievements to steam (@Ornedan)
- Recovery page has more information (@MartinFourier)
- Added 'getGameInfo' ns function (@MartinFourier)
- SF3.3 unlocks all corp API (@pigalot)
- Major improvements to corp API (@pigalot)
- Prevent seed money outside BN3 (@pigalot)
- Fix bug where using keyboard shortcuts would crash if the feature is not available (@MartinFourier)\
- Sidebar remains opened/closed on save (@MartinFourier)
- Added tooltip to sidebar when closed (@MartinFourier)
- Fix bug where Formulas.exe is not available when starting BN5 (@TheMas3212)
- Fix CI (@tvanderpol)
- Change shortcuts to match sidebar (@MartinFourier)
- Format gang respect (@attrib)
- Add modal to text editor with ram details (@violet)
- Fix several bugs with singularity focus (@violet)
- Nerf noodle bar.
## v1.3.0 - 2022-01-04 Cleaning up
** External IDE integration **
- The Steam version has a webserver that allows integration with external IDEs.
A VSCode extension is available on the market place. (The documentation for the ext. isn't
written yet)
** Source-Files **
- SF4 has been reworked.
- New SF -1.
** UI **
- Fix some edge case with skill bat tooltips (@MartinFournier)
- Made some background match theme color (@Kejikus)
- Fix problem with script editor height not adjusting correctly (@billyvg)
- Fix some formatting issues with Bladeburner (@MartinFournier, @violet)
- Fix some functions like 'alert' format messages better (@MageKing17)
- Many community themes added.
- New script editor theme (@Hedrauta, @Dexalt142)
- Improvements to tail windows (@theit8514)
- Training is more consise (@mikomyazaki)
- Fix Investopedia not displaying properly (@JotaroS)
- Remove alpha from theme editor (@MartinFournier)
- Fix corporation tooltip not displaying properly (@MartinFournier)
- Add tooltip on backdoored location names (@MartinFournier)
- Allow toasts to be dismissed by clicking them (@violet)
- Darkweb item listing now shows what you own. (@hexnaught)
** Bug fix **
- Fix unit tests (@MartinFournier)
- Fixed issue with 'cat' and 'read' not finding foldered files (@Nick-Colclasure)
- Buying on the dark web will remove incomplete exe (@hexnaught)
- Fix bug that would cause the game to crash trying to go to a job without a job (@hexnaught)
- purchaseServer validation (@violet)
- Script Editor focuses code when changing tab (@MartinFournier)
- Fix script editor for .txt files (@65-7a)
- Fix 'buy' command not displaying correctly. (@hexnaught)
- Fix hackAnalyzeThread returning NaN (@mikomyazaki)
- Electron handles exceptions better (@MageKing17)
- Electron will handle 'unresponsive' event and present the opportunity to reload the game with no scripts (@MartinFournier)
- Fix 'cp' between folders (@theit8514)
- Fix throwing null/undefined errors (@violet)
- Allow shortcuts to work when unfocused (@MageKing17)
- Fix some dependency issue (@locriacyber)
- Fix corporation state returning an object instead of a string (@antonvmironov)
- Fix 'mv' overwriting files (@theit8514)
- Fix joesguns not being influenced by hack/grow (@dou867, @MartinFournier)
- Added warning when opening external links. (@MartinFournier)
- Prevent applying for positions that aren't offered (@TheMas3212)
- Import has validation (@MartinFournier)
** Misc. **
- Added vim mode to script editor (@billyvg)
- Clean up script editor code (@Rez855)
- 'cat' works on scripts (@65-7a)
- Add wordWrap for Monaco (@MartinFournier)
- Include map bundles in electron for easier debugging (@MartinFournier)
- Fix importing very large files (@MartinFournier)
- Cache program blob, reducing ram usage of the game (@theit8514)
- Dev menu can set server to \$0 (@mikomyazaki)
- 'backdoor' allows direct connect (@mikomyazaki)
- Github workflow work (@MartinFournier)
- workForFaction / workForCompany have a new parameter (@theit8514)
- Alias accept single quotes (@sporkwitch, @FaintSpeaker)
- Add grep options to 'ps' (@maxtimum)
- Added buy all option to 'buy' (@anthonydroberts)
- Added more shortcuts to terminal input (@Frank-py)
- Refactor some port code (@ErzengelLichtes)
- Settings to control GiB vs GB (@ErzengelLichtes)
- Add electron option to export save game (@MartinFournier)
- Electron improvements (@MartinFournier)
- Expose some notifications functions to electron (@MartinFournier)
- Documentation (@MartinFournier, @cyn, @millennIumAMbiguity, @2PacIsAlive,
@TheCoderJT, @hexnaught, @sschmidTU, @FOLLGAD, @Hedrauta, @Xynrati,
@mikomyazaki, @Icehawk78, @aaronransley, @TheMas3212, @Hedrauta, @alkemann,
@ReeseJones, @amclark42, @thadguidry, @jasonhaxstuff, @pan-kuleczka, @jhollowe,
@ApatheticsAnonymous, @erplsf, @daanflore, @violet, @Kebap, @smolgumball,
@woody-lam-cwl)
## v1.1.0 - 2021-12-18 You guys are awesome (community because they're god damn awesome)
** Script Editor **
- The text editor can open several files at once. (@Rez855 / @Shadow72)
It's not perfect so keep the feedback coming.
** Steam **
- Windows has a new launch option that lets player start with killing all their scripts
This is a safety net in case all the other safety nets fail.
- Linux has several launch options that use different flags for different OS.
- Debug and Fullscreen are available in the window utility bar.
- Tried (and maybe failed) to make the game completely kill itself after closing.
This one I still don't know wtf is going.
- No longer has background throttling.
- Default color should be pitch black when loading
- Add BN13: Challenge achievement.
** Tutorial **
- I watched someone play bitburner on youtube and reworked part of
the tutorial to try to make some parts of the game clearer.
https://www.youtube.com/watch?v=-_JETXff4Zo
- Add option to restart tutorial.
** Netscript **
- getGangInformation returns more information.
- getAscensionResult added
- getMemberInformation returns more info
- Formulas API has new functions for gang.
- Added documentation for corp API.
- exec has clearer error message when you send invalid data.
- getServer returns all defined field for hacknet servers.
- Fix a bug with scp multiple files (@theit8514)
- Stack traces should be smarter at replacing blobs with filenames
- Fix a weird error message that would occur when throwing raw strings.
- Fix shortcuts not working.
- Re-added setFocus and isFocused (@theit8514)
- new function getHashUpgrades (@MartinFournier)
- enableLog accepts "ALL" like disableLog (@wynro)
- toast() doesn't crash on invalid data (@ivanjermakov)
- alert() doesn't crash on invalid data (@Siern)
- Fixed an issue where scripts don't run where they should.
- Sleeve getInformation now returns cha
- getServer does work with no argument now
- workForFaction returns false when it mistakenly returned null
** Character Overview **
- The character overview now shows the amount of exp needed to next level (@MartinFournier)
** Misc. **
- Add option to supress Game Saved! toasts (@MartinFournier)
- Fix bug where ctrl+alt+j was eaten by the wrong process. (@billyvg)
- Theme Editor lets you paste colors (@MartinFournier)
- ctrl + u/k/w should work on terminal (@billyvg)
- Game now shows commit number, this is mostly for me. (@MartinFourier)
- running a bad script will give a clearer error message (@TheCoderJT)
- Default terminal capacity is maximum (@SayntGarmo)
- Fix problems with cp and mv (@theit8514)
- Make monaco load fully offline for players behind firewalls.
- change beginer guide to use n00dles instead of foodnstuff
- BN13 is harder
- nerf int gain from manualHack
- Fix UI displaying wrong stats (@DJMatch3000)
- Fix button not disabling as it should.
- New location in Ishima.
- Add setting to suppress stock market popups.
- Typo fixes (@Hedrauta, @cvr-119, @Ationi, @millennIumAMbiguity
@TealKoi, @TheCoderJT, @cblte, @2PacIsAlive, @MageKing17,
@Xynrati, @Adraxas, @pobiega)
- Fix 100% territory achievement.
- Reword message on active scripts page.
- Fix terminal not clearing after BN
- Remove references to .fconf
- Augmentation pages shows BN difficulty with SF5
- Fix scripts saving on wrong server while 'connect'ing
- Fix gym discount not working.
- Fix scan-analyze not working with timestamps
- Hash upgrades remember last choice.
- Save files now sort by date
- The covenant no longer supports negative memory purchases
- Fix corp shares buyback triggering by pressing enter
- Staneks gift display avg / num charges
- Infiltration rewards no longer decay with better stats
- terminal 'true' is parsed as boolean not string
- tail and kill use autocomplete()
- Fix focus for coding contract
- massive boost to noodle bar.
** Special Thanks **
- Special thank you to everyone on Discord who can answer
new player questions so I can focus on more important things.
## v1.1.0 - 2021-12-03 BN13: They're Lunatics (hydroflame & community)
** BN13: They're Lunatics **
- BN13 added.
** Steam **
- Tested on all 3 major OS.
- 94 achievements added
- Release is 2021-12-10.
** Corporation API **
- Added corporation API. (Unstable)
** Netscript **
- tprintf crashes when not giving a format as first arg.
- tprintf no longer prints filename (@BartKoppelmans)
- TIX buy/sell/sellShort all return askprice/bidprice (@Insight)
- getRunningScript now works.
- Fix disableLog for gang and TIX API
- getOwnedSourceFiles is not singularity anymore (makes it easier to share scripts.) (@theit8514)
- true/false is a valid value to send to other scripts.
- workForFaction no longer returns null when trying to work for gang.
- Scripts logging no longer generates the string if logging is disabled.
This should give performance boost for some scripts.
** Gang **
- Gang with 0 territory can no longer fight
- Territory now caps at exactly 0 or 1.
** Misc. **
- Clicking "previous" on the browser will not pretend you had unsaved information
allowing you to cancel if needs be.
- Fixed some tail box coloring issue.
- Fixed BladeBurner getCityCommunities ram cost
- The download terminal command no longer duplicate extensions (@Insight)
- Fix #000 on #000 text in blackjack. (@Insight)
- Remove reference to .fconf
- Tail boxes all die on soft reset.
- Fix codign contract focus bug.
- Megacorp factions simply re-invite you instead of auto added on reset. (@theit8514)
- Tail window is bound to html body.
- Infiltration reward is tied to your potential stats, not your actual stats
So you won't lose reward for doing the same thing over and over.
- intelligence lowers program creation requirements.
- Terminal parses true as the boolean, not the string.
- Tail and kill autocomplete using the ns2 autocomplete feature.
- scan-analyze doesn't take up as many terminal entries.
- GangOtherInfo documentation now renders correctly.
- ActiveScripts search box also searches for script names.
- Infinite money no longer allows for infinite hacknet server.
- Blackjack doesn't make you lose money twice.
- Recent Scripts is now from most to least recent.
- Fix mathjax ascii art bug in NiteSec.
- Remove warning that the theme editor is slow, it's only slow in dev mode.
- In BN8 is it possible to reduce the money on a server without gaining any.
- In the options, the timestamp feature has a placeholder explaining the expected format.
- Bunch of doc typo fix. (hydroflame & @BartKoppelmans & @cvr-119)
- nerf noodle bar
## v1.0.2 - 2021-11-17 It's the little things (hydroflame)
** Breaking (very small I promise!) **
- buy / sell now return getAskPrice / getBidPrice instead of just price.
This should help solve some inconsistencies.
** Misc. **
- scripts logs are colorized. Start your log with SUCCESS, ERROR, FAIL, WARN, INFO.
- documentation for scp not say string | string[]
- Donation link updated.
- nerf noodle bar
## v1.0.1 - 2021-11-17 New documentation (hydroflame)
** Documentation **
- The new documentation for the netscript API is available at
https://github.com/bitburner-official/bitburner-src/blob/stable/markdown/bitburner.ns.md
This documentation is used in-game to validate the code, in-editor to autocomplete, and
for users to reference. This is a huge quality of life improvements for me.
** Reputation **
- Fixed favor not affecting faction work reputation gain (Yeah, I know right?)
** Hacknet **
- Servers are now considerd "purchasedByPlayers"
** Script Editor **
- solarized themes now work.
** Corporation **
- Dividends are now much more taxed.
- The 2 upgrades that reduced taxes are now much stronger.
** Misc. **
- Starting / Stopping scripts on hashnet servers immediately updates their hash rate (instead of on the next tick)
- Hacknet has tooltip showing what the result of the upgrade would be.
- Augmentations page displayes current price multiplier as well as explains the mechanic.
- Terminal now is 25x stronger.
- Tail boxes use pre-wrap for it's lines.
- Tail boxes allow you to rerun dead scripts.
- Tail boxes can no longer open the same one twice.
- Terminal now autocompletes through aliases.
- Make alter reality harder.
- Fix bladeburner cancelling actions when manually starting anything with Simulacrum.
- Buying hash upgrade to increase uni class or gym training will apply to current class.
- Internally the game no longer uses the decimal library.
- Fix an issue where 'download \*' would generate weird windows files.
- Timestamps can be set to any format in the options.
- Fix typo in documentation share popup.
- Remove bunch of debug log.
- Fix typo in corporation handbook literature.
- Fix typo in documentation
- Fix duplicate SF -1 exploit. (Yeah, an exploit of exploits, now were meta)
- Fix offline hacking earning being attributed to hacknet.
- nerf noodle bar
## v1.0.0 - 2021-11-10 Breaking the API :( (blame hydroflame)
** Announcement **
- Several API breaks have been implemented.
- See the v1.0.0 migration guide under Documentation
- Everyone gets 10 free neuroflux level.
** Netscript **
- Fix a bug that would cause RAM to not get recalculated.
- New function: hackAnalyzeSecurity
- New function: growthAnalyzeSecurity
- New function: weakenAnalyze
** Script Editor **
- Sometimes warn you about unawaited infinite loops.
- ns1 functions are now correctly colors in Monokai.
** Programs **
- Formulas.exe is a new program that lets you use the formulas API.
** Corporations **
- Real Estate takes up a tiny bit of room.
- Dividends are now taxes exponentially in certain bitnodes.
- UI displays how many level of each corporation upgrade.
- Fix exploit with going public.
- Employee salary no longer increase.
** Documentation **
- The documentation is now autogenerated into .md files.
It is usable but not yet linked to readthedocs. It's on github.
** Misc. **
- Favor is not internall floating point. Meaning I don't have to save an extra variable.
- Manually starting a Bladeburner action cancels unfocused action.
- Updated description of gang territory to be clearer.
- Hacknet expenses and profit are in different categories.
- Fixed favor equation.
- Toast messages aren't hidden behind work in progress screen.
- Fix bug that made infiltration checkmark look off by one.
- Fix some inconsistency with running files that start or don't start with /
- Can't tail the same window twice.
- Added recovery mode. Hopefully no one will ever have to use it.
- Fix readthedocs
- Programs now give int exp based on time not program.
- Many sing. functions now give int exp.
- Active Scripts page now displays some arguments next to script name.
- Fixed some invisible black text.
- Button colors can be edited.
- Added 2 new colors in the theme editor: background primary and background secondary.
- infiltration uses key instead of keycode so it should work better on non-american keyboards.
- buff noodle bar.

File diff suppressed because it is too large Load Diff

@ -48,6 +48,8 @@
- [Game Frozen or Stuck?](programming/game_frozen.md) - [Game Frozen or Stuck?](programming/game_frozen.md)
- [Tools & Resources](help/tools_and_resources.md) - [Tools & Resources](help/tools_and_resources.md)
- [Changelog](changelog.md) - [Changelog](changelog.md)
- [Changelog - Legacy v1](changelog-v1.md)
- [Changelog - Legacy v0](changelog-v0.md)
## Migration ## Migration

@ -45,19 +45,21 @@ import file42 from "!!raw-loader!./doc/basic/stats.md";
import file43 from "!!raw-loader!./doc/basic/stockmarket.md"; import file43 from "!!raw-loader!./doc/basic/stockmarket.md";
import file44 from "!!raw-loader!./doc/basic/terminal.md"; import file44 from "!!raw-loader!./doc/basic/terminal.md";
import file45 from "!!raw-loader!./doc/basic/world.md"; import file45 from "!!raw-loader!./doc/basic/world.md";
import file46 from "!!raw-loader!./doc/changelog.md"; import file46 from "!!raw-loader!./doc/changelog-v0.md";
import file47 from "!!raw-loader!./doc/help/bitnode_order.md"; import file47 from "!!raw-loader!./doc/changelog-v1.md";
import file48 from "!!raw-loader!./doc/help/getting_started.md"; import file48 from "!!raw-loader!./doc/changelog.md";
import file49 from "!!raw-loader!./doc/help/tools_and_resources.md"; import file49 from "!!raw-loader!./doc/help/bitnode_order.md";
import file50 from "!!raw-loader!./doc/index.md"; import file50 from "!!raw-loader!./doc/help/getting_started.md";
import file51 from "!!raw-loader!./doc/migrations/ns2.md"; import file51 from "!!raw-loader!./doc/help/tools_and_resources.md";
import file52 from "!!raw-loader!./doc/migrations/v1.md"; import file52 from "!!raw-loader!./doc/index.md";
import file53 from "!!raw-loader!./doc/migrations/v2.md"; import file53 from "!!raw-loader!./doc/migrations/ns2.md";
import file54 from "!!raw-loader!./doc/programming/game_frozen.md"; import file54 from "!!raw-loader!./doc/migrations/v1.md";
import file55 from "!!raw-loader!./doc/programming/go_algorithms.md"; import file55 from "!!raw-loader!./doc/migrations/v2.md";
import file56 from "!!raw-loader!./doc/programming/hackingalgorithms.md"; import file56 from "!!raw-loader!./doc/programming/game_frozen.md";
import file57 from "!!raw-loader!./doc/programming/learn.md"; import file57 from "!!raw-loader!./doc/programming/go_algorithms.md";
import file58 from "!!raw-loader!./doc/programming/remote_api.md"; import file58 from "!!raw-loader!./doc/programming/hackingalgorithms.md";
import file59 from "!!raw-loader!./doc/programming/learn.md";
import file60 from "!!raw-loader!./doc/programming/remote_api.md";
interface Document { interface Document {
default: string; default: string;
@ -109,16 +111,18 @@ AllPages["basic/stats.md"] = file42;
AllPages["basic/stockmarket.md"] = file43; AllPages["basic/stockmarket.md"] = file43;
AllPages["basic/terminal.md"] = file44; AllPages["basic/terminal.md"] = file44;
AllPages["basic/world.md"] = file45; AllPages["basic/world.md"] = file45;
AllPages["changelog.md"] = file46; AllPages["changelog-v0.md"] = file46;
AllPages["help/bitnode_order.md"] = file47; AllPages["changelog-v1.md"] = file47;
AllPages["help/getting_started.md"] = file48; AllPages["changelog.md"] = file48;
AllPages["help/tools_and_resources.md"] = file49; AllPages["help/bitnode_order.md"] = file49;
AllPages["index.md"] = file50; AllPages["help/getting_started.md"] = file50;
AllPages["migrations/ns2.md"] = file51; AllPages["help/tools_and_resources.md"] = file51;
AllPages["migrations/v1.md"] = file52; AllPages["index.md"] = file52;
AllPages["migrations/v2.md"] = file53; AllPages["migrations/ns2.md"] = file53;
AllPages["programming/game_frozen.md"] = file54; AllPages["migrations/v1.md"] = file54;
AllPages["programming/go_algorithms.md"] = file55; AllPages["migrations/v2.md"] = file55;
AllPages["programming/hackingalgorithms.md"] = file56; AllPages["programming/game_frozen.md"] = file56;
AllPages["programming/learn.md"] = file57; AllPages["programming/go_algorithms.md"] = file57;
AllPages["programming/remote_api.md"] = file58; AllPages["programming/hackingalgorithms.md"] = file58;
AllPages["programming/learn.md"] = file59;
AllPages["programming/remote_api.md"] = file60;

@ -11,8 +11,7 @@ import { Reputation } from "../../ui/React/Reputation";
import { Favor } from "../../ui/React/Favor"; import { Favor } from "../../ui/React/Favor";
import { MathJax } from "better-react-mathjax"; import { MathJax } from "better-react-mathjax";
import makeStyles from "@mui/styles/makeStyles"; import { makeStyles } from "tss-react/mui";
import createStyles from "@mui/styles/createStyles";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
@ -24,14 +23,12 @@ interface IProps {
factionInfo: FactionInfo; factionInfo: FactionInfo;
} }
const useStyles = makeStyles(() => const useStyles = makeStyles()({
createStyles({ noformat: {
noformat: { whiteSpace: "pre-wrap",
whiteSpace: "pre-wrap", lineHeight: "1em",
lineHeight: "1em", },
}, });
}),
);
function DefaultAssignment(): React.ReactElement { function DefaultAssignment(): React.ReactElement {
return ( return (
@ -46,7 +43,7 @@ function DefaultAssignment(): React.ReactElement {
export function Info(props: IProps): React.ReactElement { export function Info(props: IProps): React.ReactElement {
useRerender(200); useRerender(200);
const classes = useStyles(); const { classes } = useStyles();
const Assignment = props.factionInfo.assignment ?? DefaultAssignment; const Assignment = props.factionInfo.assignment ?? DefaultAssignment;

@ -21,7 +21,7 @@ interface IProps {
} }
export function GangMemberStats(props: IProps): React.ReactElement { export function GangMemberStats(props: IProps): React.ReactElement {
const classes = useStyles(); const { classes } = useStyles();
const asc = { const asc = {
hack: props.member.calculateAscensionMult(props.member.hack_asc_points), hack: props.member.calculateAscensionMult(props.member.hack_asc_points),

@ -14,6 +14,7 @@ import { useGang } from "./Context";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { GangConstants } from "../data/Constants";
export function GangStats(): React.ReactElement { export function GangStats(): React.ReactElement {
const gang = useGang(); const gang = useGang();
@ -40,7 +41,10 @@ export function GangStats(): React.ReactElement {
} }
> >
<Typography> <Typography>
Respect: {formatRespect(gang.respect)} ({formatRespect(5 * gang.respectGainRate)} / sec) Respect: {formatRespect(gang.respect)} ({formatRespect(5 * gang.respectGainRate)} / sec){" "}
{gang.storedCycles > 2 * GangConstants.maxCyclesToProcess
? `[Effective Gain: ${formatRespect(5 * gang.respectGainRate * GangConstants.maxCyclesToProcess)} / sec]`
: ""}
</Typography> </Typography>
</Tooltip> </Tooltip>
</Box> </Box>
@ -55,7 +59,10 @@ export function GangStats(): React.ReactElement {
} }
> >
<Typography> <Typography>
Wanted Level: {formatWanted(gang.wanted)} ({formatWanted(5 * gang.wantedGainRate)} / sec) Wanted Level: {formatWanted(gang.wanted)} ({formatWanted(5 * gang.wantedGainRate)} / sec){" "}
{gang.storedCycles > 2 * GangConstants.maxCyclesToProcess
? `[Effective Gain: ${formatWanted(5 * gang.wantedGainRate * GangConstants.maxCyclesToProcess)} / sec]`
: ""}
</Typography> </Typography>
</Tooltip> </Tooltip>
</Box> </Box>
@ -69,7 +76,14 @@ export function GangStats(): React.ReactElement {
</Box> </Box>
<Typography> <Typography>
Money gain rate: <MoneyRate money={5 * gang.moneyGainRate} /> Money gain rate: <MoneyRate money={5 * gang.moneyGainRate} />{" "}
{gang.storedCycles > 2 * GangConstants.maxCyclesToProcess ? "[Effective Gain:" : ""}{" "}
{gang.storedCycles > 2 * GangConstants.maxCyclesToProcess ? (
<MoneyRate money={5 * gang.moneyGainRate * GangConstants.maxCyclesToProcess} />
) : (
""
)}
{gang.storedCycles > 2 * GangConstants.maxCyclesToProcess ? "]" : ""}
</Typography> </Typography>
<Box display="flex"> <Box display="flex">

@ -6,7 +6,8 @@ export const opponentDetails = {
[GoOpponent.none]: { [GoOpponent.none]: {
komi: 5.5, komi: 5.5,
description: "Practice Board", description: "Practice Board",
flavorText: "Practice on a subnet where you place both colors of routers.", flavorText:
"Practice on a subnet where you place both colors of routers, or play as white against your IPvGO script.",
bonusDescription: "", bonusDescription: "",
bonusPower: 0, bonusPower: 0,
}, },
@ -67,7 +68,7 @@ export const opponentDetails = {
}, },
}; };
export const boardSizes = [5, 7, 9, 13]; export const boardSizes = [5, 7, 9, 13, 19];
export const columnIndexes = "ABCDEFGHJKLMNOPQRSTUVWXYZ"; export const columnIndexes = "ABCDEFGHJKLMNOPQRSTUVWXYZ";

@ -11,6 +11,7 @@ export class GoObject {
currentGame: BoardState = getNewBoardState(7); currentGame: BoardState = getNewBoardState(7);
stats: PartialRecord<GoOpponent, OpponentStats> = {}; stats: PartialRecord<GoOpponent, OpponentStats> = {};
nextTurn: Promise<Play> = Promise.resolve({ type: GoPlayType.gameOver, x: null, y: null }); nextTurn: Promise<Play> = Promise.resolve({ type: GoPlayType.gameOver, x: null, y: null });
storedCycles: number = 0;
prestigeAugmentation() { prestigeAugmentation() {
for (const stats of getRecordValues(this.stats)) { for (const stats of getRecordValues(this.stats)) {
@ -24,6 +25,16 @@ export class GoObject {
this.currentGame = getNewBoardState(7); this.currentGame = getNewBoardState(7);
this.stats = {}; this.stats = {};
} }
/**
* Stores offline time that is consumed to speed up the AI.
* Only stores offline time if the player has actually been using the mechanic.
*/
storeCycles(offlineCycles: number) {
if (this.previousGame) {
this.storedCycles += offlineCycles ?? 0;
}
}
} }
export const Go = new GoObject(); export const Go = new GoObject();

@ -21,6 +21,7 @@ type SaveFormat = {
previousGame: PreviousGameSaveData; previousGame: PreviousGameSaveData;
currentGame: CurrentGameSaveData; currentGame: CurrentGameSaveData;
stats: PartialRecord<GoOpponent, OpponentStats>; stats: PartialRecord<GoOpponent, OpponentStats>;
storedCycles: number;
}; };
export function getGoSave(): SaveFormat { export function getGoSave(): SaveFormat {
@ -40,6 +41,7 @@ export function getGoSave(): SaveFormat {
passCount: Go.currentGame.passCount, passCount: Go.currentGame.passCount,
}, },
stats: Go.stats, stats: Go.stats,
storedCycles: Go.storedCycles,
}; };
} }
@ -78,6 +80,7 @@ export function loadGo(data: unknown): boolean {
Go.currentGame = currentGame; Go.currentGame = currentGame;
Go.previousGame = previousGame; Go.previousGame = previousGame;
Go.stats = stats; Go.stats = stats;
Go.storeCycles(loadStoredCycles(parsedData.storedCycles));
// If it's the AI's turn, initiate their turn, which will populate nextTurn // If it's the AI's turn, initiate their turn, which will populate nextTurn
if (currentGame.previousPlayer === GoColor.black && currentGame.ai !== GoOpponent.none) makeAIMove(currentGame); if (currentGame.previousPlayer === GoColor.black && currentGame.ai !== GoOpponent.none) makeAIMove(currentGame);
@ -178,3 +181,11 @@ function loadSimpleBoard(simpleBoard: unknown, requiredSize?: number): SimpleBoa
} }
return simpleBoard; return simpleBoard;
} }
function loadStoredCycles(storedCycles: unknown): number {
if (!storedCycles || isNaN(+storedCycles)) {
return 0;
}
return +storedCycles;
}

@ -37,7 +37,7 @@ export type BoardState = {
board: Board; board: Board;
previousPlayer: GoColor | null; previousPlayer: GoColor | null;
/** The previous board positions as a SimpleBoard */ /** The previous board positions as a SimpleBoard */
previousBoards: SimpleBoard[]; previousBoards: string[];
ai: GoOpponent; ai: GoOpponent;
passCount: number; passCount: number;
cheatCount: number; cheatCount: number;

@ -1,6 +1,6 @@
import type { Board, BoardState, Neighbor, PointState, SimpleBoard } from "../Types"; import type { Board, BoardState, Neighbor, Play, PointState, SimpleBoard } from "../Types";
import { GoValidity, GoOpponent, GoColor } from "@enums"; import { GoValidity, GoOpponent, GoColor, GoPlayType } from "@enums";
import { Go } from "../Go"; import { Go } from "../Go";
import { import {
findAdjacentPointsInChain, findAdjacentPointsInChain,
@ -44,7 +44,7 @@ export function evaluateIfMoveIsValid(boardState: BoardState, x: number, y: numb
} }
// Detect if the move might be an immediate repeat (only one board of history is saved to check) // Detect if the move might be an immediate repeat (only one board of history is saved to check)
const possibleRepeat = boardState.previousBoards.find((board) => getColorOnSimpleBoard(board, x, y) === player); const possibleRepeat = boardState.previousBoards.find((board) => getColorOnBoardString(board, x, y) === player);
if (shortcut) { if (shortcut) {
// If the current point has some adjacent open spaces, it is not suicide. If the move is not repeated, it is legal // If the current point has some adjacent open spaces, it is not suicide. If the move is not repeated, it is legal
@ -86,8 +86,8 @@ export function evaluateIfMoveIsValid(boardState: BoardState, x: number, y: numb
return GoValidity.noSuicide; return GoValidity.noSuicide;
} }
if (possibleRepeat && boardState.previousBoards.length) { if (possibleRepeat && boardState.previousBoards.length) {
const simpleEvalBoard = simpleBoardFromBoard(evaluationBoard); const simpleEvalBoard = boardStringFromBoard(evaluationBoard);
if (boardState.previousBoards.find((board) => areSimpleBoardsIdentical(simpleEvalBoard, board))) { if (boardState.previousBoards.includes(simpleEvalBoard)) {
return GoValidity.boardRepeated; return GoValidity.boardRepeated;
} }
} }
@ -548,7 +548,8 @@ export function findAdjacentLibertiesAndAlliesForPoint(
} }
/** /**
* Retrieves a simplified version of the board state. "X" represents black pieces, "O" white, and "." empty points. * Retrieves a simplified version of the board state.
* "X" represents black pieces, "O" white, "." empty points, and "#" offline nodes.
* *
* For example, a 5x5 board might look like this: * For example, a 5x5 board might look like this:
* ``` * ```
@ -563,14 +564,15 @@ export function findAdjacentLibertiesAndAlliesForPoint(
* *
* Each string represents a vertical column on the board, and each character in the string represents a point. * Each string represents a vertical column on the board, and each character in the string represents a point.
* *
* Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of index [1][0]. * Traditional notation for Go is e.g. "B,1" referring to second ("B") column, first rank. This is the equivalent of
* index (1 * N) + 0 , where N is the size of the board.
* *
* Note that the [0][0] point is shown on the bottom-left on the visual board (as is traditional), and each * Note that index 0 (the [0][0] point) is shown on the bottom-left on the visual board (as is traditional), and each
* string represents a vertical column on the board. In other words, the printed example above can be understood to * string represents a vertical column on the board. In other words, the printed example above can be understood to
* be rotated 90 degrees clockwise compared to the board UI as shown in the IPvGO game. * be rotated 90 degrees clockwise compared to the board UI as shown in the IPvGO game.
* *
*/ */
export function simpleBoardFromBoard(board: Board): string[] { export function simpleBoardFromBoard(board: Board): SimpleBoard {
return board.map((column) => return board.map((column) =>
column.reduce((str, point) => { column.reduce((str, point) => {
if (!point) { if (!point) {
@ -587,6 +589,50 @@ export function simpleBoardFromBoard(board: Board): string[] {
); );
} }
/**
* Returns a string representation of the given board.
* The string representation is the same as simpleBoardFromBoard() but concatenated into a single string
*
* For example, a 5x5 board might look like this:
* ```
* "XX.O.X..OO.XO..XXO...XOO."
* ```
*/
export function boardStringFromBoard(board: Board): string {
return simpleBoardFromBoard(board).join("");
}
/**
* Returns a full board object from a string representation of the board.
* The string representation is the same as simpleBoardFromBoard() but concatenated into a single string
*
* For example, a 5x5 board might look like this:
* ```
* "XX.O.X..OO.XO..XXO...XOO."
* ```
*/
export function boardFromBoardString(boardString: string): Board {
const simpleBoardArray = simpleBoardFromBoardString(boardString);
return boardFromSimpleBoard(simpleBoardArray);
}
/**
* Slices a string representation of a board into an array of strings representing the rows on the board
*/
export function simpleBoardFromBoardString(boardString: string): SimpleBoard {
// Turn the SimpleBoard string into a string array, allowing access of each point via indexes e.g. [0][1]
const boardSize = Math.round(Math.sqrt(boardString.length));
const boardTiles = boardString.split("");
// Split the single board string into rows of length equal to the board width
const simpleBoardArray = Array(boardSize)
.fill("")
.map((_, index) => boardTiles.slice(index * boardSize, (index + 1) * boardSize).join(""));
return simpleBoardArray;
}
/** Creates a board object from a simple board. The resulting board has no analytics (liberties/chains) */ /** Creates a board object from a simple board. The resulting board has no analytics (liberties/chains) */
export function boardFromSimpleBoard(simpleBoard: SimpleBoard): Board { export function boardFromSimpleBoard(simpleBoard: SimpleBoard): Board {
return simpleBoard.map((column, x) => return simpleBoard.map((column, x) =>
@ -624,8 +670,9 @@ export function areSimpleBoardsIdentical(simpleBoard1: SimpleBoard, simpleBoard2
return simpleBoard1.every((column, x) => column === simpleBoard2[x]); return simpleBoard1.every((column, x) => column === simpleBoard2[x]);
} }
export function getColorOnSimpleBoard(simpleBoard: SimpleBoard, x: number, y: number): GoColor | null { export function getColorOnBoardString(boardString: string, x: number, y: number): GoColor | null {
const char = simpleBoard[x]?.[y]; const boardSize = Math.round(Math.sqrt(boardString.length));
const char = boardString[x * boardSize + y];
if (char === "X") return GoColor.black; if (char === "X") return GoColor.black;
if (char === "O") return GoColor.white; if (char === "O") return GoColor.white;
if (char === ".") return GoColor.empty; if (char === ".") return GoColor.empty;
@ -643,7 +690,7 @@ export function getPreviousMove(): [number, number] | null {
const row = Go.currentGame.board[+rowIndexString] ?? []; const row = Go.currentGame.board[+rowIndexString] ?? [];
for (const pointIndexString in row) { for (const pointIndexString in row) {
const point = row[+pointIndexString]; const point = row[+pointIndexString];
const priorColor = point && priorBoard && getColorOnSimpleBoard(priorBoard, point.x, point.y); const priorColor = point && priorBoard && getColorOnBoardString(priorBoard, point.x, point.y);
const currentColor = point?.color; const currentColor = point?.color;
const isPreviousPlayer = currentColor === Go.currentGame.previousPlayer; const isPreviousPlayer = currentColor === Go.currentGame.previousPlayer;
const isChanged = priorColor !== currentColor; const isChanged = priorColor !== currentColor;
@ -655,3 +702,23 @@ export function getPreviousMove(): [number, number] | null {
return null; return null;
} }
/**
* Gets the last move, if it was made by the specified color and is present
*/
export function getPreviousMoveDetails(): Play {
const priorMove = getPreviousMove();
if (priorMove) {
return {
type: GoPlayType.move,
x: priorMove[0],
y: priorMove[1],
};
}
return {
type: !priorMove && Go.currentGame?.passCount ? GoPlayType.pass : GoPlayType.gameOver,
x: null,
y: null,
};
}

@ -1,7 +1,7 @@
import type { Board, BoardState, EyeMove, Move, MoveOptions, Play, PointState } from "../Types"; import type { Board, BoardState, EyeMove, Move, MoveOptions, Play, PointState } from "../Types";
import { Player } from "@player"; import { Player } from "@player";
import { AugmentationName, GoOpponent, GoColor, GoPlayType } from "@enums"; import { AugmentationName, GoColor, GoOpponent, GoPlayType } from "@enums";
import { opponentDetails } from "../Constants"; import { opponentDetails } from "../Constants";
import { findNeighbors, isNotNullish, makeMove, passTurn } from "../boardState/boardState"; import { findNeighbors, isNotNullish, makeMove, passTurn } from "../boardState/boardState";
import { import {
@ -15,54 +15,83 @@ import {
getAllEyesByChainId, getAllEyesByChainId,
getAllNeighboringChains, getAllNeighboringChains,
getAllValidMoves, getAllValidMoves,
getPreviousMoveDetails,
} from "./boardAnalysis"; } from "./boardAnalysis";
import { findDisputedTerritory } from "./controlledTerritory"; import { findDisputedTerritory } from "./controlledTerritory";
import { findAnyMatchedPatterns } from "./patternMatching"; import { findAnyMatchedPatterns } from "./patternMatching";
import { WHRNG } from "../../Casino/RNG"; import { WHRNG } from "../../Casino/RNG";
import { Go, GoEvents } from "../Go"; import { Go, GoEvents } from "../Go";
let currentAITurn: Promise<Play> | null = null; let isAiThinking: boolean = false;
let currentTurnResolver: (() => void) | null = null;
/** /**
* Retrieves a move from the current faction in response to the player's move * Retrieves a move from the current faction in response to the player's move
*/ */
export function makeAIMove(boardState: BoardState): Promise<Play> { export function makeAIMove(boardState: BoardState, useOfflineCycles = true): Promise<Play> {
// If AI is already taking their turn, return the existing turn. // If AI is already taking their turn, return the existing turn.
if (currentAITurn) return currentAITurn; if (isAiThinking) {
currentAITurn = Go.nextTurn = getMove(boardState, GoColor.white, Go.currentGame.ai) return Go.nextTurn;
.then(async (play): Promise<Play> => { }
if (boardState !== Go.currentGame) return play; //Stale game isAiThinking = true;
// Handle AI passing // If the AI is disabled, simply make a promise to be resolved once the player makes a move as white
if (play.type === GoPlayType.pass) { if (boardState.ai === GoOpponent.none) {
passTurn(boardState, GoColor.white); GoEvents.emit();
// if passTurn called endGoGame, or the player has no valid moves left, the move should be shown as a game over // Update currentTurnResolver to call Go.nextTurn's resolve function with the last played move's details
if (boardState.previousPlayer === null || !getAllValidMoves(boardState, GoColor.black).length) { Go.nextTurn = new Promise((resolve) => (currentTurnResolver = () => resolve(getPreviousMoveDetails())));
return { type: GoPlayType.gameOver, x: null, y: null }; }
// If an AI is in use, find the faction's move in response, and resolve the Go.nextTurn promise once it is found and played.
else {
Go.nextTurn = getMove(boardState, GoColor.white, Go.currentGame.ai, useOfflineCycles).then(
async (play): Promise<Play> => {
if (boardState !== Go.currentGame) return play; //Stale game
// Handle AI passing
if (play.type === GoPlayType.pass) {
passTurn(boardState, GoColor.white);
// if passTurn called endGoGame, or the player has no valid moves left, the move should be shown as a game over
if (boardState.previousPlayer === null || !getAllValidMoves(boardState, GoColor.black).length) {
return { type: GoPlayType.gameOver, x: null, y: null };
}
return play;
} }
// Handle AI making a move
await waitCycle(useOfflineCycles);
const aiUpdatedBoard = makeMove(boardState, play.x, play.y, GoColor.white);
// Handle the AI breaking. This shouldn't ever happen.
if (!aiUpdatedBoard) {
boardState.previousPlayer = GoColor.white;
console.error(`Invalid AI move attempted: ${play.x}, ${play.y}. This should not happen.`);
}
return play; return play;
} },
);
}
// Handle AI making a move // Once the AI moves (or the player playing as white with No AI moves),
await sleep(500); // clear the isAiThinking semaphore and update the board UI.
const aiUpdatedBoard = makeMove(boardState, play.x, play.y, GoColor.white); Go.nextTurn = Go.nextTurn.finally(() => {
isAiThinking = false;
// Handle the AI breaking. This shouldn't ever happen. GoEvents.emit();
if (!aiUpdatedBoard) { });
boardState.previousPlayer = GoColor.white;
console.error(`Invalid AI move attempted: ${play.x}, ${play.y}. This should not happen.`);
}
return play;
})
.finally(() => {
currentAITurn = null;
GoEvents.emit();
});
return Go.nextTurn; return Go.nextTurn;
} }
/**
* Resolves the current turn.
* This is used for players manually playing against their script on the no-ai board.
*/
export function resolveCurrentTurn() {
// Call the resolve function on Go.nextTurn, if it exists
currentTurnResolver?.();
currentTurnResolver = null;
}
/* /*
Basic GO AIs, each with some personality and weaknesses Basic GO AIs, each with some personality and weaknesses
@ -85,9 +114,10 @@ export async function getMove(
boardState: BoardState, boardState: BoardState,
player: GoColor, player: GoColor,
opponent: GoOpponent, opponent: GoOpponent,
useOfflineCycles = true,
rngOverride?: number, rngOverride?: number,
): Promise<Play & { type: GoPlayType.move | GoPlayType.pass }> { ): Promise<Play & { type: GoPlayType.move | GoPlayType.pass }> {
await sleep(300); await waitCycle(useOfflineCycles);
const rng = new WHRNG(rngOverride || Player.totalPlaytime); const rng = new WHRNG(rngOverride || Player.totalPlaytime);
const smart = isSmart(opponent, rng.random()); const smart = isSmart(opponent, rng.random());
const moves = getMoveOptions(boardState, player, rng.random(), smart); const moves = getMoveOptions(boardState, player, rng.random(), smart);
@ -115,9 +145,9 @@ export async function getMove(
.filter((point) => evaluateIfMoveIsValid(boardState, point.x, point.y, player, false)); .filter((point) => evaluateIfMoveIsValid(boardState, point.x, point.y, player, false));
const chosenMove = moveOptions[Math.floor(rng.random() * moveOptions.length)]; const chosenMove = moveOptions[Math.floor(rng.random() * moveOptions.length)];
await waitCycle(useOfflineCycles);
if (chosenMove) { if (chosenMove) {
await sleep(200);
//console.debug(`Non-priority move chosen: ${chosenMove.x} ${chosenMove.y}`); //console.debug(`Non-priority move chosen: ${chosenMove.x} ${chosenMove.y}`);
return { type: GoPlayType.move, x: chosenMove.x, y: chosenMove.y }; return { type: GoPlayType.move, x: chosenMove.x, y: chosenMove.y };
} }
@ -759,7 +789,7 @@ function getMoveOptions(
}; };
async function retrieveMoveOption(id: keyof typeof moveOptions): Promise<Move | null> { async function retrieveMoveOption(id: keyof typeof moveOptions): Promise<Move | null> {
await sleep(100); await waitCycle();
if (moveOptions[id] !== undefined) { if (moveOptions[id] !== undefined) {
return moveOptions[id] ?? null; return moveOptions[id] ?? null;
} }
@ -786,6 +816,18 @@ export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }
/**
* Spend some time waiting to allow the UI & CSS to render smoothly
* If bonus time is available, significantly decrease the length of the wait
*/
function waitCycle(useOfflineCycles = true): Promise<void> {
if (useOfflineCycles && Go.storedCycles > 0) {
Go.storedCycles -= 2;
return sleep(40);
}
return sleep(200);
}
export function showWorldDemon() { export function showWorldDemon() {
return Player.hasAugmentation(AugmentationName.TheRedPill, true) && Player.sourceFileLvl(1); return Player.hasAugmentation(AugmentationName.TheRedPill, true) && Player.sourceFileLvl(1);
} }

@ -9,7 +9,7 @@ import {
findLibertiesForChain, findLibertiesForChain,
getAllChains, getAllChains,
boardFromSimpleBoard, boardFromSimpleBoard,
simpleBoardFromBoard, boardStringFromBoard,
} from "../boardAnalysis/boardAnalysis"; } from "../boardAnalysis/boardAnalysis";
import { endGoGame } from "../boardAnalysis/scoring"; import { endGoGame } from "../boardAnalysis/scoring";
import { addObstacles, resetCoordinates, rotate90Degrees } from "./offlineNodes"; import { addObstacles, resetCoordinates, rotate90Degrees } from "./offlineNodes";
@ -89,15 +89,12 @@ export function makeMove(boardState: BoardState, x: number, y: number, player: G
return false; return false;
} }
// Only maintain last 7 moves
boardState.previousBoards.unshift(simpleBoardFromBoard(boardState.board));
if (boardState.previousBoards.length > 7) {
boardState.previousBoards.pop();
}
const point = boardState.board[x][y]; const point = boardState.board[x][y];
if (!point) return false; if (!point) return false;
// Add move to board history
boardState.previousBoards.unshift(boardStringFromBoard(boardState.board));
point.color = player; point.color = player;
boardState.previousPlayer = player; boardState.previousPlayer = player;
boardState.passCount = 0; boardState.passCount = 0;

@ -1,9 +1,31 @@
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles"; import { makeStyles } from "tss-react/mui";
import createStyles from "@mui/styles/createStyles"; import { keyframes } from "tss-react";
export const pointStyle = makeStyles((theme: Theme) => type Size = "fiveByFive" | "sevenBySeven" | "nineByNine" | "thirteenByThirteen" | "nineteenByNineteen";
createStyles({ type Point =
| "blackPoint"
| "whitePoint"
| "innerPoint"
| "filledPoint"
| "emptyPoint"
| "broken"
| "tradStone"
| "priorStoneTrad";
type Structure = "coordinates" | "liberty" | "northLiberty" | "eastLiberty" | "westLiberty" | "southLiberty";
type Highlight = "hover" | "valid" | "priorPoint";
const fadeLoop = keyframes`
0% {
opacity: 0.4;
}
100% {
opacity: 1;
}
`;
export const pointStyle = makeStyles<void, Size | Point | Structure | Highlight>({ uniqId: "pointStyle" })(
(theme: Theme, _, classes) => ({
hover: {}, hover: {},
valid: {}, valid: {},
priorPoint: {}, priorPoint: {},
@ -12,24 +34,24 @@ export const pointStyle = makeStyles((theme: Theme) =>
height: "100%", height: "100%",
width: "100%", width: "100%",
"&$hover$valid:hover $innerPoint": { [`&.${classes.hover}.${classes.valid}:hover .${classes.innerPoint}`]: {
outlineColor: theme.colors.white, outlineColor: theme.colors.white,
}, },
"&$hover$priorPoint $innerPoint": { [`&.${classes.hover}.${classes.priorPoint} .${classes.innerPoint}`]: {
outlineColor: theme.colors.white, outlineColor: theme.colors.white,
}, },
"&$hover$priorPoint $priorStoneTrad$blackPoint": { [`&.${classes.hover}.${classes.priorPoint} .${classes.priorStoneTrad}.${classes.blackPoint}`]: {
outlineColor: theme.colors.white, outlineColor: theme.colors.white,
display: "block", display: "block",
}, },
"&$hover$priorPoint $priorStoneTrad$whitePoint": { [`&.${classes.hover}.${classes.priorPoint} .${classes.priorStoneTrad}.${classes.whitePoint}`]: {
outlineColor: theme.colors.black, outlineColor: theme.colors.black,
display: "block", display: "block",
}, },
"&$hover:hover $coordinates": { [`&.${classes.hover}:hover .${classes.coordinates}`]: {
display: "block", display: "block",
}, },
"&:hover $broken": { [`&:hover .${classes.broken}`]: {
opacity: "0.4", opacity: "0.4",
}, },
}, },
@ -43,7 +65,7 @@ export const pointStyle = makeStyles((theme: Theme) =>
width: "83%", width: "83%",
height: "83%", height: "83%",
transition: "all 0.3s", transition: "all 0.3s",
"& $coordinates": { [`& .${classes.coordinates}`]: {
fontSize: "10px", fontSize: "10px",
}, },
}, },
@ -59,142 +81,143 @@ export const pointStyle = makeStyles((theme: Theme) =>
overflow: "hidden", overflow: "hidden",
}, },
traditional: { traditional: {
"& $innerPoint": { [`& .${classes.innerPoint}`]: {
display: "none", display: "none",
}, },
"& $broken": { [`& .${classes.broken}`]: {
backgroundImage: "none", backgroundImage: "none",
backgroundColor: theme.colors.black, backgroundColor: theme.colors.black,
}, },
"& $tradStone": { [`& .${classes.tradStone}`]: {
display: "block", display: "block",
}, },
"& $liberty": { [`& .${classes.liberty}`]: {
backgroundColor: theme.colors.black, backgroundColor: theme.colors.black,
transition: "none", transition: "none",
"&:not($northLiberty):not($southLiberty):not($eastLiberty):not($westLiberty)": { [`&:not(.${classes.northLiberty}):not(.${classes.southLiberty}):not(.${classes.eastLiberty}):not(.${classes.westLiberty})`]:
width: 0, {
height: 0, width: 0,
}, height: 0,
},
}, },
"& $northLiberty, & $southLiberty": { [`& .${classes.northLiberty}, & .${classes.southLiberty}`]: {
width: "0.9px", width: "0.9px",
}, },
"& $eastLiberty, & $westLiberty": { [`& .${classes.eastLiberty}, & .${classes.westLiberty}`]: {
height: "0.9px", height: "0.9px",
}, },
"&$nineteenByNineteen": { [`&.${classes.nineteenByNineteen}`]: {
"& $blackPoint": { [`& .${classes.blackPoint}`]: {
"&:before": { "&:before": {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(30px, 5vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(30px, 5vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`,
}, },
}, },
"& $whitePoint": { [`& .${classes.whitePoint}`]: {
"&:before": { "&:before": {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(30px, 5vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(30px, 5vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`,
}, },
}, },
"& $coordinates": { [`& .${classes.coordinates}`]: {
fontSize: "0.9vw", fontSize: "0.9vw",
}, },
}, },
"&$thirteenByThirteen": { [`&.${classes.thirteenByThirteen}`]: {
"& $blackPoint": { [`& .${classes.blackPoint}`]: {
"&:before": { "&:before": {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(40px, 6vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(40px, 6vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`,
}, },
}, },
"& $whitePoint": { [`& .${classes.whitePoint}`]: {
"&:before": { "&:before": {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(40px, 6vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(40px, 6vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`,
}, },
}, },
"& $coordinates": { [`& .${classes.coordinates}`]: {
fontSize: "0.9vw", fontSize: "0.9vw",
}, },
}, },
"&$nineByNine": { [`&.${classes.nineByNine}`]: {
"& $blackPoint": { [`& .${classes.blackPoint}`]: {
"&:before": { "&:before": {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(60px, 7vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(60px, 7vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`,
}, },
}, },
"& $whitePoint": { [`& .${classes.whitePoint}`]: {
"&:before": { "&:before": {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(60px, 7vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(60px, 7vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`,
}, },
}, },
}, },
"&$sevenBySeven": { [`&.${classes.sevenBySeven}`]: {
"& $blackPoint": { [`& .${classes.blackPoint}`]: {
"&:before": { "&:before": {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(80px, 8vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(80px, 8vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`,
}, },
}, },
"& $whitePoint": { [`& .${classes.whitePoint}`]: {
"&:before": { "&:before": {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(80px, 8vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(80px, 8vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`,
}, },
}, },
}, },
"& $coordinates": { [`& .${classes.coordinates}`]: {
color: "black", color: "black",
left: "15%", left: "15%",
}, },
"& $blackPoint ~ $coordinates": { [`& .${classes.blackPoint} ~ .${classes.coordinates}`]: {
color: theme.colors.white, color: theme.colors.white,
}, },
}, },
fiveByFive: { fiveByFive: {
"& $blackPoint": { [`& .${classes.blackPoint}`]: {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(35px, 4vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(35px, 4vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`,
}, },
"& $whitePoint": { [`& .${classes.whitePoint}`]: {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(35px, 4vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(35px, 4vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`,
}, },
}, },
sevenBySeven: { sevenBySeven: {
"& $blackPoint": { [`& .${classes.blackPoint}`]: {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(23px, 3vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(23px, 3vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`,
}, },
"& $whitePoint": { [`& .${classes.whitePoint}`]: {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(25px, 3vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(25px, 3vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`,
}, },
}, },
nineByNine: { nineByNine: {
"& $filledPoint": { [`& .${classes.filledPoint}`]: {
boxShadow: "0px 0px 30px hsla(0, 100%, 100%, 0.48)", boxShadow: "0px 0px 30px hsla(0, 100%, 100%, 0.48)",
}, },
"& $blackPoint": { [`& .${classes.blackPoint}`]: {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(15px, 2vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(15px, 2vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`,
}, },
"& $whitePoint": { [`& .${classes.whitePoint}`]: {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(15px, 2vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(15px, 2vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`,
}, },
}, },
thirteenByThirteen: { thirteenByThirteen: {
"& $filledPoint": { [`& .${classes.filledPoint}`]: {
boxShadow: "0px 0px 18px hsla(0, 100%, 100%, 0.48)", boxShadow: "0px 0px 18px hsla(0, 100%, 100%, 0.48)",
}, },
"& $blackPoint": { [`& .${classes.blackPoint}`]: {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`,
}, },
"& $whitePoint": { [`& .${classes.whitePoint}`]: {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`,
}, },
}, },
nineteenByNineteen: { nineteenByNineteen: {
"& $filledPoint": { [`& .${classes.filledPoint}`]: {
boxShadow: "0px 0px 18px hsla(0, 100%, 100%, 0.48)", boxShadow: "0px 0px 18px hsla(0, 100%, 100%, 0.48)",
}, },
"& $blackPoint": { [`& .${classes.blackPoint}`]: {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`,
}, },
"& $whitePoint": { [`& .${classes.whitePoint}`]: {
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(10px, 1.5vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`,
}, },
"& $innerPoint": { [`& .${classes.innerPoint}`]: {
width: "70%", width: "70%",
height: "70%", height: "70%",
margin: "15%", margin: "15%",
@ -229,7 +252,7 @@ export const pointStyle = makeStyles((theme: Theme) =>
top: 0, top: 0,
}, },
"&$blackPoint": { [`&.${classes.blackPoint}`]: {
position: "static", position: "static",
outlineWidth: 0, outlineWidth: 0,
width: 0, width: 0,
@ -241,7 +264,7 @@ export const pointStyle = makeStyles((theme: Theme) =>
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(150px, 11vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.black} 65%), radial-gradient(calc(min(150px, 11vw)) at 42% 38%, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.25) 35%, transparent 36%)`,
}, },
}, },
"&$whitePoint": { [`&.${classes.whitePoint}`]: {
backgroundColor: "transparent", backgroundColor: "transparent",
width: 0, width: 0,
height: 0, height: 0,
@ -252,7 +275,7 @@ export const pointStyle = makeStyles((theme: Theme) =>
backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(150px, 11vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`, backgroundImage: `linear-gradient(145deg, transparent, ${theme.colors.white} 65%), radial-gradient(calc(min(150px, 11vw)) at 42% 38%, white 0%, white 35%, transparent 36%)`,
}, },
}, },
"&$emptyPoint": { [`&.${classes.emptyPoint}`]: {
width: 0, width: 0,
height: 0, height: 0,
margin: 0, margin: 0,
@ -305,15 +328,7 @@ export const pointStyle = makeStyles((theme: Theme) =>
outlineColor: theme.colors.white, outlineColor: theme.colors.white,
}, },
fadeLoopAnimation: { fadeLoopAnimation: {
animation: `$fadeLoop 800ms ${theme.transitions.easing.easeInOut} infinite alternate`, animation: `${fadeLoop} 800ms ${theme.transitions.easing.easeInOut} infinite alternate`,
},
"@keyframes fadeLoop": {
"0%": {
opacity: 0.4,
},
"100%": {
opacity: 1,
},
}, },
liberty: { liberty: {
position: "absolute", position: "absolute",
@ -381,8 +396,8 @@ export const pointStyle = makeStyles((theme: Theme) =>
}), }),
); );
export const boardStyles = makeStyles((theme: Theme) => export const boardStyles = makeStyles<void, Size | "background">({ uniqId: "boardStyles" })(
createStyles({ (theme: Theme, _, classes) => ({
tab: { tab: {
paddingTop: 0, paddingTop: 0,
paddingBottom: 0, paddingBottom: 0,
@ -469,15 +484,7 @@ export const boardStyles = makeStyles((theme: Theme) =>
borderColor: theme.colors.success, borderColor: theme.colors.success,
padding: "0 12px", padding: "0 12px",
width: "200px", width: "200px",
animation: `$fadeLoop 600ms ${theme.transitions.easing.easeInOut} infinite alternate`, animation: `${fadeLoop} 600ms ${theme.transitions.easing.easeInOut} infinite alternate`,
},
"@keyframes fadeLoop": {
"0%": {
opacity: 0.6,
},
"100%": {
opacity: 1,
},
}, },
scoreBox: { scoreBox: {
display: "inline-flex", display: "inline-flex",
@ -491,35 +498,35 @@ export const boardStyles = makeStyles((theme: Theme) =>
}, },
fiveByFive: { fiveByFive: {
height: "20%", height: "20%",
"& $fiveByFive": { [`& .${classes.fiveByFive}`]: {
width: "20%", width: "20%",
height: "100%", height: "100%",
}, },
}, },
sevenBySeven: { sevenBySeven: {
height: "14%", height: "14%",
"& $sevenBySeven": { [`& .${classes.sevenBySeven}`]: {
width: "14%", width: "14%",
height: "100%", height: "100%",
}, },
}, },
nineByNine: { nineByNine: {
height: "11%", height: "11%",
"& $nineByNine": { [`& .${classes.nineByNine}`]: {
width: "11%", width: "11%",
height: "100%", height: "100%",
}, },
}, },
thirteenByThirteen: { thirteenByThirteen: {
height: "7.5%", height: "7.5%",
"& $thirteenByThirteen": { [`& .${classes.thirteenByThirteen}`]: {
width: "7.5%", width: "7.5%",
height: "100%", height: "100%",
}, },
}, },
nineteenByNineteen: { nineteenByNineteen: {
height: "5.2%", height: "5.2%",
"& $nineteenByNineteen": { [`& .${classes.nineteenByNineteen}`]: {
width: "5.2%", width: "5.2%",
height: "100%", height: "100%",
}, },
@ -535,7 +542,7 @@ export const boardStyles = makeStyles((theme: Theme) =>
paddingTop: "15px", paddingTop: "15px",
}, },
bitverseBackground: { bitverseBackground: {
"&$background": { [`&.${classes.background}`]: {
fontSize: "calc(min(.83vh - 1px, 0.72vw, 7.856px))", fontSize: "calc(min(.83vh - 1px, 0.72vw, 7.856px))",
opacity: 0.11, opacity: 0.11,
}, },

@ -1,4 +1,4 @@
import { Play, SimpleOpponentStats } from "../Types"; import { Play, SimpleBoard, SimpleOpponentStats } from "../Types";
import { Player } from "@player"; import { Player } from "@player";
import { AugmentationName, GoColor, GoOpponent, GoPlayType, GoValidity } from "@enums"; import { AugmentationName, GoColor, GoOpponent, GoPlayType, GoValidity } from "@enums";
@ -10,8 +10,9 @@ import {
getControlledSpace, getControlledSpace,
getPreviousMove, getPreviousMove,
simpleBoardFromBoard, simpleBoardFromBoard,
simpleBoardFromBoardString,
} from "../boardAnalysis/boardAnalysis"; } from "../boardAnalysis/boardAnalysis";
import { getOpponentStats, getScore, resetWinstreak } from "../boardAnalysis/scoring"; import { endGoGame, getOpponentStats, getScore, resetWinstreak } from "../boardAnalysis/scoring";
import { WHRNG } from "../../Casino/RNG"; import { WHRNG } from "../../Casino/RNG";
import { getRecordKeys } from "../../Types/Record"; import { getRecordKeys } from "../../Types/Record";
import { CalculateEffect, getEffectTypeForFaction } from "./effect"; import { CalculateEffect, getEffectTypeForFaction } from "./effect";
@ -223,6 +224,8 @@ export function getControlledEmptyNodes() {
* Gets the status of the current game. * Gets the status of the current game.
* Shows the current player, current score, and the previous move coordinates. * Shows the current player, current score, and the previous move coordinates.
* Previous move coordinates will be [-1, -1] for a pass, or if there are no prior moves. * Previous move coordinates will be [-1, -1] for a pass, or if there are no prior moves.
*
* Also provides the white player's komi (bonus starting score), and the amount of bonus cycles from offline time remaining
*/ */
export function getGameState() { export function getGameState() {
const currentPlayer = getCurrentPlayer(); const currentPlayer = getCurrentPlayer();
@ -234,9 +237,15 @@ export function getGameState() {
whiteScore: score[GoColor.white].sum, whiteScore: score[GoColor.white].sum,
blackScore: score[GoColor.black].sum, blackScore: score[GoColor.black].sum,
previousMove, previousMove,
komi: score[GoColor.white].komi,
bonusCycles: Go.storedCycles,
}; };
} }
export function getMoveHistory(): SimpleBoard[] {
return Go.currentGame.previousBoards.map((boardString) => simpleBoardFromBoardString(boardString));
}
/** /**
* Returns 'None' if the game is over, otherwise returns the color of the current player's turn * Returns 'None' if the game is over, otherwise returns the color of the current player's turn
*/ */
@ -337,30 +346,27 @@ export async function determineCheatSuccess(
): Promise<Play> { ): Promise<Play> {
const state = Go.currentGame; const state = Go.currentGame;
const rng = new WHRNG(Player.totalPlaytime); const rng = new WHRNG(Player.totalPlaytime);
state.passCount = 0;
// If cheat is successful, run callback // If cheat is successful, run callback
if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount)) { if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount)) {
callback(); callback();
state.cheatCount++;
GoEvents.emit(); GoEvents.emit();
return makeAIMove(state);
} }
// If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly losing // If there have been prior cheat attempts, and the cheat fails, there is a 10% chance of instantly losing
else if (state.cheatCount && (ejectRngOverride ?? rng.random()) < 0.1) { else if (state.cheatCount && (ejectRngOverride ?? rng.random()) < 0.1) {
logger(`Cheat failed! You have been ejected from the subnet.`); logger(`Cheat failed! You have been ejected from the subnet.`);
resetBoardState(logger, logger, state.ai, state.board[0].length); endGoGame(state);
return { return Go.nextTurn;
type: GoPlayType.gameOver,
x: null,
y: null,
};
} }
// If the cheat fails, your turn is skipped // If the cheat fails, your turn is skipped
else { else {
logger(`Cheat failed. Your turn has been skipped.`); logger(`Cheat failed. Your turn has been skipped.`);
passTurn(state, GoColor.black, false); passTurn(state, GoColor.black, false);
state.cheatCount++;
return makeAIMove(state);
} }
state.cheatCount++;
return makeAIMove(state);
} }
/** /**
@ -395,7 +401,7 @@ export function cheatRemoveRouter(
y: number, y: number,
successRngOverride?: number, successRngOverride?: number,
ejectRngOverride?: number, ejectRngOverride?: number,
) { ): Promise<Play> {
const point = Go.currentGame.board[x][y]!; const point = Go.currentGame.board[x][y]!;
return determineCheatSuccess( return determineCheatSuccess(
logger, logger,
@ -421,7 +427,7 @@ export function cheatPlayTwoMoves(
y2: number, y2: number,
successRngOverride?: number, successRngOverride?: number,
ejectRngOverride?: number, ejectRngOverride?: number,
) { ): Promise<Play> {
const point1 = Go.currentGame.board[x1][y1]!; const point1 = Go.currentGame.board[x1][y1]!;
const point2 = Go.currentGame.board[x2][y2]!; const point2 = Go.currentGame.board[x2][y2]!;
@ -446,7 +452,7 @@ export function cheatRepairOfflineNode(
y: number, y: number,
successRngOverride?: number, successRngOverride?: number,
ejectRngOverride?: number, ejectRngOverride?: number,
) { ): Promise<Play> {
return determineCheatSuccess( return determineCheatSuccess(
logger, logger,
() => { () => {
@ -472,7 +478,7 @@ export function cheatDestroyNode(
y: number, y: number,
successRngOverride?: number, successRngOverride?: number,
ejectRngOverride?: number, ejectRngOverride?: number,
) { ): Promise<Play> {
return determineCheatSuccess( return determineCheatSuccess(
logger, logger,
() => { () => {

@ -27,7 +27,7 @@ export function GoGameboard({ boardState, traditional, clickHandler, hover }: Go
} }
const boardSize = boardState.board[0].length; const boardSize = boardState.board[0].length;
const classes = boardStyles(); const { classes } = boardStyles();
return ( return (
<Grid container id="goGameboard" className={`${classes.board} ${traditional ? classes.traditional : ""}`}> <Grid container id="goGameboard" className={`${classes.board} ${traditional ? classes.traditional : ""}`}>

@ -9,7 +9,7 @@ import { SnackbarEvents } from "../../ui/React/Snackbar";
import { getNewBoardState, getStateCopy, makeMove, passTurn, updateCaptures } from "../boardState/boardState"; import { getNewBoardState, getStateCopy, makeMove, passTurn, updateCaptures } from "../boardState/boardState";
import { bitverseArt, weiArt } from "../boardState/asciiArt"; import { bitverseArt, weiArt } from "../boardState/asciiArt";
import { getScore, resetWinstreak } from "../boardAnalysis/scoring"; import { getScore, resetWinstreak } from "../boardAnalysis/scoring";
import { boardFromSimpleBoard, evaluateIfMoveIsValid, getAllValidMoves } from "../boardAnalysis/boardAnalysis"; import { boardFromBoardString, evaluateIfMoveIsValid, getAllValidMoves } from "../boardAnalysis/boardAnalysis";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
import { OptionSwitch } from "../../ui/React/OptionSwitch"; import { OptionSwitch } from "../../ui/React/OptionSwitch";
import { boardStyles } from "../boardState/goStyles"; import { boardStyles } from "../boardState/goStyles";
@ -18,7 +18,7 @@ import { GoScoreModal } from "./GoScoreModal";
import { GoGameboard } from "./GoGameboard"; import { GoGameboard } from "./GoGameboard";
import { GoSubnetSearch } from "./GoSubnetSearch"; import { GoSubnetSearch } from "./GoSubnetSearch";
import { CorruptableText } from "../../ui/React/CorruptableText"; import { CorruptableText } from "../../ui/React/CorruptableText";
import { makeAIMove } from "../boardAnalysis/goAI"; import { makeAIMove, resolveCurrentTurn } from "../boardAnalysis/goAI";
interface GoGameboardWrapperProps { interface GoGameboardWrapperProps {
showInstructions: () => void; showInstructions: () => void;
@ -46,7 +46,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
const [scoreOpen, setScoreOpen] = useState(false); const [scoreOpen, setScoreOpen] = useState(false);
const [searchOpen, setSearchOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false);
const classes = boardStyles(); const { classes } = boardStyles();
const boardSize = boardState.board[0].length; const boardSize = boardState.board[0].length;
const currentPlayer = boardState.previousPlayer === GoColor.white ? GoColor.black : GoColor.white; const currentPlayer = boardState.previousPlayer === GoColor.white ? GoColor.black : GoColor.white;
const waitingOnAI = boardState.previousPlayer === GoColor.black && boardState.ai !== GoOpponent.none; const waitingOnAI = boardState.previousPlayer === GoColor.black && boardState.ai !== GoOpponent.none;
@ -85,7 +85,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
const didUpdateBoard = makeMove(boardState, x, y, currentPlayer); const didUpdateBoard = makeMove(boardState, x, y, currentPlayer);
if (didUpdateBoard) { if (didUpdateBoard) {
rerender(); rerender();
Go.currentGame.ai !== GoOpponent.none && takeAiTurn(boardState); takeAiTurn(boardState);
} }
} }
@ -104,12 +104,18 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
} }
setTimeout(() => { setTimeout(() => {
Go.currentGame.ai !== GoOpponent.none && takeAiTurn(boardState); takeAiTurn(boardState);
}, 100); }, 100);
} }
async function takeAiTurn(boardState: BoardState) { async function takeAiTurn(boardState: BoardState) {
const move = await makeAIMove(boardState); // If white is being played manually, halt and notify any scripts playing as black if present, instead of making an AI move
if (Go.currentGame.ai === GoOpponent.none) {
Go.currentGame.previousPlayer && resolveCurrentTurn();
return;
}
const move = await makeAIMove(boardState, false);
if (move.type === GoPlayType.pass) { if (move.type === GoPlayType.pass) {
SnackbarEvents.emit(`The opponent passes their turn; It is now your turn to move.`, ToastVariant.WARNING, 4000); SnackbarEvents.emit(`The opponent passes their turn; It is now your turn to move.`, ToastVariant.WARNING, 4000);
@ -137,13 +143,14 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
Go.currentGame = getNewBoardState(newBoardSize, newOpponent, true); Go.currentGame = getNewBoardState(newBoardSize, newOpponent, true);
rerender(); rerender();
resolveCurrentTurn();
} }
function getPriorMove() { function getPriorMove() {
if (!boardState.previousBoards.length) return boardState; if (!boardState.previousBoards.length) return boardState;
const priorState = getStateCopy(boardState); const priorState = getStateCopy(boardState);
priorState.previousPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black; priorState.previousPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
priorState.board = boardFromSimpleBoard(boardState.previousBoards[0]); priorState.board = boardFromBoardString(boardState.previousBoards[0]);
updateCaptures(priorState.board, priorState.previousPlayer); updateCaptures(priorState.board, priorState.previousPlayer);
return priorState; return priorState;
} }
@ -159,17 +166,19 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
rerender(); rerender();
} }
const endGameAvailable = boardState.previousPlayer === GoColor.white && boardState.passCount; const ongoingNoAiGame = boardState.ai === GoOpponent.none && boardState.previousPlayer;
const noLegalMoves = const manualTurnAvailable = ongoingNoAiGame || boardState.previousPlayer === GoColor.white;
boardState.previousPlayer === GoColor.white && !getAllValidMoves(boardState, GoColor.black).length; const endGameAvailable = manualTurnAvailable && boardState.passCount;
const noLegalMoves = manualTurnAvailable && !getAllValidMoves(boardState, currentPlayer).length;
const scoreBoxText = boardState.previousBoards.length const scoreBoxText = boardState.previousBoards.length
? `Score: Black: ${score[GoColor.black].sum} White: ${score[GoColor.white].sum}` ? `Score: Black: ${score[GoColor.black].sum} White: ${score[GoColor.white].sum}`
: "Place a router to begin!"; : "Place a router to begin!";
const getPassButtonLabel = () => { const getPassButtonLabel = () => {
const playerString = boardState.ai === GoOpponent.none ? ` (${currentPlayer})` : "";
if (endGameAvailable) { if (endGameAvailable) {
return "End Game"; return `End Game${playerString}`;
} }
if (boardState.previousPlayer === null) { if (boardState.previousPlayer === null) {
return "View Final Score"; return "View Final Score";
@ -177,8 +186,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
if (waitingOnAI) { if (waitingOnAI) {
return "Waiting for opponent"; return "Waiting for opponent";
} }
const currentPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black; return `Pass Turn${playerString}`;
return `Pass Turn${boardState.ai === GoOpponent.none ? ` (${currentPlayer})` : ""}`;
}; };
return ( return (

@ -16,7 +16,7 @@ import { getRecordKeys } from "../../Types/Record";
export const GoHistoryPage = (): React.ReactElement => { export const GoHistoryPage = (): React.ReactElement => {
useRerender(400); useRerender(400);
const classes = boardStyles(); const { classes } = boardStyles();
const priorBoard = Go.previousGame ?? getNewBoardState(7); const priorBoard = Go.previousGame ?? getNewBoardState(7);
const score = getScore(priorBoard); const score = getScore(priorBoard);
const opponent = priorBoard.ai; const opponent = priorBoard.ai;

@ -70,7 +70,7 @@ const makeTwoEyesChallenge = (
); );
export const GoInstructionsPage = (): React.ReactElement => { export const GoInstructionsPage = (): React.ReactElement => {
const classes = boardStyles(); const { classes } = boardStyles();
return ( return (
<div className={classes.instructionScroller}> <div className={classes.instructionScroller}>
<> <>
@ -80,6 +80,16 @@ export const GoInstructionsPage = (): React.ReactElement => {
In late 2070, the .org bubble burst, and most of the newly-implemented IPvGO 'net collapsed overnight. Since In late 2070, the .org bubble burst, and most of the newly-implemented IPvGO 'net collapsed overnight. Since
then, various factions have been fighting over small subnets to control their computational power. These then, various factions have been fighting over small subnets to control their computational power. These
subnets are very valuable in the right hands, if you can wrest them from their current owners. subnets are very valuable in the right hands, if you can wrest them from their current owners.
<br />
<br />
(For details about how to automate with the API, and for a working starter script, visit the IPvGO section of
the in-game{" "}
<Link
style={{ cursor: "pointer" }}
onClick={() => Router.toPage(Page.Documentation, { docPage: "programming/go_algorithms.md" })}
>
Bitburner Documentation)
</Link>
</Typography> </Typography>
<br /> <br />
<br /> <br />

@ -1,13 +1,12 @@
import type { BoardState } from "../Types"; import type { BoardState } from "../Types";
import React from "react"; import React from "react";
import { ClassNameMap } from "@mui/styles";
import { GoColor } from "@enums"; import { GoColor } from "@enums";
import { columnIndexes } from "../Constants"; import { columnIndexes } from "../Constants";
import { findNeighbors } from "../boardState/boardState"; import { findNeighbors } from "../boardState/boardState";
import { pointStyle } from "../boardState/goStyles"; import { boardStyles, pointStyle } from "../boardState/goStyles";
import { findAdjacentLibertiesAndAlliesForPoint, getColorOnSimpleBoard } from "../boardAnalysis/boardAnalysis"; import { findAdjacentLibertiesAndAlliesForPoint, getColorOnBoardString } from "../boardAnalysis/boardAnalysis";
interface GoPointProps { interface GoPointProps {
state: BoardState; state: BoardState;
@ -20,7 +19,7 @@ interface GoPointProps {
} }
export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwner }: GoPointProps): React.ReactElement { export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwner }: GoPointProps): React.ReactElement {
const classes = pointStyle(); const { classes } = pointStyle();
const currentPoint = state.board[x]?.[y]; const currentPoint = state.board[x]?.[y];
const player = currentPoint?.color; const player = currentPoint?.color;
@ -42,7 +41,7 @@ export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwne
const sizeClass = getSizeClass(state.board[0].length, classes); const sizeClass = getSizeClass(state.board[0].length, classes);
const isNewStone = const isNewStone =
state.previousBoards.length && getColorOnSimpleBoard(state.previousBoards[0], x, y) === GoColor.empty; state.previousBoards.length && getColorOnBoardString(state.previousBoards[0], x, y) === GoColor.empty;
const isPriorMove = player === state.previousPlayer && isNewStone; const isPriorMove = player === state.previousPlayer && isNewStone;
const emptyPointColorClass = const emptyPointColorClass =
@ -89,10 +88,7 @@ export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwne
); );
} }
export function getSizeClass( export function getSizeClass(size: number, classes: ReturnType<typeof boardStyles | typeof pointStyle>["classes"]) {
size: number,
classes: ClassNameMap<"fiveByFive" | "sevenBySeven" | "nineByNine" | "thirteenByThirteen" | "nineteenByNineteen">,
) {
switch (size) { switch (size) {
case 5: case 5:
return classes.fiveByFive; return classes.fiveByFive;

@ -9,7 +9,7 @@ import { GoGameboardWrapper } from "./GoGameboardWrapper";
import { boardStyles } from "../boardState/goStyles"; import { boardStyles } from "../boardState/goStyles";
export function GoRoot(): React.ReactElement { export function GoRoot(): React.ReactElement {
const classes = boardStyles(); const { classes } = boardStyles();
const [value, setValue] = React.useState(0); const [value, setValue] = React.useState(0);
function handleChange(event: React.SyntheticEvent, tab: number): void { function handleChange(event: React.SyntheticEvent, tab: number): void {

@ -17,7 +17,7 @@ interface Props {
} }
export const GoScoreModal = ({ open, onClose, finalScore, newSubnet, opponent }: Props): React.ReactElement => { export const GoScoreModal = ({ open, onClose, finalScore, newSubnet, opponent }: Props): React.ReactElement => {
const classes = boardStyles(); const { classes } = boardStyles();
const blackScore = finalScore[GoColor.black]; const blackScore = finalScore[GoColor.black];
const whiteScore = finalScore[GoColor.white]; const whiteScore = finalScore[GoColor.white];

@ -18,7 +18,7 @@ interface Props {
} }
export const GoScorePowerSummary = ({ finalScore, opponent }: Props) => { export const GoScorePowerSummary = ({ finalScore, opponent }: Props) => {
const classes = boardStyles(); const { classes } = boardStyles();
const status = getOpponentStats(opponent); const status = getOpponentStats(opponent);
const winStreak = status.winStreak; const winStreak = status.winStreak;
const oldWinStreak = status.winStreak; const oldWinStreak = status.winStreak;

@ -12,7 +12,7 @@ interface GoScoreSummaryTableProps {
} }
export const GoScoreSummaryTable = ({ score, opponent }: GoScoreSummaryTableProps) => { export const GoScoreSummaryTable = ({ score, opponent }: GoScoreSummaryTableProps) => {
const classes = boardStyles(); const { classes } = boardStyles();
const blackScore = score[GoColor.black]; const blackScore = score[GoColor.black];
const whiteScore = score[GoColor.white]; const whiteScore = score[GoColor.white];
const blackPlayerName = opponent === GoOpponent.none ? GoColor.black : "You"; const blackPlayerName = opponent === GoOpponent.none ? GoColor.black : "You";

@ -13,7 +13,7 @@ import { GoOpponent } from "@enums";
export const GoStatusPage = (): React.ReactElement => { export const GoStatusPage = (): React.ReactElement => {
useRerender(400); useRerender(400);
const classes = boardStyles(); const { classes } = boardStyles();
const score = getScore(Go.currentGame); const score = getScore(Go.currentGame);
const opponent = Go.currentGame.ai; const opponent = Go.currentGame.ai;
const playedOpponentList = getRecordKeys(Go.stats).filter((o) => o !== GoOpponent.none); const playedOpponentList = getRecordKeys(Go.stats).filter((o) => o !== GoOpponent.none);

@ -18,9 +18,10 @@ interface IProps {
cancel: () => void; cancel: () => void;
showInstructions: () => void; showInstructions: () => void;
} }
const boardSizeOptions = boardSizes.filter((size) => size !== 19);
export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProps): React.ReactElement => { export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProps): React.ReactElement => {
const classes = boardStyles(); const { classes } = boardStyles();
const [opponent, setOpponent] = useState<GoOpponent>(Go.currentGame?.ai ?? GoOpponent.SlumSnakes); const [opponent, setOpponent] = useState<GoOpponent>(Go.currentGame?.ai ?? GoOpponent.SlumSnakes);
const preselectedBoardSize = const preselectedBoardSize =
opponent === GoOpponent.w0r1d_d43m0n ? 19 : Math.min(Go.currentGame?.board?.[0]?.length ?? 7, 13); opponent === GoOpponent.w0r1d_d43m0n ? 19 : Math.min(Go.currentGame?.board?.[0]?.length ?? 7, 13);
@ -88,7 +89,7 @@ export const GoSubnetSearch = ({ open, search, cancel, showInstructions }: IProp
<Typography>????</Typography> <Typography>????</Typography>
) : ( ) : (
<Select value={`${boardSize}`} onChange={changeBoardSize} sx={{ mr: 1 }}> <Select value={`${boardSize}`} onChange={changeBoardSize} sx={{ mr: 1 }}>
{boardSizes.map((size) => ( {boardSizeOptions.map((size) => (
<MenuItem key={size} value={size}> <MenuItem key={size} value={size}>
{size}x{size} {size}x{size}
</MenuItem> </MenuItem>

@ -33,7 +33,7 @@ export function GoTutorialChallenge({
incorrectText2, incorrectText2,
}: IProps): React.ReactElement { }: IProps): React.ReactElement {
const stateRef = useRef(getStateCopy(state)); const stateRef = useRef(getStateCopy(state));
const classes = boardStyles(); const { classes } = boardStyles();
const [displayText, setDisplayText] = useState(description); const [displayText, setDisplayText] = useState(description);
const [showReset, setShowReset] = useState(false); const [showReset, setShowReset] = useState(false);

@ -2,6 +2,7 @@ import { currentNodeMults } from "./BitNode/BitNodeMultipliers";
import { Person as IPerson } from "@nsdefs"; import { Person as IPerson } from "@nsdefs";
import { calculateIntelligenceBonus } from "./PersonObjects/formulas/intelligence"; import { calculateIntelligenceBonus } from "./PersonObjects/formulas/intelligence";
import { Server as IServer } from "@nsdefs"; import { Server as IServer } from "@nsdefs";
import { clampNumber } from "./utils/helpers/clampNumber";
/** Returns the chance the person has to successfully hack a server */ /** Returns the chance the person has to successfully hack a server */
export function calculateHackingChance(server: IServer, person: IPerson): number { export function calculateHackingChance(server: IServer, person: IPerson): number {
@ -11,14 +12,14 @@ export function calculateHackingChance(server: IServer, person: IPerson): number
if (!server.hasAdminRights || hackDifficulty >= 100) return 0; if (!server.hasAdminRights || hackDifficulty >= 100) return 0;
const hackFactor = 1.75; const hackFactor = 1.75;
const difficultyMult = (100 - hackDifficulty) / 100; const difficultyMult = (100 - hackDifficulty) / 100;
const skillMult = hackFactor * person.skills.hacking; const skillMult = clampNumber(hackFactor * person.skills.hacking, 1);
const skillChance = (skillMult - requiredHackingSkill) / skillMult; const skillChance = (skillMult - requiredHackingSkill) / skillMult;
const chance = const chance =
skillChance * skillChance *
difficultyMult * difficultyMult *
person.mults.hacking_chance * person.mults.hacking_chance *
calculateIntelligenceBonus(person.skills.intelligence, 1); calculateIntelligenceBonus(person.skills.intelligence, 1);
return Math.min(1, Math.max(chance, 0)); return clampNumber(chance, 0, 1);
} }
/** /**

@ -4,7 +4,6 @@ import { Box, Paper, Typography } from "@mui/material";
import { AugmentationName } from "@enums"; import { AugmentationName } from "@enums";
import { Player } from "@player"; import { Player } from "@player";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { KEY } from "../../utils/helpers/keyCodes";
import { random } from "../utils"; import { random } from "../utils";
import { interpolate } from "./Difficulty"; import { interpolate } from "./Difficulty";
import { GameTimer } from "./GameTimer"; import { GameTimer } from "./GameTimer";
@ -32,19 +31,17 @@ const difficulties: {
Impossible: { timer: 4000, wiresmin: 9, wiresmax: 9, rules: 4 }, Impossible: { timer: 4000, wiresmin: 9, wiresmax: 9, rules: 4 },
}; };
const types = [KEY.PIPE, KEY.DOT, KEY.FORWARD_SLASH, KEY.HYPHEN, "█", KEY.HASH];
const colors = ["red", "#FFC107", "blue", "white"]; const colors = ["red", "#FFC107", "blue", "white"];
const colorNames: Record<string, string> = { const colorNames: Record<string, string> = {
red: "red", red: "RED",
"#FFC107": "yellow", "#FFC107": "YELLOW",
blue: "blue", blue: "BLUE",
white: "white", white: "WHITE",
}; };
interface Wire { interface Wire {
wireType: string; wireType: string[];
colors: string[]; colors: string[];
} }
@ -142,7 +139,7 @@ export function WireCuttingGame({ onSuccess, onFailure, difficulty }: IMinigameP
</Typography> </Typography>
); );
})} })}
{new Array(8).fill(0).map((_, i) => ( {new Array(11).fill(0).map((_, i) => (
<React.Fragment key={i}> <React.Fragment key={i}>
{wires.map((wire, j) => { {wires.map((wire, j) => {
if ((i === 3 || i === 4) && cutWires[j]) { if ((i === 3 || i === 4) && cutWires[j]) {
@ -153,7 +150,7 @@ export function WireCuttingGame({ onSuccess, onFailure, difficulty }: IMinigameP
hasAugment && !isCorrectWire ? Settings.theme.disabled : wire.colors[i % wire.colors.length]; hasAugment && !isCorrectWire ? Settings.theme.disabled : wire.colors[i % wire.colors.length];
return ( return (
<Typography key={j} style={{ color: wireColor }}> <Typography key={j} style={{ color: wireColor }}>
|{wire.wireType}| |{wire.wireType[i % wire.wireType.length]}|
</Typography> </Typography>
); );
})} })}
@ -172,7 +169,7 @@ function randomPositionQuestion(wires: Wire[]): Question {
toString: (): string => { toString: (): string => {
return `Cut wires number ${index + 1}.`; return `Cut wires number ${index + 1}.`;
}, },
shouldCut: (wire: Wire, i: number): boolean => { shouldCut: (_wire: Wire, i: number): boolean => {
return index === i; return index === i;
}, },
}; };
@ -209,8 +206,9 @@ function generateWires(difficulty: Difficulty): Wire[] {
if (Math.random() < 0.15) { if (Math.random() < 0.15) {
wireColors.push(colors[Math.floor(Math.random() * colors.length)]); wireColors.push(colors[Math.floor(Math.random() * colors.length)]);
} }
const wireType = [...wireColors.map((color) => colorNames[color]).join("")];
wires.push({ wires.push({
wireType: types[Math.floor(Math.random() * types.length)], wireType,
colors: wireColors, colors: wireColors,
}); });
} }

@ -18,24 +18,21 @@ import { Page } from "../../ui/Router";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles"; import { makeStyles } from "tss-react/mui";
import createStyles from "@mui/styles/createStyles";
interface IProps { interface IProps {
city: City; city: City;
} }
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles()((theme: Theme) => ({
createStyles({ location: {
location: { color: theme.colors.maplocation,
color: theme.colors.maplocation, whiteSpace: "nowrap",
whiteSpace: "nowrap", margin: "0px",
margin: "0px", padding: "0px",
padding: "0px", cursor: "pointer",
cursor: "pointer", },
}, }));
}),
);
function toLocation(location: Location): void { function toLocation(location: Location): void {
if (location.name === LocationName.TravelAgency) { if (location.name === LocationName.TravelAgency) {
@ -97,7 +94,7 @@ function ASCIICity(props: IProps): React.ReactElement {
Y: 24, Y: 24,
Z: 25, Z: 25,
}; };
const classes = useStyles(); const { classes } = useStyles();
const lineElems = (s: string): (string | React.ReactElement)[] => { const lineElems = (s: string): (string | React.ReactElement)[] => {
const elems: (string | React.ReactElement)[] = []; const elems: (string | React.ReactElement)[] = [];

@ -57,7 +57,7 @@ export function CompanyLocation(props: IProps): React.ReactElement {
*/ */
const currentPosition = jobTitle ? CompanyPositions[jobTitle] : null; const currentPosition = jobTitle ? CompanyPositions[jobTitle] : null;
Player.location = companyNameAsLocationName(props.companyName); Player.gotoLocation(companyNameAsLocationName(props.companyName));
function startInfiltration(e: React.MouseEvent<HTMLElement>): void { function startInfiltration(e: React.MouseEvent<HTMLElement>): void {
if (!e.isTrusted) { if (!e.isTrusted) {

@ -24,14 +24,12 @@ import Button from "@mui/material/Button";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
function travel(to: CityName): void { function travel(to: CityName): void {
const cost = CONSTANTS.TravelCost; if (!Player.travel(to)) {
if (!Player.canAfford(cost)) {
return; return;
} }
if (!Settings.SuppressTravelConfirmation) {
Player.loseMoney(cost, "other"); dialogBoxCreate(`You are now in ${to}!`);
Player.travel(to); }
if (!Settings.SuppressTravelConfirmation) dialogBoxCreate(`You are now in ${to}!`);
Router.toPage(Page.City); Router.toPage(Page.City);
} }
@ -41,8 +39,7 @@ export function TravelAgencyRoot(): React.ReactElement {
useRerender(1000); useRerender(1000);
function startTravel(city: CityName): void { function startTravel(city: CityName): void {
const cost = CONSTANTS.TravelCost; if (!Player.canAfford(CONSTANTS.TravelCost)) {
if (!Player.canAfford(cost)) {
return; return;
} }
if (Settings.SuppressTravelConfirmation) { if (Settings.SuppressTravelConfirmation) {

@ -14,7 +14,6 @@ interface IProps {
} }
export function TravelConfirmationModal(props: IProps): React.ReactElement { export function TravelConfirmationModal(props: IProps): React.ReactElement {
const cost = CONSTANTS.TravelCost;
function travel(): void { function travel(): void {
props.travel(); props.travel();
} }
@ -22,7 +21,8 @@ export function TravelConfirmationModal(props: IProps): React.ReactElement {
return ( return (
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={props.onClose}>
<Typography> <Typography>
Would you like to travel to {props.city}? The trip will cost <Money money={cost} forPurchase={true} />. Would you like to travel to {props.city}? The trip will cost{" "}
<Money money={CONSTANTS.TravelCost} forPurchase={true} />.
</Typography> </Typography>
<br /> <br />
<br /> <br />

@ -250,6 +250,7 @@ const go = {
makeMove: 4, makeMove: 4,
passTurn: 0, passTurn: 0,
getBoardState: 4, getBoardState: 4,
getMoveHistory: 0,
getCurrentPlayer: 0, getCurrentPlayer: 0,
getGameState: 0, getGameState: 0,
getOpponent: 0, getOpponent: 0,

@ -739,11 +739,11 @@ export const ns: InternalAPI<NSFull> = {
const args = helpers.scriptArgs(ctx, _args); const args = helpers.scriptArgs(ctx, _args);
setTimeout(() => { setTimeout(() => {
const scriptServer = GetServer(ctx.workerScript.hostname); const scriptServer = GetServer(ctx.workerScript.hostname);
if (scriptServer == null) { if (scriptServer === null) {
throw helpers.errorMessage(ctx, "Could not find server. This is a bug. Report to dev"); throw helpers.errorMessage(ctx, `Cannot find server ${ctx.workerScript.hostname}`);
} }
return runScriptFromScript("spawn", scriptServer, path, args, ctx.workerScript, runOpts); runScriptFromScript("spawn", scriptServer, path, args, ctx.workerScript, runOpts);
}, runOpts.spawnDelay); }, runOpts.spawnDelay);
helpers.log(ctx, () => `Will execute '${path}' in ${runOpts.spawnDelay} milliseconds`); helpers.log(ctx, () => `Will execute '${path}' in ${runOpts.spawnDelay} milliseconds`);

@ -17,6 +17,7 @@ import {
getCurrentPlayer, getCurrentPlayer,
getGameState, getGameState,
getLiberties, getLiberties,
getMoveHistory,
getOpponentNextMove, getOpponentNextMove,
getStats, getStats,
getValidMoves, getValidMoves,
@ -47,17 +48,20 @@ export function NetscriptGo(): InternalAPI<NSGo> {
validateMove(error(ctx), x, y, "makeMove"); validateMove(error(ctx), x, y, "makeMove");
return makePlayerMove(logger(ctx), error(ctx), x, y); return makePlayerMove(logger(ctx), error(ctx), x, y);
}, },
passTurn: (ctx: NetscriptContext) => (): Promise<Play> => { passTurn: (ctx: NetscriptContext) => async (): Promise<Play> => {
validateTurn(error(ctx), "passTurn()"); validateTurn(error(ctx), "passTurn()");
return handlePassTurn(logger(ctx)); return handlePassTurn(logger(ctx));
}, },
opponentNextTurn: (ctx: NetscriptContext) => (_logOpponentMove) => { opponentNextTurn: (ctx: NetscriptContext) => async (_logOpponentMove) => {
const logOpponentMove = typeof _logOpponentMove === "boolean" ? _logOpponentMove : true; const logOpponentMove = typeof _logOpponentMove === "boolean" ? _logOpponentMove : true;
return getOpponentNextMove(logOpponentMove, logger(ctx)); return getOpponentNextMove(logOpponentMove, logger(ctx));
}, },
getBoardState: () => () => { getBoardState: () => () => {
return simpleBoardFromBoard(Go.currentGame.board); return simpleBoardFromBoard(Go.currentGame.board);
}, },
getMoveHistory: () => () => {
return getMoveHistory();
},
getCurrentPlayer: () => () => { getCurrentPlayer: () => () => {
return getCurrentPlayer(); return getCurrentPlayer();
}, },

@ -269,7 +269,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
); );
return false; return false;
} }
Player.location = LocationName.Sector12RothmanUniversity; Player.gotoLocation(LocationName.Sector12RothmanUniversity);
break; break;
case LocationName.VolhavenZBInstituteOfTechnology.toLowerCase(): case LocationName.VolhavenZBInstituteOfTechnology.toLowerCase():
if (Player.city != CityName.Volhaven) { if (Player.city != CityName.Volhaven) {
@ -279,7 +279,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
); );
return false; return false;
} }
Player.location = LocationName.VolhavenZBInstituteOfTechnology; Player.gotoLocation(LocationName.VolhavenZBInstituteOfTechnology);
break; break;
default: default:
helpers.log(ctx, () => `Invalid university name: '${universityName}'.`); helpers.log(ctx, () => `Invalid university name: '${universityName}'.`);
@ -327,7 +327,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
); );
return false; return false;
} }
Player.location = LocationName.AevumCrushFitnessGym; Player.gotoLocation(LocationName.AevumCrushFitnessGym);
break; break;
case LocationName.AevumSnapFitnessGym.toLowerCase(): case LocationName.AevumSnapFitnessGym.toLowerCase():
if (Player.city != CityName.Aevum) { if (Player.city != CityName.Aevum) {
@ -338,7 +338,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
); );
return false; return false;
} }
Player.location = LocationName.AevumSnapFitnessGym; Player.gotoLocation(LocationName.AevumSnapFitnessGym);
break; break;
case LocationName.Sector12IronGym.toLowerCase(): case LocationName.Sector12IronGym.toLowerCase():
if (Player.city != CityName.Sector12) { if (Player.city != CityName.Sector12) {
@ -349,7 +349,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
); );
return false; return false;
} }
Player.location = LocationName.Sector12IronGym; Player.gotoLocation(LocationName.Sector12IronGym);
break; break;
case LocationName.Sector12PowerhouseGym.toLowerCase(): case LocationName.Sector12PowerhouseGym.toLowerCase():
if (Player.city != CityName.Sector12) { if (Player.city != CityName.Sector12) {
@ -360,7 +360,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
); );
return false; return false;
} }
Player.location = LocationName.Sector12PowerhouseGym; Player.gotoLocation(LocationName.Sector12PowerhouseGym);
break; break;
case LocationName.VolhavenMilleniumFitnessGym.toLowerCase(): case LocationName.VolhavenMilleniumFitnessGym.toLowerCase():
if (Player.city != CityName.Volhaven) { if (Player.city != CityName.Volhaven) {
@ -371,7 +371,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
); );
return false; return false;
} }
Player.location = LocationName.VolhavenMilleniumFitnessGym; Player.gotoLocation(LocationName.VolhavenMilleniumFitnessGym);
break; break;
default: default:
helpers.log(ctx, () => `Invalid gym name: ${gymName}. gymWorkout() failed`); helpers.log(ctx, () => `Invalid gym name: ${gymName}. gymWorkout() failed`);
@ -401,12 +401,10 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
case CityName.NewTokyo: case CityName.NewTokyo:
case CityName.Ishima: case CityName.Ishima:
case CityName.Volhaven: case CityName.Volhaven:
if (Player.money < CONSTANTS.TravelCost) { if (!Player.travel(cityName)) {
helpers.log(ctx, () => "Not enough money to travel."); helpers.log(ctx, () => "Not enough money to travel.");
return false; return false;
} }
Player.loseMoney(CONSTANTS.TravelCost, "other");
Player.city = cityName;
helpers.log(ctx, () => `Traveled to ${cityName}`); helpers.log(ctx, () => `Traveled to ${cityName}`);
Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain / 50000); Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain / 50000);
return true; return true;
@ -747,7 +745,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
quitJob: (ctx) => (_companyName) => { quitJob: (ctx) => (_companyName) => {
helpers.checkSingularityAccess(ctx); helpers.checkSingularityAccess(ctx);
const companyName = getEnumHelper("CompanyName").nsGetMember(ctx, _companyName); const companyName = getEnumHelper("CompanyName").nsGetMember(ctx, _companyName);
Player.quitJob(companyName); Player.quitJob(companyName, true);
}, },
getCompanyRep: (ctx) => (_companyName) => { getCompanyRep: (ctx) => (_companyName) => {
helpers.checkSingularityAccess(ctx); helpers.checkSingularityAccess(ctx);
@ -1123,7 +1121,9 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
if (cbScript === null) throw helpers.errorMessage(ctx, `Could not resolve file path: ${_cbScript}`); if (cbScript === null) throw helpers.errorMessage(ctx, `Could not resolve file path: ${_cbScript}`);
const wd = GetServer(SpecialServers.WorldDaemon); const wd = GetServer(SpecialServers.WorldDaemon);
if (!(wd instanceof Server)) throw new Error("WorldDaemon was not a normal server. This is a bug contact dev."); if (!(wd instanceof Server)) {
throw new Error("WorldDaemon is not a normal server. This is a bug. Please contact developers.");
}
const hackingRequirements = () => { const hackingRequirements = () => {
if (Player.skills.hacking < wd.requiredHackingSkill) return false; if (Player.skills.hacking < wd.requiredHackingSkill) return false;
if (!wd.hasAdminRights) return false; if (!wd.hasAdminRights) return false;

@ -3,7 +3,7 @@ import type { Sleeve as NetscriptSleeve } from "@nsdefs";
import type { ActionIdentifier } from "../Bladeburner/Types"; import type { ActionIdentifier } from "../Bladeburner/Types";
import { Player } from "@player"; import { Player } from "@player";
import { BladeActionType } from "@enums"; import { BladeActionType, type BladeContractName } from "@enums";
import { Augmentations } from "../Augmentation/Augmentations"; import { Augmentations } from "../Augmentation/Augmentations";
import { findCrime } from "../Crime/CrimeHelpers"; import { findCrime } from "../Crime/CrimeHelpers";
import { getEnumHelper } from "../utils/EnumHelper"; import { getEnumHelper } from "../utils/EnumHelper";
@ -79,7 +79,11 @@ export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
const cityName = getEnumHelper("CityName").nsGetMember(ctx, _cityName); const cityName = getEnumHelper("CityName").nsGetMember(ctx, _cityName);
checkSleeveAPIAccess(ctx); checkSleeveAPIAccess(ctx);
checkSleeveNumber(ctx, sleeveNumber); checkSleeveNumber(ctx, sleeveNumber);
return Player.sleeves[sleeveNumber].travel(cityName); if (!Player.sleeves[sleeveNumber].travel(cityName)) {
helpers.log(ctx, () => "Not enough money to travel.");
return false;
}
return true;
}, },
setToCompanyWork: (ctx) => (_sleeveNumber, _companyName) => { setToCompanyWork: (ctx) => (_sleeveNumber, _companyName) => {
const sleeveNumber = helpers.number(ctx, "sleeveNumber", _sleeveNumber); const sleeveNumber = helpers.number(ctx, "sleeveNumber", _sleeveNumber);
@ -238,24 +242,23 @@ export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
const action = helpers.string(ctx, "action", _action); const action = helpers.string(ctx, "action", _action);
checkSleeveAPIAccess(ctx); checkSleeveAPIAccess(ctx);
checkSleeveNumber(ctx, sleeveNumber); checkSleeveNumber(ctx, sleeveNumber);
let contractName = undefined; let contract: BladeContractName | undefined = undefined;
if (action === "Take on contracts") { if (action === "Take on contracts") {
const contractEnum = getEnumHelper("BladeContractName").nsGetMember(ctx, _contract); contract = getEnumHelper("BladeContractName").nsGetMember(ctx, _contract);
contractName = helpers.string(ctx, "contract", _contract);
for (let i = 0; i < Player.sleeves.length; ++i) { for (let i = 0; i < Player.sleeves.length; ++i) {
if (i === sleeveNumber) continue; if (i === sleeveNumber) continue;
const otherWork = Player.sleeves[i].currentWork; const otherWork = Player.sleeves[i].currentWork;
if (otherWork?.type === SleeveWorkType.BLADEBURNER && otherWork.actionId.name === contractEnum) { if (otherWork?.type === SleeveWorkType.BLADEBURNER && otherWork.actionId.name === contract) {
throw helpers.errorMessage( throw helpers.errorMessage(
ctx, ctx,
`Sleeve ${sleeveNumber} cannot take on contracts because Sleeve ${i} is already performing that action.`, `Sleeve ${sleeveNumber} cannot take on contracts because Sleeve ${i} is already performing that action.`,
); );
} }
} }
const actionId: ActionIdentifier = { type: BladeActionType.contract, name: contractEnum }; const actionId: ActionIdentifier = { type: BladeActionType.contract, name: contract };
Player.sleeves[sleeveNumber].startWork(new SleeveBladeburnerWork({ actionId })); Player.sleeves[sleeveNumber].startWork(new SleeveBladeburnerWork({ actionId }));
} }
return Player.sleeves[sleeveNumber].bladeburner(action, contractName); return Player.sleeves[sleeveNumber].bladeburner(action, contract);
}, },
}; };

@ -8,7 +8,6 @@ import { parse } from "acorn";
import { LoadedModule, ScriptURL, ScriptModule } from "./Script/LoadedModule"; import { LoadedModule, ScriptURL, ScriptModule } from "./Script/LoadedModule";
import { Script } from "./Script/Script"; import { Script } from "./Script/Script";
import { ScriptFilePath, resolveScriptFilePath } from "./Paths/ScriptFilePath"; import { ScriptFilePath, resolveScriptFilePath } from "./Paths/ScriptFilePath";
import { root } from "./Paths/Directory";
// Acorn type def is straight up incomplete so we have to fill with our own. // Acorn type def is straight up incomplete so we have to fill with our own.
export type Node = any; export type Node = any;
@ -125,7 +124,7 @@ function generateLoadedModule(script: Script, scripts: Map<ScriptFilePath, Scrip
let newCode = script.code; let newCode = script.code;
// Loop through each node and replace the script name with a blob url. // Loop through each node and replace the script name with a blob url.
for (const node of importNodes) { for (const node of importNodes) {
const filename = resolveScriptFilePath(node.filename, root, ".js"); const filename = resolveScriptFilePath(node.filename, script.filename, ".js");
if (!filename) throw new Error(`Failed to parse import: ${node.filename}`); if (!filename) throw new Error(`Failed to parse import: ${node.filename}`);
// Find the corresponding script. // Find the corresponding script.
@ -149,6 +148,7 @@ function generateLoadedModule(script: Script, scripts: Map<ScriptFilePath, Scrip
// script dedupe properly. // script dedupe properly.
const adjustedCode = newCode + `\n//# sourceURL=${script.server}/${script.filename}`; const adjustedCode = newCode + `\n//# sourceURL=${script.server}/${script.filename}`;
// At this point we have the full code and can construct a new blob / assign the URL. // At this point we have the full code and can construct a new blob / assign the URL.
const url = URL.createObjectURL(makeScriptBlob(adjustedCode)) as ScriptURL; const url = URL.createObjectURL(makeScriptBlob(adjustedCode)) as ScriptURL;
const module = config.doImport(url).catch((e) => { const module = config.doImport(url).catch((e) => {
script.invalidateModule(); script.invalidateModule();

@ -361,14 +361,16 @@ export function getNextCompanyPosition(
return pos; return pos;
} }
export function quitJob(this: PlayerObject, company: CompanyName): void { export function quitJob(this: PlayerObject, company: CompanyName, suppressDialog?: boolean): void {
if (isCompanyWork(this.currentWork) && this.currentWork.companyName === company) { if (isCompanyWork(this.currentWork) && this.currentWork.companyName === company) {
this.finishWork(true); this.finishWork(true);
} }
for (const sleeve of this.sleeves) { for (const sleeve of this.sleeves) {
if (sleeve.currentWork?.type === SleeveWorkType.COMPANY && sleeve.currentWork.companyName === company) { if (sleeve.currentWork?.type === SleeveWorkType.COMPANY && sleeve.currentWork.companyName === company) {
sleeve.stopWork(); sleeve.stopWork();
dialogBoxCreate(`You quit ${company} while one of your sleeves was working there. The sleeve is now idle.`); if (!suppressDialog) {
dialogBoxCreate(`You quit ${company} while one of your sleeves was working there. The sleeve is now idle.`);
}
} }
} }
delete this.jobs[company]; delete this.jobs[company];
@ -529,20 +531,23 @@ export function gainCodingContractReward(
} }
} }
export function travel(this: PlayerObject, to: CityName): boolean { export function travel(this: PlayerObject, cityName: CityName): boolean {
if (Cities[to] == null) { if (Cities[cityName] == null) {
console.warn(`Player.travel() called with invalid city: ${to}`); throw new Error(`Player.travel() was called with an invalid city: ${cityName}`);
}
if (!this.canAfford(CONSTANTS.TravelCost)) {
return false; return false;
} }
this.city = to;
this.loseMoney(CONSTANTS.TravelCost, "other");
this.city = cityName;
return true; return true;
} }
export function gotoLocation(this: PlayerObject, to: LocationName): boolean { export function gotoLocation(this: PlayerObject, to: LocationName): boolean {
if (Locations[to] == null) { if (Locations[to] == null) {
console.warn(`Player.gotoLocation() called with invalid location: ${to}`); throw new Error(`Player.gotoLocation() was called with an invalid location: ${to}`);
return false;
} }
this.location = to; this.location = to;

@ -44,6 +44,7 @@ import { SleeveCrimeWork } from "./Work/SleeveCrimeWork";
import * as sleeveMethods from "./SleeveMethods"; import * as sleeveMethods from "./SleeveMethods";
import { calculateIntelligenceBonus } from "../formulas/intelligence"; import { calculateIntelligenceBonus } from "../formulas/intelligence";
import { getEnumHelper } from "../../utils/EnumHelper"; import { getEnumHelper } from "../../utils/EnumHelper";
import { Cities } from "../../Locations/Cities";
export class Sleeve extends Person implements SleevePerson { export class Sleeve extends Person implements SleevePerson {
currentWork: SleeveWork | null = null; currentWork: SleeveWork | null = null;
@ -255,13 +256,16 @@ export class Sleeve extends Person implements SleevePerson {
} }
/** Travel to another City. Costs money from player */ /** Travel to another City. Costs money from player */
travel(newCity: CityName): boolean { travel(cityName: CityName): boolean {
if (Cities[cityName] == null) {
throw new Error(`Sleeve.travel() was called with an invalid city: ${cityName}`);
}
if (!Player.canAfford(CONSTANTS.TravelCost)) { if (!Player.canAfford(CONSTANTS.TravelCost)) {
return false; return false;
} }
Player.loseMoney(CONSTANTS.TravelCost, "sleeves"); Player.loseMoney(CONSTANTS.TravelCost, "sleeves");
this.city = newCity; this.city = cityName;
return true; return true;
} }

@ -27,12 +27,13 @@ export function SleeveAugmentationsModal(props: IProps): React.ReactElement {
<Modal open={props.open} onClose={props.onClose}> <Modal open={props.open} onClose={props.onClose}>
<Container component={Paper} disableGutters maxWidth="lg" sx={{ mx: 0, mb: 1, p: 1 }}> <Container component={Paper} disableGutters maxWidth="lg" sx={{ mx: 0, mb: 1, p: 1 }}>
<Typography> <Typography>
You can purchase Augmentations for your Duplicate Sleeves. These Augmentations have the same effect as they You can purchase augmentations for your Sleeves. These augmentations have the same effect as they would for
would for you. You can only purchase Augmentations that you have unlocked through Factions. you. You can only purchase augmentations that you unlocked through factions. If an augmentation is useless for
Sleeves, it will not be available. Sleeves can install an augmentation without its prerequisites.
<br /> <br />
<br /> <br />
When purchasing an Augmentation for a Duplicate Sleeve, they are immediately installed. This means that the When purchasing an augmentation for a Sleeve, it is immediately installed. This means that the Sleeve will
Duplicate Sleeve will immediately lose all of its stat experience. immediately lose all of its stat experience.
<br /> <br />
<br /> <br />
Augmentations will appear below as they become available. Augmentations will appear below as they become available.

@ -33,7 +33,7 @@ interface IProps {
} }
export function StatsElement(props: IProps): React.ReactElement { export function StatsElement(props: IProps): React.ReactElement {
const classes = useStyles(); const { classes } = useStyles();
return ( return (
<Table sx={{ display: "table", mb: 1, width: "100%" }}> <Table sx={{ display: "table", mb: 1, width: "100%" }}>
@ -109,7 +109,7 @@ export function StatsElement(props: IProps): React.ReactElement {
} }
export function EarningsElement(props: IProps): React.ReactElement { export function EarningsElement(props: IProps): React.ReactElement {
const classes = useStyles(); const { classes } = useStyles();
let data: (string | JSX.Element)[][] = []; let data: (string | JSX.Element)[][] = [];
if (isSleeveCrimeWork(props.sleeve.currentWork)) { if (isSleeveCrimeWork(props.sleeve.currentWork)) {

@ -1,6 +1,5 @@
import React from "react"; import React from "react";
import { Button, Typography } from "@mui/material"; import { Button, Typography } from "@mui/material";
import { Player } from "@player";
import { CityName } from "@enums"; import { CityName } from "@enums";
import { Sleeve } from "../Sleeve"; import { Sleeve } from "../Sleeve";
import { CONSTANTS } from "../../../Constants"; import { CONSTANTS } from "../../../Constants";
@ -19,11 +18,10 @@ interface IProps {
export function TravelModal(props: IProps): React.ReactElement { export function TravelModal(props: IProps): React.ReactElement {
function travel(city: string): void { function travel(city: string): void {
if (!Player.canAfford(CONSTANTS.TravelCost)) { if (!props.sleeve.travel(city as CityName)) {
dialogBoxCreate("You cannot afford to have this sleeve travel to another city"); dialogBoxCreate("You cannot afford to have this sleeve travel to another city");
return;
} }
props.sleeve.city = city as CityName;
Player.loseMoney(CONSTANTS.TravelCost, "sleeves");
props.sleeve.stopWork(); props.sleeve.stopWork();
props.rerender(); props.rerender();
props.onClose(); props.onClose();

@ -14,7 +14,7 @@ import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator";
import { Script } from "./Script"; import { Script } from "./Script";
import { Node } from "../NetscriptJSEvaluator"; import { Node } from "../NetscriptJSEvaluator";
import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath"; import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath";
import { root } from "../Paths/Directory"; import { ServerName } from "../Types/strings";
export interface RamUsageEntry { export interface RamUsageEntry {
type: "ns" | "dom" | "fn" | "misc"; type: "ns" | "dom" | "fn" | "misc";
@ -56,12 +56,21 @@ function getNumericCost(cost: number | (() => number)): number {
* Parses code into an AST and walks through it recursively to calculate * Parses code into an AST and walks through it recursively to calculate
* RAM usage. Also accounts for imported modules. * RAM usage. Also accounts for imported modules.
* @param otherScripts - All other scripts on the server. Used to account for imported scripts * @param otherScripts - All other scripts on the server. Used to account for imported scripts
* @param code - The code being parsed */ * @param code - The code being parsed
function parseOnlyRamCalculate(otherScripts: Map<ScriptFilePath, Script>, code: string, ns1?: boolean): RamCalculation { * @param scriptname - The name of the script that ram needs to be added to
* @param server - Servername of the scripts for Error Message
* */
function parseOnlyRamCalculate(
otherScripts: Map<ScriptFilePath, Script>,
code: string,
scriptname: ScriptFilePath,
server: ServerName,
ns1?: boolean,
): RamCalculation {
/** /**
* Maps dependent identifiers to their dependencies. * Maps dependent identifiers to their dependencies.
* *
* The initial identifier is __SPECIAL_INITIAL_MODULE__.__GLOBAL__. * The initial identifier is <name of the main script>.__GLOBAL__.
* It depends on all the functions declared in the module, all the global scopes * It depends on all the functions declared in the module, all the global scopes
* of its imports, and any identifiers referenced in this global scope. Each * of its imports, and any identifiers referenced in this global scope. Each
* function depends on all the identifiers referenced internally. * function depends on all the identifiers referenced internally.
@ -74,10 +83,10 @@ function parseOnlyRamCalculate(otherScripts: Map<ScriptFilePath, Script>, code:
const completedParses = new Set(); const completedParses = new Set();
// Scripts we've discovered that need to be parsed. // Scripts we've discovered that need to be parsed.
const parseQueue: string[] = []; const parseQueue: ScriptFilePath[] = [];
// Parses a chunk of code with a given module name, and updates parseQueue and dependencyMap. // Parses a chunk of code with a given module name, and updates parseQueue and dependencyMap.
function parseCode(code: string, moduleName: string): void { function parseCode(code: string, moduleName: ScriptFilePath): void {
const result = parseOnlyCalculateDeps(code, moduleName); const result = parseOnlyCalculateDeps(code, moduleName, ns1);
completedParses.add(moduleName); completedParses.add(moduleName);
// Add any additional modules to the parse queue; // Add any additional modules to the parse queue;
@ -92,7 +101,7 @@ function parseOnlyRamCalculate(otherScripts: Map<ScriptFilePath, Script>, code:
} }
// Parse the initial module, which is the "main" script that is being run // Parse the initial module, which is the "main" script that is being run
const initialModule = "__SPECIAL_INITIAL_MODULE__"; const initialModule = scriptname;
parseCode(code, initialModule); parseCode(code, initialModule);
// Process additional modules, which occurs if the "main" script has any imports // Process additional modules, which occurs if the "main" script has any imports
@ -101,21 +110,19 @@ function parseOnlyRamCalculate(otherScripts: Map<ScriptFilePath, Script>, code:
if (nextModule === undefined) throw new Error("nextModule should not be undefined"); if (nextModule === undefined) throw new Error("nextModule should not be undefined");
if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue; if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue;
// Using root as the path base right now. Difficult to implement const script = otherScripts.get(nextModule);
const filename = resolveScriptFilePath(nextModule, root, ns1 ? ".script" : ".js");
if (!filename) {
return { errorCode: RamCalculationErrorCode.ImportError, errorMessage: `Invalid import path: "${nextModule}"` };
}
const script = otherScripts.get(filename);
if (!script) { if (!script) {
return { errorCode: RamCalculationErrorCode.ImportError, errorMessage: `No such file on server: "${filename}"` }; return {
errorCode: RamCalculationErrorCode.ImportError,
errorMessage: `File: "${nextModule}" not found on server: ${server}`,
};
} }
parseCode(script.code, nextModule); parseCode(script.code, nextModule);
} }
// Finally, walk the reference map and generate a ram cost. The initial set of keys to scan // Finally, walk the reference map and generate a ram cost. The initial set of keys to scan
// are those that start with __SPECIAL_INITIAL_MODULE__. // are those that start with the name of the main script.
let ram = RamCostConstants.Base; let ram = RamCostConstants.Base;
const detailedCosts: RamUsageEntry[] = [{ type: "misc", name: "baseCost", cost: RamCostConstants.Base }]; const detailedCosts: RamUsageEntry[] = [{ type: "misc", name: "baseCost", cost: RamCostConstants.Base }];
const unresolvedRefs = Object.keys(dependencyMap).filter((s) => s.startsWith(initialModule)); const unresolvedRefs = Object.keys(dependencyMap).filter((s) => s.startsWith(initialModule));
@ -250,7 +257,7 @@ export function checkInfiniteLoop(code: string): number[] {
interface ParseDepsResult { interface ParseDepsResult {
dependencyMap: Record<string, Set<string> | undefined>; dependencyMap: Record<string, Set<string> | undefined>;
additionalModules: string[]; additionalModules: ScriptFilePath[];
} }
/** /**
@ -259,7 +266,7 @@ interface ParseDepsResult {
* for RAM usage calculations. It also returns an array of additional modules * for RAM usage calculations. It also returns an array of additional modules
* that need to be parsed (i.e. are 'import'ed scripts). * that need to be parsed (i.e. are 'import'ed scripts).
*/ */
function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsResult { function parseOnlyCalculateDeps(code: string, currentModule: ScriptFilePath, ns1?: boolean): ParseDepsResult {
const ast = parse(code, { sourceType: "module", ecmaVersion: "latest" }); const ast = parse(code, { sourceType: "module", ecmaVersion: "latest" });
// Everything from the global scope goes in ".". Everything else goes in ".function", where only // Everything from the global scope goes in ".". Everything else goes in ".function", where only
// the outermost layer of functions counts. // the outermost layer of functions counts.
@ -271,7 +278,7 @@ function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsR
// Filled when we import names from other modules. // Filled when we import names from other modules.
const internalToExternal: Record<string, string | undefined> = {}; const internalToExternal: Record<string, string | undefined> = {};
const additionalModules: string[] = []; const additionalModules: ScriptFilePath[] = [];
// References get added pessimistically. They are added for thisModule.name, name, and for // References get added pessimistically. They are added for thisModule.name, name, and for
// any aliases. // any aliases.
@ -338,7 +345,12 @@ function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsR
Object.assign( Object.assign(
{ {
ImportDeclaration: (node: Node, st: State) => { ImportDeclaration: (node: Node, st: State) => {
const importModuleName = node.source.value; const importModuleName = resolveScriptFilePath(node.source.value, currentModule, ns1 ? ".script" : ".js");
if (!importModuleName)
throw new Error(
`ScriptFilePath couldnt be resolved in ImportDeclaration. Value: ${node.source.value} ScriptFilePath: ${currentModule}`,
);
additionalModules.push(importModuleName); additionalModules.push(importModuleName);
// This module's global scope refers to that module's global scope, no matter how we // This module's global scope refers to that module's global scope, no matter how we
@ -397,16 +409,21 @@ function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsR
/** /**
* Calculate's a scripts RAM Usage * Calculate's a scripts RAM Usage
* @param {string} code - The script's code * @param {string} code - The script's code
* @param {ScriptFilePath} scriptname - The script's name. Used to resolve relative paths
* @param {Script[]} otherScripts - All other scripts on the server. * @param {Script[]} otherScripts - All other scripts on the server.
* Used to account for imported scripts * Used to account for imported scripts
* @param {ServerName} server - Servername of the scripts for Error Message
* @param {boolean} ns1 - Deprecated: is the fileExtension .script or .js
*/ */
export function calculateRamUsage( export function calculateRamUsage(
code: string, code: string,
scriptname: ScriptFilePath,
otherScripts: Map<ScriptFilePath, Script>, otherScripts: Map<ScriptFilePath, Script>,
server: ServerName,
ns1?: boolean, ns1?: boolean,
): RamCalculation { ): RamCalculation {
try { try {
return parseOnlyRamCalculate(otherScripts, code, ns1); return parseOnlyRamCalculate(otherScripts, code, scriptname, server, ns1);
} catch (e) { } catch (e) {
return { return {
errorCode: RamCalculationErrorCode.SyntaxError, errorCode: RamCalculationErrorCode.SyntaxError,

@ -73,7 +73,13 @@ export class Script implements ContentFile {
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports * @param {Script[]} otherScripts - Other scripts on the server. Used to process imports
*/ */
updateRamUsage(otherScripts: Map<ScriptFilePath, Script>): void { updateRamUsage(otherScripts: Map<ScriptFilePath, Script>): void {
const ramCalc = calculateRamUsage(this.code, otherScripts, this.filename.endsWith(".script")); const ramCalc = calculateRamUsage(
this.code,
this.filename,
otherScripts,
this.server,
this.filename.endsWith(".script"),
);
if (ramCalc.cost && ramCalc.cost >= RamCostConstants.Base) { if (ramCalc.cost && ramCalc.cost >= RamCostConstants.Base) {
this.ramUsage = roundToTwo(ramCalc.cost); this.ramUsage = roundToTwo(ramCalc.cost);
this.ramUsageEntries = ramCalc.entries as RamUsageEntry[]; this.ramUsageEntries = ramCalc.entries as RamUsageEntry[];

@ -15,7 +15,7 @@ export function scriptCalculateOfflineProduction(runningScript: RunningScript):
//The Player object stores the last update time from when we were online //The Player object stores the last update time from when we were online
const thisUpdate = new Date().getTime(); const thisUpdate = new Date().getTime();
const lastUpdate = Player.lastUpdate; const lastUpdate = Player.lastUpdate;
const timePassed = (thisUpdate - lastUpdate) / 1000; //Seconds const timePassed = Math.max((thisUpdate - lastUpdate) / 1000, 0); //Seconds
//Calculate the "confidence" rating of the script's true production. This is based //Calculate the "confidence" rating of the script's true production. This is based
//entirely off of time. We will arbitrarily say that if a script has been running for //entirely off of time. We will arbitrarily say that if a script has been running for

@ -3132,7 +3132,7 @@ export interface Bladeburner {
* @param level - Optional number. Action level at which to calculate the gain. Will be the action's current level if not given. * @param level - Optional number. Action level at which to calculate the gain. Will be the action's current level if not given.
* @returns Average Bladeburner reputation gain for successfully completing the specified action. * @returns Average Bladeburner reputation gain for successfully completing the specified action.
*/ */
getActionRepGain(type: string, name: string, level: number): number; getActionRepGain(type: string, name: string, level?: number): number;
/** /**
* Get action count remaining. * Get action count remaining.
@ -3321,7 +3321,9 @@ export interface Bladeburner {
* @remarks * @remarks
* RAM cost: 4 GB * RAM cost: 4 GB
* *
* Returns the number of Bladeburner team members you have assigned to the specified action. * Returns the number of available Bladeburner team members.
* You can also pass the type and name of an action to get the number of
* Bladeburner team members you have assigned to the specified action.
* *
* Setting a team is only applicable for Operations and BlackOps. This function will return 0 for other action types. * Setting a team is only applicable for Operations and BlackOps. This function will return 0 for other action types.
* *
@ -3329,7 +3331,7 @@ export interface Bladeburner {
* @param name - Name of action. Must be an exact match. * @param name - Name of action. Must be an exact match.
* @returns Number of Bladeburner team members that were assigned to the specified action. * @returns Number of Bladeburner team members that were assigned to the specified action.
*/ */
getTeamSize(type: string, name: string): number; getTeamSize(type?: string, name?: string): number;
/** /**
* Set team size. * Set team size.
@ -4049,6 +4051,21 @@ export interface Go {
*/ */
getBoardState(): string[]; getBoardState(): string[];
/**
* Returns all the prior moves in the current game, as an array of simple board states.
*
* For example, a single 5x5 prior move board might look like this:
*
[<br/>
"XX.O.",<br/>
"X..OO",<br/>
".XO..",<br/>
"XXO.#",<br/>
".XO.#",<br/>
]
*/
getMoveHistory(): string[][];
/** /**
* Returns the color of the current player, or 'None' if the game is over. * Returns the color of the current player, or 'None' if the game is over.
* @returns "White" | "Black" | "None" * @returns "White" | "Black" | "None"
@ -4065,6 +4082,8 @@ export interface Go {
whiteScore: number; whiteScore: number;
blackScore: number; blackScore: number;
previousMove: [number, number] | null; previousMove: [number, number] | null;
komi: number;
bonusCycles: number;
}; };
/** /**
@ -4079,7 +4098,7 @@ export interface Go {
* *
* Note that some factions will have a few routers on the subnet at this state. * Note that some factions will have a few routers on the subnet at this state.
* *
* opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????", * opponent is "Netburners" or "Slum Snakes" or "The Black Hand" or "Tetrads" or "Daedalus" or "Illuminati" or "????????????" or "No AI",
* *
* @returns a simplified version of the board state as an array of strings representing the board columns. See ns.Go.getBoardState() for full details * @returns a simplified version of the board state as an array of strings representing the board columns. See ns.Go.getBoardState() for full details
* *

@ -8,11 +8,13 @@ import { useBoolean } from "../../ui/React/hooks";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { Options } from "./Options"; import { Options } from "./Options";
import { FilePath } from "../../Paths/FilePath";
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
export interface ScriptEditorContextShape { export interface ScriptEditorContextShape {
ram: string; ram: string;
ramEntries: string[][]; ramEntries: string[][];
updateRAM: (newCode: string | null, server: BaseServer | null) => void; updateRAM: (newCode: string | null, filename: FilePath | null, server: BaseServer | null) => void;
isUpdatingRAM: boolean; isUpdatingRAM: boolean;
startUpdatingRAM: () => void; startUpdatingRAM: () => void;
@ -28,14 +30,13 @@ export function ScriptEditorContextProvider({ children, vim }: { children: React
const [ram, setRAM] = useState("RAM: ???"); const [ram, setRAM] = useState("RAM: ???");
const [ramEntries, setRamEntries] = useState<string[][]>([["???", ""]]); const [ramEntries, setRamEntries] = useState<string[][]>([["???", ""]]);
const updateRAM: ScriptEditorContextShape["updateRAM"] = (newCode, server) => { const updateRAM: ScriptEditorContextShape["updateRAM"] = (newCode, filename, server) => {
if (newCode === null || server === null) { if (newCode == null || filename == null || server == null || !hasScriptExtension(filename)) {
setRAM("N/A"); setRAM("N/A");
setRamEntries([["N/A", ""]]); setRamEntries([["N/A", ""]]);
return; return;
} }
const ramUsage = calculateRamUsage(newCode, filename, server.scripts, server.hostname);
const ramUsage = calculateRamUsage(newCode, server.scripts);
if (ramUsage.cost && ramUsage.cost > 0) { if (ramUsage.cost && ramUsage.cost > 0) {
const entries = ramUsage.entries?.sort((a, b) => b.cost - a.cost) ?? []; const entries = ramUsage.entries?.sort((a, b) => b.cost - a.cost) ?? [];
const entriesDisp = []; const entriesDisp = [];

@ -143,6 +143,7 @@ function Root(props: IProps): React.ReactElement {
infLoop(newCode); infLoop(newCode);
updateRAM( updateRAM(
!currentScript || currentScript.isTxt ? null : newCode, !currentScript || currentScript.isTxt ? null : newCode,
currentScript && currentScript.path,
currentScript && GetServer(currentScript.hostname), currentScript && GetServer(currentScript.hostname),
); );
finishUpdatingRAM(); finishUpdatingRAM();
@ -372,7 +373,7 @@ function Root(props: IProps): React.ReactElement {
} }
} }
const { VimStatus } = useVimEditor({ const { statusBarRef } = useVimEditor({
editor: editorRef.current, editor: editorRef.current,
vim: options.vim, vim: options.vim,
onSave: save, onSave: save,
@ -410,7 +411,7 @@ function Root(props: IProps): React.ReactElement {
<div style={{ flex: "0 0 5px" }} /> <div style={{ flex: "0 0 5px" }} />
<Editor onMount={onMount} onChange={updateCode} onUnmount={onUnmountEditor} /> <Editor onMount={onMount} onChange={updateCode} onUnmount={onUnmountEditor} />
{VimStatus} {statusBarRef.current}
<Toolbar onSave={save} editor={editorRef.current} /> <Toolbar onSave={save} editor={editorRef.current} />
</div> </div>

@ -0,0 +1,248 @@
import { Input, Typography } from "@mui/material";
import { Modal } from "../../ui/React/Modal";
import { styled } from "@mui/material/styles";
import type { editor } from "monaco-editor";
import React from "react";
type IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
const StatusBarContainer = styled("div")({
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
height: 36,
marginLeft: 4,
marginRight: 4,
});
const StatusBarLeft = styled("div")({
display: "flex",
flexDirection: "row",
alignItems: "center",
});
// This Class is injected into the monaco-vim initVimMode function to override the status bar.
export class StatusBar {
// modeInfoNode is used to display the mode in the status bar.
// notifNode is used to display notifications in the status bar.
// secInfoNode is used to display the input box in the status bar.
// keyInfoNode is used to display the operator and count in the status bar.
// editor is kept to focus when closing the input box.
// sanitizer is weird.
node: React.MutableRefObject<React.ReactElement | null>;
editor: IStandaloneCodeEditor;
mode = "--NORMAL--";
showInput = false;
inputValue = "";
buffer = "";
notification = "";
showRegisters = false;
registers: Record<string, string> = {};
rerender: () => void;
onCloseHandler: ((query: string) => void) | null;
onKeyDownHandler: ((e: React.KeyboardEvent, query: string, close: () => void) => void) | null;
onKeyUpHandler: ((e: React.KeyboardEvent, query: string, close: () => void) => void) | null;
// node is used to setup the status bar. However, we use it to forward the resulting status bar to the outside.
// sanitizer is weird, so we use it to forward a rerender hook.
constructor(
node: React.MutableRefObject<React.ReactElement | null>,
editor: IStandaloneCodeEditor,
rerender: () => void,
) {
this.node = node;
this.editor = editor;
this.rerender = rerender;
this.onCloseHandler = null;
this.onKeyDownHandler = null;
this.onKeyUpHandler = null;
this.update();
}
// this is used to change the mode shown in the status bar.
setMode(ev: { mode: string; subMode?: string }) {
if (ev.mode === "visual") {
if (ev.subMode === "linewise") {
this.mode = "--VISUAL LINE--";
} else if (ev.subMode === "blockwise") {
this.mode = "--VISUAL BLOCK--";
} else {
this.mode = "--VISUAL--";
}
} else {
this.mode = `--${ev.mode.toUpperCase()}--`;
}
this.update();
}
// this is used to set the current operator, like d, r, etc or the count, like 5j, 3w, etc.
setKeyBuffer(key: string) {
this.buffer = key;
this.update();
}
// this is used to set the input box.
setSec(
// this is the created HTML element from monaco-vim. We're not going to use it, so it is marked as unused.
__text: HTMLElement,
// this is used to close the input box and set the cursor back to the line in the monaco-editor. query is the text in the input box.
onClose: ((query: string) => void) | null,
options: {
// This handles ESC, Backspace when input is empty, CTRL-C and CTRL-[. query is the text in the input box. close is a function that closes the input box. e is the key event.
onKeyDown: ((e: React.KeyboardEvent, query: string, close: () => void) => void) | undefined;
// This handles all other key events. query is the text in the input box. close is a function that closes the input box. e is the key event.
onKeyUp: ((e: React.KeyboardEvent, query: string, close: () => void) => void) | undefined;
// this is a default value for the input box. The box should be empty if this is not set.
value: string | undefined;
},
) {
this.onCloseHandler = onClose;
this.onKeyDownHandler = options.onKeyDown ?? null;
this.onKeyUpHandler = options.onKeyUp ?? null;
this.inputValue = options.value || "";
this.showInput = true;
this.update();
}
// this is used to toggle showing the status bar.
toggleVisibility(toggle: boolean) {
if (toggle) {
this.node.current = this.StatusBar();
} else {
this.node.current = null;
}
}
// this is used to close the input box.
closeInput = () => {
this.showInput = false;
this.update();
this.editor.focus();
};
// this is used to clean up the status bar on unmount.
clear = () => {
this.node.current = null;
};
parseRegisters = (html: HTMLElement) => {
this.registers = {};
const registerValues = html.innerText.split("\n").filter((s) => s.startsWith('"'));
for (const registerValue of registerValues) {
const name = registerValue[1];
const value = registerValue.slice(2).trim();
this.registers[name] = value;
}
console.log(this.registers);
};
// this is used to show notifications.
// The package passes a new HTMLElement, but we only want to show the text.
showNotification(html: HTMLElement) {
// Registers are too big for the status bar, so we show them in a modal.
if (html.innerText.startsWith("----------Registers----------\n\n") && html.innerText.length > 31) {
this.parseRegisters(html);
this.showRegisters = true;
return this.update();
}
if (html.innerText.startsWith("----------Registers----------\n\n")) this.notification = "Registers are empty";
else this.notification = html.innerText;
const currentNotification = this.notification;
setTimeout(() => {
// don't update if the notification has changed in the meantime
if (this.notification !== currentNotification) return;
this.notification = "";
this.update();
}, 5000);
this.update();
}
update = () => {
this.node.current = this.StatusBar();
this.rerender();
};
keyUp = (e: React.KeyboardEvent) => {
if (this.onKeyUpHandler !== null) {
this.onKeyUpHandler(e, this.inputValue, this.closeInput);
} else {
// if the player somehow gets stuck here, they can also press enter to close the input box.
if (e.key === "Enter") this.closeInput();
}
};
keyDown = (e: React.KeyboardEvent) => {
if (this.onKeyDownHandler !== null) {
this.onKeyDownHandler(e, this.inputValue, this.closeInput);
}
// this handles pressing escape in the input box.
if (e.key === "Escape") {
e.stopPropagation();
this.closeInput();
}
// this handles pressing enter in the input box.
if (e.key === "Enter" && this.onCloseHandler !== null) {
e.stopPropagation();
e.preventDefault();
this.onCloseHandler(this.inputValue);
this.closeInput();
}
};
handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
this.inputValue = e.target.value;
this.update();
};
StatusBar(): React.ReactElement {
const registers = Object.entries(this.registers).map(([name, value]) => `[${name}]: ${value.slice(0, 100)}`);
return (
<StatusBarContainer>
<Modal
open={this.showRegisters}
onClose={() => {
this.showRegisters = false;
this.update();
}}
removeFocus={false}
>
{registers.map((registers) => (
<Typography key={registers}>{registers}</Typography>
))}
</Modal>
<StatusBarLeft>
<Typography sx={{ mr: 4 }}>{this.mode}</Typography>
{this.showInput && (
<Input
value={this.inputValue}
onChange={this.handleInput}
onKeyUp={this.keyUp}
onKeyDown={this.keyDown}
autoFocus={true}
sx={{ mr: 4 }}
/>
)}
<Typography overflow="hidden">{this.notification}</Typography>
</StatusBarLeft>
<Typography>{this.buffer}</Typography>
</StatusBarContainer>
);
}
}

@ -4,10 +4,10 @@ import * as MonacoVim from "monaco-vim";
import type { editor } from "monaco-editor"; import type { editor } from "monaco-editor";
type IStandaloneCodeEditor = editor.IStandaloneCodeEditor; type IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
import Box from "@mui/material/Box";
import { Router } from "../../ui/GameRoot"; import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router"; import { Page } from "../../ui/Router";
import { StatusBar } from "./StatusBar";
import { useRerender } from "../../ui/React/hooks";
interface IProps { interface IProps {
vim: boolean; vim: boolean;
@ -21,7 +21,8 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
// monaco-vim does not have types, so this is an any // monaco-vim does not have types, so this is an any
const [vimEditor, setVimEditor] = useState<any>(null); const [vimEditor, setVimEditor] = useState<any>(null);
const vimStatusRef = useRef<HTMLElement>(null); const statusBarRef = useRef<React.ReactElement | null>(null);
const rerender = useRerender();
const actionsRef = useRef({ save: onSave, openNextTab: onOpenNextTab, openPreviousTab: onOpenPreviousTab }); const actionsRef = useRef({ save: onSave, openNextTab: onOpenNextTab, openPreviousTab: onOpenPreviousTab });
actionsRef.current = { save: onSave, openNextTab: onOpenNextTab, openPreviousTab: onOpenPreviousTab }; actionsRef.current = { save: onSave, openNextTab: onOpenNextTab, openPreviousTab: onOpenPreviousTab };
@ -31,7 +32,7 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
if (vim && editor && !vimEditor) { if (vim && editor && !vimEditor) {
// Using try/catch because MonacoVim does not have types. // Using try/catch because MonacoVim does not have types.
try { try {
setVimEditor(MonacoVim.initVimMode(editor, vimStatusRef.current)); setVimEditor(MonacoVim.initVimMode(editor, statusBarRef, StatusBar, rerender));
MonacoVim.VimMode.Vim.defineEx("write", "w", function () { MonacoVim.VimMode.Vim.defineEx("write", "w", function () {
// your own implementation on what you want to do when :w is pressed // your own implementation on what you want to do when :w is pressed
actionsRef.current.save(); actionsRef.current.save();
@ -40,6 +41,10 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
Router.toPage(Page.Terminal); Router.toPage(Page.Terminal);
}); });
// Remove any macro recording, since it isn't supported.
MonacoVim.VimMode.Vim.mapCommand("q", "", "", null, { context: "normal" });
MonacoVim.VimMode.Vim.mapCommand("@", "", "", null, { context: "normal" });
const saveNQuit = (): void => { const saveNQuit = (): void => {
actionsRef.current.save(); actionsRef.current.save();
Router.toPage(Page.Terminal); Router.toPage(Page.Terminal);
@ -72,19 +77,7 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
return () => { return () => {
vimEditor?.dispose(); vimEditor?.dispose();
}; };
}, [vim, editor, vimEditor]); }, [vim, editor, vimEditor, rerender]);
const VimStatus = ( return { statusBarRef };
<Box
ref={vimStatusRef}
className="vim-display"
display="flex"
flexGrow="0"
flexDirection="row"
sx={{ p: 1 }}
alignItems="center"
/>
);
return { VimStatus };
} }

@ -154,6 +154,12 @@ export function initForeignServers(homeComputer: Server): void {
if (metadata.serverGrowth) serverParams.serverGrowth = toNumber(metadata.serverGrowth); if (metadata.serverGrowth) serverParams.serverGrowth = toNumber(metadata.serverGrowth);
const server = new Server(serverParams); const server = new Server(serverParams);
if (metadata.networkLayer) {
const layer = toNumber(metadata.networkLayer);
server.cpuCores = getRandomIntInclusive(Math.ceil(layer / 2), layer);
}
for (const filename of metadata.literature || []) { for (const filename of metadata.literature || []) {
server.messages.push(filename); server.messages.push(filename);
} }

@ -1,8 +1,7 @@
import React, { useMemo, useCallback, useState, useEffect } from "react"; import React, { useMemo, useCallback, useState, useEffect } from "react";
import { KEYCODE } from "../../utils/helpers/keyCodes"; import { KEYCODE } from "../../utils/helpers/keyCodes";
import { styled, Theme, CSSObject } from "@mui/material/styles"; import { styled, Theme, CSSObject } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles"; import { makeStyles } from "tss-react/mui";
import makeStyles from "@mui/styles/makeStyles";
import MuiDrawer from "@mui/material/Drawer"; import MuiDrawer from "@mui/material/Drawer";
import List from "@mui/material/List"; import List from "@mui/material/List";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
@ -101,14 +100,12 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== "open"
}), }),
})); }));
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles()((theme: Theme) => ({
createStyles({ active: {
active: { borderLeft: "3px solid " + theme.palette.primary.main,
borderLeft: "3px solid " + theme.palette.primary.main, },
}, listitem: {},
listitem: {}, }));
}),
);
export function SidebarRoot(props: { page: Page }): React.ReactElement { export function SidebarRoot(props: { page: Page }): React.ReactElement {
useRerender(200); useRerender(200);
@ -257,7 +254,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
return () => document.removeEventListener("keydown", handleShortcuts); return () => document.removeEventListener("keydown", handleShortcuts);
}, [canJob, clickPage, props.page]); }, [canJob, clickPage, props.page]);
const classes = useStyles(); const { classes } = useStyles();
const [open, setOpen] = useState(Settings.IsSidebarOpened); const [open, setOpen] = useState(Settings.IsSidebarOpened);
const toggleDrawer = (): void => const toggleDrawer = (): void =>
setOpen((old) => { setOpen((old) => {

@ -148,8 +148,7 @@ export const HelpTexts: Record<string, string[]> = {
cd: [ cd: [
"Usage: cd [dir]", "Usage: cd [dir]",
" ", " ",
"Change to the specified directory. Note that this works even for directories that don't exist. If you ", "Change to the specified directory. You cannot change to a directory that does not exist. Examples:",
"change to a directory that does not exist, it will not be 'created'. Examples:",
" ", " ",
" cd scripts/hacking", " cd scripts/hacking",
" ", " ",

@ -211,7 +211,7 @@ export class Terminal {
Router.toPage(Page.BitVerse, { flume: false, quick: false }); Router.toPage(Page.BitVerse, { flume: false, quick: false });
return; return;
} }
// Manunally check for faction invites // Manually check for faction invitations
Engine.Counters.checkFactionInvitations = 0; Engine.Counters.checkFactionInvitations = 0;
Engine.checkCounters(); Engine.checkCounters();
@ -225,7 +225,9 @@ export class Terminal {
server.moneyAvailable -= moneyGained; server.moneyAvailable -= moneyGained;
Player.gainMoney(moneyGained, "hacking"); Player.gainMoney(moneyGained, "hacking");
Player.gainHackingExp(expGainedOnSuccess); Player.gainHackingExp(expGainedOnSuccess);
Player.gainIntelligenceExp(expGainedOnSuccess / CONSTANTS.IntelligenceTerminalHackBaseExpGain); if (expGainedOnSuccess > 1) {
Player.gainIntelligenceExp(4 * Math.log10(expGainedOnSuccess));
}
const oldSec = server.hackDifficulty; const oldSec = server.hackDifficulty;
server.fortify(ServerConstants.ServerFortifyAmount); server.fortify(ServerConstants.ServerFortifyAmount);

@ -7,8 +7,7 @@ import type { ProgramFilePath } from "../../Paths/ProgramFilePath";
import type { ContentFilePath } from "../../Paths/ContentFile"; import type { ContentFilePath } from "../../Paths/ContentFile";
import type { ScriptFilePath } from "../../Paths/ScriptFilePath"; import type { ScriptFilePath } from "../../Paths/ScriptFilePath";
import createStyles from "@mui/styles/createStyles"; import { makeStyles } from "tss-react/mui";
import makeStyles from "@mui/styles/makeStyles";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { Router } from "../../ui/GameRoot"; import { Router } from "../../ui/GameRoot";
import { Page } from "../../ui/Router"; import { Page } from "../../ui/Router";
@ -109,7 +108,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
folders.sort(); folders.sort();
function SegmentGrid(props: { colSize: string; children: React.ReactChild[] }): React.ReactElement { function SegmentGrid(props: { colSize: string; children: React.ReactChild[] }): React.ReactElement {
const classes = makeStyles({ const { classes } = makeStyles()({
segmentGrid: { segmentGrid: {
display: "grid", display: "grid",
gridTemplateColumns: "repeat(auto-fill, var(--colSize))", gridTemplateColumns: "repeat(auto-fill, var(--colSize))",
@ -123,15 +122,13 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
); );
} }
function ClickableContentFileLink(props: { path: ScriptFilePath | TextFilePath }): React.ReactElement { function ClickableContentFileLink(props: { path: ScriptFilePath | TextFilePath }): React.ReactElement {
const classes = makeStyles((theme: Theme) => const { classes } = makeStyles()((theme: Theme) => ({
createStyles({ link: {
link: { cursor: "pointer",
cursor: "pointer", textDecorationLine: "underline",
textDecorationLine: "underline", color: theme.palette.warning.main,
color: theme.palette.warning.main, },
}, }))();
}),
)();
const fullPath = combinePath(baseDirectory, props.path); const fullPath = combinePath(baseDirectory, props.path);
function onClick() { function onClick() {
let content; let content;
@ -155,7 +152,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
} }
function ClickableMessageLink(props: { path: FilePath }): React.ReactElement { function ClickableMessageLink(props: { path: FilePath }): React.ReactElement {
const classes = makeStyles({ const { classes } = makeStyles()({
link: { link: {
cursor: "pointer", cursor: "pointer",
textDecorationLine: "underline", textDecorationLine: "underline",

@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import { createStyles, makeStyles } from "@mui/styles"; import { makeStyles } from "tss-react/mui";
import { Paper, Popper, TextField, Typography } from "@mui/material"; import { Paper, Popper, TextField, Typography } from "@mui/material";
import { KEY, KEYCODE } from "../../utils/helpers/keyCodes"; import { KEY, KEYCODE } from "../../utils/helpers/keyCodes";
@ -10,29 +10,27 @@ import { getTabCompletionPossibilities } from "../getTabCompletionPossibilities"
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { longestCommonStart } from "../../utils/StringHelperFunctions"; import { longestCommonStart } from "../../utils/StringHelperFunctions";
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles()((theme: Theme) => ({
createStyles({ input: {
input: { backgroundColor: theme.colors.backgroundprimary,
backgroundColor: theme.colors.backgroundprimary, },
}, nopadding: {
nopadding: { padding: theme.spacing(0),
padding: theme.spacing(0), },
}, preformatted: {
preformatted: { margin: theme.spacing(0),
margin: theme.spacing(0), },
}, absolute: {
absolute: { margin: theme.spacing(0),
margin: theme.spacing(0), position: "absolute",
position: "absolute", bottom: "12px",
bottom: "12px", opacity: "0.75",
opacity: "0.75", maxWidth: "100%",
maxWidth: "100%", whiteSpace: "pre",
whiteSpace: "pre", overflow: "hidden",
overflow: "hidden", pointerEvents: "none",
pointerEvents: "none", },
}, }));
}),
);
// Save command in case we de-load this screen. // Save command in case we de-load this screen.
let command = ""; let command = "";
@ -46,7 +44,7 @@ export function TerminalInput(): React.ReactElement {
const [searchResults, setSearchResults] = useState<string[]>([]); const [searchResults, setSearchResults] = useState<string[]>([]);
const [searchResultsIndex, setSearchResultsIndex] = useState(0); const [searchResultsIndex, setSearchResultsIndex] = useState(0);
const [autofilledValue, setAutofilledValue] = useState(false); const [autofilledValue, setAutofilledValue] = useState(false);
const classes = useStyles(); const { classes } = useStyles();
// If we have no data in the current terminal history, let's initialize it from the player save // If we have no data in the current terminal history, let's initialize it from the player save
if (Terminal.commandHistory.length === 0 && Player.terminalCommandHistory.length > 0) { if (Terminal.commandHistory.length === 0 && Player.terminalCommandHistory.length > 0) {

@ -1,8 +1,7 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { Link as MuiLink, Typography } from "@mui/material"; import { Link as MuiLink, Typography } from "@mui/material";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles"; import { makeStyles } from "tss-react/mui";
import createStyles from "@mui/styles/createStyles";
import _ from "lodash"; import _ from "lodash";
import { Output, Link, RawOutput } from "../OutputTypes"; import { Output, Link, RawOutput } from "../OutputTypes";
@ -16,27 +15,25 @@ import { ANSIITypography } from "../../ui/React/ANSIITypography";
import { useRerender } from "../../ui/React/hooks"; import { useRerender } from "../../ui/React/hooks";
import { TerminalActionTimer } from "./TerminalActionTimer"; import { TerminalActionTimer } from "./TerminalActionTimer";
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles()((theme: Theme) => ({
createStyles({ container: {
container: { display: "flex",
display: "flex", flexDirection: "column",
flexDirection: "column", height: "calc(100vh - 16px)",
height: "calc(100vh - 16px)", },
}, entries: {
entries: { padding: 0,
padding: 0, overflow: "scroll",
overflow: "scroll", flex: "0 1 auto",
flex: "0 1 auto", margin: "auto 0 0",
margin: "auto 0 0", },
}, preformatted: {
preformatted: { whiteSpace: "pre-wrap",
whiteSpace: "pre-wrap", overflowWrap: "anywhere",
overflowWrap: "anywhere", margin: theme.spacing(0),
margin: theme.spacing(0), width: "100%",
width: "100%", },
}, }));
}),
);
export function TerminalRoot(): React.ReactElement { export function TerminalRoot(): React.ReactElement {
const scrollHook = useRef<HTMLUListElement>(null); const scrollHook = useRef<HTMLUListElement>(null);
@ -82,7 +79,7 @@ export function TerminalRoot(): React.ReactElement {
}; };
}, []); }, []);
const classes = useStyles(); const { classes } = useStyles();
return ( return (
<div className={classes.container}> <div className={classes.container}>
<ul key={key} id="terminal" className={classes.entries} ref={scrollHook}> <ul key={key} id="terminal" className={classes.entries} ref={scrollHook}>

@ -21,10 +21,11 @@ export class CreateProgramWork extends Work {
programName: CompletedProgramName; programName: CompletedProgramName;
// amount of effective work completed on the program (time boosted by skills). // amount of effective work completed on the program (time boosted by skills).
unitCompleted: number; unitCompleted: number;
unitRate: number;
constructor(params?: CreateProgramWorkParams) { constructor(params?: CreateProgramWorkParams) {
super(WorkType.CREATE_PROGRAM, params?.singularity ?? true); super(WorkType.CREATE_PROGRAM, params?.singularity ?? true);
this.unitCompleted = 0; this.unitCompleted = 0;
this.unitRate = 0;
this.programName = params?.programName ?? CompletedProgramName.bruteSsh; this.programName = params?.programName ?? CompletedProgramName.bruteSsh;
if (params) { if (params) {
@ -63,7 +64,8 @@ export class CreateProgramWork extends Work {
skillMult *= focusBonus; skillMult *= focusBonus;
//Skill multiplier directly applied to "time worked" //Skill multiplier directly applied to "time worked"
this.cyclesWorked += cycles; this.cyclesWorked += cycles;
this.unitCompleted += CONSTANTS.MilliPerCycle * cycles * skillMult; this.unitRate = CONSTANTS.MilliPerCycle * cycles * skillMult;
this.unitCompleted += this.unitRate;
if (this.unitCompleted >= this.unitNeeded()) { if (this.unitCompleted >= this.unitNeeded()) {
return true; return true;

@ -21,10 +21,12 @@ interface GraftingWorkParams {
export class GraftingWork extends Work { export class GraftingWork extends Work {
augmentation: AugmentationName; augmentation: AugmentationName;
unitCompleted: number; unitCompleted: number;
unitRate: number;
constructor(params?: GraftingWorkParams) { constructor(params?: GraftingWorkParams) {
super(WorkType.GRAFTING, params?.singularity ?? true); super(WorkType.GRAFTING, params?.singularity ?? true);
this.unitCompleted = 0; this.unitCompleted = 0;
this.unitRate = 0;
this.augmentation = params?.augmentation ?? AugmentationName.Targeting1; this.augmentation = params?.augmentation ?? AugmentationName.Targeting1;
const gAugs = GraftableAugmentations(); const gAugs = GraftableAugmentations();
if (params) Player.loseMoney(gAugs[this.augmentation].cost, "augmentations"); if (params) Player.loseMoney(gAugs[this.augmentation].cost, "augmentations");
@ -37,8 +39,8 @@ export class GraftingWork extends Work {
process(cycles: number): boolean { process(cycles: number): boolean {
const focusBonus = Player.focusPenalty(); const focusBonus = Player.focusPenalty();
this.cyclesWorked += cycles; this.cyclesWorked += cycles;
this.unitCompleted += CONSTANTS.MilliPerCycle * cycles * graftingIntBonus() * focusBonus; this.unitRate = CONSTANTS.MilliPerCycle * cycles * graftingIntBonus() * focusBonus;
this.unitCompleted += this.unitRate;
return this.unitCompleted >= this.unitNeeded(); return this.unitCompleted >= this.unitNeeded();
} }

@ -44,6 +44,11 @@ import { setupUncaughtPromiseHandler } from "./UncaughtPromiseHandler";
import { Button, Typography } from "@mui/material"; import { Button, Typography } from "@mui/material";
import { SnackbarEvents } from "./ui/React/Snackbar"; import { SnackbarEvents } from "./ui/React/Snackbar";
import { SaveData } from "./types"; import { SaveData } from "./types";
import { Go } from "./Go/Go";
function showWarningAboutSystemClock() {
AlertEvents.emit("Warning: The system clock moved backward.");
}
/** Game engine. Handles the main game loop. */ /** Game engine. Handles the main game loop. */
const Engine: { const Engine: {
@ -246,7 +251,13 @@ const Engine: {
// Calculate the number of cycles have elapsed while offline // Calculate the number of cycles have elapsed while offline
Engine._lastUpdate = new Date().getTime(); Engine._lastUpdate = new Date().getTime();
const lastUpdate = Player.lastUpdate; const lastUpdate = Player.lastUpdate;
const timeOffline = Engine._lastUpdate - lastUpdate; let timeOffline = Engine._lastUpdate - lastUpdate;
if (timeOffline < 0) {
timeOffline = 0;
setTimeout(() => {
showWarningAboutSystemClock();
}, 250);
}
const numCyclesOffline = Math.floor(timeOffline / CONSTANTS.MilliPerCycle); const numCyclesOffline = Math.floor(timeOffline / CONSTANTS.MilliPerCycle);
// Calculate the number of chances for a contract the player had whilst offline // Calculate the number of chances for a contract the player had whilst offline
@ -330,6 +341,8 @@ const Engine: {
// Bladeburner offline progress // Bladeburner offline progress
if (Player.bladeburner) Player.bladeburner.storeCycles(numCyclesOffline); if (Player.bladeburner) Player.bladeburner.storeCycles(numCyclesOffline);
Go.storeCycles(numCyclesOffline);
staneksGift.process(numCyclesOffline); staneksGift.process(numCyclesOffline);
// Sleeves offline progress // Sleeves offline progress
@ -390,6 +403,12 @@ const Engine: {
// Get time difference // Get time difference
const _thisUpdate = new Date().getTime(); const _thisUpdate = new Date().getTime();
let diff = _thisUpdate - Engine._lastUpdate; let diff = _thisUpdate - Engine._lastUpdate;
if (diff < 0) {
diff = 0;
Engine._lastUpdate = _thisUpdate;
Player.lastUpdate = _thisUpdate;
showWarningAboutSystemClock();
}
const offset = diff % CONSTANTS.MilliPerCycle; const offset = diff % CONSTANTS.MilliPerCycle;
// Divide this by cycle time to determine how many cycles have elapsed since last update // Divide this by cycle time to determine how many cycles have elapsed since last update

Some files were not shown because too many files have changed in this diff Show More