DLC: Next Puzzle button & Search functionality, other stuff

pull/1286/head
tobspr 3 years ago
parent 06d5c6a6dc
commit 8b5cd160b6

@ -137,16 +137,20 @@
button.continue {
background: #555;
@include S(margin-right, 10px);
}
button.menu {
background: #555;
}
button.nextPuzzle {
background-color: $colorGreenBright;
}
> button {
@include S(min-width, 100px);
@include S(padding, 10px, 20px);
@include S(padding, 8px, 16px);
@include S(margin, 0, 6px);
@include IncreasedClickArea(0px);
}
}

@ -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_metadata";
@import "ingame_hud/puzzle_complete_notification";
@import "ingame_hud/puzzle_next";
// prettier-ignore
$elements:
@ -83,6 +84,7 @@ ingame_HUD_PinnedShapes,
ingame_HUD_GameMenu,
ingame_HUD_KeybindingOverlay,
ingame_HUD_PuzzleBackToMenu,
ingame_HUD_PuzzleNextPuzzle,
ingame_HUD_PuzzleEditorReview,
ingame_HUD_PuzzleEditorControls,
ingame_HUD_PuzzleEditorTitle,
@ -134,6 +136,7 @@ body.uiHidden {
#ingame_HUD_GameMenu,
#ingame_HUD_PinnedShapes,
#ingame_HUD_PuzzleBackToMenu,
#ingame_HUD_PuzzleNextPuzzle,
#ingame_HUD_PuzzleEditorReview,
#ingame_HUD_Notifications,
#ingame_HUD_TutorialHints,

@ -15,8 +15,73 @@
}
> .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 {
overflow: hidden;
display: flex;
flex-direction: column;
> .categoryChooser {
> .categories {
@ -79,8 +144,8 @@
@include S(grid-gap, 7px);
@include S(margin-top, 10px);
@include S(padding-right, 4px);
@include S(height, 320px);
overflow-y: scroll;
flex-grow: 1;
pointer-events: all;
position: relative;

@ -50,7 +50,8 @@
}
button.categoryButton,
button.about {
button.about,
button.privacy {
background-color: $colorCategoryButton;
color: #777a7f;
@ -68,6 +69,10 @@
}
}
button.privacy {
@include S(margin-top, 4px);
}
.versionbar {
@include S(margin-top, 10px);
@ -180,7 +185,8 @@
.container .content {
.sidebar {
button.categoryButton,
button.about {
button.about,
button.privacy {
color: #ccc;
background-color: darken($darkModeControlsBackground, 5);

@ -17,6 +17,8 @@ export const THIRDPARTY_URLS = {
reddit: "https://www.reddit.com/r/shapezio",
shapeViewer: "https://viewer.shapez.io",
privacyPolicy: "https://tobspr.io/privacy.html",
standaloneStorePage: "https://store.steampowered.com/app/1318690/shapezio/",
stanaloneCampaignLink: "https://get.shapez.io",
puzzleDlcStorePage: "https://store.steampowered.com/app/1625400/shapezio__Puzzle_DLC",

@ -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 { SOUNDS } from "../../../platform/sound";
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 { DynamicDomAttach } from "../dynamic_dom_attach";
import { ShapeItem } from "../../items/shape_item";
import { ShapeDefinition } from "../../shape_definition";
export class HUDPuzzleCompleteNotification extends BaseHUDPart {
initialize() {
@ -68,10 +63,21 @@ export class HUDPuzzleCompleteNotification extends BaseHUDPart {
this.menuBtn.classList.add("menu", "styledButton");
this.menuBtn.innerText = T.ingame.puzzleCompletion.menuBtn;
buttonBar.appendChild(this.menuBtn);
this.trackClicks(this.menuBtn, () => {
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() {
@ -93,6 +99,15 @@ export class HUDPuzzleCompleteNotification extends BaseHUDPart {
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) {
/** @type {PuzzlePlayGameMode} */ (this.root.gameMode)
.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 { MetaBuilding } from "../meta_building";
import { gMetaBuildingRegistry } from "../../core/global_registries";
import { HUDPuzzleNextPuzzle } from "../hud/parts/HUDPuzzleNextPuzzle";
const logger = createLogger("puzzle-play");
const copy = require("clipboard-copy");
@ -43,8 +44,9 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
* @param {GameRoot} root
* @param {object} payload
* @param {import("../../savegame/savegame_typedefs").PuzzleFullData} payload.puzzle
* @param {Array<number> | undefined} payload.nextPuzzles
*/
constructor(root, { puzzle }) {
constructor(root, { puzzle, nextPuzzles }) {
super(root);
/** @type {Array<typeof MetaBuilding>} */
@ -95,6 +97,15 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
root.signals.postLoadHook.add(this.loadPuzzle, this);
this.puzzle = puzzle;
/**
* @type {Array<number>}
*/
this.nextPuzzles = nextPuzzles || [];
if (this.nextPuzzles.length > 0) {
this.additionalHudParts.puzzleNext = HUDPuzzleNextPuzzle;
}
}
loadPuzzle() {

@ -143,6 +143,20 @@ export class ClientAPI {
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
* @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>}
*/
apiDownloadPuzzleByKey(shortKey) {

@ -64,6 +64,24 @@ export class Savegame extends ReadWriteProxy {
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}
*/

@ -13,17 +13,30 @@ const navigation = {
categories: ["official", "top-rated", "trending", "trending-weekly", "new"],
difficulties: ["easy", "medium", "hard"],
account: ["mine", "completed"],
search: ["search"],
};
const logger = createLogger("puzzle-menu");
let lastCategory = "official";
let lastSearchOptions = {
searchTerm: "",
difficulty: "any",
duration: "any",
includeCompleted: false,
};
export class PuzzleMenuState extends TextualGameState {
constructor() {
super("PuzzleMenuState");
this.loading = false;
this.activeCategory = "";
/**
* @type {Array<import("../savegame/savegame_typedefs").PuzzleMetadata>}
*/
this.puzzles = [];
}
getThemeMusic() {
@ -99,13 +112,23 @@ export class PuzzleMenuState extends TextualGameState {
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");
while (container.firstChild) {
container.removeChild(container.firstChild);
}
if (category === "search") {
this.loading = false;
this.startSearch();
return;
}
const loadingElement = document.createElement("div");
loadingElement.classList.add("loader");
loadingElement.innerText = T.global.loading + "...";
@ -160,23 +183,148 @@ export class PuzzleMenuState extends TextualGameState {
}
const children = navigation[rootCategory];
for (const category of children) {
const button = document.createElement("button");
button.setAttribute("data-category", category);
button.classList.add("styledButton", "category", "child");
button.innerText = T.puzzleMenu.categories[category];
this.trackClicks(button, () => this.selectCategory(category));
subContainer.appendChild(button);
if (children.length > 1) {
for (const category of children) {
const button = document.createElement("button");
button.setAttribute("data-category", category);
button.classList.add("styledButton", "category", "child");
button.innerText = T.puzzleMenu.categories[category];
this.trackClicks(button, () => this.selectCategory(category));
subContainer.appendChild(button);
}
}
if (rootCategory === "search") {
this.renderSearchForm(subContainer);
}
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
*/
renderPuzzles(puzzles) {
this.puzzles = puzzles;
const container = this.htmlElement.querySelector("#mainContainer");
while (container.firstChild) {
container.removeChild(container.firstChild);
@ -223,15 +371,15 @@ export class PuzzleMenuState extends TextualGameState {
difficulty.innerText = completionPercentage + "%";
stats.appendChild(difficulty);
if (completionPercentage < 40) {
if (puzzle.difficulty < 0.2) {
difficulty.classList.add("stage--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 if (completionPercentage < 80) {
} else {
difficulty.classList.add("stage--medium");
difficulty.innerText = T.puzzleMenu.difficulties.medium;
} else {
difficulty.classList.add("stage--easy");
difficulty.innerText = T.puzzleMenu.difficulties.easy;
}
}
@ -275,7 +423,7 @@ export class PuzzleMenuState extends TextualGameState {
container.appendChild(elem);
this.trackClicks(elem, () => this.playPuzzle(puzzle));
this.trackClicks(elem, () => this.playPuzzle(puzzle.id));
}
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();
this.app.clientApi.apiDownloadPuzzle(puzzle.id).then(
this.asyncChannel.watch(this.app.clientApi.apiDownloadPuzzle(puzzleId)).then(
puzzleData => {
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 => {
closeLoading();
logger.error("Failed to download puzzle", puzzle.id, ":", err);
logger.error("Failed to download puzzle", puzzleId, ":", err);
this.dialogs.showWarning(
T.dialogs.puzzleDownloadError.title,
T.dialogs.puzzleDownloadError.desc + " " + err
@ -354,18 +508,24 @@ export class PuzzleMenuState extends TextualGameState {
*
* @param {import("../savegame/savegame_typedefs").PuzzleFullData} puzzle
*/
startLoadedPuzzle(puzzle) {
const savegame = this.createEmptySavegame();
startLoadedPuzzle(puzzle, nextPuzzles) {
const savegame = Savegame.createPuzzleSavegame(this.app);
this.moveToState("InGameState", {
gameModeId: enumGameModeIds.puzzlePlay,
gameModeParameters: {
puzzle,
nextPuzzles,
},
savegame,
});
}
onEnter(payload) {
if (payload.continueQueue) {
logger.log("Continuing puzzle queue:", payload);
this.playPuzzle(payload.continueQueue[0], payload.continueQueue.slice(1));
}
// Find old category
let rootCategory = "categories";
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());
}
createEmptySavegame() {
return new Savegame(this.app, {
internalId: "puzzle",
metaDataRef: {
internalId: "puzzle",
lastUpdate: 0,
version: 0,
level: 0,
name: "puzzle",
},
});
}
loadPuzzle() {
const shortKeyInput = new FormElementInput({
id: "shortKey",
label: null,
placeholder: "",
defaultValue: "",
validator: val => ShapeDefinition.isValidShortKey(val),
validator: val => ShapeDefinition.isValidShortKey(val) || val.startsWith("/"),
});
const dialog = new DialogWithForm({
@ -422,9 +569,16 @@ export class PuzzleMenuState extends TextualGameState {
this.dialogs.internalShowDialog(dialog);
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();
this.app.clientApi.apiDownloadPuzzleByKey(shortKeyInput.getValue()).then(
this.app.clientApi.apiDownloadPuzzleByKey(searchTerm).then(
puzzle => {
closeLoading();
this.startLoadedPuzzle(puzzle);
@ -451,7 +605,7 @@ export class PuzzleMenuState extends TextualGameState {
return;
}
const savegame = this.createEmptySavegame();
const savegame = Savegame.createPuzzleSavegame(this.app);
this.moveToState("InGameState", {
gameModeId: enumGameModeIds.puzzleEdit,
savegame,

@ -1,3 +1,4 @@
import { THIRDPARTY_URLS } from "../core/config";
import { TextualGameState } from "../core/textual_game_state";
import { formatSecondsToTimeAgo } from "../core/utils";
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 privacy">Privacy Policy</button>
`
}
<div class="versionbar">
@ -109,6 +112,9 @@ export class SettingsState extends TextualGameState {
this.trackClicks(this.htmlElement.querySelector(".about"), this.onAboutClicked, {
preventDefault: false,
});
this.trackClicks(this.htmlElement.querySelector(".privacy"), this.onPrivacyClicked, {
preventDefault: false,
});
}
const keybindingsButton = this.htmlElement.querySelector(".editKeybindings");
@ -180,6 +186,10 @@ export class SettingsState extends TextualGameState {
this.moveToStateAddGoBack("AboutState");
}
onPrivacyClicked() {
this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.privacyPolicy);
}
onKeybindingsClicked() {
this.moveToStateAddGoBack("KeybindingsState");
}

@ -155,6 +155,23 @@ puzzleMenu:
account: My Puzzles
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:
easy: Easy
medium: Medium
@ -657,6 +674,7 @@ ingame:
continueBtn: Keep Playing
menuBtn: Menu
nextPuzzle: Next Puzzle
puzzleMetadata:
author: Author

Loading…
Cancel
Save