mirror of
				https://github.com/tobspr/shapez.io.git
				synced 2025-06-13 13:04:03 +00:00 
			
		
		
		
	Add ability to import savegames, add game menu, multiple smaller improvements
This commit is contained in:
		
							parent
							
								
									e1adc7d523
								
							
						
					
					
						commit
						3d5a40c4b0
					
				
							
								
								
									
										
											BIN
										
									
								
								res/ui/demo_badge.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								res/ui/demo_badge.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								res/ui/get_on_steam.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								res/ui/get_on_steam.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 38 KiB | 
							
								
								
									
										
											BIN
										
									
								
								res/ui/icons/delete.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								res/ui/icons/delete.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 825 B | 
| @ -22,9 +22,15 @@ | ||||
|         opacity: 0; | ||||
|     } | ||||
| 
 | ||||
|     &.loadingDialog { | ||||
|         * { | ||||
|             color: #fff; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     > .dialogInner { | ||||
|         background: #fff; | ||||
|         @include S(min-width, 500px); | ||||
|         @include S(min-width, 300px); | ||||
|         max-width: calc(100vw - #{D(40px)}); | ||||
|         max-height: calc(100vh - #{D(40px)}); | ||||
|         @include S(border-radius, 4px); | ||||
| @ -60,5 +66,27 @@ | ||||
|             overflow-y: auto; | ||||
|             pointer-events: all; | ||||
|         } | ||||
| 
 | ||||
|         > .buttons { | ||||
|             @include S(margin-top, 15px); | ||||
|             display: flex; | ||||
|             justify-content: flex-end; | ||||
|             > button { | ||||
|                 @include S(margin-left, 8px); | ||||
|                 @include Text; | ||||
|                 @include S(min-width, 60px); | ||||
|                 @include S(padding, 5px, 15px); | ||||
| 
 | ||||
|                 &.good { | ||||
|                     background-color: $colorGreenBright; | ||||
|                     color: #fff; | ||||
|                 } | ||||
| 
 | ||||
|                 &.bad { | ||||
|                     background-color: $colorRedBright; | ||||
|                     color: #fff; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -24,6 +24,8 @@ | ||||
|             transition-property: opacity, transform; | ||||
|             opacity: 0.9; | ||||
|             @include S(margin-left, 5px); | ||||
|             position: relative; | ||||
|             @include IncreasedClickArea(0px); | ||||
| 
 | ||||
|             &:hover { | ||||
|                 opacity: 0.8; | ||||
| @ -80,7 +82,7 @@ | ||||
|         border-radius: 0 0 #{D(4px)} #{D(4px)}; | ||||
|         @include S(padding-left, 30px); | ||||
|         @include S(margin-right, 3px); | ||||
|         @include IncreasedClickArea(10px); | ||||
|         @include IncreasedClickArea(0px); | ||||
|         @include ButtonText; | ||||
|         @include S(min-height, 30px); | ||||
|         transition: all 0.12s ease-in-out; | ||||
|  | ||||
| @ -21,8 +21,8 @@ | ||||
|         } | ||||
| 
 | ||||
|         transform-origin: 100% 50%; | ||||
| 
 | ||||
|         @include InlineAnimation(5s ease-in-out) { | ||||
|         opacity: 0; | ||||
|         @include InlineAnimation(3s ease-in-out) { | ||||
|             0% { | ||||
|                 opacity: 1; | ||||
|             } | ||||
|  | ||||
							
								
								
									
										32
									
								
								src/css/ingame_hud/settings_menu.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/css/ingame_hud/settings_menu.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| #ingame_HUD_SettingsMenu { | ||||
|     .timePlayed { | ||||
|         position: absolute; | ||||
|         @include S(left, 30px); | ||||
|         @include S(bottom, 30px); | ||||
|         color: #fff; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         strong { | ||||
|             text-transform: uppercase; | ||||
|             @include PlainText; | ||||
|         } | ||||
| 
 | ||||
|         span { | ||||
|             @include Heading; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
| 
 | ||||
|     .buttons { | ||||
|         display: grid; | ||||
|         grid-auto-flow: row; | ||||
|         @include S(grid-gap, 10px); | ||||
|         background: rgba(0, 10, 20, 0.1); | ||||
|         @include S(padding, 20px); | ||||
|         @include S(border-radius, 2px); | ||||
|     } | ||||
| } | ||||
| @ -3,6 +3,8 @@ | ||||
|         @include S(padding-right, 10px); | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         @include S(width, 500px); | ||||
| 
 | ||||
|         .upgrade { | ||||
|             display: grid; | ||||
|             grid-template-columns: auto 1fr auto; | ||||
| @ -99,6 +101,7 @@ | ||||
|                     display: flex; | ||||
|                     flex-direction: column; | ||||
|                     align-items: center; | ||||
|                     @include S(width, 65px); | ||||
| 
 | ||||
|                     button.pin { | ||||
|                         @include S(width, 12px); | ||||
|  | ||||
| @ -1,4 +1,8 @@ | ||||
| #ingame_HUD_Statistics { | ||||
|     .content { | ||||
|         @include S(width, 500px); | ||||
|     } | ||||
| 
 | ||||
|     .filterHeader { | ||||
|         display: grid; | ||||
|         grid-template-columns: auto 1fr; | ||||
|  | ||||
| @ -37,12 +37,13 @@ | ||||
| @import "ingame_hud/statistics"; | ||||
| @import "ingame_hud/pinned_shapes"; | ||||
| @import "ingame_hud/notifications"; | ||||
| @import "ingame_hud/settings_menu"; | ||||
| 
 | ||||
| // Z-Index | ||||
| $elements: ingame_Canvas, ingame_VignetteOverlay, ingame_HUD_building_placer, ingame_HUD_PinnedShapes, | ||||
|     ingame_HUD_buildings_toolbar, ingame_HUD_GameMenu, ingame_HUD_KeybindingOverlay, ingame_HUD_Notifications, | ||||
|     ingame_HUD_Shop, ingame_HUD_Statistics, ingame_HUD_BetaOverlay, ingame_HUD_MassSelector, | ||||
|     ingame_HUD_UnlockNotification; | ||||
|     ingame_HUD_UnlockNotification, ingame_HUD_SettingsMenu; | ||||
| 
 | ||||
| $zindex: 100; | ||||
| 
 | ||||
| @ -59,16 +60,15 @@ body.uiHidden { | ||||
|     #ingame_HUD_building_placer, | ||||
|     #ingame_HUD_GameMenu, | ||||
|     #ingame_HUD_MassSelector, | ||||
|     #ingame_HUD_PinnedShapes { | ||||
|     #ingame_HUD_PinnedShapes, | ||||
|     #ingame_HUD_Notifications { | ||||
|         display: none !important; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| body.modalDialogActive, | ||||
| body.ingameDialogOpen { | ||||
|     #ingame_Canvas, | ||||
|     #ingame_HUD_GameMenu, | ||||
|     #ingame_HUD_KeybindingOverlay, | ||||
|     #ingame_HUD_buildings_toolbar, | ||||
|     #ingame_HUD_PinnedShapes { | ||||
|         filter: blur(5px); | ||||
|     > *:not(.ingameDialog):not(.modalDialogParent) { | ||||
|         filter: blur(5px) !important; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -5,7 +5,6 @@ | ||||
|     flex-direction: column; | ||||
| 
 | ||||
|     background: rgb(140, 165, 194) center center / cover !important; | ||||
|     // background: $colorGreenBright !important; | ||||
| 
 | ||||
|     .fullscreenBackgroundVideo { | ||||
|         z-index: -1; | ||||
| @ -36,14 +35,88 @@ | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .mainWrapper { | ||||
|         display: grid; | ||||
|         grid-template-columns: 1fr auto 1fr; | ||||
|         @include S(padding, 0, 10px); | ||||
|         align-items: center; | ||||
|         justify-items: center; | ||||
|         @include S(grid-column-gap, 10px); | ||||
| 
 | ||||
|         .standaloneBanner { | ||||
|             background: rgb(255, 225, 238); | ||||
|             @include S(border-radius, 4px); | ||||
|             height: 100%; | ||||
|             box-sizing: border-box; | ||||
|             @include S(padding, 15px); | ||||
| 
 | ||||
|             display: flex; | ||||
|             flex-direction: column; | ||||
| 
 | ||||
|             strong { | ||||
|                 font-weight: bold; | ||||
|             } | ||||
| 
 | ||||
|             h3 { | ||||
|                 @include Heading; | ||||
|                 font-weight: bold; | ||||
|                 @include S(margin-bottom, 15px); | ||||
|                 text-transform: uppercase; | ||||
|                 color: $colorRedBright; | ||||
|             } | ||||
| 
 | ||||
|             p { | ||||
|                 @include Text; | ||||
|             } | ||||
| 
 | ||||
|             ul { | ||||
|                 @include S(margin-top, 15px); | ||||
|                 @include S(padding-left, 20px); | ||||
|                 li { | ||||
|                     @include Text; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             .steamLink { | ||||
|                 width: 100%; | ||||
|                 @include S(height, 50px); | ||||
| 
 | ||||
|                 background: uiResource("get_on_steam.png") center center / contain no-repeat; | ||||
|                 overflow: hidden; | ||||
|                 display: block; | ||||
|                 text-indent: -999em; | ||||
|                 cursor: pointer; | ||||
|                 @include S(margin-top, 20px); | ||||
|                 pointer-events: all; | ||||
|                 transition: all 0.12s ease-in; | ||||
|                 transition-property: opacity, transform; | ||||
|                 transform: skewX(-0.5deg); | ||||
|                 &:hover { | ||||
|                     transform: skewX(-1deg) scale(1.02); | ||||
|                     opacity: 0.9; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .logo { | ||||
|         display: flex; | ||||
|         flex-grow: 1; | ||||
|         align-items: center; | ||||
|         justify-content: center; | ||||
|         flex-direction: column; | ||||
|         @include S(padding-top, 20px); | ||||
|         img { | ||||
|             @include S(width, 350px); | ||||
|         } | ||||
| 
 | ||||
|         .demoBadge { | ||||
|             @include S(margin, 10px, 0); | ||||
|             @include S(width, 100px); | ||||
|             @include S(height, 30px); | ||||
|             background: uiResource("demo_badge.png") center center / contain no-repeat; | ||||
|             display: inline-block; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .betaWarning { | ||||
| @ -53,12 +126,11 @@ | ||||
|         @include S(padding, 10px); | ||||
|         @include S(border-radius, 4px); | ||||
|         color: #fff; | ||||
|         @include S(margin-bottom, 10px); | ||||
|         @include S(margin-top, 10px); | ||||
|         border: #{D(2px)} solid rgba(0, 10, 20, 0.1); | ||||
|     } | ||||
| 
 | ||||
|     .mainContainer { | ||||
|         @include S(margin-top, 10px); | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         justify-content: flex-start; | ||||
| @ -67,6 +139,8 @@ | ||||
|         @include S(padding, 20px); | ||||
|         @include S(border-radius, 4px); | ||||
|         // border: #{D(2px)} solid rgba(0, 10, 20, 0.1); | ||||
|         height: 100%; | ||||
|         box-sizing: border-box; | ||||
| 
 | ||||
|         .playButton { | ||||
|             @include SuperHeading; | ||||
| @ -82,8 +156,12 @@ | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         .importButton { | ||||
|             @include S(margin-top, 15px); | ||||
|         } | ||||
| 
 | ||||
|         .savegames { | ||||
|             @include S(max-height, 92px); | ||||
|             @include S(max-height, 105px); | ||||
|             overflow-y: auto; | ||||
|             @include S(width, 250px); | ||||
|             pointer-events: all; | ||||
| @ -101,6 +179,7 @@ | ||||
|                 grid-template-columns: 1fr auto auto; | ||||
|                 grid-template-rows: auto auto; | ||||
|                 @include S(grid-column-gap, 5px); | ||||
|                 @include S(grid-row-gap, 3px); | ||||
| 
 | ||||
|                 .internalId { | ||||
|                     grid-column: 1 / 2; | ||||
| @ -116,7 +195,8 @@ | ||||
|                 } | ||||
| 
 | ||||
|                 button.resumeGame, | ||||
|                 button.downloadGame { | ||||
|                 button.downloadGame, | ||||
|                 button.deleteGame { | ||||
|                     grid-column: 3 / 4; | ||||
|                     grid-row: 1 / 3; | ||||
|                     @include S(width, 30px); | ||||
| @ -128,8 +208,22 @@ | ||||
| 
 | ||||
|                 button.downloadGame { | ||||
|                     grid-column: 2 / 3; | ||||
|                     grid-row: 1 / 2; | ||||
|                     background-image: uiResource("icons/download.png"); | ||||
|                     @include S(width, 15px); | ||||
|                     @include IncreasedClickArea(0px); | ||||
|                     @include S(height, 15px); | ||||
|                     align-self: end; | ||||
|                     background-size: 60%; | ||||
|                 } | ||||
| 
 | ||||
|                 button.deleteGame { | ||||
|                     grid-column: 2 / 3; | ||||
|                     grid-row: 2 / 3; | ||||
|                     background-color: $colorRedBright; | ||||
|                     @include IncreasedClickArea(0px); | ||||
|                     background-image: uiResource("icons/delete.png"); | ||||
|                     @include S(width, 15px); | ||||
|                     @include S(height, 15px); | ||||
|                     align-self: end; | ||||
|                     background-size: 60%; | ||||
|  | ||||
| @ -30,6 +30,7 @@ import { GameAnalyticsInterface } from "./platform/game_analytics"; | ||||
| import { ShapezGameAnalytics } from "./platform/browser/game_analytics"; | ||||
| import { queryParamOptions } from "./core/query_parameters"; | ||||
| import { NoGameAnalytics } from "./platform/browser/no_game_analytics"; | ||||
| import { StorageImplBrowserIndexedDB } from "./platform/browser/storage_indexed_db"; | ||||
| 
 | ||||
| const logger = createLogger("application"); | ||||
| 
 | ||||
| @ -119,7 +120,12 @@ export class Application { | ||||
| 
 | ||||
|         // Start with empty ad provider
 | ||||
|         this.adProvider = new NoAdProvider(this); | ||||
|         this.storage = new StorageImplBrowser(this); | ||||
| 
 | ||||
|         if (window.indexedDB) { | ||||
|             this.storage = new StorageImplBrowserIndexedDB(this); | ||||
|         } else { | ||||
|             this.storage = new StorageImplBrowser(this); | ||||
|         } | ||||
|         this.sound = new SoundImplBrowser(this); | ||||
|         this.platformWrapper = new PlatformWrapperImplBrowser(this); | ||||
|         this.analytics = new GoogleAnalyticsImpl(this); | ||||
|  | ||||
| @ -13,7 +13,7 @@ import { round1Digit } from "./utils"; | ||||
| 
 | ||||
| const logger = createLogger("buffers"); | ||||
| 
 | ||||
| const bufferGcDurationSeconds = 3; | ||||
| const bufferGcDurationSeconds = 10; | ||||
| 
 | ||||
| export class BufferMaintainer { | ||||
|     /** | ||||
|  | ||||
| @ -32,7 +32,7 @@ export const globalConfig = { | ||||
| 
 | ||||
|     // Map
 | ||||
|     mapChunkSize: 32, | ||||
|     mapChunkPrerenderMinZoom: 1.5, | ||||
|     mapChunkPrerenderMinZoom: 1.3, | ||||
|     mapChunkOverviewMinZoom: 0.7, | ||||
| 
 | ||||
|     // Belt speeds
 | ||||
|  | ||||
							
								
								
									
										430
									
								
								src/js/core/modal_dialog_elements.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										430
									
								
								src/js/core/modal_dialog_elements.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,430 @@ | ||||
| /* typehints:start */ | ||||
| import { Application } from "../application"; | ||||
| /* typehints:end */ | ||||
| 
 | ||||
| import { Signal, STOP_PROPAGATION } from "./signal"; | ||||
| import { arrayDeleteValue, waitNextFrame } from "./utils"; | ||||
| import { ClickDetector } from "./click_detector"; | ||||
| import { SOUNDS } from "../platform/sound"; | ||||
| import { InputReceiver } from "./input_receiver"; | ||||
| import { FormElement } from "./modal_dialog_forms"; | ||||
| import { globalConfig } from "./config"; | ||||
| import { getStringForKeyCode } from "../game/key_action_mapper"; | ||||
| import { createLogger } from "./logging"; | ||||
| 
 | ||||
| const kbEnter = 13; | ||||
| const kbCancel = 27; | ||||
| 
 | ||||
| const logger = createLogger("dialogs"); | ||||
| 
 | ||||
| /** | ||||
|  * Basic text based dialog | ||||
|  */ | ||||
| export class Dialog { | ||||
|     /** | ||||
|      * | ||||
|      * Constructs a new dialog with the given options | ||||
|      * @param {object} param0 | ||||
|      * @param {Application} param0.app | ||||
|      * @param {string} param0.title Title of the dialog | ||||
|      * @param {string} param0.contentHTML Inner dialog html | ||||
|      * @param {Array<string>} param0.buttons | ||||
|      *  Button list, each button contains of up to 3 parts seperated by ':'. | ||||
|      *  Part 0: The id, one of the one defined in dialog_buttons.yaml | ||||
|      *  Part 1: The style, either good, bad or misc | ||||
|      *  Part 2 (optional): Additional parameters seperated by '/', available are: | ||||
|      *    timeout: This button is only available after some waiting time | ||||
|      *    kb_enter: This button is triggered by the enter key | ||||
|      *    kb_escape This button is triggered by the escape key | ||||
|      * @param {string=} param0.type The dialog type, either "info" or "warn" | ||||
|      * @param {boolean=} param0.closeButton Whether this dialog has a close button | ||||
|      */ | ||||
|     constructor({ app, title, contentHTML, buttons, type = "info", closeButton = false }) { | ||||
|         this.app = app; | ||||
|         this.title = title; | ||||
|         this.contentHTML = contentHTML; | ||||
|         this.type = type; | ||||
|         this.buttonIds = buttons; | ||||
|         this.closeButton = closeButton; | ||||
| 
 | ||||
|         this.closeRequested = new Signal(); | ||||
|         this.buttonSignals = {}; | ||||
| 
 | ||||
|         for (let i = 0; i < buttons.length; ++i) { | ||||
|             if (G_IS_DEV && globalConfig.debug.disableTimedButtons) { | ||||
|                 this.buttonIds[i] = this.buttonIds[i].replace(":timeout", ""); | ||||
|             } | ||||
| 
 | ||||
|             const buttonId = this.buttonIds[i].split(":")[0]; | ||||
|             this.buttonSignals[buttonId] = new Signal(); | ||||
|         } | ||||
| 
 | ||||
|         this.timeouts = []; | ||||
|         this.clickDetectors = []; | ||||
| 
 | ||||
|         this.inputReciever = new InputReceiver("dialog-" + this.title); | ||||
| 
 | ||||
|         this.inputReciever.keydown.add(this.handleKeydown, this); | ||||
| 
 | ||||
|         this.enterHandler = null; | ||||
|         this.escapeHandler = null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Internal keydown handler | ||||
|      * @param {object} param0 | ||||
|      * @param {number} param0.keyCode | ||||
|      * @param {boolean} param0.shift | ||||
|      * @param {boolean} param0.alt | ||||
|      */ | ||||
|     handleKeydown({ keyCode, shift, alt }) { | ||||
|         if (keyCode === kbEnter && this.enterHandler) { | ||||
|             this.internalButtonHandler(this.enterHandler); | ||||
|             return STOP_PROPAGATION; | ||||
|         } | ||||
| 
 | ||||
|         if (keyCode === kbCancel && this.escapeHandler) { | ||||
|             this.internalButtonHandler(this.escapeHandler); | ||||
|             return STOP_PROPAGATION; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     internalButtonHandler(id, ...payload) { | ||||
|         this.app.inputMgr.popReciever(this.inputReciever); | ||||
| 
 | ||||
|         if (id !== "close-button") { | ||||
|             this.buttonSignals[id].dispatch(...payload); | ||||
|         } | ||||
|         this.closeRequested.dispatch(); | ||||
|     } | ||||
| 
 | ||||
|     createElement() { | ||||
|         const elem = document.createElement("div"); | ||||
|         elem.classList.add("ingameDialog"); | ||||
| 
 | ||||
|         this.dialogElem = document.createElement("div"); | ||||
|         this.dialogElem.classList.add("dialogInner"); | ||||
| 
 | ||||
|         if (this.type) { | ||||
|             this.dialogElem.classList.add(this.type); | ||||
|         } | ||||
|         elem.appendChild(this.dialogElem); | ||||
| 
 | ||||
|         const title = document.createElement("h1"); | ||||
|         title.innerText = this.title; | ||||
|         title.classList.add("title"); | ||||
|         this.dialogElem.appendChild(title); | ||||
| 
 | ||||
|         if (this.closeButton) { | ||||
|             this.dialogElem.classList.add("hasCloseButton"); | ||||
| 
 | ||||
|             const closeBtn = document.createElement("button"); | ||||
|             closeBtn.classList.add("closeButton"); | ||||
| 
 | ||||
|             this.trackClicks(closeBtn, () => this.internalButtonHandler("close-button"), { | ||||
|                 applyCssClass: "pressedSmallElement", | ||||
|             }); | ||||
| 
 | ||||
|             title.appendChild(closeBtn); | ||||
|             this.inputReciever.backButton.add(() => this.internalButtonHandler("close-button")); | ||||
|         } | ||||
| 
 | ||||
|         const content = document.createElement("div"); | ||||
|         content.classList.add("content"); | ||||
|         content.innerHTML = this.contentHTML; | ||||
|         this.dialogElem.appendChild(content); | ||||
| 
 | ||||
|         if (this.buttonIds.length > 0) { | ||||
|             const buttons = document.createElement("div"); | ||||
|             buttons.classList.add("buttons"); | ||||
| 
 | ||||
|             // Create buttons
 | ||||
|             for (let i = 0; i < this.buttonIds.length; ++i) { | ||||
|                 const [buttonId, buttonStyle, rawParams] = this.buttonIds[i].split(":"); | ||||
| 
 | ||||
|                 const button = document.createElement("button"); | ||||
|                 button.classList.add("button"); | ||||
|                 button.classList.add("styledButton"); | ||||
|                 button.classList.add(buttonStyle); | ||||
|                 // button.innerText = T.dialog_buttons[buttonId];
 | ||||
|                 button.innerText = buttonId; | ||||
| 
 | ||||
|                 const params = (rawParams || "").split("/"); | ||||
|                 const useTimeout = params.indexOf("timeout") >= 0; | ||||
| 
 | ||||
|                 const isEnter = params.indexOf("enter") >= 0; | ||||
|                 const isEscape = params.indexOf("escape") >= 0; | ||||
| 
 | ||||
|                 if (isEscape && this.closeButton) { | ||||
|                     logger.warn("Showing dialog with close button, and additional cancel button"); | ||||
|                 } | ||||
| 
 | ||||
|                 if (useTimeout) { | ||||
|                     button.classList.add("timedButton"); | ||||
|                     const timeout = setTimeout(() => { | ||||
|                         button.classList.remove("timedButton"); | ||||
|                         arrayDeleteValue(this.timeouts, timeout); | ||||
|                     }, 5000); | ||||
|                     this.timeouts.push(timeout); | ||||
|                 } | ||||
|                 if (isEnter || isEscape) { | ||||
|                     // if (this.app.settings.getShowKeyboardShortcuts()) {
 | ||||
|                     // Show keybinding
 | ||||
|                     const spacer = document.createElement("code"); | ||||
|                     spacer.classList.add("keybinding"); | ||||
|                     spacer.innerHTML = getStringForKeyCode(isEnter ? kbEnter : kbCancel); | ||||
|                     button.appendChild(spacer); | ||||
|                     // }
 | ||||
| 
 | ||||
|                     if (isEnter) { | ||||
|                         this.enterHandler = buttonId; | ||||
|                     } | ||||
|                     if (isEscape) { | ||||
|                         this.escapeHandler = buttonId; | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 this.trackClicks(button, () => this.internalButtonHandler(buttonId)); | ||||
|                 buttons.appendChild(button); | ||||
|             } | ||||
| 
 | ||||
|             this.dialogElem.appendChild(buttons); | ||||
|         } else { | ||||
|             this.dialogElem.classList.add("buttonless"); | ||||
|         } | ||||
| 
 | ||||
|         this.element = elem; | ||||
|         this.app.inputMgr.pushReciever(this.inputReciever); | ||||
| 
 | ||||
|         return this.element; | ||||
|     } | ||||
| 
 | ||||
|     setIndex(index) { | ||||
|         this.element.style.zIndex = index; | ||||
|     } | ||||
| 
 | ||||
|     destroy() { | ||||
|         if (!this.element) { | ||||
|             assert(false, "Tried to destroy dialog twice"); | ||||
|             return; | ||||
|         } | ||||
|         // We need to do this here, because if the backbutton event gets
 | ||||
|         // dispatched to the modal dialogs, it will not call the internalButtonHandler,
 | ||||
|         // and thus our receiver stays attached the whole time
 | ||||
|         this.app.inputMgr.destroyReceiver(this.inputReciever); | ||||
| 
 | ||||
|         for (let i = 0; i < this.clickDetectors.length; ++i) { | ||||
|             this.clickDetectors[i].cleanup(); | ||||
|         } | ||||
|         this.clickDetectors = []; | ||||
| 
 | ||||
|         this.element.remove(); | ||||
|         this.element = null; | ||||
| 
 | ||||
|         for (let i = 0; i < this.timeouts.length; ++i) { | ||||
|             clearTimeout(this.timeouts[i]); | ||||
|         } | ||||
|         this.timeouts = []; | ||||
|     } | ||||
| 
 | ||||
|     hide() { | ||||
|         this.element.classList.remove("visible"); | ||||
|     } | ||||
| 
 | ||||
|     show() { | ||||
|         this.element.classList.add("visible"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Helper method to track clicks on an element | ||||
|      * @param {Element} elem | ||||
|      * @param {function():void} handler | ||||
|      * @param {import("./click_detector").ClickDetectorConstructorArgs=} args | ||||
|      * @returns {ClickDetector} | ||||
|      */ | ||||
|     trackClicks(elem, handler, args = {}) { | ||||
|         const detector = new ClickDetector(elem, args); | ||||
|         detector.click.add(handler, this); | ||||
|         this.clickDetectors.push(detector); | ||||
|         return detector; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Dialog which simply shows a loading spinner | ||||
|  */ | ||||
| export class DialogLoading extends Dialog { | ||||
|     constructor(app) { | ||||
|         super({ | ||||
|             app, | ||||
|             title: "", | ||||
|             contentHTML: "", | ||||
|             buttons: [], | ||||
|             type: "loading", | ||||
|         }); | ||||
| 
 | ||||
|         // Loading dialog can not get closed with back button
 | ||||
|         this.inputReciever.backButton.removeAll(); | ||||
|         this.inputReciever.context = "dialog-loading"; | ||||
|     } | ||||
| 
 | ||||
|     createElement() { | ||||
|         const elem = document.createElement("div"); | ||||
|         elem.classList.add("ingameDialog"); | ||||
|         elem.classList.add("loadingDialog"); | ||||
|         this.element = elem; | ||||
| 
 | ||||
|         const loader = document.createElement("div"); | ||||
|         loader.classList.add("prefab_LoadingTextWithAnim"); | ||||
|         loader.classList.add("loadingIndicator"); | ||||
|         loader.innerText = "Loading"; | ||||
|         elem.appendChild(loader); | ||||
| 
 | ||||
|         this.app.inputMgr.pushReciever(this.inputReciever); | ||||
| 
 | ||||
|         return elem; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class DialogOptionChooser extends Dialog { | ||||
|     constructor({ app, title, options }) { | ||||
|         let html = "<div class='optionParent'>"; | ||||
| 
 | ||||
|         options.options.forEach(({ value, text, desc = null, iconPrefix = null }) => { | ||||
|             const descHtml = desc ? `<span class="desc">${desc}</span>` : ""; | ||||
|             let iconHtml = iconPrefix ? `<span class="icon icon-${iconPrefix}-${value}"></span>` : ""; | ||||
|             html += ` | ||||
|                 <div class='option ${value === options.active ? "active" : ""} ${ | ||||
|                 iconPrefix ? "hasIcon" : "" | ||||
|             }' data-optionvalue='${value}'> | ||||
|                     ${iconHtml}     | ||||
|                     <span class='title'>${text}</span> | ||||
|                     ${descHtml} | ||||
|                 </div> | ||||
|                 `;
 | ||||
|         }); | ||||
| 
 | ||||
|         html += "</div>"; | ||||
|         super({ | ||||
|             app, | ||||
|             title, | ||||
|             contentHTML: html, | ||||
|             buttons: [], | ||||
|             type: "info", | ||||
|             closeButton: true, | ||||
|         }); | ||||
| 
 | ||||
|         this.options = options; | ||||
|         this.initialOption = options.active; | ||||
| 
 | ||||
|         this.buttonSignals.optionSelected = new Signal(); | ||||
|     } | ||||
| 
 | ||||
|     createElement() { | ||||
|         const div = super.createElement(); | ||||
|         this.dialogElem.classList.add("optionChooserDialog"); | ||||
| 
 | ||||
|         div.querySelectorAll("[data-optionvalue]").forEach(handle => { | ||||
|             const value = handle.getAttribute("data-optionvalue"); | ||||
|             if (!handle) { | ||||
|                 logger.error("Failed to bind option value in dialog:", value); | ||||
|                 return; | ||||
|             } | ||||
|             // Need click detector here to forward elements, otherwise scrolling does not work
 | ||||
|             const detector = new ClickDetector(handle, { | ||||
|                 consumeEvents: false, | ||||
|                 preventDefault: false, | ||||
|                 clickSound: null, | ||||
|                 applyCssClass: "pressedOption", | ||||
|                 targetOnly: true, | ||||
|             }); | ||||
|             this.clickDetectors.push(detector); | ||||
| 
 | ||||
|             if (value !== this.initialOption) { | ||||
|                 detector.click.add(() => { | ||||
|                     const selected = div.querySelector(".option.active"); | ||||
|                     if (selected) { | ||||
|                         selected.classList.remove("active"); | ||||
|                     } else { | ||||
|                         logger.warn("No selected option"); | ||||
|                     } | ||||
|                     handle.classList.add("active"); | ||||
|                     this.app.sound.playUiSound(SOUNDS.uiClick); | ||||
|                     this.internalButtonHandler("optionSelected", value); | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|         return div; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class DialogWithForm extends Dialog { | ||||
|     /** | ||||
|      * | ||||
|      * @param {object} param0 | ||||
|      * @param {Application} param0.app | ||||
|      * @param {string} param0.title | ||||
|      * @param {string} param0.desc | ||||
|      * @param {string=} param0.confirmButton | ||||
|      * @param {Array<FormElement>} param0.formElements | ||||
|      */ | ||||
|     constructor({ app, title, desc, formElements, confirmButton = "ok:good" }) { | ||||
|         let html = ""; | ||||
|         html += desc + "<br>"; | ||||
|         for (let i = 0; i < formElements.length; ++i) { | ||||
|             html += formElements[i].getHtml(); | ||||
|         } | ||||
| 
 | ||||
|         super({ | ||||
|             app, | ||||
|             title: title, | ||||
|             contentHTML: html, | ||||
|             buttons: ["cancel:bad", confirmButton], | ||||
|             type: "info", | ||||
|             closeButton: true, | ||||
|         }); | ||||
|         this.confirmButtonId = confirmButton.split(":")[0]; | ||||
|         this.formElements = formElements; | ||||
|     } | ||||
| 
 | ||||
|     internalButtonHandler(id, ...payload) { | ||||
|         if (id === this.confirmButtonId) { | ||||
|             if (this.hasAnyInvalid()) { | ||||
|                 this.dialogElem.classList.remove("errorShake"); | ||||
|                 waitNextFrame().then(() => { | ||||
|                     if (this.dialogElem) { | ||||
|                         this.dialogElem.classList.add("errorShake"); | ||||
|                     } | ||||
|                 }); | ||||
|                 this.app.sound.playUiSound(SOUNDS.uiError); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         super.internalButtonHandler(id, payload); | ||||
|     } | ||||
| 
 | ||||
|     hasAnyInvalid() { | ||||
|         for (let i = 0; i < this.formElements.length; ++i) { | ||||
|             if (!this.formElements[i].isValid()) { | ||||
|                 return true; | ||||
|             } | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     createElement() { | ||||
|         const div = super.createElement(); | ||||
| 
 | ||||
|         for (let i = 0; i < this.formElements.length; ++i) { | ||||
|             const elem = this.formElements[i]; | ||||
|             elem.bindEvents(div, this.clickDetectors); | ||||
|         } | ||||
| 
 | ||||
|         waitNextFrame().then(() => { | ||||
|             this.formElements[0].focus(); | ||||
|         }); | ||||
| 
 | ||||
|         return div; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										150
									
								
								src/js/core/modal_dialog_forms.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								src/js/core/modal_dialog_forms.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,150 @@ | ||||
| import { ClickDetector } from "./click_detector"; | ||||
| 
 | ||||
| export class FormElement { | ||||
|     constructor(id, label) { | ||||
|         this.id = id; | ||||
|         this.label = label; | ||||
|     } | ||||
| 
 | ||||
|     getHtml() { | ||||
|         abstract; | ||||
|         return ""; | ||||
|     } | ||||
| 
 | ||||
|     getFormElement(parent) { | ||||
|         return parent.querySelector("[data-formId='" + this.id + "']"); | ||||
|     } | ||||
| 
 | ||||
|     bindEvents(parent, clickTrackers) { | ||||
|         abstract; | ||||
|     } | ||||
| 
 | ||||
|     focus(parent) {} | ||||
| 
 | ||||
|     isValid() { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** @returns {any} */ | ||||
|     getValue() { | ||||
|         abstract; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class FormElementInput extends FormElement { | ||||
|     constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) { | ||||
|         super(id, label); | ||||
|         this.placeholder = placeholder; | ||||
|         this.defaultValue = defaultValue; | ||||
|         this.inputType = inputType; | ||||
|         this.validator = validator; | ||||
| 
 | ||||
|         this.element = null; | ||||
|     } | ||||
| 
 | ||||
|     getHtml() { | ||||
|         let classes = []; | ||||
|         let inputType = "text"; | ||||
|         let maxlength = 256; | ||||
|         switch (this.inputType) { | ||||
|             case "text": { | ||||
|                 classes.push("input-text"); | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             case "email": { | ||||
|                 classes.push("input-email"); | ||||
|                 inputType = "email"; | ||||
|                 break; | ||||
|             } | ||||
| 
 | ||||
|             case "token": { | ||||
|                 classes.push("input-token"); | ||||
|                 inputType = "text"; | ||||
|                 maxlength = 4; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return ` | ||||
|             <div class="formElement input"> | ||||
|                 ${this.label ? `<label>${this.label}</label>` : ""} | ||||
|                 <input | ||||
|                     type="${inputType}" | ||||
|                     value="${this.defaultValue.replace(/["\\]+/gi, "")}" | ||||
|                     maxlength="${maxlength}" | ||||
|                     autocomplete="off" | ||||
|                     autocorrect="off" | ||||
|                     autocapitalize="off" | ||||
|                     spellcheck="false" | ||||
|                     class="${classes.join(" ")}" | ||||
|                     placeholder="${this.placeholder.replace(/["\\]+/gi, "")}" | ||||
|                     data-formId="${this.id}"> | ||||
|             </div> | ||||
|         `;
 | ||||
|     } | ||||
| 
 | ||||
|     bindEvents(parent, clickTrackers) { | ||||
|         this.element = this.getFormElement(parent); | ||||
|         this.element.addEventListener("input", event => this.updateErrorState()); | ||||
|         this.updateErrorState(); | ||||
|     } | ||||
| 
 | ||||
|     updateErrorState() { | ||||
|         this.element.classList.toggle("errored", !this.isValid()); | ||||
|     } | ||||
| 
 | ||||
|     isValid() { | ||||
|         return !this.validator || this.validator(this.element.value); | ||||
|     } | ||||
| 
 | ||||
|     getValue() { | ||||
|         return this.element.value; | ||||
|     } | ||||
| 
 | ||||
|     focus() { | ||||
|         this.element.focus(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class FormElementCheckbox extends FormElement { | ||||
|     constructor({ id, label, defaultValue = true }) { | ||||
|         super(id, label); | ||||
|         this.defaultValue = defaultValue; | ||||
|         this.value = this.defaultValue; | ||||
| 
 | ||||
|         this.element = null; | ||||
|     } | ||||
| 
 | ||||
|     getHtml() { | ||||
|         return ` | ||||
|             <div class="formElement checkBoxFormElem"> | ||||
|             ${this.label ? `<label>${this.label}</label>` : ""} | ||||
|                 <div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'> | ||||
|                     <span class="knob"></span > | ||||
|                 </div > | ||||
|             </div> | ||||
|         `;
 | ||||
|     } | ||||
| 
 | ||||
|     bindEvents(parent, clickTrackers) { | ||||
|         this.element = this.getFormElement(parent); | ||||
|         const detector = new ClickDetector(this.element, { | ||||
|             consumeEvents: false, | ||||
|             preventDefault: false, | ||||
|         }); | ||||
|         clickTrackers.push(detector); | ||||
|         detector.click.add(this.toggle, this); | ||||
|     } | ||||
| 
 | ||||
|     getValue() { | ||||
|         return this.value; | ||||
|     } | ||||
| 
 | ||||
|     toggle() { | ||||
|         this.value = !this.value; | ||||
|         this.element.classList.toggle("checked", this.value); | ||||
|     } | ||||
| 
 | ||||
|     focus(parent) {} | ||||
| } | ||||
| @ -89,6 +89,35 @@ export class ReadWriteProxy { | ||||
|         return compressionPrefix + compressX64(checksum + jsonString); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * | ||||
|      * @param {object} text | ||||
|      */ | ||||
|     static deserializeObject(text) { | ||||
|         const decompressed = decompressX64(text.substr(compressionPrefix.length)); | ||||
|         if (!decompressed) { | ||||
|             // LZ string decompression failure
 | ||||
|             throw new Error("bad-content / decompression-failed"); | ||||
|         } | ||||
|         if (decompressed.length < 40) { | ||||
|             // String too short
 | ||||
|             throw new Error("bad-content / payload-too-small"); | ||||
|         } | ||||
| 
 | ||||
|         // Compare stored checksum with actual checksum
 | ||||
|         const checksum = decompressed.substring(0, 40); | ||||
|         const jsonString = decompressed.substr(40); | ||||
|         const desiredChecksum = sha1(jsonString + salt); | ||||
|         if (desiredChecksum !== checksum) { | ||||
|             // Checksum mismatch
 | ||||
|             throw new Error("bad-content / checksum-mismatch"); | ||||
|         } | ||||
| 
 | ||||
|         const parsed = JSON.parse(jsonString); | ||||
|         const decoded = decompressObject(parsed); | ||||
|         return decoded; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Writes the data asychronously, fails if verify() fails | ||||
|      * @returns {Promise<string>} | ||||
|  | ||||
| @ -668,9 +668,11 @@ export function makeButton(parent, classes = [], innerHTML = "") { | ||||
|  * @param {Element} elem | ||||
|  */ | ||||
| export function removeAllChildren(elem) { | ||||
|     var range = document.createRange(); | ||||
|     range.selectNodeContents(elem); | ||||
|     range.deleteContents(); | ||||
|     if (elem) { | ||||
|         var range = document.createRange(); | ||||
|         range.selectNodeContents(elem); | ||||
|         range.deleteContents(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export function smartFadeNumber(current, newOne, minFade = 0.01, maxFade = 0.9) { | ||||
|  | ||||
| @ -57,7 +57,6 @@ export class UndergroundBeltComponent extends Component { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         console.log("Takes", 1 / beltSpeed); | ||||
|         this.pendingItems.push([item, 1 / beltSpeed]); | ||||
|         return true; | ||||
|     } | ||||
| @ -85,7 +84,6 @@ export class UndergroundBeltComponent extends Component { | ||||
|         // This corresponds to the item ejector - it needs 0.5 additional tiles to eject the item.
 | ||||
|         // So instead of adding 1 we add 0.5 only.
 | ||||
|         const travelDuration = (travelDistance + 0.5) / beltSpeed; | ||||
|         console.log(travelDistance, "->", travelDuration); | ||||
| 
 | ||||
|         this.pendingItems.push([item, travelDuration]); | ||||
| 
 | ||||
|  | ||||
| @ -20,6 +20,7 @@ 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"; | ||||
| 
 | ||||
| export class GameHUD { | ||||
|     /** | ||||
| @ -53,6 +54,7 @@ export class GameHUD { | ||||
|             pinnedShapes: new HUDPinnedShapes(this.root), | ||||
| 
 | ||||
|             notifications: new HUDNotifications(this.root), | ||||
|             settingsMenu: new HUDSettingsMenu(this.root), | ||||
| 
 | ||||
|             // betaOverlay: new HUDBetaOverlay(this.root),
 | ||||
|         }; | ||||
|  | ||||
| @ -71,6 +71,7 @@ export class HUDGameMenu extends BaseHUDPart { | ||||
|         this.trackClicks(this.musicButton, this.toggleMusic); | ||||
|         this.trackClicks(this.sfxButton, this.toggleSfx); | ||||
|         this.trackClicks(this.saveButton, this.startSave); | ||||
|         this.trackClicks(this.settingsButton, this.openSettings); | ||||
| 
 | ||||
|         this.musicButton.classList.toggle("muted", this.root.app.settings.getAllSettings().musicMuted); | ||||
|         this.sfxButton.classList.toggle("muted", this.root.app.settings.getAllSettings().soundsMuted); | ||||
| @ -117,6 +118,10 @@ export class HUDGameMenu extends BaseHUDPart { | ||||
|         this.root.gameState.doSave(); | ||||
|     } | ||||
| 
 | ||||
|     openSettings() { | ||||
|         this.root.hud.parts.settingsMenu.show(); | ||||
|     } | ||||
| 
 | ||||
|     toggleMusic() { | ||||
|         const newValue = !this.root.app.settings.getAllSettings().musicMuted; | ||||
|         this.root.app.settings.updateSetting("musicMuted", newValue); | ||||
|  | ||||
| @ -61,7 +61,7 @@ export class HUDMassSelector extends BaseHUDPart { | ||||
|      */ | ||||
|     onBack() { | ||||
|         // Clear entities on escape
 | ||||
|         if (this.entityUidsMarkedForDeletion) { | ||||
|         if (this.entityUidsMarkedForDeletion.size > 0) { | ||||
|             this.entityUidsMarkedForDeletion = new Set(); | ||||
|             return STOP_PROPAGATION; | ||||
|         } | ||||
|  | ||||
							
								
								
									
										188
									
								
								src/js/game/hud/parts/modal_dialogs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								src/js/game/hud/parts/modal_dialogs.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,188 @@ | ||||
| /* typehints:start */ | ||||
| import { Application } from "../../../application"; | ||||
| /* typehints:end */ | ||||
| 
 | ||||
| import { SOUNDS } from "../../../platform/sound"; | ||||
| import { DynamicDomAttach } from "../dynamic_dom_attach"; | ||||
| import { BaseHUDPart } from "../base_hud_part"; | ||||
| import { | ||||
|     Dialog, | ||||
|     DialogLoading, | ||||
|     DialogVideoTutorial, | ||||
|     DialogOptionChooser, | ||||
| } from "../../../core/modal_dialog_elements"; | ||||
| import { makeDiv } from "../../../core/utils"; | ||||
| 
 | ||||
| export class HUDModalDialogs extends BaseHUDPart { | ||||
|     constructor(root, app) { | ||||
|         // Important: Root is not always available here! Its also used in the main menu
 | ||||
|         super(root); | ||||
| 
 | ||||
|         /** @type {Application} */ | ||||
|         this.app = app; | ||||
| 
 | ||||
|         this.dialogParent = null; | ||||
|         this.dialogStack = []; | ||||
|     } | ||||
| 
 | ||||
|     // For use inside of the game, implementation of base hud part
 | ||||
|     initialize() { | ||||
|         this.dialogParent = document.getElementById("rg_HUD_ModalDialogs"); | ||||
|         this.domWatcher = new DynamicDomAttach(this.root, this.dialogParent); | ||||
|     } | ||||
| 
 | ||||
|     shouldPauseRendering() { | ||||
|         return this.dialogStack.length > 0; | ||||
|     } | ||||
| 
 | ||||
|     shouldPauseGame() { | ||||
|         return this.shouldPauseRendering(); | ||||
|     } | ||||
| 
 | ||||
|     createElements(parent) { | ||||
|         return makeDiv(parent, "rg_HUD_ModalDialogs"); | ||||
|     } | ||||
| 
 | ||||
|     // For use outside of the game
 | ||||
|     initializeToElement(element) { | ||||
|         assert(element, "No element for dialogs given"); | ||||
|         this.dialogParent = element; | ||||
|     } | ||||
| 
 | ||||
|     // Methods
 | ||||
| 
 | ||||
|     showInfo(title, text, buttons = ["ok:good"]) { | ||||
|         const dialog = new Dialog({ | ||||
|             app: this.app, | ||||
|             title: title, | ||||
|             contentHTML: text, | ||||
|             buttons: buttons, | ||||
|             type: "info", | ||||
|         }); | ||||
|         this.internalShowDialog(dialog); | ||||
| 
 | ||||
|         if (this.app) { | ||||
|             this.app.sound.playUiSound(SOUNDS.dialogOk); | ||||
|         } | ||||
| 
 | ||||
|         return dialog.buttonSignals; | ||||
|     } | ||||
| 
 | ||||
|     showWarning(title, text, buttons = ["ok:good"]) { | ||||
|         const dialog = new Dialog({ | ||||
|             app: this.app, | ||||
|             title: title, | ||||
|             contentHTML: text, | ||||
|             buttons: buttons, | ||||
|             type: "warning", | ||||
|         }); | ||||
|         this.internalShowDialog(dialog); | ||||
| 
 | ||||
|         if (this.app) { | ||||
|             this.app.sound.playUiSound(SOUNDS.dialogError); | ||||
|         } | ||||
| 
 | ||||
|         return dialog.buttonSignals; | ||||
|     } | ||||
| 
 | ||||
|     showVideoTutorial(title, text, videoUrl) { | ||||
|         const dialog = new DialogVideoTutorial({ | ||||
|             app: this.app, | ||||
|             title: title, | ||||
|             contentHTML: text, | ||||
|             videoUrl, | ||||
|         }); | ||||
|         this.internalShowDialog(dialog); | ||||
| 
 | ||||
|         if (this.app) { | ||||
|             this.app.sound.playUiSound(SOUNDS.dialogOk); | ||||
|         } | ||||
| 
 | ||||
|         return dialog.buttonSignals; | ||||
|     } | ||||
| 
 | ||||
|     showOptionChooser(title, options) { | ||||
|         const dialog = new DialogOptionChooser({ | ||||
|             app: this.app, | ||||
|             title, | ||||
|             options, | ||||
|         }); | ||||
|         this.internalShowDialog(dialog); | ||||
|         return dialog.buttonSignals; | ||||
|     } | ||||
| 
 | ||||
|     // Returns method to be called when laoding finishd
 | ||||
|     showLoadingDialog() { | ||||
|         const dialog = new DialogLoading(this.app); | ||||
|         this.internalShowDialog(dialog); | ||||
|         return this.closeDialog.bind(this, dialog); | ||||
|     } | ||||
| 
 | ||||
|     internalShowDialog(dialog) { | ||||
|         const elem = dialog.createElement(); | ||||
|         dialog.setIndex(this.dialogStack.length); | ||||
| 
 | ||||
|         // Hide last dialog in queue
 | ||||
|         if (this.dialogStack.length > 0) { | ||||
|             this.dialogStack[this.dialogStack.length - 1].hide(); | ||||
|         } | ||||
| 
 | ||||
|         this.dialogStack.push(dialog); | ||||
| 
 | ||||
|         // Append dialog
 | ||||
|         dialog.show(); | ||||
|         dialog.closeRequested.add(this.closeDialog.bind(this, dialog)); | ||||
| 
 | ||||
|         // Append to HTML
 | ||||
|         this.dialogParent.appendChild(elem); | ||||
| 
 | ||||
|         document.body.classList.toggle("modalDialogActive", this.dialogStack.length > 0); | ||||
| 
 | ||||
|         // IMPORTANT: Attach element directly, otherwise double submit is possible
 | ||||
|         this.update(); | ||||
|     } | ||||
| 
 | ||||
|     update() { | ||||
|         if (this.domWatcher) { | ||||
|             this.domWatcher.update(this.dialogStack.length > 0); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     closeDialog(dialog) { | ||||
|         dialog.destroy(); | ||||
| 
 | ||||
|         let index = -1; | ||||
|         for (let i = 0; i < this.dialogStack.length; ++i) { | ||||
|             if (this.dialogStack[i] === dialog) { | ||||
|                 index = i; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         assert(index >= 0, "Dialog not in dialog stack"); | ||||
|         this.dialogStack.splice(index, 1); | ||||
| 
 | ||||
|         if (this.dialogStack.length > 0) { | ||||
|             // Show the dialog which was previously open
 | ||||
|             this.dialogStack[this.dialogStack.length - 1].show(); | ||||
|         } | ||||
| 
 | ||||
|         document.body.classList.toggle("modalDialogActive", this.dialogStack.length > 0); | ||||
|     } | ||||
| 
 | ||||
|     close() { | ||||
|         for (let i = 0; i < this.dialogStack.length; ++i) { | ||||
|             const dialog = this.dialogStack[i]; | ||||
|             dialog.destroy(); | ||||
|         } | ||||
|         this.dialogStack = []; | ||||
|     } | ||||
| 
 | ||||
|     cleanup() { | ||||
|         super.cleanup(); | ||||
|         for (let i = 0; i < this.dialogStack.length; ++i) { | ||||
|             this.dialogStack[i].destroy(); | ||||
|         } | ||||
|         this.dialogStack = []; | ||||
|         this.dialogParent = null; | ||||
|     } | ||||
| } | ||||
| @ -8,6 +8,8 @@ export const enumNotificationType = { | ||||
|     success: "success", | ||||
| }; | ||||
| 
 | ||||
| const notificationDuration = 3; | ||||
| 
 | ||||
| export class HUDNotifications extends BaseHUDPart { | ||||
|     createElements(parent) { | ||||
|         this.element = makeDiv(parent, "ingame_HUD_Notifications", [], ``); | ||||
| @ -35,7 +37,7 @@ export class HUDNotifications extends BaseHUDPart { | ||||
| 
 | ||||
|         this.notificationElements.push({ | ||||
|             element, | ||||
|             expireAt: this.root.time.realtimeNow() + 5, | ||||
|             expireAt: this.root.time.realtimeNow() + notificationDuration, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										97
									
								
								src/js/game/hud/parts/settings_menu.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/js/game/hud/parts/settings_menu.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,97 @@ | ||||
| import { BaseHUDPart } from "../base_hud_part"; | ||||
| import { makeDiv } from "../../../core/utils"; | ||||
| import { DynamicDomAttach } from "../dynamic_dom_attach"; | ||||
| import { InputReceiver } from "../../../core/input_receiver"; | ||||
| import { KeyActionMapper } from "../../key_action_mapper"; | ||||
| 
 | ||||
| export class HUDSettingsMenu extends BaseHUDPart { | ||||
|     createElements(parent) { | ||||
|         this.background = makeDiv(parent, "ingame_HUD_SettingsMenu", ["ingameDialog"]); | ||||
| 
 | ||||
|         this.menuElement = makeDiv(this.background, null, ["menuElement"]); | ||||
| 
 | ||||
|         this.timePlayed = makeDiv( | ||||
|             this.background, | ||||
|             null, | ||||
|             ["timePlayed"], | ||||
|             `<strong>Playtime</strong><span class="playtime"></span>` | ||||
|         ); | ||||
| 
 | ||||
|         this.buttonContainer = makeDiv(this.menuElement, null, ["buttons"]); | ||||
| 
 | ||||
|         const buttons = [ | ||||
|             { | ||||
|                 title: "Continue", | ||||
|                 action: () => this.close(), | ||||
|             }, | ||||
|             { | ||||
|                 title: "Return to menu", | ||||
|                 action: () => this.returnToMenu(), | ||||
|             }, | ||||
|         ]; | ||||
| 
 | ||||
|         for (let i = 0; i < buttons.length; ++i) { | ||||
|             const { title, action } = buttons[i]; | ||||
| 
 | ||||
|             const element = document.createElement("button"); | ||||
|             element.classList.add("styledButton"); | ||||
|             element.innerText = title; | ||||
|             this.buttonContainer.appendChild(element); | ||||
| 
 | ||||
|             this.trackClicks(element, action); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     returnToMenu() { | ||||
|         this.root.gameState.goBackToMenu(); | ||||
|     } | ||||
| 
 | ||||
|     shouldPauseGame() { | ||||
|         return this.visible; | ||||
|     } | ||||
| 
 | ||||
|     shouldPauseRendering() { | ||||
|         return this.visible; | ||||
|     } | ||||
| 
 | ||||
|     initialize() { | ||||
|         this.root.gameState.keyActionMapper.getBinding("back").add(this.show, this); | ||||
| 
 | ||||
|         this.domAttach = new DynamicDomAttach(this.root, this.background, { | ||||
|             attachClass: "visible", | ||||
|         }); | ||||
| 
 | ||||
|         this.inputReciever = new InputReceiver("settingsmenu"); | ||||
|         this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); | ||||
| 
 | ||||
|         this.keyActionMapper.getBinding("back").add(this.close, this); | ||||
| 
 | ||||
|         this.close(); | ||||
|     } | ||||
| 
 | ||||
|     cleanup() { | ||||
|         document.body.classList.remove("ingameDialogOpen"); | ||||
|     } | ||||
| 
 | ||||
|     show() { | ||||
|         this.visible = true; | ||||
|         document.body.classList.add("ingameDialogOpen"); | ||||
|         // this.background.classList.add("visible");
 | ||||
|         this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); | ||||
| 
 | ||||
|         const totalMinutesPlayed = Math.ceil(this.root.time.now() / 60.0); | ||||
|         const playtimeString = totalMinutesPlayed === 1 ? "1 minute" : totalMinutesPlayed + " minutes"; | ||||
|         this.timePlayed.querySelector(".playtime").innerText = playtimeString; | ||||
|     } | ||||
| 
 | ||||
|     close() { | ||||
|         this.visible = false; | ||||
|         document.body.classList.remove("ingameDialogOpen"); | ||||
|         this.root.app.inputMgr.makeSureDetached(this.inputReciever); | ||||
|         this.update(); | ||||
|     } | ||||
| 
 | ||||
|     update() { | ||||
|         this.domAttach.update(this.visible); | ||||
|     } | ||||
| } | ||||
| @ -20,6 +20,7 @@ export class StorageImplBrowser extends StorageInterface { | ||||
|     } | ||||
| 
 | ||||
|     initialize() { | ||||
|         logger.error("Using localStorage, please update to a newer browser"); | ||||
|         return new Promise((resolve, reject) => { | ||||
|             // Check for local storage availability in general
 | ||||
|             if (!window.localStorage) { | ||||
|  | ||||
							
								
								
									
										155
									
								
								src/js/platform/browser/storage_indexed_db.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								src/js/platform/browser/storage_indexed_db.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,155 @@ | ||||
| import { FILE_NOT_FOUND, StorageInterface } from "../storage"; | ||||
| import { createLogger } from "../../core/logging"; | ||||
| 
 | ||||
| const logger = createLogger("storage/browserIDB"); | ||||
| 
 | ||||
| const LOCAL_STORAGE_UNAVAILABLE = "local-storage-unavailable"; | ||||
| const LOCAL_STORAGE_NO_WRITE_PERMISSION = "local-storage-no-write-permission"; | ||||
| 
 | ||||
| let randomDelay = () => 0; | ||||
| 
 | ||||
| if (G_IS_DEV) { | ||||
|     // Random delay for testing
 | ||||
|     // randomDelay = () => 500;
 | ||||
| } | ||||
| 
 | ||||
| export class StorageImplBrowserIndexedDB extends StorageInterface { | ||||
|     constructor(app) { | ||||
|         super(app); | ||||
|         this.currentBusyFilename = false; | ||||
| 
 | ||||
|         /** @type {IDBDatabase} */ | ||||
|         this.database = null; | ||||
|     } | ||||
| 
 | ||||
|     initialize() { | ||||
|         logger.log("Using indexed DB storage"); | ||||
|         return new Promise((resolve, reject) => { | ||||
|             const request = window.indexedDB.open("app_storage", 10); | ||||
|             request.onerror = event => { | ||||
|                 logger.error("IDB error:", event); | ||||
|                 reject("Indexed DB access error"); | ||||
|             }; | ||||
| 
 | ||||
|             request.onsuccess = event => resolve(event.target.result); | ||||
| 
 | ||||
|             request.onupgradeneeded = /** @type {IDBVersionChangeEvent} */ event => { | ||||
|                 /** @type {IDBDatabase} */ | ||||
|                 const database = event.target.result; | ||||
| 
 | ||||
|                 const objectStore = database.createObjectStore("files", { | ||||
|                     keyPath: "filename", | ||||
|                 }); | ||||
| 
 | ||||
|                 objectStore.createIndex("filename", "filename", { unique: true }); | ||||
| 
 | ||||
|                 objectStore.transaction.onerror = event => { | ||||
|                     logger.error("IDB transaction error:", event); | ||||
|                     reject("Indexed DB transaction error during migration, check console output."); | ||||
|                 }; | ||||
| 
 | ||||
|                 objectStore.transaction.oncomplete = event => { | ||||
|                     logger.log("Object store completely initialized"); | ||||
|                     resolve(database); | ||||
|                 }; | ||||
|             }; | ||||
|         }).then(database => { | ||||
|             this.database = database; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     writeFileAsync(filename, contents) { | ||||
|         if (this.currentBusyFilename === filename) { | ||||
|             logger.warn("Attempt to write", filename, "while write process is not finished!"); | ||||
|         } | ||||
|         if (!this.database) { | ||||
|             return Promise.reject("Storage not ready"); | ||||
|         } | ||||
| 
 | ||||
|         this.currentBusyFilename = filename; | ||||
|         const transaction = this.database.transaction(["files"], "readwrite"); | ||||
| 
 | ||||
|         return new Promise((resolve, reject) => { | ||||
|             transaction.oncomplete = () => { | ||||
|                 this.currentBusyFilename = null; | ||||
|                 resolve(); | ||||
|             }; | ||||
| 
 | ||||
|             transaction.onerror = error => { | ||||
|                 this.currentBusyFilename = null; | ||||
|                 logger.error("Error while writing", filename, ":", error); | ||||
|                 reject(error); | ||||
|             }; | ||||
| 
 | ||||
|             const store = transaction.objectStore("files"); | ||||
|             store.put({ | ||||
|                 filename, | ||||
|                 contents, | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     writeFileSyncIfSupported(filename, contents) { | ||||
|         // Not supported
 | ||||
|         this.writeFileAsync(filename, contents); | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     readFileAsync(filename) { | ||||
|         if (!this.database) { | ||||
|             return Promise.reject("Storage not ready"); | ||||
|         } | ||||
| 
 | ||||
|         this.currentBusyFilename = filename; | ||||
|         const transaction = this.database.transaction(["files"], "readonly"); | ||||
| 
 | ||||
|         return new Promise((resolve, reject) => { | ||||
|             const store = transaction.objectStore("files"); | ||||
|             const request = store.get(filename); | ||||
| 
 | ||||
|             request.onsuccess = event => { | ||||
|                 this.currentBusyFilename = null; | ||||
|                 if (!request.result) { | ||||
|                     reject(FILE_NOT_FOUND); | ||||
|                     return; | ||||
|                 } | ||||
|                 resolve(request.result.contents); | ||||
|             }; | ||||
| 
 | ||||
|             request.onerror = error => { | ||||
|                 this.currentBusyFilename = null; | ||||
|                 logger.error("Error while reading", filename, ":", error); | ||||
|                 reject(error); | ||||
|             }; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     deleteFileAsync(filename) { | ||||
|         if (this.currentBusyFilename === filename) { | ||||
|             logger.warn("Attempt to delete", filename, "while write progres on it is ongoing!"); | ||||
|         } | ||||
| 
 | ||||
|         if (!this.database) { | ||||
|             return Promise.reject("Storage not ready"); | ||||
|         } | ||||
| 
 | ||||
|         this.currentBusyFilename = filename; | ||||
|         const transaction = this.database.transaction(["files"], "readwrite"); | ||||
| 
 | ||||
|         return new Promise((resolve, reject) => { | ||||
|             transaction.oncomplete = () => { | ||||
|                 this.currentBusyFilename = null; | ||||
|                 resolve(); | ||||
|             }; | ||||
| 
 | ||||
|             transaction.onerror = error => { | ||||
|                 this.currentBusyFilename = null; | ||||
|                 logger.error("Error while deleting", filename, ":", error); | ||||
|                 reject(error); | ||||
|             }; | ||||
| 
 | ||||
|             const store = transaction.objectStore("files"); | ||||
|             store.delete(filename); | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @ -76,13 +76,9 @@ export class Savegame extends ReadWriteProxy { | ||||
|      * @param {SavegameData} data | ||||
|      */ | ||||
|     migrate(data) { | ||||
|         // if (data.version === 1014) {
 | ||||
|         //     if (data.dump) {
 | ||||
|         //         const reader = new SavegameInterface_V1015(fakeLogger, data);
 | ||||
|         //         reader.migrateFrom1014();
 | ||||
|         //     }
 | ||||
|         //     data.version = 1015;
 | ||||
|         // }
 | ||||
|         if (data.version < 1000) { | ||||
|             return ExplainedResult.bad("Can not migrate savegame, too old"); | ||||
|         } | ||||
|         return ExplainedResult.good(); | ||||
|     } | ||||
| 
 | ||||
| @ -218,7 +214,6 @@ export class Savegame extends ReadWriteProxy { | ||||
|      * Updates the savegames metadata | ||||
|      */ | ||||
|     saveMetadata() { | ||||
|         const reader = this.getDumpReader(); | ||||
|         this.metaDataRef.lastUpdate = new Date().getTime(); | ||||
|         this.metaDataRef.version = this.getCurrentVersion(); | ||||
|         return this.app.savegameMgr.writeAsync(); | ||||
|  | ||||
| @ -154,6 +154,22 @@ export class SavegameManager extends ReadWriteProxy { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     importSavegame(data) { | ||||
|         const savegame = this.createNewSavegame(); | ||||
|         const migrationResult = savegame.migrate(data); | ||||
|         if (migrationResult.isBad()) { | ||||
|             return Promise.reject("Failed to migrate: " + migrationResult.reason); | ||||
|         } | ||||
| 
 | ||||
|         savegame.currentData = data; | ||||
|         const verification = savegame.verify(data); | ||||
|         if (verification.isBad()) { | ||||
|             return Promise.reject("Verification failed: " + verification.result); | ||||
|         } | ||||
| 
 | ||||
|         return savegame.writeSavegameAndMetadata().then(() => this.sortSavegames()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sorts all savegames by their creation time descending | ||||
|      * @returns {Promise<any>} | ||||
|  | ||||
| @ -1,8 +1,15 @@ | ||||
| import { GameState } from "../core/game_state"; | ||||
| import { cachebust } from "../core/cachebust"; | ||||
| import { globalConfig } from "../core/config"; | ||||
| import { makeDiv, formatSecondsToTimeAgo, generateFileDownload } from "../core/utils"; | ||||
| import { | ||||
|     makeDiv, | ||||
|     formatSecondsToTimeAgo, | ||||
|     generateFileDownload, | ||||
|     removeAllChildren, | ||||
|     waitNextFrame, | ||||
| } from "../core/utils"; | ||||
| import { ReadWriteProxy } from "../core/read_write_proxy"; | ||||
| import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; | ||||
| 
 | ||||
| export class MainMenuState extends GameState { | ||||
|     constructor() { | ||||
| @ -10,23 +17,62 @@ export class MainMenuState extends GameState { | ||||
|     } | ||||
| 
 | ||||
|     getInnerHTML() { | ||||
|         const bannerHtml = ` | ||||
|             <h3>This is a Demo Version</h3> | ||||
|              | ||||
|             <p>Get <strong>shapez.io on steam</strong> for:</p> | ||||
| 
 | ||||
|             <ul> | ||||
|                 <li>No advertisements and demo banners.</li> | ||||
|                 <li>Unlimited savegame slots.</li> | ||||
|                 <li>Supporting the developer ❤️</li> | ||||
|             </ul> | ||||
| 
 | ||||
|             <a href="https://steam.shapez.io" class="steamLink" target="_blank">Get shapez.io on steam!</a> | ||||
|         `;
 | ||||
| 
 | ||||
|         return ` | ||||
| 
 | ||||
|             <video autoplay muted loop class="fullscreenBackgroundVideo"> | ||||
|                 <source src="${cachebust("res/bg_render.webm")}" type="video/webm"> | ||||
|             </video> | ||||
| 
 | ||||
| 
 | ||||
|             <div class="logo"> | ||||
|                 <img src="${cachebust("res/logo.png")}" alt="shapez.io Logo"> | ||||
|             </div> | ||||
|          | ||||
|             <div class="betaWarning"> | ||||
|                 This game is still under development - Please report any issues! | ||||
| 
 | ||||
|                 ${ | ||||
|                     G_IS_STANDALONE | ||||
|                         ? "" | ||||
|                         : ` | ||||
|                     <div class="demoBadge"></div> | ||||
|                 ` | ||||
|                 } | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="mainContainer"> | ||||
| 
 | ||||
|                   <button class="playButton styledButton">Play</button> | ||||
|             <div class="mainWrapper"> | ||||
|              | ||||
|             ${ | ||||
|                 G_IS_STANDALONE | ||||
|                     ? "" | ||||
|                     : ` | ||||
|                 <div class="standaloneBanner leftSide">${bannerHtml}</div> | ||||
|             ` | ||||
|             }     | ||||
|                 <div class="mainContainer"> | ||||
|                     <button class="playButton styledButton">Play</button> | ||||
|                     <button class="importButton styledButton">Import savegame</button> | ||||
|                 </div> | ||||
| 
 | ||||
|                 ${ | ||||
|                     G_IS_STANDALONE | ||||
|                         ? "" | ||||
|                         : ` | ||||
|                     <div class="standaloneBanner rightSide">${bannerHtml}</div> | ||||
|                 ` | ||||
|                 }     | ||||
|      | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="footer"> | ||||
| @ -45,6 +91,60 @@ export class MainMenuState extends GameState { | ||||
|         `;
 | ||||
|     } | ||||
| 
 | ||||
|     requestImportSavegame() { | ||||
|         var input = document.createElement("input"); | ||||
|         input.type = "file"; | ||||
|         input.accept = ".bin"; | ||||
| 
 | ||||
|         input.onchange = e => { | ||||
|             const file = input.files[0]; | ||||
|             if (file) { | ||||
|                 waitNextFrame().then(() => { | ||||
|                     const closeLoader = this.dialogs.showLoadingDialog(); | ||||
|                     const reader = new FileReader(); | ||||
|                     reader.addEventListener("load", event => { | ||||
|                         const contents = event.target.result; | ||||
| 
 | ||||
|                         let realContent; | ||||
| 
 | ||||
|                         try { | ||||
|                             realContent = ReadWriteProxy.deserializeObject(contents); | ||||
|                         } catch (err) { | ||||
|                             closeLoader(); | ||||
|                             this.dialogs.showWarning( | ||||
|                                 "Import error", | ||||
|                                 "Failed to import your savegame:<br><br>" + err | ||||
|                             ); | ||||
|                             return; | ||||
|                         } | ||||
| 
 | ||||
|                         this.app.savegameMgr.importSavegame(realContent).then( | ||||
|                             () => { | ||||
|                                 closeLoader(); | ||||
|                                 this.dialogs.showWarning("Imported", "Your savegame has been imported."); | ||||
| 
 | ||||
|                                 this.renderSavegames(); | ||||
|                             }, | ||||
|                             err => { | ||||
|                                 closeLoader(); | ||||
|                                 this.dialogs.showWarning( | ||||
|                                     "Import error", | ||||
|                                     "Failed to import savegame. Please check the console output." | ||||
|                                 ); | ||||
|                             } | ||||
|                         ); | ||||
|                     }); | ||||
|                     reader.addEventListener("error", error => { | ||||
|                         console.error(error); | ||||
|                         alert("Failed to read file: " + error); | ||||
|                     }); | ||||
|                     reader.readAsText(file, "utf-8"); | ||||
|                 }); | ||||
|             } | ||||
|         }; | ||||
|         input.click(); | ||||
|     } | ||||
| 
 | ||||
|     onBackButton() { | ||||
|         this.app.platformWrapper.exitApp(); | ||||
|     } | ||||
| @ -54,8 +154,13 @@ export class MainMenuState extends GameState { | ||||
|             alert("Error while loading game: " + payload.loadError); | ||||
|         } | ||||
| 
 | ||||
|         this.dialogs = new HUDModalDialogs(null, this.app); | ||||
|         const dialogsElement = document.body.querySelector(".modalDialogParent"); | ||||
|         this.dialogs.initializeToElement(dialogsElement); | ||||
| 
 | ||||
|         const qs = this.htmlElement.querySelector.bind(this.htmlElement); | ||||
|         this.trackClicks(qs(".mainContainer .playButton"), this.onPlayButtonClicked); | ||||
|         this.trackClicks(qs(".mainContainer .importButton"), this.requestImportSavegame); | ||||
| 
 | ||||
|         if (G_IS_DEV && globalConfig.debug.fastGameEnter) { | ||||
|             this.onPlayButtonClicked(); | ||||
| @ -76,6 +181,10 @@ export class MainMenuState extends GameState { | ||||
|     } | ||||
| 
 | ||||
|     renderSavegames() { | ||||
|         const oldContainer = this.htmlElement.querySelector(".mainContainer .savegames"); | ||||
|         if (oldContainer) { | ||||
|             oldContainer.remove(); | ||||
|         } | ||||
|         const games = this.app.savegameMgr.getSavegamesMetaData(); | ||||
|         if (games.length > 0) { | ||||
|             const parent = makeDiv(this.htmlElement.querySelector(".mainContainer"), null, ["savegames"]); | ||||
| @ -91,6 +200,10 @@ export class MainMenuState extends GameState { | ||||
|                     formatSecondsToTimeAgo((new Date().getTime() - games[i].lastUpdate) / 1000.0) | ||||
|                 ); | ||||
| 
 | ||||
|                 const deleteButton = document.createElement("button"); | ||||
|                 deleteButton.classList.add("styledButton", "deleteGame"); | ||||
|                 elem.appendChild(deleteButton); | ||||
| 
 | ||||
|                 const downloadButton = document.createElement("button"); | ||||
|                 downloadButton.classList.add("styledButton", "downloadGame"); | ||||
|                 elem.appendChild(downloadButton); | ||||
| @ -99,6 +212,7 @@ export class MainMenuState extends GameState { | ||||
|                 resumeBtn.classList.add("styledButton", "resumeGame"); | ||||
|                 elem.appendChild(resumeBtn); | ||||
| 
 | ||||
|                 this.trackClicks(deleteButton, () => this.deleteGame(games[i])); | ||||
|                 this.trackClicks(downloadButton, () => this.downloadGame(games[i])); | ||||
|                 this.trackClicks(resumeBtn, () => this.resumeGame(games[i])); | ||||
|             } | ||||
| @ -117,6 +231,28 @@ export class MainMenuState extends GameState { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param {object} game | ||||
|      */ | ||||
|     deleteGame(game) { | ||||
|         const signals = this.dialogs.showWarning( | ||||
|             "Confirm Deletion", | ||||
|             "Are you sure you want to delete the game?", | ||||
|             ["delete:bad", "cancel:good"] | ||||
|         ); | ||||
| 
 | ||||
|         signals.delete.add(() => { | ||||
|             this.app.savegameMgr.deleteSavegame(game).then( | ||||
|                 () => { | ||||
|                     this.renderSavegames(); | ||||
|                 }, | ||||
|                 err => { | ||||
|                     this.dialogs.showWarning("Failed to delete", "Error: " + err); | ||||
|                 } | ||||
|             ); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param {object} game | ||||
|      */ | ||||
| @ -138,6 +274,6 @@ export class MainMenuState extends GameState { | ||||
|     } | ||||
| 
 | ||||
|     onLeave() { | ||||
|         // this.dialogs.cleanup();
 | ||||
|         this.dialogs.cleanup(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -67,10 +67,6 @@ export class PreloadState extends GameState { | ||||
|             .then(() => this.setStatus("Creating platform wrapper")) | ||||
|             .then(() => this.app.platformWrapper.initialize()) | ||||
| 
 | ||||
|             .then(() => this.setStatus("Initializing libraries")) | ||||
|             .then(() => this.app.analytics.initialize()) | ||||
|             .then(() => this.app.gameAnalytics.initialize()) | ||||
| 
 | ||||
|             .then(() => this.setStatus("Initializing local storage")) | ||||
|             .then(() => { | ||||
|                 const wrapper = this.app.platformWrapper; | ||||
| @ -95,6 +91,10 @@ export class PreloadState extends GameState { | ||||
|                 return this.app.storage.initialize(); | ||||
|             }) | ||||
| 
 | ||||
|             .then(() => this.setStatus("Initializing libraries")) | ||||
|             .then(() => this.app.analytics.initialize()) | ||||
|             .then(() => this.app.gameAnalytics.initialize()) | ||||
| 
 | ||||
|             .then(() => this.setStatus("Initializing settings")) | ||||
|             .then(() => { | ||||
|                 return this.app.settings.initialize(); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 tobspr
						tobspr