diff --git a/src/css/ingame_hud/dialogs.scss b/src/css/ingame_hud/dialogs.scss index cc742d42..b913ba35 100644 --- a/src/css/ingame_hud/dialogs.scss +++ b/src/css/ingame_hud/dialogs.scss @@ -164,7 +164,7 @@ .keybinding { position: relative; background: #eee; - @include PlainText; + @include SuperSmallText; height: unset; margin: 1px 0; } @@ -214,6 +214,69 @@ } } } + + .checkBoxFormElem, + .enumFormElem { + display: flex; + align-items: center; + @include S(margin, 10px, 0); + + > label { + @include S(margin-right, 10px); + } + } + + .checkBoxGridFormElem { + display: inline-grid; + grid-template-columns: 1fr; + @include S(margin, 10px, 0); + @include S(grid-row-gap, 10px); + + > .checkBoxFormElem { + margin: 0; + justify-content: space-between; + } + } + + .enum { + display: grid; + grid-template-columns: auto 1fr auto; + @include S(grid-gap, 4px); + @include S(min-width, 160px); + + > div { + background: $mainBgColor; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + pointer-events: all; + cursor: pointer; + @include S(border-radius, $globalBorderRadius); + @include S(padding, 4px); + + transition: background-color 0.12s ease-in-out; + &:hover { + background-color: darken($mainBgColor, 5); + } + + @include DarkThemeOverride { + background-color: $darkModeControlsBackground; + color: #ddd; + &:hover { + background-color: darken($darkModeControlsBackground, 2); + } + } + + &.toggle { + @include S(width, 16px); + } + + &.value { + transform: none !important; + } + } + } } > .buttons { diff --git a/src/js/core/modal_dialog_forms.js b/src/js/core/modal_dialog_forms.js index aac81d82..69fa5f44 100644 --- a/src/js/core/modal_dialog_forms.js +++ b/src/js/core/modal_dialog_forms.js @@ -1,6 +1,8 @@ import { BaseItem } from "../game/base_item"; import { ClickDetector } from "./click_detector"; +import { createLogger } from "./logging"; import { Signal } from "./signal"; +import { safeModulo } from "./utils"; /* * *************************************************** @@ -13,6 +15,8 @@ import { Signal } from "./signal"; * *************************************************** */ +const logger = createLogger("dialog_forms"); + export class FormElement { constructor(id, label) { this.id = id; @@ -139,7 +143,7 @@ export class FormElementCheckbox extends FormElement { getHtml() { return `
- ${this.label ? `` : ""} + ${this.label ? `` : ""}
@@ -166,7 +170,32 @@ export class FormElementCheckbox extends FormElement { this.element.classList.toggle("checked", this.value); } - focus(parent) {} + focus() {} +} + +export class FormElementCheckboxList extends FormElement { + constructor({ id, label = null, checkboxes = [] }) { + super(id, label); + this.checkboxes = checkboxes; + } + + getHtml() { + return ` +
+ ${this.checkboxes.map(checkbox => checkbox.getHtml()).join("\n")} +
+ `; + } + + bindEvents(parent, clickTrackers) { + this.checkboxes.forEach(checkbox => checkbox.bindEvents(parent, clickTrackers)); + } + + getValue() { + return this.checkboxes.map(checkbox => checkbox.getValue()); + } + + focus() {} } export class FormElementItemChooser extends FormElement { @@ -235,3 +264,64 @@ export class FormElementItemChooser extends FormElement { focus() {} } + +export class FormElementEnum extends FormElement { + constructor({ id, label = null, options, defaultValue = null, valueGetter, textGetter }) { + super(id, label); + this.options = options; + this.valueGetter = valueGetter; + this.textGetter = textGetter; + this.index = 0; + if (defaultValue !== null) { + const index = this.options.findIndex(option => option.id === defaultValue); + if (index >= 0) { + this.index = index; + } else { + logger.warn("Option ID", defaultValue, "not found in", options, "!"); + } + } + + this.element = null; + } + + getHtml() { + return ` +
+ ${this.label ? `` : ""} +
+ +
${this.textGetter(this.options[this.index])}
+ +
+
+ `; + } + + /** + * @param {HTMLElement} parent + * @param {Array} clickTrackers + */ + bindEvents(parent, clickTrackers) { + this.element = this.getFormElement(parent); + + const children = this.element.children; + for (let i = 0; i < children.length; ++i) { + const child = children[i]; + const detector = new ClickDetector(child, { preventDefault: false }); + clickTrackers.push(detector); + const change = child.classList.contains("prev") ? -1 : 1; + detector.click.add(() => this.toggle(change), this); + } + } + + getValue() { + return this.valueGetter(this.options[this.index]); + } + + toggle(amount) { + this.index = safeModulo(this.index + amount, this.options.length); + this.element.querySelector(".value").innerText = this.textGetter(this.options[this.index]); + } + + focus() {} +} diff --git a/src/js/game/hud/parts/screenshot_exporter.js b/src/js/game/hud/parts/screenshot_exporter.js index dd81f8b6..db10fa1a 100644 --- a/src/js/game/hud/parts/screenshot_exporter.js +++ b/src/js/game/hud/parts/screenshot_exporter.js @@ -8,9 +8,47 @@ import { T } from "../../../translations"; import { StaticMapEntityComponent } from "../../components/static_map_entity"; import { KEYMAPPINGS } from "../../key_action_mapper"; import { BaseHUDPart } from "../base_hud_part"; +import { DialogWithForm } from "../../../core/modal_dialog_elements"; +import { + FormElementCheckbox, + FormElementCheckboxList, + FormElementEnum, +} from "../../../core/modal_dialog_forms"; +import { ORIGINAL_SPRITE_SCALE } from "../../../core/sprites"; +import { getDeviceDPI } from "../../../core/dpi_manager"; +import { HUDMassSelector } from "./mass_selector"; +import { clamp } from "../../../core/utils"; +import { CHUNK_OVERLAY_RES, MapChunkView } from "../../map_chunk_view"; +import { enumHubGoalRewards } from "../../tutorial_goals"; const logger = createLogger("screenshot_exporter"); +const MAX_CANVAS_DIMS = 16384; +// should be odd so that the centers of tiles are rendered +// as pixels per tile must be a multiple of this +const TARGET_INVERSE_BORDER = 3; + +const screenshotQualities = [ + { + id: "high", + resolution: MAX_CANVAS_DIMS, + }, + { + id: "medium", + resolution: MAX_CANVAS_DIMS / 4, + }, + { + id: "low", + resolution: MAX_CANVAS_DIMS / 16, + }, + { + id: "pixels", + resolution: 0, + }, +]; +// @TODO: translation (T.dialogs.exportScreenshotWarning.qualities) +const qualityNames = { high: "High", medium: "Medium", low: "Low", pixels: "Pixels" }; + export class HUDScreenshotExporter extends BaseHUDPart { createElements() {} @@ -24,52 +62,228 @@ export class HUDScreenshotExporter extends BaseHUDPart { return; } - const { ok } = this.root.hud.parts.dialogs.showInfo( - T.dialogs.exportScreenshotWarning.title, - T.dialogs.exportScreenshotWarning.desc, - ["cancel:good", "ok:bad"] + /** @type {Rectangle} */ + let bounds = undefined; + const massSelector = this.root.hud.parts.massSelector; + if (massSelector instanceof HUDMassSelector) { + if (massSelector.currentSelectionStartWorld) { + const worldStart = massSelector.currentSelectionStartWorld; + const worldEnd = this.root.camera.screenToWorld(massSelector.currentSelectionEnd); + + const tileStart = worldStart.toTileSpace(); + const tileEnd = worldEnd.toTileSpace(); + + bounds = Rectangle.fromTwoPoints(tileStart, tileEnd); + bounds.w += 1; + bounds.h += 1; + } else if (massSelector.selectedUids.size > 0) { + const minTile = new Vector(Infinity, Infinity); + const maxTile = new Vector(-Infinity, -Infinity); + + const entityUids = Array.from(massSelector.selectedUids); + for (let i = 0; i < entityUids.length; ++i) { + const entityBounds = this.root.entityMgr + .findByUid(entityUids[i]) + .components.StaticMapEntity.getTileSpaceBounds(); + + minTile.x = Math.min(minTile.x, entityBounds.x); + minTile.y = Math.min(minTile.y, entityBounds.y); + + maxTile.x = Math.max(maxTile.x, entityBounds.x + entityBounds.w); + maxTile.y = Math.max(maxTile.y, entityBounds.y + entityBounds.h); + } + + bounds = Rectangle.fromTwoPoints(minTile, maxTile); + } + } + + const qualityInput = new FormElementEnum({ + id: "screenshotQuality", + label: "Quality", + options: screenshotQualities, + defaultValue: "medium", + valueGetter: quality => quality.resolution, + // @TODO: translation (T.dialogs.exportScreenshotWarning.qualityLabel) + textGetter: quality => qualityNames[quality.id], + }); + const overlayInput = new FormElementCheckbox({ + id: "screenshotView", + // @TODO: translation (T.dialogs.exportScreenshotWarning.descOverlay) + label: "Map view", + defaultValue: this.root.camera.getIsMapOverlayActive() ? true : false, + }); + const layerInput = new FormElementCheckbox({ + id: "screenshotLayer", + // @TODO: translation (T.dialogs.exportScreenshotWarning.descLayer) + label: "Wires layer", + defaultValue: this.root.currentLayer === "wires" ? true : false, + }); + const backgroundInput = new FormElementCheckbox({ + id: "screenshotBackground", + // @TODO: translation (T.dialogs.exportScreenshotWarning.descBackground) + label: "Transparent background", + defaultValue: false, + }); + const checkboxInputs = new FormElementCheckboxList({ + id: "screenshotCheckboxes", + checkboxes: [ + overlayInput, + ...(this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers) + ? [layerInput] + : []), + backgroundInput, + ], + }); + const dialog = new DialogWithForm({ + app: this.root.app, + title: T.dialogs.exportScreenshotWarning.title, + desc: bounds + ? // @TODO: translation (T.dialogs.exportScreenshotWarning.descSelection) + "You requested to export a region of your base as a screenshot. Please note that this will be quite slow for a bigger region and could potentially crash your game!" + : // @TODO: update translation (T.dialogs.exportScreenshotWarning.desc) + "You requested to export your base as a screenshot. Please note that this will be quite slow for a bigger base and could potentially crash your game!

Tip: You can select a region with to only take a screenshot of that region.".replace( + "", + "" + + this.root.keyMapper + .getBinding(KEYMAPPINGS.massSelect.massSelectStart) + .getKeyCodeString() + + "" + ), + formElements: [qualityInput, checkboxInputs], + buttons: ["cancel:good", "ok:bad"], + }); + + dialog.inputReciever.keydown.add(({ keyCode }) => { + if (keyCode === KEYMAPPINGS.ingame.exportScreenshot.keyCode) { + this.root.hud.parts.dialogs.closeDialog(dialog); + } + }); + + this.root.hud.parts.dialogs.internalShowDialog(dialog); + dialog.buttonSignals.ok.add( + () => + this.doExport( + qualityInput.getValue(), + overlayInput.getValue(), + layerInput.getValue(), + backgroundInput.getValue(), + !!bounds, + bounds + ), + this ); - ok.add(this.doExport, this); } - doExport() { + /** + * Renders a screenshot of the entire base as closely as possible to the ingame camera + * @param {number} targetResolution + * @param {boolean} overlay + * @param {boolean} wiresLayer + * @param {boolean} hideBackground + * @param {boolean} allowBorder + * @param {Rectangle?} tileBounds + */ + doExport(targetResolution, overlay, wiresLayer, hideBackground, allowBorder, tileBounds) { logger.log("Starting export ..."); - // Find extends - const staticEntities = this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent); + const boundsSelected = !!tileBounds; + if (!tileBounds) { + // Find extends + const staticEntities = this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent); - const minTile = new Vector(0, 0); - const maxTile = new Vector(0, 0); - for (let i = 0; i < staticEntities.length; ++i) { - const bounds = staticEntities[i].components.StaticMapEntity.getTileSpaceBounds(); - minTile.x = Math.min(minTile.x, bounds.x); - minTile.y = Math.min(minTile.y, bounds.y); + const minTile = new Vector(0, 0); + const maxTile = new Vector(0, 0); + for (let i = 0; i < staticEntities.length; ++i) { + const entityBounds = staticEntities[i].components.StaticMapEntity.getTileSpaceBounds(); + minTile.x = Math.min(minTile.x, entityBounds.x); + minTile.y = Math.min(minTile.y, entityBounds.y); - maxTile.x = Math.max(maxTile.x, bounds.x + bounds.w); - maxTile.y = Math.max(maxTile.y, bounds.y + bounds.h); + maxTile.x = Math.max(maxTile.x, entityBounds.x + entityBounds.w); + maxTile.y = Math.max(maxTile.y, entityBounds.y + entityBounds.h); + } + + minTile.x = Math.floor(minTile.x / globalConfig.mapChunkSize) * globalConfig.mapChunkSize; + minTile.y = Math.floor(minTile.y / globalConfig.mapChunkSize) * globalConfig.mapChunkSize; + + maxTile.x = Math.ceil(maxTile.x / globalConfig.mapChunkSize) * globalConfig.mapChunkSize; + maxTile.y = Math.ceil(maxTile.y / globalConfig.mapChunkSize) * globalConfig.mapChunkSize; + + tileBounds = Rectangle.fromTwoPoints(minTile, maxTile).expandedInAllDirections( + globalConfig.mapChunkSize + ); } - const minChunk = minTile.divideScalar(globalConfig.mapChunkSize).floor(); - const maxChunk = maxTile.divideScalar(globalConfig.mapChunkSize).ceil(); + // if the desired pixels per tile is too small, we do not create a border + // so that we have more valid values for pixels per tile + // we do not create a border for map view since there is no sprite overflow + const border = + allowBorder && + !overlay && + targetResolution / (Math.max(tileBounds.w, tileBounds.h) + 2 / TARGET_INVERSE_BORDER) >= + 3 * TARGET_INVERSE_BORDER; - const dimensions = maxChunk.sub(minChunk); - logger.log("Dimensions:", dimensions); + const bounds = border ? tileBounds.expandedInAllDirections(1 / TARGET_INVERSE_BORDER) : tileBounds; + logger.log("Bounds:", bounds); - let chunkSizePixels = 128; - const maxDimensions = Math.max(dimensions.x, dimensions.y); + const maxDimensions = Math.max(bounds.w, bounds.h); - if (maxDimensions > 128) { - chunkSizePixels = Math.max(1, Math.floor(128 * (128 / maxDimensions))); + // at least 3 pixels per tile, for bearable quality + // at most the resolution of the assets, to not be excessive + const clamped = clamp( + targetResolution / (maxDimensions + (border ? 2 / 3 : 0)), + 3, + globalConfig.assetsDpi * globalConfig.tileSize + ); + + // 1 is a fake value since it behaves the same as a border width of 0 + const inverseBorder = border ? TARGET_INVERSE_BORDER : 1; + const tileSizePixels = overlay + ? // we floor to the nearest multiple of the map view tile resolution + Math.floor(clamped / CHUNK_OVERLAY_RES) * CHUNK_OVERLAY_RES || CHUNK_OVERLAY_RES + : // we floor to the nearest odd multiple so that the center of each building is rendered + Math.floor((clamped + inverseBorder) / (2 * inverseBorder)) * (2 * inverseBorder) - + inverseBorder || inverseBorder; + logger.log("Pixels per tile:", tileSizePixels); + + if (Math.round(tileSizePixels * maxDimensions) > MAX_CANVAS_DIMS) { + logger.error("Maximum canvas size exceeded, aborting"); + this.root.hud.parts.dialogs.showInfo( + // @TODO: translation (T.dialogs.exportScreenshotFail.title) + "Too large", + boundsSelected + ? // @TODO: translation (T.dialogs.exportScreenshotFail.descSelection) + "The region selected is too large to render, sorry! Try selecting a smaller region." + : // @TODO: translation (T.dialogs.exportScreenshotFail.desc) + "The base is too large to render, sorry! Try selecting just a region of your base with .".replace( + "", + "" + + this.root.keyMapper + .getBinding(KEYMAPPINGS.massSelect.massSelectStart) + .getKeyCodeString() + + "" + ) + ); + return; } - logger.log("ChunkSizePixels:", chunkSizePixels); - const chunkScale = chunkSizePixels / globalConfig.mapChunkWorldSize; - logger.log("Scale:", chunkScale); + const zoomLevel = tileSizePixels / globalConfig.tileSize; + logger.log("Scale:", zoomLevel); + + // Compute atlas scale + const lowQuality = this.root.app.settings.getAllSettings().lowQualityTextures; + const effectiveZoomLevel = (zoomLevel / globalConfig.assetsDpi) * globalConfig.assetsSharpness; + + let desiredAtlasScale = "0.25"; + if (effectiveZoomLevel > 0.5 && !lowQuality) { + desiredAtlasScale = ORIGINAL_SPRITE_SCALE; + } else if (effectiveZoomLevel > 0.35 && !lowQuality) { + desiredAtlasScale = "0.5"; + } logger.log("Allocating buffer, if the factory grew too big it will crash here"); const [canvas, context] = makeOffscreenBuffer( - dimensions.x * chunkSizePixels, - dimensions.y * chunkSizePixels, + Math.round(bounds.w * tileSizePixels), + Math.round(bounds.h * tileSizePixels), { smooth: true, reusable: false, @@ -78,26 +292,61 @@ export class HUDScreenshotExporter extends BaseHUDPart { ); logger.log("Got buffer, rendering now ..."); - const visibleRect = new Rectangle( - minChunk.x * globalConfig.mapChunkWorldSize, - minChunk.y * globalConfig.mapChunkWorldSize, - dimensions.x * globalConfig.mapChunkWorldSize, - dimensions.y * globalConfig.mapChunkWorldSize - ); + const visibleRect = bounds.allScaled(globalConfig.tileSize); const parameters = new DrawParameters({ context, visibleRect, - desiredAtlasScale: 0.25, + desiredAtlasScale, root: this.root, - zoomLevel: chunkScale, + zoomLevel: zoomLevel, }); - context.scale(chunkScale, chunkScale); + context.scale(zoomLevel, zoomLevel); context.translate(-visibleRect.x, -visibleRect.y); + // hack but works + const currentLayer = this.root.currentLayer; + const currentAlpha = this.root.hud.parts.wiresOverlay.currentAlpha; + if (wiresLayer) { + this.root.currentLayer = "wires"; + this.root.hud.parts.wiresOverlay.currentAlpha = 1; + } else { + this.root.currentLayer = "regular"; + this.root.hud.parts.wiresOverlay.currentAlpha = 0; + } + this.root.systemMgr.systems.itemAcceptor.updateForScreenshot(); + // Render all relevant chunks - this.root.map.drawBackground(parameters); - this.root.map.drawForeground(parameters); + this.root.signals.gameFrameStarted.dispatch(); + if (overlay) { + this.root; + if (hideBackground) { + this.root.map.drawVisibleChunks(parameters, MapChunkView.prototype.drawOverlayNoBackground); + } else { + this.root.map.drawOverlay(parameters); + } + } else { + if (hideBackground) { + this.root.map.drawVisibleChunks( + parameters, + MapChunkView.prototype.drawBackgroundLayerBeltsOnly + ); + } else { + this.root.map.drawBackground(parameters); + } + this.root.systemMgr.systems.belt.drawBeltItems(parameters); + this.root.map.drawForeground(parameters); + this.root.systemMgr.systems.hub.draw(parameters); + if (this.root.hud.parts.wiresOverlay) { + this.root.hud.parts.wiresOverlay.draw(parameters); + } + if (this.root.currentLayer === "wires") { + this.root.map.drawWiresForegroundLayer(parameters); + } + } + + this.root.currentLayer = currentLayer; + this.root.hud.parts.wiresOverlay.currentAlpha = currentAlpha; // Offer export logger.log("Rendered buffer, exporting ..."); diff --git a/src/js/game/map_chunk_view.js b/src/js/game/map_chunk_view.js index 131ce37b..66593fd3 100644 --- a/src/js/game/map_chunk_view.js +++ b/src/js/game/map_chunk_view.js @@ -53,6 +53,17 @@ export class MapChunkView extends MapChunk { systems.belt.drawChunk(parameters, this); } + /** + * Draws only the belts of the background layer + * @param {DrawParameters} parameters + */ + drawBackgroundLayerBeltsOnly(parameters) { + const systems = this.root.systemMgr.systems; + + systems.beltUnderlays.drawChunk(parameters, this); + systems.belt.drawChunk(parameters, this); + } + /** * Draws the dynamic foreground layer * @param {DrawParameters} parameters @@ -130,6 +141,40 @@ export class MapChunkView extends MapChunk { } } + /** + * Overlay with transparent background + * @param {DrawParameters} parameters + */ + drawOverlayNoBackground(parameters) { + const overlaySize = globalConfig.mapChunkSize * CHUNK_OVERLAY_RES; + const sprite = this.root.buffers.getForKey({ + key: "chunknobg@" + this.root.currentLayer, + subKey: this.renderKey, + w: overlaySize, + h: overlaySize, + dpi: 1, + redrawMethod: this.generateOverlayBufferNoBackground.bind(this), + }); + + const dims = globalConfig.mapChunkWorldSize; + const extrude = 0.05; + + // Draw chunk "pixel" art + parameters.context.imageSmoothingEnabled = false; + drawSpriteClipped({ + parameters, + sprite, + x: this.x * dims - extrude, + y: this.y * dims - extrude, + w: dims + 2 * extrude, + h: dims + 2 * extrude, + originalW: overlaySize, + originalH: overlaySize, + }); + + parameters.context.imageSmoothingEnabled = true; + } + /** * * @param {HTMLCanvasElement} canvas @@ -254,6 +299,94 @@ export class MapChunkView extends MapChunk { } } + /** + * + * @param {HTMLCanvasElement} canvas + * @param {CanvasRenderingContext2D} context + * @param {number} w + * @param {number} h + * @param {number} dpi + */ + generateOverlayBufferNoBackground(canvas, context, w, h, dpi) { + for (let x = 0; x < globalConfig.mapChunkSize; ++x) { + const upperArray = this.contents[x]; + for (let y = 0; y < globalConfig.mapChunkSize; ++y) { + const upperContent = upperArray[y]; + if (upperContent) { + const staticComp = upperContent.components.StaticMapEntity; + const data = getBuildingDataFromCode(staticComp.code); + const metaBuilding = data.metaInstance; + + const overlayMatrix = metaBuilding.getSpecialOverlayRenderMatrix( + staticComp.rotation, + data.rotationVariant, + data.variant, + upperContent + ); + + if (overlayMatrix) { + context.fillStyle = metaBuilding.getSilhouetteColor( + data.variant, + data.rotationVariant + ); + for (let dx = 0; dx < 3; ++dx) { + for (let dy = 0; dy < 3; ++dy) { + const isFilled = overlayMatrix[dx + dy * 3]; + if (isFilled) { + context.fillRect( + x * CHUNK_OVERLAY_RES + dx, + y * CHUNK_OVERLAY_RES + dy, + 1, + 1 + ); + } + } + } + + continue; + } else { + context.fillStyle = metaBuilding.getSilhouetteColor( + data.variant, + data.rotationVariant + ); + context.fillRect( + x * CHUNK_OVERLAY_RES, + y * CHUNK_OVERLAY_RES, + CHUNK_OVERLAY_RES, + CHUNK_OVERLAY_RES + ); + + continue; + } + } + } + } + + if (this.root.currentLayer === "wires") { + // Draw wires overlay + + context.fillStyle = THEME.map.wires.overlayColor; + context.fillRect(0, 0, w, h); + + for (let x = 0; x < globalConfig.mapChunkSize; ++x) { + const wiresArray = this.wireContents[x]; + for (let y = 0; y < globalConfig.mapChunkSize; ++y) { + const content = wiresArray[y]; + if (!content) { + continue; + } + MapChunkView.drawSingleWiresOverviewTile({ + context, + x: x * CHUNK_OVERLAY_RES, + y: y * CHUNK_OVERLAY_RES, + entity: content, + tileSizePixels: CHUNK_OVERLAY_RES, + }); + } + } + } + } + /** * @param {object} param0 * @param {CanvasRenderingContext2D} param0.context diff --git a/src/js/game/systems/item_acceptor.js b/src/js/game/systems/item_acceptor.js index 780b4abd..ff95e21a 100644 --- a/src/js/game/systems/item_acceptor.js +++ b/src/js/game/systems/item_acceptor.js @@ -56,6 +56,36 @@ export class ItemAcceptorSystem extends GameSystemWithFilter { } } + updateForScreenshot() { + // Compute how much ticks we missed + const numTicks = this.accumulatedTicksWhileInMapOverview; + const progress = + 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; + + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + const aceptorComp = entity.components.ItemAcceptor; + const animations = aceptorComp.itemConsumptionAnimations; + + // 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; + } + } + } + } + /** * @param {DrawParameters} parameters * @param {MapChunkView} chunk