From 793d9b34ce9848485216502ace7cda727267355e Mon Sep 17 00:00:00 2001 From: Olivier Gagnon Date: Sat, 25 Sep 2021 17:21:50 -0400 Subject: [PATCH] update BN13 for new UI --- css/staneksgift.scss | 23 ++ doc/source/netscript.rst | 1 + doc/source/netscript/netscriptstanekapi.rst | 20 ++ doc/source/netscript/stanekapi/canPlace.rst | 15 + doc/source/netscript/stanekapi/charge.rst | 21 ++ doc/source/netscript/stanekapi/clear.rst | 13 + doc/source/netscript/stanekapi/deleteAt.rst | 16 + doc/source/netscript/stanekapi/fragmentAt.rst | 28 ++ .../stanekapi/fragmentDefinitions.rst | 23 ++ doc/source/netscript/stanekapi/place.rst | 15 + .../netscript/stanekapi/placedFragments.rst | 27 ++ src/Augmentation/AugmentationHelpers.tsx | 136 ++++++++ src/Augmentation/data/AugmentationNames.ts | 5 + src/BitNode/BitNode.tsx | 81 ++++- src/BitNode/ui/BitverseRoot.tsx | 2 +- src/CotMG/ActiveFragment.ts | 94 ++++++ src/CotMG/Fragment.ts | 290 ++++++++++++++++++ src/CotMG/FragmentType.ts | 27 ++ src/CotMG/Helper.tsx | 14 + src/CotMG/IStaneksGift.ts | 18 ++ src/CotMG/StaneksGift.ts | 209 +++++++++++++ src/CotMG/formulas/charge.ts | 6 + src/CotMG/formulas/effect.ts | 3 + src/CotMG/ui/Cell.tsx | 18 ++ src/CotMG/ui/FragmentInspector.tsx | 57 ++++ src/CotMG/ui/FragmentSelector.tsx | 67 ++++ src/CotMG/ui/G.tsx | 26 ++ src/CotMG/ui/Grid.tsx | 141 +++++++++ src/CotMG/ui/StaneksGiftRoot.tsx | 16 + src/Faction/FactionInfo.tsx | 42 +++ src/Locations/Locations.ts | 4 +- src/Locations/data/LocationNames.ts | 1 + src/Locations/data/LocationsMetadata.ts | 5 + src/Locations/ui/SpecialLocation.tsx | 110 +++++++ src/Netscript/RamCostGenerator.ts | 20 ++ src/NetscriptFunctions.ts | 74 ++++- src/NetscriptWorker.ts | 1 + src/PersonObjects/IPlayer.ts | 4 +- src/PersonObjects/Player/PlayerObject.ts | 5 +- .../Player/PlayerObjectGeneralMethods.tsx | 4 + src/Prestige.ts | 6 + src/SaveObject.tsx | 9 + src/Script/RamCalculations.ts | 2 + src/Sidebar/ui/SidebarRoot.tsx | 44 ++- src/engine.tsx | 4 + src/engineStyle.ts | 1 + src/ui/GameRoot.tsx | 10 + src/ui/Router.ts | 2 + src/ui/numeralFormat.ts | 12 + 49 files changed, 1763 insertions(+), 9 deletions(-) create mode 100644 css/staneksgift.scss create mode 100644 doc/source/netscript/netscriptstanekapi.rst create mode 100644 doc/source/netscript/stanekapi/canPlace.rst create mode 100644 doc/source/netscript/stanekapi/charge.rst create mode 100644 doc/source/netscript/stanekapi/clear.rst create mode 100644 doc/source/netscript/stanekapi/deleteAt.rst create mode 100644 doc/source/netscript/stanekapi/fragmentAt.rst create mode 100644 doc/source/netscript/stanekapi/fragmentDefinitions.rst create mode 100644 doc/source/netscript/stanekapi/place.rst create mode 100644 doc/source/netscript/stanekapi/placedFragments.rst create mode 100644 src/CotMG/ActiveFragment.ts create mode 100644 src/CotMG/Fragment.ts create mode 100644 src/CotMG/FragmentType.ts create mode 100644 src/CotMG/Helper.tsx create mode 100644 src/CotMG/IStaneksGift.ts create mode 100644 src/CotMG/StaneksGift.ts create mode 100644 src/CotMG/formulas/charge.ts create mode 100644 src/CotMG/formulas/effect.ts create mode 100644 src/CotMG/ui/Cell.tsx create mode 100644 src/CotMG/ui/FragmentInspector.tsx create mode 100644 src/CotMG/ui/FragmentSelector.tsx create mode 100644 src/CotMG/ui/G.tsx create mode 100644 src/CotMG/ui/Grid.tsx create mode 100644 src/CotMG/ui/StaneksGiftRoot.tsx diff --git a/css/staneksgift.scss b/css/staneksgift.scss new file mode 100644 index 000000000..83fccf3e4 --- /dev/null +++ b/css/staneksgift.scss @@ -0,0 +1,23 @@ +.staneksgift_row { + padding: 0; + margin: 0; +} + +.staneksgift_cell { + width: 25px; + height: 25px; + background-color: #808080; + font-color: white; + padding: 0px; + margin: 0px; + border: 1px solid black; + float: left; +} + +.staneksgift_cell:first-child { + clear: left; +} + +.staneksgift_container { + position: fixed; +} diff --git a/doc/source/netscript.rst b/doc/source/netscript.rst index 18e60b384..072705698 100644 --- a/doc/source/netscript.rst +++ b/doc/source/netscript.rst @@ -30,5 +30,6 @@ to reach out to the developer! Gang API Coding Contract API Sleeve API + Stanek API Formulas API Miscellaneous diff --git a/doc/source/netscript/netscriptstanekapi.rst b/doc/source/netscript/netscriptstanekapi.rst new file mode 100644 index 000000000..eba109efa --- /dev/null +++ b/doc/source/netscript/netscriptstanekapi.rst @@ -0,0 +1,20 @@ +.. _netscriptstanek: + +Netscript Stanek Functions +============================ + +.. warning:: This page contains spoilers for the game. + +The Stanek API allow you to control Stanek's Gift. + +All these function require Source-File 13-1 or to be in BitNode 13. + +.. toctree:: + charge() + fragmentDefinitions() + placedFragments() + clear() + canPlace() + place() + fragmentAt() + deleteAt() \ No newline at end of file diff --git a/doc/source/netscript/stanekapi/canPlace.rst b/doc/source/netscript/stanekapi/canPlace.rst new file mode 100644 index 000000000..343585cea --- /dev/null +++ b/doc/source/netscript/stanekapi/canPlace.rst @@ -0,0 +1,15 @@ +canPlace() Netscript Function +======================================= + +.. js:function:: canPlace(worldX, worldY, fragmentId) + + :RAM cost: 0.5 GB + :param int worldX: World X against which to align the top left of the fragment. + :param int worldY: World Y against which to align the top left of the fragment. + :param int fragmentId: ID of the fragment to place. + :returns: `true` if the fragment can be placed at that position. `false` otherwise. + + Example: + + .. code-block:: javascript + canPlace(0, 4, 17); // returns true \ No newline at end of file diff --git a/doc/source/netscript/stanekapi/charge.rst b/doc/source/netscript/stanekapi/charge.rst new file mode 100644 index 000000000..b84a8ad46 --- /dev/null +++ b/doc/source/netscript/stanekapi/charge.rst @@ -0,0 +1,21 @@ +charge() Netscript Function +======================================= + +.. js:function:: charge(worldX, worldY) + + :RAM cost: 0.4 GB + :param int worldX: World X of the fragment to charge. + :param int worldY: World Y of the fragment to charge. + + Charge a fragment, increasing it's power but also it's heat. The + effectiveness of the charge depends on the amount of ram the running script + consumes as well as the fragments current heat. This operation takes time to + complete. + + Example: + + .. code-block:: javascript + charge(0, 4); // Finishes 5 seconds later. +.. warning:: + + Netscript JS users: This function is `async` \ No newline at end of file diff --git a/doc/source/netscript/stanekapi/clear.rst b/doc/source/netscript/stanekapi/clear.rst new file mode 100644 index 000000000..bad06ae91 --- /dev/null +++ b/doc/source/netscript/stanekapi/clear.rst @@ -0,0 +1,13 @@ +clear() Netscript Function +======================================= + +.. js:function:: clear() + + :RAM cost: 0 GB + + Completely clear Stanek's Gift. + + Example: + + .. code-block:: javascript + clear(); // No more fragments. \ No newline at end of file diff --git a/doc/source/netscript/stanekapi/deleteAt.rst b/doc/source/netscript/stanekapi/deleteAt.rst new file mode 100644 index 000000000..cd38817a4 --- /dev/null +++ b/doc/source/netscript/stanekapi/deleteAt.rst @@ -0,0 +1,16 @@ +deleteAt() Netscript Function +======================================= + +.. js:function:: deleteAt(worldX, worldY) + + :RAM cost: 0.15 GB + :param int worldX: World X coordinate of the fragment to delete. + :param int worldY: World Y coordinate of the fragment to delete. + :returns: `true` if the fragment was deleted. `false` otherwise. + + Delete the fragment located at `[worldX, worldY]`. + + Example: + + .. code-block:: javascript + deleteAt(0, 4); // returns true diff --git a/doc/source/netscript/stanekapi/fragmentAt.rst b/doc/source/netscript/stanekapi/fragmentAt.rst new file mode 100644 index 000000000..44ccbecb8 --- /dev/null +++ b/doc/source/netscript/stanekapi/fragmentAt.rst @@ -0,0 +1,28 @@ +fragmentAt() Netscript Function +======================================= + +.. js:function:: fragmentAt(worldX, worldY) + + :RAM cost: 2 GB + :param int worldX: World X coordinate of the fragment. + :param int worldY: World Y coordinate of the fragment. + :returns: The fragment located at `[worldX, worldY]` in Stanek's Gift, or null. + + .. code-block:: typescript + { + // In world coordinates + x: number; + y: number; + heat: number; + charge: number; + id: number; + shape: boolean[][]; + type: string; + magnitude: number; + limit: number; + } + Example: + + .. code-block:: javascript + var fragment = fragmentAt(0, 4); + print(fragment); // {'heat': 50, 'charge': 98} diff --git a/doc/source/netscript/stanekapi/fragmentDefinitions.rst b/doc/source/netscript/stanekapi/fragmentDefinitions.rst new file mode 100644 index 000000000..18ab41c66 --- /dev/null +++ b/doc/source/netscript/stanekapi/fragmentDefinitions.rst @@ -0,0 +1,23 @@ +fragmentDefinitions() Netscript Function +======================================= + +.. js:function:: fragmentDefinitions() + + :RAM cost: 0 GB + :returns: The list of all fragment that can be embedded in Stanek's Gift. + + .. code-block:: typescript + [ + { + id: number; + shape: boolean[][]; + type: string; + magnitude: number; + limit: number; + } + ] + Example: + + .. code-block:: javascript + var fragments = fragmentDefinitions(); + print(fragment); // prints all possible fragments diff --git a/doc/source/netscript/stanekapi/place.rst b/doc/source/netscript/stanekapi/place.rst new file mode 100644 index 000000000..701720cbc --- /dev/null +++ b/doc/source/netscript/stanekapi/place.rst @@ -0,0 +1,15 @@ +place() Netscript Function +======================================= + +.. js:function:: place(worldX, worldY, fragmentId) + + :RAM cost: 5 GB + :param int worldX: World X against which to align the top left of the fragment. + :param int worldY: World Y against which to align the top left of the fragment. + :param int fragmentId: ID of the fragment to place. + :returns: `true` if the fragment has been placed at that position. `false` otherwise. + + Example: + + .. code-block:: javascript + place(0, 4, 17); // returns true \ No newline at end of file diff --git a/doc/source/netscript/stanekapi/placedFragments.rst b/doc/source/netscript/stanekapi/placedFragments.rst new file mode 100644 index 000000000..80c2f950e --- /dev/null +++ b/doc/source/netscript/stanekapi/placedFragments.rst @@ -0,0 +1,27 @@ +placedFragments() Netscript Function +======================================= + +.. js:function:: placedFragments() + + :RAM cost: 5 GB + :returns: The list of all fragment that are embedded in Stanek's Gift. + + .. code-block:: typescript + [ + { + // In world coordinates + x: number; + y: number; + heat: number; + charge: number; + id: number; + shape: boolean[][]; + type: string; + magnitude: number; + limit: number; + } + ] + Example: + + .. code-block:: javascript + var myFragments = placedFragments(); diff --git a/src/Augmentation/AugmentationHelpers.tsx b/src/Augmentation/AugmentationHelpers.tsx index 37fd019c7..24c5a4474 100644 --- a/src/Augmentation/AugmentationHelpers.tsx +++ b/src/Augmentation/AugmentationHelpers.tsx @@ -2344,6 +2344,142 @@ function initAugmentations(): void { resetAugmentation(BladesSimulacrum); } + // Special CotMG Augmentations + const ChurchOfTheMachineGodFactionName = "Church of the Machine God"; + if (factionExists(ChurchOfTheMachineGodFactionName)) { + const StaneksGift1 = new Augmentation({ + name: AugmentationNames.StaneksGift1, + repCost: 0, + moneyCost: 0, + info: + 'Allison "Mother" Stanek imparts you with her gift. An ' + + "experimental Augmentation implanted at the base of the neck. " + + "It allows you to overclock your entire system by carefully " + + "changing the configuration.

" + + "Its unstable nature decreases all your stats by 10%", + isSpecial: true, + hacking_chance_mult: 0.9, + hacking_speed_mult: 0.9, + hacking_money_mult: 0.9, + hacking_grow_mult: 0.9, + hacking_mult: 0.9, + strength_mult: 0.9, + defense_mult: 0.9, + dexterity_mult: 0.9, + agility_mult: 0.9, + charisma_mult: 0.9, + hacking_exp_mult: 0.9, + strength_exp_mult: 0.9, + defense_exp_mult: 0.9, + dexterity_exp_mult: 0.9, + agility_exp_mult: 0.9, + charisma_exp_mult: 0.9, + company_rep_mult: 0.9, + faction_rep_mult: 0.9, + crime_money_mult: 0.9, + crime_success_mult: 0.9, + hacknet_node_money_mult: 0.9, + hacknet_node_purchase_cost_mult: 1.1, + hacknet_node_ram_cost_mult: 1.1, + hacknet_node_core_cost_mult: 1.1, + hacknet_node_level_cost_mult: 1.1, + work_money_mult: 0.9, + }); + StaneksGift1.addToFactions([ChurchOfTheMachineGodFactionName]); + resetAugmentation(StaneksGift1); + + const StaneksGift2 = new Augmentation({ + name: AugmentationNames.StaneksGift2, + repCost: 1000, + moneyCost: 0, + info: + 'TODO, something about Mother being bullshit and you get more control over her "gift"

' + + "The penalty for the gift is only 5%", + prereqs: [AugmentationNames.StaneksGift1], + isSpecial: true, + hacking_chance_mult: 0.95 / 0.9, + hacking_speed_mult: 0.95 / 0.9, + hacking_money_mult: 0.95 / 0.9, + hacking_grow_mult: 0.95 / 0.9, + hacking_mult: 0.95 / 0.9, + strength_mult: 0.95 / 0.9, + defense_mult: 0.95 / 0.9, + dexterity_mult: 0.95 / 0.9, + agility_mult: 0.95 / 0.9, + charisma_mult: 0.95 / 0.9, + hacking_exp_mult: 0.95 / 0.9, + strength_exp_mult: 0.95 / 0.9, + defense_exp_mult: 0.95 / 0.9, + dexterity_exp_mult: 0.95 / 0.9, + agility_exp_mult: 0.95 / 0.9, + charisma_exp_mult: 0.95 / 0.9, + company_rep_mult: 0.95 / 0.9, + faction_rep_mult: 0.95 / 0.9, + crime_money_mult: 0.95 / 0.9, + crime_success_mult: 0.95 / 0.9, + hacknet_node_money_mult: 0.95 / 0.9, + hacknet_node_purchase_cost_mult: 1.05 / 1.1, + hacknet_node_ram_cost_mult: 1.05 / 1.1, + hacknet_node_core_cost_mult: 1.05 / 1.1, + hacknet_node_level_cost_mult: 1.05 / 1.1, + work_money_mult: 0.95 / 0.9, + }); + StaneksGift2.addToFactions([ChurchOfTheMachineGodFactionName]); + resetAugmentation(StaneksGift2); + + const StaneksGift3 = new Augmentation({ + name: AugmentationNames.StaneksGift3, + repCost: 10000, + moneyCost: 0, + info: + "TODO, learn more about Allisons scheme, gain full control over the gift.

" + + "Finally freed from the penalty of the gift.", + prereqs: [AugmentationNames.StaneksGift2], + isSpecial: true, + hacking_chance_mult: 1 / 0.95, + hacking_speed_mult: 1 / 0.95, + hacking_money_mult: 1 / 0.95, + hacking_grow_mult: 1 / 0.95, + hacking_mult: 1 / 0.95, + strength_mult: 1 / 0.95, + defense_mult: 1 / 0.95, + dexterity_mult: 1 / 0.95, + agility_mult: 1 / 0.95, + charisma_mult: 1 / 0.95, + hacking_exp_mult: 1 / 0.95, + strength_exp_mult: 1 / 0.95, + defense_exp_mult: 1 / 0.95, + dexterity_exp_mult: 1 / 0.95, + agility_exp_mult: 1 / 0.95, + charisma_exp_mult: 1 / 0.95, + company_rep_mult: 1 / 0.95, + faction_rep_mult: 1 / 0.95, + crime_money_mult: 1 / 0.95, + crime_success_mult: 1 / 0.95, + hacknet_node_money_mult: 1 / 0.95, + hacknet_node_purchase_cost_mult: 1 / 1.05, + hacknet_node_ram_cost_mult: 1 / 1.05, + hacknet_node_core_cost_mult: 1 / 1.05, + hacknet_node_level_cost_mult: 1 / 1.05, + work_money_mult: 1 / 0.95, + }); + StaneksGift3.addToFactions([ChurchOfTheMachineGodFactionName]); + resetAugmentation(StaneksGift3); + + const StaneksGiftAscension4 = new Augmentation({ + name: AugmentationNames.StaneksGift4, + repCost: 500000000, + moneyCost: 0, + info: + "Allow Allison to install an Ascension port in her Gift. Allowing you to connect with the Machine God.

" + + "(hydro notes: Finishes the BN, eventually)", + prereqs: [AugmentationNames.StaneksGift3], + isSpecial: true, + }); + StaneksGiftAscension4.addToFactions([ChurchOfTheMachineGodFactionName]); + resetAugmentation(StaneksGiftAscension4); + } + // Update costs based on how many have been purchased mult = Math.pow( CONSTANTS.MultipleAugMultiplier * [1, 0.96, 0.94, 0.93][SourceFileFlags[11]], diff --git a/src/Augmentation/data/AugmentationNames.ts b/src/Augmentation/data/AugmentationNames.ts index ff1f19c6d..e05d99f9f 100644 --- a/src/Augmentation/data/AugmentationNames.ts +++ b/src/Augmentation/data/AugmentationNames.ts @@ -110,6 +110,11 @@ export const AugmentationNames: IMap = { BladeArmorIPU: "BLADE-51b Tesla Armor: IPU Upgrade", BladesSimulacrum: "The Blade's Simulacrum", + StaneksGift1: "Stanek's Gift - Genesis", + StaneksGift2: "Stanek's Gift - Awakening", + StaneksGift3: "Stanek's Gift - Serenity", + StaneksGift4: "Stanek's Gift - Ascension", + //Wasteland Augs //PepBoy: "P.E.P-Boy", Plasma Energy Projection System //PepBoyForceField Generates plasma force fields diff --git a/src/BitNode/BitNode.tsx b/src/BitNode/BitNode.tsx index 2d3012557..0400452ad 100644 --- a/src/BitNode/BitNode.tsx +++ b/src/BitNode/BitNode.tsx @@ -521,8 +521,48 @@ BitNodes["BitNode12"] = new BitNode( ), ); +BitNodes["BitNode13"] = new BitNode( + 13, + 2, + "They're lunatics", + "1 step back, 2 steps forward", + ( + <> + With the invention of Augmentations in the 2040s a religious group known as the Church of the Machine God has + rallied far more support than anyone would have hoped. +
+
+ Their leader, Allison "Mother" Stanek is said to have created her own Augmentation whose power goes beyond any + other.' + Find her in Chongquin and gain her trust. +
+
+ In this BitNode: +
+
+ Every is significantly reduced +
+ Stanek's Gift power is significantly increased. +
+
+ Destroying this BitNode will give you Source-File 13, or if you already have this Source-File it will upgrade its + level up to a maximum of 3. This Source-File lets the Church of the Machine God appear in other BitNodes. +
+
+ This Source-File also increases Stanek's Gift multipliers by: +
+
+ Level 1: 8% +
+ Level 2: 12% +
+ Level 3: 14% +
+
+ Each level of this Source-File also increases the size of the gift. + + ), +); // Books: Frontera, Shiner -BitNodes["BitNode13"] = new BitNode(13, 2, "fOS", "COMING SOON"); //Unlocks the new game mode and the rest of the BitNodes BitNodes["BitNode14"] = new BitNode(14, 2, "", "COMING SOON"); BitNodes["BitNode15"] = new BitNode(15, 2, "", "COMING SOON"); BitNodes["BitNode16"] = new BitNode(16, 2, "", "COMING SOON"); @@ -784,6 +824,45 @@ export function initBitNodeMultipliers(p: IPlayer): void { BitNodeMultipliers.BladeburnerSkillCost = inc; break; } + case 13: { + BitNodeMultipliers.DaedalusAugsRequirement = 100; + + BitNodeMultipliers.HackingLevelMultiplier = 0.2; + BitNodeMultipliers.StrengthLevelMultiplier = 0.2; + BitNodeMultipliers.DefenseLevelMultiplier = 0.2; + BitNodeMultipliers.DexterityLevelMultiplier = 0.2; + BitNodeMultipliers.AgilityLevelMultiplier = 0.2; + BitNodeMultipliers.CharismaLevelMultiplier = 0.2; + + BitNodeMultipliers.ServerMaxMoney = 0.15; + BitNodeMultipliers.ServerStartingMoney = 0.75; + + BitNodeMultipliers.ServerStartingSecurity = 2; + + BitNodeMultipliers.ScriptHackMoney = 0.2; + BitNodeMultipliers.CompanyWorkMoney = 0.2; + BitNodeMultipliers.CrimeMoney = 0.2; + BitNodeMultipliers.HacknetNodeMoney = 0.2; + BitNodeMultipliers.CodingContractMoney = 0.2; + + BitNodeMultipliers.CompanyWorkExpGain = 0.1; + BitNodeMultipliers.ClassGymExpGain = 0.1; + BitNodeMultipliers.FactionWorkExpGain = 0.1; + BitNodeMultipliers.HackExpGain = 0.1; + BitNodeMultipliers.CrimeExpGain = 0.1; + + BitNodeMultipliers.FactionWorkRepGain = 0.4; + + BitNodeMultipliers.FourSigmaMarketDataCost = 10; + BitNodeMultipliers.FourSigmaMarketDataApiCost = 10; + + BitNodeMultipliers.CorporationValuation = 0.001; + + BitNodeMultipliers.BladeburnerRank = 0.1; + BitNodeMultipliers.BladeburnerSkillCost = 5; + BitNodeMultipliers.GangKarmaRequirement = 20; + break; + } default: console.warn("Player.bitNodeN invalid"); break; diff --git a/src/BitNode/ui/BitverseRoot.tsx b/src/BitNode/ui/BitverseRoot.tsx index c9152ae33..0440d9568 100644 --- a/src/BitNode/ui/BitverseRoot.tsx +++ b/src/BitNode/ui/BitverseRoot.tsx @@ -126,7 +126,7 @@ export function BitverseRoot(props: IProps): React.ReactElement {
O | | |  \|  |  O  /   _/ |    /    O  |  |/  | | | O
| | | |O  /  |  | O   /   |   O   O |  |  \  O| | | |
| | |/  \/  / __| | |/ \  |   \   | |__ \  \/  \| | |
-
 \| O   |  |_/    |\|   \ O    \__|    \_|  |   O |/ 
