diff --git a/src/js/core/config.js b/src/js/core/config.js index b451e848..602e0c82 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -87,17 +87,14 @@ export const globalConfig = { puzzleMaxBoundsSize: 20, puzzleValidationDurationSeconds: 30, - buildingSpeeds: { - cutter: 1 / 4, - cutterQuad: 1 / 4, - rotater: 1 / 1, - rotaterCCW: 1 / 1, - rotater180: 1 / 1, - painter: 1 / 6, - painterDouble: 1 / 8, - painterQuad: 1 / 2, - mixer: 1 / 5, - stacker: 1 / 8, + buildingRatios: { + cutter: 4, + cutterQuad: 4, + painter: 6, + painterDouble: 8, + painterQuad: 2, + mixer: 5, + stacker: 8, }, // Zooming diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index 80efecbe..6cda9f22 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -119,29 +119,18 @@ export class BeltPath extends BasicSerializableObject { this.numCompressedItemsAfterFirstItem = 0; } - /** - * Returns whether this path can accept a new item - * @returns {boolean} - */ - canAcceptItem() { - return this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts; - } - /** * Tries to accept the item * @param {BaseItem} item + * @param {number} extraProgress */ - tryAcceptItem(item) { + tryAcceptItem(item, extraProgress = 0) { if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) { // So, since we already need one tick to accept this item we will add this directly. - const beltProgressPerTick = - this.root.hubGoals.getBeltBaseSpeed() * - this.root.dynamicTickrate.deltaSeconds * - globalConfig.itemSpacingOnBelts; // First, compute how much progress we can make *at max* const maxProgress = Math.max(0, this.spacingToFirstItem - globalConfig.itemSpacingOnBelts); - const initialProgress = Math.min(maxProgress, beltProgressPerTick); + const initialProgress = Math.min(maxProgress, extraProgress); this.items.unshift([this.spacingToFirstItem - initialProgress, item]); this.spacingToFirstItem = initialProgress; @@ -227,8 +216,6 @@ export class BeltPath extends BasicSerializableObject { return; } - const noSimplifiedBelts = !this.root.app.settings.getAllSettings().simplifiedBelts; - DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid); const targetStaticComp = targetEntity.components.StaticMapEntity; const targetBeltComp = targetEntity.components.Belt; @@ -274,95 +261,15 @@ export class BeltPath extends BasicSerializableObject { } const matchingSlotIndex = matchingSlot.index; - const passOver = this.computePassOverFunctionWithoutBelts(targetEntity, matchingSlotIndex); - if (!passOver) { - return; - } - const matchingDirection = enumInvertedDirections[ejectingDirection]; - const filter = matchingSlot.slot.filter; - - return function (item, remainingProgress = 0.0) { - // Check if the acceptor has a filter - if (filter && item._type !== filter) { - return false; - } - - // Try to pass over - if (passOver(item, matchingSlotIndex)) { - // Trigger animation on the acceptor comp - if (noSimplifiedBelts) { - targetAcceptorComp.onItemAccepted( - matchingSlotIndex, - matchingDirection, - item, - remainingProgress - ); - } + return function (item, startProgress = 0.0) { + if (targetAcceptorComp.tryAcceptItem(targetEntity, matchingSlotIndex, item, startProgress)) { return true; } return false; }; } - /** - * Computes a method to pass over the item to the entity - * @param {Entity} entity - * @param {number} matchingSlotIndex - * @returns {(item: BaseItem, slotIndex: number) => boolean | void} - */ - computePassOverFunctionWithoutBelts(entity, matchingSlotIndex) { - const systems = this.root.systemMgr.systems; - const hubGoals = this.root.hubGoals; - - // NOTICE: THIS IS COPIED FROM THE ITEM EJECTOR SYSTEM FOR PEROFMANCE REASONS - - const itemProcessorComp = entity.components.ItemProcessor; - if (itemProcessorComp) { - // Its an item processor .. - return function (item) { - // Check for potential filters - if (!systems.itemProcessor.checkRequirements(entity, item, matchingSlotIndex)) { - return; - } - return itemProcessorComp.tryTakeItem(item, matchingSlotIndex); - }; - } - - const undergroundBeltComp = entity.components.UndergroundBelt; - if (undergroundBeltComp) { - // Its an underground belt. yay. - return function (item) { - return undergroundBeltComp.tryAcceptExternalItem( - item, - hubGoals.getUndergroundBeltBaseSpeed() - ); - }; - } - - const storageComp = entity.components.Storage; - if (storageComp) { - // It's a storage - return function (item) { - if (storageComp.canAcceptItem(item)) { - storageComp.takeItem(item); - return true; - } - }; - } - - const filterComp = entity.components.Filter; - if (filterComp) { - // It's a filter! Unfortunately the filter has to know a lot about it's - // surrounding state and components, so it can't be within the component itself. - return function (item) { - if (systems.filter.tryAcceptItem(entity, matchingSlotIndex, item)) { - return true; - } - }; - } - } - // Following code will be compiled out outside of dev versions /* dev:start */ diff --git a/src/js/game/buildings/balancer.js b/src/js/game/buildings/balancer.js index ce685a9a..d87c0a46 100644 --- a/src/js/game/buildings/balancer.js +++ b/src/js/game/buildings/balancer.js @@ -104,8 +104,7 @@ export class MetaBalancerBuilding extends MetaBuilding { speedMultiplier = 1; } - const speed = - (root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.balancer) / 2) * speedMultiplier; + const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.balancer) * speedMultiplier; return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; } diff --git a/src/js/game/buildings/painter.js b/src/js/game/buildings/painter.js index 432973d0..da95f4db 100644 --- a/src/js/game/buildings/painter.js +++ b/src/js/game/buildings/painter.js @@ -1,7 +1,7 @@ import { formatItemsPerSecond } from "../../core/utils"; import { enumDirection, Vector } from "../../core/vector"; import { T } from "../../translations"; -import { ItemAcceptorComponent } from "../components/item_acceptor"; +import { enumInputRequirements, ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; import { enumItemProcessorTypes, @@ -274,6 +274,7 @@ export class MetaPainterBuilding extends MetaBuilding { filter: "color", }, ]); + entity.components.ItemAcceptor.inputRequirement = enumInputRequirements.quadPainter; entity.components.ItemEjector.setSlots([ { pos: new Vector(0, 0), direction: enumDirection.top }, diff --git a/src/js/game/buildings/storage.js b/src/js/game/buildings/storage.js index 78f398be..92355557 100644 --- a/src/js/game/buildings/storage.js +++ b/src/js/game/buildings/storage.js @@ -1,7 +1,7 @@ import { formatBigNumber } from "../../core/utils"; import { enumDirection, Vector } from "../../core/vector"; import { T } from "../../translations"; -import { ItemAcceptorComponent } from "../components/item_acceptor"; +import { enumInputRequirements, ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; import { StorageComponent } from "../components/storage"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; @@ -81,6 +81,7 @@ export class MetaStorageBuilding extends MetaBuilding { direction: enumDirection.bottom, }, ], + inputRequirement: enumInputRequirements.storage, }) ); diff --git a/src/js/game/buildings/trash.js b/src/js/game/buildings/trash.js index fcf7f11f..bda36001 100644 --- a/src/js/game/buildings/trash.js +++ b/src/js/game/buildings/trash.js @@ -47,25 +47,6 @@ export class MetaTrashBuilding extends MetaBuilding { return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_cutter_and_trash); } - addAchievementReceiver(entity) { - if (!entity.root) { - return; - } - - const itemProcessor = entity.components.ItemProcessor; - const tryTakeItem = itemProcessor.tryTakeItem.bind(itemProcessor); - - itemProcessor.tryTakeItem = () => { - const taken = tryTakeItem(...arguments); - - if (taken) { - entity.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.trash1000, 1); - } - - return taken; - }; - } - /** * Creates the entity at the given location * @param {Entity} entity @@ -100,7 +81,5 @@ export class MetaTrashBuilding extends MetaBuilding { processorType: enumItemProcessorTypes.trash, }) ); - - this.addAchievementReceiver(entity); } } diff --git a/src/js/game/buildings/underground_belt.js b/src/js/game/buildings/underground_belt.js index 7009ebd7..4dbce634 100644 --- a/src/js/game/buildings/underground_belt.js +++ b/src/js/game/buildings/underground_belt.js @@ -184,7 +184,6 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding { * @param {Entity} entity */ setupEntityComponents(entity) { - // Required, since the item processor needs this. entity.addComponent( new ItemEjectorComponent({ slots: [], diff --git a/src/js/game/components/filter.js b/src/js/game/components/filter.js index 8a22a076..4c16ea6f 100644 --- a/src/js/game/components/filter.js +++ b/src/js/game/components/filter.js @@ -6,7 +6,7 @@ import { typeItemSingleton } from "../item_resolver"; /** * @typedef {{ * item: BaseItem, - * progress: number + * extraProgress: number * }} PendingFilterItem */ @@ -24,14 +24,14 @@ export class FilterComponent extends Component { pendingItemsToLeaveThrough: types.array( types.structured({ item: typeItemSingleton, - progress: types.ufloat, + extraProgress: types.ufloat, }) ), pendingItemsToReject: types.array( types.structured({ item: typeItemSingleton, - progress: types.ufloat, + extraProgress: types.ufloat, //@SENSETODO will need save migration }) ), }; diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index d3df3763..d015e5cf 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -2,6 +2,10 @@ import { enumDirection, enumInvertedDirections, Vector } from "../../core/vector import { types } from "../../savegame/serialization"; import { BaseItem } from "../base_item"; import { Component } from "../component"; +import { Entity } from "../entity"; +import { isTruthyItem } from "../items/boolean_item"; +import { typeItemSingleton } from "../item_resolver"; +import { GameRoot } from "../root"; /** * @typedef {{ @@ -24,34 +28,85 @@ import { Component } from "../component"; * filter?: ItemType * }} ItemAcceptorSlotConfig */ +/** + * @typedef {Array<{ + * slotIndex: number, + * item: BaseItem, + * animProgress: number, + * }>} ItemAcceptorInputs + * + * @typedef {Array<{ + * slotIndex: number, + * item: BaseItem, + * extraProgress: number + * }>} ItemAcceptorCompletedInputs + * + * @typedef {{ + * root: GameRoot, + * entity: Entity, + * item: BaseItem, + * slotIndex: number, + * extraProgress: number + * }} InputCompletedArgs + */ + +/** @enum {string} */ +export const enumInputRequirements = { + quadPainter: "quadPainter", + storage: "storage", +}; + +export const MOD_INPUT_REQUIREMENTS = []; + export class ItemAcceptorComponent extends Component { static getId() { return "ItemAcceptor"; } + static getSchema() { + return { + inputs: types.array( + types.structured({ + slotIndex: types.uint, + item: typeItemSingleton, + animProgress: types.ufloat, + }) + ), + completedInputs: types.array( + types.structured({ + slotIndex: types.uint, + item: typeItemSingleton, + extraProgress: types.ufloat, + }) + ), + }; + } + /** * * @param {object} param0 * @param {Array} param0.slots The slots from which we accept items + * @param {number=} param0.maxSlotInputs The maximum amount of items one slot can accept before it is full + * @param {string|null=} param0.inputRequirement The requirement to accept items */ - constructor({ slots = [] }) { + constructor({ slots = [], maxSlotInputs = 2, inputRequirement = null }) { super(); this.setSlots(slots); + + this.inputRequirement = inputRequirement; + + // setting this to 1 will cause throughput issues at very high speeds + this.maxSlotInputs = maxSlotInputs; + this.clear(); } clear() { - /** - * Fixes belt animations - * @type {Array<{ - * item: BaseItem, - * slotIndex: number, - * animProgress: number, - * direction: enumDirection - * }>} - */ - this.itemConsumptionAnimations = []; + /** @type {ItemAcceptorInputs} */ + this.inputs = []; + /** @type {ItemAcceptorCompletedInputs} */ + this.completedInputs = []; } /** @@ -74,31 +129,107 @@ export class ItemAcceptorComponent extends Component { } /** - * Returns if this acceptor can accept a new item at slot N * - * NOTICE: The belt path ignores this for performance reasons and does his own check + * @param {Entity} entity + * @param {BaseItem} item * @param {number} slotIndex - * @param {BaseItem=} item + * @returns */ - canAcceptItem(slotIndex, item) { + canAcceptItem(entity, item, slotIndex) { const slot = this.slots[slotIndex]; - return !slot.filter || slot.filter === item.getItemType(); + + // make sure there is a slot and we match the filter + if (slot && !(slot.filter && slot.filter != item.getItemType())) { + if (MOD_INPUT_REQUIREMENTS[this.inputRequirement]) { + return MOD_INPUT_REQUIREMENTS[this.inputRequirement].call(this, { + entity, + item, + slotIndex, + }); + } + + switch (this.inputRequirement) { + case null: { + return true; + } + case enumInputRequirements.quadPainter: { + const pinsComp = entity.components.WiredPins; + + if (slotIndex === 0) { + // Always accept the shape + return true; + } + + // Check the network value at the given slot + const network = pinsComp.slots[slotIndex - 1].linkedNetwork; + const slotIsEnabled = network && network.hasValue() && isTruthyItem(network.currentValue); + if (!slotIsEnabled) { + return false; + } + return true; + } + case enumInputRequirements.storage: { + const storageComp = entity.components.Storage; + + if (storageComp.storedCount >= storageComp.maximumStorage) { + return false; + } + const itemType = item.getItemType(); + if (storageComp.storedItem && itemType !== storageComp.storedItem.getItemType()) { + return false; + } + + // set the item straight away - this way different kinds of items can't be inq the acceptor + storageComp.storedItem = item; + storageComp.storedCount++; + + return true; + } + default: { + assertAlways(false, "Input requirement is not recognised: " + this.inputRequirement); + break; + } + } + } + return false; } /** - * Called when an item has been accepted so that + * Called when trying to input a new item + * @param {Entity} entity * @param {number} slotIndex - * @param {enumDirection} direction * @param {BaseItem} item - * @param {number} remainingProgress World space remaining progress, can be set to set the start position of the item + * @param {number} startProgress World space remaining progress, can be set to set the start position of the item + * @returns {boolean} if the input was succesful */ - onItemAccepted(slotIndex, direction, item, remainingProgress = 0.0) { - this.itemConsumptionAnimations.push({ - item, + tryAcceptItem(entity, slotIndex, item, startProgress = 0.0) { + // make sure we have space to actually accept + let existingInputs = 0; + for (let i = 0; i < this.inputs.length; i++) { + if (this.inputs[i].slotIndex == slotIndex) { + existingInputs++; + } + } + for (let i = 0; i < this.completedInputs.length; i++) { + if (this.completedInputs[i].slotIndex == slotIndex) { + existingInputs++; + } + } + + if (existingInputs >= this.maxSlotInputs) { + return false; + } + if (!this.canAcceptItem(entity, item, slotIndex)) { + return false; + } + + // if the start progress is bigger than 0.5, the remainder should get passed on to the ejector + this.inputs.push({ slotIndex, - direction, - animProgress: Math.min(1, remainingProgress * 2), + item, + animProgress: startProgress, }); + return true; } /** diff --git a/src/js/game/components/item_ejector.js b/src/js/game/components/item_ejector.js index bfc54cd8..5c1cdb2a 100644 --- a/src/js/game/components/item_ejector.js +++ b/src/js/game/components/item_ejector.js @@ -127,28 +127,16 @@ export class ItemEjectorComponent extends Component { * Tries to eject a given item * @param {number} slotIndex * @param {BaseItem} item + * @param {number} startingProgress * @returns {boolean} */ - tryEject(slotIndex, item) { + tryEject(slotIndex, item, startingProgress = 0.0) { if (!this.canEjectOnSlot(slotIndex)) { return false; } this.slots[slotIndex].item = item; this.slots[slotIndex].lastItem = item; - this.slots[slotIndex].progress = 0; + this.slots[slotIndex].progress = startingProgress; return true; } - - /** - * Clears the given slot and returns the item it had - * @param {number} slotIndex - * @returns {BaseItem|null} - */ - takeSlotItem(slotIndex) { - const slot = this.slots[slotIndex]; - const item = slot.item; - slot.item = null; - slot.progress = 0.0; - return item; - } } diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index be7d1ce4..da3dd3e3 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -1,6 +1,8 @@ import { types } from "../../savegame/serialization"; +import { TypeString } from "../../savegame/serialization_data_types"; import { BaseItem } from "../base_item"; import { Component } from "../component"; +import { typeItemSingleton } from "../item_resolver"; /** @enum {string} */ export const enumItemProcessorTypes = { @@ -29,6 +31,7 @@ export const enumItemProcessorRequirements = { /** @typedef {{ * item: BaseItem, + * extraProgress?: number * requiredSlot?: number, * preferredSlot?: number * }} EjectorItemToEject */ @@ -38,6 +41,13 @@ export const enumItemProcessorRequirements = { * items: Array, * }} EjectorCharge */ +/** + * @typedef {{ + * item: BaseItem + * extraProgress: number + * }} ItemProcessorInput + */ + export class ItemProcessorComponent extends Component { static getId() { return "ItemProcessor"; @@ -46,6 +56,19 @@ export class ItemProcessorComponent extends Component { static getSchema() { return { nextOutputSlot: types.uint, + currentCharge: types.nullable( + types.structured({ + remainingTime: types.ufloat, + items: types.array( + types.structured({ + item: typeItemSingleton, + extraProgress: types.nullable(types.float), + requiredSlot: types.nullable(types.uint), + preferredSlot: types.nullable(types.uint), + }) + ), + }) + ), }; } @@ -73,12 +96,6 @@ export class ItemProcessorComponent extends Component { // Type of processing requirement this.processingRequirement = processingRequirement; - /** - * Our current inputs - * @type {Map} - */ - this.inputSlots = new Map(); - this.clear(); } @@ -88,21 +105,13 @@ export class ItemProcessorComponent extends Component { // sure the outputs always match this.nextOutputSlot = 0; - this.inputSlots.clear(); - - /** - * Current input count - * @type {number} - */ - this.inputCount = 0; - /** * What we are currently processing, empty if we don't produce anything rn * requiredSlot: Item *must* be ejected on this slot * preferredSlot: Item *can* be ejected on this slot, but others are fine too if the one is not usable - * @type {Array} + * @type {EjectorCharge|null} */ - this.ongoingCharges = []; + this.currentCharge = null; /** * How much processing time we have left from the last tick @@ -115,30 +124,4 @@ export class ItemProcessorComponent extends Component { */ this.queuedEjects = []; } - - /** - * Tries to take the item - * @param {BaseItem} item - * @param {number} sourceSlot - */ - tryTakeItem(item, sourceSlot) { - if ( - this.type === enumItemProcessorTypes.hub || - this.type === enumItemProcessorTypes.trash || - this.type === enumItemProcessorTypes.goal - ) { - // Hub has special logic .. not really nice but efficient. - this.inputSlots.set(this.inputCount, item); - this.inputCount++; - return true; - } - - // Check that we only take one item per slot - if (this.inputSlots.has(sourceSlot)) { - return false; - } - this.inputSlots.set(sourceSlot, item); - this.inputCount++; - return true; - } } diff --git a/src/js/game/components/miner.js b/src/js/game/components/miner.js index 5321ae11..54ac3b4a 100644 --- a/src/js/game/components/miner.js +++ b/src/js/game/components/miner.js @@ -2,9 +2,13 @@ import { types } from "../../savegame/serialization"; import { BaseItem } from "../base_item"; import { Component } from "../component"; import { Entity } from "../entity"; -import { typeItemSingleton } from "../item_resolver"; -const chainBufferSize = 6; +/** + * @typedef {{ + * item: BaseItem, + * extraProgress?: number, + * }} MinerItem + */ export class MinerComponent extends Component { static getId() { @@ -14,17 +18,17 @@ export class MinerComponent extends Component { static getSchema() { // cachedMinedItem is not serialized. return { - lastMiningTime: types.ufloat, - itemChainBuffer: types.array(typeItemSingleton), + progress: types.ufloat, }; } constructor({ chainable = false }) { super(); - this.lastMiningTime = 0; + this.progress = 0; this.chainable = chainable; /** + * The item we are mining beneath us * @type {BaseItem} */ this.cachedMinedItem = null; @@ -35,30 +39,11 @@ export class MinerComponent extends Component { * @type {Entity|null|false} */ this.cachedChainedMiner = null; - - this.clear(); - } - - clear() { /** - * Stores items from other miners which were chained to this - * miner. - * @type {Array} + * The miner at the end of the chain, which actually ejects the items + * If the value is false, it means there is no entity, and we don't have to re-check + * @type {Entity|null|false} */ - this.itemChainBuffer = []; - } - - /** - * - * @param {BaseItem} item - */ - tryAcceptChainedItem(item) { - if (this.itemChainBuffer.length > chainBufferSize) { - // Well, this one is full - return false; - } - - this.itemChainBuffer.push(item); - return true; + this.cachedExitMiner = null; } } diff --git a/src/js/game/components/storage.js b/src/js/game/components/storage.js index 46305929..905fd71f 100644 --- a/src/js/game/components/storage.js +++ b/src/js/game/components/storage.js @@ -2,13 +2,7 @@ import { types } from "../../savegame/serialization"; import { BaseItem } from "../base_item"; import { Component } from "../component"; import { typeItemSingleton } from "../item_resolver"; -import { ColorItem } from "../items/color_item"; -import { ShapeItem } from "../items/shape_item"; -/** @type {{ - * [x: string]: (item: BaseItem) => Boolean - * }} */ -export const MODS_ADDITIONAL_STORAGE_ITEM_RESOLVER = {}; export class StorageComponent extends Component { static getId() { return "Storage"; @@ -46,42 +40,6 @@ export class StorageComponent extends Component { this.overlayOpacity = 0; } - /** - * Returns whether this storage can accept the item - * @param {BaseItem} item - */ - canAcceptItem(item) { - if (this.storedCount >= this.maximumStorage) { - return false; - } - if (!this.storedItem || this.storedCount === 0) { - return true; - } - - const itemType = item.getItemType(); - - if (itemType !== this.storedItem.getItemType()) { - // Check type matches - return false; - } - - if (MODS_ADDITIONAL_STORAGE_ITEM_RESOLVER[itemType]) { - return MODS_ADDITIONAL_STORAGE_ITEM_RESOLVER[itemType].apply(this, [item]); - } - - if (itemType === "color") { - return /** @type {ColorItem} */ (this.storedItem).color === /** @type {ColorItem} */ (item).color; - } - - if (itemType === "shape") { - return ( - /** @type {ShapeItem} */ (this.storedItem).definition.getHash() === - /** @type {ShapeItem} */ (item).definition.getHash() - ); - } - return false; - } - /** * Returns whether the storage is full * @returns {boolean} diff --git a/src/js/game/components/underground_belt.js b/src/js/game/components/underground_belt.js index 2b744edd..0164e269 100644 --- a/src/js/game/components/underground_belt.js +++ b/src/js/game/components/underground_belt.js @@ -56,61 +56,31 @@ export class UndergroundBeltComponent extends Component { this.consumptionAnimations = []; /** - * Used on both receiver and sender. - * Reciever: Used to store the next item to transfer, and to block input while doing this - * Sender: Used to store which items are currently "travelling" - * @type {Array<[BaseItem, number]>} Format is [Item, ingame time to eject the item] + * Used only on reciever to store which items are currently "travelling" + * @type {Array<[BaseItem, number]>} Format is [Item, Tile progress] */ this.pendingItems = []; } - /** - * Tries to accept an item from an external source like a regular belt or building - * @param {BaseItem} item - * @param {number} beltSpeed How fast this item travels - */ - tryAcceptExternalItem(item, beltSpeed) { - if (this.mode !== enumUndergroundBeltMode.sender) { - // Only senders accept external items - return false; - } - - if (this.pendingItems.length > 0) { - // We currently have a pending item - return false; - } - - this.pendingItems.push([item, 0]); - return true; - } - /** * Tries to accept a tunneled item * @param {BaseItem} item - * @param {number} travelDistance How many tiles this item has to travel - * @param {number} beltSpeed How fast this item travels - * @param {number} now Current ingame time + * @param {number} travelDistance + * @param {number} startProgress The starting tile progress */ - tryAcceptTunneledItem(item, travelDistance, beltSpeed, now) { + tryAcceptTunneledItem(item, travelDistance, startProgress = 0) { if (this.mode !== enumUndergroundBeltMode.receiver) { // Only receivers can accept tunneled items return false; } - // Notice: We assume that for all items the travel distance is the same - const maxItemsInTunnel = (2 + travelDistance) / globalConfig.itemSpacingOnBelts; + const maxItemsInTunnel = travelDistance / globalConfig.itemSpacingOnBelts; if (this.pendingItems.length >= maxItemsInTunnel) { // Simulate a real belt which gets full at some point return false; } - // NOTICE: - // This corresponds to the item ejector - it needs 0.5 additional tiles to eject the item. - // So instead of adding 1 we add 0.5 only. - // Additionally it takes 1 tile for the acceptor which we just add on top. - const travelDuration = (travelDistance + 1.5) / beltSpeed / globalConfig.itemSpacingOnBelts; - - this.pendingItems.push([item, now + travelDuration]); + this.pendingItems.push([item, startProgress]); return true; } } diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index a799b42a..603511b1 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -150,11 +150,13 @@ export class GameSystemManager { add("belt", BeltSystem); - add("undergroundBelt", UndergroundBeltSystem); + add("storage", StorageSystem); + + add("itemEjector", ItemEjectorSystem); add("miner", MinerSystem); - add("storage", StorageSystem); + add("undergroundBelt", UndergroundBeltSystem); add("itemProcessor", ItemProcessorSystem); @@ -162,8 +164,6 @@ export class GameSystemManager { add("itemProducer", ItemProducerSystem); - add("itemEjector", ItemEjectorSystem); - if (this.root.gameMode.hasResources()) { add("mapResources", MapResourcesSystem); } diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index 9f9c63be..531a6643 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -507,55 +507,36 @@ export class HubGoals extends BasicSerializableObject { } /** - * Processor speed + * Processor time to process * @param {enumItemProcessorTypes} processorType - * @returns {number} items / sec + * @returns {number} process time in seconds */ - getProcessorBaseSpeed(processorType) { + getProcessingTime(processorType) { if (this.root.gameMode.throughputDoesNotMatter()) { - return globalConfig.beltSpeedItemsPerSecond * globalConfig.puzzleModeSpeed * 10; + return 0; } switch (processorType) { case enumItemProcessorTypes.trash: case enumItemProcessorTypes.hub: case enumItemProcessorTypes.goal: - return 1e30; case enumItemProcessorTypes.balancer: - return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2; case enumItemProcessorTypes.reader: - return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt; + case enumItemProcessorTypes.rotater: + case enumItemProcessorTypes.rotaterCCW: + case enumItemProcessorTypes.rotater180: + return 0; case enumItemProcessorTypes.mixer: case enumItemProcessorTypes.painter: case enumItemProcessorTypes.painterDouble: case enumItemProcessorTypes.painterQuad: { - assert( - globalConfig.buildingSpeeds[processorType], - "Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType - ); - return ( - globalConfig.beltSpeedItemsPerSecond * - this.upgradeImprovements.painting * - globalConfig.buildingSpeeds[processorType] - ); + return this.getProcessorTimeWithUpgrades(this.upgradeImprovements.painting, processorType); } - case enumItemProcessorTypes.cutter: case enumItemProcessorTypes.cutterQuad: - case enumItemProcessorTypes.rotater: - case enumItemProcessorTypes.rotaterCCW: - case enumItemProcessorTypes.rotater180: case enumItemProcessorTypes.stacker: { - assert( - globalConfig.buildingSpeeds[processorType], - "Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType - ); - return ( - globalConfig.beltSpeedItemsPerSecond * - this.upgradeImprovements.processors * - globalConfig.buildingSpeeds[processorType] - ); + return this.getProcessorTimeWithUpgrades(this.upgradeImprovements.processors, processorType); } default: if (MOD_ITEM_PROCESSOR_SPEEDS[processorType]) { @@ -564,6 +545,34 @@ export class HubGoals extends BasicSerializableObject { assertAlways(false, "invalid processor type: " + processorType); } - return 1 / globalConfig.beltSpeedItemsPerSecond; + return 0; + } + + /** + * @param {number} upgrade + * @param {enumItemProcessorTypes} processorType + */ + getProcessorTimeWithUpgrades(upgrade, processorType) { + assert( + globalConfig.buildingRatios[processorType], + "Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType + ); + + const processorTime = + globalConfig.buildingRatios[processorType] / globalConfig.beltSpeedItemsPerSecond; + return processorTime / upgrade; + } + + /** + * Processor speed + * @param {enumItemProcessorTypes} processorType + * @returns {number} items/sec + */ + getProcessorBaseSpeed(processorType) { + const time = this.getProcessingTime(processorType); + if (!time) { + return this.getBeltBaseSpeed(); + } + return 1 / time; } } diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index d2904720..218af719 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -303,17 +303,14 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { const mouseTile = worldPos.toTileSpace(); // Compute best rotation variant - const { - rotation, - rotationVariant, - connectedEntities, - } = metaBuilding.computeOptimalDirectionAndRotationVariantAtTile({ - root: this.root, - tile: mouseTile, - rotation: this.currentBaseRotation, - variant: this.currentVariant.get(), - layer: metaBuilding.getLayer(), - }); + const { rotation, rotationVariant, connectedEntities } = + metaBuilding.computeOptimalDirectionAndRotationVariantAtTile({ + root: this.root, + tile: mouseTile, + rotation: this.currentBaseRotation, + variant: this.currentVariant.get(), + layer: metaBuilding.getLayer(), + }); // Check if there are connected entities if (connectedEntities) { @@ -657,8 +654,16 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { // Connected to a belt isConnected = true; } else if (minerComp && minerComp.chainable && destMiner && destMiner.chainable) { - // Chainable miners connected to eachother - isConnected = true; + const worldTile = staticComp.localTileToWorld(slot.pos); + if ( + this.root.map.getLowerLayerContentXY(worldTile.x, worldTile.y) == + destMiner.cachedMinedItem + ) { + // Chainable miners connected to eachother + isConnected = true; + } else { + isBlocked = true; + } } else { // This one is blocked isBlocked = true; diff --git a/src/js/game/hud/parts/miner_highlight.js b/src/js/game/hud/parts/miner_highlight.js index a0c6919d..0e5faef1 100644 --- a/src/js/game/hud/parts/miner_highlight.js +++ b/src/js/game/hud/parts/miner_highlight.js @@ -134,6 +134,7 @@ export class HUDMinerHighlight extends BaseHUDPart { findConnectedMiners(entity, seenUids = new Set()) { let results = []; const origin = entity.components.StaticMapEntity.origin; + const originMinerComp = entity.components.Miner; if (!seenUids.has(entity.uid)) { seenUids.add(entity.uid); @@ -157,7 +158,11 @@ export class HUDMinerHighlight extends BaseHUDPart { ); if (contents) { const minerComp = contents.components.Miner; - if (minerComp && minerComp.chainable) { + if ( + minerComp && + minerComp.chainable && + originMinerComp.cachedMinedItem == minerComp.cachedMinedItem + ) { // Found a miner connected to this entity if (!seenUids.has(contents.uid)) { if (this.root.systemMgr.systems.miner.findChainedMiner(contents) === entity) { diff --git a/src/js/game/systems/filter.js b/src/js/game/systems/filter.js index a6442b41..e8008bf8 100644 --- a/src/js/game/systems/filter.js +++ b/src/js/game/systems/filter.js @@ -1,4 +1,3 @@ -import { globalConfig } from "../../core/config"; import { BaseItem } from "../base_item"; import { FilterComponent } from "../components/filter"; import { Entity } from "../entity"; @@ -13,32 +12,27 @@ export class FilterSystem extends GameSystemWithFilter { } update() { - const progress = - this.root.dynamicTickrate.deltaSeconds * - this.root.hubGoals.getBeltBaseSpeed() * - globalConfig.itemSpacingOnBelts; - - const requiredProgress = 1 - progress; - for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; const filterComp = entity.components.Filter; + const acceptorComp = entity.components.ItemAcceptor; const ejectorComp = entity.components.ItemEjector; - // Process payloads + // Take items from acceptor + const input = acceptorComp.completedInputs[0]; + if (input && this.tryAcceptItem(entity, input.item, input.extraProgress)) { + acceptorComp.completedInputs.shift(); + } + + // Output to ejector const slotsAndLists = [filterComp.pendingItemsToLeaveThrough, filterComp.pendingItemsToReject]; for (let slotIndex = 0; slotIndex < slotsAndLists.length; ++slotIndex) { const pendingItems = slotsAndLists[slotIndex]; for (let j = 0; j < pendingItems.length; ++j) { const nextItem = pendingItems[j]; - // Advance next item - nextItem.progress = Math.min(requiredProgress, nextItem.progress + progress); - // Check if it's ready to eject - if (nextItem.progress >= requiredProgress - 1e-5) { - if (ejectorComp.tryEject(slotIndex, nextItem.item)) { - pendingItems.shift(); - } + if (ejectorComp.tryEject(slotIndex, nextItem.item)) { + pendingItems.shift(); } } } @@ -48,10 +42,10 @@ export class FilterSystem extends GameSystemWithFilter { /** * * @param {Entity} entity - * @param {number} slot * @param {BaseItem} item + * @param {number} startProgress */ - tryAcceptItem(entity, slot, item) { + tryAcceptItem(entity, item, startProgress) { const network = entity.components.WiredPins.slots[0].linkedNetwork; if (!network || !network.hasValue()) { // Filter is not connected @@ -78,7 +72,7 @@ export class FilterSystem extends GameSystemWithFilter { // Actually accept item listToCheck.push({ item, - progress: 0.0, + extraProgress: startProgress, }); return true; } diff --git a/src/js/game/systems/item_acceptor.js b/src/js/game/systems/item_acceptor.js index 780b4abd..db9bc7e1 100644 --- a/src/js/game/systems/item_acceptor.js +++ b/src/js/game/systems/item_acceptor.js @@ -1,6 +1,5 @@ import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; -import { fastArrayDelete } from "../../core/utils"; import { enumDirectionToVector } from "../../core/vector"; import { ItemAcceptorComponent } from "../components/item_acceptor"; import { GameSystemWithFilter } from "../game_system_with_filter"; @@ -9,49 +8,36 @@ import { MapChunkView } from "../map_chunk_view"; export class ItemAcceptorSystem extends GameSystemWithFilter { constructor(root) { super(root, [ItemAcceptorComponent]); - - // Well ... it's better to be verbose I guess? - this.accumulatedTicksWhileInMapOverview = 0; } update() { - if (this.root.app.settings.getAllSettings().simplifiedBelts) { - // Disabled in potato mode - return; - } - - // This system doesn't render anything while in map overview, - // so simply accumulate ticks - if (this.root.camera.getIsMapOverlayActive()) { - ++this.accumulatedTicksWhileInMapOverview; - return; - } - - // Compute how much ticks we missed - const numTicks = 1 + this.accumulatedTicksWhileInMapOverview; - const progress = + // same code for belts, acceptors and ejectors - add helper method??? + const progressGrowth = this.root.dynamicTickrate.deltaSeconds * - 2 * this.root.hubGoals.getBeltBaseSpeed() * - globalConfig.itemSpacingOnBelts * // * 2 because its only a half tile - numTicks; - - // Reset accumulated ticks - this.accumulatedTicksWhileInMapOverview = 0; + globalConfig.itemSpacingOnBelts; for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; - const aceptorComp = entity.components.ItemAcceptor; - const animations = aceptorComp.itemConsumptionAnimations; + const acceptorComp = entity.components.ItemAcceptor; + const inputs = acceptorComp.inputs; + const maxProgress = 0.5; - // Process item consumption animations to avoid items popping from the belts - for (let animIndex = 0; animIndex < animations.length; ++animIndex) { - const anim = animations[animIndex]; - anim.animProgress += progress; - if (anim.animProgress > 1) { - fastArrayDelete(animations, animIndex); - animIndex -= 1; + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i]; + input.animProgress += progressGrowth; + + if (input.animProgress < maxProgress) { + continue; } + + inputs.splice(i, 1); + i--; + acceptorComp.completedInputs.push({ + slotIndex: input.slotIndex, + item: input.item, + extraProgress: input.animProgress - maxProgress, + }); // will be handled on the SAME frame due to processor system being afterwards } } } @@ -75,10 +61,9 @@ export class ItemAcceptorSystem extends GameSystemWithFilter { } const staticComp = entity.components.StaticMapEntity; - for (let animIndex = 0; animIndex < acceptorComp.itemConsumptionAnimations.length; ++animIndex) { - const { item, slotIndex, animProgress, direction } = acceptorComp.itemConsumptionAnimations[ - animIndex - ]; + for (let i = 0; i < acceptorComp.inputs.length; i++) { + const input = acceptorComp.inputs[i]; + const { item, animProgress, slotIndex } = input; const slotData = acceptorComp.slots[slotIndex]; const realSlotPos = staticComp.localTileToWorld(slotData.pos); @@ -88,10 +73,11 @@ export class ItemAcceptorSystem extends GameSystemWithFilter { continue; } - const fadeOutDirection = enumDirectionToVector[staticComp.localDirectionToWorld(direction)]; + const fadeOutDirection = + enumDirectionToVector[staticComp.localDirectionToWorld(slotData.direction)]; const finalTile = realSlotPos.subScalars( - fadeOutDirection.x * (animProgress / 2 - 0.5), - fadeOutDirection.y * (animProgress / 2 - 0.5) + fadeOutDirection.x * (animProgress - 0.5), + fadeOutDirection.y * (animProgress - 0.5) ); item.drawItemCenteredClipped( diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index 8c7468ad..e7fe22e0 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -4,7 +4,6 @@ import { createLogger } from "../../core/logging"; import { Rectangle } from "../../core/rectangle"; import { StaleAreaDetector } from "../../core/stale_area_detector"; import { enumDirection, enumDirectionToVector } from "../../core/vector"; -import { BaseItem } from "../base_item"; import { BeltComponent } from "../components/belt"; import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; @@ -139,10 +138,15 @@ export class ItemEjectorSystem extends GameSystemWithFilter { this.staleAreaDetector.update(); // Precompute effective belt speed - let progressGrowth = 2 * this.root.dynamicTickrate.deltaSeconds; + let progressGrowth = + this.root.dynamicTickrate.deltaSeconds * + this.root.hubGoals.getBeltBaseSpeed() * + globalConfig.itemSpacingOnBelts; + // it's only half a belt + const maxProgress = 0.5; if (G_IS_DEV && globalConfig.debug.instantBelts) { - progressGrowth = 1; + progressGrowth = maxProgress; } // Go over all cache entries @@ -159,29 +163,32 @@ export class ItemEjectorSystem extends GameSystemWithFilter { continue; } - // Advance items on the slot - sourceSlot.progress = Math.min( - 1, - sourceSlot.progress + - progressGrowth * - this.root.hubGoals.getBeltBaseSpeed() * - globalConfig.itemSpacingOnBelts - ); + // Limit progress here as well + let progressLimit = maxProgress; + const destPath = sourceSlot.cachedBeltPath; + if (destPath) { + progressLimit += destPath.spacingToFirstItem - globalConfig.itemSpacingOnBelts; + } + + if (sourceSlot.progress < progressLimit) { + // Advance items on the slot + sourceSlot.progress += progressGrowth; + } if (G_IS_DEV && globalConfig.debug.disableEjectorProcessing) { - sourceSlot.progress = 1.0; + sourceSlot.progress = maxProgress; } // Check if we are still in the process of ejecting, can't proceed then - if (sourceSlot.progress < 1.0) { + if (sourceSlot.progress < maxProgress) { continue; } - // Check if we are ejecting to a belt path - const destPath = sourceSlot.cachedBeltPath; + const extraProgress = sourceSlot.progress - maxProgress; + if (destPath) { // Try passing the item over - if (destPath.tryAcceptItem(item)) { + if (destPath.tryAcceptItem(item, extraProgress)) { sourceSlot.item = null; } @@ -193,110 +200,17 @@ export class ItemEjectorSystem extends GameSystemWithFilter { // Check if the target acceptor can actually accept this item const destEntity = sourceSlot.cachedTargetEntity; const destSlot = sourceSlot.cachedDestSlot; - if (destSlot) { + if (destEntity && destSlot) { const targetAcceptorComp = destEntity.components.ItemAcceptor; - if (!targetAcceptorComp.canAcceptItem(destSlot.index, item)) { - continue; - } - - // Try to hand over the item - if (this.tryPassOverItem(item, destEntity, destSlot.index)) { + if (targetAcceptorComp.tryAcceptItem(destEntity, destSlot.index, item, extraProgress)) { // Handover successful, clear slot - if (!this.root.app.settings.getAllSettings().simplifiedBelts) { - targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.slot.direction, item); - } sourceSlot.item = null; - continue; } } } } } - /** - * - * @param {BaseItem} item - * @param {Entity} receiver - * @param {number} slotIndex - */ - tryPassOverItem(item, receiver, slotIndex) { - // Try figuring out how what to do with the item - // @TODO: Kinda hacky. How to solve this properly? Don't want to go through inheritance hell. - - const beltComp = receiver.components.Belt; - if (beltComp) { - const path = beltComp.assignedPath; - assert(path, "belt has no path"); - if (path.tryAcceptItem(item)) { - return true; - } - // Belt can have nothing else - return false; - } - - //////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////// - // - // NOTICE ! THIS CODE IS DUPLICATED IN THE BELT PATH FOR PERFORMANCE REASONS - // - //////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////// - - const itemProcessorComp = receiver.components.ItemProcessor; - if (itemProcessorComp) { - // Check for potential filters - if (!this.root.systemMgr.systems.itemProcessor.checkRequirements(receiver, item, slotIndex)) { - return false; - } - - // Its an item processor .. - if (itemProcessorComp.tryTakeItem(item, slotIndex)) { - return true; - } - // Item processor can have nothing else - return false; - } - - const undergroundBeltComp = receiver.components.UndergroundBelt; - if (undergroundBeltComp) { - // Its an underground belt. yay. - if ( - undergroundBeltComp.tryAcceptExternalItem( - item, - this.root.hubGoals.getUndergroundBeltBaseSpeed() - ) - ) { - return true; - } - - // Underground belt can have nothing else - return false; - } - - const storageComp = receiver.components.Storage; - if (storageComp) { - // It's a storage - if (storageComp.canAcceptItem(item)) { - storageComp.takeItem(item); - return true; - } - - // Storage can't have anything else - return false; - } - - const filterComp = receiver.components.Filter; - if (filterComp) { - // It's a filter! Unfortunately the filter has to know a lot about it's - // surrounding state and components, so it can't be within the component itself. - if (this.root.systemMgr.systems.filter.tryAcceptItem(receiver, slotIndex, item)) { - return true; - } - } - - return false; - } - /** * @param {DrawParameters} parameters * @param {MapChunkView} chunk @@ -333,7 +247,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter { } // Limit the progress to the maximum available space on the next belt (also see #1000) - let progress = slot.progress; + let progress = Math.min(0.5, slot.progress); const nextBeltPath = slot.cachedBeltPath; if (nextBeltPath) { /* @@ -368,20 +282,11 @@ export class ItemEjectorSystem extends GameSystemWithFilter { ^ ^ item @ 0.9 ^ max progress = 0.3 - Because now our range actually only goes to the end of the building, and not towards the center of the building, we need to multiply - all values by 2: - - Building Belt - | X | X | - | 0.........1.........2 | - ^ ^ item @ 1.8 - ^ max progress = 0.6 - And that's it! If you summarize the calculations from above into a formula, you get the one below. */ const maxProgress = - (0.5 + nextBeltPath.spacingToFirstItem - globalConfig.itemSpacingOnBelts) * 2; + 0.5 + nextBeltPath.spacingToFirstItem - globalConfig.itemSpacingOnBelts; progress = Math.min(maxProgress, progress); } @@ -399,8 +304,8 @@ export class ItemEjectorSystem extends GameSystemWithFilter { const realDirection = staticComp.localDirectionToWorld(slot.direction); const realDirectionVector = enumDirectionToVector[realDirection]; - const tileX = realPosition.x + 0.5 + realDirectionVector.x * 0.5 * progress; - const tileY = realPosition.y + 0.5 + realDirectionVector.y * 0.5 * progress; + const tileX = realPosition.x + 0.5 + realDirectionVector.x * progress; + const tileY = realPosition.y + 0.5 + realDirectionVector.y * progress; const worldX = tileX * globalConfig.tileSize; const worldY = tileY * globalConfig.tileSize; diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 50e3e9ba..d4a33730 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -1,4 +1,5 @@ import { globalConfig } from "../../core/config"; +import { ACHIEVEMENTS } from "../../platform/achievement_provider"; import { BaseItem } from "../base_item"; import { enumColorMixingResults, enumColors } from "../colors"; import { @@ -12,16 +13,12 @@ import { isTruthyItem } from "../items/boolean_item"; import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item"; import { ShapeItem } from "../items/shape_item"; -/** - * We need to allow queuing charges, otherwise the throughput will stall - */ -const MAX_QUEUED_CHARGES = 2; - /** * Whole data for a produced item * * @typedef {{ * item: BaseItem, + * extraProgress?: number, * preferredSlot?: number, * requiredSlot?: number, * doNotTrack?: boolean @@ -33,29 +30,19 @@ const MAX_QUEUED_CHARGES = 2; * @typedef {{ * entity: Entity, * items: Map, - * inputCount: number, * outItems: Array * }} ProcessorImplementationPayload */ -/** - * Type of a processor implementation - * @typedef {{ - * entity: Entity, - * item: BaseItem, - * slotIndex: number - * }} ProccessingRequirementsImplementationPayload - */ - /** * @type {Object void>} */ export const MOD_ITEM_PROCESSOR_HANDLERS = {}; /** - * @type {Object boolean>} + * @type {Object boolean>} */ -export const MODS_PROCESSING_REQUIREMENTS = {}; +export const MODS_CAN_PROCESS = {}; /** * @type {Object boolean>} @@ -101,8 +88,14 @@ export class ItemProcessorSystem extends GameSystemWithFilter { const processorComp = entity.components.ItemProcessor; const ejectorComp = entity.components.ItemEjector; - const currentCharge = processorComp.ongoingCharges[0]; + // Check if we have an empty queue and can start a new charge - do this first so we don't waste a tick + if (!processorComp.currentCharge) { + if (this.canProcess(entity)) { + this.startNewCharge(entity); + } + } + const currentCharge = processorComp.currentCharge; if (currentCharge) { // Process next charge if (currentCharge.remainingTime > 0.0) { @@ -122,19 +115,25 @@ export class ItemProcessorSystem extends GameSystemWithFilter { processorComp.queuedEjects.push(itemsToEject[j]); } - processorComp.ongoingCharges.shift(); - } - } - - // Check if we have an empty queue and can start a new charge - if (processorComp.ongoingCharges.length < MAX_QUEUED_CHARGES) { - if (this.canProcess(entity)) { - this.startNewCharge(entity); + processorComp.currentCharge = null; + + // now that the charge is complete, empty the inputs now + let usedSlots = []; + const acceptorComp = entity.components.ItemAcceptor; + for (let i = 0; i < acceptorComp.completedInputs.length; i++) { + const index = acceptorComp.completedInputs[i].slotIndex; + if (!usedSlots.includes(index)) { + usedSlots.push(index); + acceptorComp.completedInputs.splice(i, 1); + i--; + } + } } } + // Go over all items and try to eject them for (let j = 0; j < processorComp.queuedEjects.length; ++j) { - const { item, requiredSlot, preferredSlot } = processorComp.queuedEjects[j]; + const { item, requiredSlot, preferredSlot, extraProgress } = processorComp.queuedEjects[j]; assert(ejectorComp, "To eject items, the building needs to have an ejector"); @@ -158,7 +157,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { if (slot !== null) { // Alright, we can actually eject - if (!ejectorComp.tryEject(slot, item)) { + if (!ejectorComp.tryEject(slot, item, extraProgress)) { assert(false, "Failed to eject"); } else { processorComp.queuedEjects.splice(j, 1); @@ -169,54 +168,14 @@ export class ItemProcessorSystem extends GameSystemWithFilter { } } - /** - * Returns true if the entity should accept the given item on the given slot. - * This should only be called with matching items! I.e. if a color item is expected - * on the given slot, then only a color item must be passed. - * @param {Entity} entity - * @param {BaseItem} item The item to accept - * @param {number} slotIndex The slot index - * @returns {boolean} - */ - checkRequirements(entity, item, slotIndex) { - const itemProcessorComp = entity.components.ItemProcessor; - const pinsComp = entity.components.WiredPins; - - if (MODS_PROCESSING_REQUIREMENTS[itemProcessorComp.processingRequirement]) { - return MODS_PROCESSING_REQUIREMENTS[itemProcessorComp.processingRequirement].bind(this)({ - entity, - item, - slotIndex, - }); - } - - switch (itemProcessorComp.processingRequirement) { - case enumItemProcessorRequirements.painterQuad: { - if (slotIndex === 0) { - // Always accept the shape - return true; - } - - // Check the network value at the given slot - const network = pinsComp.slots[slotIndex - 1].linkedNetwork; - const slotIsEnabled = network && network.hasValue() && isTruthyItem(network.currentValue); - if (!slotIsEnabled) { - return false; - } - return true; - } - - // By default, everything is accepted - default: - return true; - } - } + // input requirements are now handled in the item acceptor, which also fits better with what the acceptor is supposed to do /** * Checks whether it's possible to process something * @param {Entity} entity */ canProcess(entity) { + const acceptorComp = entity.components.ItemAcceptor; const processorComp = entity.components.ItemProcessor; if (MODS_CAN_PROCESS[processorComp.processingRequirement]) { @@ -229,16 +188,35 @@ export class ItemProcessorSystem extends GameSystemWithFilter { // DEFAULT // By default, we can start processing once all inputs are there case null: { - return processorComp.inputCount >= processorComp.inputsPerCharge; + // Since each slot might have more than one input, don't check each slot more than once + let usedSlots = []; + for (let i = 0; i < acceptorComp.completedInputs.length; i++) { + const index = acceptorComp.completedInputs[i].slotIndex; + if (!usedSlots.includes(index)) { + usedSlots.push(index); + } + } + return usedSlots.length >= processorComp.inputsPerCharge; } // QUAD PAINTER // For the quad painter, it might be possible to start processing earlier case enumItemProcessorRequirements.painterQuad: { const pinsComp = entity.components.WiredPins; + const inputs = acceptorComp.completedInputs; + + // split inputs efficiently + let items = new Map(); + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i]; + + if (!items.get(input.slotIndex)) { + items.set(input.slotIndex, input.item); + } + } // First slot is the shape, so if it's not there we can't do anything - const shapeItem = /** @type {ShapeItem} */ (processorComp.inputSlots.get(0)); + const shapeItem = /** @type {ShapeItem} */ (items.get(0)); if (!shapeItem) { return false; } @@ -267,7 +245,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { // Check if all colors of the enabled slots are there for (let i = 0; i < slotStatus.length; ++i) { - if (slotStatus[i] && !processorComp.inputSlots.get(1 + i)) { + if (slotStatus[i] && !items.get(1 + i)) { // A slot which is enabled wasn't enabled. Make sure if there is anything on the quadrant, // it is not possible to paint, but if there is nothing we can ignore it for (let j = 0; j < 4; ++j) { @@ -278,7 +256,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter { } } } - return true; } @@ -292,10 +269,25 @@ export class ItemProcessorSystem extends GameSystemWithFilter { * @param {Entity} entity */ startNewCharge(entity) { + const acceptorComp = entity.components.ItemAcceptor; const processorComp = entity.components.ItemProcessor; - // First, take items - const items = processorComp.inputSlots; + // First, take inputs - but only one from each + const inputs = acceptorComp.completedInputs; + + // split inputs efficiently + let items = new Map(); + let extraProgress = 0; + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i]; + + if (!items.get(input.slotIndex)) { + items.set(input.slotIndex, input.item); + extraProgress = Math.max(extraProgress, input.extraProgress); + //inputs.splice(i, 1); + //i--; + } + } /** @type {Array} */ const outItems = []; @@ -309,7 +301,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter { entity, items, outItems, - inputCount: processorComp.inputCount, }); // Track produced items @@ -317,23 +308,23 @@ export class ItemProcessorSystem extends GameSystemWithFilter { if (!outItems[i].doNotTrack) { this.root.signals.itemProduced.dispatch(outItems[i].item); } + + // also set extra progress + outItems[i].extraProgress = extraProgress; } // Queue Charge - const baseSpeed = this.root.hubGoals.getProcessorBaseSpeed(processorComp.type); - const originalTime = 1 / baseSpeed; + const originalTime = this.root.hubGoals.getProcessingTime(processorComp.type); const bonusTimeToApply = Math.min(originalTime, processorComp.bonusTime); const timeToProcess = originalTime - bonusTimeToApply; processorComp.bonusTime -= bonusTimeToApply; - processorComp.ongoingCharges.push({ + + processorComp.currentCharge = { items: outItems, remainingTime: timeToProcess, - }); - - processorComp.inputSlots.clear(); - processorComp.inputCount = 0; + }; } /** @@ -478,7 +469,14 @@ export class ItemProcessorSystem extends GameSystemWithFilter { * @param {ProcessorImplementationPayload} payload */ process_TRASH(payload) { - // Do nothing .. + // Hardcoded - 4 inputs + for (let i = 0; i < 4; ++i) { + const item = /** @type {ShapeItem} */ (payload.items.get(i)); + if (!item) { + continue; + } + payload.entity.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.trash1000, 1); + } } /** @@ -602,8 +600,8 @@ export class ItemProcessorSystem extends GameSystemWithFilter { const hubComponent = payload.entity.components.Hub; assert(hubComponent, "Hub item processor has no hub component"); - // Hardcoded - for (let i = 0; i < payload.inputCount; ++i) { + // Hardcoded - 16 inputs + for (let i = 0; i < 16; ++i) { const item = /** @type {ShapeItem} */ (payload.items.get(i)); if (!item) { continue; diff --git a/src/js/game/systems/item_producer.js b/src/js/game/systems/item_producer.js index 8ca29ae1..ec194df0 100644 --- a/src/js/game/systems/item_producer.js +++ b/src/js/game/systems/item_producer.js @@ -24,7 +24,9 @@ export class ItemProducerSystem extends GameSystemWithFilter { } this.item = network.currentValue; - ejectorComp.tryEject(0, this.item); + + // Basically start ejecting at the exit of the ejector. Hacky, but who cares. It works, and its not in the base game :) + ejectorComp.tryEject(0, this.item, 0.5); } } } diff --git a/src/js/game/systems/miner.js b/src/js/game/systems/miner.js index cd478be3..fe76f4ed 100644 --- a/src/js/game/systems/miner.js +++ b/src/js/game/systems/miner.js @@ -1,7 +1,6 @@ import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; import { enumDirectionToVector } from "../../core/vector"; -import { BaseItem } from "../base_item"; import { MinerComponent } from "../components/miner"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; @@ -31,20 +30,18 @@ export class MinerSystem extends GameSystemWithFilter { } update() { - let miningSpeed = this.root.hubGoals.getMinerBaseSpeed(); + let progressGrowth = this.root.dynamicTickrate.deltaSeconds * this.root.hubGoals.getMinerBaseSpeed(); + + const targetProgress = 1; + if (G_IS_DEV && globalConfig.debug.instantMiners) { - miningSpeed *= 100; + progressGrowth = targetProgress; } for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; const minerComp = entity.components.Miner; - // Reset everything on recompute - if (this.needsRecompute) { - minerComp.cachedChainedMiner = null; - } - // Check if miner is above an actual tile if (!minerComp.cachedMinedItem) { const staticComp = entity.components.StaticMapEntity; @@ -58,25 +55,64 @@ export class MinerSystem extends GameSystemWithFilter { minerComp.cachedMinedItem = tileBelow; } - // First, try to get rid of chained items - if (minerComp.itemChainBuffer.length > 0) { - if (this.tryPerformMinerEject(entity, minerComp.itemChainBuffer[0])) { - minerComp.itemChainBuffer.shift(); + // Reset everything on recompute + if (this.needsRecompute) { + minerComp.cachedChainedMiner = null; + minerComp.cachedExitMiner = null; + } + + // Check if we are a chained miner + if (minerComp.chainable) { + if (!minerComp.cachedChainedMiner) { + minerComp.cachedChainedMiner = this.findChainedMiner(entity); + } + + // don't calculate on the same tick as recompute, or so miners wont have caches yet + if (minerComp.cachedChainedMiner && !minerComp.cachedExitMiner && !this.needsRecompute) { + minerComp.cachedExitMiner = this.findExitMiner(entity); + } + + // Check if we now have a target at the end of the chain - if so, that's what we will progress + const exitEntity = minerComp.cachedExitMiner; + if (exitEntity) { + const exitMinerComp = exitEntity.components.Miner; + if (exitMinerComp.progress < targetProgress + 0.5) { + // we can add on some extra progress + exitMinerComp.progress += progressGrowth; + } continue; } } - const mineDuration = 1 / miningSpeed; - const timeSinceMine = this.root.time.now() - minerComp.lastMiningTime; - if (timeSinceMine > mineDuration) { - // Store how much we overflowed - const buffer = Math.min(timeSinceMine - mineDuration, this.root.dynamicTickrate.deltaSeconds); + //make sure progress never gets out of control + minerComp.progress = Math.min(minerComp.progress, targetProgress + 0.5); + if (minerComp.progress >= targetProgress) { + // We can try to eject + const extraProgress = minerComp.progress - targetProgress; - if (this.tryPerformMinerEject(entity, minerComp.cachedMinedItem)) { + const ejectorComp = entity.components.ItemEjector; + if (ejectorComp.tryEject(0, minerComp.cachedMinedItem, extraProgress)) { // Analytics hook this.root.signals.itemProduced.dispatch(minerComp.cachedMinedItem); - // Store mining time - minerComp.lastMiningTime = this.root.time.now() - buffer; + + minerComp.progress -= targetProgress; + } + } + + if (minerComp.progress < targetProgress) { + minerComp.progress += progressGrowth; + } + + if (minerComp.progress >= targetProgress) { + // We can try to eject + const extraProgress = minerComp.progress - targetProgress; + + const ejectorComp = entity.components.ItemEjector; + if (ejectorComp.tryEject(0, minerComp.cachedMinedItem, extraProgress)) { + // Analytics hook + this.root.signals.itemProduced.dispatch(minerComp.cachedMinedItem); + + minerComp.progress -= targetProgress; } } } @@ -93,6 +129,7 @@ export class MinerSystem extends GameSystemWithFilter { findChainedMiner(entity) { const ejectComp = entity.components.ItemEjector; const staticComp = entity.components.StaticMapEntity; + const minedItem = entity.components.Miner.cachedMinedItem; const contentsBelow = this.root.map.getLowerLayerContentXY(staticComp.origin.x, staticComp.origin.y); if (!contentsBelow) { // This miner has no contents @@ -109,7 +146,11 @@ export class MinerSystem extends GameSystemWithFilter { // Check if we are connected to another miner and thus do not eject directly if (targetContents) { const targetMinerComp = targetContents.components.Miner; - if (targetMinerComp && targetMinerComp.chainable) { + if ( + targetMinerComp && + targetMinerComp.chainable && + targetMinerComp.cachedMinedItem == minedItem + ) { const targetLowerLayer = this.root.map.getLowerLayerContentXY(targetTile.x, targetTile.y); if (targetLowerLayer) { return targetContents; @@ -121,39 +162,37 @@ export class MinerSystem extends GameSystemWithFilter { } /** - * + * Finds the target exit miner for a given entity * @param {Entity} entity - * @param {BaseItem} item + * @returns {Entity|false} The exit miner entity or null if not found */ - tryPerformMinerEject(entity, item) { + findExitMiner(entity) { const minerComp = entity.components.Miner; - const ejectComp = entity.components.ItemEjector; + // Recompute exit miner if we are not at the front + let targetEntity = minerComp.cachedChainedMiner; - // Check if we are a chained miner - if (minerComp.chainable) { - const targetEntity = minerComp.cachedChainedMiner; + const ourPosition = entity.components.StaticMapEntity.origin; - // Check if the cache has to get recomputed - if (targetEntity === null) { - minerComp.cachedChainedMiner = this.findChainedMiner(entity); - } - - // Check if we now have a target - if (targetEntity) { - const targetMinerComp = targetEntity.components.Miner; - if (targetMinerComp.tryAcceptChainedItem(item)) { - return true; - } else { - return false; - } + /** @type {Entity|null|false} */ + let nextTarget = targetEntity; + while (nextTarget) { + targetEntity = nextTarget; + if (targetEntity.components.StaticMapEntity.origin == ourPosition) { + // we are in a loop, do nothing + targetEntity = null; + break; } + const targetMinerComp = targetEntity.components.Miner; + nextTarget = targetMinerComp.cachedChainedMiner; } - // Seems we are a regular miner or at the end of a row, try actually ejecting - if (ejectComp.tryEject(0, item)) { - return true; + if (targetEntity) { + const targetMinerComp = targetEntity.components.Miner; + if (targetMinerComp.cachedMinedItem == minerComp.cachedMinedItem) { + // only chain the same items + return targetEntity; + } } - return false; } diff --git a/src/js/game/systems/storage.js b/src/js/game/systems/storage.js index 20204a89..1fd23d25 100644 --- a/src/js/game/systems/storage.js +++ b/src/js/game/systems/storage.js @@ -31,6 +31,9 @@ export class StorageSystem extends GameSystemWithFilter { const storageComp = entity.components.Storage; const pinsComp = entity.components.WiredPins; + // storage needs to delete completed inputs, since the items are already added + entity.components.ItemAcceptor.completedInputs = []; + // Eject from storage if (storageComp.storedItem && storageComp.storedCount > 0) { const ejectorComp = entity.components.ItemEjector; diff --git a/src/js/game/systems/underground_belt.js b/src/js/game/systems/underground_belt.js index 9b31eec1..aa3df6cc 100644 --- a/src/js/game/systems/underground_belt.js +++ b/src/js/game/systems/underground_belt.js @@ -3,7 +3,6 @@ import { Loader } from "../../core/loader"; import { createLogger } from "../../core/logging"; import { Rectangle } from "../../core/rectangle"; import { StaleAreaDetector } from "../../core/stale_area_detector"; -import { fastArrayDelete } from "../../core/utils"; import { enumAngleToDirection, enumDirection, @@ -225,7 +224,11 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { this.staleAreaWatcher.update(); const sender = enumUndergroundBeltMode.sender; - const now = this.root.time.now(); + + const progressGrowth = + this.root.dynamicTickrate.deltaSeconds * + this.root.hubGoals.getBeltBaseSpeed() * + globalConfig.itemSpacingOnBelts; for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; @@ -233,7 +236,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { if (undergroundComp.mode === sender) { this.handleSender(entity); } else { - this.handleReceiver(entity, now); + this.handleReceiver(entity, progressGrowth); } } } @@ -253,8 +256,8 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { // Search in the direction of the tunnel for ( - let searchOffset = 0; - searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier]; + let searchOffset = 1; + searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier] + 1; ++searchOffset ) { currentTile = currentTile.add(searchVector); @@ -281,6 +284,8 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { break; } + // make sure to link the other way as well + receiverUndergroundComp.cachedLinkedEntity = { entity: null, distance: searchOffset }; return { entity: potentialReceiver, distance: searchOffset }; } @@ -294,6 +299,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { */ handleSender(entity) { const undergroundComp = entity.components.UndergroundBelt; + const acceptorComp = entity.components.ItemAcceptor; // Find the current receiver let cacheEntry = undergroundComp.cachedLinkedEntity; @@ -307,22 +313,17 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { return; } - // Check if we have any items to eject - const nextItemAndDuration = undergroundComp.pendingItems[0]; - if (nextItemAndDuration) { - assert(undergroundComp.pendingItems.length === 1, "more than 1 pending"); - + const input = acceptorComp.completedInputs[0]; + if (input) { // Check if the receiver can accept it if ( cacheEntry.entity.components.UndergroundBelt.tryAcceptTunneledItem( - nextItemAndDuration[0], + input.item, cacheEntry.distance, - this.root.hubGoals.getUndergroundBeltBaseSpeed(), - this.root.time.now() + input.extraProgress ) ) { - // Drop this item - fastArrayDelete(undergroundComp.pendingItems, 0); + acceptorComp.completedInputs.shift(); } } } @@ -330,20 +331,28 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { /** * * @param {Entity} entity - * @param {number} now + * @param {number} progressGrowth */ - handleReceiver(entity, now) { + handleReceiver(entity, progressGrowth) { const undergroundComp = entity.components.UndergroundBelt; - // Try to eject items, we only check the first one because it is sorted by remaining time - const nextItemAndDuration = undergroundComp.pendingItems[0]; - if (nextItemAndDuration) { - if (now > nextItemAndDuration[1]) { + if (!undergroundComp.cachedLinkedEntity) return; + const distance = undergroundComp.cachedLinkedEntity.distance; + + // Move items along + for (let i = 0; i < undergroundComp.pendingItems.length; i++) { + const itemAndProgress = undergroundComp.pendingItems[i]; + if (itemAndProgress[1] < distance) { + itemAndProgress[1] += progressGrowth; + } + + if (itemAndProgress[1] >= distance) { const ejectorComp = entity.components.ItemEjector; const nextSlotIndex = ejectorComp.getFirstFreeSlot(); if (nextSlotIndex !== null) { - if (ejectorComp.tryEject(nextSlotIndex, nextItemAndDuration[0])) { + const extraProgress = itemAndProgress[1] - distance; + if (ejectorComp.tryEject(nextSlotIndex, itemAndProgress[0], extraProgress)) { undergroundComp.pendingItems.shift(); } } diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index b4472b2b..4c274759 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -16,6 +16,7 @@ import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1009 } from "./schemas/1009"; import { MODS } from "../mods/modloader"; import { SavegameInterface_V1010 } from "./schemas/1010"; +import { SavegameInterface_V1011 } from "./schemas/1011"; const logger = createLogger("savegame"); @@ -56,7 +57,7 @@ export class Savegame extends ReadWriteProxy { * @returns {number} */ static getCurrentVersion() { - return 1010; + return 1011; } /** @@ -168,6 +169,11 @@ export class Savegame extends ReadWriteProxy { data.version = 1010; } + if (data.version === 1010) { + SavegameInterface_V1011.migrate1010to1011(data); + data.version = 1011; + } + return ExplainedResult.good(); } diff --git a/src/js/savegame/savegame_interface_registry.js b/src/js/savegame/savegame_interface_registry.js index 089b15fc..fb645b57 100644 --- a/src/js/savegame/savegame_interface_registry.js +++ b/src/js/savegame/savegame_interface_registry.js @@ -11,6 +11,7 @@ import { SavegameInterface_V1007 } from "./schemas/1007"; import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1009 } from "./schemas/1009"; import { SavegameInterface_V1010 } from "./schemas/1010"; +import { SavegameInterface_V1011 } from "./schemas/1011"; /** @type {Object.} */ export const savegameInterfaces = { @@ -25,6 +26,7 @@ export const savegameInterfaces = { 1008: SavegameInterface_V1008, 1009: SavegameInterface_V1009, 1010: SavegameInterface_V1010, + 1011: SavegameInterface_V1011, }; const logger = createLogger("savegame_interface_registry"); diff --git a/src/js/savegame/schemas/1011.js b/src/js/savegame/schemas/1011.js new file mode 100644 index 00000000..8f203250 --- /dev/null +++ b/src/js/savegame/schemas/1011.js @@ -0,0 +1,44 @@ +import { createLogger } from "../../core/logging.js"; +import { ItemProcessorComponent } from "../../game/components/item_processor.js"; +import { MinerComponent } from "../../game/components/miner.js"; +import { Entity } from "../../game/entity.js"; +import { SavegameInterface_V1010 } from "./1010.js"; + +const schema = require("./1011.json"); +const logger = createLogger("savegame_interface/1011"); + +export class SavegameInterface_V1011 extends SavegameInterface_V1010 { + getVersion() { + return 1011; + } + + getSchemaUncached() { + return schema; + } + + /** + * @param {import("../savegame_typedefs.js").SavegameData} data + */ + static migrate1010to1011(data) { + logger.log("Migrating 1010 to 1011"); + const dump = data.dump; + if (!dump) { + return true; + } + + /** @type {Array