1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-13 13:04:03 +00:00
This commit is contained in:
Sense101 2022-03-06 00:34:02 +00:00 committed by GitHub
commit 0f022f54cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 624 additions and 714 deletions

View File

@ -87,17 +87,14 @@ export const globalConfig = {
puzzleMaxBoundsSize: 20, puzzleMaxBoundsSize: 20,
puzzleValidationDurationSeconds: 30, puzzleValidationDurationSeconds: 30,
buildingSpeeds: { buildingRatios: {
cutter: 1 / 4, cutter: 4,
cutterQuad: 1 / 4, cutterQuad: 4,
rotater: 1 / 1, painter: 6,
rotaterCCW: 1 / 1, painterDouble: 8,
rotater180: 1 / 1, painterQuad: 2,
painter: 1 / 6, mixer: 5,
painterDouble: 1 / 8, stacker: 8,
painterQuad: 1 / 2,
mixer: 1 / 5,
stacker: 1 / 8,
}, },
// Zooming // Zooming

View File

@ -119,29 +119,18 @@ export class BeltPath extends BasicSerializableObject {
this.numCompressedItemsAfterFirstItem = 0; this.numCompressedItemsAfterFirstItem = 0;
} }
/**
* Returns whether this path can accept a new item
* @returns {boolean}
*/
canAcceptItem() {
return this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts;
}
/** /**
* Tries to accept the item * Tries to accept the item
* @param {BaseItem} item * @param {BaseItem} item
* @param {number} extraProgress
*/ */
tryAcceptItem(item) { tryAcceptItem(item, extraProgress = 0) {
if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) { if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) {
// So, since we already need one tick to accept this item we will add this directly. // So, since we already need one tick to accept this item we will add this directly.
const beltProgressPerTick =
this.root.hubGoals.getBeltBaseSpeed() *
this.root.dynamicTickrate.deltaSeconds *
globalConfig.itemSpacingOnBelts;
// First, compute how much progress we can make *at max* // First, compute how much progress we can make *at max*
const maxProgress = Math.max(0, this.spacingToFirstItem - globalConfig.itemSpacingOnBelts); const maxProgress = Math.max(0, this.spacingToFirstItem - globalConfig.itemSpacingOnBelts);
const initialProgress = Math.min(maxProgress, beltProgressPerTick); const initialProgress = Math.min(maxProgress, extraProgress);
this.items.unshift([this.spacingToFirstItem - initialProgress, item]); this.items.unshift([this.spacingToFirstItem - initialProgress, item]);
this.spacingToFirstItem = initialProgress; this.spacingToFirstItem = initialProgress;
@ -227,8 +216,6 @@ export class BeltPath extends BasicSerializableObject {
return; return;
} }
const noSimplifiedBelts = !this.root.app.settings.getAllSettings().simplifiedBelts;
DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid); DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid);
const targetStaticComp = targetEntity.components.StaticMapEntity; const targetStaticComp = targetEntity.components.StaticMapEntity;
const targetBeltComp = targetEntity.components.Belt; const targetBeltComp = targetEntity.components.Belt;
@ -274,95 +261,15 @@ export class BeltPath extends BasicSerializableObject {
} }
const matchingSlotIndex = matchingSlot.index; const matchingSlotIndex = matchingSlot.index;
const passOver = this.computePassOverFunctionWithoutBelts(targetEntity, matchingSlotIndex);
if (!passOver) {
return;
}
const matchingDirection = enumInvertedDirections[ejectingDirection]; return function (item, startProgress = 0.0) {
const filter = matchingSlot.slot.filter; if (targetAcceptorComp.tryAcceptItem(targetEntity, matchingSlotIndex, item, startProgress)) {
return function (item, remainingProgress = 0.0) {
// Check if the acceptor has a filter
if (filter && item._type !== filter) {
return false;
}
// Try to pass over
if (passOver(item, matchingSlotIndex)) {
// Trigger animation on the acceptor comp
if (noSimplifiedBelts) {
targetAcceptorComp.onItemAccepted(
matchingSlotIndex,
matchingDirection,
item,
remainingProgress
);
}
return true; return true;
} }
return false; 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 // Following code will be compiled out outside of dev versions
/* dev:start */ /* dev:start */

View File

@ -104,8 +104,7 @@ export class MetaBalancerBuilding extends MetaBuilding {
speedMultiplier = 1; speedMultiplier = 1;
} }
const speed = const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.balancer) * speedMultiplier;
(root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.balancer) / 2) * speedMultiplier;
return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]];
} }

View File

@ -1,7 +1,7 @@
import { formatItemsPerSecond } from "../../core/utils"; import { formatItemsPerSecond } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector"; import { enumDirection, Vector } from "../../core/vector";
import { T } from "../../translations"; import { T } from "../../translations";
import { ItemAcceptorComponent } from "../components/item_acceptor"; import { enumInputRequirements, ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector"; import { ItemEjectorComponent } from "../components/item_ejector";
import { import {
enumItemProcessorTypes, enumItemProcessorTypes,
@ -274,6 +274,7 @@ export class MetaPainterBuilding extends MetaBuilding {
filter: "color", filter: "color",
}, },
]); ]);
entity.components.ItemAcceptor.inputRequirement = enumInputRequirements.quadPainter;
entity.components.ItemEjector.setSlots([ entity.components.ItemEjector.setSlots([
{ pos: new Vector(0, 0), direction: enumDirection.top }, { pos: new Vector(0, 0), direction: enumDirection.top },

View File

@ -1,7 +1,7 @@
import { formatBigNumber } from "../../core/utils"; import { formatBigNumber } from "../../core/utils";
import { enumDirection, Vector } from "../../core/vector"; import { enumDirection, Vector } from "../../core/vector";
import { T } from "../../translations"; import { T } from "../../translations";
import { ItemAcceptorComponent } from "../components/item_acceptor"; import { enumInputRequirements, ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector"; import { ItemEjectorComponent } from "../components/item_ejector";
import { StorageComponent } from "../components/storage"; import { StorageComponent } from "../components/storage";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
@ -81,6 +81,7 @@ export class MetaStorageBuilding extends MetaBuilding {
direction: enumDirection.bottom, direction: enumDirection.bottom,
}, },
], ],
inputRequirement: enumInputRequirements.storage,
}) })
); );

View File

@ -47,25 +47,6 @@ export class MetaTrashBuilding extends MetaBuilding {
return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_cutter_and_trash); 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 * Creates the entity at the given location
* @param {Entity} entity * @param {Entity} entity
@ -100,7 +81,5 @@ export class MetaTrashBuilding extends MetaBuilding {
processorType: enumItemProcessorTypes.trash, processorType: enumItemProcessorTypes.trash,
}) })
); );
this.addAchievementReceiver(entity);
} }
} }

View File

