update BN13 for new UI

This commit is contained in:
Olivier Gagnon 2021-09-25 17:21:50 -04:00
parent 3aacab504b
commit 793d9b34ce
49 changed files with 1763 additions and 9 deletions

23
css/staneksgift.scss Normal 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;
}

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

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

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

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

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

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

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

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

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

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

@ -2344,6 +2344,142 @@ function initAugmentations(): void {
resetAugmentation(BladesSimulacrum); 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.<br><br>" +
"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"<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,
});
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,
});
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,
});
StaneksGiftAscension4.addToFactions([ChurchOfTheMachineGodFactionName]);
resetAugmentation(StaneksGiftAscension4);
}
// Update costs based on how many have been purchased // Update costs based on how many have been purchased
mult = Math.pow( mult = Math.pow(
CONSTANTS.MultipleAugMultiplier * [1, 0.96, 0.94, 0.93][SourceFileFlags[11]], CONSTANTS.MultipleAugMultiplier * [1, 0.96, 0.94, 0.93][SourceFileFlags[11]],

@ -110,6 +110,11 @@ export const AugmentationNames: IMap<string> = {
BladeArmorIPU: "BLADE-51b Tesla Armor: IPU Upgrade", BladeArmorIPU: "BLADE-51b Tesla Armor: IPU Upgrade",
BladesSimulacrum: "The Blade's Simulacrum", 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 //Wasteland Augs
//PepBoy: "P.E.P-Boy", Plasma Energy Projection System //PepBoy: "P.E.P-Boy", Plasma Energy Projection System
//PepBoyForceField Generates plasma force fields //PepBoyForceField Generates plasma force fields

@ -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.
<br />
<br />
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.
<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 />
This Source-File also increases Stanek's Gift multipliers by:
<br />
<br />
Level 1: 8%
<br />
Level 2: 12%
<br />
Level 3: 14%
<br />
<br />
Each level of this Source-File also increases the size of the gift.
</>
),
);
// Books: Frontera, Shiner // 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["BitNode14"] = new BitNode(14, 2, "", "COMING SOON");
BitNodes["BitNode15"] = new BitNode(15, 2, "", "COMING SOON"); BitNodes["BitNode15"] = new BitNode(15, 2, "", "COMING SOON");
BitNodes["BitNode16"] = new BitNode(16, 2, "", "COMING SOON"); BitNodes["BitNode16"] = new BitNode(16, 2, "", "COMING SOON");
@ -784,6 +824,45 @@ export function initBitNodeMultipliers(p: IPlayer): void {
BitNodeMultipliers.BladeburnerSkillCost = inc; BitNodeMultipliers.BladeburnerSkillCost = inc;
break; 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: default:
console.warn("Player.bitNodeN invalid"); console.warn("Player.bitNodeN invalid");
break; break;

@ -126,7 +126,7 @@ export function BitverseRoot(props: IProps): React.ReactElement {
<pre>O | | | \| | O / _/ | / O | |/ | | | O</pre> <pre>O | | | \| | O / _/ | / O | |/ | | | O</pre>
<pre>| | | |O / | | O / | O O | | \ O| | | |</pre> <pre>| | | |O / | | O / | O O | | \ O| | | |</pre>
<pre>| | |/ \/ / __| | |/ \ | \ | |__ \ \/ \| | |</pre> <pre>| | |/ \/ / __| | |/ \ | \ | |__ \ \/ \| | |</pre>
<pre> \| O | |_/ |\| \ O \__| \_| | O |/ </pre> <pre> \| O | |_/ |\| \ <BitNodePortal n={13} level={nextSourceFileFlags[13]} enter={enter} flume={props.flume} destroyedBitNode={destroyed} /> \__| \_| | O |/ </pre>
<pre> | | |_/ | | \| / | \_| | | </pre> <pre> | | |_/ | | \| / | \_| | | </pre>
<pre> \| / \| | / / \ |/ </pre> <pre> \| / \| | / / \ |/ </pre>
<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} /> | </pre> <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} /> | </pre>

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

290
src/CotMG/Fragment.ts Normal file

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

27
src/CotMG/FragmentType.ts Normal file

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

14
src/CotMG/Helper.tsx Normal 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();
}
}

