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:
parent
846e66a9c8
commit
f5c1e26256
BIN
res/ui/icons/puzzle_action_liked_no.png
Normal file
BIN
res/ui/icons/puzzle_action_liked_no.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
res/ui/icons/puzzle_action_liked_yes.png
Normal file
BIN
res/ui/icons/puzzle_action_liked_yes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
BIN
res/ui/icons/puzzle_plays.png
Normal file
BIN
res/ui/icons/puzzle_plays.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.5 KiB |
192
src/css/ingame_hud/puzzle_complete_notification.scss
Normal file
192
src/css/ingame_hud/puzzle_complete_notification.scss
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
#ingame_HUD_PuzzleCompleteNotification {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow: auto;
|
||||||
|
pointer-events: all;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
& {
|
||||||
|
/* @load-async */
|
||||||
|
background: rgba(#333538, 0.98) uiResource("dialog_bg_pattern.png") top left / #{D(10px)} repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include InlineAnimation(0.1s ease-in-out) {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .dialog {
|
||||||
|
// background: rgba(#222428, 0.5);
|
||||||
|
@include S(border-radius, $globalBorderRadius);
|
||||||
|
@include S(padding, 30px);
|
||||||
|
|
||||||
|
@include InlineAnimation(0.5s ease-in-out) {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
> .title {
|
||||||
|
@include SuperHeading;
|
||||||
|
text-transform: uppercase;
|
||||||
|
@include S(font-size, 30px);
|
||||||
|
@include S(margin-bottom, 40px);
|
||||||
|
color: $colorGreenBright !important;
|
||||||
|
|
||||||
|
@include InlineAnimation(0.5s ease-in-out) {
|
||||||
|
0% {
|
||||||
|
transform: translateY(-50vh);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(5vh);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translateY(-2vh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contents {
|
||||||
|
@include S(width, 400px);
|
||||||
|
@include S(height, 170px);
|
||||||
|
@include InlineAnimation(0.5s ease-in-out) {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100vw);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateX(5vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
transform: translateX(-2vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
> .stepLike {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
@include S(margin-bottom, 10px);
|
||||||
|
|
||||||
|
> .buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
@include S(margin, 10px, 0);
|
||||||
|
|
||||||
|
> 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);
|
||||||
|
transition: opacity 0.12s ease-in-out, background-color 0.12s ease-in-out;
|
||||||
|
|
||||||
|
&.liked-yes {
|
||||||
|
/* @load-async */
|
||||||
|
background: uiResource("icons/puzzle_action_liked_yes.png") center center / 60%
|
||||||
|
no-repeat;
|
||||||
|
}
|
||||||
|
&.liked-no {
|
||||||
|
/* @load-async */
|
||||||
|
background: uiResource("icons/puzzle_action_liked_no.png") center center / 60%
|
||||||
|
no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.active) {
|
||||||
|
opacity: 0.5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #151118 !important;
|
||||||
|
}
|
||||||
|
&:not(.active) {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .stepDifficulty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
@include S(margin-bottom, 10px);
|
||||||
|
|
||||||
|
> .shapes {
|
||||||
|
@include S(margin-top, 10px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> canvas {
|
||||||
|
@include S(margin, 0, 5px);
|
||||||
|
@include S(width, 30px);
|
||||||
|
@include S(height, 30px);
|
||||||
|
@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;
|
||||||
|
&.active {
|
||||||
|
background-color: #151118 !important;
|
||||||
|
box-shadow: 0 0 0 D(2px) #151118;
|
||||||
|
}
|
||||||
|
|
||||||
|
&: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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.close {
|
||||||
|
border: 0;
|
||||||
|
position: relative;
|
||||||
|
@include S(margin-top, 30px);
|
||||||
|
|
||||||
|
&:not(.visible) {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -62,6 +62,7 @@
|
|||||||
@import "ingame_hud/puzzle_editor_controls";
|
@import "ingame_hud/puzzle_editor_controls";
|
||||||
@import "ingame_hud/puzzle_editor_settings";
|
@import "ingame_hud/puzzle_editor_settings";
|
||||||
@import "ingame_hud/puzzle_play_metadata";
|
@import "ingame_hud/puzzle_play_metadata";
|
||||||
|
@import "ingame_hud/puzzle_complete_notification";
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
$elements:
|
$elements:
|
||||||
@ -109,6 +110,7 @@ ingame_HUD_Statistics,
|
|||||||
ingame_HUD_ShapeViewer,
|
ingame_HUD_ShapeViewer,
|
||||||
ingame_HUD_StandaloneAdvantages,
|
ingame_HUD_StandaloneAdvantages,
|
||||||
ingame_HUD_UnlockNotification,
|
ingame_HUD_UnlockNotification,
|
||||||
|
ingame_HUD_PuzzleCompleteNotification,
|
||||||
ingame_HUD_SettingsMenu,
|
ingame_HUD_SettingsMenu,
|
||||||
ingame_HUD_ModalDialogs,
|
ingame_HUD_ModalDialogs,
|
||||||
ingame_HUD_CatMemes;
|
ingame_HUD_CatMemes;
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
> h1 {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .container {
|
> .container {
|
||||||
@ -43,10 +47,9 @@
|
|||||||
|
|
||||||
> .puzzles {
|
> .puzzles {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(D(150px), 1fr));
|
||||||
@include S(grid-auto-rows, 120px);
|
@include S(grid-auto-rows, 120px);
|
||||||
@include S(grid-gap, 3px);
|
@include S(grid-gap, 3px);
|
||||||
@include S(grid-auto-columns, 1fr);
|
|
||||||
@include S(margin-top, 10px);
|
@include S(margin-top, 10px);
|
||||||
@include S(padding-right, 4px);
|
@include S(padding-right, 4px);
|
||||||
@include S(height, 360px);
|
@include S(height, 360px);
|
||||||
@ -113,38 +116,59 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .playcount {
|
> .stats {
|
||||||
grid-column: 1 / 2;
|
|
||||||
display: none;
|
|
||||||
grid-row: 3 / 4;
|
|
||||||
@include SuperSmallText;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .upvotes {
|
|
||||||
@include SuperSmallText;
|
|
||||||
grid-column: 2 / 3;
|
grid-column: 2 / 3;
|
||||||
grid-row: 3 / 4;
|
grid-row: 3 / 4;
|
||||||
color: #444;
|
display: flex;
|
||||||
align-self: end;
|
align-items: center;
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
|
align-self: end;
|
||||||
|
|
||||||
|
> .downloads {
|
||||||
|
@include SuperSmallText;
|
||||||
|
color: #000;
|
||||||
|
align-self: start;
|
||||||
|
justify-self: start;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@include S(padding-right, 12px);
|
@include S(margin-right, 10px);
|
||||||
opacity: 0.89;
|
@include S(padding-left, 14px);
|
||||||
|
opacity: 0.7;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
& {
|
& {
|
||||||
/* @load-async */
|
/* @load-async */
|
||||||
background: uiResource("icons/puzzle_upvotes.png") calc(100% - #{D(2px)}) #{D(
|
background: uiResource("icons/puzzle_plays.png") #{D(2px)} center / #{D(8px)}
|
||||||
3.5px
|
#{D(8px)} no-repeat;
|
||||||
)} / #{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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.completed {
|
&.completed {
|
||||||
.icon,
|
> .icon,
|
||||||
.upvotes,
|
> .stats,
|
||||||
.playcount,
|
> .author,
|
||||||
.author,
|
> .title {
|
||||||
.title {
|
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,12 +192,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .loader {
|
> .loader,
|
||||||
|
> .empty {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
grid-row: 1 / 3;
|
grid-row: 1 / 3;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: $accentColorBright;
|
color: $accentColorDark;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,8 @@ import { SettingsState } from "./states/settings";
|
|||||||
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
|
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
|
||||||
import { RestrictionManager } from "./core/restriction_manager";
|
import { RestrictionManager } from "./core/restriction_manager";
|
||||||
import { PuzzleMenuState } from "./states/puzzle_menu";
|
import { PuzzleMenuState } from "./states/puzzle_menu";
|
||||||
|
import { ClientAPI } from "./platform/api";
|
||||||
|
import { LoginState } from "./states/login";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface
|
* @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface
|
||||||
@ -73,6 +75,7 @@ export class Application {
|
|||||||
this.savegameMgr = new SavegameManager(this);
|
this.savegameMgr = new SavegameManager(this);
|
||||||
this.inputMgr = new InputDistributor(this);
|
this.inputMgr = new InputDistributor(this);
|
||||||
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
|
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
|
||||||
|
this.clientApi = new ClientAPI(this);
|
||||||
|
|
||||||
// Restrictions (Like demo etc)
|
// Restrictions (Like demo etc)
|
||||||
this.restrictionMgr = new RestrictionManager(this);
|
this.restrictionMgr = new RestrictionManager(this);
|
||||||
@ -161,6 +164,7 @@ export class Application {
|
|||||||
AboutState,
|
AboutState,
|
||||||
ChangelogState,
|
ChangelogState,
|
||||||
PuzzleMenuState,
|
PuzzleMenuState,
|
||||||
|
LoginState,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = 0; i < states.length; ++i) {
|
for (let i = 0; i < states.length; ++i) {
|
||||||
|
@ -79,11 +79,9 @@ export class GameHUD {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const additionalParts = this.root.gameMode.additionalHudParts;
|
const additionalParts = this.root.gameMode.additionalHudParts;
|
||||||
console.log(additionalParts);
|
|
||||||
for (const [partId, part] of Object.entries(additionalParts)) {
|
for (const [partId, part] of Object.entries(additionalParts)) {
|
||||||
this.parts[partId] = new part(this.root);
|
this.parts[partId] = new part(this.root);
|
||||||
}
|
}
|
||||||
console.log(this.parts);
|
|
||||||
|
|
||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
for (const key in this.parts) {
|
for (const key in this.parts) {
|
||||||
|
136
src/js/game/hud/parts/puzzle_complete_notification.js
Normal file
136
src/js/game/hud/parts/puzzle_complete_notification.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export class HUDPuzzleCompleteNotification extends BaseHUDPart {
|
||||||
|
initialize() {
|
||||||
|
this.visible = false;
|
||||||
|
|
||||||
|
this.domAttach = new DynamicDomAttach(this.root, this.element, {
|
||||||
|
timeToKeepSeconds: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.root.signals.puzzleComplete.add(this.show, this);
|
||||||
|
|
||||||
|
this.selectionLiked = null;
|
||||||
|
this.selectionDifficulty = null;
|
||||||
|
this.timeOfCompletion = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
createElements(parent) {
|
||||||
|
this.inputReciever = new InputReceiver("puzzle-complete");
|
||||||
|
|
||||||
|
this.element = makeDiv(parent, "ingame_HUD_PuzzleCompleteNotification", ["noBlur"]);
|
||||||
|
|
||||||
|
const dialog = makeDiv(this.element, null, ["dialog"]);
|
||||||
|
|
||||||
|
this.elemTitle = makeDiv(dialog, null, ["title"], T.ingame.puzzleCompletion.title);
|
||||||
|
this.elemContents = makeDiv(dialog, null, ["contents"]);
|
||||||
|
|
||||||
|
const stepLike = makeDiv(this.elemContents, null, ["step", "stepLike"]);
|
||||||
|
makeDiv(stepLike, null, ["title"], T.ingame.puzzleCompletion.titleLike);
|
||||||
|
|
||||||
|
const buttons = makeDiv(stepLike, null, ["buttons"]);
|
||||||
|
|
||||||
|
this.buttonLikeYes = document.createElement("button");
|
||||||
|
this.buttonLikeYes.classList.add("liked-yes");
|
||||||
|
buttons.appendChild(this.buttonLikeYes);
|
||||||
|
this.trackClicks(this.buttonLikeYes, () => {
|
||||||
|
this.selectionLiked = true;
|
||||||
|
this.updateState();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.buttonLikeNo = document.createElement("button");
|
||||||
|
this.buttonLikeNo.classList.add("liked-no");
|
||||||
|
buttons.appendChild(this.buttonLikeNo);
|
||||||
|
this.trackClicks(this.buttonLikeNo, () => {
|
||||||
|
this.selectionLiked = false;
|
||||||
|
this.updateState();
|
||||||
|
});
|
||||||
|
|
||||||
|
const stepDifficulty = makeDiv(this.elemContents, null, ["step", "stepDifficulty"]);
|
||||||
|
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 = [];
|
||||||
|
let index = 0;
|
||||||
|
for (const shape of items) {
|
||||||
|
const localIndex = index;
|
||||||
|
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, () => {
|
||||||
|
this.selectionDifficulty = localIndex;
|
||||||
|
this.updateState();
|
||||||
|
});
|
||||||
|
this.difficultyCanvases.push(canvas);
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.btnClose = document.createElement("button");
|
||||||
|
this.btnClose.classList.add("close", "styledButton");
|
||||||
|
this.btnClose.innerText = T.ingame.puzzleCompletion.buttonSubmit;
|
||||||
|
dialog.appendChild(this.btnClose);
|
||||||
|
|
||||||
|
this.trackClicks(this.btnClose, this.close);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState() {
|
||||||
|
this.buttonLikeYes.classList.toggle("active", this.selectionLiked === true);
|
||||||
|
this.buttonLikeNo.classList.toggle("active", this.selectionLiked === false);
|
||||||
|
this.difficultyCanvases.forEach((canvas, index) =>
|
||||||
|
canvas.classList.toggle("active", index === this.selectionDifficulty)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.btnClose.classList.toggle(
|
||||||
|
"visible",
|
||||||
|
typeof this.selectionDifficulty === "number" && typeof this.selectionLiked === "boolean"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.root.soundProxy.playUi(SOUNDS.levelComplete);
|
||||||
|
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
|
||||||
|
this.visible = true;
|
||||||
|
this.timeOfCompletion = this.root.time.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
|
||||||
|
}
|
||||||
|
|
||||||
|
isBlockingOverlay() {
|
||||||
|
return this.visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
/** @type {PuzzlePlayGameMode} */ (this.root.gameMode)
|
||||||
|
.trackCompleted(this.selectionLiked, this.selectionDifficulty, Math.round(this.timeOfCompletion))
|
||||||
|
.then(() => {
|
||||||
|
this.root.gameState.moveToState("PuzzleMenuState");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.domAttach.update(this.visible);
|
||||||
|
}
|
||||||
|
}
|
@ -58,14 +58,14 @@ export class HUDPuzzleEditorReview extends BaseHUDPart {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
startSubmit() {
|
startSubmit(title = "", shortKey = "") {
|
||||||
const regex = /^[a-zA-Z0-9_\- ]{1,20}$/;
|
const regex = /^[a-zA-Z0-9_\- ]{4,20}$/;
|
||||||
const nameInput = new FormElementInput({
|
const nameInput = new FormElementInput({
|
||||||
id: "nameInput",
|
id: "nameInput",
|
||||||
label: T.dialogs.submitPuzzle.descName,
|
label: T.dialogs.submitPuzzle.descName,
|
||||||
placeholder: T.dialogs.submitPuzzle.placeholderName,
|
placeholder: T.dialogs.submitPuzzle.placeholderName,
|
||||||
defaultValue: "",
|
defaultValue: title,
|
||||||
validator: val => val.match(regex) && trim(val).length > 0,
|
validator: val => trim(val).match(regex) && trim(val).length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
let items = new Set();
|
let items = new Set();
|
||||||
@ -93,7 +93,7 @@ export class HUDPuzzleEditorReview extends BaseHUDPart {
|
|||||||
id: "shapeKeyInput",
|
id: "shapeKeyInput",
|
||||||
label: null,
|
label: null,
|
||||||
placeholder: "CuCuCuCu",
|
placeholder: "CuCuCuCu",
|
||||||
defaultValue: "",
|
defaultValue: shortKey,
|
||||||
validator: val => ShapeDefinition.isValidShortKey(trim(val)),
|
validator: val => ShapeDefinition.isValidShortKey(trim(val)),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -126,7 +126,32 @@ export class HUDPuzzleEditorReview extends BaseHUDPart {
|
|||||||
|
|
||||||
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(T.puzzleMenu.submittingPuzzle);
|
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(T.puzzleMenu.submittingPuzzle);
|
||||||
|
|
||||||
// @todo
|
this.root.app.clientApi
|
||||||
|
.apiSubmitPuzzle({
|
||||||
|
title,
|
||||||
|
shortKey,
|
||||||
|
data: serialized,
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
() => {
|
||||||
|
closeLoading();
|
||||||
|
const { ok } = this.root.hud.parts.dialogs.showInfo(
|
||||||
|
T.dialogs.puzzleSubmitOk.title,
|
||||||
|
T.dialogs.puzzleSubmitOk.desc
|
||||||
|
);
|
||||||
|
ok.add(() => this.root.gameState.moveToState("PuzzleMenuState"));
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
closeLoading();
|
||||||
|
logger.warn("Failed to submit puzzle:", err);
|
||||||
|
const signals = this.root.hud.parts.dialogs.showWarning(
|
||||||
|
T.dialogs.puzzleSubmitError.title,
|
||||||
|
T.dialogs.puzzleSubmitError.desc + " " + err,
|
||||||
|
["cancel", "retry:good"]
|
||||||
|
);
|
||||||
|
signals.retry.add(() => this.startSubmit(title, shortKey));
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
|
@ -25,6 +25,10 @@ import { HUDConstantSignalEdit } from "../hud/parts/constant_signal_edit";
|
|||||||
import { PuzzleSerializer } from "../../savegame/puzzle_serializer";
|
import { PuzzleSerializer } from "../../savegame/puzzle_serializer";
|
||||||
import { T } from "../../translations";
|
import { T } from "../../translations";
|
||||||
import { HUDPuzzlePlayMetadata } from "../hud/parts/puzzle_play_metadata";
|
import { HUDPuzzlePlayMetadata } from "../hud/parts/puzzle_play_metadata";
|
||||||
|
import { createLogger } from "../../core/logging";
|
||||||
|
import { HUDPuzzleCompleteNotification } from "../hud/parts/puzzle_complete_notification";
|
||||||
|
|
||||||
|
const logger = createLogger("puzzle-play");
|
||||||
|
|
||||||
export class PuzzlePlayGameMode extends PuzzleGameMode {
|
export class PuzzlePlayGameMode extends PuzzleGameMode {
|
||||||
static getId() {
|
static getId() {
|
||||||
@ -62,6 +66,7 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
|
|||||||
];
|
];
|
||||||
|
|
||||||
this.additionalHudParts.puzzlePlayMetadata = HUDPuzzlePlayMetadata;
|
this.additionalHudParts.puzzlePlayMetadata = HUDPuzzlePlayMetadata;
|
||||||
|
this.additionalHudParts.puzzleCompleteNotification = HUDPuzzleCompleteNotification;
|
||||||
|
|
||||||
root.signals.postLoadHook.add(this.loadPuzzle, this);
|
root.signals.postLoadHook.add(this.loadPuzzle, this);
|
||||||
|
|
||||||
@ -70,6 +75,7 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
|
|||||||
|
|
||||||
loadPuzzle() {
|
loadPuzzle() {
|
||||||
let errorText;
|
let errorText;
|
||||||
|
logger.log("Loading puzzle", this.puzzle);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
errorText = new PuzzleSerializer().deserializePuzzle(this.root, this.puzzle.game);
|
errorText = new PuzzleSerializer().deserializePuzzle(this.root, this.puzzle.game);
|
||||||
@ -81,11 +87,40 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (errorText) {
|
if (errorText) {
|
||||||
const signals = this.root.hud.parts.dialogs.showWarning(
|
this.root.gameState.moveToState("PuzzleMenuState", {
|
||||||
T.dialogs.puzzleLoadError.title,
|
error: {
|
||||||
T.dialogs.puzzleLoadError.desc + " " + errorText
|
title: T.dialogs.puzzleLoadError.title,
|
||||||
);
|
desc: T.dialogs.puzzleLoadError.desc + " " + errorText,
|
||||||
signals.ok.add(() => this.root.gameState.moveToState("PuzzleMenuState"));
|
},
|
||||||
|
});
|
||||||
|
// const signals = this.root.hud.parts.dialogs.showWarning(
|
||||||
|
// T.dialogs.puzzleLoadError.title,
|
||||||
|
// T.dialogs.puzzleLoadError.desc + " " + errorText
|
||||||
|
// );
|
||||||
|
// signals.ok.add(() => this.root.gameState.moveToState("PuzzleMenuState"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {boolean} liked
|
||||||
|
* @param {number} difficulty
|
||||||
|
* @param {number} time
|
||||||
|
*/
|
||||||
|
trackCompleted(liked, difficulty, time) {
|
||||||
|
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog();
|
||||||
|
|
||||||
|
return this.root.app.clientApi
|
||||||
|
.apiCompletePuzzle(this.puzzle.meta.id, {
|
||||||
|
time,
|
||||||
|
difficulty,
|
||||||
|
liked,
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
logger.warn("Failed to complete puzzle:", err);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
closeLoading();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,8 +57,8 @@ import { queryParamOptions } from "../../core/query_parameters";
|
|||||||
* throughputOnly?: boolean
|
* throughputOnly?: boolean
|
||||||
* }} LevelDefinition */
|
* }} LevelDefinition */
|
||||||
|
|
||||||
const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
|
export const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
|
||||||
const finalGameShape = "RuCw--Cw:----Ru--";
|
export const finalGameShape = "RuCw--Cw:----Ru--";
|
||||||
const preparementShape = "CpRpCp--:SwSwSwSw";
|
const preparementShape = "CpRpCp--:SwSwSwSw";
|
||||||
|
|
||||||
// Tiers need % of the previous tier as requirement too
|
// Tiers need % of the previous tier as requirement too
|
||||||
|
@ -183,6 +183,9 @@ export class GameRoot {
|
|||||||
// Called with an achievement key and necessary args to validate it can be unlocked.
|
// Called with an achievement key and necessary args to validate it can be unlocked.
|
||||||
achievementCheck: /** @type {TypedSignal<[string, any]>} */ (new Signal()),
|
achievementCheck: /** @type {TypedSignal<[string, any]>} */ (new Signal()),
|
||||||
bulkAchievementCheck: /** @type {TypedSignal<(string|any)[]>} */ (new Signal()),
|
bulkAchievementCheck: /** @type {TypedSignal<(string|any)[]>} */ (new Signal()),
|
||||||
|
|
||||||
|
// Puzzle mode
|
||||||
|
puzzleComplete: /** @type {TypedSignal<[]>} */ (new Signal()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// RNG's
|
// RNG's
|
||||||
|
@ -6,10 +6,9 @@ import { fillInLinkIntoTranslation } from "../../core/utils";
|
|||||||
import { T } from "../../translations";
|
import { T } from "../../translations";
|
||||||
import { BaseItem } from "../base_item";
|
import { BaseItem } from "../base_item";
|
||||||
import { enumColors } from "../colors";
|
import { enumColors } from "../colors";
|
||||||
import { enumConstantSignalType, ConstantSignalComponent } from "../components/constant_signal";
|
import { ConstantSignalComponent, enumConstantSignalType } from "../components/constant_signal";
|
||||||
import { Entity } from "../entity";
|
import { Entity } from "../entity";
|
||||||
import { GameSystemWithFilter } from "../game_system_with_filter";
|
import { GameSystemWithFilter } from "../game_system_with_filter";
|
||||||
import { HUDPinnedShapes } from "../hud/parts/pinned_shapes";
|
|
||||||
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item";
|
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item";
|
||||||
import { COLOR_ITEM_SINGLETONS } from "../items/color_item";
|
import { COLOR_ITEM_SINGLETONS } from "../items/color_item";
|
||||||
import { ShapeDefinition } from "../shape_definition";
|
import { ShapeDefinition } from "../shape_definition";
|
||||||
@ -60,13 +59,20 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
|
|||||||
validator: val => this.parseSignalCode(entity.components.ConstantSignal.type, val),
|
validator: val => this.parseSignalCode(entity.components.ConstantSignal.type, val),
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = [
|
const items = [...Object.values(COLOR_ITEM_SINGLETONS)];
|
||||||
...Object.values(COLOR_ITEM_SINGLETONS),
|
|
||||||
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(this.root.gameMode.getBlueprintShapeKey()),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (entity.components.ConstantSignal.type === enumConstantSignalType.wired) {
|
if (entity.components.ConstantSignal.type === enumConstantSignalType.wired) {
|
||||||
items.unshift(BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON);
|
items.unshift(BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON);
|
||||||
|
items.push(
|
||||||
|
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(
|
||||||
|
this.root.gameMode.getBlueprintShapeKey()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else if (entity.components.ConstantSignal.type === enumConstantSignalType.wireless) {
|
||||||
|
const shapes = ["CuCuCuCu", "RuRuRuRu", "WuWuWuWu", "SuSuSuSu"];
|
||||||
|
items.unshift(
|
||||||
|
...shapes.reverse().map(key => this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.root.gameMode.hasHub()) {
|
if (this.root.gameMode.hasHub()) {
|
||||||
|
@ -41,7 +41,7 @@ export class GoalAcceptorSystem extends GameSystemWithFilter {
|
|||||||
allAccepted &&
|
allAccepted &&
|
||||||
!this.root.gameMode.getIsEditor()
|
!this.root.gameMode.getIsEditor()
|
||||||
) {
|
) {
|
||||||
this.root.hud.parts.dialogs.showInfo("Puzzle completed", "Congrats!");
|
this.root.signals.puzzleComplete.dispatch();
|
||||||
this.puzzleCompleted = true;
|
this.puzzleCompleted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
163
src/js/platform/api.js
Normal file
163
src/js/platform/api.js
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
/* typehints:start */
|
||||||
|
import { Application } from "../application";
|
||||||
|
/* typehints:end */
|
||||||
|
import { createLogger } from "../core/logging";
|
||||||
|
|
||||||
|
const logger = createLogger("puzzle-api");
|
||||||
|
|
||||||
|
export class ClientAPI {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Application} app
|
||||||
|
*/
|
||||||
|
constructor(app) {
|
||||||
|
this.app = app;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current users session token
|
||||||
|
* @type {string|null}
|
||||||
|
*/
|
||||||
|
this.token = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEndpoint() {
|
||||||
|
if (G_IS_DEV) {
|
||||||
|
return "http://localhost:15001";
|
||||||
|
}
|
||||||
|
if (window.location.host === "beta.shapez.io") {
|
||||||
|
return "https://api-staging.shapez.io";
|
||||||
|
}
|
||||||
|
return "https://api.shapez.io";
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoggedIn() {
|
||||||
|
return Boolean(this.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} endpoint
|
||||||
|
* @param {object} options
|
||||||
|
* @param {"GET"|"POST"=} options.method
|
||||||
|
* @param {any=} options.body
|
||||||
|
*/
|
||||||
|
_request(endpoint, options) {
|
||||||
|
const headers = {
|
||||||
|
"x-api-key": "d5c54aaa491f200709afff082c153ef1",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.token) {
|
||||||
|
headers["x-token"] = this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.race([
|
||||||
|
fetch(this.getEndpoint() + endpoint, {
|
||||||
|
cache: "no-cache",
|
||||||
|
mode: "cors",
|
||||||
|
headers,
|
||||||
|
method: options.method || "GET",
|
||||||
|
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw "bad-status: " + res.status + " / " + res.statusText;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.then(res => res.json()),
|
||||||
|
new Promise(resolve => setTimeout(resolve, 5000)),
|
||||||
|
])
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
throw data.error;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
logger.warn("Failure:", endpoint, ":", err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tryLogin() {
|
||||||
|
return this.apiTryLogin()
|
||||||
|
.then(({ token }) => {
|
||||||
|
this.token = token;
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
logger.warn("Failed to login:", err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<{token: string}>}
|
||||||
|
*/
|
||||||
|
apiTryLogin() {
|
||||||
|
return this._request("/v1/public/login", {
|
||||||
|
method: "POST",
|
||||||
|
body: {
|
||||||
|
hello: "world",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {"new"|"top-rated"|"mine"} category
|
||||||
|
* @returns {Promise<import("../savegame/savegame_typedefs").PuzzleMetadata[]>}
|
||||||
|
*/
|
||||||
|
apiListPuzzles(category) {
|
||||||
|
if (!this.isLoggedIn()) {
|
||||||
|
return Promise.reject("not-logged-in");
|
||||||
|
}
|
||||||
|
return this._request("/v1/puzzles/list/" + category, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} puzzleId
|
||||||
|
* @returns {Promise<import("../savegame/savegame_typedefs").PuzzleFullData>}
|
||||||
|
*/
|
||||||
|
apiDownloadPuzzle(puzzleId) {
|
||||||
|
if (!this.isLoggedIn()) {
|
||||||
|
return Promise.reject("not-logged-in");
|
||||||
|
}
|
||||||
|
return this._request("/v1/puzzles/download/" + puzzleId, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} puzzleId
|
||||||
|
* @param {object} payload
|
||||||
|
* @param {number} payload.time
|
||||||
|
* @param {number} payload.difficulty
|
||||||
|
* @param {boolean} payload.liked
|
||||||
|
* @returns {Promise<{ success: true }>}
|
||||||
|
*/
|
||||||
|
apiCompletePuzzle(puzzleId, payload) {
|
||||||
|
if (!this.isLoggedIn()) {
|
||||||
|
return Promise.reject("not-logged-in");
|
||||||
|
}
|
||||||
|
return this._request("/v1/puzzles/complete/" + puzzleId, {
|
||||||
|
method: "POST",
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} payload
|
||||||
|
* @param {string} payload.title
|
||||||
|
* @param {string} payload.shortKey
|
||||||
|
* @param {import("../savegame/savegame_typedefs").PuzzleGameData} payload.data
|
||||||
|
* @returns {Promise<{ success: true }>}
|
||||||
|
*/
|
||||||
|
apiSubmitPuzzle(payload) {
|
||||||
|
if (!this.isLoggedIn()) {
|
||||||
|
return Promise.reject("not-logged-in");
|
||||||
|
}
|
||||||
|
return this._request("/v1/puzzles/submit", {
|
||||||
|
method: "POST",
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -41,11 +41,13 @@
|
|||||||
* }} SavegamesData
|
* }} SavegamesData
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Notice: Update backend too
|
||||||
/**
|
/**
|
||||||
* @typedef {{
|
* @typedef {{
|
||||||
|
* id: number;
|
||||||
* shortKey: string;
|
* shortKey: string;
|
||||||
* upvotes: number;
|
* likes: number;
|
||||||
* playcount: number;
|
* downloads: number;
|
||||||
* title: string;
|
* title: string;
|
||||||
* author: string;
|
* author: string;
|
||||||
* completed: boolean;
|
* completed: boolean;
|
||||||
|
102
src/js/states/login.js
Normal file
102
src/js/states/login.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { GameState } from "../core/game_state";
|
||||||
|
import { getRandomHint } from "../game/hints";
|
||||||
|
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
||||||
|
import { T } from "../translations";
|
||||||
|
|
||||||
|
export class LoginState extends GameState {
|
||||||
|
constructor() {
|
||||||
|
super("LoginState");
|
||||||
|
}
|
||||||
|
|
||||||
|
getInnerHTML() {
|
||||||
|
return `
|
||||||
|
<div class="loadingImage"></div>
|
||||||
|
<div class="loadingStatus">
|
||||||
|
<span class="desc">${T.global.loggingIn}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="prefab_GameHint"></span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} payload
|
||||||
|
* @param {string} payload.nextStateId
|
||||||
|
*/
|
||||||
|
onEnter(payload) {
|
||||||
|
this.payload = payload;
|
||||||
|
if (!this.payload.nextStateId) {
|
||||||
|
throw new Error("No next state id");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.app.clientApi.isLoggedIn()) {
|
||||||
|
this.finishLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialogs = new HUDModalDialogs(null, this.app);
|
||||||
|
const dialogsElement = document.body.querySelector(".modalDialogParent");
|
||||||
|
this.dialogs.initializeToElement(dialogsElement);
|
||||||
|
|
||||||
|
this.htmlElement.classList.add("prefab_LoadingState");
|
||||||
|
|
||||||
|
/** @type {HTMLElement} */
|
||||||
|
this.hintsText = this.htmlElement.querySelector(".prefab_GameHint");
|
||||||
|
this.lastHintShown = -1000;
|
||||||
|
this.nextHintDuration = 0;
|
||||||
|
|
||||||
|
this.tryLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
tryLogin() {
|
||||||
|
this.app.clientApi.tryLogin().then(success => {
|
||||||
|
console.log("Logged in:", success);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
const signals = this.dialogs.showWarning(
|
||||||
|
T.dialogs.offlineMode.title,
|
||||||
|
T.dialogs.offlineMode.desc,
|
||||||
|
["retry", "playOffline:bad"]
|
||||||
|
);
|
||||||
|
signals.retry.add(() => setTimeout(() => this.tryLogin(), 2000), this);
|
||||||
|
signals.playOffline.add(this.finishLoading, this);
|
||||||
|
} else {
|
||||||
|
this.finishLoading();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
finishLoading() {
|
||||||
|
this.moveToState(this.payload.nextStateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultPreviousState() {
|
||||||
|
return "MainMenuState";
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
const now = performance.now();
|
||||||
|
if (now - this.lastHintShown > this.nextHintDuration) {
|
||||||
|
this.lastHintShown = now;
|
||||||
|
const hintText = getRandomHint();
|
||||||
|
|
||||||
|
this.hintsText.innerHTML = hintText;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute how long the user will need to read the hint.
|
||||||
|
* We calculate with 130 words per minute, with an average of 5 chars
|
||||||
|
* that is 650 characters / minute
|
||||||
|
*/
|
||||||
|
this.nextHintDuration = Math.max(2500, (hintText.length / 650) * 60 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onRender() {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
onBackgroundTick() {
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
}
|
@ -323,53 +323,10 @@ export class MainMenuState extends GameState {
|
|||||||
this.trackClicks(puzzleModeButton, this.onPuzzleModeButtonClicked);
|
this.trackClicks(puzzleModeButton, this.onPuzzleModeButtonClicked);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPuzzleModeMenu() {
|
|
||||||
const savegames = this.htmlElement.querySelector(".mainContainer .savegames");
|
|
||||||
|
|
||||||
if (savegames) {
|
|
||||||
savegames.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttonContainer = this.htmlElement.querySelector(".mainContainer .buttons");
|
|
||||||
removeAllChildren(buttonContainer);
|
|
||||||
|
|
||||||
const playButtonElement = makeButtonElement(["playModeButton", "styledButton"], T.puzzleMenu.play);
|
|
||||||
const editButtonElement = makeButtonElement(["editModeButton", "styledButton"], T.puzzleMenu.edit);
|
|
||||||
|
|
||||||
buttonContainer.appendChild(playButtonElement);
|
|
||||||
this.trackClicks(playButtonElement, this.onPuzzlePlayButtonClicked);
|
|
||||||
buttonContainer.appendChild(editButtonElement);
|
|
||||||
this.trackClicks(editButtonElement, this.onPuzzleEditButtonClicked);
|
|
||||||
|
|
||||||
const bottomButtonContainer = this.htmlElement.querySelector(".bottomContainer .buttons");
|
|
||||||
removeAllChildren(bottomButtonContainer);
|
|
||||||
|
|
||||||
const backButton = makeButton(bottomButtonContainer, ["styledButton"], T.mainMenu.back);
|
|
||||||
|
|
||||||
bottomButtonContainer.appendChild(backButton);
|
|
||||||
this.trackClicks(backButton, this.onBackButtonClicked);
|
|
||||||
}
|
|
||||||
|
|
||||||
onPuzzlePlayButtonClicked() {
|
|
||||||
const savegame = this.app.savegameMgr.createNewSavegame();
|
|
||||||
|
|
||||||
this.moveToState("InGameState", {
|
|
||||||
gameModeId: enumGameModeIds.puzzlePlay,
|
|
||||||
savegame,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPuzzleEditButtonClicked() {
|
|
||||||
const savegame = this.app.savegameMgr.createNewSavegame();
|
|
||||||
|
|
||||||
this.moveToState("InGameState", {
|
|
||||||
gameModeId: enumGameModeIds.puzzleEdit,
|
|
||||||
savegame,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPuzzleModeButtonClicked() {
|
onPuzzleModeButtonClicked() {
|
||||||
this.moveToState("PuzzleMenuState");
|
this.moveToState("LoginState", {
|
||||||
|
nextStateId: "PuzzleMenuState",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onBackButtonClicked() {
|
onBackButtonClicked() {
|
||||||
|
@ -57,8 +57,6 @@ export class PreloadState extends GameState {
|
|||||||
this.lastHintShown = -1000;
|
this.lastHintShown = -1000;
|
||||||
this.nextHintDuration = 0;
|
this.nextHintDuration = 0;
|
||||||
|
|
||||||
this.currentStatus = "booting";
|
|
||||||
|
|
||||||
this.startLoading();
|
this.startLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,36 +1,48 @@
|
|||||||
import { globalConfig } from "../core/config";
|
import { globalConfig } from "../core/config";
|
||||||
|
import { createLogger } from "../core/logging";
|
||||||
import { TextualGameState } from "../core/textual_game_state";
|
import { TextualGameState } from "../core/textual_game_state";
|
||||||
import { formatBigNumberFull } from "../core/utils";
|
import { formatBigNumberFull } from "../core/utils";
|
||||||
import { enumGameModeIds } from "../game/game_mode";
|
import { enumGameModeIds } from "../game/game_mode";
|
||||||
import { ShapeDefinition } from "../game/shape_definition";
|
import { ShapeDefinition } from "../game/shape_definition";
|
||||||
import { T } from "../translations";
|
import { T } from "../translations";
|
||||||
|
|
||||||
const categories = ["levels", "new", "topRated", "myPuzzles"];
|
const categories = ["levels", "new", "top-rated", "mine"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import("../savegame/savegame_typedefs").PuzzleMetadata}
|
||||||
|
*/
|
||||||
const SAMPLE_PUZZLE = {
|
const SAMPLE_PUZZLE = {
|
||||||
|
id: 1,
|
||||||
shortKey: "CuCuCuCu",
|
shortKey: "CuCuCuCu",
|
||||||
upvotes: 10000,
|
downloads: 0,
|
||||||
playcount: 1000,
|
likes: 0,
|
||||||
title: "Level 1",
|
title: "Level 1",
|
||||||
author: "verylongsteamnamewhichbreaks",
|
author: "verylongsteamnamewhichbreaks",
|
||||||
completed: false,
|
completed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const BUILTIN_PUZZLES = [
|
/**
|
||||||
{ ...SAMPLE_PUZZLE, completed: true },
|
* @type {import("../savegame/savegame_typedefs").PuzzleMetadata[]}
|
||||||
{ ...SAMPLE_PUZZLE, completed: true },
|
*/
|
||||||
SAMPLE_PUZZLE,
|
const BUILTIN_PUZZLES = G_IS_DEV
|
||||||
SAMPLE_PUZZLE,
|
? [
|
||||||
SAMPLE_PUZZLE,
|
// { ...SAMPLE_PUZZLE, completed: true },
|
||||||
SAMPLE_PUZZLE,
|
// { ...SAMPLE_PUZZLE, completed: true },
|
||||||
SAMPLE_PUZZLE,
|
// SAMPLE_PUZZLE,
|
||||||
SAMPLE_PUZZLE,
|
// SAMPLE_PUZZLE,
|
||||||
SAMPLE_PUZZLE,
|
// SAMPLE_PUZZLE,
|
||||||
SAMPLE_PUZZLE,
|
// SAMPLE_PUZZLE,
|
||||||
SAMPLE_PUZZLE,
|
// SAMPLE_PUZZLE,
|
||||||
SAMPLE_PUZZLE,
|
// SAMPLE_PUZZLE,
|
||||||
SAMPLE_PUZZLE,
|
// SAMPLE_PUZZLE,
|
||||||
];
|
// SAMPLE_PUZZLE,
|
||||||
|
// SAMPLE_PUZZLE,
|
||||||
|
// SAMPLE_PUZZLE,
|
||||||
|
// SAMPLE_PUZZLE,
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const logger = createLogger("puzzle-menu");
|
||||||
|
|
||||||
export class PuzzleMenuState extends TextualGameState {
|
export class PuzzleMenuState extends TextualGameState {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -124,6 +136,7 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
T.dialogs.puzzleLoadFailed.title,
|
T.dialogs.puzzleLoadFailed.title,
|
||||||
T.dialogs.puzzleLoadFailed.desc + " " + error
|
T.dialogs.puzzleLoadFailed.desc + " " + error
|
||||||
);
|
);
|
||||||
|
this.renderPuzzles([]);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(() => (this.loading = false));
|
.then(() => (this.loading = false));
|
||||||
@ -158,19 +171,19 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
elem.appendChild(author);
|
elem.appendChild(author);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (puzzle.upvotes) {
|
const stats = document.createElement("div");
|
||||||
const upvotes = document.createElement("div");
|
stats.classList.add("stats");
|
||||||
upvotes.classList.add("upvotes");
|
elem.appendChild(stats);
|
||||||
upvotes.innerText = formatBigNumberFull(puzzle.upvotes);
|
|
||||||
elem.appendChild(upvotes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (puzzle.playcount) {
|
const downloads = document.createElement("div");
|
||||||
const playcount = document.createElement("div");
|
downloads.classList.add("downloads");
|
||||||
playcount.classList.add("playcount");
|
downloads.innerText = String(puzzle.downloads);
|
||||||
playcount.innerText = String(puzzle.playcount) + " plays";
|
stats.appendChild(downloads);
|
||||||
elem.appendChild(playcount);
|
|
||||||
}
|
const likes = document.createElement("div");
|
||||||
|
likes.classList.add("likes");
|
||||||
|
likes.innerText = formatBigNumberFull(puzzle.likes);
|
||||||
|
stats.appendChild(likes);
|
||||||
|
|
||||||
const definition = ShapeDefinition.fromShortKey(puzzle.shortKey);
|
const definition = ShapeDefinition.fromShortKey(puzzle.shortKey);
|
||||||
const canvas = definition.generateAsCanvas(100 * this.app.getEffectiveUiScale());
|
const canvas = definition.generateAsCanvas(100 * this.app.getEffectiveUiScale());
|
||||||
@ -184,10 +197,30 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
|
|
||||||
this.trackClicks(elem, () => this.playPuzzle(puzzle));
|
this.trackClicks(elem, () => this.playPuzzle(puzzle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (puzzles.length === 0) {
|
||||||
|
const elem = document.createElement("div");
|
||||||
|
elem.classList.add("empty");
|
||||||
|
elem.innerText = T.puzzleMenu.noPuzzles;
|
||||||
|
container.appendChild(elem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} category
|
||||||
|
* @returns {Promise<import("../savegame/savegame_typedefs").PuzzleMetadata[]}
|
||||||
|
*/
|
||||||
getPuzzlesForCategory(category) {
|
getPuzzlesForCategory(category) {
|
||||||
return new Promise(resolve => setTimeout(() => resolve(BUILTIN_PUZZLES), 100));
|
if (category === "levels") {
|
||||||
|
return Promise.resolve(BUILTIN_PUZZLES);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = this.app.clientApi.apiListPuzzles(category);
|
||||||
|
return result.catch(err => {
|
||||||
|
logger.error("Failed to get", category, ":", err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -195,53 +228,46 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
* @param {import("../savegame/savegame_typedefs").PuzzleMetadata} puzzle
|
* @param {import("../savegame/savegame_typedefs").PuzzleMetadata} puzzle
|
||||||
*/
|
*/
|
||||||
playPuzzle(puzzle) {
|
playPuzzle(puzzle) {
|
||||||
/**
|
const closeLoading = this.dialogs.showLoadingDialog();
|
||||||
* @type {import("../savegame/savegame_typedefs").PuzzleGameData}
|
|
||||||
*/
|
|
||||||
const puzzleData = {
|
|
||||||
version: 1,
|
|
||||||
buildings: [
|
|
||||||
{
|
|
||||||
type: "emitter",
|
|
||||||
item: "CuCuCuCu",
|
|
||||||
pos: { x: -2, y: 2, r: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "emitter",
|
|
||||||
item: "red",
|
|
||||||
pos: { x: 1, y: 2, r: 0 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "goal",
|
|
||||||
item: "CrCrCrCr",
|
|
||||||
pos: { x: 0, y: -3, r: 0 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
bounds: { w: 4, h: 6 },
|
|
||||||
};
|
|
||||||
|
|
||||||
|
this.app.clientApi.apiDownloadPuzzle(puzzle.id).then(
|
||||||
|
puzzleData => {
|
||||||
|
closeLoading();
|
||||||
|
|
||||||
|
logger.log("Got puzzle:", puzzleData);
|
||||||
const savegame = this.app.savegameMgr.createNewSavegame();
|
const savegame = this.app.savegameMgr.createNewSavegame();
|
||||||
this.moveToState("InGameState", {
|
this.moveToState("InGameState", {
|
||||||
gameModeId: enumGameModeIds.puzzlePlay,
|
gameModeId: enumGameModeIds.puzzlePlay,
|
||||||
gameModeParameters: {
|
gameModeParameters: {
|
||||||
puzzle: {
|
puzzle: puzzleData,
|
||||||
meta: puzzle,
|
|
||||||
game: puzzleData,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
savegame,
|
savegame,
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
closeLoading();
|
||||||
|
logger.error("Failed to download puzzle", puzzle.id, ":", err);
|
||||||
|
this.dialogs.showWarning(
|
||||||
|
T.dialogs.puzzleDownloadError.title,
|
||||||
|
T.dialogs.puzzleDownloadError.desc + " " + err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnter() {
|
onEnter(payload) {
|
||||||
this.selectCategory("levels");
|
this.selectCategory("levels");
|
||||||
|
|
||||||
|
if (payload && payload.error) {
|
||||||
|
this.dialogs.showWarning(payload.error.title, payload.error.desc);
|
||||||
|
}
|
||||||
|
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
const button = this.htmlElement.querySelector(`[data-category="${category}"]`);
|
const button = this.htmlElement.querySelector(`[data-category="${category}"]`);
|
||||||
this.trackClicks(button, () => this.selectCategory(category));
|
this.trackClicks(button, () => this.selectCategory(category));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.trackClicks(this.htmlElement.querySelector("button.createPuzzle"), this.createNewPuzzle);
|
this.trackClicks(this.htmlElement.querySelector("button.createPuzzle"), () => this.createNewPuzzle());
|
||||||
|
|
||||||
if (G_IS_DEV && globalConfig.debug.testPuzzleMode) {
|
if (G_IS_DEV && globalConfig.debug.testPuzzleMode) {
|
||||||
// this.createNewPuzzle();
|
// this.createNewPuzzle();
|
||||||
@ -249,7 +275,17 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createNewPuzzle() {
|
createNewPuzzle(force = false) {
|
||||||
|
if (!force && !this.app.clientApi.isLoggedIn()) {
|
||||||
|
const signals = this.dialogs.showWarning(
|
||||||
|
T.dialogs.puzzleCreateOffline.title,
|
||||||
|
T.dialogs.puzzleCreateOffline.desc,
|
||||||
|
["cancel:good", "continue:bad"]
|
||||||
|
);
|
||||||
|
signals.continue.add(() => this.createNewPuzzle(true));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const savegame = this.app.savegameMgr.createNewSavegame();
|
const savegame = this.app.savegameMgr.createNewSavegame();
|
||||||
this.moveToState("InGameState", {
|
this.moveToState("InGameState", {
|
||||||
gameModeId: enumGameModeIds.puzzleEdit,
|
gameModeId: enumGameModeIds.puzzleEdit,
|
||||||
|
@ -48,6 +48,7 @@ steamPage:
|
|||||||
global:
|
global:
|
||||||
loading: Loading
|
loading: Loading
|
||||||
error: Error
|
error: Error
|
||||||
|
loggingIn: Logging in
|
||||||
|
|
||||||
# How big numbers are rendered, e.g. "10,000"
|
# How big numbers are rendered, e.g. "10,000"
|
||||||
thousandsDivider: ","
|
thousandsDivider: ","
|
||||||
@ -127,12 +128,13 @@ puzzleMenu:
|
|||||||
reviewPuzzle: Review & Publish
|
reviewPuzzle: Review & Publish
|
||||||
validtingPuzzle: Validating Puzzle
|
validtingPuzzle: Validating Puzzle
|
||||||
submittingPuzzle: Submitting Puzzle
|
submittingPuzzle: Submitting Puzzle
|
||||||
|
noPuzzles: There are currently no puzzles in this section.
|
||||||
|
|
||||||
categories:
|
categories:
|
||||||
levels: Levels
|
levels: Levels
|
||||||
new: New
|
new: New
|
||||||
topRated: Top Rated
|
top-rated: Top Rated
|
||||||
myPuzzles: My Puzzles
|
mine: My Puzzles
|
||||||
|
|
||||||
validation:
|
validation:
|
||||||
title: Invalid Puzzle
|
title: Invalid Puzzle
|
||||||
@ -158,6 +160,9 @@ dialogs:
|
|||||||
viewUpdate: View Update
|
viewUpdate: View Update
|
||||||
showUpgrades: Show Upgrades
|
showUpgrades: Show Upgrades
|
||||||
showKeybindings: Show Keybindings
|
showKeybindings: Show Keybindings
|
||||||
|
retry: Retry
|
||||||
|
continue: Continue
|
||||||
|
playOffline: Play Offline
|
||||||
|
|
||||||
importSavegameError:
|
importSavegameError:
|
||||||
title: Import Error
|
title: Import Error
|
||||||
@ -307,6 +312,31 @@ dialogs:
|
|||||||
desc: >-
|
desc: >-
|
||||||
The puzzle failed to load:
|
The puzzle failed to load:
|
||||||
|
|
||||||
|
offlineMode:
|
||||||
|
title: Offline Mode
|
||||||
|
desc: >-
|
||||||
|
We couldn't reach the backend servers, so the game has to run in offline mode. Please make sure you have an active internect connection.
|
||||||
|
|
||||||
|
puzzleDownloadError:
|
||||||
|
title: Download Error
|
||||||
|
desc: >-
|
||||||
|
Failed to download the puzzle:
|
||||||
|
|
||||||
|
puzzleSubmitError:
|
||||||
|
title: Submission Error
|
||||||
|
desc: >-
|
||||||
|
Failed to submit your puzzle:
|
||||||
|
|
||||||
|
puzzleSubmitOk:
|
||||||
|
title: Puzzle Published
|
||||||
|
desc: >-
|
||||||
|
Congratulations! Your puzzle has been published and can now be played by others. You can now find it in the "My puzzles" section.
|
||||||
|
|
||||||
|
puzzleCreateOffline:
|
||||||
|
title: Offline Mode
|
||||||
|
desc: >-
|
||||||
|
Since you are offline, you will not be able to save and/or publish your puzzle. Would you still like to continue?
|
||||||
|
|
||||||
ingame:
|
ingame:
|
||||||
# This is shown in the top left corner and displays useful keybindings in
|
# This is shown in the top left corner and displays useful keybindings in
|
||||||
# every situation
|
# every situation
|
||||||
@ -547,6 +577,15 @@ ingame:
|
|||||||
- 4. Once you click review, your puzzle will be validated and you can publish it.
|
- 4. Once you click review, your puzzle will be validated and you can publish it.
|
||||||
- 5. Upon release, <strong>all buildings will be removed</strong> except for the Producers and Goal Acceptors - That's the part that the player is supposed to figure out for themselves, after all :)
|
- 5. Upon release, <strong>all buildings will be removed</strong> except for the Producers and Goal Acceptors - That's the part that the player is supposed to figure out for themselves, after all :)
|
||||||
|
|
||||||
|
puzzleCompletion:
|
||||||
|
title: Puzzle Completed!
|
||||||
|
|
||||||
|
titleLike: >-
|
||||||
|
Please rate the puzzle:
|
||||||
|
titleRating: How difficult did you find the puzzle?
|
||||||
|
|
||||||
|
buttonSubmit: Submit
|
||||||
|
|
||||||
# All shop upgrades
|
# All shop upgrades
|
||||||
shopUpgrades:
|
shopUpgrades:
|
||||||
belt:
|
belt:
|
||||||
|
Loading…
Reference in New Issue
Block a user