Compare commits

...

21 Commits

Author SHA1 Message Date
Olivier Gagnon
87c63cde59 merge v0.56.0 2021-10-12 01:35:30 -04:00
hydroflame
383b02fdbb Merge pull request #1457 from Snarling/patch-3
Change effect replacer text to x% instead of +x%
2021-10-11 14:27:01 -04:00
Snarling
41de3102c7 Change effect replacer text to x% instead of +x%
This will make replacement work for hacknet cost (which is -x% so currently is not seen for replacement) and will keep the + or - for effectiveness (e.g. +12.3% hacking skill instead of just 12.3% hacking skill)
2021-10-10 23:00:11 -04:00
Olivier Gagnon
783750051e build w dev 2021-10-09 12:51:44 -04:00
Olivier Gagnon
0aa26df9d7 build sg 2021-10-09 01:09:27 -04:00
Olivier Gagnon
4355420349 made staneks gift work with prestiges 2021-10-08 03:16:51 -04:00
Olivier Gagnon
f4ecbd9b48 merge dev 2021-10-08 00:26:35 -04:00
Olivier Gagnon
528a8f30db more elements around 2021-10-08 00:13:18 -04:00
Olivier Gagnon
500063e87e Merge branch 'dev' into sg 2021-10-07 23:53:49 -04:00
Olivier Gagnon
8d7f0488f8 merge dev 2021-10-07 17:58:32 -04:00
Olivier Gagnon
62bdfb1875 notes 2021-10-07 14:49:40 -04:00
Olivier Gagnon
8e5c10cc2f enable dev for beta 2021-10-07 01:47:13 -04:00
Olivier Gagnon
15a03dd532 enable dev for beta 2021-10-07 01:42:06 -04:00
Olivier Gagnon
8e58482db0 sg 2021-10-07 01:36:59 -04:00
Olivier Gagnon
da746a63c3 add incremental game plaza stuff 2021-10-04 23:56:24 -04:00
Olivier Gagnon
2f677c7ec8 more work 2021-10-04 23:51:39 -04:00
Olivier Gagnon
c5e29dafc4 fix mc 2021-10-04 22:31:07 -04:00
Olivier Gagnon
880654c222 ui work 2021-10-04 12:28:57 -04:00
Olivier Gagnon
4fc6d393e4 fix mc 2021-10-03 21:44:15 -04:00
Olivier Gagnon
7304e5379f sg 2021-10-03 20:34:36 -04:00
Olivier Gagnon
793d9b34ce update BN13 for new UI 2021-09-25 17:21:50 -04:00
60 changed files with 2178 additions and 13 deletions

23
css/staneksgift.scss Normal file
View File

@@ -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;
}

64
dist/vendor.bundle.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -30,5 +30,6 @@ to reach out to the developer!
Gang API <netscript/netscriptgangapi>
Coding Contract API <netscript/netscriptcodingcontractapi>
Sleeve API <netscript/netscriptsleeveapi>
Stanek API <netscript/netscriptstanekapi>
Formulas API <netscript/netscriptformulasapi>
Miscellaneous <netscript/netscriptmisc>

View File

@@ -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() <stanekapi/charge>
fragmentDefinitions() <stanekapi/fragmentDefinitions>
placedFragments() <stanekapi/placedFragments>
clear() <stanekapi/clear>
canPlace() <stanekapi/canPlace>
place() <stanekapi/place>
fragmentAt() <stanekapi/fragmentAt>
deleteAt() <stanekapi/deleteAt>

View File

@@ -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

View File

