1
0
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:
Даниїл Григор'єв 2024-04-16 10:25:16 +03:00 committed by GitHub
parent 62b170a92d
commit aa49f063c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 180 additions and 2404 deletions

View File

@ -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 }))
);
}

View File

@ -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,
});
}
});
}

View File

@ -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) {

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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";

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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.
}
/**

View File

@ -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!

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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) {

View File

@ -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";

View File

@ -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() {

View File

@ -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() {

View File

@ -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);
}

View File

@ -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
View File

@ -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>;

View File

@ -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) {}
}

View File

@ -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");
});
}
}

View File

@ -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();
}
}
}

View File

@ -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"));
});
}
}

View File

@ -1,11 +0,0 @@
import { AdProviderInterface } from "../ad_provider";
export class NoAdProvider extends AdProviderInterface {
getHasAds() {
return false;
}
getCanShowVideoAd() {
return false;
}
}

View File

@ -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() {}
}

View File

@ -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);
});
}
/**

View File

@ -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);
}
}

View File

@ -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",
});
}
}
}

View File

@ -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);

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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";
}

View File

@ -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())

View File

@ -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
);
}
}