More work on terminal.

This commit is contained in:
Olivier Gagnon
2021-09-16 14:43:39 -04:00
parent f628a18551
commit 4a5fb04d41
6 changed files with 189 additions and 213 deletions

View File

@ -12,6 +12,7 @@ import MenuItem from "@material-ui/core/MenuItem";
import IconButton from "@material-ui/core/IconButton";
import ReplyAllIcon from "@material-ui/icons/ReplyAll";
import ReplyIcon from "@material-ui/icons/Reply";
import ClearIcon from "@material-ui/icons/Clear";
interface IProps {
player: IPlayer;
@ -33,6 +34,11 @@ export function Augmentations(props: IProps): React.ReactElement {
props.player.queueAugmentation(augName);
}
}
function clearAugs(): void {
props.player.augmentations = [];
}
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
@ -61,6 +67,13 @@ export function Augmentations(props: IProps): React.ReactElement {
</IconButton>
</>
}
endAdornment={
<>
<IconButton color="primary" onClick={clearAugs}>
<ClearIcon />
</IconButton>
</>
}
>
{Object.values(AugmentationNames).map((aug) => (
<MenuItem key={aug} value={aug}>

View File

@ -1,54 +1,5 @@
import { determineAllPossibilitiesForTabCompletion } from "./Terminal/determineAllPossibilitiesForTabCompletion";
import { tabCompletion } from "./Terminal/tabCompletion";
import { CONSTANTS } from "./Constants";
import { Engine } from "./engine";
import { FconfSettings } from "./Fconf/FconfSettings";
import { Player } from "./Player";
import { setTimeoutRef } from "./utils/SetTimeoutRef";
import { Page, routing } from "./ui/navigationTracking";
import { KEY } from "../utils/helpers/keyCodes";
import { getTimestamp } from "../utils/helpers/getTimestamp";
import { Terminal as TTerminal } from "./Terminal/Terminal";
const Terminal = new TTerminal();
// Defines key commands in terminal
$(document).keydown(function (event) {
// Terminal
if (event.keyCode === KEY.C && event.ctrlKey) {
if (Engine._actionInProgress) {
// Cancel action
// post("Cancelling...");
Engine._actionInProgress = false;
Terminal.finishAction(true);
} else if (FconfSettings.ENABLE_BASH_HOTKEYS) {
// Dont prevent default so it still copies
Terminal.clear(); // Clear Terminal
}
}
});
$(document).keydown(function (e) {
if (routing.isOn(Page.Terminal)) {
if (e.which == KEY.CTRL) {
terminalCtrlPressed = true;
} else if (e.shiftKey) {
shiftKeyPressed = true;
} else if (terminalCtrlPressed || shiftKeyPressed || Terminal.contractOpen) {
// Don't focus
} else {
var inputTextBox = document.getElementById("terminal-input-text-box");
if (inputTextBox != null) {
inputTextBox.focus();
}
terminalCtrlPressed = false;
shiftKeyPressed = false;
}
}
});
export { Terminal };

View File

@ -3,23 +3,44 @@ import { Script } from "../Script/Script";
import { IPlayer } from "../PersonObjects/IPlayer";
import { IEngine } from "../IEngine";
export interface IOutput {
export class Output {
text: string;
color: "inherit" | "initial" | "primary" | "secondary" | "error" | "textPrimary" | "textSecondary" | undefined;
constructor(
text: string,
color: "inherit" | "initial" | "primary" | "secondary" | "error" | "textPrimary" | "textSecondary" | undefined,
) {
this.text = text;
this.color = color;
}
}
export class Link {
hostname: string;
constructor(hostname: string) {
this.hostname = hostname;
}
}
export class TTimer {
time: number;
timeLeft: number;
action: "h" | "b" | "a";
constructor(time: number, action: "h" | "b" | "a") {
this.time = time;
this.timeLeft = time;
this.action = action;
}
}
export interface ITerminal {
// Flags to determine whether the player is currently running a hack or an analyze
hackFlag: boolean;
backdoorFlag: boolean;
analyzeFlag: boolean;
actionStarted: boolean;
actionTime: number;
action: TTimer | null;
commandHistory: string[];
commandHistoryIndex: number;
outputHistory: IOutput[];
outputHistory: (Output | Link)[];
// True if a Coding Contract prompt is opened
contractOpen: boolean;
@ -53,4 +74,7 @@ export interface ITerminal {
executeCommands(engine: IEngine, player: IPlayer, commands: string): void;
// If there was any changes, will return true, once.
pollChanges(): boolean;
process(player: IPlayer, cycles: number): void;
prestige(): void;
getProgressText(): string;
}

View File

@ -1,5 +1,5 @@
import { postContent, hackProgressBarPost, hackProgressPost } from "../ui/postToTerminal";
import { ITerminal, IOutput } from "./ITerminal";
import { ITerminal, Output, Link, TTimer } from "./ITerminal";
import { IEngine } from "../IEngine";
import { IPlayer } from "../PersonObjects/IPlayer";
import { HacknetServer } from "../Hacknet/HacknetServer";
@ -8,7 +8,6 @@ import { hackWorldDaemon } from "../RedPill";
import { Programs } from "../Programs/Programs";
import { CodingContractResult } from "../CodingContracts";
import { Terminal as OldTerminal } from "../Terminal";
import { TextFile } from "../TextFile";
import { Script } from "../Script/Script";
import { isScriptFilename } from "../Script/ScriptHelpersTS";
@ -24,6 +23,7 @@ import { TerminalHelpText } from "./HelpText";
import { GetServerByHostname, getServer, getServerOnNetwork } from "../Server/ServerHelpers";
import { ParseCommand, ParseCommands } from "./Parser";
import { SpecialServerIps, SpecialServerNames } from "../Server/SpecialServerIps";
import { createProgressBarText } from "../../utils/helpers/createProgressBarText";
import {
calculateHackingChance,
calculateHackingExpGain,
@ -72,16 +72,12 @@ import { wget } from "./commands/wget";
export class Terminal implements ITerminal {
hasChanges = false;
// Flags to determine whether the player is currently running a hack or an analyze
hackFlag = false;
backdoorFlag = false;
analyzeFlag = false;
actionStarted = false;
actionTime = 0;
action: TTimer | null = null;
commandHistory: string[] = [];
commandHistoryIndex = 0;
outputHistory: IOutput[] = [{ text: `Bitburner v${CONSTANTS.Version}`, color: "primary" }];
outputHistory: (Output | Link)[] = [{ text: `Bitburner v${CONSTANTS.Version}`, color: "primary" }];
// True if a Coding Contract prompt is opened
contractOpen = false;
@ -90,6 +86,13 @@ export class Terminal implements ITerminal {
// Excludes the trailing forward slash
currDir = "/";
process(player: IPlayer, cycles: number): void {
if (this.action === null) return;
this.action.timeLeft -= (CONSTANTS._idleSpeed * cycles) / 1000;
this.hasChanges = true;
if (this.action.timeLeft < 0) this.finishAction(player, false);
}
pollChanges(): boolean {
if (this.hasChanges) {
this.hasChanges = false;
@ -99,105 +102,85 @@ export class Terminal implements ITerminal {
}
print(s: string, config?: any): void {
this.outputHistory.push({ text: s, color: "primary" });
this.outputHistory.push(new Output(s, "primary"));
this.hasChanges = true;
}
error(s: string): void {
this.outputHistory.push({ text: s, color: "error" });
this.outputHistory.push(new Output(s, "error"));
this.hasChanges = true;
}
startHack(player: IPlayer): void {
this.hackFlag = true;
// Hacking through Terminal should be faster than hacking through a script
this.actionTime = calculateHackingTime(player.getCurrentServer(), player) / 4;
this.startAction();
this.startAction(calculateHackingTime(player.getCurrentServer(), player) / 4, "h");
}
startBackdoor(player: IPlayer): void {
this.backdoorFlag = true;
// Backdoor should take the same amount of time as hack
this.actionTime = calculateHackingTime(player.getCurrentServer(), player) / 4;
this.startAction();
this.startAction(calculateHackingTime(player.getCurrentServer(), player) / 4, "b");
}
startAnalyze(): void {
this.analyzeFlag = true;
this.actionTime = 1;
this.print("Analyzing system...");
this.startAction();
this.startAction(1, "a");
}
startAction(): void {
this.actionStarted = true;
hackProgressPost("Time left:");
hackProgressBarPost("[");
// Disable terminal
const elem = document.getElementById("terminal-input-td");
if (!elem) throw new Error("terminal-input-td should not be null");
elem.innerHTML = '<input type="text" class="terminal-input"/>';
$("input[class=terminal-input]").prop("disabled", true);
startAction(n: number, action: "h" | "b" | "a"): void {
this.action = new TTimer(n, action);
}
// Complete the hack/analyze command
finishHack(player: IPlayer, cancelled = false): void {
if (!cancelled) {
const server = player.getCurrentServer();
if (cancelled) return;
const server = player.getCurrentServer();
// Calculate whether hack was successful
const hackChance = calculateHackingChance(server, player);
const rand = Math.random();
const expGainedOnSuccess = calculateHackingExpGain(server, player);
const expGainedOnFailure = expGainedOnSuccess / 4;
if (rand < hackChance) {
// Success!
if (
SpecialServerIps[SpecialServerNames.WorldDaemon] &&
SpecialServerIps[SpecialServerNames.WorldDaemon] == server.ip
) {
if (player.bitNodeN == null) {
player.bitNodeN = 1;
}
hackWorldDaemon(player.bitNodeN);
this.hackFlag = false;
return;
// Calculate whether hack was successful
const hackChance = calculateHackingChance(server, player);
const rand = Math.random();
const expGainedOnSuccess = calculateHackingExpGain(server, player);
const expGainedOnFailure = expGainedOnSuccess / 4;
if (rand < hackChance) {
// Success!
if (
SpecialServerIps[SpecialServerNames.WorldDaemon] &&
SpecialServerIps[SpecialServerNames.WorldDaemon] == server.ip
) {
if (player.bitNodeN == null) {
player.bitNodeN = 1;
}
server.backdoorInstalled = true;
let moneyGained = calculatePercentMoneyHacked(server, player);
moneyGained = Math.floor(server.moneyAvailable * moneyGained);
if (moneyGained <= 0) {
moneyGained = 0;
} // Safety check
server.moneyAvailable -= moneyGained;
player.gainMoney(moneyGained);
player.recordMoneySource(moneyGained, "hacking");
player.gainHackingExp(expGainedOnSuccess);
player.gainIntelligenceExp(expGainedOnSuccess / CONSTANTS.IntelligenceTerminalHackBaseExpGain);
server.fortify(CONSTANTS.ServerFortifyAmount);
this.print(
`Hack successful! Gained ${numeralWrapper.formatMoney(moneyGained)} and ${numeralWrapper.formatExp(
expGainedOnSuccess,
)} hacking exp`,
);
} else {
// Failure
// player only gains 25% exp for failure? TODO Can change this later to balance
player.gainHackingExp(expGainedOnFailure);
this.print(
`Failed to hack ${server.hostname}. Gained ${numeralWrapper.formatExp(expGainedOnFailure)} hacking exp`,
);
hackWorldDaemon(player.bitNodeN);
return;
}
server.backdoorInstalled = true;
let moneyGained = calculatePercentMoneyHacked(server, player);
moneyGained = Math.floor(server.moneyAvailable * moneyGained);
if (moneyGained <= 0) {
moneyGained = 0;
} // Safety check
server.moneyAvailable -= moneyGained;
player.gainMoney(moneyGained);
player.recordMoneySource(moneyGained, "hacking");
player.gainHackingExp(expGainedOnSuccess);
player.gainIntelligenceExp(expGainedOnSuccess / CONSTANTS.IntelligenceTerminalHackBaseExpGain);
server.fortify(CONSTANTS.ServerFortifyAmount);
this.print(
`Hack successful! Gained ${numeralWrapper.formatMoney(moneyGained)} and ${numeralWrapper.formatExp(
expGainedOnSuccess,
)} hacking exp`,
);
} else {
// Failure
// player only gains 25% exp for failure? TODO Can change this later to balance
player.gainHackingExp(expGainedOnFailure);
this.print(
`Failed to hack ${server.hostname}. Gained ${numeralWrapper.formatExp(expGainedOnFailure)} hacking exp`,
);
}
this.hackFlag = false;
}
finishBackdoor(player: IPlayer, cancelled = false): void {
@ -211,13 +194,11 @@ export class Terminal implements ITerminal {
player.bitNodeN = 1;
}
hackWorldDaemon(player.bitNodeN);
this.backdoorFlag = false;
return;
}
server.backdoorInstalled = true;
this.print("Backdoor successful!");
}
this.backdoorFlag = false;
}
finishAnalyze(player: IPlayer, cancelled = false): void {
@ -248,22 +229,25 @@ export class Terminal implements ITerminal {
this.print("HTTP port: " + (currServ.httpPortOpen ? "Open" : "Closed"));
this.print("SQL port: " + (currServ.sqlPortOpen ? "Open" : "Closed"));
}
this.analyzeFlag = false;
}
finishAction(player: IPlayer, cancelled = false): void {
if (this.hackFlag) {
if (this.action === null) {
if (!cancelled) throw new Error("Finish action called when there was no action");
return;
}
this.print(this.getProgressText());
if (this.action.action === "h") {
this.finishHack(player, cancelled);
} else if (this.backdoorFlag) {
} else if (this.action.action === "b") {
this.finishBackdoor(player, cancelled);
} else if (this.analyzeFlag) {
} else if (this.action.action === "a") {
this.finishAnalyze(player, cancelled);
}
// Rename the progress bar so that the next hacks dont trigger it. Re-enable terminal
$("#hack-progress-bar").attr("id", "old-hack-progress-bar");
$("#hack-progress").attr("id", "old-hack-progress");
$("input[class=terminal-input]").prop("disabled", false);
if (cancelled) {
this.print("Cancelled");
}
this.action = null;
}
getFile(player: IPlayer, filename: string): Script | TextFile | string | null {
@ -427,7 +411,7 @@ export class Terminal implements ITerminal {
} // Don't print current server
const titleDashes = Array((d - 1) * 4 + 1).join("-");
if (player.hasProgram(Programs.AutoLink.name)) {
this.print(s.hostname);
this.outputHistory.push(new Link(s.hostname));
} else {
this.print(s.hostname);
}
@ -452,7 +436,7 @@ export class Terminal implements ITerminal {
(() => {
const hostname = links[i].innerHTML.toString();
links[i].addEventListener("onclick", () => {
if (this.analyzeFlag || this.hackFlag || this.backdoorFlag) {
if (this.action !== null) {
return;
}
this.connectToServer(player, hostname);
@ -498,12 +482,17 @@ export class Terminal implements ITerminal {
}
clear(): void {
this.outputHistory = [{ text: `Bitburner v${CONSTANTS.Version}`, color: "primary" }];
this.outputHistory = [new Output(`Bitburner v${CONSTANTS.Version}`, "primary")];
this.hasChanges = true;
}
prestige(): void {
this.action = null;
this.clear();
}
executeCommand(engine: IEngine, player: IPlayer, command: string): void {
if (this.hackFlag || this.backdoorFlag || this.analyzeFlag) {
if (this.action !== null) {
this.error(`Cannot execute command (${command}) while an action is in progress`);
return;
}
@ -718,4 +707,12 @@ export class Terminal implements ITerminal {
f(this, engine, player, s, commandArray.slice(1));
}
getProgressText(): string {
if (this.action === null) throw new Error("trying to get the progress text when there's no action");
return createProgressBarText({
progress: (this.action.time - this.action.timeLeft) / this.action.time,
totalTicks: 50,
});
}
}

View File

@ -2,16 +2,30 @@ import React, { useState, useEffect, useRef } from "react";
import Typography from "@material-ui/core/Typography";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import { Link as MuiLink } from "@material-ui/core";
import { makeStyles, createStyles, Theme } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import Box from "@material-ui/core/Box";
import { KEY } from "../../../utils/helpers/keyCodes";
import { ITerminal } from "../ITerminal";
import { ITerminal, Output, Link, TTimer } from "../ITerminal";
import { IEngine } from "../../IEngine";
import { IPlayer } from "../../PersonObjects/IPlayer";
import { determineAllPossibilitiesForTabCompletion } from "../determineAllPossibilitiesForTabCompletion";
import { tabCompletion } from "../tabCompletion";
import { FconfSettings } from "../../Fconf/FconfSettings";
import { createProgressBarText } from "../../../utils/helpers/createProgressBarText";
interface IActionTimerProps {
terminal: ITerminal;
}
function ActionTimer({ terminal }: IActionTimerProps): React.ReactElement {
return (
<Typography color={"primary"} paragraph={false}>
{terminal.getProgressText()}
</Typography>
);
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@ -150,6 +164,11 @@ export function TerminalRoot({ terminal, engine, player }: IProps): React.ReactE
if (terminal.contractOpen) return;
const ref = terminalInput.current;
if (ref) ref.focus();
// Cancel action
if (event.keyCode === KEY.C && event.ctrlKey) {
terminal.finishAction(player, true);
}
}
document.addEventListener("keydown", keyDown);
return () => document.removeEventListener("keydown", keyDown);
@ -328,15 +347,31 @@ export function TerminalRoot({ terminal, engine, player }: IProps): React.ReactE
}
return (
<Box position="fixed" bottom="0" width="100%">
<Box position="fixed" bottom="0" width="100%" px={1}>
<List classes={{ root: classes.nopadding }}>
{terminal.outputHistory.map((output, i) => (
<ListItem key={i} classes={{ root: classes.nopadding }}>
<Typography classes={{ root: classes.preformatted }} color={output.color} paragraph={false}>
{output.text}
</Typography>
</ListItem>
))}
{terminal.outputHistory.map((item, i) => {
if (item instanceof Output)
return (
<ListItem key={i} classes={{ root: classes.nopadding }}>
<Typography classes={{ root: classes.preformatted }} color={item.color} paragraph={false}>
{item.text}
</Typography>
</ListItem>
);
if (item instanceof Link)
return (
<ListItem key={i} classes={{ root: classes.nopadding }}>
<MuiLink
classes={{ root: classes.preformatted }}
color={"secondary"}
paragraph={false}
onClick={() => terminal.connectToServer(player, item.hostname)}
>
&gt;&nbsp;{item.hostname}
</MuiLink>
</ListItem>
);
})}
</List>
{possibilities.length > 0 && (
<>
@ -348,17 +383,23 @@ export function TerminalRoot({ terminal, engine, player }: IProps): React.ReactE
</Typography>
</>
)}
{terminal.action !== null && <ActionTimer terminal={terminal} />}
<TextField
color={terminal.action === null ? "primary" : "secondary"}
autoFocus
disabled={terminal.action !== null}
autoComplete="off"
classes={{ root: classes.textfield }}
value={value}
onChange={handleValueChange}
inputRef={terminalInput}
InputProps={{
// for players to hook in
id: "terminal-input",
className: classes.input,
startAdornment: (
<>
<Typography color="primary">
<Typography color={terminal.action === null ? "primary" : "secondary"}>
[{player.getCurrentServer().hostname}&nbsp;~{terminal.cwd()}]&gt;&nbsp;
</Typography>
</>

View File

@ -444,16 +444,7 @@ const Engine = {
Player.playtimeSinceLastAug += time;
Player.playtimeSinceLastBitnode += time;
// Start Manual hack
if (Terminal.actionStarted === true) {
Engine._totalActionTime = Terminal.actionTime;
Engine._actionTimeLeft = Terminal.actionTime;
Engine._actionInProgress = true;
Engine._actionProgressBarCount = 1;
Engine._actionProgressStr = "[ ]";
Engine._actionTimeStr = "Time left: ";
Terminal.actionStarted = false;
}
Terminal.process(Player, numCycles);
// Working
if (Player.isWorking) {
@ -519,11 +510,6 @@ const Engine = {
Engine.decrementAllCounters(numCycles);
Engine.checkCounters();
// Manual hacks
if (Engine._actionInProgress == true) {
Engine.updateHackProgress(numCycles);
}
// Update the running time of all active scripts
updateOnlineScriptTimes(numCycles);
@ -623,42 +609,6 @@ const Engine = {
}
},
// Calculates the hack progress for a manual (non-scripted) hack and updates the progress bar/time accordingly
// TODO Refactor this into Terminal module
_totalActionTime: 0,
_actionTimeLeft: 0,
_actionTimeStr: "Time left: ",
_actionProgressStr: "[ ]",
_actionProgressBarCount: 1,
_actionInProgress: false,
updateHackProgress: function (numCycles = 1) {
var timeElapsedMilli = numCycles * Engine._idleSpeed;
Engine._actionTimeLeft -= timeElapsedMilli / 1000; // Substract idle speed (ms)
Engine._actionTimeLeft = Math.max(Engine._actionTimeLeft, 0);
// Calculate percent filled
var percent = Math.round((1 - Engine._actionTimeLeft / Engine._totalActionTime) * 100);
// Update progress bar
while (Engine._actionProgressBarCount * 2 <= percent) {
Engine._actionProgressStr = replaceAt(Engine._actionProgressStr, Engine._actionProgressBarCount, "|");
Engine._actionProgressBarCount += 1;
}
// Update hack time remaining
Engine._actionTimeStr = "Time left: " + Math.max(0, Math.round(Engine._actionTimeLeft)).toString() + "s";
document.getElementById("hack-progress").innerHTML = Engine._actionTimeStr;
// Dynamically update progress bar
document.getElementById("hack-progress-bar").innerHTML = Engine._actionProgressStr.replace(/ /g, "&nbsp;");
// Once percent is 100, the hack is completed
if (percent >= 100) {
Engine._actionInProgress = false;
Terminal.finishAction();
}
},
/**
* Used in game when clicking on a main menu header (NOT used for initialization)
* @param open {boolean} Whether header is being opened or closed