mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-12-11 09:11:50 +00:00
Remove ads, analytics and Steam SSO, simplify HTML tasks (#21)
* Remove ad support, analytics and Wegame leftovers The game may be somewhat broken in a few places, but it doesn't matter for now. This is still not the end. * Remove Steam SSO and demo stuff Steam SSO is completely removed, a few things from demo like simplified level sets are gone as well. Puzzle DLC on the other hand is now always "owned" and will ask for a token to log in. Removes * Use shapez dialogs for Puzzle DLC token input Yes, this sucks *a lot*. But it's a temporary measure, trust me :P * Simplify HTML tasks Removes the web (demo) index.html page and makes HTML tasks independent of the build variant. This might not be the best solution, but it works for now.
This commit is contained in:
parent
62b170a92d
commit
aa49f063c3
@ -164,8 +164,8 @@ function serveHTML({ version = "web-dev" }) {
|
||||
gulp.watch(["../src/**/*.scss"], gulp.series("css.dev"));
|
||||
|
||||
// Watch .html files, those trigger a html rebuild
|
||||
gulp.watch("../src/**/*.html", gulp.series("html." + version + ".dev"));
|
||||
gulp.watch("./preloader/*.*", gulp.series("html." + version + ".dev"));
|
||||
gulp.watch("../src/**/*.html", gulp.series("html.dev"));
|
||||
gulp.watch("./preloader/*.*", gulp.series("html.dev"));
|
||||
|
||||
// Watch translations
|
||||
gulp.watch("../translations/**/*.yaml", gulp.series("translations.convertToJson"));
|
||||
@ -253,7 +253,7 @@ for (const variant in BUILD_VARIANTS) {
|
||||
|
||||
gulp.task(
|
||||
buildName + ".all",
|
||||
gulp.series(buildName + ".resourcesAndCode", "css.prod-standalone", "html." + variant + ".prod")
|
||||
gulp.series(buildName + ".resourcesAndCode", "css.prod-standalone", "html.prod")
|
||||
);
|
||||
|
||||
gulp.task(buildName, gulp.series("utils.cleanup", buildName + ".all", "step.postbuild"));
|
||||
@ -279,7 +279,7 @@ for (const variant in BUILD_VARIANTS) {
|
||||
// serve
|
||||
gulp.task(
|
||||
"serve." + variant,
|
||||
gulp.series("build.prepare.dev", "html." + variant + ".dev", () => serveHTML({ version: variant }))
|
||||
gulp.series("build.prepare.dev", "html.dev", () => serveHTML({ version: variant }))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
118
gulp/html.js
118
gulp/html.js
@ -1,8 +1,7 @@
|
||||
import { getRevision, cachebust as cachebustUtil } from "./buildutils.js";
|
||||
import { getRevision } from "./buildutils.js";
|
||||
import fs from "fs";
|
||||
import path from "path/posix";
|
||||
import crypto from "crypto";
|
||||
import { BUILD_VARIANTS } from "./build_variants.js";
|
||||
|
||||
import gulpDom from "gulp-dom";
|
||||
import gulpHtmlmin from "gulp-htmlmin";
|
||||
@ -16,25 +15,16 @@ function computeIntegrityHash(fullPath, algorithm = "sha256") {
|
||||
}
|
||||
|
||||
/**
|
||||
* PROVIDES (per <variant>)
|
||||
* PROVIDES
|
||||
*
|
||||
* html.<variant>.dev
|
||||
* html.<variant>.prod
|
||||
* html.dev
|
||||
* html.prod
|
||||
*/
|
||||
export default function gulptasksHTML(gulp, buildFolder) {
|
||||
const commitHash = getRevision();
|
||||
async function buildHtml({ standalone = false, integrity = true, enableCachebust = true }) {
|
||||
function cachebust(url) {
|
||||
if (enableCachebust) {
|
||||
return cachebustUtil(url, commitHash);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
const hasLocalFiles = standalone;
|
||||
|
||||
async function buildHtml({ integrity = true }) {
|
||||
return gulp
|
||||
.src("../src/html/" + (standalone ? "index.standalone.html" : "index.html"))
|
||||
.src("../src/html/index.html")
|
||||
.pipe(
|
||||
gulpDom(
|
||||
/** @this {Document} **/ function () {
|
||||
@ -46,7 +36,7 @@ export default function gulptasksHTML(gulp, buildFolder) {
|
||||
css.type = "text/css";
|
||||
css.media = "none";
|
||||
css.setAttribute("onload", "this.media='all'");
|
||||
css.href = cachebust("main.css");
|
||||
css.href = "main.css";
|
||||
if (integrity) {
|
||||
css.setAttribute(
|
||||
"integrity",
|
||||
@ -55,40 +45,13 @@ export default function gulptasksHTML(gulp, buildFolder) {
|
||||
}
|
||||
document.head.appendChild(css);
|
||||
|
||||
// Do not need to preload in app or standalone
|
||||
if (!hasLocalFiles) {
|
||||
// Preload essentials
|
||||
const preloads = [
|
||||
"res/fonts/GameFont.woff2",
|
||||
// "async-resources.css",
|
||||
// "res/sounds/music/theme-short.mp3",
|
||||
];
|
||||
|
||||
preloads.forEach(src => {
|
||||
const preloadLink = document.createElement("link");
|
||||
preloadLink.rel = "preload";
|
||||
preloadLink.href = cachebust(src);
|
||||
if (src.endsWith(".woff2")) {
|
||||
preloadLink.setAttribute("crossorigin", "anonymous");
|
||||
preloadLink.setAttribute("as", "font");
|
||||
} else if (src.endsWith(".css")) {
|
||||
preloadLink.setAttribute("as", "style");
|
||||
} else if (src.endsWith(".mp3")) {
|
||||
preloadLink.setAttribute("as", "audio");
|
||||
} else {
|
||||
preloadLink.setAttribute("as", "image");
|
||||
}
|
||||
document.head.appendChild(preloadLink);
|
||||
});
|
||||
}
|
||||
|
||||
let fontCss = `
|
||||
@font-face {
|
||||
font-family: "GameFont";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-display: swap;
|
||||
src: url('${cachebust("res/fonts/GameFont.woff2")}') format("woff2");
|
||||
src: url('res/fonts/GameFont.woff2') format("woff2");
|
||||
}
|
||||
`;
|
||||
let loadingCss =
|
||||
@ -103,40 +66,16 @@ export default function gulptasksHTML(gulp, buildFolder) {
|
||||
.readFileSync(path.join("preloader", "preloader.html"))
|
||||
.toString();
|
||||
|
||||
// Append loader, but not in standalone (directly include bundle there)
|
||||
if (standalone) {
|
||||
const bundleScript = document.createElement("script");
|
||||
bundleScript.type = "text/javascript";
|
||||
bundleScript.src = "bundle.js";
|
||||
if (integrity) {
|
||||
bundleScript.setAttribute(
|
||||
"integrity",
|
||||
computeIntegrityHash(path.join(buildFolder, "bundle.js"))
|
||||
);
|
||||
}
|
||||
document.head.appendChild(bundleScript);
|
||||
} else {
|
||||
const loadJs = document.createElement("script");
|
||||
loadJs.type = "text/javascript";
|
||||
let scriptContent = "";
|
||||
scriptContent += `var bundleSrc = '${cachebust("bundle.js")}';\n`;
|
||||
|
||||
if (integrity) {
|
||||
scriptContent +=
|
||||
"var bundleIntegrity = '" +
|
||||
computeIntegrityHash(path.join(buildFolder, "bundle.js")) +
|
||||
"';\n";
|
||||
} else {
|
||||
scriptContent += "var bundleIntegrity = null;\n";
|
||||
scriptContent += "var bundleIntegrityTranspiled = null;\n";
|
||||
}
|
||||
|
||||
scriptContent += fs
|
||||
.readFileSync(path.join("preloader", "preloader.js"))
|
||||
.toString();
|
||||
loadJs.textContent = scriptContent;
|
||||
document.head.appendChild(loadJs);
|
||||
const bundleScript = document.createElement("script");
|
||||
bundleScript.type = "text/javascript";
|
||||
bundleScript.src = "bundle.js";
|
||||
if (integrity) {
|
||||
bundleScript.setAttribute(
|
||||
"integrity",
|
||||
computeIntegrityHash(path.join(buildFolder, "bundle.js"))
|
||||
);
|
||||
}
|
||||
document.head.appendChild(bundleScript);
|
||||
|
||||
document.body.innerHTML = bodyContent;
|
||||
}
|
||||
@ -160,21 +99,14 @@ export default function gulptasksHTML(gulp, buildFolder) {
|
||||
.pipe(gulp.dest(buildFolder));
|
||||
}
|
||||
|
||||
for (const variant in BUILD_VARIANTS) {
|
||||
const data = BUILD_VARIANTS[variant];
|
||||
gulp.task("html." + variant + ".dev", () => {
|
||||
return buildHtml({
|
||||
standalone: data.standalone,
|
||||
integrity: false,
|
||||
enableCachebust: false,
|
||||
});
|
||||
gulp.task("html.dev", () => {
|
||||
return buildHtml({
|
||||
integrity: false,
|
||||
});
|
||||
gulp.task("html." + variant + ".prod", () => {
|
||||
return buildHtml({
|
||||
standalone: data.standalone,
|
||||
integrity: true,
|
||||
enableCachebust: !data.standalone,
|
||||
});
|
||||
});
|
||||
gulp.task("html.prod", () => {
|
||||
return buildHtml({
|
||||
integrity: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,20 +2,6 @@
|
||||
var loadTimeout = null;
|
||||
var callbackDone = false;
|
||||
|
||||
var searchString = window.location.search;
|
||||
if (searchString.includes("steam_sso_auth_token=")) {
|
||||
var pos = searchString.indexOf("steam_sso_auth_token");
|
||||
const authToken = searchString.substring(pos + 21, pos + 57);
|
||||
try {
|
||||
window.localStorage.setItem("steam_sso_auth_token", authToken);
|
||||
window.location.replace(window.location.protocol + "//" + window.location.host);
|
||||
} catch (ex) {
|
||||
alert("Failed to login via Steam SSO: " + ex);
|
||||
window.location.replace("https://shapez.io");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Catch load errors
|
||||
|
||||
function errorHandler(event, source, lineno, colno, error) {
|
||||
|
||||
@ -1,110 +0,0 @@
|
||||
#aip_gdpr {
|
||||
&,
|
||||
* {
|
||||
text-shadow: none !important;
|
||||
pointer-events: all;
|
||||
color: #111 !important;
|
||||
}
|
||||
|
||||
#aip_gdpr_banner {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
#aip_gdpr_message {
|
||||
padding: 0px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
#adinplayVideoContainer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 20000;
|
||||
background: rgba($mainBgColor, 0.9);
|
||||
pointer-events: all;
|
||||
cursor: default;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
*,
|
||||
& {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
&:not(.visible) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.waitingForFinish {
|
||||
.videoInner {
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
@include InlineAnimation(0.2s ease-in-out) {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
& {
|
||||
background: rgba($mainBgColor, 0.9) uiResource("loading.svg") center center / #{D(60px)} no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include InlineAnimation(1s ease-in-out) {
|
||||
0% {
|
||||
background: rgba($mainBgColor, 0.1);
|
||||
}
|
||||
100% {
|
||||
background: rgba($mainBgColor, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.adInner {
|
||||
@include BoxShadow3D(lighten($mainBgColor, 15));
|
||||
@include S(border-radius, $globalBorderRadius);
|
||||
@include S(padding, 15px);
|
||||
// max-width: 960px;
|
||||
display: block !important;
|
||||
|
||||
.topbar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
@include S(margin-bottom, 15px);
|
||||
@include S(grid-column-gap, 10px);
|
||||
|
||||
.desc {
|
||||
@include TextShadow3D(#fff);
|
||||
@include PlainText;
|
||||
}
|
||||
|
||||
button.getOnSteam {
|
||||
@include Text;
|
||||
}
|
||||
}
|
||||
|
||||
.videoInner {
|
||||
// width: 960px;
|
||||
// height: 570px;
|
||||
// min-width: 960px;
|
||||
// min-height: 570px;
|
||||
background: darken($mainBgColor, 1);
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -714,18 +714,6 @@ input.rangeInput {
|
||||
}
|
||||
}
|
||||
|
||||
.xpaystation-widget-lightbox {
|
||||
z-index: 19999;
|
||||
.xpaystation-widget-lightbox-overlay {
|
||||
background: rgba($mainBgColor, 0.94);
|
||||
}
|
||||
&,
|
||||
iframe {
|
||||
pointer-events: all;
|
||||
user-select: all;
|
||||
}
|
||||
}
|
||||
|
||||
iframe {
|
||||
pointer-events: all;
|
||||
user-select: all;
|
||||
@ -744,32 +732,3 @@ iframe {
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.sentry-error-embed-wrapper {
|
||||
z-index: 10000;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
* {
|
||||
text-shadow: none !important;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.cpmsrendertarget {
|
||||
&,
|
||||
* {
|
||||
pointer-events: all;
|
||||
}
|
||||
background: rgba($mainBgColor, 0.94) !important;
|
||||
.cpmsvideoclosebanner {
|
||||
font-family: GameFont !important;
|
||||
font-size: 16px !important;
|
||||
border-radius: 2px !important;
|
||||
background: $themeColor !important;
|
||||
@include BoxShadow3D(darken($mainBgColor, 12));
|
||||
color: #eee !important;
|
||||
&:active {
|
||||
@include BoxShadow3D(darken($mainBgColor, 12), $size: 1px);
|
||||
transform: translateY(2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,10 +17,8 @@
|
||||
@import "animations";
|
||||
@import "game_state";
|
||||
@import "textual_game_state";
|
||||
@import "adinplay";
|
||||
@import "changelog_skins";
|
||||
|
||||
@import "states/wegame_splash";
|
||||
@import "states/preload";
|
||||
@import "states/main_menu";
|
||||
@import "states/ingame";
|
||||
|
||||
@ -252,15 +252,6 @@
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.onlinePlayerCount {
|
||||
color: #333;
|
||||
display: none;
|
||||
@include S(margin-top, 15px);
|
||||
@include SuperSmallText;
|
||||
@include S(height, 15px);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@include Heading;
|
||||
font-weight: bold;
|
||||
@ -1167,39 +1158,6 @@
|
||||
@include S(gap, 30px);
|
||||
@include S(padding, 15px, 25px, 15px, 20px);
|
||||
|
||||
&.wegameDisclaimer {
|
||||
@include SuperSmallText;
|
||||
display: grid;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
> .disclaimer {
|
||||
grid-column: 2 / 3;
|
||||
|
||||
@include DarkThemeOverride {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
> .rating {
|
||||
grid-column: 3 / 4;
|
||||
justify-self: end;
|
||||
align-self: end;
|
||||
|
||||
@include S(width, 32px);
|
||||
@include S(height, 40px);
|
||||
background: green;
|
||||
cursor: pointer !important;
|
||||
pointer-events: all;
|
||||
@include S(border-radius, 4px);
|
||||
overflow: hidden;
|
||||
|
||||
& {
|
||||
background: #fff uiResource("wegame_isbn_rating.jpg") center center / contain no-repeat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
#state_WegameSplashState {
|
||||
background: #000 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.wrapper {
|
||||
opacity: 0;
|
||||
@include InlineAnimation(5.9s ease-in-out) {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
@include Heading;
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
@include SuperHeading;
|
||||
@include S(margin-bottom, 20px);
|
||||
}
|
||||
|
||||
div {
|
||||
@include S(margin-bottom, 10px);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,46 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="--ui-scale: 1.33;">
|
||||
<html>
|
||||
<head>
|
||||
<title>shapez Demo - Factory Automation Game</title>
|
||||
<title>shapez</title>
|
||||
|
||||
<!-- mobile stuff -->
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=5, shrink-to-fit=no, viewport-fit=cover"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="HandheldFriendly" content="true" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<meta name="theme-color" content="#393747" />
|
||||
|
||||
<!-- seo -->
|
||||
<meta name="copyright" content="2022 tobspr Games" />
|
||||
<meta name="author" content="tobspr Games - tobspr.io" />
|
||||
<meta
|
||||
name="description"
|
||||
content="shapez is a factory automation game about combining and producing different types of shapes. Build, optimize and grow your factory to finally automate everything!"
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="shapes, automation, factory, factorio, incremental, upgrades, base building"
|
||||
/>
|
||||
<meta property="og:title" content="shapez" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="shapez is a fun factory base building game about combining shapes"
|
||||
/>
|
||||
<meta property="og:url" content="https://shapez.io/" />
|
||||
<meta property="og:image" content="https://shapez.io/og_thumb.png" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<!-- misc -->
|
||||
<meta http-equiv="Cache-Control" content="private, max-age=0, no-store, no-cache, must-revalidate" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="canonical" href="https://shapez.io" />
|
||||
</head>
|
||||
|
||||
<body oncontextmenu="return false" style="background: #393747;"></body>
|
||||
</html>
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>shapez</title>
|
||||
|
||||
<!-- mobile stuff -->
|
||||
<meta name="format-detection" content="telephone=no" />
|
||||
<meta name="msapplication-tap-highlight" content="no" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="HandheldFriendly" content="true" />
|
||||
<meta name="MobileOptimized" content="320" />
|
||||
<meta name="theme-color" content="#393747" />
|
||||
|
||||
<!-- misc -->
|
||||
<meta http-equiv="Cache-Control" content="private, max-age=0, no-store, no-cache, must-revalidate" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
</head>
|
||||
<body oncontextmenu="return false" style="background: #393747;"></body>
|
||||
</html>
|
||||
@ -10,11 +10,7 @@ import { StateManager } from "./core/state_manager";
|
||||
import { TrackedState } from "./core/tracked_state";
|
||||
import { getPlatformName, waitNextFrame } from "./core/utils";
|
||||
import { Vector } from "./core/vector";
|
||||
import { AdProviderInterface } from "./platform/ad_provider";
|
||||
import { NoAdProvider } from "./platform/ad_providers/no_ad_provider";
|
||||
import { NoAchievementProvider } from "./platform/browser/no_achievement_provider";
|
||||
import { AnalyticsInterface } from "./platform/analytics";
|
||||
import { GoogleAnalyticsImpl } from "./platform/browser/google_analytics";
|
||||
import { SoundImplBrowser } from "./platform/browser/sound";
|
||||
import { PlatformWrapperImplBrowser } from "./platform/browser/wrapper";
|
||||
import { PlatformWrapperImplElectron } from "./platform/electron/wrapper";
|
||||
@ -29,11 +25,9 @@ import { MainMenuState } from "./states/main_menu";
|
||||
import { MobileWarningState } from "./states/mobile_warning";
|
||||
import { PreloadState } from "./states/preload";
|
||||
import { SettingsState } from "./states/settings";
|
||||
import { ShapezGameAnalytics } from "./platform/browser/game_analytics";
|
||||
import { PuzzleMenuState } from "./states/puzzle_menu";
|
||||
import { ClientAPI } from "./platform/api";
|
||||
import { LoginState } from "./states/login";
|
||||
import { WegameSplashState } from "./states/wegame_splash";
|
||||
import { MODS } from "./mods/modloader";
|
||||
import { MOD_SIGNALS } from "./mods/mod_signals";
|
||||
import { ModsState } from "./states/mods";
|
||||
@ -107,15 +101,6 @@ export class Application {
|
||||
/** @type {AchievementProviderInterface} */
|
||||
this.achievementProvider = null;
|
||||
|
||||
/** @type {AdProviderInterface} */
|
||||
this.adProvider = null;
|
||||
|
||||
/** @type {AnalyticsInterface} */
|
||||
this.analytics = null;
|
||||
|
||||
/** @type {ShapezGameAnalytics} */
|
||||
this.gameAnalytics = null;
|
||||
|
||||
this.initPlatformDependentInstances();
|
||||
|
||||
// Track if the window is focused (only relevant for browser)
|
||||
@ -178,11 +163,7 @@ export class Application {
|
||||
this.platformWrapper = new PlatformWrapperImplBrowser(this);
|
||||
}
|
||||
|
||||
// Start with empty ad provider
|
||||
this.adProvider = new NoAdProvider(this);
|
||||
this.sound = new SoundImplBrowser(this);
|
||||
this.analytics = new GoogleAnalyticsImpl(this);
|
||||
this.gameAnalytics = new ShapezGameAnalytics(this);
|
||||
this.achievementProvider = new NoAchievementProvider(this);
|
||||
}
|
||||
|
||||
@ -192,7 +173,6 @@ export class Application {
|
||||
registerStates() {
|
||||
/** @type {Array<typeof GameState>} */
|
||||
const states = [
|
||||
WegameSplashState,
|
||||
PreloadState,
|
||||
MobileWarningState,
|
||||
MainMenuState,
|
||||
@ -326,11 +306,7 @@ export class Application {
|
||||
}
|
||||
|
||||
onAppPlayingStateChanged(playing) {
|
||||
try {
|
||||
this.adProvider.setPlayStatus(playing);
|
||||
} catch (ex) {
|
||||
console.warn("Play status changed");
|
||||
}
|
||||
// TODO: Check for usages and alternatives. This can be turned into a singal.
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -25,7 +25,6 @@ export const THIRDPARTY_URLS = {
|
||||
patreon: "https://www.patreon.com/tobsprgames",
|
||||
privacyPolicy: "https://tobspr.io/privacy.html",
|
||||
|
||||
standaloneCampaignLink: "https://get.shapez.io/bundle/$campaign",
|
||||
puzzleDlcStorePage: "https://get.shapez.io/mm_puzzle_dlc?target=dlc",
|
||||
|
||||
levelTutorialVideos: {
|
||||
@ -37,17 +36,6 @@ export const THIRDPARTY_URLS = {
|
||||
modBrowser: "https://shapez.mod.io/",
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Application} app
|
||||
* @param {string} campaign
|
||||
*/
|
||||
export function openStandaloneLink(app, campaign) {
|
||||
const discount = globalConfig.currentDiscount > 0 ? "_discount" + globalConfig.currentDiscount : "";
|
||||
const event = campaign + discount;
|
||||
app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneCampaignLink.replace("$campaign", event));
|
||||
app.gameAnalytics.noteMinor("g.stdlink." + event);
|
||||
}
|
||||
|
||||
export const globalConfig = {
|
||||
// Size of a single tile in Pixels.
|
||||
// NOTICE: Update webpack.production.config too!
|
||||
|
||||
@ -35,32 +35,6 @@ export function createLogger(context) {
|
||||
return new Logger(context);
|
||||
}
|
||||
|
||||
function prepareObjectForLogging(obj, maxDepth = 1) {
|
||||
if (!window.Sentry) {
|
||||
// Not required without sentry
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (typeof obj !== "object" && !Array.isArray(obj)) {
|
||||
return obj;
|
||||
}
|
||||
const result = {};
|
||||
for (const key in obj) {
|
||||
const val = obj[key];
|
||||
|
||||
if (typeof val === "object") {
|
||||
if (maxDepth > 0) {
|
||||
result[key] = prepareObjectForLogging(val, maxDepth - 1);
|
||||
} else {
|
||||
result[key] = "[object]";
|
||||
}
|
||||
} else {
|
||||
result[key] = val;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes an error
|
||||
* @param {Error|ErrorEvent} err
|
||||
@ -155,19 +129,12 @@ export function globalError(context, ...args) {
|
||||
args = prepareArgsForLogging(args);
|
||||
// eslint-disable-next-line no-console
|
||||
logInternal(context, console.error, args);
|
||||
|
||||
if (window.Sentry) {
|
||||
window.Sentry.withScope(scope => {
|
||||
scope.setExtra("args", args);
|
||||
window.Sentry.captureMessage(internalBuildStringFromArgs(args), "error");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function prepareArgsForLogging(args) {
|
||||
let result = [];
|
||||
for (let i = 0; i < args.length; ++i) {
|
||||
result.push(prepareObjectForLogging(args[i]));
|
||||
result.push(args[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
const options = Object.fromEntries(new URLSearchParams(location.search).entries());
|
||||
|
||||
export let queryParamOptions = {
|
||||
embedProvider: null,
|
||||
abtVariant: null,
|
||||
campaign: null,
|
||||
fbclid: null,
|
||||
gclid: null,
|
||||
};
|
||||
|
||||
if (options.embed) {
|
||||
queryParamOptions.embedProvider = options.embed;
|
||||
}
|
||||
|
||||
if (options.abtVariant) {
|
||||
queryParamOptions.abtVariant = options.abtVariant;
|
||||
}
|
||||
|
||||
if (options.fbclid) {
|
||||
queryParamOptions.fbclid = options.fbclid;
|
||||
}
|
||||
|
||||
if (options.gclid) {
|
||||
queryParamOptions.gclid = options.gclid;
|
||||
}
|
||||
if (options.utm_campaign) {
|
||||
queryParamOptions.campaign = options.utm_campaign;
|
||||
}
|
||||
@ -105,8 +105,6 @@ export class StateManager {
|
||||
|
||||
this.currentState.onResized(this.app.screenWidth, this.app.screenHeight);
|
||||
|
||||
this.app.analytics.trackStateEnter(key);
|
||||
|
||||
window.history.pushState(
|
||||
{
|
||||
key,
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
import { T } from "../translations";
|
||||
import { openStandaloneLink } from "./config";
|
||||
|
||||
export let WEB_STEAM_SSO_AUTHENTICATED = false;
|
||||
|
||||
export async function authorizeViaSSOToken(app, dialogs) {
|
||||
if (G_IS_STANDALONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.location.search.includes("sso_logout_silent")) {
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
window.location.replace("/");
|
||||
return new Promise(() => null);
|
||||
}
|
||||
|
||||
if (window.location.search.includes("sso_logout")) {
|
||||
const { ok } = dialogs.showWarning(T.dialogs.steamSsoError.title, T.dialogs.steamSsoError.desc);
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
ok.add(() => window.location.replace("/"));
|
||||
return new Promise(() => null);
|
||||
}
|
||||
|
||||
if (window.location.search.includes("steam_sso_no_ownership")) {
|
||||
const { ok, getStandalone } = dialogs.showWarning(
|
||||
T.dialogs.steamSsoNoOwnership.title,
|
||||
T.dialogs.steamSsoNoOwnership.desc,
|
||||
["ok", "getStandalone:good"]
|
||||
);
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
getStandalone.add(() => {
|
||||
openStandaloneLink(app, "sso_ownership");
|
||||
window.location.replace("/");
|
||||
});
|
||||
ok.add(() => window.location.replace("/"));
|
||||
return new Promise(() => null);
|
||||
}
|
||||
|
||||
const token = window.localStorage.getItem("steam_sso_auth_token");
|
||||
if (!token) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const apiUrl = app.clientApi.getEndpoint();
|
||||
console.warn("Authorizing via token:", token);
|
||||
|
||||
const verify = async () => {
|
||||
const token = window.localStorage.getItem("steam_sso_auth_token");
|
||||
if (!token) {
|
||||
window.location.replace("?sso_logout");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await Promise.race([
|
||||
fetch(apiUrl + "/v1/sso/refresh", {
|
||||
method: "POST",
|
||||
body: token,
|
||||
headers: {
|
||||
"x-api-key": "d5c54aaa491f200709afff082c153ef2",
|
||||
},
|
||||
}),
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(() => reject("timeout exceeded"), 20000);
|
||||
}),
|
||||
]);
|
||||
|
||||
const responseText = await response.json();
|
||||
if (!responseText.token) {
|
||||
console.warn("Failed to register");
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
window.location.replace("?sso_logout");
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem("steam_sso_auth_token", responseText.token);
|
||||
app.clientApi.token = responseText.token;
|
||||
WEB_STEAM_SSO_AUTHENTICATED = true;
|
||||
} catch (ex) {
|
||||
console.warn("Auth failure", ex);
|
||||
window.localStorage.setItem("steam_sso_auth_token", "");
|
||||
window.location.replace("/");
|
||||
return new Promise(() => null);
|
||||
}
|
||||
};
|
||||
|
||||
await verify();
|
||||
setInterval(verify, 120000);
|
||||
}
|
||||
@ -114,7 +114,7 @@ export class HubGoals extends BasicSerializableObject {
|
||||
window.addEventListener("keydown", ev => {
|
||||
if (ev.key === "p") {
|
||||
// root is not guaranteed to exist within ~0.5s after loading in
|
||||
if (this.root && this.root.app && this.root.app.gameAnalytics) {
|
||||
if (this.root) {
|
||||
if (!this.isEndOfDemoReached()) {
|
||||
this.onGoalCompleted();
|
||||
}
|
||||
@ -262,7 +262,6 @@ export class HubGoals extends BasicSerializableObject {
|
||||
const reward = this.currentGoal.reward;
|
||||
this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1;
|
||||
|
||||
this.root.app.gameAnalytics.handleLevelCompleted(this.level);
|
||||
++this.level;
|
||||
this.computeNextGoal();
|
||||
|
||||
@ -352,8 +351,6 @@ export class HubGoals extends BasicSerializableObject {
|
||||
|
||||
this.root.signals.upgradePurchased.dispatch(upgradeId);
|
||||
|
||||
this.root.app.gameAnalytics.handleUpgradeUnlocked(upgradeId, currentLevel);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -7,8 +7,6 @@ import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { Dialog, DialogLoading, DialogOptionChooser } from "../../../core/modal_dialog_elements";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { T } from "../../../translations";
|
||||
import { openStandaloneLink } from "../../../core/config";
|
||||
|
||||
export class HUDModalDialogs extends BaseHUDPart {
|
||||
constructor(root, app) {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { queryParamOptions } from "../../../core/query_parameters";
|
||||
import { makeDiv } from "../../../core/utils";
|
||||
import { BaseHUDPart } from "../base_hud_part";
|
||||
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||
|
||||
@ -61,9 +61,7 @@ export class HUDSettingsMenu extends BaseHUDPart {
|
||||
}
|
||||
|
||||
returnToMenu() {
|
||||
this.root.app.adProvider.showVideoAd().then(() => {
|
||||
this.root.gameState.goBackToMenu();
|
||||
});
|
||||
this.root.gameState.goBackToMenu();
|
||||
}
|
||||
|
||||
goToSettings() {
|
||||
|
||||
@ -24,8 +24,6 @@ export class HUDUnlockNotification extends BaseHUDPart {
|
||||
}
|
||||
|
||||
this.buttonShowTimeout = null;
|
||||
|
||||
this.root.app.gameAnalytics.noteMinor("game.started");
|
||||
}
|
||||
|
||||
shouldPauseGame() {
|
||||
@ -69,8 +67,6 @@ export class HUDUnlockNotification extends BaseHUDPart {
|
||||
return;
|
||||
}
|
||||
|
||||
this.root.app.gameAnalytics.noteMinor("game.level.complete-" + level);
|
||||
|
||||
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
|
||||
this.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace(
|
||||
"<level>",
|
||||
@ -134,33 +130,31 @@ export class HUDUnlockNotification extends BaseHUDPart {
|
||||
}
|
||||
|
||||
requestClose() {
|
||||
this.root.app.adProvider.showVideoAd().then(() => {
|
||||
this.close();
|
||||
this.close();
|
||||
|
||||
this.root.hud.signals.unlockNotificationFinished.dispatch();
|
||||
this.root.hud.signals.unlockNotificationFinished.dispatch();
|
||||
|
||||
if (!this.root.app.settings.getAllSettings().offerHints) {
|
||||
return;
|
||||
}
|
||||
if (!this.root.app.settings.getAllSettings().offerHints) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.root.hubGoals.level === 3) {
|
||||
const { showUpgrades } = this.root.hud.parts.dialogs.showInfo(
|
||||
T.dialogs.upgradesIntroduction.title,
|
||||
T.dialogs.upgradesIntroduction.desc,
|
||||
["showUpgrades:good:timeout"]
|
||||
);
|
||||
showUpgrades.add(() => this.root.hud.parts.shop.show());
|
||||
}
|
||||
if (this.root.hubGoals.level === 3) {
|
||||
const { showUpgrades } = this.root.hud.parts.dialogs.showInfo(
|
||||
T.dialogs.upgradesIntroduction.title,
|
||||
T.dialogs.upgradesIntroduction.desc,
|
||||
["showUpgrades:good:timeout"]
|
||||
);
|
||||
showUpgrades.add(() => this.root.hud.parts.shop.show());
|
||||
}
|
||||
|
||||
if (this.root.hubGoals.level === 5) {
|
||||
const { showKeybindings } = this.root.hud.parts.dialogs.showInfo(
|
||||
T.dialogs.keybindingsIntroduction.title,
|
||||
T.dialogs.keybindingsIntroduction.desc,
|
||||
["showKeybindings:misc", "ok:good:timeout"]
|
||||
);
|
||||
showKeybindings.add(() => this.root.gameState.goToKeybindings());
|
||||
}
|
||||
});
|
||||
if (this.root.hubGoals.level === 5) {
|
||||
const { showKeybindings } = this.root.hud.parts.dialogs.showInfo(
|
||||
T.dialogs.keybindingsIntroduction.title,
|
||||
T.dialogs.keybindingsIntroduction.desc,
|
||||
["showKeybindings:misc", "ok:good:timeout"]
|
||||
);
|
||||
showKeybindings.add(() => this.root.gameState.goToKeybindings());
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
|
||||
@ -1,166 +1,8 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../../application";
|
||||
/* typehints:end */
|
||||
import { WEB_STEAM_SSO_AUTHENTICATED } from "../../core/steam_sso";
|
||||
import { enumHubGoalRewards } from "../tutorial_goals";
|
||||
|
||||
export const finalGameShape = "RuCw--Cw:----Ru--";
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Application} app
|
||||
* @returns
|
||||
*/
|
||||
const WEB_DEMO_LEVELS = app => {
|
||||
const levels = [
|
||||
// 1
|
||||
// Circle
|
||||
{
|
||||
shape: "CuCuCuCu", // belts t1
|
||||
required: 10,
|
||||
reward: enumHubGoalRewards.reward_cutter_and_trash,
|
||||
},
|
||||
|
||||
// 2
|
||||
// Cutter
|
||||
{
|
||||
shape: "----CuCu", //
|
||||
required: 20,
|
||||
reward: enumHubGoalRewards.no_reward,
|
||||
},
|
||||
|
||||
// 3
|
||||
// Rectangle
|
||||
{
|
||||
shape: "RuRuRuRu", // miners t1
|
||||
required: 30,
|
||||
reward: enumHubGoalRewards.reward_balancer,
|
||||
},
|
||||
|
||||
// 4
|
||||
{
|
||||
shape: "RuRu----", // processors t2
|
||||
required: 30,
|
||||
reward: enumHubGoalRewards.reward_rotater,
|
||||
},
|
||||
|
||||
// 5
|
||||
// Rotater
|
||||
{
|
||||
shape: "Cu----Cu", // belts t2
|
||||
required: 75,
|
||||
reward: enumHubGoalRewards.reward_tunnel,
|
||||
},
|
||||
|
||||
// 6
|
||||
// Painter
|
||||
{
|
||||
shape: "Cu------", // miners t2
|
||||
required: 50,
|
||||
reward: enumHubGoalRewards.reward_painter,
|
||||
},
|
||||
|
||||
// 7
|
||||
{
|
||||
shape: "CrCrCrCr", // unused
|
||||
required: 85,
|
||||
reward: enumHubGoalRewards.reward_rotater_ccw,
|
||||
},
|
||||
|
||||
// 8
|
||||
{
|
||||
shape: "RbRb----", // painter t2
|
||||
required: 100,
|
||||
reward: enumHubGoalRewards.reward_mixer,
|
||||
},
|
||||
{
|
||||
shape: "RpRp----",
|
||||
required: 0,
|
||||
reward: enumHubGoalRewards.reward_demo_end,
|
||||
},
|
||||
];
|
||||
|
||||
return levels;
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const STEAM_DEMO_LEVELS = () => [
|
||||
// 1
|
||||
// Circle
|
||||
{
|
||||
shape: "CuCuCuCu", // belts t1
|
||||
required: 35,
|
||||
reward: enumHubGoalRewards.reward_cutter_and_trash,
|
||||
},
|
||||
|
||||
// 2
|
||||
// Cutter
|
||||
{
|
||||
shape: "----CuCu", //
|
||||
required: 45,
|
||||
reward: enumHubGoalRewards.no_reward,
|
||||
},
|
||||
|
||||
// 3
|
||||
// Rectangle
|
||||
{
|
||||
shape: "RuRuRuRu", // miners t1
|
||||
required: 90,
|
||||
reward: enumHubGoalRewards.reward_balancer,
|
||||
},
|
||||
|
||||
// 4
|
||||
{
|
||||
shape: "RuRu----", // processors t2
|
||||
required: 70,
|
||||
reward: enumHubGoalRewards.reward_rotater,
|
||||
},
|
||||
|
||||
// 5
|
||||
// Rotater
|
||||
{
|
||||
shape: "Cu----Cu", // belts t2
|
||||
required: 160,
|
||||
reward: enumHubGoalRewards.reward_tunnel,
|
||||
},
|
||||
|
||||
// 6
|
||||
{
|
||||
shape: "Cu------", // miners t2
|
||||
required: 160,
|
||||
reward: enumHubGoalRewards.reward_painter,
|
||||
},
|
||||
|
||||
// 7
|
||||
// Painter
|
||||
{
|
||||
shape: "CrCrCrCr", // unused
|
||||
required: 140,
|
||||
reward: enumHubGoalRewards.reward_rotater_ccw,
|
||||
},
|
||||
// 8
|
||||
{
|
||||
shape: "RbRb----", // painter t2
|
||||
required: 225,
|
||||
reward: enumHubGoalRewards.reward_mixer,
|
||||
},
|
||||
// End of demo
|
||||
{
|
||||
shape: "CpCpCpCp",
|
||||
required: 0,
|
||||
reward: enumHubGoalRewards.reward_demo_end,
|
||||
},
|
||||
];
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const STANDALONE_LEVELS = () => [
|
||||
export const REGULAR_MODE_LEVELS = [
|
||||
// 1
|
||||
// Circle
|
||||
{
|
||||
@ -361,13 +203,3 @@ const STANDALONE_LEVELS = () => [
|
||||
reward: enumHubGoalRewards.reward_freeplay,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Generates the level definitions
|
||||
*/
|
||||
export function generateLevelsForVariant(app) {
|
||||
if (G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED) {
|
||||
return STANDALONE_LEVELS();
|
||||
}
|
||||
return WEB_DEMO_LEVELS(app);
|
||||
}
|
||||
|
||||
@ -34,8 +34,7 @@ import { HUDInteractiveTutorial } from "../hud/parts/interactive_tutorial";
|
||||
import { MetaBlockBuilding } from "../buildings/block";
|
||||
import { MetaItemProducerBuilding } from "../buildings/item_producer";
|
||||
import { MOD_SIGNALS } from "../../mods/mod_signals";
|
||||
import { finalGameShape, generateLevelsForVariant } from "./levels";
|
||||
import { WEB_STEAM_SSO_AUTHENTICATED } from "../../core/steam_sso";
|
||||
import { finalGameShape, REGULAR_MODE_LEVELS } from "./levels";
|
||||
|
||||
/** @typedef {{
|
||||
* shape: string,
|
||||
@ -63,16 +62,16 @@ const preparementShape = "CpRpCp--:SwSwSwSw";
|
||||
// Tiers need % of the previous tier as requirement too
|
||||
const tierGrowth = 2.5;
|
||||
|
||||
const upgradesCache = {};
|
||||
// TODO: Convert this file to TS and fix types. Maybe split the levels and upgrades as well
|
||||
let upgradesCache = null;
|
||||
|
||||
/**
|
||||
* Generates all upgrades
|
||||
* @returns {Object<string, UpgradeTiers>}
|
||||
*/
|
||||
function generateUpgrades(limitedVersion = false, difficulty = 1) {
|
||||
// TODO: Remove the limitedVersion parameter
|
||||
if (upgradesCache[limitedVersion]) {
|
||||
return upgradesCache[limitedVersion];
|
||||
function generateUpgrades() {
|
||||
if (upgradesCache) {
|
||||
return upgradesCache;
|
||||
}
|
||||
|
||||
const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1];
|
||||
@ -243,9 +242,6 @@ function generateUpgrades(limitedVersion = false, difficulty = 1) {
|
||||
const tierHandle = upgradeTiers[i];
|
||||
tierHandle.improvement = fixedImprovements[i];
|
||||
|
||||
tierHandle.required.forEach(required => {
|
||||
required.amount = Math.round(required.amount * difficulty);
|
||||
});
|
||||
const originalRequired = tierHandle.required.slice();
|
||||
|
||||
for (let k = currentTierRequirements.length - 1; k >= 0; --k) {
|
||||
@ -286,7 +282,7 @@ function generateUpgrades(limitedVersion = false, difficulty = 1) {
|
||||
}
|
||||
}
|
||||
|
||||
upgradesCache[limitedVersion] = upgrades;
|
||||
upgradesCache = upgrades;
|
||||
return upgrades;
|
||||
}
|
||||
|
||||
@ -295,12 +291,15 @@ let levelDefinitionsCache = null;
|
||||
/**
|
||||
* Generates the level definitions
|
||||
*/
|
||||
export function generateLevelDefinitions(app) {
|
||||
export function generateLevelDefinitions() {
|
||||
// NOTE: This cache is useless in production, but is there because of the G_IS_DEV validation
|
||||
if (levelDefinitionsCache) {
|
||||
return levelDefinitionsCache;
|
||||
}
|
||||
const levelDefinitions = generateLevelsForVariant(app);
|
||||
|
||||
const levelDefinitions = REGULAR_MODE_LEVELS;
|
||||
MOD_SIGNALS.modifyLevelDefinitions.dispatch(levelDefinitions);
|
||||
|
||||
if (G_IS_DEV) {
|
||||
levelDefinitions.forEach(({ shape }) => {
|
||||
try {
|
||||
@ -310,6 +309,7 @@ export function generateLevelDefinitions(app) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
levelDefinitionsCache = levelDefinitions;
|
||||
return levelDefinitions;
|
||||
}
|
||||
@ -366,19 +366,12 @@ export class RegularGameMode extends GameMode {
|
||||
];
|
||||
}
|
||||
|
||||
get difficultyMultiplicator() {
|
||||
if (G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED) {
|
||||
return 1;
|
||||
}
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return all available upgrades
|
||||
* @returns {Object<string, UpgradeTiers>}
|
||||
*/
|
||||
getUpgrades() {
|
||||
return generateUpgrades(false, this.difficultyMultiplicator);
|
||||
return generateUpgrades();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -386,7 +379,7 @@ export class RegularGameMode extends GameMode {
|
||||
* @returns {Array<LevelDefinition>}
|
||||
*/
|
||||
getLevelDefinitions() {
|
||||
return generateLevelDefinitions(this.root.app);
|
||||
return generateLevelDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
45
src/js/globals.d.ts
vendored
45
src/js/globals.d.ts
vendored
@ -46,54 +46,14 @@ declare interface Logger {
|
||||
error(...args);
|
||||
}
|
||||
|
||||
// Cordova
|
||||
declare interface Device {
|
||||
uuid: string;
|
||||
platform: string;
|
||||
available: boolean;
|
||||
version: string;
|
||||
cordova: string;
|
||||
model: string;
|
||||
manufacturer: string;
|
||||
isVirtual: boolean;
|
||||
serial: string;
|
||||
}
|
||||
|
||||
declare interface MobileAccessibility {
|
||||
usePreferredTextZoom(boolean);
|
||||
}
|
||||
|
||||
declare interface Window {
|
||||
// Cordova
|
||||
device: Device;
|
||||
StatusBar: any;
|
||||
AndroidFullScreen: any;
|
||||
AndroidNotch: any;
|
||||
plugins: any;
|
||||
|
||||
// Adinplay
|
||||
aiptag: any;
|
||||
adPlayer: any;
|
||||
aipPlayer: any;
|
||||
MobileAccessibility: MobileAccessibility;
|
||||
LocalFileSystem: any;
|
||||
|
||||
// Debugging
|
||||
activeClickDetectors: Array<any>;
|
||||
|
||||
// Experimental/Newer apis
|
||||
FontFace: any;
|
||||
TouchEvent: undefined | TouchEvent;
|
||||
|
||||
// Thirdparty
|
||||
XPayStationWidget: any;
|
||||
Sentry: any;
|
||||
LogRocket: any;
|
||||
grecaptcha: any;
|
||||
gtag: any;
|
||||
cpmstarAPI: any;
|
||||
CrazyGames: any;
|
||||
|
||||
// Mods
|
||||
$shapez_registerMod: any;
|
||||
anyModLoaded: any;
|
||||
@ -115,11 +75,6 @@ declare interface Navigator {
|
||||
splashscreen: any;
|
||||
}
|
||||
|
||||
// FontFace
|
||||
declare interface Document {
|
||||
fonts: any;
|
||||
}
|
||||
|
||||
// Webpack
|
||||
declare interface WebpackContext {
|
||||
keys(): Array<string>;
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
export class AdProviderInterface {
|
||||
/** @param {Application} app */
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the storage
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
initialize() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this provider serves ads at all
|
||||
* @returns {boolean}
|
||||
* @abstract
|
||||
*/
|
||||
getHasAds() {
|
||||
abstract;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if it would be possible to show a video ad *now*. This can be false if for
|
||||
* example the last video ad is
|
||||
* @returns {boolean}
|
||||
* @abstract
|
||||
*/
|
||||
getCanShowVideoAd() {
|
||||
abstract;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an video ad
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
showVideoAd() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
setPlayStatus(playing) {}
|
||||
}
|
||||
@ -1,191 +0,0 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../../application";
|
||||
/* typehints:end */
|
||||
|
||||
import { AdProviderInterface } from "../ad_provider";
|
||||
import { createLogger } from "../../core/logging";
|
||||
import { ClickDetector } from "../../core/click_detector";
|
||||
import { clamp } from "../../core/utils";
|
||||
import { T } from "../../translations";
|
||||
|
||||
const logger = createLogger("adprovider/adinplay");
|
||||
|
||||
const minimumTimeBetweenVideoAdsMs = G_IS_DEV ? 1 : 15 * 60 * 1000;
|
||||
|
||||
export class AdinplayAdProvider extends AdProviderInterface {
|
||||
/**
|
||||
*
|
||||
* @param {Application} app
|
||||
*/
|
||||
constructor(app) {
|
||||
super(app);
|
||||
|
||||
/** @type {ClickDetector} */
|
||||
this.getOnSteamClickDetector = null;
|
||||
|
||||
/** @type {Element} */
|
||||
this.adContainerMainElement = null;
|
||||
|
||||
/**
|
||||
* The resolve function to finish the current video ad. Null if none is currently running
|
||||
* @type {Function}
|
||||
*/
|
||||
this.videoAdResolveFunction = null;
|
||||
|
||||
/**
|
||||
* The current timer which will timeout the resolve
|
||||
*/
|
||||
this.videoAdResolveTimer = null;
|
||||
|
||||
/**
|
||||
* When we showed the last video ad
|
||||
*/
|
||||
this.lastVideoAdShowTime = -1e20;
|
||||
}
|
||||
|
||||
getHasAds() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getCanShowVideoAd() {
|
||||
return (
|
||||
this.getHasAds() &&
|
||||
!this.videoAdResolveFunction &&
|
||||
performance.now() - this.lastVideoAdShowTime > minimumTimeBetweenVideoAdsMs
|
||||
);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// No point to initialize everything if ads are not supported
|
||||
if (!this.getHasAds()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
logger.log("🎬 Initializing Adinplay");
|
||||
|
||||
// Add the preroll element
|
||||
this.adContainerMainElement = document.createElement("div");
|
||||
this.adContainerMainElement.id = "adinplayVideoContainer";
|
||||
this.adContainerMainElement.innerHTML = `
|
||||
<div class="adInner">
|
||||
<div class="videoInner">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add the setup script
|
||||
const setupScript = document.createElement("script");
|
||||
setupScript.textContent = `
|
||||
var aiptag = aiptag || {};
|
||||
aiptag.cmd = aiptag.cmd || [];
|
||||
aiptag.cmd.display = aiptag.cmd.display || [];
|
||||
aiptag.cmd.player = aiptag.cmd.player || [];
|
||||
`;
|
||||
document.head.appendChild(setupScript);
|
||||
|
||||
window.aiptag.gdprShowConsentTool = 0;
|
||||
window.aiptag.gdprAlternativeConsentTool = 1;
|
||||
window.aiptag.gdprConsent = 1;
|
||||
|
||||
const scale = this.app.getEffectiveUiScale();
|
||||
const targetW = 960;
|
||||
const targetH = 540;
|
||||
|
||||
const maxScaleX = (window.innerWidth - 100 * scale) / targetW;
|
||||
const maxScaleY = (window.innerHeight - 150 * scale) / targetH;
|
||||
|
||||
const scaleFactor = clamp(Math.min(maxScaleX, maxScaleY), 0.25, 2);
|
||||
|
||||
const w = Math.round(targetW * scaleFactor);
|
||||
const h = Math.round(targetH * scaleFactor);
|
||||
|
||||
// Add the player
|
||||
const videoElement = this.adContainerMainElement.querySelector(".videoInner");
|
||||
/** @type {HTMLElement} */
|
||||
const adInnerElement = this.adContainerMainElement.querySelector(".adInner");
|
||||
|
||||
adInnerElement.style.maxWidth = w + "px";
|
||||
|
||||
const self = this;
|
||||
window.aiptag.cmd.player.push(function () {
|
||||
window.adPlayer = new window.aipPlayer({
|
||||
AD_WIDTH: w,
|
||||
AD_HEIGHT: h,
|
||||
AD_FULLSCREEN: false,
|
||||
AD_CENTERPLAYER: false,
|
||||
LOADING_TEXT: T.global.loading,
|
||||
PREROLL_ELEM: function () {
|
||||
return videoElement;
|
||||
},
|
||||
AIP_COMPLETE: function () {
|
||||
logger.log("🎬 ADINPLAY AD: completed");
|
||||
self.adContainerMainElement.classList.add("waitingForFinish");
|
||||
},
|
||||
AIP_REMOVE: function () {
|
||||
logger.log("🎬 ADINPLAY AD: remove");
|
||||
if (self.videoAdResolveFunction) {
|
||||
self.videoAdResolveFunction();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Load the ads
|
||||
const aipScript = document.createElement("script");
|
||||
aipScript.src = "https://api.adinplay.com/libs/aiptag/pub/YRG/shapez.io/tag.min.js";
|
||||
aipScript.setAttribute("async", "");
|
||||
document.head.appendChild(aipScript);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
showVideoAd() {
|
||||
assert(this.getHasAds(), "Called showVideoAd but ads are not supported!");
|
||||
assert(!this.videoAdResolveFunction, "Video ad still running, can not show again!");
|
||||
this.lastVideoAdShowTime = performance.now();
|
||||
document.body.appendChild(this.adContainerMainElement);
|
||||
this.adContainerMainElement.classList.add("visible");
|
||||
this.adContainerMainElement.classList.remove("waitingForFinish");
|
||||
|
||||
try {
|
||||
// @ts-ignore
|
||||
window.aiptag.cmd.player.push(function () {
|
||||
console.log("🎬 ADINPLAY AD: Start pre roll");
|
||||
window.adPlayer.startPreRoll();
|
||||
});
|
||||
} catch (ex) {
|
||||
logger.warn("🎬 Failed to play video ad:", ex);
|
||||
document.body.removeChild(this.adContainerMainElement);
|
||||
this.adContainerMainElement.classList.remove("visible");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
// So, wait for the remove call but also remove after N seconds
|
||||
this.videoAdResolveFunction = () => {
|
||||
this.videoAdResolveFunction = null;
|
||||
clearTimeout(this.videoAdResolveTimer);
|
||||
this.videoAdResolveTimer = null;
|
||||
|
||||
// When the ad closed, also set the time
|
||||
this.lastVideoAdShowTime = performance.now();
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.videoAdResolveTimer = setTimeout(() => {
|
||||
logger.warn(this, "Automatically closing ad after not receiving callback");
|
||||
if (this.videoAdResolveFunction) {
|
||||
this.videoAdResolveFunction();
|
||||
}
|
||||
}, 120 * 1000);
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error("Error while resolving video ad:", err);
|
||||
})
|
||||
.then(() => {
|
||||
document.body.removeChild(this.adContainerMainElement);
|
||||
this.adContainerMainElement.classList.remove("visible");
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
import { AdProviderInterface } from "../ad_provider";
|
||||
import { createLogger } from "../../core/logging";
|
||||
import { timeoutPromise } from "../../core/utils";
|
||||
|
||||
const logger = createLogger("crazygames");
|
||||
|
||||
export class CrazygamesAdProvider extends AdProviderInterface {
|
||||
getHasAds() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getCanShowVideoAd() {
|
||||
return this.getHasAds() && this.sdkInstance;
|
||||
}
|
||||
|
||||
get sdkInstance() {
|
||||
try {
|
||||
return window.CrazyGames.CrazySDK.getInstance();
|
||||
} catch (ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if (!this.getHasAds()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
logger.log("🎬 Initializing crazygames SDK");
|
||||
|
||||
const scriptTag = document.createElement("script");
|
||||
scriptTag.type = "text/javascript";
|
||||
|
||||
return timeoutPromise(
|
||||
new Promise((resolve, reject) => {
|
||||
scriptTag.onload = resolve;
|
||||
scriptTag.onerror = reject;
|
||||
scriptTag.src = "https://sdk.crazygames.com/crazygames-sdk-v1.js";
|
||||
document.head.appendChild(scriptTag);
|
||||
})
|
||||
.then(() => {
|
||||
logger.log("🎬 Crazygames SDK loaded, now initializing");
|
||||
this.sdkInstance.init();
|
||||
})
|
||||
.catch(ex => {
|
||||
console.warn("Failed to init crazygames SDK:", ex);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
showVideoAd() {
|
||||
const instance = this.sdkInstance;
|
||||
if (!instance) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
logger.log("Set sound volume to 0");
|
||||
this.app.sound.setMusicVolume(0);
|
||||
this.app.sound.setSoundVolume(0);
|
||||
|
||||
return timeoutPromise(
|
||||
new Promise(resolve => {
|
||||
console.log("🎬 crazygames: Start ad");
|
||||
document.body.classList.add("externalAdOpen");
|
||||
|
||||
const finish = () => {
|
||||
instance.removeEventListener("adError", finish);
|
||||
instance.removeEventListener("adFinished", finish);
|
||||
resolve();
|
||||
};
|
||||
|
||||
instance.addEventListener("adError", finish);
|
||||
instance.addEventListener("adFinished", finish);
|
||||
|
||||
instance.requestAd();
|
||||
}),
|
||||
60000
|
||||
)
|
||||
.catch(ex => {
|
||||
console.warn("Error while resolving video ad:", ex);
|
||||
})
|
||||
.then(() => {
|
||||
document.body.classList.remove("externalAdOpen");
|
||||
|
||||
logger.log("Restored sound volume");
|
||||
|
||||
this.app.sound.setMusicVolume(this.app.settings.getSetting("musicVolume"));
|
||||
this.app.sound.setSoundVolume(this.app.settings.getSetting("soundVolume"));
|
||||
});
|
||||
}
|
||||
setPlayStatus(playing) {
|
||||
console.log("crazygames::playing:", playing);
|
||||
if (playing) {
|
||||
this.sdkInstance.gameplayStart();
|
||||
} else {
|
||||
this.sdkInstance.gameplayStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,131 +0,0 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../../application";
|
||||
/* typehints:end */
|
||||
|
||||
import { AdProviderInterface } from "../ad_provider";
|
||||
import { createLogger } from "../../core/logging";
|
||||
|
||||
const minimumTimeBetweenVideoAdsMs = G_IS_DEV ? 1 : 5 * 60 * 1000;
|
||||
|
||||
const logger = createLogger("gamedistribution");
|
||||
|
||||
export class GamedistributionAdProvider extends AdProviderInterface {
|
||||
/**
|
||||
*
|
||||
* @param {Application} app
|
||||
*/
|
||||
constructor(app) {
|
||||
super(app);
|
||||
|
||||
/**
|
||||
* The resolve function to finish the current video ad. Null if none is currently running
|
||||
* @type {Function}
|
||||
*/
|
||||
this.videoAdResolveFunction = null;
|
||||
|
||||
/**
|
||||
* The current timer which will timeout the resolve
|
||||
*/
|
||||
this.videoAdResolveTimer = null;
|
||||
|
||||
/**
|
||||
* When we showed the last video ad
|
||||
*/
|
||||
this.lastVideoAdShowTime = -1e20;
|
||||
}
|
||||
|
||||
getHasAds() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getCanShowVideoAd() {
|
||||
return (
|
||||
this.getHasAds() &&
|
||||
!this.videoAdResolveFunction &&
|
||||
performance.now() - this.lastVideoAdShowTime > minimumTimeBetweenVideoAdsMs
|
||||
);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
// No point to initialize everything if ads are not supported
|
||||
if (!this.getHasAds()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
logger.log("🎬 Initializing gamedistribution ads");
|
||||
|
||||
try {
|
||||
parent.postMessage("shapezio://gd.game_loaded", "*");
|
||||
} catch (ex) {
|
||||
return Promise.reject("Frame communication not allowed");
|
||||
}
|
||||
|
||||
window.addEventListener(
|
||||
"message",
|
||||
event => {
|
||||
if (event.data === "shapezio://gd.ad_started") {
|
||||
console.log("🎬 Got ad started callback");
|
||||
} else if (event.data === "shapezio://gd.ad_finished") {
|
||||
console.log("🎬 Got ad finished callback");
|
||||
if (this.videoAdResolveFunction) {
|
||||
this.videoAdResolveFunction();
|
||||
}
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
showVideoAd() {
|
||||
assert(this.getHasAds(), "Called showVideoAd but ads are not supported!");
|
||||
assert(!this.videoAdResolveFunction, "Video ad still running, can not show again!");
|
||||
this.lastVideoAdShowTime = performance.now();
|
||||
|
||||
console.log("🎬 Gamedistribution: Start ad");
|
||||
try {
|
||||
parent.postMessage("shapezio://gd.show_ad", "*");
|
||||
} catch (ex) {
|
||||
logger.warn("🎬 Failed to send message for gd ad:", ex);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
document.body.classList.add("externalAdOpen");
|
||||
|
||||
logger.log("Set sound volume to 0");
|
||||
this.app.sound.setMusicVolume(0);
|
||||
this.app.sound.setSoundVolume(0);
|
||||
|
||||
return new Promise(resolve => {
|
||||
// So, wait for the remove call but also remove after N seconds
|
||||
this.videoAdResolveFunction = () => {
|
||||
this.videoAdResolveFunction = null;
|
||||
clearTimeout(this.videoAdResolveTimer);
|
||||
this.videoAdResolveTimer = null;
|
||||
|
||||
// When the ad closed, also set the time
|
||||
this.lastVideoAdShowTime = performance.now();
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.videoAdResolveTimer = setTimeout(() => {
|
||||
logger.warn("Automatically closing ad after not receiving callback");
|
||||
if (this.videoAdResolveFunction) {
|
||||
this.videoAdResolveFunction();
|
||||
}
|
||||
}, 35000);
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error(this, "Error while resolving video ad:", err);
|
||||
})
|
||||
.then(() => {
|
||||
document.body.classList.remove("externalAdOpen");
|
||||
|
||||
logger.log("Restored sound volume");
|
||||
|
||||
this.app.sound.setMusicVolume(this.app.settings.getSetting("musicVolume"));
|
||||
this.app.sound.setSoundVolume(this.app.settings.getSetting("soundVolume"));
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
import { AdProviderInterface } from "../ad_provider";
|
||||
|
||||
export class NoAdProvider extends AdProviderInterface {
|
||||
getHasAds() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getCanShowVideoAd() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
export class AnalyticsInterface {
|
||||
constructor(app) {
|
||||
/** @type {Application} */
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the analytics
|
||||
* @returns {Promise<void>}
|
||||
* @abstract
|
||||
*/
|
||||
initialize() {
|
||||
abstract;
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the player name for analytics
|
||||
* @param {string} userName
|
||||
*/
|
||||
setUserContext(userName) {}
|
||||
|
||||
/**
|
||||
* Tracks when a new state is entered
|
||||
* @param {string} stateId
|
||||
*/
|
||||
trackStateEnter(stateId) {}
|
||||
|
||||
/**
|
||||
* Tracks a new user decision
|
||||
* @param {string} name
|
||||
*/
|
||||
trackDecision(name) {}
|
||||
|
||||
// LEGACY 1.5.3
|
||||
trackUiClick() {}
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
import { DialogWithForm } from "root/core/modal_dialog_elements";
|
||||
import { FormElementInput } from "root/core/modal_dialog_forms";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { compressX64 } from "../core/lzstring";
|
||||
import { timeoutPromise } from "../core/utils";
|
||||
@ -24,12 +27,7 @@ export class ClientAPI {
|
||||
}
|
||||
|
||||
getEndpoint() {
|
||||
if (G_IS_DEV) {
|
||||
return "http://localhost:15001";
|
||||
}
|
||||
if (window.location.host === "beta.shapez.io") {
|
||||
return "https://api-staging.shapez.io";
|
||||
}
|
||||
// TODO: Custom Puzzle DLC server / extract API into a mod?
|
||||
return "https://api.shapez.io";
|
||||
}
|
||||
|
||||
@ -100,32 +98,51 @@ export class ClientAPI {
|
||||
* @returns {Promise<{token: string}>}
|
||||
*/
|
||||
apiTryLogin() {
|
||||
if (!G_IS_STANDALONE) {
|
||||
let token = window.localStorage.getItem("steam_sso_auth_token");
|
||||
if (!token && G_IS_DEV) {
|
||||
token = window.prompt(
|
||||
"Please enter the auth token for the puzzle DLC (If you have none, you can't login):"
|
||||
);
|
||||
window.localStorage.setItem("dev_api_auth_token", token);
|
||||
}
|
||||
// TODO: Wrap the dialogs hack properly (with a meaningful error at least)
|
||||
// ...AND REDUCE THIS BOILERPLATE!!!
|
||||
let token = window.localStorage.getItem("dev_api_auth_token");
|
||||
|
||||
if (token !== null) {
|
||||
return Promise.resolve({ token });
|
||||
}
|
||||
|
||||
return timeoutPromise(ipcRenderer.invoke("steam:get-ticket"), 15000).then(
|
||||
ticket => {
|
||||
logger.log("Got auth ticket:", ticket);
|
||||
return this._request("/v1/public/login", {
|
||||
method: "POST",
|
||||
body: {
|
||||
token: ticket,
|
||||
},
|
||||
const state = this.app.stateMgr.getCurrentState();
|
||||
if (!("dialogs" in state)) {
|
||||
return Promise.reject(new Error("Failed to show token input dialog"));
|
||||
}
|
||||
|
||||
/** @type {import("../game/hud/parts/modal_dialogs").HUDModalDialogs} */
|
||||
// @ts-ignore
|
||||
const dialogs = state.dialogs;
|
||||
|
||||
const apiTokenInput = new FormElementInput({
|
||||
id: "apiToken",
|
||||
placeholder: "",
|
||||
validator: value => value.trim().length > 0,
|
||||
});
|
||||
|
||||
const dialog = new DialogWithForm({
|
||||
app: this.app,
|
||||
title: "API Login",
|
||||
desc: "Enter the Puzzle DLC API token:",
|
||||
formElements: [apiTokenInput],
|
||||
buttons: ["cancel", "ok:good"],
|
||||
closeButton: false,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
dialog.buttonSignals.ok.add(() => {
|
||||
resolve({
|
||||
token: apiTokenInput.getValue(),
|
||||
});
|
||||
},
|
||||
err => {
|
||||
logger.error("Failed to get auth ticket from steam: ", err);
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
dialog.buttonSignals.cancel.add(() => {
|
||||
reject(new Error("Token input dismissed"));
|
||||
});
|
||||
|
||||
dialogs.internalShowDialog(dialog);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,358 +0,0 @@
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { createLogger } from "../../core/logging";
|
||||
import { queryParamOptions } from "../../core/query_parameters";
|
||||
import { randomInt } from "../../core/utils";
|
||||
import { BeltComponent } from "../../game/components/belt";
|
||||
import { StaticMapEntityComponent } from "../../game/components/static_map_entity";
|
||||
import { RegularGameMode } from "../../game/modes/regular";
|
||||
import { GameRoot } from "../../game/root";
|
||||
import { InGameState } from "../../states/ingame";
|
||||
import { SteamAchievementProvider } from "../electron/steam_achievement_provider";
|
||||
import { GameAnalyticsInterface } from "../game_analytics";
|
||||
import { FILE_NOT_FOUND } from "../storage";
|
||||
import { WEB_STEAM_SSO_AUTHENTICATED } from "../../core/steam_sso";
|
||||
|
||||
const logger = createLogger("game_analytics");
|
||||
|
||||
const analyticsUrl = G_IS_DEV ? "http://localhost:8001" : "https://analytics.shapez.io";
|
||||
|
||||
// Be sure to increment the ID whenever it changes
|
||||
const analyticsLocalFile = "shapez_token_123.bin";
|
||||
|
||||
const CURRENT_ABT = "abt_bsl2";
|
||||
const CURRENT_ABT_COUNT = 1;
|
||||
|
||||
export class ShapezGameAnalytics extends GameAnalyticsInterface {
|
||||
constructor(app) {
|
||||
super(app);
|
||||
this.abtVariant = "0";
|
||||
}
|
||||
|
||||
get environment() {
|
||||
if (G_IS_DEV) {
|
||||
return "dev";
|
||||
}
|
||||
|
||||
if (G_IS_STANDALONE) {
|
||||
return "steam";
|
||||
}
|
||||
|
||||
if (WEB_STEAM_SSO_AUTHENTICATED) {
|
||||
return "prod-full";
|
||||
}
|
||||
|
||||
if (G_IS_RELEASE) {
|
||||
return "prod";
|
||||
}
|
||||
|
||||
if (window.location.host.indexOf("alpha") >= 0) {
|
||||
return "alpha";
|
||||
} else {
|
||||
return "beta";
|
||||
}
|
||||
}
|
||||
|
||||
fetchABVariant() {
|
||||
return this.app.storage.readFileAsync("shapez_" + CURRENT_ABT + ".bin").then(
|
||||
abt => {
|
||||
if (typeof queryParamOptions.abtVariant === "string") {
|
||||
this.abtVariant = queryParamOptions.abtVariant;
|
||||
logger.log("Set", CURRENT_ABT, "to (OVERRIDE) ", this.abtVariant);
|
||||
} else {
|
||||
this.abtVariant = abt;
|
||||
logger.log("Read abtVariant:", abt);
|
||||
}
|
||||
},
|
||||
err => {
|
||||
if (err === FILE_NOT_FOUND) {
|
||||
if (typeof queryParamOptions.abtVariant === "string") {
|
||||
this.abtVariant = queryParamOptions.abtVariant;
|
||||
logger.log("Set", CURRENT_ABT, "to (OVERRIDE) ", this.abtVariant);
|
||||
} else {
|
||||
this.abtVariant = String(randomInt(0, CURRENT_ABT_COUNT - 1));
|
||||
logger.log("Set", CURRENT_ABT, "to", this.abtVariant);
|
||||
}
|
||||
this.app.storage.writeFileAsync("shapez_" + CURRENT_ABT + ".bin", this.abtVariant);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
note(action) {
|
||||
// TODO: Remove game analytics altogether
|
||||
}
|
||||
|
||||
noteMinor(action, payload = "") {}
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
initialize() {
|
||||
this.syncKey = null;
|
||||
|
||||
// Retrieve sync key from player
|
||||
return this.fetchABVariant().then(() => {
|
||||
setInterval(() => this.sendTimePoints(), 60 * 1000);
|
||||
|
||||
return this.app.storage.readFileAsync(analyticsLocalFile).then(
|
||||
syncKey => {
|
||||
this.syncKey = syncKey;
|
||||
logger.log("Player sync key read:", this.syncKey);
|
||||
},
|
||||
error => {
|
||||
// File was not found, retrieve new key
|
||||
if (error === FILE_NOT_FOUND) {
|
||||
logger.log("Retrieving new player key");
|
||||
|
||||
let authTicket = Promise.resolve(undefined);
|
||||
|
||||
if (G_IS_STANDALONE) {
|
||||
logger.log("Will retrieve auth ticket");
|
||||
authTicket = ipcRenderer.invoke("steam:get-ticket");
|
||||
}
|
||||
|
||||
authTicket
|
||||
.then(
|
||||
ticket => {
|
||||
logger.log("Got ticket:", ticket);
|
||||
|
||||
// Perform call to get a new key from the API
|
||||
return this.sendToApi("/v1/register", {
|
||||
environment: this.environment,
|
||||
standalone:
|
||||
G_IS_STANDALONE &&
|
||||
this.app.achievementProvider instanceof SteamAchievementProvider,
|
||||
commit: G_BUILD_COMMIT_HASH,
|
||||
ticket,
|
||||
});
|
||||
},
|
||||
err => {
|
||||
logger.warn("Failed to get steam auth ticket for register:", err);
|
||||
}
|
||||
)
|
||||
.then(res => {
|
||||
// Try to read and parse the key from the api
|
||||
if (res.key && typeof res.key === "string" && res.key.length === 40) {
|
||||
this.syncKey = res.key;
|
||||
logger.log("Key retrieved:", this.syncKey);
|
||||
this.app.storage.writeFileAsync(analyticsLocalFile, res.key);
|
||||
} else {
|
||||
throw new Error("Bad response from analytics server: " + res);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error("Failed to register on analytics api:", err);
|
||||
});
|
||||
} else {
|
||||
logger.error("Failed to read ga key:", error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure a DLC is activated on steam
|
||||
* @param {string} dlc
|
||||
*/
|
||||
activateDlc(dlc) {
|
||||
logger.log("Activating dlc:", dlc);
|
||||
return this.sendToApi("/v1/activate-dlc/" + dlc, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the api
|
||||
* @param {string} endpoint Endpoint without base url
|
||||
* @param {object} data payload
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
sendToApi(endpoint, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject("Request to " + endpoint + " timed out"), 20000);
|
||||
|
||||
fetch(analyticsUrl + endpoint, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
cache: "no-cache",
|
||||
referrer: "no-referrer",
|
||||
credentials: "omit",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"x-api-key": globalConfig.info.analyticsApiKey,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(res => {
|
||||
clearTimeout(timeout);
|
||||
if (!res.ok || res.status !== 200) {
|
||||
reject("Fetch error: Bad status " + res.status);
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reason => {
|
||||
clearTimeout(timeout);
|
||||
reject(reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a game event to the analytics
|
||||
* @param {string} category
|
||||
* @param {string} value
|
||||
*/
|
||||
sendGameEvent(category, value) {
|
||||
if (G_IS_DEV) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.syncKey) {
|
||||
logger.warn("Can not send event due to missing sync key");
|
||||
return;
|
||||
}
|
||||
|
||||
const gameState = this.app.stateMgr.currentState;
|
||||
if (!(gameState instanceof InGameState)) {
|
||||
logger.warn("Trying to send analytics event outside of ingame state");
|
||||
return;
|
||||
}
|
||||
|
||||
const savegame = gameState.savegame;
|
||||
if (!savegame) {
|
||||
logger.warn("Ingame state has empty savegame");
|
||||
return;
|
||||
}
|
||||
|
||||
const savegameId = savegame.internalId;
|
||||
if (!gameState.core) {
|
||||
logger.warn("Game state has no core");
|
||||
return;
|
||||
}
|
||||
const root = gameState.core.root;
|
||||
if (!root) {
|
||||
logger.warn("Root is not initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(root.gameMode instanceof RegularGameMode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log("Sending event", category, value);
|
||||
|
||||
this.sendToApi("/v1/game-event", {
|
||||
playerKey: this.syncKey,
|
||||
gameKey: savegameId,
|
||||
ingameTime: root.time.now(),
|
||||
environment: this.environment,
|
||||
category,
|
||||
value,
|
||||
version: G_BUILD_VERSION,
|
||||
level: root.hubGoals.level,
|
||||
gameDump: this.generateGameDump(root),
|
||||
}).catch(err => {
|
||||
console.warn("Request failed", err);
|
||||
});
|
||||
}
|
||||
|
||||
sendTimePoints() {
|
||||
const gameState = this.app.stateMgr.currentState;
|
||||
if (gameState instanceof InGameState) {
|
||||
logger.log("Syncing analytics");
|
||||
this.sendGameEvent("sync", "");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the shape is interesting
|
||||
* @param {GameRoot} root
|
||||
* @param {string} key
|
||||
*/
|
||||
isInterestingShape(root, key) {
|
||||
if (key === root.gameMode.getBlueprintShapeKey()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if its a story goal
|
||||
const levels = root.gameMode.getLevelDefinitions();
|
||||
for (let i = 0; i < levels.length; ++i) {
|
||||
if (key === levels[i].shape) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if its required to unlock an upgrade
|
||||
const upgrades = root.gameMode.getUpgrades();
|
||||
for (const upgradeKey in upgrades) {
|
||||
const upgradeTiers = upgrades[upgradeKey];
|
||||
for (let i = 0; i < upgradeTiers.length; ++i) {
|
||||
const tier = upgradeTiers[i];
|
||||
const required = tier.required;
|
||||
for (let k = 0; k < required.length; ++k) {
|
||||
if (required[k].shape === key) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a game dump
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
generateGameDump(root) {
|
||||
const shapeIds = Object.keys(root.hubGoals.storedShapes).filter(key =>
|
||||
this.isInterestingShape(root, key)
|
||||
);
|
||||
let shapes = {};
|
||||
for (let i = 0; i < shapeIds.length; ++i) {
|
||||
shapes[shapeIds[i]] = root.hubGoals.storedShapes[shapeIds[i]];
|
||||
}
|
||||
return {
|
||||
shapes,
|
||||
upgrades: root.hubGoals.upgradeLevels,
|
||||
belts: root.entityMgr.getAllWithComponent(BeltComponent).length,
|
||||
buildings:
|
||||
root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length -
|
||||
root.entityMgr.getAllWithComponent(BeltComponent).length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
handleGameStarted() {
|
||||
this.sendGameEvent("game_start", "");
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
handleGameResumed() {
|
||||
this.sendTimePoints();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given level completed
|
||||
* @param {number} level
|
||||
*/
|
||||
handleLevelCompleted(level) {
|
||||
logger.log("Complete level", level);
|
||||
this.sendGameEvent("level_complete", "" + level);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given upgrade completed
|
||||
* @param {string} id
|
||||
* @param {number} level
|
||||
*/
|
||||
handleUpgradeUnlocked(id, level) {
|
||||
logger.log("Unlock upgrade", id, level);
|
||||
this.sendGameEvent("upgrade_unlock", id + "@" + level);
|
||||
}
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
import { AnalyticsInterface } from "../analytics";
|
||||
import { createLogger } from "../../core/logging";
|
||||
|
||||
const logger = createLogger("ga");
|
||||
|
||||
export class GoogleAnalyticsImpl extends AnalyticsInterface {
|
||||
initialize() {
|
||||
this.lastUiClickTracked = -1000;
|
||||
|
||||
setInterval(() => this.internalTrackAfkEvent(), 120 * 1000);
|
||||
|
||||
// Analytics is already loaded in the html
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
setUserContext(userName) {
|
||||
try {
|
||||
if (window.gtag) {
|
||||
logger.log("📊 Setting user context:", userName);
|
||||
window.gtag("set", {
|
||||
player: userName,
|
||||
});
|
||||
}
|
||||
} catch (ex) {
|
||||
logger.warn("📊 Failed to set user context:", ex);
|
||||
}
|
||||
}
|
||||
|
||||
trackStateEnter(stateId) {
|
||||
const nonInteractionStates = [
|
||||
"LoginState",
|
||||
"MainMenuState",
|
||||
"PreloadState",
|
||||
"RegisterState",
|
||||
"WatchAdState",
|
||||
];
|
||||
|
||||
try {
|
||||
if (window.gtag) {
|
||||
logger.log("📊 Tracking state enter:", stateId);
|
||||
window.gtag("event", "enter_state", {
|
||||
event_category: "ui",
|
||||
event_label: stateId,
|
||||
non_interaction: nonInteractionStates.indexOf(stateId) >= 0,
|
||||
});
|
||||
}
|
||||
} catch (ex) {
|
||||
logger.warn("📊 Failed to track state analytcis:", ex);
|
||||
}
|
||||
}
|
||||
|
||||
trackDecision(decisionName) {
|
||||
try {
|
||||
if (window.gtag) {
|
||||
logger.log("📊 Tracking decision:", decisionName);
|
||||
window.gtag("event", "decision", {
|
||||
event_category: "ui",
|
||||
event_label: decisionName,
|
||||
non_interaction: true,
|
||||
});
|
||||
}
|
||||
} catch (ex) {
|
||||
logger.warn("📊 Failed to track state analytcis:", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks an event so GA keeps track of the user
|
||||
*/
|
||||
internalTrackAfkEvent() {
|
||||
if (window.gtag) {
|
||||
window.gtag("event", "afk", {
|
||||
event_category: "ping",
|
||||
event_label: "timed",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,6 @@
|
||||
import { globalConfig, IS_MOBILE } from "../../core/config";
|
||||
import { createLogger } from "../../core/logging";
|
||||
import { queryParamOptions } from "../../core/query_parameters";
|
||||
import { WEB_STEAM_SSO_AUTHENTICATED } from "../../core/steam_sso";
|
||||
import { clamp } from "../../core/utils";
|
||||
import { CrazygamesAdProvider } from "../ad_providers/crazygames";
|
||||
import { GamedistributionAdProvider } from "../ad_providers/gamedistribution";
|
||||
import { NoAdProvider } from "../ad_providers/no_ad_provider";
|
||||
import { SteamAchievementProvider } from "../electron/steam_achievement_provider";
|
||||
import { PlatformWrapperInterface } from "../wrapper";
|
||||
import { NoAchievementProvider } from "./no_achievement_provider";
|
||||
@ -16,63 +11,7 @@ const logger = createLogger("platform/browser");
|
||||
|
||||
export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
|
||||
initialize() {
|
||||
this.recaptchaTokenCallback = null;
|
||||
|
||||
this.embedProvider = {
|
||||
id: "shapezio-website",
|
||||
adProvider: NoAdProvider,
|
||||
iframed: false,
|
||||
externalLinks: true,
|
||||
};
|
||||
|
||||
if (!G_IS_STANDALONE && !WEB_STEAM_SSO_AUTHENTICATED && queryParamOptions.embedProvider) {
|
||||
const providerId = queryParamOptions.embedProvider;
|
||||
this.embedProvider.iframed = true;
|
||||
|
||||
switch (providerId) {
|
||||
case "armorgames": {
|
||||
this.embedProvider.id = "armorgames";
|
||||
break;
|
||||
}
|
||||
|
||||
case "iogames.space": {
|
||||
this.embedProvider.id = "iogames.space";
|
||||
break;
|
||||
}
|
||||
|
||||
case "miniclip": {
|
||||
this.embedProvider.id = "miniclip";
|
||||
break;
|
||||
}
|
||||
|
||||
case "gamedistribution": {
|
||||
this.embedProvider.id = "gamedistribution";
|
||||
this.embedProvider.externalLinks = false;
|
||||
this.embedProvider.adProvider = GamedistributionAdProvider;
|
||||
break;
|
||||
}
|
||||
|
||||
case "kongregate": {
|
||||
this.embedProvider.id = "kongregate";
|
||||
break;
|
||||
}
|
||||
|
||||
case "crazygames": {
|
||||
this.embedProvider.id = "crazygames";
|
||||
this.embedProvider.adProvider = CrazygamesAdProvider;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
logger.error("Got unsupported embed provider:", providerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.log("Embed provider:", this.embedProvider.id);
|
||||
|
||||
return this.detectStorageImplementation()
|
||||
.then(() => this.initializeAdProvider())
|
||||
.then(() => this.initializeAchievementProvider())
|
||||
.then(() => super.initialize());
|
||||
}
|
||||
@ -113,7 +52,7 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
|
||||
}
|
||||
|
||||
getId() {
|
||||
return "browser@" + this.embedProvider.id;
|
||||
return "browser";
|
||||
}
|
||||
|
||||
getUiScale() {
|
||||
@ -143,54 +82,6 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if there is an adblocker installed
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
detectAdblock() {
|
||||
return Promise.race([
|
||||
new Promise(resolve => {
|
||||
// If the request wasn't blocked within a very short period of time, this means
|
||||
// the adblocker is not active and the request was actually made -> ignore it then
|
||||
setTimeout(() => resolve(false), 30);
|
||||
}),
|
||||
new Promise(resolve => {
|
||||
fetch("https://googleads.g.doubleclick.net/pagead/id", {
|
||||
method: "HEAD",
|
||||
mode: "no-cors",
|
||||
})
|
||||
.then(res => {
|
||||
resolve(false);
|
||||
})
|
||||
.catch(err => {
|
||||
resolve(true);
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
initializeAdProvider() {
|
||||
if (G_IS_DEV && !globalConfig.debug.testAds) {
|
||||
logger.log("Ads disabled in local environment");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// First, detect adblocker
|
||||
return this.detectAdblock().then(hasAdblocker => {
|
||||
if (hasAdblocker) {
|
||||
logger.log("Adblock detected");
|
||||
return;
|
||||
}
|
||||
|
||||
const adProvider = this.embedProvider.adProvider;
|
||||
this.app.adProvider = new adProvider(this.app);
|
||||
return this.app.adProvider.initialize().catch(err => {
|
||||
logger.error("Failed to initialize ad provider, disabling ads:", err);
|
||||
this.app.adProvider = new NoAdProvider(this.app);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initializeAchievementProvider() {
|
||||
if (G_IS_DEV && globalConfig.debug.testAchievements) {
|
||||
this.app.achievementProvider = new SteamAchievementProvider(this.app);
|
||||
|
||||
@ -57,10 +57,6 @@ export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
initializeAdProvider() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
initializeAchievementProvider() {
|
||||
return this.app.achievementProvider.initialize().catch(err => {
|
||||
logger.error("Failed to initialize achievement provider, disabling:", err);
|
||||
@ -76,17 +72,6 @@ export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser {
|
||||
res => {
|
||||
logger.log("Got DLC ownership:", res);
|
||||
this.dlcs.puzzle = Boolean(res);
|
||||
|
||||
if (this.dlcs.puzzle && !G_IS_DEV) {
|
||||
this.app.gameAnalytics.activateDlc("puzzle").then(
|
||||
() => {
|
||||
logger.log("Puzzle DLC successfully activated");
|
||||
},
|
||||
error => {
|
||||
logger.error("Failed to activate puzzle DLC:", error);
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
err => {
|
||||
logger.error("Failed to get DLC ownership:", err);
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
/**
|
||||
* @typedef {import("../application").Application} Application
|
||||
*/
|
||||
|
||||
export class GameAnalyticsInterface {
|
||||
constructor(app) {
|
||||
/** @type {Application} */
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the analytics
|
||||
* @returns {Promise<void>}
|
||||
* @abstract
|
||||
*/
|
||||
initialize() {
|
||||
abstract;
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a new game which was started
|
||||
*/
|
||||
handleGameStarted() {}
|
||||
|
||||
/**
|
||||
* Handles a resumed game
|
||||
*/
|
||||
handleGameResumed() {}
|
||||
|
||||
/**
|
||||
* Handles the given level completed
|
||||
* @param {number} level
|
||||
*/
|
||||
handleLevelCompleted(level) {}
|
||||
|
||||
/**
|
||||
* Handles the given upgrade completed
|
||||
* @param {string} id
|
||||
* @param {number} level
|
||||
*/
|
||||
handleUpgradeUnlocked(id, level) {}
|
||||
|
||||
/**
|
||||
* Activates a DLC
|
||||
* @param {string} dlc
|
||||
* @abstract
|
||||
*/
|
||||
activateDlc(dlc) {
|
||||
abstract;
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
@ -42,14 +42,6 @@ export class PlatformWrapperInterface {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should initialize the apps ad provider in case supported
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
initializeAdProvider() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return the minimum supported zoom level
|
||||
* @returns {number}
|
||||
|
||||
@ -3,7 +3,6 @@ import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
import { createLogger } from "../core/logging";
|
||||
import { WEB_STEAM_SSO_AUTHENTICATED } from "../core/steam_sso";
|
||||
import { T } from "../translations";
|
||||
|
||||
const logger = createLogger("setting_types");
|
||||
@ -153,13 +152,7 @@ export class EnumSetting extends BaseSetting {
|
||||
|
||||
return `
|
||||
<div class="setting cardbox ${available ? "enabled" : "disabled"}">
|
||||
${
|
||||
available
|
||||
? ""
|
||||
: `<span class="standaloneOnlyHint">${
|
||||
WEB_STEAM_SSO_AUTHENTICATED ? "" : T.demo.settingNotAvailable
|
||||
}</span>`
|
||||
}
|
||||
${available ? "" : `<span class="standaloneOnlyHint">${T.demo.settingNotAvailable}</span>`}
|
||||
<div class="row">
|
||||
<label>${T.settings.labels[this.id].title}</label>
|
||||
<div class="value enum" data-setting="${this.id}"></div>
|
||||
@ -234,16 +227,11 @@ export class BoolSetting extends BaseSetting {
|
||||
* @param {Application} app
|
||||
*/
|
||||
getHtml(app) {
|
||||
// TODO: Rewrite the settings system entirely
|
||||
const available = this.getIsAvailable(app);
|
||||
return `
|
||||
<div class="setting cardbox ${available ? "enabled" : "disabled"}">
|
||||
${
|
||||
available
|
||||
? ""
|
||||
: `<span class="standaloneOnlyHint">${
|
||||
WEB_STEAM_SSO_AUTHENTICATED ? "" : T.demo.settingNotAvailable
|
||||
}</span>`
|
||||
}
|
||||
${available ? "" : `<span class="standaloneOnlyHint">${T.demo.settingNotAvailable}</span>`}
|
||||
|
||||
<div class="row">
|
||||
<label>${T.settings.labels[this.id].title}</label>
|
||||
@ -303,13 +291,7 @@ export class RangeSetting extends BaseSetting {
|
||||
const available = this.getIsAvailable(app);
|
||||
return `
|
||||
<div class="setting cardbox ${available ? "enabled" : "disabled"}">
|
||||
${
|
||||
available
|
||||
? ""
|
||||
: `<span class="standaloneOnlyHint">${
|
||||
WEB_STEAM_SSO_AUTHENTICATED ? "" : T.demo.settingNotAvailable
|
||||
}</span>`
|
||||
}
|
||||
${available ? "" : `<span class="standaloneOnlyHint">${T.demo.settingNotAvailable}</span>`}
|
||||
|
||||
<div class="row">
|
||||
<label>${T.settings.labels[this.id].title}</label>
|
||||
|
||||
@ -262,7 +262,6 @@ export class InGameState extends GameState {
|
||||
if (this.savegame.hasGameDump()) {
|
||||
this.stage4bResumeGame();
|
||||
} else {
|
||||
this.app.gameAnalytics.handleGameStarted();
|
||||
this.stage4aInitEmptyGame();
|
||||
}
|
||||
},
|
||||
@ -300,7 +299,6 @@ export class InGameState extends GameState {
|
||||
this.onInitializationFailure("Savegame is corrupt and can not be restored.");
|
||||
return;
|
||||
}
|
||||
this.app.gameAnalytics.handleGameResumed();
|
||||
this.stage5FirstUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import { cachebust } from "../core/cachebust";
|
||||
import { globalConfig, openStandaloneLink, THIRDPARTY_URLS } from "../core/config";
|
||||
import { globalConfig, THIRDPARTY_URLS } from "../core/config";
|
||||
import { GameState } from "../core/game_state";
|
||||
import { DialogWithForm } from "../core/modal_dialog_elements";
|
||||
import { FormElementInput } from "../core/modal_dialog_forms";
|
||||
import { ReadWriteProxy } from "../core/read_write_proxy";
|
||||
import { STOP_PROPAGATION } from "../core/signal";
|
||||
import { WEB_STEAM_SSO_AUTHENTICATED } from "../core/steam_sso";
|
||||
import {
|
||||
formatSecondsToTimeAgo,
|
||||
generateFileDownload,
|
||||
@ -19,7 +17,6 @@ import {
|
||||
} from "../core/utils";
|
||||
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
||||
import { MODS } from "../mods/modloader";
|
||||
import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
|
||||
import { PlatformWrapperImplElectron } from "../platform/electron/wrapper";
|
||||
import { Savegame } from "../savegame/savegame";
|
||||
import { T } from "../translations";
|
||||
@ -32,40 +29,19 @@ import { T } from "../translations";
|
||||
export class MainMenuState extends GameState {
|
||||
constructor() {
|
||||
super("MainMenuState");
|
||||
|
||||
this.refreshInterval = null;
|
||||
}
|
||||
|
||||
getInnerHTML() {
|
||||
const showExitAppButton = G_IS_STANDALONE;
|
||||
const showPuzzleDLC = G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED;
|
||||
const hasMods = MODS.anyModsActive();
|
||||
|
||||
let showExternalLinks = true;
|
||||
|
||||
if (!G_IS_STANDALONE) {
|
||||
const wrapper = /** @type {PlatformWrapperImplBrowser} */ (this.app.platformWrapper);
|
||||
if (!wrapper.embedProvider.externalLinks) {
|
||||
showExternalLinks = false;
|
||||
}
|
||||
}
|
||||
|
||||
const ownsPuzzleDLC =
|
||||
WEB_STEAM_SSO_AUTHENTICATED ||
|
||||
(G_IS_STANDALONE &&
|
||||
/** @type { PlatformWrapperImplElectron}*/ (this.app.platformWrapper).dlcs.puzzle);
|
||||
|
||||
const showShapez2 = showExternalLinks && MODS.mods.length === 0;
|
||||
|
||||
return `
|
||||
<div class="topButtons">
|
||||
<button aria-label="Choose Language" class="languageChoose" data-languageicon="${this.app.settings.getLanguage()}"></button>
|
||||
|
||||
<button class="settingsButton" aria-label="Settings"></button>
|
||||
${showExitAppButton ? `<button class="exitAppButton" aria-label="Exit App"></button>` : ""}
|
||||
<button class="exitAppButton" aria-label="Exit App"></button>
|
||||
</div>
|
||||
|
||||
|
||||
<video autoplay muted loop class="fullscreenBackgroundVideo">
|
||||
<source src="${cachebust("res/bg_render.webm")}" type="video/webm">
|
||||
</video>
|
||||
@ -78,84 +54,25 @@ export class MainMenuState extends GameState {
|
||||
${/*showUpdateLabel ? `<span class="updateLabel">MODS UPDATE!</span>` : ""*/ ""}
|
||||
</div>
|
||||
|
||||
<div class="mainWrapper" data-columns="${showPuzzleDLC ? 2 : 1}">
|
||||
<div class="mainWrapper" data-columns="2">
|
||||
<div class="mainContainer">
|
||||
<div class="buttons"></div>
|
||||
<div class="savegamesMount"></div>
|
||||
${
|
||||
G_IS_STANDALONE || !WEB_STEAM_SSO_AUTHENTICATED
|
||||
? `<div class="steamSso">
|
||||
<span class="description">${
|
||||
G_IS_STANDALONE
|
||||
? T.mainMenu.playFullVersionStandalone
|
||||
: T.mainMenu.playFullVersionV2
|
||||
}</span>
|
||||
<a class="ssoSignIn" target="_blank" href="${
|
||||
this.app.clientApi.getEndpoint() + "/v1/noauth/steam-sso"
|
||||
}">Sign in</a>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
WEB_STEAM_SSO_AUTHENTICATED
|
||||
? `
|
||||
<div class="steamSso">
|
||||
<span class="description">${T.mainMenu.playingFullVersion}</span>
|
||||
<a class="ssoSignOut" href="?sso_logout_silent">${T.mainMenu.logout}</a>
|
||||
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="sideContainer">
|
||||
${
|
||||
showShapez2
|
||||
? `<div class="mainNews shapez2">
|
||||
<div class="text">We are currently prototyping Shapez 2!</div>
|
||||
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
showPuzzleDLC
|
||||
!hasMods
|
||||
? `
|
||||
|
||||
${
|
||||
ownsPuzzleDLC && !hasMods
|
||||
? `
|
||||
<div class="puzzleContainer owned">
|
||||
<button class="styledButton puzzleDlcPlayButton">${T.mainMenu.play}</button>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
|
||||
${
|
||||
!ownsPuzzleDLC && !hasMods
|
||||
? `
|
||||
<div class="puzzleContainer notOwned">
|
||||
<p>${T.mainMenu.puzzleDlcText}</p>
|
||||
<button class="styledButton puzzleDlcGetButton">${T.mainMenu.puzzleDlcViewNow}</button>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
|
||||
|
||||
|
||||
`
|
||||
<div class="puzzleContainer owned">
|
||||
<button class="styledButton puzzleDlcPlayButton">${T.mainMenu.play}</button>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
|
||||
|
||||
${
|
||||
hasMods
|
||||
? `
|
||||
|
||||
<div class="modsOverview">
|
||||
<div class="header">
|
||||
<h3>${T.mods.title}</h3>
|
||||
@ -177,96 +94,51 @@ export class MainMenuState extends GameState {
|
||||
<div class="dlcHint">
|
||||
${T.mainMenu.mods.warningPuzzleDLC}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer ${showExternalLinks ? "" : "noLinks"} ">
|
||||
<div class="footer">
|
||||
|
||||
<div class="socialLinks">
|
||||
${
|
||||
showExternalLinks
|
||||
? `<a class="patreonLink boxLink" target="_blank">
|
||||
<span class="thirdpartyLogo patreonLogo"></span>
|
||||
<span class="label">Patreon</span>
|
||||
</a>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
showExternalLinks && !G_IS_STANDALONE
|
||||
? `<a class="steamLinkSocial boxLink" target="_blank">
|
||||
<span class="thirdpartyLogo steamLogo"></span>
|
||||
<span class="label">steam</span>
|
||||
</a>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
showExternalLinks
|
||||
? `
|
||||
<a class="githubLink boxLink" target="_blank">
|
||||
<span class="thirdpartyLogo githubLogo"></span>
|
||||
<span class="label">GitHub</span>
|
||||
</a>`
|
||||
: ""
|
||||
}
|
||||
<div class="socialLinks">
|
||||
<a class="patreonLink boxLink" target="_blank">
|
||||
<span class="thirdpartyLogo patreonLogo"></span>
|
||||
<span class="label">Patreon</span>
|
||||
</a>
|
||||
|
||||
<a class="githubLink boxLink" target="_blank">
|
||||
<span class="thirdpartyLogo githubLogo"></span>
|
||||
<span class="label">GitHub</span>
|
||||
</a>
|
||||
|
||||
${
|
||||
showExternalLinks
|
||||
? `<a class="discordLink boxLink" target="_blank">
|
||||
<span class="thirdpartyLogo discordLogo"></span>
|
||||
<span class="label">Discord</span>
|
||||
</a>`
|
||||
: ""
|
||||
}
|
||||
<a class="discordLink boxLink" target="_blank">
|
||||
<span class="thirdpartyLogo discordLogo"></span>
|
||||
<span class="label">Discord</span>
|
||||
</a>
|
||||
|
||||
${
|
||||
showExternalLinks
|
||||
? `<a class="redditLink boxLink" target="_blank">
|
||||
<span class="thirdpartyLogo redditLogo"></span>
|
||||
<span class="label">Reddit</span>
|
||||
</a>`
|
||||
: ""
|
||||
}
|
||||
<a class="redditLink boxLink" target="_blank">
|
||||
<span class="thirdpartyLogo redditLogo"></span>
|
||||
<span class="label">Reddit</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
${
|
||||
/*
|
||||
showExternalLinks
|
||||
? `<a class="twitterLink boxLink" target="_blank">
|
||||
<span class="thirdpartyLogo twitterLogo"></span>
|
||||
<span class="label">Twitter</span>
|
||||
</a>`
|
||||
: ""
|
||||
*/
|
||||
""
|
||||
}
|
||||
<div class="footerGrow">
|
||||
<a class="changelog">${T.changelog.title}</a>
|
||||
<a class="helpTranslate">${T.mainMenu.helpTranslate}</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footerGrow">
|
||||
${showExternalLinks ? `<a class="changelog">${T.changelog.title}</a>` : ""}
|
||||
|
||||
${showExternalLinks ? `<a class="helpTranslate">${T.mainMenu.helpTranslate}</a>` : ""}
|
||||
|
||||
</div>
|
||||
<div class="author"><a class="producerLink" href="https://tobspr.io" target="_blank" title="tobspr Games" rel="follow">
|
||||
<div class="author">
|
||||
<a class="producerLink" href="https://tobspr.io" target="_blank" title="tobspr Games" rel="follow">
|
||||
<img src="${cachebust("res/logo-tobspr-games.svg")}" alt="tobspr Games"
|
||||
height="${25 * 0.8 * this.app.getEffectiveUiScale()}"
|
||||
width="${82 * 0.8 * this.app.getEffectiveUiScale()}"
|
||||
>
|
||||
|
||||
</a></div>
|
||||
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -274,8 +146,6 @@ export class MainMenuState extends GameState {
|
||||
* Asks the user to import a savegame
|
||||
*/
|
||||
requestImportSavegame() {
|
||||
this.app.gameAnalytics.note("startimport");
|
||||
|
||||
// Create a 'fake' file-input to accept savegames
|
||||
startFileChoose(".bin").then(file => {
|
||||
if (file) {
|
||||
@ -376,14 +246,10 @@ export class MainMenuState extends GameState {
|
||||
".settingsButton": this.onSettingsButtonClicked,
|
||||
".languageChoose": this.onLanguageChooseClicked,
|
||||
".redditLink": this.onRedditClicked,
|
||||
".twitterLink": this.onTwitterLinkClicked,
|
||||
".patreonLink": this.onPatreonLinkClicked,
|
||||
".changelog": this.onChangelogClicked,
|
||||
".helpTranslate": this.onTranslationHelpLinkClicked,
|
||||
".exitAppButton": this.onExitAppButtonClicked,
|
||||
".steamLink": this.onSteamLinkClicked,
|
||||
".steamLinkSocial": this.onSteamLinkClickedSocial,
|
||||
".shapez2": this.onShapez2Clicked,
|
||||
".discordLink": () => {
|
||||
this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.discord);
|
||||
},
|
||||
@ -392,7 +258,6 @@ export class MainMenuState extends GameState {
|
||||
},
|
||||
".puzzleDlcPlayButton": this.onPuzzleModeButtonClicked,
|
||||
".puzzleDlcGetButton": this.onPuzzleWishlistButtonClicked,
|
||||
".wegameDisclaimer > .rating": this.onWegameRatingClicked,
|
||||
".editMods": this.onModsClicked,
|
||||
};
|
||||
|
||||
@ -406,11 +271,6 @@ export class MainMenuState extends GameState {
|
||||
|
||||
this.renderMainMenu();
|
||||
this.renderSavegames();
|
||||
this.fetchPlayerCount();
|
||||
|
||||
this.refreshInterval = setInterval(() => this.fetchPlayerCount(), 10000);
|
||||
|
||||
this.app.gameAnalytics.noteMinor("menu.enter");
|
||||
}
|
||||
|
||||
renderMainMenu() {
|
||||
@ -458,26 +318,6 @@ export class MainMenuState extends GameState {
|
||||
buttonContainer.appendChild(outerDiv);
|
||||
}
|
||||
|
||||
fetchPlayerCount() {
|
||||
/** @type {HTMLDivElement} */
|
||||
const element = this.htmlElement.querySelector(".onlinePlayerCount");
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
fetch("https://analytics.shapez.io/v1/player-count", {
|
||||
cache: "no-cache",
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
count => {
|
||||
element.innerText = T.demoBanners.playerCount.replace("<playerCount>", String(count));
|
||||
},
|
||||
ex => {
|
||||
console.warn("Failed to get player count:", ex);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onPuzzleModeButtonClicked(force = false) {
|
||||
const hasUnlockedBlueprints = this.app.savegameMgr.getSavegamesMetaData().some(s => s.level >= 12);
|
||||
if (!force && !hasUnlockedBlueprints) {
|
||||
@ -499,25 +339,11 @@ export class MainMenuState extends GameState {
|
||||
this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.puzzleDlcStorePage);
|
||||
}
|
||||
|
||||
onShapez2Clicked() {
|
||||
this.app.platformWrapper.openExternalLink("https://tobspr.io/shapez-2?utm_medium=shapez");
|
||||
}
|
||||
|
||||
onBackButtonClicked() {
|
||||
this.renderMainMenu();
|
||||
this.renderSavegames();
|
||||
}
|
||||
|
||||
onSteamLinkClicked() {
|
||||
openStandaloneLink(this.app, "shapez_mainmenu");
|
||||
return false;
|
||||
}
|
||||
|
||||
onSteamLinkClickedSocial() {
|
||||
openStandaloneLink(this.app, "shapez_mainmenu_social");
|
||||
return false;
|
||||
}
|
||||
|
||||
onExitAppButtonClicked() {
|
||||
this.app.platformWrapper.exitApp();
|
||||
}
|
||||
@ -686,24 +512,22 @@ export class MainMenuState extends GameState {
|
||||
* @param {SavegameMetadata} game
|
||||
*/
|
||||
resumeGame(game) {
|
||||
this.app.adProvider.showVideoAd().then(() => {
|
||||
const savegame = this.app.savegameMgr.getSavegameById(game.internalId);
|
||||
savegame
|
||||
.readAsync()
|
||||
.then(() => this.checkForModDifferences(savegame))
|
||||
.then(() => {
|
||||
this.moveToState("InGameState", {
|
||||
savegame,
|
||||
});
|
||||
})
|
||||
|
||||
.catch(err => {
|
||||
this.dialogs.showWarning(
|
||||
T.dialogs.gameLoadFailure.title,
|
||||
T.dialogs.gameLoadFailure.text + "<br><br>" + err
|
||||
);
|
||||
const savegame = this.app.savegameMgr.getSavegameById(game.internalId);
|
||||
savegame
|
||||
.readAsync()
|
||||
.then(() => this.checkForModDifferences(savegame))
|
||||
.then(() => {
|
||||
this.moveToState("InGameState", {
|
||||
savegame,
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
.catch(err => {
|
||||
this.dialogs.showWarning(
|
||||
T.dialogs.gameLoadFailure.title,
|
||||
T.dialogs.gameLoadFailure.text + "<br><br>" + err
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -797,22 +621,6 @@ export class MainMenuState extends GameState {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a hint that the slot limit has been reached
|
||||
*/
|
||||
showSavegameSlotLimit() {
|
||||
const { getStandalone } = this.dialogs.showWarning(
|
||||
T.dialogs.oneSavegameLimit.title,
|
||||
T.dialogs.oneSavegameLimit.desc,
|
||||
["cancel:bad", "getStandalone:good"]
|
||||
);
|
||||
getStandalone.add(() => {
|
||||
openStandaloneLink(this.app, "shapez_slotlimit");
|
||||
});
|
||||
|
||||
this.app.gameAnalytics.note("slotlimit");
|
||||
}
|
||||
|
||||
onSettingsButtonClicked() {
|
||||
this.moveToState("SettingsState");
|
||||
}
|
||||
@ -824,30 +632,14 @@ export class MainMenuState extends GameState {
|
||||
}
|
||||
|
||||
onPlayButtonClicked() {
|
||||
this.app.adProvider.showVideoAd().then(() => {
|
||||
this.app.gameAnalytics.noteMinor("menu.play");
|
||||
const savegame = this.app.savegameMgr.createNewSavegame();
|
||||
const savegame = this.app.savegameMgr.createNewSavegame();
|
||||
|
||||
this.moveToState("InGameState", {
|
||||
savegame,
|
||||
});
|
||||
this.moveToState("InGameState", {
|
||||
savegame,
|
||||
});
|
||||
}
|
||||
|
||||
onWegameRatingClicked() {
|
||||
this.dialogs.showInfo(
|
||||
"提示说明:",
|
||||
`
|
||||
1)本游戏是一款休闲建造类单机游戏,画面简洁而乐趣充足。适用于年满8周岁及以上的用户,建议未成年人在家长监护下使用游戏产品。<br>
|
||||
2)本游戏模拟简单的生产流水线,剧情简单且积极向上,没有基于真实历史和现实事件的改编内容。游戏玩法为摆放简单的部件,完成生产目标。游戏为单机作品,没有基于文字和语音的陌生人社交系统。<br>
|
||||
3)本游戏中有用户实名认证系统,认证为未成年人的用户将接受以下管理:未满8周岁的用户不能付费;8周岁以上未满16周岁的未成年人用户,单次充值金额不得超过50元人民币,每月充值金额累计不得超过200元人民币;16周岁以上的未成年人用户,单次充值金额不得超过100元人民币,每月充值金额累计不得超过400元人民币。未成年玩家,仅可在周五、周六、周日和法定节假日每日20时至21时进行游戏。<br>
|
||||
4)游戏功能说明:一款关于传送带自动化生产特定形状产品的工厂流水线模拟游戏,画面简洁而乐趣充足,可以让玩家在轻松愉快的氛围下获得各种游戏乐趣,体验完成目标的成就感。游戏没有失败功能,自动存档,不存在较强的挫折体验。
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
onModsClicked() {
|
||||
this.app.gameAnalytics.noteMinor("menu.mods");
|
||||
this.moveToState("ModsState", {
|
||||
backToStateId: "MainMenuState",
|
||||
});
|
||||
@ -869,10 +661,8 @@ export class MainMenuState extends GameState {
|
||||
return;
|
||||
}
|
||||
|
||||
this.app.gameAnalytics.noteMinor("menu.continue");
|
||||
savegame
|
||||
.readAsync()
|
||||
.then(() => this.app.adProvider.showVideoAd())
|
||||
.then(() => this.checkForModDifferences(savegame))
|
||||
.then(() => {
|
||||
this.moveToState("InGameState", {
|
||||
@ -883,6 +673,5 @@ export class MainMenuState extends GameState {
|
||||
|
||||
onLeave() {
|
||||
this.dialogs.cleanup();
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { openStandaloneLink, THIRDPARTY_URLS } from "../core/config";
|
||||
import { WEB_STEAM_SSO_AUTHENTICATED } from "../core/steam_sso";
|
||||
import { THIRDPARTY_URLS } from "../core/config";
|
||||
import { TextualGameState } from "../core/textual_game_state";
|
||||
import { MODS } from "../mods/modloader";
|
||||
import { T } from "../translations";
|
||||
@ -14,10 +13,7 @@ export class ModsState extends TextualGameState {
|
||||
}
|
||||
|
||||
get modsSupported() {
|
||||
return (
|
||||
!WEB_STEAM_SSO_AUTHENTICATED &&
|
||||
(G_IS_STANDALONE || (G_IS_DEV && !window.location.href.includes("demo")))
|
||||
);
|
||||
return G_IS_STANDALONE || G_IS_DEV;
|
||||
}
|
||||
|
||||
internalGetFullHtml() {
|
||||
@ -53,11 +49,9 @@ export class ModsState extends TextualGameState {
|
||||
return `
|
||||
<div class="noModSupport">
|
||||
|
||||
<p>${WEB_STEAM_SSO_AUTHENTICATED ? T.mods.browserNoSupport : T.mods.noModSupport}</p>
|
||||
<p>${T.mods.noModSupport}</p>
|
||||
<br>
|
||||
<button class="styledButton browseMods">${T.mods.browseMods}</button>
|
||||
<a href="#" class="steamLink steam_dlbtn_0" target="_blank">Get on Steam!</a>
|
||||
|
||||
|
||||
</div>
|
||||
`;
|
||||
@ -107,10 +101,6 @@ export class ModsState extends TextualGameState {
|
||||
}
|
||||
|
||||
onEnter() {
|
||||
const steamLink = this.htmlElement.querySelector(".steamLink");
|
||||
if (steamLink) {
|
||||
this.trackClicks(steamLink, this.onSteamLinkClicked);
|
||||
}
|
||||
const openModsFolder = this.htmlElement.querySelector(".openModsFolder");
|
||||
if (openModsFolder) {
|
||||
this.trackClicks(openModsFolder, this.openModsFolder);
|
||||
@ -142,11 +132,6 @@ export class ModsState extends TextualGameState {
|
||||
this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.modBrowser);
|
||||
}
|
||||
|
||||
onSteamLinkClicked() {
|
||||
openStandaloneLink(this.app, "shapez_modsettings");
|
||||
return false;
|
||||
}
|
||||
|
||||
getDefaultPreviousState() {
|
||||
return "SettingsState";
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { CHANGELOG } from "../changelog";
|
||||
import { cachebust } from "../core/cachebust";
|
||||
import { globalConfig, THIRDPARTY_URLS } from "../core/config";
|
||||
import { globalConfig } from "../core/config";
|
||||
import { GameState } from "../core/game_state";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { queryParamOptions } from "../core/query_parameters";
|
||||
import { authorizeViaSSOToken } from "../core/steam_sso";
|
||||
import { getLogoSprite, timeoutPromise } from "../core/utils";
|
||||
import { getRandomHint } from "../game/hints";
|
||||
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
|
||||
@ -65,30 +63,7 @@ export class PreloadState extends GameState {
|
||||
}
|
||||
|
||||
async sendBeacon() {
|
||||
if (G_IS_STANDALONE) {
|
||||
return;
|
||||
}
|
||||
if (queryParamOptions.campaign) {
|
||||
fetch(
|
||||
"https://analytics.shapez.io/campaign/" +
|
||||
queryParamOptions.campaign +
|
||||
"?lpurl=nocontent&fbclid=" +
|
||||
(queryParamOptions.fbclid || "") +
|
||||
"&gclid=" +
|
||||
(queryParamOptions.gclid || "")
|
||||
).catch(err => {
|
||||
console.warn("Failed to send beacon:", err);
|
||||
});
|
||||
}
|
||||
if (queryParamOptions.embedProvider) {
|
||||
fetch(
|
||||
"https://analytics.shapez.io/campaign/embed_" +
|
||||
queryParamOptions.embedProvider +
|
||||
"?lpurl=nocontent"
|
||||
).catch(err => {
|
||||
console.warn("Failed to send beacon:", err);
|
||||
});
|
||||
}
|
||||
// TODO: Get rid of this analytics stuff
|
||||
}
|
||||
|
||||
onLeave() {
|
||||
@ -97,20 +72,7 @@ export class PreloadState extends GameState {
|
||||
|
||||
startLoading() {
|
||||
this.setStatus("Booting")
|
||||
.then(() => {
|
||||
try {
|
||||
window.localStorage.setItem("local_storage_feature_detection", "1");
|
||||
} catch (ex) {
|
||||
throw new Error(
|
||||
"Could not access local storage. Make sure you are not playing in incognito mode and allow thirdparty cookies!"
|
||||
);
|
||||
}
|
||||
})
|
||||
.then(() => this.setStatus("Creating platform wrapper", 3))
|
||||
|
||||
.then(() => this.sendBeacon())
|
||||
.then(() => authorizeViaSSOToken(this.app, this.dialogs))
|
||||
|
||||
.then(() => this.app.platformWrapper.initialize())
|
||||
|
||||
.then(() => this.setStatus("Initializing local storage", 6))
|
||||
@ -139,10 +101,6 @@ export class PreloadState extends GameState {
|
||||
return this.app.storage.initialize();
|
||||
})
|
||||
|
||||
.then(() => this.setStatus("Initializing libraries", 12))
|
||||
.then(() => this.app.analytics.initialize())
|
||||
.then(() => this.app.gameAnalytics.initialize())
|
||||
|
||||
.then(() => this.setStatus("Connecting to api", 15))
|
||||
.then(() => this.fetchDiscounts())
|
||||
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import { GameState } from "../core/game_state";
|
||||
|
||||
export class WegameSplashState extends GameState {
|
||||
constructor() {
|
||||
super("WegameSplashState");
|
||||
}
|
||||
|
||||
getInnerHTML() {
|
||||
return `
|
||||
<div class="wrapper">
|
||||
<strong>健康游戏忠告</strong>
|
||||
<div>抵制不良游戏,拒绝盗版游戏。</div>
|
||||
<div>注意自我保护,谨防受骗上当。</div>
|
||||
<div>适度游戏益脑,沉迷游戏伤身。</div>
|
||||
<div>合理安排时间,享受健康生活。</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
onEnter() {
|
||||
setTimeout(
|
||||
() => {
|
||||
this.app.stateMgr.moveToState("PreloadState");
|
||||
},
|
||||
G_IS_DEV ? 1 : 6000
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user