import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; 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"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; import { MapChunkView } from "../map_chunk_view"; const logger = createLogger("systems/ejector"); export class ItemEjectorSystem extends GameSystemWithFilter { constructor(root) { super(root, [ItemEjectorComponent]); this.staleAreaDetector = new StaleAreaDetector({ root: this.root, name: "item-ejector", recomputeMethod: this.recomputeArea.bind(this), }); this.staleAreaDetector.recomputeOnComponentsChanged( [ItemEjectorComponent, ItemAcceptorComponent, BeltComponent], 1 ); this.root.signals.postLoadHook.add(this.recomputeCacheFull, this); } /** * Recomputes an area after it changed * @param {Rectangle} area */ recomputeArea(area) { /** @type {Set} */ const seenUids = new Set(); for (let x = 0; x < area.w; ++x) { for (let y = 0; y < area.h; ++y) { const tileX = area.x + x; const tileY = area.y + y; // @NOTICE: Item ejector currently only supports regular layer const contents = this.root.map.getLayerContentXY(tileX, tileY, "regular"); if (contents && contents.components.ItemEjector) { if (!seenUids.has(contents.uid)) { seenUids.add(contents.uid); this.recomputeSingleEntityCache(contents); } } } } } /** * Recomputes the whole cache after the game has loaded */ recomputeCacheFull() { logger.log("Full cache recompute in post load hook"); for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; this.recomputeSingleEntityCache(entity); } } /** * @param {Entity} entity */ recomputeSingleEntityCache(entity) { const ejectorComp = entity.components.ItemEjector; const staticComp = entity.components.StaticMapEntity; for (let slotIndex = 0; slotIndex < ejectorComp.slots.length; ++slotIndex) { const ejectorSlot = ejectorComp.slots[slotIndex]; // Clear the old cache. ejectorSlot.cachedDestSlot = null; ejectorSlot.cachedTargetEntity = null; ejectorSlot.cachedBeltPath = null; // Figure out where and into which direction we eject items const ejectSlotWsTile = staticComp.localTileToWorld(ejectorSlot.pos); const ejectSlotWsDirection = staticComp.localDirectionToWorld(ejectorSlot.direction); const ejectSlotWsDirectionVector = enumDirectionToVector[ejectSlotWsDirection]; const ejectSlotTargetWsTile = ejectSlotWsTile.add(ejectSlotWsDirectionVector); // Try to find the given acceptor component to take the item // Since there can be cross layer dependencies, check on all layers const targetEntities = this.root.map.getLayersContentsMultipleXY( ejectSlotTargetWsTile.x, ejectSlotTargetWsTile.y ); for (let i = 0; i < targetEntities.length; ++i) { const targetEntity = targetEntities[i]; const targetStaticComp = targetEntity.components.StaticMapEntity; const targetBeltComp = targetEntity.components.Belt; // Check for belts (special case) if (targetBeltComp) { const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top); if (ejectSlotWsDirection === beltAcceptingDirection) { ejectorSlot.cachedTargetEntity = targetEntity; ejectorSlot.cachedBeltPath = targetBeltComp.assignedPath; break; } } // Check for item acceptors const targetAcceptorComp = targetEntity.components.ItemAcceptor; if (!targetAcceptorComp) { // Entity doesn't accept items continue; } const matchingSlot = targetAcceptorComp.findMatchingSlot( targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile), targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection) ); if (!matchingSlot) { // No matching slot found continue; } // A slot can always be connected to one other slot only ejectorSlot.cachedTargetEntity = targetEntity; ejectorSlot.cachedDestSlot = matchingSlot; break; } } } update() { this.staleAreaDetector.update(); // Precompute effective belt speed let progressGrowth = 2 * this.root.dynamicTickrate.deltaSeconds; if (G_IS_DEV && globalConfig.debug.instantBelts) { progressGrowth = 1; } // Go over all cache entries for (let i = 0; i < this.allEntities.length; ++i) { const sourceEntity = this.allEntities[i]; const sourceEjectorComp = sourceEntity.components.ItemEjector; const slots = sourceEjectorComp.slots; for (let j = 0; j < slots.length; ++j) { const sourceSlot = slots[j]; const item = sourceSlot.item; if (!item) { // No item available to be ejected continue; } // Advance items on the slot sourceSlot.progress = Math.min( 1, sourceSlot.progress + progressGrowth * this.root.hubGoals.getBeltBaseSpeed() * globalConfig.itemSpacingOnBelts ); if (G_IS_DEV && globalConfig.debug.disableEjectorProcessing) { sourceSlot.progress = 1.0; } // Check if we are still in the process of ejecting, can't proceed then if (sourceSlot.progress < 1.0) { continue; } // Check if we are ejecting to a belt path const destPath = sourceSlot.cachedBeltPath; if (destPath) { // Try passing the item over if (destPath.tryAcceptItem(item)) { sourceSlot.item = null; } // Always stop here, since there can *either* be a belt path *or* // a slot continue; } // Check if the target acceptor can actually accept this item const destEntity = sourceSlot.cachedTargetEntity; const destSlot = sourceSlot.cachedDestSlot; if (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 targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.acceptedDirection, 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. // Also its just a few cases (hope it stays like this .. :x). 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; } 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; } return false; } /** * @param {DrawParameters} parameters * @param {MapChunkView} chunk */ drawChunk(parameters, chunk) { const contents = chunk.containedEntitiesByLayer.regular; for (let i = 0; i < contents.length; ++i) { const entity = contents[i]; const ejectorComp = entity.components.ItemEjector; if (!ejectorComp) { continue; } const staticComp = entity.components.StaticMapEntity; for (let i = 0; i < ejectorComp.slots.length; ++i) { const slot = ejectorComp.slots[i]; const ejectedItem = slot.item; if (!ejectedItem) { // No item continue; } if (!ejectorComp.renderFloatingItems && !slot.cachedTargetEntity) { // Not connected to any building continue; } const realPosition = staticComp.localTileToWorld(slot.pos); if (!chunk.tileSpaceRectangle.containsPoint(realPosition.x, realPosition.y)) { // Not within this chunk continue; } const realDirection = staticComp.localDirectionToWorld(slot.direction); const realDirectionVector = enumDirectionToVector[realDirection]; const tileX = realPosition.x + 0.5 + realDirectionVector.x * 0.5 * slot.progress; const tileY = realPosition.y + 0.5 + realDirectionVector.y * 0.5 * slot.progress; const worldX = tileX * globalConfig.tileSize; const worldY = tileY * globalConfig.tileSize; ejectedItem.drawItemCenteredClipped( worldX, worldY, parameters, globalConfig.defaultItemDiameter ); } } } }