Make the storage its own building, add sanity checks

This commit is contained in:
tobspr 2020-09-23 08:59:39 +02:00
parent 47b26f4779
commit f8371a96cf
18 changed files with 223 additions and 182 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -584,7 +584,7 @@
"spriteSourceSize": {"x":13,"y":0,"w":261,"h":144},
"sourceSize": {"w":288,"h":144}
},
"sprites/blueprints/trash-storage.png":
"sprites/blueprints/storage.png":
{
"frame": {"x":4,"y":1001,"w":250,"h":288},
"rotated": false,
@ -992,7 +992,7 @@
"spriteSourceSize": {"x":14,"y":0,"w":260,"h":143},
"sourceSize": {"w":288,"h":144}
},
"sprites/buildings/trash-storage.png":
"sprites/buildings/storage.png":
{
"frame": {"x":4,"y":1295,"w":248,"h":288},
"rotated": false,
@ -1551,6 +1551,6 @@
"format": "RGBA8888",
"size": {"w":2048,"h":2048},
"scale": "0.75",
"smartupdate": "$TexturePacker:SmartUpdate:c57f50d18c59efc0edbd4a3a732323a4:3fcf23da2ddc6370c437cf41f6d44ed0:908b89f5ca8ff73e331a35a3b14d0604$"
"smartupdate": "$TexturePacker:SmartUpdate:5aa559a5b0e7b321ad8bc0595f1e8ec0:3fcf23da2ddc6370c437cf41f6d44ed0:908b89f5ca8ff73e331a35a3b14d0604$"
}
}

View File

@ -584,7 +584,7 @@
"spriteSourceSize": {"x":3,"y":0,"w":89,"h":48},
"sourceSize": {"w":96,"h":48}
},
"sprites/blueprints/trash-storage.png":
"sprites/blueprints/storage.png":
{
"frame": {"x":768,"y":112,"w":85,"h":96},
"rotated": false,
@ -992,7 +992,7 @@
"spriteSourceSize": {"x":4,"y":0,"w":88,"h":48},
"sourceSize": {"w":96,"h":48}
},
"sprites/buildings/trash-storage.png":
"sprites/buildings/storage.png":
{
"frame": {"x":859,"y":112,"w":85,"h":96},
"rotated": false,
@ -1551,6 +1551,6 @@
"format": "RGBA8888",
"size": {"w":1024,"h":1024},
"scale": "0.25",
"smartupdate": "$TexturePacker:SmartUpdate:c57f50d18c59efc0edbd4a3a732323a4:3fcf23da2ddc6370c437cf41f6d44ed0:908b89f5ca8ff73e331a35a3b14d0604$"
"smartupdate": "$TexturePacker:SmartUpdate:5aa559a5b0e7b321ad8bc0595f1e8ec0:3fcf23da2ddc6370c437cf41f6d44ed0:908b89f5ca8ff73e331a35a3b14d0604$"
}
}

View File

