This commit is contained in:
Olivier Gagnon 2024-06-09 13:08:29 -04:00
parent c6c2b3ba99
commit 5f2efd94e5
No known key found for this signature in database
GPG Key ID: 0018772EA86FA03F
22 changed files with 1431 additions and 732 deletions

@ -1,55 +0,0 @@
import {
Bot,
Entity,
EntityType,
ContainerEntity,
Item,
Dispenser,
Dock,
Crafter,
Chest,
Wall,
BaseEntity,
EntityID,
} from "@nsdefs";
import { factory } from "./Helper";
export interface Factory {
bits: number;
entities: Entity[];
}
export const distance = (a: Entity, b: Entity) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
export const adjacent = (a: Entity, b: Entity) => distance(a, b) === 1;
export const findBot = (name: string) =>
factory.entities.find((e): e is Bot => e.type === EntityType.Bot && e.name === name);
export const findEntityAt = <T extends Entity>(x: number, y: number, type?: EntityType): T | undefined =>
factory.entities.find((e): e is T => (!type || e.type === type) && e.x === x && e.y === y);
export const bitsMap: Record<Item, number> = {
[Item.BasicR]: 1,
[Item.BasicG]: 1,
[Item.BasicB]: 1,
[Item.ComplexR]: 4,
[Item.ComplexG]: 4,
[Item.ComplexB]: 4,
};
export const isEntityContainer = (entity: BaseEntity): entity is ContainerEntity => "inventory" in entity;
export const isEntityBot = (e: Entity): e is Bot => e.type === EntityType.Bot;
export const isEntityDispenser = (e: Entity): e is Dispenser => e.type === EntityType.Dispenser;
export const isEntityDock = (e: Entity): e is Dock => e.type === EntityType.Dock;
export const isEntityCrafter = (e: Entity): e is Crafter => e.type === EntityType.Crafter;
export const isEntityChest = (e: Entity): e is Chest => e.type === EntityType.Chest;
export const isEntityWall = (e: Entity): e is Wall => e.type === EntityType.Wall;
export const findEntity = (id: EntityID, type?: EntityType): Entity | undefined =>
factory.entities.find(
typeof id === "string"
? (e) => e.name === id && (!type || type === e.type)
: (e) => e.x === id[0] && e.y === id[1] && (!type || type === e.type),
);

@ -1,110 +0,0 @@
import { EntityType, Item } from "@nsdefs";
import { Factory } from "./Factory";
export const factorySize = 12;
const defaultFactory: Factory = {
bits: 0,
entities: [],
};
export const factory: Factory = defaultFactory;
export const NewBot = (name: string, x: number, y: number) => {
factory.entities.push({
type: EntityType.Bot,
x,
y,
inventory: [],
energy: 16,
name,
maxInventory: 1,
});
};
export const NewChest = (name: string, x: number, y: number) => {
factory.entities.push({
name,
type: EntityType.Chest,
inventory: [],
maxInventory: 1,
x,
y,
});
};
export const NewCrafter = (name: string, x: number, y: number) => {
factory.entities.push({
name,
type: EntityType.Crafter,
x,
y,
inventory: [],
maxInventory: 2,
recipe: {
input: [Item.BasicR, Item.BasicR],
output: [Item.ComplexR],
},
});
};
export const NewDispenser = (name: string, x: number, y: number, dispensing: Item) => {
factory.entities.push({
name,
type: EntityType.Dispenser,
x,
y,
dispensing,
cooldown: 10000,
cooldownUntil: 0,
inventory: [dispensing],
maxInventory: 1,
});
};
export const NewDock = (name: string, x: number, y: number) => {
const potential = [Item.BasicR, Item.BasicG, Item.BasicB];
factory.entities.push({
name,
type: EntityType.Dock,
x,
y,
potentialRequest: [Item.BasicR, Item.BasicG, Item.BasicB],
potentialRequestCount: 1,
currentRequest: [potential[Math.floor(Math.random() * potential.length)]],
inventory: [],
maxInventory: 1,
});
};
export const NewWall = (name: string, x: number, y: number) => {
factory.entities.push({
name,
type: EntityType.Wall,
x,
y,
});
};
export const loadFactory = (save: string) => {
if (!save) return;
// const savedFactory = JSON.parse(save);
// Object.assign(factory, savedFactory);
};
export const resetFactory = () => {
factory.bits = 0;
factory.entities = [];
Object.assign(factory, defaultFactory);
NewBot("alice", Math.floor(factorySize / 2), Math.floor(factorySize / 2));
NewDispenser("disp0", Math.floor(factorySize / 4), 0, Item.BasicR);
NewDispenser("disp1", Math.floor(factorySize / 2), 0, Item.BasicG);
NewDispenser("disp2", Math.floor((factorySize * 3) / 4), 0, Item.BasicB);
NewDock("dock0", Math.floor(factorySize / 4), Math.floor(factorySize - 1));
NewDock("dock1", Math.floor(factorySize / 2), Math.floor(factorySize - 1));
NewDock("dock2", Math.floor((factorySize * 3) / 4), Math.floor(factorySize - 1));
NewWall("wall0", 2, 2);
};
resetFactory();

@ -1,35 +0,0 @@
import { EntityType } from "@nsdefs";
export type FactoryFormulaParams = [number, number, number, number];
export const inventoryScale: Record<EntityType, FactoryFormulaParams> = {
[EntityType.Bot]: [8, 0.5, 2, 0],
[EntityType.Dispenser]: [4, 1, 5, 0],
[EntityType.Dock]: [Infinity, Infinity, Infinity, Infinity],
[EntityType.Crafter]: [Infinity, 1, -1, 4095],
[EntityType.Chest]: [1.2, 10, 0, 63],
[EntityType.Wall]: [Infinity, Infinity, Infinity, Infinity],
};
// a^(b*X+c)+d
const exp = (p: FactoryFormulaParams, x: number): number => Math.floor(Math.pow(p[0], p[1] * x + p[2]) + p[3]);
export const upgradeMaxInventoryCost = (type: EntityType, currentMaxInventory: number): number =>
exp(inventoryScale[type], currentMaxInventory);
export const botPrice = (currentBots: number): number => Math.pow(2, currentBots + 3);
/**
glitches:
1 - moving dock & dispensers
2 - energy consumption
3 - ugrade degradation
4 - power hinderance (speed & transfer)
5 - dexterity hinderance (craft & build)
6 - dock complexity
7 - random walls
*/

@ -1,153 +0,0 @@
import React from "react";
import SmartToyIcon from "@mui/icons-material/SmartToy";
import { styled } from "@mui/system";
import { Dispenser, Entity, EntityType, Item } from "@nsdefs";
import MoveToInboxIcon from "@mui/icons-material/MoveToInbox";
import OutboxIcon from "@mui/icons-material/Outbox";
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank";
import PrecisionManufacturingIcon from "@mui/icons-material/PrecisionManufacturing";
import BlockIcon from "@mui/icons-material/Block";
import { Tooltip, Typography } from "@mui/material";
import { isEntityContainer } from "../Factory";
export const cellSize = 48;
const defaultIconStyle = {
width: cellSize + "px",
height: cellSize + "px",
color: "white",
};
const colorRed = "red";
const colorBlue = "#1E90FF";
const colorGreen = "#7CFC00";
const itemColorMap: Record<Item, string> = {
[Item.BasicR]: colorRed,
[Item.BasicB]: colorBlue,
[Item.BasicG]: colorGreen,
[Item.ComplexR]: colorRed,
[Item.ComplexB]: colorBlue,
[Item.ComplexG]: colorGreen,
};
const WallIcon = styled(BlockIcon)(defaultIconStyle);
const BotIcon = styled(SmartToyIcon)(defaultIconStyle);
const DispenserIcon = styled(OutboxIcon)((props: { dispenser: Dispenser; col: string }) => ({
...defaultIconStyle,
color: new Date().getTime() > props.dispenser.cooldownUntil ? props.col : "gray",
}));
const DockIcon = styled(MoveToInboxIcon)(defaultIconStyle);
const CrafterIcon = styled(PrecisionManufacturingIcon)(defaultIconStyle);
const ChestIcon = styled(CheckBoxOutlineBlankIcon)(defaultIconStyle);
interface ITooltipContentProps {
entity: Entity;
content: React.ReactElement;
}
const TooltipContent = ({ entity, content }: ITooltipContentProps): React.ReactElement => {
return (
<>
<Typography>
{entity.name} ({entity.type})
</Typography>
{content}
</>
);
};
const TooltipInventory = ({ entity }: { entity: Entity }): React.ReactElement => {
if (!isEntityContainer(entity)) return <></>;
return (
<Typography component="span">
{entity.inventory.map((item) => (
<span key={item} style={{ color: itemColorMap[item] }}>
{item}
</span>
))}
</Typography>
);
};
export const EntityIcon = ({ entity }: { entity: Entity }): React.ReactElement => {
switch (entity.type) {
case EntityType.Wall: {
return <WallIcon />;
}
case EntityType.Chest: {
return (
<Tooltip title={<TooltipContent entity={entity} content={<TooltipInventory entity={entity} />} />}>
<ChestIcon />
</Tooltip>
);
}
case EntityType.Bot: {
return (
<Tooltip
title={
<Typography>
{entity.name}
<br /> <TooltipInventory entity={entity} />
</Typography>
}
>
<BotIcon />
</Tooltip>
);
}
case EntityType.Dispenser: {
return (
<Tooltip
title={
<>
<TooltipContent entity={entity} content={<Typography>{`dispensing: ${entity.dispensing}`}</Typography>} />
<TooltipInventory entity={entity} />
</>
}
>
<DispenserIcon dispenser={entity} col={itemColorMap[entity.dispensing]} />
</Tooltip>
);
break;
}
case EntityType.Dock: {
return (
<Tooltip
title={
<TooltipContent
entity={entity}
content={<Typography>{`requesting: ${entity.currentRequest}`}</Typography>}
/>
}
>
<DockIcon sx={{ color: itemColorMap[entity.currentRequest[0] ?? Item.BasicR] }} />
</Tooltip>
);
}
case EntityType.Crafter: {
return (
<Tooltip
title={
<TooltipContent
entity={entity}
content={
<>
<Typography>{`input: ${entity.recipe.input}, output: ${entity.recipe.output}`}</Typography>
<TooltipInventory entity={entity} />
</>
}
/>
}
>
<CrafterIcon />
</Tooltip>
);
break;
}
}
return <></>;
};

