1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-13 13:04:03 +00:00

Refactor item acceptor and processor to be cleaner and work more smoothly

This commit is contained in:
Sense101 2022-02-01 16:48:24 +00:00
parent f13a48e812
commit 242b99b19b
19 changed files with 351 additions and 574 deletions

View File

@ -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

View File

@ -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 */

View File

@ -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)]];
}

View File

@ -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);
}
}

View File

@ -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: [],

View File

@ -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
})
),
};

View File

@ -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<ItemAcceptorSlotConfig>} 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;
}
/**

View File

@ -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;
}
}

View File

@ -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<EjectorItemToEject>,
* }} 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<number, BaseItem>}
*/
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<EjectorCharge>}
* @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;
}
}

View File

@ -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;
}
/**

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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(

View File

@ -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;

View File

@ -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<number, BaseItem>,
* inputCount: number,
* outItems: Array<ProducedItem>
* }} 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<ProducedItem>} */
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;

View File

@ -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;

View File

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