@ -584,7 +584,7 @@
"spriteSourceSize": {"x":8,"y":0,"w":175,"h":96},
"sourceSize": {"w":192,"h":96}
},
"sprites/blueprints/trash-storage.png":
"sprites/blueprints/storage.png":
{
"frame": {"x":574,"y":310,"w":167,"h":192},
"rotated": false,
@ -992,7 +992,7 @@
"spriteSourceSize": {"x":9,"y":0,"w":174,"h":96},
"sourceSize": {"w":192,"h":96}
},
"sprites/buildings/trash-storage.png":
"sprites/buildings/storage.png":
{
"frame": {"x":574,"y":508,"w":166,"h":192},
"rotated": false,
@ -1551,6 +1551,6 @@
"format": "RGBA8888",
"size": {"w":1024,"h":2048},
"scale": "0.5",
"smartupdate": "$TexturePacker:SmartUpdate:c57f50d18c59efc0edbd4a3a732323a4:3fcf23da2ddc6370c437cf41f6d44ed0:908b89f5ca8ff73e331a35a3b14d0604$"
"smartupdate": "$TexturePacker:SmartUpdate:5aa559a5b0e7b321ad8bc0595f1e8ec0:3fcf23da2ddc6370c437cf41f6d44ed0:908b89f5ca8ff73e331a35a3b14d0604$"
}
}

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -1,5 +1,5 @@
$buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, trash, underground_belt, wire,
constant_signal, logic_gate, lever, filter, wire_tunnel, display, virtual_processor, reader;
constant_signal, logic_gate, lever, filter, wire_tunnel, display, virtual_processor, reader, storage;
@each $building in $buildings {
[data-icon="building_icons/#{$building}.png"] {
@ -9,7 +9,7 @@ $buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, tra
$buildingsAndVariants: belt, balancer, balancer-merger, balancer-splitter, underground_belt,
underground_belt-tier2, miner, miner-chainable, cutter, cutter-quad, rotater, rotater-ccw, rotater-fl,
stacker, mixer, painter, painter-double, painter-quad, trash, trash-storage;
stacker, mixer, painter, painter-double, painter-quad, trash, storage;
@each $building in $buildingsAndVariants {
[data-icon="building_tutorials/#{$building}.png"] {
background-image: uiResource("res/ui/building_tutorials/#{$building}.png") !important;

View File

@ -0,0 +1,101 @@
import { formatBigNumber } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { T } from "../../translations";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { StorageComponent } from "../components/storage";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
const storageSize = 5000;
export class MetaStorageBuilding extends MetaBuilding {
constructor() {
super("storage");
}
getSilhouetteColor() {
return "#bbdf6d";
}
/**
* @returns {Array<[string, string]>}
*/
getAdditionalStatistics(root, variant) {
return [[T.ingame.buildingPlacement.infoTexts.storage, formatBigNumber(storageSize)]];
}
getDimensions() {
return new Vector(2, 2);
}
/**
* @param {GameRoot} root
*/
getIsUnlocked(root) {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_storage);
}
/**
* Creates the entity at the given location
* @param {Entity} entity
*/
setupEntityComponents(entity) {
// Required, since the item processor needs this.
entity.addComponent(
new ItemEjectorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.top,
},
{
pos: new Vector(1, 0),
direction: enumDirection.top,
},
],
})
);
entity.addComponent(
new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 1),
directions: [enumDirection.bottom],
},
{
pos: new Vector(1, 1),
directions: [enumDirection.bottom],
},
],
})
);
entity.addComponent(
new StorageComponent({
maximumStorage: storageSize,
})
);
entity.addComponent(
new WiredPinsComponent({
slots: [
{
pos: new Vector(1, 1),
direction: enumDirection.right,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 1),
direction: enumDirection.left,
type: enumPinSlotType.logicalEjector,
},
],
})
);
}
}

View File