18
src/CotMG/IStaneksGift.ts Normal file

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

209
src/CotMG/StaneksGift.ts Normal file

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

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

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

18
src/CotMG/ui/Cell.tsx Normal file

@ -0,0 +1,18 @@
import * as React from "react";
type IProps = {
onMouseEnter?: () => void;
onClick?: () => void;
color: string;
};
export function Cell(cellProps: IProps) {
return (
<div
className="staneksgift_cell"
style={{ backgroundColor: cellProps.color }}
onMouseEnter={cellProps.onMouseEnter}
onClick={cellProps.onClick}
/>
);
}

@ -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 (<MuiPaper variant="outlined">
<pre className="text">
ID: N/A<br />
Type: N/A<br />
Magnitude: N/A<br />
Charge: N/A<br />
Heat: N/A<br />
Effect: N/A<br />
[X, Y] N/A<br />
</pre>
</MuiPaper>)
}
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 (<MuiPaper variant="outlined">
<pre className="text">
ID: {props.fragment.id}<br />
Type: {FragmentType[f.type]}<br />
Power: {numeralWrapper.formatStaneksGiftPower(f.power)}<br />
Charge: {charge}<br />
Heat: {heat}<br />
Effect: {effect}<br />
[X, Y] {props.fragment.x}, {props.fragment.y}<br />
</pre>
</MuiPaper>)
}

@ -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 (<>
<ListItem button onClick={()=>props.selectFragment(props.fragment)} style={{backgroundColor: '#222'}}>
<p style={{marginBottom: '1em'}}>
{FragmentType[props.fragment.type]}<br />
power: {numeralWrapper.formatStaneksGiftPower(props.fragment.power)}<br />
{remaining}
</p><br />
<G width={props.fragment.width()}
height={props.fragment.height()}
colorAt={(x, y) => props.fragment.fullAt(x, y) ? "green" : ""} />
</ListItem>
<Divider />
</>)
}
type IProps = {
gift: IStaneksGift;
selectFragment: (fragment: Fragment) => void;
}
export function FragmentSelector(props: IProps) {
return (<List style={{maxHeight: '250px', overflow: 'auto'}}>
<ListItem button onClick={()=>props.selectFragment(NoneFragment)} style={{backgroundColor: '#222'}}>
<p style={{marginBottom: '1em'}}>
None
</p>
</ListItem><Divider />
<ListItem button onClick={()=>props.selectFragment(DeleteFragment)} style={{backgroundColor: '#222'}}>
<p style={{marginBottom: '1em'}}>
Delete
</p>
</ListItem><Divider />
{Fragments.map(fragment => <FragmentOption
key={fragment.id}
gift={props.gift}
selectFragment={props.selectFragment}
fragment={fragment}
/>)}
</List>)
}

26
src/CotMG/ui/G.tsx Normal file

@ -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(<Cell key={i} color={props.colorAt(i, j)} />);
}
elems.push(
<div key={j} className="staneksgift_row">
{cells}
</div>,
);
}
return <div style={{ float: "left" }}>{elems}</div>;
}

141
src/CotMG/ui/Grid.tsx Normal file

@ -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(
<Cell key={i} onMouseEnter={() => moveGhost(i, j)} onClick={() => clickAt(i, j)} color={color(i, j)} />,
);
}
elems.push(
<div key={j} className="staneksgift_row">
{cells}
</div>,
);
}
function updateSelectedFragment(fragment: Fragment): void {
setSelectedFragment(fragment);
const newgrid = zeros([props.gift.width(), props.gift.height()]);
setGhostGrid(newgrid);
}
return (
<div>
<FragmentSelector gift={props.gift} selectFragment={updateSelectedFragment} />
<StdButton onClick={clear} text="Clear" />
<div style={{ float: "left" }}>{elems}</div>
<div>
<FragmentInspector fragment={props.gift.fragmentAt(pos[0], pos[1])} />
</div>
</div>
);
}

