From 1b99445d6f4058728d26da95b5fe0a07de02e28d Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 8 Sep 2024 17:14:08 -0600 Subject: [PATCH] allow importing saved factories --- dist/index.html | 4 +- dist/style.css | 14 +++ src/AppData.ts | 87 ++++++++++++++--- src/CustomData/CustomMachine.ts | 16 ++++ src/CustomData/LinkedFactory.ts | 47 ++++++++++ src/FactoryImporter.ts | 94 +++++++++++++++++++ src/GameData/GameData.ts | 10 +- src/GameData/GameMachine.ts | 4 +- src/GameData/GameRecipe.ts | 48 +++++++++- src/Machine.ts | 7 ++ src/Recipe.ts | 14 +++ src/RecipeSelectionModal.ts | 4 +- .../NodeConfiguration/NodeConfiguration.ts | 26 ++++- src/Sankey/NodeResourceDisplay.ts | 29 +++--- src/Sankey/SankeyNode.ts | 41 +++++--- src/SavesLoaderMenu.ts | 12 +++ src/main.ts | 18 +++- 17 files changed, 420 insertions(+), 55 deletions(-) create mode 100644 src/CustomData/CustomMachine.ts create mode 100644 src/CustomData/LinkedFactory.ts create mode 100644 src/FactoryImporter.ts create mode 100644 src/Machine.ts create mode 100644 src/Recipe.ts diff --git a/dist/index.html b/dist/index.html index d62ce52..b04d1fd 100644 --- a/dist/index.html +++ b/dist/index.html @@ -236,10 +236,10 @@

Machines amount

-
+

Overclock (every machine)