+
 \| O   |  |_/    |\|   \     \__|    \_|  |   O |/ 
  | |   |_/       | |    \|    /  |       \_|   | |  
   \|   /          \|     |   /  /          \   |/   
    |              |     |  /  |              |    
diff --git a/src/CotMG/ActiveFragment.ts b/src/CotMG/ActiveFragment.ts new file mode 100644 index 000000000..41467c1b2 --- /dev/null +++ b/src/CotMG/ActiveFragment.ts @@ -0,0 +1,94 @@ +import { Fragment, FragmentById } from "./Fragment"; +import { FragmentType } from "./FragmentType"; +import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver"; + +const noCharge = [FragmentType.None, FragmentType.Delete, FragmentType.Booster, FragmentType.Cooling]; + +export interface IActiveFragmentParams { + x: number; + y: number; + fragment: Fragment; +} + +export class ActiveFragment { + id: number; + charge: number; + heat: number; + x: number; + y: number; + + constructor(params?: IActiveFragmentParams) { + if (params) { + this.id = params.fragment.id; + this.x = params.x; + this.y = params.y; + this.charge = 1; + if (noCharge.includes(params.fragment.type)) this.charge = 0; + this.heat = 1; + } else { + this.id = -1; + this.x = -1; + this.y = -1; + this.charge = -1; + this.heat = -1; + } + } + + collide(other: ActiveFragment): boolean { + const thisFragment = this.fragment(); + const otherFragment = other.fragment(); + // These 2 variables converts 'this' local coordinates to world to other local. + const dx: number = other.x - this.x; + const dy: number = other.y - this.y; + for (let j = 0; j < thisFragment.shape.length; j++) { + for (let i = 0; i < thisFragment.shape[j].length; i++) { + if (thisFragment.fullAt(i, j) && otherFragment.fullAt(i - dx, j - dy)) return true; + } + } + + return false; + } + + fragment(): Fragment { + const fragment = FragmentById(this.id); + if (fragment === null) throw "ActiveFragment id refers to unknown Fragment."; + return fragment; + } + + fullAt(worldX: number, worldY: number): boolean { + return this.fragment().fullAt(worldX - this.x, worldY - this.y); + } + + neighboors(): number[][] { + return this.fragment() + .neighboors() + .map((cell) => [this.x + cell[0], this.y + cell[1]]); + } + + copy(): ActiveFragment { + // We have to do a round trip because the constructor. + const fragment = FragmentById(this.id); + if (fragment === null) throw "ActiveFragment id refers to unknown Fragment."; + const c = new ActiveFragment({ x: this.x, y: this.y, fragment: fragment }); + c.charge = this.charge; + c.heat = this.heat; + return c; + } + + /** + * Serialize an active fragment to a JSON save state. + */ + toJSON(): any { + return Generic_toJSON("ActiveFragment", this); + } + + /** + * Initializes an acive fragment from a JSON save state + */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + static fromJSON(value: any): ActiveFragment { + return Generic_fromJSON(ActiveFragment, value.data); + } +} + +Reviver.constructors.ActiveFragment = ActiveFragment; diff --git a/src/CotMG/Fragment.ts b/src/CotMG/Fragment.ts new file mode 100644 index 000000000..9ae0b951c --- /dev/null +++ b/src/CotMG/Fragment.ts @@ -0,0 +1,290 @@ +import { FragmentType } from "./FragmentType"; + +export const Fragments: Fragment[] = []; + +export class Fragment { + id: number; + shape: boolean[][]; + type: FragmentType; + power: number; + limit: number; + + constructor(id: number, shape: boolean[][], type: FragmentType, power: number, limit: number) { + this.id = id; + this.shape = shape; + this.type = type; + this.power = power; + this.limit = limit; + } + + fullAt(x: number, y: number): boolean { + if(y < 0) return false; + if(y >= this.shape.length) return false; + if(x < 0) return false; + if(x >= this.shape[y].length) return false; + // Yes it's ordered y first. + return this.shape[y][x]; + } + + width() { + // check every line for robustness. + return Math.max(...this.shape.map(line => line.length)); + } + + height() { + return this.shape.length; + } + + // List of direct neighboors of this fragment. + neighboors(): number[][] { + let candidates: number[][] = []; + + const add = (x: number, y: number): void => { + if(this.fullAt(x, y)) return; + if(candidates.some(coord => coord[0] === x && coord[1] === y)) return; + candidates.push([x, y]); + }; + for(let y = 0; y < this.shape.length; y++) { + for(let x = 0; x < this.shape[y].length; x++) { + // This cell is full, add all it's neighboors. + if(!this.shape[y][x]) continue; + add(x-1, y); + add(x+1, y); + add(x, y-1); + add(x, y+1); + } + } + const cells: number[][] = []; + for(const candidate of candidates) { + if(cells.some(cell => cell[0] === candidate[0] && cell[1] === candidate[1])) continue; + cells.push(candidate); + } + + return cells; + } + + copy(): Fragment { + return new Fragment(this.id, this.shape.map(a => a.slice()), this.type, this.power, this.limit); + } +} + +export function FragmentById(id: number): Fragment | null { + for(const fragment of Fragments) { + if(fragment.id === id) return fragment; + } + return null; +} + + +(function() { + const _ = false; + const X = true; + Fragments.push(new Fragment( + 0, // id + [ // shape + [X,X,X], + [_,_,X], + [_,_,X], + ], + FragmentType.Hacking, // type + 10, + 1, // limit + )); + Fragments.push(new Fragment( + 1, // id + [ // shape + [_,X,_], + [X,X,X], + [_,X,_], + ], + FragmentType.Hacking, // type + 10, + 1, // limit + )); + Fragments.push(new Fragment( + 2, // id + [ // shape + [X,X,X], + [X,_,X], + [X,X,X], + ], + FragmentType.Booster, // type + 500, + 3, // limit + )); + Fragments.push(new Fragment( + 3, // id + [ // shape + [X,X], + [X,X], + ], + FragmentType.Cooling, // type + 200, + Infinity, // limit + )); + Fragments.push(new Fragment( + 4, // id + [ // shape + [X], + ], + FragmentType.Cooling, // type + 50, + 1, // limit + )); + Fragments.push(new Fragment( + 5, // id + [ // shape + [X, X], + ], + FragmentType.HackingSpeed, // type + 50, + 1, // limit + )); + + Fragments.push(new Fragment( + 6, // id + [ + [X, _], + [X, X], + ], // shape + FragmentType.HackingMoney, // type + 10, // power + 1, // limit + )); + Fragments.push(new Fragment( + 7, // id + [ + [X, X], + [X, X], + ], // shape + FragmentType.HackingGrow, // type + 30, // power + 1, // limit + )); + Fragments.push(new Fragment( + 8, // id + [ + [X, X, X], + [_, X, _], + [X, X, X], + ], // shape + FragmentType.Hacking, // type + 50, // power + 1, // limit + )); + Fragments.push(new Fragment( + 10, // id + [ + [X, X], + [_, X], + ], // shape + FragmentType.Strength, // type + 50, // power + 1, // limit + )); + Fragments.push(new Fragment( + 12, // id + [ + [_, X], + [X, X], + ], // shape + FragmentType.Defense, // type + 50, // power + 1, // limit + )); + Fragments.push(new Fragment( + 14, // id + [ + [X, X], + [X, _], + ], // shape + FragmentType.Dexterity, // type + 50, // power + 1, // limit + )); + Fragments.push(new Fragment( + 16, // id + [ + [X, _], + [X, X], + ], // shape + FragmentType.Agility, // type + 50, // power + 1, // limit + )); + Fragments.push(new Fragment( + 18, // id + [ + [X, X], + [X, _], + ], // shape + FragmentType.Charisma, // type + 50, // power + 1, // limit + )); + Fragments.push(new Fragment( + 20, // id + [ + [X, _, _], + [X, X, _], + [X, X, X], + ], // shape + FragmentType.HacknetMoney, // type + 30, // power + 1, // limit + )); + Fragments.push(new Fragment( + 21, // id + [ + [X, X], + [_, X], + [_, X], + ], // shape + FragmentType.HacknetCost, // type + -10, // power + 1, // limit + )); + Fragments.push(new Fragment( + 25, // id + [ + [X, X, X], + [_, X, _], + ], // shape + FragmentType.Rep, // type + 100, // power + 1, // limit + )); + Fragments.push(new Fragment( + 27, // id + [ + [X, _], + [_, X], + ], // shape + FragmentType.WorkMoney, // type + 20, // power + 1, // limit + )); + Fragments.push(new Fragment( + 28, // id + [ + [X, X], + ], // shape + FragmentType.Crime, // type + 20, // power + 1, // limit + )); + Fragments.push(new Fragment( + 30, // id + [ + [X, X, X], + [X, X, X], + [X, X, X], + ], // shape + FragmentType.Bladeburner, // type + 50, // power + 1, // limit + )); +})(); + +export const NoneFragment = new Fragment(-2, [], FragmentType.None, 0, Infinity); +export const DeleteFragment = new Fragment(-2, [], FragmentType.Delete, 0, Infinity); diff --git a/src/CotMG/FragmentType.ts b/src/CotMG/FragmentType.ts new file mode 100644 index 000000000..cd7c7d921 --- /dev/null +++ b/src/CotMG/FragmentType.ts @@ -0,0 +1,27 @@ +export enum FragmentType { + // Special fragments for the UI + None, + Delete, + + // Stats boosting fragments + HackingChance, + HackingSpeed, + HackingMoney, + HackingGrow, + Hacking, + Strength, + Defense, + Dexterity, + Agility, + Charisma, + HacknetMoney, + HacknetCost, + Rep, + WorkMoney, + Crime, + Bladeburner, + + // utility fragments. + Booster, + Cooling, +} \ No newline at end of file diff --git a/src/CotMG/Helper.tsx b/src/CotMG/Helper.tsx new file mode 100644 index 000000000..c8f07c388 --- /dev/null +++ b/src/CotMG/Helper.tsx @@ -0,0 +1,14 @@ +import { Reviver } from "../utils/JSONReviver"; + +import { IStaneksGift } from "./IStaneksGift"; +import { StaneksGift } from "./StaneksGift"; + +export let staneksGift: IStaneksGift = new StaneksGift(); + +export function loadStaneksGift(saveString: string): void { + if (saveString) { + staneksGift = JSON.parse(saveString, Reviver); + } else { + staneksGift = new StaneksGift(); + } +} diff --git a/src/CotMG/IStaneksGift.ts b/src/CotMG/IStaneksGift.ts new file mode 100644 index 000000000..7efb4a5b5 --- /dev/null +++ b/src/CotMG/IStaneksGift.ts @@ -0,0 +1,18 @@ +import { StaneksGift } from "./StaneksGift"; +import { ActiveFragment } from "./ActiveFragment"; +import { Fragment } from "./Fragment"; +import { IPlayer } from "../PersonObjects/IPlayer"; + +export interface IStaneksGift { + fragments: ActiveFragment[]; + width(): number; + height(): number; + charge(worldX: number, worldY: number, ram: number): number; + process(p: IPlayer, n: number): void; + canPlace(x: number, y: number, fragment: Fragment): boolean; + place(x: number, y: number, fragment: Fragment): boolean; + fragmentAt(worldX: number, worldY: number): ActiveFragment | null; + deleteAt(worldX: number, worldY: number): boolean; + clear(): void; + count(fragment: Fragment): number; +}; \ No newline at end of file diff --git a/src/CotMG/StaneksGift.ts b/src/CotMG/StaneksGift.ts new file mode 100644 index 000000000..13d859ea4 --- /dev/null +++ b/src/CotMG/StaneksGift.ts @@ -0,0 +1,209 @@ +import { Fragment, FragmentById } from "./Fragment"; +import { ActiveFragment } from "./ActiveFragment"; +import { FragmentType } from "./FragmentType"; +import { IStaneksGift } from "./IStaneksGift"; +import { IPlayer } from "../PersonObjects/IPlayer"; +import { Factions } from "../Faction/Factions"; +import { CalculateEffect } from "./formulas/effect"; +import { CalculateCharge } from "./formulas/charge"; +import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver"; + +export class StaneksGift implements IStaneksGift { + fragments: ActiveFragment[] = []; + + width(): number { + return 7; + } + height(): number { + return 6; + } + + charge(worldX: number, worldY: number, ram: number): number { + const af = this.fragmentAt(worldX, worldY); + if (af === null) return 0; + + // Find all the neighbooring cells + const cells = af.neighboors(); + + // find the neighbooring active fragments. + const maybeFragments = cells.map((n) => this.fragmentAt(n[0], n[1])); + + // Filter out nulls with typescript "Type guard". Whatever + let neighboors = maybeFragments.filter((v: ActiveFragment | null): v is ActiveFragment => !!v); + + // filter unique fragments + neighboors = neighboors.filter((value, index) => neighboors.indexOf(value) === index); + + // count number of neighbooring boosts and cooling. + let boost = 1; + let cool = 1; + for (const neighboor of neighboors) { + const f = neighboor.fragment(); + if (f.type === FragmentType.Cooling) cool *= 1 + f.power / 1000; + if (f.type === FragmentType.Booster) boost *= 1 + f.power / 1000; + } + + const [extraCharge, extraHeat] = CalculateCharge(ram, af.heat, boost, cool); + af.charge += extraCharge; + af.heat += extraHeat; + + Factions["Church of the Machine God"].playerReputation += extraCharge; + + return ram; + } + + process(p: IPlayer, numCycles: number): void { + for (const activeFragment of this.fragments) { + const fragment = activeFragment.fragment(); + + // Boosters and cooling don't deal with heat. + if (fragment.type === FragmentType.Booster || fragment.type === FragmentType.Cooling) continue; + activeFragment.heat *= 0.98; + activeFragment.heat -= 1; + if (activeFragment.heat < 1) activeFragment.heat = 1; + } + + this.updateMults(p); + } + + canPlace(x: number, y: number, fragment: Fragment): boolean { + if (x + fragment.width() > this.width()) return false; + if (y + fragment.height() > this.height()) return false; + if (this.count(fragment) >= fragment.limit) return false; + const newFrag = new ActiveFragment({ x: x, y: y, fragment: fragment }); + for (const aFrag of this.fragments) { + if (aFrag.collide(newFrag)) return false; + } + return true; + } + + place(x: number, y: number, fragment: Fragment): boolean { + if (!this.canPlace(x, y, fragment)) return false; + this.fragments.push(new ActiveFragment({ x: x, y: y, fragment: fragment })); + return true; + } + + fragmentAt(worldX: number, worldY: number): ActiveFragment | null { + for (const aFrag of this.fragments) { + if (aFrag.fullAt(worldX, worldY)) { + return aFrag; + } + } + + return null; + } + + count(fragment: Fragment): number { + let amt = 0; + for (const aFrag of this.fragments) { + if (aFrag.fragment().id === fragment.id) amt++; + } + return amt; + } + + deleteAt(worldX: number, worldY: number): boolean { + for (let i = 0; i < this.fragments.length; i++) { + if (this.fragments[i].fullAt(worldX, worldY)) { + this.fragments.splice(i, 1); + return true; + } + } + + return false; + } + + clear(): void { + this.fragments = []; + } + + updateMults(p: IPlayer): void { + p.reapplyAllAugmentations(true); + p.reapplyAllSourceFiles(); + + for (const aFrag of this.fragments) { + const fragment = aFrag.fragment(); + const power = CalculateEffect(aFrag.charge, fragment.power); + switch (fragment.type) { + case FragmentType.HackingChance: + p.hacking_chance_mult *= power; + break; + case FragmentType.HackingSpeed: + p.hacking_speed_mult *= power; + break; + case FragmentType.HackingMoney: + p.hacking_money_mult *= power; + break; + case FragmentType.HackingGrow: + p.hacking_grow_mult *= power; + break; + case FragmentType.Hacking: + p.hacking_mult *= power; + p.hacking_exp_mult *= power; + break; + case FragmentType.Strength: + p.strength_mult *= power; + p.strength_exp_mult *= power; + break; + case FragmentType.Defense: + p.defense_mult *= power; + p.defense_exp_mult *= power; + break; + case FragmentType.Dexterity: + p.dexterity_mult *= power; + p.dexterity_exp_mult *= power; + break; + case FragmentType.Agility: + p.agility_mult *= power; + p.agility_exp_mult *= power; + break; + case FragmentType.Charisma: + p.charisma_mult *= power; + p.charisma_exp_mult *= power; + break; + case FragmentType.HacknetMoney: + p.hacknet_node_money_mult *= power; + break; + case FragmentType.HacknetCost: + p.hacknet_node_purchase_cost_mult *= power; + p.hacknet_node_ram_cost_mult *= power; + p.hacknet_node_core_cost_mult *= power; + p.hacknet_node_level_cost_mult *= power; + break; + case FragmentType.Rep: + p.company_rep_mult *= power; + p.faction_rep_mult *= power; + break; + case FragmentType.WorkMoney: + p.work_money_mult *= power; + break; + case FragmentType.Crime: + p.crime_success_mult *= power; + p.crime_money_mult *= power; + break; + case FragmentType.Bladeburner: + p.bladeburner_max_stamina_mult *= power; + p.bladeburner_stamina_gain_mult *= power; + p.bladeburner_analysis_mult *= power; + p.bladeburner_success_chance_mult *= power; + break; + } + } + } + + /** + * Serialize Staneks Gift to a JSON save state. + */ + toJSON(): any { + return Generic_toJSON("StaneksGift", this); + } + + /** + * Initializes Staneks Gift from a JSON save state + */ + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + static fromJSON(value: any): StaneksGift { + return Generic_fromJSON(StaneksGift, value.data); + } +} + +Reviver.constructors.StaneksGift = StaneksGift; diff --git a/src/CotMG/formulas/charge.ts b/src/CotMG/formulas/charge.ts new file mode 100644 index 000000000..4364303dc --- /dev/null +++ b/src/CotMG/formulas/charge.ts @@ -0,0 +1,6 @@ +export function CalculateCharge(ram: number, currentHeat: number, boost: number, cool: number): number[] { + const heatPenalty = Math.log(1+currentHeat)/Math.log(2); + const extraCharge = ram*Math.pow(boost, 2)/(heatPenalty*cool); + const extraHeat = ram; + return [extraCharge, extraHeat]; +} \ No newline at end of file diff --git a/src/CotMG/formulas/effect.ts b/src/CotMG/formulas/effect.ts new file mode 100644 index 000000000..ec09e083f --- /dev/null +++ b/src/CotMG/formulas/effect.ts @@ -0,0 +1,3 @@ +export function CalculateEffect(charge: number, power: number): number { + return Math.pow((power/1000)+1, Math.log(charge+1)/Math.log(8)) +} \ No newline at end of file diff --git a/src/CotMG/ui/Cell.tsx b/src/CotMG/ui/Cell.tsx new file mode 100644 index 000000000..1a1df7890 --- /dev/null +++ b/src/CotMG/ui/Cell.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; + +type IProps = { + onMouseEnter?: () => void; + onClick?: () => void; + color: string; +}; + +export function Cell(cellProps: IProps) { + return ( +
+ ); +} diff --git a/src/CotMG/ui/FragmentInspector.tsx b/src/CotMG/ui/FragmentInspector.tsx new file mode 100644 index 000000000..770bb086f --- /dev/null +++ b/src/CotMG/ui/FragmentInspector.tsx @@ -0,0 +1,57 @@ +import React, {useState, useEffect} from "react"; +import { ActiveFragment } from "../ActiveFragment"; +import { FragmentType } from "../FragmentType"; +import { numeralWrapper } from "../../ui/numeralFormat"; +import { MuiPaper } from "../../ui/React/MuiPaper"; +import { CalculateEffect } from "../formulas/effect"; + +type IProps = { + fragment: ActiveFragment | null; +} + +export function FragmentInspector(props: IProps) { + const [, setC] = useState(new Date()) + + useEffect(() => { + const id = setInterval(() => setC(new Date()), 250); + + return () => clearInterval(id); + }, []); + + if(props.fragment === null) { + return ( +
+                ID:        N/A
+ Type: N/A
+ Magnitude: N/A
+ Charge: N/A
+ Heat: N/A
+ Effect: N/A
+ [X, Y] N/A
+
+
) + } + const f = props.fragment.fragment(); + + let charge = numeralWrapper.formatStaneksGiftCharge(props.fragment.charge); + let heat = numeralWrapper.formatStaneksGiftHeat(props.fragment.heat); + // Boosters and cooling don't deal with heat. + if(f.type === FragmentType.Booster || + f.type === FragmentType.Cooling) { + charge = "N/A"; + heat = "N/A"; + } + const effect = numeralWrapper.format((CalculateEffect(props.fragment.charge, f.power)-1), "+0.00%"); + + return ( +
+            ID:     {props.fragment.id}
+ Type: {FragmentType[f.type]}
+ Power: {numeralWrapper.formatStaneksGiftPower(f.power)}
+ Charge: {charge}
+ Heat: {heat}
+ Effect: {effect}
+ [X, Y] {props.fragment.x}, {props.fragment.y}
+
+
) +} \ No newline at end of file diff --git a/src/CotMG/ui/FragmentSelector.tsx b/src/CotMG/ui/FragmentSelector.tsx new file mode 100644 index 000000000..4d36f99a1 --- /dev/null +++ b/src/CotMG/ui/FragmentSelector.tsx @@ -0,0 +1,67 @@ +import * as React from "react"; +import { + Fragments, + Fragment, + NoneFragment, + DeleteFragment, +} from "../Fragment"; +import { FragmentType } from "../FragmentType"; +import { IStaneksGift } from "../IStaneksGift"; +import { G } from "./G"; +import { numeralWrapper } from "../../ui/numeralFormat"; + +import List from '@material-ui/core/List'; +import ListItem, { ListItemProps } from '@material-ui/core/ListItem'; +import ListItemText from '@material-ui/core/ListItemText'; +import Divider from '@material-ui/core/Divider'; + + +type IOptionProps = { + gift: IStaneksGift; + fragment: Fragment; + selectFragment: (fragment: Fragment) => void; +} + +function FragmentOption(props: IOptionProps) { + const remaining = props.fragment.limit !== Infinity ? (<>{props.fragment.limit - props.gift.count(props.fragment)} remaining) : (<>); + return (<> + props.selectFragment(props.fragment)} style={{backgroundColor: '#222'}}> +

+ {FragmentType[props.fragment.type]}
+ power: {numeralWrapper.formatStaneksGiftPower(props.fragment.power)}
+ {remaining} +


+ props.fragment.fullAt(x, y) ? "green" : ""} /> +
+ + ) +} + +type IProps = { + gift: IStaneksGift; + selectFragment: (fragment: Fragment) => void; +} + +export function FragmentSelector(props: IProps) { + return ( + props.selectFragment(NoneFragment)} style={{backgroundColor: '#222'}}> +

+ None +

+
+ props.selectFragment(DeleteFragment)} style={{backgroundColor: '#222'}}> +