@ -1,65 +1,26 @@
import { formatBigNumber } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector";
import { T } from "../../translations";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
import { StorageComponent } from "../components/storage";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { enumHubGoalRewards } from "../tutorial_goals";
/** @enum {string} */
export const enumTrashVariants = { storage: "storage" };
const trashSize = 5000;
export class MetaTrashBuilding extends MetaBuilding {
constructor() {
super("trash");
}
getIsRotateable(variant) {
return variant !== defaultBuildingVariant;
getIsRotateable() {
return false;
}
getSilhouetteColor() {
return "#cd7d86";
}
/**
* @param {GameRoot} root
* @param {string} variant
* @returns {Array<[string, string]>}
*/
getAdditionalStatistics(root, variant) {
if (variant === enumTrashVariants.storage) {
return [[T.ingame.buildingPlacement.infoTexts.storage, formatBigNumber(trashSize)]];
}
return [];
}
getDimensions(variant) {
switch (variant) {
case defaultBuildingVariant:
return new Vector(1, 1);
case enumTrashVariants.storage:
return new Vector(2, 2);
default:
assertAlways(false, "Unknown trash variant: " + variant);
}
}
/**
* @param {GameRoot} root
*/
getAvailableVariants(root) {
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_storage)) {
return [defaultBuildingVariant, enumTrashVariants.storage];
}
return super.getAvailableVariants(root);
getDimensions() {
return new Vector(1, 1);
}
/**
@ -74,13 +35,6 @@ export class MetaTrashBuilding extends MetaBuilding {
* @param {Entity} entity
*/
setupEntityComponents(entity) {
// Required, since the item processor needs this.
entity.addComponent(
new ItemEjectorComponent({
slots: [],
})
);
entity.addComponent(
new ItemAcceptorComponent({
slots: [
@ -96,99 +50,11 @@ export class MetaTrashBuilding extends MetaBuilding {
],
})
);
}
/**
*
* @param {Entity} entity
* @param {number} rotationVariant
* @param {string} variant
*/
updateVariants(entity, rotationVariant, variant) {
switch (variant) {
case defaultBuildingVariant: {
if (!entity.components.ItemProcessor) {
entity.addComponent(
new ItemProcessorComponent({
inputsPerCharge: 1,
processorType: enumItemProcessorTypes.trash,
})
);
}
if (entity.components.Storage) {
entity.removeComponent(StorageComponent);
}
if (entity.components.WiredPins) {
entity.removeComponent(WiredPinsComponent);
}
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
directions: [
enumDirection.top,
enumDirection.right,
enumDirection.bottom,
enumDirection.left,
],
},
]);
entity.components.ItemEjector.setSlots([]);
entity.components.ItemProcessor.type = enumItemProcessorTypes.trash;
break;
}
case enumTrashVariants.storage: {
if (entity.components.ItemProcessor) {
entity.removeComponent(ItemProcessorComponent);
}
if (!entity.components.Storage) {
entity.addComponent(new StorageComponent({}));
}
if (!entity.components.WiredPins) {
entity.addComponent(
new WiredPinsComponent({
slots: [
{
pos: new Vector(1, 1),
direction: enumDirection.right,
type: enumPinSlotType.logicalEjector,
},
{
pos: new Vector(0, 1),
direction: enumDirection.left,
type: enumPinSlotType.logicalEjector,
},
],
})
);
}
entity.components.Storage.maximumStorage = trashSize;
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 1),
directions: [enumDirection.bottom],
},
{
pos: new Vector(1, 1),
directions: [enumDirection.bottom],
},
]);
entity.components.ItemEjector.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.top,
},
{
pos: new Vector(1, 0),
direction: enumDirection.top,
},
]);
break;
}
default:
assertAlways(false, "Unknown trash variant: " + variant);
}
entity.addComponent(
new ItemProcessorComponent({
inputsPerCharge: 1,
processorType: enumItemProcessorTypes.trash,
})
);
}
}

View File

