From 10231b6c66473dd2d75553aa980fb7d615de44ed Mon Sep 17 00:00:00 2001 From: danielyxie Date: Thu, 17 Jan 2019 07:40:43 -0800 Subject: [PATCH] Finished ResleeveUI implementation (untested) --- src/PersonObjects/IPlayer.ts | 59 ++++++ src/PersonObjects/Person.ts | 66 ++----- src/PersonObjects/Resleeving/README.md | 2 +- src/PersonObjects/Resleeving/Resleeve.ts | 17 ++ src/PersonObjects/Resleeving/Resleeving.ts | 29 ++- src/PersonObjects/Resleeving/ResleevingUI.ts | 181 ++++++++++++++++++- src/PersonObjects/Sleeve/README.md | 9 + src/PersonObjects/Sleeve/Sleeve.ts | 2 +- src/PersonObjects/Sleeve/SleeveUI.ts | 19 +- src/engine.js | 4 + src/ui/navigationTracking.ts | 5 + 11 files changed, 320 insertions(+), 73 deletions(-) create mode 100644 src/PersonObjects/IPlayer.ts create mode 100644 src/PersonObjects/Sleeve/README.md diff --git a/src/PersonObjects/IPlayer.ts b/src/PersonObjects/IPlayer.ts new file mode 100644 index 000000000..1a2fd506a --- /dev/null +++ b/src/PersonObjects/IPlayer.ts @@ -0,0 +1,59 @@ +// Interface for an object that represents the player (PlayerObject) +// Used because at the time of implementation, the PlayerObject +// cant be converted to TypeScript. +// +// Only contains the needed properties for Sleeve implementation +import { Resleeve } from "./Resleeving/Resleeve"; +import { Sleeve } from "./Sleeve/Sleeve"; + +import { IMap } from "../types"; + +import { IPlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation"; + +export interface IPlayer { + augmentations: IPlayerOwnedAugmentation[]; + companyName: string; + factions: string[]; + jobs: IMap; + money: any; + queuedAugmentations: IPlayerOwnedAugmentation[]; + resleeves: Resleeve[]; + sleeves: Sleeve[]; + + hacking_skill: number; + strength: number; + defense: number; + dexterity: number; + agility: number; + charisma: number; + intelligence: number; + + hacking_exp: number; + strength_exp: number; + defense_exp: number; + dexterity_exp: number; + agility_exp: number; + charisma_exp: number; + + crime_success_mult: number; + + gainHackingExp(exp: number): void; + gainStrengthExp(exp: number): void; + gainDefenseExp(exp: number): void; + gainDexterityExp(exp: number): void; + gainAgilityExp(exp: number): void; + gainCharismaExp(exp: number): void; + gainMoney(money: number): void; + loseMoney(money: number): void; + reapplyAllAugmentations(resetMultipliers: boolean): void; + startCrime(crimeType: string, + hackExp: number, + strExp: number, + defExp: number, + dexExp: number, + agiExp: number, + chaExp: number, + money: number, + time: number, + singParams: any): void; +} diff --git a/src/PersonObjects/Person.ts b/src/PersonObjects/Person.ts index 39ffeeb92..9cdebeedd 100644 --- a/src/PersonObjects/Person.ts +++ b/src/PersonObjects/Person.ts @@ -1,61 +1,12 @@ // Base class representing a person-like object +import { Augmentation } from "../Augmentation/Augmentation"; +import { Augmentations } from "../Augmentation/Augmentations"; import { IPlayerOwnedAugmentation } from "../Augmentation/PlayerOwnedAugmentation"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; import { Cities } from "../Locations/Cities"; import { CONSTANTS } from "../Constants"; import { IMap } from "../types"; -// Interface for an object that represents the player (PlayerObject) -// Used because at the time of implementation, the PlayerObject -// cant be converted to TypeScript. -// -// Only contains the needed properties for Sleeve implementation -export interface IPlayer { - augmentations: IPlayerOwnedAugmentation[]; - companyName: string; - factions: string[]; - jobs: IMap; - money: any; - queuedAugmentations: IPlayerOwnedAugmentation[]; - - hacking_skill: number; - strength: number; - defense: number; - dexterity: number; - agility: number; - charisma: number; - intelligence: number; - - hacking_exp: number; - strength_exp: number; - defense_exp: number; - dexterity_exp: number; - agility_exp: number; - charisma_exp: number; - - crime_success_mult: number; - - gainHackingExp(exp: number): void; - gainStrengthExp(exp: number): void; - gainDefenseExp(exp: number): void; - gainDexterityExp(exp: number): void; - gainAgilityExp(exp: number): void; - gainCharismaExp(exp: number): void; - gainMoney(money: number): void; - loseMoney(money: number): void; - reapplyAllAugmentations(resetMultipliers: boolean): void; - startCrime(crimeType: string, - hackExp: number, - strExp: number, - defExp: number, - dexExp: number, - agiExp: number, - chaExp: number, - money: number, - time: number, - singParams: any): void; -} - // Interface that defines a generic object used to track experience/money // earnings for tasks export interface ITaskTracker { @@ -147,6 +98,19 @@ export abstract class Person { constructor() {} + /** + * Updates this object's multipliers for the given augmentation + */ + applyAugmentation(aug: Augmentation, reapply=false) { + for (const mult in aug.mults) { + if ((this)[mult] == null) { + console.warn(`Augmentation has unrecognized multiplier property: ${mult}`); + } else { + (this)[mult] *= aug.mults[mult]; + } + } + } + /** * Given an experience amount and stat multiplier, calculates the * stat level. Stat-agnostic (same formula for every stat) diff --git a/src/PersonObjects/Resleeving/README.md b/src/PersonObjects/Resleeving/README.md index e9ac3e146..ba3116b94 100644 --- a/src/PersonObjects/Resleeving/README.md +++ b/src/PersonObjects/Resleeving/README.md @@ -7,4 +7,4 @@ the user to use it in other BitNodes (provided that they purchase the required cortical stack Augmentation) While they are based on the same concept, this feature is different than the -"Sleeve" mechanic. +"Duplicate Sleeve" mechanic (which is referred to as just "Sleeve" in the source code). diff --git a/src/PersonObjects/Resleeving/Resleeve.ts b/src/PersonObjects/Resleeving/Resleeve.ts index 98dca9a8e..7887d7856 100644 --- a/src/PersonObjects/Resleeving/Resleeve.ts +++ b/src/PersonObjects/Resleeving/Resleeve.ts @@ -8,7 +8,16 @@ import { Augmentation } from "../../Augmentation/Augmentation"; import { Augmentations } from "../../Augmentation/Augmentations"; import { CONSTANTS } from "../../Constants"; +import { Generic_fromJSON, Generic_toJSON, Reviver } from "../../../utils/JSONReviver"; + export class Resleeve extends Person { + /** + * Initiatizes a Resleeve object from a JSON save state. + */ + static fromJSON(value: any): Resleeve { + return Generic_fromJSON(Resleeve, value.data); + } + constructor() { super(); } @@ -42,4 +51,12 @@ export class Resleeve extends Person { return (totalExp * CostPerExp) + (totalAugmentationCost * Math.pow(this.augmentations.length, NumAugsExponent)); } + /** + * Serialize the current object to a JSON save state. + */ + toJSON(): any { + return Generic_toJSON("Resleeve", this); + } } + +Reviver.constructors.Resleeve = Resleeve; diff --git a/src/PersonObjects/Resleeving/Resleeving.ts b/src/PersonObjects/Resleeving/Resleeving.ts index 41cd4f20e..54139f3da 100644 --- a/src/PersonObjects/Resleeving/Resleeving.ts +++ b/src/PersonObjects/Resleeving/Resleeving.ts @@ -11,17 +11,19 @@ * As of right now, this feature is only available in BitNode 10 */ import { Resleeve } from "./Resleeve"; -import { IPlayer } from "../Person"; +import { IPlayer } from "../IPlayer"; import { Augmentation } from "../../Augmentation/Augmentation"; import { Augmentations } from "../../Augmentation/Augmentations"; -import { PlayerOwnedAugmentation } from "../../Augmentation/PlayerOwnedAugmentation"; +import { IPlayerOwnedAugmentation, + PlayerOwnedAugmentation } from "../../Augmentation/PlayerOwnedAugmentation"; +import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; import { getRandomInt } from "../../../utils/helpers/getRandomInt"; // Executes the actual re-sleeve when one is purchased -export function resleeve(r: Resleeve, p: IPlayer):void { +export function purchaseResleeve(r: Resleeve, p: IPlayer):void { // Set the player's exp p.hacking_exp = r.hacking_exp; p.strength_exp = r.strength_exp; @@ -30,15 +32,28 @@ export function resleeve(r: Resleeve, p: IPlayer):void { p.agility_exp = r.agility_exp; p.charisma_exp = r.charisma_exp; - // Clear all of the player's augmentations, including those that are - // purchased but not installed - p.queuedAugmentations.length = 0; - p.augmentations.length = 0; + // Clear all of the player's augmentations, except the NeuroFlux Governor + // which is kept + for (let i = p.augmentations.length - 1; i >= 0; --i) { + if (p.augmentations[i].name !== AugmentationNames.NeuroFluxGovernor) { + p.augmentations.splice(i, 1); + } + } for (let i = 0; i < r.augmentations.length; ++i) { p.augmentations.push(new PlayerOwnedAugmentation(r.augmentations[i].name)); } + // The player's purchased Augmentations should remain the same, but any purchased + // Augmentations that are given by the resleeve should be removed so there are no duplicates + for (let i = p.queuedAugmentations.length - 1; i >= 0; --i) { + const name: string = p.queuedAugmentations[i].name; + + if (p.augmentations.filter((e: IPlayerOwnedAugmentation) => {e.name !== AugmentationNames.NeuroFluxGovernor && e.name === name}).length >= 1) { + p.queuedAugmentations.splice(i, 1); + } + } + p.reapplyAllAugmentations(true); } diff --git a/src/PersonObjects/Resleeving/ResleevingUI.ts b/src/PersonObjects/Resleeving/ResleevingUI.ts index ab7af3da7..7e6494f44 100644 --- a/src/PersonObjects/Resleeving/ResleevingUI.ts +++ b/src/PersonObjects/Resleeving/ResleevingUI.ts @@ -3,17 +3,196 @@ */ import { Resleeve } from "./Resleeve"; import { generateResleeves, - resleeve } from "./Resleeving"; + purchaseResleeve } from "./Resleeving"; + +import { IPlayer } from "../IPlayer"; import { IMap } from "../../types"; +import { Augmentation } from "../../Augmentation/Augmentation"; +import { Augmentations } from "../../Augmentation/Augmentations"; + import { numeralWrapper } from "../../ui/numeralFormat"; import { Page, routing } from "../../ui/navigationTracking"; +import { dialogBoxCreate } from "../../../utils/DialogBox"; + +import { exceptionAlert } from "../../../utils/helpers/exceptionAlert"; + import { createElement } from "../../../utils/uiHelpers/createElement"; import { createOptionElement } from "../../../utils/uiHelpers/createOptionElement"; import { getSelectValue } from "../../../utils/uiHelpers/getSelectData"; import { removeChildrenFromElement } from "../../../utils/uiHelpers/removeChildrenFromElement"; import { removeElement } from "../../../utils/uiHelpers/removeElement"; import { removeElementById } from "../../../utils/uiHelpers/removeElementById"; + +interface IResleeveUIElems { + container: HTMLElement | null; + statsPanel: HTMLElement | null; + stats: HTMLElement | null; + multipliersButton: HTMLElement | null; + augPanel: HTMLElement | null; + augSelector: HTMLSelectElement | null; + augDescription: HTMLElement | null; + costPanel: HTMLElement | null; + costText: HTMLElement | null; + buyButton: HTMLElement | null; +} + +interface IPageUIElems { + container: HTMLElement | null; + info: HTMLElement | null; + resleeveList: HTMLElement | null; + resleeves: IResleeveUIElems[] | null; +} + +const UIElems: IPageUIElems = { + container: null, + info: null, + resleeveList: null, + resleeves: null, +} + +let playerRef: IPlayer | null; + +export function createResleevesPage(p: IPlayer) { + if (!routing.isOn(Page.Resleeves)) { return; } + + try { + playerRef = p; + + UIElems.container = createElement("div", { + class: "generic-menupage-container", + id: "resleeves-container", + position: "fixed", + }); + + UIElems.info = createElement("p", { + display: "inline-block", + innerText: "TOODOOO", + }); + + UIElems.resleeveList = createElement("ul"); + UIElems.resleeves = []; + + if (p.resleeves.length === 0) { + p.resleeves = generateResleeves(); + } + + for (const resleeve of p.resleeves) { + const resleeveUi = createResleeveUi(resleeve); + UIElems.resleeveList.appendChild(resleeveUi.container!); + UIElems.resleeves.push(resleeveUi); + } + + UIElems.container.appendChild(UIElems.info); + UIElems.container.appendChild(UIElems.resleeveList); + + document.getElementById("entire-game-container")!.appendChild(UIElems.container); + } catch(e) { + exceptionAlert(e); + } +} + +export function clearResleevesPage() { + removeElement(UIElems.container); + for (const prop in UIElems) { + (UIElems)[prop] = null; + } + + playerRef = null; +} + +function createResleeveUi(resleeve: Resleeve): IResleeveUIElems { + const elems: IResleeveUIElems = { + container: null, + statsPanel: null, + stats: null, + multipliersButton: null, + augPanel: null, + augSelector: null, + augDescription: null, + costPanel: null, + costText: null, + buyButton: null, + }; + + if (!routing.isOn(Page.Resleeves)) { return elems; } + + elems.container = createElement("div", { + class: "resleeve-container", + display: "block", + }); + + elems.statsPanel = createElement("div", { class: "resleeve-panel" }); + elems.stats = createElement("p", { class: "resleeve-stats-text" }); + elems.multipliersButton = createElement("button", { + class: "std-button", + innerText: "Multipliers", + clickListener: () => { + dialogBoxCreate( + [ + "

