diff --git a/src/js/core/game_state.ts b/src/js/core/game_state.ts index bea5f8d4..9d58c380 100644 --- a/src/js/core/game_state.ts +++ b/src/js/core/game_state.ts @@ -1,14 +1,11 @@ -/* typehints:start */ -import { Application } from "../application"; -import { StateManager } from "./state_manager"; -/* typehints:end */ - -import { MUSIC } from "../platform/sound"; +import { MUSIC } from "@/platform/sound"; +import type { Application } from "../application"; import { ClickDetector } from "./click_detector"; import { globalConfig } from "./config"; import { InputReceiver } from "./input_receiver"; import { createLogger, logSection } from "./logging"; import { RequestChannel } from "./request_channel"; +import type { StateManager } from "./state_manager"; import { waitNextFrame } from "./utils"; const logger = createLogger("game_state"); @@ -17,49 +14,41 @@ const logger = createLogger("game_state"); * Basic state of the game state machine. This is the base of the whole game */ export class GameState { + public app: Application = null; + public readonly key: string; + public inputReceiver: InputReceiver; + + /** A channel we can use to perform async ops */ + protected asyncChannel = new RequestChannel(); + protected clickDetectors: ClickDetector[] = []; + + /** @todo review this */ + protected htmlElement: HTMLElement | undefined; + + private stateManager: StateManager = null; + + /** Store if we are currently fading out */ + private fadingOut = false; + /** * 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 + * @param key The id of the state. We use ids to refer to states because otherwise we get + * circular references */ - constructor(key) { + constructor(key: string) { 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} */ - this.clickDetectors = []; - // Every state captures keyboard events by default this.inputReceiver = new InputReceiver("state-" + key); this.inputReceiver.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() { + getDivElement(): HTMLElement { return document.getElementById("state_" + this.key); } @@ -120,9 +109,9 @@ export class GameState { /** * Callback when entering the state, to be overriddemn - * @param {any} payload Arbitrary data passed from the state which we are transferring from + * @param payload Arbitrary data passed from the state which we are transferring from */ - onEnter(payload) {} + onEnter(payload: {}) {} /** * Callback when leaving the state @@ -141,22 +130,22 @@ export class GameState { /** * Render callback - * @param {number} dt Delta time in ms since last render + * @param dt Delta time in ms since last render */ - onRender(dt) {} + onRender(dt: number) {} /** * Background tick callback, called while the game is inactiev - * @param {number} dt Delta time in ms since last tick + * @param dt Delta time in ms since last tick */ - onBackgroundTick(dt) {} + onBackgroundTick(dt: number) {} /** * Called when the screen resized - * @param {number} w window/screen width - * @param {number} h window/screen height + * @param w window/screen width + * @param h window/screen height */ - onResized(w, h) {} + onResized(w: number, h: number) {} /** * Internal backbutton handler, called when the hardware back button is pressed or @@ -168,9 +157,9 @@ export class GameState { /** * Should return how many mulliseconds to fade in / out the state. Not recommended to override! - * @returns {number} Time in milliseconds to fade out + * @returns Time in milliseconds to fade out */ - getInOutFadeTime() { + getInOutFadeTime(): number { if (globalConfig.debug.noArtificialDelays) { return 0; } @@ -180,39 +169,43 @@ export class GameState { /** * Should return whether to fade in the game state. This will then apply the right css classes * for the fadein. - * @returns {boolean} */ - getHasFadeIn() { + getHasFadeIn(): boolean { 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() { + getHasFadeOut(): boolean { 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 + * @returns true to pause the updating of the game */ - getPauseOnFocusLost() { + getPauseOnFocusLost(): boolean { return true; } /** * Should return the html code of the state. - * @returns {string} - * @abstract + * @deprecated use {@link getContentLayout} instead */ - getInnerHTML() { - abstract; + getInnerHTML(): string { return ""; } + /** + * Should return the element(s) to be displayed in the state. + * If null, {@link getInnerHTML} will be used instead. + */ + protected getContentLayout(): Node { + return null; + } + /** * Returns if the state has an unload confirmation, this is the * "Are you sure you want to leave the page" message. @@ -223,25 +216,22 @@ export class GameState { /** * Should return the theme music for this state - * @returns {string|null} */ - getThemeMusic() { + getThemeMusic(): string | null { return MUSIC.menu; } /** * Should return true if the player is currently ingame - * @returns {boolean} */ - getIsIngame() { + getIsIngame(): boolean { return false; } /** * Should return whether to clear the whole body content before entering the state. - * @returns {boolean} */ - getRemovePreviousContent() { + getRemovePreviousContent(): boolean { return true; } @@ -251,9 +241,8 @@ export class GameState { /** * Internal callback from the manager. Do not override! - * @param {StateManager} stateManager */ - internalRegisterCallback(stateManager, app) { + internalRegisterCallback(stateManager: StateManager, app: Application) { assert(stateManager, "No state manager"); assert(app, "No app"); this.stateManager = stateManager; @@ -262,10 +251,10 @@ export class GameState { /** * 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 + * @param payload Arbitrary data passed from the state which we are transferring from + * @param callCallback Whether to call the onEnter callback */ - internalEnterCallback(payload, callCallback = true) { + internalEnterCallback(payload: any, callCallback = true) { logSection(this.key, "#26a69a"); this.app.inputMgr.pushReceiver(this.inputReceiver); @@ -325,18 +314,33 @@ export class GameState { } /** - * Internal method to get the HTML of the game state. - * @returns {string} + * Internal method to get all elements of the game state. Can be + * called from subclasses to provide support for both HTMLElements + * and HTML strings. */ - internalGetFullHtml() { - return this.getInnerHTML(); + internalGetWrappedContent(): Node { + const elements = this.getContentLayout(); + if (elements instanceof Node) { + return elements; + } + + if (Array.isArray(elements)) { + const fragment = document.createDocumentFragment(); + fragment.append(...(elements as Node[])); + return fragment; + } + + // Fall back to deprecated HTML strings solution + const template = document.createElement("template"); + template.innerHTML = this.getInnerHTML(); + return template.content; } /** * Internal method to compute the time to fade in / out - * @returns {number} time to fade in / out in ms + * @returns time to fade in / out in ms */ - internalGetFadeInOutTime() { + internalGetFadeInOutTime(): number { if (G_IS_DEV && globalConfig.debug.fastGameEnter) { return 1; } diff --git a/src/js/core/state_manager.js b/src/js/core/state_manager.js index 001537cd..6045675f 100644 --- a/src/js/core/state_manager.js +++ b/src/js/core/state_manager.js @@ -2,10 +2,10 @@ import { Application } from "../application"; /* typehints:end*/ +import { MOD_SIGNALS } from "../mods/mod_signals"; import { GameState } from "./game_state"; import { createLogger } from "./logging"; -import { waitNextFrame, removeAllChildren } from "./utils"; -import { MOD_SIGNALS } from "../mods/mod_signals"; +import { removeAllChildren, waitNextFrame } from "./utils"; const logger = createLogger("state_manager"); @@ -34,7 +34,7 @@ export class StateManager { // Create a dummy to retrieve the key const dummy = new stateClass(); assert(dummy instanceof GameState, "Not a state!"); - const key = dummy.getKey(); + const key = dummy.key; assert(!this.stateClasses[key], `State '${key}' is already registered!`); this.stateClasses[key] = stateClass; } @@ -61,7 +61,7 @@ export class StateManager { } if (this.currentState) { - if (key === this.currentState.getKey()) { + if (key === this.currentState.key) { logger.error(`State '${key}' is already active!`); return false; } @@ -88,7 +88,8 @@ export class StateManager { document.body.id = "state_" + key; if (this.currentState.getRemovePreviousContent()) { - document.body.innerHTML = this.currentState.internalGetFullHtml(); + const content = this.currentState.internalGetWrappedContent(); + document.body.append(content); } const dialogParent = document.createElement("div"); diff --git a/src/js/core/textual_game_state.tsx b/src/js/core/textual_game_state.tsx index 52a1f946..95755223 100644 --- a/src/js/core/textual_game_state.tsx +++ b/src/js/core/textual_game_state.tsx @@ -1,40 +1,76 @@ import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { GameState } from "./game_state"; -import { T } from "../translations"; /** * Baseclass for all game states which are structured similary: A header with back button + some * scrollable content. */ -export class TextualGameState extends GameState { - ///// INTERFACE //// +export abstract class TextualGameState extends GameState { + private backToStateId: string | null = null; + private backToStatePayload: {} | null = null; + + protected headerElement: HTMLElement; + protected containerElement: HTMLElement; + protected dialogs: HUDModalDialogs; /** * Should return the states inner html. If not overriden, will create a scrollable container * with the content of getMainContentHTML() - * @returns {string} + * @deprecated */ - getInnerHTML() { - return ` -
- ${this.getMainContentHTML()} -
- `; + getInnerHTML(): string { + return ""; } /** * Should return the states HTML content. + * @deprecated */ - getMainContentHTML() { + getMainContentHTML(): string { return ""; } + protected override getContentLayout(): Node { + let content = this.getInitialContent(); + + if (content === null) { + // Fall back either to getMainContentHTML or getInnerHTML (if not "") + let html = this.getInnerHTML(); + if (html === "") { + html = ` +
+ ${this.getMainContentHTML()} +
+ `; + } + + const template = document.createElement("template"); + template.innerHTML = html; + content = template.content; + } + + return ( + <> +
+

+ + {this.getStateHeaderTitle() ?? ""} +

+
+
{content}
+ + ); + } + + protected getInitialContent(): Node { + return null; + } + /** * Should return the title of the game state. If null, no title and back button will * get created - * @returns {string|null} */ - getStateHeaderTitle() { + protected getStateHeaderTitle(): string | null { return null; } @@ -44,7 +80,7 @@ export class TextualGameState extends GameState { * Back button handler, can be overridden. Per default it goes back to the main menu, * or if coming from the game it moves back to the game again. */ - onBackButton() { + override onBackButton() { if (this.backToStateId) { this.moveToState(this.backToStateId, this.backToStatePayload); } else { @@ -61,9 +97,9 @@ export class TextualGameState extends GameState { /** * Goes to a new state, telling him to go back to this state later - * @param {string} stateId + * @param stateId */ - moveToStateAddGoBack(stateId) { + moveToStateAddGoBack(stateId: string) { this.moveToState(stateId, { backToStateId: this.key, backToStatePayload: { @@ -89,43 +125,20 @@ export class TextualGameState extends GameState { } } - /** - * Overrides the GameState implementation to provide our own html - */ - internalGetFullHtml() { - let headerHtml = ""; - if (this.getStateHeaderTitle()) { - headerHtml = ` -
- -

${this.getStateHeaderTitle()}

-
`; - } - - return ` - ${headerHtml} -
- ${this.getInnerHTML()} - -
- `; - } - //// INTERNALS ///// /** * Overrides the GameState leave callback to cleanup stuff */ - internalLeaveCallback() { + override internalLeaveCallback() { super.internalLeaveCallback(); this.dialogs.cleanup(); } /** * Overrides the GameState enter callback to setup required stuff - * @param {any} payload */ - internalEnterCallback(payload) { + override internalEnterCallback(payload: any) { super.internalEnterCallback(payload, false); if (payload.backToStateId) { this.backToStateId = payload.backToStateId;