Add ability to import savegames, add game menu, multiple smaller improvements
This commit is contained in:
parent
c1d720ca52
commit
224bc6c7e5
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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