mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-06-13 13:04:03 +00:00
Merge 731c1416ce
into 260041702b
This commit is contained in:
commit
9757db2e55
@ -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 {
|
||||
|
@ -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 `
|
||||
<div class="formElement checkBoxFormElem">
|
||||
${this.label ? `<label>${this.label}</label>` : ""}
|
||||
${this.label ? `<label>${this.label}</label>` : ""}
|
||||
<div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'>
|
||||
<span class="knob"></span >
|
||||
</div >
|
||||
@ -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 `
|
||||
<div class="formElement checkBoxGridFormElem">
|
||||
${this.checkboxes.map(checkbox => checkbox.getHtml()).join("\n")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="formElement enumFormElem">
|
||||
${this.label ? `<label>${this.label}</label>` : ""}
|
||||
<div class="enum" data-formId="${this.id}">
|
||||
<div class="toggle prev">⯇</div>
|
||||
<div class="value">${this.textGetter(this.options[this.index])}</div>
|
||||
<div class="toggle next">⯈</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} parent
|
||||
* @param {Array<ClickDetector>} 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() {}
|
||||
}
|
||||
|
@ -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!<br><br>Tip: You can select a region with <key> to only take a screenshot of that region.".replace(
|
||||
"<key>",
|
||||
"<code class='keybinding'>" +
|
||||
this.root.keyMapper
|
||||
.getBinding(KEYMAPPINGS.massSelect.massSelectStart)
|
||||
.getKeyCodeString() +
|
||||
"</code>"
|
||||
),
|
||||
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 <key>.".replace(
|
||||
"<key>",
|
||||
"<code class='keybinding'>" +
|
||||
this.root.keyMapper
|
||||
.getBinding(KEYMAPPINGS.massSelect.massSelectStart)
|
||||
.getKeyCodeString() +
|
||||
"</code>"
|
||||
)
|
||||
);
|
||||
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 ...");
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user