@@ -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`

View File

@@ -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.

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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

View File

@@ -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();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2366,6 +2366,145 @@ 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.",
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,
stats: <>Its unstable nature decreases all your stats by 10%</>,
});
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"<br><br>' +
"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,
stats: null,
});
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.<br><br>" +
"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,
stats: null,
});
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.<br><br>" +
"(hydro notes: Finishes the BN, eventually)",
prereqs: [AugmentationNames.StaneksGift3],
isSpecial: true,
stats: null,
});
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]],

View File

@@ -108,6 +108,10 @@ export const AugmentationNames: {
BladeArmorOmnibeam: string;
BladeArmorIPU: string;
BladesSimulacrum: string;
StaneksGift1: string;
StaneksGift2: string;
StaneksGift3: string;
StaneksGift4: string;
} = {
Targeting1: "Augmented Targeting I",
Targeting2: "Augmented Targeting II",
@@ -219,6 +223,11 @@ export const AugmentationNames: {
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

View File

@@ -530,8 +530,38 @@ 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.
<br />
<br />
Their leader, Allison "Mother" Stanek is said to have created her own Augmentation whose power goes beyond any
other. Find her in Chongquing and gain her trust.
<br />
<br />
In this BitNode:
<br />
<br />
Every is significantly reduced
<br />
Stanek's Gift power is significantly increased.
<br />
<br />
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.
<br />
<br />
Each level of this Source-File increases the size of Stanek's 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");
@@ -553,6 +583,8 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers[mult] = 1;
}
}
// Special case.
BitNodeMultipliers.StaneksGiftExtraSize = 0;
switch (p.bitNodeN) {
case 1: // Source Genesis (every multiplier is 1)
@@ -793,6 +825,47 @@ 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;
BitNodeMultipliers.StaneksGiftPowerMultiplier = 2;
BitNodeMultipliers.StaneksGiftExtraSize = 1;
break;
}
default:
console.warn("Player.bitNodeN invalid");
break;

View File

@@ -212,6 +212,16 @@ interface IBitNodeMultipliers {
*/
StrengthLevelMultiplier: number;
/**
* Influences the power of the gift.
*/
StaneksGiftPowerMultiplier: number;
/**
* Influences the size of the gift.
*/
StaneksGiftExtraSize: number;
// Index signature
[key: string]: number;
}
@@ -274,4 +284,7 @@ export const BitNodeMultipliers: IBitNodeMultipliers = {
DaedalusAugsRequirement: 1,
GangKarmaRequirement: 1,
StaneksGiftPowerMultiplier: 1,
StaneksGiftExtraSize: 0,
};

View File

@@ -160,7 +160,7 @@ export function BitverseRoot(props: IProps): React.ReactElement {
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}>O | | | \| | O / _/ | / O | |/ | | | O</Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}>| | | |O / | | O / | O O | | \ O| | | |</Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}>| | |/ \/ / __| | |/ \ | \ | |__ \ \/ \| | |</Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \| O | |_/ |\| \ O \__| \_| | O |/ </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \| O | |_/ |\| \ <BitNodePortal n={13} level={nextSourceFileFlags[13]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> \__| \_| | O |/ </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | | |_/ | | \| / | \_| | | </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> \| / \| | / / \ |/ </Typography>
<Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}> | <BitNodePortal n={10} level={nextSourceFileFlags[10]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> | | / | <BitNodePortal n={11} level={nextSourceFileFlags[11]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> | </Typography>

View File

@@ -0,0 +1,90 @@
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];
export interface IActiveFragmentParams {
x: number;
y: number;
fragment: Fragment;
}
export class ActiveFragment {
id: number;
charge: 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;
} else {
this.id = -1;
this.x = -1;
this.y = -1;
this.charge = -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 new Error("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 new Error("ActiveFragment id refers to unknown Fragment.");
const c = new ActiveFragment({ x: this.x, y: this.y, fragment: fragment });
c.charge = this.charge;
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;

329
src/CotMG/Fragment.ts Normal file
View File

@@ -0,0 +1,329 @@
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(): number {
// check every line for robustness.
return Math.max(...this.shape.map((line) => line.length));
}
height(): number {
return this.shape.length;
}
// List of direct neighboors of this fragment.
neighboors(): number[][] {
const 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
1,
1, // limit
),
);
Fragments.push(
new Fragment(
1, // id
[
// shape
[_, X, _],
[X, X, X],
[_, X, _],
],
FragmentType.Hacking, // type
1,
1, // limit
),
);
Fragments.push(
new Fragment(
5, // id
[
// shape
[X, X],
],
FragmentType.HackingSpeed, // type
1.3,
1, // limit
),
);
Fragments.push(
new Fragment(
6, // id
[
[X, _],
[X, X],
], // shape
FragmentType.HackingMoney, // type
2, // power
1, // limit
),
);
Fragments.push(
new Fragment(
7, // id
[
[X, X],
[X, X],
], // shape
FragmentType.HackingGrow, // type
0.5, // power
1, // limit
),
);
Fragments.push(
new Fragment(
8, // id
[
[X, X, X],
[_, X, _],
[X, X, X],
], // shape
FragmentType.Hacking, // type
1, // power
1, // limit
),
);
Fragments.push(
new Fragment(
10, // id
[
[X, X],
[_, X],
], // shape
FragmentType.Strength, // type
2, // power
1, // limit
),
);
Fragments.push(
new Fragment(
12, // id
[
[_, X],
[X, X],
], // shape
FragmentType.Defense, // type
2, // power
1, // limit
),
);
Fragments.push(
new Fragment(
14, // id
[
[X, X],
[X, _],
], // shape
FragmentType.Dexterity, // type
2, // power
1, // limit
),
);
Fragments.push(
new Fragment(
16, // id
[
[X, _],
[X, X],
], // shape
FragmentType.Agility, // type
2, // power
1, // limit
),
);
Fragments.push(
new Fragment(
18, // id
[
[X, X],
[X, _],
], // shape
FragmentType.Charisma, // type
3, // power
1, // limit
),
);
Fragments.push(
new Fragment(
20, // id
[
[X, _, _],
[X, X, _],
[X, X, X],
], // shape
FragmentType.HacknetMoney, // type
1, // power
1, // limit
),
);
Fragments.push(
new Fragment(
21, // id
[
[X, X],
[_, X],
[_, X],
], // shape
FragmentType.HacknetCost, // type
-1, // power
1, // limit
),
);
Fragments.push(
new Fragment(
25, // id
[
[X, X, X],
[_, X, _],
], // shape
FragmentType.Rep, // type
0.5, // power
1, // limit
),
);
Fragments.push(
new Fragment(
27, // id
[
[X, _],
[_, X],
], // shape
FragmentType.WorkMoney, // type
10, // power
1, // limit
),
);
Fragments.push(
new Fragment(
28, // id
[[X, X]], // shape
FragmentType.Crime, // type
2, // power
1, // limit
),
);
Fragments.push(
new Fragment(
30, // id
[
[X, X, X],
[X, X, X],
[X, X, X],
], // shape
FragmentType.Bladeburner, // type
1.3, // power
1, // limit
),
);
Fragments.push(
new Fragment(
2, // id
[
// shape
[X, X, X],
[X, _, X],
[X, X, X],
],
FragmentType.Booster, // type
1.1, // power
3, // limit
),
);
Fragments.push(
new Fragment(
31, // id
[
// shape
[X],
[X],
[X],
[X],
],
FragmentType.Booster, // type
1.1, // power
3, // limit
),
);
})();
export const NoneFragment = new Fragment(-2, [], FragmentType.None, 0, Infinity);
export const DeleteFragment = new Fragment(-2, [], FragmentType.Delete, 0, Infinity);

96
src/CotMG/FragmentType.ts Normal file
View File

@@ -0,0 +1,96 @@
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,
}
export function Effect(tpe: FragmentType): string {
switch (tpe) {
case FragmentType.HackingChance: {
return "+x% hack() success chance";
break;
}
case FragmentType.HackingSpeed: {
return "+x% faster hack(), grow(), and weaken()";
break;
}
case FragmentType.HackingMoney: {
return "+x% hack() power";
break;
}
case FragmentType.HackingGrow: {
return "+x% grow() power";
break;
}
case FragmentType.Hacking: {
return "+x% hacking skill";
break;
}
case FragmentType.Strength: {
return "+x% strength skill";
break;
}
case FragmentType.Defense: {
return "+x% defense skill";
break;
}
case FragmentType.Dexterity: {
return "+x% dexterity skill";
break;
}
case FragmentType.Agility: {
return "+x% agility skill";
break;
}
case FragmentType.Charisma: {
return "+x% charisma skill";
break;
}
case FragmentType.HacknetMoney: {
return "+x% hacknet production";
break;
}
case FragmentType.HacknetCost: {
return "-x% all hacknet cost";
break;
}
case FragmentType.Rep: {
return "+x% reputation from factions and companies";
break;
}
case FragmentType.WorkMoney: {
return "+x% work money";
break;
}
case FragmentType.Crime: {
return "+x% crime money";
break;
}
case FragmentType.Bladeburner: {
return "+x% all bladeburner stats";
break;
}
}
throw new Error("Calling effect for fragment type that doesn't have an effect " + tpe);
}

14
src/CotMG/Helper.tsx Normal file
View File

@@ -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();
}
}

22
src/CotMG/IStaneksGift.ts Normal file
View File

@@ -0,0 +1,22 @@
import { ActiveFragment } from "./ActiveFragment";
import { Fragment } from "./Fragment";
import { IPlayer } from "../PersonObjects/IPlayer";
export interface IStaneksGift {
storedCycles: number;
fragments: ActiveFragment[];
width(): number;
height(): number;
charge(worldX: number, worldY: number, ram: number): number;
process(p: IPlayer, n: number): void;
effect(fragment: ActiveFragment): number;
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;
inBonus(): boolean;
prestigeAugmentation(): void;
prestigeSourceFile(): void;
}

222
src/CotMG/StaneksGift.ts Normal file
View File

@@ -0,0 +1,222 @@
import { Fragment } 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 { StaneksGiftEvents } from "./StaneksGiftEvents";
import { Generic_fromJSON, Generic_toJSON, Reviver } from "../utils/JSONReviver";
import { CONSTANTS } from "../Constants";
import { StanekConstants } from "./data/Constants";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { Player } from "../Player";
export class StaneksGift implements IStaneksGift {
storedCycles = 0;
fragments: ActiveFragment[] = [];
baseSize(): number {
return StanekConstants.BaseSize + BitNodeMultipliers.StaneksGiftExtraSize + Player.sourceFileLvl(13);
}
width(): number {
return Math.floor(this.baseSize() / 2 + 1);
}
height(): number {
return Math.floor(this.baseSize() / 2 + 0.6);
}
charge(worldX: number, worldY: number, ram: number): number {
const af = this.fragmentAt(worldX, worldY);
if (af === null) return 0;
const charge = CalculateCharge(ram);
af.charge += charge;
Factions["Church of the Machine God"].playerReputation += Math.log(ram) / Math.log(2);
return ram;
}
inBonus(): boolean {
return (this.storedCycles * CONSTANTS._idleSpeed) / 1000 > 1;
}
process(p: IPlayer, numCycles = 1): void {
this.storedCycles += numCycles;
this.storedCycles -= 5;
this.storedCycles = Math.max(0, this.storedCycles);
this.updateMults(p);
StaneksGiftEvents.emit();
}
effect(fragment: ActiveFragment): number {
// Find all the neighbooring cells
const cells = fragment.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);
neighboors = neighboors.filter((fragment) => fragment.fragment().type === FragmentType.Booster);
let boost = 1;
for (const neighboor of neighboors) {
boost *= neighboor.fragment().power;
}
return CalculateEffect(fragment.charge, fragment.fragment().power, boost);
}
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 = this.effect(aFrag);
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;
}
}
}
prestigeAugmentation(): void {
this.clear();
}
prestigeSourceFile(): void {
this.clear();
}
/**
* 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;

View File

@@ -0,0 +1,2 @@
import { EventEmitter } from "../utils/EventEmitter";
export const StaneksGiftEvents = new EventEmitter<[]>();

View File

@@ -0,0 +1,7 @@
export const StanekConstants: {
RAMBonus: number;
BaseSize: number;
} = {
RAMBonus: 0.1,
BaseSize: 12,
};

View File

@@ -0,0 +1,5 @@
import { StanekConstants } from "../data/Constants";
export function CalculateCharge(ram: number): number {
return ram * Math.pow(1 + Math.log2(ram) * StanekConstants.RAMBonus, 0.7);
}

View File

@@ -0,0 +1,3 @@
export function CalculateEffect(charge: number, power: number, boost: number): number {
return 1 + (Math.log(charge + 1) / (Math.log(3) * 100)) * power * boost;
}

4
src/CotMG/notes Normal file
View File

@@ -0,0 +1,4 @@
incentive for more threads
boosters just multiply output, eg 20% * 1.5 = 30%
git remote add danielyxie git@github.com:danielyxie/bitburner.git

40
src/CotMG/ui/Cell.tsx Normal file
View File

@@ -0,0 +1,40 @@
import * as React from "react";
import makeStyles from "@mui/styles/makeStyles";
import { TableCell as MuiTableCell, TableCellProps } from "@mui/material";
const useStyles = makeStyles({
root: {
border: "1px solid white",
width: "5px",
height: "5px",
},
});
export const TableCell: React.FC<TableCellProps> = (props: TableCellProps) => {
return (
<MuiTableCell
{...props}
classes={{
root: useStyles().root,
...props.classes,
}}
/>
);
};
type IProps = {
onMouseEnter?: () => void;
onClick?: () => void;
color: string;
};
export function Cell(cellProps: IProps): React.ReactElement {
return (
<TableCell
style={{ backgroundColor: cellProps.color }}
onMouseEnter={cellProps.onMouseEnter}
onClick={cellProps.onClick}
></TableCell>
);
}

View File

@@ -0,0 +1,79 @@
import React, { useState, useEffect } from "react";
import { ActiveFragment } from "../ActiveFragment";
import { IStaneksGift } from "../IStaneksGift";
import { FragmentType, Effect } from "../FragmentType";
import { numeralWrapper } from "../../ui/numeralFormat";
import Paper from "@mui/material/Paper";
import Typography from "@mui/material/Typography";
type IProps = {
gift: IStaneksGift;
fragment: ActiveFragment | null;
x: number;
y: number;
};
export function FragmentInspector(props: IProps): React.ReactElement {
const [, setC] = useState(new Date());
useEffect(() => {
const id = setInterval(() => setC(new Date()), 250);
return () => clearInterval(id);
}, []);
if (props.fragment === null) {
return (
<Paper>
<Typography>
ID: N/A
<br />
Effect: N/A
<br />
Magnitude: N/A
<br />
Charge: N/A
<br />
Heat: N/A
<br />
Effect: N/A
<br />
[X, Y] N/A
<br />
[X, Y] {props.x}, {props.y}
</Typography>
</Paper>
);
}
const f = props.fragment.fragment();
let charge = numeralWrapper.formatStaneksGiftCharge(props.fragment.charge);
let effect = "N/A";
// Boosters and cooling don't deal with heat.
if ([FragmentType.Booster, FragmentType.None, FragmentType.Delete].includes(f.type)) {
charge = "N/A";
effect = `${f.power}x adjacent fragment power`;
} else {
effect = Effect(f.type).replace("x%", numeralWrapper.formatPercentage(props.gift.effect(props.fragment) - 1));
}
return (
<Paper>
<Typography>
ID: {props.fragment.id}
<br />
Effect: {effect}
<br />
Power: {numeralWrapper.formatStaneksGiftPower(f.power)}
<br />
Charge: {charge}
<br />
<br />
root [X, Y] {props.fragment.x}, {props.fragment.y}
<br />
[X, Y] {props.x}, {props.y}
</Typography>
</Paper>
);
}

View File

@@ -0,0 +1,31 @@
import * as React from "react";
import { Cell } from "./Cell";
import TableRow from "@mui/material/TableRow";
import TableBody from "@mui/material/TableBody";
import { Table } from "../../ui/React/Table";
type IProps = {
width: number;
height: number;
colorAt: (x: number, y: number) => string;
};
export function FragmentPreview(props: IProps): React.ReactElement {
// 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(<Cell key={i} color={props.colorAt(i, j)} />);
}
elems.push(<TableRow key={j}>{cells}</TableRow>);
}
return (
<Table>
<TableBody>{elems}</TableBody>
</Table>
);
}

View File

@@ -0,0 +1,91 @@
import React, { useState } from "react";
import { Fragments, Fragment, NoneFragment, DeleteFragment } from "../Fragment";
import { FragmentType, Effect } from "../FragmentType";
import { IStaneksGift } from "../IStaneksGift";
import { FragmentPreview } from "./FragmentPreview";
import { numeralWrapper } from "../../ui/numeralFormat";
import Select, { SelectChangeEvent } from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
type IOptionProps = {
gift: IStaneksGift;
fragment: Fragment;
selectFragment: (fragment: Fragment) => void;
};
function FragmentOption(props: IOptionProps): React.ReactElement {
const remaining =
props.fragment.limit !== Infinity ? (
<>{props.fragment.limit - props.gift.count(props.fragment)} remaining</>
) : (
<></>
);
return (
<Box display="flex">
<Box sx={{ mx: 2 }}>
<FragmentPreview
width={props.fragment.width()}
height={props.fragment.height()}
colorAt={(x, y) => {
return !props.fragment.fullAt(x, y) ? "" : props.fragment.type === FragmentType.Booster ? "blue" : "green";
}}
/>
</Box>
<Typography>
{props.fragment.type === FragmentType.Booster
? `${props.fragment.power}x adjacent fragment power`
: Effect(props.fragment.type)}
<br />
power: {numeralWrapper.formatStaneksGiftPower(props.fragment.power)}
<br />
{remaining}
</Typography>
</Box>
);
}
type IProps = {
gift: IStaneksGift;
selectFragment: (fragment: Fragment) => void;
};
export function FragmentSelector(props: IProps): React.ReactElement {
const [value, setValue] = useState<string | number>("None");
function onChange(event: SelectChangeEvent<string | number>): void {
const v = event.target.value;
setValue(v);
if (v === "None") {
props.selectFragment(NoneFragment);
return;
} else if (v === "Delete") {
props.selectFragment(DeleteFragment);
return;
}
const fragment = Fragments.find((f) => f.id === v);
if (fragment === undefined) throw new Error("Fragment selector selected an undefined fragment with id " + v);
if (typeof v === "number") props.selectFragment(fragment);
}
return (
<Select sx={{ width: "100%" }} onChange={onChange} value={value}>
<MenuItem value="None">
<Typography>None</Typography>
</MenuItem>
<MenuItem value="Delete">
<Typography>Delete</Typography>
</MenuItem>
{Fragments.map((fragment) => (
<MenuItem key={fragment.id} value={fragment.id}>
<FragmentOption
key={fragment.id}
gift={props.gift}
selectFragment={props.selectFragment}
fragment={fragment}
/>
</MenuItem>
))}
</Select>
);
}

145
src/CotMG/ui/Grid.tsx Normal file
View File

@@ -0,0 +1,145 @@
import * as React from "react";
import { Fragment, NoneFragment } from "../Fragment";
import { ActiveFragment } from "../ActiveFragment";
import { FragmentType } from "../FragmentType";
import { IStaneksGift } from "../IStaneksGift";
import { Cell } from "./Cell";
import { FragmentInspector } from "./FragmentInspector";
import { FragmentSelector } from "./FragmentSelector";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import TableRow from "@mui/material/TableRow";
import TableBody from "@mui/material/TableBody";
import { Table } from "../../ui/React/Table";
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 (!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(
<Cell key={i} onMouseEnter={() => moveGhost(i, j)} onClick={() => clickAt(i, j)} color={color(i, j)} />,
);
}
elems.push(
<TableRow key={j} className="staneksgift_row">
{cells}
</TableRow>,
);
}
function updateSelectedFragment(fragment: Fragment): void {
setSelectedFragment(fragment);
const newgrid = zeros([props.gift.width(), props.gift.height()]);
setGhostGrid(newgrid);
}
return (
<>
<Button onClick={clear}>Clear</Button>
<Box display="flex">
<Table>
<TableBody>{elems}</TableBody>
</Table>
<FragmentInspector gift={props.gift} x={pos[0]} y={pos[1]} fragment={props.gift.fragmentAt(pos[0], pos[1])} />
</Box>
<FragmentSelector gift={props.gift} selectFragment={updateSelectedFragment} />
</>
);
}

View File

@@ -0,0 +1,36 @@
import React, { useState, useEffect } from "react";
import { convertTimeMsToTimeElapsedString } from "../../utils/StringHelperFunctions";
import { CONSTANTS } from "../../Constants";
import { StaneksGiftEvents } from "../StaneksGiftEvents";
import { Grid } from "./Grid";
import { IStaneksGift } from "../IStaneksGift";
import Typography from "@mui/material/Typography";
type IProps = {
staneksGift: IStaneksGift;
};
export function StaneksGiftRoot({ staneksGift }: IProps): React.ReactElement {
const setRerender = useState(true)[1];
function rerender(): void {
setRerender((o) => !o);
}
useEffect(() => StaneksGiftEvents.subscribe(rerender), []);
return (
<>
<Typography variant="h4">Stanek's Gift</Typography>
<Typography>
The gift is a grid on which you can place upgrades called fragments. The main type of fragment increases a stat,
like your hacking skill or agility exp. Once a stat fragment is placed it then needs to be charged via scripts
in order to become useful. The other kind of fragment is called booster fragments. They increase the efficiency
of the charged happening on fragments neighboring them (no diagonal)
</Typography>
{staneksGift.storedCycles > 5 && (
<Typography>
Bonus time: {convertTimeMsToTimeElapsedString(CONSTANTS._idleSpeed * staneksGift.storedCycles)}
</Typography>
)}
<Grid gift={staneksGift} />
</>
);
}

View File

@@ -2,6 +2,7 @@ import { IPlayer } from "./PersonObjects/IPlayer";
import { Bladeburner } from "./Bladeburner/Bladeburner";
import { IEngine } from "./IEngine";
import { IRouter } from "./ui/Router";
import { AugmentationNames } from "./Augmentation/data/AugmentationNames";
import React from "react";
@@ -19,6 +20,7 @@ import { Corporation } from "./DevMenu/ui/Corporation";
import { CodingContracts } from "./DevMenu/ui/CodingContracts";
import { StockMarket } from "./DevMenu/ui/StockMarket";
import { Sleeves } from "./DevMenu/ui/Sleeves";
import { Stanek } from "./DevMenu/ui/Stanek";
import { TimeSkip } from "./DevMenu/ui/TimeSkip";
import Typography from "@mui/material/Typography";
@@ -52,6 +54,7 @@ export function DevMenuRoot(props: IProps): React.ReactElement {
{props.player.hasWseAccount && <StockMarket />}
{props.player.sleeves.length > 0 && <Sleeves player={props.player} />}
{props.player.augmentations.some((aug) => aug.name === AugmentationNames.StaneksGift1) && <Stanek />}
<TimeSkip player={props.player} engine={props.engine} />
</>

79
src/DevMenu/ui/Stanek.tsx Normal file
View File

@@ -0,0 +1,79 @@
import React from "react";
import { staneksGift } from "../../CotMG/Helper";
import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import Typography from "@mui/material/Typography";
import { Adjuster } from "./Adjuster";
export function Stanek(): React.ReactElement {
function addCycles(): void {
staneksGift.storedCycles = 1e6;
}
function modCycles(modify: number): (x: number) => void {
return function (cycles: number): void {
staneksGift.storedCycles += cycles * modify;
};
}
function resetCycles(): void {
staneksGift.storedCycles = 0;
}
function addCharge(): void {
staneksGift.fragments.forEach((f) => (f.charge = 1e21));
}
function modCharge(modify: number): (x: number) => void {
return function (cycles: number): void {
staneksGift.fragments.forEach((f) => (f.charge += cycles * modify));
};
}
function resetCharge(): void {
staneksGift.fragments.forEach((f) => (f.charge = 0));
}
return (
<Accordion TransitionProps={{ unmountOnExit: true }}>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Stanek's Gift</Typography>
</AccordionSummary>
<AccordionDetails>
<table>
<tbody>
<tr>
<td>
<Adjuster
label="cycles"
placeholder="amt"
tons={addCycles}
add={modCycles(1)}
subtract={modCycles(-1)}
reset={resetCycles}
/>
</td>
</tr>
<tr>
<td>
<Adjuster
label="all charge"
placeholder="amt"
tons={addCharge}
add={modCharge(1)}
subtract={modCharge(-1)}
reset={resetCharge}
/>
</td>
</tr>
</tbody>
</table>
</AccordionDetails>
</Accordion>
);
}

View File

@@ -526,4 +526,47 @@ export const FactionInfos: IMap<FactionInfo> = {
true,
false,
),
// prettier-ignore
"Church of the Machine God": new FactionInfo(<>
{" `` "}<br />
{" -odmmNmds: "}<br />
{" `hNmo:..-omNh. "}<br />
{" yMd` `hNh "}<br />
{" mMd oNm "}<br />
{" oMNo .mM/ "}<br />
{" `dMN+ -mM+ "}<br />
{" -mMNo -mN+ "}<br />
{" .+- :mMNo/mN/ "}<br />
{":yNMd. :NMNNN/ "}<br />
{"-mMMMh. /NMMh` "}<br />
{" .dMMMd. /NMMMy` "}<br />
{" `yMMMd. /NNyNMMh` "}<br />
{" `sMMMd. +Nm: +NMMh. "}<br />
{" oMMMm- oNm: /NMMd. "}<br />
{" +NMMmsMm- :mMMd. "}<br />
{" /NMMMm- -mMMd. "}<br />
{" /MMMm- -mMMd. "}<br />
{" `sMNMMm- .mMmo "}<br />
{" `sMd:hMMm. ./. "}<br />
{" `yMy` `yNMd` "}<br />
{" `hMs` oMMy "}<br />
{" `hMh sMN- "}<br />
{" /MM- .NMo "}<br />
{" +MM: :MM+ "}<br />
{" sNNo-.`.-omNy` "}<br />
{" -smNNNNmdo- "}<br />
{" `..` "}<br /><br />
Many cultures predict an end to humanity in the near future, a final
Armageddon that will end the world; but we disagree.
<br /><br />Note that for this faction, reputation can
only be gained by charging Stanek's gift.</>,
[],
false,
false,
false,
false,
true,
true,
),
};