119
src/Myrian/Helper.tsx Normal file

@ -0,0 +1,119 @@
import { DeviceType, Component, Lock } from "@nsdefs";
import { Myrian } from "./Myrian";
import { getNextISocketRequest } from "./formulas/formulas";
export const myrianSize = 12;
const defaultMyrian: Myrian = {
vulns: 0,
totalVulns: 0,
devices: [],
};
export const myrian: Myrian = defaultMyrian;
export const NewBus = (name: string, x: number, y: number) => {
myrian.devices.push({
name,
type: DeviceType.Bus,
busy: false,
x,
y,
content: [],
maxContent: 1,
moveLvl: 1,
transferLvl: 1,
reduceLvl: 1,
installLvl: 1,
// energy: 16,
});
};
export const NewCache = (name: string, x: number, y: number) => {
myrian.devices.push({
name,
type: DeviceType.Cache,
busy: false,
content: [],
maxContent: 1,
x,
y,
});
};
export const NewReducer = (name: string, x: number, y: number) => {
myrian.devices.push({
name,
type: DeviceType.Reducer,
busy: false,
x,
y,
content: [],
maxContent: 2,
tier: 0,
});
};
export const NewISocket = (name: string, x: number, y: number, dispensing: Component) => {
myrian.devices.push({
name,
type: DeviceType.ISocket,
busy: false,
x,
y,
emitting: dispensing,
cooldown: 10000,
cooldownUntil: 0,
content: [dispensing],
maxContent: 1,
});
};
export const NewOSocket = (name: string, x: number, y: number) => {
myrian.devices.push({
name,
type: DeviceType.OSocket,
busy: false,
tier: 0,
x,
y,
currentRequest: getNextISocketRequest(0),
content: [],
maxContent: 1,
});
};
export const NewLock = (name: string, x: number, y: number) => {
const lock: Lock = {
name,
type: DeviceType.Lock,
busy: false,
x,
y,
};
myrian.devices.push(lock);
return lock;
};
export const loadMyrian = (save: string) => {
if (!save) return;
// const savedFactory = JSON.parse(save);
// Object.assign(factory, savedFactory);
};
export const resetMyrian = () => {
myrian.vulns = 0;
myrian.devices = [];
Object.assign(myrian, defaultMyrian);
NewBus("alice", Math.floor(myrianSize / 2), Math.floor(myrianSize / 2));
NewISocket("disp0", Math.floor(myrianSize / 4), 0, Component.R0);
NewISocket("disp1", Math.floor(myrianSize / 2), 0, Component.Y1);
NewISocket("disp2", Math.floor((myrianSize * 3) / 4), 0, Component.B0);
NewOSocket("dock0", Math.floor(myrianSize / 4), Math.floor(myrianSize - 1));
NewOSocket("dock1", Math.floor(myrianSize / 2), Math.floor(myrianSize - 1));
NewOSocket("dock2", Math.floor((myrianSize * 3) / 4), Math.floor(myrianSize - 1));
};
resetMyrian();

125
src/Myrian/Myrian.ts Normal file

@ -0,0 +1,125 @@
import {
Bus,
Device,
DeviceType,
ContainerDevice,
Component,
ISocket,
OSocket,
Reducer,
Cache,
Lock,
BaseDevice,
DeviceID,
} from "@nsdefs";
import { myrian, myrianSize } from "./Helper";
export interface Myrian {
vulns: number;
totalVulns: number;
devices: Device[];
}
export const distance = (a: Device, b: Device) => Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
export const distanceCoord2D = (a: Device, coord: [number, number]) =>
Math.abs(a.x - coord[0]) + Math.abs(a.y - coord[1]);
export const adjacent = (a: Device, b: Device) => distance(a, b) === 1;
export const adjacentCoord2D = (a: Device, coord: [number, number]) => distanceCoord2D(a, coord) === 1;
export const inventoryMatches = (a: Component[], b: Component[]) => {
if (a.length != b.length) return false;
return a.every((i) => b.includes(i));
};
export const inMyrianBounds = (x: number, y: number) => x >= 0 && x < myrianSize && y >= 0 && y < myrianSize;
export const vulnsMap: Record<Component, number> = {
// tier 0
[Component.R0]: 1,
[Component.G0]: 1,
[Component.B0]: 1,
// tier 1
[Component.R1]: 4,
[Component.G1]: 4,
[Component.B1]: 4,
[Component.Y1]: 4,
[Component.C1]: 4,
[Component.M1]: 4,
// tier 2
[Component.R2]: 16,
[Component.G2]: 16,
[Component.B2]: 16,
[Component.Y2]: 16,
[Component.C2]: 16,
[Component.M2]: 16,
[Component.W2]: 16,
// tier 3
[Component.R3]: 64,
[Component.G3]: 64,
[Component.B3]: 64,
[Component.Y3]: 64,
[Component.C3]: 64,
[Component.M3]: 64,
[Component.W3]: 64,
// tier 4
[Component.R4]: 256,
[Component.G4]: 256,
[Component.B4]: 256,
[Component.Y4]: 256,
[Component.C4]: 256,
[Component.M4]: 256,
[Component.W4]: 256,
// tier 5
[Component.R5]: 1024,
[Component.G5]: 1024,
[Component.B5]: 1024,
[Component.Y5]: 1024,
[Component.C5]: 1024,
[Component.M5]: 1024,
[Component.W5]: 1024,
// tier 6
[Component.Y6]: 4096,
[Component.C6]: 4096,
[Component.M6]: 4096,
[Component.W6]: 4096,
// tier 7
[Component.W7]: 16384,
};
export const isDeviceContainer = (device: BaseDevice): device is ContainerDevice => "inventory" in device;
export const isDeviceBus = (d: Device): d is Bus => d.type === DeviceType.Bus;
export const isDeviceISocket = (d: Device): d is ISocket => d.type === DeviceType.ISocket;
export const isDeviceOSocket = (d: Device): d is OSocket => d.type === DeviceType.OSocket;
export const isDeviceReducer = (d: Device): d is Reducer => d.type === DeviceType.Reducer;
export const isDeviceCache = (d: Device): d is Cache => d.type === DeviceType.Cache;
export const isDeviceLock = (d: Device): d is Lock => d.type === DeviceType.Lock;
export const findDevice = (id: DeviceID, type?: DeviceType): Device | undefined =>
myrian.devices.find(
(e) => (typeof id === "string" ? e.name === id : e.x === id[0] && e.y === id[1]) && (!type || type === e.type),
);
export const removeDevice = (id: DeviceID, type?: DeviceType) => {
myrian.devices = myrian.devices.filter(
(e) => !((typeof id === "string" ? e.name === id : e.x === id[0] && e.y === id[1]) && (!type || type === e.type)),
);
};

