1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2026-03-02 03:39:21 +00:00

Puzzle DLC (#1172)

* Puzzle mode (#1135)

* Add mode button to main menu

* [WIP] Add mode menu. Add factory-based gameMode creation

* Add savefile migration, serialize, deserialize

* Add hidden HUD elements, zone, and zoom, boundary constraints

* Clean up lint issues

* Add building, HUD exclusion, building exclusion, and refactor

- [WIP] Add ConstantProducer building that combines ConstantSignal
and ItemProducer functionality. Currently using temp assets.
- Add pre-placement check to the zone
- Use Rectangles for zone and boundary
- Simplify zone drawing
- Account for exclusion in savegame data
- [WIP] Add puzzle play and edit buttons in puzzle mode menu

* [WIP] Add building, component, and systems for producing and
accepting user-specified items and checking goal criteria

* Add ingame puzzle mode UI elements

- Add minimal menus in puzzle mode for back, next navigation
- Add lower menu for changing zone dimenensions

Co-authored-by: Greg Considine <gconsidine@users.noreply.github.com>

* Performance optimizations (#1154)

* 1.3.1 preparations

* Minor fixes, update translations

* Fix achievements not working

* Lots of belt optimizations, ~15% performance boost

* Puzzle mode, part 1

* Puzzle mode, part 2

* Fix missing import

* Puzzle mode, part 3

* Fix typo

* Puzzle mode, part 4

* Puzzle Mode fixes: Correct zone restrictions and more (#1155)

* Hide Puzzle Editor Controls in regular game mode, fix typo

* Disallow shrinking zone if there are buildings

* Fix multi-tile buildings for shrinking

* Puzzle mode, Refactor hud

* Puzzle mode

* Fixed typo in latest puzzle commit (#1156)

* Allow completing puzzles

* Puzzle mode, almost done

* Bump version to 1.4.0

* Fixes

* [puzzle] Prevent pipette cheats (miners, emitters) (#1158)

* Puzzle mode, almost done

* Allow clearing belts with 'B'

* Multiple users for the puzzle dlc

* Bump api key

* Minor adjustments

* Update

* Minor fixes

* Fix throughput

* Fix belts

* Minor puzzle adjustments

* New difficulty

* Minor puzzle improvements

* Fix belt path

* Update translations

* Added a button to return to the menu after a puzzle is completed (#1170)

* added another button to return to the menu

* improved menu return

* fixed continue button to not go back to menu

* [Puzzle] Added ability to lock buildings in the puzzle editor! (#1164)

* initial test

* tried to get it to work

* added icon

* added test exclusion

* reverted css

* completed flow for building locking

* added lock option

* finalized look and changed locked building to same sprite

* removed unused art

* added clearing every goal acceptor on lock to prevent creating impossible puzzles

* heavily improved validation and prevented autocompletion

* validation only checks every 100 ticks to improve performance

* validation only checks every 100 ticks to improve performance

* removed clearing goal acceptors as it isn't needed because of validation

* Add soundtrack, puzzle dlc fixes

Co-authored-by: Greg Considine <gconsidine@users.noreply.github.com>
Co-authored-by: dengr1065 <dengr1065@gmail.com>
Co-authored-by: Sense101 <67970865+Sense101@users.noreply.github.com>
This commit is contained in:
tobspr
2021-05-23 16:32:05 +02:00
committed by GitHub
parent 5f0a95ba11
commit 931c8a5821
167 changed files with 14001 additions and 8193 deletions

View File

@@ -8,6 +8,7 @@ import { KeyActionMapper } from "../game/key_action_mapper";
import { Savegame } from "../savegame/savegame";
import { GameCore } from "../game/core";
import { MUSIC } from "../platform/sound";
import { enumGameModeIds } from "../game/game_mode";
const logger = createLogger("state/ingame");
@@ -39,8 +40,14 @@ export class GameCreationPayload {
/** @type {boolean|undefined} */
this.fastEnter;
/** @type {string} */
this.gameModeId;
/** @type {Savegame} */
this.savegame;
/** @type {object|undefined} */
this.gameModeParameters;
}
}
@@ -97,6 +104,9 @@ export class InGameState extends GameState {
}
getThemeMusic() {
if (this.creationPayload.gameModeId && this.creationPayload.gameModeId.includes("puzzle")) {
return MUSIC.puzzle;
}
return MUSIC.theme;
}
@@ -147,7 +157,11 @@ export class InGameState extends GameState {
* Goes back to the menu state
*/
goBackToMenu() {
this.saveThenGoToState("MainMenuState");
if ([enumGameModeIds.puzzleEdit, enumGameModeIds.puzzlePlay].includes(this.gameModeId)) {
this.saveThenGoToState("PuzzleMenuState");
} else {
this.saveThenGoToState("MainMenuState");
}
}
/**
@@ -220,7 +234,7 @@ export class InGameState extends GameState {
logger.log("Creating new game core");
this.core = new GameCore(this.app);
this.core.initializeRoot(this, this.savegame);
this.core.initializeRoot(this, this.savegame, this.gameModeId);
if (this.savegame.hasGameDump()) {
this.stage4bResumeGame();
@@ -354,6 +368,7 @@ export class InGameState extends GameState {
this.creationPayload = payload;
this.savegame = payload.savegame;
this.gameModeId = payload.gameModeId;
this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement());
this.loadingOverlay.showBasic();
@@ -361,7 +376,13 @@ export class InGameState extends GameState {
// Remove unneded default element
document.body.querySelector(".modalDialogParent").remove();
this.asyncChannel.watch(waitNextFrame()).then(() => this.stage3CreateCore());
this.asyncChannel
.watch(waitNextFrame())
.then(() => this.stage3CreateCore())
.catch(ex => {
logger.error(ex);
throw ex;
});
}
/**
@@ -433,6 +454,11 @@ export class InGameState extends GameState {
logger.warn("Skipping double save and returning same promise");
return this.currentSavePromise;
}
if (!this.core.root.gameMode.getIsSaveable()) {
return Promise.resolve();
}
logger.log("Starting to save game ...");
this.savegame.updateData(this.core.root);

102
src/js/states/login.js Normal file
View 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();
}
}

View File

@@ -66,7 +66,7 @@ export class MainMenuState extends GameState {
<img src="${cachebust(
G_CHINA_VERSION ? "res/logo_cn.png" : "res/logo.png"
)}" alt="shapez.io Logo">
<span class="updateLabel">v${G_BUILD_VERSION} - Achievements!</span>
<span class="updateLabel">v${G_BUILD_VERSION} - Puzzle DLC!</span>
</div>
<div class="mainWrapper ${showDemoBadges ? "demo" : "noDemo"}">
@@ -82,6 +82,19 @@ export class MainMenuState extends GameState {
}
<div class="buttons"></div>
</div>
${
// @TODO: Only display if DLC is owned, otherwise show ad for store page
showDemoBadges
? ""
: `
<div class="puzzleContainer">
<img class="dlcLogo" src="${cachebust(
"res/puzzle_dlc_logo.png"
)}" alt="shapez.io Logo">
<button class="styledButton puzzleDlcPlayButton">Play</button>
</div>`
}
</div>
<div class="footer ${G_CHINA_VERSION ? "china" : ""}">
@@ -203,6 +216,11 @@ export class MainMenuState extends GameState {
const qs = this.htmlElement.querySelector.bind(this.htmlElement);
if (G_IS_DEV && globalConfig.debug.testPuzzleMode) {
this.onPuzzleModeButtonClicked(true);
return;
}
if (G_IS_DEV && globalConfig.debug.fastGameEnter) {
const games = this.app.savegameMgr.getSavegamesMetaData();
if (games.length > 0 && globalConfig.debug.resumeGameOnFastEnter) {
@@ -304,6 +322,34 @@ export class MainMenuState extends GameState {
this.trackClicks(playBtn, this.onPlayButtonClicked);
buttonContainer.appendChild(importButtonElement);
}
const puzzleModeButton = this.htmlElement.querySelector(".puzzleDlcPlayButton");
if (puzzleModeButton) {
this.trackClicks(puzzleModeButton, () => this.onPuzzleModeButtonClicked());
}
}
onPuzzleModeButtonClicked(force = false) {
const hasUnlockedBlueprints = this.app.savegameMgr.getSavegamesMetaData().some(s => s.level >= 12);
console.log(hasUnlockedBlueprints);
if (!force && !hasUnlockedBlueprints) {
const { ok } = this.dialogs.showWarning(
T.dialogs.puzzlePlayRegularRecommendation.title,
T.dialogs.puzzlePlayRegularRecommendation.desc,
["cancel:good", "ok:bad:timeout"]
);
ok.add(() => this.onPuzzleModeButtonClicked(true));
return;
}
this.moveToState("LoginState", {
nextStateId: "PuzzleMenuState",
});
}
onBackButtonClicked() {
this.renderMainMenu();
this.renderSavegames();
}
onSteamLinkClicked() {

View File

@@ -57,8 +57,6 @@ export class PreloadState extends GameState {
this.lastHintShown = -1000;
this.nextHintDuration = 0;
this.currentStatus = "booting";
this.startLoading();
}

View File

@@ -0,0 +1,385 @@
import { globalConfig } from "../core/config";
import { createLogger } from "../core/logging";
import { DialogWithForm } from "../core/modal_dialog_elements";
import { FormElementInput } from "../core/modal_dialog_forms";
import { TextualGameState } from "../core/textual_game_state";
import { formatBigNumberFull } from "../core/utils";
import { enumGameModeIds } from "../game/game_mode";
import { ShapeDefinition } from "../game/shape_definition";
import { Savegame } from "../savegame/savegame";
import { T } from "../translations";
const categories = ["top-rated", "new", "easy", "short", "hard", "completed", "mine"];
/**
* @type {import("../savegame/savegame_typedefs").PuzzleMetadata}
*/
const SAMPLE_PUZZLE = {
id: 1,
shortKey: "CuCuCuCu",
downloads: 0,
likes: 0,
averageTime: 1,
completions: 1,
difficulty: null,
title: "Level 1",
author: "verylongsteamnamewhichbreaks",
completed: false,
};
/**
* @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");
let lastCategory = categories[0];
export class PuzzleMenuState extends TextualGameState {
constructor() {
super("PuzzleMenuState");
this.loading = false;
this.activeCategory = "";
}
getStateHeaderTitle() {
return T.puzzleMenu.title;
}
/**
* Overrides the GameState implementation to provide our own html
*/
internalGetFullHtml() {
let headerHtml = `
<div class="headerBar">
<h1><button class="backButton"></button> ${this.getStateHeaderTitle()}</h1>
<div class="actions">
<button class="styledButton loadPuzzle">${T.puzzleMenu.loadPuzzle}</button>
<button class="styledButton createPuzzle">+ ${T.puzzleMenu.createPuzzle}</button>
</div>
</div>`;
return `
${headerHtml}
<div class="container">
${this.getInnerHTML()}
</div>
`;
}
getMainContentHTML() {
let html = `
<div class="categoryChooser">
${categories
.map(
category => `
<button data-category="${category}" class="styledButton category">${T.puzzleMenu.categories[category]}</button>
`
)
.join("")}
</div>
<div class="puzzles" id="mainContainer"></div>
`;
return html;
}
selectCategory(category) {
lastCategory = category;
if (category === this.activeCategory) {
return;
}
if (this.loading) {
return;
}
this.loading = true;
this.activeCategory = category;
const activeCategory = this.htmlElement.querySelector(".active[data-category]");
if (activeCategory) {
activeCategory.classList.remove("active");
}
this.htmlElement.querySelector(`[data-category="${category}"]`).classList.add("active");
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.getPuzzlesForCategory(category))
.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) {
const container = this.htmlElement.querySelector("#mainContainer");
while (container.firstChild) {
container.removeChild(container.firstChild);
}
for (const puzzle of puzzles) {
const elem = document.createElement("div");
elem.classList.add("puzzle");
elem.classList.toggle("completed", puzzle.completed);
if (puzzle.title) {
const title = document.createElement("div");
title.classList.add("title");
title.innerText = puzzle.title;
elem.appendChild(title);
}
if (puzzle.author) {
const author = document.createElement("div");
author.classList.add("author");
author.innerText = "by " + puzzle.author;
elem.appendChild(author);
}
const stats = document.createElement("div");
stats.classList.add("stats");
elem.appendChild(stats);
if (puzzle.downloads > 0) {
const difficulty = document.createElement("div");
difficulty.classList.add("difficulty");
const completionPercentage = Math.max(
0,
Math.min(100, Math.round((puzzle.completions / puzzle.downloads) * 100.0))
);
difficulty.innerText = completionPercentage + "%";
stats.appendChild(difficulty);
if (completionPercentage < 10) {
difficulty.classList.add("stage--hard");
} else if (completionPercentage < 30) {
difficulty.classList.add("stage--medium");
} else if (completionPercentage < 60) {
difficulty.classList.add("stage--normal");
} else {
difficulty.classList.add("stage--easy");
}
}
if (this.activeCategory === "mine") {
const downloads = document.createElement("div");
downloads.classList.add("downloads");
downloads.innerText = String(puzzle.downloads);
stats.appendChild(downloads);
stats.classList.add("withDownloads");
}
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());
const icon = document.createElement("div");
icon.classList.add("icon");
icon.appendChild(canvas);
elem.appendChild(icon);
container.appendChild(elem);
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) {
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;
});
}
/**
*
* @param {import("../savegame/savegame_typedefs").PuzzleMetadata} puzzle
*/
playPuzzle(puzzle) {
const closeLoading = this.dialogs.showLoadingDialog();
this.app.clientApi.apiDownloadPuzzle(puzzle.id).then(
puzzleData => {
closeLoading();
logger.log("Got puzzle:", puzzleData);
this.startLoadedPuzzle(puzzleData);
},
err => {
closeLoading();
logger.error("Failed to download puzzle", puzzle.id, ":", err);
this.dialogs.showWarning(
T.dialogs.puzzleDownloadError.title,
T.dialogs.puzzleDownloadError.desc + " " + err
);
}
);
}
/**
*
* @param {import("../savegame/savegame_typedefs").PuzzleFullData} puzzle
*/
startLoadedPuzzle(puzzle) {
const savegame = this.createEmptySavegame();
this.moveToState("InGameState", {
gameModeId: enumGameModeIds.puzzlePlay,
gameModeParameters: {
puzzle,
},
savegame,
});
}
onEnter(payload) {
this.selectCategory(lastCategory);
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.loadPuzzle"), () => this.loadPuzzle());
if (G_IS_DEV && globalConfig.debug.testPuzzleMode) {
// this.createNewPuzzle();
this.playPuzzle(SAMPLE_PUZZLE);
}
}
createEmptySavegame() {
return new Savegame(this.app, {
internalId: "puzzle",
metaDataRef: {
internalId: "puzzle",
lastUpdate: 0,
version: 0,
level: 0,
name: "puzzle",
},
});
}
loadPuzzle() {
const shortKeyInput = new FormElementInput({
id: "shortKey",
label: null,
placeholder: "",
defaultValue: "",
validator: val => ShapeDefinition.isValidShortKey(val),
});
const dialog = new DialogWithForm({
app: this.app,
title: T.dialogs.puzzleLoadShortKey.title,
desc: T.dialogs.puzzleLoadShortKey.desc,
formElements: [shortKeyInput],
buttons: ["ok:good:enter"],
});
this.dialogs.internalShowDialog(dialog);
dialog.buttonSignals.ok.add(() => {
const closeLoading = this.dialogs.showLoadingDialog();
this.app.clientApi.apiDownloadPuzzleByKey(shortKeyInput.getValue()).then(
puzzle => {
closeLoading();
this.startLoadedPuzzle(puzzle);
},
err => {
closeLoading();
this.dialogs.showWarning(
T.dialogs.puzzleDownloadError.title,
T.dialogs.puzzleDownloadError.desc + " " + err
);
}
);
});
}
createNewPuzzle(force = false) {
if (!force && !this.app.clientApi.isLoggedIn()) {
const signals = this.dialogs.showWarning(
T.dialogs.puzzleCreateOffline.title,
T.dialogs.puzzleCreateOffline.desc,
["cancel:good", "continue:bad"]
);
signals.continue.add(() => this.createNewPuzzle(true));
return;
}
const savegame = this.createEmptySavegame();
this.moveToState("InGameState", {
gameModeId: enumGameModeIds.puzzleEdit,
savegame,
});
}
}