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.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 " +
+ 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