2020-05-09 14:45:23 +00:00
|
|
|
/* typehints:start */
|
|
|
|
import { Application } from "../application";
|
|
|
|
import { StateManager } from "./state_manager";
|
|
|
|
/* typehints:end */
|
|
|
|
|
|
|
|
import { globalConfig } from "./config";
|
|
|
|
import { ClickDetector } from "./click_detector";
|
|
|
|
import { logSection, createLogger } from "./logging";
|
|
|
|
import { InputReceiver } from "./input_receiver";
|
|
|
|
import { waitNextFrame } from "./utils";
|
|
|
|
import { RequestChannel } from "./request_channel";
|
|
|
|
import { MUSIC } from "../platform/sound";
|
|
|
|
|
|
|
|
const logger = createLogger("game_state");
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Basic state of the game state machine. This is the base of the whole game
|
|
|
|
*/
|
|
|
|
export class GameState {
|
|
|
|
/**
|
|
|
|
* Constructs a new state with the given id
|
|
|
|
* @param {string} key The id of the state. We use ids to refer to states because otherwise we get
|
|
|
|
* circular references
|
|
|
|
*/
|
|
|
|
constructor(key) {
|
|
|
|
this.key = key;
|
|
|
|
|
|
|
|
/** @type {StateManager} */
|
|
|
|
this.stateManager = null;
|
|
|
|
|
|
|
|
/** @type {Application} */
|
|
|
|
this.app = null;
|
|
|
|
|
|
|
|
// Store if we are currently fading out
|
|
|
|
this.fadingOut = false;
|
|
|
|
|
|
|
|
/** @type {Array<ClickDetector>} */
|
|
|
|
this.clickDetectors = [];
|
|
|
|
|
|
|
|
// Every state captures keyboard events by default
|
|
|
|
this.inputReciever = new InputReceiver("state-" + key);
|
|
|
|
this.inputReciever.backButton.add(this.onBackButton, this);
|
|
|
|
|
|
|
|
// A channel we can use to perform async ops
|
|
|
|
this.asyncChannel = new RequestChannel();
|
|
|
|
}
|
|
|
|
|
|
|
|
//// GETTERS / HELPER METHODS ////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the states key
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
getKey() {
|
|
|
|
return this.key;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the html element of the state
|
|
|
|
* @returns {HTMLElement}
|
|
|
|
*/
|
|
|
|
getDivElement() {
|
|
|
|
return document.getElementById("state_" + this.key);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Transfers to a new state
|
|
|
|
* @param {string} stateKey The id of the new state
|
|
|
|
*/
|
|
|
|
moveToState(stateKey, payload = {}, skipFadeOut = false) {
|
|
|
|
if (this.fadingOut) {
|
|
|
|
logger.warn("Skipping move to '" + stateKey + "' since already fading out");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clean up event listeners
|
|
|
|
this.internalCleanUpClickDetectors();
|
|
|
|
|
|
|
|
// Fading
|
|
|
|
const fadeTime = this.internalGetFadeInOutTime();
|
|
|
|
const doFade = !skipFadeOut && this.getHasFadeOut() && fadeTime !== 0;
|
|
|
|
logger.log("Moving to", stateKey, "(fading=", doFade, ")");
|
|
|
|
if (doFade) {
|
|
|
|
this.htmlElement.classList.remove("arrived");
|
|
|
|
this.fadingOut = true;
|
|
|
|
setTimeout(() => {
|
|
|
|
this.stateManager.moveToState(stateKey, payload);
|
|
|
|
}, fadeTime);
|
|
|
|
} else {
|
|
|
|
this.stateManager.moveToState(stateKey, payload);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {string} nextStateId
|
|
|
|
* @param {object=} nextStatePayload
|
|
|
|
*/
|
|
|
|
watchAdAndMoveToState(nextStateId, nextStatePayload = {}) {
|
|
|
|
if (this.app.adProvider.getCanShowVideoAd() && this.app.isRenderable()) {
|
|
|
|
this.moveToState(
|
|
|
|
"WatchAdState",
|
|
|
|
{
|
|
|
|
nextStateId,
|
|
|
|
nextStatePayload,
|
|
|
|
},
|
|
|
|
true
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
this.moveToState(nextStateId, nextStatePayload);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Tracks clicks on a given element and calls the given callback *on this state*.
|
|
|
|
* If you want to call another function wrap it inside a lambda.
|
|
|
|
* @param {Element} element The element to track clicks on
|
|
|
|
* @param {function():void} handler The handler to call
|
|
|
|
* @param {import("./click_detector").ClickDetectorConstructorArgs=} args Click detector arguments
|
|
|
|
*/
|
|
|
|
trackClicks(element, handler, args = {}) {
|
|
|
|
const detector = new ClickDetector(element, args);
|
|
|
|
detector.click.add(handler, this);
|
|
|
|
if (G_IS_DEV) {
|
|
|
|
// Append a source so we can check where the click detector is from
|
|
|
|
// @ts-ignore
|
|
|
|
detector._src = "state-" + this.key;
|
|
|
|
}
|
|
|
|
this.clickDetectors.push(detector);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cancels all promises on the api as well as our async channel
|
|
|
|
*/
|
|
|
|
cancelAllAsyncOperations() {
|
|
|
|
this.asyncChannel.cancelAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
//// CALLBACKS ////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callback when entering the state, to be overriddemn
|
|
|
|
* @param {any} payload Arbitrary data passed from the state which we are transferring from
|
|
|
|
*/
|
|
|
|
onEnter(payload) {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callback when leaving the state
|
|
|
|
*/
|
|
|
|
onLeave() {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callback before leaving the game state or when the page is unloaded
|
|
|
|
*/
|
|
|
|
onBeforeExit() {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callback when the app got paused (on android, this means in background)
|
|
|
|
*/
|
|
|
|
onAppPause() {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callback when the app got resumed (on android, this means in foreground again)
|
|
|
|
*/
|
|
|
|
onAppResume() {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Render callback
|
|
|
|
* @param {number} dt Delta time in ms since last render
|
|
|
|
*/
|
|
|
|
onRender(dt) {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Background tick callback, called while the game is inactiev
|
|
|
|
* @param {number} dt Delta time in ms since last tick
|
|
|
|
*/
|
|
|
|
onBackgroundTick(dt) {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when the screen resized
|
|
|
|
* @param {number} w window/screen width
|
|
|
|
* @param {number} h window/screen height
|
|
|
|
*/
|
|
|
|
onResized(w, h) {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal backbutton handler, called when the hardware back button is pressed or
|
|
|
|
* the escape key is pressed
|
|
|
|
*/
|
|
|
|
onBackButton() {}
|
|
|
|
|
|
|
|
//// INTERFACE ////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Should return how many mulliseconds to fade in / out the state. Not recommended to override!
|
|
|
|
* @returns {number} Time in milliseconds to fade out
|
|
|
|
*/
|
|
|
|
getInOutFadeTime() {
|
|
|
|
if (globalConfig.debug.noArtificialDelays) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return 200;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Should return whether to fade in the game state. This will then apply the right css classes
|
|
|
|
* for the fadein.
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
getHasFadeIn() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Should return whether to fade out the game state. This will then apply the right css classes
|
|
|
|
* for the fadeout and wait the delay before moving states
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
getHasFadeOut() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns if this state should get paused if it does not have focus
|
|
|
|
* @returns {boolean} true to pause the updating of the game
|
|
|
|
*/
|
|
|
|
getPauseOnFocusLost() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Should return the html code of the state.
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
getInnerHTML() {
|
|
|
|
abstract;
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns if the state has an unload confirmation, this is the
|
|
|
|
* "Are you sure you want to leave the page" message.
|
|
|
|
*/
|
|
|
|
getHasUnloadConfirmation() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Should return the theme music for this state
|
|
|
|
* @returns {string|null}
|
|
|
|
*/
|
|
|
|
getThemeMusic() {
|
2020-05-16 07:49:00 +00:00
|
|
|
return MUSIC.menu;
|
2020-05-09 14:45:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
////////////////////
|
|
|
|
|
|
|
|
//// INTERNAL ////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal callback from the manager. Do not override!
|
|
|
|
* @param {StateManager} stateManager
|
|
|
|
*/
|
|
|
|
internalRegisterCallback(stateManager, app) {
|
|
|
|
assert(stateManager, "No state manager");
|
|
|
|
assert(app, "No app");
|
|
|
|
this.stateManager = stateManager;
|
|
|
|
this.app = app;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal callback when entering the state. Do not override!
|
|
|
|
* @param {any} payload Arbitrary data passed from the state which we are transferring from
|
|
|
|
* @param {boolean} callCallback Whether to call the onEnter callback
|
|
|
|
*/
|
|
|
|
internalEnterCallback(payload, callCallback = true) {
|
|
|
|
logSection(this.key, "#26a69a");
|
|
|
|
this.app.inputMgr.pushReciever(this.inputReciever);
|
|
|
|
|
|
|
|
this.htmlElement = this.getDivElement();
|
|
|
|
this.htmlElement.classList.add("active");
|
|
|
|
|
|
|
|
// Apply classes in the next frame so the css transition keeps up
|
|
|
|
waitNextFrame().then(() => {
|
|
|
|
if (this.htmlElement) {
|
|
|
|
this.htmlElement.classList.remove("fadingOut");
|
|
|
|
this.htmlElement.classList.remove("fadingIn");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Call handler
|
|
|
|
if (callCallback) {
|
|
|
|
this.onEnter(payload);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal callback when the state is left. Do not override!
|
|
|
|
*/
|
|
|
|
internalLeaveCallback() {
|
|
|
|
this.onLeave();
|
|
|
|
|
|
|
|
this.htmlElement.classList.remove("active");
|
|
|
|
this.app.inputMgr.popReciever(this.inputReciever);
|
|
|
|
this.internalCleanUpClickDetectors();
|
|
|
|
this.asyncChannel.cancelAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal callback *before* the state is left. Also is called on page unload
|
|
|
|
*/
|
|
|
|
internalOnBeforeExitCallback() {
|
|
|
|
this.onBeforeExit();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal app pause callback
|
|
|
|
*/
|
|
|
|
internalOnAppPauseCallback() {
|
|
|
|
this.onAppPause();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal app resume callback
|
|
|
|
*/
|
|
|
|
internalOnAppResumeCallback() {
|
|
|
|
this.onAppResume();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cleans up all click detectors
|
|
|
|
*/
|
|
|
|
internalCleanUpClickDetectors() {
|
|
|
|
if (this.clickDetectors) {
|
|
|
|
for (let i = 0; i < this.clickDetectors.length; ++i) {
|
|
|
|
this.clickDetectors[i].cleanup();
|
|
|
|
}
|
|
|
|
this.clickDetectors = [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal method to get the HTML of the game state.
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
internalGetFullHtml() {
|
|
|
|
return this.getInnerHTML();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal method to compute the time to fade in / out
|
|
|
|
* @returns {number} time to fade in / out in ms
|
|
|
|
*/
|
|
|
|
internalGetFadeInOutTime() {
|
|
|
|
if (G_IS_DEV && globalConfig.debug.fastGameEnter) {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
if (G_IS_DEV && globalConfig.debug.noArtificialDelays) {
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
return this.getInOutFadeTime();
|
|
|
|
}
|
|
|
|
}
|