View File

@@ -85,7 +85,7 @@ Cities[CityName.Chongqing].asciiArt = `
\\ o 78 [kuaigong international]
\\ /
38 o----x--x------x------A------G--
/ 39 | 41 [church]
/ 39 | 41 [church]
37 o + 79 o--x--x-C-0
/ | /
/ x-----+-----x-----0 [hospital]

View File

@@ -17,6 +17,9 @@ import Button from "@mui/material/Button";
import { Location } from "../Location";
import { CreateCorporationModal } from "../../Corporation/ui/CreateCorporationModal";
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";
@@ -109,6 +112,21 @@ export function SpecialLocation(props: IProps): React.ReactElement {
return <Button onClick={handleResleeving}>Re-Sleeve</Button>;
}
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 = <Typography sx={{lineHeight: '1em',whiteSpace: 'pre'}}>
@@ -140,15 +158,56 @@ export function SpecialLocation(props: IProps): React.ReactElement {
{" sNNo-.`.-omNy` "}<br />
{" -smNNNNmdo- "}<br />
{" `..` "}</Typography>
if (player.factions.includes("Church of the Machine God")) {
return (
<>
<Typography>
<i>Allison "Mother" Stanek: Welcome back my child!</i>
</Typography>
{symbol}
</>
);
}
if (!player.canAccessCotMG()) {
return (
<>
<Typography>
A decrepit altar stands in the middle of a dilapidated church.
<br />
<br />A symbol is carved in the altar.
</Typography>
<br />
{symbol}
</>
);
}
if (
player.augmentations.filter((a) => a.name !== AugmentationNames.NeuroFluxGovernor).length > 0 ||
player.queuedAugmentations.filter((a) => a.name !== AugmentationNames.NeuroFluxGovernor).length > 0
) {
return (
<>
<Typography>
<i>
Allison "Mother" Stanek: Begone you filth! My gift must be the first modification that your body should
have!
</i>
</Typography>
</>
);
}
return (
<>
<Typography>
A decrepit altar stands in the middle of a dilapidated church.
<br />
<br />A symbol is carved in the altar.
<i>
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.
</i>
</Typography>
<br />
<Button onClick={handleCotMG}>Accept Stanek's Gift</Button>
{symbol}
</>
);

View File

@@ -54,6 +54,15 @@ export const RamCostConstants: IMap<number> = {
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<any> = {
@@ -313,6 +322,17 @@ export const RamCosts: IMap<any> = {
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,

View File

@@ -146,6 +146,7 @@ import { INetscriptGang, NetscriptGang } from "./NetscriptFunctions/Gang";
import { INetscriptSleeve, NetscriptSleeve } from "./NetscriptFunctions/Sleeve";
import { INetscriptExtra, NetscriptExtra } from "./NetscriptFunctions/Extra";
import { INetscriptHacknet, NetscriptHacknet } from "./NetscriptFunctions/Hacknet";
import { INetscriptStanek, NetscriptStanek } from "./NetscriptFunctions/Stanek";
const defaultInterpreter = new Interpreter("", () => undefined);
@@ -187,6 +188,7 @@ interface NS extends INetscriptExtra {
hacknet: INetscriptHacknet;
gang: INetscriptGang;
sleeve: INetscriptSleeve;
stanek: INetscriptStanek;
}
function NetscriptFunctions(workerScript: WorkerScript): NS {
@@ -477,6 +479,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 getBladeburnerActionObject = function (func: any, type: any, name: any): any {
const bladeburner = Player.bladeburner;
if (bladeburner === null) throw new Error("Must have joined bladeburner");
@@ -717,6 +728,7 @@ function NetscriptFunctions(workerScript: WorkerScript): NS {
const sleeve = NetscriptSleeve(Player, workerScript, helper);
const extra = NetscriptExtra(Player, workerScript);
const hacknet = NetscriptHacknet(Player, workerScript, helper);
const stanek = NetscriptStanek(Player, workerScript, helper);
const functions = {
hacknet: hacknet,
@@ -4469,7 +4481,11 @@ function NetscriptFunctions(workerScript: WorkerScript): NS {
},
}, // End coding contracts
// Duplicate Sleeve API
sleeve: sleeve,
stanek: stanek,
formulas: {
basic: {
calculateSkill: function (exp: any, mult: any = 1): any {

View File

@@ -0,0 +1,85 @@
import { INetscriptHelper } from "./INetscriptHelper";
import { IPlayer } from "../PersonObjects/IPlayer";
import { WorkerScript } from "../Netscript/WorkerScript";
import { netscriptDelay } from "../NetscriptEvaluator";
import { getRamCost } from "../Netscript/RamCostGenerator";
import { staneksGift } from "../CotMG/Helper";
import { Fragments, FragmentById } from "../CotMG/Fragment";
export interface INetscriptStanek {
charge(worldX: number, worldY: number): any;
fragmentDefinitions(): any;
placedFragments(): any;
clear(): void;
canPlace(worldX: number, worldY: number, fragmentId: number): boolean;
place(worldX: number, worldY: number, fragmentId: number): boolean;
fragmentAt(worldX: number, worldY: number): any;
deleteAt(worldX: number, worldY: number): boolean;
}
export function NetscriptStanek(
player: IPlayer,
workerScript: WorkerScript,
helper: INetscriptHelper,
): INetscriptStanek {
return {
charge: function (worldX: any, worldY: any): any {
helper.updateDynamicRam("charge", getRamCost("stanek", "charge"));
//checkStanekAPIAccess("charge");
const fragment = staneksGift.fragmentAt(worldX, worldY);
if (!fragment) throw helper.makeRuntimeErrorMsg("stanek.charge", `No fragment at (${worldX}, ${worldY})`);
const time = staneksGift.inBonus() ? 200 : 1000;
return netscriptDelay(time, 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 () {
helper.updateDynamicRam("fragmentDefinitions", getRamCost("stanek", "fragmentDefinitions"));
//checkStanekAPIAccess("fragmentDefinitions");
return Fragments.map((f) => f.copy());
},
placedFragments: function () {
helper.updateDynamicRam("placedFragments", getRamCost("stanek", "placedFragments"));
//checkStanekAPIAccess("placedFragments");
return staneksGift.fragments.map((af) => {
return { ...af.copy(), ...af.fragment().copy() };
});
},
clear: function () {
helper.updateDynamicRam("clear", getRamCost("stanek", "clear"));
//checkStanekAPIAccess("clear");
staneksGift.clear();
},
canPlace: function (worldX: any, worldY: any, fragmentId: any): any {
helper.updateDynamicRam("canPlace", getRamCost("stanek", "canPlace"));
//checkStanekAPIAccess("canPlace");
const fragment = FragmentById(fragmentId);
if (!fragment) throw helper.makeRuntimeErrorMsg("stanek.canPlace", `Invalid fragment id: ${fragmentId}`);
return staneksGift.canPlace(worldX, worldY, fragment);
},
place: function (worldX: any, worldY: any, fragmentId: any): any {
helper.updateDynamicRam("place", getRamCost("stanek", "place"));
//checkStanekAPIAccess("place");
const fragment = FragmentById(fragmentId);
if (!fragment) throw helper.makeRuntimeErrorMsg("stanek.place", `Invalid fragment id: ${fragmentId}`);
return staneksGift.place(worldX, worldY, fragment);
},
fragmentAt: function (worldX: any, worldY: any): any {
helper.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 {
helper.updateDynamicRam("deleteAt", getRamCost("stanek", "deleteAt"));
//checkStanekAPIAccess("deleteAt");
return staneksGift.deleteAt(worldX, worldY);
},
};
}

View File

@@ -113,6 +113,7 @@ function startNetscript2Script(workerScript: WorkerScript): Promise<WorkerScript
if (typeof workerScript.env.vars[prop] !== "function") continue;
workerScript.env.vars[prop] = wrap(prop, workerScript.env.vars[prop]);
}
workerScript.env.vars.stanek.charge = wrap("stanek.prop", workerScript.env.vars.stanek.charge);
// Note: the environment that we pass to the JS script only needs to contain the functions visible
// to that script, which env.vars does at this point.

View File

@@ -275,4 +275,7 @@ export interface IPlayer {
setBitNodeNumber(n: number): void;
getMult(name: string): number;
setMult(name: string, mult: number): void;
canAccessCotMG(): boolean;
sourceFileLvl(n: number): number;
}

View File

@@ -282,6 +282,8 @@ export class PlayerObject implements IPlayer {
setBitNodeNumber: (n: number) => void;
getMult: (name: string) => number;
setMult: (name: string, mult: number) => void;
canAccessCotMG: () => boolean;
sourceFileLvl: (n: number) => number;
constructor() {
//Skills and stats
@@ -574,6 +576,9 @@ export class PlayerObject implements IPlayer {
this.getMult = generalMethods.getMult;
this.setMult = generalMethods.setMult;
this.canAccessCotMG = generalMethods.canAccessCotMG;
this.sourceFileLvl = generalMethods.sourceFileLvl;
}
/**

View File

@@ -2655,3 +2655,13 @@ 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;
}
export function sourceFileLvl(this: IPlayer, n: number): number {
const sf = this.sourceFiles.find((sf) => sf.n === n);
if (!sf) return 0;
return sf.lvl;
}

View File

@@ -137,6 +137,12 @@ function prestigeAugmentation(): void {
}
}
if (augmentationExists(AugmentationNames.StaneksGift1) && Augmentations[AugmentationNames.StaneksGift1].owned) {
// TODO(hydroflame): refactor faction names so we don't have to hard
// code strings.
joinFaction(Factions["Church of the Machine God"]);
}
resetPidCounter();
}
@@ -235,6 +241,10 @@ function prestigeSourceFile(flume: boolean): void {
dialogBoxCreate("Visit VitaLife in New Tokyo if you'd like to purchase a new sleeve!");
}
if (Player.bitNodeN === 13) {
dialogBoxCreate("Trouble is brewing in Chongqing");
}
// Reset Stock market, gang, and corporation
if (Player.hasWseAccount) {
initStockMarket();

View File

@@ -9,6 +9,7 @@ import { saveAllServers, loadAllServers } from "./Server/AllServers";
import { Settings } from "./Settings/Settings";
import { SourceFileFlags } from "./SourceFile/SourceFileFlags";
import { loadStockMarket, StockMarket } from "./StockMarket/StockMarket";
import { staneksGift, loadStaneksGift } from "./CotMG/Helper";
import { GameSavedEvents } from "./ui/React/Snackbar";
@@ -35,6 +36,7 @@ class BitburnerSaveObject {
VersionSave = "";
AllGangsSave = "";
LastExportBonus = "";
StaneksGiftSave = "";
getSaveString(): string {
this.PlayerSave = JSON.stringify(Player);
@@ -49,6 +51,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);
}
@@ -166,6 +169,12 @@ function loadGame(saveString: string): boolean {
loadCompanies(saveObj.CompaniesSave);
loadFactions(saveObj.FactionsSave);
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);

View File

@@ -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) {

View File

@@ -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 { KEY } from "../../utils/helpers/keyCodes";
@@ -155,13 +156,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.augmentations.some((aug) => aug.name === AugmentationNames.StaneksGift1);
function clickTerminal(): void {
props.router.toTerminal();
@@ -186,6 +186,10 @@ export function SidebarRoot(props: IProps): React.ReactElement {
props.router.toCreateProgram();
}
function clickStaneksGift(): void {
props.router.toStaneksGift();
}
function clickFactions(): void {
props.router.toFactions();
}
@@ -425,6 +429,25 @@ export function SidebarRoot(props: IProps): React.ReactElement {
</ListItemText>
</ListItem>
)}
{canStaneksGift && (
<ListItem
button
key={"Staneks Gift"}
className={clsx({
[classes.active]: props.page === Page.StaneksGift,
})}
onClick={clickStaneksGift}
>
<ListItemIcon>
<DeveloperBoardIcon color={props.page !== Page.StaneksGift ? "secondary" : "primary"} />
</ListItemIcon>
<ListItemText>
<Typography color={props.page !== Page.StaneksGift ? "secondary" : "primary"}>
Stanek's Gift
</Typography>
</ListItemText>
</ListItem>
)}
</List>
</Collapse>

View File

@@ -204,3 +204,7 @@ SourceFiles["SourceFile12"] = new SourceFile(
12,
<>This Source-File lets the player start with Neuroflux Governor equal to the level of this Source-File.</>,
);
SourceFiles["SourceFile13"] = new SourceFile(
13,
<>Each level of this Source-File increases the size of Stanek's Gift.</>,
);

View File

@@ -163,7 +163,10 @@ export function applySourceFile(srcFile: PlayerOwnedSourceFile): void {
break;
}
case 12: // The Recursion
// No effects, grants neuroflux.
// Grants neuroflux.
break;
case 13: // They're Lunatics
// Grants more space on Stanek's Gift.
break;
default:
console.error(`Invalid source file number: ${srcFile.n}`);

View File

@@ -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";
import { SetupTextEditor } from "./ScriptEditor/ui/ScriptEditorRoot";
@@ -101,6 +102,9 @@ const Engine: {
Player.gang.process(numCycles, Player);
}
// Staneks gift
staneksGift.process(Player, numCycles);
// Corporation
if (Player.corporation instanceof Corporation) {
// Stores cycles in a "buffer". Processed separately using Engine Counters
@@ -329,6 +333,8 @@ const Engine: {
Player.bladeburner.storeCycles(numCyclesOffline);
}
staneksGift.process(Player, numCyclesOffline);
// Sleeves offline progress
for (let i = 0; i < Player.sleeves.length; ++i) {
if (Player.sleeves[i] instanceof Sleeve) {

33
src/engineStyle.ts Normal file
View File

@@ -0,0 +1,33 @@
// These should really be imported with the module that is presenting that UI, but because they very much depend on the
// cascade order, we'll pull them all in here.
import "normalize.css";
import "../css/styles.scss";
import "../css/tooltips.scss";
import "../css/buttons.scss";
import "../css/mainmenu.scss";
import "../css/characteroverview.scss";
import "../css/scripteditor.scss";
import "../css/hacknetnodes.scss";
import "../css/menupages.scss";
import "../css/augmentations.scss";
import "../css/redpill.scss";
import "../css/stockmarket.scss";
import "../css/workinprogress.scss";
import "../css/popupboxes.scss";
import "../css/gameoptions.scss";
import "../css/interactivetutorial.scss";
import "../css/loader.scss";
import "../css/missions.scss";
import "../css/companymanagement.scss";
import "../css/bladeburner.scss";
import "../css/gang.scss";
import "../css/sleeves.scss";
import "../css/resleeving.scss";
import "../css/treant.css";
import "../css/grid.min.css";
import "../css/dev-menu.css";
import "../css/casino.scss";
import "../css/milestones.scss";
import "../css/infiltration.scss";
import "../css/staneksgift.scss";

View File

@@ -61,6 +61,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 = {
toLocation: () => {
throw new Error("Router called before initialization");
},
toStaneksGift: () => {
throw new Error("Router called before initialization");
},
};
function determineStartPage(player: IPlayer): Page {
@@ -267,6 +272,9 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
setLocation(location);
setPage(Page.Location);
},
toStaneksGift: () => {
setPage(Page.StaneksGift);
},
};
useEffect(() => {
@@ -303,6 +311,8 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
<SleeveRoot />
) : page === Page.Stats ? (
<CharacterStats />
) : page === Page.StaneksGift ? (
<StaneksGiftRoot staneksGift={staneksGift} />
) : page === Page.ScriptEditor ? (
<ScriptEditorRoot filename={filename} code={code} player={player} router={Router} />
) : page === Page.ActiveScripts ? (

View File

@@ -638,6 +638,9 @@ export function GameOptionsRoot(props: IProps): React.ReactElement {
<Link href="https://www.reddit.com/r/bitburner" target="_blank">
<Typography>Reddit</Typography>
</Link>
<Link href="https://plaza.dsolver.ca/games/bitburner" target="_blank">
<Typography>Incremental game plaza</Typography>
</Link>
</Box>
</Grid>
</Grid>

View File

@@ -34,6 +34,7 @@ export enum Page {
BladeburnerCinematic,
Location,
Loading,
StaneksGift,
}
/**
@@ -73,4 +74,5 @@ export interface IRouter {
toWork(): void;
toBladeburnerCinematic(): void;
toLocation(location: Location): void;
toStaneksGift(): void;
}

View File

@@ -169,6 +169,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.00");
}
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