@ -0,0 +1,12 @@
import { Component } from "@nsdefs";
export const componentTiers = [
[Component.R0, Component.G0, Component.B0],
[Component.R1, Component.G1, Component.B1, Component.Y1, Component.C1, Component.M1],
[Component.R2, Component.G2, Component.B2, Component.Y2, Component.C2, Component.M2, Component.W2],
[Component.R3, Component.G3, Component.B3, Component.Y3, Component.C3, Component.M3, Component.W3],
[Component.R4, Component.G4, Component.B4, Component.Y4, Component.C4, Component.M4, Component.W4],
[Component.R5, Component.G5, Component.B5, Component.Y5, Component.C5, Component.M5, Component.W5],
[Component.Y6, Component.C6, Component.M6, Component.W6],
[Component.W7],
];

@ -0,0 +1,71 @@
import { DeviceType } from "@nsdefs";
import { myrian } from "../Helper";
import { componentTiers } from "./components";
export type FactoryFormulaParams = [number, number, number, number];
export const maxContentScale: Record<DeviceType, FactoryFormulaParams> = {
[DeviceType.Bus]: [8, 0.5, 2, 0],
[DeviceType.ISocket]: [4, 1, 5, 0],
[DeviceType.OSocket]: [Infinity, Infinity, Infinity, Infinity],
[DeviceType.Reducer]: [Infinity, 1, -1, 4095],
[DeviceType.Cache]: [1.2, 10, 0, 63],
[DeviceType.Lock]: [Infinity, Infinity, Infinity, Infinity],
};
// a^(b*X+c)+d
const exp = (p: FactoryFormulaParams, x: number): number => Math.pow(p[0], p[1] * x + p[2]) + p[3];
export const upgradeMaxContentCost = (type: DeviceType, currentMaxContent: number): number =>
exp(maxContentScale[type], currentMaxContent);
export const busPrice = (currentBusses: number): number => Math.pow(2, currentBusses + 3);
export const moveSpeed = (level: number) => 1000 / (level + 9);
export const reduceSpeed = (level: number) => 50000 / (level + 9);
export const transferSpeed = (level: number) => 1000 / (level + 9);
export const installSpeed = (level: number) => 100000 / (level + 9);
const countDevices = (type: DeviceType) => myrian.devices.reduce((acc, d) => (d.type === type ? acc + 1 : acc), 0);
export const deviceScale: Record<DeviceType, FactoryFormulaParams> = {
[DeviceType.Bus]: [4, 0.5, 2, 0],
[DeviceType.ISocket]: [2, 1, 4, 0],
[DeviceType.OSocket]: [4, 1, 3, 0],
[DeviceType.Reducer]: [1.5, 1, 2, 0],
[DeviceType.Cache]: [1.2, 10, 0, 63],
[DeviceType.Lock]: [Infinity, Infinity, Infinity, Infinity],
};
export const deviceCost = (type: DeviceType, count?: number) =>
exp(deviceScale[type], count === undefined ? countDevices(type) : count);
export const getNextISocketRequest = (tier: number) => {
const potential = componentTiers.slice(0, tier + 1).flat();
return new Array(Math.floor(Math.pow(Math.random() * tier, 0.75) + 1))
.fill(null)
.map(() => potential[Math.floor(Math.random() * potential.length)]);
};
export const tierScale: Record<DeviceType, FactoryFormulaParams> = {
[DeviceType.Bus]: [Infinity, Infinity, Infinity, Infinity],
[DeviceType.ISocket]: [Infinity, Infinity, Infinity, Infinity],
[DeviceType.OSocket]: [2, 1, 3, 0],
[DeviceType.Reducer]: [1.5, 1, 2, 0],
[DeviceType.Cache]: [Infinity, Infinity, Infinity, Infinity],
[DeviceType.Lock]: [Infinity, Infinity, Infinity, Infinity],
};
/**
glitches:
- random walls (higher level more randomly spawning walls, level 0 is no walls)
- moving dock & dispensers (higher level move faster, level 0 does not move)
- dock complexity (higher level more complex, level 0 is repeating request)
- energy consumption (higher level consume more, level 0 is no consumption)
- ugrade degradation (higher level degrade faster, level 0 does not degrade)
- move hinderance (speed) (higher level slower, level 0 is no hinderance)
- connection hinderance (transfer / charge) (higher level slower, level 0 is immediate transfer speed and charge)
- allocation hinderance (craft & build) (higher level slower, level 0 is no hinderance)
special requests like "has red" that increases the reward
*/

@ -0,0 +1,214 @@
import { Component, Recipe } from "@nsdefs";
export const Tier1Recipes: Recipe[] = [
{
input: [Component.R0, Component.R0],
output: Component.R1,
},
{
input: [Component.G0, Component.G0],
output: Component.G1,
},
{
input: [Component.B0, Component.B0],
output: Component.B1,
},
{
input: [Component.R0, Component.G0],
output: Component.Y1,
},
{
input: [Component.G0, Component.B0],
output: Component.C1,
},
{
input: [Component.B0, Component.R0],
output: Component.M1,
},
];
export const Tier2Recipes: Recipe[] = [
// primary
{
input: [Component.R1, Component.R1],
output: Component.R2,
},
{
input: [Component.G1, Component.G1],
output: Component.G2,
},
{
input: [Component.B1, Component.B1],
output: Component.B2,
},
// secondary
{
input: [Component.R1, Component.G1],
output: Component.Y2,
},
{
input: [Component.G1, Component.B1],
output: Component.C2,
},
{
input: [Component.B1, Component.R1],
output: Component.M2,
},
// white
{
input: [Component.Y1, Component.C1, Component.M1],
output: Component.W2,
},
];
export const Tier3Recipes: Recipe[] = [
// primary
{
input: [Component.R2, Component.R2],
output: Component.R3,
},
{
input: [Component.G2, Component.G2],
output: Component.G3,
},
{
input: [Component.B2, Component.B2],
output: Component.B3,
},
// secondary
{
input: [Component.R2, Component.G2],
output: Component.Y3,
},
{
input: [Component.G2, Component.B2],
output: Component.C3,
},
{
input: [Component.B2, Component.R2],
output: Component.M3,
},
// white
{
input: [Component.Y2, Component.C2, Component.M2],
output: Component.W3,
},
];
export const Tier4Recipes: Recipe[] = [
// primary
{
input: [Component.R3, Component.R3],
output: Component.R4,
},
{
input: [Component.G3, Component.G3],
output: Component.G4,
},
{
input: [Component.B3, Component.B3],
output: Component.B4,
},
// secondary
{
input: [Component.R3, Component.G3],
output: Component.Y4,
},
{
input: [Component.G3, Component.B3],
output: Component.C4,
},
{
input: [Component.B3, Component.R3],
output: Component.M4,
},
// white
{
input: [Component.Y3, Component.C3, Component.M3],
output: Component.W4,
},
];
export const Tier5Recipes: Recipe[] = [
// primary
{
input: [Component.R4, Component.R4],
output: Component.R5,
},
{
input: [Component.G4, Component.G4],
output: Component.G5,
},
{
input: [Component.B4, Component.B4],
output: Component.B5,
},
// secondary
{
input: [Component.R4, Component.G4],
output: Component.Y5,
},
{
input: [Component.G4, Component.B4],
output: Component.C5,
},
{
input: [Component.B4, Component.R4],
output: Component.M5,
},
// white
{
input: [Component.Y4, Component.C4, Component.M4],
output: Component.W5,
},
];
export const Tier6Recipes: Recipe[] = [
// secondary
{
input: [Component.R5, Component.G5],
output: Component.Y6,
},
{
input: [Component.G5, Component.B5],
output: Component.C6,
},
{
input: [Component.B5, Component.R5],
output: Component.M6,
},
// white
{
input: [Component.Y5, Component.C5, Component.M5],
output: Component.W6,
},
];
export const Tier7Recipes: Recipe[] = [
// white
{
input: [Component.Y6, Component.C6, Component.M6],
output: Component.W7,
},
];
export const recipes: Recipe[][] = [
[],
Tier1Recipes,
Tier2Recipes,
Tier3Recipes,
Tier4Recipes,
Tier5Recipes,
Tier6Recipes,
Tier7Recipes,
];

56
src/Myrian/tutorial.md Normal file

