From d5c9306395f4c9a9d528389a24f27a01cd21768d Mon Sep 17 00:00:00 2001 From: Olivier Gagnon Date: Thu, 9 Sep 2021 21:38:05 -0400 Subject: [PATCH] Convert sleeves to react, fix shock recovery bug --- css/sleeves.scss | 13 +- src/PersonObjects/Sleeve/Sleeve.ts | 3 +- .../Sleeve/SleeveAugmentationsUI.tsx | 129 --- src/PersonObjects/Sleeve/SleeveUI.tsx | 733 ------------------ .../Sleeve/ui/EarningsTableElement.tsx | 11 +- src/PersonObjects/Sleeve/ui/SleeveElem.tsx | 240 ++++++ src/PersonObjects/Sleeve/ui/SleeveRoot.tsx | 93 +++ src/PersonObjects/Sleeve/ui/StatsElement.tsx | 28 +- src/PersonObjects/Sleeve/ui/TaskSelector.tsx | 290 +++++++ src/PersonObjects/Sleeve/ui/TravelPopup.tsx | 4 +- src/engine.jsx | 28 +- src/index.html | 1 + 12 files changed, 672 insertions(+), 901 deletions(-) delete mode 100644 src/PersonObjects/Sleeve/SleeveAugmentationsUI.tsx delete mode 100644 src/PersonObjects/Sleeve/SleeveUI.tsx create mode 100644 src/PersonObjects/Sleeve/ui/SleeveElem.tsx create mode 100644 src/PersonObjects/Sleeve/ui/SleeveRoot.tsx create mode 100644 src/PersonObjects/Sleeve/ui/TaskSelector.tsx diff --git a/css/sleeves.scss b/css/sleeves.scss index 4f85bbfc2..30cf2472d 100644 --- a/css/sleeves.scss +++ b/css/sleeves.scss @@ -3,14 +3,15 @@ */ @import "theme"; -.sleeve-container { +#sleeves-container { + position: fixed; + padding: 6px; +} + +.sleeve-elem { border: 1px solid white; margin: 4px; - width: 75%; - - p { - font-size: $defaultFontSize * 0.875; - } + display: block; } .sleeves-page-info { diff --git a/src/PersonObjects/Sleeve/Sleeve.ts b/src/PersonObjects/Sleeve/Sleeve.ts index 2424002d8..583e98029 100644 --- a/src/PersonObjects/Sleeve/Sleeve.ts +++ b/src/PersonObjects/Sleeve/Sleeve.ts @@ -252,6 +252,7 @@ export class Sleeve extends Person { // Experience is first multiplied by shock. Then 'synchronization' // is accounted for + const multFac = (this.shock / 100) * (this.sync / 100) * numCycles; const pHackExp = exp.hack * multFac; const pStrExp = exp.str * multFac; @@ -491,7 +492,7 @@ export class Sleeve extends Person { this.currentTaskTime += time; // Shock gradually goes towards 100 - this.shock = Math.min(100, this.shock + 0.0001 * this.storedCycles); + this.shock = Math.min(100, this.shock + 0.0001 * cyclesUsed); let retValue: ITaskTracker = createTaskTracker(); switch (this.currentTask) { diff --git a/src/PersonObjects/Sleeve/SleeveAugmentationsUI.tsx b/src/PersonObjects/Sleeve/SleeveAugmentationsUI.tsx deleted file mode 100644 index 4e73fa660..000000000 --- a/src/PersonObjects/Sleeve/SleeveAugmentationsUI.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Module for handling the UI for purchasing Sleeve Augmentations - * This UI is a popup, not a full page - */ -import React from "react"; -import { Sleeve } from "./Sleeve"; -import { findSleevePurchasableAugs } from "./SleeveHelpers"; - -import { IPlayer } from "../IPlayer"; - -import { Augmentation } from "../../Augmentation/Augmentation"; -import { Augmentations } from "../../Augmentation/Augmentations"; - -import { Money } from "../../ui/React/Money"; - -import { dialogBoxCreate } from "../../../utils/DialogBox"; - -import { createElement } from "../../../utils/uiHelpers/createElement"; -import { createPopup } from "../../../utils/uiHelpers/createPopup"; -import { createPopupCloseButton } from "../../../utils/uiHelpers/createPopupCloseButton"; -import { removeElementById } from "../../../utils/uiHelpers/removeElementById"; - -import { renderToStaticMarkup } from "react-dom/server"; - -export function createSleevePurchaseAugsPopup(sleeve: Sleeve, p: IPlayer): void { - // Array of all owned Augmentations. Names only - const ownedAugNames: string[] = sleeve.augmentations.map((e) => { - return e.name; - }); - - // You can only purchase Augmentations that are actually available from - // your factions. I.e. you must be in a faction that has the Augmentation - // and you must also have enough rep in that faction in order to purchase it. - const availableAugs = findSleevePurchasableAugs(sleeve, p); - - // Create popup - const popupId = "purchase-sleeve-augs-popup"; - - // Close popup button - const closeBtn = createPopupCloseButton(popupId, { innerText: "Cancel" }); - - // General info about owned Augmentations - const ownedAugsInfo = createElement("p", { - display: "block", - innerHTML: "Owned Augmentations:", - }); - - const popupElems: HTMLElement[] = [closeBtn, ownedAugsInfo]; - - // Show owned augmentations - // First we'll make a div with a reduced width, so the tooltips don't go off - // the edge of the popup - const ownedAugsDiv = createElement("div", { width: "70%" }); - for (const ownedAug of ownedAugNames) { - const aug: Augmentation | null = Augmentations[ownedAug]; - if (aug == null) { - console.warn(`Invalid Augmentation: ${ownedAug}`); - continue; - } - - let tooltip = aug.info; - if (typeof tooltip !== "string") { - tooltip = renderToStaticMarkup(tooltip); - } - tooltip += "

"; - tooltip += renderToStaticMarkup(aug.stats); - - ownedAugsDiv.appendChild( - createElement("div", { - class: "gang-owned-upgrade", // Reusing a class from the Gang UI - innerText: ownedAug, - tooltip: tooltip, - }), - ); - } - popupElems.push(ownedAugsDiv); - - // General info about buying Augmentations - const info = createElement("p", { - innerHTML: [ - `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.

`, - `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.`, - ].join(" "), - }); - - popupElems.push(info); - - for (const aug of availableAugs) { - const div = createElement("div", { - class: "cmpy-mgmt-upgrade-div", // We'll reuse this CSS class - }); - - let info = aug.info; - if (typeof info !== "string") { - info = renderToStaticMarkup(info); - } - info += "

"; - info += renderToStaticMarkup(aug.stats); - - div.appendChild( - createElement("p", { - fontSize: "12px", - innerHTML: [ - `

${aug.name}


`, - `Cost: ${renderToStaticMarkup()}

`, - `${info}`, - ].join(" "), - padding: "2px", - clickListener: () => { - if (sleeve.tryBuyAugmentation(p, aug)) { - dialogBoxCreate(`Installed ${aug.name} on Duplicate Sleeve!`, false); - removeElementById(popupId); - createSleevePurchaseAugsPopup(sleeve, p); - } else { - dialogBoxCreate(`You cannot afford ${aug.name}`, false); - } - }, - }), - ); - - popupElems.push(div); - } - - createPopup(popupId, popupElems); -} diff --git a/src/PersonObjects/Sleeve/SleeveUI.tsx b/src/PersonObjects/Sleeve/SleeveUI.tsx deleted file mode 100644 index 70ed0b2f5..000000000 --- a/src/PersonObjects/Sleeve/SleeveUI.tsx +++ /dev/null @@ -1,733 +0,0 @@ -/** - * Module for handling the Sleeve UI - */ -import React from "react"; -import { Sleeve } from "./Sleeve"; -import { SleeveTaskType } from "./SleeveTaskTypesEnum"; -import { SleeveFaq } from "./data/SleeveFaq"; - -import { IPlayer } from "../IPlayer"; - -import { Faction } from "../../Faction/Faction"; -import { Factions } from "../../Faction/Factions"; -import { FactionWorkType } from "../../Faction/FactionWorkTypeEnum"; - -import { Crime } from "../../Crime/Crime"; -import { Crimes } from "../../Crime/Crimes"; -import { CityName } from "../../Locations/data/CityNames"; -import { LocationName } from "../../Locations/data/LocationNames"; - -import { numeralWrapper } from "../../ui/numeralFormat"; -import { Page, routing } from "../../ui/navigationTracking"; - -import { dialogBoxCreate } from "../../../utils/DialogBox"; - -import { createProgressBarText } from "../../../utils/helpers/createProgressBarText"; -import { exceptionAlert } from "../../../utils/helpers/exceptionAlert"; - -import { clearEventListeners } from "../../../utils/uiHelpers/clearEventListeners"; -import { createElement } from "../../../utils/uiHelpers/createElement"; -import { createOptionElement } from "../../../utils/uiHelpers/createOptionElement"; -import { createPopup } from "../../ui/React/createPopup"; -import { getSelectValue } from "../../../utils/uiHelpers/getSelectData"; -import { removeChildrenFromElement } from "../../../utils/uiHelpers/removeChildrenFromElement"; -import { removeElement } from "../../../utils/uiHelpers/removeElement"; - -import { SleeveAugmentationsPopup } from "./ui/SleeveAugmentationsPopup"; -import { TravelPopup } from "./ui/TravelPopup"; -import { EarningsTableElement } from "./ui/EarningsTableElement"; -import { Money } from "../../ui/React/Money"; -import { MoneyRate } from "../../ui/React/MoneyRate"; -import { ReputationRate } from "../../ui/React/ReputationRate"; -import { StatsElement } from "./ui/StatsElement"; -import { MoreStatsContent } from "./ui/MoreStatsContent"; -import { MoreEarningsContent } from "./ui/MoreEarningsContent"; -import * as ReactDOM from "react-dom"; - -// Object that keeps track of all DOM elements for the UI for a single Sleeve -interface ISleeveUIElems { - container: HTMLElement | null; - statsPanel: HTMLElement | null; - stats: HTMLElement | null; - moreStatsButton: HTMLElement | null; - travelButton: HTMLElement | null; - purchaseAugsButton: HTMLElement | null; - taskPanel: HTMLElement | null; - taskSelector: HTMLSelectElement | null; - taskDetailsSelector: HTMLSelectElement | null; - taskDetailsSelector2: HTMLSelectElement | null; - taskDescription: HTMLElement | null; - taskSetButton: HTMLElement | null; - taskProgressBar: HTMLElement | null; - earningsPanel: HTMLElement | null; - currentEarningsInfo: HTMLElement | null; - totalEarningsButton: HTMLElement | null; -} - -// Object that keeps track of all DOM elements for the entire Sleeve UI -interface IPageUIElems { - container: HTMLElement | null; - docButton: HTMLElement | null; - faqButton: HTMLElement | null; - info: HTMLElement | null; - sleeveList: HTMLElement | null; - sleeves: ISleeveUIElems[] | null; -} - -const UIElems: IPageUIElems = { - container: null, - docButton: null, - faqButton: null, - info: null, - sleeveList: null, - sleeves: null, -}; - -// Creates the UI for the entire Sleeves page -let playerRef: IPlayer | null; -export function createSleevesPage(p: IPlayer): void { - if (!routing.isOn(Page.Sleeves)) { - return; - } - - try { - playerRef = p; - - UIElems.container = createElement("div", { - class: "generic-menupage-container", - id: "sleeves-container", - position: "fixed", - }); - - UIElems.info = createElement("p", { - class: "sleeves-page-info", - innerHTML: - "

Sleeves

Duplicate Sleeves are MK-V Synthoids (synthetic androids) into which your " + - "consciousness has been copied. In other words, these Synthoids contain " + - "a perfect duplicate of your mind.

" + - "Sleeves can be used to perform different tasks synchronously.

", - }); - - UIElems.faqButton = createElement("button", { - class: "std-button", - display: "inline-block", - innerText: "FAQ", - clickListener: () => { - dialogBoxCreate(SleeveFaq, false); - }, - }); - - UIElems.docButton = createElement("a", { - class: "std-button", - display: "inline-block", - href: "https://bitburner.readthedocs.io/en/latest/advancedgameplay/sleeves.html#duplicate-sleeves", - innerText: "Documentation", - target: "_blank", - }); - - UIElems.sleeveList = createElement("ul"); - UIElems.sleeves = []; - - // Create UI modules for all Sleeve - for (const sleeve of p.sleeves) { - const sleeveUi = createSleeveUi(sleeve, p.sleeves); - if (sleeveUi.container == null) throw new Error("sleeveUi.container is null in createSleevesPage()"); - UIElems.sleeveList.appendChild(sleeveUi.container); - UIElems.sleeves.push(sleeveUi); - } - - UIElems.container.appendChild(UIElems.info); - UIElems.container.appendChild(UIElems.faqButton); - UIElems.container.appendChild(UIElems.docButton); - UIElems.container.appendChild(UIElems.sleeveList); - - const container = document.getElementById("entire-game-container"); - if (container === null) throw new Error("entire-game-container not found in createSleevesPage()"); - container.appendChild(UIElems.container); - } catch (e) { - exceptionAlert(e); - } -} - -// Updates the UI for the entire Sleeves page -export function updateSleevesPage(): void { - if (!routing.isOn(Page.Sleeves)) { - return; - } - if (playerRef === null) throw new Error("playerRef is null in updateSleevesPage()"); - if (UIElems.sleeves === null) throw new Error("UIElems.sleeves is null in updateSleevesPage()"); - - try { - for (let i = 0; i < playerRef.sleeves.length; ++i) { - const sleeve: Sleeve = playerRef.sleeves[i]; - const elems: ISleeveUIElems = UIElems.sleeves[i]; - updateSleeveUi(sleeve, elems); - } - } catch (e) { - exceptionAlert(e); - } -} - -export function clearSleevesPage(): void { - if (UIElems.container instanceof HTMLElement) { - removeElement(UIElems.container); - } - - for (const prop in UIElems) { - (UIElems as any)[prop] = null; - } - - playerRef = null; -} - -// Creates the UI for a single Sleeve -// Returns an object containing the DOM elements in the UI (ISleeveUIElems) -function createSleeveUi(sleeve: Sleeve, allSleeves: Sleeve[]): ISleeveUIElems { - const elems: ISleeveUIElems = { - container: null, - statsPanel: null, - stats: null, - moreStatsButton: null, - travelButton: null, - purchaseAugsButton: null, - taskPanel: null, - taskSelector: null, - taskDetailsSelector: null, - taskDetailsSelector2: null, - taskDescription: null, - taskSetButton: null, - taskProgressBar: null, - earningsPanel: null, - currentEarningsInfo: null, - totalEarningsButton: null, - }; - if (playerRef === null) return elems; - - if (!routing.isOn(Page.Sleeves)) { - return elems; - } - - elems.container = createElement("div", { - class: "sleeve-container", - display: "block", - }); - - elems.statsPanel = createElement("div", { - class: "sleeve-panel", - width: "25%", - }); - elems.stats = createElement("div", { class: "sleeve-stats-text" }); - elems.moreStatsButton = createElement("button", { - class: "std-button", - innerText: "More Stats", - clickListener: () => { - dialogBoxCreate(); - }, - }); - elems.travelButton = createElement("button", { - class: "std-button", - innerText: "Travel", - clickListener: () => { - if (playerRef == null) throw new Error("playerRef is null in purchaseAugsButton.click()"); - const popupId = "sleeve-travel-popup"; - createPopup(popupId, TravelPopup, { - popupId: popupId, - sleeve: sleeve, - player: playerRef, - }); - }, - }); - elems.purchaseAugsButton = createElement("button", { - class: "std-button", - display: "block", - innerText: "Manage Augmentations", - clickListener: () => { - if (playerRef == null) throw new Error("playerRef is null in purchaseAugsButton.click()"); - const popupId = "sleeve-augmentation-popup"; - createPopup(popupId, SleeveAugmentationsPopup, { - sleeve: sleeve, - player: playerRef, - }); - }, - }); - elems.statsPanel.appendChild(elems.stats); - elems.statsPanel.appendChild(elems.moreStatsButton); - elems.statsPanel.appendChild(elems.travelButton); - if (sleeve.shock >= 100) { - // You can only buy augs when shock recovery is 0 - elems.statsPanel.appendChild(elems.purchaseAugsButton); - } - - elems.taskPanel = createElement("div", { - class: "sleeve-panel", - width: "40%", - }); - elems.taskSelector = createElement("select", { - class: "dropdown", - }) as HTMLSelectElement; - elems.taskSelector.add(createOptionElement("------")); - elems.taskSelector.add(createOptionElement("Work for Company")); - elems.taskSelector.add(createOptionElement("Work for Faction")); - elems.taskSelector.add(createOptionElement("Commit Crime")); - elems.taskSelector.add(createOptionElement("Take University Course")); - elems.taskSelector.add(createOptionElement("Workout at Gym")); - elems.taskSelector.add(createOptionElement("Shock Recovery")); - elems.taskSelector.add(createOptionElement("Synchronize")); - elems.taskDetailsSelector = createElement("select", { - class: "dropdown", - }) as HTMLSelectElement; - elems.taskDetailsSelector2 = createElement("select", { - class: "dropdown", - }) as HTMLSelectElement; - elems.taskDescription = createElement("p"); - elems.taskProgressBar = createElement("p"); - elems.taskSelector.addEventListener("change", () => { - updateSleeveTaskSelector(sleeve, elems, allSleeves); - }); - elems.taskSelector.selectedIndex = sleeve.currentTask; // Set initial value for Task Selector - elems.taskSelector.dispatchEvent(new Event("change")); - updateSleeveTaskDescription(sleeve, elems); - elems.taskSetButton = createElement("button", { - class: "std-button", - innerText: "Set Task", - clickListener: () => { - setSleeveTask(sleeve, elems); - }, - }); - elems.taskPanel.appendChild(elems.taskSelector); - elems.taskPanel.appendChild(elems.taskDetailsSelector); - elems.taskPanel.appendChild(elems.taskDetailsSelector2); - elems.taskPanel.appendChild(elems.taskSetButton); - elems.taskPanel.appendChild(elems.taskDescription); - elems.taskPanel.appendChild(elems.taskProgressBar); - - elems.earningsPanel = createElement("div", { - class: "sleeve-panel", - width: "35%", - }); - elems.currentEarningsInfo = createElement("div"); - elems.totalEarningsButton = createElement("button", { - class: "std-button", - innerText: "More Earnings Info", - clickListener: () => { - dialogBoxCreate(); - }, - }); - - elems.earningsPanel.appendChild(elems.currentEarningsInfo); - elems.earningsPanel.appendChild(elems.totalEarningsButton); - - updateSleeveUi(sleeve, elems); - - elems.container.appendChild(elems.statsPanel); - elems.container.appendChild(elems.taskPanel); - elems.container.appendChild(elems.earningsPanel); - - return elems; -} - -// Updates the UI for a single Sleeve -function updateSleeveUi(sleeve: Sleeve, elems: ISleeveUIElems): void { - if (!routing.isOn(Page.Sleeves)) { - return; - } - if (playerRef == null) throw new Error("playerRef is null in updateSleeveUi()"); - if (elems.taskProgressBar == null) throw new Error("elems.taskProgressBar is null"); - if (elems.stats == null) throw new Error("elems.stats is null"); - if (elems.currentEarningsInfo == null) throw new Error("elems.currentEarningsInfo is null"); - - ReactDOM.render(StatsElement(sleeve), elems.stats); - - if (sleeve.currentTask === SleeveTaskType.Crime) { - const data = [ - [`Money`, , `(on success)`], - [`Hacking Exp`, numeralWrapper.formatExp(sleeve.gainRatesForTask.hack), `(2x on success)`], - [`Strength Exp`, numeralWrapper.formatExp(sleeve.gainRatesForTask.str), `(2x on success)`], - [`Defense Exp`, numeralWrapper.formatExp(sleeve.gainRatesForTask.def), `(2x on success)`], - [`Dexterity Exp`, numeralWrapper.formatExp(sleeve.gainRatesForTask.dex), `(2x on success)`], - [`Agility Exp`, numeralWrapper.formatExp(sleeve.gainRatesForTask.agi), `(2x on success)`], - [`Charisma Exp`, numeralWrapper.formatExp(sleeve.gainRatesForTask.cha), `(2x on success)`], - ]; - ReactDOM.render(EarningsTableElement("Earnings (Pre-Synchronization)", data), elems.currentEarningsInfo); - - elems.taskProgressBar.innerText = createProgressBarText({ - progress: sleeve.currentTaskTime / sleeve.currentTaskMaxTime, - totalTicks: 25, - }); - } else { - const data = [ - [`Money:`, MoneyRate(5 * sleeve.gainRatesForTask.money)], - [`Hacking Exp:`, `${numeralWrapper.formatExp(5 * sleeve.gainRatesForTask.hack)} / s`], - [`Strength Exp:`, `${numeralWrapper.formatExp(5 * sleeve.gainRatesForTask.str)} / s`], - [`Defense Exp:`, `${numeralWrapper.formatExp(5 * sleeve.gainRatesForTask.def)} / s`], - [`Dexterity Exp:`, `${numeralWrapper.formatExp(5 * sleeve.gainRatesForTask.dex)} / s`], - [`Agility Exp:`, `${numeralWrapper.formatExp(5 * sleeve.gainRatesForTask.agi)} / s`], - [`Charisma Exp:`, `${numeralWrapper.formatExp(5 * sleeve.gainRatesForTask.cha)} / s`], - ]; - if (sleeve.currentTask === SleeveTaskType.Company || sleeve.currentTask === SleeveTaskType.Faction) { - const repGain: number = sleeve.getRepGain(playerRef); - data.push([`Reputation:`, ReputationRate(5 * repGain)]); - } - ReactDOM.render(EarningsTableElement("Earnings (Pre-Synchronization)", data), elems.currentEarningsInfo); - - elems.taskProgressBar.innerText = ""; - } -} - -const universitySelectorOptions: string[] = [ - "Study Computer Science", - "Data Structures", - "Networks", - "Algorithms", - "Management", - "Leadership", -]; - -const gymSelectorOptions: string[] = ["Train Strength", "Train Defense", "Train Dexterity", "Train Agility"]; - -// Whenever a new task is selected, the "details" selector must update accordingly -function updateSleeveTaskSelector(sleeve: Sleeve, elems: ISleeveUIElems, allSleeves: Sleeve[]): void { - if (playerRef == null) { - throw new Error(`playerRef is null in updateSleeveTaskSelector()`); - } - - // Array of all companies that other sleeves are working at - const forbiddenCompanies: string[] = []; - for (const otherSleeve of allSleeves) { - if (sleeve === otherSleeve) { - continue; - } - if (otherSleeve.currentTask === SleeveTaskType.Company) { - forbiddenCompanies.push(otherSleeve.currentTaskLocation); - } - } - - // Array of all factions that other sleeves are working for - const forbiddenFactions: string[] = []; - for (const otherSleeve of allSleeves) { - if (sleeve === otherSleeve) { - continue; - } - if (otherSleeve.currentTask === SleeveTaskType.Faction) { - forbiddenFactions.push(otherSleeve.currentTaskLocation); - } - } - - if (elems.taskDetailsSelector === null) { - throw new Error("elems.taskDetailsSelector is null"); - } - if (elems.taskDetailsSelector2 === null) { - throw new Error("elems.taskDetailsSelector is null"); - } - - // Reset Selectors - removeChildrenFromElement(elems.taskDetailsSelector); - removeChildrenFromElement(elems.taskDetailsSelector2); - elems.taskDetailsSelector2 = clearEventListeners(elems.taskDetailsSelector2) as HTMLSelectElement; - - const value: string = getSelectValue(elems.taskSelector); - switch (value) { - case "Work for Company": { - let companyCount = 0; - const allJobs: string[] = Object.keys(playerRef.jobs); - for (let i = 0; i < allJobs.length; ++i) { - if (!forbiddenCompanies.includes(allJobs[i])) { - elems.taskDetailsSelector.add(createOptionElement(allJobs[i])); - - // Set initial value of the 'Details' selector - if (sleeve.currentTaskLocation === allJobs[i]) { - elems.taskDetailsSelector.selectedIndex = companyCount; - } - - ++companyCount; - } - - elems.taskDetailsSelector2.add(createOptionElement("------")); - } - break; - } - case "Work for Faction": { - let factionCount = 0; - for (const fac of playerRef.factions) { - if (!forbiddenFactions.includes(fac)) { - elems.taskDetailsSelector.add(createOptionElement(fac)); - - // Set initial value of the 'Details' Selector - if (sleeve.currentTaskLocation === fac) { - elems.taskDetailsSelector.selectedIndex = factionCount; - } - - ++factionCount; - } - } - - // The available faction work types depends on the faction - elems.taskDetailsSelector.addEventListener("change", () => { - if (elems.taskDetailsSelector2 === null) throw new Error("elems.taskDetailsSelector2 is null"); - const facName = getSelectValue(elems.taskDetailsSelector); - const faction: Faction | null = Factions[facName]; - if (faction == null) { - console.warn(`Invalid faction name when trying to update Sleeve Task Selector: ${facName}`); - return; - } - const facInfo = faction.getInfo(); - removeChildrenFromElement(elems.taskDetailsSelector2); - let numOptionsAdded = 0; - if (facInfo.offerHackingWork) { - elems.taskDetailsSelector2.add(createOptionElement("Hacking Contracts")); - if (sleeve.factionWorkType === FactionWorkType.Hacking) { - elems.taskDetailsSelector2.selectedIndex = numOptionsAdded; - } - ++numOptionsAdded; - } - if (facInfo.offerFieldWork) { - elems.taskDetailsSelector2.add(createOptionElement("Field Work")); - if (sleeve.factionWorkType === FactionWorkType.Field) { - elems.taskDetailsSelector2.selectedIndex = numOptionsAdded; - } - ++numOptionsAdded; - } - if (facInfo.offerSecurityWork) { - elems.taskDetailsSelector2.add(createOptionElement("Security Work")); - if (sleeve.factionWorkType === FactionWorkType.Security) { - elems.taskDetailsSelector2.selectedIndex = numOptionsAdded; - } - ++numOptionsAdded; - } - }); - elems.taskDetailsSelector.dispatchEvent(new Event("change")); - break; - } - case "Commit Crime": { - let i = 0; - for (const crimeLabel in Crimes) { - const name: string = Crimes[crimeLabel].name; - elems.taskDetailsSelector.add(createOptionElement(name, crimeLabel)); - - // Set initial value for crime type - if (sleeve.crimeType === "") { - continue; - } - const crime: Crime | null = Crimes[sleeve.crimeType]; - if (crime === null) { - continue; - } - if (name === crime.name) { - elems.taskDetailsSelector.selectedIndex = i; - } - - ++i; - } - - elems.taskDetailsSelector2.add(createOptionElement("------")); - break; - } - case "Take University Course": - // First selector has class type - for (let i = 0; i < universitySelectorOptions.length; ++i) { - elems.taskDetailsSelector.add(createOptionElement(universitySelectorOptions[i])); - - // Set initial value - if (sleeve.className === universitySelectorOptions[i]) { - elems.taskDetailsSelector.selectedIndex = i; - } - } - - // Second selector has which university - switch (sleeve.city) { - case CityName.Aevum: - elems.taskDetailsSelector2.add(createOptionElement(LocationName.AevumSummitUniversity)); - break; - case CityName.Sector12: - elems.taskDetailsSelector2.add(createOptionElement(LocationName.Sector12RothmanUniversity)); - break; - case CityName.Volhaven: - elems.taskDetailsSelector2.add(createOptionElement(LocationName.VolhavenZBInstituteOfTechnology)); - break; - default: - elems.taskDetailsSelector2.add(createOptionElement("No university available in city!")); - break; - } - break; - case "Workout at Gym": - // First selector has what stat is being trained - for (let i = 0; i < gymSelectorOptions.length; ++i) { - elems.taskDetailsSelector.add(createOptionElement(gymSelectorOptions[i])); - - // Set initial value - if (sleeve.gymStatType === gymSelectorOptions[i].substring(6, 9).toLowerCase()) { - elems.taskDetailsSelector.selectedIndex = i; - } - } - - // Second selector has gym - // In this switch statement we also set the initial value of the second selector - switch (sleeve.city) { - case CityName.Aevum: - elems.taskDetailsSelector2.add(createOptionElement(LocationName.AevumCrushFitnessGym)); - elems.taskDetailsSelector2.add(createOptionElement(LocationName.AevumSnapFitnessGym)); - - // Set initial value - if (sleeve.currentTaskLocation === LocationName.AevumCrushFitnessGym) { - elems.taskDetailsSelector2.selectedIndex = 0; - } else if (sleeve.currentTaskLocation === LocationName.AevumSnapFitnessGym) { - elems.taskDetailsSelector2.selectedIndex = 1; - } - break; - case CityName.Sector12: - elems.taskDetailsSelector2.add(createOptionElement(LocationName.Sector12IronGym)); - elems.taskDetailsSelector2.add(createOptionElement(LocationName.Sector12PowerhouseGym)); - - // Set initial value - if (sleeve.currentTaskLocation === LocationName.Sector12IronGym) { - elems.taskDetailsSelector2.selectedIndex = 0; - } else if (sleeve.currentTaskLocation === LocationName.Sector12PowerhouseGym) { - elems.taskDetailsSelector2.selectedIndex = 1; - } - break; - case CityName.Volhaven: - elems.taskDetailsSelector2.add(createOptionElement(LocationName.VolhavenMilleniumFitnessGym)); - break; - default: - elems.taskDetailsSelector2.add(createOptionElement("No gym available in city!")); - break; - } - - break; - case "Shock Recovery": - case "Synchronize": - case "------": - // No options in "Details" selector - elems.taskDetailsSelector.add(createOptionElement("------")); - elems.taskDetailsSelector2.add(createOptionElement("------")); - return; - default: - break; - } -} - -function setSleeveTask(sleeve: Sleeve, elems: ISleeveUIElems): boolean { - try { - if (playerRef == null) { - throw new Error("playerRef is null in Sleeve UI's setSleeveTask()"); - } - if (elems.taskDescription == null) throw new Error("elems.taskDescription is null"); - - const taskValue: string = getSelectValue(elems.taskSelector); - const detailValue: string = getSelectValue(elems.taskDetailsSelector); - const detailValue2: string = getSelectValue(elems.taskDetailsSelector2); - - let res = false; - switch (taskValue) { - case "------": - elems.taskDescription.innerText = "This sleeve is currently idle"; - break; - case "Work for Company": - res = sleeve.workForCompany(playerRef, detailValue); - break; - case "Work for Faction": - res = sleeve.workForFaction(playerRef, detailValue, detailValue2); - break; - case "Commit Crime": - res = sleeve.commitCrime(playerRef, detailValue); - break; - case "Take University Course": - res = sleeve.takeUniversityCourse(playerRef, detailValue2, detailValue); - break; - case "Workout at Gym": - res = sleeve.workoutAtGym(playerRef, detailValue2, detailValue); - break; - case "Shock Recovery": - sleeve.currentTask = SleeveTaskType.Recovery; - res = sleeve.shockRecovery(playerRef); - break; - case "Synchronize": - res = sleeve.synchronize(playerRef); - break; - default: - console.error(`Invalid/Unrecognized taskValue in setSleeveTask(): ${taskValue}`); - } - - if (res) { - updateSleeveTaskDescription(sleeve, elems); - } else { - switch (taskValue) { - case "Work for Faction": - elems.taskDescription.innerText = - "Failed to assign sleeve to task. This is most likely because the selected faction does not offer the selected work type."; - break; - default: - elems.taskDescription.innerText = "Failed to assign sleeve to task. Invalid choice(s)."; - break; - } - } - - if (routing.isOn(Page.Sleeves)) { - updateSleevesPage(); - - // Update the task selector for all sleeves by triggering a change event - if (UIElems.sleeves == null) throw new Error("UIElems.sleeves is null"); - for (const e of UIElems.sleeves) { - if (e.taskSelector == null) throw new Error("e.taskSelector is null"); - e.taskSelector.dispatchEvent(new Event("change")); - } - } - - return res; - } catch (e) { - console.error(`Exception caught in setSleeveTask(): ${e}`); - exceptionAlert(e); - return false; - } -} - -function updateSleeveTaskDescription(sleeve: Sleeve, elems: ISleeveUIElems): void { - try { - if (playerRef == null) { - throw new Error("playerRef is null in Sleeve UI's setSleeveTask()"); - } - - const taskValue: string = getSelectValue(elems.taskSelector); - const detailValue: string = getSelectValue(elems.taskDetailsSelector); - const detailValue2: string = getSelectValue(elems.taskDetailsSelector2); - if (elems.taskDescription == null) throw new Error("elems.taskDescription should not be null"); - - switch (taskValue) { - case "------": - elems.taskDescription.innerText = "This sleeve is currently idle"; - break; - case "Work for Company": - elems.taskDescription.innerText = `This sleeve is currently working your job at ${sleeve.currentTaskLocation}.`; - break; - case "Work for Faction": - elems.taskDescription.innerText = `This sleeve is currently doing ${detailValue2} for ${sleeve.currentTaskLocation}.`; - break; - case "Commit Crime": - elems.taskDescription.innerText = `This sleeve is currently attempting to ${ - Crimes[detailValue].type - } (Success Rate: ${numeralWrapper.formatPercentage(Crimes[detailValue].successRate(sleeve))}).`; - break; - case "Take University Course": - elems.taskDescription.innerText = `This sleeve is currently studying/taking a course at ${sleeve.currentTaskLocation}.`; - break; - case "Workout at Gym": - elems.taskDescription.innerText = `This sleeve is currently working out at ${sleeve.currentTaskLocation}.`; - break; - case "Shock Recovery": - elems.taskDescription.innerText = - "This sleeve is currently set to focus on shock recovery. This causes " + - "the Sleeve's shock to decrease at a faster rate."; - break; - case "Synchronize": - elems.taskDescription.innerText = - "This sleeve is currently set to synchronize with the original consciousness. " + - "This causes the Sleeve's synchronization to increase."; - break; - default: - console.error(`Invalid/Unrecognized taskValue in updateSleeveTaskDescription(): ${taskValue}`); - } - } catch (e) { - console.error(`Exception caught in updateSleeveTaskDescription(): ${e}`); - exceptionAlert(e); - } -} diff --git a/src/PersonObjects/Sleeve/ui/EarningsTableElement.tsx b/src/PersonObjects/Sleeve/ui/EarningsTableElement.tsx index f8435cb8f..9ef33f824 100644 --- a/src/PersonObjects/Sleeve/ui/EarningsTableElement.tsx +++ b/src/PersonObjects/Sleeve/ui/EarningsTableElement.tsx @@ -1,12 +1,17 @@ import * as React from "react"; -export function EarningsTableElement(title: string, stats: any[][]): React.ReactElement { +interface IProps { + title: string; + stats: any[][]; +} + +export function EarningsTableElement(props: IProps): React.ReactElement { return ( <> -
{title}
+
{props.title}
- {stats.map((stat: any[], i: number) => ( + {props.stats.map((stat: any[], i: number) => ( {stat.map((s: any, i: number) => { let style = {}; diff --git a/src/PersonObjects/Sleeve/ui/SleeveElem.tsx b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx new file mode 100644 index 000000000..e825139eb --- /dev/null +++ b/src/PersonObjects/Sleeve/ui/SleeveElem.tsx @@ -0,0 +1,240 @@ +import React, { useState, useEffect } from "react"; + +import { Sleeve } from "../Sleeve"; +import { SleeveTaskType } from "../SleeveTaskTypesEnum"; +import { SleeveFaq } from "../data/SleeveFaq"; + +import { IPlayer } from "../../IPlayer"; + +import { Faction } from "../../../Faction/Faction"; +import { Factions } from "../../../Faction/Factions"; +import { FactionWorkType } from "../../../Faction/FactionWorkTypeEnum"; + +import { Crime } from "../../../Crime/Crime"; +import { Crimes } from "../../../Crime/Crimes"; +import { CityName } from "../../../Locations/data/CityNames"; +import { LocationName } from "../../../Locations/data/LocationNames"; + +import { numeralWrapper } from "../../../ui/numeralFormat"; +import { Page, routing } from "../../../ui/navigationTracking"; + +import { dialogBoxCreate } from "../../../../utils/DialogBox"; + +import { createProgressBarText } from "../../../../utils/helpers/createProgressBarText"; +import { exceptionAlert } from "../../../../utils/helpers/exceptionAlert"; + +import { clearEventListeners } from "../../../../utils/uiHelpers/clearEventListeners"; +import { createElement } from "../../../../utils/uiHelpers/createElement"; +import { createOptionElement } from "../../../../utils/uiHelpers/createOptionElement"; +import { createPopup } from "../../../ui/React/createPopup"; +import { getSelectValue } from "../../../../utils/uiHelpers/getSelectData"; +import { removeChildrenFromElement } from "../../../../utils/uiHelpers/removeChildrenFromElement"; +import { removeElement } from "../../../../utils/uiHelpers/removeElement"; + +import { SleeveAugmentationsPopup } from "../ui/SleeveAugmentationsPopup"; +import { TravelPopup } from "../ui/TravelPopup"; +import { EarningsTableElement } from "../ui/EarningsTableElement"; +import { Money } from "../../../ui/React/Money"; +import { MoneyRate } from "../../../ui/React/MoneyRate"; +import { ReputationRate } from "../../../ui/React/ReputationRate"; +import { StatsElement } from "../ui/StatsElement"; +import { MoreStatsContent } from "../ui/MoreStatsContent"; +import { MoreEarningsContent } from "../ui/MoreEarningsContent"; +import { TaskSelector } from "../ui/TaskSelector"; + +interface IProps { + player: IPlayer; + sleeve: Sleeve; + rerender: () => void; +} + +export function SleeveElem(props: IProps): React.ReactElement { + const [abc, setABC] = useState(["------", "------", "------"]); + + function openMoreStats(): void { + dialogBoxCreate(); + } + + function openTravel(): void { + const popupId = "sleeve-travel-popup"; + createPopup(popupId, TravelPopup, { + popupId: popupId, + sleeve: props.sleeve, + player: props.player, + rerender: props.rerender, + }); + } + + function openManageAugmentations(): void { + const popupId = "sleeve-augmentation-popup"; + createPopup(popupId, SleeveAugmentationsPopup, { + sleeve: props.sleeve, + player: props.player, + }); + } + + function openMoreEarnings(): void { + dialogBoxCreate(); + } + + function setTask(): void { + props.sleeve.resetTaskStatus(); // sets to idle + let res; + switch (abc[0]) { + case "------": + break; + case "Work for Company": + res = props.sleeve.workForCompany(props.player, abc[1]); + break; + case "Work for Faction": + res = props.sleeve.workForFaction(props.player, abc[1], abc[2]); + break; + case "Commit Crime": + res = props.sleeve.commitCrime(props.player, abc[1]); + break; + case "Take University Course": + res = props.sleeve.takeUniversityCourse(props.player, abc[2], abc[1]); + break; + case "Workout at Gym": + res = props.sleeve.workoutAtGym(props.player, abc[2], abc[1]); + break; + case "Shock Recovery": + res = props.sleeve.shockRecovery(props.player); + break; + case "Synchronize": + res = props.sleeve.synchronize(props.player); + break; + default: + console.error(`Invalid/Unrecognized taskValue in setSleeveTask(): ${abc[0]}`); + } + props.rerender(); + } + + let desc = <>; + switch (props.sleeve.currentTask) { + case SleeveTaskType.Idle: + desc = <>This sleeve is currently idle; + break; + case SleeveTaskType.Company: + desc = <>This sleeve is currently working your job at ${props.sleeve.currentTaskLocation}.; + break; + case SleeveTaskType.Faction: + desc = ( + <> + This sleeve is currently doing ${props.sleeve.factionWorkType} for ${props.sleeve.currentTaskLocation}. + + ); + break; + case SleeveTaskType.Crime: + desc = ( + <> + This sleeve is currently attempting to {Crimes[props.sleeve.crimeType].type} (Success Rate:{" "} + {numeralWrapper.formatPercentage(Crimes[props.sleeve.crimeType].successRate(props.sleeve))}). + + ); + break; + case SleeveTaskType.Class: + desc = <>This sleeve is currently studying/taking a course at {props.sleeve.currentTaskLocation}.; + break; + case SleeveTaskType.Gym: + desc = <>This sleeve is currently working out at {props.sleeve.currentTaskLocation}.; + break; + case SleeveTaskType.Recovery: + desc = ( + <> + This sleeve is currently set to focus on shock recovery. This causes the Sleeve's shock to decrease at a + faster rate. + + ); + break; + case SleeveTaskType.Synchro: + desc = ( + <> + This sleeve is currently set to synchronize with the original consciousness. This causes the Sleeve's + synchronization to increase. + + ); + break; + default: + console.error(`Invalid/Unrecognized taskValue in updateSleeveTaskDescription(): ${abc[0]}`); + } + + let data: any[][] = []; + if (props.sleeve.currentTask === SleeveTaskType.Crime) { + data = [ + [`Money`, , `(on success)`], + [`Hacking Exp`, numeralWrapper.formatExp(props.sleeve.gainRatesForTask.hack), `(2x on success)`], + [`Strength Exp`, numeralWrapper.formatExp(props.sleeve.gainRatesForTask.str), `(2x on success)`], + [`Defense Exp`, numeralWrapper.formatExp(props.sleeve.gainRatesForTask.def), `(2x on success)`], + [`Dexterity Exp`, numeralWrapper.formatExp(props.sleeve.gainRatesForTask.dex), `(2x on success)`], + [`Agility Exp`, numeralWrapper.formatExp(props.sleeve.gainRatesForTask.agi), `(2x on success)`], + [`Charisma Exp`, numeralWrapper.formatExp(props.sleeve.gainRatesForTask.cha), `(2x on success)`], + ]; + + // elems.taskProgressBar.innerText = createProgressBarText({ + // progress: props.sleeve.currentTaskTime / props.sleeve.currentTaskMaxTime, + // totalTicks: 25, + // }); + } else { + data = [ + [`Money:`, MoneyRate(5 * props.sleeve.gainRatesForTask.money)], + [`Hacking Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.hack)} / s`], + [`Strength Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.str)} / s`], + [`Defense Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.def)} / s`], + [`Dexterity Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.dex)} / s`], + [`Agility Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.agi)} / s`], + [`Charisma Exp:`, `${numeralWrapper.formatExp(5 * props.sleeve.gainRatesForTask.cha)} / s`], + ]; + if (props.sleeve.currentTask === SleeveTaskType.Company || props.sleeve.currentTask === SleeveTaskType.Faction) { + const repGain: number = props.sleeve.getRepGain(props.player); + data.push([`Reputation:`, ReputationRate(5 * repGain)]); + } + + // elems.taskProgressBar.innerText = ""; + } + + return ( +
+
+
+ + + + +
+
+
+ +

{desc}

+

+ {props.sleeve.currentTask === SleeveTaskType.Crime && + createProgressBarText({ + progress: props.sleeve.currentTaskTime / props.sleeve.currentTaskMaxTime, + totalTicks: 25, + })} +

+ +
+
+ + +
+
+ ); +} diff --git a/src/PersonObjects/Sleeve/ui/SleeveRoot.tsx b/src/PersonObjects/Sleeve/ui/SleeveRoot.tsx new file mode 100644 index 000000000..b609dbe9f --- /dev/null +++ b/src/PersonObjects/Sleeve/ui/SleeveRoot.tsx @@ -0,0 +1,93 @@ +import React, { useState, useEffect } from "react"; + +import { Sleeve } from "../Sleeve"; +import { SleeveTaskType } from "../SleeveTaskTypesEnum"; +import { SleeveFaq } from "../data/SleeveFaq"; + +import { IPlayer } from "../../IPlayer"; + +import { Faction } from "../../../Faction/Faction"; +import { Factions } from "../../../Faction/Factions"; +import { FactionWorkType } from "../../../Faction/FactionWorkTypeEnum"; + +import { Crime } from "../../../Crime/Crime"; +import { Crimes } from "../../../Crime/Crimes"; +import { CityName } from "../../../Locations/data/CityNames"; +import { LocationName } from "../../../Locations/data/LocationNames"; + +import { numeralWrapper } from "../../../ui/numeralFormat"; +import { Page, routing } from "../../../ui/navigationTracking"; + +import { dialogBoxCreate } from "../../../../utils/DialogBox"; + +import { createProgressBarText } from "../../../../utils/helpers/createProgressBarText"; +import { exceptionAlert } from "../../../../utils/helpers/exceptionAlert"; + +import { clearEventListeners } from "../../../../utils/uiHelpers/clearEventListeners"; +import { createElement } from "../../../../utils/uiHelpers/createElement"; +import { createOptionElement } from "../../../../utils/uiHelpers/createOptionElement"; +import { createPopup } from "../../../ui/React/createPopup"; +import { getSelectValue } from "../../../../utils/uiHelpers/getSelectData"; +import { removeChildrenFromElement } from "../../../../utils/uiHelpers/removeChildrenFromElement"; +import { removeElement } from "../../../../utils/uiHelpers/removeElement"; + +import { SleeveAugmentationsPopup } from "../ui/SleeveAugmentationsPopup"; +import { TravelPopup } from "../ui/TravelPopup"; +import { EarningsTableElement } from "../ui/EarningsTableElement"; +import { Money } from "../../../ui/React/Money"; +import { MoneyRate } from "../../../ui/React/MoneyRate"; +import { ReputationRate } from "../../../ui/React/ReputationRate"; +import { StatsElement } from "../ui/StatsElement"; +import { MoreStatsContent } from "../ui/MoreStatsContent"; +import { MoreEarningsContent } from "../ui/MoreEarningsContent"; +import { SleeveElem } from "../ui/SleeveElem"; + +interface IProps { + player: IPlayer; +} + +export function SleeveRoot(props: IProps): React.ReactElement { + const setRerender = useState(false)[1]; + function rerender(): void { + setRerender((old) => !old); + } + + useEffect(() => { + const id = setInterval(rerender, 150); + return () => clearInterval(id); + }, []); + + return ( +
+

Sleeves

+

+ Duplicate Sleeves are MK-V Synthoids (synthetic androids) into which your consciousness has been copied. In + other words, these Synthoids contain a perfect duplicate of your mind. +
+
+ Sleeves can be used to perform different tasks synchronously. +
+
+

+ + + + Documentation + +
    + {props.player.sleeves.map((sleeve, i) => ( +
  • + +
  • + ))} +
+
+ ); +} diff --git a/src/PersonObjects/Sleeve/ui/StatsElement.tsx b/src/PersonObjects/Sleeve/ui/StatsElement.tsx index 19bb51794..9dddc14a0 100644 --- a/src/PersonObjects/Sleeve/ui/StatsElement.tsx +++ b/src/PersonObjects/Sleeve/ui/StatsElement.tsx @@ -2,7 +2,11 @@ import { Sleeve } from "../Sleeve"; import { numeralWrapper } from "../../../ui/numeralFormat"; import * as React from "react"; -export function StatsElement(sleeve: Sleeve): React.ReactElement { +interface IProps { + sleeve: Sleeve; +} + +export function StatsElement(props: IProps): React.ReactElement { let style = {}; style = { textAlign: "right" }; return ( @@ -12,65 +16,65 @@ export function StatsElement(sleeve: Sleeve): React.ReactElement {
- + diff --git a/src/PersonObjects/Sleeve/ui/TaskSelector.tsx b/src/PersonObjects/Sleeve/ui/TaskSelector.tsx new file mode 100644 index 000000000..c03ae30b4 --- /dev/null +++ b/src/PersonObjects/Sleeve/ui/TaskSelector.tsx @@ -0,0 +1,290 @@ +import React, { useState } from "react"; +import { Sleeve } from "../Sleeve"; +import { IPlayer } from "../../IPlayer"; +import { SleeveTaskType } from "../SleeveTaskTypesEnum"; +import { Crimes } from "../../../Crime/Crimes"; +import { LocationName } from "../../../Locations/data/LocationNames"; +import { CityName } from "../../../Locations/data/CityNames"; +import { Factions } from "../../../Faction/Factions"; +import { FactionWorkType } from "../../../Faction/FactionWorkTypeEnum"; + +const universitySelectorOptions: string[] = [ + "Study Computer Science", + "Data Structures", + "Networks", + "Algorithms", + "Management", + "Leadership", +]; + +const gymSelectorOptions: string[] = ["Train Strength", "Train Defense", "Train Dexterity", "Train Agility"]; + +interface IProps { + sleeve: Sleeve; + player: IPlayer; + setABC: (abc: string[]) => void; +} + +interface ITaskDetails { + first: string[]; + second: (s1: string) => string[]; +} + +function possibleJobs(player: IPlayer, sleeve: Sleeve): string[] { + // Array of all companies that other sleeves are working at + const forbiddenCompanies = []; + for (const otherSleeve of player.sleeves) { + if (sleeve === otherSleeve) { + continue; + } + if (otherSleeve.currentTask === SleeveTaskType.Company) { + forbiddenCompanies.push(otherSleeve.currentTaskLocation); + } + } + let allJobs: string[] = Object.keys(player.jobs); + for (let i = 0; i < allJobs.length; ++i) { + if (!forbiddenCompanies.includes(allJobs[i])) { + allJobs[i]; + } + } + + return allJobs; +} + +function possibleFactions(player: IPlayer, sleeve: Sleeve): string[] { + // Array of all factions that other sleeves are working for + const forbiddenFactions = []; + for (const otherSleeve of player.sleeves) { + if (sleeve === otherSleeve) { + continue; + } + if (otherSleeve.currentTask === SleeveTaskType.Faction) { + forbiddenFactions.push(otherSleeve.currentTaskLocation); + } + } + + const factions = []; + for (const fac of player.factions) { + if (!forbiddenFactions.includes(fac)) { + factions.push(fac); + } + } + + return factions; +} + +const tasks: { + [key: string]: undefined | ((player: IPlayer, sleeve: Sleeve) => ITaskDetails); + ["------"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; + ["Work for Company"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; + ["Work for Faction"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; + ["Commit Crime"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; + ["Take University Course"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; + ["Workout at Gym"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; + ["Shock Recovery"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; + ["Synchronize"]: (player: IPlayer, sleeve: Sleeve) => ITaskDetails; +} = { + "------": (player: IPlayer, sleeve: Sleeve): ITaskDetails => { + return { first: ["------"], second: () => ["------"] }; + }, + "Work for Company": (player: IPlayer, sleeve: Sleeve): ITaskDetails => { + let jobs = possibleJobs(player, sleeve); + + if (jobs.length === 0) jobs = ["------"]; + return { first: jobs, second: () => ["------"] }; + }, + "Work for Faction": (player: IPlayer, sleeve: Sleeve): ITaskDetails => { + let factions = possibleFactions(player, sleeve); + if (factions.length === 0) factions = ["------"]; + + return { + first: factions, + second: (s1: string) => { + const faction = Factions[s1]; + const facInfo = faction.getInfo(); + const options: string[] = []; + if (facInfo.offerHackingWork) { + options.push("Hacking Contracts"); + } + if (facInfo.offerFieldWork) { + options.push("Field Work"); + } + if (facInfo.offerSecurityWork) { + options.push("Security Work"); + } + return options; + }, + }; + }, + "Commit Crime": (player: IPlayer, sleeve: Sleeve): ITaskDetails => { + return { first: Object.keys(Crimes), second: () => ["------"] }; + }, + "Take University Course": (player: IPlayer, sleeve: Sleeve): ITaskDetails => { + let universities: string[] = []; + switch (sleeve.city) { + case CityName.Aevum: + universities = [LocationName.AevumSummitUniversity]; + break; + case CityName.Sector12: + universities = [LocationName.Sector12RothmanUniversity]; + break; + case CityName.Volhaven: + universities = [LocationName.VolhavenZBInstituteOfTechnology]; + break; + default: + universities = ["No university available in city!"]; + break; + } + + return { first: universitySelectorOptions, second: () => universities }; + }, + "Workout at Gym": (player: IPlayer, sleeve: Sleeve): ITaskDetails => { + let gyms: string[] = []; + switch (sleeve.city) { + case CityName.Aevum: + gyms = [LocationName.AevumCrushFitnessGym, LocationName.AevumSnapFitnessGym]; + break; + case CityName.Sector12: + gyms = [LocationName.Sector12IronGym, LocationName.Sector12PowerhouseGym]; + break; + case CityName.Volhaven: + gyms = [LocationName.VolhavenMilleniumFitnessGym]; + break; + default: + gyms = ["No gym available in city!"]; + break; + } + + return { first: gymSelectorOptions, second: () => gyms }; + }, + "Shock Recovery": (player: IPlayer, sleeve: Sleeve): ITaskDetails => { + return { first: ["------"], second: () => ["------"] }; + }, + Synchronize: (player: IPlayer, sleeve: Sleeve): ITaskDetails => { + return { first: ["------"], second: () => ["------"] }; + }, +}; + +const canDo: { + [key: string]: undefined | ((player: IPlayer, sleeve: Sleeve) => boolean); + ["------"]: (player: IPlayer, sleeve: Sleeve) => boolean; + ["Work for Company"]: (player: IPlayer, sleeve: Sleeve) => boolean; + ["Work for Faction"]: (player: IPlayer, sleeve: Sleeve) => boolean; + ["Commit Crime"]: (player: IPlayer, sleeve: Sleeve) => boolean; + ["Take University Course"]: (player: IPlayer, sleeve: Sleeve) => boolean; + ["Workout at Gym"]: (player: IPlayer, sleeve: Sleeve) => boolean; + ["Shock Recovery"]: (player: IPlayer, sleeve: Sleeve) => boolean; + ["Synchronize"]: (player: IPlayer, sleeve: Sleeve) => boolean; +} = { + ["------"]: () => true, + ["Work for Company"]: (player: IPlayer, sleeve: Sleeve) => possibleJobs(player, sleeve).length > 0, + ["Work for Faction"]: (player: IPlayer, sleeve: Sleeve) => possibleFactions(player, sleeve).length > 0, + ["Commit Crime"]: () => true, + ["Take University Course"]: (player: IPlayer, sleeve: Sleeve) => + [CityName.Aevum, CityName.Sector12, CityName.Volhaven].includes(sleeve.city), + ["Workout at Gym"]: (player: IPlayer, sleeve: Sleeve) => + [CityName.Aevum, CityName.Sector12, CityName.Volhaven].includes(sleeve.city), + ["Shock Recovery"]: (player: IPlayer, sleeve: Sleeve) => sleeve.shock < 100, + ["Synchronize"]: (player: IPlayer, sleeve: Sleeve) => sleeve.sync < 100, +}; + +function getABC(sleeve: Sleeve): [string, string, string] { + switch (sleeve.currentTask) { + case SleeveTaskType.Idle: + return ["------", "------", "------"]; + case SleeveTaskType.Company: + return ["Work for Company", sleeve.currentTaskLocation, "------"]; + case SleeveTaskType.Faction: + let workType = ""; + switch (sleeve.factionWorkType) { + case FactionWorkType.Hacking: + workType = "Hacking Contracts"; + break; + case FactionWorkType.Field: + workType = "Field Work"; + break; + case FactionWorkType.Security: + workType = "Security Work"; + break; + } + return ["Work for Faction", sleeve.currentTaskLocation, workType]; + case SleeveTaskType.Crime: + return ["Commit Crime", sleeve.crimeType, "------"]; + case SleeveTaskType.Class: + return ["Take University Course", sleeve.className, sleeve.currentTaskLocation]; + case SleeveTaskType.Gym: + return ["Workout at Gym", sleeve.gymStatType, sleeve.currentTaskLocation]; + case SleeveTaskType.Recovery: + return ["Shock Recovery", "------", "------"]; + case SleeveTaskType.Synchro: + return ["Synchronize", "------", "------"]; + } +} + +export function TaskSelector(props: IProps): React.ReactElement { + const abc = getABC(props.sleeve); + const [s0, setS0] = useState(abc[0]); + const [s1, setS1] = useState(abc[1]); + const [s2, setS2] = useState(abc[2]); + + const validActions = Object.keys(canDo).filter((k) => + (canDo[k] as (player: IPlayer, sleeve: Sleeve) => boolean)(props.player, props.sleeve), + ); + + const detailsF = tasks[s0]; + if (detailsF === undefined) throw new Error(`No function for task '${s0}'`); + const details = detailsF(props.player, props.sleeve); + const details2 = details.second(s1); + + function onS0Change(event: React.ChangeEvent): void { + const n = event.target.value; + const detailsF = tasks[n]; + if (detailsF === undefined) throw new Error(`No function for task '${s0}'`); + const details = detailsF(props.player, props.sleeve); + const details2 = details.second(details.first[0]); + setS2(details2[0]); + setS1(details.first[0]); + setS0(n); + props.setABC([n, details.first[0], details2[0]]); + } + + function onS1Change(event: React.ChangeEvent): void { + setS1(event.target.value); + props.setABC([s0, event.target.value, s2]); + } + + function onS2Change(event: React.ChangeEvent): void { + setS2(event.target.value); + props.setABC([s0, s1, event.target.value]); + } + + return ( + <> + + {!(details.first.length === 1 && details.first[0] === "------") && ( + + )} + {!(details2.length === 1 && details2[0] === "------") && ( + + )} + + ); +} diff --git a/src/PersonObjects/Sleeve/ui/TravelPopup.tsx b/src/PersonObjects/Sleeve/ui/TravelPopup.tsx index 22620104b..dad35b90f 100644 --- a/src/PersonObjects/Sleeve/ui/TravelPopup.tsx +++ b/src/PersonObjects/Sleeve/ui/TravelPopup.tsx @@ -12,6 +12,7 @@ interface IProps { popupId: string; sleeve: Sleeve; player: IPlayer; + rerender: () => void; } export function TravelPopup(props: IProps): React.ReactElement { @@ -23,6 +24,7 @@ export function TravelPopup(props: IProps): React.ReactElement { props.player.loseMoney(CONSTANTS.TravelCost); props.sleeve.resetTaskStatus(); removePopup(props.popupId); + props.rerender(); } return ( @@ -35,7 +37,7 @@ export function TravelPopup(props: IProps): React.ReactElement { {Object.keys(Cities) .filter((city: string) => props.sleeve.city !== city) .map((city: string) => ( -
travel(city)}> +
travel(city)}> {city}
))} diff --git a/src/engine.jsx b/src/engine.jsx index 904853c15..4cfceede5 100644 --- a/src/engine.jsx +++ b/src/engine.jsx @@ -23,6 +23,7 @@ import { Root as BladeburnerRoot } from "./Bladeburner/ui/Root"; import { Root as GangRoot } from "./Gang/ui/Root"; import { CorporationRoot } from "./Corporation/ui/CorporationRoot"; import { ResleeveRoot } from "./PersonObjects/Resleeving/ui/ResleeveRoot"; +import { SleeveRoot } from "./PersonObjects/Sleeve/ui/SleeveRoot"; import { displayInfiltrationContent } from "./Infiltration/Helper"; import { getHackingWorkRepGain, @@ -57,7 +58,6 @@ import { initSymbolToStockMap, processStockPrices, displayStockMarketContent } f import { displayMilestonesContent } from "./Milestones/MilestoneHelpers"; import { Terminal, postNetburnerText } from "./Terminal"; import { Sleeve } from "./PersonObjects/Sleeve/Sleeve"; -import { clearSleevesPage, createSleevesPage, updateSleevesPage } from "./PersonObjects/Sleeve/SleeveUI"; import { createStatusText } from "./ui/createStatusText"; import { CharacterInfo } from "./ui/CharacterInfo"; @@ -190,6 +190,7 @@ const Engine = { gangContent: null, bladeburnerContent: null, resleeveContent: null, + sleeveContent: null, corporationContent: null, locationContent: null, workInProgressContent: null, @@ -429,14 +430,10 @@ const Engine = { }, loadSleevesContent: function () { - // This is for Duplicate Sleeves page, not Re-sleeving @ Vita Life - try { - Engine.hideAllContent(); - routing.navigateTo(Page.Sleeves); - createSleevesPage(Player); - } catch (e) { - exceptionAlert(e); - } + Engine.hideAllContent(); + routing.navigateTo(Page.Sleeves); + Engine.Display.sleevesContent.style.display = "block"; + ReactDOM.render(, Engine.Display.sleevesContent); }, loadResleevingContent: function () { @@ -487,20 +484,18 @@ const Engine = { Engine.Display.resleeveContent.style.display = "none"; ReactDOM.unmountComponentAtNode(Engine.Display.resleeveContent); + Engine.Display.sleevesContent.style.display = "none"; + ReactDOM.unmountComponentAtNode(Engine.Display.sleevesContent); + Engine.Display.corporationContent.style.display = "none"; ReactDOM.unmountComponentAtNode(Engine.Display.corporationContent); - Engine.Display.resleeveContent.style.display = "none"; - ReactDOM.unmountComponentAtNode(Engine.Display.resleeveContent); - Engine.Display.workInProgressContent.style.display = "none"; Engine.Display.redPillContent.style.display = "none"; Engine.Display.cinematicTextContent.style.display = "none"; Engine.Display.stockMarketContent.style.display = "none"; Engine.Display.missionContent.style.display = "none"; - clearSleevesPage(); - // Make nav menu tabs inactive Engine.inactivateMainMenuLinks(); @@ -738,8 +733,6 @@ const Engine = { Engine.displayCharacterOverviewInfo(); if (routing.isOn(Page.CreateProgram)) { displayCreateProgramContent(); - } else if (routing.isOn(Page.Sleeves)) { - updateSleevesPage(); } Engine.Counters.updateDisplays = 3; @@ -1258,6 +1251,9 @@ const Engine = { Engine.Display.resleeveContent = document.getElementById("resleeve-container"); Engine.Display.resleeveContent.style.display = "none"; + Engine.Display.sleevesContent = document.getElementById("sleeves-container"); + Engine.Display.sleevesContent.style.display = "none"; + Engine.Display.corporationContent = document.getElementById("corporation-container"); Engine.Display.corporationContent.style.display = "none"; diff --git a/src/index.html b/src/index.html index 14d1be80f..c1c0528f3 100644 --- a/src/index.html +++ b/src/index.html @@ -367,6 +367,7 @@
+
HP: - {numeralWrapper.formatHp(sleeve.hp)} / {numeralWrapper.formatHp(sleeve.max_hp)} + {numeralWrapper.formatHp(props.sleeve.hp)} / {numeralWrapper.formatHp(props.sleeve.max_hp)}
City: {sleeve.city}{props.sleeve.city}
Hacking: - {numeralWrapper.formatSkill(sleeve.hacking_skill)} + {numeralWrapper.formatSkill(props.sleeve.hacking_skill)}
Strength: - {numeralWrapper.formatSkill(sleeve.strength)} + {numeralWrapper.formatSkill(props.sleeve.strength)}
Defense: - {numeralWrapper.formatSkill(sleeve.defense)} + {numeralWrapper.formatSkill(props.sleeve.defense)}
Dexterity: - {numeralWrapper.formatSkill(sleeve.dexterity)} + {numeralWrapper.formatSkill(props.sleeve.dexterity)}
Agility: - {numeralWrapper.formatSkill(sleeve.agility)} + {numeralWrapper.formatSkill(props.sleeve.agility)}
Charisma: - {numeralWrapper.formatSkill(sleeve.charisma)} + {numeralWrapper.formatSkill(props.sleeve.charisma)}
Shock: - {numeralWrapper.formatSleeveShock(100 - sleeve.shock)} + {numeralWrapper.formatSleeveShock(100 - props.sleeve.shock)}
Sync: - {numeralWrapper.formatSleeveSynchro(sleeve.sync)} + {numeralWrapper.formatSleeveSynchro(props.sleeve.sync)}
Memory: - {numeralWrapper.formatSleeveMemory(sleeve.memory)} + {numeralWrapper.formatSleeveMemory(props.sleeve.memory)}