1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-13 13:04:03 +00:00

Puzzle mode, almost done

This commit is contained in:
tobspr 2021-05-03 15:07:03 +02:00
parent 2ffdbc5b13
commit 97d4b26c2b
49 changed files with 911 additions and 199 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -22,6 +22,8 @@
@include S(width, 30px);
@include S(height, 30px);
@include DarkThemeInvert;
opacity: 1;
&:hover {
opacity: 0.9 !important;

View File

@ -60,7 +60,7 @@
}
}
.contents {
> .contents {
@include S(width, 400px);
@include S(height, 170px);
@include InlineAnimation(0.5s ease-in-out) {
@ -94,7 +94,6 @@
> button {
@include S(width, 40px);
@include S(height, 40px);
background: green;
@include S(margin, 0, 10px);
box-sizing: border-box;
@include S(border-radius, $globalBorderRadius);
@ -136,16 +135,31 @@
display: flex;
align-items: center;
> canvas {
@include S(margin, 0, 5px);
@include S(width, 30px);
@include S(height, 30px);
> .rating {
@include S(border-radius, $globalBorderRadius);
transition: opacity 0.12s ease-in-out, background-color 0.12s ease-in-out,
box-shadow 0.12s ease-in-out;
pointer-events: all;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
@include S(margin, 0, 5px);
@include S(width, 65px);
@include S(height, 50px);
> canvas {
@include S(width, 30px);
@include S(height, 30px);
transition: opacity 0.12s ease-in-out, background-color 0.12s ease-in-out,
box-shadow 0.12s ease-in-out;
}
> .description {
@include SuperSmallText;
white-space: nowrap;
}
&.active {
background-color: #151118 !important;
box-shadow: 0 0 0 D(2px) #151118;
@ -154,27 +168,24 @@
&:not(.active) {
opacity: 0.4;
}
}
}
}
}
&:nth-child(1) {
transform: scale(0.8) !important;
}
&:nth-child(2) {
transform: scale(0.9) !important;
}
&:nth-child(3) {
transform: scale(1) !important;
}
&:nth-child(4) {
transform: scale(1.1) !important;
}
&:nth-child(5) {
transform: scale(1.2) !important;
}
&:nth-child(6) {
transform: scale(1.3) !important;
}
}
> .actions {
position: absolute;
@include S(bottom, 40px);
display: grid;
@include S(grid-gap, 15px);
grid-auto-flow: column;
button {
@include SuperSmallText;
}
.report {
background-color: $accentColorDark;
}
}
@ -182,6 +193,8 @@
border: 0;
position: relative;
@include S(margin-top, 30px);
background: $colorGreenBright;
@include S(padding, 10px, 40px);
&:not(.visible) {
opacity: 0;

View File

@ -9,4 +9,11 @@
/* @load-async */
background: uiResource("puzzle_dlc_logo.png") center center / contain no-repeat;
}
@include DarkThemeOverride {
& {
/* @load-async */
background: uiResource("puzzle_dlc_logo_inverse.png") center center / contain no-repeat;
}
}
}

View File

@ -16,6 +16,8 @@
font-weight: bold;
}
}
@include DarkThemeInvert;
}
#ingame_HUD_PuzzleEditorTitle {
@ -27,4 +29,6 @@
text-transform: uppercase;
@include Heading;
text-align: center;
@include DarkThemeInvert;
}

View File

@ -23,6 +23,8 @@
@include S(padding-right, 25px);
opacity: 1;
@include DarkThemeInvert;
&:hover {
opacity: 0.9 !important;
}

View File

@ -44,9 +44,19 @@
}
}
> .buttons > button.trim {
> .buttons {
> .buttonBar {
display: flex;
align-items: center;
@include S(margin-top, 10px);
> button {
@include S(margin-right, 4px);
@include SuperSmallText;
&:last-child {
margin-right: 0;
}
}
}
}
}
}

View File

@ -6,14 +6,94 @@
display: flex;
flex-direction: column;
@include SuperDuperSmallText;
@include S(width, 200px);
> span {
@include S(margin-bottom, 10px);
> .info {
display: flex;
flex-direction: column;
@include SuperSmallText;
@include S(margin-bottom, 5px);
strong {
> label {
text-transform: uppercase;
@include SuperSmallText;
}
> span {
display: flex;
@include SuperSmallText;
}
}
> .plays {
display: flex;
align-items: center;
justify-self: end;
align-self: end;
flex-direction: row;
@include DarkThemeInvert;
opacity: 0.4;
> .downloads {
@include SuperSmallText;
color: #000;
align-self: start;
justify-self: start;
font-weight: bold;
@include S(margin-right, 10px);
@include S(padding-left, 14px);
opacity: 0.7;
display: inline-flex;
align-items: center;
justify-content: center;
& {
/* @load-async */
background: uiResource("icons/puzzle_plays.png") #{D(2px)} center / #{D(8px)} #{D(8px)} no-repeat;
}
}
> .likes {
@include SuperSmallText;
align-items: center;
justify-content: center;
color: #000;
align-self: start;
justify-self: start;
font-weight: bold;
@include S(padding-left, 14px);
opacity: 0.7;
& {
/* @load-async */
background: uiResource("icons/puzzle_upvotes.png") #{D(2px)} center / #{D(8px)} #{D(8px)} no-repeat;
}
}
}
> .key {
button {
@include S(margin-top, 2px);
}
}
button {
@include SuperSmallText;
align-self: start;
@include S(min-width, 50px);
&.report {
background-color: $accentColorDark;
@include SuperDuperSmallText;
}
}
> .buttons {
display: flex;
flex-direction: column;
> button {
@include S(margin-bottom, 4px);
}
}
}

View File

@ -0,0 +1,23 @@
#ingame_HUD_PuzzlePlaySettings {
position: absolute;
background: $ingameHudBg;
@include S(padding, 10px);
@include S(bottom, 60px);
@include S(left, 10px);
@include SuperSmallText;
color: #eee;
display: flex;
flex-direction: column;
@include S(border-radius, $globalBorderRadius);
> .section {
display: grid;
@include S(grid-gap, 10px);
grid-auto-flow: row;
> button {
@include SuperSmallText;
}
}
}

View File