@ -0,0 +1,56 @@
# Myrian
Myrian is the name of the OS that the BitNodes run on. Eventually you'll unlock the mechanic by going to the glitch in Ishima and "breaking partially free" of the BN.
By gaining access to the OS directly you can start to break it apart by generating vulnerabilities (vulns).
You do so by interracting with the various devices in the OS, represented by a grid.
## Devices
### Bus
The most important device is the Bus. Here's a few of the things it can do:
- move, only 1 tile at a time and that tile must be empty. No diagonal.
- transfer content, most entity can store items called components, bus are the only device that can transfer components to and from other devices
- use other devices, certain devices can be used.
Performing any action makes all devices involved "busy". If a Bus is transfering content between itself and a cache (chest), they both become busy until the operation finishes.
Contrary to every other mechanic in the game. Async functions using myrian functions CAN run simultenaously.
### ISocket
These devices produce basic components that can be used for other devices, [r0, y0, b0]. They must be picked up by busses and will eventually produce another one after a certain cooldown has passed.
### OSocket
These devices request components and produce vulns in return, a bus simply needs to transfer a component into a OSocket content in order to fulfill the request
### Reducer
These devices can be used by a bus, when being used they will first check their content, if the content matches one of the recipe they will take some time to consume their content in order to produce a new, upgraded, more valuable component, e.g. r0 + r0 => r1
### Cache
These devices act as storage for components.
### Lock
These devices cannot be installed. They appear after various conditions are fulfilled in order to block certain tiles.
## Installing
Bus can install new devices, when they do so a lock will appear over the tile that will eventually become the device. The cost of any device depends on the number of that type of device currently in the OS.
### Uninstalling
A bus can remove a device, there is no refund.
## Tiers
Currently 2 devices have tiers, reducers and OSockets.
Upgrading a reducer allows it to reduce components of a higher tier and ONLY that higher tier. A tier 2 reducer can only tier 2 components like r1 + r1 => r2 and loses access to r0 + r0 => r1
Upgrading a OSocket allows it to request higher tier components (as well as more components at a time).

@ -0,0 +1,218 @@
import React from "react";
import DirectionsBusIcon from "@mui/icons-material/DirectionsBus";
import { styled } from "@mui/system";
import { ISocket, Device, DeviceType, Component } from "@nsdefs";
import MoveToInboxIcon from "@mui/icons-material/MoveToInbox";
import OutboxIcon from "@mui/icons-material/Outbox";
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank";
import MergeTypeIcon from "@mui/icons-material/MergeType";
import BlockIcon from "@mui/icons-material/Block";
import { Tooltip, Typography } from "@mui/material";
import { isDeviceContainer } from "../Myrian";
export const cellSize = 48;
const defaultIconStyle = {
width: cellSize + "px",
height: cellSize + "px",
color: "white",
};
const ColorR = "red";
const ColorG = "#7CFC00";
const ColorB = "#1E90FF";
const ColorY = "yellow";
const ColorC = "cyan";
const ColorM = "magenta";
const ColorW = "whte";
const itemColorMap: Record<Component, string> = {
// tier 0
[Component.R0]: ColorR,
[Component.G0]: ColorG,
[Component.B0]: ColorB,
// tier 1
[Component.R1]: ColorR,
[Component.G1]: ColorG,
[Component.B1]: ColorB,
[Component.Y1]: ColorY,
[Component.C1]: ColorC,
[Component.M1]: ColorM,
// tier 2
[Component.R2]: ColorR,
[Component.G2]: ColorG,
[Component.B2]: ColorB,
[Component.Y2]: ColorY,
[Component.C2]: ColorC,
[Component.M2]: ColorM,
[Component.W2]: ColorW,
// tier 3
[Component.R3]: ColorR,
[Component.G3]: ColorG,
[Component.B3]: ColorB,
[Component.Y3]: ColorY,
[Component.C3]: ColorC,
[Component.M3]: ColorM,
[Component.W3]: ColorW,
// tier 4
[Component.R4]: ColorR,
[Component.G4]: ColorG,
[Component.B4]: ColorB,
[Component.Y4]: ColorY,
[Component.C4]: ColorC,
[Component.M4]: ColorM,
[Component.W4]: ColorW,
// tier 5
[Component.R5]: ColorR,
[Component.G5]: ColorG,
[Component.B5]: ColorB,
[Component.Y5]: ColorY,
[Component.C5]: ColorC,
[Component.M5]: ColorM,
[Component.W5]: ColorW,
// tier 6
[Component.Y6]: ColorY,
[Component.C6]: ColorC,
[Component.M6]: ColorM,
[Component.W6]: ColorW,
// tier 7
[Component.W7]: ColorW,
};
const LockIcon = styled(BlockIcon)(defaultIconStyle);
const BusIcon = styled(DirectionsBusIcon)(defaultIconStyle);
const ISocketIcon = styled(OutboxIcon)((props: { dispenser: ISocket; col: string }) => ({
...defaultIconStyle,
color: new Date().getTime() > props.dispenser.cooldownUntil ? props.col : "gray",
}));
const OSocketIcon = styled(MoveToInboxIcon)(defaultIconStyle);
const ReducerIcon = styled(MergeTypeIcon)(defaultIconStyle);
const CacheIcon = styled(CheckBoxOutlineBlankIcon)(defaultIconStyle);
interface ITooltipContentProps {
device: Device;
content: React.ReactElement;
}
const TooltipContent = ({ device, content }: ITooltipContentProps): React.ReactElement => {
return (
<>
<Typography>
{device.name} ({device.type})
</Typography>
{content}
</>
);
};
const TooltipInventory = ({ device }: { device: Device }): React.ReactElement => {
if (!isDeviceContainer(device)) return <></>;
return (
<Typography component="span">
{device.content.map((item) => (
<span key={item} style={{ color: itemColorMap[item] }}>
{item}
</span>
))}
</Typography>
);
};
export const DeviceIcon = ({ device }: { device: Device }): React.ReactElement => {
switch (device.type) {
case DeviceType.Lock: {
return <LockIcon />;
}
case DeviceType.Cache: {
return (
<Tooltip title={<TooltipContent device={device} content={<TooltipInventory device={device} />} />}>
<CacheIcon />
</Tooltip>
);
}
case DeviceType.Bus: {
return (
<Tooltip
title={
<Typography>
{device.name}
<br /> <TooltipInventory device={device} />
</Typography>
}
>
<BusIcon />
</Tooltip>
);
}
case DeviceType.ISocket: {
return (
<Tooltip
title={
<>
<TooltipContent device={device} content={<Typography>{`dispensing: ${device.emitting}`}</Typography>} />
<TooltipInventory device={device} />
</>
}
>
<ISocketIcon dispenser={device} col={itemColorMap[device.emitting]} />
</Tooltip>
);
break;
}
case DeviceType.OSocket: {
return (
<Tooltip
title={
<TooltipContent
device={device}
content={<Typography>{`requesting: ${device.currentRequest}`}</Typography>}
/>
}
>
<OSocketIcon sx={{ color: itemColorMap[device.currentRequest[0] ?? Component.R0] }} />
</Tooltip>
);
}
case DeviceType.Reducer: {
return (
<Tooltip
title={
<TooltipContent
device={device}
content={
<>
<Typography>Tier: {device.tier}</Typography>
<TooltipInventory device={device} />
</>
}
/>
}
>
<ReducerIcon />
</Tooltip>
);
break;
}
}
return <></>;
};