+ Delete +

+
+ {Fragments.map(fragment => )} +
) +} + diff --git a/src/CotMG/ui/G.tsx b/src/CotMG/ui/G.tsx new file mode 100644 index 000000000..0683719fd --- /dev/null +++ b/src/CotMG/ui/G.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import { Cell } from "./Cell"; + +type IProps = { + width: number; + height: number; + colorAt: (x: number, y: number) => string; +}; + +export function G(props: IProps) { + // switch the width/length to make axis consistent. + const elems = []; + for (let j = 0; j < props.height; j++) { + const cells = []; + for (let i = 0; i < props.width; i++) { + cells.push(); + } + elems.push( +
+ {cells} +
, + ); + } + + return
{elems}
; +} diff --git a/src/CotMG/ui/Grid.tsx b/src/CotMG/ui/Grid.tsx new file mode 100644 index 000000000..fe449ac93 --- /dev/null +++ b/src/CotMG/ui/Grid.tsx @@ -0,0 +1,141 @@ +import * as React from "react"; +import { Fragment, Fragments, NoneFragment } from "../Fragment"; +import { ActiveFragment } from "../ActiveFragment"; +import { FragmentType } from "../FragmentType"; +import { IStaneksGift } from "../IStaneksGift"; +import { StdButton } from "../../ui/React/StdButton"; +import { Cell } from "./Cell"; +import { FragmentInspector } from "./FragmentInspector"; +import { FragmentSelector } from "./FragmentSelector"; + +function zeros(dimensions: number[]): any { + const array = []; + + for (let i = 0; i < dimensions[0]; ++i) { + array.push(dimensions.length == 1 ? 0 : zeros(dimensions.slice(1))); + } + + return array; +} + +function randomColor(fragment: ActiveFragment): string { + // Can't set Math.random seed so copy casino. TODO refactor both RNG later. + let s1 = Math.pow((fragment.x + 1) * (fragment.y + 1), 10); + let s2 = s1; + let s3 = s1; + + const colors = []; + for (let i = 0; i < 3; i++) { + s1 = (171 * s1) % 30269; + s2 = (172 * s2) % 30307; + s3 = (170 * s3) % 30323; + colors.push((s1 / 30269.0 + s2 / 30307.0 + s3 / 30323.0) % 1.0); + } + + return `rgb(${colors[0] * 256}, ${colors[1] * 256}, ${colors[2] * 256})`; +} + +type GridProps = { + gift: IStaneksGift; +}; + +export function Grid(props: GridProps): React.ReactElement { + function calculateGrid(gift: IStaneksGift): any { + const newgrid = zeros([gift.width(), gift.height()]); + for (let i = 0; i < gift.width(); i++) { + for (let j = 0; j < gift.height(); j++) { + const fragment = gift.fragmentAt(i, j); + if (fragment === null) continue; + newgrid[i][j] = 1; + } + } + + return newgrid; + } + + const [grid, setGrid] = React.useState(calculateGrid(props.gift)); + const [ghostGrid, setGhostGrid] = React.useState(zeros([props.gift.width(), props.gift.height()])); + const [pos, setPos] = React.useState([0, 0]); + const [selectedFragment, setSelectedFragment] = React.useState(NoneFragment); + + function moveGhost(worldX: number, worldY: number): void { + const newgrid = zeros([props.gift.width(), props.gift.height()]); + for (let i = 0; i < selectedFragment.shape.length; i++) { + for (let j = 0; j < selectedFragment.shape[i].length; j++) { + if (worldX + i > newgrid.length - 1) continue; + if (worldY + j > newgrid[worldX + i].length - 1) continue; + if (!selectedFragment.shape[i][j]) continue; + if (worldX + j > newgrid.length - 1) continue; + if (worldY + i > newgrid[worldX + j].length - 1) continue; + newgrid[worldX + j][worldY + i] = 1; + } + } + + setGhostGrid(newgrid); + setPos([worldX, worldY]); + } + + function deleteAt(worldX: number, worldY: number): boolean { + return props.gift.deleteAt(worldX, worldY); + } + + function clickAt(worldX: number, worldY: number): void { + if (selectedFragment.type == FragmentType.None) return; + if (selectedFragment.type == FragmentType.Delete) { + deleteAt(worldX, worldY); + } else { + if (!props.gift.canPlace(worldX, worldY, selectedFragment)) return; + props.gift.place(worldX, worldY, selectedFragment); + } + setGrid(calculateGrid(props.gift)); + } + + function color(worldX: number, worldY: number): string { + if (ghostGrid[worldX][worldY] && grid[worldX][worldY]) return "red"; + if (ghostGrid[worldX][worldY]) return "white"; + if (grid[worldX][worldY]) { + const fragment = props.gift.fragmentAt(worldX, worldY); + if (fragment === null) throw new Error("ActiveFragment should not be null"); + return randomColor(fragment); + } + return ""; + } + + function clear(): void { + props.gift.clear(); + setGrid(zeros([props.gift.width(), props.gift.height()])); + } + + // switch the width/length to make axis consistent. + const elems = []; + for (let j = 0; j < props.gift.height(); j++) { + const cells = []; + for (let i = 0; i < props.gift.width(); i++) { + cells.push( + moveGhost(i, j)} onClick={() => clickAt(i, j)} color={color(i, j)} />, + ); + } + elems.push( +
+ {cells} +
, + ); + } + + function updateSelectedFragment(fragment: Fragment): void { + setSelectedFragment(fragment); + const newgrid = zeros([props.gift.width(), props.gift.height()]); + setGhostGrid(newgrid); + } + + return ( +
+ + +
{elems}
+
+ +
+
+ ); +} diff --git a/src/CotMG/ui/StaneksGiftRoot.tsx b/src/CotMG/ui/StaneksGiftRoot.tsx new file mode 100644 index 000000000..a7ae665d5 --- /dev/null +++ b/src/CotMG/ui/StaneksGiftRoot.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import { Grid } from "./Grid"; +import { IStaneksGift } from "../IStaneksGift"; + +type IProps = { + staneksGift: IStaneksGift; +}; + +export function StaneksGiftRoot({ staneksGift }: IProps): React.ReactElement { + return ( + <> +

Stanek's Gift

+ + + ); +} diff --git a/src/Faction/FactionInfo.tsx b/src/Faction/FactionInfo.tsx index fa9eb1aa6..929669908 100644 --- a/src/Faction/FactionInfo.tsx +++ b/src/Faction/FactionInfo.tsx @@ -468,4 +468,46 @@ export const FactionInfos: IMap = { false, false, ), + + // prettier-ignore + "Church of the Machine God": new FactionInfo(<> + {" `` "}
+ {" -odmmNmds: "}
+ {" `hNmo:..-omNh. "}
+ {" yMd` `hNh "}
+ {" mMd oNm "}
+ {" oMNo .mM/ "}
+ {" `dMN+ -mM+ "}
+ {" -mMNo -mN+ "}
+ {" .+- :mMNo/mN/ "}
+ {":yNMd. :NMNNN/ "}
+ {"-mMMMh. /NMMh` "}
+ {" .dMMMd. /NMMMy` "}
+ {" `yMMMd. /NNyNMMh` "}
+ {" `sMMMd. +Nm: +NMMh. "}
+ {" oMMMm- oNm: /NMMd. "}
+ {" +NMMmsMm- :mMMd. "}
+ {" /NMMMm- -mMMd. "}
+ {" /MMMm- -mMMd. "}
+ {" `sMNMMm- .mMmo "}
+ {" `sMd:hMMm. ./. "}
+ {" `yMy` `yNMd` "}
+ {" `hMs` oMMy "}
+ {" `hMh sMN- "}
+ {" /MM- .NMo "}
+ {" +MM: :MM+ "}
+ {" sNNo-.`.-omNy` "}
+ {" -smNNNNmdo- "}
+ {" `..` "}