@ -13,6 +13,7 @@ import { MetaStackerBuilding } from "../../buildings/stacker";
import { MetaTrashBuilding } from "../../buildings/trash";
import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt";
import { HUDBaseToolbar } from "./base_toolbar";
import { MetaStorageBuilding } from "../../buildings/storage";
const supportedBuildings = [
MetaBeltBuilding,
@ -25,6 +26,7 @@ const supportedBuildings = [
MetaMixerBuilding,
MetaPainterBuilding,
MetaTrashBuilding,
MetaStorageBuilding,
MetaLeverBuilding,
MetaFilterBuilding,
MetaDisplayBuilding,

View File

@ -54,6 +54,7 @@ export const KEYMAPPINGS = {
mixer: { keyCode: key("8") },
painter: { keyCode: key("9") },
trash: { keyCode: key("0") },
storage: { keyCode: key("I") },
lever: { keyCode: key("L") },
filter: { keyCode: key("B") },
@ -352,6 +353,13 @@ export class KeyActionMapper {
}
this.keybindings[key] = new Keybinding(this, this.root.app, payload);
if (G_IS_DEV) {
// Sanity
if (!T.keybindings.mappings[key]) {
assertAlways(false, "Keybinding " + key + " has no translation!");
}
}
}
}

View File

@ -9,7 +9,7 @@ import { enumPainterVariants, MetaPainterBuilding } from "./buildings/painter";
import { enumRotaterVariants, MetaRotaterBuilding } from "./buildings/rotater";
import { enumBalancerVariants, MetaBalancerBuilding } from "./buildings/balancer";
import { MetaStackerBuilding } from "./buildings/stacker";
import { enumTrashVariants, MetaTrashBuilding } from "./buildings/trash";
import { MetaTrashBuilding } from "./buildings/trash";
import { enumUndergroundBeltVariants, MetaUndergroundBeltBuilding } from "./buildings/underground_belt";
import { MetaWireBuilding } from "./buildings/wire";
import { buildBuildingCodeCache, gBuildingVariants, registerBuildingVariant } from "./building_codes";
@ -22,6 +22,9 @@ import { MetaWireTunnelBuilding, enumWireTunnelVariants } from "./buildings/wire
import { MetaDisplayBuilding } from "./buildings/display";
import { MetaVirtualProcessorBuilding, enumVirtualProcessorVariants } from "./buildings/virtual_processor";
import { MetaReaderBuilding } from "./buildings/reader";
import { MetaStorageBuilding } from "./buildings/storage";
import { KEYMAPPINGS } from "./key_action_mapper";
import { T } from "../translations";
const logger = createLogger("building_registry");
@ -34,6 +37,7 @@ export function initMetaBuildingRegistry() {
gMetaBuildingRegistry.register(MetaMixerBuilding);
gMetaBuildingRegistry.register(MetaPainterBuilding);
gMetaBuildingRegistry.register(MetaTrashBuilding);
gMetaBuildingRegistry.register(MetaStorageBuilding);
gMetaBuildingRegistry.register(MetaBeltBuilding);
gMetaBuildingRegistry.register(MetaUndergroundBeltBuilding);
gMetaBuildingRegistry.register(MetaHubBuilding);
@ -86,7 +90,9 @@ export function initMetaBuildingRegistry() {
// Trash
registerBuildingVariant(20, MetaTrashBuilding);
registerBuildingVariant(21, MetaTrashBuilding, enumTrashVariants.storage);
// Storage
registerBuildingVariant(21, MetaStorageBuilding);
// Underground belt
registerBuildingVariant(22, MetaUndergroundBeltBuilding, defaultBuildingVariant, 0);
@ -157,6 +163,29 @@ export function initMetaBuildingRegistry() {
}
}
// Check for valid keycodes
if (G_IS_DEV) {
gMetaBuildingRegistry.entries.forEach(metaBuilding => {
const id = metaBuilding.getId();
if (!["hub"].includes(id)) {
if (!KEYMAPPINGS.buildings[id]) {
assertAlways(
false,
"Building " + id + " has no keybinding assigned! Add it to key_action_mapper.js"
);
}
if (!T.buildings[id]) {
assertAlways(false, "Translation for building " + id + " missing!");
}
if (!T.buildings[id].default) {
assertAlways(false, "Translation for building " + id + " missing (default variant)!");
}
}
});
}
logger.log("Registered", gMetaBuildingRegistry.getNumEntries(), "buildings");
logger.log("Registered", Object.keys(gBuildingVariants).length, "building codes");
}

View File

@ -94,6 +94,8 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
for (let j = 0; j < itemsToEject.length; ++j) {
const { item, requiredSlot, preferredSlot } = itemsToEject[j];
assert(ejectorComp, "To eject items, the building needs to have an ejector");
let slot = null;
if (requiredSlot !== null && requiredSlot !== undefined) {
// We have a slot override, check if that is free
@ -306,6 +308,10 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
* @param {ProcessorImplementationPayload} payload
*/
process_BALANCER(payload) {
assert(
payload.entity.components.ItemEjector,
"To be a balancer, the building needs to have an ejector"
);
const availableSlots = payload.entity.components.ItemEjector.slots.length;
const processorComp = payload.entity.components.ItemProcessor;

View File

@ -1,20 +1,19 @@
import { MetaBuilding, defaultBuildingVariant } from "./meta_building";
import { MetaCutterBuilding, enumCutterVariants } from "./buildings/cutter";
import { MetaRotaterBuilding, enumRotaterVariants } from "./buildings/rotater";
import { MetaPainterBuilding, enumPainterVariants } from "./buildings/painter";
import { MetaMixerBuilding } from "./buildings/mixer";
import { MetaStackerBuilding } from "./buildings/stacker";
import { MetaBalancerBuilding, enumBalancerVariants } from "./buildings/balancer";
import { MetaUndergroundBeltBuilding, enumUndergroundBeltVariants } from "./buildings/underground_belt";
import { MetaMinerBuilding, enumMinerVariants } from "./buildings/miner";
import { MetaTrashBuilding, enumTrashVariants } from "./buildings/trash";
/** @typedef {Array<[typeof MetaBuilding, string]>} TutorialGoalReward */
import { enumHubGoalRewards } from "./tutorial_goals";
import { MetaReaderBuilding } from "./buildings/reader";
import { MetaDisplayBuilding } from "./buildings/display";
import { T } from "../translations";
import { enumBalancerVariants, MetaBalancerBuilding } from "./buildings/balancer";
import { MetaConstantSignalBuilding } from "./buildings/constant_signal";
import { enumCutterVariants, MetaCutterBuilding } from "./buildings/cutter";
import { MetaDisplayBuilding } from "./buildings/display";
import { enumMinerVariants, MetaMinerBuilding } from "./buildings/miner";
import { MetaMixerBuilding } from "./buildings/mixer";
import { enumPainterVariants, MetaPainterBuilding } from "./buildings/painter";
import { MetaReaderBuilding } from "./buildings/reader";
import { enumRotaterVariants, MetaRotaterBuilding } from "./buildings/rotater";
import { MetaStackerBuilding } from "./buildings/stacker";
import { MetaStorageBuilding } from "./buildings/storage";
import { enumUndergroundBeltVariants, MetaUndergroundBeltBuilding } from "./buildings/underground_belt";
import { defaultBuildingVariant, MetaBuilding } from "./meta_building";
/** @typedef {Array<[typeof MetaBuilding, string]>} TutorialGoalReward */
import { enumHubGoalRewards } from "./tutorial_goals";
/**
* Helper method for proper types
@ -46,7 +45,7 @@ export const enumHubGoalRewardsToContentUnlocked = {
[enumHubGoalRewards.reward_cutter_quad]: typed([[MetaCutterBuilding, enumCutterVariants.quad]]),
[enumHubGoalRewards.reward_painter_double]: typed([[MetaPainterBuilding, enumPainterVariants.double]]),
[enumHubGoalRewards.reward_painter_quad]: typed([[MetaPainterBuilding, enumPainterVariants.quad]]),
[enumHubGoalRewards.reward_storage]: typed([[MetaTrashBuilding, enumTrashVariants.storage]]),
[enumHubGoalRewards.reward_storage]: typed([[MetaStorageBuilding]]),
[enumHubGoalRewards.reward_belt_reader]: typed([[MetaReaderBuilding, defaultBuildingVariant]]),
[enumHubGoalRewards.reward_display]: typed([[MetaDisplayBuilding, defaultBuildingVariant]]),
@ -54,9 +53,31 @@ export const enumHubGoalRewardsToContentUnlocked = {
[MetaConstantSignalBuilding, defaultBuildingVariant],
]),
[enumHubGoalRewards.reward_second_wire]: null, // @TODO!
[enumHubGoalRewards.reward_logic_gates]: null, // @TODO!
[enumHubGoalRewards.reward_virtual_processing]: null, // @TODO!
[enumHubGoalRewards.reward_wires_filters_and_levers]: null,
[enumHubGoalRewards.reward_freeplay]: null,
[enumHubGoalRewards.reward_blueprints]: null,
[enumHubGoalRewards.no_reward]: null,
[enumHubGoalRewards.no_reward_freeplay]: null,
};
if (G_IS_DEV) {
// Sanity check
for (const rewardId in enumHubGoalRewards) {
const mapping = enumHubGoalRewardsToContentUnlocked[rewardId];
if (typeof mapping === "undefined") {
assertAlways(
false,
"Please define a mapping for the reward " + rewardId + " in tutorial_goals_mappings.js"
);
}
const translation = T.storyRewards[rewardId];
if (!translation || !translation.title || !translation.desc) {
assertAlways(false, "Translation for reward " + rewardId + "missing");
}
}
}

View File

@ -1,5 +1,6 @@
import { gMetaBuildingRegistry } from "../../core/global_registries.js";
import { createLogger } from "../../core/logging.js";
import { enumBalancerVariants, MetaBalancerBuilding } from "../../game/buildings/balancer.js";
import { MetaBeltBuilding } from "../../game/buildings/belt.js";
import { enumCutterVariants, MetaCutterBuilding } from "../../game/buildings/cutter.js";
import { MetaHubBuilding } from "../../game/buildings/hub.js";
@ -7,9 +8,9 @@ import { enumMinerVariants, MetaMinerBuilding } from "../../game/buildings/miner
import { MetaMixerBuilding } from "../../game/buildings/mixer.js";
import { enumPainterVariants, MetaPainterBuilding } from "../../game/buildings/painter.js";
import { enumRotaterVariants, MetaRotaterBuilding } from "../../game/buildings/rotater.js";
import { enumBalancerVariants, MetaBalancerBuilding } from "../../game/buildings/balancer.js";
import { MetaStackerBuilding } from "../../game/buildings/stacker.js";
import { enumTrashVariants, MetaTrashBuilding } from "../../game/buildings/trash.js";
import { MetaStorageBuilding } from "../../game/buildings/storage.js";
import { MetaTrashBuilding } from "../../game/buildings/trash.js";
import {
enumUndergroundBeltVariants,
MetaUndergroundBeltBuilding,
@ -126,9 +127,11 @@ export class SavegameInterface_V1006 extends SavegameInterface_V1005 {
),
"sprites/blueprints/painter-quad.png": findCode(MetaPainterBuilding, enumPainterVariants.quad),
// Trash / Storage
// Trash
"sprites/blueprints/trash.png": findCode(MetaTrashBuilding),
"sprites/blueprints/trash-storage.png": findCode(MetaTrashBuilding, enumTrashVariants.storage),
// Storage
"sprites/blueprints/trash-storage.png": findCode(MetaStorageBuilding),
};
}

View File

@ -557,8 +557,9 @@ buildings:
name: &trash Trash
description: Accepts inputs from all sides and destroys them. Forever.
storage:
name: Storage
storage:
default:
name: &storage Storage
description: Stores excess items, up to a given capacity. Can be used as an overflow gate.
wire:
@ -726,7 +727,9 @@ storyRewards:
reward_storage:
title: Storage Buffer
desc: You have unlocked a variant of the <strong>trash</strong> - It allows you to store items up to a given capacity!
desc: >-
You have unlocked the <strong>storage</strong> building - It allows you to store items up to a given capacity!<br><br>
It priorities the left output, so you can also use it as an <strong>overflow gate</strong>!
reward_blueprints:
title: Blueprints
@ -1030,6 +1033,7 @@ keybindings:
mixer: *mixer
painter: *painter
trash: *trash
storage: *storage
wire: *wire
constant_signal: *constant_signal
logic_gate: *logic_gate
@ -1038,6 +1042,7 @@ keybindings:
wire_tunnel: *wire_tunnel
display: *display
reader: *reader
virtual_processor: *virtual_processor
# ---
pipette: Pipette