Puzzle mode, almost done
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 129 KiB |
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 100 KiB |
BIN
res/ui/icons/puzzle_complete_indicator_inverse.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
res/ui/puzzle_dlc_logo_inverse.png
Normal file
After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
@ -22,6 +22,8 @@
|
||||
@include S(width, 30px);
|
||||
@include S(height, 30px);
|
||||
|
||||
@include DarkThemeInvert;
|
||||
|
||||
opacity: 1;
|
||||
&:hover {
|
||||
opacity: 0.9 !important;
|
||||
|
@ -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,34 +168,33 @@
|
||||
&: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;
|
||||
}
|
||||
}
|
||||
|
||||
button.close {
|
||||
border: 0;
|
||||
position: relative;
|
||||
@include S(margin-top, 30px);
|
||||
background: $colorGreenBright;
|
||||
@include S(padding, 10px, 40px);
|
||||
|
||||
&:not(.visible) {
|
||||
opacity: 0;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -23,6 +23,8 @@
|
||||
@include S(padding-right, 25px);
|
||||
opacity: 1;
|
||||
|
||||
@include DarkThemeInvert;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9 !important;
|
||||
}
|
||||
|
@ -44,9 +44,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
> .buttons > button.trim {
|
||||
@include S(margin-top, 10px);
|
||||
@include SuperSmallText;
|
||||
> .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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
23
src/css/ingame_hud/puzzle_play_settings.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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}
|
||||
|
@ -149,29 +149,31 @@ export class Blueprint {
|
||||
*/
|
||||
tryPlace(root, tile) {
|
||||
return root.logic.performBulkOperation(() => {
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.entities.length; ++i) {
|
||||
const entity = this.entities[i];
|
||||
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
|
||||
continue;
|
||||
return root.logic.performImmutableOperation(() => {
|
||||
let count = 0;
|
||||
for (let i = 0; i < this.entities.length; ++i) {
|
||||
const entity = this.entities[i];
|
||||
if (!root.logic.checkCanPlaceEntity(entity, tile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const clone = entity.clone();
|
||||
clone.components.StaticMapEntity.origin.addInplace(tile);
|
||||
root.logic.freeEntityAreaBeforeBuild(clone);
|
||||
root.map.placeStaticEntity(clone);
|
||||
root.entityMgr.registerEntity(clone);
|
||||
count++;
|
||||
}
|
||||
|
||||
const clone = entity.clone();
|
||||
clone.components.StaticMapEntity.origin.addInplace(tile);
|
||||
root.logic.freeEntityAreaBeforeBuild(clone);
|
||||
root.map.placeStaticEntity(clone);
|
||||
root.entityMgr.registerEntity(clone);
|
||||
count++;
|
||||
}
|
||||
root.signals.bulkAchievementCheck.dispatch(
|
||||
ACHIEVEMENTS.placeBlueprint,
|
||||
count,
|
||||
ACHIEVEMENTS.placeBp1000,
|
||||
count
|
||||
);
|
||||
|
||||
root.signals.bulkAchievementCheck.dispatch(
|
||||
ACHIEVEMENTS.placeBlueprint,
|
||||
count,
|
||||
ACHIEVEMENTS.placeBp1000,
|
||||
count
|
||||
);
|
||||
|
||||
return count !== 0;
|
||||
return count !== 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -23,6 +23,11 @@ export class Component extends BasicSerializableObject {
|
||||
*/
|
||||
copyAdditionalStateTo(otherComponent) {}
|
||||
|
||||
/**
|
||||
* Clears all items and state
|
||||
*/
|
||||
clear() {}
|
||||
|
||||
/* dev:start */
|
||||
|
||||
/**
|
||||
|
@ -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>}
|
||||
|
@ -40,6 +40,10 @@ export class FilterComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.clear();
|
||||
}
|
||||
|
||||
clear() {
|
||||
/**
|
||||
* Items in queue to leave through
|
||||
* @type {Array<PendingFilterItem>}
|
||||
|
@ -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 = [];
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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 = [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -170,11 +170,6 @@ export class GameMode extends BasicSerializableObject {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getIsDeterministic() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getIsEditor() {
|
||||
return false;
|
||||
|
@ -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)
|
||||
);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -41,7 +41,10 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
|
||||
<button class="styledButton plus">+</button>
|
||||
</div>
|
||||
|
||||
<button class="styledButton trim">${T.ingame.puzzleEditorSettings.trimZone}</button>
|
||||
<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);
|
||||
}
|
||||
}
|
||||
|
||||
trim() {
|
||||
const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode);
|
||||
clear() {
|
||||
this.root.logic.clearAllBeltsAndItems();
|
||||
}
|
||||
|
||||
let w = mode.zoneWidth;
|
||||
let h = mode.zoneHeight;
|
||||
if (this.anyBuildingOutsideZone(w, h)) {
|
||||
logger.error("Trim: Zone is already too small");
|
||||
trim() {
|
||||
// Now, find the center
|
||||
const buildings = this.root.entityMgr.entities.slice();
|
||||
|
||||
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();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
36
src/js/game/hud/parts/puzzle_play_settings.js
Normal 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,10 +84,6 @@ export class PuzzleGameMode extends GameMode {
|
||||
return false;
|
||||
}
|
||||
|
||||
getIsDeterministic() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getFixedTickrate() {
|
||||
return 300;
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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()),
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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)"
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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)),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,8 @@
|
||||
* shortKey: string;
|
||||
* likes: number;
|
||||
* downloads: number;
|
||||
* difficulty: number | null;
|
||||
* averageTime: number | null;
|
||||
* title: string;
|
||||
* author: string;
|
||||
* completed: boolean;
|
||||
|
@ -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() {
|
||||
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;
|
||||
}
|
||||
|
||||
this.moveToState("LoginState", {
|
||||
nextStateId: "PuzzleMenuState",
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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!
|
||||
|