mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-24 00:23:49 +01:00
merge dev
This commit is contained in:
commit
1294f8f045
@ -9,7 +9,7 @@ Get the reputation gain of an action.
|
||||
**Signature:**
|
||||
|
||||
```typescript
|
||||
getActionRepGain(type: string, name: string, level: number): number;
|
||||
getActionRepGain(type: string, name: string, level?: number): number;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
@ -18,7 +18,7 @@ getActionRepGain(type: string, name: string, level: number): number;
|
||||
| --- | --- | --- |
|
||||
| type | string | Type of action. |
|
||||
| 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:**
|
||||
|
||||
|
@ -9,15 +9,15 @@ Get team size.
|
||||
**Signature:**
|
||||
|
||||
```typescript
|
||||
getTeamSize(type: string, name: string): number;
|
||||
getTeamSize(type?: string, name?: string): number;
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| type | string | Type of action. |
|
||||
| name | string | Name of action. Must be an exact match. |
|
||||
| type | string | _(Optional)_ Type of action. |
|
||||
| name | string | _(Optional)_ Name of action. Must be an exact match. |
|
||||
|
||||
**Returns:**
|
||||
|
||||
@ -29,7 +29,7 @@ Number of Bladeburner team members that were assigned to the specified action.
|
||||
|
||||
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.
|
||||
|
||||
|
@ -14,9 +14,11 @@ getGameState(): {
|
||||
whiteScore: number;
|
||||
blackScore: number;
|
||||
previousMove: [number, number] | null;
|
||||
komi: number;
|
||||
bonusCycles: number;
|
||||
};
|
||||
```
|
||||
**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; }
|
||||
|
||||
|
21
markdown/bitburner.go.getmovehistory.md
Normal file
21
markdown/bitburner.go.getmovehistory.md
Normal file
@ -0,0 +1,21 @@
|
||||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [bitburner](./bitburner.md) > [Go](./bitburner.go.md) > [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> |
|
||||
| [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. |
|
||||
| [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. |
|
||||
| [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. |
|
||||
| [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.
|
||||
|
||||
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:**
|
||||
|
||||
|
27
package-lock.json
generated
27
package-lock.json
generated
@ -43,7 +43,8 @@
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sprintf-js": "^1.1.3"
|
||||
"sprintf-js": "^1.1.3",
|
||||
"tss-react": "^4.9.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.0",
|
||||
@ -16896,6 +16897,30 @@
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
|
11
package.json
11
package.json
@ -43,7 +43,8 @@
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"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",
|
||||
"devDependencies": {
|
||||
@ -86,13 +87,13 @@
|
||||
"prettier": "^2.8.8",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-refresh": "^0.14.0",
|
||||
"rehype-mathjax": "^4.0.3",
|
||||
"remark-math": "^5.1.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"typescript": "^5.2.2",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.2",
|
||||
"remark-math": "^5.1.1",
|
||||
"rehype-mathjax": "^4.0.3"
|
||||
"webpack-dev-server": "^4.15.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@ -118,7 +119,7 @@
|
||||
"watch": "webpack --watch --mode production",
|
||||
"watch:dev": "webpack --watch --mode development",
|
||||
"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",
|
||||
"version": "sh ./tools/build-release.sh && git add --all",
|
||||
"postversion": "git push -u origin dev && git push --tags",
|
||||
|
@ -19,7 +19,6 @@ import { HacknetNode } from "../Hacknet/HacknetNode";
|
||||
import { HacknetServer } from "../Hacknet/HacknetServer";
|
||||
import { Player } from "@player";
|
||||
import { GetAllServers, GetServer } from "../Server/AllServers";
|
||||
import { SpecialServers } from "../Server/data/SpecialServers";
|
||||
import { Server } from "../Server/Server";
|
||||
import { Router } from "../ui/GameRoot";
|
||||
import { Page } from "../ui/Router";
|
||||
@ -30,7 +29,7 @@ import { workerScripts } from "../Netscript/WorkerScripts";
|
||||
|
||||
import { getRecordValues } from "../Types/Record";
|
||||
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...
|
||||
const achievementData = (<AchievementDataJson>(<unknown>data)).achievements;
|
||||
@ -61,15 +60,8 @@ export interface AchievementData {
|
||||
Description: string;
|
||||
}
|
||||
|
||||
function bitNodeFinishedState(): boolean {
|
||||
const wd = GetServer(SpecialServers.WorldDaemon);
|
||||
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 canAccessBitNodeFeature(bitNode: number): boolean {
|
||||
return Player.bitNodeN === bitNode || Player.sourceFileLvl(bitNode) > 0;
|
||||
}
|
||||
|
||||
function knowsAboutBitverse(): boolean {
|
||||
@ -338,25 +330,25 @@ export const achievements: Record<string, Achievement> = {
|
||||
GANG: {
|
||||
...achievementData.GANG,
|
||||
Icon: "GANG",
|
||||
Visible: () => hasAccessToSF(2),
|
||||
Visible: () => canAccessBitNodeFeature(2),
|
||||
Condition: () => Player.gang !== null,
|
||||
},
|
||||
FULL_GANG: {
|
||||
...achievementData.FULL_GANG,
|
||||
Icon: "GANGMAX",
|
||||
Visible: () => hasAccessToSF(2),
|
||||
Visible: () => canAccessBitNodeFeature(2),
|
||||
Condition: () => Player.gang !== null && Player.gang.members.length === GangConstants.MaximumGangMembers,
|
||||
},
|
||||
GANG_TERRITORY: {
|
||||
...achievementData.GANG_TERRITORY,
|
||||
Icon: "GANG100%",
|
||||
Visible: () => hasAccessToSF(2),
|
||||
Visible: () => canAccessBitNodeFeature(2),
|
||||
Condition: () => Player.gang !== null && AllGangs[Player.gang.facName].territory >= 0.999,
|
||||
},
|
||||
GANG_MEMBER_POWER: {
|
||||
...achievementData.GANG_MEMBER_POWER,
|
||||
Icon: "GANG10000",
|
||||
Visible: () => hasAccessToSF(2),
|
||||
Visible: () => canAccessBitNodeFeature(2),
|
||||
Condition: () =>
|
||||
Player.gang !== null &&
|
||||
Player.gang.members.some(
|
||||
@ -367,19 +359,19 @@ export const achievements: Record<string, Achievement> = {
|
||||
CORPORATION: {
|
||||
...achievementData.CORPORATION,
|
||||
Icon: "CORP",
|
||||
Visible: () => hasAccessToSF(3),
|
||||
Visible: () => canAccessBitNodeFeature(3),
|
||||
Condition: () => Player.corporation !== null,
|
||||
},
|
||||
CORPORATION_BRIBE: {
|
||||
...achievementData.CORPORATION_BRIBE,
|
||||
Icon: "CORPLOBBY",
|
||||
Visible: () => hasAccessToSF(3),
|
||||
Visible: () => canAccessBitNodeFeature(3),
|
||||
Condition: () => !!Player.corporation && Player.corporation.unlocks.has(CorpUnlockName.GovernmentPartnership),
|
||||
},
|
||||
CORPORATION_PROD_1000: {
|
||||
...achievementData.CORPORATION_PROD_1000,
|
||||
Icon: "CORP1000",
|
||||
Visible: () => hasAccessToSF(3),
|
||||
Visible: () => canAccessBitNodeFeature(3),
|
||||
Condition: () => {
|
||||
if (!Player.corporation) return false;
|
||||
for (const division of Player.corporation.divisions.values()) {
|
||||
@ -391,7 +383,7 @@ export const achievements: Record<string, Achievement> = {
|
||||
CORPORATION_EMPLOYEE_3000: {
|
||||
...achievementData.CORPORATION_EMPLOYEE_3000,
|
||||
Icon: "CORPCITY",
|
||||
Visible: () => hasAccessToSF(3),
|
||||
Visible: () => canAccessBitNodeFeature(3),
|
||||
Condition: (): boolean => {
|
||||
if (!Player.corporation) return false;
|
||||
for (const division of Player.corporation.divisions.values()) {
|
||||
@ -406,7 +398,7 @@ export const achievements: Record<string, Achievement> = {
|
||||
Icon: "CORPRE",
|
||||
Name: "Own the land",
|
||||
Description: "Expand to the Real Estate division.",
|
||||
Visible: () => hasAccessToSF(3),
|
||||
Visible: () => canAccessBitNodeFeature(3),
|
||||
Condition: () => {
|
||||
if (!Player.corporation) return false;
|
||||
for (const division of Player.corporation.divisions.values()) {
|
||||
@ -418,26 +410,26 @@ export const achievements: Record<string, Achievement> = {
|
||||
INTELLIGENCE_255: {
|
||||
...achievementData.INTELLIGENCE_255,
|
||||
Icon: "INT255",
|
||||
Visible: () => hasAccessToSF(5),
|
||||
Visible: () => canAccessBitNodeFeature(5),
|
||||
Condition: () => Player.skills.intelligence >= 255,
|
||||
},
|
||||
BLADEBURNER_DIVISION: {
|
||||
...achievementData.BLADEBURNER_DIVISION,
|
||||
Icon: "BLADE",
|
||||
Visible: () => hasAccessToSF(6),
|
||||
Visible: () => canAccessBitNodeFeature(6),
|
||||
Condition: () => Player.bladeburner !== null,
|
||||
},
|
||||
BLADEBURNER_OVERCLOCK: {
|
||||
...achievementData.BLADEBURNER_OVERCLOCK,
|
||||
Icon: "BLADEOVERCLOCK",
|
||||
Visible: () => hasAccessToSF(6),
|
||||
Visible: () => canAccessBitNodeFeature(6),
|
||||
Condition: () =>
|
||||
Player.bladeburner?.getSkillLevel(BladeSkillName.overclock) === Skills[BladeSkillName.overclock].maxLvl,
|
||||
},
|
||||
BLADEBURNER_UNSPENT_100000: {
|
||||
...achievementData.BLADEBURNER_UNSPENT_100000,
|
||||
Icon: "BLADE100K",
|
||||
Visible: () => hasAccessToSF(6),
|
||||
Visible: () => canAccessBitNodeFeature(6),
|
||||
Condition: () => Player.bladeburner !== null && Player.bladeburner.skillPoints >= 100000,
|
||||
},
|
||||
"4S": {
|
||||
@ -448,21 +440,21 @@ export const achievements: Record<string, Achievement> = {
|
||||
FIRST_HACKNET_SERVER: {
|
||||
...achievementData.FIRST_HACKNET_SERVER,
|
||||
Icon: "HASHNET",
|
||||
Visible: () => hasAccessToSF(9),
|
||||
Visible: () => canAccessBitNodeFeature(9),
|
||||
Condition: () => hasHacknetServers() && Player.hacknetNodes.length > 0,
|
||||
AdditionalUnlock: [achievementData.FIRST_HACKNET_NODE.ID],
|
||||
},
|
||||
ALL_HACKNET_SERVER: {
|
||||
...achievementData.ALL_HACKNET_SERVER,
|
||||
Icon: "HASHNETALL",
|
||||
Visible: () => hasAccessToSF(9),
|
||||
Visible: () => canAccessBitNodeFeature(9),
|
||||
Condition: () => hasHacknetServers() && Player.hacknetNodes.length === HacknetServerConstants.MaxServers,
|
||||
AdditionalUnlock: [achievementData["30_HACKNET_NODE"].ID],
|
||||
},
|
||||
MAX_HACKNET_SERVER: {
|
||||
...achievementData.MAX_HACKNET_SERVER,
|
||||
Icon: "HASHNETALL",
|
||||
Visible: () => hasAccessToSF(9),
|
||||
Visible: () => canAccessBitNodeFeature(9),
|
||||
Condition: (): boolean => {
|
||||
if (!hasHacknetServers()) return false;
|
||||
for (const h of Player.hacknetNodes) {
|
||||
@ -484,14 +476,14 @@ export const achievements: Record<string, Achievement> = {
|
||||
HACKNET_SERVER_1B: {
|
||||
...achievementData.HACKNET_SERVER_1B,
|
||||
Icon: "HASHNETMONEY",
|
||||
Visible: () => hasAccessToSF(9),
|
||||
Visible: () => canAccessBitNodeFeature(9),
|
||||
Condition: () => hasHacknetServers() && Player.moneySourceB.hacknet >= 1e9,
|
||||
AdditionalUnlock: [achievementData.HACKNET_NODE_10M.ID],
|
||||
},
|
||||
MAX_CACHE: {
|
||||
...achievementData.MAX_CACHE,
|
||||
Icon: "HASHNETCAP",
|
||||
Visible: () => hasAccessToSF(9),
|
||||
Visible: () => canAccessBitNodeFeature(9),
|
||||
Condition: () =>
|
||||
hasHacknetServers() &&
|
||||
Player.hashManager.hashes === Player.hashManager.capacity &&
|
||||
@ -500,7 +492,7 @@ export const achievements: Record<string, Achievement> = {
|
||||
SLEEVE_8: {
|
||||
...achievementData.SLEEVE_8,
|
||||
Icon: "SLEEVE8",
|
||||
Visible: () => hasAccessToSF(10),
|
||||
Visible: () => canAccessBitNodeFeature(10),
|
||||
Condition: () => Player.sleeves.length === 8 && Player.sourceFileLvl(10) === 3,
|
||||
},
|
||||
INDECISIVE: {
|
||||
@ -523,7 +515,7 @@ export const achievements: Record<string, Achievement> = {
|
||||
...achievementData.FAST_BN,
|
||||
Icon: "2DAYS",
|
||||
Visible: knowsAboutBitverse,
|
||||
Condition: () => bitNodeFinishedState() && Player.playtimeSinceLastBitnode < 1000 * 60 * 60 * 24 * 2,
|
||||
Condition: () => isBitNodeFinished() && Player.playtimeSinceLastBitnode < 1000 * 60 * 60 * 24 * 2,
|
||||
},
|
||||
CHALLENGE_BN1: {
|
||||
...achievementData.CHALLENGE_BN1,
|
||||
@ -531,57 +523,57 @@ export const achievements: Record<string, Achievement> = {
|
||||
Visible: knowsAboutBitverse,
|
||||
Condition: () =>
|
||||
Player.bitNodeN === 1 &&
|
||||
bitNodeFinishedState() &&
|
||||
isBitNodeFinished() &&
|
||||
Player.getHomeComputer().maxRam <= 128 &&
|
||||
Player.getHomeComputer().cpuCores === 1,
|
||||
},
|
||||
CHALLENGE_BN2: {
|
||||
...achievementData.CHALLENGE_BN2,
|
||||
Icon: "BN2+",
|
||||
Visible: () => hasAccessToSF(2),
|
||||
Condition: () => Player.bitNodeN === 2 && bitNodeFinishedState() && Player.gang === null,
|
||||
Visible: () => canAccessBitNodeFeature(2),
|
||||
Condition: () => Player.bitNodeN === 2 && isBitNodeFinished() && Player.gang === null,
|
||||
},
|
||||
CHALLENGE_BN3: {
|
||||
...achievementData.CHALLENGE_BN3,
|
||||
Icon: "BN3+",
|
||||
Visible: () => hasAccessToSF(3),
|
||||
Condition: () => Player.bitNodeN === 3 && bitNodeFinishedState() && Player.corporation === null,
|
||||
Visible: () => canAccessBitNodeFeature(3),
|
||||
Condition: () => Player.bitNodeN === 3 && isBitNodeFinished() && Player.corporation === null,
|
||||
},
|
||||
CHALLENGE_BN6: {
|
||||
...achievementData.CHALLENGE_BN6,
|
||||
Icon: "BN6+",
|
||||
Visible: () => hasAccessToSF(6),
|
||||
Condition: () => Player.bitNodeN === 6 && bitNodeFinishedState() && Player.bladeburner === null,
|
||||
Visible: () => canAccessBitNodeFeature(6),
|
||||
Condition: () => Player.bitNodeN === 6 && isBitNodeFinished() && Player.bladeburner === null,
|
||||
},
|
||||
CHALLENGE_BN7: {
|
||||
...achievementData.CHALLENGE_BN7,
|
||||
Icon: "BN7+",
|
||||
Visible: () => hasAccessToSF(7),
|
||||
Condition: () => Player.bitNodeN === 7 && bitNodeFinishedState() && Player.bladeburner === null,
|
||||
Visible: () => canAccessBitNodeFeature(7),
|
||||
Condition: () => Player.bitNodeN === 7 && isBitNodeFinished() && Player.bladeburner === null,
|
||||
},
|
||||
CHALLENGE_BN8: {
|
||||
...achievementData.CHALLENGE_BN8,
|
||||
Icon: "BN8+",
|
||||
Visible: () => hasAccessToSF(8),
|
||||
Condition: () => Player.bitNodeN === 8 && bitNodeFinishedState() && !Player.has4SData && !Player.has4SDataTixApi,
|
||||
Visible: () => canAccessBitNodeFeature(8),
|
||||
Condition: () => Player.bitNodeN === 8 && isBitNodeFinished() && !Player.has4SData && !Player.has4SDataTixApi,
|
||||
},
|
||||
CHALLENGE_BN9: {
|
||||
...achievementData.CHALLENGE_BN9,
|
||||
Icon: "BN9+",
|
||||
Visible: () => hasAccessToSF(9),
|
||||
Visible: () => canAccessBitNodeFeature(9),
|
||||
Condition: () =>
|
||||
Player.bitNodeN === 9 &&
|
||||
bitNodeFinishedState() &&
|
||||
isBitNodeFinished() &&
|
||||
Player.moneySourceB.hacknet === 0 &&
|
||||
Player.moneySourceB.hacknet_expenses === 0,
|
||||
},
|
||||
CHALLENGE_BN10: {
|
||||
...achievementData.CHALLENGE_BN10,
|
||||
Icon: "BN10+",
|
||||
Visible: () => hasAccessToSF(10),
|
||||
Visible: () => canAccessBitNodeFeature(10),
|
||||
Condition: () =>
|
||||
Player.bitNodeN === 10 &&
|
||||
bitNodeFinishedState() &&
|
||||
isBitNodeFinished() &&
|
||||
!Player.sleeves.some(
|
||||
(s) =>
|
||||
s.augmentations.length > 0 ||
|
||||
@ -596,7 +588,7 @@ export const achievements: Record<string, Achievement> = {
|
||||
CHALLENGE_BN12: {
|
||||
...achievementData.CHALLENGE_BN12,
|
||||
Icon: "BN12+",
|
||||
Visible: () => hasAccessToSF(12),
|
||||
Visible: () => canAccessBitNodeFeature(12),
|
||||
Condition: () => Player.sourceFileLvl(12) >= 50,
|
||||
},
|
||||
BYPASS: {
|
||||
@ -657,10 +649,10 @@ export const achievements: Record<string, Achievement> = {
|
||||
CHALLENGE_BN13: {
|
||||
...achievementData.CHALLENGE_BN13,
|
||||
Icon: "BN13+",
|
||||
Visible: () => hasAccessToSF(13),
|
||||
Visible: () => canAccessBitNodeFeature(13),
|
||||
Condition: () =>
|
||||
Player.bitNodeN === 13 &&
|
||||
bitNodeFinishedState() &&
|
||||
isBitNodeFinished() &&
|
||||
!Player.augmentations.some((a) => a.name === AugmentationName.StaneksGift1),
|
||||
},
|
||||
DEVMENU: {
|
||||
|
@ -1,26 +1,22 @@
|
||||
import React from "react";
|
||||
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
|
||||
import { AchievementList } from "./AchievementList";
|
||||
import { achievements } from "./Achievements";
|
||||
import { Typography } from "@mui/material";
|
||||
import { Player } from "@player";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: 50,
|
||||
padding: theme.spacing(2),
|
||||
userSelect: "none",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const useStyles = makeStyles()((theme: Theme) => ({
|
||||
root: {
|
||||
width: 50,
|
||||
padding: theme.spacing(2),
|
||||
userSelect: "none",
|
||||
},
|
||||
}));
|
||||
|
||||
export function AchievementsRoot(): JSX.Element {
|
||||
const classes = useStyles();
|
||||
const { classes } = useStyles();
|
||||
return (
|
||||
<div className={classes.root} style={{ width: "90%" }}>
|
||||
<Typography variant="h4">Achievements</Typography>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { PartialRecord, getRecordEntries } from "../Types/Record";
|
||||
|
||||
import { clampNumber } from "../utils/helpers/clampNumber";
|
||||
/**
|
||||
* 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
|
||||
@ -176,7 +176,7 @@ export class BitNodeMultipliers {
|
||||
CorporationDivisions = 1;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
11
src/BitNode/BitNodeUtils.ts
Normal file
11
src/BitNode/BitNodeUtils.ts
Normal file
@ -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 { CinematicText } from "../../ui/React/CinematicText";
|
||||
import { Player } from "@player";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
@ -12,33 +11,31 @@ import { Settings } from "../../Settings/Settings";
|
||||
import Button from "@mui/material/Button";
|
||||
import { CompletedProgramName } from "@enums";
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
portal: {
|
||||
cursor: "pointer",
|
||||
fontFamily: "inherit",
|
||||
fontSize: "1rem",
|
||||
fontWeight: "bold",
|
||||
lineHeight: 1,
|
||||
padding: 0,
|
||||
"&:hover": {
|
||||
color: "#fff",
|
||||
},
|
||||
const useStyles = makeStyles()(() => ({
|
||||
portal: {
|
||||
cursor: "pointer",
|
||||
fontFamily: "inherit",
|
||||
fontSize: "1rem",
|
||||
fontWeight: "bold",
|
||||
lineHeight: 1,
|
||||
padding: 0,
|
||||
"&:hover": {
|
||||
color: "#fff",
|
||||
},
|
||||
level0: {
|
||||
color: Settings.theme.bnlvl0,
|
||||
},
|
||||
level1: {
|
||||
color: Settings.theme.bnlvl1,
|
||||
},
|
||||
level2: {
|
||||
color: Settings.theme.bnlvl2,
|
||||
},
|
||||
level3: {
|
||||
color: Settings.theme.bnlvl3,
|
||||
},
|
||||
}),
|
||||
);
|
||||
},
|
||||
level0: {
|
||||
color: Settings.theme.bnlvl0,
|
||||
},
|
||||
level1: {
|
||||
color: Settings.theme.bnlvl1,
|
||||
},
|
||||
level2: {
|
||||
color: Settings.theme.bnlvl2,
|
||||
},
|
||||
level3: {
|
||||
color: Settings.theme.bnlvl3,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IPortalProps {
|
||||
n: number;
|
||||
@ -48,7 +45,7 @@ interface IPortalProps {
|
||||
}
|
||||
function BitNodePortal(props: IPortalProps): React.ReactElement {
|
||||
const [portalOpen, setPortalOpen] = useState(false);
|
||||
const classes = useStyles();
|
||||
const { classes } = useStyles();
|
||||
const bitNode = BitNodes[`BitNode${props.n}`];
|
||||
if (bitNode == null) {
|
||||
return <>O</>;
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
} from "@enums";
|
||||
import { getKeyList } from "../utils/helpers/getKeyList";
|
||||
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 { City } from "./City";
|
||||
import { Player } from "@player";
|
||||
@ -46,6 +46,8 @@ import { clampInteger, clampNumber } from "../utils/helpers/clampNumber";
|
||||
import { parseCommand } from "../Terminal/Parser";
|
||||
import { BlackOperations } from "./data/BlackOperations";
|
||||
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 };
|
||||
|
||||
@ -115,7 +117,6 @@ export class Bladeburner {
|
||||
}
|
||||
|
||||
calculateStaminaPenalty(): number {
|
||||
if (this.stamina === this.maxStamina) return 1;
|
||||
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 {
|
||||
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();
|
||||
const action = this.getActionObject(actionIdent);
|
||||
switch (action.type) {
|
||||
@ -899,12 +916,12 @@ export class Bladeburner {
|
||||
this.changeRank(person, gain);
|
||||
if (isOperation && this.logging.ops) {
|
||||
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) {
|
||||
this.log(
|
||||
`${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 = "";
|
||||
if (loss > 0) {
|
||||
logLossText += "Lost " + formatNumberNoSuffix(loss, 3) + " rank. ";
|
||||
logLossText += ` Lost ${formatNumberNoSuffix(loss, 3)} rank.`;
|
||||
}
|
||||
if (damage > 0) {
|
||||
logLossText += "Took " + formatNumberNoSuffix(damage, 0) + " damage.";
|
||||
logLossText += ` Took ${formatNumberNoSuffix(damage, 0)} damage.${getExtraLogAfterTakingDamage(damage)}`;
|
||||
}
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
@ -977,7 +994,9 @@ export class Bladeburner {
|
||||
teamLossMax = Math.ceil(teamCount / 2);
|
||||
|
||||
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 {
|
||||
retValue = this.getActionStats(action, person, false);
|
||||
@ -1003,7 +1022,7 @@ export class Bladeburner {
|
||||
`${person.whoAmI()}: ${action.name} failed! Lost ${formatNumberNoSuffix(
|
||||
rankLoss,
|
||||
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;
|
||||
if (this.logging.blackops) {
|
||||
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) +
|
||||
" agi exp, " +
|
||||
formatBigNumber(staminaGain) +
|
||||
" max stamina",
|
||||
" max stamina.",
|
||||
);
|
||||
}
|
||||
break;
|
||||
@ -1089,7 +1108,7 @@ export class Bladeburner {
|
||||
`${person.whoAmI()}: ` +
|
||||
`Field analysis completed. Gained ${formatBigNumber(rankGain)} rank, ` +
|
||||
`${formatExp(hackingExpGain)} hacking exp, and ` +
|
||||
`${formatExp(charismaExpGain)} charisma exp`,
|
||||
`${formatExp(charismaExpGain)} charisma exp.`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
@ -1105,7 +1124,7 @@ export class Bladeburner {
|
||||
`${person.whoAmI()}: ` +
|
||||
"Successfully recruited a team member! Gained " +
|
||||
formatExp(expGain) +
|
||||
" charisma exp",
|
||||
" charisma exp.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@ -1116,7 +1135,7 @@ export class Bladeburner {
|
||||
`${person.whoAmI()}: ` +
|
||||
"Failed to recruit a team member. Gained " +
|
||||
formatExp(expGain) +
|
||||
" charisma exp",
|
||||
" charisma exp.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1132,7 +1151,7 @@ export class Bladeburner {
|
||||
this.log(
|
||||
`${person.whoAmI()}: Diplomacy completed. Chaos levels in the current city fell by ${formatPercent(
|
||||
1 - eff,
|
||||
)}`,
|
||||
)}.`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
@ -1140,14 +1159,22 @@ export class Bladeburner {
|
||||
case BladeGeneralActionName.hyperbolicRegen: {
|
||||
person.regenerateHp(BladeburnerConstants.HrcHpGain);
|
||||
|
||||
const currentStamina = this.stamina;
|
||||
const staminaGain = this.maxStamina * (BladeburnerConstants.HrcStaminaGain / 100);
|
||||
this.stamina = Math.min(this.maxStamina, this.stamina + staminaGain);
|
||||
if (this.logging.general) {
|
||||
this.log(
|
||||
`${person.whoAmI()}: Rested in Hyperbolic Regeneration Chamber. Restored ${
|
||||
BladeburnerConstants.HrcHpGain
|
||||
} HP and gained ${formatStamina(staminaGain)} stamina`,
|
||||
);
|
||||
let extraLog = "";
|
||||
if (Player.hp.current > currentHp) {
|
||||
extraLog += ` Restored ${formatHp(BladeburnerConstants.HrcHpGain)} HP. Current HP is ${formatHp(
|
||||
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;
|
||||
}
|
||||
@ -1258,13 +1285,16 @@ export class Bladeburner {
|
||||
|
||||
calculateMaxStamina(): void {
|
||||
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(
|
||||
(baseStamina + this.staminaBonus) *
|
||||
this.getSkillMult(BladeMultName.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
|
||||
const oldMax = this.maxStamina;
|
||||
this.maxStamina = maxStamina;
|
||||
@ -1431,6 +1461,16 @@ export class Bladeburner {
|
||||
loadOperationsData(operationsData, bladeburner.operations);
|
||||
// Regenerate skill multiplier data, which is not included in savedata
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
55
src/Bladeburner/ui/ActionHeader.tsx
Normal file
55
src/Bladeburner/ui/ActionHeader.tsx
Normal file
@ -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 { BladeburnerConstants } from "../data/Constants";
|
||||
import { Contract } from "../Actions";
|
||||
|
||||
interface ActionLevelProps {
|
||||
action: LevelableAction;
|
||||
@ -18,6 +19,11 @@ interface ActionLevelProps {
|
||||
export function ActionLevel({ action, isActive, bladeburner, rerender }: ActionLevelProps): React.ReactElement {
|
||||
const canIncrease = action.level < action.maxLevel;
|
||||
const canDecrease = action.level > 1;
|
||||
const successesNeededForNextLevel = action.getSuccessesNeededForNextLevel(
|
||||
action instanceof Contract
|
||||
? BladeburnerConstants.ContractSuccessesPerLevel
|
||||
: BladeburnerConstants.OperationSuccessesPerLevel,
|
||||
);
|
||||
|
||||
function increaseLevel(): void {
|
||||
if (!canIncrease) return;
|
||||
@ -36,21 +42,7 @@ export function ActionLevel({ action, isActive, bladeburner, rerender }: ActionL
|
||||
return (
|
||||
<Box display="flex" flexDirection="row" alignItems="center">
|
||||
<Box display="flex">
|
||||
<Tooltip
|
||||
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>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Tooltip title={<Typography>{successesNeededForNextLevel} successes needed for next level</Typography>}>
|
||||
<Typography>
|
||||
Level: {action.level} / {action.maxLevel}
|
||||
</Typography>
|
||||
|
@ -7,72 +7,41 @@ import { Paper, Typography } from "@mui/material";
|
||||
import { Player } from "@player";
|
||||
import { formatNumberNoSuffix } from "../../ui/formatNumber";
|
||||
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 { StartButton } from "./StartButton";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { ActionHeader } from "./ActionHeader";
|
||||
|
||||
interface BlackOpElemProps {
|
||||
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 isCompleted = bladeburner.numBlackOpsComplete > blackOp.n;
|
||||
const isCompleted = bladeburner.numBlackOpsComplete > action.n;
|
||||
if (isCompleted) {
|
||||
return (
|
||||
<Paper sx={{ my: 1, p: 1 }}>
|
||||
<Typography>{blackOp.name} (COMPLETED)</Typography>
|
||||
<Typography>{action.name} (COMPLETED)</Typography>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
const isActive = bladeburner.action?.name === blackOp.name;
|
||||
const actionTime = blackOp.getActionTime(bladeburner, Player);
|
||||
const hasReqdRank = bladeburner.rank >= blackOp.reqdRank;
|
||||
const computedActionTimeCurrent = Math.min(
|
||||
bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow,
|
||||
bladeburner.actionTimeToComplete,
|
||||
);
|
||||
const actionTime = action.getActionTime(bladeburner, Player);
|
||||
const hasRequiredRank = bladeburner.rank >= action.reqdRank;
|
||||
|
||||
return (
|
||||
<Paper sx={{ my: 1, p: 1 }}>
|
||||
{isActive ? (
|
||||
<>
|
||||
<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} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<ActionHeader bladeburner={bladeburner} action={action} rerender={rerender}></ActionHeader>
|
||||
<br />
|
||||
<Typography whiteSpace={"pre-wrap"}>{action.desc}</Typography>
|
||||
<br />
|
||||
<Typography whiteSpace={"pre-wrap"}>{blackOp.desc}</Typography>
|
||||
<br />
|
||||
<br />
|
||||
<Typography color={hasReqdRank ? "primary" : "error"}>
|
||||
Required Rank: {formatNumberNoSuffix(blackOp.reqdRank, 0)}
|
||||
<Typography color={hasRequiredRank ? "primary" : "error"}>
|
||||
Required Rank: {formatNumberNoSuffix(action.reqdRank, 0)}
|
||||
</Typography>
|
||||
<br />
|
||||
<Typography>
|
||||
<SuccessChance action={blackOp} bladeburner={bladeburner} />
|
||||
<SuccessChance action={action} bladeburner={bladeburner} />
|
||||
<br />
|
||||
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
|
||||
</Typography>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { Bladeburner } from "../Bladeburner";
|
||||
|
||||
import * as React from "react";
|
||||
import React from "react";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import { FactionName } from "@enums";
|
||||
import { BlackOpElem } from "./BlackOpElem";
|
||||
@ -8,13 +8,25 @@ import { Router } from "../../ui/GameRoot";
|
||||
import { Page } from "../../ui/Router";
|
||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||
import { blackOpsArray } from "../data/BlackOperations";
|
||||
import { GetServer } from "../../Server/AllServers";
|
||||
import { SpecialServers } from "../../Server/data/SpecialServers";
|
||||
import { Server } from "../../Server/Server";
|
||||
|
||||
interface BlackOpPageProps {
|
||||
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 {
|
||||
const blackOps = blackOpsArray.slice(0, bladeburner.numBlackOpsComplete + 1).reverse();
|
||||
const blackOperations = blackOpsArray.slice(0, bladeburner.numBlackOpsComplete + 1).reverse();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -33,13 +45,13 @@ export function BlackOpPage({ bladeburner }: BlackOpPageProps): React.ReactEleme
|
||||
losses.
|
||||
</Typography>
|
||||
{bladeburner.numBlackOpsComplete >= blackOpsArray.length ? (
|
||||
<Button sx={{ my: 1, p: 1 }} onClick={() => Router.toPage(Page.BitVerse, { flume: false, quick: false })}>
|
||||
<CorruptableText content="Destroy w0rld_d34mon" spoiler={false}></CorruptableText>
|
||||
<Button sx={{ my: 1, p: 1 }} onClick={finishBitNode}>
|
||||
<CorruptableText content="Destroy w0r1d_d43m0n" spoiler={false}></CorruptableText>
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
{blackOps.map((blackOp) => (
|
||||
<BlackOpElem key={blackOp.name} bladeburner={bladeburner} blackOp={blackOp} />
|
||||
{blackOperations.map((blackOperation) => (
|
||||
<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 { Theme } from "@mui/material/styles";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
|
||||
interface ILineProps {
|
||||
content: React.ReactNode;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
textfield: {
|
||||
margin: theme.spacing(0),
|
||||
width: "100%",
|
||||
},
|
||||
input: {
|
||||
backgroundColor: theme.colors.backgroundsecondary,
|
||||
},
|
||||
nopadding: {
|
||||
padding: theme.spacing(0),
|
||||
},
|
||||
preformatted: {
|
||||
whiteSpace: "pre-wrap",
|
||||
margin: theme.spacing(0),
|
||||
},
|
||||
list: {
|
||||
padding: theme.spacing(0),
|
||||
height: "100%",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const useStyles = makeStyles()((theme: Theme) => ({
|
||||
textfield: {
|
||||
margin: theme.spacing(0),
|
||||
width: "100%",
|
||||
},
|
||||
input: {
|
||||
backgroundColor: theme.colors.backgroundsecondary,
|
||||
},
|
||||
nopadding: {
|
||||
padding: theme.spacing(0),
|
||||
},
|
||||
preformatted: {
|
||||
whiteSpace: "pre-wrap",
|
||||
margin: theme.spacing(0),
|
||||
},
|
||||
list: {
|
||||
padding: theme.spacing(0),
|
||||
height: "100%",
|
||||
},
|
||||
}));
|
||||
|
||||
function Line(props: ILineProps): React.ReactElement {
|
||||
return (
|
||||
@ -49,7 +46,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export function Console(props: IProps): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const { classes } = useStyles();
|
||||
const [command, setCommand] = useState("");
|
||||
const consoleInput = useRef<HTMLInputElement>(null);
|
||||
useRerender(1000);
|
||||
|
@ -2,18 +2,15 @@ import type { Bladeburner } from "../Bladeburner";
|
||||
import type { Contract } from "../Actions/Contract";
|
||||
|
||||
import React from "react";
|
||||
import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
|
||||
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
|
||||
import { Player } from "@player";
|
||||
import { SuccessChance } from "./SuccessChance";
|
||||
import { CopyableText } from "../../ui/React/CopyableText";
|
||||
import { ActionLevel } from "./ActionLevel";
|
||||
import { Autolevel } from "./Autolevel";
|
||||
import { StartButton } from "./StartButton";
|
||||
import { formatNumberNoSuffix, formatBigNumber } from "../../ui/formatNumber";
|
||||
import { formatBigNumber } from "../../ui/formatNumber";
|
||||
import { Paper, Typography } from "@mui/material";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { getEnumHelper } from "../../utils/EnumHelper";
|
||||
import { ActionHeader } from "./ActionHeader";
|
||||
|
||||
interface ContractElemProps {
|
||||
bladeburner: Bladeburner;
|
||||
@ -22,41 +19,15 @@ interface ContractElemProps {
|
||||
|
||||
export function ContractElem({ bladeburner, action }: ContractElemProps): React.ReactElement {
|
||||
const rerender = useRerender();
|
||||
// Temp special return
|
||||
if (!getEnumHelper("BladeContractName").isMember(action.name)) return <></>;
|
||||
const isActive = action.name === bladeburner.action?.name;
|
||||
const computedActionTimeCurrent = Math.min(
|
||||
bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow,
|
||||
bladeburner.actionTimeToComplete,
|
||||
);
|
||||
const actionTime = action.getActionTime(bladeburner, Player);
|
||||
|
||||
return (
|
||||
<Paper sx={{ my: 1, p: 1 }}>
|
||||
{isActive ? (
|
||||
<>
|
||||
<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 />
|
||||
<ActionHeader bladeburner={bladeburner} action={action} rerender={rerender}></ActionHeader>
|
||||
<br />
|
||||
<ActionLevel action={action} bladeburner={bladeburner} isActive={isActive} rerender={rerender} />
|
||||
<br />
|
||||
<br />
|
||||
<Typography whiteSpace={"pre-wrap"}>
|
||||
{action.desc}
|
||||
<br />
|
||||
|
@ -2,15 +2,12 @@ import type { Bladeburner } from "../Bladeburner";
|
||||
import type { GeneralAction } from "../Actions/GeneralAction";
|
||||
|
||||
import React from "react";
|
||||
import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
|
||||
import { formatNumberNoSuffix } from "../../ui/formatNumber";
|
||||
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
|
||||
import { Player } from "@player";
|
||||
import { CopyableText } from "../../ui/React/CopyableText";
|
||||
import { StartButton } from "./StartButton";
|
||||
import { StopButton } from "./StopButton";
|
||||
import { Box, Paper, Typography } from "@mui/material";
|
||||
import { Paper, Typography } from "@mui/material";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { ActionHeader } from "./ActionHeader";
|
||||
|
||||
interface GeneralActionElemProps {
|
||||
bladeburner: Bladeburner;
|
||||
@ -19,44 +16,16 @@ interface GeneralActionElemProps {
|
||||
|
||||
export function GeneralActionElem({ bladeburner, action }: GeneralActionElemProps): React.ReactElement {
|
||||
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 successChance =
|
||||
action.name === "Recruitment" ? Math.max(0, Math.min(bladeburner.getRecruitmentSuccessChance(Player), 1)) : -1;
|
||||
|
||||
return (
|
||||
<Paper sx={{ my: 1, p: 1 }}>
|
||||
{isActive ? (
|
||||
<>
|
||||
<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 />
|
||||
<ActionHeader bladeburner={bladeburner} action={action} rerender={rerender}></ActionHeader>
|
||||
<br />
|
||||
<Typography>{action.desc}</Typography>
|
||||
<br />
|
||||
<br />
|
||||
<Typography>
|
||||
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
|
||||
{successChance !== -1 && (
|
||||
|
@ -5,76 +5,47 @@ import React from "react";
|
||||
import { Paper, Typography } from "@mui/material";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
|
||||
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
|
||||
import { SuccessChance } from "./SuccessChance";
|
||||
import { ActionLevel } from "./ActionLevel";
|
||||
import { Autolevel } from "./Autolevel";
|
||||
import { StartButton } from "./StartButton";
|
||||
import { TeamSizeButton } from "./TeamSizeButton";
|
||||
import { CopyableText } from "../../ui/React/CopyableText";
|
||||
import { formatNumberNoSuffix, formatBigNumber } from "../../ui/formatNumber";
|
||||
import { formatBigNumber } from "../../ui/formatNumber";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { BladeActionType } from "@enums";
|
||||
import { ActionHeader } from "./ActionHeader";
|
||||
|
||||
interface OperationElemProps {
|
||||
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 isActive =
|
||||
bladeburner.action?.type === BladeActionType.operation && operation.name === bladeburner.action?.name;
|
||||
const computedActionTimeCurrent = Math.min(
|
||||
bladeburner.actionTimeCurrent + bladeburner.actionTimeOverflow,
|
||||
bladeburner.actionTimeToComplete,
|
||||
);
|
||||
const actionTime = operation.getActionTime(bladeburner, Player);
|
||||
const isActive = bladeburner.action?.type === BladeActionType.operation && action.name === bladeburner.action?.name;
|
||||
const actionTime = action.getActionTime(bladeburner, Player);
|
||||
|
||||
return (
|
||||
<Paper sx={{ my: 1, p: 1 }}>
|
||||
{isActive ? (
|
||||
<>
|
||||
<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} />
|
||||
<ActionHeader bladeburner={bladeburner} action={action} rerender={rerender}></ActionHeader>
|
||||
<br />
|
||||
<ActionLevel action={action} bladeburner={bladeburner} isActive={isActive} rerender={rerender} />
|
||||
<br />
|
||||
<Typography whiteSpace={"pre-wrap"}>
|
||||
{operation.desc}
|
||||
{action.desc}
|
||||
<br />
|
||||
<br />
|
||||
<SuccessChance action={operation} bladeburner={bladeburner} />
|
||||
<SuccessChance action={action} bladeburner={bladeburner} />
|
||||
<br />
|
||||
Time Required: {convertTimeMsToTimeElapsedString(actionTime * 1000)}
|
||||
<br />
|
||||
Operations remaining: {formatBigNumber(Math.floor(operation.count))}
|
||||
Operations remaining: {formatBigNumber(Math.floor(action.count))}
|
||||
<br />
|
||||
Successes: {formatBigNumber(operation.successes)}
|
||||
Successes: {formatBigNumber(action.successes)}
|
||||
<br />
|
||||
Failures: {formatBigNumber(operation.failures)}
|
||||
Failures: {formatBigNumber(action.failures)}
|
||||
</Typography>
|
||||
<br />
|
||||
<Autolevel rerender={rerender} action={operation} />
|
||||
<Autolevel rerender={rerender} action={action} />
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { Bladeburner } from "../Bladeburner";
|
||||
|
||||
import * as React from "react";
|
||||
import React from "react";
|
||||
import { OperationElem } from "./OperationElem";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
@ -30,7 +30,7 @@ export function OperationPage({ bladeburner }: OperationPageProps): React.ReactE
|
||||
difficult, but grant more rank and experience.
|
||||
</Typography>
|
||||
{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 (
|
||||
<ButtonWithTooltip disabledTooltip={disabledReason} onClick={onStart}>
|
||||
<ButtonWithTooltip
|
||||
buttonProps={{ style: { marginLeft: "1rem" } }}
|
||||
disabledTooltip={disabledReason}
|
||||
onClick={onStart}
|
||||
>
|
||||
Start
|
||||
</ButtonWithTooltip>
|
||||
);
|
||||
|
@ -13,5 +13,9 @@ export function StopButton({ bladeburner, rerender }: StopButtonProps): React.Re
|
||||
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 (
|
||||
<>
|
||||
<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)})
|
||||
</Button>
|
||||
<TeamSizeModal open={open} onClose={() => setOpen(false)} action={action} bladeburner={bladeburner} />
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { FC } from "react";
|
||||
import { Card, Suit } from "./Card";
|
||||
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
import Paper from "@mui/material/Paper";
|
||||
|
||||
interface Props {
|
||||
@ -10,35 +9,33 @@ interface Props {
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
card: {
|
||||
padding: "10px",
|
||||
border: "solid 1px #808080",
|
||||
backgroundColor: "white",
|
||||
display: "inline-block",
|
||||
borderRadius: "10px",
|
||||
fontSize: "18.5px",
|
||||
textAlign: "center",
|
||||
margin: "3px",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
red: {
|
||||
color: "red",
|
||||
},
|
||||
const useStyles = makeStyles()(() => ({
|
||||
card: {
|
||||
padding: "10px",
|
||||
border: "solid 1px #808080",
|
||||
backgroundColor: "white",
|
||||
display: "inline-block",
|
||||
borderRadius: "10px",
|
||||
fontSize: "18.5px",
|
||||
textAlign: "center",
|
||||
margin: "3px",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
red: {
|
||||
color: "red",
|
||||
},
|
||||
|
||||
black: {
|
||||
color: "black",
|
||||
},
|
||||
value: {
|
||||
fontSize: "20px",
|
||||
fontFamily: "sans-serif",
|
||||
},
|
||||
}),
|
||||
);
|
||||
black: {
|
||||
color: "black",
|
||||
},
|
||||
value: {
|
||||
fontSize: "20px",
|
||||
fontFamily: "sans-serif",
|
||||
},
|
||||
}));
|
||||
|
||||
export const ReactCard: FC<Props> = ({ card, hidden }) => {
|
||||
const classes = useStyles();
|
||||
const { classes } = useStyles();
|
||||
let suit: React.ReactNode;
|
||||
switch (card.suit) {
|
||||
case Suit.Clubs:
|
||||
|
@ -24,7 +24,6 @@ export const CONSTANTS: {
|
||||
IntelligenceCrimeBaseExpGain: number;
|
||||
IntelligenceProgramBaseExpGain: number;
|
||||
IntelligenceGraftBaseExpGain: number;
|
||||
IntelligenceTerminalHackBaseExpGain: number;
|
||||
IntelligenceSingFnBaseExpGain: number;
|
||||
MillisecondsPer20Hours: number;
|
||||
GameCyclesPer20Hours: number;
|
||||
@ -101,7 +100,6 @@ export const CONSTANTS: {
|
||||
IntelligenceCrimeBaseExpGain: 0.05,
|
||||
IntelligenceProgramBaseExpGain: 0.1, // Program required hack level divided by this to determine int exp gain
|
||||
IntelligenceGraftBaseExpGain: 0.05,
|
||||
IntelligenceTerminalHackBaseExpGain: 200, // Hacking exp divided by this to determine int exp gain
|
||||
IntelligenceSingFnBaseExpGain: 1.5,
|
||||
|
||||
// Time-related constants
|
||||
@ -158,10 +156,48 @@ export const CONSTANTS: {
|
||||
|
||||
// Also update doc/source/changelog.rst
|
||||
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
|
||||
|
||||
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 { Material } from "./Material";
|
||||
import { getKeyList } from "../utils/helpers/getKeyList";
|
||||
import { calculateMarkupMultiplier } from "./helpers";
|
||||
|
||||
interface DivisionParams {
|
||||
name: string;
|
||||
@ -582,26 +583,13 @@ export class Division {
|
||||
}
|
||||
mat.uiMarketPrice = sCost;
|
||||
|
||||
// Calculate how much of the material sells (per second)
|
||||
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;
|
||||
}
|
||||
}
|
||||
const markupMultiplier = calculateMarkupMultiplier(sCost, mat.marketPrice, markupLimit);
|
||||
|
||||
// Calculate how much of the material sells (per second)
|
||||
mat.maxSellPerCycle =
|
||||
(mat.quality + 0.001) *
|
||||
marketFactor *
|
||||
markup *
|
||||
markupMultiplier *
|
||||
businessFactor *
|
||||
corporation.getSalesMult() *
|
||||
advertisingFactor *
|
||||
@ -920,26 +908,22 @@ export class Division {
|
||||
sCost = sellPrice;
|
||||
}
|
||||
product.uiMarketPrice[city] = sCost;
|
||||
let markup = 1;
|
||||
if (sCost > product.cityData[city].productionCost) {
|
||||
if (sCost - product.cityData[city].productionCost > markupLimit) {
|
||||
markup = markupLimit / (sCost - product.cityData[city].productionCost);
|
||||
}
|
||||
}
|
||||
|
||||
const markupMultiplier = calculateMarkupMultiplier(sCost, product.cityData[city].productionCost, markupLimit);
|
||||
|
||||
product.maxSellAmount =
|
||||
0.5 *
|
||||
Math.pow(product.cityData[city].effectiveRating, 0.65) *
|
||||
marketFactor *
|
||||
corporation.getSalesMult() *
|
||||
Math.pow(markup, 2) *
|
||||
markupMultiplier *
|
||||
businessFactor *
|
||||
advertisingFactor *
|
||||
this.getSalesMultiplier();
|
||||
sellAmt = Math.min(product.maxSellAmount, sellAmt);
|
||||
sellAmt = sellAmt * corpConstants.secondsPerMarketCycle * marketCycles;
|
||||
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
|
||||
totalProfit += sellAmt * sCost;
|
||||
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 "";
|
||||
}
|
||||
|
||||
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
|
||||
// (right-side panel in the Industry UI)
|
||||
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 * as corpConstants from "../data/Constants";
|
||||
import { CityName, CorpUnlockName } from "@enums";
|
||||
@ -32,16 +32,14 @@ interface WarehouseProps {
|
||||
rerender: () => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
retainHeight: {
|
||||
minHeight: "3em",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const useStyles = makeStyles()(() => ({
|
||||
retainHeight: {
|
||||
minHeight: "3em",
|
||||
},
|
||||
}));
|
||||
|
||||
function WarehouseRoot(props: WarehouseProps): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const { classes } = useStyles();
|
||||
const corp = useCorporation();
|
||||
const division = useDivision();
|
||||
const [smartSupplyOpen, setSmartSupplyOpen] = useState(false);
|
||||
|
@ -2,19 +2,16 @@ import * as React from "react";
|
||||
import { formatMoney } from "../../ui/formatNumber";
|
||||
import { Corporation } from "../Corporation";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
unbuyable: {
|
||||
color: theme.palette.action.disabled,
|
||||
},
|
||||
money: {
|
||||
color: theme.colors.money,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const useStyles = makeStyles()((theme: Theme) => ({
|
||||
unbuyable: {
|
||||
color: theme.palette.action.disabled,
|
||||
},
|
||||
money: {
|
||||
color: theme.colors.money,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IProps {
|
||||
money: number;
|
||||
@ -22,7 +19,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
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>;
|
||||
|
||||
return <span className={classes.money}>{formatMoney(props.money)}</span>;
|
||||
|
@ -1,9 +1,9 @@
|
||||
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";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
const useStyles = makeStyles()({
|
||||
root: {
|
||||
border: "1px solid white",
|
||||
width: "5px",
|
||||
@ -16,7 +16,7 @@ export const TableCell: React.FC<TableCellProps> = (props: TableCellProps) => {
|
||||
<MuiTableCell
|
||||
{...props}
|
||||
classes={{
|
||||
root: useStyles().root,
|
||||
root: useStyles().classes.root,
|
||||
...props.classes,
|
||||
}}
|
||||
/>
|
||||
|
@ -2,7 +2,7 @@ import React, { useCallback } from "react";
|
||||
|
||||
import { Accordion, AccordionSummary, AccordionDetails, Button, ButtonGroup, Typography } from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { Sleeve } from "../../PersonObjects/Sleeve/Sleeve";
|
||||
@ -11,7 +11,7 @@ import { MaxSleevesFromCovenant } from "../../PersonObjects/Sleeve/SleeveCovenan
|
||||
|
||||
// Update as additional BitNodes get implemented
|
||||
const validSFN = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
|
||||
const useStyles = makeStyles({
|
||||
const useStyles = makeStyles()({
|
||||
group: {
|
||||
display: "inline-flex",
|
||||
placeItems: "center",
|
||||
@ -23,7 +23,7 @@ const useStyles = makeStyles({
|
||||
});
|
||||
|
||||
export function SourceFilesDev({ parentRerender }: { parentRerender: () => void }): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const { classes } = useStyles();
|
||||
|
||||
const setSF = useCallback(
|
||||
(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.
|
||||
- `MarkupMultiplier`: initialize with 1.
|
||||
- `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 `(SellingPrice > MarketPrice + MarkupLimit)`:
|
||||
$$MarkupMultiplier = \left(\frac{MarkupLimit}{SellingPrice - MarketPrice}\right)^{2}$$
|
||||
- If item is material and `SellingPrice` is less than `MarketPrice`:
|
||||
- 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)`:
|
||||
- If `(SellingPrice > MarketPrice + MarkupLimit)`:
|
||||
$$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
|
||||
|
||||
|
@ -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.
|
||||
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
|
||||
|
||||
|
2518
src/Documentation/doc/changelog-v0.md
Normal file
2518
src/Documentation/doc/changelog-v0.md
Normal file
File diff suppressed because it is too large
Load Diff
659
src/Documentation/doc/changelog-v1.md
Normal file
659
src/Documentation/doc/changelog-v1.md
Normal file
@ -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)
|
||||
- [Tools & Resources](help/tools_and_resources.md)
|
||||
- [Changelog](changelog.md)
|
||||
- [Changelog - Legacy v1](changelog-v1.md)
|
||||
- [Changelog - Legacy v0](changelog-v0.md)
|
||||
|
||||
## Migration
|
||||
|
||||
|
@ -45,19 +45,21 @@ import file42 from "!!raw-loader!./doc/basic/stats.md";
|
||||
import file43 from "!!raw-loader!./doc/basic/stockmarket.md";
|
||||
import file44 from "!!raw-loader!./doc/basic/terminal.md";
|
||||
import file45 from "!!raw-loader!./doc/basic/world.md";
|
||||
import file46 from "!!raw-loader!./doc/changelog.md";
|
||||
import file47 from "!!raw-loader!./doc/help/bitnode_order.md";
|
||||
import file48 from "!!raw-loader!./doc/help/getting_started.md";
|
||||
import file49 from "!!raw-loader!./doc/help/tools_and_resources.md";
|
||||
import file50 from "!!raw-loader!./doc/index.md";
|
||||
import file51 from "!!raw-loader!./doc/migrations/ns2.md";
|
||||
import file52 from "!!raw-loader!./doc/migrations/v1.md";
|
||||
import file53 from "!!raw-loader!./doc/migrations/v2.md";
|
||||
import file54 from "!!raw-loader!./doc/programming/game_frozen.md";
|
||||
import file55 from "!!raw-loader!./doc/programming/go_algorithms.md";
|
||||
import file56 from "!!raw-loader!./doc/programming/hackingalgorithms.md";
|
||||
import file57 from "!!raw-loader!./doc/programming/learn.md";
|
||||
import file58 from "!!raw-loader!./doc/programming/remote_api.md";
|
||||
import file46 from "!!raw-loader!./doc/changelog-v0.md";
|
||||
import file47 from "!!raw-loader!./doc/changelog-v1.md";
|
||||
import file48 from "!!raw-loader!./doc/changelog.md";
|
||||
import file49 from "!!raw-loader!./doc/help/bitnode_order.md";
|
||||
import file50 from "!!raw-loader!./doc/help/getting_started.md";
|
||||
import file51 from "!!raw-loader!./doc/help/tools_and_resources.md";
|
||||
import file52 from "!!raw-loader!./doc/index.md";
|
||||
import file53 from "!!raw-loader!./doc/migrations/ns2.md";
|
||||
import file54 from "!!raw-loader!./doc/migrations/v1.md";
|
||||
import file55 from "!!raw-loader!./doc/migrations/v2.md";
|
||||
import file56 from "!!raw-loader!./doc/programming/game_frozen.md";
|
||||
import file57 from "!!raw-loader!./doc/programming/go_algorithms.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 {
|
||||
default: string;
|
||||
@ -109,16 +111,18 @@ AllPages["basic/stats.md"] = file42;
|
||||
AllPages["basic/stockmarket.md"] = file43;
|
||||
AllPages["basic/terminal.md"] = file44;
|
||||
AllPages["basic/world.md"] = file45;
|
||||
AllPages["changelog.md"] = file46;
|
||||
AllPages["help/bitnode_order.md"] = file47;
|
||||
AllPages["help/getting_started.md"] = file48;
|
||||
AllPages["help/tools_and_resources.md"] = file49;
|
||||
AllPages["index.md"] = file50;
|
||||
AllPages["migrations/ns2.md"] = file51;
|
||||
AllPages["migrations/v1.md"] = file52;
|
||||
AllPages["migrations/v2.md"] = file53;
|
||||
AllPages["programming/game_frozen.md"] = file54;
|
||||
AllPages["programming/go_algorithms.md"] = file55;
|
||||
AllPages["programming/hackingalgorithms.md"] = file56;
|
||||
AllPages["programming/learn.md"] = file57;
|
||||
AllPages["programming/remote_api.md"] = file58;
|
||||
AllPages["changelog-v0.md"] = file46;
|
||||
AllPages["changelog-v1.md"] = file47;
|
||||
AllPages["changelog.md"] = file48;
|
||||
AllPages["help/bitnode_order.md"] = file49;
|
||||
AllPages["help/getting_started.md"] = file50;
|
||||
AllPages["help/tools_and_resources.md"] = file51;
|
||||
AllPages["index.md"] = file52;
|
||||
AllPages["migrations/ns2.md"] = file53;
|
||||
AllPages["migrations/v1.md"] = file54;
|
||||
AllPages["migrations/v2.md"] = file55;
|
||||
AllPages["programming/game_frozen.md"] = file56;
|
||||
AllPages["programming/go_algorithms.md"] = file57;
|
||||
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 { MathJax } from "better-react-mathjax";
|
||||
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Box from "@mui/material/Box";
|
||||
@ -24,14 +23,12 @@ interface IProps {
|
||||
factionInfo: FactionInfo;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
noformat: {
|
||||
whiteSpace: "pre-wrap",
|
||||
lineHeight: "1em",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const useStyles = makeStyles()({
|
||||
noformat: {
|
||||
whiteSpace: "pre-wrap",
|
||||
lineHeight: "1em",
|
||||
},
|
||||
});
|
||||
|
||||
function DefaultAssignment(): React.ReactElement {
|
||||
return (
|
||||
@ -46,7 +43,7 @@ function DefaultAssignment(): React.ReactElement {
|
||||
|
||||
export function Info(props: IProps): React.ReactElement {
|
||||
useRerender(200);
|
||||
const classes = useStyles();
|
||||
const { classes } = useStyles();
|
||||
|
||||
const Assignment = props.factionInfo.assignment ?? DefaultAssignment;
|
||||
|
||||
|
@ -21,7 +21,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export function GangMemberStats(props: IProps): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const { classes } = useStyles();
|
||||
|
||||
const asc = {
|
||||
hack: props.member.calculateAscensionMult(props.member.hack_asc_points),
|
||||
|
@ -14,6 +14,7 @@ import { useGang } from "./Context";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import Box from "@mui/material/Box";
|
||||
import { GangConstants } from "../data/Constants";
|
||||
|
||||
export function GangStats(): React.ReactElement {
|
||||
const gang = useGang();
|
||||
@ -40,7 +41,10 @@ export function GangStats(): React.ReactElement {
|
||||
}
|
||||
>
|
||||
<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>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
@ -55,7 +59,10 @@ export function GangStats(): React.ReactElement {
|
||||
}
|
||||
>
|
||||
<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>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
@ -69,7 +76,14 @@ export function GangStats(): React.ReactElement {
|
||||
</Box>
|
||||
|
||||
<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>
|
||||
|
||||
<Box display="flex">
|
||||
|
@ -6,7 +6,8 @@ export const opponentDetails = {
|
||||
[GoOpponent.none]: {
|
||||
komi: 5.5,
|
||||
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: "",
|
||||
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";
|
||||
|
||||
|
11
src/Go/Go.ts
11
src/Go/Go.ts
@ -11,6 +11,7 @@ export class GoObject {
|
||||
currentGame: BoardState = getNewBoardState(7);
|
||||
stats: PartialRecord<GoOpponent, OpponentStats> = {};
|
||||
nextTurn: Promise<Play> = Promise.resolve({ type: GoPlayType.gameOver, x: null, y: null });
|
||||
storedCycles: number = 0;
|
||||
|
||||
prestigeAugmentation() {
|
||||
for (const stats of getRecordValues(this.stats)) {
|
||||
@ -24,6 +25,16 @@ export class GoObject {
|
||||
this.currentGame = getNewBoardState(7);
|
||||
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();
|
||||
|
@ -21,6 +21,7 @@ type SaveFormat = {
|
||||
previousGame: PreviousGameSaveData;
|
||||
currentGame: CurrentGameSaveData;
|
||||
stats: PartialRecord<GoOpponent, OpponentStats>;
|
||||
storedCycles: number;
|
||||
};
|
||||
|
||||
export function getGoSave(): SaveFormat {
|
||||
@ -40,6 +41,7 @@ export function getGoSave(): SaveFormat {
|
||||
passCount: Go.currentGame.passCount,
|
||||
},
|
||||
stats: Go.stats,
|
||||
storedCycles: Go.storedCycles,
|
||||
};
|
||||
}
|
||||
|
||||
@ -78,6 +80,7 @@ export function loadGo(data: unknown): boolean {
|
||||
Go.currentGame = currentGame;
|
||||
Go.previousGame = previousGame;
|
||||
Go.stats = stats;
|
||||
Go.storeCycles(loadStoredCycles(parsedData.storedCycles));
|
||||
|
||||
// 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);
|
||||
@ -178,3 +181,11 @@ function loadSimpleBoard(simpleBoard: unknown, requiredSize?: number): SimpleBoa
|
||||
}
|
||||
return simpleBoard;
|
||||
}
|
||||
|
||||
function loadStoredCycles(storedCycles: unknown): number {
|
||||
if (!storedCycles || isNaN(+storedCycles)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return +storedCycles;
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ export type BoardState = {
|
||||
board: Board;
|
||||
previousPlayer: GoColor | null;
|
||||
/** The previous board positions as a SimpleBoard */
|
||||
previousBoards: SimpleBoard[];
|
||||
previousBoards: string[];
|
||||
ai: GoOpponent;
|
||||
passCount: 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 {
|
||||
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)
|
||||
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 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;
|
||||
}
|
||||
if (possibleRepeat && boardState.previousBoards.length) {
|
||||
const simpleEvalBoard = simpleBoardFromBoard(evaluationBoard);
|
||||
if (boardState.previousBoards.find((board) => areSimpleBoardsIdentical(simpleEvalBoard, board))) {
|
||||
const simpleEvalBoard = boardStringFromBoard(evaluationBoard);
|
||||
if (boardState.previousBoards.includes(simpleEvalBoard)) {
|
||||
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:
|
||||
* ```
|
||||
@ -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.
|
||||
*
|
||||
* 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
|
||||
* 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) =>
|
||||
column.reduce((str, 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) */
|
||||
export function boardFromSimpleBoard(simpleBoard: SimpleBoard): Board {
|
||||
return simpleBoard.map((column, x) =>
|
||||
@ -624,8 +670,9 @@ export function areSimpleBoardsIdentical(simpleBoard1: SimpleBoard, simpleBoard2
|
||||
return simpleBoard1.every((column, x) => column === simpleBoard2[x]);
|
||||
}
|
||||
|
||||
export function getColorOnSimpleBoard(simpleBoard: SimpleBoard, x: number, y: number): GoColor | null {
|
||||
const char = simpleBoard[x]?.[y];
|
||||
export function getColorOnBoardString(boardString: string, x: number, y: number): GoColor | null {
|
||||
const boardSize = Math.round(Math.sqrt(boardString.length));
|
||||
const char = boardString[x * boardSize + y];
|
||||
if (char === "X") return GoColor.black;
|
||||
if (char === "O") return GoColor.white;
|
||||
if (char === ".") return GoColor.empty;
|
||||
@ -643,7 +690,7 @@ export function getPreviousMove(): [number, number] | null {
|
||||
const row = Go.currentGame.board[+rowIndexString] ?? [];
|
||||
for (const pointIndexString in row) {
|
||||
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 isPreviousPlayer = currentColor === Go.currentGame.previousPlayer;
|
||||
const isChanged = priorColor !== currentColor;
|
||||
@ -655,3 +702,23 @@ export function getPreviousMove(): [number, number] | 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 { Player } from "@player";
|
||||
import { AugmentationName, GoOpponent, GoColor, GoPlayType } from "@enums";
|
||||
import { AugmentationName, GoColor, GoOpponent, GoPlayType } from "@enums";
|
||||
import { opponentDetails } from "../Constants";
|
||||
import { findNeighbors, isNotNullish, makeMove, passTurn } from "../boardState/boardState";
|
||||
import {
|
||||
@ -15,54 +15,83 @@ import {
|
||||
getAllEyesByChainId,
|
||||
getAllNeighboringChains,
|
||||
getAllValidMoves,
|
||||
getPreviousMoveDetails,
|
||||
} from "./boardAnalysis";
|
||||
import { findDisputedTerritory } from "./controlledTerritory";
|
||||
import { findAnyMatchedPatterns } from "./patternMatching";
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
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
|
||||
*/
|
||||
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 (currentAITurn) return currentAITurn;
|
||||
currentAITurn = Go.nextTurn = getMove(boardState, GoColor.white, Go.currentGame.ai)
|
||||
.then(async (play): Promise<Play> => {
|
||||
if (boardState !== Go.currentGame) return play; //Stale game
|
||||
if (isAiThinking) {
|
||||
return Go.nextTurn;
|
||||
}
|
||||
isAiThinking = true;
|
||||
|
||||
// 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 };
|
||||
// If the AI is disabled, simply make a promise to be resolved once the player makes a move as white
|
||||
if (boardState.ai === GoOpponent.none) {
|
||||
GoEvents.emit();
|
||||
// Update currentTurnResolver to call Go.nextTurn's resolve function with the last played move's details
|
||||
Go.nextTurn = new Promise((resolve) => (currentTurnResolver = () => resolve(getPreviousMoveDetails())));
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Handle AI making a move
|
||||
await sleep(500);
|
||||
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;
|
||||
})
|
||||
.finally(() => {
|
||||
currentAITurn = null;
|
||||
GoEvents.emit();
|
||||
});
|
||||
// Once the AI moves (or the player playing as white with No AI moves),
|
||||
// clear the isAiThinking semaphore and update the board UI.
|
||||
Go.nextTurn = Go.nextTurn.finally(() => {
|
||||
isAiThinking = false;
|
||||
GoEvents.emit();
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
@ -85,9 +114,10 @@ export async function getMove(
|
||||
boardState: BoardState,
|
||||
player: GoColor,
|
||||
opponent: GoOpponent,
|
||||
useOfflineCycles = true,
|
||||
rngOverride?: number,
|
||||
): Promise<Play & { type: GoPlayType.move | GoPlayType.pass }> {
|
||||
await sleep(300);
|
||||
await waitCycle(useOfflineCycles);
|
||||
const rng = new WHRNG(rngOverride || Player.totalPlaytime);
|
||||
const smart = isSmart(opponent, rng.random());
|
||||
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));
|
||||
|
||||
const chosenMove = moveOptions[Math.floor(rng.random() * moveOptions.length)];
|
||||
await waitCycle(useOfflineCycles);
|
||||
|
||||
if (chosenMove) {
|
||||
await sleep(200);
|
||||
//console.debug(`Non-priority move chosen: ${chosenMove.x} ${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> {
|
||||
await sleep(100);
|
||||
await waitCycle();
|
||||
if (moveOptions[id] !== undefined) {
|
||||
return moveOptions[id] ?? null;
|
||||
}
|
||||
@ -786,6 +816,18 @@ export function sleep(ms: number): Promise<void> {
|
||||
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() {
|
||||
return Player.hasAugmentation(AugmentationName.TheRedPill, true) && Player.sourceFileLvl(1);
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
findLibertiesForChain,
|
||||
getAllChains,
|
||||
boardFromSimpleBoard,
|
||||
simpleBoardFromBoard,
|
||||
boardStringFromBoard,
|
||||
} from "../boardAnalysis/boardAnalysis";
|
||||
import { endGoGame } from "../boardAnalysis/scoring";
|
||||
import { addObstacles, resetCoordinates, rotate90Degrees } from "./offlineNodes";
|
||||
@ -89,15 +89,12 @@ export function makeMove(boardState: BoardState, x: number, y: number, player: G
|
||||
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];
|
||||
if (!point) return false;
|
||||
|
||||
// Add move to board history
|
||||
boardState.previousBoards.unshift(boardStringFromBoard(boardState.board));
|
||||
|
||||
point.color = player;
|
||||
boardState.previousPlayer = player;
|
||||
boardState.passCount = 0;
|
||||
|
@ -1,9 +1,31 @@
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
import { keyframes } from "tss-react";
|
||||
|
||||
export const pointStyle = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
type Size = "fiveByFive" | "sevenBySeven" | "nineByNine" | "thirteenByThirteen" | "nineteenByNineteen";
|
||||
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: {},
|
||||
valid: {},
|
||||
priorPoint: {},
|
||||
@ -12,24 +34,24 @@ export const pointStyle = makeStyles((theme: Theme) =>
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
|
||||
"&$hover$valid:hover $innerPoint": {
|
||||
[`&.${classes.hover}.${classes.valid}:hover .${classes.innerPoint}`]: {
|
||||
outlineColor: theme.colors.white,
|
||||
},
|
||||
"&$hover$priorPoint $innerPoint": {
|
||||
[`&.${classes.hover}.${classes.priorPoint} .${classes.innerPoint}`]: {
|
||||
outlineColor: theme.colors.white,
|
||||
},
|
||||
"&$hover$priorPoint $priorStoneTrad$blackPoint": {
|
||||
[`&.${classes.hover}.${classes.priorPoint} .${classes.priorStoneTrad}.${classes.blackPoint}`]: {
|
||||
outlineColor: theme.colors.white,
|
||||
display: "block",
|
||||
},
|
||||
"&$hover$priorPoint $priorStoneTrad$whitePoint": {
|
||||
[`&.${classes.hover}.${classes.priorPoint} .${classes.priorStoneTrad}.${classes.whitePoint}`]: {
|
||||
outlineColor: theme.colors.black,
|
||||
display: "block",
|
||||
},
|
||||
"&$hover:hover $coordinates": {
|
||||
[`&.${classes.hover}:hover .${classes.coordinates}`]: {
|
||||
display: "block",
|
||||
},
|
||||
"&:hover $broken": {
|
||||
[`&:hover .${classes.broken}`]: {
|
||||
opacity: "0.4",
|
||||
},
|
||||
},
|
||||
@ -43,7 +65,7 @@ export const pointStyle = makeStyles((theme: Theme) =>
|
||||
width: "83%",
|
||||
height: "83%",
|
||||
transition: "all 0.3s",
|
||||
"& $coordinates": {
|
||||
[`& .${classes.coordinates}`]: {
|
||||
fontSize: "10px",
|
||||
},
|
||||
},
|
||||
@ -59,142 +81,143 @@ export const pointStyle = makeStyles((theme: Theme) =>
|
||||
overflow: "hidden",
|
||||
},
|
||||
traditional: {
|
||||
"& $innerPoint": {
|
||||
[`& .${classes.innerPoint}`]: {
|
||||
display: "none",
|
||||
},
|
||||
"& $broken": {
|
||||
[`& .${classes.broken}`]: {
|
||||
backgroundImage: "none",
|
||||
backgroundColor: theme.colors.black,
|
||||
},
|
||||
"& $tradStone": {
|
||||
[`& .${classes.tradStone}`]: {
|
||||
display: "block",
|
||||
},
|
||||
"& $liberty": {
|
||||
[`& .${classes.liberty}`]: {
|
||||
backgroundColor: theme.colors.black,
|
||||
transition: "none",
|
||||
"&:not($northLiberty):not($southLiberty):not($eastLiberty):not($westLiberty)": {
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
[`&:not(.${classes.northLiberty}):not(.${classes.southLiberty}):not(.${classes.eastLiberty}):not(.${classes.westLiberty})`]:
|
||||
{
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
},
|
||||
"& $northLiberty, & $southLiberty": {
|
||||
[`& .${classes.northLiberty}, & .${classes.southLiberty}`]: {
|
||||
width: "0.9px",
|
||||
},
|
||||
"& $eastLiberty, & $westLiberty": {
|
||||
[`& .${classes.eastLiberty}, & .${classes.westLiberty}`]: {
|
||||
height: "0.9px",
|
||||
},
|
||||
"&$nineteenByNineteen": {
|
||||
"& $blackPoint": {
|
||||
[`&.${classes.nineteenByNineteen}`]: {
|
||||
[`& .${classes.blackPoint}`]: {
|
||||
"&: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%)`,
|
||||
},
|
||||
},
|
||||
"& $whitePoint": {
|
||||
[`& .${classes.whitePoint}`]: {
|
||||
"&: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%)`,
|
||||
},
|
||||
},
|
||||
"& $coordinates": {
|
||||
[`& .${classes.coordinates}`]: {
|
||||
fontSize: "0.9vw",
|
||||
},
|
||||
},
|
||||
"&$thirteenByThirteen": {
|
||||
"& $blackPoint": {
|
||||
[`&.${classes.thirteenByThirteen}`]: {
|
||||
[`& .${classes.blackPoint}`]: {
|
||||
"&: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%)`,
|
||||
},
|
||||
},
|
||||
"& $whitePoint": {
|
||||
[`& .${classes.whitePoint}`]: {
|
||||
"&: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%)`,
|
||||
},
|
||||
},
|
||||
"& $coordinates": {
|
||||
[`& .${classes.coordinates}`]: {
|
||||
fontSize: "0.9vw",
|
||||
},
|
||||
},
|
||||
"&$nineByNine": {
|
||||
"& $blackPoint": {
|
||||
[`&.${classes.nineByNine}`]: {
|
||||
[`& .${classes.blackPoint}`]: {
|
||||
"&: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%)`,
|
||||
},
|
||||
},
|
||||
"& $whitePoint": {
|
||||
[`& .${classes.whitePoint}`]: {
|
||||
"&: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%)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
"&$sevenBySeven": {
|
||||
"& $blackPoint": {
|
||||
[`&.${classes.sevenBySeven}`]: {
|
||||
[`& .${classes.blackPoint}`]: {
|
||||
"&: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%)`,
|
||||
},
|
||||
},
|
||||
"& $whitePoint": {
|
||||
[`& .${classes.whitePoint}`]: {
|
||||
"&: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%)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
"& $coordinates": {
|
||||
[`& .${classes.coordinates}`]: {
|
||||
color: "black",
|
||||
left: "15%",
|
||||
},
|
||||
"& $blackPoint ~ $coordinates": {
|
||||
[`& .${classes.blackPoint} ~ .${classes.coordinates}`]: {
|
||||
color: theme.colors.white,
|
||||
},
|
||||
},
|
||||
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%)`,
|
||||
},
|
||||
"& $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%)`,
|
||||
},
|
||||
},
|
||||
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%)`,
|
||||
},
|
||||
"& $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%)`,
|
||||
},
|
||||
},
|
||||
nineByNine: {
|
||||
"& $filledPoint": {
|
||||
[`& .${classes.filledPoint}`]: {
|
||||
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%)`,
|
||||
},
|
||||
"& $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%)`,
|
||||
},
|
||||
},
|
||||
thirteenByThirteen: {
|
||||
"& $filledPoint": {
|
||||
[`& .${classes.filledPoint}`]: {
|
||||
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%)`,
|
||||
},
|
||||
"& $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%)`,
|
||||
},
|
||||
},
|
||||
nineteenByNineteen: {
|
||||
"& $filledPoint": {
|
||||
[`& .${classes.filledPoint}`]: {
|
||||
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%)`,
|
||||
},
|
||||
"& $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%)`,
|
||||
},
|
||||
|
||||
"& $innerPoint": {
|
||||
[`& .${classes.innerPoint}`]: {
|
||||
width: "70%",
|
||||
height: "70%",
|
||||
margin: "15%",
|
||||
@ -229,7 +252,7 @@ export const pointStyle = makeStyles((theme: Theme) =>
|
||||
top: 0,
|
||||
},
|
||||
|
||||
"&$blackPoint": {
|
||||
[`&.${classes.blackPoint}`]: {
|
||||
position: "static",
|
||||
outlineWidth: 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%)`,
|
||||
},
|
||||
},
|
||||
"&$whitePoint": {
|
||||
[`&.${classes.whitePoint}`]: {
|
||||
backgroundColor: "transparent",
|
||||
width: 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%)`,
|
||||
},
|
||||
},
|
||||
"&$emptyPoint": {
|
||||
[`&.${classes.emptyPoint}`]: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
margin: 0,
|
||||
@ -305,15 +328,7 @@ export const pointStyle = makeStyles((theme: Theme) =>
|
||||
outlineColor: theme.colors.white,
|
||||
},
|
||||
fadeLoopAnimation: {
|
||||
animation: `$fadeLoop 800ms ${theme.transitions.easing.easeInOut} infinite alternate`,
|
||||
},
|
||||
"@keyframes fadeLoop": {
|
||||
"0%": {
|
||||
opacity: 0.4,
|
||||
},
|
||||
"100%": {
|
||||
opacity: 1,
|
||||
},
|
||||
animation: `${fadeLoop} 800ms ${theme.transitions.easing.easeInOut} infinite alternate`,
|
||||
},
|
||||
liberty: {
|
||||
position: "absolute",
|
||||
@ -381,8 +396,8 @@ export const pointStyle = makeStyles((theme: Theme) =>
|
||||
}),
|
||||
);
|
||||
|
||||
export const boardStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
export const boardStyles = makeStyles<void, Size | "background">({ uniqId: "boardStyles" })(
|
||||
(theme: Theme, _, classes) => ({
|
||||
tab: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
@ -469,15 +484,7 @@ export const boardStyles = makeStyles((theme: Theme) =>
|
||||
borderColor: theme.colors.success,
|
||||
padding: "0 12px",
|
||||
width: "200px",
|
||||
animation: `$fadeLoop 600ms ${theme.transitions.easing.easeInOut} infinite alternate`,
|
||||
},
|
||||
"@keyframes fadeLoop": {
|
||||
"0%": {
|
||||
opacity: 0.6,
|
||||
},
|
||||
"100%": {
|
||||
opacity: 1,
|
||||
},
|
||||
animation: `${fadeLoop} 600ms ${theme.transitions.easing.easeInOut} infinite alternate`,
|
||||
},
|
||||
scoreBox: {
|
||||
display: "inline-flex",
|
||||
@ -491,35 +498,35 @@ export const boardStyles = makeStyles((theme: Theme) =>
|
||||
},
|
||||
fiveByFive: {
|
||||
height: "20%",
|
||||
"& $fiveByFive": {
|
||||
[`& .${classes.fiveByFive}`]: {
|
||||
width: "20%",
|
||||
height: "100%",
|
||||
},
|
||||
},
|
||||
sevenBySeven: {
|
||||
height: "14%",
|
||||
"& $sevenBySeven": {
|
||||
[`& .${classes.sevenBySeven}`]: {
|
||||
width: "14%",
|
||||
height: "100%",
|
||||
},
|
||||
},
|
||||
nineByNine: {
|
||||
height: "11%",
|
||||
"& $nineByNine": {
|
||||
[`& .${classes.nineByNine}`]: {
|
||||
width: "11%",
|
||||
height: "100%",
|
||||
},
|
||||
},
|
||||
thirteenByThirteen: {
|
||||
height: "7.5%",
|
||||
"& $thirteenByThirteen": {
|
||||
[`& .${classes.thirteenByThirteen}`]: {
|
||||
width: "7.5%",
|
||||
height: "100%",
|
||||
},
|
||||
},
|
||||
nineteenByNineteen: {
|
||||
height: "5.2%",
|
||||
"& $nineteenByNineteen": {
|
||||
[`& .${classes.nineteenByNineteen}`]: {
|
||||
width: "5.2%",
|
||||
height: "100%",
|
||||
},
|
||||
@ -535,7 +542,7 @@ export const boardStyles = makeStyles((theme: Theme) =>
|
||||
paddingTop: "15px",
|
||||
},
|
||||
bitverseBackground: {
|
||||
"&$background": {
|
||||
[`&.${classes.background}`]: {
|
||||
fontSize: "calc(min(.83vh - 1px, 0.72vw, 7.856px))",
|
||||
opacity: 0.11,
|
||||
},
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Play, SimpleOpponentStats } from "../Types";
|
||||
import { Play, SimpleBoard, SimpleOpponentStats } from "../Types";
|
||||
|
||||
import { Player } from "@player";
|
||||
import { AugmentationName, GoColor, GoOpponent, GoPlayType, GoValidity } from "@enums";
|
||||
@ -10,8 +10,9 @@ import {
|
||||
getControlledSpace,
|
||||
getPreviousMove,
|
||||
simpleBoardFromBoard,
|
||||
simpleBoardFromBoardString,
|
||||
} from "../boardAnalysis/boardAnalysis";
|
||||
import { getOpponentStats, getScore, resetWinstreak } from "../boardAnalysis/scoring";
|
||||
import { endGoGame, getOpponentStats, getScore, resetWinstreak } from "../boardAnalysis/scoring";
|
||||
import { WHRNG } from "../../Casino/RNG";
|
||||
import { getRecordKeys } from "../../Types/Record";
|
||||
import { CalculateEffect, getEffectTypeForFaction } from "./effect";
|
||||
@ -223,6 +224,8 @@ export function getControlledEmptyNodes() {
|
||||
* 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.
|
||||
*
|
||||
* Also provides the white player's komi (bonus starting score), and the amount of bonus cycles from offline time remaining
|
||||
*/
|
||||
export function getGameState() {
|
||||
const currentPlayer = getCurrentPlayer();
|
||||
@ -234,9 +237,15 @@ export function getGameState() {
|
||||
whiteScore: score[GoColor.white].sum,
|
||||
blackScore: score[GoColor.black].sum,
|
||||
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
|
||||
*/
|
||||
@ -337,30 +346,27 @@ export async function determineCheatSuccess(
|
||||
): Promise<Play> {
|
||||
const state = Go.currentGame;
|
||||
const rng = new WHRNG(Player.totalPlaytime);
|
||||
state.passCount = 0;
|
||||
|
||||
// If cheat is successful, run callback
|
||||
if ((successRngOverride ?? rng.random()) <= cheatSuccessChance(state.cheatCount)) {
|
||||
callback();
|
||||
state.cheatCount++;
|
||||
GoEvents.emit();
|
||||
return makeAIMove(state);
|
||||
}
|
||||
// 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) {
|
||||
logger(`Cheat failed! You have been ejected from the subnet.`);
|
||||
resetBoardState(logger, logger, state.ai, state.board[0].length);
|
||||
return {
|
||||
type: GoPlayType.gameOver,
|
||||
x: null,
|
||||
y: null,
|
||||
};
|
||||
endGoGame(state);
|
||||
return Go.nextTurn;
|
||||
}
|
||||
// If the cheat fails, your turn is skipped
|
||||
else {
|
||||
logger(`Cheat failed. Your turn has been skipped.`);
|
||||
passTurn(state, GoColor.black, false);
|
||||
state.cheatCount++;
|
||||
return makeAIMove(state);
|
||||
}
|
||||
|
||||
state.cheatCount++;
|
||||
return makeAIMove(state);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -395,7 +401,7 @@ export function cheatRemoveRouter(
|
||||
y: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
) {
|
||||
): Promise<Play> {
|
||||
const point = Go.currentGame.board[x][y]!;
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
@ -421,7 +427,7 @@ export function cheatPlayTwoMoves(
|
||||
y2: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
) {
|
||||
): Promise<Play> {
|
||||
const point1 = Go.currentGame.board[x1][y1]!;
|
||||
const point2 = Go.currentGame.board[x2][y2]!;
|
||||
|
||||
@ -446,7 +452,7 @@ export function cheatRepairOfflineNode(
|
||||
y: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
) {
|
||||
): Promise<Play> {
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
@ -472,7 +478,7 @@ export function cheatDestroyNode(
|
||||
y: number,
|
||||
successRngOverride?: number,
|
||||
ejectRngOverride?: number,
|
||||
) {
|
||||
): Promise<Play> {
|
||||
return determineCheatSuccess(
|
||||
logger,
|
||||
() => {
|
||||
|
@ -27,7 +27,7 @@ export function GoGameboard({ boardState, traditional, clickHandler, hover }: Go
|
||||
}
|
||||
|
||||
const boardSize = boardState.board[0].length;
|
||||
const classes = boardStyles();
|
||||
const { classes } = boardStyles();
|
||||
|
||||
return (
|
||||
<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 { bitverseArt, weiArt } from "../boardState/asciiArt";
|
||||
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 { OptionSwitch } from "../../ui/React/OptionSwitch";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
@ -18,7 +18,7 @@ import { GoScoreModal } from "./GoScoreModal";
|
||||
import { GoGameboard } from "./GoGameboard";
|
||||
import { GoSubnetSearch } from "./GoSubnetSearch";
|
||||
import { CorruptableText } from "../../ui/React/CorruptableText";
|
||||
import { makeAIMove } from "../boardAnalysis/goAI";
|
||||
import { makeAIMove, resolveCurrentTurn } from "../boardAnalysis/goAI";
|
||||
|
||||
interface GoGameboardWrapperProps {
|
||||
showInstructions: () => void;
|
||||
@ -46,7 +46,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
|
||||
const [scoreOpen, setScoreOpen] = useState(false);
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
|
||||
const classes = boardStyles();
|
||||
const { classes } = boardStyles();
|
||||
const boardSize = boardState.board[0].length;
|
||||
const currentPlayer = boardState.previousPlayer === GoColor.white ? GoColor.black : GoColor.white;
|
||||
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);
|
||||
if (didUpdateBoard) {
|
||||
rerender();
|
||||
Go.currentGame.ai !== GoOpponent.none && takeAiTurn(boardState);
|
||||
takeAiTurn(boardState);
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,12 +104,18 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
Go.currentGame.ai !== GoOpponent.none && takeAiTurn(boardState);
|
||||
takeAiTurn(boardState);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
rerender();
|
||||
resolveCurrentTurn();
|
||||
}
|
||||
|
||||
function getPriorMove() {
|
||||
if (!boardState.previousBoards.length) return boardState;
|
||||
const priorState = getStateCopy(boardState);
|
||||
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);
|
||||
return priorState;
|
||||
}
|
||||
@ -159,17 +166,19 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
|
||||
rerender();
|
||||
}
|
||||
|
||||
const endGameAvailable = boardState.previousPlayer === GoColor.white && boardState.passCount;
|
||||
const noLegalMoves =
|
||||
boardState.previousPlayer === GoColor.white && !getAllValidMoves(boardState, GoColor.black).length;
|
||||
const ongoingNoAiGame = boardState.ai === GoOpponent.none && boardState.previousPlayer;
|
||||
const manualTurnAvailable = ongoingNoAiGame || boardState.previousPlayer === GoColor.white;
|
||||
const endGameAvailable = manualTurnAvailable && boardState.passCount;
|
||||
const noLegalMoves = manualTurnAvailable && !getAllValidMoves(boardState, currentPlayer).length;
|
||||
|
||||
const scoreBoxText = boardState.previousBoards.length
|
||||
? `Score: Black: ${score[GoColor.black].sum} White: ${score[GoColor.white].sum}`
|
||||
: "Place a router to begin!";
|
||||
|
||||
const getPassButtonLabel = () => {
|
||||
const playerString = boardState.ai === GoOpponent.none ? ` (${currentPlayer})` : "";
|
||||
if (endGameAvailable) {
|
||||
return "End Game";
|
||||
return `End Game${playerString}`;
|
||||
}
|
||||
if (boardState.previousPlayer === null) {
|
||||
return "View Final Score";
|
||||
@ -177,8 +186,7 @@ export function GoGameboardWrapper({ showInstructions }: GoGameboardWrapperProps
|
||||
if (waitingOnAI) {
|
||||
return "Waiting for opponent";
|
||||
}
|
||||
const currentPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
|
||||
return `Pass Turn${boardState.ai === GoOpponent.none ? ` (${currentPlayer})` : ""}`;
|
||||
return `Pass Turn${playerString}`;
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -16,7 +16,7 @@ import { getRecordKeys } from "../../Types/Record";
|
||||
|
||||
export const GoHistoryPage = (): React.ReactElement => {
|
||||
useRerender(400);
|
||||
const classes = boardStyles();
|
||||
const { classes } = boardStyles();
|
||||
const priorBoard = Go.previousGame ?? getNewBoardState(7);
|
||||
const score = getScore(priorBoard);
|
||||
const opponent = priorBoard.ai;
|
||||
|
@ -70,7 +70,7 @@ const makeTwoEyesChallenge = (
|
||||
);
|
||||
|
||||
export const GoInstructionsPage = (): React.ReactElement => {
|
||||
const classes = boardStyles();
|
||||
const { classes } = boardStyles();
|
||||
return (
|
||||
<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
|
||||
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.
|
||||
<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>
|
||||
<br />
|
||||
<br />
|
||||
|
@ -1,13 +1,12 @@
|
||||
import type { BoardState } from "../Types";
|
||||
|
||||
import React from "react";
|
||||
import { ClassNameMap } from "@mui/styles";
|
||||
|
||||
import { GoColor } from "@enums";
|
||||
import { columnIndexes } from "../Constants";
|
||||
import { findNeighbors } from "../boardState/boardState";
|
||||
import { pointStyle } from "../boardState/goStyles";
|
||||
import { findAdjacentLibertiesAndAlliesForPoint, getColorOnSimpleBoard } from "../boardAnalysis/boardAnalysis";
|
||||
import { boardStyles, pointStyle } from "../boardState/goStyles";
|
||||
import { findAdjacentLibertiesAndAlliesForPoint, getColorOnBoardString } from "../boardAnalysis/boardAnalysis";
|
||||
|
||||
interface GoPointProps {
|
||||
state: BoardState;
|
||||
@ -20,7 +19,7 @@ interface GoPointProps {
|
||||
}
|
||||
|
||||
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 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 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 emptyPointColorClass =
|
||||
@ -89,10 +88,7 @@ export function GoPoint({ state, x, y, traditional, hover, valid, emptyPointOwne
|
||||
);
|
||||
}
|
||||
|
||||
export function getSizeClass(
|
||||
size: number,
|
||||
classes: ClassNameMap<"fiveByFive" | "sevenBySeven" | "nineByNine" | "thirteenByThirteen" | "nineteenByNineteen">,
|
||||
) {
|
||||
export function getSizeClass(size: number, classes: ReturnType<typeof boardStyles | typeof pointStyle>["classes"]) {
|
||||
switch (size) {
|
||||
case 5:
|
||||
return classes.fiveByFive;
|
||||
|
@ -9,7 +9,7 @@ import { GoGameboardWrapper } from "./GoGameboardWrapper";
|
||||
import { boardStyles } from "../boardState/goStyles";
|
||||
|
||||
export function GoRoot(): React.ReactElement {
|
||||
const classes = boardStyles();
|
||||
const { classes } = boardStyles();
|
||||
const [value, setValue] = React.useState(0);
|
||||
|
||||
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 => {
|
||||
const classes = boardStyles();
|
||||
const { classes } = boardStyles();
|
||||
|
||||
const blackScore = finalScore[GoColor.black];
|
||||
const whiteScore = finalScore[GoColor.white];
|
||||
|
@ -18,7 +18,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const GoScorePowerSummary = ({ finalScore, opponent }: Props) => {
|
||||
const classes = boardStyles();
|
||||
const { classes } = boardStyles();
|
||||
const status = getOpponentStats(opponent);
|
||||
const winStreak = status.winStreak;
|
||||
const oldWinStreak = status.winStreak;
|
||||
|
@ -12,7 +12,7 @@ interface GoScoreSummaryTableProps {
|
||||
}
|
||||
|
||||
export const GoScoreSummaryTable = ({ score, opponent }: GoScoreSummaryTableProps) => {
|
||||
const classes = boardStyles();
|
||||
const { classes } = boardStyles();
|
||||
const blackScore = score[GoColor.black];
|
||||
const whiteScore = score[GoColor.white];
|
||||
const blackPlayerName = opponent === GoOpponent.none ? GoColor.black : "You";
|
||||
|
@ -13,7 +13,7 @@ import { GoOpponent } from "@enums";
|
||||
|
||||
export const GoStatusPage = (): React.ReactElement => {
|
||||
useRerender(400);
|
||||
const classes = boardStyles();
|
||||
const { classes } = boardStyles();
|
||||
const score = getScore(Go.currentGame);
|
||||
const opponent = Go.currentGame.ai;
|
||||
const playedOpponentList = getRecordKeys(Go.stats).filter((o) => o !== GoOpponent.none);
|
||||
|
@ -18,9 +18,10 @@ interface IProps {
|
||||
cancel: () => void;
|
||||
showInstructions: () => void;
|
||||
}
|
||||
const boardSizeOptions = boardSizes.filter((size) => size !== 19);
|
||||
|
||||
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 preselectedBoardSize =
|
||||
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>
|
||||
) : (
|
||||
<Select value={`${boardSize}`} onChange={changeBoardSize} sx={{ mr: 1 }}>
|
||||
{boardSizes.map((size) => (
|
||||
{boardSizeOptions.map((size) => (
|
||||
<MenuItem key={size} value={size}>
|
||||
{size}x{size}
|
||||
</MenuItem>
|
||||
|
@ -33,7 +33,7 @@ export function GoTutorialChallenge({
|
||||
incorrectText2,
|
||||
}: IProps): React.ReactElement {
|
||||
const stateRef = useRef(getStateCopy(state));
|
||||
const classes = boardStyles();
|
||||
const { classes } = boardStyles();
|
||||
const [displayText, setDisplayText] = useState(description);
|
||||
const [showReset, setShowReset] = useState(false);
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { currentNodeMults } from "./BitNode/BitNodeMultipliers";
|
||||
import { Person as IPerson } from "@nsdefs";
|
||||
import { calculateIntelligenceBonus } from "./PersonObjects/formulas/intelligence";
|
||||
import { Server as IServer } from "@nsdefs";
|
||||
import { clampNumber } from "./utils/helpers/clampNumber";
|
||||
|
||||
/** Returns the chance the person has to successfully hack a server */
|
||||
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;
|
||||
const hackFactor = 1.75;
|
||||
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 chance =
|
||||
skillChance *
|
||||
difficultyMult *
|
||||
person.mults.hacking_chance *
|
||||
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 { Player } from "@player";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { KEY } from "../../utils/helpers/keyCodes";
|
||||
import { random } from "../utils";
|
||||
import { interpolate } from "./Difficulty";
|
||||
import { GameTimer } from "./GameTimer";
|
||||
@ -32,19 +31,17 @@ const difficulties: {
|
||||
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 colorNames: Record<string, string> = {
|
||||
red: "red",
|
||||
"#FFC107": "yellow",
|
||||
blue: "blue",
|
||||
white: "white",
|
||||
red: "RED",
|
||||
"#FFC107": "YELLOW",
|
||||
blue: "BLUE",
|
||||
white: "WHITE",
|
||||
};
|
||||
|
||||
interface Wire {
|
||||
wireType: string;
|
||||
wireType: string[];
|
||||
colors: string[];
|
||||
}
|
||||
|
||||
@ -142,7 +139,7 @@ export function WireCuttingGame({ onSuccess, onFailure, difficulty }: IMinigameP
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
{new Array(8).fill(0).map((_, i) => (
|
||||
{new Array(11).fill(0).map((_, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{wires.map((wire, 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];
|
||||
return (
|
||||
<Typography key={j} style={{ color: wireColor }}>
|
||||
|{wire.wireType}|
|
||||
|{wire.wireType[i % wire.wireType.length]}|
|
||||
</Typography>
|
||||
);
|
||||
})}
|
||||
@ -172,7 +169,7 @@ function randomPositionQuestion(wires: Wire[]): Question {
|
||||
toString: (): string => {
|
||||
return `Cut wires number ${index + 1}.`;
|
||||
},
|
||||
shouldCut: (wire: Wire, i: number): boolean => {
|
||||
shouldCut: (_wire: Wire, i: number): boolean => {
|
||||
return index === i;
|
||||
},
|
||||
};
|
||||
@ -209,8 +206,9 @@ function generateWires(difficulty: Difficulty): Wire[] {
|
||||
if (Math.random() < 0.15) {
|
||||
wireColors.push(colors[Math.floor(Math.random() * colors.length)]);
|
||||
}
|
||||
const wireType = [...wireColors.map((color) => colorNames[color]).join("")];
|
||||
wires.push({
|
||||
wireType: types[Math.floor(Math.random() * types.length)],
|
||||
wireType,
|
||||
colors: wireColors,
|
||||
});
|
||||
}
|
||||
|
@ -18,24 +18,21 @@ import { Page } from "../../ui/Router";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
|
||||
interface IProps {
|
||||
city: City;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
location: {
|
||||
color: theme.colors.maplocation,
|
||||
whiteSpace: "nowrap",
|
||||
margin: "0px",
|
||||
padding: "0px",
|
||||
cursor: "pointer",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const useStyles = makeStyles()((theme: Theme) => ({
|
||||
location: {
|
||||
color: theme.colors.maplocation,
|
||||
whiteSpace: "nowrap",
|
||||
margin: "0px",
|
||||
padding: "0px",
|
||||
cursor: "pointer",
|
||||
},
|
||||
}));
|
||||
|
||||
function toLocation(location: Location): void {
|
||||
if (location.name === LocationName.TravelAgency) {
|
||||
@ -97,7 +94,7 @@ function ASCIICity(props: IProps): React.ReactElement {
|
||||
Y: 24,
|
||||
Z: 25,
|
||||
};
|
||||
const classes = useStyles();
|
||||
const { classes } = useStyles();
|
||||
|
||||
const lineElems = (s: string): (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;
|
||||
|
||||
Player.location = companyNameAsLocationName(props.companyName);
|
||||
Player.gotoLocation(companyNameAsLocationName(props.companyName));
|
||||
|
||||
function startInfiltration(e: React.MouseEvent<HTMLElement>): void {
|
||||
if (!e.isTrusted) {
|
||||
|
@ -24,14 +24,12 @@ import Button from "@mui/material/Button";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
|
||||
function travel(to: CityName): void {
|
||||
const cost = CONSTANTS.TravelCost;
|
||||
if (!Player.canAfford(cost)) {
|
||||
if (!Player.travel(to)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Player.loseMoney(cost, "other");
|
||||
Player.travel(to);
|
||||
if (!Settings.SuppressTravelConfirmation) dialogBoxCreate(`You are now in ${to}!`);
|
||||
if (!Settings.SuppressTravelConfirmation) {
|
||||
dialogBoxCreate(`You are now in ${to}!`);
|
||||
}
|
||||
Router.toPage(Page.City);
|
||||
}
|
||||
|
||||
@ -41,8 +39,7 @@ export function TravelAgencyRoot(): React.ReactElement {
|
||||
useRerender(1000);
|
||||
|
||||
function startTravel(city: CityName): void {
|
||||
const cost = CONSTANTS.TravelCost;
|
||||
if (!Player.canAfford(cost)) {
|
||||
if (!Player.canAfford(CONSTANTS.TravelCost)) {
|
||||
return;
|
||||
}
|
||||
if (Settings.SuppressTravelConfirmation) {
|
||||
|
@ -14,7 +14,6 @@ interface IProps {
|
||||
}
|
||||
|
||||
export function TravelConfirmationModal(props: IProps): React.ReactElement {
|
||||
const cost = CONSTANTS.TravelCost;
|
||||
function travel(): void {
|
||||
props.travel();
|
||||
}
|
||||
@ -22,7 +21,8 @@ export function TravelConfirmationModal(props: IProps): React.ReactElement {
|
||||
return (
|
||||
<Modal open={props.open} onClose={props.onClose}>
|
||||
<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>
|
||||
<br />
|
||||
<br />
|
||||
|
@ -250,6 +250,7 @@ const go = {
|
||||
makeMove: 4,
|
||||
passTurn: 0,
|
||||
getBoardState: 4,
|
||||
getMoveHistory: 0,
|
||||
getCurrentPlayer: 0,
|
||||
getGameState: 0,
|
||||
getOpponent: 0,
|
||||
|
@ -739,11 +739,11 @@ export const ns: InternalAPI<NSFull> = {
|
||||
const args = helpers.scriptArgs(ctx, _args);
|
||||
setTimeout(() => {
|
||||
const scriptServer = GetServer(ctx.workerScript.hostname);
|
||||
if (scriptServer == null) {
|
||||
throw helpers.errorMessage(ctx, "Could not find server. This is a bug. Report to dev");
|
||||
if (scriptServer === null) {
|
||||
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);
|
||||
|
||||
helpers.log(ctx, () => `Will execute '${path}' in ${runOpts.spawnDelay} milliseconds`);
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
getCurrentPlayer,
|
||||
getGameState,
|
||||
getLiberties,
|
||||
getMoveHistory,
|
||||
getOpponentNextMove,
|
||||
getStats,
|
||||
getValidMoves,
|
||||
@ -47,17 +48,20 @@ export function NetscriptGo(): InternalAPI<NSGo> {
|
||||
validateMove(error(ctx), x, y, "makeMove");
|
||||
return makePlayerMove(logger(ctx), error(ctx), x, y);
|
||||
},
|
||||
passTurn: (ctx: NetscriptContext) => (): Promise<Play> => {
|
||||
passTurn: (ctx: NetscriptContext) => async (): Promise<Play> => {
|
||||
validateTurn(error(ctx), "passTurn()");
|
||||
return handlePassTurn(logger(ctx));
|
||||
},
|
||||
opponentNextTurn: (ctx: NetscriptContext) => (_logOpponentMove) => {
|
||||
opponentNextTurn: (ctx: NetscriptContext) => async (_logOpponentMove) => {
|
||||
const logOpponentMove = typeof _logOpponentMove === "boolean" ? _logOpponentMove : true;
|
||||
return getOpponentNextMove(logOpponentMove, logger(ctx));
|
||||
},
|
||||
getBoardState: () => () => {
|
||||
return simpleBoardFromBoard(Go.currentGame.board);
|
||||
},
|
||||
getMoveHistory: () => () => {
|
||||
return getMoveHistory();
|
||||
},
|
||||
getCurrentPlayer: () => () => {
|
||||
return getCurrentPlayer();
|
||||
},
|
||||
|
@ -269,7 +269,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
Player.location = LocationName.Sector12RothmanUniversity;
|
||||
Player.gotoLocation(LocationName.Sector12RothmanUniversity);
|
||||
break;
|
||||
case LocationName.VolhavenZBInstituteOfTechnology.toLowerCase():
|
||||
if (Player.city != CityName.Volhaven) {
|
||||
@ -279,7 +279,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
Player.location = LocationName.VolhavenZBInstituteOfTechnology;
|
||||
Player.gotoLocation(LocationName.VolhavenZBInstituteOfTechnology);
|
||||
break;
|
||||
default:
|
||||
helpers.log(ctx, () => `Invalid university name: '${universityName}'.`);
|
||||
@ -327,7 +327,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
Player.location = LocationName.AevumCrushFitnessGym;
|
||||
Player.gotoLocation(LocationName.AevumCrushFitnessGym);
|
||||
break;
|
||||
case LocationName.AevumSnapFitnessGym.toLowerCase():
|
||||
if (Player.city != CityName.Aevum) {
|
||||
@ -338,7 +338,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
Player.location = LocationName.AevumSnapFitnessGym;
|
||||
Player.gotoLocation(LocationName.AevumSnapFitnessGym);
|
||||
break;
|
||||
case LocationName.Sector12IronGym.toLowerCase():
|
||||
if (Player.city != CityName.Sector12) {
|
||||
@ -349,7 +349,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
Player.location = LocationName.Sector12IronGym;
|
||||
Player.gotoLocation(LocationName.Sector12IronGym);
|
||||
break;
|
||||
case LocationName.Sector12PowerhouseGym.toLowerCase():
|
||||
if (Player.city != CityName.Sector12) {
|
||||
@ -360,7 +360,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
Player.location = LocationName.Sector12PowerhouseGym;
|
||||
Player.gotoLocation(LocationName.Sector12PowerhouseGym);
|
||||
break;
|
||||
case LocationName.VolhavenMilleniumFitnessGym.toLowerCase():
|
||||
if (Player.city != CityName.Volhaven) {
|
||||
@ -371,7 +371,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
Player.location = LocationName.VolhavenMilleniumFitnessGym;
|
||||
Player.gotoLocation(LocationName.VolhavenMilleniumFitnessGym);
|
||||
break;
|
||||
default:
|
||||
helpers.log(ctx, () => `Invalid gym name: ${gymName}. gymWorkout() failed`);
|
||||
@ -401,12 +401,10 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
case CityName.NewTokyo:
|
||||
case CityName.Ishima:
|
||||
case CityName.Volhaven:
|
||||
if (Player.money < CONSTANTS.TravelCost) {
|
||||
if (!Player.travel(cityName)) {
|
||||
helpers.log(ctx, () => "Not enough money to travel.");
|
||||
return false;
|
||||
}
|
||||
Player.loseMoney(CONSTANTS.TravelCost, "other");
|
||||
Player.city = cityName;
|
||||
helpers.log(ctx, () => `Traveled to ${cityName}`);
|
||||
Player.gainIntelligenceExp(CONSTANTS.IntelligenceSingFnBaseExpGain / 50000);
|
||||
return true;
|
||||
@ -747,7 +745,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
|
||||
quitJob: (ctx) => (_companyName) => {
|
||||
helpers.checkSingularityAccess(ctx);
|
||||
const companyName = getEnumHelper("CompanyName").nsGetMember(ctx, _companyName);
|
||||
Player.quitJob(companyName);
|
||||
Player.quitJob(companyName, true);
|
||||
},
|
||||
getCompanyRep: (ctx) => (_companyName) => {
|
||||
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}`);
|
||||
|
||||
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 = () => {
|
||||
if (Player.skills.hacking < wd.requiredHackingSkill) 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 { Player } from "@player";
|
||||
import { BladeActionType } from "@enums";
|
||||
import { BladeActionType, type BladeContractName } from "@enums";
|
||||
import { Augmentations } from "../Augmentation/Augmentations";
|
||||
import { findCrime } from "../Crime/CrimeHelpers";
|
||||
import { getEnumHelper } from "../utils/EnumHelper";
|
||||
@ -79,7 +79,11 @@ export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
|
||||
const cityName = getEnumHelper("CityName").nsGetMember(ctx, _cityName);
|
||||
checkSleeveAPIAccess(ctx);
|
||||
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) => {
|
||||
const sleeveNumber = helpers.number(ctx, "sleeveNumber", _sleeveNumber);
|
||||
@ -238,24 +242,23 @@ export function NetscriptSleeve(): InternalAPI<NetscriptSleeve> {
|
||||
const action = helpers.string(ctx, "action", _action);
|
||||
checkSleeveAPIAccess(ctx);
|
||||
checkSleeveNumber(ctx, sleeveNumber);
|
||||
let contractName = undefined;
|
||||
let contract: BladeContractName | undefined = undefined;
|
||||
if (action === "Take on contracts") {
|
||||
const contractEnum = getEnumHelper("BladeContractName").nsGetMember(ctx, _contract);
|
||||
contractName = helpers.string(ctx, "contract", _contract);
|
||||
contract = getEnumHelper("BladeContractName").nsGetMember(ctx, _contract);
|
||||
for (let i = 0; i < Player.sleeves.length; ++i) {
|
||||
if (i === sleeveNumber) continue;
|
||||
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(
|
||||
ctx,
|
||||
`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 }));
|
||||
}
|
||||
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 { Script } from "./Script/Script";
|
||||
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.
|
||||
export type Node = any;
|
||||
@ -125,7 +124,7 @@ function generateLoadedModule(script: Script, scripts: Map<ScriptFilePath, Scrip
|
||||
let newCode = script.code;
|
||||
// Loop through each node and replace the script name with a blob url.
|
||||
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}`);
|
||||
|
||||
// Find the corresponding script.
|
||||
@ -149,6 +148,7 @@ function generateLoadedModule(script: Script, scripts: Map<ScriptFilePath, Scrip
|
||||
// script dedupe properly.
|
||||
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.
|
||||
|
||||
const url = URL.createObjectURL(makeScriptBlob(adjustedCode)) as ScriptURL;
|
||||
const module = config.doImport(url).catch((e) => {
|
||||
script.invalidateModule();
|
||||
|
@ -361,14 +361,16 @@ export function getNextCompanyPosition(
|
||||
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) {
|
||||
this.finishWork(true);
|
||||
}
|
||||
for (const sleeve of this.sleeves) {
|
||||
if (sleeve.currentWork?.type === SleeveWorkType.COMPANY && sleeve.currentWork.companyName === company) {
|
||||
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];
|
||||
@ -529,20 +531,23 @@ export function gainCodingContractReward(
|
||||
}
|
||||
}
|
||||
|
||||
export function travel(this: PlayerObject, to: CityName): boolean {
|
||||
if (Cities[to] == null) {
|
||||
console.warn(`Player.travel() called with invalid city: ${to}`);
|
||||
export function travel(this: PlayerObject, cityName: CityName): boolean {
|
||||
if (Cities[cityName] == null) {
|
||||
throw new Error(`Player.travel() was called with an invalid city: ${cityName}`);
|
||||
}
|
||||
if (!this.canAfford(CONSTANTS.TravelCost)) {
|
||||
return false;
|
||||
}
|
||||
this.city = to;
|
||||
|
||||
this.loseMoney(CONSTANTS.TravelCost, "other");
|
||||
this.city = cityName;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function gotoLocation(this: PlayerObject, to: LocationName): boolean {
|
||||
if (Locations[to] == null) {
|
||||
console.warn(`Player.gotoLocation() called with invalid location: ${to}`);
|
||||
return false;
|
||||
throw new Error(`Player.gotoLocation() was called with an invalid location: ${to}`);
|
||||
}
|
||||
this.location = to;
|
||||
|
||||
|
@ -44,6 +44,7 @@ import { SleeveCrimeWork } from "./Work/SleeveCrimeWork";
|
||||
import * as sleeveMethods from "./SleeveMethods";
|
||||
import { calculateIntelligenceBonus } from "../formulas/intelligence";
|
||||
import { getEnumHelper } from "../../utils/EnumHelper";
|
||||
import { Cities } from "../../Locations/Cities";
|
||||
|
||||
export class Sleeve extends Person implements SleevePerson {
|
||||
currentWork: SleeveWork | null = null;
|
||||
@ -255,13 +256,16 @@ export class Sleeve extends Person implements SleevePerson {
|
||||
}
|
||||
|
||||
/** 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)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Player.loseMoney(CONSTANTS.TravelCost, "sleeves");
|
||||
this.city = newCity;
|
||||
this.city = cityName;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -27,12 +27,13 @@ export function SleeveAugmentationsModal(props: IProps): React.ReactElement {
|
||||
<Modal open={props.open} onClose={props.onClose}>
|
||||
<Container component={Paper} disableGutters maxWidth="lg" sx={{ mx: 0, mb: 1, p: 1 }}>
|
||||
<Typography>
|
||||
You can purchase Augmentations for your Duplicate Sleeves. These Augmentations have the same effect as they
|
||||
would for you. You can only purchase Augmentations that you have unlocked through Factions.
|
||||
You can purchase augmentations for your Sleeves. These augmentations have the same effect as they would for
|
||||
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 />
|
||||
When purchasing an Augmentation for a Duplicate Sleeve, they are immediately installed. This means that the
|
||||
Duplicate Sleeve will immediately lose all of its stat experience.
|
||||
When purchasing an augmentation for a Sleeve, it is immediately installed. This means that the Sleeve will
|
||||
immediately lose all of its stat experience.
|
||||
<br />
|
||||
<br />
|
||||
Augmentations will appear below as they become available.
|
||||
|
@ -33,7 +33,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export function StatsElement(props: IProps): React.ReactElement {
|
||||
const classes = useStyles();
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<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 {
|
||||
const classes = useStyles();
|
||||
const { classes } = useStyles();
|
||||
|
||||
let data: (string | JSX.Element)[][] = [];
|
||||
if (isSleeveCrimeWork(props.sleeve.currentWork)) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import { Player } from "@player";
|
||||
import { CityName } from "@enums";
|
||||
import { Sleeve } from "../Sleeve";
|
||||
import { CONSTANTS } from "../../../Constants";
|
||||
@ -19,11 +18,10 @@ interface IProps {
|
||||
|
||||
export function TravelModal(props: IProps): React.ReactElement {
|
||||
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");
|
||||
return;
|
||||
}
|
||||
props.sleeve.city = city as CityName;
|
||||
Player.loseMoney(CONSTANTS.TravelCost, "sleeves");
|
||||
props.sleeve.stopWork();
|
||||
props.rerender();
|
||||
props.onClose();
|
||||
|
@ -14,7 +14,7 @@ import { RamCosts, RamCostConstants } from "../Netscript/RamCostGenerator";
|
||||
import { Script } from "./Script";
|
||||
import { Node } from "../NetscriptJSEvaluator";
|
||||
import { ScriptFilePath, resolveScriptFilePath } from "../Paths/ScriptFilePath";
|
||||
import { root } from "../Paths/Directory";
|
||||
import { ServerName } from "../Types/strings";
|
||||
|
||||
export interface RamUsageEntry {
|
||||
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
|
||||
* RAM usage. Also accounts for imported modules.
|
||||
* @param otherScripts - All other scripts on the server. Used to account for imported scripts
|
||||
* @param code - The code being parsed */
|
||||
function parseOnlyRamCalculate(otherScripts: Map<ScriptFilePath, Script>, code: string, ns1?: boolean): RamCalculation {
|
||||
* @param code - The code being parsed
|
||||
* @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.
|
||||
*
|
||||
* 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
|
||||
* of its imports, and any identifiers referenced in this global scope. Each
|
||||
* function depends on all the identifiers referenced internally.
|
||||
@ -74,10 +83,10 @@ function parseOnlyRamCalculate(otherScripts: Map<ScriptFilePath, Script>, code:
|
||||
const completedParses = new Set();
|
||||
|
||||
// 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.
|
||||
function parseCode(code: string, moduleName: string): void {
|
||||
const result = parseOnlyCalculateDeps(code, moduleName);
|
||||
function parseCode(code: string, moduleName: ScriptFilePath): void {
|
||||
const result = parseOnlyCalculateDeps(code, moduleName, ns1);
|
||||
completedParses.add(moduleName);
|
||||
|
||||
// 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
|
||||
const initialModule = "__SPECIAL_INITIAL_MODULE__";
|
||||
const initialModule = scriptname;
|
||||
parseCode(code, initialModule);
|
||||
|
||||
// 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.startsWith("https://") || nextModule.startsWith("http://")) continue;
|
||||
|
||||
// Using root as the path base right now. Difficult to implement
|
||||
const filename = resolveScriptFilePath(nextModule, root, ns1 ? ".script" : ".js");
|
||||
if (!filename) {
|
||||
return { errorCode: RamCalculationErrorCode.ImportError, errorMessage: `Invalid import path: "${nextModule}"` };
|
||||
}
|
||||
const script = otherScripts.get(filename);
|
||||
const script = otherScripts.get(nextModule);
|
||||
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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
const detailedCosts: RamUsageEntry[] = [{ type: "misc", name: "baseCost", cost: RamCostConstants.Base }];
|
||||
const unresolvedRefs = Object.keys(dependencyMap).filter((s) => s.startsWith(initialModule));
|
||||
@ -250,7 +257,7 @@ export function checkInfiniteLoop(code: string): number[] {
|
||||
|
||||
interface ParseDepsResult {
|
||||
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
|
||||
* 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" });
|
||||
// Everything from the global scope goes in ".". Everything else goes in ".function", where only
|
||||
// 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.
|
||||
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
|
||||
// any aliases.
|
||||
@ -338,7 +345,12 @@ function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsR
|
||||
Object.assign(
|
||||
{
|
||||
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);
|
||||
|
||||
// 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
|
||||
* @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.
|
||||
* 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(
|
||||
code: string,
|
||||
scriptname: ScriptFilePath,
|
||||
otherScripts: Map<ScriptFilePath, Script>,
|
||||
server: ServerName,
|
||||
ns1?: boolean,
|
||||
): RamCalculation {
|
||||
try {
|
||||
return parseOnlyRamCalculate(otherScripts, code, ns1);
|
||||
return parseOnlyRamCalculate(otherScripts, code, scriptname, server, ns1);
|
||||
} catch (e) {
|
||||
return {
|
||||
errorCode: RamCalculationErrorCode.SyntaxError,
|
||||
|
@ -73,7 +73,13 @@ export class Script implements ContentFile {
|
||||
* @param {Script[]} otherScripts - Other scripts on the server. Used to process imports
|
||||
*/
|
||||
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) {
|
||||
this.ramUsage = roundToTwo(ramCalc.cost);
|
||||
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
|
||||
const thisUpdate = new Date().getTime();
|
||||
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
|
||||
//entirely off of time. We will arbitrarily say that if a script has been running for
|
||||
|
27
src/ScriptEditor/NetscriptDefinitions.d.ts
vendored
27
src/ScriptEditor/NetscriptDefinitions.d.ts
vendored
@ -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.
|
||||
* @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.
|
||||
@ -3321,7 +3321,9 @@ export interface Bladeburner {
|
||||
* @remarks
|
||||
* 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.
|
||||
*
|
||||
@ -3329,7 +3331,7 @@ export interface Bladeburner {
|
||||
* @param name - Name of action. Must be an exact match.
|
||||
* @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.
|
||||
@ -4049,6 +4051,21 @@ export interface Go {
|
||||
*/
|
||||
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 "White" | "Black" | "None"
|
||||
@ -4065,6 +4082,8 @@ export interface Go {
|
||||
whiteScore: number;
|
||||
blackScore: number;
|
||||
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.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
|
@ -8,11 +8,13 @@ import { useBoolean } from "../../ui/React/hooks";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
|
||||
import { Options } from "./Options";
|
||||
import { FilePath } from "../../Paths/FilePath";
|
||||
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
|
||||
|
||||
export interface ScriptEditorContextShape {
|
||||
ram: string;
|
||||
ramEntries: string[][];
|
||||
updateRAM: (newCode: string | null, server: BaseServer | null) => void;
|
||||
updateRAM: (newCode: string | null, filename: FilePath | null, server: BaseServer | null) => void;
|
||||
|
||||
isUpdatingRAM: boolean;
|
||||
startUpdatingRAM: () => void;
|
||||
@ -28,14 +30,13 @@ export function ScriptEditorContextProvider({ children, vim }: { children: React
|
||||
const [ram, setRAM] = useState("RAM: ???");
|
||||
const [ramEntries, setRamEntries] = useState<string[][]>([["???", ""]]);
|
||||
|
||||
const updateRAM: ScriptEditorContextShape["updateRAM"] = (newCode, server) => {
|
||||
if (newCode === null || server === null) {
|
||||
const updateRAM: ScriptEditorContextShape["updateRAM"] = (newCode, filename, server) => {
|
||||
if (newCode == null || filename == null || server == null || !hasScriptExtension(filename)) {
|
||||
setRAM("N/A");
|
||||
setRamEntries([["N/A", ""]]);
|
||||
return;
|
||||
}
|
||||
|
||||
const ramUsage = calculateRamUsage(newCode, server.scripts);
|
||||
const ramUsage = calculateRamUsage(newCode, filename, server.scripts, server.hostname);
|
||||
if (ramUsage.cost && ramUsage.cost > 0) {
|
||||
const entries = ramUsage.entries?.sort((a, b) => b.cost - a.cost) ?? [];
|
||||
const entriesDisp = [];
|
||||
|
@ -143,6 +143,7 @@ function Root(props: IProps): React.ReactElement {
|
||||
infLoop(newCode);
|
||||
updateRAM(
|
||||
!currentScript || currentScript.isTxt ? null : newCode,
|
||||
currentScript && currentScript.path,
|
||||
currentScript && GetServer(currentScript.hostname),
|
||||
);
|
||||
finishUpdatingRAM();
|
||||
@ -372,7 +373,7 @@ function Root(props: IProps): React.ReactElement {
|
||||
}
|
||||
}
|
||||
|
||||
const { VimStatus } = useVimEditor({
|
||||
const { statusBarRef } = useVimEditor({
|
||||
editor: editorRef.current,
|
||||
vim: options.vim,
|
||||
onSave: save,
|
||||
@ -410,7 +411,7 @@ function Root(props: IProps): React.ReactElement {
|
||||
<div style={{ flex: "0 0 5px" }} />
|
||||
<Editor onMount={onMount} onChange={updateCode} onUnmount={onUnmountEditor} />
|
||||
|
||||
{VimStatus}
|
||||
{statusBarRef.current}
|
||||
|
||||
<Toolbar onSave={save} editor={editorRef.current} />
|
||||
</div>
|
||||
|
248
src/ScriptEditor/ui/StatusBar.tsx
Normal file
248
src/ScriptEditor/ui/StatusBar.tsx
Normal file
@ -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";
|
||||
type IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
import { Router } from "../../ui/GameRoot";
|
||||
import { Page } from "../../ui/Router";
|
||||
import { StatusBar } from "./StatusBar";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
|
||||
interface IProps {
|
||||
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
|
||||
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 });
|
||||
actionsRef.current = { save: onSave, openNextTab: onOpenNextTab, openPreviousTab: onOpenPreviousTab };
|
||||
@ -31,7 +32,7 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
|
||||
if (vim && editor && !vimEditor) {
|
||||
// Using try/catch because MonacoVim does not have types.
|
||||
try {
|
||||
setVimEditor(MonacoVim.initVimMode(editor, vimStatusRef.current));
|
||||
setVimEditor(MonacoVim.initVimMode(editor, statusBarRef, StatusBar, rerender));
|
||||
MonacoVim.VimMode.Vim.defineEx("write", "w", function () {
|
||||
// your own implementation on what you want to do when :w is pressed
|
||||
actionsRef.current.save();
|
||||
@ -40,6 +41,10 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
|
||||
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 => {
|
||||
actionsRef.current.save();
|
||||
Router.toPage(Page.Terminal);
|
||||
@ -72,19 +77,7 @@ export function useVimEditor({ editor, vim, onOpenNextTab, onOpenPreviousTab, on
|
||||
return () => {
|
||||
vimEditor?.dispose();
|
||||
};
|
||||
}, [vim, editor, vimEditor]);
|
||||
}, [vim, editor, vimEditor, rerender]);
|
||||
|
||||
const VimStatus = (
|
||||
<Box
|
||||
ref={vimStatusRef}
|
||||
className="vim-display"
|
||||
display="flex"
|
||||
flexGrow="0"
|
||||
flexDirection="row"
|
||||
sx={{ p: 1 }}
|
||||
alignItems="center"
|
||||
/>
|
||||
);
|
||||
|
||||
return { VimStatus };
|
||||
return { statusBarRef };
|
||||
}
|
||||
|
@ -154,6 +154,12 @@ export function initForeignServers(homeComputer: Server): void {
|
||||
if (metadata.serverGrowth) serverParams.serverGrowth = toNumber(metadata.serverGrowth);
|
||||
|
||||
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 || []) {
|
||||
server.messages.push(filename);
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { useMemo, useCallback, useState, useEffect } from "react";
|
||||
import { KEYCODE } from "../../utils/helpers/keyCodes";
|
||||
import { styled, Theme, CSSObject } from "@mui/material/styles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
import MuiDrawer from "@mui/material/Drawer";
|
||||
import List from "@mui/material/List";
|
||||
import Divider from "@mui/material/Divider";
|
||||
@ -101,14 +100,12 @@ const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== "open"
|
||||
}),
|
||||
}));
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
active: {
|
||||
borderLeft: "3px solid " + theme.palette.primary.main,
|
||||
},
|
||||
listitem: {},
|
||||
}),
|
||||
);
|
||||
const useStyles = makeStyles()((theme: Theme) => ({
|
||||
active: {
|
||||
borderLeft: "3px solid " + theme.palette.primary.main,
|
||||
},
|
||||
listitem: {},
|
||||
}));
|
||||
|
||||
export function SidebarRoot(props: { page: Page }): React.ReactElement {
|
||||
useRerender(200);
|
||||
@ -257,7 +254,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
|
||||
return () => document.removeEventListener("keydown", handleShortcuts);
|
||||
}, [canJob, clickPage, props.page]);
|
||||
|
||||
const classes = useStyles();
|
||||
const { classes } = useStyles();
|
||||
const [open, setOpen] = useState(Settings.IsSidebarOpened);
|
||||
const toggleDrawer = (): void =>
|
||||
setOpen((old) => {
|
||||
|
@ -148,8 +148,7 @@ export const HelpTexts: Record<string, string[]> = {
|
||||
cd: [
|
||||
"Usage: cd [dir]",
|
||||
" ",
|
||||
"Change to the specified directory. Note that this works even for directories that don't exist. If you ",
|
||||
"change to a directory that does not exist, it will not be 'created'. Examples:",
|
||||
"Change to the specified directory. You cannot change to a directory that does not exist. Examples:",
|
||||
" ",
|
||||
" cd scripts/hacking",
|
||||
" ",
|
||||
|
@ -211,7 +211,7 @@ export class Terminal {
|
||||
Router.toPage(Page.BitVerse, { flume: false, quick: false });
|
||||
return;
|
||||
}
|
||||
// Manunally check for faction invites
|
||||
// Manually check for faction invitations
|
||||
Engine.Counters.checkFactionInvitations = 0;
|
||||
Engine.checkCounters();
|
||||
|
||||
@ -225,7 +225,9 @@ export class Terminal {
|
||||
server.moneyAvailable -= moneyGained;
|
||||
Player.gainMoney(moneyGained, "hacking");
|
||||
Player.gainHackingExp(expGainedOnSuccess);
|
||||
Player.gainIntelligenceExp(expGainedOnSuccess / CONSTANTS.IntelligenceTerminalHackBaseExpGain);
|
||||
if (expGainedOnSuccess > 1) {
|
||||
Player.gainIntelligenceExp(4 * Math.log10(expGainedOnSuccess));
|
||||
}
|
||||
|
||||
const oldSec = server.hackDifficulty;
|
||||
server.fortify(ServerConstants.ServerFortifyAmount);
|
||||
|
@ -7,8 +7,7 @@ import type { ProgramFilePath } from "../../Paths/ProgramFilePath";
|
||||
import type { ContentFilePath } from "../../Paths/ContentFile";
|
||||
import type { ScriptFilePath } from "../../Paths/ScriptFilePath";
|
||||
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
import { BaseServer } from "../../Server/BaseServer";
|
||||
import { Router } from "../../ui/GameRoot";
|
||||
import { Page } from "../../ui/Router";
|
||||
@ -109,7 +108,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
|
||||
folders.sort();
|
||||
|
||||
function SegmentGrid(props: { colSize: string; children: React.ReactChild[] }): React.ReactElement {
|
||||
const classes = makeStyles({
|
||||
const { classes } = makeStyles()({
|
||||
segmentGrid: {
|
||||
display: "grid",
|
||||
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 {
|
||||
const classes = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
link: {
|
||||
cursor: "pointer",
|
||||
textDecorationLine: "underline",
|
||||
color: theme.palette.warning.main,
|
||||
},
|
||||
}),
|
||||
)();
|
||||
const { classes } = makeStyles()((theme: Theme) => ({
|
||||
link: {
|
||||
cursor: "pointer",
|
||||
textDecorationLine: "underline",
|
||||
color: theme.palette.warning.main,
|
||||
},
|
||||
}))();
|
||||
const fullPath = combinePath(baseDirectory, props.path);
|
||||
function onClick() {
|
||||
let content;
|
||||
@ -155,7 +152,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
|
||||
}
|
||||
|
||||
function ClickableMessageLink(props: { path: FilePath }): React.ReactElement {
|
||||
const classes = makeStyles({
|
||||
const { classes } = makeStyles()({
|
||||
link: {
|
||||
cursor: "pointer",
|
||||
textDecorationLine: "underline",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
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 { KEY, KEYCODE } from "../../utils/helpers/keyCodes";
|
||||
@ -10,29 +10,27 @@ import { getTabCompletionPossibilities } from "../getTabCompletionPossibilities"
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { longestCommonStart } from "../../utils/StringHelperFunctions";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
input: {
|
||||
backgroundColor: theme.colors.backgroundprimary,
|
||||
},
|
||||
nopadding: {
|
||||
padding: theme.spacing(0),
|
||||
},
|
||||
preformatted: {
|
||||
margin: theme.spacing(0),
|
||||
},
|
||||
absolute: {
|
||||
margin: theme.spacing(0),
|
||||
position: "absolute",
|
||||
bottom: "12px",
|
||||
opacity: "0.75",
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "pre",
|
||||
overflow: "hidden",
|
||||
pointerEvents: "none",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const useStyles = makeStyles()((theme: Theme) => ({
|
||||
input: {
|
||||
backgroundColor: theme.colors.backgroundprimary,
|
||||
},
|
||||
nopadding: {
|
||||
padding: theme.spacing(0),
|
||||
},
|
||||
preformatted: {
|
||||
margin: theme.spacing(0),
|
||||
},
|
||||
absolute: {
|
||||
margin: theme.spacing(0),
|
||||
position: "absolute",
|
||||
bottom: "12px",
|
||||
opacity: "0.75",
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "pre",
|
||||
overflow: "hidden",
|
||||
pointerEvents: "none",
|
||||
},
|
||||
}));
|
||||
|
||||
// Save command in case we de-load this screen.
|
||||
let command = "";
|
||||
@ -46,7 +44,7 @@ export function TerminalInput(): React.ReactElement {
|
||||
const [searchResults, setSearchResults] = useState<string[]>([]);
|
||||
const [searchResultsIndex, setSearchResultsIndex] = useState(0);
|
||||
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 (Terminal.commandHistory.length === 0 && Player.terminalCommandHistory.length > 0) {
|
||||
|
@ -1,8 +1,7 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Link as MuiLink, Typography } from "@mui/material";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import makeStyles from "@mui/styles/makeStyles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import { makeStyles } from "tss-react/mui";
|
||||
import _ from "lodash";
|
||||
|
||||
import { Output, Link, RawOutput } from "../OutputTypes";
|
||||
@ -16,27 +15,25 @@ import { ANSIITypography } from "../../ui/React/ANSIITypography";
|
||||
import { useRerender } from "../../ui/React/hooks";
|
||||
import { TerminalActionTimer } from "./TerminalActionTimer";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
container: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "calc(100vh - 16px)",
|
||||
},
|
||||
entries: {
|
||||
padding: 0,
|
||||
overflow: "scroll",
|
||||
flex: "0 1 auto",
|
||||
margin: "auto 0 0",
|
||||
},
|
||||
preformatted: {
|
||||
whiteSpace: "pre-wrap",
|
||||
overflowWrap: "anywhere",
|
||||
margin: theme.spacing(0),
|
||||
width: "100%",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const useStyles = makeStyles()((theme: Theme) => ({
|
||||
container: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "calc(100vh - 16px)",
|
||||
},
|
||||
entries: {
|
||||
padding: 0,
|
||||
overflow: "scroll",
|
||||
flex: "0 1 auto",
|
||||
margin: "auto 0 0",
|
||||
},
|
||||
preformatted: {
|
||||
whiteSpace: "pre-wrap",
|
||||
overflowWrap: "anywhere",
|
||||
margin: theme.spacing(0),
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
|
||||
export function TerminalRoot(): React.ReactElement {
|
||||
const scrollHook = useRef<HTMLUListElement>(null);
|
||||
@ -82,7 +79,7 @@ export function TerminalRoot(): React.ReactElement {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const classes = useStyles();
|
||||
const { classes } = useStyles();
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<ul key={key} id="terminal" className={classes.entries} ref={scrollHook}>
|
||||
|
@ -21,10 +21,11 @@ export class CreateProgramWork extends Work {
|
||||
programName: CompletedProgramName;
|
||||
// amount of effective work completed on the program (time boosted by skills).
|
||||
unitCompleted: number;
|
||||
|
||||
unitRate: number;
|
||||
constructor(params?: CreateProgramWorkParams) {
|
||||
super(WorkType.CREATE_PROGRAM, params?.singularity ?? true);
|
||||
this.unitCompleted = 0;
|
||||
this.unitRate = 0;
|
||||
this.programName = params?.programName ?? CompletedProgramName.bruteSsh;
|
||||
|
||||
if (params) {
|
||||
@ -63,7 +64,8 @@ export class CreateProgramWork extends Work {
|
||||
skillMult *= focusBonus;
|
||||
//Skill multiplier directly applied to "time worked"
|
||||
this.cyclesWorked += cycles;
|
||||
this.unitCompleted += CONSTANTS.MilliPerCycle * cycles * skillMult;
|
||||
this.unitRate = CONSTANTS.MilliPerCycle * cycles * skillMult;
|
||||
this.unitCompleted += this.unitRate;
|
||||
|
||||
if (this.unitCompleted >= this.unitNeeded()) {
|
||||
return true;
|
||||
|
@ -21,10 +21,12 @@ interface GraftingWorkParams {
|
||||
export class GraftingWork extends Work {
|
||||
augmentation: AugmentationName;
|
||||
unitCompleted: number;
|
||||
unitRate: number;
|
||||
|
||||
constructor(params?: GraftingWorkParams) {
|
||||
super(WorkType.GRAFTING, params?.singularity ?? true);
|
||||
this.unitCompleted = 0;
|
||||
this.unitRate = 0;
|
||||
this.augmentation = params?.augmentation ?? AugmentationName.Targeting1;
|
||||
const gAugs = GraftableAugmentations();
|
||||
if (params) Player.loseMoney(gAugs[this.augmentation].cost, "augmentations");
|
||||
@ -37,8 +39,8 @@ export class GraftingWork extends Work {
|
||||
process(cycles: number): boolean {
|
||||
const focusBonus = Player.focusPenalty();
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,11 @@ import { setupUncaughtPromiseHandler } from "./UncaughtPromiseHandler";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import { SnackbarEvents } from "./ui/React/Snackbar";
|
||||
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. */
|
||||
const Engine: {
|
||||
@ -246,7 +251,13 @@ const Engine: {
|
||||
// Calculate the number of cycles have elapsed while offline
|
||||
Engine._lastUpdate = new Date().getTime();
|
||||
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);
|
||||
|
||||
// Calculate the number of chances for a contract the player had whilst offline
|
||||
@ -330,6 +341,8 @@ const Engine: {
|
||||
// Bladeburner offline progress
|
||||
if (Player.bladeburner) Player.bladeburner.storeCycles(numCyclesOffline);
|
||||
|
||||
Go.storeCycles(numCyclesOffline);
|
||||
|
||||
staneksGift.process(numCyclesOffline);
|
||||
|
||||
// Sleeves offline progress
|
||||
@ -390,6 +403,12 @@ const Engine: {
|
||||
// Get time difference
|
||||
const _thisUpdate = new Date().getTime();
|
||||
let diff = _thisUpdate - Engine._lastUpdate;
|
||||
if (diff < 0) {
|
||||
diff = 0;
|
||||
Engine._lastUpdate = _thisUpdate;
|
||||
Player.lastUpdate = _thisUpdate;
|
||||
showWarningAboutSystemClock();
|
||||
}
|
||||
const offset = diff % CONSTANTS.MilliPerCycle;
|
||||
|
||||
// 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
Loading…
Reference in New Issue
Block a user