@ -61,6 +61,7 @@
@import "ingame_hud/puzzle_dlc_logo";
@import "ingame_hud/puzzle_editor_controls";
@import "ingame_hud/puzzle_editor_settings";
@import "ingame_hud/puzzle_play_settings";
@import "ingame_hud/puzzle_play_metadata";
@import "ingame_hud/puzzle_complete_notification";
@ -85,6 +86,7 @@ ingame_HUD_PuzzleEditorReview,
ingame_HUD_PuzzleEditorControls,
ingame_HUD_PuzzleEditorTitle,
ingame_HUD_PuzzleEditorSettings,
ingame_HUD_PuzzlePlaySettings,
ingame_HUD_PuzzlePlayMetadata,
ingame_HUD_PuzzlePlayTitle,
ingame_HUD_Notifications,

View File

@ -183,7 +183,7 @@
.updateLabel {
position: absolute;
transform: translateX(50%) rotate(-5deg);
color: #3291e9;
color: #ff590b;
@include Heading;
font-weight: bold;
@include S(right, 40px);

View File

@ -7,6 +7,10 @@
> h1 {
justify-self: start;
}
.createPuzzle {
background-color: $colorGreenBright;
}
}
> .container {
@ -42,12 +46,22 @@
color: #fff;
cursor: default;
}
@include DarkThemeOverride {
background: $accentColorDark;
color: #bbbbc4;
&.active {
background: $colorBlueBright;
color: #fff;
}
}
}
}
> .puzzles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(D(150px), 1fr));
grid-template-columns: repeat(auto-fit, D(150px));
@include S(grid-auto-rows, 120px);
@include S(grid-gap, 3px);
@include S(margin-top, 10px);
@ -55,6 +69,7 @@
@include S(height, 360px);
overflow-y: scroll;
pointer-events: all;
position: relative;
> .puzzle {
width: 100%;
@ -72,6 +87,10 @@
cursor: pointer;
position: relative;
@include DarkThemeOverride {
background: rgba(0, 0, 10, 0.2);
}
@include InlineAnimation(0.12s ease-in-out) {
0% {
opacity: 0;
@ -86,9 +105,12 @@
}
> .title {
grid-column: 1 / 2;
grid-row: 1/ 2;
grid-column: 1 / 3;
grid-row: 1 / 2;
@include PlainText;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
> .icon {
@ -122,12 +144,14 @@
display: flex;
align-items: center;
justify-self: end;
justify-content: center;
align-self: end;
@include DarkThemeInvert;
> .downloads {
@include SuperSmallText;
color: #000;
align-self: start;
justify-self: start;
font-weight: bold;
@include S(margin-right, 10px);
@ -149,7 +173,6 @@
align-items: center;
justify-content: center;
color: #000;
align-self: start;
justify-self: start;
font-weight: bold;
@include S(padding-left, 14px);
@ -162,6 +185,14 @@
)} #{D(8px)} no-repeat;
}
}
> .difficulty {
@include S(margin-top, 1px);
@include S(margin-right, 7px);
display: inline-flex;
align-items: center;
justify-content: center;
}
}
&.completed {
@ -189,17 +220,27 @@
contain no-repeat;
}
}
@include DarkThemeOverride {
&::after {
/* @load-async */
background: uiResource("icons/puzzle_complete_indicator_inverse.png") center
center / contain no-repeat;
}
}
}
}
> .loader,
> .empty {
grid-column: 1 / -1;
grid-row: 1 / 3;
display: flex;
align-items: center;
color: $accentColorDark;
justify-content: center;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
}

View File