@ -2,9 +2,9 @@ import React from "react";
import { Container, Typography } from "@mui/material";
import { styled } from "@mui/system";
import { factory, factorySize } from "../Helper";
import { myrian, myrianSize } from "../Helper";
import { useRerender } from "../../ui/React/hooks";
import { EntityIcon, cellSize } from "./EntityIcon";
import { DeviceIcon, cellSize } from "./DeviceIcon";
const CellD = styled("div")({
width: cellSize + "px",
@ -17,8 +17,8 @@ const CellD = styled("div")({
});
const Cell = ({ x, y }: { x: number; y: number }): React.ReactElement => {
const entity = factory.entities.find((e) => e.x === x && e.y === y);
return <CellD>{entity && <EntityIcon entity={entity} />}</CellD>;
const device = myrian.devices.find((e) => e.x === x && e.y === y);
return <CellD>{device && <DeviceIcon device={device} />}</CellD>;
};
const RowD = styled("div")({
@ -33,7 +33,7 @@ interface IColProps {
const Col = ({ x }: IColProps): React.ReactElement => {
return (
<RowD>
{new Array(factorySize + 1).fill(null).map((_, y) => {
{new Array(myrianSize + 1).fill(null).map((_, y) => {
if (y === 0)
return (
<CellD
@ -62,7 +62,7 @@ const Table = styled("div")({
const YHeader = () => {
return (
<RowD>
{new Array(factorySize + 1).fill(null).map((_, y) => {
{new Array(myrianSize + 1).fill(null).map((_, y) => {
return (
<CellD
sx={{ display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "#00000000" }}
@ -78,14 +78,17 @@ const YHeader = () => {
interface IProps {}
export const FactoryRoot = (__props: IProps): React.ReactElement => {
export const MyrianRoot = (__props: IProps): React.ReactElement => {
useRerender(200);
return (
<Container maxWidth="lg" disableGutters sx={{ mx: 0 }}>
<Typography variant="h4">Factory</Typography>
<Typography variant="h4">Myrian OS</Typography>
<Typography>
{myrian.vulns} vulns : {myrian.totalVulns} total vulns
</Typography>
<div style={{ display: "flex" }}>
<Table>
{new Array(factorySize + 1).fill(null).map((_, x) => {
{new Array(myrianSize + 1).fill(null).map((_, x) => {
if (x === 0) return <YHeader key={x} />;
return <Col key={x} x={x} />;
})}

@ -4,7 +4,7 @@ import type {
Person as IPerson,
Server as IServer,
ScriptArg,
EntityID,
DeviceID,
} from "@nsdefs";
import React from "react";
@ -81,7 +81,7 @@ export const helpers = {
gangTask,
log,
coord2d,
entityID,
deviceID: entityID,
filePath,
scriptPath,
getRunningScript,
@ -169,11 +169,11 @@ function coord2d(ctx: NetscriptContext, argName: string, v: unknown): [number, n
return v;
}
function isEntityID(v: unknown): v is EntityID {
function isEntityID(v: unknown): v is DeviceID {
return typeof v === "string" || isCoord2D(v);
}
function entityID(ctx: NetscriptContext, argName: string, v: unknown): EntityID {
function entityID(ctx: NetscriptContext, argName: string, v: unknown): DeviceID {
if (!isEntityID(v)) throw errorMessage(ctx, `${argName} should be string | [number, number], was ${v}`, "TYPE");
return v;
}
@ -328,7 +328,7 @@ function checkEnvFlags(ctx: NetscriptContext): void {
}
/** Set a timeout for performing a task, mark the script as busy in the meantime. */
function netscriptDelay(ctx: NetscriptContext, time: number): Promise<void> {
function netscriptDelay(ctx: NetscriptContext, time: number, ignoreOthers?: boolean): Promise<void> {
const ws = ctx.workerScript;
return new Promise(function (resolve, reject) {
ws.delay = window.setTimeout(() => {
@ -339,7 +339,7 @@ function netscriptDelay(ctx: NetscriptContext, time: number): Promise<void> {
else resolve();
}, time);
ws.delayReject = reject;
ws.env.runningFn = ctx.function;
if (ignoreOthers) ws.env.runningFn = ctx.function;
});
}

@ -367,7 +367,7 @@ const stanek = {
acceptGift: RamCostConstants.StanekAcceptGift,
} as const;
const factory: any = new Proxy(
const myrian: any = new Proxy(
{},
{
get() {
@ -478,7 +478,7 @@ export const RamCosts: RamCostTree<NSFull> = {
codingcontract,
sleeve,
stanek,
factory,
myrian,
ui,
grafting,

@ -108,7 +108,7 @@ import { getEnumHelper } from "./utils/EnumHelper";
import { setDeprecatedProperties, deprecationWarning } from "./utils/DeprecationHelper";
import { ServerConstants } from "./Server/data/Constants";
import { assertFunction } from "./Netscript/TypeAssertion";
import { NetscriptFactory } from "./NetscriptFunctions/Factory";
import { NetscriptMyrian } from "./NetscriptFunctions/Myrian";
export const enums: NSEnums = {
CityName,
@ -136,7 +136,7 @@ export const ns: InternalAPI<NSFull> = {
sleeve: NetscriptSleeve(),
corporation: NetscriptCorporation(),
stanek: NetscriptStanek(),
factory: NetscriptFactory(),
myrian: NetscriptMyrian(),
infiltration: NetscriptInfiltration(),
ui: NetscriptUserInterface(),
formulas: NetscriptFormulas(),

@ -1,222 +0,0 @@
import { Bot, Factory as IFactory, EntityType, Item, Crafter } from "@nsdefs";
import { InternalAPI } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers";
import { NewBot, factory, factorySize, resetFactory } from "../Factory/Helper";
import { adjacent, bitsMap, findEntity, isEntityContainer } from "../Factory/Factory";
import { botPrice, upgradeMaxInventoryCost } from "../Factory/formulas/formulas";
export function NetscriptFactory(): InternalAPI<IFactory> {
return {
getBits: () => () => factory.bits,
getBotPrice: () => () => {
const botCount = factory.entities.reduce((acc, e) => (e.type === EntityType.Bot ? acc + 1 : acc), 0);
return botPrice(botCount);
},
purchaseBot: (ctx) => (_name, _coord) => {
const name = helpers.string(ctx, "name", _name);
const [x, y] = helpers.coord2d(ctx, "coord", _coord);
const another = factory.entities.find((e): e is Bot => e.type === EntityType.Bot && e.name === name);
if (another) {
helpers.log(ctx, () => `bot "${name}" already exists`);
return false;
}
const location = factory.entities.find((e) => e.x === x && e.y === y);
if (location) {
helpers.log(ctx, () => `location [${x}, ${y}] is occupied`);
return false;
}
const botCount = factory.entities.reduce((acc, e) => (e.type === EntityType.Bot ? acc + 1 : acc), 0);
const price = botPrice(botCount);
if (factory.bits < price) {
helpers.log(ctx, () => `not enough bits to purchase bot`);
return false;
}
factory.bits -= price;
NewBot(name, x, y);
return false;
},
moveBot:
(ctx) =>
async (_name, _coord): Promise<boolean> => {
const name = helpers.string(ctx, "name", _name);
const [x, y] = helpers.coord2d(ctx, "coord", _coord);
const bot = factory.entities.find((e) => e.type === EntityType.Bot && e.name === name);
if (!bot) return Promise.resolve(false);
const dist = Math.abs(bot.x - x) + Math.abs(bot.y - y);
if (dist !== 1 || x < 0 || x > factorySize || y < 0 || y > factorySize) return Promise.resolve(false);
if (factory.entities.find((e) => e.x === x && e.y === y)) return Promise.resolve(false);
return helpers.netscriptDelay(ctx, 50).then(function () {
bot.x = x;
bot.y = y;
return Promise.resolve(true);
});
},
getEntity: (ctx) => (_id) => {
const id = helpers.entityID(ctx, "id", _id);
return findEntity(id);
},
entities: (__ctx) => () => JSON.parse(JSON.stringify(factory.entities)),
transfer:
(ctx) =>
async (_from, _to, _pickup, _drop): Promise<boolean> => {
const botID = helpers.entityID(ctx, "from", _from);
const containerID = helpers.entityID(ctx, "to", _to);
const pickup = _pickup as Item[];
const drop = (_drop ?? []) as Item[];
const bot = findEntity(botID, EntityType.Bot) as Bot;
if (!bot) {
helpers.log(ctx, () => `bot ${botID} not found`);
return Promise.resolve(false);
}
const container = findEntity(containerID);
if (!container) {
helpers.log(ctx, () => `container ${containerID} not found`);
return Promise.resolve(false);
}
if (!isEntityContainer(container)) {
helpers.log(ctx, () => `entity ${containerID} is not a container`);
return Promise.resolve(false);
}
if (!adjacent(bot, container)) {
helpers.log(ctx, () => "bot is not adjacent to container");
return Promise.resolve(false);
}
const botFinalSize = bot.inventory.length + pickup.length - drop.length;
const containerFinalSize = container.inventory.length + drop.length - pickup.length;
if (botFinalSize > bot.maxInventory || containerFinalSize > container.maxInventory) {
helpers.log(ctx, () => "not enough space in bot or container");
return Promise.resolve(false);
}
const containerHasItems = pickup.every((item) => container.inventory.includes(item));
const botHasItems = drop.every((item) => bot.inventory.includes(item));
if (!containerHasItems || !botHasItems) {
helpers.log(ctx, () => "container or bot does not have items");
return Promise.resolve(false);
}
container.inventory = container.inventory.filter((item) => !pickup.includes(item));
container.inventory.push(...drop);
bot.inventory = bot.inventory.filter((item) => !drop.includes(item));
bot.inventory.push(...pickup);
switch (container.type) {
case EntityType.Dispenser: {
container.cooldownUntil = Date.now() + container.cooldown;
setTimeout(() => {
if (container.inventory.length < container.maxInventory) {
container.inventory = new Array(container.maxInventory).fill(container.dispensing);
}
}, container.cooldown);
break;
}
case EntityType.Dock: {
if (
container.inventory.every((item) => container.currentRequest.includes(item)) &&
container.currentRequest.every((item) => container.inventory.includes(item))
) {
factory.bits += container.inventory.map((i) => bitsMap[i]).reduce((a, b) => a + b, 0);
container.inventory = [];
const newRequest = new Array(container.potentialRequestCount)
.fill(null)
.map(() => container.potentialRequest[Math.floor(Math.random() * container.potentialRequest.length)]);
container.currentRequest = newRequest;
container.maxInventory = newRequest.length;
}
break;
}
}
return Promise.resolve(true);
},
craft:
(ctx) =>
async (_botID, _crafterID): Promise<boolean> => {
const botID = helpers.entityID(ctx, "bot", _botID);
const crafterID = helpers.entityID(ctx, "crafter", _crafterID);
const bot = findEntity(botID, EntityType.Bot) as Bot;
if (!bot) {
helpers.log(ctx, () => `bot ${botID} not found`);
return Promise.resolve(false);
}
const crafter = findEntity(crafterID, EntityType.Crafter) as Crafter;
if (!crafter) {
helpers.log(ctx, () => `crafter ${crafterID} not found`);
return Promise.resolve(false);
}
if (!adjacent(bot, crafter)) {
helpers.log(ctx, () => "bot is not adjacent to crafter");
return Promise.resolve(false);
}
if (
!crafter.recipe.input.every((item) => crafter.inventory.includes(item)) &&
crafter.inventory.every((item) => crafter.recipe.input.includes(item))
) {
helpers.log(ctx, () => "crafter is missing ingredients");
return Promise.resolve(false);
}
return helpers.netscriptDelay(ctx, 5000).then(function () {
crafter.inventory = [...crafter.recipe.output];
return Promise.resolve(true);
});
},
upgradeMaxInventory: (ctx) => (_id) => {
const id = helpers.entityID(ctx, "id", _id);
const container = findEntity(id);
if (!container) {
helpers.log(ctx, () => `container ${id} not found`);
return false;
}
if (!isEntityContainer(container)) {
helpers.log(ctx, () => `entity ${id} is not a container`);
return false;
}
const cost = upgradeMaxInventoryCost(container.type, container.maxInventory);
if (factory.bits < cost) {
helpers.log(ctx, () => `not enough bits to upgrade container`);
return false;
}
factory.bits -= cost;
container.maxInventory++;
return true;
},
getUpgradeMaxInventoryCost: (ctx) => (_id) => {
const id = helpers.entityID(ctx, "id", _id);
const container = findEntity(id);
if (!container) {
helpers.log(ctx, () => `container ${id} not found`);
return -1;
}
if (!isEntityContainer(container)) {
helpers.log(ctx, () => `entity ${id} is not a container`);
return -1;
}
return upgradeMaxInventoryCost(container.type, container.maxInventory);
},
reset: () => resetFactory,
};
}

@ -0,0 +1,386 @@
import { Bus, Myrian as IMyrian, DeviceType, Component, Reducer } from "@nsdefs";
import { InternalAPI } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers";
import {
NewBus,
NewCache,
NewISocket,
NewLock,
NewOSocket,
NewReducer,
myrian as myrian,
myrianSize,
resetMyrian,
} from "../Myrian/Helper";
import {
adjacent,
adjacentCoord2D,
vulnsMap,
findDevice,
inMyrianBounds,
inventoryMatches,
isDeviceContainer,
isDeviceBus,
removeDevice,
} from "../Myrian/Myrian";
import {
deviceCost,
getNextISocketRequest,
installSpeed,
moveSpeed,
reduceSpeed,
transferSpeed,
upgradeMaxContentCost,
} from "../Myrian/formulas/formulas";
import { recipes } from "../Myrian/formulas/recipes";
import { componentTiers } from "../Myrian/formulas/components";
export function NetscriptMyrian(): InternalAPI<IMyrian> {
return {
reset: () => resetMyrian,
getDevice: (ctx) => (_id) => {
const id = helpers.deviceID(ctx, "id", _id);
return JSON.parse(JSON.stringify(findDevice(id)));
},
getDevices: (__ctx) => () => JSON.parse(JSON.stringify(myrian.devices)),
getVulns: () => () => myrian.vulns,
moveBus:
(ctx) =>
async (_bus, _coord): Promise<boolean> => {
const busID = helpers.string(ctx, "bus", _bus);
const [x, y] = helpers.coord2d(ctx, "coord", _coord);
const bus = findDevice(busID, DeviceType.Bus) as Bus;
if (!bus) {
helpers.log(ctx, () => `bus does not exist`);
return Promise.resolve(false);
}
if (!adjacentCoord2D(bus, [x, y])) {
helpers.log(ctx, () => `bus ${busID} is not adjacent to [${x}, ${y}]`);
return Promise.resolve(false);
}
if (!inMyrianBounds(x, y)) {
helpers.log(ctx, () => `[${x}, ${y}] is out of bounds`);
return Promise.resolve(false);
}
if (findDevice([x, y])) {
helpers.log(ctx, () => `[${x}, ${y}] is occupied`);
return Promise.resolve(false);
}
if (bus.busy) {
helpers.log(ctx, () => `bus ${busID} is busy`);
return Promise.resolve(false);
}
bus.busy = true;
return helpers.netscriptDelay(ctx, moveSpeed(bus.moveLvl), true).then(() => {
if (findDevice([x, y])) {
helpers.log(ctx, () => `[${x}, ${y}] is occupied`);
return Promise.resolve(false);
}
bus.x = x;
bus.y = y;
bus.busy = false;
return Promise.resolve(true);
});
},
transfer:
(ctx) =>
async (_from, _to, _input, _output): Promise<boolean> => {
const fromID = helpers.deviceID(ctx, "from", _from);
const toID = helpers.deviceID(ctx, "to", _to);
const input = _input as Component[];
const output = (_output ?? []) as Component[];
const fromDevice = findDevice(fromID);
if (!fromDevice) {
helpers.log(ctx, () => `device ${fromID} not found`);
return Promise.resolve(false);
}
if (!isDeviceContainer(fromDevice)) {
helpers.log(ctx, () => `device ${fromID} is not a container`);
return Promise.resolve(false);
}
const toDevice = findDevice(toID);
if (!toDevice) {
helpers.log(ctx, () => `device ${toID} not found`);
return Promise.resolve(false);
}
if (!isDeviceContainer(toDevice)) {
helpers.log(ctx, () => `device ${toID} is not a container`);
return Promise.resolve(false);
}
if (!adjacent(fromDevice, toDevice)) {
helpers.log(ctx, () => "entities are not adjacent");
return Promise.resolve(false);
}
if (!isDeviceBus(fromDevice) && !isDeviceBus(toDevice)) {
helpers.log(ctx, () => "neither device is a bus");
return Promise.resolve(false);
}
const fromFinalSize = fromDevice.content.length + input.length - output.length;
const toFinalSize = toDevice.content.length + output.length - input.length;
if (fromFinalSize > fromDevice.maxContent || toFinalSize > toDevice.maxContent) {
helpers.log(ctx, () => "not enough space in one of the containers");
return Promise.resolve(false);
}
const fromHas = input.every((item) => toDevice.content.includes(item));
const toHas = output.every((item) => fromDevice.content.includes(item));
if (!fromHas || !toHas) {
helpers.log(ctx, () => "one of the entities does not have the items");
return Promise.resolve(false);
}
if (fromDevice.busy || toDevice.busy) {
helpers.log(ctx, () => "one of the entities is busy");
return Promise.resolve(false);
}
const bus = [fromDevice, toDevice].find((e) => e.type === DeviceType.Bus) as Bus;
const container = [fromDevice, toDevice].find((e) => e.type !== DeviceType.Bus)!;
fromDevice.busy = true;
toDevice.busy = true;
return helpers.netscriptDelay(ctx, transferSpeed(bus.transferLvl), true).then(() => {
fromDevice.busy = false;
toDevice.busy = false;
toDevice.content = toDevice.content.filter((item) => !input.includes(item));
toDevice.content.push(...output);
fromDevice.content = fromDevice.content.filter((item) => !output.includes(item));
fromDevice.content.push(...input);
switch (container.type) {
case DeviceType.ISocket: {
container.cooldownUntil = Date.now() + container.cooldown;
setTimeout(() => {
container.content = new Array(container.maxContent).fill(container.emitting);
}, container.cooldown);
break;
}
case DeviceType.OSocket: {
if (inventoryMatches(container.currentRequest, container.content)) {
const gain = container.content.map((i) => vulnsMap[i]).reduce((a, b) => a + b, 0);
myrian.vulns += gain;
myrian.totalVulns += gain;
container.content = [];
const request = getNextISocketRequest(container.tier);
container.currentRequest = request;
container.maxContent = request.length;
}
break;
}
}
return Promise.resolve(true);
});
},
reduce:
(ctx) =>
async (_busID, _reducerID): Promise<boolean> => {
const busID = helpers.deviceID(ctx, "bus", _busID);
const reducerID = helpers.deviceID(ctx, "reducer", _reducerID);
const bus = findDevice(busID, DeviceType.Bus) as Bus;
if (!bus) {
helpers.log(ctx, () => `bus ${busID} not found`);
return Promise.resolve(false);
}
const reducer = findDevice(reducerID, DeviceType.Reducer) as Reducer;
if (!reducer) {
helpers.log(ctx, () => `reducer ${reducerID} not found`);
return Promise.resolve(false);
}
if (!adjacent(bus, reducer)) {
helpers.log(ctx, () => "entites are not adjacent");
return Promise.resolve(false);
}
const recipe = recipes[reducer.tier].find((r) => inventoryMatches(r.input, reducer.content));
if (!recipe) {
helpers.log(ctx, () => "reducer content matches no recipe");
return Promise.resolve(false);
}
if (bus.busy || reducer.busy) {
helpers.log(ctx, () => "bus or reducer is busy");
return Promise.resolve(false);
}
bus.busy = true;
reducer.busy = true;
return helpers.netscriptDelay(ctx, reduceSpeed(bus.reduceLvl), true).then(() => {
bus.busy = false;
reducer.busy = false;
reducer.content = [recipe.output];
return Promise.resolve(true);
});
},
upgradeMaxContent: (ctx) => (_id) => {
const id = helpers.deviceID(ctx, "id", _id);
const container = findDevice(id);
if (!container) {
helpers.log(ctx, () => `device ${id} not found`);
return false;
}
if (!isDeviceContainer(container)) {
helpers.log(ctx, () => `device ${id} is not a container`);
return false;
}
const cost = upgradeMaxContentCost(container.type, container.maxContent);
if (myrian.vulns < cost) {
helpers.log(ctx, () => `not enough vulns to upgrade container`);
return false;
}
myrian.vulns -= cost;
container.maxContent++;
return true;
},
getUpgradeMaxContentCost: (ctx) => (_id) => {
const id = helpers.deviceID(ctx, "id", _id);
const container = findDevice(id);
if (!container) {
helpers.log(ctx, () => `container ${id} not found`);
return -1;
}
if (!isDeviceContainer(container)) {
helpers.log(ctx, () => `device ${id} is not a container`);
return -1;
}
return upgradeMaxContentCost(container.type, container.maxContent);
},
getDeviceCost: (ctx) => (_type) => {
const type = helpers.string(ctx, "type", _type);
return deviceCost(type as DeviceType);
},
installDevice: (ctx) => async (_bus, _name, _coord, _deviceType) => {
const busID = helpers.deviceID(ctx, "bus", _bus);
const name = helpers.string(ctx, "name", _name);
const [x, y] = helpers.coord2d(ctx, "coord", _coord);
const deviceType = helpers.string(ctx, "deviceType", _deviceType) as DeviceType;
const bus = findDevice(busID, DeviceType.Bus) as Bus;
if (!bus) {
helpers.log(ctx, () => `bus ${busID} not found`);
return Promise.resolve(false);
}
if (findDevice(name)) {
helpers.log(ctx, () => `device ${name} already exists`);
return Promise.resolve(false);
}
const placedDevice = findDevice([x, y]);
if (placedDevice) {
helpers.log(ctx, () => `location [${x}, ${y}] is occupied`);
return Promise.resolve(false);
}
if (bus.busy) {
helpers.log(ctx, () => `bus ${busID} is busy`);
return Promise.resolve(false);
}
const cost = deviceCost(deviceType);
if (myrian.vulns < cost) {
helpers.log(ctx, () => `not enough vulns to install device`);
return Promise.resolve(false);
}
myrian.vulns -= cost;
if (deviceType === DeviceType.ISocket && y !== 0) {
helpers.log(ctx, () => `ISocket must be placed on the top row`);
return Promise.resolve(false);
}
if (deviceType === DeviceType.OSocket && y !== myrianSize - 1) {
helpers.log(ctx, () => `OSocket must be placed on the bottom row`);
return Promise.resolve(false);
}
bus.busy = true;
const lockName = `lock-${busID}`;
const lock = NewLock(lockName, x, y);
lock.busy = true;
return helpers.netscriptDelay(ctx, installSpeed(bus.installLvl), true).then(() => {
bus.busy = false;
removeDevice(lockName);
switch (deviceType) {
case DeviceType.Bus: {
NewBus(name, x, y);
break;
}
case DeviceType.ISocket: {
NewISocket(name, x, y, componentTiers[0][Math.floor(Math.random() * componentTiers[0].length)]);
break;
}
case DeviceType.OSocket: {
NewOSocket(name, x, y);
break;
}
case DeviceType.Reducer: {
NewReducer(name, x, y);
break;
}
case DeviceType.Cache: {
NewCache(name, x, y);
}
}
return Promise.resolve(true);
});
},
uninstallDevice: (ctx) => async (_bus, _coord) => {
const busID = helpers.string(ctx, "bus", _bus);
const [x, y] = helpers.coord2d(ctx, "coord", _coord);
const bus = findDevice(busID, DeviceType.Bus) as Bus;
if (!bus) {
helpers.log(ctx, () => `bus ${busID} not found`);
return Promise.resolve(false);
}
const placedDevice = findDevice([x, y]);
if (!placedDevice) {
helpers.log(ctx, () => `location [${x}, ${y}] is empty`);
return Promise.resolve(false);
}
if (bus.busy || placedDevice.busy) {
helpers.log(ctx, () => `bus or device is busy`);
return Promise.resolve(false);
}
bus.busy = true;
placedDevice.busy = true;
return helpers.netscriptDelay(ctx, installSpeed(bus.installLvl), true).then(() => {
bus.busy = false;
placedDevice.busy = false;
removeDevice([x, y]);
return Promise.resolve(true);
});
},
};
}

@ -45,7 +45,7 @@ import { downloadContentAsFile } from "./utils/FileUtils";
import { showAPIBreaks } from "./utils/APIBreaks/APIBreak";
import { breakInfos261 } from "./utils/APIBreaks/2.6.1";
import { handleGetSaveDataError } from "./Netscript/ErrorMessages";
import { factory, loadFactory } from "./Factory/Helper";
import { myrian, loadMyrian } from "./Myrian/Helper";
/* SaveObject.js
* Defines the object used to save/load games
@ -118,7 +118,7 @@ class BitburnerSaveObject {
this.VersionSave = JSON.stringify(CONSTANTS.VersionNumber);
this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
this.StaneksGiftSave = JSON.stringify(staneksGift);
this.FactorySave = JSON.stringify(factory);
this.FactorySave = JSON.stringify(myrian);
this.GoSave = JSON.stringify(getGoSave());
if (Player.gang) this.AllGangsSave = JSON.stringify(AllGangs);
@ -766,7 +766,7 @@ async function loadGame(saveData: SaveData): Promise<boolean> {
}
if (Object.hasOwn(saveObj, "FactorySave")) {
loadFactory(saveObj.FactorySave);
loadMyrian(saveObj.FactorySave);
} else {
console.warn(`Could not load Factory from save`);
}

@ -5143,168 +5143,238 @@ interface Stanek {
acceptGift(): boolean;
}
export enum Tile {
Empty = 0,
export enum DeviceType {
Bus = "bus",
ISocket = "isocket",
OSocket = "osocket",
Reducer = "reducer",
Cache = "cache",
Lock = "lock",
}
export enum EntityType {
Bot = "bot",
Dispenser = "dispenser",
Dock = "dock",
Crafter = "crafter",
Chest = "chest",
Wall = "wall",
export enum Component {
// tier 0
R0 = "r0",
G0 = "g0",
B0 = "b0",
// tier 1
R1 = "r1",
G1 = "g1",
B1 = "b1",
Y1 = "y1",
C1 = "c1",
M1 = "m1",
// tier 2
R2 = "r2",
G2 = "g2",
B2 = "b2",
Y2 = "y2",
C2 = "c2",
M2 = "m2",
W2 = "w2",
// tier 3
R3 = "r3",
G3 = "g3",
B3 = "b3",
Y3 = "y3",
C3 = "c3",
M3 = "m3",
W3 = "w3",
// tier 4
R4 = "r4",
G4 = "g4",
B4 = "b4",
Y4 = "y4",
C4 = "c4",
M4 = "m4",
W4 = "w4",
// tier 5
R5 = "r5",
G5 = "g5",
B5 = "b5",
Y5 = "y5",
C5 = "c5",
M5 = "m5",
W5 = "w5",
// tier 6
Y6 = "y6",
C6 = "c6",
M6 = "m6",
W6 = "w6",
// tier 7
W7 = "w7",
}
export enum Item {
BasicR = "basicr",
BasicG = "basicg",
BasicB = "basicb",
ComplexR = "complexr",
ComplexG = "complexg",
ComplexB = "complexb",
}
export interface BaseEntity {
export interface BaseDevice {
name: string;
type: EntityType;
type: DeviceType;
x: number;
y: number;
busy: boolean;
}
export interface Bot extends ContainerEntity {
type: EntityType.Bot;
energy: number;
export interface Bus extends ContainerDevice {
type: DeviceType.Bus;
moveLvl: number;
transferLvl: number;
reduceLvl: number;
installLvl: number;
// energy: number;
// maxEnergy: number;
}
export interface ContainerEntity extends BaseEntity {
inventory: Item[];
maxInventory: number;
export interface ContainerDevice extends BaseDevice {
content: Component[];
maxContent: number;
}
export interface Dispenser extends ContainerEntity {
type: EntityType.Dispenser;
dispensing: Item;
export interface ISocket extends ContainerDevice {
type: DeviceType.ISocket;
emitting: Component;
cooldown: number;
cooldownUntil: number;
}
export interface Dock extends ContainerEntity {
type: EntityType.Dock;
potentialRequest: Item[];
potentialRequestCount: number;
currentRequest: Item[];
export interface OSocket extends ContainerDevice {
type: DeviceType.OSocket;
tier: number;
currentRequest: Component[];
}
export interface Chest extends ContainerEntity {
type: EntityType.Chest;
export interface Cache extends ContainerDevice {
type: DeviceType.Cache;
}
export interface Crafter extends ContainerEntity {
type: EntityType.Crafter;
recipe: Recipe;
export interface Reducer extends ContainerDevice {
type: DeviceType.Reducer;
tier: number;
}
export interface Wall extends BaseEntity {
type: EntityType.Wall;
export interface Lock extends BaseDevice {
type: DeviceType.Lock;
}
export interface Recipe {
input: Item[];
output: Item[];
input: Component[];
output: Component;
}
export type EntityID = string | [number, number];
export type DeviceID = string | [number, number];
export type Entity = Bot | Dispenser | Dock | Crafter | Chest | Wall;
export type Device = Bus | ISocket | OSocket | Reducer | Cache | Lock;
interface Factory {
interface Myrian {
/**
* Move a bot
* @remarks
* RAM cost: 0 GB
* @returns true if the move succeeded, false otherwise.
*/
moveBot(name: EntityID, coord: [number, number]): Promise<boolean>;
/**
* Get entity
* @remarks
* RAM cost: 0GB
* @returns entity with this ID
*/
getEntity(entity: EntityID): Entity | undefined;
/**
* Get all entities
* @remarks
* RAM cost: 0 GB
* @returns all entities
*/
entities(): Entity[];
/**
* Transfer items between entities
* @remarks
* RAM cost: 0 GB
* @returns true if the transfer succeeded, false otherwise.
*/
transfer(from: EntityID, to: EntityID, pickup: Item[], drop?: Item[]): Promise<boolean>;
/**
* Make a bot use a crafter in order to craft an item.
* @remarks
* RAM cost: 0 GB
* @returns true if the crafting succeeded, false otherwise.
*/
craft(bot: EntityID, crafter: EntityID): Promise<boolean>;
/**
* get number of bits available
* @remarks
* RAM cost: 0 GB
* @returns number of bits available
*/
getBits(): number;
/**
* Get the price of the next bot
* @remarks
* RAM cost: 0 GB
* @returns price of the next bot
*/
getBotPrice(): number;
/**
* Purchase a new bot and place it at location [x, y]
* @remarks
* RAM cost: 0 GB
* @returns true if the purchase succeeded, false otherwise.
*/
purchaseBot(name: string, coord: [number, number]): boolean;
/**
* Upgrade the inventory of an entity
* @remarks
* RAM cost: 0 GB
* @returns true if the upgrade succeeded, false otherwise.
*/
upgradeMaxInventory(entity: EntityID): boolean;
/**
* Get the cost of upgrading the inventory of an entity
* @remarks
* RAM cost: 0 GB
* @returns cost of upgrading the inventory of an entity, -1 on failure.
*/
getUpgradeMaxInventoryCost(entity: EntityID): number;
/**
* Completely reset the factory, for debug purposes
* Completely reset the myrian kernel, for debug purposes
* @remarks
* RAM cost: 0 GB
*/
reset(): void;
/**
* Get device
* @remarks
* RAM cost: 0GB
* @returns device with this ID
*/
getDevice(device: DeviceID): Device | undefined;
/**
* Get all devices
* @remarks
* RAM cost: 0 GB
* @returns all devices
*/
getDevices(): Device[];
/**
* get number of vulnerabilities available
* @remarks
* RAM cost: 0 GB
* @returns number of vulnerabilities available
*/
getVulns(): number;
/**
* Move a bus
* @remarks
* RAM cost: 0 GB
* @returns true if the move succeeded, false otherwise.
*/
moveBus(bus: DeviceID, coord: [number, number]): Promise<boolean>;
/**
* Transfer components between devices, one of them must be a bus.
* @remarks
* RAM cost: 0 GB
* @returns true if the transfer succeeded, false otherwise.
*/
transfer(from: DeviceID, to: DeviceID, input: Component[], output?: Component[]): Promise<boolean>;
/**
* Make a bus use a reducer in order to produce an component.
* @remarks
* RAM cost: 0 GB
* @returns true if the crafting succeeded, false otherwise.
*/
reduce(bus: DeviceID, reducer: DeviceID): Promise<boolean>;
/**
* Get the cost of a device.
* @remarks
* RAM cost: 0 GB
* @returns cost of the next device of that type
*/
getDeviceCost(type: DeviceType): number;
/**
* Make a bus install a new device
* @remarks
* RAM cost: 0 GB
* @returns true if the installation succeeded, false otherwise.
*/
installDevice(bus: DeviceID, name: string, coord: [number, number], deviceType: DeviceType): Promise<boolean>;
/**
* Make a bus uninstall a device
* @remarks
* RAM cost: 0 GB
* @returns true if the uninstallation succeeded, false otherwise.
*/
uninstallDevice(bus: DeviceID, coord: [number, number]): Promise<boolean>;
/**
* Upgrade the max content of a device
* @remarks
* RAM cost: 0 GB
* @returns true if the upgrade succeeded, false otherwise.
*/
upgradeMaxContent(device: DeviceID): boolean;
/**
* Get the cost of upgrading the content of a device
* @remarks
* RAM cost: 0 GB
* @returns cost of upgrading the content of a device, -1 on failure.
*/
getUpgradeMaxContentCost(device: DeviceID): number;
}
/** @public */
@ -5513,10 +5583,10 @@ export interface NS {
readonly stanek: Stanek;
/**
* Namespace for factory functions. Contains spoilers.
* Namespace for myrian functions. Contains spoilers.
* @remarks RAM cost: 0 GB
*/
readonly factory: Factory;
readonly myrian: Myrian;
/**
* Namespace for infiltration functions.

@ -354,7 +354,7 @@ export function SidebarRoot(props: { page: Page }): React.ReactElement {
canCorporation && { key_: Page.Corporation, icon: BusinessIcon },
canGang && { key_: Page.Gang, icon: SportsMmaIcon },
canIPvGO && { key_: Page.Go, icon: BorderInnerSharp },
{ key_: Page.Factory, icon: SportsMmaIcon },
{ key_: Page.MyrianOS, icon: DeveloperBoardIcon },
]}
/>
<Divider />

@ -72,7 +72,7 @@ import { MathJaxContext } from "better-react-mathjax";
import { useRerender } from "./React/hooks";
import { HistoryProvider } from "./React/Documentation";
import { GoRoot } from "../Go/ui/GoRoot";
import { FactoryRoot } from "../Factory/ui/FactoryRoot";
import { MyrianRoot } from "../Myrian/ui/MyrianRoot";
const htmlLocation = location;
@ -116,7 +116,7 @@ export let Router: IRouter = {
function determineStartPage() {
if (RecoveryMode) return Page.Recovery;
if (Player.currentWork !== null) return Page.Work;
return Page.Factory;
return Page.MyrianOS;
return Page.Terminal;
}
@ -362,8 +362,8 @@ export function GameRoot(): React.ReactElement {
mainPage = <GoRoot />;
break;
}
case Page.Factory: {
mainPage = <FactoryRoot />;
case Page.MyrianOS: {
mainPage = <MyrianRoot />;
break;
}
case Page.Achievements: {

@ -37,7 +37,7 @@ export enum SimplePage {
Recovery = "Recovery",
Achievements = "Achievements",
ThemeBrowser = "Theme Browser",
Factory = "Factory",
MyrianOS = "Myrian OS",
}
export enum ComplexPage {