+ Many cultures predict an end to humanity in the near future, a final + Armageddon that will end the world; but we disagree. +

Note that for this faction, reputation can + only be gained by charging Stanek's gift., + [], + false, + false, + false, + false, + true, + ), }; diff --git a/src/Locations/Locations.ts b/src/Locations/Locations.ts index fd18bd452..73dfbe27c 100644 --- a/src/Locations/Locations.ts +++ b/src/Locations/Locations.ts @@ -84,8 +84,8 @@ Cities[CityName.Chongqing].asciiArt = ` [world stock exchange] F | \\ o 78 [kuaigong international] \\ / - 38 o----x--x------x------A--------- - / 39 | 41 + 38 o----x--x------x------A------G-- + / 39 | 41 [church] 37 o + 79 o--x--x-C-0 / | / / x-----+-----x-----0 [hospital] diff --git a/src/Locations/data/LocationNames.ts b/src/Locations/data/LocationNames.ts index 3880fc018..42bd5e16e 100644 --- a/src/Locations/data/LocationNames.ts +++ b/src/Locations/data/LocationNames.ts @@ -29,6 +29,7 @@ export enum LocationName { // Chongqing locations ChongqingKuaiGongInternational = "KuaiGong International", ChongqingSolarisSpaceSystems = "Solaris Space Systems", + ChongqingChurchOfTheMachineGod = "Church of the Machine God", // Sector 12 Sector12AlphaEnterprises = "Alpha Enterprises", diff --git a/src/Locations/data/LocationsMetadata.ts b/src/Locations/data/LocationsMetadata.ts index 1a1a7731d..b40d0b165 100644 --- a/src/Locations/data/LocationsMetadata.ts +++ b/src/Locations/data/LocationsMetadata.ts @@ -440,4 +440,9 @@ export const LocationsMetadata: IConstructorParams[] = [ name: LocationName.WorldStockExchange, types: [LocationType.StockMarket], }, + { + city: CityName.Chongqing, + name: LocationName.ChongqingChurchOfTheMachineGod, + types: [LocationType.Special], + }, ]; diff --git a/src/Locations/ui/SpecialLocation.tsx b/src/Locations/ui/SpecialLocation.tsx index 21fcb7585..2b2c308cd 100644 --- a/src/Locations/ui/SpecialLocation.tsx +++ b/src/Locations/ui/SpecialLocation.tsx @@ -18,10 +18,14 @@ import { Location } from "../Location"; import { CreateCorporationPopup } from "../../Corporation/ui/CreateCorporationPopup"; import { createPopup } from "../../ui/React/createPopup"; import { LocationName } from "../data/LocationNames"; +import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; +import { Factions } from "../../Faction/Factions"; +import { joinFaction } from "../../Faction/FactionHelpers"; import { use } from "../../ui/Context"; import { dialogBoxCreate } from "../../ui/React/DialogBox"; +import { RadioButtonUncheckedRounded } from "@mui/icons-material"; type IProps = { loc: Location; @@ -117,6 +121,109 @@ export function SpecialLocation(props: IProps): React.ReactElement { return ; } + function handleCotMG(): void { + const faction = Factions["Church of the Machine God"]; + if (!player.factions.includes("Church of the Machine God")) { + joinFaction(faction); + } + if ( + !player.augmentations.some((a) => a.name === AugmentationNames.StaneksGift1) && + !player.queuedAugmentations.some((a) => a.name === AugmentationNames.StaneksGift1) + ) { + player.queueAugmentation(AugmentationNames.StaneksGift1); + } + + router.toFaction(faction); + } + + function renderCotMG(): React.ReactElement { + // prettier-ignore + const symbol =
+        {"                 ``          "}
+ {" -odmmNmds: "}
+ {" `hNmo:..-omNh. "}
+ {" yMd` `hNh "}
+ {" mMd oNm "}
+ {" oMNo .mM/ "}
+ {" `dMN+ -mM+ "}
+ {" -mMNo -mN+ "}
+ {" .+- :mMNo/mN/ "}
+ {":yNMd. :NMNNN/ "}
+ {"-mMMMh. /NMMh` "}
+ {" .dMMMd. /NMMMy` "}
+ {" `yMMMd. /NNyNMMh` "}
+ {" `sMMMd. +Nm: +NMMh. "}
+ {" oMMMm- oNm: /NMMd. "}
+ {" +NMMmsMm- :mMMd. "}
+ {" /NMMMm- -mMMd. "}
+ {" /MMMm- -mMMd. "}
+ {" `sMNMMm- .mMmo "}
+ {" `sMd:hMMm. ./. "}
+ {" `yMy` `yNMd` "}
+ {" `hMs` oMMy "}
+ {" `hMh sMN- "}
+ {" /MM- .NMo "}
+ {" +MM: :MM+ "}
+ {" sNNo-.`.-omNy` "}
+ {" -smNNNNmdo- "}
+ {" `..` "}
+ if (player.factions.includes("Church of the Machine God")) { + return ( +
+

+ Allison "Mother" Stanek: Welcome back my child! +

+ {symbol} +
+ ); + } + + if (!player.canAccessCotMG()) { + return ( + <> +

+ + A decrepit altar stands in the middle of a dilapidated church. +
+
A symbol is carved in the altar. +
+

+
+ {symbol} + + ); + } + + if ( + player.augmentations.filter((a) => a.name !== AugmentationNames.NeuroFluxGovernor).length > 0 || + player.queuedAugmentations.filter((a) => a.name !== AugmentationNames.NeuroFluxGovernor).length > 0 + ) { + return ( +
+

+ + Allison "Mother" Stanek: Begone you filth! My gift must be the first modification that your body should + have! + +

+
+ ); + } + + return ( +
+

+ + Allison "Mother" Stanek: Welcome child, I see your body is pure. Are you ready to ascend beyond our human + form? If you are, accept my gift. + +

+ + {symbol} +
+ ); + } + switch (props.loc.name) { case LocationName.NewTokyoVitaLife: { return renderResleeving(); @@ -130,6 +237,9 @@ export function SpecialLocation(props: IProps): React.ReactElement { case LocationName.NewTokyoNoodleBar: { return renderNoodleBar(); } + case LocationName.ChongqingChurchOfTheMachineGod: { + return renderCotMG(); + } default: console.error(`Location ${props.loc.name} doesn't have any special properties`); return <>; diff --git a/src/Netscript/RamCostGenerator.ts b/src/Netscript/RamCostGenerator.ts index 4d5674d74..80d3e5a7e 100644 --- a/src/Netscript/RamCostGenerator.ts +++ b/src/Netscript/RamCostGenerator.ts @@ -54,6 +54,15 @@ export const RamCostConstants: IMap = { ScriptGangApiBaseRamCost: 4, ScriptBladeburnerApiBaseRamCost: 4, + + ScriptStanekCharge: 0.4, + ScriptStanekFragmentDefinitions: 0, + ScriptStanekPlacedFragments: 5, + ScriptStanekClear: 0, + ScriptStanekCanPlace: 0.5, + ScriptStanekPlace: 5, + ScriptStanekFragmentAt: 2, + ScriptStanekDeleteAt: 0.15, }; export const RamCosts: IMap = { @@ -313,6 +322,17 @@ export const RamCosts: IMap = { purchaseSleeveAug: () => RamCostConstants.ScriptSleeveBaseRamCost, }, + stanek: { + charge: () => RamCostConstants.ScriptStanekCharge, + fragmentDefinitions: () => RamCostConstants.ScriptStanekFragmentDefinitions, + placedFragments: () => RamCostConstants.ScriptStanekPlacedFragments, + clear: () => RamCostConstants.ScriptStanekClear, + canPlace: () => RamCostConstants.ScriptStanekCanPlace, + place: () => RamCostConstants.ScriptStanekPlace, + fragmentAt: () => RamCostConstants.ScriptStanekFragmentAt, + deleteAt: () => RamCostConstants.ScriptStanekDeleteAt, + }, + heart: { // Easter egg function break: () => 0, diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts index 1be026f79..b7b04b1b0 100644 --- a/src/NetscriptFunctions.ts +++ b/src/NetscriptFunctions.ts @@ -171,6 +171,9 @@ import { GangMemberTask } from "./Gang/GangMemberTask"; import { Stock } from "./StockMarket/Stock"; import { BaseServer } from "./Server/BaseServer"; +import { staneksGift } from "./CotMG/Helper"; +import { Fragments, FragmentById } from "./CotMG/Fragment"; + const defaultInterpreter = new Interpreter("", () => undefined); // the acorn interpreter has a bug where it doesn't convert arrays correctly. @@ -375,7 +378,7 @@ function NetscriptFunctions(workerScript: WorkerScript): NS { }; // Utility function to get Hacknet Node object - const getHacknetNode = function (i: any, callingFn: string = ""): HacknetNode | HacknetServer { + const getHacknetNode = function (i: any, callingFn = ""): HacknetNode | HacknetServer { if (isNaN(i)) { throw makeRuntimeErrorMsg(callingFn, "Invalid index specified for Hacknet Node: " + i); } @@ -545,6 +548,15 @@ function NetscriptFunctions(workerScript: WorkerScript): NS { return contract; }; + const checkStanekAPIAccess = function (func: string): void { + if (Player.bitNodeN !== 13 && !SourceFileFlags[13]) { + throw makeRuntimeErrorMsg( + `stanek.${func}`, + "You do not currently have access to the Stanek API. This is either because you are not in BitNode-13 or because you do not have Source-File 13", + ); + } + }; + const checkGangApiAccess = function (func: any): void { const gang = Player.gang; if (gang === null) throw new Error("Must have joined gang"); @@ -5199,6 +5211,66 @@ function NetscriptFunctions(workerScript: WorkerScript): NS { return Player.sleeves[sleeveNumber].tryBuyAugmentation(Player, aug); }, }, // End sleeve + + // Stanek's gift API + stanek: { + charge: function (worldX: any, worldY: any): any { + updateDynamicRam("charge", getRamCost("stanek", "charge")); + //checkStanekAPIAccess("charge"); + const fragment = staneksGift.fragmentAt(worldX, worldY); + if (!fragment) throw makeRuntimeErrorMsg("stanek.charge", `No fragment at (${worldX}, ${worldY})`); + return netscriptDelay(1000, workerScript).then(function () { + if (workerScript.env.stopFlag) { + return Promise.reject(workerScript); + } + const ram = workerScript.scriptRef.ramUsage * workerScript.scriptRef.threads; + return Promise.resolve(staneksGift.charge(worldX, worldY, ram)); + }); + }, + fragmentDefinitions: function () { + updateDynamicRam("fragmentDefinitions", getRamCost("stanek", "fragmentDefinitions")); + //checkStanekAPIAccess("fragmentDefinitions"); + return Fragments.map((f) => f.copy()); + }, + placedFragments: function () { + updateDynamicRam("placedFragments", getRamCost("stanek", "placedFragments")); + //checkStanekAPIAccess("placedFragments"); + return staneksGift.fragments.map((af) => { + return { ...af.copy(), ...af.fragment().copy() }; + }); + }, + clear: function () { + updateDynamicRam("clear", getRamCost("stanek", "clear")); + //checkStanekAPIAccess("clear"); + staneksGift.clear(); + }, + canPlace: function (worldX: any, worldY: any, fragmentId: any): any { + updateDynamicRam("canPlace", getRamCost("stanek", "canPlace")); + //checkStanekAPIAccess("canPlace"); + const fragment = FragmentById(fragmentId); + if (!fragment) throw makeRuntimeErrorMsg("stanek.canPlace", `Invalid fragment id: ${fragmentId}`); + return staneksGift.canPlace(worldX, worldY, fragment); + }, + place: function (worldX: any, worldY: any, fragmentId: any): any { + updateDynamicRam("place", getRamCost("stanek", "place")); + //checkStanekAPIAccess("place"); + const fragment = FragmentById(fragmentId); + if (!fragment) throw makeRuntimeErrorMsg("stanek.place", `Invalid fragment id: ${fragmentId}`); + return staneksGift.place(worldX, worldY, fragment); + }, + fragmentAt: function (worldX: any, worldY: any): any { + updateDynamicRam("fragmentAt", getRamCost("stanek", "fragmentAt")); + //checkStanekAPIAccess("fragmentAt"); + const fragment = staneksGift.fragmentAt(worldX, worldY); + if (fragment !== null) return fragment.copy(); + return null; + }, + deleteAt: function (worldX: any, worldY: any): any { + updateDynamicRam("deleteAt", getRamCost("stanek", "deleteAt")); + //checkStanekAPIAccess("deleteAt"); + return staneksGift.deleteAt(worldX, worldY); + }, + }, // End stanek formulas: { basic: { calculateSkill: function (exp: any, mult: any = 1): any { diff --git a/src/NetscriptWorker.ts b/src/NetscriptWorker.ts index b5c9d01e9..5e2298fd8 100644 --- a/src/NetscriptWorker.ts +++ b/src/NetscriptWorker.ts @@ -114,6 +114,7 @@ function startNetscript2Script(workerScript: WorkerScript): Promise CompanyPosition | null; getUpgradeHomeRamCost: () => number; gotoLocation: (to: LocationName) => boolean; - hasAugmentation: (aug: Augmentation) => boolean; + hasAugmentation: (aug: string | Augmentation) => boolean; hasCorporation: () => boolean; hasGangWith: (facName: string) => boolean; hasTorRouter: () => boolean; @@ -281,6 +281,7 @@ export class PlayerObject implements IPlayer { setBitNodeNumber: (n: number) => void; getMult: (name: string) => number; setMult: (name: string, mult: number) => void; + canAccessCotMG: () => boolean; constructor() { //Skills and stats @@ -575,6 +576,8 @@ export class PlayerObject implements IPlayer { this.getMult = generalMethods.getMult; this.setMult = generalMethods.setMult; + + this.canAccessCotMG = generalMethods.canAccessCotMG; } /** diff --git a/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx b/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx index de5b6b334..3db2dffc1 100644 --- a/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx +++ b/src/PersonObjects/Player/PlayerObjectGeneralMethods.tsx @@ -2632,3 +2632,7 @@ export function setMult(this: IPlayer, name: string, mult: number): void { if (!this.hasOwnProperty(name)) return; (this as any)[name] = mult; } + +export function canAccessCotMG(this: IPlayer): boolean { + return this.bitNodeN === 13 || SourceFileFlags[13] > 0; +} diff --git a/src/Prestige.ts b/src/Prestige.ts index 3196c38fb..7a6b69da4 100755 --- a/src/Prestige.ts +++ b/src/Prestige.ts @@ -143,6 +143,12 @@ function prestigeAugmentation(): void { } } + if (augmentationExists(AugmentationNames.StaneksGift) && Augmentations[AugmentationNames.StaneksGift].owned) { + // TODO(hydroflame): refactor faction names so we don't have to hard + // code strings. + joinFaction(Factions["Church of the Machine God"]); + } + resetPidCounter(); } diff --git a/src/SaveObject.tsx b/src/SaveObject.tsx index 407a61574..a9a34a1b3 100755 --- a/src/SaveObject.tsx +++ b/src/SaveObject.tsx @@ -10,6 +10,7 @@ import { Settings } from "./Settings/Settings"; import { loadSpecialServerIps, SpecialServerIps } from "./Server/SpecialServerIps"; import { SourceFileFlags } from "./SourceFile/SourceFileFlags"; import { loadStockMarket, StockMarket } from "./StockMarket/StockMarket"; +import { staneksGift, loadStaneksGift } from "./CotMG/Helper"; import { GameSavedEvents } from "./ui/React/Snackbar"; @@ -38,6 +39,7 @@ class BitburnerSaveObject { VersionSave = ""; AllGangsSave = ""; LastExportBonus = ""; + StaneksGiftSave = ""; getSaveString(): string { this.PlayerSave = JSON.stringify(Player); @@ -67,6 +69,7 @@ class BitburnerSaveObject { this.SettingsSave = JSON.stringify(Settings); this.VersionSave = JSON.stringify(CONSTANTS.Version); this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus); + this.StaneksGiftSave = JSON.stringify(staneksGift); if (Player.inGang()) { this.AllGangsSave = JSON.stringify(AllGangs); } @@ -173,6 +176,12 @@ function loadGame(saveString: string): boolean { loadFactions(saveObj.FactionsSave); loadSpecialServerIps(saveObj.SpecialServerIpsSave); + if (saveObj.hasOwnProperty("StaneksGiftSave")) { + loadStaneksGift(saveObj.StaneksGiftSave); + } else { + console.warn(`Could not load Staneks Gift from save`); + loadStaneksGift(""); + } if (saveObj.hasOwnProperty("AliasesSave")) { try { loadAliases(saveObj.AliasesSave); diff --git a/src/Script/RamCalculations.ts b/src/Script/RamCalculations.ts index ca3257408..a28483853 100644 --- a/src/Script/RamCalculations.ts +++ b/src/Script/RamCalculations.ts @@ -197,6 +197,8 @@ async function parseOnlyRamCalculate( func = workerScript.env.vars.bladeburner[ref]; } else if (ref in workerScript.env.vars.codingcontract) { func = workerScript.env.vars.codingcontract[ref]; + } else if (ref in workerScript.env.vars.stanek) { + func = workerScript.env.vars.stanek[ref]; } else if (ref in workerScript.env.vars.gang) { func = workerScript.env.vars.gang[ref]; } else if (ref in workerScript.env.vars.sleeve) { diff --git a/src/Sidebar/ui/SidebarRoot.tsx b/src/Sidebar/ui/SidebarRoot.tsx index e314e6393..631425713 100644 --- a/src/Sidebar/ui/SidebarRoot.tsx +++ b/src/Sidebar/ui/SidebarRoot.tsx @@ -49,6 +49,7 @@ import { iTutorialSteps, iTutorialNextStep, ITutorial } from "../../InteractiveT import { getAvailableCreatePrograms } from "../../Programs/ProgramHelpers"; import { Settings } from "../../Settings/Settings"; import { redPillFlag } from "../../RedPill"; +import { AugmentationNames } from "../../Augmentation/data/AugmentationNames"; import { inMission } from "../../Missions"; import { KEY } from "../../utils/helpers/keyCodes"; @@ -156,13 +157,12 @@ export function SidebarRoot(props: IProps): React.ReactElement { const canOpenSleeves = props.player.sleeves.length > 0; - // TODO(hydroflame): these should not as any but right now the def is that it - // can only be defined; const canCorporation = !!(props.player.corporation as any); const canGang = !!(props.player.gang as any); const canJob = props.player.companyName !== ""; const canStockMarket = props.player.hasWseAccount; const canBladeburner = !!(props.player.bladeburner as any); + const canStaneksGift = props.player.hasAugmentation(AugmentationNames.StaneksGift1); function clickTerminal(): void { props.router.toTerminal(); @@ -187,6 +187,10 @@ export function SidebarRoot(props: IProps): React.ReactElement { props.router.toCreateProgram(); } + function clickStaneksGift(): void { + props.router.toStaneksGift(); + } + function clickFactions(): void { props.router.toFactions(); } @@ -405,6 +409,25 @@ export function SidebarRoot(props: IProps): React.ReactElement { + {/* {canStaneksGift && ( + + + + + + + Staneks Gift + + + + )} */} {canCreateProgram && ( )} + {canStaneksGift && ( + + + + + + StaneksGift + + + )} diff --git a/src/engine.tsx b/src/engine.tsx index 7923809d4..2efb37e5f 100644 --- a/src/engine.tsx +++ b/src/engine.tsx @@ -12,6 +12,7 @@ import { initCompanies } from "./Company/Companies"; import { Corporation } from "./Corporation/Corporation"; import { CONSTANTS } from "./Constants"; import { Factions, initFactions } from "./Faction/Factions"; +import { staneksGift } from "./CotMG/Helper"; import { processPassiveFactionRepGain, inviteToFaction } from "./Faction/FactionHelpers"; import { Router } from "./ui/GameRoot"; @@ -136,6 +137,9 @@ const Engine: { currMission.process(numCycles); } + // Staneks gift + staneksGift.process(Player, numCycles); + // Corporation if (Player.corporation instanceof Corporation) { // Stores cycles in a "buffer". Processed separately using Engine Counters diff --git a/src/engineStyle.ts b/src/engineStyle.ts index 6d6de4735..aada36e0d 100644 --- a/src/engineStyle.ts +++ b/src/engineStyle.ts @@ -30,3 +30,4 @@ import "../css/dev-menu.css"; import "../css/casino.scss"; import "../css/milestones.scss"; import "../css/infiltration.scss"; +import "../css/staneksgift.scss"; diff --git a/src/ui/GameRoot.tsx b/src/ui/GameRoot.tsx index 370bab0f4..563056f84 100644 --- a/src/ui/GameRoot.tsx +++ b/src/ui/GameRoot.tsx @@ -62,6 +62,8 @@ import { CharacterStats } from "./CharacterStats"; import { TravelAgencyRoot } from "../Locations/ui/TravelAgencyRoot"; import { StockMarketRoot } from "../StockMarket/ui/StockMarketRoot"; import { BitverseRoot } from "../BitNode/ui/BitverseRoot"; +import { StaneksGiftRoot } from "../CotMG/ui/StaneksGiftRoot"; +import { staneksGift } from "../CotMG/Helper"; import { CharacterOverview } from "./React/CharacterOverview"; import { BladeburnerCinematic } from "../Bladeburner/ui/BladeburnerCinematic"; import { workerScripts } from "../Netscript/WorkerScripts"; @@ -178,6 +180,9 @@ export let Router: IRouter = { toHackingMission: () => { throw new Error("Router called before initialization"); }, + toStaneksGift: () => { + throw new Error("Router called before initialization"); + }, }; function determineStartPage(player: IPlayer): Page { @@ -271,6 +276,9 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme setPage(Page.HackingMission); setFaction(faction); }, + toStaneksGift: () => { + setPage(Page.StaneksGift); + }, }; useEffect(() => { @@ -309,6 +317,8 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme ) : page === Page.Stats ? ( + ) : page === Page.StaneksGift ? ( + ) : page === Page.CreateScript ? ( ) : page === Page.ActiveScripts ? ( diff --git a/src/ui/Router.ts b/src/ui/Router.ts index 206377b42..d780e0cee 100644 --- a/src/ui/Router.ts +++ b/src/ui/Router.ts @@ -35,6 +35,7 @@ export enum Page { Location, HackingMission, Loading, + StaneksGift, } /** @@ -75,4 +76,5 @@ export interface IRouter { toBladeburnerCinematic(): void; toLocation(location: Location): void; toHackingMission(faction: Faction): void; + toStaneksGift(): void; } diff --git a/src/ui/numeralFormat.ts b/src/ui/numeralFormat.ts index 6e45176ad..801c1b1ef 100644 --- a/src/ui/numeralFormat.ts +++ b/src/ui/numeralFormat.ts @@ -165,6 +165,18 @@ class NumeralFormatter { return this.format(n, "0,0"); } + formatStaneksGiftHeat(n: number): string { + return this.format(n, "0.000a"); + } + + formatStaneksGiftCharge(n: number): string { + return this.format(n, "0.000a"); + } + + formatStaneksGiftPower(n: number): string { + return this.format(n, "0"); + } + parseMoney(s: string): number { // numeral library does not handle formats like 1e10 well (returns 110), // so if both return a valid number, return the biggest one