From 5f2efd94e554259a2e2842cd205f0a6dd0a9d6f2 Mon Sep 17 00:00:00 2001 From: Olivier Gagnon Date: Sun, 9 Jun 2024 13:08:29 -0400 Subject: [PATCH] v0.2 --- src/Factory/Factory.ts | 55 --- src/Factory/Helper.tsx | 110 ----- src/Factory/formulas/formulas.ts | 35 -- src/Factory/ui/EntityIcon.tsx | 153 ------- src/Myrian/Helper.tsx | 119 ++++++ src/Myrian/Myrian.ts | 125 ++++++ src/Myrian/formulas/components.ts | 12 + src/Myrian/formulas/formulas.ts | 71 ++++ src/Myrian/formulas/recipes.ts | 214 ++++++++++ src/Myrian/tutorial.md | 56 +++ src/Myrian/ui/DeviceIcon.tsx | 218 ++++++++++ .../ui/MyrianRoot.tsx} | 21 +- src/Netscript/NetscriptHelpers.tsx | 12 +- src/Netscript/RamCostGenerator.ts | 4 +- src/NetscriptFunctions.ts | 4 +- src/NetscriptFunctions/Factory.ts | 222 ---------- src/NetscriptFunctions/Myrian.ts | 386 ++++++++++++++++++ src/SaveObject.ts | 6 +- src/ScriptEditor/NetscriptDefinitions.d.ts | 228 +++++++---- src/Sidebar/ui/SidebarRoot.tsx | 2 +- src/ui/GameRoot.tsx | 8 +- src/ui/Router.ts | 2 +- 22 files changed, 1381 insertions(+), 682 deletions(-) delete mode 100644 src/Factory/Factory.ts delete mode 100644 src/Factory/Helper.tsx delete mode 100644 src/Factory/formulas/formulas.ts delete mode 100644 src/Factory/ui/EntityIcon.tsx create mode 100644 src/Myrian/Helper.tsx create mode 100644 src/Myrian/Myrian.ts create mode 100644 src/Myrian/formulas/components.ts create mode 100644 src/Myrian/formulas/formulas.ts create mode 100644 src/Myrian/formulas/recipes.ts create mode 100644 src/Myrian/tutorial.md create mode 100644 src/Myrian/ui/DeviceIcon.tsx rename src/{Factory/ui/FactoryRoot.tsx => Myrian/ui/MyrianRoot.tsx} (74%) delete mode 100644 src/NetscriptFunctions/Factory.ts create mode 100644 src/NetscriptFunctions/Myrian.ts diff --git a/src/Factory/Factory.ts b/src/Factory/Factory.ts deleted file mode 100644 index 2cd9e8019..000000000 --- a/src/Factory/Factory.ts +++ /dev/null @@ -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 = (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.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), - ); diff --git a/src/Factory/Helper.tsx b/src/Factory/Helper.tsx deleted file mode 100644 index e724ffa60..000000000 --- a/src/Factory/Helper.tsx +++ /dev/null @@ -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(); diff --git a/src/Factory/formulas/formulas.ts b/src/Factory/formulas/formulas.ts deleted file mode 100644 index aeac2a0e4..000000000 --- a/src/Factory/formulas/formulas.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { EntityType } from "@nsdefs"; - -export type FactoryFormulaParams = [number, number, number, number]; - -export const inventoryScale: Record = { - [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 - - - - */ diff --git a/src/Factory/ui/EntityIcon.tsx b/src/Factory/ui/EntityIcon.tsx deleted file mode 100644 index df5926a9a..000000000 --- a/src/Factory/ui/EntityIcon.tsx +++ /dev/null @@ -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.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 ( - <> - - {entity.name} ({entity.type}) - - {content} - - ); -}; - -const TooltipInventory = ({ entity }: { entity: Entity }): React.ReactElement => { - if (!isEntityContainer(entity)) return <>; - return ( - - {entity.inventory.map((item) => ( - - {item} - - ))} - - ); -}; - -export const EntityIcon = ({ entity }: { entity: Entity }): React.ReactElement => { - switch (entity.type) { - case EntityType.Wall: { - return ; - } - case EntityType.Chest: { - return ( - } />}> - - - ); - } - case EntityType.Bot: { - return ( - - {entity.name} -
- - } - > - -
- ); - } - case EntityType.Dispenser: { - return ( - - {`dispensing: ${entity.dispensing}`}} /> - - - } - > - - - ); - break; - } - case EntityType.Dock: { - return ( - {`requesting: ${entity.currentRequest}`}} - /> - } - > - - - ); - } - case EntityType.Crafter: { - return ( - - {`input: ${entity.recipe.input}, output: ${entity.recipe.output}`} - - - } - /> - } - > - - - ); - break; - } - } - return <>; -}; diff --git a/src/Myrian/Helper.tsx b/src/Myrian/Helper.tsx new file mode 100644 index 000000000..a38333a26 --- /dev/null +++ b/src/Myrian/Helper.tsx @@ -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(); diff --git a/src/Myrian/Myrian.ts b/src/Myrian/Myrian.ts new file mode 100644 index 000000000..7b5389c12 --- /dev/null +++ b/src/Myrian/Myrian.ts @@ -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 = { + // 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)), + ); +}; diff --git a/src/Myrian/formulas/components.ts b/src/Myrian/formulas/components.ts new file mode 100644 index 000000000..e8671b04c --- /dev/null +++ b/src/Myrian/formulas/components.ts @@ -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], +]; diff --git a/src/Myrian/formulas/formulas.ts b/src/Myrian/formulas/formulas.ts new file mode 100644 index 000000000..bb789723f --- /dev/null +++ b/src/Myrian/formulas/formulas.ts @@ -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.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.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.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 +*/ diff --git a/src/Myrian/formulas/recipes.ts b/src/Myrian/formulas/recipes.ts new file mode 100644 index 000000000..7a1e6f378 --- /dev/null +++ b/src/Myrian/formulas/recipes.ts @@ -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, +]; diff --git a/src/Myrian/tutorial.md b/src/Myrian/tutorial.md new file mode 100644 index 000000000..118a15c25 --- /dev/null +++ b/src/Myrian/tutorial.md @@ -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). diff --git a/src/Myrian/ui/DeviceIcon.tsx b/src/Myrian/ui/DeviceIcon.tsx new file mode 100644 index 000000000..edbfd2234 --- /dev/null +++ b/src/Myrian/ui/DeviceIcon.tsx @@ -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 = { + // 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 ( + <> + + {device.name} ({device.type}) + + {content} + + ); +}; + +const TooltipInventory = ({ device }: { device: Device }): React.ReactElement => { + if (!isDeviceContainer(device)) return <>; + return ( + + {device.content.map((item) => ( + + {item} + + ))} + + ); +}; + +export const DeviceIcon = ({ device }: { device: Device }): React.ReactElement => { + switch (device.type) { + case DeviceType.Lock: { + return ; + } + case DeviceType.Cache: { + return ( + } />}> + + + ); + } + case DeviceType.Bus: { + return ( + + {device.name} +
+ + } + > + +
+ ); + } + case DeviceType.ISocket: { + return ( + + {`dispensing: ${device.emitting}`}} /> + + + } + > + + + ); + break; + } + case DeviceType.OSocket: { + return ( + {`requesting: ${device.currentRequest}`}} + /> + } + > + + + ); + } + case DeviceType.Reducer: { + return ( + + Tier: {device.tier} + + + } + /> + } + > + + + ); + break; + } + } + return <>; +}; diff --git a/src/Factory/ui/FactoryRoot.tsx b/src/Myrian/ui/MyrianRoot.tsx similarity index 74% rename from src/Factory/ui/FactoryRoot.tsx rename to src/Myrian/ui/MyrianRoot.tsx index 99814be1c..02f9402c4 100644 --- a/src/Factory/ui/FactoryRoot.tsx +++ b/src/Myrian/ui/MyrianRoot.tsx @@ -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 {entity && }; + const device = myrian.devices.find((e) => e.x === x && e.y === y); + return {device && }; }; const RowD = styled("div")({ @@ -33,7 +33,7 @@ interface IColProps { const Col = ({ x }: IColProps): React.ReactElement => { return ( - {new Array(factorySize + 1).fill(null).map((_, y) => { + {new Array(myrianSize + 1).fill(null).map((_, y) => { if (y === 0) return ( { return ( - {new Array(factorySize + 1).fill(null).map((_, y) => { + {new Array(myrianSize + 1).fill(null).map((_, y) => { return ( { interface IProps {} -export const FactoryRoot = (__props: IProps): React.ReactElement => { +export const MyrianRoot = (__props: IProps): React.ReactElement => { useRerender(200); return ( - Factory + Myrian OS + + {myrian.vulns} vulns : {myrian.totalVulns} total vulns +
- {new Array(factorySize + 1).fill(null).map((_, x) => { + {new Array(myrianSize + 1).fill(null).map((_, x) => { if (x === 0) return ; return ; })} diff --git a/src/Netscript/NetscriptHelpers.tsx b/src/Netscript/NetscriptHelpers.tsx index 2e05594b3..15702bb06 100644 --- a/src/Netscript/NetscriptHelpers.tsx +++ b/src/Netscript/NetscriptHelpers.tsx @@ -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 { +function netscriptDelay(ctx: NetscriptContext, time: number, ignoreOthers?: boolean): Promise { 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 { else resolve(); }, time); ws.delayReject = reject; - ws.env.runningFn = ctx.function; + if (ignoreOthers) ws.env.runningFn = ctx.function; }); } diff --git a/src/Netscript/RamCostGenerator.ts b/src/Netscript/RamCostGenerator.ts index 455b8a447..df42186e9 100644 --- a/src/Netscript/RamCostGenerator.ts +++ b/src/Netscript/RamCostGenerator.ts @@ -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 = { codingcontract, sleeve, stanek, - factory, + myrian, ui, grafting, diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts index 9d3bdc17c..7dfffb1f8 100644 --- a/src/NetscriptFunctions.ts +++ b/src/NetscriptFunctions.ts @@ -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 = { sleeve: NetscriptSleeve(), corporation: NetscriptCorporation(), stanek: NetscriptStanek(), - factory: NetscriptFactory(), + myrian: NetscriptMyrian(), infiltration: NetscriptInfiltration(), ui: NetscriptUserInterface(), formulas: NetscriptFormulas(), diff --git a/src/NetscriptFunctions/Factory.ts b/src/NetscriptFunctions/Factory.ts deleted file mode 100644 index 060471653..000000000 --- a/src/NetscriptFunctions/Factory.ts +++ /dev/null @@ -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 { - 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 => { - 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 => { - 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 => { - 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, - }; -} diff --git a/src/NetscriptFunctions/Myrian.ts b/src/NetscriptFunctions/Myrian.ts new file mode 100644 index 000000000..14f2d8310 --- /dev/null +++ b/src/NetscriptFunctions/Myrian.ts @@ -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 { + 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 => { + 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 => { + 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 => { + 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); + }); + }, + }; +} diff --git a/src/SaveObject.ts b/src/SaveObject.ts index 6ee5d99b1..ab19f1a99 100644 --- a/src/SaveObject.ts +++ b/src/SaveObject.ts @@ -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 { } if (Object.hasOwn(saveObj, "FactorySave")) { - loadFactory(saveObj.FactorySave); + loadMyrian(saveObj.FactorySave); } else { console.warn(`Could not load Factory from save`); } diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts index 97dc0aecf..bdfe7a494 100644 --- a/src/ScriptEditor/NetscriptDefinitions.d.ts +++ b/src/ScriptEditor/NetscriptDefinitions.d.ts @@ -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", -export enum Item { - BasicR = "basicr", - BasicG = "basicg", - BasicB = "basicb", - ComplexR = "complexr", - ComplexG = "complexg", - ComplexB = "complexb", + W6 = "w6", + + // tier 7 + W7 = "w7", } -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 + * Completely reset the myrian kernel, for debug purposes * @remarks * RAM cost: 0 GB - * @returns true if the move succeeded, false otherwise. */ - moveBot(name: EntityID, coord: [number, number]): Promise; + reset(): void; /** - * Get entity + * Get device * @remarks * RAM cost: 0GB - * @returns entity with this ID + * @returns device with this ID */ - getEntity(entity: EntityID): Entity | undefined; + getDevice(device: DeviceID): Device | undefined; /** - * Get all entities + * Get all devices * @remarks * RAM cost: 0 GB - * @returns all entities + * @returns all devices */ - entities(): Entity[]; + getDevices(): Device[]; /** - * Transfer items between entities + * get number of vulnerabilities available * @remarks * RAM cost: 0 GB - * @returns true if the transfer succeeded, false otherwise. + * @returns number of vulnerabilities available */ - transfer(from: EntityID, to: EntityID, pickup: Item[], drop?: Item[]): Promise; + getVulns(): number; /** - * Make a bot use a crafter in order to craft an item. + * Move a bus * @remarks * RAM cost: 0 GB - * @returns true if the crafting succeeded, false otherwise. + * @returns true if the move succeeded, false otherwise. + */ + moveBus(bus: DeviceID, coord: [number, number]): Promise; + + /** + * Transfer components between devices, one of them must be a bus. + * @remarks + * RAM cost: 0 GB + * @returns true if the transfer succeeded, false otherwise. */ - craft(bot: EntityID, crafter: EntityID): Promise; + transfer(from: DeviceID, to: DeviceID, input: Component[], output?: Component[]): Promise; /** - * get number of bits available + * Make a bus use a reducer in order to produce an component. * @remarks * RAM cost: 0 GB - * @returns number of bits available + * @returns true if the crafting succeeded, false otherwise. */ - getBits(): number; + reduce(bus: DeviceID, reducer: DeviceID): Promise; /** - * Get the price of the next bot + * Get the cost of a device. * @remarks * RAM cost: 0 GB - * @returns price of the next bot + * @returns cost of the next device of that type */ - getBotPrice(): number; + getDeviceCost(type: DeviceType): number; /** - * Purchase a new bot and place it at location [x, y] + * Make a bus install a new device * @remarks * RAM cost: 0 GB - * @returns true if the purchase succeeded, false otherwise. + * @returns true if the installation succeeded, false otherwise. */ - purchaseBot(name: string, coord: [number, number]): boolean; + installDevice(bus: DeviceID, name: string, coord: [number, number], deviceType: DeviceType): Promise; /** - * Upgrade the inventory of an entity + * Make a bus uninstall a device * @remarks * RAM cost: 0 GB - * @returns true if the upgrade succeeded, false otherwise. + * @returns true if the uninstallation succeeded, false otherwise. */ - upgradeMaxInventory(entity: EntityID): boolean; + uninstallDevice(bus: DeviceID, coord: [number, number]): Promise; /** - * Get the cost of upgrading the inventory of an entity + * Upgrade the max content of a device * @remarks * RAM cost: 0 GB - * @returns cost of upgrading the inventory of an entity, -1 on failure. + * @returns true if the upgrade succeeded, false otherwise. */ - getUpgradeMaxInventoryCost(entity: EntityID): number; + upgradeMaxContent(device: DeviceID): boolean; /** - * Completely reset the factory, for debug purposes + * 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. */ - reset(): void; + 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. diff --git a/src/Sidebar/ui/SidebarRoot.tsx b/src/Sidebar/ui/SidebarRoot.tsx index 34332e797..d490d713e 100644 --- a/src/Sidebar/ui/SidebarRoot.tsx +++ b/src/Sidebar/ui/SidebarRoot.tsx @@ -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 }, ]} /> diff --git a/src/ui/GameRoot.tsx b/src/ui/GameRoot.tsx index 3d4baf17e..63ff0665c 100644 --- a/src/ui/GameRoot.tsx +++ b/src/ui/GameRoot.tsx @@ -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 = ; break; } - case Page.Factory: { - mainPage = ; + case Page.MyrianOS: { + mainPage = ; break; } case Page.Achievements: { diff --git a/src/ui/Router.ts b/src/ui/Router.ts index a461f8e95..313c2aa10 100644 --- a/src/ui/Router.ts +++ b/src/ui/Router.ts @@ -37,7 +37,7 @@ export enum SimplePage { Recovery = "Recovery", Achievements = "Achievements", ThemeBrowser = "Theme Browser", - Factory = "Factory", + MyrianOS = "Myrian OS", } export enum ComplexPage {