mirror of
https://github.com/tobspr/shapez.io.git
synced 2024-10-27 20:34:29 +00:00
DLC: Next Puzzle button & Search functionality, other stuff
This commit is contained in:
parent
06d5c6a6dc
commit
8b5cd160b6
@ -137,16 +137,20 @@
|
|||||||
|
|
||||||
button.continue {
|
button.continue {
|
||||||
background: #555;
|
background: #555;
|
||||||
@include S(margin-right, 10px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button.menu {
|
button.menu {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.nextPuzzle {
|
||||||
background-color: $colorGreenBright;
|
background-color: $colorGreenBright;
|
||||||
}
|
}
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
@include S(min-width, 100px);
|
@include S(min-width, 100px);
|
||||||
@include S(padding, 10px, 20px);
|
@include S(padding, 8px, 16px);
|
||||||
|
@include S(margin, 0, 6px);
|
||||||
@include IncreasedClickArea(0px);
|
@include IncreasedClickArea(0px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
41
src/css/ingame_hud/puzzle_next.scss
Normal file
41
src/css/ingame_hud/puzzle_next.scss
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#ingame_HUD_PuzzleNextPuzzle {
|
||||||
|
position: absolute;
|
||||||
|
@include S(top, 17px);
|
||||||
|
@include S(right, 10px);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
backdrop-filter: blur(D(1px));
|
||||||
|
padding: D(3px);
|
||||||
|
|
||||||
|
> .button {
|
||||||
|
@include ButtonText;
|
||||||
|
@include IncreasedClickArea(0px);
|
||||||
|
pointer-events: all;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
color: #333438;
|
||||||
|
transition: all 0.12s ease-in-out;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition-property: opacity, transform;
|
||||||
|
@include PlainText;
|
||||||
|
@include S(padding-right, 25px);
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
@include DarkThemeInvert;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pressed {
|
||||||
|
transform: scale(0.95) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
& {
|
||||||
|
/* @load-async */
|
||||||
|
background: uiResource("icons/state_next_button.png") right center / D(15px) no-repeat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -65,6 +65,7 @@
|
|||||||
@import "ingame_hud/puzzle_play_settings";
|
@import "ingame_hud/puzzle_play_settings";
|
||||||
@import "ingame_hud/puzzle_play_metadata";
|
@import "ingame_hud/puzzle_play_metadata";
|
||||||
@import "ingame_hud/puzzle_complete_notification";
|
@import "ingame_hud/puzzle_complete_notification";
|
||||||
|
@import "ingame_hud/puzzle_next";
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
$elements:
|
$elements:
|
||||||
@ -83,6 +84,7 @@ ingame_HUD_PinnedShapes,
|
|||||||
ingame_HUD_GameMenu,
|
ingame_HUD_GameMenu,
|
||||||
ingame_HUD_KeybindingOverlay,
|
ingame_HUD_KeybindingOverlay,
|
||||||
ingame_HUD_PuzzleBackToMenu,
|
ingame_HUD_PuzzleBackToMenu,
|
||||||
|
ingame_HUD_PuzzleNextPuzzle,
|
||||||
ingame_HUD_PuzzleEditorReview,
|
ingame_HUD_PuzzleEditorReview,
|
||||||
ingame_HUD_PuzzleEditorControls,
|
ingame_HUD_PuzzleEditorControls,
|
||||||
ingame_HUD_PuzzleEditorTitle,
|
ingame_HUD_PuzzleEditorTitle,
|
||||||
@ -134,6 +136,7 @@ body.uiHidden {
|
|||||||
#ingame_HUD_GameMenu,
|
#ingame_HUD_GameMenu,
|
||||||
#ingame_HUD_PinnedShapes,
|
#ingame_HUD_PinnedShapes,
|
||||||
#ingame_HUD_PuzzleBackToMenu,
|
#ingame_HUD_PuzzleBackToMenu,
|
||||||
|
#ingame_HUD_PuzzleNextPuzzle,
|
||||||
#ingame_HUD_PuzzleEditorReview,
|
#ingame_HUD_PuzzleEditorReview,
|
||||||
#ingame_HUD_Notifications,
|
#ingame_HUD_Notifications,
|
||||||
#ingame_HUD_TutorialHints,
|
#ingame_HUD_TutorialHints,
|
||||||
|
@ -15,8 +15,73 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .container {
|
> .container {
|
||||||
|
.searchForm {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
color: #333;
|
||||||
|
background: $accentColorBright;
|
||||||
|
@include S(padding, 5px);
|
||||||
|
@include S(border-radius, $globalBorderRadius);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
input.search {
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
display: inline-block;
|
||||||
|
flex-grow: 1;
|
||||||
|
@include S(padding, 5px, 10px);
|
||||||
|
@include S(min-width, 50px);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
color: #333;
|
||||||
|
border: 0;
|
||||||
|
@include S(padding, 5px);
|
||||||
|
@include S(border-radius, $globalBorderRadius);
|
||||||
|
@include S(padding, 7px, 10px);
|
||||||
|
@include S(margin-left, 5px);
|
||||||
|
@include PlainText;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterCompleted {
|
||||||
|
@include S(margin-left, 20px);
|
||||||
|
pointer-events: all;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
@include PlainText;
|
||||||
|
@include S(margin-right, 10px);
|
||||||
|
|
||||||
|
input {
|
||||||
|
@include S(width, 15px);
|
||||||
|
@include S(height, 15px);
|
||||||
|
@include S(margin-right, 5px);
|
||||||
|
@include S(border-radius, $globalBorderRadius);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button[type="submit"] {
|
||||||
|
@include S(padding, 7px, 10px, 5px);
|
||||||
|
@include S(margin-left, 20px);
|
||||||
|
@include S(margin-top, 4px);
|
||||||
|
@include S(margin-bottom, 4px);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
> .mainContent {
|
> .mainContent {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
> .categoryChooser {
|
> .categoryChooser {
|
||||||
> .categories {
|
> .categories {
|
||||||
@ -79,8 +144,8 @@
|
|||||||
@include S(grid-gap, 7px);
|
@include S(grid-gap, 7px);
|
||||||
@include S(margin-top, 10px);
|
@include S(margin-top, 10px);
|
||||||
@include S(padding-right, 4px);
|
@include S(padding-right, 4px);
|
||||||
@include S(height, 320px);
|
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
flex-grow: 1;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -50,7 +50,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
button.categoryButton,
|
button.categoryButton,
|
||||||
button.about {
|
button.about,
|
||||||
|
button.privacy {
|
||||||
background-color: $colorCategoryButton;
|
background-color: $colorCategoryButton;
|
||||||
color: #777a7f;
|
color: #777a7f;
|
||||||
|
|
||||||
@ -68,6 +69,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.privacy {
|
||||||
|
@include S(margin-top, 4px);
|
||||||
|
}
|
||||||
|
|
||||||
.versionbar {
|
.versionbar {
|
||||||
@include S(margin-top, 10px);
|
@include S(margin-top, 10px);
|
||||||
|
|
||||||
@ -180,7 +185,8 @@
|
|||||||
.container .content {
|
.container .content {
|
||||||
.sidebar {
|
.sidebar {
|
||||||
button.categoryButton,
|
button.categoryButton,
|
||||||
button.about {
|
button.about,
|
||||||
|
button.privacy {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
background-color: darken($darkModeControlsBackground, 5);
|
background-color: darken($darkModeControlsBackground, 5);
|
||||||
|
|
||||||
|
@ -17,6 +17,8 @@ export const THIRDPARTY_URLS = {
|
|||||||
reddit: "https://www.reddit.com/r/shapezio",
|
reddit: "https://www.reddit.com/r/shapezio",
|
||||||
shapeViewer: "https://viewer.shapez.io",
|
shapeViewer: "https://viewer.shapez.io",
|
||||||
|
|
||||||
|
privacyPolicy: "https://tobspr.io/privacy.html",
|
||||||
|
|
||||||
standaloneStorePage: "https://store.steampowered.com/app/1318690/shapezio/",
|
standaloneStorePage: "https://store.steampowered.com/app/1318690/shapezio/",
|
||||||
stanaloneCampaignLink: "https://get.shapez.io",
|
stanaloneCampaignLink: "https://get.shapez.io",
|
||||||
puzzleDlcStorePage: "https://store.steampowered.com/app/1625400/shapezio__Puzzle_DLC",
|
puzzleDlcStorePage: "https://store.steampowered.com/app/1625400/shapezio__Puzzle_DLC",
|
||||||
|
25
src/js/game/hud/parts/HUDPuzzleNextPuzzle.js
Normal file
25
src/js/game/hud/parts/HUDPuzzleNextPuzzle.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { makeDiv } from "../../../core/utils";
|
||||||
|
import { T } from "../../../translations";
|
||||||
|
import { PuzzlePlayGameMode } from "../../modes/puzzle_play";
|
||||||
|
import { BaseHUDPart } from "../base_hud_part";
|
||||||
|
|
||||||
|
export class HUDPuzzleNextPuzzle extends BaseHUDPart {
|
||||||
|
createElements(parent) {
|
||||||
|
this.element = makeDiv(parent, "ingame_HUD_PuzzleNextPuzzle");
|
||||||
|
this.button = document.createElement("button");
|
||||||
|
this.button.classList.add("button");
|
||||||
|
this.button.innerText = T.ingame.puzzleCompletion.nextPuzzle;
|
||||||
|
this.element.appendChild(this.button);
|
||||||
|
|
||||||
|
this.trackClicks(this.button, this.nextPuzzle);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {}
|
||||||
|
|
||||||
|
nextPuzzle() {
|
||||||
|
const gameMode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
|
||||||
|
this.root.gameState.moveToState("PuzzleMenuState", {
|
||||||
|
continueQueue: gameMode.nextPuzzles,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -6,13 +6,8 @@ import { InputReceiver } from "../../../core/input_receiver";
|
|||||||
import { makeDiv } from "../../../core/utils";
|
import { makeDiv } from "../../../core/utils";
|
||||||
import { SOUNDS } from "../../../platform/sound";
|
import { SOUNDS } from "../../../platform/sound";
|
||||||
import { T } from "../../../translations";
|
import { T } from "../../../translations";
|
||||||
import { enumColors } from "../../colors";
|
|
||||||
import { ColorItem } from "../../items/color_item";
|
|
||||||
import { finalGameShape, rocketShape } from "../../modes/regular";
|
|
||||||
import { BaseHUDPart } from "../base_hud_part";
|
import { BaseHUDPart } from "../base_hud_part";
|
||||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||||
import { ShapeItem } from "../../items/shape_item";
|
|
||||||
import { ShapeDefinition } from "../../shape_definition";
|
|
||||||
|
|
||||||
export class HUDPuzzleCompleteNotification extends BaseHUDPart {
|
export class HUDPuzzleCompleteNotification extends BaseHUDPart {
|
||||||
initialize() {
|
initialize() {
|
||||||
@ -68,10 +63,21 @@ export class HUDPuzzleCompleteNotification extends BaseHUDPart {
|
|||||||
this.menuBtn.classList.add("menu", "styledButton");
|
this.menuBtn.classList.add("menu", "styledButton");
|
||||||
this.menuBtn.innerText = T.ingame.puzzleCompletion.menuBtn;
|
this.menuBtn.innerText = T.ingame.puzzleCompletion.menuBtn;
|
||||||
buttonBar.appendChild(this.menuBtn);
|
buttonBar.appendChild(this.menuBtn);
|
||||||
|
|
||||||
this.trackClicks(this.menuBtn, () => {
|
this.trackClicks(this.menuBtn, () => {
|
||||||
this.close(true);
|
this.close(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const gameMode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
|
||||||
|
if (gameMode.nextPuzzles.length > 0) {
|
||||||
|
this.nextPuzzleBtn = document.createElement("button");
|
||||||
|
this.nextPuzzleBtn.classList.add("nextPuzzle", "styledButton");
|
||||||
|
this.nextPuzzleBtn.innerText = T.ingame.puzzleCompletion.nextPuzzle;
|
||||||
|
buttonBar.appendChild(this.nextPuzzleBtn);
|
||||||
|
|
||||||
|
this.trackClicks(this.nextPuzzleBtn, () => {
|
||||||
|
this.nextPuzzle();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState() {
|
updateState() {
|
||||||
@ -93,6 +99,15 @@ export class HUDPuzzleCompleteNotification extends BaseHUDPart {
|
|||||||
return this.visible;
|
return this.visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextPuzzle() {
|
||||||
|
const gameMode = /** @type {PuzzlePlayGameMode} */ (this.root.gameMode);
|
||||||
|
gameMode.trackCompleted(this.userDidLikePuzzle, Math.round(this.timeOfCompletion)).then(() => {
|
||||||
|
this.root.gameState.moveToState("PuzzleMenuState", {
|
||||||
|
continueQueue: gameMode.nextPuzzles,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
close(toMenu) {
|
close(toMenu) {
|
||||||
/** @type {PuzzlePlayGameMode} */ (this.root.gameMode)
|
/** @type {PuzzlePlayGameMode} */ (this.root.gameMode)
|
||||||
.trackCompleted(this.userDidLikePuzzle, Math.round(this.timeOfCompletion))
|
.trackCompleted(this.userDidLikePuzzle, Math.round(this.timeOfCompletion))
|
||||||
|
@ -30,6 +30,7 @@ import { HUDPuzzlePlaySettings } from "../hud/parts/puzzle_play_settings";
|
|||||||
import { MetaBlockBuilding } from "../buildings/block";
|
import { MetaBlockBuilding } from "../buildings/block";
|
||||||
import { MetaBuilding } from "../meta_building";
|
import { MetaBuilding } from "../meta_building";
|
||||||
import { gMetaBuildingRegistry } from "../../core/global_registries";
|
import { gMetaBuildingRegistry } from "../../core/global_registries";
|
||||||
|
import { HUDPuzzleNextPuzzle } from "../hud/parts/HUDPuzzleNextPuzzle";
|
||||||
|
|
||||||
const logger = createLogger("puzzle-play");
|
const logger = createLogger("puzzle-play");
|
||||||
const copy = require("clipboard-copy");
|
const copy = require("clipboard-copy");
|
||||||
@ -43,8 +44,9 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
|
|||||||
* @param {GameRoot} root
|
* @param {GameRoot} root
|
||||||
* @param {object} payload
|
* @param {object} payload
|
||||||
* @param {import("../../savegame/savegame_typedefs").PuzzleFullData} payload.puzzle
|
* @param {import("../../savegame/savegame_typedefs").PuzzleFullData} payload.puzzle
|
||||||
|
* @param {Array<number> | undefined} payload.nextPuzzles
|
||||||
*/
|
*/
|
||||||
constructor(root, { puzzle }) {
|
constructor(root, { puzzle, nextPuzzles }) {
|
||||||
super(root);
|
super(root);
|
||||||
|
|
||||||
/** @type {Array<typeof MetaBuilding>} */
|
/** @type {Array<typeof MetaBuilding>} */
|
||||||
@ -95,6 +97,15 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
|
|||||||
root.signals.postLoadHook.add(this.loadPuzzle, this);
|
root.signals.postLoadHook.add(this.loadPuzzle, this);
|
||||||
|
|
||||||
this.puzzle = puzzle;
|
this.puzzle = puzzle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<number>}
|
||||||
|
*/
|
||||||
|
this.nextPuzzles = nextPuzzles || [];
|
||||||
|
|
||||||
|
if (this.nextPuzzles.length > 0) {
|
||||||
|
this.additionalHudParts.puzzleNext = HUDPuzzleNextPuzzle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPuzzle() {
|
loadPuzzle() {
|
||||||
|
@ -143,6 +143,20 @@ export class ClientAPI {
|
|||||||
return this._request("/v1/puzzles/list/" + category, {});
|
return this._request("/v1/puzzles/list/" + category, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ searchTerm: string; difficulty: string; duration: string }} searchOptions
|
||||||
|
* @returns {Promise<import("../savegame/savegame_typedefs").PuzzleMetadata[]>}
|
||||||
|
*/
|
||||||
|
apiSearchPuzzles(searchOptions) {
|
||||||
|
if (!this.isLoggedIn()) {
|
||||||
|
return Promise.reject("not-logged-in");
|
||||||
|
}
|
||||||
|
return this._request("/v1/puzzles/search", {
|
||||||
|
method: "POST",
|
||||||
|
body: searchOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} puzzleId
|
* @param {number} puzzleId
|
||||||
* @returns {Promise<import("../savegame/savegame_typedefs").PuzzleFullData>}
|
* @returns {Promise<import("../savegame/savegame_typedefs").PuzzleFullData>}
|
||||||
@ -169,7 +183,7 @@ export class ClientAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} shortKey
|
* @param {string} shortKey
|
||||||
* @returns {Promise<import("../savegame/savegame_typedefs").PuzzleFullData>}
|
* @returns {Promise<import("../savegame/savegame_typedefs").PuzzleFullData>}
|
||||||
*/
|
*/
|
||||||
apiDownloadPuzzleByKey(shortKey) {
|
apiDownloadPuzzleByKey(shortKey) {
|
||||||
|
@ -64,6 +64,24 @@ export class Savegame extends ReadWriteProxy {
|
|||||||
return savegameInterfaces[Savegame.getCurrentVersion()];
|
return savegameInterfaces[Savegame.getCurrentVersion()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Application} app
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
static createPuzzleSavegame(app) {
|
||||||
|
return new Savegame(app, {
|
||||||
|
internalId: "puzzle",
|
||||||
|
metaDataRef: {
|
||||||
|
internalId: "puzzle",
|
||||||
|
lastUpdate: 0,
|
||||||
|
version: 0,
|
||||||
|
level: 0,
|
||||||
|
name: "puzzle",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
|
@ -13,17 +13,30 @@ const navigation = {
|
|||||||
categories: ["official", "top-rated", "trending", "trending-weekly", "new"],
|
categories: ["official", "top-rated", "trending", "trending-weekly", "new"],
|
||||||
difficulties: ["easy", "medium", "hard"],
|
difficulties: ["easy", "medium", "hard"],
|
||||||
account: ["mine", "completed"],
|
account: ["mine", "completed"],
|
||||||
|
search: ["search"],
|
||||||
};
|
};
|
||||||
|
|
||||||
const logger = createLogger("puzzle-menu");
|
const logger = createLogger("puzzle-menu");
|
||||||
|
|
||||||
let lastCategory = "official";
|
let lastCategory = "official";
|
||||||
|
|
||||||
|
let lastSearchOptions = {
|
||||||
|
searchTerm: "",
|
||||||
|
difficulty: "any",
|
||||||
|
duration: "any",
|
||||||
|
includeCompleted: false,
|
||||||
|
};
|
||||||
|
|
||||||
export class PuzzleMenuState extends TextualGameState {
|
export class PuzzleMenuState extends TextualGameState {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("PuzzleMenuState");
|
super("PuzzleMenuState");
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.activeCategory = "";
|
this.activeCategory = "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<import("../savegame/savegame_typedefs").PuzzleMetadata>}
|
||||||
|
*/
|
||||||
|
this.puzzles = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getThemeMusic() {
|
getThemeMusic() {
|
||||||
@ -99,13 +112,23 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
activeCategory.classList.remove("active");
|
activeCategory.classList.remove("active");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.htmlElement.querySelector(`[data-category="${category}"]`).classList.add("active");
|
const categoryElement = this.htmlElement.querySelector(`[data-category="${category}"]`);
|
||||||
|
if (categoryElement) {
|
||||||
|
categoryElement.classList.add("active");
|
||||||
|
}
|
||||||
|
|
||||||
const container = this.htmlElement.querySelector("#mainContainer");
|
const container = this.htmlElement.querySelector("#mainContainer");
|
||||||
while (container.firstChild) {
|
while (container.firstChild) {
|
||||||
container.removeChild(container.firstChild);
|
container.removeChild(container.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (category === "search") {
|
||||||
|
this.loading = false;
|
||||||
|
|
||||||
|
this.startSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loadingElement = document.createElement("div");
|
const loadingElement = document.createElement("div");
|
||||||
loadingElement.classList.add("loader");
|
loadingElement.classList.add("loader");
|
||||||
loadingElement.innerText = T.global.loading + "...";
|
loadingElement.innerText = T.global.loading + "...";
|
||||||
@ -160,6 +183,7 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const children = navigation[rootCategory];
|
const children = navigation[rootCategory];
|
||||||
|
if (children.length > 1) {
|
||||||
for (const category of children) {
|
for (const category of children) {
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.setAttribute("data-category", category);
|
button.setAttribute("data-category", category);
|
||||||
@ -168,15 +192,139 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
this.trackClicks(button, () => this.selectCategory(category));
|
this.trackClicks(button, () => this.selectCategory(category));
|
||||||
subContainer.appendChild(button);
|
subContainer.appendChild(button);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootCategory === "search") {
|
||||||
|
this.renderSearchForm(subContainer);
|
||||||
|
}
|
||||||
|
|
||||||
this.selectCategory(subCategory);
|
this.selectCategory(subCategory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderSearchForm(parent) {
|
||||||
|
const container = document.createElement("form");
|
||||||
|
container.classList.add("searchForm");
|
||||||
|
|
||||||
|
// Search
|
||||||
|
const searchField = document.createElement("input");
|
||||||
|
searchField.value = lastSearchOptions.searchTerm;
|
||||||
|
searchField.classList.add("search");
|
||||||
|
searchField.setAttribute("type", "text");
|
||||||
|
searchField.setAttribute("placeholder", T.puzzleMenu.search.placeholder);
|
||||||
|
searchField.addEventListener("input", () => {
|
||||||
|
lastSearchOptions.searchTerm = searchField.value.trim();
|
||||||
|
});
|
||||||
|
container.appendChild(searchField);
|
||||||
|
|
||||||
|
// Difficulty
|
||||||
|
const difficultyFilter = document.createElement("select");
|
||||||
|
for (const difficulty of ["any", "easy", "medium", "hard"]) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = difficulty;
|
||||||
|
option.innerText = T.puzzleMenu.search.difficulties[difficulty];
|
||||||
|
if (option.value === lastSearchOptions.difficulty) {
|
||||||
|
option.setAttribute("selected", "selected");
|
||||||
|
}
|
||||||
|
difficultyFilter.appendChild(option);
|
||||||
|
}
|
||||||
|
difficultyFilter.addEventListener("change", () => {
|
||||||
|
const option = difficultyFilter.value;
|
||||||
|
lastSearchOptions.difficulty = option;
|
||||||
|
});
|
||||||
|
container.appendChild(difficultyFilter);
|
||||||
|
|
||||||
|
// Duration
|
||||||
|
const durationFilter = document.createElement("select");
|
||||||
|
for (const duration of ["any", "short", "medium", "long"]) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = duration;
|
||||||
|
option.innerText = T.puzzleMenu.search.durations[duration];
|
||||||
|
if (option.value === lastSearchOptions.duration) {
|
||||||
|
option.setAttribute("selected", "selected");
|
||||||
|
}
|
||||||
|
durationFilter.appendChild(option);
|
||||||
|
}
|
||||||
|
durationFilter.addEventListener("change", () => {
|
||||||
|
const option = durationFilter.value;
|
||||||
|
lastSearchOptions.duration = option;
|
||||||
|
});
|
||||||
|
container.appendChild(durationFilter);
|
||||||
|
|
||||||
|
// Include completed
|
||||||
|
const labelCompleted = document.createElement("label");
|
||||||
|
labelCompleted.classList.add("filterCompleted");
|
||||||
|
|
||||||
|
const inputCompleted = document.createElement("input");
|
||||||
|
inputCompleted.setAttribute("type", "checkbox");
|
||||||
|
if (lastSearchOptions.includeCompleted) {
|
||||||
|
inputCompleted.setAttribute("checked", "checked");
|
||||||
|
}
|
||||||
|
inputCompleted.addEventListener("change", () => {
|
||||||
|
lastSearchOptions.includeCompleted = inputCompleted.checked;
|
||||||
|
});
|
||||||
|
|
||||||
|
labelCompleted.appendChild(inputCompleted);
|
||||||
|
|
||||||
|
const text = document.createTextNode(T.puzzleMenu.search.includeCompleted);
|
||||||
|
labelCompleted.appendChild(text);
|
||||||
|
|
||||||
|
container.appendChild(labelCompleted);
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
const submitButton = document.createElement("button");
|
||||||
|
submitButton.classList.add("styledButton");
|
||||||
|
submitButton.setAttribute("type", "submit");
|
||||||
|
submitButton.innerText = T.puzzleMenu.search.action;
|
||||||
|
container.appendChild(submitButton);
|
||||||
|
|
||||||
|
container.addEventListener("submit", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
console.log("Search:", searchField.value.trim());
|
||||||
|
this.startSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
parent.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
startSearch() {
|
||||||
|
if (this.loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
const container = this.htmlElement.querySelector("#mainContainer");
|
||||||
|
while (container.firstChild) {
|
||||||
|
container.removeChild(container.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadingElement = document.createElement("div");
|
||||||
|
loadingElement.classList.add("loader");
|
||||||
|
loadingElement.innerText = T.global.loading + "...";
|
||||||
|
container.appendChild(loadingElement);
|
||||||
|
|
||||||
|
this.asyncChannel
|
||||||
|
.watch(this.app.clientApi.apiSearchPuzzles(lastSearchOptions))
|
||||||
|
.then(
|
||||||
|
puzzles => this.renderPuzzles(puzzles),
|
||||||
|
error => {
|
||||||
|
this.dialogs.showWarning(
|
||||||
|
T.dialogs.puzzleLoadFailed.title,
|
||||||
|
T.dialogs.puzzleLoadFailed.desc + " " + error
|
||||||
|
);
|
||||||
|
this.renderPuzzles([]);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => (this.loading = false));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {import("../savegame/savegame_typedefs").PuzzleMetadata[]} puzzles
|
* @param {import("../savegame/savegame_typedefs").PuzzleMetadata[]} puzzles
|
||||||
*/
|
*/
|
||||||
renderPuzzles(puzzles) {
|
renderPuzzles(puzzles) {
|
||||||
|
this.puzzles = puzzles;
|
||||||
|
|
||||||
const container = this.htmlElement.querySelector("#mainContainer");
|
const container = this.htmlElement.querySelector("#mainContainer");
|
||||||
while (container.firstChild) {
|
while (container.firstChild) {
|
||||||
container.removeChild(container.firstChild);
|
container.removeChild(container.firstChild);
|
||||||
@ -223,15 +371,15 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
difficulty.innerText = completionPercentage + "%";
|
difficulty.innerText = completionPercentage + "%";
|
||||||
stats.appendChild(difficulty);
|
stats.appendChild(difficulty);
|
||||||
|
|
||||||
if (completionPercentage < 40) {
|
if (puzzle.difficulty < 0.2) {
|
||||||
difficulty.classList.add("stage--hard");
|
|
||||||
difficulty.innerText = T.puzzleMenu.difficulties.hard;
|
|
||||||
} else if (completionPercentage < 80) {
|
|
||||||
difficulty.classList.add("stage--medium");
|
|
||||||
difficulty.innerText = T.puzzleMenu.difficulties.medium;
|
|
||||||
} else {
|
|
||||||
difficulty.classList.add("stage--easy");
|
difficulty.classList.add("stage--easy");
|
||||||
difficulty.innerText = T.puzzleMenu.difficulties.easy;
|
difficulty.innerText = T.puzzleMenu.difficulties.easy;
|
||||||
|
} else if (puzzle.difficulty > 0.6) {
|
||||||
|
difficulty.classList.add("stage--hard");
|
||||||
|
difficulty.innerText = T.puzzleMenu.difficulties.hard;
|
||||||
|
} else {
|
||||||
|
difficulty.classList.add("stage--medium");
|
||||||
|
difficulty.innerText = T.puzzleMenu.difficulties.medium;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,7 +423,7 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
|
|
||||||
container.appendChild(elem);
|
container.appendChild(elem);
|
||||||
|
|
||||||
this.trackClicks(elem, () => this.playPuzzle(puzzle));
|
this.trackClicks(elem, () => this.playPuzzle(puzzle.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (puzzles.length === 0) {
|
if (puzzles.length === 0) {
|
||||||
@ -328,20 +476,26 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {import("../savegame/savegame_typedefs").PuzzleMetadata} puzzle
|
* @param {number} puzzleId
|
||||||
|
* @param {Array<number>=} nextPuzzles
|
||||||
*/
|
*/
|
||||||
playPuzzle(puzzle) {
|
playPuzzle(puzzleId, nextPuzzles) {
|
||||||
const closeLoading = this.dialogs.showLoadingDialog();
|
const closeLoading = this.dialogs.showLoadingDialog();
|
||||||
|
|
||||||
this.app.clientApi.apiDownloadPuzzle(puzzle.id).then(
|
this.asyncChannel.watch(this.app.clientApi.apiDownloadPuzzle(puzzleId)).then(
|
||||||
puzzleData => {
|
puzzleData => {
|
||||||
closeLoading();
|
closeLoading();
|
||||||
logger.log("Got puzzle:", puzzleData);
|
|
||||||
this.startLoadedPuzzle(puzzleData);
|
nextPuzzles =
|
||||||
|
nextPuzzles || this.puzzles.filter(puzzle => !puzzle.completed).map(puzzle => puzzle.id);
|
||||||
|
nextPuzzles = nextPuzzles.filter(id => id !== puzzleId);
|
||||||
|
|
||||||
|
logger.log("Got puzzle:", puzzleData, "next puzzles:", nextPuzzles);
|
||||||
|
this.startLoadedPuzzle(puzzleData, nextPuzzles);
|
||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
closeLoading();
|
closeLoading();
|
||||||
logger.error("Failed to download puzzle", puzzle.id, ":", err);
|
logger.error("Failed to download puzzle", puzzleId, ":", err);
|
||||||
this.dialogs.showWarning(
|
this.dialogs.showWarning(
|
||||||
T.dialogs.puzzleDownloadError.title,
|
T.dialogs.puzzleDownloadError.title,
|
||||||
T.dialogs.puzzleDownloadError.desc + " " + err
|
T.dialogs.puzzleDownloadError.desc + " " + err
|
||||||
@ -354,18 +508,24 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
*
|
*
|
||||||
* @param {import("../savegame/savegame_typedefs").PuzzleFullData} puzzle
|
* @param {import("../savegame/savegame_typedefs").PuzzleFullData} puzzle
|
||||||
*/
|
*/
|
||||||
startLoadedPuzzle(puzzle) {
|
startLoadedPuzzle(puzzle, nextPuzzles) {
|
||||||
const savegame = this.createEmptySavegame();
|
const savegame = Savegame.createPuzzleSavegame(this.app);
|
||||||
this.moveToState("InGameState", {
|
this.moveToState("InGameState", {
|
||||||
gameModeId: enumGameModeIds.puzzlePlay,
|
gameModeId: enumGameModeIds.puzzlePlay,
|
||||||
gameModeParameters: {
|
gameModeParameters: {
|
||||||
puzzle,
|
puzzle,
|
||||||
|
nextPuzzles,
|
||||||
},
|
},
|
||||||
savegame,
|
savegame,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnter(payload) {
|
onEnter(payload) {
|
||||||
|
if (payload.continueQueue) {
|
||||||
|
logger.log("Continuing puzzle queue:", payload);
|
||||||
|
this.playPuzzle(payload.continueQueue[0], payload.continueQueue.slice(1));
|
||||||
|
}
|
||||||
|
|
||||||
// Find old category
|
// Find old category
|
||||||
let rootCategory = "categories";
|
let rootCategory = "categories";
|
||||||
for (const [id, children] of Object.entries(navigation)) {
|
for (const [id, children] of Object.entries(navigation)) {
|
||||||
@ -390,26 +550,13 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
this.trackClicks(this.htmlElement.querySelector("button.loadPuzzle"), () => this.loadPuzzle());
|
this.trackClicks(this.htmlElement.querySelector("button.loadPuzzle"), () => this.loadPuzzle());
|
||||||
}
|
}
|
||||||
|
|
||||||
createEmptySavegame() {
|
|
||||||
return new Savegame(this.app, {
|
|
||||||
internalId: "puzzle",
|
|
||||||
metaDataRef: {
|
|
||||||
internalId: "puzzle",
|
|
||||||
lastUpdate: 0,
|
|
||||||
version: 0,
|
|
||||||
level: 0,
|
|
||||||
name: "puzzle",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadPuzzle() {
|
loadPuzzle() {
|
||||||
const shortKeyInput = new FormElementInput({
|
const shortKeyInput = new FormElementInput({
|
||||||
id: "shortKey",
|
id: "shortKey",
|
||||||
label: null,
|
label: null,
|
||||||
placeholder: "",
|
placeholder: "",
|
||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
validator: val => ShapeDefinition.isValidShortKey(val),
|
validator: val => ShapeDefinition.isValidShortKey(val) || val.startsWith("/"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const dialog = new DialogWithForm({
|
const dialog = new DialogWithForm({
|
||||||
@ -422,9 +569,16 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
this.dialogs.internalShowDialog(dialog);
|
this.dialogs.internalShowDialog(dialog);
|
||||||
|
|
||||||
dialog.buttonSignals.ok.add(() => {
|
dialog.buttonSignals.ok.add(() => {
|
||||||
|
const searchTerm = shortKeyInput.getValue();
|
||||||
|
|
||||||
|
if (searchTerm === "/apikey") {
|
||||||
|
alert("Your api key is: " + this.app.clientApi.token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const closeLoading = this.dialogs.showLoadingDialog();
|
const closeLoading = this.dialogs.showLoadingDialog();
|
||||||
|
|
||||||
this.app.clientApi.apiDownloadPuzzleByKey(shortKeyInput.getValue()).then(
|
this.app.clientApi.apiDownloadPuzzleByKey(searchTerm).then(
|
||||||
puzzle => {
|
puzzle => {
|
||||||
closeLoading();
|
closeLoading();
|
||||||
this.startLoadedPuzzle(puzzle);
|
this.startLoadedPuzzle(puzzle);
|
||||||
@ -451,7 +605,7 @@ export class PuzzleMenuState extends TextualGameState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const savegame = this.createEmptySavegame();
|
const savegame = Savegame.createPuzzleSavegame(this.app);
|
||||||
this.moveToState("InGameState", {
|
this.moveToState("InGameState", {
|
||||||
gameModeId: enumGameModeIds.puzzleEdit,
|
gameModeId: enumGameModeIds.puzzleEdit,
|
||||||
savegame,
|
savegame,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { THIRDPARTY_URLS } from "../core/config";
|
||||||
import { TextualGameState } from "../core/textual_game_state";
|
import { TextualGameState } from "../core/textual_game_state";
|
||||||
import { formatSecondsToTimeAgo } from "../core/utils";
|
import { formatSecondsToTimeAgo } from "../core/utils";
|
||||||
import { allApplicationSettings, enumCategories } from "../profile/application_settings";
|
import { allApplicationSettings, enumCategories } from "../profile/application_settings";
|
||||||
@ -34,6 +35,8 @@ export class SettingsState extends TextualGameState {
|
|||||||
? ""
|
? ""
|
||||||
: `
|
: `
|
||||||
<button class="styledButton about">${T.about.title}</button>
|
<button class="styledButton about">${T.about.title}</button>
|
||||||
|
<button class="styledButton privacy">Privacy Policy</button>
|
||||||
|
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
<div class="versionbar">
|
<div class="versionbar">
|
||||||
@ -109,6 +112,9 @@ export class SettingsState extends TextualGameState {
|
|||||||
this.trackClicks(this.htmlElement.querySelector(".about"), this.onAboutClicked, {
|
this.trackClicks(this.htmlElement.querySelector(".about"), this.onAboutClicked, {
|
||||||
preventDefault: false,
|
preventDefault: false,
|
||||||
});
|
});
|
||||||
|
this.trackClicks(this.htmlElement.querySelector(".privacy"), this.onPrivacyClicked, {
|
||||||
|
preventDefault: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const keybindingsButton = this.htmlElement.querySelector(".editKeybindings");
|
const keybindingsButton = this.htmlElement.querySelector(".editKeybindings");
|
||||||
@ -180,6 +186,10 @@ export class SettingsState extends TextualGameState {
|
|||||||
this.moveToStateAddGoBack("AboutState");
|
this.moveToStateAddGoBack("AboutState");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPrivacyClicked() {
|
||||||
|
this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.privacyPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
onKeybindingsClicked() {
|
onKeybindingsClicked() {
|
||||||
this.moveToStateAddGoBack("KeybindingsState");
|
this.moveToStateAddGoBack("KeybindingsState");
|
||||||
}
|
}
|
||||||
|
@ -155,6 +155,23 @@ puzzleMenu:
|
|||||||
account: My Puzzles
|
account: My Puzzles
|
||||||
search: Search
|
search: Search
|
||||||
|
|
||||||
|
search:
|
||||||
|
action: Search
|
||||||
|
placeholder: Enter a puzzle or author name
|
||||||
|
includeCompleted: Include Completed
|
||||||
|
|
||||||
|
difficulties:
|
||||||
|
any: Any Difficulty
|
||||||
|
easy: Easy
|
||||||
|
medium: Medium
|
||||||
|
hard: Hard
|
||||||
|
|
||||||
|
durations:
|
||||||
|
any: Any Duration
|
||||||
|
short: Short (< 2 min)
|
||||||
|
medium: Normal
|
||||||
|
long: Long (> 10 min)
|
||||||
|
|
||||||
difficulties:
|
difficulties:
|
||||||
easy: Easy
|
easy: Easy
|
||||||
medium: Medium
|
medium: Medium
|
||||||
@ -657,6 +674,7 @@ ingame:
|
|||||||
|
|
||||||
continueBtn: Keep Playing
|
continueBtn: Keep Playing
|
||||||
menuBtn: Menu
|
menuBtn: Menu
|
||||||
|
nextPuzzle: Next Puzzle
|
||||||
|
|
||||||
puzzleMetadata:
|
puzzleMetadata:
|
||||||
author: Author
|
author: Author
|
||||||
|
Loading…
Reference in New Issue
Block a user