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_settings";
|
||||
@import "ingame_hud/puzzle_play_metadata";
|
||||
@import "ingame_hud/puzzle_complete_notification";
|
||||
|
||||
// prettier-ignore
|
||||
$elements:
|
||||
@ -109,6 +110,7 @@ ingame_HUD_Statistics,
|
||||
ingame_HUD_ShapeViewer,
|
||||
ingame_HUD_StandaloneAdvantages,
|
||||
ingame_HUD_UnlockNotification,
|
||||
ingame_HUD_PuzzleCompleteNotification,
|
||||
ingame_HUD_SettingsMenu,
|
||||
ingame_HUD_ModalDialogs,
|
||||
ingame_HUD_CatMemes;
|
||||
|
@ -3,6 +3,10 @@
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
|
||||
> h1 {
|
||||
justify-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
> .container {
|
||||
@ -43,10 +47,9 @@
|
||||
|
||||
> .puzzles {
|
||||
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-gap, 3px);
|
||||
@include S(grid-auto-columns, 1fr);
|
||||
@include S(margin-top, 10px);
|
||||
@include S(padding-right, 4px);
|
||||
@include S(height, 360px);
|
||||
@ -113,38 +116,59 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
> .playcount {
|
||||
grid-column: 1 / 2;
|
||||
display: none;
|
||||
grid-row: 3 / 4;
|
||||
@include SuperSmallText;
|
||||
}
|
||||
|
||||
> .upvotes {
|
||||
@include SuperSmallText;
|
||||
> .stats {
|
||||
grid-column: 2 / 3;
|
||||
grid-row: 3 / 4;
|
||||
color: #444;
|
||||
align-self: end;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-self: end;
|
||||
font-weight: bold;
|
||||
@include S(padding-right, 12px);
|
||||
opacity: 0.89;
|
||||
align-self: end;
|
||||
|
||||
& {
|
||||
/* @load-async */
|
||||
background: uiResource("icons/puzzle_upvotes.png") calc(100% - #{D(2px)}) #{D(
|
||||
3.5px
|
||||
)} / #{D(8px)} #{D(8px)} no-repeat;
|
||||
> .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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.completed {
|
||||
.icon,
|
||||
.upvotes,
|
||||
.playcount,
|
||||
.author,
|
||||
.title {
|
||||
> .icon,
|
||||
> .stats,
|
||||
> .author,
|
||||
> .title {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@ -168,12 +192,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
> .loader {
|
||||
> .loader,
|
||||
> .empty {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1 / 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $accentColorBright;
|
||||
color: $accentColorDark;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ import { SettingsState } from "./states/settings";
|
||||
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
|
||||
import { RestrictionManager } from "./core/restriction_manager";
|
||||
import { PuzzleMenuState } from "./states/puzzle_menu";
|
||||
import { ClientAPI } from "./platform/api";
|
||||
import { LoginState } from "./states/login";
|
||||
|
||||
/**
|
||||
* @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface
|
||||
@ -73,6 +75,7 @@ export class Application {
|
||||
this.savegameMgr = new SavegameManager(this);
|
||||
this.inputMgr = new InputDistributor(this);
|
||||
this.backgroundResourceLoader = new BackgroundResourcesLoader(this);
|
||||
this.clientApi = new ClientAPI(this);
|
||||
|
||||
// Restrictions (Like demo etc)
|
||||
this.restrictionMgr = new RestrictionManager(this);
|
||||
@ -161,6 +164,7 @@ export class Application {
|
||||
AboutState,
|
||||
ChangelogState,
|
||||
PuzzleMenuState,
|
||||
LoginState,
|
||||
];
|
||||
|
||||
for (let i = 0; i < states.length; ++i) {
|
||||
|
@ -79,11 +79,9 @@ export class GameHUD {
|
||||
}
|
||||
|
||||
const additionalParts = this.root.gameMode.additionalHudParts;
|
||||
console.log(additionalParts);
|
||||
for (const [partId, part] of Object.entries(additionalParts)) {
|
||||
this.parts[partId] = new part(this.root);
|
||||
}
|
||||
console.log(this.parts);
|
||||
|
||||
const frag = document.createDocumentFragment();
|
||||
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() {
|
||||
const regex = /^[a-zA-Z0-9_\- ]{1,20}$/;
|
||||
startSubmit(title = "", shortKey = "") {
|
||||
const regex = /^[a-zA-Z0-9_\- ]{4,20}$/;
|
||||
const nameInput = new FormElementInput({
|
||||
id: "nameInput",
|
||||
label: T.dialogs.submitPuzzle.descName,
|
||||
placeholder: T.dialogs.submitPuzzle.placeholderName,
|
||||
defaultValue: "",
|
||||
validator: val => val.match(regex) && trim(val).length > 0,
|
||||
defaultValue: title,
|
||||
validator: val => trim(val).match(regex) && trim(val).length > 0,
|
||||
});
|
||||
|
||||
let items = new Set();
|
||||
@ -93,7 +93,7 @@ export class HUDPuzzleEditorReview extends BaseHUDPart {
|
||||
id: "shapeKeyInput",
|
||||
label: null,
|
||||
placeholder: "CuCuCuCu",
|
||||
defaultValue: "",
|
||||
defaultValue: shortKey,
|
||||
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);
|
||||
|
||||
// @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() {
|
||||
|
@ -25,6 +25,10 @@ import { HUDConstantSignalEdit } from "../hud/parts/constant_signal_edit";
|
||||
import { PuzzleSerializer } from "../../savegame/puzzle_serializer";
|
||||
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";
|
||||
|
||||
const logger = createLogger("puzzle-play");
|
||||
|
||||
export class PuzzlePlayGameMode extends PuzzleGameMode {
|
||||
static getId() {
|
||||
@ -62,6 +66,7 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
|
||||
];
|
||||
|
||||
this.additionalHudParts.puzzlePlayMetadata = HUDPuzzlePlayMetadata;
|
||||
this.additionalHudParts.puzzleCompleteNotification = HUDPuzzleCompleteNotification;
|
||||
|
||||
root.signals.postLoadHook.add(this.loadPuzzle, this);
|
||||
|
||||
@ -70,6 +75,7 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
|
||||
|
||||
loadPuzzle() {
|
||||
let errorText;
|
||||
logger.log("Loading puzzle", this.puzzle);
|
||||
|
||||
try {
|
||||
errorText = new PuzzleSerializer().deserializePuzzle(this.root, this.puzzle.game);
|
||||
@ -81,11 +87,40 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
|
||||
}
|
||||
|
||||
if (errorText) {
|
||||
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"));
|
||||
this.root.gameState.moveToState("PuzzleMenuState", {
|
||||
error: {
|
||||
title: T.dialogs.puzzleLoadError.title,
|
||||
desc: T.dialogs.puzzleLoadError.desc + " " + errorText,
|
||||
},
|
||||
});
|
||||
// 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
|
||||
* }} LevelDefinition */
|
||||
|
||||
const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
|
||||
const finalGameShape = "RuCw--Cw:----Ru--";
|
||||
export const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw";
|
||||
export const finalGameShape = "RuCw--Cw:----Ru--";
|
||||
const preparementShape = "CpRpCp--:SwSwSwSw";
|
||||
|
||||
// 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.
|
||||
achievementCheck: /** @type {TypedSignal<[string, any]>} */ (new Signal()),
|
||||
bulkAchievementCheck: /** @type {TypedSignal<(string|any)[]>} */ (new Signal()),
|
||||
|
||||
// Puzzle mode
|
||||
puzzleComplete: /** @type {TypedSignal<[]>} */ (new Signal()),
|
||||
};
|
||||
|
||||
// RNG's
|
||||
|
@ -6,10 +6,9 @@ import { fillInLinkIntoTranslation } from "../../core/utils";
|
||||
import { T } from "../../translations";
|
||||
import { BaseItem } from "../base_item";
|
||||
import { enumColors } from "../colors";
|
||||
import { enumConstantSignalType, ConstantSignalComponent } from "../components/constant_signal";
|
||||
import { ConstantSignalComponent, enumConstantSignalType } from "../components/constant_signal";
|
||||
import { Entity } from "../entity";
|
||||
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 { COLOR_ITEM_SINGLETONS } from "../items/color_item";
|
||||
import { ShapeDefinition } from "../shape_definition";
|
||||
@ -60,13 +59,20 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
|
||||
validator: val => this.parseSignalCode(entity.components.ConstantSignal.type, val),
|
||||
});
|
||||
|
||||
const items = [
|
||||
...Object.values(COLOR_ITEM_SINGLETONS),
|
||||
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(this.root.gameMode.getBlueprintShapeKey()),
|
||||
];
|
||||
const items = [...Object.values(COLOR_ITEM_SINGLETONS)];
|
||||
|
||||
if (entity.components.ConstantSignal.type === enumConstantSignalType.wired) {
|
||||
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()) {
|
||||
|
@ -41,7 +41,7 @@ export class GoalAcceptorSystem extends GameSystemWithFilter {
|
||||
allAccepted &&
|
||||
!this.root.gameMode.getIsEditor()
|
||||
) {
|
||||
this.root.hud.parts.dialogs.showInfo("Puzzle completed", "Congrats!");
|
||||
this.root.signals.puzzleComplete.dispatch();
|
||||
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,14 +41,16 @@
|
||||
* }} SavegamesData
|
||||
*/
|
||||
|
||||
// Notice: Update backend too
|
||||
/**
|
||||
* @typedef {{
|
||||
* shortKey: string;
|
||||
* upvotes: number;
|
||||
* playcount: number;
|
||||
* title: string;
|
||||
* author: string;
|
||||
* completed: boolean;
|
||||
* id: number;
|
||||
* shortKey: string;
|
||||
* likes: number;
|
||||
* downloads: number;
|
||||
* title: string;
|
||||
* author: string;
|
||||
* completed: boolean;
|
||||
* }} PuzzleMetadata
|
||||
*/
|
||||
|
||||
|
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);
|
||||
}
|
||||
|
||||
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() {
|
||||
this.moveToState("PuzzleMenuState");
|
||||
this.moveToState("LoginState", {
|
||||
nextStateId: "PuzzleMenuState",
|
||||
});
|
||||
}
|
||||
|
||||
onBackButtonClicked() {
|
||||
|
@ -57,8 +57,6 @@ export class PreloadState extends GameState {
|
||||
this.lastHintShown = -1000;
|
||||
this.nextHintDuration = 0;
|
||||
|
||||
this.currentStatus = "booting";
|
||||
|
||||
this.startLoading();
|
||||
}
|
||||
|
||||
|
@ -1,36 +1,48 @@
|
||||
import { globalConfig } from "../core/config";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { TextualGameState } from "../core/textual_game_state";
|
||||
import { formatBigNumberFull } from "../core/utils";
|
||||
import { enumGameModeIds } from "../game/game_mode";
|
||||
import { ShapeDefinition } from "../game/shape_definition";
|
||||
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 = {
|
||||
id: 1,
|
||||
shortKey: "CuCuCuCu",
|
||||
upvotes: 10000,
|
||||
playcount: 1000,
|
||||
downloads: 0,
|
||||
likes: 0,
|
||||
title: "Level 1",
|
||||
author: "verylongsteamnamewhichbreaks",
|
||||
completed: false,
|
||||
};
|
||||
|
||||
const BUILTIN_PUZZLES = [
|
||||
{ ...SAMPLE_PUZZLE, completed: true },
|
||||
{ ...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,
|
||||
];
|
||||
/**
|
||||
* @type {import("../savegame/savegame_typedefs").PuzzleMetadata[]}
|
||||
*/
|
||||
const BUILTIN_PUZZLES = G_IS_DEV
|
||||
? [
|
||||
// { ...SAMPLE_PUZZLE, completed: true },
|
||||
// { ...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,
|
||||
]
|
||||
: [];
|
||||
|
||||
const logger = createLogger("puzzle-menu");
|
||||
|
||||
export class PuzzleMenuState extends TextualGameState {
|
||||
constructor() {
|
||||
@ -124,6 +136,7 @@ export class PuzzleMenuState extends TextualGameState {
|
||||
T.dialogs.puzzleLoadFailed.title,
|
||||
T.dialogs.puzzleLoadFailed.desc + " " + error
|
||||
);
|
||||
this.renderPuzzles([]);
|
||||
}
|
||||
)
|
||||
.then(() => (this.loading = false));
|
||||
@ -158,19 +171,19 @@ export class PuzzleMenuState extends TextualGameState {
|
||||
elem.appendChild(author);
|
||||
}
|
||||
|
||||
if (puzzle.upvotes) {
|
||||
const upvotes = document.createElement("div");
|
||||
upvotes.classList.add("upvotes");
|
||||
upvotes.innerText = formatBigNumberFull(puzzle.upvotes);
|
||||
elem.appendChild(upvotes);
|
||||
}
|
||||
const stats = document.createElement("div");
|
||||
stats.classList.add("stats");
|
||||
elem.appendChild(stats);
|
||||
|
||||
if (puzzle.playcount) {
|
||||
const playcount = document.createElement("div");
|
||||
playcount.classList.add("playcount");
|
||||
playcount.innerText = String(puzzle.playcount) + " plays";
|
||||
elem.appendChild(playcount);
|
||||
}
|
||||
const downloads = document.createElement("div");
|
||||
downloads.classList.add("downloads");
|
||||
downloads.innerText = String(puzzle.downloads);
|
||||
stats.appendChild(downloads);
|
||||
|
||||
const likes = document.createElement("div");
|
||||
likes.classList.add("likes");
|
||||
likes.innerText = formatBigNumberFull(puzzle.likes);
|
||||
stats.appendChild(likes);
|
||||
|
||||
const definition = ShapeDefinition.fromShortKey(puzzle.shortKey);
|
||||
const canvas = definition.generateAsCanvas(100 * this.app.getEffectiveUiScale());
|
||||
@ -184,10 +197,30 @@ export class PuzzleMenuState extends TextualGameState {
|
||||
|
||||
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) {
|
||||
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
|
||||
*/
|
||||
playPuzzle(puzzle) {
|
||||
/**
|
||||
* @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 },
|
||||
};
|
||||
const closeLoading = this.dialogs.showLoadingDialog();
|
||||
|
||||
const savegame = this.app.savegameMgr.createNewSavegame();
|
||||
this.moveToState("InGameState", {
|
||||
gameModeId: enumGameModeIds.puzzlePlay,
|
||||
gameModeParameters: {
|
||||
puzzle: {
|
||||
meta: puzzle,
|
||||
game: puzzleData,
|
||||
},
|
||||
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,
|
||||
});
|
||||
},
|
||||
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");
|
||||
|
||||
if (payload && payload.error) {
|
||||
this.dialogs.showWarning(payload.error.title, payload.error.desc);
|
||||
}
|
||||
|
||||
for (const category of categories) {
|
||||
const button = this.htmlElement.querySelector(`[data-category="${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) {
|
||||
// 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();
|
||||
this.moveToState("InGameState", {
|
||||
gameModeId: enumGameModeIds.puzzleEdit,
|
||||
|
@ -48,6 +48,7 @@ steamPage:
|
||||
global:
|
||||
loading: Loading
|
||||
error: Error
|
||||
loggingIn: Logging in
|
||||
|
||||
# How big numbers are rendered, e.g. "10,000"
|
||||
thousandsDivider: ","
|
||||
@ -127,12 +128,13 @@ puzzleMenu:
|
||||
reviewPuzzle: Review & Publish
|
||||
validtingPuzzle: Validating Puzzle
|
||||
submittingPuzzle: Submitting Puzzle
|
||||
noPuzzles: There are currently no puzzles in this section.
|
||||
|
||||
categories:
|
||||
levels: Levels
|
||||
new: New
|
||||
topRated: Top Rated
|
||||
myPuzzles: My Puzzles
|
||||
top-rated: Top Rated
|
||||
mine: My Puzzles
|
||||
|
||||
validation:
|
||||
title: Invalid Puzzle
|
||||
@ -158,6 +160,9 @@ dialogs:
|
||||
viewUpdate: View Update
|
||||
showUpgrades: Show Upgrades
|
||||
showKeybindings: Show Keybindings
|
||||
retry: Retry
|
||||
continue: Continue
|
||||
playOffline: Play Offline
|
||||
|
||||
importSavegameError:
|
||||
title: Import Error
|
||||
@ -307,6 +312,31 @@ dialogs:
|
||||
desc: >-
|
||||
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:
|
||||
# This is shown in the top left corner and displays useful keybindings in
|
||||
# every situation
|
||||
@ -547,6 +577,15 @@ ingame:
|
||||
- 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 :)
|
||||
|
||||
puzzleCompletion:
|
||||
title: Puzzle Completed!
|
||||
|
||||
titleLike: >-
|
||||
Please rate the puzzle:
|
||||
titleRating: How difficult did you find the puzzle?
|
||||
|
||||
buttonSubmit: Submit
|
||||
|
||||
# All shop upgrades
|
||||
shopUpgrades:
|
||||
belt:
|
||||
|
Loading…
Reference in New Issue
Block a user