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