diff --git a/gulp/webpack.production.config.js b/gulp/webpack.production.config.js index 1779a76f..dc9559b4 100644 --- a/gulp/webpack.production.config.js +++ b/gulp/webpack.production.config.js @@ -40,7 +40,7 @@ module.exports = ({ G_ALL_UI_IMAGES: JSON.stringify(getAllResourceImages()), }; - const minifyNames = environment === "prod"; + const minifyNames = false; return { mode: "production", diff --git a/res/ui/building_icons/constant_producer.png b/res/ui/building_icons/constant_producer.png index ca267ea6..887a3ae6 100644 Binary files a/res/ui/building_icons/constant_producer.png and b/res/ui/building_icons/constant_producer.png differ diff --git a/res/ui/building_icons/goal_acceptor.png b/res/ui/building_icons/goal_acceptor.png index ca267ea6..81451757 100644 Binary files a/res/ui/building_icons/goal_acceptor.png and b/res/ui/building_icons/goal_acceptor.png differ diff --git a/res/ui/icons/puzzle_complete_indicator.png b/res/ui/icons/puzzle_complete_indicator.png new file mode 100644 index 00000000..e2c95b8b Binary files /dev/null and b/res/ui/icons/puzzle_complete_indicator.png differ diff --git a/res/ui/icons/puzzle_upvotes.png b/res/ui/icons/puzzle_upvotes.png new file mode 100644 index 00000000..cf6ba212 Binary files /dev/null and b/res/ui/icons/puzzle_upvotes.png differ diff --git a/res/ui/icons/state_next_button.png b/res/ui/icons/state_next_button.png new file mode 100644 index 00000000..d6e09644 Binary files /dev/null and b/res/ui/icons/state_next_button.png differ diff --git a/res/ui/puzzle_dlc_logo.png b/res/ui/puzzle_dlc_logo.png new file mode 100644 index 00000000..1c430c82 Binary files /dev/null and b/res/ui/puzzle_dlc_logo.png differ diff --git a/res_raw/sprites/blueprints/constant_producer.png b/res_raw/sprites/blueprints/constant_producer.png index 2288c07d..85b55ded 100644 Binary files a/res_raw/sprites/blueprints/constant_producer.png and b/res_raw/sprites/blueprints/constant_producer.png differ diff --git a/res_raw/sprites/blueprints/goal_acceptor.png b/res_raw/sprites/blueprints/goal_acceptor.png index 2288c07d..bb67385b 100644 Binary files a/res_raw/sprites/blueprints/goal_acceptor.png and b/res_raw/sprites/blueprints/goal_acceptor.png differ diff --git a/res_raw/sprites/blueprints/underground_belt_exit-tier2.png b/res_raw/sprites/blueprints/underground_belt_exit-tier2.png index be78107b..15dc6b86 100644 Binary files a/res_raw/sprites/blueprints/underground_belt_exit-tier2.png and b/res_raw/sprites/blueprints/underground_belt_exit-tier2.png differ diff --git a/res_raw/sprites/buildings/constant_producer.png b/res_raw/sprites/buildings/constant_producer.png index 573ef372..4bd8a60f 100644 Binary files a/res_raw/sprites/buildings/constant_producer.png and b/res_raw/sprites/buildings/constant_producer.png differ diff --git a/res_raw/sprites/buildings/goal_acceptor.png b/res_raw/sprites/buildings/goal_acceptor.png index 573ef372..9e5f0808 100644 Binary files a/res_raw/sprites/buildings/goal_acceptor.png and b/res_raw/sprites/buildings/goal_acceptor.png differ diff --git a/res_raw/sprites/create_blueprint_previews.py b/res_raw/sprites/create_blueprint_previews.py index 96688fe4..714804d3 100644 --- a/res_raw/sprites/create_blueprint_previews.py +++ b/res_raw/sprites/create_blueprint_previews.py @@ -41,7 +41,7 @@ def process_image(data, outfilename, src_image): if isWire: targetR = 255 targetG = 104 - targetB = 232 + targetB = 232 for x in range(img.width): for y in range(img.height): @@ -85,6 +85,8 @@ def generate_blueprint_sprite(infilename, outfilename): buildings = listdir("buildings") for buildingId in buildings: + if not ".png" in buildingId: + continue if "hub" in buildingId: continue if "wire-" in buildingId: diff --git a/src/css/ingame_hud/mode_menu_back.scss b/src/css/ingame_hud/mode_menu_back.scss index 27d07592..56a489db 100644 --- a/src/css/ingame_hud/mode_menu_back.scss +++ b/src/css/ingame_hud/mode_menu_back.scss @@ -18,18 +18,23 @@ color: #333438; transition: all 0.12s ease-in-out; transition-property: opacity, transform; + text-transform: uppercase; + @include PlainText; - opacity: 0.8; + opacity: 1; &:hover { - opacity: 1 !important; + opacity: 0.9 !important; } &.pressed { - transform: scale(0.9) !important; + transform: scale(0.95) !important; } - @include DarkThemeOverride { - color: #fff; + @include S(padding-left, 25px); + + & { + /* @load-async */ + background: uiResource("icons/state_back_button.png") left center / D(15px) no-repeat; } } } diff --git a/src/css/ingame_hud/mode_menu_next.scss b/src/css/ingame_hud/mode_menu_next.scss index 45239d85..7c0cfb4b 100644 --- a/src/css/ingame_hud/mode_menu_next.scss +++ b/src/css/ingame_hud/mode_menu_next.scss @@ -17,26 +17,32 @@ 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; - opacity: 0.8; &:hover { - opacity: 1 !important; + opacity: 0.9 !important; } &.pressed { - transform: scale(0.9) !important; + transform: scale(0.95) !important; } - @include DarkThemeOverride { - color: #fff; + & { + /* @load-async */ + background: uiResource("icons/state_next_button.png") right center / D(15px) no-repeat; } } > .content { - @include SuperSmallText; - @include S(font-size, 7px); - @include S(width, 150px); + @include SuperDuperSmallText; + @include S(width, 180px); + @include S(padding-right, 25px); text-align: right; + text-transform: uppercase; + color: $accentColorDark; } } diff --git a/src/css/ingame_hud/puzzle_dlc_logo.scss b/src/css/ingame_hud/puzzle_dlc_logo.scss new file mode 100644 index 00000000..921e853c --- /dev/null +++ b/src/css/ingame_hud/puzzle_dlc_logo.scss @@ -0,0 +1,12 @@ +#ingame_HUD_PuzzleDLCLogo { + position: absolute; + @include S(width, 150px); + @include S(height, 40px); + @include S(bottom, 10px); + @include S(right, 15px); + + & { + /* @load-async */ + background: uiResource("puzzle_dlc_logo.png") center center / contain no-repeat; + } +} diff --git a/src/css/main.scss b/src/css/main.scss index f857050e..89ebbb10 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -29,6 +29,7 @@ @import "states/about"; @import "states/mobile_warning"; @import "states/changelog"; +@import "states/puzzle_menu"; @import "ingame_hud/buildings_toolbar"; @import "ingame_hud/building_placer"; @@ -59,12 +60,14 @@ @import "ingame_hud/mode_menu_next"; @import "ingame_hud/mode_menu"; @import "ingame_hud/mode_settings"; +@import "ingame_hud/puzzle_dlc_logo"; // prettier-ignore $elements: // Base ingame_Canvas, ingame_VignetteOverlay, +ingame_HUD_PuzzleDLCLogo, // Ingame overlays ingame_HUD_Waypoints, diff --git a/src/css/states/puzzle_menu.scss b/src/css/states/puzzle_menu.scss new file mode 100644 index 00000000..e3c139a8 --- /dev/null +++ b/src/css/states/puzzle_menu.scss @@ -0,0 +1,181 @@ +#state_PuzzleMenuState { + > .headerBar { + display: grid; + grid-template-columns: 1fr auto; + align-items: center; + } + + > .container { + > .mainContent { + overflow: hidden; + + > .categoryChooser { + display: grid; + grid-auto-columns: 1fr; + grid-auto-flow: column; + @include S(grid-gap, 2px); + @include S(padding-right, 10px); + + > .category { + background: $accentColorBright; + border-radius: 0; + color: $accentColorDark; + transition: all 0.12s ease-in-out; + transition-property: opacity, background-color, color; + + &:first-child { + @include S(border-top-left-radius, $globalBorderRadius); + @include S(border-bottom-left-radius, $globalBorderRadius); + } + &:last-child { + border-top-right-radius: $globalBorderRadius; + border-bottom-right-radius: $globalBorderRadius; + } + + &.active { + background: $colorBlueBright; + opacity: 1 !important; + color: #fff; + cursor: default; + } + } + } + + > .puzzles { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 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); + overflow-y: scroll; + pointer-events: all; + + > .puzzle { + width: 100%; + @include S(height, 120px); + background: #f3f3f8; + @include S(border-radius, $globalBorderRadius); + + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: D(15px) 1fr auto; + @include S(padding, 5px); + @include S(grid-column-gap, 5px); + box-sizing: border-box; + pointer-events: all; + cursor: pointer; + position: relative; + + @include InlineAnimation(0.12s ease-in-out) { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + + &:hover { + background: #f0f0f8; + } + + > .title { + grid-column: 1 / 2; + grid-row: 1/ 2; + @include PlainText; + } + + > .icon { + grid-column: 1 / 3; + grid-row: 2 / 3; + align-self: center; + justify-self: center; + @include S(width, 70px); + @include S(height, 70px); + + canvas { + width: 100%; + height: 100%; + } + } + + > .author { + grid-column: 1 / 2; + grid-row: 3 / 4; + @include SuperSmallText; + color: $accentColorDark; + align-self: end; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + > .playcount { + grid-column: 1 / 2; + display: none; + grid-row: 3 / 4; + @include SuperSmallText; + } + + > .upvotes { + @include SuperSmallText; + grid-column: 2 / 3; + grid-row: 3 / 4; + color: $accentColorDark; + align-self: end; + justify-self: end; + font-weight: bold; + @include S(padding-right, 12px); + + & { + /* @load-async */ + background: uiResource("icons/puzzle_upvotes.png") calc(100% - #{D(2px)}) #{D( + 3.3px + )} / #{D(8px)} #{D(8px)} no-repeat; + } + } + + &.completed { + .icon, + .upvotes, + .playcount, + .author, + .title { + opacity: 0.5; + } + + background: #fafafa; + + &::after { + content: ""; + position: absolute; + @include S(top, 10px); + @include S(right, 10px); + @include S(width, 30px); + @include S(height, 30px); + opacity: 0.1; + + & { + /* @load-async */ + background: uiResource("icons/puzzle_complete_indicator.png") center center / + contain no-repeat; + } + } + } + } + + > .loader { + grid-column: 1 / -1; + grid-row: 1 / 3; + display: flex; + align-items: center; + color: $accentColorBright; + justify-content: center; + } + } + } + } +} diff --git a/src/css/variables.scss b/src/css/variables.scss index d2798f41..f7bdc900 100644 --- a/src/css/variables.scss +++ b/src/css/variables.scss @@ -18,8 +18,10 @@ $textLineHeight: 21px; $plainTextFontSize: 13px; $plainTextLineHeight: 17px; -$supersmallTextFontSize: 10px; -$supersmallTextLineHeight: 13px; +$superDuperSmallTextFontSize: 8px; +$superDuperSmallTextLineHeight: 9px; +$superSmallTextFontSize: 10px; +$superSmallTextLineHeight: 13px; $buttonFontSize: 14px; $buttonLineHeight: 18px; @@ -76,8 +78,16 @@ $mainFontScale: 1; // } } +@mixin SuperDuperSmallText { + @include ScaleFont($superDuperSmallTextFontSize, $superDuperSmallTextLineHeight); + font-weight: $mainFontWeight; + font-family: $mainFont; + letter-spacing: $mainFontSpacing; + @include DebugText(green); +} + @mixin SuperSmallText { - @include ScaleFont($supersmallTextFontSize, $supersmallTextLineHeight); + @include ScaleFont($superSmallTextFontSize, $superSmallTextLineHeight); font-weight: $mainFontWeight; font-family: $mainFont; letter-spacing: $mainFontSpacing; diff --git a/src/js/application.js b/src/js/application.js index 2c632ef9..3921c474 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -31,6 +31,7 @@ import { PreloadState } from "./states/preload"; import { SettingsState } from "./states/settings"; import { ShapezGameAnalytics } from "./platform/browser/game_analytics"; import { RestrictionManager } from "./core/restriction_manager"; +import { PuzzleMenuState } from "./states/puzzle_menu"; /** * @typedef {import("./platform/achievement_provider").AchievementProviderInterface} AchievementProviderInterface @@ -159,6 +160,7 @@ export class Application { KeybindingsState, AboutState, ChangelogState, + PuzzleMenuState, ]; for (let i = 0; i < states.length; ++i) { diff --git a/src/js/game/camera.js b/src/js/game/camera.js index a62745e8..68968d6b 100644 --- a/src/js/game/camera.js +++ b/src/js/game/camera.js @@ -474,6 +474,7 @@ export class Camera extends BasicSerializableObject { // Clamp everything afterwards this.clampZoomLevel(); + this.clampToBounds(); return false; } @@ -759,18 +760,16 @@ export class Camera extends BasicSerializableObject { } /** - * Clamps x, y position within set boundaries - * @param {Vector} vector + * Clamps the center within set boundaries */ - clampToBounds(vector) { + clampToBounds() { if (!this.root.gameMode.hasBounds()) { return; } const bounds = this.root.gameMode.getBounds().allScaled(globalConfig.tileSize); - - vector.x = clamp(vector.x, bounds.x, bounds.x + bounds.w); - vector.y = clamp(vector.y, bounds.y, bounds.y + bounds.h); + this.center.x = clamp(this.center.x, bounds.x, bounds.x + bounds.w); + this.center.y = clamp(this.center.y, bounds.y, bounds.y + bounds.h); } /** @@ -876,7 +875,7 @@ export class Camera extends BasicSerializableObject { // Panning this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06); this.center = this.center.add(this.currentPan.multiplyScalar((0.5 * dt) / this.zoomLevel)); - this.clampToBounds(this.center); + this.clampToBounds(); } } @@ -942,7 +941,7 @@ export class Camera extends BasicSerializableObject { ) ); - this.clampToBounds(this.center); + this.clampToBounds(); } /** @@ -1029,7 +1028,7 @@ export class Camera extends BasicSerializableObject { this.center.x += moveAmount * forceX * movementSpeed; this.center.y += moveAmount * forceY * movementSpeed; - this.clampToBounds(this.center); + this.clampToBounds(); } } } diff --git a/src/js/game/game_mode.js b/src/js/game/game_mode.js index f2b46c55..31189306 100644 --- a/src/js/game/game_mode.js +++ b/src/js/game/game_mode.js @@ -177,6 +177,16 @@ export class GameMode extends BasicSerializableObject { return false; } + /** @returns {boolean} */ + getIsSaveable() { + return true; + } + + /** @returns {boolean} */ + getSupportsCopyPaste() { + return true; + } + /** @returns {string} */ getBlueprintShapeKey() { return "CbCbCbRb:CwCwCwCw"; diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index c6f12af1..8f4257b9 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -1,58 +1,56 @@ /* typehints:start */ -import { GameRoot } from "../root"; -/* typehints:end */ - -/* dev:start */ -import { TrailerMaker } from "./trailer_maker"; -/* dev:end */ - -import { Signal } from "../../core/signal"; +import { globalConfig, IS_MOBILE } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; +/* dev:end */ +import { Signal } from "../../core/signal"; +import { KEYMAPPINGS } from "../key_action_mapper"; +import { MetaBuilding } from "../meta_building"; +import { GameRoot } from "../root"; +import { ShapeDefinition } from "../shape_definition"; +import { HUDBetaOverlay } from "./parts/beta_overlay"; +import { HUDBlueprintPlacer } from "./parts/blueprint_placer"; import { HUDBuildingsToolbar } from "./parts/buildings_toolbar"; import { HUDBuildingPlacer } from "./parts/building_placer"; -import { HUDBlueprintPlacer } from "./parts/blueprint_placer"; -import { HUDKeybindingOverlay } from "./parts/keybinding_overlay"; -import { HUDUnlockNotification } from "./parts/unlock_notification"; -import { HUDGameMenu } from "./parts/game_menu"; -import { HUDShop } from "./parts/shop"; -import { IS_MOBILE, globalConfig } from "../../core/config"; -import { HUDMassSelector } from "./parts/mass_selector"; -import { HUDVignetteOverlay } from "./parts/vignette_overlay"; -import { HUDStatistics } from "./parts/statistics"; -import { MetaBuilding } from "../meta_building"; -import { HUDPinnedShapes } from "./parts/pinned_shapes"; -import { ShapeDefinition } from "../shape_definition"; -import { HUDNotifications, enumNotificationType } from "./parts/notifications"; -import { HUDSettingsMenu } from "./parts/settings_menu"; +import { HUDCatMemes } from "./parts/cat_memes"; +import { HUDColorBlindHelper } from "./parts/color_blind_helper"; +import { HUDConstantSignalEdit } from "./parts/constant_signal_edit"; +import { HUDChangesDebugger } from "./parts/debug_changes"; import { HUDDebugInfo } from "./parts/debug_info"; import { HUDEntityDebugger } from "./parts/entity_debugger"; -import { KEYMAPPINGS } from "../key_action_mapper"; -import { HUDWatermark } from "./parts/watermark"; -import { HUDModalDialogs } from "./parts/modal_dialogs"; -import { HUDPartTutorialHints } from "./parts/tutorial_hints"; -import { HUDWaypoints } from "./parts/waypoints"; +import { HUDGameMenu } from "./parts/game_menu"; import { HUDInteractiveTutorial } from "./parts/interactive_tutorial"; -import { HUDScreenshotExporter } from "./parts/screenshot_exporter"; -import { HUDColorBlindHelper } from "./parts/color_blind_helper"; -import { HUDShapeViewer } from "./parts/shape_viewer"; -import { HUDWiresOverlay } from "./parts/wires_overlay"; -import { HUDChangesDebugger } from "./parts/debug_changes"; -import { queryParamOptions } from "../../core/query_parameters"; -import { HUDSandboxController } from "./parts/sandbox_controller"; -import { HUDWiresToolbar } from "./parts/wires_toolbar"; -import { HUDWireInfo } from "./parts/wire_info"; -import { HUDLeverToggle } from "./parts/lever_toggle"; +import { HUDKeybindingOverlay } from "./parts/keybinding_overlay"; import { HUDLayerPreview } from "./parts/layer_preview"; +import { HUDLeverToggle } from "./parts/lever_toggle"; +import { HUDMassSelector } from "./parts/mass_selector"; import { HUDMinerHighlight } from "./parts/miner_highlight"; -import { HUDBetaOverlay } from "./parts/beta_overlay"; -import { HUDStandaloneAdvantages } from "./parts/standalone_advantages"; -import { HUDCatMemes } from "./parts/cat_memes"; -import { HUDTutorialVideoOffer } from "./parts/tutorial_video_offer"; -import { HUDConstantSignalEdit } from "./parts/constant_signal_edit"; +import { HUDModalDialogs } from "./parts/modal_dialogs"; +import { HUDModeMenu } from "./parts/mode_menu"; import { HUDModeMenuBack } from "./parts/mode_menu_back"; import { HUDModeMenuNext } from "./parts/mode_menu_next"; -import { HUDModeMenu } from "./parts/mode_menu"; import { HUDModeSettings } from "./parts/mode_settings"; +import { enumNotificationType, HUDNotifications } from "./parts/notifications"; +import { HUDPinnedShapes } from "./parts/pinned_shapes"; +import { HUDPuzzleDLCLogo } from "./parts/puzzle_dlc_logo"; +import { HUDSandboxController } from "./parts/sandbox_controller"; +import { HUDScreenshotExporter } from "./parts/screenshot_exporter"; +import { HUDSettingsMenu } from "./parts/settings_menu"; +import { HUDShapeViewer } from "./parts/shape_viewer"; +import { HUDShop } from "./parts/shop"; +import { HUDStandaloneAdvantages } from "./parts/standalone_advantages"; +import { HUDStatistics } from "./parts/statistics"; +import { HUDPartTutorialHints } from "./parts/tutorial_hints"; +import { HUDTutorialVideoOffer } from "./parts/tutorial_video_offer"; +import { HUDUnlockNotification } from "./parts/unlock_notification"; +import { HUDVignetteOverlay } from "./parts/vignette_overlay"; +import { HUDWatermark } from "./parts/watermark"; +import { HUDWaypoints } from "./parts/waypoints"; +import { HUDWiresOverlay } from "./parts/wires_overlay"; +import { HUDWiresToolbar } from "./parts/wires_toolbar"; +import { HUDWireInfo } from "./parts/wire_info"; +/* typehints:end */ +/* dev:start */ +import { TrailerMaker } from "./trailer_maker"; export class GameHUD { /** @@ -96,6 +94,7 @@ export class GameHUD { modeMenuNext: HUDModeMenuNext, modeMenu: HUDModeMenu, modeSettings: HUDModeSettings, + puzzleDlcLogo: HUDPuzzleDLCLogo, // Must always exist pinnedShapes: HUDPinnedShapes, diff --git a/src/js/game/hud/parts/mode_menu_back.js b/src/js/game/hud/parts/mode_menu_back.js index ff729706..9eae74cc 100644 --- a/src/js/game/hud/parts/mode_menu_back.js +++ b/src/js/game/hud/parts/mode_menu_back.js @@ -9,7 +9,7 @@ export class HUDModeMenuBack extends BaseHUDPart { this.element = makeDiv(parent, "ingame_HUD_ModeMenuBack"); this.button = document.createElement("button"); this.button.classList.add("button"); - this.button.textContent = "⬅ " + T.ingame.modeMenu[key].back.title; + this.button.textContent = T.ingame.modeMenu[key].back.title; this.element.appendChild(this.button); this.trackClicks(this.button, this.back); diff --git a/src/js/game/hud/parts/mode_menu_next.js b/src/js/game/hud/parts/mode_menu_next.js index a3698269..6453a793 100644 --- a/src/js/game/hud/parts/mode_menu_next.js +++ b/src/js/game/hud/parts/mode_menu_next.js @@ -9,7 +9,7 @@ export class HUDModeMenuNext extends BaseHUDPart { this.element = makeDiv(parent, "ingame_HUD_ModeMenuNext"); this.button = document.createElement("button"); this.button.classList.add("button"); - this.button.textContent = T.ingame.modeMenu[key].next.title + " ➡ "; + this.button.textContent = T.ingame.modeMenu[key].next.title; this.element.appendChild(this.button); this.content = makeDiv(this.element, null, ["content"], T.ingame.modeMenu[key].next.desc); diff --git a/src/js/game/hud/parts/puzzle_dlc_logo.js b/src/js/game/hud/parts/puzzle_dlc_logo.js new file mode 100644 index 00000000..ec50808a --- /dev/null +++ b/src/js/game/hud/parts/puzzle_dlc_logo.js @@ -0,0 +1,13 @@ +import { makeDiv } from "../../../core/utils"; +import { BaseHUDPart } from "../base_hud_part"; + +export class HUDPuzzleDLCLogo extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_PuzzleDLCLogo"); + parent.appendChild(this.element); + } + + initialize() {} + + next() {} +} diff --git a/src/js/game/hud/parts/settings_menu.js b/src/js/game/hud/parts/settings_menu.js index eb902934..16da0440 100644 --- a/src/js/game/hud/parts/settings_menu.js +++ b/src/js/game/hud/parts/settings_menu.js @@ -13,17 +13,19 @@ export class HUDSettingsMenu extends BaseHUDPart { this.menuElement = makeDiv(this.background, null, ["menuElement"]); - this.statsElement = makeDiv( - this.background, - null, - ["statsElement"], - ` + if (this.root.gameMode.hasHub()) { + this.statsElement = makeDiv( + this.background, + null, + ["statsElement"], + ` ${T.ingame.settingsMenu.beltsPlaced} ${T.ingame.settingsMenu.buildingsPlaced} ${T.ingame.settingsMenu.playtime} ` - ); + ); + } this.buttonContainer = makeDiv(this.menuElement, null, ["buttons"]); @@ -94,23 +96,25 @@ export class HUDSettingsMenu extends BaseHUDPart { const totalMinutesPlayed = Math.ceil(this.root.time.now() / 60); - /** @type {HTMLElement} */ - const playtimeElement = this.statsElement.querySelector(".playtime"); - /** @type {HTMLElement} */ - const buildingsPlacedElement = this.statsElement.querySelector(".buildingsPlaced"); - /** @type {HTMLElement} */ - const beltsPlacedElement = this.statsElement.querySelector(".beltsPlaced"); + if (this.root.gameMode.hasHub()) { + /** @type {HTMLElement} */ + const playtimeElement = this.statsElement.querySelector(".playtime"); + /** @type {HTMLElement} */ + const buildingsPlacedElement = this.statsElement.querySelector(".buildingsPlaced"); + /** @type {HTMLElement} */ + const beltsPlacedElement = this.statsElement.querySelector(".beltsPlaced"); - playtimeElement.innerText = T.global.time.xMinutes.replace("", `${totalMinutesPlayed}`); + playtimeElement.innerText = T.global.time.xMinutes.replace("", `${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 = ` +
+

${this.getStateHeaderTitle()}

+ +
+ +
+
`; + + 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