@ -184,7 +184,6 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding {
* @param {Entity} entity * @param {Entity} entity
*/ */
setupEntityComponents(entity) { setupEntityComponents(entity) {
// Required, since the item processor needs this.
entity.addComponent( entity.addComponent(
new ItemEjectorComponent({ new ItemEjectorComponent({
slots: [], slots: [],

View File

@ -6,7 +6,7 @@ import { typeItemSingleton } from "../item_resolver";
/** /**
* @typedef {{ * @typedef {{
* item: BaseItem, * item: BaseItem,
* progress: number * extraProgress: number
* }} PendingFilterItem * }} PendingFilterItem
*/ */
@ -24,14 +24,14 @@ export class FilterComponent extends Component {
pendingItemsToLeaveThrough: types.array( pendingItemsToLeaveThrough: types.array(
types.structured({ types.structured({
item: typeItemSingleton, item: typeItemSingleton,
progress: types.ufloat, extraProgress: types.ufloat,
}) })
), ),
pendingItemsToReject: types.array( pendingItemsToReject: types.array(
types.structured({ types.structured({
item: typeItemSingleton, item: typeItemSingleton,
progress: types.ufloat, extraProgress: types.ufloat, //@SENSETODO will need save migration
}) })
), ),
}; };

View File

@ -2,6 +2,10 @@ import { enumDirection, enumInvertedDirections, Vector } from "../../core/vector
import { types } from "../../savegame/serialization"; import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item"; import { BaseItem } from "../base_item";
import { Component } from "../component"; import { Component } from "../component";
import { Entity } from "../entity";
import { isTruthyItem } from "../items/boolean_item";
import { typeItemSingleton } from "../item_resolver";
import { GameRoot } from "../root";
/** /**
* @typedef {{ * @typedef {{
@ -24,34 +28,85 @@ import { Component } from "../component";
* filter?: ItemType * filter?: ItemType
* }} ItemAcceptorSlotConfig */ * }} ItemAcceptorSlotConfig */
/**
* @typedef {Array<{
* slotIndex: number,
* item: BaseItem,
* animProgress: number,
* }>} ItemAcceptorInputs
*
* @typedef {Array<{
* slotIndex: number,
* item: BaseItem,
* extraProgress: number
* }>} ItemAcceptorCompletedInputs
*
* @typedef {{
* root: GameRoot,
* entity: Entity,
* item: BaseItem,
* slotIndex: number,
* extraProgress: number
* }} InputCompletedArgs
*/
/** @enum {string} */
export const enumInputRequirements = {
quadPainter: "quadPainter",
storage: "storage",
};
export const MOD_INPUT_REQUIREMENTS = [];
export class ItemAcceptorComponent extends Component { export class ItemAcceptorComponent extends Component {
static getId() { static getId() {
return "ItemAcceptor"; 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 {object} param0
* @param {Array<ItemAcceptorSlotConfig>} param0.slots The slots from which we accept items * @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
* @param {string|null=} param0.inputRequirement The requirement to accept items
*/ */
constructor({ slots = [] }) { constructor({ slots = [], maxSlotInputs = 2, inputRequirement = null }) {
super(); super();
this.setSlots(slots); this.setSlots(slots);
this.inputRequirement = inputRequirement;
// setting this to 1 will cause throughput issues at very high speeds
this.maxSlotInputs = maxSlotInputs;
this.clear(); this.clear();
} }
clear() { clear() {
/** /** @type {ItemAcceptorInputs} */
* Fixes belt animations this.inputs = [];
* @type {Array<{ /** @type {ItemAcceptorCompletedInputs} */
* item: BaseItem, this.completedInputs = [];
* slotIndex: number,
* animProgress: number,
* direction: enumDirection
* }>}
*/
this.itemConsumptionAnimations = [];
} }
/** /**
@ -74,31 +129,107 @@ export class ItemAcceptorComponent extends Component {
} }
/** /**
* Returns if this acceptor can accept a new item at slot N
* *
* NOTICE: The belt path ignores this for performance reasons and does his own check * @param {Entity} entity
* @param {BaseItem} item
* @param {number} slotIndex * @param {number} slotIndex
* @param {BaseItem=} item * @returns
*/ */
canAcceptItem(slotIndex, item) { canAcceptItem(entity, item, slotIndex) {
const slot = this.slots[slotIndex]; const slot = this.slots[slotIndex];
return !slot.filter || slot.filter === item.getItemType();
// make sure there is a slot and we match the filter
if (slot && !(slot.filter && slot.filter != item.getItemType())) {
if (MOD_INPUT_REQUIREMENTS[this.inputRequirement]) {
return MOD_INPUT_REQUIREMENTS[this.inputRequirement].call(this, {
entity,
item,
slotIndex,
});
}
switch (this.inputRequirement) {
case null: {
return true;
}
case enumInputRequirements.quadPainter: {
const pinsComp = entity.components.WiredPins;
if (slotIndex === 0) {
// Always accept the shape
return true;
}
// Check the network value at the given slot
const network = pinsComp.slots[slotIndex - 1].linkedNetwork;
const slotIsEnabled = network && network.hasValue() && isTruthyItem(network.currentValue);
if (!slotIsEnabled) {
return false;
}
return true;
}
case enumInputRequirements.storage: {
const storageComp = entity.components.Storage;
if (storageComp.storedCount >= storageComp.maximumStorage) {
return false;
}
const itemType = item.getItemType();
if (storageComp.storedItem && itemType !== storageComp.storedItem.getItemType()) {
return false;
}
// set the item straight away - this way different kinds of items can't be inq the acceptor
storageComp.storedItem = item;
storageComp.storedCount++;
return true;
}
default: {
assertAlways(false, "Input requirement is not recognised: " + this.inputRequirement);
break;
}
}
}
return false;
} }
/** /**
* Called when an item has been accepted so that * Called when trying to input a new item
* @param {Entity} entity
* @param {number} slotIndex * @param {number} slotIndex
* @param {enumDirection} direction
* @param {BaseItem} item * @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) { tryAcceptItem(entity, slotIndex, item, startProgress = 0.0) {
this.itemConsumptionAnimations.push({ // make sure we have space to actually accept
item, let existingInputs = 0;
for (let i = 0; i < this.inputs.length; i++) {
if (this.inputs[i].slotIndex == slotIndex) {
existingInputs++;
}
}
for (let i = 0; i < this.completedInputs.length; i++) {
if (this.completedInputs[i].slotIndex == slotIndex) {
existingInputs++;
}
}
if (existingInputs >= this.maxSlotInputs) {
return false;
}
if (!this.canAcceptItem(entity, item, slotIndex)) {
return false;
}
// if the start progress is bigger than 0.5, the remainder should get passed on to the ejector
this.inputs.push({
slotIndex, slotIndex,
direction, item,
animProgress: Math.min(1, remainingProgress * 2), animProgress: startProgress,
}); });
return true;
} }
/** /**

View File

@ -127,28 +127,16 @@ export class ItemEjectorComponent extends Component {
* Tries to eject a given item * Tries to eject a given item
* @param {number} slotIndex * @param {number} slotIndex
* @param {BaseItem} item * @param {BaseItem} item
* @param {number} startingProgress
* @returns {boolean} * @returns {boolean}
*/ */
tryEject(slotIndex, item) { tryEject(slotIndex, item, startingProgress = 0.0) {
if (!this.canEjectOnSlot(slotIndex)) { if (!this.canEjectOnSlot(slotIndex)) {
return false; return false;
} }
this.slots[slotIndex].item = item; this.slots[slotIndex].item = item;
this.slots[slotIndex].lastItem = item; this.slots[slotIndex].lastItem = item;
this.slots[slotIndex].progress = 0; this.slots[slotIndex].progress = startingProgress;
return true; 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

@ -1,6 +1,8 @@
import { types } from "../../savegame/serialization"; import { types } from "../../savegame/serialization";
import { TypeString } from "../../savegame/serialization_data_types";
import { BaseItem } from "../base_item"; import { BaseItem } from "../base_item";
import { Component } from "../component"; import { Component } from "../component";
import { typeItemSingleton } from "../item_resolver";
/** @enum {string} */ /** @enum {string} */
export const enumItemProcessorTypes = { export const enumItemProcessorTypes = {
@ -29,6 +31,7 @@ export const enumItemProcessorRequirements = {
/** @typedef {{ /** @typedef {{
* item: BaseItem, * item: BaseItem,
* extraProgress?: number
* requiredSlot?: number, * requiredSlot?: number,
* preferredSlot?: number * preferredSlot?: number
* }} EjectorItemToEject */ * }} EjectorItemToEject */
@ -38,6 +41,13 @@ export const enumItemProcessorRequirements = {
* items: Array<EjectorItemToEject>, * items: Array<EjectorItemToEject>,
* }} EjectorCharge */ * }} EjectorCharge */
/**
* @typedef {{
* item: BaseItem
* extraProgress: number
* }} ItemProcessorInput
*/
export class ItemProcessorComponent extends Component { export class ItemProcessorComponent extends Component {
static getId() { static getId() {
return "ItemProcessor"; return "ItemProcessor";
@ -46,6 +56,19 @@ export class ItemProcessorComponent extends Component {
static getSchema() { static getSchema() {
return { return {
nextOutputSlot: types.uint, nextOutputSlot: types.uint,
currentCharge: types.nullable(
types.structured({
remainingTime: types.ufloat,
items: types.array(
types.structured({
item: typeItemSingleton,
extraProgress: types.nullable(types.float),
requiredSlot: types.nullable(types.uint),
preferredSlot: types.nullable(types.uint),
})
),
})
),
}; };
} }
@ -73,12 +96,6 @@ export class ItemProcessorComponent extends Component {
// Type of processing requirement // Type of processing requirement
this.processingRequirement = processingRequirement; this.processingRequirement = processingRequirement;
/**
* Our current inputs
* @type {Map<number, BaseItem>}
*/
this.inputSlots = new Map();
this.clear(); this.clear();
} }
@ -88,21 +105,13 @@ export class ItemProcessorComponent extends Component {
// sure the outputs always match // sure the outputs always match
this.nextOutputSlot = 0; 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 * What we are currently processing, empty if we don't produce anything rn
* requiredSlot: Item *must* be ejected on this slot * 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 * 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 * How much processing time we have left from the last tick
@ -115,30 +124,4 @@ export class ItemProcessorComponent extends Component {
*/ */
this.queuedEjects = []; 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

@ -2,9 +2,13 @@ import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item"; import { BaseItem } from "../base_item";
import { Component } from "../component"; import { Component } from "../component";
import { Entity } from "../entity"; import { Entity } from "../entity";
import { typeItemSingleton } from "../item_resolver";
const chainBufferSize = 6; /**
* @typedef {{
* item: BaseItem,
* extraProgress?: number,
* }} MinerItem
*/
export class MinerComponent extends Component { export class MinerComponent extends Component {
static getId() { static getId() {
@ -14,17 +18,17 @@ export class MinerComponent extends Component {
static getSchema() { static getSchema() {
// cachedMinedItem is not serialized. // cachedMinedItem is not serialized.
return { return {
lastMiningTime: types.ufloat, progress: types.ufloat,
itemChainBuffer: types.array(typeItemSingleton),
}; };
} }
constructor({ chainable = false }) { constructor({ chainable = false }) {
super(); super();
this.lastMiningTime = 0; this.progress = 0;
this.chainable = chainable; this.chainable = chainable;
/** /**
* The item we are mining beneath us
* @type {BaseItem} * @type {BaseItem}
*/ */
this.cachedMinedItem = null; this.cachedMinedItem = null;
@ -35,30 +39,11 @@ export class MinerComponent extends Component {
* @type {Entity|null|false} * @type {Entity|null|false}
*/ */
this.cachedChainedMiner = null; this.cachedChainedMiner = null;
this.clear();
}
clear() {
/** /**
* Stores items from other miners which were chained to this * The miner at the end of the chain, which actually ejects the items
* miner. * If the value is false, it means there is no entity, and we don't have to re-check
* @type {Array<BaseItem>} * @type {Entity|null|false}
*/ */
this.itemChainBuffer = []; this.cachedExitMiner = null;
}
/**
*
* @param {BaseItem} item
*/
tryAcceptChainedItem(item) {
if (this.itemChainBuffer.length > chainBufferSize) {
// Well, this one is full
return false;
}
this.itemChainBuffer.push(item);
return true;
} }
} }

View File

@ -2,13 +2,7 @@ import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item"; import { BaseItem } from "../base_item";
import { Component } from "../component"; import { Component } from "../component";
import { typeItemSingleton } from "../item_resolver"; import { typeItemSingleton } from "../item_resolver";
import { ColorItem } from "../items/color_item";
import { ShapeItem } from "../items/shape_item";
/** @type {{
* [x: string]: (item: BaseItem) => Boolean
* }} */
export const MODS_ADDITIONAL_STORAGE_ITEM_RESOLVER = {};
export class StorageComponent extends Component { export class StorageComponent extends Component {
static getId() { static getId() {
return "Storage"; return "Storage";
@ -46,42 +40,6 @@ export class StorageComponent extends Component {
this.overlayOpacity = 0; this.overlayOpacity = 0;
} }
/**
* Returns whether this storage can accept the item
* @param {BaseItem} item
*/
canAcceptItem(item) {
if (this.storedCount >= this.maximumStorage) {
return false;
}
if (!this.storedItem || this.storedCount === 0) {
return true;
}
const itemType = item.getItemType();
if (itemType !== this.storedItem.getItemType()) {
// Check type matches
return false;
}
if (MODS_ADDITIONAL_STORAGE_ITEM_RESOLVER[itemType]) {
return MODS_ADDITIONAL_STORAGE_ITEM_RESOLVER[itemType].apply(this, [item]);
}
if (itemType === "color") {
return /** @type {ColorItem} */ (this.storedItem).color === /** @type {ColorItem} */ (item).color;
}
if (itemType === "shape") {
return (
/** @type {ShapeItem} */ (this.storedItem).definition.getHash() ===
/** @type {ShapeItem} */ (item).definition.getHash()
);
}
return false;
}
/** /**
* Returns whether the storage is full * Returns whether the storage is full
* @returns {boolean} * @returns {boolean}

View File

@ -56,61 +56,31 @@ export class UndergroundBeltComponent extends Component {
this.consumptionAnimations = []; this.consumptionAnimations = [];
/** /**
* Used on both receiver and sender. * Used only on reciever to store which items are currently "travelling"
* Reciever: Used to store the next item to transfer, and to block input while doing this * @type {Array<[BaseItem, number]>} Format is [Item, Tile progress]
* Sender: Used to store which items are currently "travelling"
* @type {Array<[BaseItem, number]>} Format is [Item, ingame time to eject the item]
*/ */
this.pendingItems = []; 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 * Tries to accept a tunneled item
* @param {BaseItem} item * @param {BaseItem} item
* @param {number} travelDistance How many tiles this item has to travel * @param {number} travelDistance
* @param {number} beltSpeed How fast this item travels * @param {number} startProgress The starting tile progress
* @param {number} now Current ingame time
*/ */
tryAcceptTunneledItem(item, travelDistance, beltSpeed, now) { tryAcceptTunneledItem(item, travelDistance, startProgress = 0) {
if (this.mode !== enumUndergroundBeltMode.receiver) { if (this.mode !== enumUndergroundBeltMode.receiver) {
// Only receivers can accept tunneled items // Only receivers can accept tunneled items
return false; return false;
} }
// Notice: We assume that for all items the travel distance is the same // 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) { if (this.pendingItems.length >= maxItemsInTunnel) {
// Simulate a real belt which gets full at some point // Simulate a real belt which gets full at some point
return false; return false;
} }
// NOTICE: this.pendingItems.push([item, startProgress]);
// 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]);
return true; return true;
} }
} }

View File

@ -150,11 +150,13 @@ export class GameSystemManager {
add("belt", BeltSystem); add("belt", BeltSystem);
add("undergroundBelt", UndergroundBeltSystem); add("storage", StorageSystem);
add("itemEjector", ItemEjectorSystem);
add("miner", MinerSystem); add("miner", MinerSystem);
add("storage", StorageSystem); add("undergroundBelt", UndergroundBeltSystem);
add("itemProcessor", ItemProcessorSystem); add("itemProcessor", ItemProcessorSystem);
@ -162,8 +164,6 @@ export class GameSystemManager {
add("itemProducer", ItemProducerSystem); add("itemProducer", ItemProducerSystem);
add("itemEjector", ItemEjectorSystem);
if (this.root.gameMode.hasResources()) { if (this.root.gameMode.hasResources()) {
add("mapResources", MapResourcesSystem); add("mapResources", MapResourcesSystem);
} }

View File

@ -507,55 +507,36 @@ export class HubGoals extends BasicSerializableObject {
} }
/** /**
* Processor speed * Processor time to process
* @param {enumItemProcessorTypes} processorType * @param {enumItemProcessorTypes} processorType
* @returns {number} items / sec * @returns {number} process time in seconds
*/ */
getProcessorBaseSpeed(processorType) { getProcessingTime(processorType) {
if (this.root.gameMode.throughputDoesNotMatter()) { if (this.root.gameMode.throughputDoesNotMatter()) {
return globalConfig.beltSpeedItemsPerSecond * globalConfig.puzzleModeSpeed * 10; return 0;
} }
switch (processorType) { switch (processorType) {
case enumItemProcessorTypes.trash: case enumItemProcessorTypes.trash:
case enumItemProcessorTypes.hub: case enumItemProcessorTypes.hub:
case enumItemProcessorTypes.goal: case enumItemProcessorTypes.goal:
return 1e30;
case enumItemProcessorTypes.balancer: case enumItemProcessorTypes.balancer:
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2;
case enumItemProcessorTypes.reader: 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.mixer:
case enumItemProcessorTypes.painter: case enumItemProcessorTypes.painter:
case enumItemProcessorTypes.painterDouble: case enumItemProcessorTypes.painterDouble:
case enumItemProcessorTypes.painterQuad: { case enumItemProcessorTypes.painterQuad: {
assert( return this.getProcessorTimeWithUpgrades(this.upgradeImprovements.painting, processorType);
globalConfig.buildingSpeeds[processorType],
"Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType
);
return (
globalConfig.beltSpeedItemsPerSecond *
this.upgradeImprovements.painting *
globalConfig.buildingSpeeds[processorType]
);
} }
case enumItemProcessorTypes.cutter: case enumItemProcessorTypes.cutter:
case enumItemProcessorTypes.cutterQuad: case enumItemProcessorTypes.cutterQuad:
case enumItemProcessorTypes.rotater:
case enumItemProcessorTypes.rotaterCCW:
case enumItemProcessorTypes.rotater180:
case enumItemProcessorTypes.stacker: { case enumItemProcessorTypes.stacker: {
assert( return this.getProcessorTimeWithUpgrades(this.upgradeImprovements.processors, processorType);
globalConfig.buildingSpeeds[processorType],
"Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType
);
return (
globalConfig.beltSpeedItemsPerSecond *
this.upgradeImprovements.processors *
globalConfig.buildingSpeeds[processorType]
);
} }
default: default:
if (MOD_ITEM_PROCESSOR_SPEEDS[processorType]) { if (MOD_ITEM_PROCESSOR_SPEEDS[processorType]) {
@ -564,6 +545,34 @@ export class HubGoals extends BasicSerializableObject {
assertAlways(false, "invalid processor type: " + processorType); 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

@ -303,17 +303,14 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
const mouseTile = worldPos.toTileSpace(); const mouseTile = worldPos.toTileSpace();
// Compute best rotation variant // Compute best rotation variant
const { const { rotation, rotationVariant, connectedEntities } =
rotation, metaBuilding.computeOptimalDirectionAndRotationVariantAtTile({
rotationVariant, root: this.root,
connectedEntities, tile: mouseTile,
} = metaBuilding.computeOptimalDirectionAndRotationVariantAtTile({ rotation: this.currentBaseRotation,
root: this.root, variant: this.currentVariant.get(),
tile: mouseTile, layer: metaBuilding.getLayer(),
rotation: this.currentBaseRotation, });
variant: this.currentVariant.get(),
layer: metaBuilding.getLayer(),
});
// Check if there are connected entities // Check if there are connected entities
if (connectedEntities) { if (connectedEntities) {
@ -657,8 +654,16 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
// Connected to a belt // Connected to a belt
isConnected = true; isConnected = true;
} else if (minerComp && minerComp.chainable && destMiner && destMiner.chainable) { } else if (minerComp && minerComp.chainable && destMiner && destMiner.chainable) {
// Chainable miners connected to eachother const worldTile = staticComp.localTileToWorld(slot.pos);
isConnected = true; if (
this.root.map.getLowerLayerContentXY(worldTile.x, worldTile.y) ==
destMiner.cachedMinedItem
) {
// Chainable miners connected to eachother
isConnected = true;
} else {
isBlocked = true;
}
} else { } else {
// This one is blocked // This one is blocked
isBlocked = true; isBlocked = true;

View File

@ -134,6 +134,7 @@ export class HUDMinerHighlight extends BaseHUDPart {
findConnectedMiners(entity, seenUids = new Set()) { findConnectedMiners(entity, seenUids = new Set()) {
let results = []; let results = [];
const origin = entity.components.StaticMapEntity.origin; const origin = entity.components.StaticMapEntity.origin;
const originMinerComp = entity.components.Miner;
if (!seenUids.has(entity.uid)) { if (!seenUids.has(entity.uid)) {
seenUids.add(entity.uid); seenUids.add(entity.uid);
@ -157,7 +158,11 @@ export class HUDMinerHighlight extends BaseHUDPart {
); );
if (contents) { if (contents) {
const minerComp = contents.components.Miner; const minerComp = contents.components.Miner;
if (minerComp && minerComp.chainable) { if (
minerComp &&
minerComp.chainable &&
originMinerComp.cachedMinedItem == minerComp.cachedMinedItem
) {
// Found a miner connected to this entity // Found a miner connected to this entity
if (!seenUids.has(contents.uid)) { if (!seenUids.has(contents.uid)) {
if (this.root.systemMgr.systems.miner.findChainedMiner(contents) === entity) { if (this.root.systemMgr.systems.miner.findChainedMiner(contents) === entity) {

View File

@ -1,4 +1,3 @@
import { globalConfig } from "../../core/config";
import { BaseItem } from "../base_item"; import { BaseItem } from "../base_item";
import { FilterComponent } from "../components/filter"; import { FilterComponent } from "../components/filter";
import { Entity } from "../entity"; import { Entity } from "../entity";
@ -13,32 +12,27 @@ export class FilterSystem extends GameSystemWithFilter {
} }
update() { 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) { for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i]; const entity = this.allEntities[i];
const filterComp = entity.components.Filter; const filterComp = entity.components.Filter;
const acceptorComp = entity.components.ItemAcceptor;
const ejectorComp = entity.components.ItemEjector; 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]; const slotsAndLists = [filterComp.pendingItemsToLeaveThrough, filterComp.pendingItemsToReject];
for (let slotIndex = 0; slotIndex < slotsAndLists.length; ++slotIndex) { for (let slotIndex = 0; slotIndex < slotsAndLists.length; ++slotIndex) {
const pendingItems = slotsAndLists[slotIndex]; const pendingItems = slotsAndLists[slotIndex];
for (let j = 0; j < pendingItems.length; ++j) { for (let j = 0; j < pendingItems.length; ++j) {
const nextItem = pendingItems[j]; const nextItem = pendingItems[j];
// Advance next item if (ejectorComp.tryEject(slotIndex, nextItem.item)) {
nextItem.progress = Math.min(requiredProgress, nextItem.progress + progress); pendingItems.shift();
// Check if it's ready to eject
if (nextItem.progress >= requiredProgress - 1e-5) {
if (ejectorComp.tryEject(slotIndex, nextItem.item)) {
pendingItems.shift();
}
} }
} }
} }
@ -48,10 +42,10 @@ export class FilterSystem extends GameSystemWithFilter {
/** /**
* *
* @param {Entity} entity * @param {Entity} entity
* @param {number} slot
* @param {BaseItem} item * @param {BaseItem} item
* @param {number} startProgress
*/ */
tryAcceptItem(entity, slot, item) { tryAcceptItem(entity, item, startProgress) {
const network = entity.components.WiredPins.slots[0].linkedNetwork; const network = entity.components.WiredPins.slots[0].linkedNetwork;
if (!network || !network.hasValue()) { if (!network || !network.hasValue()) {
// Filter is not connected // Filter is not connected
@ -78,7 +72,7 @@ export class FilterSystem extends GameSystemWithFilter {
// Actually accept item // Actually accept item
listToCheck.push({ listToCheck.push({
item, item,
progress: 0.0, extraProgress: startProgress,
}); });
return true; return true;
} }

View File

@ -1,6 +1,5 @@
import { globalConfig } from "../../core/config"; import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters"; import { DrawParameters } from "../../core/draw_parameters";
import { fastArrayDelete } from "../../core/utils";
import { enumDirectionToVector } from "../../core/vector"; import { enumDirectionToVector } from "../../core/vector";
import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemAcceptorComponent } from "../components/item_acceptor";
import { GameSystemWithFilter } from "../game_system_with_filter"; import { GameSystemWithFilter } from "../game_system_with_filter";
@ -9,49 +8,36 @@ import { MapChunkView } from "../map_chunk_view";
export class ItemAcceptorSystem extends GameSystemWithFilter { export class ItemAcceptorSystem extends GameSystemWithFilter {
constructor(root) { constructor(root) {
super(root, [ItemAcceptorComponent]); super(root, [ItemAcceptorComponent]);
// Well ... it's better to be verbose I guess?
this.accumulatedTicksWhileInMapOverview = 0;
} }
update() { update() {
if (this.root.app.settings.getAllSettings().simplifiedBelts) { // same code for belts, acceptors and ejectors - add helper method???
// Disabled in potato mode const progressGrowth =
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 =
this.root.dynamicTickrate.deltaSeconds * this.root.dynamicTickrate.deltaSeconds *
2 *
this.root.hubGoals.getBeltBaseSpeed() * this.root.hubGoals.getBeltBaseSpeed() *
globalConfig.itemSpacingOnBelts * // * 2 because its only a half tile globalConfig.itemSpacingOnBelts;
numTicks;
// Reset accumulated ticks
this.accumulatedTicksWhileInMapOverview = 0;
for (let i = 0; i < this.allEntities.length; ++i) { for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i]; const entity = this.allEntities[i];
const aceptorComp = entity.components.ItemAcceptor; const acceptorComp = entity.components.ItemAcceptor;
const animations = aceptorComp.itemConsumptionAnimations; const inputs = acceptorComp.inputs;
const maxProgress = 0.5;
// Process item consumption animations to avoid items popping from the belts for (let i = 0; i < inputs.length; i++) {
for (let animIndex = 0; animIndex < animations.length; ++animIndex) { const input = inputs[i];
const anim = animations[animIndex]; input.animProgress += progressGrowth;
anim.animProgress += progress;
if (anim.animProgress > 1) { if (input.animProgress < maxProgress) {
fastArrayDelete(animations, animIndex); continue;
animIndex -= 1;
} }
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; const staticComp = entity.components.StaticMapEntity;
for (let animIndex = 0; animIndex < acceptorComp.itemConsumptionAnimations.length; ++animIndex) { for (let i = 0; i < acceptorComp.inputs.length; i++) {
const { item, slotIndex, animProgress, direction } = acceptorComp.itemConsumptionAnimations[ const input = acceptorComp.inputs[i];
animIndex const { item, animProgress, slotIndex } = input;
];
const slotData = acceptorComp.slots[slotIndex]; const slotData = acceptorComp.slots[slotIndex];
const realSlotPos = staticComp.localTileToWorld(slotData.pos); const realSlotPos = staticComp.localTileToWorld(slotData.pos);
@ -88,10 +73,11 @@ export class ItemAcceptorSystem extends GameSystemWithFilter {
continue; continue;
} }
const fadeOutDirection = enumDirectionToVector[staticComp.localDirectionToWorld(direction)]; const fadeOutDirection =
enumDirectionToVector[staticComp.localDirectionToWorld(slotData.direction)];
const finalTile = realSlotPos.subScalars( const finalTile = realSlotPos.subScalars(
fadeOutDirection.x * (animProgress / 2 - 0.5), fadeOutDirection.x * (animProgress - 0.5),
fadeOutDirection.y * (animProgress / 2 - 0.5) fadeOutDirection.y * (animProgress - 0.5)
); );
item.drawItemCenteredClipped( item.drawItemCenteredClipped(

View File

@ -4,7 +4,6 @@ import { createLogger } from "../../core/logging";
import { Rectangle } from "../../core/rectangle"; import { Rectangle } from "../../core/rectangle";
import { StaleAreaDetector } from "../../core/stale_area_detector"; import { StaleAreaDetector } from "../../core/stale_area_detector";
import { enumDirection, enumDirectionToVector } from "../../core/vector"; import { enumDirection, enumDirectionToVector } from "../../core/vector";
import { BaseItem } from "../base_item";
import { BeltComponent } from "../components/belt"; import { BeltComponent } from "../components/belt";
import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector"; import { ItemEjectorComponent } from "../components/item_ejector";
@ -139,10 +138,15 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
this.staleAreaDetector.update(); this.staleAreaDetector.update();
// Precompute effective belt speed // 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) { if (G_IS_DEV && globalConfig.debug.instantBelts) {
progressGrowth = 1; progressGrowth = maxProgress;
} }
// Go over all cache entries // Go over all cache entries
@ -159,29 +163,32 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
continue; continue;
} }
// Advance items on the slot // Limit progress here as well
sourceSlot.progress = Math.min( let progressLimit = maxProgress;
1, const destPath = sourceSlot.cachedBeltPath;
sourceSlot.progress + if (destPath) {
progressGrowth * progressLimit += destPath.spacingToFirstItem - globalConfig.itemSpacingOnBelts;
this.root.hubGoals.getBeltBaseSpeed() * }
globalConfig.itemSpacingOnBelts
); if (sourceSlot.progress < progressLimit) {
// Advance items on the slot
sourceSlot.progress += progressGrowth;
}
if (G_IS_DEV && globalConfig.debug.disableEjectorProcessing) { 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 // Check if we are still in the process of ejecting, can't proceed then
if (sourceSlot.progress < 1.0) { if (sourceSlot.progress < maxProgress) {
continue; continue;
} }
// Check if we are ejecting to a belt path const extraProgress = sourceSlot.progress - maxProgress;
const destPath = sourceSlot.cachedBeltPath;
if (destPath) { if (destPath) {
// Try passing the item over // Try passing the item over
if (destPath.tryAcceptItem(item)) { if (destPath.tryAcceptItem(item, extraProgress)) {
sourceSlot.item = null; sourceSlot.item = null;
} }
@ -193,110 +200,17 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
// Check if the target acceptor can actually accept this item // Check if the target acceptor can actually accept this item
const destEntity = sourceSlot.cachedTargetEntity; const destEntity = sourceSlot.cachedTargetEntity;
const destSlot = sourceSlot.cachedDestSlot; const destSlot = sourceSlot.cachedDestSlot;
if (destSlot) { if (destEntity && destSlot) {
const targetAcceptorComp = destEntity.components.ItemAcceptor; const targetAcceptorComp = destEntity.components.ItemAcceptor;
if (!targetAcceptorComp.canAcceptItem(destSlot.index, item)) { if (targetAcceptorComp.tryAcceptItem(destEntity, destSlot.index, item, extraProgress)) {
continue;
}
// Try to hand over the item
if (this.tryPassOverItem(item, destEntity, destSlot.index)) {
// Handover successful, clear slot // Handover successful, clear slot
if (!this.root.app.settings.getAllSettings().simplifiedBelts) {
targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.slot.direction, item);
}
sourceSlot.item = null; 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 {DrawParameters} parameters
* @param {MapChunkView} chunk * @param {MapChunkView} chunk
@ -333,7 +247,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
} }
// Limit the progress to the maximum available space on the next belt (also see #1000) // 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; const nextBeltPath = slot.cachedBeltPath;
if (nextBeltPath) { if (nextBeltPath) {
/* /*
@ -368,20 +282,11 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
^ ^ item @ 0.9 ^ ^ item @ 0.9
^ max progress = 0.3 ^ 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. And that's it! If you summarize the calculations from above into a formula, you get the one below.
*/ */
const maxProgress = const maxProgress =
(0.5 + nextBeltPath.spacingToFirstItem - globalConfig.itemSpacingOnBelts) * 2; 0.5 + nextBeltPath.spacingToFirstItem - globalConfig.itemSpacingOnBelts;
progress = Math.min(maxProgress, progress); progress = Math.min(maxProgress, progress);
} }
@ -399,8 +304,8 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
const realDirection = staticComp.localDirectionToWorld(slot.direction); const realDirection = staticComp.localDirectionToWorld(slot.direction);
const realDirectionVector = enumDirectionToVector[realDirection]; const realDirectionVector = enumDirectionToVector[realDirection];
const tileX = realPosition.x + 0.5 + realDirectionVector.x * 0.5 * progress; const tileX = realPosition.x + 0.5 + realDirectionVector.x * progress;
const tileY = realPosition.y + 0.5 + realDirectionVector.y * 0.5 * progress; const tileY = realPosition.y + 0.5 + realDirectionVector.y * progress;
const worldX = tileX * globalConfig.tileSize; const worldX = tileX * globalConfig.tileSize;
const worldY = tileY * globalConfig.tileSize; const worldY = tileY * globalConfig.tileSize;

View File

@ -1,4 +1,5 @@
import { globalConfig } from "../../core/config"; import { globalConfig } from "../../core/config";
import { ACHIEVEMENTS } from "../../platform/achievement_provider";
import { BaseItem } from "../base_item"; import { BaseItem } from "../base_item";
import { enumColorMixingResults, enumColors } from "../colors"; import { enumColorMixingResults, enumColors } from "../colors";
import { import {
@ -12,16 +13,12 @@ import { isTruthyItem } from "../items/boolean_item";
import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item"; import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { ShapeItem } from "../items/shape_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 * Whole data for a produced item
* *
* @typedef {{ * @typedef {{
* item: BaseItem, * item: BaseItem,
* extraProgress?: number,
* preferredSlot?: number, * preferredSlot?: number,
* requiredSlot?: number, * requiredSlot?: number,
* doNotTrack?: boolean * doNotTrack?: boolean
@ -33,29 +30,19 @@ const MAX_QUEUED_CHARGES = 2;
* @typedef {{ * @typedef {{
* entity: Entity, * entity: Entity,
* items: Map<number, BaseItem>, * items: Map<number, BaseItem>,
* inputCount: number,
* outItems: Array<ProducedItem> * outItems: Array<ProducedItem>
* }} ProcessorImplementationPayload * }} ProcessorImplementationPayload
*/ */
/**
* Type of a processor implementation
* @typedef {{
* entity: Entity,
* item: BaseItem,
* slotIndex: number
* }} ProccessingRequirementsImplementationPayload
*/
/** /**
* @type {Object<string, (ProcessorImplementationPayload) => void>} * @type {Object<string, (ProcessorImplementationPayload) => void>}
*/ */
export const MOD_ITEM_PROCESSOR_HANDLERS = {}; export const MOD_ITEM_PROCESSOR_HANDLERS = {};
/** /**
* @type {Object<string, (ProccessingRequirementsImplementationPayload) => boolean>} * @type {Object<string, ({entity: Entity}) => boolean>}
*/ */
export const MODS_PROCESSING_REQUIREMENTS = {}; export const MODS_CAN_PROCESS = {};
/** /**
* @type {Object<string, ({entity: Entity}) => boolean>} * @type {Object<string, ({entity: Entity}) => boolean>}
@ -101,8 +88,14 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
const processorComp = entity.components.ItemProcessor; const processorComp = entity.components.ItemProcessor;
const ejectorComp = entity.components.ItemEjector; 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) { if (currentCharge) {
// Process next charge // Process next charge
if (currentCharge.remainingTime > 0.0) { if (currentCharge.remainingTime > 0.0) {
@ -122,19 +115,25 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
processorComp.queuedEjects.push(itemsToEject[j]); processorComp.queuedEjects.push(itemsToEject[j]);
} }
processorComp.ongoingCharges.shift(); processorComp.currentCharge = null;
}
} // now that the charge is complete, empty the inputs now
let usedSlots = [];
// Check if we have an empty queue and can start a new charge const acceptorComp = entity.components.ItemAcceptor;
if (processorComp.ongoingCharges.length < MAX_QUEUED_CHARGES) { for (let i = 0; i < acceptorComp.completedInputs.length; i++) {
if (this.canProcess(entity)) { const index = acceptorComp.completedInputs[i].slotIndex;
this.startNewCharge(entity); if (!usedSlots.includes(index)) {
usedSlots.push(index);
acceptorComp.completedInputs.splice(i, 1);
i--;
}
}
} }
} }
// Go over all items and try to eject them
for (let j = 0; j < processorComp.queuedEjects.length; ++j) { 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"); assert(ejectorComp, "To eject items, the building needs to have an ejector");
@ -158,7 +157,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
if (slot !== null) { if (slot !== null) {
// Alright, we can actually eject // Alright, we can actually eject
if (!ejectorComp.tryEject(slot, item)) { if (!ejectorComp.tryEject(slot, item, extraProgress)) {
assert(false, "Failed to eject"); assert(false, "Failed to eject");
} else { } else {
processorComp.queuedEjects.splice(j, 1); processorComp.queuedEjects.splice(j, 1);
@ -169,54 +168,14 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
} }
} }
/** // input requirements are now handled in the item acceptor, which also fits better with what the acceptor is supposed to do
* Returns true if the entity should accept the given item on the given slot.
* This should only be called with matching items! I.e. if a color item is expected
* on the given slot, then only a color item must be passed.
* @param {Entity} entity
* @param {BaseItem} item The item to accept
* @param {number} slotIndex The slot index
* @returns {boolean}
*/
checkRequirements(entity, item, slotIndex) {
const itemProcessorComp = entity.components.ItemProcessor;
const pinsComp = entity.components.WiredPins;
if (MODS_PROCESSING_REQUIREMENTS[itemProcessorComp.processingRequirement]) {
return MODS_PROCESSING_REQUIREMENTS[itemProcessorComp.processingRequirement].bind(this)({
entity,
item,
slotIndex,
});
}
switch (itemProcessorComp.processingRequirement) {
case enumItemProcessorRequirements.painterQuad: {
if (slotIndex === 0) {
// Always accept the shape
return true;
}
// Check the network value at the given slot
const network = pinsComp.slots[slotIndex - 1].linkedNetwork;
const slotIsEnabled = network && network.hasValue() && isTruthyItem(network.currentValue);
if (!slotIsEnabled) {
return false;
}
return true;
}
// By default, everything is accepted
default:
return true;
}
}
/** /**
* Checks whether it's possible to process something * Checks whether it's possible to process something
* @param {Entity} entity * @param {Entity} entity
*/ */
canProcess(entity) { canProcess(entity) {
const acceptorComp = entity.components.ItemAcceptor;
const processorComp = entity.components.ItemProcessor; const processorComp = entity.components.ItemProcessor;
if (MODS_CAN_PROCESS[processorComp.processingRequirement]) { if (MODS_CAN_PROCESS[processorComp.processingRequirement]) {
@ -229,16 +188,35 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
// DEFAULT // DEFAULT
// By default, we can start processing once all inputs are there // By default, we can start processing once all inputs are there
case null: { case null: {
return processorComp.inputCount >= processorComp.inputsPerCharge; // Since each slot might have more than one input, don't check each slot more than once
let usedSlots = [];
for (let i = 0; i < acceptorComp.completedInputs.length; i++) {
const index = acceptorComp.completedInputs[i].slotIndex;
if (!usedSlots.includes(index)) {
usedSlots.push(index);
}
}
return usedSlots.length >= processorComp.inputsPerCharge;
} }
// QUAD PAINTER // QUAD PAINTER
// For the quad painter, it might be possible to start processing earlier // For the quad painter, it might be possible to start processing earlier
case enumItemProcessorRequirements.painterQuad: { case enumItemProcessorRequirements.painterQuad: {
const pinsComp = entity.components.WiredPins; const pinsComp = entity.components.WiredPins;
const inputs = acceptorComp.completedInputs;
// split inputs efficiently
let items = new Map();
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];
if (!items.get(input.slotIndex)) {
items.set(input.slotIndex, input.item);
}
}
// First slot is the shape, so if it's not there we can't do anything // First slot is the shape, so if it's not there we can't do anything
const shapeItem = /** @type {ShapeItem} */ (processorComp.inputSlots.get(0)); const shapeItem = /** @type {ShapeItem} */ (items.get(0));
if (!shapeItem) { if (!shapeItem) {
return false; return false;
} }
@ -267,7 +245,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
// Check if all colors of the enabled slots are there // Check if all colors of the enabled slots are there
for (let i = 0; i < slotStatus.length; ++i) { for (let i = 0; i < slotStatus.length; ++i) {
if (slotStatus[i] && !processorComp.inputSlots.get(1 + i)) { if (slotStatus[i] && !items.get(1 + i)) {
// A slot which is enabled wasn't enabled. Make sure if there is anything on the quadrant, // 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 // it is not possible to paint, but if there is nothing we can ignore it
for (let j = 0; j < 4; ++j) { for (let j = 0; j < 4; ++j) {
@ -278,7 +256,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
} }
} }
} }
return true; return true;
} }
@ -292,10 +269,25 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
* @param {Entity} entity * @param {Entity} entity
*/ */
startNewCharge(entity) { startNewCharge(entity) {
const acceptorComp = entity.components.ItemAcceptor;
const processorComp = entity.components.ItemProcessor; const processorComp = entity.components.ItemProcessor;
// First, take items // First, take inputs - but only one from each
const items = processorComp.inputSlots; const inputs = acceptorComp.completedInputs;
// split inputs efficiently
let items = new Map();
let extraProgress = 0;
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];
if (!items.get(input.slotIndex)) {
items.set(input.slotIndex, input.item);
extraProgress = Math.max(extraProgress, input.extraProgress);
//inputs.splice(i, 1);
//i--;
}
}
/** @type {Array<ProducedItem>} */ /** @type {Array<ProducedItem>} */
const outItems = []; const outItems = [];
@ -309,7 +301,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
entity, entity,
items, items,
outItems, outItems,
inputCount: processorComp.inputCount,
}); });
// Track produced items // Track produced items
@ -317,23 +308,23 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
if (!outItems[i].doNotTrack) { if (!outItems[i].doNotTrack) {
this.root.signals.itemProduced.dispatch(outItems[i].item); this.root.signals.itemProduced.dispatch(outItems[i].item);
} }
// also set extra progress
outItems[i].extraProgress = extraProgress;
} }
// Queue Charge // Queue Charge
const baseSpeed = this.root.hubGoals.getProcessorBaseSpeed(processorComp.type); const originalTime = this.root.hubGoals.getProcessingTime(processorComp.type);
const originalTime = 1 / baseSpeed;
const bonusTimeToApply = Math.min(originalTime, processorComp.bonusTime); const bonusTimeToApply = Math.min(originalTime, processorComp.bonusTime);
const timeToProcess = originalTime - bonusTimeToApply; const timeToProcess = originalTime - bonusTimeToApply;
processorComp.bonusTime -= bonusTimeToApply; processorComp.bonusTime -= bonusTimeToApply;
processorComp.ongoingCharges.push({
processorComp.currentCharge = {
items: outItems, items: outItems,
remainingTime: timeToProcess, remainingTime: timeToProcess,
}); };
processorComp.inputSlots.clear();
processorComp.inputCount = 0;
} }
/** /**
@ -478,7 +469,14 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
* @param {ProcessorImplementationPayload} payload * @param {ProcessorImplementationPayload} payload
*/ */
process_TRASH(payload) { process_TRASH(payload) {
// Do nothing .. // Hardcoded - 4 inputs
for (let i = 0; i < 4; ++i) {
const item = /** @type {ShapeItem} */ (payload.items.get(i));
if (!item) {
continue;
}
payload.entity.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.trash1000, 1);
}
} }
/** /**
@ -602,8 +600,8 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
const hubComponent = payload.entity.components.Hub; const hubComponent = payload.entity.components.Hub;
assert(hubComponent, "Hub item processor has no hub component"); assert(hubComponent, "Hub item processor has no hub component");
// Hardcoded // Hardcoded - 16 inputs
for (let i = 0; i < payload.inputCount; ++i) { for (let i = 0; i < 16; ++i) {
const item = /** @type {ShapeItem} */ (payload.items.get(i)); const item = /** @type {ShapeItem} */ (payload.items.get(i));
if (!item) { if (!item) {
continue; continue;

View File

@ -24,7 +24,9 @@ export class ItemProducerSystem extends GameSystemWithFilter {
} }
this.item = network.currentValue; this.item = network.currentValue;
ejectorComp.tryEject(0, this.item);
// Basically start ejecting at the exit of the ejector. Hacky, but who cares. It works, and its not in the base game :)
ejectorComp.tryEject(0, this.item, 0.5);
} }
} }
} }

View File

@ -1,7 +1,6 @@
import { globalConfig } from "../../core/config"; import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters"; import { DrawParameters } from "../../core/draw_parameters";
import { enumDirectionToVector } from "../../core/vector"; import { enumDirectionToVector } from "../../core/vector";
import { BaseItem } from "../base_item";
import { MinerComponent } from "../components/miner"; import { MinerComponent } from "../components/miner";
import { Entity } from "../entity"; import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter"; import { GameSystemWithFilter } from "../game_system_with_filter";
@ -31,20 +30,18 @@ export class MinerSystem extends GameSystemWithFilter {
} }
update() { update() {
let miningSpeed = this.root.hubGoals.getMinerBaseSpeed(); let progressGrowth = this.root.dynamicTickrate.deltaSeconds * this.root.hubGoals.getMinerBaseSpeed();
const targetProgress = 1;
if (G_IS_DEV && globalConfig.debug.instantMiners) { if (G_IS_DEV && globalConfig.debug.instantMiners) {
miningSpeed *= 100; progressGrowth = targetProgress;
} }
for (let i = 0; i < this.allEntities.length; ++i) { for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i]; const entity = this.allEntities[i];
const minerComp = entity.components.Miner; const minerComp = entity.components.Miner;
// Reset everything on recompute
if (this.needsRecompute) {
minerComp.cachedChainedMiner = null;
}
// Check if miner is above an actual tile // Check if miner is above an actual tile
if (!minerComp.cachedMinedItem) { if (!minerComp.cachedMinedItem) {
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
@ -58,25 +55,64 @@ export class MinerSystem extends GameSystemWithFilter {
minerComp.cachedMinedItem = tileBelow; minerComp.cachedMinedItem = tileBelow;
} }
// First, try to get rid of chained items // Reset everything on recompute
if (minerComp.itemChainBuffer.length > 0) { if (this.needsRecompute) {
if (this.tryPerformMinerEject(entity, minerComp.itemChainBuffer[0])) { minerComp.cachedChainedMiner = null;
minerComp.itemChainBuffer.shift(); minerComp.cachedExitMiner = null;
}
// Check if we are a chained miner
if (minerComp.chainable) {
if (!minerComp.cachedChainedMiner) {
minerComp.cachedChainedMiner = this.findChainedMiner(entity);
}
// don't calculate on the same tick as recompute, or so miners wont have caches yet
if (minerComp.cachedChainedMiner && !minerComp.cachedExitMiner && !this.needsRecompute) {
minerComp.cachedExitMiner = this.findExitMiner(entity);
}
// Check if we now have a target at the end of the chain - if so, that's what we will progress
const exitEntity = minerComp.cachedExitMiner;
if (exitEntity) {
const exitMinerComp = exitEntity.components.Miner;
if (exitMinerComp.progress < targetProgress + 0.5) {
// we can add on some extra progress
exitMinerComp.progress += progressGrowth;
}
continue; continue;
} }
} }
const mineDuration = 1 / miningSpeed; //make sure progress never gets out of control
const timeSinceMine = this.root.time.now() - minerComp.lastMiningTime; minerComp.progress = Math.min(minerComp.progress, targetProgress + 0.5);
if (timeSinceMine > mineDuration) { if (minerComp.progress >= targetProgress) {
// Store how much we overflowed // We can try to eject
const buffer = Math.min(timeSinceMine - mineDuration, this.root.dynamicTickrate.deltaSeconds); const extraProgress = minerComp.progress - targetProgress;
if (this.tryPerformMinerEject(entity, minerComp.cachedMinedItem)) { const ejectorComp = entity.components.ItemEjector;
if (ejectorComp.tryEject(0, minerComp.cachedMinedItem, extraProgress)) {
// Analytics hook // Analytics hook
this.root.signals.itemProduced.dispatch(minerComp.cachedMinedItem); this.root.signals.itemProduced.dispatch(minerComp.cachedMinedItem);
// Store mining time
minerComp.lastMiningTime = this.root.time.now() - buffer; minerComp.progress -= targetProgress;
}
}
if (minerComp.progress < targetProgress) {
minerComp.progress += progressGrowth;
}
if (minerComp.progress >= targetProgress) {
// We can try to eject
const extraProgress = minerComp.progress - targetProgress;
const ejectorComp = entity.components.ItemEjector;
if (ejectorComp.tryEject(0, minerComp.cachedMinedItem, extraProgress)) {
// Analytics hook
this.root.signals.itemProduced.dispatch(minerComp.cachedMinedItem);
minerComp.progress -= targetProgress;
} }
} }
} }
@ -93,6 +129,7 @@ export class MinerSystem extends GameSystemWithFilter {
findChainedMiner(entity) { findChainedMiner(entity) {
const ejectComp = entity.components.ItemEjector; const ejectComp = entity.components.ItemEjector;
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
const minedItem = entity.components.Miner.cachedMinedItem;
const contentsBelow = this.root.map.getLowerLayerContentXY(staticComp.origin.x, staticComp.origin.y); const contentsBelow = this.root.map.getLowerLayerContentXY(staticComp.origin.x, staticComp.origin.y);
if (!contentsBelow) { if (!contentsBelow) {
// This miner has no contents // This miner has no contents
@ -109,7 +146,11 @@ export class MinerSystem extends GameSystemWithFilter {
// Check if we are connected to another miner and thus do not eject directly // Check if we are connected to another miner and thus do not eject directly
if (targetContents) { if (targetContents) {
const targetMinerComp = targetContents.components.Miner; const targetMinerComp = targetContents.components.Miner;
if (targetMinerComp && targetMinerComp.chainable) { if (
targetMinerComp &&
targetMinerComp.chainable &&
targetMinerComp.cachedMinedItem == minedItem
) {
const targetLowerLayer = this.root.map.getLowerLayerContentXY(targetTile.x, targetTile.y); const targetLowerLayer = this.root.map.getLowerLayerContentXY(targetTile.x, targetTile.y);
if (targetLowerLayer) { if (targetLowerLayer) {
return targetContents; return targetContents;
@ -121,39 +162,37 @@ export class MinerSystem extends GameSystemWithFilter {
} }
/** /**
* * Finds the target exit miner for a given entity
* @param {Entity} entity * @param {Entity} entity
* @param {BaseItem} item * @returns {Entity|false} The exit miner entity or null if not found
*/ */
tryPerformMinerEject(entity, item) { findExitMiner(entity) {
const minerComp = entity.components.Miner; const minerComp = entity.components.Miner;
const ejectComp = entity.components.ItemEjector; // Recompute exit miner if we are not at the front
let targetEntity = minerComp.cachedChainedMiner;
// Check if we are a chained miner const ourPosition = entity.components.StaticMapEntity.origin;
if (minerComp.chainable) {
const targetEntity = minerComp.cachedChainedMiner;
// Check if the cache has to get recomputed /** @type {Entity|null|false} */
if (targetEntity === null) { let nextTarget = targetEntity;
minerComp.cachedChainedMiner = this.findChainedMiner(entity); while (nextTarget) {
} targetEntity = nextTarget;
if (targetEntity.components.StaticMapEntity.origin == ourPosition) {
// Check if we now have a target // we are in a loop, do nothing
if (targetEntity) { targetEntity = null;
const targetMinerComp = targetEntity.components.Miner; break;
if (targetMinerComp.tryAcceptChainedItem(item)) {
return true;
} else {
return false;
}
} }
const targetMinerComp = targetEntity.components.Miner;
nextTarget = targetMinerComp.cachedChainedMiner;
} }
// Seems we are a regular miner or at the end of a row, try actually ejecting if (targetEntity) {
if (ejectComp.tryEject(0, item)) { const targetMinerComp = targetEntity.components.Miner;
return true; if (targetMinerComp.cachedMinedItem == minerComp.cachedMinedItem) {
// only chain the same items
return targetEntity;
}
} }
return false; return false;
} }

View File

@ -31,6 +31,9 @@ export class StorageSystem extends GameSystemWithFilter {
const storageComp = entity.components.Storage; const storageComp = entity.components.Storage;
const pinsComp = entity.components.WiredPins; const pinsComp = entity.components.WiredPins;
// storage needs to delete completed inputs, since the items are already added
entity.components.ItemAcceptor.completedInputs = [];
// Eject from storage // Eject from storage
if (storageComp.storedItem && storageComp.storedCount > 0) { if (storageComp.storedItem && storageComp.storedCount > 0) {
const ejectorComp = entity.components.ItemEjector; const ejectorComp = entity.components.ItemEjector;

View File

@ -3,7 +3,6 @@ import { Loader } from "../../core/loader";
import { createLogger } from "../../core/logging"; import { createLogger } from "../../core/logging";
import { Rectangle } from "../../core/rectangle"; import { Rectangle } from "../../core/rectangle";
import { StaleAreaDetector } from "../../core/stale_area_detector"; import { StaleAreaDetector } from "../../core/stale_area_detector";
import { fastArrayDelete } from "../../core/utils";
import { import {
enumAngleToDirection, enumAngleToDirection,
enumDirection, enumDirection,
@ -225,7 +224,11 @@ export class UndergroundBeltSystem extends GameSystemWithFilter {
this.staleAreaWatcher.update(); this.staleAreaWatcher.update();
const sender = enumUndergroundBeltMode.sender; 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) { for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i]; const entity = this.allEntities[i];
@ -233,7 +236,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter {
if (undergroundComp.mode === sender) { if (undergroundComp.mode === sender) {
this.handleSender(entity); this.handleSender(entity);
} else { } 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 // Search in the direction of the tunnel
for ( for (
let searchOffset = 0; let searchOffset = 1;
searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier]; searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier] + 1;
++searchOffset ++searchOffset
) { ) {
currentTile = currentTile.add(searchVector); currentTile = currentTile.add(searchVector);
@ -281,6 +284,8 @@ export class UndergroundBeltSystem extends GameSystemWithFilter {
break; break;
} }
// make sure to link the other way as well
receiverUndergroundComp.cachedLinkedEntity = { entity: null, distance: searchOffset };
return { entity: potentialReceiver, distance: searchOffset }; return { entity: potentialReceiver, distance: searchOffset };
} }
@ -294,6 +299,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter {
*/ */
handleSender(entity) { handleSender(entity) {
const undergroundComp = entity.components.UndergroundBelt; const undergroundComp = entity.components.UndergroundBelt;
const acceptorComp = entity.components.ItemAcceptor;
// Find the current receiver // Find the current receiver
let cacheEntry = undergroundComp.cachedLinkedEntity; let cacheEntry = undergroundComp.cachedLinkedEntity;
@ -307,22 +313,17 @@ export class UndergroundBeltSystem extends GameSystemWithFilter {
return; return;
} }
// Check if we have any items to eject const input = acceptorComp.completedInputs[0];
const nextItemAndDuration = undergroundComp.pendingItems[0]; if (input) {
if (nextItemAndDuration) {
assert(undergroundComp.pendingItems.length === 1, "more than 1 pending");
// Check if the receiver can accept it // Check if the receiver can accept it
if ( if (
cacheEntry.entity.components.UndergroundBelt.tryAcceptTunneledItem( cacheEntry.entity.components.UndergroundBelt.tryAcceptTunneledItem(
nextItemAndDuration[0], input.item,
cacheEntry.distance, cacheEntry.distance,
this.root.hubGoals.getUndergroundBeltBaseSpeed(), input.extraProgress
this.root.time.now()
) )
) { ) {
// Drop this item acceptorComp.completedInputs.shift();
fastArrayDelete(undergroundComp.pendingItems, 0);
} }
} }
} }
@ -330,20 +331,28 @@ export class UndergroundBeltSystem extends GameSystemWithFilter {
/** /**
* *
* @param {Entity} entity * @param {Entity} entity
* @param {number} now * @param {number} progressGrowth
*/ */
handleReceiver(entity, now) { handleReceiver(entity, progressGrowth) {
const undergroundComp = entity.components.UndergroundBelt; const undergroundComp = entity.components.UndergroundBelt;
// Try to eject items, we only check the first one because it is sorted by remaining time if (!undergroundComp.cachedLinkedEntity) return;
const nextItemAndDuration = undergroundComp.pendingItems[0]; const distance = undergroundComp.cachedLinkedEntity.distance;
if (nextItemAndDuration) {
if (now > nextItemAndDuration[1]) { // 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 ejectorComp = entity.components.ItemEjector;
const nextSlotIndex = ejectorComp.getFirstFreeSlot(); const nextSlotIndex = ejectorComp.getFirstFreeSlot();
if (nextSlotIndex !== null) { if (nextSlotIndex !== null) {
if (ejectorComp.tryEject(nextSlotIndex, nextItemAndDuration[0])) { const extraProgress = itemAndProgress[1] - distance;
if (ejectorComp.tryEject(nextSlotIndex, itemAndProgress[0], extraProgress)) {
undergroundComp.pendingItems.shift(); undergroundComp.pendingItems.shift();
} }
} }

View File

@ -16,6 +16,7 @@ import { SavegameInterface_V1008 } from "./schemas/1008";
import { SavegameInterface_V1009 } from "./schemas/1009"; import { SavegameInterface_V1009 } from "./schemas/1009";
import { MODS } from "../mods/modloader"; import { MODS } from "../mods/modloader";
import { SavegameInterface_V1010 } from "./schemas/1010"; import { SavegameInterface_V1010 } from "./schemas/1010";
import { SavegameInterface_V1011 } from "./schemas/1011";
const logger = createLogger("savegame"); const logger = createLogger("savegame");
@ -56,7 +57,7 @@ export class Savegame extends ReadWriteProxy {
* @returns {number} * @returns {number}
*/ */
static getCurrentVersion() { static getCurrentVersion() {
return 1010; return 1011;
} }
/** /**
@ -168,6 +169,11 @@ export class Savegame extends ReadWriteProxy {
data.version = 1010; data.version = 1010;
} }
if (data.version === 1010) {
SavegameInterface_V1011.migrate1010to1011(data);
data.version = 1011;
}
return ExplainedResult.good(); return ExplainedResult.good();
} }

View File

@ -11,6 +11,7 @@ import { SavegameInterface_V1007 } from "./schemas/1007";
import { SavegameInterface_V1008 } from "./schemas/1008"; import { SavegameInterface_V1008 } from "./schemas/1008";
import { SavegameInterface_V1009 } from "./schemas/1009"; import { SavegameInterface_V1009 } from "./schemas/1009";
import { SavegameInterface_V1010 } from "./schemas/1010"; import { SavegameInterface_V1010 } from "./schemas/1010";
import { SavegameInterface_V1011 } from "./schemas/1011";
/** @type {Object.<number, typeof BaseSavegameInterface>} */ /** @type {Object.<number, typeof BaseSavegameInterface>} */
export const savegameInterfaces = { export const savegameInterfaces = {
@ -25,6 +26,7 @@ export const savegameInterfaces = {
1008: SavegameInterface_V1008, 1008: SavegameInterface_V1008,
1009: SavegameInterface_V1009, 1009: SavegameInterface_V1009,
1010: SavegameInterface_V1010, 1010: SavegameInterface_V1010,
1011: SavegameInterface_V1011,
}; };
const logger = createLogger("savegame_interface_registry"); const logger = createLogger("savegame_interface_registry");

View File

@ -0,0 +1,44 @@
import { createLogger } from "../../core/logging.js";
import { ItemProcessorComponent } from "../../game/components/item_processor.js";
import { MinerComponent } from "../../game/components/miner.js";
import { Entity } from "../../game/entity.js";
import { SavegameInterface_V1010 } from "./1010.js";
const schema = require("./1011.json");
const logger = createLogger("savegame_interface/1011");
export class SavegameInterface_V1011 extends SavegameInterface_V1010 {
getVersion() {
return 1011;
}
getSchemaUncached() {
return schema;
}
/**
* @param {import("../savegame_typedefs.js").SavegameData} data
*/
static migrate1010to1011(data) {
logger.log("Migrating 1010 to 1011");
const dump = data.dump;
if (!dump) {
return true;
}
/** @type {Array<Entity} */
const entities = dump.entities;
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
const minerComp = entity.components.Miner;
if (minerComp) {
minerComp.progress = 0;
}
const processorComp = entity.components.ItemProcessor;
if (processorComp) {
processorComp.currentCharge = null;
}
}
}
}

View File

@ -0,0 +1,5 @@
{
"type": "object",
"required": [],
"additionalProperties": true
}