@ -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 (
<>
<h1>Stanek's Gift</h1>
<Grid gift={staneksGift} />
</>
);
}

@ -468,4 +468,46 @@ export const FactionInfos: IMap<FactionInfo> = {
false, false,
false, 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,
),
}; };

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

@ -29,6 +29,7 @@ export enum LocationName {
// Chongqing locations // Chongqing locations
ChongqingKuaiGongInternational = "KuaiGong International", ChongqingKuaiGongInternational = "KuaiGong International",
ChongqingSolarisSpaceSystems = "Solaris Space Systems", ChongqingSolarisSpaceSystems = "Solaris Space Systems",
ChongqingChurchOfTheMachineGod = "Church of the Machine God",
// Sector 12 // Sector 12
Sector12AlphaEnterprises = "Alpha Enterprises", Sector12AlphaEnterprises = "Alpha Enterprises",

@ -440,4 +440,9 @@ export const LocationsMetadata: IConstructorParams[] = [
name: LocationName.WorldStockExchange, name: LocationName.WorldStockExchange,
types: [LocationType.StockMarket], types: [LocationType.StockMarket],
}, },
{
city: CityName.Chongqing,
name: LocationName.ChongqingChurchOfTheMachineGod,
types: [LocationType.Special],
},
]; ];

@ -18,10 +18,14 @@ import { Location } from "../Location";
import { CreateCorporationPopup } from "../../Corporation/ui/CreateCorporationPopup"; import { CreateCorporationPopup } from "../../Corporation/ui/CreateCorporationPopup";
import { createPopup } from "../../ui/React/createPopup"; import { createPopup } from "../../ui/React/createPopup";
import { LocationName } from "../data/LocationNames"; 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 { use } from "../../ui/Context";
import { dialogBoxCreate } from "../../ui/React/DialogBox"; import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { RadioButtonUncheckedRounded } from "@mui/icons-material";
type IProps = { type IProps = {
loc: Location; loc: Location;
@ -117,6 +121,109 @@ export function SpecialLocation(props: IProps): React.ReactElement {
return <Button onClick={handleResleeving}>Re-Sleeve</Button>; 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 = <pre>
{" `` "}<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 />
{" `..` "}</pre>
if (player.factions.includes("Church of the Machine God")) {
return (
<div style={{ width: "60%" }}>
<p>
<i className="text">Allison "Mother" Stanek: Welcome back my child!</i>
</p>
{symbol}
</div>
);
}
if (!player.canAccessCotMG()) {
return (
<>
<p>
<i className="text">
A decrepit altar stands in the middle of a dilapidated church.
<br />
<br />A symbol is carved in the altar.
</i>
</p>
<br />
{symbol}
</>
);
}
if (
player.augmentations.filter((a) => a.name !== AugmentationNames.NeuroFluxGovernor).length > 0 ||
player.queuedAugmentations.filter((a) => a.name !== AugmentationNames.NeuroFluxGovernor).length > 0
) {
return (
<div style={{ width: "60%" }}>
<p>
<i className="text">
Allison "Mother" Stanek: Begone you filth! My gift must be the first modification that your body should
have!
</i>
</p>
</div>
);
}
return (
<div style={{ width: "60%" }}>
<p>
<i className="text">
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>
</p>
<Button onClick={handleCotMG}>Accept Stanek's Gift</Button>
{symbol}
</div>
);
}
switch (props.loc.name) { switch (props.loc.name) {
case LocationName.NewTokyoVitaLife: { case LocationName.NewTokyoVitaLife: {
return renderResleeving(); return renderResleeving();
@ -130,6 +237,9 @@ export function SpecialLocation(props: IProps): React.ReactElement {
case LocationName.NewTokyoNoodleBar: { case LocationName.NewTokyoNoodleBar: {
return renderNoodleBar(); return renderNoodleBar();
} }
case LocationName.ChongqingChurchOfTheMachineGod: {
return renderCotMG();
}
default: default:
console.error(`Location ${props.loc.name} doesn't have any special properties`); console.error(`Location ${props.loc.name} doesn't have any special properties`);
return <></>; return <></>;

@ -54,6 +54,15 @@ export const RamCostConstants: IMap<number> = {
ScriptGangApiBaseRamCost: 4, ScriptGangApiBaseRamCost: 4,
ScriptBladeburnerApiBaseRamCost: 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> = { export const RamCosts: IMap<any> = {
@ -313,6 +322,17 @@ export const RamCosts: IMap<any> = {
purchaseSleeveAug: () => RamCostConstants.ScriptSleeveBaseRamCost, 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: { heart: {
// Easter egg function // Easter egg function
break: () => 0, break: () => 0,

@ -171,6 +171,9 @@ import { GangMemberTask } from "./Gang/GangMemberTask";
import { Stock } from "./StockMarket/Stock"; import { Stock } from "./StockMarket/Stock";
import { BaseServer } from "./Server/BaseServer"; import { BaseServer } from "./Server/BaseServer";
import { staneksGift } from "./CotMG/Helper";
import { Fragments, FragmentById } from "./CotMG/Fragment";
const defaultInterpreter = new Interpreter("", () => undefined); const defaultInterpreter = new Interpreter("", () => undefined);
// the acorn interpreter has a bug where it doesn't convert arrays correctly. // 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 // 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)) { if (isNaN(i)) {
throw makeRuntimeErrorMsg(callingFn, "Invalid index specified for Hacknet Node: " + i); throw makeRuntimeErrorMsg(callingFn, "Invalid index specified for Hacknet Node: " + i);
} }
@ -545,6 +548,15 @@ function NetscriptFunctions(workerScript: WorkerScript): NS {
return contract; 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 checkGangApiAccess = function (func: any): void {
const gang = Player.gang; const gang = Player.gang;
if (gang === null) throw new Error("Must have joined 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); return Player.sleeves[sleeveNumber].tryBuyAugmentation(Player, aug);
}, },
}, // End sleeve }, // 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: { formulas: {
basic: { basic: {
calculateSkill: function (exp: any, mult: any = 1): any { calculateSkill: function (exp: any, mult: any = 1): any {

@ -114,6 +114,7 @@ function startNetscript2Script(workerScript: WorkerScript): Promise<WorkerScript
if (typeof workerScript.env.vars[prop] !== "function") continue; if (typeof workerScript.env.vars[prop] !== "function") continue;
workerScript.env.vars[prop] = wrap(prop, workerScript.env.vars[prop]); 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 // 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. // to that script, which env.vars does at this point.

@ -192,7 +192,7 @@ export interface IPlayer {
getNextCompanyPosition(company: Company, entryPosType: CompanyPosition): CompanyPosition | null; getNextCompanyPosition(company: Company, entryPosType: CompanyPosition): CompanyPosition | null;
getUpgradeHomeRamCost(): number; getUpgradeHomeRamCost(): number;
gotoLocation(to: LocationName): boolean; gotoLocation(to: LocationName): boolean;
hasAugmentation(aug: Augmentation): boolean; hasAugmentation(aug: string | Augmentation): boolean;
hasCorporation(): boolean; hasCorporation(): boolean;
hasGangWith(facName: string): boolean; hasGangWith(facName: string): boolean;
hasTorRouter(): boolean; hasTorRouter(): boolean;
@ -274,4 +274,6 @@ export interface IPlayer {
setBitNodeNumber(n: number): void; setBitNodeNumber(n: number): void;
getMult(name: string): number; getMult(name: string): number;
setMult(name: string, mult: number): void; setMult(name: string, mult: number): void;
canAccessCotMG(): boolean;
} }

@ -199,7 +199,7 @@ export class PlayerObject implements IPlayer {
getNextCompanyPosition: (company: Company, entryPosType: CompanyPosition) => CompanyPosition | null; getNextCompanyPosition: (company: Company, entryPosType: CompanyPosition) => CompanyPosition | null;
getUpgradeHomeRamCost: () => number; getUpgradeHomeRamCost: () => number;
gotoLocation: (to: LocationName) => boolean; gotoLocation: (to: LocationName) => boolean;
hasAugmentation: (aug: Augmentation) => boolean; hasAugmentation: (aug: string | Augmentation) => boolean;
hasCorporation: () => boolean; hasCorporation: () => boolean;
hasGangWith: (facName: string) => boolean; hasGangWith: (facName: string) => boolean;
hasTorRouter: () => boolean; hasTorRouter: () => boolean;
@ -281,6 +281,7 @@ export class PlayerObject implements IPlayer {
setBitNodeNumber: (n: number) => void; setBitNodeNumber: (n: number) => void;
getMult: (name: string) => number; getMult: (name: string) => number;
setMult: (name: string, mult: number) => void; setMult: (name: string, mult: number) => void;
canAccessCotMG: () => boolean;
constructor() { constructor() {
//Skills and stats //Skills and stats
@ -575,6 +576,8 @@ export class PlayerObject implements IPlayer {
this.getMult = generalMethods.getMult; this.getMult = generalMethods.getMult;
this.setMult = generalMethods.setMult; this.setMult = generalMethods.setMult;
this.canAccessCotMG = generalMethods.canAccessCotMG;
} }
/** /**

@ -2632,3 +2632,7 @@ export function setMult(this: IPlayer, name: string, mult: number): void {
if (!this.hasOwnProperty(name)) return; if (!this.hasOwnProperty(name)) return;
(this as any)[name] = mult; (this as any)[name] = mult;
} }
export function canAccessCotMG(this: IPlayer): boolean {
return this.bitNodeN === 13 || SourceFileFlags[13] > 0;
}

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

@ -10,6 +10,7 @@ import { Settings } from "./Settings/Settings";
import { loadSpecialServerIps, SpecialServerIps } from "./Server/SpecialServerIps"; import { loadSpecialServerIps, SpecialServerIps } from "./Server/SpecialServerIps";
import { SourceFileFlags } from "./SourceFile/SourceFileFlags"; import { SourceFileFlags } from "./SourceFile/SourceFileFlags";
import { loadStockMarket, StockMarket } from "./StockMarket/StockMarket"; import { loadStockMarket, StockMarket } from "./StockMarket/StockMarket";
import { staneksGift, loadStaneksGift } from "./CotMG/Helper";
import { GameSavedEvents } from "./ui/React/Snackbar"; import { GameSavedEvents } from "./ui/React/Snackbar";
@ -38,6 +39,7 @@ class BitburnerSaveObject {
VersionSave = ""; VersionSave = "";
AllGangsSave = ""; AllGangsSave = "";
LastExportBonus = ""; LastExportBonus = "";
StaneksGiftSave = "";
getSaveString(): string { getSaveString(): string {
this.PlayerSave = JSON.stringify(Player); this.PlayerSave = JSON.stringify(Player);
@ -67,6 +69,7 @@ class BitburnerSaveObject {
this.SettingsSave = JSON.stringify(Settings); this.SettingsSave = JSON.stringify(Settings);
this.VersionSave = JSON.stringify(CONSTANTS.Version); this.VersionSave = JSON.stringify(CONSTANTS.Version);
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus); this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
this.StaneksGiftSave = JSON.stringify(staneksGift);
if (Player.inGang()) { if (Player.inGang()) {
this.AllGangsSave = JSON.stringify(AllGangs); this.AllGangsSave = JSON.stringify(AllGangs);
} }
@ -173,6 +176,12 @@ function loadGame(saveString: string): boolean {
loadFactions(saveObj.FactionsSave); loadFactions(saveObj.FactionsSave);
loadSpecialServerIps(saveObj.SpecialServerIpsSave); 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")) { if (saveObj.hasOwnProperty("AliasesSave")) {
try { try {
loadAliases(saveObj.AliasesSave); loadAliases(saveObj.AliasesSave);

@ -197,6 +197,8 @@ async function parseOnlyRamCalculate(
func = workerScript.env.vars.bladeburner[ref]; func = workerScript.env.vars.bladeburner[ref];
} else if (ref in workerScript.env.vars.codingcontract) { } else if (ref in workerScript.env.vars.codingcontract) {
func = workerScript.env.vars.codingcontract[ref]; 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) { } else if (ref in workerScript.env.vars.gang) {
func = workerScript.env.vars.gang[ref]; func = workerScript.env.vars.gang[ref];
} else if (ref in workerScript.env.vars.sleeve) { } else if (ref in workerScript.env.vars.sleeve) {

@ -49,6 +49,7 @@ import { iTutorialSteps, iTutorialNextStep, ITutorial } from "../../InteractiveT
import { getAvailableCreatePrograms } from "../../Programs/ProgramHelpers"; import { getAvailableCreatePrograms } from "../../Programs/ProgramHelpers";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { redPillFlag } from "../../RedPill"; import { redPillFlag } from "../../RedPill";
import { AugmentationNames } from "../../Augmentation/data/AugmentationNames";
import { inMission } from "../../Missions"; import { inMission } from "../../Missions";
import { KEY } from "../../utils/helpers/keyCodes"; import { KEY } from "../../utils/helpers/keyCodes";
@ -156,13 +157,12 @@ export function SidebarRoot(props: IProps): React.ReactElement {
const canOpenSleeves = props.player.sleeves.length > 0; 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 canCorporation = !!(props.player.corporation as any);
const canGang = !!(props.player.gang as any); const canGang = !!(props.player.gang as any);
const canJob = props.player.companyName !== ""; const canJob = props.player.companyName !== "";
const canStockMarket = props.player.hasWseAccount; const canStockMarket = props.player.hasWseAccount;
const canBladeburner = !!(props.player.bladeburner as any); const canBladeburner = !!(props.player.bladeburner as any);
const canStaneksGift = props.player.hasAugmentation(AugmentationNames.StaneksGift1);
function clickTerminal(): void { function clickTerminal(): void {
props.router.toTerminal(); props.router.toTerminal();
@ -187,6 +187,10 @@ export function SidebarRoot(props: IProps): React.ReactElement {
props.router.toCreateProgram(); props.router.toCreateProgram();
} }
function clickStaneksGift(): void {
props.router.toStaneksGift();
}
function clickFactions(): void { function clickFactions(): void {
props.router.toFactions(); props.router.toFactions();
} }
@ -405,6 +409,25 @@ export function SidebarRoot(props: IProps): React.ReactElement {
</Typography> </Typography>
</ListItemText> </ListItemText>
</ListItem> </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"}>
Staneks Gift
</Typography>
</ListItemText>
</ListItem>
)} */}
{canCreateProgram && ( {canCreateProgram && (
<ListItem <ListItem
button button
@ -426,6 +449,23 @@ export function SidebarRoot(props: IProps): React.ReactElement {
</ListItemText> </ListItemText>
</ListItem> </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"}>StaneksGift</Typography>
</ListItemText>
</ListItem>
)}
</List> </List>
</Collapse> </Collapse>

@ -12,6 +12,7 @@ import { initCompanies } from "./Company/Companies";
import { Corporation } from "./Corporation/Corporation"; import { Corporation } from "./Corporation/Corporation";
import { CONSTANTS } from "./Constants"; import { CONSTANTS } from "./Constants";
import { Factions, initFactions } from "./Faction/Factions"; import { Factions, initFactions } from "./Faction/Factions";
import { staneksGift } from "./CotMG/Helper";
import { processPassiveFactionRepGain, inviteToFaction } from "./Faction/FactionHelpers"; import { processPassiveFactionRepGain, inviteToFaction } from "./Faction/FactionHelpers";
import { Router } from "./ui/GameRoot"; import { Router } from "./ui/GameRoot";
@ -136,6 +137,9 @@ const Engine: {
currMission.process(numCycles); currMission.process(numCycles);
} }
// Staneks gift
staneksGift.process(Player, numCycles);
// Corporation // Corporation
if (Player.corporation instanceof Corporation) { if (Player.corporation instanceof Corporation) {
// Stores cycles in a "buffer". Processed separately using Engine Counters // Stores cycles in a "buffer". Processed separately using Engine Counters

@ -30,3 +30,4 @@ import "../css/dev-menu.css";
import "../css/casino.scss"; import "../css/casino.scss";
import "../css/milestones.scss"; import "../css/milestones.scss";
import "../css/infiltration.scss"; import "../css/infiltration.scss";
import "../css/staneksgift.scss";

@ -62,6 +62,8 @@ import { CharacterStats } from "./CharacterStats";
import { TravelAgencyRoot } from "../Locations/ui/TravelAgencyRoot"; import { TravelAgencyRoot } from "../Locations/ui/TravelAgencyRoot";
import { StockMarketRoot } from "../StockMarket/ui/StockMarketRoot"; import { StockMarketRoot } from "../StockMarket/ui/StockMarketRoot";
import { BitverseRoot } from "../BitNode/ui/BitverseRoot"; import { BitverseRoot } from "../BitNode/ui/BitverseRoot";
import { StaneksGiftRoot } from "../CotMG/ui/StaneksGiftRoot";
import { staneksGift } from "../CotMG/Helper";
import { CharacterOverview } from "./React/CharacterOverview"; import { CharacterOverview } from "./React/CharacterOverview";
import { BladeburnerCinematic } from "../Bladeburner/ui/BladeburnerCinematic"; import { BladeburnerCinematic } from "../Bladeburner/ui/BladeburnerCinematic";
import { workerScripts } from "../Netscript/WorkerScripts"; import { workerScripts } from "../Netscript/WorkerScripts";
@ -178,6 +180,9 @@ export let Router: IRouter = {
toHackingMission: () => { toHackingMission: () => {
throw new Error("Router called before initialization"); throw new Error("Router called before initialization");
}, },
toStaneksGift: () => {
throw new Error("Router called before initialization");
},
}; };
function determineStartPage(player: IPlayer): Page { function determineStartPage(player: IPlayer): Page {
@ -271,6 +276,9 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
setPage(Page.HackingMission); setPage(Page.HackingMission);
setFaction(faction); setFaction(faction);
}, },
toStaneksGift: () => {
setPage(Page.StaneksGift);
},
}; };
useEffect(() => { useEffect(() => {
@ -309,6 +317,8 @@ export function GameRoot({ player, engine, terminal }: IProps): React.ReactEleme
<SleeveRoot player={player} /> <SleeveRoot player={player} />
) : page === Page.Stats ? ( ) : page === Page.Stats ? (
<CharacterStats /> <CharacterStats />
) : page === Page.StaneksGift ? (
<StaneksGiftRoot staneksGift={staneksGift} />
) : page === Page.CreateScript ? ( ) : page === Page.CreateScript ? (
<ScriptEditorRoot filename={filename} code={code} player={player} router={Router} /> <ScriptEditorRoot filename={filename} code={code} player={player} router={Router} />
) : page === Page.ActiveScripts ? ( ) : page === Page.ActiveScripts ? (

@ -35,6 +35,7 @@ export enum Page {
Location, Location,
HackingMission, HackingMission,
Loading, Loading,
StaneksGift,
} }
/** /**
@ -75,4 +76,5 @@ export interface IRouter {
toBladeburnerCinematic(): void; toBladeburnerCinematic(): void;
toLocation(location: Location): void; toLocation(location: Location): void;
toHackingMission(faction: Faction): void; toHackingMission(faction: Faction): void;
toStaneksGift(): void;
} }

@ -165,6 +165,18 @@ class NumeralFormatter {
return this.format(n, "0,0"); 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 { parseMoney(s: string): number {
// numeral library does not handle formats like 1e10 well (returns 110), // numeral library does not handle formats like 1e10 well (returns 110),
// so if both return a valid number, return the biggest one // so if both return a valid number, return the biggest one