mirror of
https://github.com/tobspr/shapez.io.git
synced 2026-03-02 03:39:21 +00:00
Initial commit
This commit is contained in:
45
src/js/platform/ad_provider.js
Normal file
45
src/js/platform/ad_provider.js
Normal file
@@ -0,0 +1,45 @@
|
||||
/* 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}
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
getCanShowVideoAd() {
|
||||
abstract;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows an video ad
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
showVideoAd() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
188
src/js/platform/ad_providers/adinplay.js
Normal file
188
src/js/platform/ad_providers/adinplay.js
Normal file
@@ -0,0 +1,188 @@
|
||||
/* 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 { performanceNow } from "../../core/builtins";
|
||||
import { clamp } from "../../core/utils";
|
||||
|
||||
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 &&
|
||||
performanceNow() - 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");
|
||||
this.adContainerMainElement.querySelector(".adInner").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: "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 = performanceNow();
|
||||
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 = performanceNow();
|
||||
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");
|
||||
});
|
||||
}
|
||||
}
|
||||
11
src/js/platform/ad_providers/no_ad_provider.js
Normal file
11
src/js/platform/ad_providers/no_ad_provider.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AdProviderInterface } from "../ad_provider";
|
||||
|
||||
export class NoAdProvider extends AdProviderInterface {
|
||||
getHasAds() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getCanShowVideoAd() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
43
src/js/platform/analytics.js
Normal file
43
src/js/platform/analytics.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
export class AnalyticsInterface {
|
||||
constructor(app) {
|
||||
/** @type {Application} */
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the analytics
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
initialize() {
|
||||
abstract;
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the player name for analytics
|
||||
* @param {string} userName
|
||||
*/
|
||||
setUserContext(userName) {}
|
||||
|
||||
/**
|
||||
* Tracks a click no an ui element
|
||||
* @param {string} elementName
|
||||
*/
|
||||
trackUiClick(elementName) {}
|
||||
|
||||
/**
|
||||
* Tracks when a new state is entered
|
||||
* @param {string} stateId
|
||||
*/
|
||||
trackStateEnter(stateId) {}
|
||||
|
||||
/**
|
||||
* Tracks a new user decision
|
||||
* @param {string} name
|
||||
*/
|
||||
trackDecision(name) {}
|
||||
}
|
||||
47
src/js/platform/browser/embed_provider.js
Normal file
47
src/js/platform/browser/embed_provider.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { AdProviderInterface } from "../ad_provider";
|
||||
|
||||
/**
|
||||
* Stores information about where we are iframed
|
||||
*/
|
||||
export class EmbedProvider {
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
getId() {
|
||||
abstract;
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this provider supports ads
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getSupportsAds() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ad provider
|
||||
* @returns {typeof AdProviderInterface}
|
||||
*/
|
||||
getAdProvider() {
|
||||
abstract;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whetherexternal links are supported
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getSupportsExternalLinks() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this provider is iframed
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getIsIframed() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
16
src/js/platform/browser/embed_providers/armorgames.js
Normal file
16
src/js/platform/browser/embed_providers/armorgames.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { AdinplayAdProvider } from "../../ad_providers/adinplay";
|
||||
import { ShapezioWebsiteEmbedProvider } from "./shapezio_website";
|
||||
|
||||
export class ArmorgamesEmbedProvider extends ShapezioWebsiteEmbedProvider {
|
||||
getId() {
|
||||
return "armorgames";
|
||||
}
|
||||
|
||||
getAdProvider() {
|
||||
return AdinplayAdProvider;
|
||||
}
|
||||
|
||||
getIsIframed() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
11
src/js/platform/browser/embed_providers/crazygames.js
Normal file
11
src/js/platform/browser/embed_providers/crazygames.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ShapezioWebsiteEmbedProvider } from "./shapezio_website";
|
||||
|
||||
export class CrazygamesEmbedProvider extends ShapezioWebsiteEmbedProvider {
|
||||
getId() {
|
||||
return "crazygames";
|
||||
}
|
||||
|
||||
getIsIframed() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
20
src/js/platform/browser/embed_providers/gamedistribution.js
Normal file
20
src/js/platform/browser/embed_providers/gamedistribution.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { AdinplayAdProvider } from "../../ad_providers/adinplay";
|
||||
import { ShapezioWebsiteEmbedProvider } from "./shapezio_website";
|
||||
|
||||
export class GamedistributionEmbedProvider extends ShapezioWebsiteEmbedProvider {
|
||||
getId() {
|
||||
return "gamedistribution";
|
||||
}
|
||||
|
||||
getAdProvider() {
|
||||
return AdinplayAdProvider;
|
||||
}
|
||||
|
||||
getSupportsExternalLinks() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getIsIframed() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
15
src/js/platform/browser/embed_providers/iogames_space.js
Normal file
15
src/js/platform/browser/embed_providers/iogames_space.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ShapezioWebsiteEmbedProvider } from "./shapezio_website";
|
||||
|
||||
export class IogamesSpaceEmbedProvider extends ShapezioWebsiteEmbedProvider {
|
||||
getId() {
|
||||
return "iogames.space";
|
||||
}
|
||||
|
||||
getShowUpvoteHints() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getIsIframed() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
24
src/js/platform/browser/embed_providers/kongregate.js
Normal file
24
src/js/platform/browser/embed_providers/kongregate.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NoAdProvider } from "../../ad_providers/no_ad_provider";
|
||||
import { EmbedProvider } from "../embed_provider";
|
||||
|
||||
export class KongregateEmbedProvider extends EmbedProvider {
|
||||
getId() {
|
||||
return "kongregate";
|
||||
}
|
||||
|
||||
getSupportsAds() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getAdProvider() {
|
||||
return NoAdProvider;
|
||||
}
|
||||
|
||||
getSupportsExternalLinks() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getIsIframed() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
11
src/js/platform/browser/embed_providers/miniclip.js
Normal file
11
src/js/platform/browser/embed_providers/miniclip.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ShapezioWebsiteEmbedProvider } from "./shapezio_website";
|
||||
|
||||
export class MiniclipEmbedProvider extends ShapezioWebsiteEmbedProvider {
|
||||
getId() {
|
||||
return "miniclip";
|
||||
}
|
||||
|
||||
getIsIframed() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
24
src/js/platform/browser/embed_providers/shapezio_website.js
Normal file
24
src/js/platform/browser/embed_providers/shapezio_website.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { EmbedProvider } from "../embed_provider";
|
||||
import { AdinplayAdProvider } from "../../ad_providers/adinplay";
|
||||
|
||||
export class ShapezioWebsiteEmbedProvider extends EmbedProvider {
|
||||
getId() {
|
||||
return "shapezio";
|
||||
}
|
||||
|
||||
getSupportsAds() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getAdProvider() {
|
||||
return AdinplayAdProvider;
|
||||
}
|
||||
|
||||
getIsIframed() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getSupportsExternalLinks() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
69
src/js/platform/browser/game_analytics.js
Normal file
69
src/js/platform/browser/game_analytics.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { GameAnalyticsInterface } from "../game_analytics";
|
||||
import { createLogger } from "../../core/logging";
|
||||
import { ShapeDefinition } from "../../game/shape_definition";
|
||||
import { gameCreationAction } from "../../states/ingame";
|
||||
|
||||
const logger = createLogger("ga_com");
|
||||
|
||||
export class GameAnalyticsDotCom extends GameAnalyticsInterface {
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
initialize() {
|
||||
try {
|
||||
const ga = window.gameanalytics.GameAnalytics;
|
||||
ga.configureBuild(G_APP_ENVIRONMENT + "@" + G_BUILD_VERSION + "@" + G_BUILD_COMMIT_HASH);
|
||||
|
||||
ga.setEnabledInfoLog(G_IS_DEV);
|
||||
ga.setEnabledVerboseLog(G_IS_DEV);
|
||||
|
||||
// @ts-ignore
|
||||
ga.initialize(window.ga_comKey, window.ga_comToken);
|
||||
|
||||
// start new session
|
||||
ga.startSession();
|
||||
} catch (ex) {
|
||||
logger.warn("ga_com init error:", ex);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ShapeDefinition} definition
|
||||
*/
|
||||
handleShapeDelivered(definition) {
|
||||
const hash = definition.getHash();
|
||||
logger.log("Deliver:", hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given level completed
|
||||
* @param {number} level
|
||||
*/
|
||||
handleLevelCompleted(level) {
|
||||
logger.log("Complete level", level);
|
||||
try {
|
||||
const gaD = window.gameanalytics;
|
||||
const ga = gaD.GameAnalytics;
|
||||
ga.addProgressionEvent(gaD.EGAProgressionStatus.Complete, "story", "" + level);
|
||||
} catch (ex) {
|
||||
logger.error("ga_com lvl complete error:", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given upgrade completed
|
||||
* @param {string} id
|
||||
* @param {number} level
|
||||
*/
|
||||
handleUpgradeUnlocked(id, level) {
|
||||
logger.log("Unlock upgrade", id, level);
|
||||
try {
|
||||
const gaD = window.gameanalytics;
|
||||
const ga = gaD.GameAnalytics;
|
||||
ga.addProgressionEvent(gaD.EGAProgressionStatus.Complete, "upgrade", id, "" + level);
|
||||
} catch (ex) {
|
||||
logger.error("ga_com upgrade unlock error:", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
101
src/js/platform/browser/google_analytics.js
Normal file
101
src/js/platform/browser/google_analytics.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import { AnalyticsInterface } from "../analytics";
|
||||
import { Math_random, performanceNow } from "../../core/builtins";
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
trackUiClick(elementName) {
|
||||
// Only track a fraction of clicks to not annoy google analytics
|
||||
if (Math_random() < 0.9) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stateKey = this.app.stateMgr.getCurrentState().key;
|
||||
const fullSelector = stateKey + ">" + elementName;
|
||||
|
||||
try {
|
||||
if (window.gtag) {
|
||||
logger.log("📊 Tracking click on:", fullSelector);
|
||||
window.gtag("event", "click", {
|
||||
event_category: "ui",
|
||||
event_label: fullSelector,
|
||||
});
|
||||
}
|
||||
} catch (ex) {
|
||||
logger.warn("📊 Failed to track ui click:", 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",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
146
src/js/platform/browser/sound.js
Normal file
146
src/js/platform/browser/sound.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import { MusicInstanceInterface, SoundInstanceInterface, SoundInterface } from "../sound";
|
||||
import { cachebust } from "../../core/cachebust";
|
||||
import { createLogger } from "../../core/logging";
|
||||
|
||||
const { Howl, Howler } = require("howler");
|
||||
|
||||
const logger = createLogger("sound/browser");
|
||||
|
||||
class SoundInstance extends SoundInstanceInterface {
|
||||
constructor(key, url) {
|
||||
super(key, url);
|
||||
this.howl = null;
|
||||
this.instance = null;
|
||||
}
|
||||
|
||||
load() {
|
||||
return Promise.race([
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(reject, G_IS_DEV ? 5000 : 60000);
|
||||
}),
|
||||
new Promise(resolve => {
|
||||
this.howl = new Howl({
|
||||
src: cachebust("res/sounds/" + this.url),
|
||||
autoplay: false,
|
||||
loop: false,
|
||||
volume: 0,
|
||||
preload: true,
|
||||
onload: () => {
|
||||
resolve();
|
||||
},
|
||||
onloaderror: (id, err) => {
|
||||
logger.warn("Sound", this.url, "failed to load:", id, err);
|
||||
this.howl = null;
|
||||
resolve();
|
||||
},
|
||||
onplayerror: (id, err) => {
|
||||
logger.warn("Sound", this.url, "failed to play:", id, err);
|
||||
},
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
play(volume) {
|
||||
if (this.howl) {
|
||||
if (!this.instance) {
|
||||
this.instance = this.howl.play();
|
||||
} else {
|
||||
this.howl.play(this.instance);
|
||||
this.howl.seek(0, this.instance);
|
||||
}
|
||||
this.howl.volume(volume, this.instance);
|
||||
}
|
||||
}
|
||||
|
||||
deinitialize() {
|
||||
if (this.howl) {
|
||||
this.howl.unload();
|
||||
this.howl = null;
|
||||
this.instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MusicInstance extends MusicInstanceInterface {
|
||||
constructor(key, url) {
|
||||
super(key, url);
|
||||
this.howl = null;
|
||||
this.instance = null;
|
||||
this.playing = false;
|
||||
}
|
||||
load() {
|
||||
return Promise.race([
|
||||
new Promise((resolve, reject) => {
|
||||
setTimeout(reject, G_IS_DEV ? 5000 : 60000);
|
||||
}),
|
||||
new Promise((resolve, reject) => {
|
||||
this.howl = new Howl({
|
||||
src: cachebust("res/sounds/music/" + this.url),
|
||||
autoplay: false,
|
||||
loop: true,
|
||||
html5: true,
|
||||
volume: 1,
|
||||
preload: true,
|
||||
pool: 2,
|
||||
|
||||
onload: () => {
|
||||
resolve();
|
||||
},
|
||||
onloaderror: (id, err) => {
|
||||
logger.warn(this, "Music", this.url, "failed to load:", id, err);
|
||||
this.howl = null;
|
||||
resolve();
|
||||
},
|
||||
onplayerror: (id, err) => {
|
||||
logger.warn(this, "Music", this.url, "failed to play:", id, err);
|
||||
},
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.howl && this.instance) {
|
||||
this.playing = false;
|
||||
this.howl.pause(this.instance);
|
||||
}
|
||||
}
|
||||
|
||||
isPlaying() {
|
||||
return this.playing;
|
||||
}
|
||||
|
||||
play() {
|
||||
if (this.howl) {
|
||||
this.playing = true;
|
||||
if (this.instance) {
|
||||
this.howl.play(this.instance);
|
||||
} else {
|
||||
this.instance = this.howl.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinitialize() {
|
||||
if (this.howl) {
|
||||
this.howl.unload();
|
||||
this.howl = null;
|
||||
this.instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SoundImplBrowser extends SoundInterface {
|
||||
constructor(app) {
|
||||
super(app, SoundInstance, MusicInstance);
|
||||
}
|
||||
|
||||
initialize() {
|
||||
return super.initialize();
|
||||
}
|
||||
|
||||
deinitialize() {
|
||||
return super.deinitialize().then(() => Howler.unload());
|
||||
}
|
||||
}
|
||||
97
src/js/platform/browser/storage.js
Normal file
97
src/js/platform/browser/storage.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import { FILE_NOT_FOUND, StorageInterface } from "../storage";
|
||||
import { createLogger } from "../../core/logging";
|
||||
|
||||
const logger = createLogger("storage/browser");
|
||||
|
||||
const LOCAL_STORAGE_UNAVAILABLE = "local-storage-unavailable";
|
||||
const LOCAL_STORAGE_NO_WRITE_PERMISSION = "local-storage-no-write-permission";
|
||||
|
||||
let randomDelay = () => 0;
|
||||
|
||||
if (G_IS_DEV) {
|
||||
// Random delay for testing
|
||||
// randomDelay = () => 500;
|
||||
}
|
||||
|
||||
export class StorageImplBrowser extends StorageInterface {
|
||||
constructor(app) {
|
||||
super(app);
|
||||
this.currentBusyFilename = false;
|
||||
}
|
||||
|
||||
initialize() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Check for local storage availability in general
|
||||
if (!window.localStorage) {
|
||||
alert("Local storage is not available! Please upgrade to a newer browser!");
|
||||
reject(LOCAL_STORAGE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
// Check if we can set and remove items
|
||||
try {
|
||||
window.localStorage.setItem("storage_availability_test", "1");
|
||||
window.localStorage.removeItem("storage_availability_test");
|
||||
} catch (e) {
|
||||
alert(
|
||||
"It seems we don't have permission to write to local storage! Please update your browsers settings or use a different browser!"
|
||||
);
|
||||
reject(LOCAL_STORAGE_NO_WRITE_PERMISSION);
|
||||
return;
|
||||
}
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
|
||||
writeFileAsync(filename, contents) {
|
||||
if (this.currentBusyFilename === filename) {
|
||||
logger.warn("Attempt to write", filename, "while write process is not finished!");
|
||||
}
|
||||
|
||||
this.currentBusyFilename = filename;
|
||||
window.localStorage.setItem(filename, contents);
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
this.currentBusyFilename = false;
|
||||
resolve();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
writeFileSyncIfSupported(filename, contents) {
|
||||
window.localStorage.setItem(filename, contents);
|
||||
return true;
|
||||
}
|
||||
|
||||
readFileAsync(filename) {
|
||||
if (this.currentBusyFilename === filename) {
|
||||
logger.warn("Attempt to read", filename, "while write progress on it is ongoing!");
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const contents = window.localStorage.getItem(filename);
|
||||
if (!contents) {
|
||||
// File not found
|
||||
setTimeout(() => reject(FILE_NOT_FOUND), randomDelay());
|
||||
return;
|
||||
}
|
||||
|
||||
// File read, simulate delay
|
||||
setTimeout(() => resolve(contents), 0);
|
||||
});
|
||||
}
|
||||
|
||||
deleteFileAsync(filename) {
|
||||
if (this.currentBusyFilename === filename) {
|
||||
logger.warn("Attempt to delete", filename, "while write progres on it is ongoing!");
|
||||
}
|
||||
|
||||
this.currentBusyFilename = filename;
|
||||
return new Promise((resolve, reject) => {
|
||||
window.localStorage.removeItem(filename);
|
||||
setTimeout(() => {
|
||||
this.currentBusyFilename = false;
|
||||
resolve();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
242
src/js/platform/browser/wrapper.js
Normal file
242
src/js/platform/browser/wrapper.js
Normal file
@@ -0,0 +1,242 @@
|
||||
import { Math_min } from "../../core/builtins";
|
||||
import { createLogger } from "../../core/logging";
|
||||
import { queryParamOptions } from "../../core/query_parameters";
|
||||
import { clamp } from "../../core/utils";
|
||||
import { globalConfig, IS_MOBILE } from "../../core/config";
|
||||
import { NoAdProvider } from "../ad_providers/no_ad_provider";
|
||||
import { PlatformWrapperInterface } from "../wrapper";
|
||||
import { ShapezioWebsiteEmbedProvider } from "./embed_providers/shapezio_website";
|
||||
import { ArmorgamesEmbedProvider } from "./embed_providers/armorgames";
|
||||
import { IogamesSpaceEmbedProvider } from "./embed_providers/iogames_space";
|
||||
import { MiniclipEmbedProvider } from "./embed_providers/miniclip";
|
||||
import { GamedistributionEmbedProvider } from "./embed_providers/gamedistribution";
|
||||
import { KongregateEmbedProvider } from "./embed_providers/kongregate";
|
||||
import { CrazygamesEmbedProvider } from "./embed_providers/crazygames";
|
||||
import { EmbedProvider } from "./embed_provider";
|
||||
|
||||
const logger = createLogger("platform/browser");
|
||||
|
||||
export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
|
||||
initialize() {
|
||||
this.recaptchaTokenCallback = null;
|
||||
|
||||
this.embedProvider = new ShapezioWebsiteEmbedProvider();
|
||||
|
||||
if (!G_IS_STANDALONE && queryParamOptions.embedProvider) {
|
||||
const providerId = queryParamOptions.embedProvider;
|
||||
|
||||
switch (providerId) {
|
||||
case "armorgames": {
|
||||
this.embedProvider = new ArmorgamesEmbedProvider();
|
||||
break;
|
||||
}
|
||||
|
||||
case "iogames.space": {
|
||||
this.embedProvider = new IogamesSpaceEmbedProvider();
|
||||
break;
|
||||
}
|
||||
|
||||
case "miniclip": {
|
||||
this.embedProvider = new MiniclipEmbedProvider();
|
||||
break;
|
||||
}
|
||||
|
||||
case "gamedistribution": {
|
||||
this.embedProvider = new GamedistributionEmbedProvider();
|
||||
break;
|
||||
}
|
||||
|
||||
case "kongregate": {
|
||||
this.embedProvider = new KongregateEmbedProvider();
|
||||
break;
|
||||
}
|
||||
|
||||
case "crazygames": {
|
||||
this.embedProvider = new CrazygamesEmbedProvider();
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
logger.error("Got unsupported embed provider:", providerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.log("Embed provider:", this.embedProvider.getId());
|
||||
|
||||
return super.initialize().then(() => {
|
||||
// SENTRY
|
||||
if (!G_IS_DEV && false) {
|
||||
logger.log(this, "Loading sentry");
|
||||
const sentryTag = document.createElement("script");
|
||||
sentryTag.src = "https://browser.sentry-cdn.com/5.7.1/bundle.min.js";
|
||||
sentryTag.setAttribute("integrity", "TODO_SENTRY");
|
||||
sentryTag.setAttribute("crossorigin", "anonymous");
|
||||
sentryTag.addEventListener("load", this.onSentryLoaded.bind(this));
|
||||
document.head.appendChild(sentryTag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {EmbedProvider}
|
||||
*/
|
||||
getEmbedProvider() {
|
||||
return this.embedProvider;
|
||||
}
|
||||
|
||||
onSentryLoaded() {
|
||||
logger.log("Initializing sentry");
|
||||
window.Sentry.init({
|
||||
dsn: "TODO SENTRY DSN",
|
||||
release: G_APP_ENVIRONMENT + "-" + G_BUILD_VERSION + "@" + G_BUILD_COMMIT_HASH,
|
||||
// Will cause a deprecation warning, but the demise of `ignoreErrors` is still under discussion.
|
||||
// See: https://github.com/getsentry/raven-js/issues/73
|
||||
ignoreErrors: [
|
||||
// Random plugins/extensions
|
||||
"top.GLOBALS",
|
||||
// See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html
|
||||
"originalCreateNotification",
|
||||
"canvas.contentDocument",
|
||||
"MyApp_RemoveAllHighlights",
|
||||
"http://tt.epicplay.com",
|
||||
"Can't find variable: ZiteReader",
|
||||
"jigsaw is not defined",
|
||||
"ComboSearch is not defined",
|
||||
"http://loading.retry.widdit.com/",
|
||||
"atomicFindClose",
|
||||
// Facebook borked
|
||||
"fb_xd_fragment",
|
||||
// ISP "optimizing" proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha)
|
||||
// See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy
|
||||
"bmi_SafeAddOnload",
|
||||
"EBCallBackMessageReceived",
|
||||
// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
|
||||
"conduitPage",
|
||||
// Generic error code from errors outside the security sandbox
|
||||
// You can delete this if using raven.js > 1.0, which ignores these automatically.
|
||||
"Script error.",
|
||||
|
||||
// Errors from ads
|
||||
"Cannot read property 'postMessage' of null",
|
||||
|
||||
// Firefox only
|
||||
"AbortError: The operation was aborted.",
|
||||
|
||||
"<unknown>",
|
||||
],
|
||||
ignoreUrls: [
|
||||
// Facebook flakiness
|
||||
/graph\.facebook\.com/i,
|
||||
// Facebook blocked
|
||||
/connect\.facebook\.net\/en_US\/all\.js/i,
|
||||
// Woopra flakiness
|
||||
/eatdifferent\.com\.woopra-ns\.com/i,
|
||||
/static\.woopra\.com\/js\/woopra\.js/i,
|
||||
// Chrome extensions
|
||||
/extensions\//i,
|
||||
/^chrome:\/\//i,
|
||||
// Other plugins
|
||||
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
|
||||
/webappstoolbarba\.texthelp\.com\//i,
|
||||
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
|
||||
],
|
||||
beforeSend(event, hint) {
|
||||
if (window.anyModLoaded) {
|
||||
return null;
|
||||
}
|
||||
return event;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getId() {
|
||||
return "browser@" + this.embedProvider.getId();
|
||||
}
|
||||
|
||||
getUiScale() {
|
||||
if (IS_MOBILE) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const avgDims = Math_min(this.app.screenWidth, this.app.screenHeight);
|
||||
return clamp((avgDims / 1000.0) * 1.9, 0.1, 10);
|
||||
}
|
||||
|
||||
getSupportsRestart() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getTouchPanStrength() {
|
||||
return IS_MOBILE ? 1 : 0.5;
|
||||
}
|
||||
|
||||
openExternalLink(url, force = false) {
|
||||
logger.log("Opening external:", url);
|
||||
// if (force || this.embedProvider.getSupportsExternalLinks()) {
|
||||
window.open(url);
|
||||
// } else {
|
||||
// // Do nothing
|
||||
// alert("This platform does not allow opening external links. You can play on the website directly to open them.");
|
||||
// }
|
||||
}
|
||||
|
||||
getSupportsAds() {
|
||||
return this.embedProvider.getSupportsAds();
|
||||
}
|
||||
|
||||
performRestart() {
|
||||
logger.log("Performing restart");
|
||||
window.location.reload(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// First, detect adblocker
|
||||
return this.detectAdblock().then(hasAdblocker => {
|
||||
if (hasAdblocker) {
|
||||
return;
|
||||
}
|
||||
|
||||
const adProvider = this.embedProvider.getAdProvider();
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exitApp() {
|
||||
// Can not exit app
|
||||
}
|
||||
}
|
||||
38
src/js/platform/game_analytics.js
Normal file
38
src/js/platform/game_analytics.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
import { ShapeDefinition } from "../game/shape_definition";
|
||||
/* typehints:end */
|
||||
|
||||
export class GameAnalyticsInterface {
|
||||
constructor(app) {
|
||||
/** @type {Application} */
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the analytics
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
initialize() {
|
||||
abstract;
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ShapeDefinition} definition
|
||||
*/
|
||||
handleShapeDelivered(definition) {}
|
||||
|
||||
/**
|
||||
* Handles the given level completed
|
||||
* @param {number} level
|
||||
*/
|
||||
handleLevelCompleted(level) {}
|
||||
|
||||
/**
|
||||
* Handles the given upgrade completed
|
||||
* @param {string} id
|
||||
* @param {number} level
|
||||
*/
|
||||
handleUpgradeUnlocked(id, level) {}
|
||||
}
|
||||
277
src/js/platform/sound.js
Normal file
277
src/js/platform/sound.js
Normal file
@@ -0,0 +1,277 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
import { Vector } from "../core/vector";
|
||||
import { GameRoot } from "../game/root";
|
||||
/* typehints:end */
|
||||
|
||||
import { newEmptyMap, clamp } from "../core/utils";
|
||||
import { createLogger } from "../core/logging";
|
||||
import { globalConfig } from "../core/config";
|
||||
|
||||
const logger = createLogger("sound");
|
||||
|
||||
export const SOUNDS = {
|
||||
// Menu and such
|
||||
uiClick: "ui/ui_click.mp3",
|
||||
uiError: "ui/ui_error.mp3",
|
||||
dialogError: "ui/dialog_error.mp3",
|
||||
dialogOk: "ui/dialog_ok.mp3",
|
||||
swishHide: "ui/ui_swish_hide.mp3",
|
||||
swishShow: "ui/ui_swish_show.mp3",
|
||||
};
|
||||
|
||||
export const MUSIC = {
|
||||
mainMenu: "main_menu.mp3",
|
||||
gameBg: "theme_full.mp3",
|
||||
};
|
||||
|
||||
export class SoundInstanceInterface {
|
||||
constructor(key, url) {
|
||||
this.key = key;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/** @returns {Promise<void>} */
|
||||
load() {
|
||||
abstract;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
play(volume) {
|
||||
abstract;
|
||||
}
|
||||
|
||||
deinitialize() {}
|
||||
}
|
||||
|
||||
export class MusicInstanceInterface {
|
||||
constructor(key, url) {
|
||||
this.key = key;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
stop() {
|
||||
abstract;
|
||||
}
|
||||
|
||||
play() {
|
||||
abstract;
|
||||
}
|
||||
|
||||
/** @returns {Promise<void>} */
|
||||
load() {
|
||||
abstract;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
isPlaying() {
|
||||
abstract;
|
||||
return false;
|
||||
}
|
||||
|
||||
deinitialize() {}
|
||||
}
|
||||
|
||||
export class SoundInterface {
|
||||
constructor(app, soundClass, musicClass) {
|
||||
/** @type {Application} */
|
||||
this.app = app;
|
||||
|
||||
this.soundClass = soundClass;
|
||||
this.musicClass = musicClass;
|
||||
|
||||
/** @type {Object<string, SoundInstanceInterface>} */
|
||||
this.sounds = newEmptyMap();
|
||||
|
||||
/** @type {Object<string, MusicInstanceInterface>} */
|
||||
this.music = newEmptyMap();
|
||||
|
||||
/** @type {MusicInstanceInterface} */
|
||||
this.currentMusic = null;
|
||||
|
||||
this.pageIsVisible = true;
|
||||
|
||||
this.musicMuted = false;
|
||||
this.soundsMuted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the sound
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
initialize() {
|
||||
for (const soundKey in SOUNDS) {
|
||||
const soundPath = SOUNDS[soundKey];
|
||||
const sound = new this.soundClass(soundKey, soundPath);
|
||||
this.sounds[soundPath] = sound;
|
||||
}
|
||||
|
||||
for (const musicKey in MUSIC) {
|
||||
const musicPath = MUSIC[musicKey];
|
||||
const music = new this.musicClass(musicKey, musicPath);
|
||||
this.music[musicPath] = music;
|
||||
}
|
||||
|
||||
// this.musicMuted = this.app.userProfile.getMusicMuted();
|
||||
// this.soundsMuted = this.app.userProfile.getSoundsMuted();
|
||||
|
||||
this.musicMuted = false;
|
||||
this.soundsMuted = false;
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.disableMusic) {
|
||||
this.musicMuted = true;
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-Loads the given sounds
|
||||
* @param {string} key
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadSound(key) {
|
||||
if (this.sounds[key]) {
|
||||
return this.sounds[key].load();
|
||||
} else if (this.music[key]) {
|
||||
return this.music[key].load();
|
||||
} else {
|
||||
logger.error("Sound/Music by key not found:", key);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/** Deinits the sound */
|
||||
deinitialize() {
|
||||
const promises = [];
|
||||
for (const key in this.sounds) {
|
||||
promises.push(this.sounds[key].deinitialize());
|
||||
}
|
||||
for (const key in this.music) {
|
||||
promises.push(this.music[key].deinitialize());
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the music is muted
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getMusicMuted() {
|
||||
return this.musicMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if sounds are muted
|
||||
* @returns {boolean}
|
||||
*/
|
||||
getSoundsMuted() {
|
||||
return this.soundsMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if the music is muted
|
||||
* @param {boolean} muted
|
||||
*/
|
||||
setMusicMuted(muted) {
|
||||
this.musicMuted = muted;
|
||||
if (this.musicMuted) {
|
||||
if (this.currentMusic) {
|
||||
this.currentMusic.stop();
|
||||
}
|
||||
} else {
|
||||
if (this.currentMusic) {
|
||||
this.currentMusic.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets if the sounds are muted
|
||||
* @param {boolean} muted
|
||||
*/
|
||||
setSoundsMuted(muted) {
|
||||
this.soundsMuted = muted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus change handler, called by the pap
|
||||
* @param {boolean} pageIsVisible
|
||||
*/
|
||||
onPageRenderableStateChanged(pageIsVisible) {
|
||||
this.pageIsVisible = pageIsVisible;
|
||||
if (this.currentMusic) {
|
||||
if (pageIsVisible) {
|
||||
if (!this.currentMusic.isPlaying() && !this.musicMuted) {
|
||||
this.currentMusic.play();
|
||||
}
|
||||
} else {
|
||||
this.currentMusic.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
playUiSound(key) {
|
||||
if (this.soundsMuted) {
|
||||
return;
|
||||
}
|
||||
if (!this.sounds[key]) {
|
||||
logger.warn("Sound", key, "not found, probably not loaded yet");
|
||||
return;
|
||||
}
|
||||
this.sounds[key].play(1.0);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {Vector} worldPosition
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
play3DSound(key, worldPosition, root) {
|
||||
if (!this.sounds[key]) {
|
||||
logger.warn("Music", key, "not found, probably not loaded yet");
|
||||
return;
|
||||
}
|
||||
if (!this.pageIsVisible || this.soundsMuted) {
|
||||
return;
|
||||
}
|
||||
|
||||
// hack, but works
|
||||
if (root.time.getIsPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let volume = 1.0;
|
||||
if (!root.camera.isWorldPointOnScreen(worldPosition)) {
|
||||
volume = 0.2;
|
||||
}
|
||||
volume *= clamp(root.camera.zoomLevel / 3);
|
||||
this.sounds[key].play(clamp(volume));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
playThemeMusic(key) {
|
||||
const music = this.music[key];
|
||||
if (key !== null && !music) {
|
||||
logger.warn("Music", key, "not found");
|
||||
}
|
||||
if (this.currentMusic !== music) {
|
||||
if (this.currentMusic) {
|
||||
logger.log("Stopping", this.currentMusic.key);
|
||||
this.currentMusic.stop();
|
||||
}
|
||||
this.currentMusic = music;
|
||||
if (music && this.pageIsVisible && !this.musicMuted) {
|
||||
logger.log("Starting", this.currentMusic.key);
|
||||
music.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/js/platform/storage.js
Normal file
62
src/js/platform/storage.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
export const FILE_NOT_FOUND = "file_not_found";
|
||||
|
||||
export class StorageInterface {
|
||||
constructor(app) {
|
||||
/** @type {Application} */
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the storage
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
initialize() {
|
||||
abstract;
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a string to a file asynchronously
|
||||
* @param {string} filename
|
||||
* @param {string} contents
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
writeFileAsync(filename, contents) {
|
||||
abstract;
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to write a file synchronously, used in unload handler
|
||||
* @param {string} filename
|
||||
* @param {string} contents
|
||||
*/
|
||||
writeFileSyncIfSupported(filename, contents) {
|
||||
abstract;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a string asynchronously. Returns Promise<FILE_NOT_FOUND> if file was not found.
|
||||
* @param {string} filename
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
readFileAsync(filename) {
|
||||
abstract;
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to delete a file
|
||||
* @param {string} filename
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
deleteFileAsync(filename) {
|
||||
// Default implementation does not allow deleting files
|
||||
return Promise.reject();
|
||||
}
|
||||
}
|
||||
131
src/js/platform/wrapper.js
Normal file
131
src/js/platform/wrapper.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/* typehints:start */
|
||||
import { Application } from "../application";
|
||||
/* typehints:end */
|
||||
|
||||
import { IS_MOBILE } from "../core/config";
|
||||
|
||||
export class PlatformWrapperInterface {
|
||||
constructor(app) {
|
||||
/** @type {Application} */
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/** @returns {string} */
|
||||
getId() {
|
||||
abstract;
|
||||
return "unknown-platform";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UI scale, called on every resize
|
||||
* @returns {number} */
|
||||
getUiScale() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
getSupportsRestart() {
|
||||
abstract;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the strength of touch pans with the mouse
|
||||
*/
|
||||
getTouchPanStrength() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** @returns {Promise<void>} */
|
||||
initialize() {
|
||||
document.documentElement.classList.add("p-" + this.getId());
|
||||
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}
|
||||
*/
|
||||
getMinimumZoom() {
|
||||
return 0.2 * this.getScreenScale();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return the maximum supported zoom level
|
||||
* @returns {number}
|
||||
*/
|
||||
getMaximumZoom() {
|
||||
return 4 * this.getScreenScale();
|
||||
}
|
||||
|
||||
getScreenScale() {
|
||||
return Math.min(window.innerWidth, window.innerHeight) / 1024.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should return if this platform supports ads at all
|
||||
*/
|
||||
getSupportsAds() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to open an external url
|
||||
* @param {string} url
|
||||
* @param {boolean=} force Whether to always open the url even if not allowed
|
||||
*/
|
||||
openExternalLink(url, force = false) {
|
||||
abstract;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to restart the app
|
||||
*/
|
||||
performRestart() {
|
||||
abstract;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this platform supports a toggleable fullscreen
|
||||
*/
|
||||
getSupportsFullscreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should set the apps fullscreen state to the desired state
|
||||
* @param {boolean} flag
|
||||
*/
|
||||
setFullscreen(flag) {
|
||||
abstract;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this platform supports quitting the app
|
||||
*/
|
||||
getSupportsAppExit() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to quit the app
|
||||
*/
|
||||
exitApp() {
|
||||
abstract;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this platform supports a keyboard
|
||||
*/
|
||||
getSupportsKeyboard() {
|
||||
return !IS_MOBILE;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user