-
+
Multiplier
diff --git a/dist/style.css b/dist/style.css index 3619864..2925140 100644 --- a/dist/style.css +++ b/dist/style.css @@ -1638,6 +1638,7 @@ div#resources-summary div.content { #saves-loader div.loaded-plan:hover, #saves-loader .plan-selector div.plan-name:hover, #saves-loader .plan-selector div.delete-button:hover, +#saves-loader .plan-selector div.import-button:hover, #saves-loader .plan-selector.selected div.plan-name { background-color: var(--background-lv2); } @@ -1665,6 +1666,15 @@ div#resources-summary div.content { min-height: 48px; } +#saves-loader .plan-selector div.import-button { + display: flex; + align-items: center; + justify-content: center; + + min-width: 48px; + min-height: 48px; +} + #saves-loader div.collapsible-container { display: flex; flex-direction: column; @@ -1709,6 +1719,10 @@ div#resources-summary div.content { cursor: pointer; } +#saves-loader .plan-selector div.import-button { + cursor: pointer; +} + #saves-loader div.create-new { display: flex; align-items: stretch; diff --git a/src/AppData.ts b/src/AppData.ts index 0016f8a..af0a031 100644 --- a/src/AppData.ts +++ b/src/AppData.ts @@ -1,3 +1,4 @@ +import { GameRecipe } from "./GameData/GameRecipe"; import { SankeyNode } from "./Sankey/SankeyNode"; export class AppData extends EventTarget @@ -90,23 +91,32 @@ export class AppData extends EventTarget public loadDatabasePlan(planName: string): void { this.currentPlanName = planName; + let dataEncoded = this.getEncodedPlanFromDatabase(planName); + if (dataEncoded !== "") + { + this.loadFromEncoded(dataEncoded); + return; + } + + if (planName !== "") + { + // If suitable plan wasn't found, load the "None" one. + this.loadDatabasePlan(""); + } + } + public getEncodedPlanFromDatabase(planName: string): string + { for (const dbPlanName in this._database.plans) { let dataEncoded = this._database.plans[planName]; if (dbPlanName === planName) { - this.loadFromEncoded(dataEncoded); - return; + return dataEncoded; } } - - if (planName !== "") - { - // If suitable plan wasn't found, load the "None" one. - this.loadDatabasePlan(""); - } + return ""; } public deleteDatabasePlan(planName: string) @@ -157,6 +167,14 @@ export class AppData extends EventTarget return JSON.stringify(AppData.objToArray(data)); } + + public static getSerializableData(json: string): AppData.SerializableData + { + let parsedJson: any[] = JSON.parse(json); + + return this.dataFromArray(parsedJson); + } + private saveToUrl(dataEncoded: string): void { location.hash = dataEncoded; @@ -164,9 +182,7 @@ export class AppData extends EventTarget private loadFromJson(json: string) { - let parsedJson: any[] = JSON.parse(json); - - let data = AppData.dataFromArray(parsedJson); + let data = AppData.getSerializableData(json) let nodeIds = new Map(); @@ -277,6 +293,16 @@ export class AppData extends EventTarget nodes: [], }; + let defaultRecipe: AppData.SerializableCustomRecipe = { + ingredients: [], + products: [] + }; + + let defaultResources: AppData.SerializableRecipeResource = { + id: "", + amount: 0 + }; + let defaultNode: AppData.SerializableNode = { id: 0, recipeId: "", @@ -285,6 +311,9 @@ export class AppData extends EventTarget positionX: 0, positionY: 0, outputsGroups: [], + recipeType: "", + customRecipe: defaultRecipe, + customPower: 0 }; let defaultGroup: AppData.SerializableSlotsGroup = { @@ -315,6 +344,24 @@ export class AppData extends EventTarget = this.objFromArray(data.nodes[nodeIndex].outputsGroups[groupIndex].connectedOutputs[slotIndex] as unknown as any[], defaultSlot); } } + + // Custom recipes + if (data.nodes[nodeIndex].recipeType === "LinkedFactory") + { + data.nodes[nodeIndex].customRecipe + = this.objFromArray(data.nodes[nodeIndex].customRecipe as unknown as any[], defaultRecipe); + + for (let ingredIndex = 0; ingredIndex < data.nodes[nodeIndex].customRecipe.ingredients.length; ++ingredIndex) + { + data.nodes[nodeIndex].customRecipe.ingredients[ingredIndex] + = this.objFromArray(data.nodes[nodeIndex].customRecipe.ingredients[ingredIndex] as unknown as any[], defaultResources); + } + for (let productIdx = 0; productIdx < data.nodes[nodeIndex].customRecipe.products.length; ++productIdx) + { + data.nodes[nodeIndex].customRecipe.products[productIdx] + = this.objFromArray(data.nodes[nodeIndex].customRecipe.products[productIdx] as unknown as any[], defaultResources); + } + } } return data; @@ -380,6 +427,16 @@ export namespace AppData connectedOutputs: SerializableConnectedSlot[], }; + export type SerializableRecipeResource = { + id: string, + amount: number, + } + + export type SerializableCustomRecipe = { + ingredients: SerializableRecipeResource[], + products: SerializableRecipeResource[], + } + export type SerializableNode = { id: number, @@ -392,6 +449,14 @@ export namespace AppData positionY: number, outputsGroups: SerializableSlotsGroup[], + + // The type of recipe that is being saved (GameRecipe, LinkedFactory) + recipeType: string, + + // These fields are populated for arbitrary nodes that aren't represented by any "real" recipe or factory. + // These are also populated in addition to the "linkedFactory" to allow URL sharing even when the canvas includes a linkedFactory + customRecipe: SerializableCustomRecipe, + customPower: number, }; }; diff --git a/src/CustomData/CustomMachine.ts b/src/CustomData/CustomMachine.ts new file mode 100644 index 0000000..86a0598 --- /dev/null +++ b/src/CustomData/CustomMachine.ts @@ -0,0 +1,16 @@ +import { Machine } from "../Machine"; + +export class CustomMachine implements Machine +{ + public constructor( + public iconPath: string, + public displayName: string, + public powerConsumption: number, + public powerConsumptionExponent: number = 1, + ) { } + + public static getPlaceholderMachine(displayName: string, customPower: number): CustomMachine { + // Temp placeholder. + return new CustomMachine("Buildable/Factory/SmelterMk1/SmelterMk1.png", displayName, customPower); + } +} diff --git a/src/CustomData/LinkedFactory.ts b/src/CustomData/LinkedFactory.ts new file mode 100644 index 0000000..3044427 --- /dev/null +++ b/src/CustomData/LinkedFactory.ts @@ -0,0 +1,47 @@ +import { AppData } from "../AppData"; +import { Machine } from "../Machine"; +import { Recipe } from "../Recipe"; +import { CustomMachine } from "./CustomMachine"; + +// Container for data that represents a saved factory that has been loaded into a separate factory. +export class LinkedFactory implements Recipe +{ + private _machine: Machine; + // LinkedFactories are always reported in items per minute + public manufacturingDuration: number = 60; + + public constructor( + public id: string, + public displayName: string, + public ingredients: RecipeResource[], + public products: RecipeResource[], + public customPower: number, + ) { + + let machine = CustomMachine.getPlaceholderMachine("Temp",customPower); + this._machine = machine; + } + + public getRecipeType(): string { + return "LinkedFactory"; + } + + public getMachine(): Machine + { + return this._machine; + } + + public toSerializable(): AppData.SerializableCustomRecipe + { + return { + "ingredients": this.ingredients, + "products": this.products + } + } + + public static fromSerializable(id: string, serialized: AppData.SerializableCustomRecipe, customPower: number): LinkedFactory + { + let factory = new LinkedFactory(id, id, serialized.ingredients, serialized.products, customPower); + return factory; + } +} diff --git a/src/FactoryImporter.ts b/src/FactoryImporter.ts new file mode 100644 index 0000000..3e90cc0 --- /dev/null +++ b/src/FactoryImporter.ts @@ -0,0 +1,94 @@ +import { AppData } from "./AppData"; +import { LinkedFactory } from "./CustomData/LinkedFactory"; +import { GameRecipe } from "./GameData/GameRecipe"; +import { Recipe } from "./Recipe"; + +export class FactoryImporter extends EventTarget +{ + public static readonly factoryImportedEvent = "factory-imported"; + + private static _instance = new FactoryImporter(); + + public static get instance(): FactoryImporter + { + return FactoryImporter._instance; + } + + public static importSavedFactory(factoryToImport: string): void + { + let pageCenter = { + x: document.documentElement.clientWidth / 2, + y: document.documentElement.clientHeight / 2 + }; + + + let encodedData = AppData.instance.getEncodedPlanFromDatabase(factoryToImport); + let jsonData = atob(decodeURI(encodedData)); + + // Process the factory string to get the inputs/outputs + let data = AppData.getSerializableData(jsonData); + let loadedFactoryNodes = data.nodes + + let resources = { + "input": new Map(), + "output": new Map(), + } + + let powerConsumption = 0; + loadedFactoryNodes.forEach((node) => + { + let recipe: Recipe; + switch (node.recipeType) + { + case "": + case undefined: + case "GameRecipe": + recipe = GameRecipe.fromSerializable(node.recipeId); + break; + case "LinkedFactory": + recipe = LinkedFactory.fromSerializable(node.recipeId, node.customRecipe, node.customPower); + break; + default: + throw Error("Unknown RecipeType [" + node.recipeType + "]"); + + } + let machine = recipe.getMachine(); + + let opsPerMinute = (60.0 / recipe.manufacturingDuration) + powerConsumption += machine.powerConsumption * node.machinesAmount; + recipe.ingredients.forEach(ingredient => { + let currentAmount = resources.input.get(ingredient.id) || 0 + let addedAmount = ingredient.amount * node.machinesAmount * opsPerMinute; + resources.input.set(ingredient.id, currentAmount + addedAmount); + }); + recipe.products.forEach(product => { + let currentAmount = resources.output.get(product.id) || 0 + let addedAmount = product.amount * node.machinesAmount * opsPerMinute; + resources.output.set(product.id, currentAmount + addedAmount); + // Remove resources from overall Inputs/Outputs if this output is supplying an input. + node.outputsGroups.forEach(outputGroup => + { + let resourceId = outputGroup.resourceId; + outputGroup.connectedOutputs.forEach(connectedOutput => + { + let currentInputAmount = resources.input.get(resourceId) || 0 + resources.input.set(resourceId, currentInputAmount - connectedOutput.resourcesAmount); + let currentOutputAmount = resources.output.get(resourceId) || 0 + resources.output.set(resourceId, currentOutputAmount - connectedOutput.resourcesAmount); + }) + }) + }); + }) + + let factoryRecipe = new LinkedFactory( + factoryToImport, + factoryToImport, + Array.from(resources.input).filter(([key, value]) => value > 0).map(([key, value]) => ({ "id": key, "amount": value })), + Array.from(resources.output).filter(([key, value]) => value > 0).map(([key, value]) => ({ "id": key, "amount": value })), + powerConsumption + ) + let detail = { recipe: factoryRecipe, machine: factoryRecipe.getMachine() }; + const importFactoryEvent = new CustomEvent(FactoryImporter.factoryImportedEvent, { detail: detail}); + FactoryImporter._instance.dispatchEvent(importFactoryEvent); + } +} \ No newline at end of file diff --git a/src/GameData/GameData.ts b/src/GameData/GameData.ts index de6784e..118fc31 100644 --- a/src/GameData/GameData.ts +++ b/src/GameData/GameData.ts @@ -51,18 +51,18 @@ export function loadSatisfactoryRecipe(recipeId: string): { recipe: GameRecipe, { for (const machine of satisfactoryData.machines) { - let result = machine.recipes.find((recipe: GameRecipe) => recipe.id === recipeId); + let result = machine.recipes.find((recipe) => recipe.id === recipeId); if (result != undefined) { - return { recipe: result, machine: machine }; + return { recipe: GameRecipe.fromRawData(result, machine), machine: machine }; } - let alternate = machine.alternateRecipes.find((recipe: GameRecipe) => recipe.id === recipeId); + let alternate = machine.alternateRecipes.find((recipe) => recipe.id === recipeId); if (alternate != undefined) { - return { recipe: alternate, machine: machine }; + return { recipe: GameRecipe.fromRawData(alternate, machine), machine: machine }; } } @@ -92,7 +92,7 @@ export function loadSingleSatisfactoryRecipe(requiredItem: { id: string; type: " return false; } - suitableRecipe = recipe; + suitableRecipe = GameRecipe.fromRawData(recipe, machine); suitableMachine = machine; resourceAmount = foundResource.amount; } diff --git a/src/GameData/GameMachine.ts b/src/GameData/GameMachine.ts index 7c2e204..30410c3 100644 --- a/src/GameData/GameMachine.ts +++ b/src/GameData/GameMachine.ts @@ -1,4 +1,6 @@ -export class GameMachine +import { Machine } from "../Machine"; + +export class GameMachine implements Machine { public constructor( public iconPath: string, diff --git a/src/GameData/GameRecipe.ts b/src/GameData/GameRecipe.ts index b41a1ef..42dbf13 100644 --- a/src/GameData/GameRecipe.ts +++ b/src/GameData/GameRecipe.ts @@ -1,14 +1,54 @@ import { GameMachine } from "./GameMachine"; - -export class GameRecipe +import { AppData } from "../AppData"; +import { Recipe } from "../Recipe"; +import { Machine } from "../Machine"; +import { loadSatisfactoryRecipe } from "./GameData"; +// Represents a receipe that exists in the game. +export class GameRecipe implements Recipe { public constructor( public id: string, public displayName: string, public ingredients: RecipeResource[], public products: RecipeResource[], - public manufacturingDuration: number - ) { } + public manufacturingDuration: number, + private _machine: GameMachine, + ) {} + + public getRecipeType(): string { + return "GameRecipe"; + } + + // We don't actually serialize standard recipes, they get loaded from game data + public toSerializable(): AppData.SerializableCustomRecipe { + return { + "ingredients": [], + "products": [] + } + } + + public static fromSerializable(id: string): GameRecipe + { + return loadSatisfactoryRecipe(id).recipe; + } + + public static fromRawData(object: {id: string, displayName: string, ingredients: RecipeResource[], products: RecipeResource[], manufacturingDuration: number}, machine: GameMachine): GameRecipe + { + let recipe = new GameRecipe( + object.id, + object.displayName, + object.ingredients, + object.products, + object.manufacturingDuration, + machine + ); + return recipe; + } + + public getMachine(): Machine + { + return this._machine; + } } export class GameRecipeEvent extends Event diff --git a/src/Machine.ts b/src/Machine.ts new file mode 100644 index 0000000..d746301 --- /dev/null +++ b/src/Machine.ts @@ -0,0 +1,7 @@ +export interface Machine +{ + iconPath: string, + displayName: string, + powerConsumption: number, + powerConsumptionExponent: number, +} diff --git a/src/Recipe.ts b/src/Recipe.ts new file mode 100644 index 0000000..dfa841e --- /dev/null +++ b/src/Recipe.ts @@ -0,0 +1,14 @@ +import { AppData } from "./AppData"; +import { Machine } from "./Machine"; + +export interface Recipe +{ + id: string, + displayName: string, + ingredients: RecipeResource[], + products: RecipeResource[], + manufacturingDuration: number, + getRecipeType: () => string, + toSerializable: () => AppData.SerializableCustomRecipe, + getMachine: () => Machine +} diff --git a/src/RecipeSelectionModal.ts b/src/RecipeSelectionModal.ts index eb5860a..8ef42f0 100644 --- a/src/RecipeSelectionModal.ts +++ b/src/RecipeSelectionModal.ts @@ -377,7 +377,7 @@ export class RecipeSelectionModal extends EventTarget recipeSelector.classList.remove("animate-progress"); - this._selectedRecipe = { recipe: recipe, madeIn: madeIn }; + this._selectedRecipe = { recipe: GameRecipe.fromRawData(recipe, madeIn), madeIn: madeIn }; this.dispatchEvent(new Event(RecipeSelectionModal.recipeSelectedEvent)); @@ -392,7 +392,7 @@ export class RecipeSelectionModal extends EventTarget let progressBarTimerId = setTimeout(() => { - this._selectedRecipe = { recipe: recipe, madeIn: madeIn }; + this._selectedRecipe = { recipe: GameRecipe.fromRawData(recipe, madeIn), madeIn: madeIn }; this.dispatchEvent(new Event(RecipeSelectionModal.recipeSelectedEvent)); diff --git a/src/Sankey/NodeConfiguration/NodeConfiguration.ts b/src/Sankey/NodeConfiguration/NodeConfiguration.ts index a53a2ab..700628d 100644 --- a/src/Sankey/NodeConfiguration/NodeConfiguration.ts +++ b/src/Sankey/NodeConfiguration/NodeConfiguration.ts @@ -1,6 +1,7 @@ +import { LinkedFactory } from '../../CustomData/LinkedFactory'; import { loadSatisfactoryResource, overclockPower, toItemsInMinute } from '../../GameData/GameData'; -import { GameMachine } from "../../GameData/GameMachine"; -import { GameRecipe } from "../../GameData/GameRecipe"; +import { Machine } from "../../Machine"; +import { Recipe } from "../../Recipe"; import { Configurators } from './Configurator'; import { ConfiguratorBuilder } from './ConfiguratorBuilder'; @@ -11,7 +12,7 @@ export class NodeConfiguration extends EventTarget public static readonly configurationUpdatedEvent = "configuration-updated"; - public constructor(recipe: GameRecipe, machine: GameMachine) + public constructor(recipe: Recipe, machine: Machine) { super(); @@ -96,7 +97,7 @@ export class NodeConfiguration extends EventTarget }); } - public openConfigurationWindow(openingMachinesAmount: number, openingOverclockRatio: number): void + public openConfigurationWindow(openingMachinesAmount: number, openingOverclockRatio: number, recipe: Recipe): void { this._openingMachinesAmount = openingMachinesAmount; this._openingOverclockRatio = openingOverclockRatio; @@ -138,6 +139,18 @@ export class NodeConfiguration extends EventTarget this._overclockConfigurators.powerConfigurator! ); + // Don't show overclock section for LinkedFactories + if (recipe instanceof LinkedFactory) + { + NodeConfiguration._overclockLabel.classList.add("hidden"); + NodeConfiguration._overclockData.classList.add("hidden"); + } + else + { + NodeConfiguration._overclockLabel.classList.remove("hidden"); + NodeConfiguration._overclockData.classList.remove("hidden"); + } + /* Modal window */ NodeConfiguration._modalContainer.classList.remove("hidden"); @@ -166,7 +179,7 @@ export class NodeConfiguration extends EventTarget this.closeConfigurationWindow(); } - private setupTableElements(recipe: GameRecipe, machine: GameMachine) + private setupTableElements(recipe: Recipe, machine: Machine) { let minOverclockRatio = NodeConfiguration._minOverclockRatio; let maxOverclockRatio = NodeConfiguration._maxOverclockRatio; @@ -410,6 +423,9 @@ export class NodeConfiguration extends EventTarget private static readonly _overclockOutputsColumn = NodeConfiguration.getColumn("overclock", "outputs"); private static readonly _overclockPowerColumn = NodeConfiguration.getColumn("overclock", "power"); + private static readonly _overclockLabel = document.querySelector("#overclock-label") as HTMLDivElement; + private static readonly _overclockData = document.querySelector("#overclock-config") as HTMLDivElement; + private static readonly _resetButton = NodeConfiguration.queryModalSuccessor(".reset-button") as HTMLDivElement; private static readonly _restoreButton = diff --git a/src/Sankey/NodeResourceDisplay.ts b/src/Sankey/NodeResourceDisplay.ts index 9f09b51..59c6c18 100644 --- a/src/Sankey/NodeResourceDisplay.ts +++ b/src/Sankey/NodeResourceDisplay.ts @@ -1,13 +1,14 @@ -import { GameRecipe } from "../GameData/GameRecipe"; +import { Machine } from "../Machine"; +import { Recipe } from "../Recipe"; import { Rectangle } from "../Geometry/Rectangle"; import { SvgFactory } from "../SVG/SvgFactory"; import { loadSatisfactoryResource, overclockPower, satisfactoryIconPath, toItemsInMinute } from '../GameData/GameData'; -import { GameMachine } from '../GameData/GameMachine'; import { SankeyNode } from './SankeyNode'; +import { GameRecipe } from "../GameData/GameRecipe"; export class NodeResourceDisplay { - public constructor(associatedNode: SankeyNode, recipe: GameRecipe, machine: GameMachine) + public constructor(associatedNode: SankeyNode, recipe: Recipe, machine: Machine) { this._recipe = recipe; this._machine = machine; @@ -16,7 +17,10 @@ export class NodeResourceDisplay let recipeContainer = this.createHtmlElement("div", "recipe-container") as HTMLDivElement; this.createMachineDisplay(recipeContainer, machine); - this.createOverclockDisplay(recipeContainer); + if (recipe instanceof GameRecipe) + { + this.createOverclockDisplay(recipeContainer); + } this.createInputsDisplay(recipeContainer, recipe); this.createOutputsDisplay(recipeContainer, recipe); this.createPowerDisplay(recipeContainer, machine.powerConsumption); @@ -42,7 +46,7 @@ export class NodeResourceDisplay element.appendChild(this._displayContainer); } - private createMachineDisplay(parent: HTMLDivElement, machine: GameMachine) + private createMachineDisplay(parent: HTMLDivElement, machine: Machine) { let machineDisplay = this.createHtmlElement("div", "property") as HTMLDivElement; @@ -61,7 +65,7 @@ export class NodeResourceDisplay parent.appendChild(machineDisplay); } - private createInputsDisplay(parent: HTMLDivElement, recipe: GameRecipe) + private createInputsDisplay(parent: HTMLDivElement, recipe: Recipe) { let inputsDisplay = this.createHtmlElement("div", "property") as HTMLDivElement; @@ -81,7 +85,7 @@ export class NodeResourceDisplay parent.appendChild(inputsDisplay); } - private createOutputsDisplay(parent: HTMLDivElement, recipe: GameRecipe) + private createOutputsDisplay(parent: HTMLDivElement, recipe: Recipe) { let outputsDisplay = this.createHtmlElement("div", "property") as HTMLDivElement; @@ -109,7 +113,7 @@ export class NodeResourceDisplay title.innerText = "Power"; this._powerDisplay = this.createHtmlElement("div", "text") as HTMLDivElement; - this._powerDisplay.innerText = `${powerConsumption} MW`; + this._powerDisplay.innerText = `${powerConsumption.toFixed(1)} MW`; powerDisplay.appendChild(title); powerDisplay.appendChild(this._powerDisplay); @@ -181,7 +185,10 @@ export class NodeResourceDisplay let toFixed = (value: number) => +value.toFixed(2); this._machinesAmountDisplay.innerText = `${toFixed(associatedNode.machinesAmount)}`; - this._overclockDisplay.innerText = `${toFixed(associatedNode.overclockRatio * 100)}%`; + if (associatedNode.recipe instanceof GameRecipe) + { + this._overclockDisplay.innerText = `${toFixed(associatedNode.overclockRatio * 100)}%`; + } for (const inputDisplay of this._inputDisplays) { @@ -210,8 +217,8 @@ export class NodeResourceDisplay this._powerDisplay.innerText = `${toFixed(overclockedPower * associatedNode.machinesAmount)} MW`; } - private readonly _recipe: GameRecipe; - private readonly _machine: GameMachine; + private readonly _recipe: Recipe; + private readonly _machine: Machine; private readonly _displayContainer: SVGForeignObjectElement; diff --git a/src/Sankey/SankeyNode.ts b/src/Sankey/SankeyNode.ts index bf39fa4..32e51b0 100644 --- a/src/Sankey/SankeyNode.ts +++ b/src/Sankey/SankeyNode.ts @@ -2,8 +2,9 @@ import { Point } from "../Geometry/Point"; import { SankeySlot } from "./Slots/SankeySlot"; import { SlotsGroup, SlotsGroupType } from "./SlotsGroup"; import { SvgFactory } from "../SVG/SvgFactory"; -import { GameRecipe } from "../GameData/GameRecipe"; -import { GameMachine } from "../GameData/GameMachine"; +import { LinkedFactory } from "../CustomData/LinkedFactory"; +import { Recipe } from "../Recipe"; +import { Machine } from "../Machine"; import { NodeContextMenu } from '../ContextMenu/NodeContextMenu'; import { NodeConfiguration } from './NodeConfiguration/NodeConfiguration'; import { loadSatisfactoryRecipe, overclockPower, overclockToShards, toItemsInMinute } from '../GameData/GameData'; @@ -27,15 +28,15 @@ export class SankeyNode extends EventTarget public constructor( position: Point, - recipe: GameRecipe, - machine: GameMachine, + recipe: Recipe, + machine: Machine, ) { super(); this.id = SankeyNode.acquireId(); - this._recipe = { ...recipe }; - this._machine = { ...machine }; + this._recipe = recipe; + this._machine = machine; this._height = SankeyNode._nodeHeight; let sumResources = (sum: number, product: RecipeResource) => @@ -121,6 +122,9 @@ export class SankeyNode extends EventTarget positionX: position.x, positionY: position.y, outputsGroups: outputGroups, + recipeType: this._recipe.getRecipeType(), + customRecipe: this._recipe.toSerializable(), + customPower: this.powerConsumption / this.machinesAmount }; return serializable; @@ -128,8 +132,21 @@ export class SankeyNode extends EventTarget public static fromSerializable(serializable: AppData.SerializableNode): SankeyNode { - let recipe = loadSatisfactoryRecipe(serializable.recipeId); - + let recipe: {recipe: Recipe, machine: Machine} + switch (serializable.recipeType) + { + case "": + case undefined: + case "GameRecipe": + recipe = loadSatisfactoryRecipe(serializable.recipeId); + break; + case "LinkedFactory": + let deserializedRecipe = LinkedFactory.fromSerializable(serializable.recipeId, serializable.customRecipe, serializable.customPower); + recipe = {recipe: deserializedRecipe, machine: deserializedRecipe.getMachine()}; + break; + default: + throw Error("Unknown RecipeType [" + serializable.recipeType + "]"); + } let node = new SankeyNode( { x: serializable.positionX, y: serializable.positionY }, recipe.recipe, @@ -324,7 +341,7 @@ export class SankeyNode extends EventTarget this.dispatchEvent(new Event(SankeyNode.resourcesAmountChangedEvent)); } - private configureContextMenu(recipe: GameRecipe, machine: GameMachine): void + private configureContextMenu(recipe: Recipe, machine: Machine): void { let nodeContextMenu = new NodeContextMenu(this.nodeSvg); @@ -337,7 +354,7 @@ export class SankeyNode extends EventTarget let openConfigurator = (event: Event) => { - configurator.openConfigurationWindow(this.machinesAmount, this.overclockRatio); + configurator.openConfigurationWindow(this.machinesAmount, this.overclockRatio, this._recipe); event.stopPropagation(); }; @@ -438,8 +455,8 @@ export class SankeyNode extends EventTarget SankeyNode._nextId = nextId; } - private _recipe: GameRecipe; - private _machine: GameMachine; + private _recipe: Recipe; + private _machine: Machine; private _inputResourcesAmount: number; private _outputResourcesAmount: number; diff --git a/src/SavesLoaderMenu.ts b/src/SavesLoaderMenu.ts index 1822cb5..0abfe63 100644 --- a/src/SavesLoaderMenu.ts +++ b/src/SavesLoaderMenu.ts @@ -1,4 +1,5 @@ import { AppData } from "./AppData"; +import { FactoryImporter } from "./FactoryImporter"; import { SvgIcons } from "./SVG/SvgIcons"; export class SavesLoaderMenu @@ -195,12 +196,15 @@ export class SavesLoaderMenu let planSelector = createHtmlElement("div", "plan-selector") as HTMLDivElement; let planNameElement = createHtmlElement("div", "plan-name"); + let importButton = createHtmlElement("div", "import-button"); let deleteButton = createHtmlElement("div", "delete-button"); planNameElement.innerText = name; + importButton.appendChild(SvgIcons.createIcon("plus")); deleteButton.appendChild(SvgIcons.createIcon("delete")); planSelector.appendChild(planNameElement); + planSelector.appendChild(importButton); planSelector.appendChild(deleteButton); planNameElement.addEventListener("click", (event) => @@ -210,6 +214,12 @@ export class SavesLoaderMenu this.close(); }); + importButton.addEventListener("click", (event) => + { + FactoryImporter.importSavedFactory(name); + this.close(); + }); + deleteButton.addEventListener("click", (event) => { event.stopPropagation(); @@ -230,10 +240,12 @@ export class SavesLoaderMenu if (name === AppData.instance.currentPlanName) { planSelector.classList.add("selected"); + importButton.classList.add("hidden"); } else { planSelector.classList.remove("selected"); + importButton.classList.remove("hidden"); } }; diff --git a/src/main.ts b/src/main.ts index f5104d3..74cab3b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { Point } from "./Geometry/Point"; import { MouseHandler } from "./MouseHandler"; import { GameRecipe } from "./GameData/GameRecipe"; import { GameMachine } from "./GameData/GameMachine"; +import { Recipe } from './Recipe'; import { Settings } from "./Settings"; import { CanvasContextMenu } from "./ContextMenu/CanvasContextMenu"; import { ResourcesSummary } from "./ResourcesSummary"; @@ -16,6 +17,8 @@ import { loadSatisfactoryResource, loadSingleSatisfactoryRecipe } from "./GameDa import { SankeyLink } from "./Sankey/SankeyLink"; import { SlotsGroup } from "./Sankey/SlotsGroup"; import { SavesLoaderMenu } from "./SavesLoaderMenu"; +import { FactoryImporter } from "./FactoryImporter"; +import { Machine } from "./Machine"; async function main() { @@ -85,9 +88,15 @@ async function main() let onceNodeCreated: ((node: SankeyNode) => void) | undefined; - function createNode(recipe: GameRecipe, machine: GameMachine): SankeyNode + function createNode(recipe: Recipe, machine: GameMachine): SankeyNode { - const node = new SankeyNode(nodeCreationPosition, recipe, machine); + let pageCenter = { + x: document.documentElement.clientWidth / 2, + y: document.documentElement.clientHeight / 2 + }; + + let creationPosition = nodeCreationPosition || MouseHandler.clientToCanvasPosition(pageCenter); + const node = new SankeyNode(creationPosition, recipe, machine); registerNode(node); @@ -401,6 +410,11 @@ async function main() } }); + FactoryImporter.instance.addEventListener(FactoryImporter.factoryImportedEvent, ((event: CustomEvent<{recipe: Recipe, machine: Machine}>) => + { + createNode(event.detail.recipe, event.detail.machine); + }) as EventListener); + AppData.instance.loadFromUrl(); let _savesLoaderMenu = new SavesLoaderMenu();