From 242b99b19b55fca58bcef78264e26d5b0744a10f Mon Sep 17 00:00:00 2001 From: Sense101 Date: Tue, 1 Feb 2022 16:48:24 +0000 Subject: [PATCH 01/11] Refactor item acceptor and processor to be cleaner and work more smoothly --- src/js/core/config.js | 19 ++- src/js/game/belt_path.js | 99 ++----------- src/js/game/buildings/balancer.js | 3 +- src/js/game/buildings/trash.js | 21 --- src/js/game/buildings/underground_belt.js | 1 - src/js/game/components/filter.js | 6 +- src/js/game/components/item_acceptor.js | 117 +++++++++++----- src/js/game/components/item_ejector.js | 18 +-- src/js/game/components/item_processor.js | 52 ++----- src/js/game/components/storage.js | 23 +-- src/js/game/components/underground_belt.js | 44 +----- src/js/game/game_system_manager.js | 8 +- src/js/game/hub_goals.js | 69 +++++---- src/js/game/systems/filter.js | 32 ++--- src/js/game/systems/item_acceptor.js | 68 ++++----- src/js/game/systems/item_ejector.js | 156 +++++---------------- src/js/game/systems/item_processor.js | 133 +++++++++--------- src/js/game/systems/storage.js | 3 + src/js/game/systems/underground_belt.js | 53 ++++--- 19 files changed, 351 insertions(+), 574 deletions(-) diff --git a/src/js/core/config.js b/src/js/core/config.js index b451e848..602e0c82 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -87,17 +87,14 @@ export const globalConfig = { puzzleMaxBoundsSize: 20, puzzleValidationDurationSeconds: 30, - buildingSpeeds: { - cutter: 1 / 4, - cutterQuad: 1 / 4, - rotater: 1 / 1, - rotaterCCW: 1 / 1, - rotater180: 1 / 1, - painter: 1 / 6, - painterDouble: 1 / 8, - painterQuad: 1 / 2, - mixer: 1 / 5, - stacker: 1 / 8, + buildingRatios: { + cutter: 4, + cutterQuad: 4, + painter: 6, + painterDouble: 8, + painterQuad: 2, + mixer: 5, + stacker: 8, }, // Zooming diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index e1b466e9..bf797c61 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -130,10 +130,12 @@ export class BeltPath extends BasicSerializableObject { /** * Tries to accept the item * @param {BaseItem} item + * @param {number} extraProgress */ - tryAcceptItem(item) { + tryAcceptItem(item, extraProgress = 0) { if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) { // So, since we already need one tick to accept this item we will add this directly. + // this means we are moving it forwards twice in one tick, but otherwise belts won't be full :( const beltProgressPerTick = this.root.hubGoals.getBeltBaseSpeed() * this.root.dynamicTickrate.deltaSeconds * @@ -141,7 +143,7 @@ export class BeltPath extends BasicSerializableObject { // First, compute how much progress we can make *at max* const maxProgress = Math.max(0, this.spacingToFirstItem - globalConfig.itemSpacingOnBelts); - const initialProgress = Math.min(maxProgress, beltProgressPerTick); + const initialProgress = Math.min(maxProgress, beltProgressPerTick + extraProgress); this.items.unshift([this.spacingToFirstItem - initialProgress, item]); this.spacingToFirstItem = initialProgress; @@ -227,8 +229,6 @@ export class BeltPath extends BasicSerializableObject { return; } - const noSimplifiedBelts = !this.root.app.settings.getAllSettings().simplifiedBelts; - DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid); const targetStaticComp = targetEntity.components.StaticMapEntity; const targetBeltComp = targetEntity.components.Belt; @@ -274,95 +274,24 @@ export class BeltPath extends BasicSerializableObject { } const matchingSlotIndex = matchingSlot.index; - const passOver = this.computePassOverFunctionWithoutBelts(targetEntity, matchingSlotIndex); - if (!passOver) { - return; - } - const matchingDirection = enumInvertedDirections[ejectingDirection]; - const filter = matchingSlot.slot.filter; - - return function (item, remainingProgress = 0.0) { - // Check if the acceptor has a filter - if (filter && item._type !== filter) { - return false; + return function (item, startProgress = 0.0) { + const storageComp = targetEntity.components.Storage; + if ( + storageComp && + storageComp.tryAcceptItem(item) && + targetAcceptorComp.tryAcceptItem(matchingSlotIndex, item, startProgress) + ) { + // unique duplicated code for storage + return true; } - - // Try to pass over - if (passOver(item, matchingSlotIndex)) { - // Trigger animation on the acceptor comp - if (noSimplifiedBelts) { - targetAcceptorComp.onItemAccepted( - matchingSlotIndex, - matchingDirection, - item, - remainingProgress - ); - } + if (targetAcceptorComp.tryAcceptItem(matchingSlotIndex, item, startProgress)) { return true; } return false; }; } - /** - * Computes a method to pass over the item to the entity - * @param {Entity} entity - * @param {number} matchingSlotIndex - * @returns {(item: BaseItem, slotIndex: number) => boolean | void} - */ - computePassOverFunctionWithoutBelts(entity, matchingSlotIndex) { - const systems = this.root.systemMgr.systems; - const hubGoals = this.root.hubGoals; - - // NOTICE: THIS IS COPIED FROM THE ITEM EJECTOR SYSTEM FOR PEROFMANCE REASONS - - const itemProcessorComp = entity.components.ItemProcessor; - if (itemProcessorComp) { - // Its an item processor .. - return function (item) { - // Check for potential filters - if (!systems.itemProcessor.checkRequirements(entity, item, matchingSlotIndex)) { - return; - } - return itemProcessorComp.tryTakeItem(item, matchingSlotIndex); - }; - } - - const undergroundBeltComp = entity.components.UndergroundBelt; - if (undergroundBeltComp) { - // Its an underground belt. yay. - return function (item) { - return undergroundBeltComp.tryAcceptExternalItem( - item, - hubGoals.getUndergroundBeltBaseSpeed() - ); - }; - } - - const storageComp = entity.components.Storage; - if (storageComp) { - // It's a storage - return function (item) { - if (storageComp.canAcceptItem(item)) { - storageComp.takeItem(item); - return true; - } - }; - } - - const filterComp = entity.components.Filter; - if (filterComp) { - // It's a filter! Unfortunately the filter has to know a lot about it's - // surrounding state and components, so it can't be within the component itself. - return function (item) { - if (systems.filter.tryAcceptItem(entity, matchingSlotIndex, item)) { - return true; - } - }; - } - } - // Following code will be compiled out outside of dev versions /* dev:start */ diff --git a/src/js/game/buildings/balancer.js b/src/js/game/buildings/balancer.js index ce685a9a..d87c0a46 100644 --- a/src/js/game/buildings/balancer.js +++ b/src/js/game/buildings/balancer.js @@ -104,8 +104,7 @@ export class MetaBalancerBuilding extends MetaBuilding { speedMultiplier = 1; } - const speed = - (root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.balancer) / 2) * speedMultiplier; + const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.balancer) * speedMultiplier; return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; } diff --git a/src/js/game/buildings/trash.js b/src/js/game/buildings/trash.js index fcf7f11f..bda36001 100644 --- a/src/js/game/buildings/trash.js +++ b/src/js/game/buildings/trash.js @@ -47,25 +47,6 @@ export class MetaTrashBuilding extends MetaBuilding { return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_cutter_and_trash); } - addAchievementReceiver(entity) { - if (!entity.root) { - return; - } - - const itemProcessor = entity.components.ItemProcessor; - const tryTakeItem = itemProcessor.tryTakeItem.bind(itemProcessor); - - itemProcessor.tryTakeItem = () => { - const taken = tryTakeItem(...arguments); - - if (taken) { - entity.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.trash1000, 1); - } - - return taken; - }; - } - /** * Creates the entity at the given location * @param {Entity} entity @@ -100,7 +81,5 @@ export class MetaTrashBuilding extends MetaBuilding { processorType: enumItemProcessorTypes.trash, }) ); - - this.addAchievementReceiver(entity); } } diff --git a/src/js/game/buildings/underground_belt.js b/src/js/game/buildings/underground_belt.js index 7009ebd7..4dbce634 100644 --- a/src/js/game/buildings/underground_belt.js +++ b/src/js/game/buildings/underground_belt.js @@ -184,7 +184,6 @@ export class MetaUndergroundBeltBuilding extends MetaBuilding { * @param {Entity} entity */ setupEntityComponents(entity) { - // Required, since the item processor needs this. entity.addComponent( new ItemEjectorComponent({ slots: [], diff --git a/src/js/game/components/filter.js b/src/js/game/components/filter.js index 8a22a076..4c16ea6f 100644 --- a/src/js/game/components/filter.js +++ b/src/js/game/components/filter.js @@ -6,7 +6,7 @@ import { typeItemSingleton } from "../item_resolver"; /** * @typedef {{ * item: BaseItem, - * progress: number + * extraProgress: number * }} PendingFilterItem */ @@ -24,14 +24,14 @@ export class FilterComponent extends Component { pendingItemsToLeaveThrough: types.array( types.structured({ item: typeItemSingleton, - progress: types.ufloat, + extraProgress: types.ufloat, }) ), pendingItemsToReject: types.array( types.structured({ item: typeItemSingleton, - progress: types.ufloat, + extraProgress: types.ufloat, //@SENSETODO will need save migration }) ), }; diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index d3df3763..987de239 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -2,6 +2,9 @@ import { enumDirection, enumInvertedDirections, Vector } from "../../core/vector import { types } from "../../savegame/serialization"; import { BaseItem } from "../base_item"; import { Component } from "../component"; +import { Entity } from "../entity"; +import { typeItemSingleton } from "../item_resolver"; +import { GameRoot } from "../root"; /** * @typedef {{ @@ -24,34 +27,69 @@ import { Component } from "../component"; * filter?: ItemType * }} ItemAcceptorSlotConfig */ +/** + * @typedef {Array<{ + * slotIndex: number, + * item: BaseItem, + * animProgress: number, + * }>} ItemAcceptorInputs + * + * @typedef {Array<{ + * slotIndex: number, + * item: BaseItem, + * extraProgress: number + * }>} ItemAcceptorCompletedInputs + * + * @typedef {{ + * root: GameRoot, + * entity: Entity, + * item: BaseItem, + * slotIndex: number, + * extraProgress: number + * }} InputCompletedArgs + */ + export class ItemAcceptorComponent extends Component { static getId() { return "ItemAcceptor"; } + static getSchema() { + return { + inputs: types.array( + types.structured({ + slotIndex: types.uint, + item: typeItemSingleton, + animProgress: types.ufloat, + }) + ), + completedInputs: types.array( + types.structured({ + slotIndex: types.uint, + item: typeItemSingleton, + extraProgress: types.ufloat, + }) + ), + }; + } + /** * * @param {object} param0 * @param {Array} param0.slots The slots from which we accept items + * @param {number=} param0.maxSlotInputs The maximum amount of items one slot can accept before it is full */ - constructor({ slots = [] }) { + constructor({ slots = [], maxSlotInputs = 2 }) { super(); + /** @type {ItemAcceptorInputs} */ + this.inputs = []; + /** @type {ItemAcceptorCompletedInputs} */ + this.completedInputs = []; this.setSlots(slots); - this.clear(); - } - clear() { - /** - * Fixes belt animations - * @type {Array<{ - * item: BaseItem, - * slotIndex: number, - * animProgress: number, - * direction: enumDirection - * }>} - */ - this.itemConsumptionAnimations = []; + // setting this to 1 will cause throughput issues at very high speeds + this.maxSlotInputs = maxSlotInputs; } /** @@ -74,31 +112,42 @@ export class ItemAcceptorComponent extends Component { } /** - * Returns if this acceptor can accept a new item at slot N - * - * NOTICE: The belt path ignores this for performance reasons and does his own check + * Called when trying to input a new item * @param {number} slotIndex - * @param {BaseItem=} item - */ - canAcceptItem(slotIndex, item) { - const slot = this.slots[slotIndex]; - return !slot.filter || slot.filter === item.getItemType(); - } - - /** - * Called when an item has been accepted so that - * @param {number} slotIndex - * @param {enumDirection} direction * @param {BaseItem} item - * @param {number} remainingProgress World space remaining progress, can be set to set the start position of the item + * @param {number} startProgress World space remaining progress, can be set to set the start position of the item + * @returns {boolean} if the input was succesful */ - onItemAccepted(slotIndex, direction, item, remainingProgress = 0.0) { - this.itemConsumptionAnimations.push({ - item, + tryAcceptItem(slotIndex, item, startProgress = 0.0) { + const slot = this.slots[slotIndex]; + + let existingInputs = 0; + for (let i = 0; i < this.inputs.length; i++) { + if (this.inputs[i].slotIndex == slotIndex) { + existingInputs++; + } + } + for (let i = 0; i < this.completedInputs.length; i++) { + if (this.completedInputs[i].slotIndex == slotIndex) { + existingInputs++; + } + } + + if (existingInputs >= this.maxSlotInputs) { + return false; + } + + if (slot.filter && slot.filter != item.getItemType()) { + return false; + } + + // if the start progress is bigger than 0.5, the remainder should get passed on to the ejector + this.inputs.push({ slotIndex, - direction, - animProgress: Math.min(1, remainingProgress * 2), + item, + animProgress: startProgress, }); + return true; } /** diff --git a/src/js/game/components/item_ejector.js b/src/js/game/components/item_ejector.js index bfc54cd8..5c1cdb2a 100644 --- a/src/js/game/components/item_ejector.js +++ b/src/js/game/components/item_ejector.js @@ -127,28 +127,16 @@ export class ItemEjectorComponent extends Component { * Tries to eject a given item * @param {number} slotIndex * @param {BaseItem} item + * @param {number} startingProgress * @returns {boolean} */ - tryEject(slotIndex, item) { + tryEject(slotIndex, item, startingProgress = 0.0) { if (!this.canEjectOnSlot(slotIndex)) { return false; } this.slots[slotIndex].item = item; this.slots[slotIndex].lastItem = item; - this.slots[slotIndex].progress = 0; + this.slots[slotIndex].progress = startingProgress; return true; } - - /** - * Clears the given slot and returns the item it had - * @param {number} slotIndex - * @returns {BaseItem|null} - */ - takeSlotItem(slotIndex) { - const slot = this.slots[slotIndex]; - const item = slot.item; - slot.item = null; - slot.progress = 0.0; - return item; - } } diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index be7d1ce4..9da8489a 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -29,6 +29,7 @@ export const enumItemProcessorRequirements = { /** @typedef {{ * item: BaseItem, + * extraProgress?: number * requiredSlot?: number, * preferredSlot?: number * }} EjectorItemToEject */ @@ -38,6 +39,13 @@ export const enumItemProcessorRequirements = { * items: Array, * }} EjectorCharge */ +/** + * @typedef {{ + * item: BaseItem + * extraProgress: number + * }} ItemProcessorInput + */ + export class ItemProcessorComponent extends Component { static getId() { return "ItemProcessor"; @@ -73,12 +81,6 @@ export class ItemProcessorComponent extends Component { // Type of processing requirement this.processingRequirement = processingRequirement; - /** - * Our current inputs - * @type {Map} - */ - this.inputSlots = new Map(); - this.clear(); } @@ -88,21 +90,13 @@ export class ItemProcessorComponent extends Component { // sure the outputs always match this.nextOutputSlot = 0; - this.inputSlots.clear(); - - /** - * Current input count - * @type {number} - */ - this.inputCount = 0; - /** * What we are currently processing, empty if we don't produce anything rn * requiredSlot: Item *must* be ejected on this slot * preferredSlot: Item *can* be ejected on this slot, but others are fine too if the one is not usable - * @type {Array} + * @type {EjectorCharge|null} */ - this.ongoingCharges = []; + this.currentCharge = null; /** * How much processing time we have left from the last tick @@ -115,30 +109,4 @@ export class ItemProcessorComponent extends Component { */ this.queuedEjects = []; } - - /** - * Tries to take the item - * @param {BaseItem} item - * @param {number} sourceSlot - */ - tryTakeItem(item, sourceSlot) { - if ( - this.type === enumItemProcessorTypes.hub || - this.type === enumItemProcessorTypes.trash || - this.type === enumItemProcessorTypes.goal - ) { - // Hub has special logic .. not really nice but efficient. - this.inputSlots.set(this.inputCount, item); - this.inputCount++; - return true; - } - - // Check that we only take one item per slot - if (this.inputSlots.has(sourceSlot)) { - return false; - } - this.inputSlots.set(sourceSlot, item); - this.inputCount++; - return true; - } } diff --git a/src/js/game/components/storage.js b/src/js/game/components/storage.js index be243a44..ce179416 100644 --- a/src/js/game/components/storage.js +++ b/src/js/game/components/storage.js @@ -46,32 +46,19 @@ export class StorageComponent extends Component { * Returns whether this storage can accept the item * @param {BaseItem} item */ - canAcceptItem(item) { + tryAcceptItem(item) { if (this.storedCount >= this.maximumStorage) { return false; } - if (!this.storedItem || this.storedCount === 0) { - return true; - } - const itemType = item.getItemType(); - - // Check type matches - if (itemType !== this.storedItem.getItemType()) { + if (this.storedCount > 0 && this.storedItem && itemType !== this.storedItem.getItemType()) { return false; } - if (itemType === "color") { - return /** @type {ColorItem} */ (this.storedItem).color === /** @type {ColorItem} */ (item).color; - } + this.storedItem = item; + this.storedCount++; - if (itemType === "shape") { - return ( - /** @type {ShapeItem} */ (this.storedItem).definition.getHash() === - /** @type {ShapeItem} */ (item).definition.getHash() - ); - } - return false; + return true; } /** diff --git a/src/js/game/components/underground_belt.js b/src/js/game/components/underground_belt.js index 2b744edd..0164e269 100644 --- a/src/js/game/components/underground_belt.js +++ b/src/js/game/components/underground_belt.js @@ -56,61 +56,31 @@ export class UndergroundBeltComponent extends Component { this.consumptionAnimations = []; /** - * Used on both receiver and sender. - * Reciever: Used to store the next item to transfer, and to block input while doing this - * Sender: Used to store which items are currently "travelling" - * @type {Array<[BaseItem, number]>} Format is [Item, ingame time to eject the item] + * Used only on reciever to store which items are currently "travelling" + * @type {Array<[BaseItem, number]>} Format is [Item, Tile progress] */ this.pendingItems = []; } - /** - * Tries to accept an item from an external source like a regular belt or building - * @param {BaseItem} item - * @param {number} beltSpeed How fast this item travels - */ - tryAcceptExternalItem(item, beltSpeed) { - if (this.mode !== enumUndergroundBeltMode.sender) { - // Only senders accept external items - return false; - } - - if (this.pendingItems.length > 0) { - // We currently have a pending item - return false; - } - - this.pendingItems.push([item, 0]); - return true; - } - /** * Tries to accept a tunneled item * @param {BaseItem} item - * @param {number} travelDistance How many tiles this item has to travel - * @param {number} beltSpeed How fast this item travels - * @param {number} now Current ingame time + * @param {number} travelDistance + * @param {number} startProgress The starting tile progress */ - tryAcceptTunneledItem(item, travelDistance, beltSpeed, now) { + tryAcceptTunneledItem(item, travelDistance, startProgress = 0) { if (this.mode !== enumUndergroundBeltMode.receiver) { // Only receivers can accept tunneled items return false; } - // Notice: We assume that for all items the travel distance is the same - const maxItemsInTunnel = (2 + travelDistance) / globalConfig.itemSpacingOnBelts; + const maxItemsInTunnel = travelDistance / globalConfig.itemSpacingOnBelts; if (this.pendingItems.length >= maxItemsInTunnel) { // Simulate a real belt which gets full at some point return false; } - // NOTICE: - // This corresponds to the item ejector - it needs 0.5 additional tiles to eject the item. - // So instead of adding 1 we add 0.5 only. - // Additionally it takes 1 tile for the acceptor which we just add on top. - const travelDuration = (travelDistance + 1.5) / beltSpeed / globalConfig.itemSpacingOnBelts; - - this.pendingItems.push([item, now + travelDuration]); + this.pendingItems.push([item, startProgress]); return true; } } diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index a799b42a..d8c16200 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -150,20 +150,20 @@ export class GameSystemManager { add("belt", BeltSystem); - add("undergroundBelt", UndergroundBeltSystem); - add("miner", MinerSystem); add("storage", StorageSystem); + add("itemEjector", ItemEjectorSystem); + + add("undergroundBelt", UndergroundBeltSystem); + add("itemProcessor", ItemProcessorSystem); add("filter", FilterSystem); add("itemProducer", ItemProducerSystem); - add("itemEjector", ItemEjectorSystem); - if (this.root.gameMode.hasResources()) { add("mapResources", MapResourcesSystem); } diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index 9f9c63be..531a6643 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -507,55 +507,36 @@ export class HubGoals extends BasicSerializableObject { } /** - * Processor speed + * Processor time to process * @param {enumItemProcessorTypes} processorType - * @returns {number} items / sec + * @returns {number} process time in seconds */ - getProcessorBaseSpeed(processorType) { + getProcessingTime(processorType) { if (this.root.gameMode.throughputDoesNotMatter()) { - return globalConfig.beltSpeedItemsPerSecond * globalConfig.puzzleModeSpeed * 10; + return 0; } switch (processorType) { case enumItemProcessorTypes.trash: case enumItemProcessorTypes.hub: case enumItemProcessorTypes.goal: - return 1e30; case enumItemProcessorTypes.balancer: - return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2; case enumItemProcessorTypes.reader: - return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt; + case enumItemProcessorTypes.rotater: + case enumItemProcessorTypes.rotaterCCW: + case enumItemProcessorTypes.rotater180: + return 0; case enumItemProcessorTypes.mixer: case enumItemProcessorTypes.painter: case enumItemProcessorTypes.painterDouble: case enumItemProcessorTypes.painterQuad: { - assert( - globalConfig.buildingSpeeds[processorType], - "Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType - ); - return ( - globalConfig.beltSpeedItemsPerSecond * - this.upgradeImprovements.painting * - globalConfig.buildingSpeeds[processorType] - ); + return this.getProcessorTimeWithUpgrades(this.upgradeImprovements.painting, processorType); } - case enumItemProcessorTypes.cutter: case enumItemProcessorTypes.cutterQuad: - case enumItemProcessorTypes.rotater: - case enumItemProcessorTypes.rotaterCCW: - case enumItemProcessorTypes.rotater180: case enumItemProcessorTypes.stacker: { - assert( - globalConfig.buildingSpeeds[processorType], - "Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType - ); - return ( - globalConfig.beltSpeedItemsPerSecond * - this.upgradeImprovements.processors * - globalConfig.buildingSpeeds[processorType] - ); + return this.getProcessorTimeWithUpgrades(this.upgradeImprovements.processors, processorType); } default: if (MOD_ITEM_PROCESSOR_SPEEDS[processorType]) { @@ -564,6 +545,34 @@ export class HubGoals extends BasicSerializableObject { assertAlways(false, "invalid processor type: " + processorType); } - return 1 / globalConfig.beltSpeedItemsPerSecond; + return 0; + } + + /** + * @param {number} upgrade + * @param {enumItemProcessorTypes} processorType + */ + getProcessorTimeWithUpgrades(upgrade, processorType) { + assert( + globalConfig.buildingRatios[processorType], + "Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType + ); + + const processorTime = + globalConfig.buildingRatios[processorType] / globalConfig.beltSpeedItemsPerSecond; + return processorTime / upgrade; + } + + /** + * Processor speed + * @param {enumItemProcessorTypes} processorType + * @returns {number} items/sec + */ + getProcessorBaseSpeed(processorType) { + const time = this.getProcessingTime(processorType); + if (!time) { + return this.getBeltBaseSpeed(); + } + return 1 / time; } } diff --git a/src/js/game/systems/filter.js b/src/js/game/systems/filter.js index a6442b41..e8008bf8 100644 --- a/src/js/game/systems/filter.js +++ b/src/js/game/systems/filter.js @@ -1,4 +1,3 @@ -import { globalConfig } from "../../core/config"; import { BaseItem } from "../base_item"; import { FilterComponent } from "../components/filter"; import { Entity } from "../entity"; @@ -13,32 +12,27 @@ export class FilterSystem extends GameSystemWithFilter { } update() { - const progress = - this.root.dynamicTickrate.deltaSeconds * - this.root.hubGoals.getBeltBaseSpeed() * - globalConfig.itemSpacingOnBelts; - - const requiredProgress = 1 - progress; - for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; const filterComp = entity.components.Filter; + const acceptorComp = entity.components.ItemAcceptor; const ejectorComp = entity.components.ItemEjector; - // Process payloads + // Take items from acceptor + const input = acceptorComp.completedInputs[0]; + if (input && this.tryAcceptItem(entity, input.item, input.extraProgress)) { + acceptorComp.completedInputs.shift(); + } + + // Output to ejector const slotsAndLists = [filterComp.pendingItemsToLeaveThrough, filterComp.pendingItemsToReject]; for (let slotIndex = 0; slotIndex < slotsAndLists.length; ++slotIndex) { const pendingItems = slotsAndLists[slotIndex]; for (let j = 0; j < pendingItems.length; ++j) { const nextItem = pendingItems[j]; - // Advance next item - nextItem.progress = Math.min(requiredProgress, nextItem.progress + progress); - // Check if it's ready to eject - if (nextItem.progress >= requiredProgress - 1e-5) { - if (ejectorComp.tryEject(slotIndex, nextItem.item)) { - pendingItems.shift(); - } + if (ejectorComp.tryEject(slotIndex, nextItem.item)) { + pendingItems.shift(); } } } @@ -48,10 +42,10 @@ export class FilterSystem extends GameSystemWithFilter { /** * * @param {Entity} entity - * @param {number} slot * @param {BaseItem} item + * @param {number} startProgress */ - tryAcceptItem(entity, slot, item) { + tryAcceptItem(entity, item, startProgress) { const network = entity.components.WiredPins.slots[0].linkedNetwork; if (!network || !network.hasValue()) { // Filter is not connected @@ -78,7 +72,7 @@ export class FilterSystem extends GameSystemWithFilter { // Actually accept item listToCheck.push({ item, - progress: 0.0, + extraProgress: startProgress, }); return true; } diff --git a/src/js/game/systems/item_acceptor.js b/src/js/game/systems/item_acceptor.js index 780b4abd..db9bc7e1 100644 --- a/src/js/game/systems/item_acceptor.js +++ b/src/js/game/systems/item_acceptor.js @@ -1,6 +1,5 @@ import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; -import { fastArrayDelete } from "../../core/utils"; import { enumDirectionToVector } from "../../core/vector"; import { ItemAcceptorComponent } from "../components/item_acceptor"; import { GameSystemWithFilter } from "../game_system_with_filter"; @@ -9,49 +8,36 @@ import { MapChunkView } from "../map_chunk_view"; export class ItemAcceptorSystem extends GameSystemWithFilter { constructor(root) { super(root, [ItemAcceptorComponent]); - - // Well ... it's better to be verbose I guess? - this.accumulatedTicksWhileInMapOverview = 0; } update() { - if (this.root.app.settings.getAllSettings().simplifiedBelts) { - // Disabled in potato mode - return; - } - - // This system doesn't render anything while in map overview, - // so simply accumulate ticks - if (this.root.camera.getIsMapOverlayActive()) { - ++this.accumulatedTicksWhileInMapOverview; - return; - } - - // Compute how much ticks we missed - const numTicks = 1 + this.accumulatedTicksWhileInMapOverview; - const progress = + // same code for belts, acceptors and ejectors - add helper method??? + const progressGrowth = this.root.dynamicTickrate.deltaSeconds * - 2 * this.root.hubGoals.getBeltBaseSpeed() * - globalConfig.itemSpacingOnBelts * // * 2 because its only a half tile - numTicks; - - // Reset accumulated ticks - this.accumulatedTicksWhileInMapOverview = 0; + globalConfig.itemSpacingOnBelts; for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; - const aceptorComp = entity.components.ItemAcceptor; - const animations = aceptorComp.itemConsumptionAnimations; + const acceptorComp = entity.components.ItemAcceptor; + const inputs = acceptorComp.inputs; + const maxProgress = 0.5; - // Process item consumption animations to avoid items popping from the belts - for (let animIndex = 0; animIndex < animations.length; ++animIndex) { - const anim = animations[animIndex]; - anim.animProgress += progress; - if (anim.animProgress > 1) { - fastArrayDelete(animations, animIndex); - animIndex -= 1; + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i]; + input.animProgress += progressGrowth; + + if (input.animProgress < maxProgress) { + continue; } + + inputs.splice(i, 1); + i--; + acceptorComp.completedInputs.push({ + slotIndex: input.slotIndex, + item: input.item, + extraProgress: input.animProgress - maxProgress, + }); // will be handled on the SAME frame due to processor system being afterwards } } } @@ -75,10 +61,9 @@ export class ItemAcceptorSystem extends GameSystemWithFilter { } const staticComp = entity.components.StaticMapEntity; - for (let animIndex = 0; animIndex < acceptorComp.itemConsumptionAnimations.length; ++animIndex) { - const { item, slotIndex, animProgress, direction } = acceptorComp.itemConsumptionAnimations[ - animIndex - ]; + for (let i = 0; i < acceptorComp.inputs.length; i++) { + const input = acceptorComp.inputs[i]; + const { item, animProgress, slotIndex } = input; const slotData = acceptorComp.slots[slotIndex]; const realSlotPos = staticComp.localTileToWorld(slotData.pos); @@ -88,10 +73,11 @@ export class ItemAcceptorSystem extends GameSystemWithFilter { continue; } - const fadeOutDirection = enumDirectionToVector[staticComp.localDirectionToWorld(direction)]; + const fadeOutDirection = + enumDirectionToVector[staticComp.localDirectionToWorld(slotData.direction)]; const finalTile = realSlotPos.subScalars( - fadeOutDirection.x * (animProgress / 2 - 0.5), - fadeOutDirection.y * (animProgress / 2 - 0.5) + fadeOutDirection.x * (animProgress - 0.5), + fadeOutDirection.y * (animProgress - 0.5) ); item.drawItemCenteredClipped( diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index 8c7468ad..ea8e7381 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -4,7 +4,6 @@ import { createLogger } from "../../core/logging"; import { Rectangle } from "../../core/rectangle"; import { StaleAreaDetector } from "../../core/stale_area_detector"; import { enumDirection, enumDirectionToVector } from "../../core/vector"; -import { BaseItem } from "../base_item"; import { BeltComponent } from "../components/belt"; import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; @@ -139,10 +138,15 @@ export class ItemEjectorSystem extends GameSystemWithFilter { this.staleAreaDetector.update(); // Precompute effective belt speed - let progressGrowth = 2 * this.root.dynamicTickrate.deltaSeconds; + let progressGrowth = + this.root.dynamicTickrate.deltaSeconds * + this.root.hubGoals.getBeltBaseSpeed() * + globalConfig.itemSpacingOnBelts; + // it's only half a belt + const maxProgress = 0.5; if (G_IS_DEV && globalConfig.debug.instantBelts) { - progressGrowth = 1; + progressGrowth = maxProgress; } // Go over all cache entries @@ -159,29 +163,27 @@ export class ItemEjectorSystem extends GameSystemWithFilter { continue; } - // Advance items on the slot - sourceSlot.progress = Math.min( - 1, - sourceSlot.progress + - progressGrowth * - this.root.hubGoals.getBeltBaseSpeed() * - globalConfig.itemSpacingOnBelts - ); + if (sourceSlot.progress < maxProgress) { + // Advance items on the slot + sourceSlot.progress += progressGrowth; + } if (G_IS_DEV && globalConfig.debug.disableEjectorProcessing) { - sourceSlot.progress = 1.0; + sourceSlot.progress = maxProgress; } // Check if we are still in the process of ejecting, can't proceed then - if (sourceSlot.progress < 1.0) { + if (sourceSlot.progress < maxProgress) { continue; } + const extraProgress = sourceSlot.progress - maxProgress; + // Check if we are ejecting to a belt path const destPath = sourceSlot.cachedBeltPath; if (destPath) { // Try passing the item over - if (destPath.tryAcceptItem(item)) { + if (destPath.tryAcceptItem(item, extraProgress)) { sourceSlot.item = null; } @@ -193,110 +195,27 @@ export class ItemEjectorSystem extends GameSystemWithFilter { // Check if the target acceptor can actually accept this item const destEntity = sourceSlot.cachedTargetEntity; const destSlot = sourceSlot.cachedDestSlot; - if (destSlot) { + if (destEntity && destSlot) { const targetAcceptorComp = destEntity.components.ItemAcceptor; - if (!targetAcceptorComp.canAcceptItem(destSlot.index, item)) { - continue; - } - - // Try to hand over the item - if (this.tryPassOverItem(item, destEntity, destSlot.index)) { - // Handover successful, clear slot - if (!this.root.app.settings.getAllSettings().simplifiedBelts) { - targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.slot.direction, item); - } + const storageComp = destEntity.components.Storage; + if ( + storageComp && + storageComp.tryAcceptItem(item) && + targetAcceptorComp.tryAcceptItem(destSlot.index, item, extraProgress) + ) { + // unique duplicated code for storage - hacky :( + sourceSlot.item = null; + return; + } + if (targetAcceptorComp.tryAcceptItem(destSlot.index, item, extraProgress)) { + // Handover successful, clear slot sourceSlot.item = null; - continue; } } } } } - /** - * - * @param {BaseItem} item - * @param {Entity} receiver - * @param {number} slotIndex - */ - tryPassOverItem(item, receiver, slotIndex) { - // Try figuring out how what to do with the item - // @TODO: Kinda hacky. How to solve this properly? Don't want to go through inheritance hell. - - const beltComp = receiver.components.Belt; - if (beltComp) { - const path = beltComp.assignedPath; - assert(path, "belt has no path"); - if (path.tryAcceptItem(item)) { - return true; - } - // Belt can have nothing else - return false; - } - - //////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////// - // - // NOTICE ! THIS CODE IS DUPLICATED IN THE BELT PATH FOR PERFORMANCE REASONS - // - //////////////////////////////////////////////////////////////////////////////// - //////////////////////////////////////////////////////////////////////////////// - - const itemProcessorComp = receiver.components.ItemProcessor; - if (itemProcessorComp) { - // Check for potential filters - if (!this.root.systemMgr.systems.itemProcessor.checkRequirements(receiver, item, slotIndex)) { - return false; - } - - // Its an item processor .. - if (itemProcessorComp.tryTakeItem(item, slotIndex)) { - return true; - } - // Item processor can have nothing else - return false; - } - - const undergroundBeltComp = receiver.components.UndergroundBelt; - if (undergroundBeltComp) { - // Its an underground belt. yay. - if ( - undergroundBeltComp.tryAcceptExternalItem( - item, - this.root.hubGoals.getUndergroundBeltBaseSpeed() - ) - ) { - return true; - } - - // Underground belt can have nothing else - return false; - } - - const storageComp = receiver.components.Storage; - if (storageComp) { - // It's a storage - if (storageComp.canAcceptItem(item)) { - storageComp.takeItem(item); - return true; - } - - // Storage can't have anything else - return false; - } - - const filterComp = receiver.components.Filter; - if (filterComp) { - // It's a filter! Unfortunately the filter has to know a lot about it's - // surrounding state and components, so it can't be within the component itself. - if (this.root.systemMgr.systems.filter.tryAcceptItem(receiver, slotIndex, item)) { - return true; - } - } - - return false; - } - /** * @param {DrawParameters} parameters * @param {MapChunkView} chunk @@ -333,7 +252,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter { } // Limit the progress to the maximum available space on the next belt (also see #1000) - let progress = slot.progress; + let progress = Math.min(0.5, slot.progress); const nextBeltPath = slot.cachedBeltPath; if (nextBeltPath) { /* @@ -368,20 +287,11 @@ export class ItemEjectorSystem extends GameSystemWithFilter { ^ ^ item @ 0.9 ^ max progress = 0.3 - Because now our range actually only goes to the end of the building, and not towards the center of the building, we need to multiply - all values by 2: - - Building Belt - | X | X | - | 0.........1.........2 | - ^ ^ item @ 1.8 - ^ max progress = 0.6 - And that's it! If you summarize the calculations from above into a formula, you get the one below. */ const maxProgress = - (0.5 + nextBeltPath.spacingToFirstItem - globalConfig.itemSpacingOnBelts) * 2; + 0.5 + nextBeltPath.spacingToFirstItem - globalConfig.itemSpacingOnBelts; progress = Math.min(maxProgress, progress); } @@ -399,8 +309,8 @@ export class ItemEjectorSystem extends GameSystemWithFilter { const realDirection = staticComp.localDirectionToWorld(slot.direction); const realDirectionVector = enumDirectionToVector[realDirection]; - const tileX = realPosition.x + 0.5 + realDirectionVector.x * 0.5 * progress; - const tileY = realPosition.y + 0.5 + realDirectionVector.y * 0.5 * progress; + const tileX = realPosition.x + 0.5 + realDirectionVector.x * progress; + const tileY = realPosition.y + 0.5 + realDirectionVector.y * progress; const worldX = tileX * globalConfig.tileSize; const worldY = tileY * globalConfig.tileSize; diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 6e1032c9..efbdf516 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -1,4 +1,5 @@ import { globalConfig } from "../../core/config"; +import { ACHIEVEMENTS } from "../../platform/achievement_provider"; import { BaseItem } from "../base_item"; import { enumColorMixingResults, enumColors } from "../colors"; import { @@ -12,16 +13,12 @@ import { isTruthyItem } from "../items/boolean_item"; import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item"; import { ShapeItem } from "../items/shape_item"; -/** - * We need to allow queuing charges, otherwise the throughput will stall - */ -const MAX_QUEUED_CHARGES = 2; - /** * Whole data for a produced item * * @typedef {{ * item: BaseItem, + * extraProgress?: number, * preferredSlot?: number, * requiredSlot?: number, * doNotTrack?: boolean @@ -33,7 +30,6 @@ const MAX_QUEUED_CHARGES = 2; * @typedef {{ * entity: Entity, * items: Map, - * inputCount: number, * outItems: Array * }} ProcessorImplementationPayload */ @@ -82,8 +78,14 @@ export class ItemProcessorSystem extends GameSystemWithFilter { const processorComp = entity.components.ItemProcessor; const ejectorComp = entity.components.ItemEjector; - const currentCharge = processorComp.ongoingCharges[0]; + // Check if we have an empty queue and can start a new charge - do this first so we don't waste a tick + if (!processorComp.currentCharge) { + if (this.canProcess(entity)) { + this.startNewCharge(entity); + } + } + const currentCharge = processorComp.currentCharge; if (currentCharge) { // Process next charge if (currentCharge.remainingTime > 0.0) { @@ -103,19 +105,13 @@ export class ItemProcessorSystem extends GameSystemWithFilter { processorComp.queuedEjects.push(itemsToEject[j]); } - processorComp.ongoingCharges.shift(); - } - } - - // Check if we have an empty queue and can start a new charge - if (processorComp.ongoingCharges.length < MAX_QUEUED_CHARGES) { - if (this.canProcess(entity)) { - this.startNewCharge(entity); + processorComp.currentCharge = null; } } + // Go over all items and try to eject them for (let j = 0; j < processorComp.queuedEjects.length; ++j) { - const { item, requiredSlot, preferredSlot } = processorComp.queuedEjects[j]; + const { item, requiredSlot, preferredSlot, extraProgress } = processorComp.queuedEjects[j]; assert(ejectorComp, "To eject items, the building needs to have an ejector"); @@ -139,7 +135,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { if (slot !== null) { // Alright, we can actually eject - if (!ejectorComp.tryEject(slot, item)) { + if (!ejectorComp.tryEject(slot, item, extraProgress)) { assert(false, "Failed to eject"); } else { processorComp.queuedEjects.splice(j, 1); @@ -150,53 +146,21 @@ export class ItemProcessorSystem extends GameSystemWithFilter { } } - /** - * Returns true if the entity should accept the given item on the given slot. - * This should only be called with matching items! I.e. if a color item is expected - * on the given slot, then only a color item must be passed. - * @param {Entity} entity - * @param {BaseItem} item The item to accept - * @param {number} slotIndex The slot index - * @returns {boolean} - */ - checkRequirements(entity, item, slotIndex) { - const itemProcessorComp = entity.components.ItemProcessor; - const pinsComp = entity.components.WiredPins; - - switch (itemProcessorComp.processingRequirement) { - case enumItemProcessorRequirements.painterQuad: { - if (slotIndex === 0) { - // Always accept the shape - return true; - } - - // Check the network value at the given slot - const network = pinsComp.slots[slotIndex - 1].linkedNetwork; - const slotIsEnabled = network && network.hasValue() && isTruthyItem(network.currentValue); - if (!slotIsEnabled) { - return false; - } - return true; - } - - // By default, everything is accepted - default: - return true; - } - } + // requirements are no longer needed as items will always be accepted, only the next method is. /** * Checks whether it's possible to process something * @param {Entity} entity */ canProcess(entity) { + const acceptorComp = entity.components.ItemAcceptor; const processorComp = entity.components.ItemProcessor; switch (processorComp.processingRequirement) { // DEFAULT // By default, we can start processing once all inputs are there case null: { - return processorComp.inputCount >= processorComp.inputsPerCharge; + return acceptorComp.completedInputs.length >= processorComp.inputsPerCharge; } // QUAD PAINTER @@ -204,8 +168,13 @@ export class ItemProcessorSystem extends GameSystemWithFilter { case enumItemProcessorRequirements.painterQuad: { const pinsComp = entity.components.WiredPins; + const input = acceptorComp.completedInputs[0]; + if (!input) { + return false; + } + // First slot is the shape, so if it's not there we can't do anything - const shapeItem = /** @type {ShapeItem} */ (processorComp.inputSlots.get(0)); + const shapeItem = /** @type {ShapeItem} */ (input.item); if (!shapeItem) { return false; } @@ -234,7 +203,10 @@ export class ItemProcessorSystem extends GameSystemWithFilter { // Check if all colors of the enabled slots are there for (let i = 0; i < slotStatus.length; ++i) { - if (slotStatus[i] && !processorComp.inputSlots.get(1 + i)) { + if ( + slotStatus[i] && + !acceptorComp.completedInputs.find(input => input.slotIndex == i + 1) // @TODO this is slow + ) { // A slot which is enabled wasn't enabled. Make sure if there is anything on the quadrant, // it is not possible to paint, but if there is nothing we can ignore it for (let j = 0; j < 4; ++j) { @@ -259,10 +231,21 @@ export class ItemProcessorSystem extends GameSystemWithFilter { * @param {Entity} entity */ startNewCharge(entity) { + const acceptorComp = entity.components.ItemAcceptor; const processorComp = entity.components.ItemProcessor; - // First, take items - const items = processorComp.inputSlots; + // First, take inputs - but only one from each + const inputs = acceptorComp.completedInputs; + + // split inputs efficiently + let items = new Map(); + let extraProgress = 0; + for (let i = 0; i < inputs.length; i++) { + const input = inputs[i]; + + items.set(input.slotIndex, input.item); + extraProgress = Math.max(extraProgress, input.extraProgress); + } /** @type {Array} */ const outItems = []; @@ -276,7 +259,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter { entity, items, outItems, - inputCount: processorComp.inputCount, }); // Track produced items @@ -284,23 +266,35 @@ export class ItemProcessorSystem extends GameSystemWithFilter { if (!outItems[i].doNotTrack) { this.root.signals.itemProduced.dispatch(outItems[i].item); } + + // also set extra progress + outItems[i].extraProgress = extraProgress; } // Queue Charge - const baseSpeed = this.root.hubGoals.getProcessorBaseSpeed(processorComp.type); - const originalTime = 1 / baseSpeed; + const originalTime = this.root.hubGoals.getProcessingTime(processorComp.type); const bonusTimeToApply = Math.min(originalTime, processorComp.bonusTime); const timeToProcess = originalTime - bonusTimeToApply; processorComp.bonusTime -= bonusTimeToApply; - processorComp.ongoingCharges.push({ + + processorComp.currentCharge = { items: outItems, remainingTime: timeToProcess, - }); + }; - processorComp.inputSlots.clear(); - processorComp.inputCount = 0; + // only remove one item from each slot - we don't want to delete extra items! + let usedSlots = []; + for (let i = 0; i < acceptorComp.completedInputs.length; i++) { + const index = acceptorComp.completedInputs[i].slotIndex; + + if (!usedSlots.includes(index)) { + usedSlots.push(index); + acceptorComp.completedInputs.splice(i, 1); + i--; + } + } } /** @@ -445,7 +439,14 @@ export class ItemProcessorSystem extends GameSystemWithFilter { * @param {ProcessorImplementationPayload} payload */ process_TRASH(payload) { - // Do nothing .. + // Hardcoded - 4 inputs + for (let i = 0; i < 4; ++i) { + const item = /** @type {ShapeItem} */ (payload.items.get(i)); + if (!item) { + continue; + } + payload.entity.root.signals.achievementCheck.dispatch(ACHIEVEMENTS.trash1000, 1); + } } /** @@ -569,8 +570,8 @@ export class ItemProcessorSystem extends GameSystemWithFilter { const hubComponent = payload.entity.components.Hub; assert(hubComponent, "Hub item processor has no hub component"); - // Hardcoded - for (let i = 0; i < payload.inputCount; ++i) { + // Hardcoded - 16 inputs + for (let i = 0; i < 16; ++i) { const item = /** @type {ShapeItem} */ (payload.items.get(i)); if (!item) { continue; diff --git a/src/js/game/systems/storage.js b/src/js/game/systems/storage.js index 20204a89..1fd23d25 100644 --- a/src/js/game/systems/storage.js +++ b/src/js/game/systems/storage.js @@ -31,6 +31,9 @@ export class StorageSystem extends GameSystemWithFilter { const storageComp = entity.components.Storage; const pinsComp = entity.components.WiredPins; + // storage needs to delete completed inputs, since the items are already added + entity.components.ItemAcceptor.completedInputs = []; + // Eject from storage if (storageComp.storedItem && storageComp.storedCount > 0) { const ejectorComp = entity.components.ItemEjector; diff --git a/src/js/game/systems/underground_belt.js b/src/js/game/systems/underground_belt.js index 9b31eec1..aa3df6cc 100644 --- a/src/js/game/systems/underground_belt.js +++ b/src/js/game/systems/underground_belt.js @@ -3,7 +3,6 @@ import { Loader } from "../../core/loader"; import { createLogger } from "../../core/logging"; import { Rectangle } from "../../core/rectangle"; import { StaleAreaDetector } from "../../core/stale_area_detector"; -import { fastArrayDelete } from "../../core/utils"; import { enumAngleToDirection, enumDirection, @@ -225,7 +224,11 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { this.staleAreaWatcher.update(); const sender = enumUndergroundBeltMode.sender; - const now = this.root.time.now(); + + const progressGrowth = + this.root.dynamicTickrate.deltaSeconds * + this.root.hubGoals.getBeltBaseSpeed() * + globalConfig.itemSpacingOnBelts; for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; @@ -233,7 +236,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { if (undergroundComp.mode === sender) { this.handleSender(entity); } else { - this.handleReceiver(entity, now); + this.handleReceiver(entity, progressGrowth); } } } @@ -253,8 +256,8 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { // Search in the direction of the tunnel for ( - let searchOffset = 0; - searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier]; + let searchOffset = 1; + searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier] + 1; ++searchOffset ) { currentTile = currentTile.add(searchVector); @@ -281,6 +284,8 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { break; } + // make sure to link the other way as well + receiverUndergroundComp.cachedLinkedEntity = { entity: null, distance: searchOffset }; return { entity: potentialReceiver, distance: searchOffset }; } @@ -294,6 +299,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { */ handleSender(entity) { const undergroundComp = entity.components.UndergroundBelt; + const acceptorComp = entity.components.ItemAcceptor; // Find the current receiver let cacheEntry = undergroundComp.cachedLinkedEntity; @@ -307,22 +313,17 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { return; } - // Check if we have any items to eject - const nextItemAndDuration = undergroundComp.pendingItems[0]; - if (nextItemAndDuration) { - assert(undergroundComp.pendingItems.length === 1, "more than 1 pending"); - + const input = acceptorComp.completedInputs[0]; + if (input) { // Check if the receiver can accept it if ( cacheEntry.entity.components.UndergroundBelt.tryAcceptTunneledItem( - nextItemAndDuration[0], + input.item, cacheEntry.distance, - this.root.hubGoals.getUndergroundBeltBaseSpeed(), - this.root.time.now() + input.extraProgress ) ) { - // Drop this item - fastArrayDelete(undergroundComp.pendingItems, 0); + acceptorComp.completedInputs.shift(); } } } @@ -330,20 +331,28 @@ export class UndergroundBeltSystem extends GameSystemWithFilter { /** * * @param {Entity} entity - * @param {number} now + * @param {number} progressGrowth */ - handleReceiver(entity, now) { + handleReceiver(entity, progressGrowth) { const undergroundComp = entity.components.UndergroundBelt; - // Try to eject items, we only check the first one because it is sorted by remaining time - const nextItemAndDuration = undergroundComp.pendingItems[0]; - if (nextItemAndDuration) { - if (now > nextItemAndDuration[1]) { + if (!undergroundComp.cachedLinkedEntity) return; + const distance = undergroundComp.cachedLinkedEntity.distance; + + // Move items along + for (let i = 0; i < undergroundComp.pendingItems.length; i++) { + const itemAndProgress = undergroundComp.pendingItems[i]; + if (itemAndProgress[1] < distance) { + itemAndProgress[1] += progressGrowth; + } + + if (itemAndProgress[1] >= distance) { const ejectorComp = entity.components.ItemEjector; const nextSlotIndex = ejectorComp.getFirstFreeSlot(); if (nextSlotIndex !== null) { - if (ejectorComp.tryEject(nextSlotIndex, nextItemAndDuration[0])) { + const extraProgress = itemAndProgress[1] - distance; + if (ejectorComp.tryEject(nextSlotIndex, itemAndProgress[0], extraProgress)) { undergroundComp.pendingItems.shift(); } } From 5b036d7f5b27c0a0535ccd7cd34f18edd4d63900 Mon Sep 17 00:00:00 2001 From: Sense101 Date: Thu, 3 Feb 2022 16:10:46 +0000 Subject: [PATCH 02/11] Revert back to exact speed - doesn't eject fast enough on higher speeds --- src/js/game/belt_path.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index bf797c61..f172b1a3 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -119,14 +119,6 @@ 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 @@ -135,15 +127,10 @@ export class BeltPath extends BasicSerializableObject { tryAcceptItem(item, extraProgress = 0) { if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) { // So, since we already need one tick to accept this item we will add this directly. - // this means we are moving it forwards twice in one tick, but otherwise belts won't be full :( - const beltProgressPerTick = - this.root.hubGoals.getBeltBaseSpeed() * - this.root.dynamicTickrate.deltaSeconds * - 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 + extraProgress); + const initialProgress = Math.min(maxProgress, extraProgress); this.items.unshift([this.spacingToFirstItem - initialProgress, item]); this.spacingToFirstItem = initialProgress; From 07f89c45a393a99ee5f5567af0f6f731ce1e648c Mon Sep 17 00:00:00 2001 From: Sense101 Date: Sun, 6 Feb 2022 15:42:07 +0000 Subject: [PATCH 03/11] Fix miner and item producer, and refactor chained miners --- src/js/game/components/miner.js | 41 +++----- src/js/game/game_system_manager.js | 4 +- src/js/game/hud/parts/building_placer.js | 31 +++--- src/js/game/hud/parts/miner_highlight.js | 7 +- src/js/game/systems/item_ejector.js | 11 +- src/js/game/systems/item_producer.js | 4 +- src/js/game/systems/miner.js | 125 +++++++++++++++-------- 7 files changed, 130 insertions(+), 93 deletions(-) diff --git a/src/js/game/components/miner.js b/src/js/game/components/miner.js index 5321ae11..54ac3b4a 100644 --- a/src/js/game/components/miner.js +++ b/src/js/game/components/miner.js @@ -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} + * 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; } } diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index d8c16200..603511b1 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -150,12 +150,12 @@ export class GameSystemManager { add("belt", BeltSystem); - add("miner", MinerSystem); - add("storage", StorageSystem); add("itemEjector", ItemEjectorSystem); + add("miner", MinerSystem); + add("undergroundBelt", UndergroundBeltSystem); add("itemProcessor", ItemProcessorSystem); diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index d2904720..218af719 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -303,17 +303,14 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { const mouseTile = worldPos.toTileSpace(); // Compute best rotation variant - const { - rotation, - rotationVariant, - connectedEntities, - } = metaBuilding.computeOptimalDirectionAndRotationVariantAtTile({ - root: this.root, - tile: mouseTile, - rotation: this.currentBaseRotation, - variant: this.currentVariant.get(), - layer: metaBuilding.getLayer(), - }); + const { rotation, rotationVariant, connectedEntities } = + metaBuilding.computeOptimalDirectionAndRotationVariantAtTile({ + root: this.root, + tile: mouseTile, + rotation: this.currentBaseRotation, + variant: this.currentVariant.get(), + layer: metaBuilding.getLayer(), + }); // Check if there are connected entities if (connectedEntities) { @@ -657,8 +654,16 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { // Connected to a belt isConnected = true; } else if (minerComp && minerComp.chainable && destMiner && destMiner.chainable) { - // Chainable miners connected to eachother - isConnected = true; + 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; diff --git a/src/js/game/hud/parts/miner_highlight.js b/src/js/game/hud/parts/miner_highlight.js index a0c6919d..0e5faef1 100644 --- a/src/js/game/hud/parts/miner_highlight.js +++ b/src/js/game/hud/parts/miner_highlight.js @@ -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) { diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index ea8e7381..0cc871c5 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -163,7 +163,14 @@ export class ItemEjectorSystem extends GameSystemWithFilter { continue; } - if (sourceSlot.progress < maxProgress) { + // 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 += progressGrowth; } @@ -179,8 +186,6 @@ export class ItemEjectorSystem extends GameSystemWithFilter { const extraProgress = sourceSlot.progress - maxProgress; - // Check if we are ejecting to a belt path - const destPath = sourceSlot.cachedBeltPath; if (destPath) { // Try passing the item over if (destPath.tryAcceptItem(item, extraProgress)) { diff --git a/src/js/game/systems/item_producer.js b/src/js/game/systems/item_producer.js index 8ca29ae1..ec194df0 100644 --- a/src/js/game/systems/item_producer.js +++ b/src/js/game/systems/item_producer.js @@ -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); } } } diff --git a/src/js/game/systems/miner.js b/src/js/game/systems/miner.js index cd478be3..9404c4ca 100644 --- a/src/js/game/systems/miner.js +++ b/src/js/game/systems/miner.js @@ -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,60 @@ 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; + exitMinerComp.progress += progressGrowth; continue; } } + //console.log(minerComp.progress); - 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); + 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 +125,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 +142,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 +158,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); - } - - // Check if we now have a target - if (targetEntity) { - const targetMinerComp = targetEntity.components.Miner; - if (targetMinerComp.tryAcceptChainedItem(item)) { - return true; - } else { - return false; - } + /** @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; } - // Seems we are a regular miner or at the end of a row, try actually ejecting - if (ejectComp.tryEject(0, item)) { - return true; + if (targetEntity) { + const targetMinerComp = targetEntity.components.Miner; + if (targetMinerComp.cachedMinedItem == minerComp.cachedMinedItem) { + // only chain the same items + return targetEntity; + } } - return false; } From 694b95c25cc2be2556423f60a14e5a27b021d576 Mon Sep 17 00:00:00 2001 From: Sense101 Date: Sun, 6 Feb 2022 15:42:35 +0000 Subject: [PATCH 04/11] Add save migration --- src/js/game/components/item_processor.js | 15 +++++++ src/js/savegame/savegame.js | 8 +++- .../savegame/savegame_interface_registry.js | 2 + src/js/savegame/schemas/1011.js | 44 +++++++++++++++++++ src/js/savegame/schemas/1011.json | 5 +++ 5 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/js/savegame/schemas/1011.js create mode 100644 src/js/savegame/schemas/1011.json diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index 9da8489a..da3dd3e3 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -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 = { @@ -54,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), + }) + ), + }) + ), }; } diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index b4472b2b..4c274759 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -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(); } diff --git a/src/js/savegame/savegame_interface_registry.js b/src/js/savegame/savegame_interface_registry.js index 089b15fc..fb645b57 100644 --- a/src/js/savegame/savegame_interface_registry.js +++ b/src/js/savegame/savegame_interface_registry.js @@ -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.} */ 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"); diff --git a/src/js/savegame/schemas/1011.js b/src/js/savegame/schemas/1011.js new file mode 100644 index 00000000..8f203250 --- /dev/null +++ b/src/js/savegame/schemas/1011.js @@ -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 Date: Sun, 6 Feb 2022 15:57:48 +0000 Subject: [PATCH 05/11] Minor cleanup --- src/js/game/systems/miner.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/js/game/systems/miner.js b/src/js/game/systems/miner.js index 9404c4ca..36305548 100644 --- a/src/js/game/systems/miner.js +++ b/src/js/game/systems/miner.js @@ -76,11 +76,13 @@ export class MinerSystem extends GameSystemWithFilter { const exitEntity = minerComp.cachedExitMiner; if (exitEntity) { const exitMinerComp = exitEntity.components.Miner; - exitMinerComp.progress += progressGrowth; + if (exitMinerComp.progress < targetProgress + 0.5) { + // we can add on some extra progress + exitMinerComp.progress += progressGrowth; + } continue; } } - //console.log(minerComp.progress); if (minerComp.progress >= targetProgress) { // We can try to eject From 0cacbcd2674380ec42145975df27aabd7b0cf4e3 Mon Sep 17 00:00:00 2001 From: Sense101 Date: Tue, 8 Feb 2022 14:39:43 +0000 Subject: [PATCH 06/11] Clear duplicated code and add input requirements for acceptor --- src/js/game/belt_path.js | 11 +-- src/js/game/buildings/painter.js | 3 +- src/js/game/buildings/storage.js | 3 +- src/js/game/components/item_acceptor.js | 92 ++++++++++++++++++++++--- src/js/game/systems/item_ejector.js | 12 +--- src/js/game/systems/item_processor.js | 52 +++++++------- 6 files changed, 115 insertions(+), 58 deletions(-) diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index f172b1a3..bb59b19b 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -263,16 +263,7 @@ export class BeltPath extends BasicSerializableObject { const matchingSlotIndex = matchingSlot.index; return function (item, startProgress = 0.0) { - const storageComp = targetEntity.components.Storage; - if ( - storageComp && - storageComp.tryAcceptItem(item) && - targetAcceptorComp.tryAcceptItem(matchingSlotIndex, item, startProgress) - ) { - // unique duplicated code for storage - return true; - } - if (targetAcceptorComp.tryAcceptItem(matchingSlotIndex, item, startProgress)) { + if (targetAcceptorComp.tryAcceptItem(targetEntity, matchingSlotIndex, item, startProgress)) { return true; } return false; diff --git a/src/js/game/buildings/painter.js b/src/js/game/buildings/painter.js index 432973d0..da95f4db 100644 --- a/src/js/game/buildings/painter.js +++ b/src/js/game/buildings/painter.js @@ -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 }, diff --git a/src/js/game/buildings/storage.js b/src/js/game/buildings/storage.js index 78f398be..92355557 100644 --- a/src/js/game/buildings/storage.js +++ b/src/js/game/buildings/storage.js @@ -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, }) ); diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index 987de239..bc74d9d0 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -3,6 +3,7 @@ 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"; @@ -49,6 +50,12 @@ import { GameRoot } from "../root"; * }} InputCompletedArgs */ +/** @enum {string} */ +export const enumInputRequirements = { + quadPainter: "quadPainter", + storage: "storage", +}; + export class ItemAcceptorComponent extends Component { static getId() { return "ItemAcceptor"; @@ -78,18 +85,26 @@ export class ItemAcceptorComponent extends Component { * @param {object} param0 * @param {Array} param0.slots The slots from which we accept items * @param {number=} param0.maxSlotInputs The maximum amount of items one slot can accept before it is full + * @param {string|null=} param0.inputRequirement The requirement to accept items */ - constructor({ slots = [], maxSlotInputs = 2 }) { + 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() { /** @type {ItemAcceptorInputs} */ this.inputs = []; /** @type {ItemAcceptorCompletedInputs} */ this.completedInputs = []; - this.setSlots(slots); - - // setting this to 1 will cause throughput issues at very high speeds - this.maxSlotInputs = maxSlotInputs; } /** @@ -111,16 +126,74 @@ export class ItemAcceptorComponent extends Component { } } + /** + * + * @param {Entity} entity + * @param {BaseItem} item + * @param {number} slotIndex + * @returns + */ + canAcceptItem(entity, item, slotIndex) { + const slot = this.slots[slotIndex]; + + // make sure there is a slot and we match the filter + if (slot && !(slot.filter && slot.filter != item.getItemType())) { + 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: " + slot.filter); + break; + } + } + } + return false; + } + /** * Called when trying to input a new item + * @param {Entity} entity * @param {number} slotIndex * @param {BaseItem} 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 */ - tryAcceptItem(slotIndex, item, startProgress = 0.0) { - const slot = this.slots[slotIndex]; - + 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) { @@ -136,8 +209,7 @@ export class ItemAcceptorComponent extends Component { if (existingInputs >= this.maxSlotInputs) { return false; } - - if (slot.filter && slot.filter != item.getItemType()) { + if (!this.canAcceptItem(entity, item, slotIndex)) { return false; } diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index 0cc871c5..e7fe22e0 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -202,17 +202,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter { const destSlot = sourceSlot.cachedDestSlot; if (destEntity && destSlot) { const targetAcceptorComp = destEntity.components.ItemAcceptor; - const storageComp = destEntity.components.Storage; - if ( - storageComp && - storageComp.tryAcceptItem(item) && - targetAcceptorComp.tryAcceptItem(destSlot.index, item, extraProgress) - ) { - // unique duplicated code for storage - hacky :( - sourceSlot.item = null; - return; - } - if (targetAcceptorComp.tryAcceptItem(destSlot.index, item, extraProgress)) { + if (targetAcceptorComp.tryAcceptItem(destEntity, destSlot.index, item, extraProgress)) { // Handover successful, clear slot sourceSlot.item = null; } diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index efbdf516..774cfdcc 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -146,7 +146,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter { } } - // requirements are no longer needed as items will always be accepted, only the next method is. + // 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 @@ -160,21 +160,35 @@ export class ItemProcessorSystem extends GameSystemWithFilter { // DEFAULT // By default, we can start processing once all inputs are there case null: { - return acceptorComp.completedInputs.length >= 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; - const input = acceptorComp.completedInputs[0]; - if (!input) { - return false; + // 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} */ (input.item); + const shapeItem = /** @type {ShapeItem} */ (items.get(0)); if (!shapeItem) { return false; } @@ -203,10 +217,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] && - !acceptorComp.completedInputs.find(input => input.slotIndex == i + 1) // @TODO this is slow - ) { + 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) { @@ -217,7 +228,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter { } } } - return true; } @@ -243,8 +253,12 @@ export class ItemProcessorSystem extends GameSystemWithFilter { for (let i = 0; i < inputs.length; i++) { const input = inputs[i]; - items.set(input.slotIndex, input.item); - extraProgress = Math.max(extraProgress, input.extraProgress); + if (!items.get(input.slotIndex)) { + items.set(input.slotIndex, input.item); + extraProgress = Math.max(extraProgress, input.extraProgress); + inputs.splice(i, 1); + i--; + } } /** @type {Array} */ @@ -283,18 +297,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter { items: outItems, remainingTime: timeToProcess, }; - - // only remove one item from each slot - we don't want to delete extra items! - let usedSlots = []; - for (let i = 0; i < acceptorComp.completedInputs.length; i++) { - const index = acceptorComp.completedInputs[i].slotIndex; - - if (!usedSlots.includes(index)) { - usedSlots.push(index); - acceptorComp.completedInputs.splice(i, 1); - i--; - } - } } /** From e7a4307a5ee1e0a2eb21d69dc004d9fd8f0cb6a9 Mon Sep 17 00:00:00 2001 From: Sense101 Date: Tue, 8 Feb 2022 14:43:18 +0000 Subject: [PATCH 07/11] Add mod hook for adding custom input requirements --- src/js/game/components/item_acceptor.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index bc74d9d0..1f544d76 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -56,6 +56,8 @@ export const enumInputRequirements = { storage: "storage", }; +export const MOD_INPUT_REQUIREMENTS = []; + export class ItemAcceptorComponent extends Component { static getId() { return "ItemAcceptor"; @@ -138,6 +140,14 @@ export class ItemAcceptorComponent extends Component { // 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].bind(this)({ + entity, + item, + slotIndex, + }); + } + switch (this.inputRequirement) { case null: { return true; From 4dd4724b2913e553460543792ec9188e6cc7b0f6 Mon Sep 17 00:00:00 2001 From: Sense101 Date: Tue, 8 Feb 2022 15:03:06 +0000 Subject: [PATCH 08/11] add extra cap for miner progress just to be sure --- src/js/game/systems/miner.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/js/game/systems/miner.js b/src/js/game/systems/miner.js index 36305548..fe76f4ed 100644 --- a/src/js/game/systems/miner.js +++ b/src/js/game/systems/miner.js @@ -84,6 +84,8 @@ export class MinerSystem extends GameSystemWithFilter { } } + //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; From 31bea991cb1a0ab86acd59b826289cdc9e6751f8 Mon Sep 17 00:00:00 2001 From: Sense101 Date: Thu, 10 Feb 2022 00:29:01 +0000 Subject: [PATCH 09/11] Fixed item input requirement mod handling --- src/js/game/components/item_acceptor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index 1f544d76..60c78e86 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -141,7 +141,7 @@ export class ItemAcceptorComponent extends Component { // 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].bind(this)({ + return MOD_INPUT_REQUIREMENTS[this.inputRequirement].call(this, { entity, item, slotIndex, From 55f968ea4a8192dd6b87899f530abbfc9de08573 Mon Sep 17 00:00:00 2001 From: Sense101 Date: Sun, 20 Feb 2022 13:38:08 +0000 Subject: [PATCH 10/11] Cleanup and processing improvement --- src/js/game/components/item_acceptor.js | 2 +- src/js/game/components/storage.js | 19 ------------------- src/js/game/systems/item_processor.js | 16 ++++++++++++++-- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index 60c78e86..d015e5cf 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -186,7 +186,7 @@ export class ItemAcceptorComponent extends Component { return true; } default: { - assertAlways(false, "Input requirement is not recognised: " + slot.filter); + assertAlways(false, "Input requirement is not recognised: " + this.inputRequirement); break; } } diff --git a/src/js/game/components/storage.js b/src/js/game/components/storage.js index 9b48877a..905fd71f 100644 --- a/src/js/game/components/storage.js +++ b/src/js/game/components/storage.js @@ -40,25 +40,6 @@ export class StorageComponent extends Component { this.overlayOpacity = 0; } - /** - * Returns whether this storage can accept the item - * @param {BaseItem} item - */ - tryAcceptItem(item) { - if (this.storedCount >= this.maximumStorage) { - return false; - } - const itemType = item.getItemType(); - if (this.storedCount > 0 && this.storedItem && itemType !== this.storedItem.getItemType()) { - return false; - } - - this.storedItem = item; - this.storedCount++; - - return true; - } - /** * Returns whether the storage is full * @returns {boolean} diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 774cfdcc..1790525c 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -106,6 +106,18 @@ export class ItemProcessorSystem extends GameSystemWithFilter { } 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--; + } + } } } @@ -256,8 +268,8 @@ export class ItemProcessorSystem extends GameSystemWithFilter { if (!items.get(input.slotIndex)) { items.set(input.slotIndex, input.item); extraProgress = Math.max(extraProgress, input.extraProgress); - inputs.splice(i, 1); - i--; + //inputs.splice(i, 1); + //i--; } } From bf17ccf202500961d86b660b39c464f9549285bf Mon Sep 17 00:00:00 2001 From: Sense101 Date: Sat, 5 Mar 2022 21:42:29 +0000 Subject: [PATCH 11/11] Update to latest version --- README.md | 2 +- gulp/webpack.config.js | 1 - mod_examples/README.md | 2 +- mod_examples/class_extensions.js | 4 +- mod_examples/new_item_type.js | 26 ++++----- src/js/changelog.js | 7 ++- src/js/core/game_state.js | 1 + src/js/core/query_parameters.js | 12 ---- src/js/core/rectangle.js | 2 +- src/js/core/restriction_manager.js | 5 -- src/js/core/sprites.js | 1 + src/js/game/base_item.js | 5 +- src/js/game/belt_path.js | 12 +++- src/js/game/component.js | 1 + src/js/game/entity.js | 1 + src/js/game/game_mode.js | 1 + src/js/game/hud/base_hud_part.js | 1 + .../game/hud/parts/building_placer_logic.js | 2 +- src/js/game/hud/parts/keybinding_overlay.js | 2 +- src/js/game/item_resolver.js | 2 +- src/js/game/logic.js | 14 ++++- src/js/game/meta_building.js | 8 ++- src/js/game/modes/puzzle_edit.js | 2 + src/js/game/modes/regular.js | 17 ++---- src/js/game/systems/item_processor.js | 11 ++++ src/js/game/systems/wired_pins.js | 18 +++++- src/js/platform/achievement_provider.js | 4 ++ src/js/platform/ad_provider.js | 2 + src/js/platform/analytics.js | 1 + src/js/platform/browser/game_analytics.js | 10 ---- src/js/platform/game_analytics.js | 2 + src/js/platform/storage.js | 3 + src/js/platform/wrapper.js | 4 ++ src/js/profile/setting_types.js | 3 + src/js/savegame/serialization_data_types.js | 4 ++ translations/base-ja.yaml | 10 +--- translations/base-ru.yaml | 57 ++++++++++--------- translations/base-sv.yaml | 2 +- version | 2 +- 39 files changed, 154 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 2b8d208b..3d93ece5 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Your goal is to produce shapes by cutting, rotating, merging and painting parts ## Building - Make sure `ffmpeg` is on your path -- Install Node.js and Yarn +- Install Node.js (v16.0 or earlier) and Yarn - Install Java (required for textures) - Run `yarn` in the root folder - Cd into `gulp` folder diff --git a/gulp/webpack.config.js b/gulp/webpack.config.js index c696d2bd..3c07d7cc 100644 --- a/gulp/webpack.config.js +++ b/gulp/webpack.config.js @@ -97,7 +97,6 @@ module.exports = ({ watch = false, standalone = false, chineseVersion = false, w loader: path.resolve(__dirname, "mod.js"), }, ], - ], }, { test: /\.worker\.js$/, diff --git a/mod_examples/README.md b/mod_examples/README.md index c0b101e4..b8e501c8 100644 --- a/mod_examples/README.md +++ b/mod_examples/README.md @@ -50,7 +50,7 @@ To get into shapez.io modding, I highly recommend checking out all of the exampl | [usage_statistics.js](usage_statistics.js) | Displays a percentage on every building showing its utilization | Adding a new component, Adding a new GameSystem, Drawing within a GameSystem, Modifying builtin buildings, Adding custom game logic | | [new_item_type.js](new_item_type.js) | Adds a new type of items to the map (fluids) | Adding a new item type, modifying map generation | | [buildings_have_cost.js](buildings_have_cost.js) | Adds a new currency, and belts cost 1 of that currency | Extending and replacing builtin methods, Adding CSS and custom sprites | -| [mirrored_cutter.js](mirrored_cutter.js) | Adds a mirorred variant of the cutter | Adding a new variant to existing buildings | +| [mirrored_cutter.js](mirrored_cutter.js) | Adds a mirrored variant of the cutter | Adding a new variant to existing buildings | ### Creating new sprites diff --git a/mod_examples/class_extensions.js b/mod_examples/class_extensions.js index 8647fd45..ace5aae9 100644 --- a/mod_examples/class_extensions.js +++ b/mod_examples/class_extensions.js @@ -15,9 +15,9 @@ const BeltExtension = ({ $super, $old }) => ({ return !$old.getShowWiresLayerPreview(); }, - getIsReplaceable() { + getIsReplaceable(variant, rotationVariant) { // Instead of super, use $super - return $super.getIsReplaceable.call(this); + return $super.getIsReplaceable.call(this, variant, rotationVariant); }, getIsRemoveable() { diff --git a/mod_examples/new_item_type.js b/mod_examples/new_item_type.js index 3f47d4d2..104ef0a0 100644 --- a/mod_examples/new_item_type.js +++ b/mod_examples/new_item_type.js @@ -87,7 +87,7 @@ class FluidItem extends shapez.BaseItem { * @param {number} diameter * @param {DrawParameters} parameters */ - drawItemCenteredClipped(x, y, parameters, diameter = globalConfig.defaultItemDiameter) { + drawItemCenteredClipped(x, y, parameters, diameter = shapez.globalConfig.defaultItemDiameter) { const realDiameter = diameter * 0.6; if (!this.cachedSprite) { this.cachedSprite = shapez.Loader.getSprite(`sprites/fluids/${this.fluidType}.png`); @@ -120,19 +120,19 @@ class Mod extends shapez.Mod { this.modInterface.registerSprite("sprites/fluids/water.png", RESOURCES["water.png"]); // Make the item spawn on the map - this.modInterface.runAfterMethod( - shapez.MapChunk, - "generatePatches", - function ({ rng, chunkCenter, distanceToOriginInChunks }) { - // Generate a simple patch - // ALWAYS use rng and NEVER use Math.random() otherwise the map will look different - // every time you resume the game - if (rng.next() > 0.8) { - const fluidType = rng.choice(Array.from(Object.keys(enumFluidType))); - this.internalGeneratePatch(rng, 4, FLUID_ITEM_SINGLETONS[fluidType]); - } + this.modInterface.runAfterMethod(shapez.MapChunk, "generatePatches", function ({ + rng, + chunkCenter, + distanceToOriginInChunks, + }) { + // Generate a simple patch + // ALWAYS use rng and NEVER use Math.random() otherwise the map will look different + // every time you resume the game + if (rng.next() > 0.8) { + const fluidType = rng.choice(Array.from(Object.keys(enumFluidType))); + this.internalGeneratePatch(rng, 4, FLUID_ITEM_SINGLETONS[fluidType]); } - ); + }); this.modInterface.registerItem(FluidItem, itemData => FLUID_ITEM_SINGLETONS[itemData]); } diff --git a/src/js/changelog.js b/src/js/changelog.js index 05c7395c..6640dd9e 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -1,9 +1,10 @@ export const CHANGELOG = [ { - version: "1.5.0", - date: "unreleased", + version: "1.5.1", + date: "25.02.2022", entries: [ - "This version adds an official modloader! You can now load mods by placing it in the mods/ folder of the game.", + "This version adds an official modloader! You can now load mods by extracting them and placing the .js file in the mods/ folder of the game.", + "Mods can be found here", "When holding shift while placing a belt, the indicator now becomes red when crossing buildings", "Lots of performance improvements, leading to up to 50% more FPS", ], diff --git a/src/js/core/game_state.js b/src/js/core/game_state.js index b08bef77..fb08e28d 100644 --- a/src/js/core/game_state.js +++ b/src/js/core/game_state.js @@ -211,6 +211,7 @@ export class GameState { /** * Should return the html code of the state. * @returns {string} + * @abstract */ getInnerHTML() { abstract; diff --git a/src/js/core/query_parameters.js b/src/js/core/query_parameters.js index 7837acb5..b3dab1b3 100644 --- a/src/js/core/query_parameters.js +++ b/src/js/core/query_parameters.js @@ -3,20 +3,8 @@ const options = queryString.parse(location.search); export let queryParamOptions = { embedProvider: null, - fullVersion: false, - sandboxMode: false, }; if (options.embed) { queryParamOptions.embedProvider = options.embed; } - -// Allow testing full version outside of standalone -if (options.fullVersion && !G_IS_RELEASE) { - queryParamOptions.fullVersion = true; -} - -// Allow testing full version outside of standalone -if (options.sandboxMode && !G_IS_RELEASE) { - queryParamOptions.sandboxMode = true; -} diff --git a/src/js/core/rectangle.js b/src/js/core/rectangle.js index bd3421d9..e2c85082 100644 --- a/src/js/core/rectangle.js +++ b/src/js/core/rectangle.js @@ -268,7 +268,7 @@ export class Rectangle { } /** - * Returns if hte rectangle contains the given point + * Returns if the rectangle contains the given point * @param {number} x * @param {number} y * @returns {boolean} diff --git a/src/js/core/restriction_manager.js b/src/js/core/restriction_manager.js index c899b494..3ca30597 100644 --- a/src/js/core/restriction_manager.js +++ b/src/js/core/restriction_manager.js @@ -84,11 +84,6 @@ export class RestrictionManager extends ReadWriteProxy { return false; } - if (queryParamOptions.fullVersion) { - // Full version is activated via flag - return false; - } - if (queryParamOptions.embedProvider === "gamedistribution") { // also full version on gamedistribution return false; diff --git a/src/js/core/sprites.js b/src/js/core/sprites.js index 51032e4e..d568f994 100644 --- a/src/js/core/sprites.js +++ b/src/js/core/sprites.js @@ -11,6 +11,7 @@ export class BaseSprite { /** * Returns the raw handle * @returns {HTMLImageElement|HTMLCanvasElement} + * @abstract */ getRawTexture() { abstract; diff --git a/src/js/game/base_item.js b/src/js/game/base_item.js index f6ed1672..a6b38a08 100644 --- a/src/js/game/base_item.js +++ b/src/js/game/base_item.js @@ -29,6 +29,7 @@ export class BaseItem extends BasicSerializableObject { /** * Returns a string id of the item * @returns {string} + * @abstract */ getAsCopyableKey() { abstract; @@ -49,9 +50,9 @@ export class BaseItem extends BasicSerializableObject { /** * Override for custom comparison - * @abstract * @param {BaseItem} other * @returns {boolean} + * @abstract */ equalsImpl(other) { abstract; @@ -62,6 +63,7 @@ export class BaseItem extends BasicSerializableObject { * Draws the item to a canvas * @param {CanvasRenderingContext2D} context * @param {number} size + * @abstract */ drawFullSizeOnCanvas(context, size) { abstract; @@ -86,6 +88,7 @@ export class BaseItem extends BasicSerializableObject { * @param {number} y * @param {DrawParameters} parameters * @param {number=} diameter + * @abstract */ drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) { abstract; diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index bb59b19b..6cda9f22 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -1526,7 +1526,17 @@ export class BeltPath extends BasicSerializableObject { const sprite = this.root.buffers.getForKey({ key: "beltpaths", - subKey: "stack-" + directionProp + "-" + dpi + "-" + stack.length + firstItem[1].serialize(), + subKey: + "stack-" + + directionProp + + "-" + + dpi + + "#" + + stack.length + + "#" + + firstItem[1].getItemType() + + "#" + + firstItem[1].serialize(), dpi, w: dimensions.x, h: dimensions.y, diff --git a/src/js/game/component.js b/src/js/game/component.js index cff14d62..9e1b63f4 100644 --- a/src/js/game/component.js +++ b/src/js/game/component.js @@ -4,6 +4,7 @@ export class Component extends BasicSerializableObject { /** * Returns the components unique id * @returns {string} + * @abstract */ static getId() { abstract; diff --git a/src/js/game/entity.js b/src/js/game/entity.js index 3010f067..9acaf26b 100644 --- a/src/js/game/entity.js +++ b/src/js/game/entity.js @@ -224,6 +224,7 @@ export class Entity extends BasicSerializableObject { /** * override, should draw the entity * @param {DrawParameters} parameters + * @abstract */ drawImpl(parameters) { abstract; diff --git a/src/js/game/game_mode.js b/src/js/game/game_mode.js index 5414306c..2c4527e3 100644 --- a/src/js/game/game_mode.js +++ b/src/js/game/game_mode.js @@ -144,6 +144,7 @@ export class GameMode extends BasicSerializableObject { /** * @param {number} w * @param {number} h + * @abstract */ adjustZone(w = 0, h = 0) { abstract; diff --git a/src/js/game/hud/base_hud_part.js b/src/js/game/hud/base_hud_part.js index 84b6d619..91b3fd3a 100644 --- a/src/js/game/hud/base_hud_part.js +++ b/src/js/game/hud/base_hud_part.js @@ -25,6 +25,7 @@ export class BaseHUDPart { /** * Should initialize the element, called *after* the elements have been created + * @abstract */ initialize() { abstract; diff --git a/src/js/game/hud/parts/building_placer_logic.js b/src/js/game/hud/parts/building_placer_logic.js index 7ed412f6..23ac6df3 100644 --- a/src/js/game/hud/parts/building_placer_logic.js +++ b/src/js/game/hud/parts/building_placer_logic.js @@ -192,7 +192,7 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart { const metaBuilding = this.currentMetaBuilding.get(); return ( metaBuilding && - metaBuilding.getHasDirectionLockAvailable() && + metaBuilding.getHasDirectionLockAvailable(this.currentVariant.get()) && this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.lockBeltDirection).pressed ); } diff --git a/src/js/game/hud/parts/keybinding_overlay.js b/src/js/game/hud/parts/keybinding_overlay.js index 2384ab84..0a050484 100644 --- a/src/js/game/hud/parts/keybinding_overlay.js +++ b/src/js/game/hud/parts/keybinding_overlay.js @@ -49,7 +49,7 @@ export class HUDKeybindingOverlay extends BaseHUDPart { !this.mapOverviewActive && placer && placer.currentMetaBuilding.get() && - placer.currentMetaBuilding.get().getHasDirectionLockAvailable() + placer.currentMetaBuilding.get().getHasDirectionLockAvailable(placer.currentVariant.get()) ); } diff --git a/src/js/game/item_resolver.js b/src/js/game/item_resolver.js index ff91b0a3..3e7c87af 100644 --- a/src/js/game/item_resolver.js +++ b/src/js/game/item_resolver.js @@ -16,7 +16,7 @@ export function itemResolverSingleton(root, data) { const itemData = data.data; if (MODS_ADDITIONAL_ITEMS[itemType]) { - return MODS_ADDITIONAL_ITEMS[itemType](itemData); + return MODS_ADDITIONAL_ITEMS[itemType](itemData, root); } switch (itemType) { diff --git a/src/js/game/logic.js b/src/js/game/logic.js index 0e915fea..3fdc871e 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -72,8 +72,13 @@ export class GameLogic { // Check if there is any direct collision const otherEntity = this.root.map.getLayerContentXY(x, y, entity.layer); if (otherEntity) { - const metaClass = otherEntity.components.StaticMapEntity.getMetaBuilding(); - if (!allowReplaceBuildings || !metaClass.getIsReplaceable()) { + const staticComp = otherEntity.components.StaticMapEntity; + if ( + !allowReplaceBuildings || + !staticComp + .getMetaBuilding() + .getIsReplaceable(staticComp.getVariant(), staticComp.getRotationVariant()) + ) { // This one is a direct blocker return false; } @@ -140,8 +145,11 @@ export class GameLogic { for (let y = rect.y; y < rect.y + rect.h; ++y) { const contents = this.root.map.getLayerContentXY(x, y, entity.layer); if (contents) { + const staticComp = contents.components.StaticMapEntity; assertAlways( - contents.components.StaticMapEntity.getMetaBuilding().getIsReplaceable(), + staticComp + .getMetaBuilding() + .getIsReplaceable(staticComp.getVariant(), staticComp.getRotationVariant()), "Tried to replace non-repleaceable entity" ); if (!this.tryDeleteBuilding(contents)) { diff --git a/src/js/game/meta_building.js b/src/js/game/meta_building.js index 0e92d3d9..c661b84f 100644 --- a/src/js/game/meta_building.js +++ b/src/js/game/meta_building.js @@ -52,8 +52,9 @@ export class MetaBuilding { /** * Returns whether the building has the direction lock switch available + * @param {string} variant */ - getHasDirectionLockAvailable() { + getHasDirectionLockAvailable(variant) { return false; } @@ -88,8 +89,10 @@ export class MetaBuilding { /** * Returns whether this building can get replaced + * @param {string} variant + * @param {number} rotationVariant */ - getIsReplaceable() { + getIsReplaceable(variant, rotationVariant) { return false; } @@ -278,6 +281,7 @@ export class MetaBuilding { * Should setup the entity components * @param {Entity} entity * @param {GameRoot} root + * @abstract */ setupEntityComponents(entity, root) { abstract; diff --git a/src/js/game/modes/puzzle_edit.js b/src/js/game/modes/puzzle_edit.js index e3d2e40d..28b59184 100644 --- a/src/js/game/modes/puzzle_edit.js +++ b/src/js/game/modes/puzzle_edit.js @@ -22,6 +22,7 @@ import { MetaTransistorBuilding } from "../buildings/transistor"; import { HUDPuzzleEditorControls } from "../hud/parts/puzzle_editor_controls"; import { HUDPuzzleEditorReview } from "../hud/parts/puzzle_editor_review"; import { HUDPuzzleEditorSettings } from "../hud/parts/puzzle_editor_settings"; +import { HUDConstantSignalEdit } from "../hud/parts/constant_signal_edit"; export class PuzzleEditGameMode extends PuzzleGameMode { static getId() { @@ -58,6 +59,7 @@ export class PuzzleEditGameMode extends PuzzleGameMode { this.additionalHudParts.puzzleEditorControls = HUDPuzzleEditorControls; this.additionalHudParts.puzzleEditorReview = HUDPuzzleEditorReview; this.additionalHudParts.puzzleEditorSettings = HUDPuzzleEditorSettings; + this.additionalHudParts.constantSignalEdit = HUDConstantSignalEdit; } getIsEditor() { diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index cd14833e..68fbb3ad 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -594,18 +594,13 @@ export class RegularGameMode extends GameMode { this.additionalHudParts.interactiveTutorial = HUDInteractiveTutorial; } - // @ts-ignore - if (queryParamOptions.sandboxMode || window.sandboxMode || G_IS_DEV) { - this.additionalHudParts.sandboxController = HUDSandboxController; - } - /** @type {(typeof MetaBuilding)[]} */ - this.hiddenBuildings = [MetaConstantProducerBuilding, MetaGoalAcceptorBuilding, MetaBlockBuilding]; - - // @ts-ignore - if (!(G_IS_DEV || window.sandboxMode || queryParamOptions.sandboxMode)) { - this.hiddenBuildings.push(MetaItemProducerBuilding); - } + this.hiddenBuildings = [ + MetaConstantProducerBuilding, + MetaGoalAcceptorBuilding, + MetaBlockBuilding, + MetaItemProducerBuilding, + ]; } /** diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 1790525c..c5294427 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -39,6 +39,11 @@ import { ShapeItem } from "../items/shape_item"; */ export const MOD_ITEM_PROCESSOR_HANDLERS = {}; +/** + * @type {Object boolean>} + */ +export const MODS_CAN_PROCESS = {}; + export class ItemProcessorSystem extends GameSystemWithFilter { constructor(root) { super(root, [ItemProcessorComponent]); @@ -168,6 +173,12 @@ export class ItemProcessorSystem extends GameSystemWithFilter { const acceptorComp = entity.components.ItemAcceptor; const processorComp = entity.components.ItemProcessor; + if (MODS_CAN_PROCESS[processorComp.processingRequirement]) { + return MODS_CAN_PROCESS[processorComp.processingRequirement].bind(this)({ + entity, + }); + } + switch (processorComp.processingRequirement) { // DEFAULT // By default, we can start processing once all inputs are there diff --git a/src/js/game/systems/wired_pins.js b/src/js/game/systems/wired_pins.js index e8bc1882..e73e85f0 100644 --- a/src/js/game/systems/wired_pins.js +++ b/src/js/game/systems/wired_pins.js @@ -59,7 +59,11 @@ export class WiredPinsSystem extends GameSystemWithFilter { continue; } - if (staticComp.getMetaBuilding().getIsReplaceable()) { + if ( + staticComp + .getMetaBuilding() + .getIsReplaceable(staticComp.getVariant(), staticComp.getRotationVariant()) + ) { // Don't mind here, even if there would be a collision we // could replace it continue; @@ -113,7 +117,12 @@ export class WiredPinsSystem extends GameSystemWithFilter { // If there's an entity, and it can't get removed -> That's a collision if (collidingEntity) { - if (!collidingEntity.components.StaticMapEntity.getMetaBuilding().getIsReplaceable()) { + const staticComp = collidingEntity.components.StaticMapEntity; + if ( + !staticComp + .getMetaBuilding() + .getIsReplaceable(staticComp.getVariant(), staticComp.getRotationVariant()) + ) { return true; } } @@ -138,8 +147,11 @@ export class WiredPinsSystem extends GameSystemWithFilter { const worldPos = entity.components.StaticMapEntity.localTileToWorld(slot.pos); const collidingEntity = this.root.map.getLayerContentXY(worldPos.x, worldPos.y, "wires"); if (collidingEntity) { + const staticComp = collidingEntity.components.StaticMapEntity; assertAlways( - collidingEntity.components.StaticMapEntity.getMetaBuilding().getIsReplaceable(), + staticComp + .getMetaBuilding() + .getIsReplaceable(staticComp.getVariant(), staticComp.getRotationVariant()), "Tried to replace non-repleaceable entity for pins" ); if (!this.root.logic.tryDeleteBuilding(collidingEntity)) { diff --git a/src/js/platform/achievement_provider.js b/src/js/platform/achievement_provider.js index 583dbfb2..3b60ad95 100644 --- a/src/js/platform/achievement_provider.js +++ b/src/js/platform/achievement_provider.js @@ -92,6 +92,7 @@ export class AchievementProviderInterface { /** * Initializes the achievement provider. * @returns {Promise} + * @abstract */ initialize() { abstract; @@ -102,6 +103,7 @@ export class AchievementProviderInterface { * Opportunity to do additional initialization work with the GameRoot. * @param {GameRoot} root * @returns {Promise} + * @abstract */ onLoad(root) { abstract; @@ -118,6 +120,7 @@ export class AchievementProviderInterface { * Call to activate an achievement with the provider * @param {string} key - Maps to an Achievement * @returns {Promise} + * @abstract */ activate(key) { abstract; @@ -127,6 +130,7 @@ export class AchievementProviderInterface { /** * Checks if achievements are supported in the current build * @returns {boolean} + * @abstract */ hasAchievements() { abstract; diff --git a/src/js/platform/ad_provider.js b/src/js/platform/ad_provider.js index a614a793..4aa8949c 100644 --- a/src/js/platform/ad_provider.js +++ b/src/js/platform/ad_provider.js @@ -19,6 +19,7 @@ export class AdProviderInterface { /** * Returns if this provider serves ads at all * @returns {boolean} + * @abstract */ getHasAds() { abstract; @@ -29,6 +30,7 @@ export class AdProviderInterface { * Returns if it would be possible to show a video ad *now*. This can be false if for * example the last video ad is * @returns {boolean} + * @abstract */ getCanShowVideoAd() { abstract; diff --git a/src/js/platform/analytics.js b/src/js/platform/analytics.js index 7bd7ae50..cf839aca 100644 --- a/src/js/platform/analytics.js +++ b/src/js/platform/analytics.js @@ -11,6 +11,7 @@ export class AnalyticsInterface { /** * Initializes the analytics * @returns {Promise} + * @abstract */ initialize() { abstract; diff --git a/src/js/platform/browser/game_analytics.js b/src/js/platform/browser/game_analytics.js index 9411b258..aa0ac796 100644 --- a/src/js/platform/browser/game_analytics.js +++ b/src/js/platform/browser/game_analytics.js @@ -1,6 +1,5 @@ import { globalConfig } from "../../core/config"; import { createLogger } from "../../core/logging"; -import { queryParamOptions } from "../../core/query_parameters"; import { BeltComponent } from "../../game/components/belt"; import { StaticMapEntityComponent } from "../../game/components/static_map_entity"; import { RegularGameMode } from "../../game/modes/regular"; @@ -24,9 +23,6 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { } if (G_IS_STANDALONE) { - if (queryParamOptions.sandboxMode) { - return "steam-sandbox"; - } return "steam"; } @@ -35,14 +31,8 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { } if (window.location.host.indexOf("alpha") >= 0) { - if (queryParamOptions.sandboxMode) { - return "alpha-sandbox"; - } return "alpha"; } else { - if (queryParamOptions.sandboxMode) { - return "beta-sandbox"; - } return "beta"; } } diff --git a/src/js/platform/game_analytics.js b/src/js/platform/game_analytics.js index 00286fc2..19fdf752 100644 --- a/src/js/platform/game_analytics.js +++ b/src/js/platform/game_analytics.js @@ -11,6 +11,7 @@ export class GameAnalyticsInterface { /** * Initializes the analytics * @returns {Promise} + * @abstract */ initialize() { abstract; @@ -43,6 +44,7 @@ export class GameAnalyticsInterface { /** * Activates a DLC * @param {string} dlc + * @abstract */ activateDlc(dlc) { abstract; diff --git a/src/js/platform/storage.js b/src/js/platform/storage.js index 165ee828..c5c3701c 100644 --- a/src/js/platform/storage.js +++ b/src/js/platform/storage.js @@ -13,6 +13,7 @@ export class StorageInterface { /** * Initializes the storage * @returns {Promise} + * @abstract */ initialize() { abstract; @@ -24,6 +25,7 @@ export class StorageInterface { * @param {string} filename * @param {string} contents * @returns {Promise} + * @abstract */ writeFileAsync(filename, contents) { abstract; @@ -34,6 +36,7 @@ export class StorageInterface { * Reads a string asynchronously. Returns Promise if file was not found. * @param {string} filename * @returns {Promise} + * @abstract */ readFileAsync(filename) { abstract; diff --git a/src/js/platform/wrapper.js b/src/js/platform/wrapper.js index f80c2fd6..e0a896fb 100644 --- a/src/js/platform/wrapper.js +++ b/src/js/platform/wrapper.js @@ -81,6 +81,7 @@ export class PlatformWrapperInterface { * Attempt to open an external url * @param {string} url * @param {boolean=} force Whether to always open the url even if not allowed + * @abstract */ openExternalLink(url, force = false) { abstract; @@ -88,6 +89,7 @@ export class PlatformWrapperInterface { /** * Attempt to restart the app + * @abstract */ performRestart() { abstract; @@ -103,6 +105,7 @@ export class PlatformWrapperInterface { /** * Should set the apps fullscreen state to the desired state * @param {boolean} flag + * @abstract */ setFullscreen(flag) { abstract; @@ -117,6 +120,7 @@ export class PlatformWrapperInterface { /** * Attempts to quit the app + * @abstract */ exitApp() { abstract; diff --git a/src/js/profile/setting_types.js b/src/js/profile/setting_types.js index 4df02892..943e8e53 100644 --- a/src/js/profile/setting_types.js +++ b/src/js/profile/setting_types.js @@ -64,6 +64,7 @@ export class BaseSetting { /** * Returns the HTML for this setting * @param {Application} app + * @abstract */ getHtml(app) { abstract; @@ -84,6 +85,7 @@ export class BaseSetting { /** * Attempts to modify the setting + * @abstract */ modify() { abstract; @@ -107,6 +109,7 @@ export class BaseSetting { * Validates the set value * @param {any} value * @returns {boolean} + * @abstract */ validate(value) { abstract; diff --git a/src/js/savegame/serialization_data_types.js b/src/js/savegame/serialization_data_types.js index df352e78..c27e2295 100644 --- a/src/js/savegame/serialization_data_types.js +++ b/src/js/savegame/serialization_data_types.js @@ -48,6 +48,7 @@ export class BaseDataType { /** * Serializes a given raw value * @param {any} value + * @abstract */ serialize(value) { abstract; @@ -68,6 +69,7 @@ export class BaseDataType { * @param {object} targetObject * @param {string|number} targetKey * @returns {string|void} String error code or null on success + * @abstract */ deserialize(value, targetObject, targetKey, root) { abstract; @@ -92,6 +94,7 @@ export class BaseDataType { /** * INTERNAL Should return the json schema representation + * @abstract */ getAsJsonSchemaUncached() { abstract; @@ -131,6 +134,7 @@ export class BaseDataType { /** * Should return a cacheable key + * @abstract */ getCacheKey() { abstract; diff --git a/translations/base-ja.yaml b/translations/base-ja.yaml index c98a1b85..ebce66dd 100644 --- a/translations/base-ja.yaml +++ b/translations/base-ja.yaml @@ -1013,10 +1013,8 @@ tips: - SHIFTを押したままにするとベルトプランナーが有効になり、長距離のベルトを簡単に配置できます。 - 切断機は配置された向きを考慮せず、常に垂直に切断します。 - ストレージは左側の出力を優先します。 - - 増築可能なデザインを作るために時間を使ってください。それだけの価値があります! - - Invest time to build repeatable designs - it's worth it! + - 増築可能なデザインを目指してみましょう。それだけの価値があります! - ALTを押しながらベルトを設置すると、向きを逆転できます。 - - You can hold ALT to invert the direction of placed belts. - ハブから遠くに離れるほど、形状資源はより複雑な形になります。 - 機械の速度には上限があるので、最大効率を得るためには入力を分割してください。 - 効率を最大化するために分配機/合流機を使用できます。 @@ -1032,8 +1030,7 @@ tips: - モジュールがあれば、空間はただの認識に過ぎなくなる――生ある人間に対する気遣いだ。 - 設計図としての工場を別に作っておくと、工場のモジュール化において重要な役割を果たします。 - 混色機をよく見ると、色の混ぜ方が解ります。 - - Have a closer look at the color mixer, and your questions will be answered. - - Use CTRL + Click to select an area. + - CTRLを押したままクリックすると、領域を選択できます。 - アップグレードリストの各形状の横にあるピンのアイコンは、その形状を画面左に固定表示します。 - 三原色全てを混ぜ合わせると白になります! - マップは無限の広さがあります。臆せずに拡張してください。 @@ -1046,8 +1043,7 @@ tips: - ベルトの中身をクリアするには、範囲選択して同じ場所に貼り付けをします。 - F4を押すことで、FPSとTickレートを表示できます。 - F4を2回押すと、マウスとカメラの座標を表示できます。 - - 左のピン留めされた図形をクリックすると、固定を解除できます。 - - You can click a pinned shape on the left side to unpin it. + - 左のピン留めされた図形をクリックすると、ピン留めを解除できます。 puzzleMenu: play: Play edit: Edit diff --git a/translations/base-ru.yaml b/translations/base-ru.yaml index f3bb7c95..b605180e 100644 --- a/translations/base-ru.yaml +++ b/translations/base-ru.yaml @@ -50,7 +50,7 @@ global: escape: ESC shift: SHIFT space: ПРОБЕЛ - loggingIn: Logging in + loggingIn: Вход demoBanners: title: Демоверсия intro: Приобретите полную версию, чтобы разблокировать все возможности! @@ -74,14 +74,14 @@ mainMenu: puzzleMode: Головоломка back: Назад puzzleDlcText: Нравится оптимизировать фабрики и делать их меньше? Купите - обновление "Головоломка" в Steam сейчас и получите еще больше + обновление «Головоломка» в Steam сейчас и получите еще больше удовольствия! puzzleDlcWishlist: Добавь в список желаемого! puzzleDlcViewNow: Посмотреть mods: - title: Active Mods - warningPuzzleDLC: Playing the Puzzle DLC is not possible with mods. Please - disable all mods to play the DLC. + title: Активные моды + warningPuzzleDLC: Обновление «Головоломка» невозможна с модами. Пожалуйста, + отключите все моды, чтобы играть в DLC. dialogs: buttons: ok: OK @@ -230,8 +230,8 @@ dialogs: desc: "Не удалось отправить вашу головоломку:" puzzleSubmitOk: title: Головоломка опубликована - desc: Поздравляю! Ваша головоломка была опубликована, и теперь в нее могут - играть остальные. Теперь вы можете найти ее в разделе "Мои + desc: Поздравляю! Ваша головоломка была опубликована, и теперь в неё могут + играть остальные. Теперь вы можете найти её в разделе "Мои головоломки". puzzleCreateOffline: title: Оффлайн режим @@ -265,12 +265,13 @@ dialogs: title: Удалить головоломку? desc: Вы уверены, что хотите удалить ''? Это действие нельзя отменить! modsDifference: - title: Mod Warning - desc: The currently installed mods differ from the mods the savegame was created - with. This might cause the savegame to break or not load at all. Are - you sure you want to continue? - missingMods: Missing Mods - newMods: Newly installed Mods + title: Предупреждение Мода + desc: Установленные в данный момент моды отличаются от модов, с которыми была + создана игра сохранения с ним. Это может привести к тому, что сохранение + не загрузится или вообще ничего не загрузится. Вы вы уверены, что хотите + продолжить? + missingMods: Отсутствуют Моды + newMods: Недавно установленные Моды ingame: keybindingsOverlay: moveMap: Передвижение @@ -1312,19 +1313,19 @@ backendErrors: все еще хотите удалить ее, обратитесь в support@shapez.io! no-permission: У вас нет прав на выполнение этого действия. mods: - title: Mods - author: Author - version: Version - modWebsite: Website - openFolder: Open Mods Folder - folderOnlyStandalone: Opening the mod folder is only possible when running the standalone. - browseMods: Browse Mods - modsInfo: To install and manage mods, copy them to the mods folder within the - game directory. You can also use the 'Open Mods Folder' button on the - top right. - noModSupport: You need the standalone version on Steam to install mods. + title: Моды + author: Автор + version: Версия + modWebsite: Веб-сайт + openFolder: Открыть папку с Модами + folderOnlyStandalone: Открытие папки Модов возможно только при запуске автономного режима. + browseMods: Просмотреть Моды + modsInfo: Чтобы установить и управлять модами, скопируйте их в папку модов в + директории игры. Вы также можете воспользоваться кнопкой "Открыть папку модов" в + справа вверху. + noModSupport: Для установки модов вам нужна автономная версия в Steam. togglingComingSoon: - title: Coming Soon - description: Enabling or disabling mods is currently only possible by copying - the mod file from or to the mods/ folder. However, being able to - toggle them here is planned for a future update! + title: Совсем скоро + description: Включение или отключение модов в настоящее время возможно только + путем копирования файла мода из/в папку mods/. Однако, возможность + управлять ими здесь планируется в будущем обновлении! diff --git a/translations/base-sv.yaml b/translations/base-sv.yaml index 5ad75c0c..617ffa27 100644 --- a/translations/base-sv.yaml +++ b/translations/base-sv.yaml @@ -52,7 +52,7 @@ global: escape: ESC shift: SKIFT space: MELLANSLAG - loggingIn: Logging in + loggingIn: Loggar in demoBanners: title: Demo-version intro: Skaffa den fristående versionen för att låsa upp alla funktioner! diff --git a/version b/version index 3e1ad720..8e03717d 100644 --- a/version +++ b/version @@ -1 +1 @@ -1.5.0 \ No newline at end of file +1.5.1 \ No newline at end of file