@ -2,7 +2,7 @@ export const CHANGELOG = [
{
version: "1.4.0",
date: "UNRELEASED",
entries: ["Added puzzle mode"],
entries: ["Added puzzle mode", "Belts in blueprints should now always paste correctly"],
},
{
version: "1.3.1",

View File

@ -76,6 +76,7 @@ export const globalConfig = {
puzzleModeSpeed: 3,
puzzleMinBoundsSize: 2,
puzzleMaxBoundsSize: 20,
puzzleValidationDurationSeconds: 30,
buildingSpeeds: {
cutter: 1 / 4,
@ -99,7 +100,7 @@ export const globalConfig = {
gameSpeed: 1,
warmupTimeSecondsFast: 0.5,
warmupTimeSecondsRegular: 3,
warmupTimeSecondsRegular: 1.5,
smoothing: {
smoothMainCanvas: smoothCanvas && true,

View File

@ -108,6 +108,14 @@ export class BeltPath extends BasicSerializableObject {
}
}
/**
* Clears all items
*/
clearAllItems() {
this.items = [];
this.spacingToFirstItem = this.totalLength;
}
/**
* Returns whether this path can accept a new item
* @returns {boolean}

View File

@ -149,6 +149,7 @@ export class Blueprint {
*/
tryPlace(root, tile) {
return root.logic.performBulkOperation(() => {
return root.logic.performImmutableOperation(() => {
let count = 0;
for (let i = 0; i < this.entities.length; ++i) {
const entity = this.entities[i];
@ -173,5 +174,6 @@ export class Blueprint {
return count !== 0;
});
});
}
}

View File

@ -102,18 +102,6 @@ export class MetaBalancerBuilding extends MetaBuilding {
available.push(enumBalancerVariants.splitter, enumBalancerVariants.splitterInverse);
}
if (root.gameMode.getIsDeterministic()) {
// mergers are not deterministic
available = available.filter(
v =>
![
enumBalancerVariants.merger,
enumBalancerVariants.mergerInverse,
defaultBuildingVariant,
].includes(v)
);
}
return available;
}

View File

@ -74,7 +74,10 @@ export class MetaPainterBuilding extends MetaBuilding {
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_painter_double)) {
variants.push(enumPainterVariants.double);
}
if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers)) {
if (
root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_wires_painter_and_levers) &&
root.gameMode.getSupportsWires()
) {
variants.push(enumPainterVariants.quad);
}
return variants;

View File

@ -23,6 +23,11 @@ export class Component extends BasicSerializableObject {
*/
copyAdditionalStateTo(otherComponent) {}
/**
* Clears all items and state
*/
clear() {}
/* dev:start */
/**

View File

@ -30,6 +30,10 @@ export class BeltReaderComponent extends Component {
this.type = type;
this.clear();
}
clear() {
/**
* Which items went through the reader, we only store the time
* @type {Array<number>}

View File

@ -40,6 +40,10 @@ export class FilterComponent extends Component {
constructor() {
super();
this.clear();
}
clear() {
/**
* Items in queue to leave through
* @type {Array<PendingFilterItem>}

View File

@ -26,6 +26,10 @@ export class GoalAcceptorComponent extends Component {
/** @type {BaseItem | undefined} */
this.item = item;
this.clear();
}
clear() {
// the last items we delivered
/** @type {{ item: BaseItem; time: number; }[]} */
this.deliveryHistory = [];

View File

@ -36,6 +36,11 @@ export class ItemAcceptorComponent extends Component {
constructor({ slots = [] }) {
super();
this.setSlots(slots);
this.clear();
}
clear() {
/**
* Fixes belt animations
* @type {Array<{
@ -46,8 +51,6 @@ export class ItemAcceptorComponent extends Component {
* }>}
*/
this.itemConsumptionAnimations = [];
this.setSlots(slots);
}
/**

View File

@ -48,6 +48,13 @@ export class ItemEjectorComponent extends Component {
this.renderFloatingItems = renderFloatingItems;
}
clear() {
for (const slot of this.slots) {
slot.item = null;
slot.progress = 0;
}
}
/**
* @param {Array<{pos: Vector, direction: enumDirection }>} slots The slots to eject on
*/

View File

@ -64,10 +64,8 @@ export class ItemProcessorComponent extends Component {
}) {
super();
// Which slot to emit next, this is only a preference and if it can't emit
// it will take the other one. Some machines ignore this (e.g. the balancer) to make
// sure the outputs always match
this.nextOutputSlot = 0;
// How many inputs we need for one charge
this.inputsPerCharge = inputsPerCharge;
// Type of the processor
this.type = processorType;
@ -75,8 +73,14 @@ export class ItemProcessorComponent extends Component {
// Type of processing requirement
this.processingRequirement = processingRequirement;
// How many inputs we need for one charge
this.inputsPerCharge = inputsPerCharge;
this.clear();
}
clear() {
// Which slot to emit next, this is only a preference and if it can't emit
// it will take the other one. Some machines ignore this (e.g. the balancer) to make
// sure the outputs always match
this.nextOutputSlot = 0;
/**
* Our current inputs

View File

@ -24,13 +24,6 @@ export class MinerComponent extends Component {
this.lastMiningTime = 0;
this.chainable = chainable;
/**
* Stores items from other miners which were chained to this
* miner.
* @type {Array<BaseItem>}
*/
this.itemChainBuffer = [];
/**
* @type {BaseItem}
*/
@ -42,6 +35,17 @@ export class MinerComponent extends Component {
* @type {Entity|null|false}
*/
this.cachedChainedMiner = null;
this.clear();
}
clear() {
/**
* Stores items from other miners which were chained to this
* miner.
* @type {Array<BaseItem>}
*/
this.itemChainBuffer = [];
}
/**

View File

@ -71,6 +71,14 @@ export class StaticMapEntityComponent extends Component {
return getBuildingDataFromCode(this.code).variant;
}
/**
* Returns the buildings rotation variant
* @returns {number}
*/
getRotationVariant() {
return getBuildingDataFromCode(this.code).rotationVariant;
}
/**
* Copy the current state to another component
* @param {Component} otherComponent

View File

@ -41,6 +41,17 @@ export class UndergroundBeltComponent extends Component {
this.mode = mode;
this.tier = tier;
/**
* The linked entity, used to speed up performance. This contains either
* the entrance or exit depending on the tunnel type
* @type {LinkedUndergroundBelt}
*/
this.cachedLinkedEntity = null;
this.clear();
}
clear() {
/** @type {Array<{ item: BaseItem, progress: number }>} */
this.consumptionAnimations = [];
@ -51,13 +62,6 @@ export class UndergroundBeltComponent extends Component {
* @type {Array<[BaseItem, number]>} Format is [Item, ingame time to eject the item]
*/
this.pendingItems = [];
/**
* The linked entity, used to speed up performance. This contains either
* the entrance or exit depending on the tunnel type
* @type {LinkedUndergroundBelt}
*/
this.cachedLinkedEntity = null;
}
/**

View File

@ -170,11 +170,6 @@ export class GameMode extends BasicSerializableObject {
return true;
}
/** @returns {boolean} */
getIsDeterministic() {
return false;
}
/** @returns {boolean} */
getIsEditor() {
return false;

View File

@ -1,13 +1,26 @@
/* typehints:start */
import { PuzzlePlayGameMode } from "../../modes/puzzle_play";
/* typehints:end */
import { InputReceiver } from "../../../core/input_receiver";
import { makeDiv } from "../../../core/utils";
import { SOUNDS } from "../../../platform/sound";
import { T } from "../../../translations";
import { enumColors } from "../../colors";
import { ColorItem } from "../../items/color_item";
import { PuzzlePlayGameMode } from "../../modes/puzzle_play";
import { finalGameShape, rocketShape } from "../../modes/regular";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach";
import { ShapeItem } from "../../items/shape_item";
import { ShapeDefinition } from "../../shape_definition";
export const PUZZLE_RATINGS = [
new ColorItem(enumColors.red),
new ShapeItem(ShapeDefinition.fromShortKey("CuCuCuCu")),
new ShapeItem(ShapeDefinition.fromShortKey("WwWwWwWw")),
new ShapeItem(ShapeDefinition.fromShortKey(finalGameShape)),
new ShapeItem(ShapeDefinition.fromShortKey(rocketShape)),
];
export class HUDPuzzleCompleteNotification extends BaseHUDPart {
initialize() {
@ -33,15 +46,28 @@ export class HUDPuzzleCompleteNotification extends BaseHUDPart {
this.elemTitle = makeDiv(dialog, null, ["title"], T.ingame.puzzleCompletion.title);
this.elemContents = makeDiv(dialog, null, ["contents"]);
this.elemActions = makeDiv(dialog, null, ["actions"]);
const reportBtn = document.createElement("button");
reportBtn.classList.add("styledButton", "report");
reportBtn.innerHTML = T.ingame.puzzleEditorSettings.report;
this.elemActions.appendChild(reportBtn);
this.trackClicks(reportBtn, this.report);
const shareBtn = document.createElement("button");
shareBtn.classList.add("styledButton", "share");
shareBtn.innerHTML = T.ingame.puzzleEditorSettings.share;
this.elemActions.appendChild(shareBtn);
this.trackClicks(shareBtn, this.share);
const stepLike = makeDiv(this.elemContents, null, ["step", "stepLike"]);
makeDiv(stepLike, null, ["title"], T.ingame.puzzleCompletion.titleLike);
const buttons = makeDiv(stepLike, null, ["buttons"]);
const likeButtons = makeDiv(stepLike, null, ["buttons"]);
this.buttonLikeYes = document.createElement("button");
this.buttonLikeYes.classList.add("liked-yes");
buttons.appendChild(this.buttonLikeYes);
likeButtons.appendChild(this.buttonLikeYes);
this.trackClicks(this.buttonLikeYes, () => {
this.selectionLiked = true;
this.updateState();
@ -49,7 +75,7 @@ export class HUDPuzzleCompleteNotification extends BaseHUDPart {
this.buttonLikeNo = document.createElement("button");
this.buttonLikeNo.classList.add("liked-no");
buttons.appendChild(this.buttonLikeNo);
likeButtons.appendChild(this.buttonLikeNo);
this.trackClicks(this.buttonLikeNo, () => {
this.selectionLiked = false;
this.updateState();
@ -59,30 +85,33 @@ export class HUDPuzzleCompleteNotification extends BaseHUDPart {
makeDiv(stepDifficulty, null, ["title"], T.ingame.puzzleCompletion.titleRating);
const shapeContainer = makeDiv(stepDifficulty, null, ["shapes"]);
const items = [
new ColorItem(enumColors.red),
this.root.shapeDefinitionMgr.getShapeItemFromShortKey("CuCuCuCu"),
this.root.shapeDefinitionMgr.getShapeItemFromShortKey("WwWwWwWw"),
this.root.shapeDefinitionMgr.getShapeItemFromShortKey("WrRgWrRg:CwCrCwCr:SgSgSgSg"),
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(finalGameShape),
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(rocketShape),
];
this.difficultyCanvases = [];
this.difficultyElements = [];
let index = 0;
for (const shape of items) {
for (const shape of PUZZLE_RATINGS) {
const localIndex = index;
const elem = document.createElement("div");
elem.classList.add("rating");
shapeContainer.appendChild(elem);
const canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const context = canvas.getContext("2d");
shape.drawFullSizeOnCanvas(context, 128);
shapeContainer.appendChild(canvas);
this.trackClicks(canvas, () => {
elem.appendChild(canvas);
this.trackClicks(elem, () => {
this.selectionDifficulty = localIndex;
this.updateState();
});
this.difficultyCanvases.push(canvas);
this.difficultyElements.push(elem);
const desc = document.createElement("div");
desc.classList.add("description");
desc.innerText = T.ingame.puzzleCompletion.difficulties[localIndex];
elem.appendChild(desc);
++index;
}
@ -94,10 +123,20 @@ export class HUDPuzzleCompleteNotification extends BaseHUDPart {
this.trackClicks(this.btnClose, this.close);
}
share() {
const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
mode.sharePuzzle();
}
report() {
const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
mode.reportPuzzle();
}
updateState() {
this.buttonLikeYes.classList.toggle("active", this.selectionLiked === true);
this.buttonLikeNo.classList.toggle("active", this.selectionLiked === false);
this.difficultyCanvases.forEach((canvas, index) =>
this.difficultyElements.forEach((canvas, index) =>
canvas.classList.toggle("active", index === this.selectionDifficulty)
);

View File

@ -19,9 +19,6 @@ const logger = createLogger("puzzle-review");
export class HUDPuzzleEditorReview extends BaseHUDPart {
constructor(root) {
super(root);
this.validationEndsIn = null;
this.callOnceValidationEnded = null;
}
createElements(parent) {
@ -45,9 +42,37 @@ export class HUDPuzzleEditorReview extends BaseHUDPart {
return;
}
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(T.puzzleMenu.validtingPuzzle);
this.validationEndsIn = this.root.time.now() + globalConfig.goalAcceptorMinimumDurationSeconds;
this.callOnceValidationEnded = () => {
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(T.puzzleMenu.validatingPuzzle);
// Wait a bit, so the user sees the puzzle actually got validated
setTimeout(() => {
// Manually simulate ticks
this.root.logic.clearAllBeltsAndItems();
const ticks =
this.root.gameMode.getFixedTickrate() * globalConfig.puzzleValidationDurationSeconds;
const deltaMs = this.root.dynamicTickrate.deltaMs;
logger.log("Simulating", ticks, "ticks, start=", this.root.time.now().toFixed(1));
const now = performance.now();
for (let i = 0; i < ticks; ++i) {
if (i % Math.round((ticks - 1) / 10) === 0) {
console.log("Ticking", Math.round((i / ticks) * 100) + "%");
}
// Perform logic ticks
this.root.time.performTicks(deltaMs, this.root.gameState.core.boundInternalTick);
}
const duration = performance.now() - now;
logger.log(
"Simulated",
ticks,
"ticks, end=",
this.root.time.now().toFixed(1),
"duration=",
duration.toFixed(2),
"ms"
);
closeLoading();
const validationError = this.validatePuzzle();
if (validationError) {
@ -55,7 +80,7 @@ export class HUDPuzzleEditorReview extends BaseHUDPart {
return;
}
this.startSubmit();
};
}, 750);
}
startSubmit(title = "", shortKey = "") {
@ -102,7 +127,7 @@ export class HUDPuzzleEditorReview extends BaseHUDPart {
title: T.dialogs.submitPuzzle.title,
desc: "",
formElements: [nameInput, itemInput, shapeKeyInput],
buttons: ["cancel:bad:escape", "ok:good:enter"],
buttons: ["ok:good:enter"],
});
itemInput.valueChosen.add(value => {
@ -154,18 +179,6 @@ export class HUDPuzzleEditorReview extends BaseHUDPart {
);
}
update() {
if (
this.validationEndsIn &&
this.validationEndsIn < this.root.time.now() &&
this.callOnceValidationEnded
) {
const callMethod = this.callOnceValidationEnded;
this.callOnceValidationEnded = null;
callMethod();
}
}
validatePuzzle() {
// Check there is at least one constant producer and goal acceptor
const producers = this.root.entityMgr.getAllWithComponent(ConstantSignalComponent);

View File

@ -41,7 +41,10 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
<button class="styledButton plus">+</button>
</div>
<div class="buttonBar">
<button class="styledButton trim">${T.ingame.puzzleEditorSettings.trimZone}</button>
<button class="styledButton clear">${T.ingame.puzzleEditorSettings.clearItems}</button>
</div>
</div>`
);
@ -50,20 +53,82 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
bind(".zoneHeight .minus", () => this.modifyZone(0, -1));
bind(".zoneHeight .plus", () => this.modifyZone(0, 1));
bind("button.trim", this.trim);
bind("button.clear", this.clear);
}
}
clear() {
this.root.logic.clearAllBeltsAndItems();
}
trim() {
const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode);
// Now, find the center
const buildings = this.root.entityMgr.entities.slice();
let w = mode.zoneWidth;
let h = mode.zoneHeight;
if (this.anyBuildingOutsideZone(w, h)) {
logger.error("Trim: Zone is already too small");
if (buildings.length === 0) {
// nothing to do
return;
}
logger.log("Zone trim: Starts at", w, h);
let minRect = null;
for (const building of buildings) {
const staticComp = building.components.StaticMapEntity;
const bounds = staticComp.getTileSpaceBounds();
if (!minRect) {
minRect = bounds;
} else {
minRect = minRect.getUnion(bounds);
}
}
const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode);
const moveByInverse = minRect.getCenter().round();
// move buildings
if (moveByInverse.length() > 0) {
// increase area size
mode.zoneWidth = globalConfig.puzzleMaxBoundsSize;
mode.zoneHeight = globalConfig.puzzleMaxBoundsSize;
// First, remove any items etc
this.root.logic.clearAllBeltsAndItems();
this.root.logic.performImmutableOperation(() => {
// 1. remove all buildings
for (const building of buildings) {
if (!this.root.logic.tryDeleteBuilding(building)) {
assertAlways(false, "Failed to remove building in trim");
}
}
// 2. place them again, but centered
for (const building of buildings) {
const staticComp = building.components.StaticMapEntity;
const result = this.root.logic.tryPlaceBuilding({
origin: staticComp.origin.sub(moveByInverse),
building: staticComp.getMetaBuilding(),
originalRotation: staticComp.originalRotation,
rotation: staticComp.rotation,
rotationVariant: staticComp.getRotationVariant(),
variant: staticComp.getVariant(),
});
if (!result) {
this.root.bulkOperationRunning = false;
assertAlways(false, "Failed to re-place building in trim");
}
if (building.components.ConstantSignal) {
result.components.ConstantSignal.signal = building.components.ConstantSignal.signal;
}
}
});
}
// 3. Actually trim
let w = mode.zoneWidth;
let h = mode.zoneHeight;
while (!this.anyBuildingOutsideZone(w - 1, h)) {
--w;
@ -73,12 +138,6 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
--h;
}
logger.log("Zone trim: After height pass at", w, h);
if (this.anyBuildingOutsideZone(w, h)) {
logger.error("Trim: Zone is too small *after* trim");
return;
}
mode.zoneWidth = w;
mode.zoneHeight = h;
this.updateZoneValues();

View File

@ -1,22 +1,76 @@
import { makeDiv } from "../../../core/utils";
/* typehints:start */
import { PuzzlePlayGameMode } from "../../modes/puzzle_play";
/* typehints:end */
import { formatBigNumberFull, formatSeconds, makeDiv } from "../../../core/utils";
import { T } from "../../../translations";
import { BaseHUDPart } from "../base_hud_part";
const copy = require("clipboard-copy");
export class HUDPuzzlePlayMetadata extends BaseHUDPart {
createElements(parent) {
this.titleElement = makeDiv(parent, "ingame_HUD_PuzzlePlayTitle");
this.titleElement.innerText = "PUZZLE";
const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
const puzzle = mode.puzzle;
this.puzzleNameElement = makeDiv(this.titleElement, null, ["name"]);
this.puzzleNameElement.innerText = "tobspr's first puzzle";
this.puzzleNameElement.innerText = puzzle.meta.title;
this.element = makeDiv(parent, "ingame_HUD_PuzzlePlayMetadata");
this.element.innerHTML = `
<div class="author">Author: tobspr</div>
<div class="plays">Plays: 12.000</div>
<div class="likes">Likes: 512</div>
<div class="info plays">
<span class="downloads">${formatBigNumberFull(puzzle.meta.downloads)}</span>
<span class="likes">${formatBigNumberFull(puzzle.meta.likes)}</span>
</div>
<div class="info author"><label>${T.ingame.puzzleMetadata.author}</label><span></span></div>
<div class="info key">
<label>${T.ingame.puzzleMetadata.shortKey}</label><span>${puzzle.meta.shortKey}</span>
</div>
<div class="info rating">
<label>${T.ingame.puzzleMetadata.rating}</label>
<span>${
puzzle.meta.difficulty
? puzzle.meta.difficulty.toFixed(1)
: T.puzzleMenu.difficultyNotDetermined
}</span>
</div>
<div class="info rating">
<label>${T.ingame.puzzleMetadata.averageDuration}</label>
<span>${
puzzle.meta.averageTime
? formatSeconds(puzzle.meta.averageTime)
: T.puzzleMenu.difficultyNotDetermined
}</span>
</div>
<div class="buttons">
<button class="styledButton share">${T.ingame.puzzleEditorSettings.share}</button>
<button class="styledButton report">${T.ingame.puzzleEditorSettings.report}</button>
</div>
`;
this.trackClicks(this.element.querySelector("button.share"), this.share);
this.trackClicks(this.element.querySelector("button.report"), this.report);
/** @type {HTMLElement} */ (this.element.querySelector(".author span")).innerText =
puzzle.meta.author;
}
initialize() {}
share() {
const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
mode.sharePuzzle();
}
report() {
const mode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
mode.reportPuzzle();
}
}

View File

@ -0,0 +1,36 @@
import { createLogger } from "../../../core/logging";
import { makeDiv } from "../../../core/utils";
import { T } from "../../../translations";
import { BaseHUDPart } from "../base_hud_part";
const logger = createLogger("puzzle-play");
export class HUDPuzzlePlaySettings extends BaseHUDPart {
createElements(parent) {
this.element = makeDiv(parent, "ingame_HUD_PuzzlePlaySettings");
if (this.root.gameMode.getBuildableZones()) {
const bind = (selector, handler) =>
this.trackClicks(this.element.querySelector(selector), handler);
makeDiv(
this.element,
null,
["section"],
`
<button class="styledButton clear">${T.ingame.puzzleEditorSettings.clearItems}</button>
`
);
bind("button.clear", this.clear);
}
}
clear() {
this.root.logic.clearAllBeltsAndItems();
}
initialize() {
this.visible = true;
}
}

View File

@ -4,6 +4,7 @@ import { STOP_PROPAGATION } from "../core/signal";
import { round2Digits } from "../core/utils";
import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector";
import { getBuildingDataFromCode } from "./building_codes";
import { Component } from "./component";
import { enumWireVariant } from "./components/wire";
import { Entity } from "./entity";
import { CHUNK_OVERLAY_RES } from "./map_chunk_view";
@ -161,6 +162,27 @@ export class GameLogic {
return returnValue;
}
/**
* Performs a immutable operation, causing no recalculations
* @param {function} operation
*/
performImmutableOperation(operation) {
logger.warn("Running immutable operation ...");
assert(!this.root.immutableOperationRunning, "Can not run two immutalbe operations twice");
this.root.immutableOperationRunning = true;
const now = performance.now();
const returnValue = operation();
const duration = performance.now() - now;
logger.log("Done in", round2Digits(duration), "ms");
assert(
this.root.immutableOperationRunning,
"Immutable operation = false while immutable operation was running"
);
this.root.immutableOperationRunning = false;
this.root.signals.immutableOperationFinished.dispatch();
return returnValue;
}
/**
* Returns whether the given building can get removed
* @param {Entity} building
@ -342,8 +364,6 @@ export class GameLogic {
return !!overlayMatrix[localPosition.x + localPosition.y * 3];
}
g(tile, edge) {}
/**
* Returns the acceptors and ejectors which affect the current tile
* @param {Vector} tile
@ -425,4 +445,22 @@ export class GameLogic {
}
return { ejectors, acceptors };
}
/**
* Clears all belts and items
*/
clearAllBeltsAndItems() {
// Belts
const beltPaths = this.root.systemMgr.systems.belt.beltPaths;
for (const path of beltPaths) {
path.clearAllItems();
}
// Acceptors
for (const entity of this.root.entityMgr.entities) {
for (const component of Object.values(entity.components)) {
/** @type {Component} */ (component).clear();
}
}
}
}

View File

@ -84,10 +84,6 @@ export class PuzzleGameMode extends GameMode {
return false;
}
getIsDeterministic() {
return true;
}
getFixedTickrate() {
return 300;
}

View File

@ -27,8 +27,10 @@ import { T } from "../../translations";
import { HUDPuzzlePlayMetadata } from "../hud/parts/puzzle_play_metadata";
import { createLogger } from "../../core/logging";
import { HUDPuzzleCompleteNotification } from "../hud/parts/puzzle_complete_notification";
import { HUDPuzzlePlaySettings } from "../hud/parts/puzzle_play_settings";
const logger = createLogger("puzzle-play");
const copy = require("clipboard-copy");
export class PuzzlePlayGameMode extends PuzzleGameMode {
static getId() {
@ -66,6 +68,7 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
];
this.additionalHudParts.puzzlePlayMetadata = HUDPuzzlePlayMetadata;
this.additionalHudParts.puzzlePlaySettings = HUDPuzzlePlaySettings;
this.additionalHudParts.puzzleCompleteNotification = HUDPuzzleCompleteNotification;
root.signals.postLoadHook.add(this.loadPuzzle, this);
@ -122,4 +125,47 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
closeLoading();
});
}
sharePuzzle() {
copy(this.puzzle.meta.shortKey);
this.root.hud.parts.dialogs.showInfo(
T.dialogs.puzzleShare.title,
T.dialogs.puzzleShare.desc.replace("<key>", this.puzzle.meta.shortKey)
);
}
reportPuzzle() {
const { optionSelected } = this.root.hud.parts.dialogs.showOptionChooser(
T.dialogs.puzzleReport.title,
{
options: [
{ value: "profane", text: T.dialogs.puzzleReport.options.profane },
{ value: "unsolvable", text: T.dialogs.puzzleReport.options.unsolvable },
{ value: "trolling", text: T.dialogs.puzzleReport.options.trolling },
],
}
);
optionSelected.add(option => {
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog();
this.root.app.clientApi.apiReportPuzzle(this.puzzle.meta.id, option).then(
() => {
closeLoading();
this.root.hud.parts.dialogs.showInfo(
T.dialogs.puzzleReportComplete.title,
T.dialogs.puzzleReportComplete.desc
);
},
err => {
closeLoading();
this.root.hud.parts.dialogs.showInfo(
T.dialogs.puzzleReportError.title,
T.dialogs.puzzleReportError.desc + " " + err
);
}
);
});
}
}

View File

@ -79,6 +79,11 @@ export class GameRoot {
*/
this.bulkOperationRunning = false;
/**
* Whether a immutable operation is running
*/
this.immutableOperationRunning = false;
//////// Other properties ///////
/** @type {Camera} */
@ -169,6 +174,7 @@ export class GameRoot {
itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()),
bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
immutableOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
editModeChanged: /** @type {TypedSignal<[Layer]>} */ (new Signal()),

View File

@ -123,6 +123,10 @@ export class BeltSystem extends GameSystemWithFilter {
return;
}
if (this.root.immutableOperationRunning) {
return;
}
const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBuilding);
// Compute affected area
const originalRect = staticComp.getTileSpaceBounds();

View File

@ -50,10 +50,8 @@
},
"zone": {
"background": "#fff",
"border": "rgba(23, 192, 255, 0.1)",
"borderSolid": "rgba(23, 192, 255, 0.7)",
"outerColor": "rgba(240, 240, 255, 0.5)"
"borderSolid": "rgba(23, 192, 255, 1)",
"outerColor": "rgba(20 , 20, 25, 0.5)"
}
},

View File

@ -2,6 +2,8 @@
import { Application } from "../application";
/* typehints:end */
import { createLogger } from "../core/logging";
import { compressX64 } from "../core/lzstring";
import { T } from "../translations";
const logger = createLogger("puzzle-api");
@ -70,7 +72,8 @@ export class ClientAPI {
])
.then(data => {
if (data.error) {
throw data.error;
logger.warn("Got error from api:", data);
throw T.backendErrors[data.error] || data.error;
}
return data;
})
@ -126,6 +129,31 @@ export class ClientAPI {
return this._request("/v1/puzzles/download/" + puzzleId, {});
}
/**
* @param {number} shortKey
* @returns {Promise<import("../savegame/savegame_typedefs").PuzzleFullData>}
*/
apiDownloadPuzzleByKey(shortKey) {
if (!this.isLoggedIn()) {
return Promise.reject("not-logged-in");
}
return this._request("/v1/puzzles/download/" + shortKey, {});
}
/**
* @param {number} puzzleId
* @returns {Promise<void>}
*/
apiReportPuzzle(puzzleId, reason) {
if (!this.isLoggedIn()) {
return Promise.reject("not-logged-in");
}
return this._request("/v1/puzzles/report/" + puzzleId, {
method: "POST",
body: { reason },
});
}
/**
* @param {number} puzzleId
* @param {object} payload
@ -157,7 +185,10 @@ export class ClientAPI {
}
return this._request("/v1/puzzles/submit", {
method: "POST",
body: payload,
body: {
...payload,
data: compressX64(JSON.stringify(payload.data)),
},
});
}
}

View File

@ -48,6 +48,8 @@
* shortKey: string;
* likes: number;
* downloads: number;
* difficulty: number | null;
* averageTime: number | null;
* title: string;
* author: string;
* completed: boolean;

View File

@ -67,7 +67,7 @@ export class MainMenuState extends GameState {
<img src="${cachebust(
G_CHINA_VERSION ? "res/logo_cn.png" : "res/logo.png"
)}" alt="shapez.io Logo">
<span class="updateLabel">v${G_BUILD_VERSION} - Achievements!</span>
<span class="updateLabel">v${G_BUILD_VERSION} - Puzzle DLC!</span>
</div>
<div class="mainWrapper ${showDemoBadges ? "demo" : "noDemo"}">
@ -208,7 +208,7 @@ export class MainMenuState extends GameState {
const qs = this.htmlElement.querySelector.bind(this.htmlElement);
if (G_IS_DEV && globalConfig.debug.testPuzzleMode) {
this.onPuzzleModeButtonClicked();
this.onPuzzleModeButtonClicked(true);
return;
}
@ -320,10 +320,22 @@ export class MainMenuState extends GameState {
const puzzleModeButton = makeButton(bottomButtonContainer, ["styledButton"], T.mainMenu.puzzleMode);
bottomButtonContainer.appendChild(puzzleModeButton);
this.trackClicks(puzzleModeButton, this.onPuzzleModeButtonClicked);
this.trackClicks(puzzleModeButton, () => this.onPuzzleModeButtonClicked());
}
onPuzzleModeButtonClicked(force = false) {
const hasUnlockedBlueprints = this.app.savegameMgr.getSavegamesMetaData().some(s => s.level >= 12);
console.log(hasUnlockedBlueprints);
if (!force && !hasUnlockedBlueprints) {
const { ok } = this.dialogs.showWarning(
T.dialogs.puzzlePlayRegularRecommendation.title,
T.dialogs.puzzlePlayRegularRecommendation.desc,
["cancel:good", "ok:bad:timeout"]
);
ok.add(() => this.onPuzzleModeButtonClicked(true));
return;
}
onPuzzleModeButtonClicked() {
this.moveToState("LoginState", {
nextStateId: "PuzzleMenuState",
});

View File

@ -1,12 +1,16 @@
import { globalConfig } from "../core/config";
import { createLogger } from "../core/logging";
import { DialogWithForm } from "../core/modal_dialog_elements";
import { FormElementInput } from "../core/modal_dialog_forms";
import { TextualGameState } from "../core/textual_game_state";
import { formatBigNumberFull } from "../core/utils";
import { clamp, formatBigNumberFull } from "../core/utils";
import { enumGameModeIds } from "../game/game_mode";
import { PUZZLE_RATINGS } from "../game/hud/parts/puzzle_complete_notification";
import { ShapeDefinition } from "../game/shape_definition";
import { Savegame } from "../savegame/savegame";
import { T } from "../translations";
const categories = ["levels", "new", "top-rated", "mine"];
const categories = ["top-rated", "short", "hard", "new", "mine"];
/**
* @type {import("../savegame/savegame_typedefs").PuzzleMetadata}
@ -16,6 +20,8 @@ const SAMPLE_PUZZLE = {
shortKey: "CuCuCuCu",
downloads: 0,
likes: 0,
averageTime: 1,
difficulty: null,
title: "Level 1",
author: "verylongsteamnamewhichbreaks",
completed: false,
@ -63,6 +69,7 @@ export class PuzzleMenuState extends TextualGameState {
<h1><button class="backButton"></button> ${this.getStateHeaderTitle()}</h1>
<div class="actions">
<button class="styledButton loadPuzzle">${T.puzzleMenu.loadPuzzle}</button>
<button class="styledButton createPuzzle">+ ${T.puzzleMenu.createPuzzle}</button>
</div>
</div>`;
@ -89,12 +96,7 @@ export class PuzzleMenuState extends TextualGameState {
.join("")}
</div>
<div class="puzzles" id="mainContainer">
<div class="puzzle"></div>
<div class="puzzle"></div>
<div class="puzzle"></div>
<div class="puzzle"></div>
</div>
<div class="puzzles" id="mainContainer"></div>
`;
return html;
@ -104,9 +106,11 @@ export class PuzzleMenuState extends TextualGameState {
if (category === this.activeCategory) {
return;
}
if (this.loading) {
return;
}
this.loading = true;
this.activeCategory = category;
@ -175,6 +179,22 @@ export class PuzzleMenuState extends TextualGameState {
stats.classList.add("stats");
elem.appendChild(stats);
if (puzzle.difficulty !== null) {
const difficulty = document.createElement("div");
difficulty.classList.add("difficulty");
const canvas = document.createElement("canvas");
canvas.width = 32;
canvas.height = 32;
const context = canvas.getContext("2d");
PUZZLE_RATINGS[
clamp(Math.round(puzzle.difficulty), 0, PUZZLE_RATINGS.length - 1)
].drawFullSizeOnCanvas(context, 32);
difficulty.appendChild(canvas);
stats.appendChild(difficulty);
}
const downloads = document.createElement("div");
downloads.classList.add("downloads");
downloads.innerText = String(puzzle.downloads);
@ -233,16 +253,8 @@ export class PuzzleMenuState extends TextualGameState {
this.app.clientApi.apiDownloadPuzzle(puzzle.id).then(
puzzleData => {
closeLoading();
logger.log("Got puzzle:", puzzleData);
const savegame = this.app.savegameMgr.createNewSavegame();
this.moveToState("InGameState", {
gameModeId: enumGameModeIds.puzzlePlay,
gameModeParameters: {
puzzle: puzzleData,
},
savegame,
});
this.startLoadedPuzzle(puzzleData);
},
err => {
closeLoading();
@ -255,8 +267,23 @@ export class PuzzleMenuState extends TextualGameState {
);
}
/**
*
* @param {import("../savegame/savegame_typedefs").PuzzleFullData} puzzle
*/
startLoadedPuzzle(puzzle) {
const savegame = this.createEmptySavegame();
this.moveToState("InGameState", {
gameModeId: enumGameModeIds.puzzlePlay,
gameModeParameters: {
puzzle,
},
savegame,
});
}
onEnter(payload) {
this.selectCategory("levels");
this.selectCategory(categories[0]);
if (payload && payload.error) {
this.dialogs.showWarning(payload.error.title, payload.error.desc);
@ -268,6 +295,7 @@ export class PuzzleMenuState extends TextualGameState {
}
this.trackClicks(this.htmlElement.querySelector("button.createPuzzle"), () => this.createNewPuzzle());
this.trackClicks(this.htmlElement.querySelector("button.loadPuzzle"), () => this.loadPuzzle());
if (G_IS_DEV && globalConfig.debug.testPuzzleMode) {
// this.createNewPuzzle();
@ -275,6 +303,56 @@ export class PuzzleMenuState extends TextualGameState {
}
}
createEmptySavegame() {
return new Savegame(this.app, {
internalId: "puzzle",
metaDataRef: {
internalId: "puzzle",
lastUpdate: 0,
version: 0,
level: 0,
name: "puzzle",
},
});
}
loadPuzzle() {
const shortKeyInput = new FormElementInput({
id: "shortKey",
label: null,
placeholder: "",
defaultValue: "",
validator: val => ShapeDefinition.isValidShortKey(val),
});
const dialog = new DialogWithForm({
app: this.app,
title: T.dialogs.puzzleLoadShortKey.title,
desc: T.dialogs.puzzleLoadShortKey.desc,
formElements: [shortKeyInput],
buttons: ["ok:good:enter"],
});
this.dialogs.internalShowDialog(dialog);
dialog.buttonSignals.ok.add(() => {
const closeLoading = this.dialogs.showLoadingDialog();
this.app.clientApi.apiDownloadPuzzleByKey(shortKeyInput.getValue()).then(
puzzle => {
closeLoading();
this.startLoadedPuzzle(puzzle);
},
err => {
closeLoading();
this.dialogs.showWarning(
T.dialogs.puzzleDownloadError.title,
T.dialogs.puzzleDownloadError.desc + " " + err
);
}
);
});
}
createNewPuzzle(force = false) {
if (!force && !this.app.clientApi.isLoggedIn()) {
const signals = this.dialogs.showWarning(
@ -286,7 +364,7 @@ export class PuzzleMenuState extends TextualGameState {
return;
}
const savegame = this.app.savegameMgr.createNewSavegame();
const savegame = this.createEmptySavegame();
this.moveToState("InGameState", {
gameModeId: enumGameModeIds.puzzleEdit,
savegame,

View File

@ -125,16 +125,20 @@ puzzleMenu:
edit: Edit
title: Puzzle Mode
createPuzzle: Create Puzzle
loadPuzzle: Load
reviewPuzzle: Review & Publish
validtingPuzzle: Validating Puzzle
validatingPuzzle: Validating Puzzle
submittingPuzzle: Submitting Puzzle
noPuzzles: There are currently no puzzles in this section.
difficultyNotDetermined: Not yet determined
categories:
levels: Levels
new: New
top-rated: Top Rated
mine: My Puzzles
short: Short
hard: Hard
validation:
title: Invalid Puzzle
@ -337,6 +341,38 @@ dialogs:
desc: >-
Since you are offline, you will not be able to save and/or publish your puzzle. Would you still like to continue?
puzzlePlayRegularRecommendation:
title: Recommendation
desc: >-
I <strong>strongly</strong> recommend playing the normal game to level 12 before attempting the puzzle DLC, otherwise you will have comprehension problems. Do you still want to continue?
puzzleShare:
title: Short Key Copied
desc: >-
The short key of the puzzle (<key>) has been copied to your clipboard! It can be entered in the puzzle menu to access the puzzle.
puzzleReport:
title: Report Puzzle
options:
profane: Profane
unsolvable: Not solvable
trolling: Trolling
puzzleReportComplete:
title: Thank you for your feedback!
desc: >-
The puzzle has been flagged.
puzzleReportError:
title: Failed to report
desc: >-
Your report could not get processed:
puzzleLoadShortKey:
title: Enter short key
desc: >-
Enter the short key of the puzzle to load it.
ingame:
# This is shown in the top left corner and displays useful keybindings in
# every situation
@ -567,6 +603,9 @@ ingame:
zoneWidth: Width
zoneHeight: Height
trimZone: Trim
clearItems: Clear Items
share: Share
report: Report
puzzleEditorControls:
title: Puzzle Creator
@ -584,7 +623,20 @@ ingame:
Please rate the puzzle:
titleRating: How difficult did you find the puzzle?
buttonSubmit: Submit
buttonSubmit: Continue
difficulties:
- No challenge
- Easy
- Medium
- Hard
- Impossible
puzzleMetadata:
author: Author
shortKey: Short Key
rating: Difficulty
averageDuration: Avg. Duration
# All shop upgrades
shopUpgrades:
@ -1303,6 +1355,26 @@ demo:
settingNotAvailable: Not available in the demo.
backendErrors:
ratelimit: You are performing your actions too frequent. Please wait a bit.
invalid-api-key: Failed to communicate with the backend, please try to update/restart the game (Invalid Api Key).
unauthorized: Failed to communicate with the backend, please try to update/restart the game (Unauthorized).
bad-token: Failed to communicate with the backend, please try to update/restart the game (Bad Token).
bad-id: Invalid puzzle identifier.
not-found: The given puzzle could not be found.
bad-category: The given category could not be found.
bad-short-key: The given short key is invalid.
profane-title: Your puzzle title contains profane words.
bad-title-too-many-spaces: Your puzzle title is too short.
bad-shape-key-in-emitter: A constant producer has an invalid item.
bad-shape-key-in-goal: A goal acceptor has an invalid item.
no-emitters: Your puzzle does not contain any constant producers.
no-goals: Your puzzle does not contain any goal acceptors.
short-key-already-taken: This short key is already taken, please use another one.
can-not-report-your-own-puzzle: You can not report your own puzzle.
bad-payload: The request contains invalid data.
bad-building-placement: Your puzzle contains invalid placed buildings.
tips:
- The hub will accept any input, not just the current shape!
- Make sure your factories are modular - it will pay out!