mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-06-13 13:04:03 +00:00
Merge bf17ccf202
into 4e5e5c8ef7
This commit is contained in:
commit
0f022f54cf
@ -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
|
||||
|
@ -119,29 +119,18 @@ export class BeltPath extends BasicSerializableObject {
|
||||
this.numCompressedItemsAfterFirstItem = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this path can accept a new item
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canAcceptItem() {
|
||||
return this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to accept the item
|
||||
* @param {BaseItem} item
|
||||
* @param {number} extraProgress
|
||||
*/
|
||||
tryAcceptItem(item) {
|
||||
tryAcceptItem(item, extraProgress = 0) {
|
||||
if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) {
|
||||
// So, since we already need one tick to accept this item we will add this directly.
|
||||
const beltProgressPerTick =
|
||||
this.root.hubGoals.getBeltBaseSpeed() *
|
||||
this.root.dynamicTickrate.deltaSeconds *
|
||||
globalConfig.itemSpacingOnBelts;
|
||||
|
||||
// First, compute how much progress we can make *at max*
|
||||
const maxProgress = Math.max(0, this.spacingToFirstItem - globalConfig.itemSpacingOnBelts);
|
||||
const initialProgress = Math.min(maxProgress, beltProgressPerTick);
|
||||
const initialProgress = Math.min(maxProgress, extraProgress);
|
||||
|
||||
this.items.unshift([this.spacingToFirstItem - initialProgress, item]);
|
||||
this.spacingToFirstItem = initialProgress;
|
||||
@ -227,8 +216,6 @@ export class BeltPath extends BasicSerializableObject {
|
||||
return;
|
||||
}
|
||||
|
||||
const noSimplifiedBelts = !this.root.app.settings.getAllSettings().simplifiedBelts;
|
||||
|
||||
DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid);
|
||||
const targetStaticComp = targetEntity.components.StaticMapEntity;
|
||||
const targetBeltComp = targetEntity.components.Belt;
|
||||
@ -274,95 +261,15 @@ export class BeltPath extends BasicSerializableObject {
|
||||
}
|
||||
|
||||
const matchingSlotIndex = matchingSlot.index;
|
||||
const passOver = this.computePassOverFunctionWithoutBelts(targetEntity, matchingSlotIndex);
|
||||
if (!passOver) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingDirection = enumInvertedDirections[ejectingDirection];
|
||||
const filter = matchingSlot.slot.filter;
|
||||
|
||||
return function (item, remainingProgress = 0.0) {
|
||||
// Check if the acceptor has a filter
|
||||
if (filter && item._type !== filter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to pass over
|
||||
if (passOver(item, matchingSlotIndex)) {
|
||||
// Trigger animation on the acceptor comp
|
||||
if (noSimplifiedBelts) {
|
||||
targetAcceptorComp.onItemAccepted(
|
||||
matchingSlotIndex,
|
||||
matchingDirection,
|
||||
item,
|
||||
remainingProgress
|
||||
);
|
||||
}
|
||||
return function (item, startProgress = 0.0) {
|
||||
if (targetAcceptorComp.tryAcceptItem(targetEntity, matchingSlotIndex, item, startProgress)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a method to pass over the item to the entity
|
||||
* @param {Entity} entity
|
||||
* @param {number} matchingSlotIndex
|
||||
* @returns {(item: BaseItem, slotIndex: number) => boolean | void}
|
||||
*/
|
||||
computePassOverFunctionWithoutBelts(entity, matchingSlotIndex) {
|
||||
const systems = this.root.systemMgr.systems;
|
||||
const hubGoals = this.root.hubGoals;
|
||||
|
||||
// NOTICE: THIS IS COPIED FROM THE ITEM EJECTOR SYSTEM FOR PEROFMANCE REASONS
|
||||
|
||||
const itemProcessorComp = entity.components.ItemProcessor;
|
||||
if (itemProcessorComp) {
|
||||
// Its an item processor ..
|
||||
return function (item) {
|
||||
// Check for potential filters
|
||||
if (!systems.itemProcessor.checkRequirements(entity, item, matchingSlotIndex)) {
|
||||
return;
|
||||
}
|
||||
return itemProcessorComp.tryTakeItem(item, matchingSlotIndex);
|
||||
};
|
||||
}
|
||||
|
||||
const undergroundBeltComp = entity.components.UndergroundBelt;
|
||||
if (undergroundBeltComp) {
|
||||
// Its an underground belt. yay.
|
||||
return function (item) {
|
||||
return undergroundBeltComp.tryAcceptExternalItem(
|
||||
item,
|
||||
hubGoals.getUndergroundBeltBaseSpeed()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const storageComp = entity.components.Storage;
|
||||
if (storageComp) {
|
||||
// It's a storage
|
||||
return function (item) {
|
||||
if (storageComp.canAcceptItem(item)) {
|
||||
storageComp.takeItem(item);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const filterComp = entity.components.Filter;
|
||||
if (filterComp) {
|
||||
// It's a filter! Unfortunately the filter has to know a lot about it's
|
||||
// surrounding state and components, so it can't be within the component itself.
|
||||
return function (item) {
|
||||
if (systems.filter.tryAcceptItem(entity, matchingSlotIndex, item)) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Following code will be compiled out outside of dev versions
|
||||
/* dev:start */
|
||||
|
||||
|
@ -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)]];
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { formatItemsPerSecond } from "../../core/utils";
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { T } from "../../translations";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { enumInputRequirements, ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import {
|
||||
enumItemProcessorTypes,
|
||||
@ -274,6 +274,7 @@ export class MetaPainterBuilding extends MetaBuilding {
|
||||
filter: "color",
|
||||
},
|
||||
]);
|
||||
entity.components.ItemAcceptor.inputRequirement = enumInputRequirements.quadPainter;
|
||||
|
||||
entity.components.ItemEjector.setSlots([
|
||||
{ pos: new Vector(0, 0), direction: enumDirection.top },
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { formatBigNumber } from "../../core/utils";
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { T } from "../../translations";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { enumInputRequirements, ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { StorageComponent } from "../components/storage";
|
||||
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
@ -81,6 +81,7 @@ export class MetaStorageBuilding extends MetaBuilding {
|
||||
direction: enumDirection.bottom,
|
||||
},
|
||||
],
|
||||
inputRequirement: enumInputRequirements.storage,
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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: [],
|
||||
|
@ -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
|
||||
})
|
||||
),
|
||||
};
|
||||
|
@ -2,6 +2,10 @@ import { enumDirection, enumInvertedDirections, Vector } from "../../core/vector
|
||||
import { types } from "../../savegame/serialization";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { Component } from "../component";
|
||||
import { Entity } from "../entity";
|
||||
import { isTruthyItem } from "../items/boolean_item";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
import { GameRoot } from "../root";
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
@ -24,34 +28,85 @@ import { Component } from "../component";
|
||||
* filter?: ItemType
|
||||
* }} ItemAcceptorSlotConfig */
|
||||
|
||||
/**
|
||||
* @typedef {Array<{
|
||||
* slotIndex: number,
|
||||
* item: BaseItem,
|
||||
* animProgress: number,
|
||||
* }>} ItemAcceptorInputs
|
||||
*
|
||||
* @typedef {Array<{
|
||||
* slotIndex: number,
|
||||
* item: BaseItem,
|
||||
* extraProgress: number
|
||||
* }>} ItemAcceptorCompletedInputs
|
||||
*
|
||||
* @typedef {{
|
||||
* root: GameRoot,
|
||||
* entity: Entity,
|
||||
* item: BaseItem,
|
||||
* slotIndex: number,
|
||||
* extraProgress: number
|
||||
* }} InputCompletedArgs
|
||||
*/
|
||||
|
||||
/** @enum {string} */
|
||||
export const enumInputRequirements = {
|
||||
quadPainter: "quadPainter",
|
||||
storage: "storage",
|
||||
};
|
||||
|
||||
export const MOD_INPUT_REQUIREMENTS = [];
|
||||
|
||||
export class ItemAcceptorComponent extends Component {
|
||||
static getId() {
|
||||
return "ItemAcceptor";
|
||||
}
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
inputs: types.array(
|
||||
types.structured({
|
||||
slotIndex: types.uint,
|
||||
item: typeItemSingleton,
|
||||
animProgress: types.ufloat,
|
||||
})
|
||||
),
|
||||
completedInputs: types.array(
|
||||
types.structured({
|
||||
slotIndex: types.uint,
|
||||
item: typeItemSingleton,
|
||||
extraProgress: types.ufloat,
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} param0
|
||||
* @param {Array<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();
|
||||
|
||||
this.setSlots(slots);
|
||||
|
||||
this.inputRequirement = inputRequirement;
|
||||
|
||||
// setting this to 1 will cause throughput issues at very high speeds
|
||||
this.maxSlotInputs = maxSlotInputs;
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
/**
|
||||
* Fixes belt animations
|
||||
* @type {Array<{
|
||||
* item: BaseItem,
|
||||
* slotIndex: number,
|
||||
* animProgress: number,
|
||||
* direction: enumDirection
|
||||
* }>}
|
||||
*/
|
||||
this.itemConsumptionAnimations = [];
|
||||
/** @type {ItemAcceptorInputs} */
|
||||
this.inputs = [];
|
||||
/** @type {ItemAcceptorCompletedInputs} */
|
||||
this.completedInputs = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -74,31 +129,107 @@ export class ItemAcceptorComponent extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this acceptor can accept a new item at slot N
|
||||
*
|
||||
* NOTICE: The belt path ignores this for performance reasons and does his own check
|
||||
* @param {Entity} entity
|
||||
* @param {BaseItem} item
|
||||
* @param {number} slotIndex
|
||||
* @param {BaseItem=} item
|
||||
* @returns
|
||||
*/
|
||||
canAcceptItem(slotIndex, item) {
|
||||
canAcceptItem(entity, item, slotIndex) {
|
||||
const slot = this.slots[slotIndex];
|
||||
return !slot.filter || slot.filter === item.getItemType();
|
||||
|
||||
// make sure there is a slot and we match the filter
|
||||
if (slot && !(slot.filter && slot.filter != item.getItemType())) {
|
||||
if (MOD_INPUT_REQUIREMENTS[this.inputRequirement]) {
|
||||
return MOD_INPUT_REQUIREMENTS[this.inputRequirement].call(this, {
|
||||
entity,
|
||||
item,
|
||||
slotIndex,
|
||||
});
|
||||
}
|
||||
|
||||
switch (this.inputRequirement) {
|
||||
case null: {
|
||||
return true;
|
||||
}
|
||||
case enumInputRequirements.quadPainter: {
|
||||
const pinsComp = entity.components.WiredPins;
|
||||
|
||||
if (slotIndex === 0) {
|
||||
// Always accept the shape
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check the network value at the given slot
|
||||
const network = pinsComp.slots[slotIndex - 1].linkedNetwork;
|
||||
const slotIsEnabled = network && network.hasValue() && isTruthyItem(network.currentValue);
|
||||
if (!slotIsEnabled) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case enumInputRequirements.storage: {
|
||||
const storageComp = entity.components.Storage;
|
||||
|
||||
if (storageComp.storedCount >= storageComp.maximumStorage) {
|
||||
return false;
|
||||
}
|
||||
const itemType = item.getItemType();
|
||||
if (storageComp.storedItem && itemType !== storageComp.storedItem.getItemType()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// set the item straight away - this way different kinds of items can't be inq the acceptor
|
||||
storageComp.storedItem = item;
|
||||
storageComp.storedCount++;
|
||||
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
assertAlways(false, "Input requirement is not recognised: " + this.inputRequirement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item has been accepted so that
|
||||
* Called when trying to input a new item
|
||||
* @param {Entity} entity
|
||||
* @param {number} slotIndex
|
||||
* @param {enumDirection} direction
|
||||
* @param {BaseItem} item
|
||||
* @param {number} remainingProgress World space remaining progress, can be set to set the start position of the item
|
||||
* @param {number} startProgress World space remaining progress, can be set to set the start position of the item
|
||||
* @returns {boolean} if the input was succesful
|
||||
*/
|
||||
onItemAccepted(slotIndex, direction, item, remainingProgress = 0.0) {
|
||||
this.itemConsumptionAnimations.push({
|
||||
item,
|
||||
tryAcceptItem(entity, slotIndex, item, startProgress = 0.0) {
|
||||
// make sure we have space to actually accept
|
||||
let existingInputs = 0;
|
||||
for (let i = 0; i < this.inputs.length; i++) {
|
||||
if (this.inputs[i].slotIndex == slotIndex) {
|
||||
existingInputs++;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < this.completedInputs.length; i++) {
|
||||
if (this.completedInputs[i].slotIndex == slotIndex) {
|
||||
existingInputs++;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingInputs >= this.maxSlotInputs) {
|
||||
return false;
|
||||
}
|
||||
if (!this.canAcceptItem(entity, item, slotIndex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the start progress is bigger than 0.5, the remainder should get passed on to the ejector
|
||||
this.inputs.push({
|
||||
slotIndex,
|
||||
direction,
|
||||
animProgress: Math.min(1, remainingProgress * 2),
|
||||
item,
|
||||
animProgress: startProgress,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { types } from "../../savegame/serialization";
|
||||
import { TypeString } from "../../savegame/serialization_data_types";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { Component } from "../component";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
|
||||
/** @enum {string} */
|
||||
export const enumItemProcessorTypes = {
|
||||
@ -29,6 +31,7 @@ export const enumItemProcessorRequirements = {
|
||||
|
||||
/** @typedef {{
|
||||
* item: BaseItem,
|
||||
* extraProgress?: number
|
||||
* requiredSlot?: number,
|
||||
* preferredSlot?: number
|
||||
* }} EjectorItemToEject */
|
||||
@ -38,6 +41,13 @@ export const enumItemProcessorRequirements = {
|
||||
* items: Array<EjectorItemToEject>,
|
||||
* }} EjectorCharge */
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* item: BaseItem
|
||||
* extraProgress: number
|
||||
* }} ItemProcessorInput
|
||||
*/
|
||||
|
||||
export class ItemProcessorComponent extends Component {
|
||||
static getId() {
|
||||
return "ItemProcessor";
|
||||
@ -46,6 +56,19 @@ export class ItemProcessorComponent extends Component {
|
||||
static getSchema() {
|
||||
return {
|
||||
nextOutputSlot: types.uint,
|
||||
currentCharge: types.nullable(
|
||||
types.structured({
|
||||
remainingTime: types.ufloat,
|
||||
items: types.array(
|
||||
types.structured({
|
||||
item: typeItemSingleton,
|
||||
extraProgress: types.nullable(types.float),
|
||||
requiredSlot: types.nullable(types.uint),
|
||||
preferredSlot: types.nullable(types.uint),
|
||||
})
|
||||
),
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@ -73,12 +96,6 @@ export class ItemProcessorComponent extends Component {
|
||||
// Type of processing requirement
|
||||
this.processingRequirement = processingRequirement;
|
||||
|
||||
/**
|
||||
* Our current inputs
|
||||
* @type {Map<number, BaseItem>}
|
||||
*/
|
||||
this.inputSlots = new Map();
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
@ -88,21 +105,13 @@ export class ItemProcessorComponent extends Component {
|
||||
// sure the outputs always match
|
||||
this.nextOutputSlot = 0;
|
||||
|
||||
this.inputSlots.clear();
|
||||
|
||||
/**
|
||||
* Current input count
|
||||
* @type {number}
|
||||
*/
|
||||
this.inputCount = 0;
|
||||
|
||||
/**
|
||||
* What we are currently processing, empty if we don't produce anything rn
|
||||
* requiredSlot: Item *must* be ejected on this slot
|
||||
* preferredSlot: Item *can* be ejected on this slot, but others are fine too if the one is not usable
|
||||
* @type {Array<EjectorCharge>}
|
||||
* @type {EjectorCharge|null}
|
||||
*/
|
||||
this.ongoingCharges = [];
|
||||
this.currentCharge = null;
|
||||
|
||||
/**
|
||||
* How much processing time we have left from the last tick
|
||||
@ -115,30 +124,4 @@ export class ItemProcessorComponent extends Component {
|
||||
*/
|
||||
this.queuedEjects = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to take the item
|
||||
* @param {BaseItem} item
|
||||
* @param {number} sourceSlot
|
||||
*/
|
||||
tryTakeItem(item, sourceSlot) {
|
||||
if (
|
||||
this.type === enumItemProcessorTypes.hub ||
|
||||
this.type === enumItemProcessorTypes.trash ||
|
||||
this.type === enumItemProcessorTypes.goal
|
||||
) {
|
||||
// Hub has special logic .. not really nice but efficient.
|
||||
this.inputSlots.set(this.inputCount, item);
|
||||
this.inputCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check that we only take one item per slot
|
||||
if (this.inputSlots.has(sourceSlot)) {
|
||||
return false;
|
||||
}
|
||||
this.inputSlots.set(sourceSlot, item);
|
||||
this.inputCount++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,13 @@ import { types } from "../../savegame/serialization";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { Component } from "../component";
|
||||
import { Entity } from "../entity";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
|
||||
const chainBufferSize = 6;
|
||||
/**
|
||||
* @typedef {{
|
||||
* item: BaseItem,
|
||||
* extraProgress?: number,
|
||||
* }} MinerItem
|
||||
*/
|
||||
|
||||
export class MinerComponent extends Component {
|
||||
static getId() {
|
||||
@ -14,17 +18,17 @@ export class MinerComponent extends Component {
|
||||
static getSchema() {
|
||||
// cachedMinedItem is not serialized.
|
||||
return {
|
||||
lastMiningTime: types.ufloat,
|
||||
itemChainBuffer: types.array(typeItemSingleton),
|
||||
progress: types.ufloat,
|
||||
};
|
||||
}
|
||||
|
||||
constructor({ chainable = false }) {
|
||||
super();
|
||||
this.lastMiningTime = 0;
|
||||
this.progress = 0;
|
||||
this.chainable = chainable;
|
||||
|
||||
/**
|
||||
* The item we are mining beneath us
|
||||
* @type {BaseItem}
|
||||
*/
|
||||
this.cachedMinedItem = null;
|
||||
@ -35,30 +39,11 @@ export class MinerComponent extends Component {
|
||||
* @type {Entity|null|false}
|
||||
*/
|
||||
this.cachedChainedMiner = null;
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
/**
|
||||
* Stores items from other miners which were chained to this
|
||||
* miner.
|
||||
* @type {Array<BaseItem>}
|
||||
* The miner at the end of the chain, which actually ejects the items
|
||||
* If the value is false, it means there is no entity, and we don't have to re-check
|
||||
* @type {Entity|null|false}
|
||||
*/
|
||||
this.itemChainBuffer = [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {BaseItem} item
|
||||
*/
|
||||
tryAcceptChainedItem(item) {
|
||||
if (this.itemChainBuffer.length > chainBufferSize) {
|
||||
// Well, this one is full
|
||||
return false;
|
||||
}
|
||||
|
||||
this.itemChainBuffer.push(item);
|
||||
return true;
|
||||
this.cachedExitMiner = null;
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,7 @@ import { types } from "../../savegame/serialization";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { Component } from "../component";
|
||||
import { typeItemSingleton } from "../item_resolver";
|
||||
import { ColorItem } from "../items/color_item";
|
||||
import { ShapeItem } from "../items/shape_item";
|
||||
|
||||
/** @type {{
|
||||
* [x: string]: (item: BaseItem) => Boolean
|
||||
* }} */
|
||||
export const MODS_ADDITIONAL_STORAGE_ITEM_RESOLVER = {};
|
||||
export class StorageComponent extends Component {
|
||||
static getId() {
|
||||
return "Storage";
|
||||
@ -46,42 +40,6 @@ export class StorageComponent extends Component {
|
||||
this.overlayOpacity = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this storage can accept the item
|
||||
* @param {BaseItem} item
|
||||
*/
|
||||
canAcceptItem(item) {
|
||||
if (this.storedCount >= this.maximumStorage) {
|
||||
return false;
|
||||
}
|
||||
if (!this.storedItem || this.storedCount === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const itemType = item.getItemType();
|
||||
|
||||
if (itemType !== this.storedItem.getItemType()) {
|
||||
// Check type matches
|
||||
return false;
|
||||
}
|
||||
|
||||
if (MODS_ADDITIONAL_STORAGE_ITEM_RESOLVER[itemType]) {
|
||||
return MODS_ADDITIONAL_STORAGE_ITEM_RESOLVER[itemType].apply(this, [item]);
|
||||
}
|
||||
|
||||
if (itemType === "color") {
|
||||
return /** @type {ColorItem} */ (this.storedItem).color === /** @type {ColorItem} */ (item).color;
|
||||
}
|
||||
|
||||
if (itemType === "shape") {
|
||||
return (
|
||||
/** @type {ShapeItem} */ (this.storedItem).definition.getHash() ===
|
||||
/** @type {ShapeItem} */ (item).definition.getHash()
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the storage is full
|
||||
* @returns {boolean}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -150,11 +150,13 @@ export class GameSystemManager {
|
||||
|
||||
add("belt", BeltSystem);
|
||||
|
||||
add("undergroundBelt", UndergroundBeltSystem);
|
||||
add("storage", StorageSystem);
|
||||
|
||||
add("itemEjector", ItemEjectorSystem);
|
||||
|
||||
add("miner", MinerSystem);
|
||||
|
||||
add("storage", StorageSystem);
|
||||
add("undergroundBelt", UndergroundBeltSystem);
|
||||
|
||||
add("itemProcessor", ItemProcessorSystem);
|
||||
|
||||
@ -162,8 +164,6 @@ export class GameSystemManager {
|
||||
|
||||
add("itemProducer", ItemProducerSystem);
|
||||
|
||||
add("itemEjector", ItemEjectorSystem);
|
||||
|
||||
if (this.root.gameMode.hasResources()) {
|
||||
add("mapResources", MapResourcesSystem);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -303,11 +303,8 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
|
||||
const mouseTile = worldPos.toTileSpace();
|
||||
|
||||
// Compute best rotation variant
|
||||
const {
|
||||
rotation,
|
||||
rotationVariant,
|
||||
connectedEntities,
|
||||
} = metaBuilding.computeOptimalDirectionAndRotationVariantAtTile({
|
||||
const { rotation, rotationVariant, connectedEntities } =
|
||||
metaBuilding.computeOptimalDirectionAndRotationVariantAtTile({
|
||||
root: this.root,
|
||||
tile: mouseTile,
|
||||
rotation: this.currentBaseRotation,
|
||||
@ -657,8 +654,16 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic {
|
||||
// Connected to a belt
|
||||
isConnected = true;
|
||||
} else if (minerComp && minerComp.chainable && destMiner && destMiner.chainable) {
|
||||
const worldTile = staticComp.localTileToWorld(slot.pos);
|
||||
if (
|
||||
this.root.map.getLowerLayerContentXY(worldTile.x, worldTile.y) ==
|
||||
destMiner.cachedMinedItem
|
||||
) {
|
||||
// Chainable miners connected to eachother
|
||||
isConnected = true;
|
||||
} else {
|
||||
isBlocked = true;
|
||||
}
|
||||
} else {
|
||||
// This one is blocked
|
||||
isBlocked = true;
|
||||
|
@ -134,6 +134,7 @@ export class HUDMinerHighlight extends BaseHUDPart {
|
||||
findConnectedMiners(entity, seenUids = new Set()) {
|
||||
let results = [];
|
||||
const origin = entity.components.StaticMapEntity.origin;
|
||||
const originMinerComp = entity.components.Miner;
|
||||
|
||||
if (!seenUids.has(entity.uid)) {
|
||||
seenUids.add(entity.uid);
|
||||
@ -157,7 +158,11 @@ export class HUDMinerHighlight extends BaseHUDPart {
|
||||
);
|
||||
if (contents) {
|
||||
const minerComp = contents.components.Miner;
|
||||
if (minerComp && minerComp.chainable) {
|
||||
if (
|
||||
minerComp &&
|
||||
minerComp.chainable &&
|
||||
originMinerComp.cachedMinedItem == minerComp.cachedMinedItem
|
||||
) {
|
||||
// Found a miner connected to this entity
|
||||
if (!seenUids.has(contents.uid)) {
|
||||
if (this.root.systemMgr.systems.miner.findChainedMiner(contents) === entity) {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { FilterComponent } from "../components/filter";
|
||||
import { Entity } from "../entity";
|
||||
@ -13,29 +12,25 @@ 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();
|
||||
}
|
||||
@ -43,15 +38,14 @@ 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;
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -4,7 +4,6 @@ import { createLogger } from "../../core/logging";
|
||||
import { Rectangle } from "../../core/rectangle";
|
||||
import { StaleAreaDetector } from "../../core/stale_area_detector";
|
||||
import { enumDirection, enumDirectionToVector } from "../../core/vector";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { BeltComponent } from "../components/belt";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
@ -139,10 +138,15 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
this.staleAreaDetector.update();
|
||||
|
||||
// Precompute effective belt speed
|
||||
let progressGrowth = 2 * this.root.dynamicTickrate.deltaSeconds;
|
||||
let progressGrowth =
|
||||
this.root.dynamicTickrate.deltaSeconds *
|
||||
this.root.hubGoals.getBeltBaseSpeed() *
|
||||
globalConfig.itemSpacingOnBelts;
|
||||
// it's only half a belt
|
||||
const maxProgress = 0.5;
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.instantBelts) {
|
||||
progressGrowth = 1;
|
||||
progressGrowth = maxProgress;
|
||||
}
|
||||
|
||||
// Go over all cache entries
|
||||
@ -159,29 +163,32 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Limit progress here as well
|
||||
let progressLimit = maxProgress;
|
||||
const destPath = sourceSlot.cachedBeltPath;
|
||||
if (destPath) {
|
||||
progressLimit += destPath.spacingToFirstItem - globalConfig.itemSpacingOnBelts;
|
||||
}
|
||||
|
||||
if (sourceSlot.progress < progressLimit) {
|
||||
// Advance items on the slot
|
||||
sourceSlot.progress = Math.min(
|
||||
1,
|
||||
sourceSlot.progress +
|
||||
progressGrowth *
|
||||
this.root.hubGoals.getBeltBaseSpeed() *
|
||||
globalConfig.itemSpacingOnBelts
|
||||
);
|
||||
sourceSlot.progress += progressGrowth;
|
||||
}
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.disableEjectorProcessing) {
|
||||
sourceSlot.progress = 1.0;
|
||||
sourceSlot.progress = maxProgress;
|
||||
}
|
||||
|
||||
// Check if we are still in the process of ejecting, can't proceed then
|
||||
if (sourceSlot.progress < 1.0) {
|
||||
if (sourceSlot.progress < maxProgress) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we are ejecting to a belt path
|
||||
const destPath = sourceSlot.cachedBeltPath;
|
||||
const extraProgress = sourceSlot.progress - maxProgress;
|
||||
|
||||
if (destPath) {
|
||||
// Try passing the item over
|
||||
if (destPath.tryAcceptItem(item)) {
|
||||
if (destPath.tryAcceptItem(item, extraProgress)) {
|
||||
sourceSlot.item = null;
|
||||
}
|
||||
|
||||
@ -193,110 +200,17 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
// Check if the target acceptor can actually accept this item
|
||||
const destEntity = sourceSlot.cachedTargetEntity;
|
||||
const destSlot = sourceSlot.cachedDestSlot;
|
||||
if (destSlot) {
|
||||
if (destEntity && destSlot) {
|
||||
const targetAcceptorComp = destEntity.components.ItemAcceptor;
|
||||
if (!targetAcceptorComp.canAcceptItem(destSlot.index, item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to hand over the item
|
||||
if (this.tryPassOverItem(item, destEntity, destSlot.index)) {
|
||||
if (targetAcceptorComp.tryAcceptItem(destEntity, destSlot.index, item, extraProgress)) {
|
||||
// Handover successful, clear slot
|
||||
if (!this.root.app.settings.getAllSettings().simplifiedBelts) {
|
||||
targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.slot.direction, item);
|
||||
}
|
||||
sourceSlot.item = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {BaseItem} item
|
||||
* @param {Entity} receiver
|
||||
* @param {number} slotIndex
|
||||
*/
|
||||
tryPassOverItem(item, receiver, slotIndex) {
|
||||
// Try figuring out how what to do with the item
|
||||
// @TODO: Kinda hacky. How to solve this properly? Don't want to go through inheritance hell.
|
||||
|
||||
const beltComp = receiver.components.Belt;
|
||||
if (beltComp) {
|
||||
const path = beltComp.assignedPath;
|
||||
assert(path, "belt has no path");
|
||||
if (path.tryAcceptItem(item)) {
|
||||
return true;
|
||||
}
|
||||
// Belt can have nothing else
|
||||
return false;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// NOTICE ! THIS CODE IS DUPLICATED IN THE BELT PATH FOR PERFORMANCE REASONS
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const itemProcessorComp = receiver.components.ItemProcessor;
|
||||
if (itemProcessorComp) {
|
||||
// Check for potential filters
|
||||
if (!this.root.systemMgr.systems.itemProcessor.checkRequirements(receiver, item, slotIndex)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Its an item processor ..
|
||||
if (itemProcessorComp.tryTakeItem(item, slotIndex)) {
|
||||
return true;
|
||||
}
|
||||
// Item processor can have nothing else
|
||||
return false;
|
||||
}
|
||||
|
||||
const undergroundBeltComp = receiver.components.UndergroundBelt;
|
||||
if (undergroundBeltComp) {
|
||||
// Its an underground belt. yay.
|
||||
if (
|
||||
undergroundBeltComp.tryAcceptExternalItem(
|
||||
item,
|
||||
this.root.hubGoals.getUndergroundBeltBaseSpeed()
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Underground belt can have nothing else
|
||||
return false;
|
||||
}
|
||||
|
||||
const storageComp = receiver.components.Storage;
|
||||
if (storageComp) {
|
||||
// It's a storage
|
||||
if (storageComp.canAcceptItem(item)) {
|
||||
storageComp.takeItem(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Storage can't have anything else
|
||||
return false;
|
||||
}
|
||||
|
||||
const filterComp = receiver.components.Filter;
|
||||
if (filterComp) {
|
||||
// It's a filter! Unfortunately the filter has to know a lot about it's
|
||||
// surrounding state and components, so it can't be within the component itself.
|
||||
if (this.root.systemMgr.systems.filter.tryAcceptItem(receiver, slotIndex, item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DrawParameters} parameters
|
||||
* @param {MapChunkView} chunk
|
||||
@ -333,7 +247,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
}
|
||||
|
||||
// Limit the progress to the maximum available space on the next belt (also see #1000)
|
||||
let progress = slot.progress;
|
||||
let progress = Math.min(0.5, slot.progress);
|
||||
const nextBeltPath = slot.cachedBeltPath;
|
||||
if (nextBeltPath) {
|
||||
/*
|
||||
@ -368,20 +282,11 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
^ ^ item @ 0.9
|
||||
^ max progress = 0.3
|
||||
|
||||
Because now our range actually only goes to the end of the building, and not towards the center of the building, we need to multiply
|
||||
all values by 2:
|
||||
|
||||
Building Belt
|
||||
| X | X |
|
||||
| 0.........1.........2 |
|
||||
^ ^ item @ 1.8
|
||||
^ max progress = 0.6
|
||||
|
||||
And that's it! If you summarize the calculations from above into a formula, you get the one below.
|
||||
*/
|
||||
|
||||
const maxProgress =
|
||||
(0.5 + nextBeltPath.spacingToFirstItem - globalConfig.itemSpacingOnBelts) * 2;
|
||||
0.5 + nextBeltPath.spacingToFirstItem - globalConfig.itemSpacingOnBelts;
|
||||
progress = Math.min(maxProgress, progress);
|
||||
}
|
||||
|
||||
@ -399,8 +304,8 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
|
||||
const realDirection = staticComp.localDirectionToWorld(slot.direction);
|
||||
const realDirectionVector = enumDirectionToVector[realDirection];
|
||||
|
||||
const tileX = realPosition.x + 0.5 + realDirectionVector.x * 0.5 * progress;
|
||||
const tileY = realPosition.y + 0.5 + realDirectionVector.y * 0.5 * progress;
|
||||
const tileX = realPosition.x + 0.5 + realDirectionVector.x * progress;
|
||||
const tileY = realPosition.y + 0.5 + realDirectionVector.y * progress;
|
||||
|
||||
const worldX = tileX * globalConfig.tileSize;
|
||||
const worldY = tileY * globalConfig.tileSize;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { ACHIEVEMENTS } from "../../platform/achievement_provider";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { enumColorMixingResults, enumColors } from "../colors";
|
||||
import {
|
||||
@ -12,16 +13,12 @@ import { isTruthyItem } from "../items/boolean_item";
|
||||
import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item";
|
||||
import { ShapeItem } from "../items/shape_item";
|
||||
|
||||
/**
|
||||
* We need to allow queuing charges, otherwise the throughput will stall
|
||||
*/
|
||||
const MAX_QUEUED_CHARGES = 2;
|
||||
|
||||
/**
|
||||
* Whole data for a produced item
|
||||
*
|
||||
* @typedef {{
|
||||
* item: BaseItem,
|
||||
* extraProgress?: number,
|
||||
* preferredSlot?: number,
|
||||
* requiredSlot?: number,
|
||||
* doNotTrack?: boolean
|
||||
@ -33,29 +30,19 @@ const MAX_QUEUED_CHARGES = 2;
|
||||
* @typedef {{
|
||||
* entity: Entity,
|
||||
* items: Map<number, BaseItem>,
|
||||
* inputCount: number,
|
||||
* outItems: Array<ProducedItem>
|
||||
* }} ProcessorImplementationPayload
|
||||
*/
|
||||
|
||||
/**
|
||||
* Type of a processor implementation
|
||||
* @typedef {{
|
||||
* entity: Entity,
|
||||
* item: BaseItem,
|
||||
* slotIndex: number
|
||||
* }} ProccessingRequirementsImplementationPayload
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {Object<string, (ProcessorImplementationPayload) => void>}
|
||||
*/
|
||||
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>}
|
||||
@ -101,8 +88,14 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
const processorComp = entity.components.ItemProcessor;
|
||||
const ejectorComp = entity.components.ItemEjector;
|
||||
|
||||
const currentCharge = processorComp.ongoingCharges[0];
|
||||
// Check if we have an empty queue and can start a new charge - do this first so we don't waste a tick
|
||||
if (!processorComp.currentCharge) {
|
||||
if (this.canProcess(entity)) {
|
||||
this.startNewCharge(entity);
|
||||
}
|
||||
}
|
||||
|
||||
const currentCharge = processorComp.currentCharge;
|
||||
if (currentCharge) {
|
||||
// Process next charge
|
||||
if (currentCharge.remainingTime > 0.0) {
|
||||
@ -122,19 +115,25 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
processorComp.queuedEjects.push(itemsToEject[j]);
|
||||
}
|
||||
|
||||
processorComp.ongoingCharges.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have an empty queue and can start a new charge
|
||||
if (processorComp.ongoingCharges.length < MAX_QUEUED_CHARGES) {
|
||||
if (this.canProcess(entity)) {
|
||||
this.startNewCharge(entity);
|
||||
processorComp.currentCharge = null;
|
||||
|
||||
// now that the charge is complete, empty the inputs now
|
||||
let usedSlots = [];
|
||||
const acceptorComp = entity.components.ItemAcceptor;
|
||||
for (let i = 0; i < acceptorComp.completedInputs.length; i++) {
|
||||
const index = acceptorComp.completedInputs[i].slotIndex;
|
||||
if (!usedSlots.includes(index)) {
|
||||
usedSlots.push(index);
|
||||
acceptorComp.completedInputs.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go over all items and try to eject them
|
||||
for (let j = 0; j < processorComp.queuedEjects.length; ++j) {
|
||||
const { item, requiredSlot, preferredSlot } = processorComp.queuedEjects[j];
|
||||
const { item, requiredSlot, preferredSlot, extraProgress } = processorComp.queuedEjects[j];
|
||||
|
||||
assert(ejectorComp, "To eject items, the building needs to have an ejector");
|
||||
|
||||
@ -158,7 +157,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
|
||||
if (slot !== null) {
|
||||
// Alright, we can actually eject
|
||||
if (!ejectorComp.tryEject(slot, item)) {
|
||||
if (!ejectorComp.tryEject(slot, item, extraProgress)) {
|
||||
assert(false, "Failed to eject");
|
||||
} else {
|
||||
processorComp.queuedEjects.splice(j, 1);
|
||||
@ -169,54 +168,14 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the entity should accept the given item on the given slot.
|
||||
* This should only be called with matching items! I.e. if a color item is expected
|
||||
* on the given slot, then only a color item must be passed.
|
||||
* @param {Entity} entity
|
||||
* @param {BaseItem} item The item to accept
|
||||
* @param {number} slotIndex The slot index
|
||||
* @returns {boolean}
|
||||
*/
|
||||
checkRequirements(entity, item, slotIndex) {
|
||||
const itemProcessorComp = entity.components.ItemProcessor;
|
||||
const pinsComp = entity.components.WiredPins;
|
||||
|
||||
if (MODS_PROCESSING_REQUIREMENTS[itemProcessorComp.processingRequirement]) {
|
||||
return MODS_PROCESSING_REQUIREMENTS[itemProcessorComp.processingRequirement].bind(this)({
|
||||
entity,
|
||||
item,
|
||||
slotIndex,
|
||||
});
|
||||
}
|
||||
|
||||
switch (itemProcessorComp.processingRequirement) {
|
||||
case enumItemProcessorRequirements.painterQuad: {
|
||||
if (slotIndex === 0) {
|
||||
// Always accept the shape
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check the network value at the given slot
|
||||
const network = pinsComp.slots[slotIndex - 1].linkedNetwork;
|
||||
const slotIsEnabled = network && network.hasValue() && isTruthyItem(network.currentValue);
|
||||
if (!slotIsEnabled) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// By default, everything is accepted
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// input requirements are now handled in the item acceptor, which also fits better with what the acceptor is supposed to do
|
||||
|
||||
/**
|
||||
* Checks whether it's possible to process something
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
canProcess(entity) {
|
||||
const acceptorComp = entity.components.ItemAcceptor;
|
||||
const processorComp = entity.components.ItemProcessor;
|
||||
|
||||
if (MODS_CAN_PROCESS[processorComp.processingRequirement]) {
|
||||
@ -229,16 +188,35 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
// DEFAULT
|
||||
// By default, we can start processing once all inputs are there
|
||||
case null: {
|
||||
return processorComp.inputCount >= processorComp.inputsPerCharge;
|
||||
// Since each slot might have more than one input, don't check each slot more than once
|
||||
let usedSlots = [];
|
||||
for (let i = 0; i < acceptorComp.completedInputs.length; i++) {
|
||||
const index = acceptorComp.completedInputs[i].slotIndex;
|
||||
if (!usedSlots.includes(index)) {
|
||||
usedSlots.push(index);
|
||||
}
|
||||
}
|
||||
return usedSlots.length >= processorComp.inputsPerCharge;
|
||||
}
|
||||
|
||||
// QUAD PAINTER
|
||||
// For the quad painter, it might be possible to start processing earlier
|
||||
case enumItemProcessorRequirements.painterQuad: {
|
||||
const pinsComp = entity.components.WiredPins;
|
||||
const inputs = acceptorComp.completedInputs;
|
||||
|
||||
// split inputs efficiently
|
||||
let items = new Map();
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i];
|
||||
|
||||
if (!items.get(input.slotIndex)) {
|
||||
items.set(input.slotIndex, input.item);
|
||||
}
|
||||
}
|
||||
|
||||
// First slot is the shape, so if it's not there we can't do anything
|
||||
const shapeItem = /** @type {ShapeItem} */ (processorComp.inputSlots.get(0));
|
||||
const shapeItem = /** @type {ShapeItem} */ (items.get(0));
|
||||
if (!shapeItem) {
|
||||
return false;
|
||||
}
|
||||
@ -267,7 +245,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
|
||||
// Check if all colors of the enabled slots are there
|
||||
for (let i = 0; i < slotStatus.length; ++i) {
|
||||
if (slotStatus[i] && !processorComp.inputSlots.get(1 + i)) {
|
||||
if (slotStatus[i] && !items.get(1 + i)) {
|
||||
// A slot which is enabled wasn't enabled. Make sure if there is anything on the quadrant,
|
||||
// it is not possible to paint, but if there is nothing we can ignore it
|
||||
for (let j = 0; j < 4; ++j) {
|
||||
@ -278,7 +256,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -292,10 +269,25 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
startNewCharge(entity) {
|
||||
const acceptorComp = entity.components.ItemAcceptor;
|
||||
const processorComp = entity.components.ItemProcessor;
|
||||
|
||||
// First, take items
|
||||
const items = processorComp.inputSlots;
|
||||
// First, take inputs - but only one from each
|
||||
const inputs = acceptorComp.completedInputs;
|
||||
|
||||
// split inputs efficiently
|
||||
let items = new Map();
|
||||
let extraProgress = 0;
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const input = inputs[i];
|
||||
|
||||
if (!items.get(input.slotIndex)) {
|
||||
items.set(input.slotIndex, input.item);
|
||||
extraProgress = Math.max(extraProgress, input.extraProgress);
|
||||
//inputs.splice(i, 1);
|
||||
//i--;
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {Array<ProducedItem>} */
|
||||
const outItems = [];
|
||||
@ -309,7 +301,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
entity,
|
||||
items,
|
||||
outItems,
|
||||
inputCount: processorComp.inputCount,
|
||||
});
|
||||
|
||||
// Track produced items
|
||||
@ -317,23 +308,23 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
if (!outItems[i].doNotTrack) {
|
||||
this.root.signals.itemProduced.dispatch(outItems[i].item);
|
||||
}
|
||||
|
||||
// also set extra progress
|
||||
outItems[i].extraProgress = extraProgress;
|
||||
}
|
||||
|
||||
// Queue Charge
|
||||
const baseSpeed = this.root.hubGoals.getProcessorBaseSpeed(processorComp.type);
|
||||
const originalTime = 1 / baseSpeed;
|
||||
const originalTime = this.root.hubGoals.getProcessingTime(processorComp.type);
|
||||
|
||||
const bonusTimeToApply = Math.min(originalTime, processorComp.bonusTime);
|
||||
const timeToProcess = originalTime - bonusTimeToApply;
|
||||
|
||||
processorComp.bonusTime -= bonusTimeToApply;
|
||||
processorComp.ongoingCharges.push({
|
||||
|
||||
processorComp.currentCharge = {
|
||||
items: outItems,
|
||||
remainingTime: timeToProcess,
|
||||
});
|
||||
|
||||
processorComp.inputSlots.clear();
|
||||
processorComp.inputCount = 0;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -478,7 +469,14 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
* @param {ProcessorImplementationPayload} payload
|
||||
*/
|
||||
process_TRASH(payload) {
|
||||
// Do nothing ..
|
||||
// Hardcoded - 4 inputs
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
const item = /** @type {ShapeItem} */ (payload.items.get(i));
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
payload.entity.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.trash1000, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -602,8 +600,8 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
|
||||
const hubComponent = payload.entity.components.Hub;
|
||||
assert(hubComponent, "Hub item processor has no hub component");
|
||||
|
||||
// Hardcoded
|
||||
for (let i = 0; i < payload.inputCount; ++i) {
|
||||
// Hardcoded - 16 inputs
|
||||
for (let i = 0; i < 16; ++i) {
|
||||
const item = /** @type {ShapeItem} */ (payload.items.get(i));
|
||||
if (!item) {
|
||||
continue;
|
||||
|
@ -24,7 +24,9 @@ export class ItemProducerSystem extends GameSystemWithFilter {
|
||||
}
|
||||
|
||||
this.item = network.currentValue;
|
||||
ejectorComp.tryEject(0, this.item);
|
||||
|
||||
// Basically start ejecting at the exit of the ejector. Hacky, but who cares. It works, and its not in the base game :)
|
||||
ejectorComp.tryEject(0, this.item, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { DrawParameters } from "../../core/draw_parameters";
|
||||
import { enumDirectionToVector } from "../../core/vector";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { MinerComponent } from "../components/miner";
|
||||
import { Entity } from "../entity";
|
||||
import { GameSystemWithFilter } from "../game_system_with_filter";
|
||||
@ -31,20 +30,18 @@ export class MinerSystem extends GameSystemWithFilter {
|
||||
}
|
||||
|
||||
update() {
|
||||
let miningSpeed = this.root.hubGoals.getMinerBaseSpeed();
|
||||
let progressGrowth = this.root.dynamicTickrate.deltaSeconds * this.root.hubGoals.getMinerBaseSpeed();
|
||||
|
||||
const targetProgress = 1;
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.instantMiners) {
|
||||
miningSpeed *= 100;
|
||||
progressGrowth = targetProgress;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
const minerComp = entity.components.Miner;
|
||||
|
||||
// Reset everything on recompute
|
||||
if (this.needsRecompute) {
|
||||
minerComp.cachedChainedMiner = null;
|
||||
}
|
||||
|
||||
// Check if miner is above an actual tile
|
||||
if (!minerComp.cachedMinedItem) {
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
@ -58,25 +55,64 @@ export class MinerSystem extends GameSystemWithFilter {
|
||||
minerComp.cachedMinedItem = tileBelow;
|
||||
}
|
||||
|
||||
// First, try to get rid of chained items
|
||||
if (minerComp.itemChainBuffer.length > 0) {
|
||||
if (this.tryPerformMinerEject(entity, minerComp.itemChainBuffer[0])) {
|
||||
minerComp.itemChainBuffer.shift();
|
||||
// Reset everything on recompute
|
||||
if (this.needsRecompute) {
|
||||
minerComp.cachedChainedMiner = null;
|
||||
minerComp.cachedExitMiner = null;
|
||||
}
|
||||
|
||||
// Check if we are a chained miner
|
||||
if (minerComp.chainable) {
|
||||
if (!minerComp.cachedChainedMiner) {
|
||||
minerComp.cachedChainedMiner = this.findChainedMiner(entity);
|
||||
}
|
||||
|
||||
// don't calculate on the same tick as recompute, or so miners wont have caches yet
|
||||
if (minerComp.cachedChainedMiner && !minerComp.cachedExitMiner && !this.needsRecompute) {
|
||||
minerComp.cachedExitMiner = this.findExitMiner(entity);
|
||||
}
|
||||
|
||||
// Check if we now have a target at the end of the chain - if so, that's what we will progress
|
||||
const exitEntity = minerComp.cachedExitMiner;
|
||||
if (exitEntity) {
|
||||
const exitMinerComp = exitEntity.components.Miner;
|
||||
if (exitMinerComp.progress < targetProgress + 0.5) {
|
||||
// we can add on some extra progress
|
||||
exitMinerComp.progress += progressGrowth;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const mineDuration = 1 / miningSpeed;
|
||||
const timeSinceMine = this.root.time.now() - minerComp.lastMiningTime;
|
||||
if (timeSinceMine > mineDuration) {
|
||||
// Store how much we overflowed
|
||||
const buffer = Math.min(timeSinceMine - mineDuration, this.root.dynamicTickrate.deltaSeconds);
|
||||
//make sure progress never gets out of control
|
||||
minerComp.progress = Math.min(minerComp.progress, targetProgress + 0.5);
|
||||
if (minerComp.progress >= targetProgress) {
|
||||
// We can try to eject
|
||||
const extraProgress = minerComp.progress - targetProgress;
|
||||
|
||||
if (this.tryPerformMinerEject(entity, minerComp.cachedMinedItem)) {
|
||||
const ejectorComp = entity.components.ItemEjector;
|
||||
if (ejectorComp.tryEject(0, minerComp.cachedMinedItem, extraProgress)) {
|
||||
// Analytics hook
|
||||
this.root.signals.itemProduced.dispatch(minerComp.cachedMinedItem);
|
||||
// Store mining time
|
||||
minerComp.lastMiningTime = this.root.time.now() - buffer;
|
||||
|
||||
minerComp.progress -= targetProgress;
|
||||
}
|
||||
}
|
||||
|
||||
if (minerComp.progress < targetProgress) {
|
||||
minerComp.progress += progressGrowth;
|
||||
}
|
||||
|
||||
if (minerComp.progress >= targetProgress) {
|
||||
// We can try to eject
|
||||
const extraProgress = minerComp.progress - targetProgress;
|
||||
|
||||
const ejectorComp = entity.components.ItemEjector;
|
||||
if (ejectorComp.tryEject(0, minerComp.cachedMinedItem, extraProgress)) {
|
||||
// Analytics hook
|
||||
this.root.signals.itemProduced.dispatch(minerComp.cachedMinedItem);
|
||||
|
||||
minerComp.progress -= targetProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -93,6 +129,7 @@ export class MinerSystem extends GameSystemWithFilter {
|
||||
findChainedMiner(entity) {
|
||||
const ejectComp = entity.components.ItemEjector;
|
||||
const staticComp = entity.components.StaticMapEntity;
|
||||
const minedItem = entity.components.Miner.cachedMinedItem;
|
||||
const contentsBelow = this.root.map.getLowerLayerContentXY(staticComp.origin.x, staticComp.origin.y);
|
||||
if (!contentsBelow) {
|
||||
// This miner has no contents
|
||||
@ -109,7 +146,11 @@ export class MinerSystem extends GameSystemWithFilter {
|
||||
// Check if we are connected to another miner and thus do not eject directly
|
||||
if (targetContents) {
|
||||
const targetMinerComp = targetContents.components.Miner;
|
||||
if (targetMinerComp && targetMinerComp.chainable) {
|
||||
if (
|
||||
targetMinerComp &&
|
||||
targetMinerComp.chainable &&
|
||||
targetMinerComp.cachedMinedItem == minedItem
|
||||
) {
|
||||
const targetLowerLayer = this.root.map.getLowerLayerContentXY(targetTile.x, targetTile.y);
|
||||
if (targetLowerLayer) {
|
||||
return targetContents;
|
||||
@ -121,39 +162,37 @@ export class MinerSystem extends GameSystemWithFilter {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Finds the target exit miner for a given entity
|
||||
* @param {Entity} entity
|
||||
* @param {BaseItem} item
|
||||
* @returns {Entity|false} The exit miner entity or null if not found
|
||||
*/
|
||||
tryPerformMinerEject(entity, item) {
|
||||
findExitMiner(entity) {
|
||||
const minerComp = entity.components.Miner;
|
||||
const ejectComp = entity.components.ItemEjector;
|
||||
// Recompute exit miner if we are not at the front
|
||||
let targetEntity = minerComp.cachedChainedMiner;
|
||||
|
||||
// Check if we are a chained miner
|
||||
if (minerComp.chainable) {
|
||||
const targetEntity = minerComp.cachedChainedMiner;
|
||||
const ourPosition = entity.components.StaticMapEntity.origin;
|
||||
|
||||
// Check if the cache has to get recomputed
|
||||
if (targetEntity === null) {
|
||||
minerComp.cachedChainedMiner = this.findChainedMiner(entity);
|
||||
/** @type {Entity|null|false} */
|
||||
let nextTarget = targetEntity;
|
||||
while (nextTarget) {
|
||||
targetEntity = nextTarget;
|
||||
if (targetEntity.components.StaticMapEntity.origin == ourPosition) {
|
||||
// we are in a loop, do nothing
|
||||
targetEntity = null;
|
||||
break;
|
||||
}
|
||||
const targetMinerComp = targetEntity.components.Miner;
|
||||
nextTarget = targetMinerComp.cachedChainedMiner;
|
||||
}
|
||||
|
||||
// Check if we now have a target
|
||||
if (targetEntity) {
|
||||
const targetMinerComp = targetEntity.components.Miner;
|
||||
if (targetMinerComp.tryAcceptChainedItem(item)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
if (targetMinerComp.cachedMinedItem == minerComp.cachedMinedItem) {
|
||||
// only chain the same items
|
||||
return targetEntity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seems we are a regular miner or at the end of a row, try actually ejecting
|
||||
if (ejectComp.tryEject(0, item)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import { SavegameInterface_V1008 } from "./schemas/1008";
|
||||
import { SavegameInterface_V1009 } from "./schemas/1009";
|
||||
import { MODS } from "../mods/modloader";
|
||||
import { SavegameInterface_V1010 } from "./schemas/1010";
|
||||
import { SavegameInterface_V1011 } from "./schemas/1011";
|
||||
|
||||
const logger = createLogger("savegame");
|
||||
|
||||
@ -56,7 +57,7 @@ export class Savegame extends ReadWriteProxy {
|
||||
* @returns {number}
|
||||
*/
|
||||
static getCurrentVersion() {
|
||||
return 1010;
|
||||
return 1011;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -168,6 +169,11 @@ export class Savegame extends ReadWriteProxy {
|
||||
data.version = 1010;
|
||||
}
|
||||
|
||||
if (data.version === 1010) {
|
||||
SavegameInterface_V1011.migrate1010to1011(data);
|
||||
data.version = 1011;
|
||||
}
|
||||
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ import { SavegameInterface_V1007 } from "./schemas/1007";
|
||||
import { SavegameInterface_V1008 } from "./schemas/1008";
|
||||
import { SavegameInterface_V1009 } from "./schemas/1009";
|
||||
import { SavegameInterface_V1010 } from "./schemas/1010";
|
||||
import { SavegameInterface_V1011 } from "./schemas/1011";
|
||||
|
||||
/** @type {Object.<number, typeof BaseSavegameInterface>} */
|
||||
export const savegameInterfaces = {
|
||||
@ -25,6 +26,7 @@ export const savegameInterfaces = {
|
||||
1008: SavegameInterface_V1008,
|
||||
1009: SavegameInterface_V1009,
|
||||
1010: SavegameInterface_V1010,
|
||||
1011: SavegameInterface_V1011,
|
||||
};
|
||||
|
||||
const logger = createLogger("savegame_interface_registry");
|
||||
|
44
src/js/savegame/schemas/1011.js
Normal file
44
src/js/savegame/schemas/1011.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
src/js/savegame/schemas/1011.json
Normal file
5
src/js/savegame/schemas/1011.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"additionalProperties": true
|
||||
}
|
Loading…
Reference in New Issue
Block a user