", `${totalMinutesPlayed}`);
- buildingsPlacedElement.innerText = formatBigNumberFull(
- this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length -
+ buildingsPlacedElement.innerText = formatBigNumberFull(
+ this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length -
+ this.root.entityMgr.getAllWithComponent(BeltComponent).length
+ );
+
+ beltsPlacedElement.innerText = formatBigNumberFull(
this.root.entityMgr.getAllWithComponent(BeltComponent).length
- );
-
- beltsPlacedElement.innerText = formatBigNumberFull(
- this.root.entityMgr.getAllWithComponent(BeltComponent).length
- );
+ );
+ }
}
close() {
diff --git a/src/js/game/modes/puzzle.js b/src/js/game/modes/puzzle.js
index 2f7f50ca..31cfcb3d 100644
--- a/src/js/game/modes/puzzle.js
+++ b/src/js/game/modes/puzzle.js
@@ -11,6 +11,7 @@ import { HUDKeybindingOverlay } from "../hud/parts/keybinding_overlay";
import { HUDPartTutorialHints } from "../hud/parts/tutorial_hints";
import { HUDPinnedShapes } from "../hud/parts/pinned_shapes";
import { HUDWaypoints } from "../hud/parts/waypoints";
+import { HUDMassSelector } from "../hud/parts/mass_selector";
export class PuzzleGameMode extends GameMode {
static getType() {
@@ -33,6 +34,7 @@ export class PuzzleGameMode extends GameMode {
this.setHudParts({
[HUDGameMenu.name]: false,
+ [HUDMassSelector.name]: false,
[HUDInteractiveTutorial.name]: false,
[HUDKeybindingOverlay.name]: false,
[HUDPartTutorialHints.name]: false,
@@ -122,6 +124,14 @@ export class PuzzleGameMode extends GameMode {
return 1;
}
+ getIsSaveable() {
+ return false;
+ }
+
+ getSupportsCopyPaste() {
+ return false;
+ }
+
/** @returns {boolean} */
getIsFreeplayAvailable() {
return true;
diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js
index 0a2e127f..4f8e9ec2 100644
--- a/src/js/game/modes/regular.js
+++ b/src/js/game/modes/regular.js
@@ -14,6 +14,7 @@ import { HUDModeSettings } from "../hud/parts/mode_settings";
import { enumGameModeIds, enumGameModeTypes, GameMode } from "../game_mode";
import { ShapeDefinition } from "../shape_definition";
import { enumHubGoalRewards } from "../tutorial_goals";
+import { HUDPuzzleDLCLogo } from "../hud/parts/puzzle_dlc_logo";
/** @typedef {{
* shape: string,
@@ -522,6 +523,7 @@ export class RegularGameMode extends GameMode {
[HUDModeMenuNext.name]: false,
[HUDModeMenu.name]: false,
[HUDModeSettings.name]: false,
+ [HUDPuzzleDLCLogo.name]: false,
});
this.setBuildings({
diff --git a/src/js/game/systems/constant_producer.js b/src/js/game/systems/constant_producer.js
index 9b1ec96f..0f167829 100644
--- a/src/js/game/systems/constant_producer.js
+++ b/src/js/game/systems/constant_producer.js
@@ -1,11 +1,12 @@
/* typehints:start */
-import { GameRoot } from "../root";
/* typehints:end */
-
import { globalConfig } from "../../core/config";
+import { DrawParameters } from "../../core/draw_parameters";
import { ConstantSignalComponent } from "../components/constant_signal";
import { ItemProducerComponent } from "../components/item_producer";
import { GameSystemWithFilter } from "../game_system_with_filter";
+import { MapChunk } from "../map_chunk";
+import { GameRoot } from "../root";
export class ConstantProducerSystem extends GameSystemWithFilter {
/** @param {GameRoot} root */
@@ -29,6 +30,12 @@ export class ConstantProducerSystem extends GameSystemWithFilter {
}
}
+ /**
+ *
+ * @param {DrawParameters} parameters
+ * @param {MapChunk} chunk
+ * @returns
+ */
drawChunk(parameters, chunk) {
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
@@ -48,7 +55,7 @@ export class ConstantProducerSystem extends GameSystemWithFilter {
// TODO: Better looking overlay
const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace();
- item.drawItemCenteredClipped(center.x, center.y, parameters, globalConfig.tileSize);
+ item.drawItemCenteredClipped(center.x, center.y + 1, parameters, globalConfig.tileSize * 0.65);
}
}
}
diff --git a/src/js/game/systems/constant_signal.js b/src/js/game/systems/constant_signal.js
index 6a9c2a02..2fab1eb8 100644
--- a/src/js/game/systems/constant_signal.js
+++ b/src/js/game/systems/constant_signal.js
@@ -61,12 +61,14 @@ export class ConstantSignalSystem extends GameSystemWithFilter {
});
const items = [
- BOOL_FALSE_SINGLETON,
- BOOL_TRUE_SINGLETON,
...Object.values(COLOR_ITEM_SINGLETONS),
this.root.shapeDefinitionMgr.getShapeItemFromShortKey(this.root.gameMode.getBlueprintShapeKey()),
];
+ if (entity.components.ConstantSignal.type === enumConstantSignalType.wired) {
+ items.unshift(BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON);
+ }
+
if (this.root.gameMode.hasHub()) {
items.push(
this.root.shapeDefinitionMgr.getShapeItemFromDefinition(
diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js
index 34f360e3..32ef3edc 100644
--- a/src/js/states/ingame.js
+++ b/src/js/states/ingame.js
@@ -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");
@@ -150,7 +151,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");
+ }
}
/**
@@ -437,6 +442,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);
diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js
index b82252ff..13cabdaa 100644
--- a/src/js/states/main_menu.js
+++ b/src/js/states/main_menu.js
@@ -207,12 +207,12 @@ export class MainMenuState extends GameState {
const qs = this.htmlElement.querySelector.bind(this.htmlElement);
- if (G_IS_DEV && globalConfig.debug.fastGameEnter) {
- if (globalConfig.debug.testPuzzleMode) {
- this.onPuzzleEditButtonClicked();
- return;
- }
+ if (G_IS_DEV && globalConfig.debug.testPuzzleMode) {
+ this.onPuzzleModeButtonClicked();
+ return;
+ }
+ if (G_IS_DEV && globalConfig.debug.fastGameEnter) {
const games = this.app.savegameMgr.getSavegamesMetaData();
if (games.length > 0 && globalConfig.debug.resumeGameOnFastEnter) {
this.resumeGame(games[0]);
@@ -369,7 +369,7 @@ export class MainMenuState extends GameState {
}
onPuzzleModeButtonClicked() {
- this.renderPuzzleModeMenu();
+ this.moveToState("PuzzleMenuState");
}
onBackButtonClicked() {
diff --git a/src/js/states/puzzle_menu.js b/src/js/states/puzzle_menu.js
new file mode 100644
index 00000000..d6465627
--- /dev/null
+++ b/src/js/states/puzzle_menu.js
@@ -0,0 +1,218 @@
+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"];
+
+/**
+ * @typedef {{
+ * shortKey: string;
+ * upvotes: number;
+ * playcount: number;
+ * title: string;
+ * author: string;
+ * completed: boolean;
+ * }} PuzzleMetadata
+ */
+
+const SAMPLE_PUZZLE = {
+ shortKey: "CuCuCuCu",
+ upvotes: 10000,
+ playcount: 1000,
+ 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,
+];
+
+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 = `
+ `;
+
+ return `
+ ${headerHtml}
+
+ ${this.getInnerHTML()}
+
+ `;
+ }
+
+ getMainContentHTML() {
+ let html = `
+
+
+
+ ${categories
+ .map(
+ category => `
+
+ `
+ )
+ .join("")}
+
+
+
+ `;
+
+ return html;
+ }
+
+ selectCategory(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
+ );
+ }
+ )
+ .then(() => (this.loading = false));
+ }
+
+ /**
+ *
+ * @param {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);
+ }
+
+ if (puzzle.upvotes) {
+ const upvotes = document.createElement("div");
+ upvotes.classList.add("upvotes");
+ upvotes.innerText = formatBigNumberFull(puzzle.upvotes);
+ elem.appendChild(upvotes);
+ }
+
+ if (puzzle.playcount) {
+ const playcount = document.createElement("div");
+ playcount.classList.add("playcount");
+ playcount.innerText = String(puzzle.playcount) + " plays";
+ elem.appendChild(playcount);
+ }
+
+ 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);
+ }
+ }
+
+ getPuzzlesForCategory(category) {
+ return new Promise(resolve => setTimeout(() => resolve(BUILTIN_PUZZLES), 100));
+ }
+
+ onEnter() {
+ this.selectCategory("levels");
+
+ 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);
+ }
+
+ createNewPuzzle() {
+ const savegame = this.app.savegameMgr.createNewSavegame();
+ this.moveToState("InGameState", {
+ gameModeId: enumGameModeIds.puzzleEdit,
+ savegame,
+ });
+ }
+}
diff --git a/translations/base-en.yaml b/translations/base-en.yaml
index a2bb4fde..29c610a7 100644
--- a/translations/base-en.yaml
+++ b/translations/base-en.yaml
@@ -122,6 +122,14 @@ mainMenu:
puzzleMenu:
play: Play
edit: Edit
+ title: Puzzle Mode
+ createPuzzle: Create Puzzle
+
+ categories:
+ levels: Levels
+ new: New
+ topRated: Top Rated
+ myPuzzles: My Puzzles
dialogs:
buttons:
@@ -259,6 +267,11 @@ dialogs:
title: Tutorial Available
desc: There is a tutorial video available for this level, but it is only available in English. Would you like to watch it?
+ puzzleLoadFailed:
+ title: Puzzles failed to load
+ desc: >-
+ Unfortunately the puzzles could not be loaded:
+
ingame:
# This is shown in the top left corner and displays useful keybindings in
# every situation
@@ -486,18 +499,18 @@ ingame:
modeMenu:
puzzleEditMode:
back:
- title: Main Menu
- next:
+ title: Menu
+ next:
title: Playtest
- desc: You will have to complete the puzzle before being able to publish it
+ desc: Required for publishing
puzzleEditTestMode:
- back:
+ back:
title: Edit
- next:
+ next:
title: Publish
puzzlePlayMode:
back:
- title: Puzzle Menu
+ title: Menu
next:
title: Next