From 242b99b19b55fca58bcef78264e26d5b0744a10f Mon Sep 17 00:00:00 2001 From: Sense101 Date: Tue, 1 Feb 2022 16:48:24 +0000 Subject: [PATCH] Refactor item acceptor and processor to be cleaner and work more smoothly --- src/js/core/config.js | 19 ++- src/js/game/belt_path.js | 99 ++----------- src/js/game/buildings/balancer.js | 3 +- src/js/game/buildings/trash.js | 21 --- src/js/game/buildings/underground_belt.js | 1 - src/js/game/components/filter.js | 6 +- src/js/game/components/item_acceptor.js | 117 +++++++++++----- src/js/game/components/item_ejector.js | 18 +-- src/js/game/components/item_processor.js | 52 ++----- src/js/game/components/storage.js | 23 +-- src/js/game/components/underground_belt.js | 44 +----- src/js/game/game_system_manager.js | 8 +- src/js/game/hub_goals.js | 69 +++++---- src/js/game/systems/filter.js | 32 ++--- src/js/game/systems/item_acceptor.js | 68 ++++----- src/js/game/systems/item_ejector.js | 156 +++++---------------- src/js/game/systems/item_processor.js | 133 +++++++++--------- src/js/game/systems/storage.js | 3 + src/js/game/systems/underground_belt.js | 53 ++++--- 19 files changed, 351 insertions(+), 574 deletions(-) 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 e1b466e9..bf797c61 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -130,10 +130,12 @@ export class BeltPath extends BasicSerializableObject { /** * 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. + // this means we are moving it forwards twice in one tick, but otherwise belts won't be full :( const beltProgressPerTick = this.root.hubGoals.getBeltBaseSpeed() * this.root.dynamicTickrate.deltaSeconds * @@ -141,7 +143,7 @@ export class BeltPath extends BasicSerializableObject { // 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, beltProgressPerTick + extraProgress); this.items.unshift([this.spacingToFirstItem - initialProgress, item]); this.spacingToFirstItem = initialProgress; @@ -227,8 +229,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 +274,24 @@ 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; + return function (item, startProgress = 0.0) { + const storageComp = targetEntity.components.Storage; + if ( + storageComp && + storageComp.tryAcceptItem(item) && + targetAcceptorComp.tryAcceptItem(matchingSlotIndex, item, startProgress) + ) { + // unique duplicated code for storage + return true; } - - // Try to pass over - if (passOver(item, matchingSlotIndex)) { - // Trigger animation on the acceptor comp - if (noSimplifiedBelts) { - targetAcceptorComp.onItemAccepted( - matchingSlotIndex, - matchingDirection, - item, - remainingProgress - ); - } + if (targetAcceptorComp.tryAcceptItem(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/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..987de239 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -2,6 +2,9 @@ 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 { typeItemSingleton } from "../item_resolver"; +import { GameRoot } from "../root"; /** * @typedef {{ @@ -24,34 +27,69 @@ 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 + */ + 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 */ - constructor({ slots = [] }) { + constructor({ slots = [], maxSlotInputs = 2 }) { super(); + /** @type {ItemAcceptorInputs} */ + this.inputs = []; + /** @type {ItemAcceptorCompletedInputs} */ + this.completedInputs = []; this.setSlots(slots); - this.clear(); - } - clear() { - /** - * Fixes belt animations - * @type {Array<{ - * item: BaseItem, - * slotIndex: number, - * animProgress: number, - * direction: enumDirection - * }>} - */ - this.itemConsumptionAnimations = []; + // setting this to 1 will cause throughput issues at very high speeds + this.maxSlotInputs = maxSlotInputs; } /** @@ -74,31 +112,42 @@ 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 + * Called when trying to input a new item * @param {number} slotIndex - * @param {BaseItem=} item - */ - canAcceptItem(slotIndex, item) { - const slot = this.slots[slotIndex]; - return !slot.filter || slot.filter === item.getItemType(); - } - - /** - * Called when an item has been accepted so that - * @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(slotIndex, item, startProgress = 0.0) { + const slot = this.slots[slotIndex]; + + 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 (slot.filter && slot.filter != item.getItemType()) { + 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..9da8489a 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -29,6 +29,7 @@ export const enumItemProcessorRequirements = { /** @typedef {{ * item: BaseItem, + * extraProgress?: number * requiredSlot?: number, * preferredSlot?: number * }} EjectorItemToEject */ @@ -38,6 +39,13 @@ export const enumItemProcessorRequirements = { * items: Array, * }} EjectorCharge */ +/** + * @typedef {{ + * item: BaseItem + * extraProgress: number + * }} ItemProcessorInput + */ + export class ItemProcessorComponent extends Component { static getId() { return "ItemProcessor"; @@ -73,12 +81,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 +90,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 +109,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/storage.js b/src/js/game/components/storage.js index be243a44..ce179416 100644 --- a/src/js/game/components/storage.js +++ b/src/js/game/components/storage.js @@ -46,32 +46,19 @@ export class StorageComponent extends Component { * Returns whether this storage can accept the item * @param {BaseItem} item */ - canAcceptItem(item) { + tryAcceptItem(item) { if (this.storedCount >= this.maximumStorage) { return false; } - if (!this.storedItem || this.storedCount === 0) { - return true; - } - const itemType = item.getItemType(); - - // Check type matches - if (itemType !== this.storedItem.getItemType()) { + if (this.storedCount > 0 && this.storedItem && itemType !== this.storedItem.getItemType()) { return false; } - if (itemType === "color") { - return /** @type {ColorItem} */ (this.storedItem).color === /** @type {ColorItem} */ (item).color; - } + this.storedItem = item; + this.storedCount++; - if (itemType === "shape") { - return ( - /** @type {ShapeItem} */ (this.storedItem).definition.getHash() === - /** @type {ShapeItem} */ (item).definition.getHash() - ); - } - return false; + return true; } /** 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..d8c16200 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -150,20 +150,20 @@ export class GameSystemManager { add("belt", BeltSystem); - add("undergroundBelt", UndergroundBeltSystem); - add("miner", MinerSystem); add("storage", StorageSystem); + add("itemEjector", ItemEjectorSystem); + + add("undergroundBelt", UndergroundBeltSystem); + add("itemProcessor", ItemProcessorSystem); add("filter", FilterSystem); 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/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..ea8e7381 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,27 @@ 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 - ); + if (sourceSlot.progress < maxProgress) { + // 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; } + const extraProgress = sourceSlot.progress - maxProgress; + // Check if we are ejecting to a belt path const destPath = sourceSlot.cachedBeltPath; if (destPath) { // Try passing the item over - if (destPath.tryAcceptItem(item)) { + if (destPath.tryAcceptItem(item, extraProgress)) { sourceSlot.item = null; } @@ -193,110 +195,27 @@ 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)) { - // Handover successful, clear slot - if (!this.root.app.settings.getAllSettings().simplifiedBelts) { - targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.slot.direction, item); - } + const storageComp = destEntity.components.Storage; + if ( + storageComp && + storageComp.tryAcceptItem(item) && + targetAcceptorComp.tryAcceptItem(destSlot.index, item, extraProgress) + ) { + // unique duplicated code for storage - hacky :( + sourceSlot.item = null; + return; + } + if (targetAcceptorComp.tryAcceptItem(destSlot.index, item, extraProgress)) { + // Handover successful, clear slot 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 +252,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 +287,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 +309,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 6e1032c9..efbdf516 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,7 +30,6 @@ const MAX_QUEUED_CHARGES = 2; * @typedef {{ * entity: Entity, * items: Map, - * inputCount: number, * outItems: Array * }} ProcessorImplementationPayload */ @@ -82,8 +78,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) { @@ -103,19 +105,13 @@ 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; } } + // 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"); @@ -139,7 +135,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); @@ -150,53 +146,21 @@ 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; - - 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; - } - } + // requirements are no longer needed as items will always be accepted, only the next method is. /** * Checks whether it's possible to process something * @param {Entity} entity */ canProcess(entity) { + const acceptorComp = entity.components.ItemAcceptor; const processorComp = entity.components.ItemProcessor; switch (processorComp.processingRequirement) { // DEFAULT // By default, we can start processing once all inputs are there case null: { - return processorComp.inputCount >= processorComp.inputsPerCharge; + return acceptorComp.completedInputs.length >= processorComp.inputsPerCharge; } // QUAD PAINTER @@ -204,8 +168,13 @@ export class ItemProcessorSystem extends GameSystemWithFilter { case enumItemProcessorRequirements.painterQuad: { const pinsComp = entity.components.WiredPins; + const input = acceptorComp.completedInputs[0]; + if (!input) { + return false; + } + // 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} */ (input.item); if (!shapeItem) { return false; } @@ -234,7 +203,10 @@ 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] && + !acceptorComp.completedInputs.find(input => input.slotIndex == i + 1) // @TODO this is slow + ) { // 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) { @@ -259,10 +231,21 @@ 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]; + + items.set(input.slotIndex, input.item); + extraProgress = Math.max(extraProgress, input.extraProgress); + } /** @type {Array} */ const outItems = []; @@ -276,7 +259,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter { entity, items, outItems, - inputCount: processorComp.inputCount, }); // Track produced items @@ -284,23 +266,35 @@ 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; + // only remove one item from each slot - we don't want to delete extra items! + let usedSlots = []; + 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--; + } + } } /** @@ -445,7 +439,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); + } } /** @@ -569,8 +570,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/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(); } }