Total Multipliers:

", + `Hacking Level multiplier: ${numeralWrapper.formatPercentage(resleeve.hacking_mult)}`, + `Hacking Experience multiplier: ${numeralWrapper.formatPercentage(resleeve.hacking_exp_mult)}`, + `Strength Level multiplier: ${numeralWrapper.formatPercentage(resleeve.strength_mult)}`, + `Strength Experience multiplier: ${numeralWrapper.formatPercentage(resleeve.strength_exp_mult)}`, + `Defense Level multiplier: ${numeralWrapper.formatPercentage(resleeve.defense_mult)}`, + `Defense Experience multiplier: ${numeralWrapper.formatPercentage(resleeve.defense_exp_mult)}`, + `Dexterity Level multiplier: ${numeralWrapper.formatPercentage(resleeve.dexterity_mult)}`, + `Dexterity Experience multiplier: ${numeralWrapper.formatPercentage(resleeve.dexterity_exp_mult)}`, + `Agility Level multiplier: ${numeralWrapper.formatPercentage(resleeve.agility_mult)}`, + `Agility Experience multiplier: ${numeralWrapper.formatPercentage(resleeve.agility_exp_mult)}`, + `Charisma Level multiplier: ${numeralWrapper.formatPercentage(resleeve.charisma_mult)}`, + `Charisma Experience multiplier: ${numeralWrapper.formatPercentage(resleeve.charisma_exp_mult)}`, + `Hacking Chance multiplier: ${numeralWrapper.formatPercentage(resleeve.hacking_chance_mult)}`, + `Hacking Speed multiplier: ${numeralWrapper.formatPercentage(resleeve.hacking_speed_mult)}`, + `Hacking Money multiplier: ${numeralWrapper.formatPercentage(resleeve.hacking_money_mult)}`, + `Hacking Growth multiplier: ${numeralWrapper.formatPercentage(resleeve.hacking_grow_mult)}`, + `Salary multiplier: ${numeralWrapper.formatPercentage(resleeve.work_money_mult)}`, + `Company Reputation Gain multiplier: ${numeralWrapper.formatPercentage(resleeve.company_rep_mult)}`, + `Faction Reputation Gain multiplier: ${numeralWrapper.formatPercentage(resleeve.faction_rep_mult)}`, + `Crime Money multiplier: ${numeralWrapper.formatPercentage(resleeve.crime_money_mult)}`, + `Crime Success multiplier: ${numeralWrapper.formatPercentage(resleeve.crime_success_mult)}`, + ].join("
"), false + ) + } + }); + + elems.augPanel = createElement("div", { class: "resleeve-panel" }); + elems.augSelector = createElement("select") as HTMLSelectElement; + for (let i = 0; i < resleeve.augmentations.length; ++i) { + elems.augSelector.add(createOptionElement(resleeve.augmentations[i].name)); + }; + elems.augSelector.addEventListener("change", () => { + updateAugDescription(elems); + }); + elems.augDescription = createElement("p"); + + elems.costPanel = createElement("div", { class: "resleeve-panel" }); + elems.costText = createElement("p", { + innerText: `It costs ${numeralWrapper.formatMoney(resleeve.getCost())} ` + + `to purchase this Sleeve.`, + }); + elems.buyButton = createElement("button", { + class: "std-button", + innerText: "Purchase", + clickListener: () => { + purchaseResleeve(resleeve, playerRef!); + } + }); + + return elems; +} + +function updateAugDescription(elems: IResleeveUIElems) { + const augName: string = getSelectValue(elems.augSelector); + const aug: Augmentation | null = Augmentations[augName]; + if (aug == null) { + console.warn(`Could not find Augmentation with name ${augName}`); + return; + } + + elems.augDescription.innerHTML = aug!.info; +} diff --git a/src/PersonObjects/Sleeve/README.md b/src/PersonObjects/Sleeve/README.md new file mode 100644 index 000000000..8e1f09afd --- /dev/null +++ b/src/PersonObjects/Sleeve/README.md @@ -0,0 +1,9 @@ +Implements the "Duplicate Sleeves" feature, which allows the player to purchase +new duplicate sleeves. These are synthetic bodies that contain the player's +cloned consciousness. The player can use these sleeves to perform +different tasks synchronously. + +This feature is introduced and unlocked in BitNode-10. + +Note that while they are based on the same concept, this feature is different +than the "Re-sleeving" mechanic (which is referred to as "Resleeve" in the source code). diff --git a/src/PersonObjects/Sleeve/Sleeve.ts b/src/PersonObjects/Sleeve/Sleeve.ts index e4b89053b..0d14233b6 100644 --- a/src/PersonObjects/Sleeve/Sleeve.ts +++ b/src/PersonObjects/Sleeve/Sleeve.ts @@ -8,8 +8,8 @@ */ import { SleeveTaskType } from "./SleeveTaskTypesEnum"; +import { IPlayer } from "../IPlayer"; import { Person, - IPlayer, ITaskTracker, createTaskTracker } from "../Person"; diff --git a/src/PersonObjects/Sleeve/SleeveUI.ts b/src/PersonObjects/Sleeve/SleeveUI.ts index beef9b14e..456d43df1 100644 --- a/src/PersonObjects/Sleeve/SleeveUI.ts +++ b/src/PersonObjects/Sleeve/SleeveUI.ts @@ -4,7 +4,7 @@ import { Sleeve } from "./Sleeve"; import { SleeveTaskType } from "./SleeveTaskTypesEnum"; -import { IPlayer } from "../Person"; +import { IPlayer } from "../IPlayer"; import { Locations } from "../../Locations"; @@ -47,9 +47,9 @@ interface ISleeveUIElems { // Object that keeps track of all DOM elements for the entire Sleeve UI interface IPageUIElems { - container: Element | null; - info: Element | null, - sleeveList: Element | null, + container: HTMLElement | null; + info: HTMLElement | null, + sleeveList: HTMLElement | null, sleeves: ISleeveUIElems[] | null, } @@ -60,14 +60,9 @@ const UIElems: IPageUIElems = { sleeves: null, } -// Interface for Player object -interface ISleeveUiPlayer extends IPlayer { - sleeves: Sleeve[]; -} - // Creates the UI for the entire Sleeves page -let playerRef: ISleeveUiPlayer | null; -export function createSleevesPage(p: ISleeveUiPlayer) { +let playerRef: IPlayer | null; +export function createSleevesPage(p: IPlayer) { if (!routing.isOn(Page.Sleeves)) { return; } try { @@ -147,7 +142,7 @@ function createSleeveUi(sleeve: Sleeve, allSleeves: Sleeve[]): ISleeveUIElems { }); elems.statsPanel = createElement("div", { class: "sleeve-panel" }); - elems.stats = createElement("p", { class: "sleeve-stats-text tooltip" }); + elems.stats = createElement("p", { class: "sleeve-stats-text" }); elems.moreStatsButton = createElement("button", { class: "std-button", innerText: "More Stats", diff --git a/src/engine.js b/src/engine.js index 031266ed5..d33fc3f7d 100644 --- a/src/engine.js +++ b/src/engine.js @@ -477,6 +477,10 @@ const Engine = { } }, + loadSleevesContent: function() { + + }, + //Helper function that hides all content hideAllContent: function() { Engine.Display.terminalContent.style.display = "none"; diff --git a/src/ui/navigationTracking.ts b/src/ui/navigationTracking.ts index c55801d9d..e276dc7c8 100644 --- a/src/ui/navigationTracking.ts +++ b/src/ui/navigationTracking.ts @@ -117,6 +117,11 @@ export enum Page { * Manage your Sleeves */ Sleeves = "Sleeves", + + /** + * Purchase Resleeves + */ + Resleeves = "Re-sleeving", } /**