1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-13 02:01:51 +00:00

Merge pull request #39 from tobspr-games/dengr1065/textual-game-state-work

Basic support for JSX/TSX in GameStates
This commit is contained in:
Даниїл Григор'єв 2024-09-22 11:21:26 +03:00 committed by GitHub
commit 7dac0baa6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 350 additions and 511 deletions

View File

@ -87,7 +87,7 @@ export default {
fallback: { fs: false }, fallback: { fs: false },
alias: { alias: {
"global-compression": resolve("../src/js/core/lzstring.js"), "global-compression": resolve("../src/js/core/lzstring.js"),
"root": resolve("../src/js/"), "@": resolve("../src/js/"),
}, },
fullySpecified: false, fullySpecified: false,
extensions: [".ts", ".js", ".tsx", ".jsx"], extensions: [".ts", ".js", ".tsx", ".jsx"],

View File

@ -1,9 +1,9 @@
import { resolve } from "path/posix"; import { resolve } from "path/posix";
import TerserPlugin from "terser-webpack-plugin"; import TerserPlugin from "terser-webpack-plugin";
import webpack from "webpack"; import webpack from "webpack";
const { DefinePlugin, IgnorePlugin } = webpack;
import DeadCodePlugin from "webpack-deadcode-plugin"; import DeadCodePlugin from "webpack-deadcode-plugin";
import { getAllResourceImages, getRevision, getVersion } from "./buildutils.js"; import { getAllResourceImages, getRevision, getVersion } from "./buildutils.js";
const { DefinePlugin, IgnorePlugin } = webpack;
const globalDefs = { const globalDefs = {
"assert": "false && window.assert", "assert": "false && window.assert",
@ -96,7 +96,7 @@ export default {
fallback: { fs: false }, fallback: { fs: false },
alias: { alias: {
"global-compression": resolve("../src/js/core/lzstring.js"), "global-compression": resolve("../src/js/core/lzstring.js"),
"root": resolve("../src/js/"), "@": resolve("../src/js/"),
}, },
fullySpecified: false, fullySpecified: false,
extensions: [".ts", ".js", ".tsx", ".jsx"], extensions: [".ts", ".js", ".tsx", ".jsx"],

View File

@ -1,144 +1,34 @@
#state_ModsState { #state_ModsState {
.mainContent { .modsGrid {
display: flex;
flex-direction: column;
}
> .headerBar {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: repeat(3, 1fr);
align-items: center; gap: 0.5em;
> h1 { .noMods {
justify-self: start; grid-template-columns: unset;
} place-items: center;
.openModsFolder {
background-color: $modsColor;
} }
} }
.noModSupport { .mod {
display: flex; display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column; flex-direction: column;
text-align: center; padding: 0.6em;
max-width: 80%; gap: 0.3em;
align-self: center; background-color: $mainBgColor;
@include S(border-radius, 0.75 * $globalBorderRadius);
.steamLink {
@include S(height, 50px);
@include S(width, 220px);
background: #171a23 center center / contain no-repeat;
overflow: hidden;
display: block;
text-indent: -999em;
cursor: pointer;
@include S(margin-top, 30px);
pointer-events: all;
transition: all 0.12s ease-in;
transition-property: opacity, transform;
box-shadow: 0 D(3px) D(10px) rgba(96, 163, 136, 0.5);
@include S(border-radius, $globalBorderRadius);
&:hover {
opacity: 0.9;
}
}
}
.modsStats {
@include PlainText; @include PlainText;
color: $accentColorDark;
&.noMods { .title b {
@include S(width, 400px);
align-self: center;
justify-self: center;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
@include Text; @include Text;
@include S(margin-top, 100px);
color: lighten($accentColorDark, 15);
button {
@include S(margin-top, 10px);
@include S(padding, 10px, 20px);
}
&::before {
@include S(margin-bottom, 15px);
content: "";
@include S(width, 50px);
@include S(height, 50px);
background-position: center center;
background-size: contain;
opacity: 0.2;
}
&::before {
background-image: uiResource("res/ui/icons/mods.png") !important;
}
} }
}
.modsList { .advanced {
@include S(margin-top, 10px); @include SuperSmallText;
overflow-y: scroll; }
pointer-events: all;
@include S(padding-right, 5px);
flex-grow: 1;
.mod { @include DarkThemeOverride {
@include S(border-radius, $globalBorderRadius); background-color: $darkModeGameBackground;
background: #eeeff4;
@include S(margin-bottom, 4px);
@include S(padding, 7px, 10px);
@include S(grid-gap, 15px);
display: grid;
grid-template-columns: 1fr D(100px) D(80px) D(50px);
@include DarkThemeOverride {
background: darken($darkModeControlsBackground, 5);
}
.checkbox {
align-self: center;
justify-self: center;
}
.mainInfo {
display: flex;
flex-direction: column;
.description {
@include PlainText;
@include S(margin-top, 5px);
color: $accentColorDark;
}
.website {
text-transform: uppercase;
align-self: start;
@include PlainText;
@include S(margin-top, 5px);
}
}
.version,
.author {
display: flex;
flex-direction: column;
align-self: center;
strong {
text-transform: uppercase;
color: $accentColorDark;
@include SuperSmallText;
}
}
} }
} }
} }

View File

@ -1,15 +1,12 @@
/* typehints:start */ import { MUSIC } from "@/platform/sound";
import { Application } from "../application"; import type { Application } from "../application";
import { StateManager } from "./state_manager";
/* typehints:end */
import { globalConfig } from "./config";
import { ClickDetector } from "./click_detector"; import { ClickDetector } from "./click_detector";
import { logSection, createLogger } from "./logging"; import { globalConfig } from "./config";
import { InputReceiver } from "./input_receiver"; import { InputReceiver } from "./input_receiver";
import { waitNextFrame } from "./utils"; import { createLogger, logSection } from "./logging";
import { RequestChannel } from "./request_channel"; import { RequestChannel } from "./request_channel";
import { MUSIC } from "../platform/sound"; import type { StateManager } from "./state_manager";
import { waitNextFrame } from "./utils";
const logger = createLogger("game_state"); 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 * Basic state of the game state machine. This is the base of the whole game
*/ */
export class GameState { 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 * 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 * @param key The id of the state. We use ids to refer to states because otherwise we get
* circular references * circular references
*/ */
constructor(key) { constructor(key: string) {
this.key = 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 // Every state captures keyboard events by default
this.inputReciever = new InputReceiver("state-" + key); this.inputReceiver = new InputReceiver("state-" + key);
this.inputReciever.backButton.add(this.onBackButton, this); this.inputReceiver.backButton.add(this.onBackButton, this);
// A channel we can use to perform async ops
this.asyncChannel = new RequestChannel();
} }
//// GETTERS / HELPER METHODS //// //// GETTERS / HELPER METHODS ////
/**
* Returns the states key
* @returns {string}
*/
getKey() {
return this.key;
}
/** /**
* Returns the html element of the state * Returns the html element of the state
* @returns {HTMLElement}
*/ */
getDivElement() { getDivElement(): HTMLElement {
return document.getElementById("state_" + this.key); return document.getElementById("state_" + this.key);
} }
@ -120,9 +109,9 @@ export class GameState {
/** /**
* Callback when entering the state, to be overriddemn * 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 * Callback when leaving the state
@ -141,22 +130,22 @@ export class GameState {
/** /**
* Render callback * 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 * 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 * Called when the screen resized
* @param {number} w window/screen width * @param w window/screen width
* @param {number} h window/screen height * @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 * 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! * 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) { if (globalConfig.debug.noArtificialDelays) {
return 0; return 0;
} }
@ -180,39 +169,45 @@ export class GameState {
/** /**
* Should return whether to fade in the game state. This will then apply the right css classes * Should return whether to fade in the game state. This will then apply the right css classes
* for the fadein. * for the fadein.
* @returns {boolean}
*/ */
getHasFadeIn() { getHasFadeIn(): boolean {
return true; return true;
} }
/** /**
* Should return whether to fade out the game state. This will then apply the right css classes * 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 * for the fadeout and wait the delay before moving states
* @returns {boolean}
*/ */
getHasFadeOut() { getHasFadeOut(): boolean {
return true; return true;
} }
/** /**
* Returns if this state should get paused if it does not have focus * 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; return true;
} }
/** /**
* Should return the html code of the state. * Should return the html code of the state.
* @returns {string} * @deprecated use {@link getContentLayout} instead
* @abstract
*/ */
getInnerHTML() { getInnerHTML(): string {
abstract;
return ""; return "";
} }
/**
* Should return the element(s) to be displayed in the state.
* If not overridden, {@link getInnerHTML} will be used to provide the layout.
*/
protected getContentLayout(): Node {
const template = document.createElement("template");
template.innerHTML = this.getInnerHTML();
return template.content;
}
/** /**
* Returns if the state has an unload confirmation, this is the * Returns if the state has an unload confirmation, this is the
* "Are you sure you want to leave the page" message. * "Are you sure you want to leave the page" message.
@ -223,25 +218,22 @@ export class GameState {
/** /**
* Should return the theme music for this state * Should return the theme music for this state
* @returns {string|null}
*/ */
getThemeMusic() { getThemeMusic(): string | null {
return MUSIC.menu; return MUSIC.menu;
} }
/** /**
* Should return true if the player is currently ingame * Should return true if the player is currently ingame
* @returns {boolean}
*/ */
getIsIngame() { getIsIngame(): boolean {
return false; return false;
} }
/** /**
* Should return whether to clear the whole body content before entering the state. * Should return whether to clear the whole body content before entering the state.
* @returns {boolean}
*/ */
getRemovePreviousContent() { getRemovePreviousContent(): boolean {
return true; return true;
} }
@ -251,9 +243,8 @@ export class GameState {
/** /**
* Internal callback from the manager. Do not override! * 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(stateManager, "No state manager");
assert(app, "No app"); assert(app, "No app");
this.stateManager = stateManager; this.stateManager = stateManager;
@ -262,12 +253,12 @@ export class GameState {
/** /**
* Internal callback when entering the state. Do not override! * Internal callback when entering the state. Do not override!
* @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
* @param {boolean} callCallback Whether to call the onEnter callback * @param callCallback Whether to call the onEnter callback
*/ */
internalEnterCallback(payload, callCallback = true) { internalEnterCallback(payload: any, callCallback = true) {
logSection(this.key, "#26a69a"); logSection(this.key, "#26a69a");
this.app.inputMgr.pushReciever(this.inputReciever); this.app.inputMgr.pushReceiver(this.inputReceiver);
this.htmlElement = this.getDivElement(); this.htmlElement = this.getDivElement();
this.htmlElement.classList.add("active"); this.htmlElement.classList.add("active");
@ -293,7 +284,7 @@ export class GameState {
this.onLeave(); this.onLeave();
this.htmlElement.classList.remove("active"); this.htmlElement.classList.remove("active");
this.app.inputMgr.popReciever(this.inputReciever); this.app.inputMgr.popReceiver(this.inputReceiver);
this.internalCleanUpClickDetectors(); this.internalCleanUpClickDetectors();
this.asyncChannel.cancelAll(); this.asyncChannel.cancelAll();
} }
@ -325,18 +316,27 @@ export class GameState {
} }
/** /**
* Internal method to get the HTML of the game state. * Internal method to get all elements of the game state. Can be
* @returns {string} * called from subclasses to provide support for both HTMLElements
* and HTML strings.
*/ */
internalGetFullHtml() { internalGetWrappedContent(): Node {
return this.getInnerHTML(); const elements = this.getContentLayout();
if (Array.isArray(elements)) {
const fragment = document.createDocumentFragment();
fragment.append(...(elements as Node[]));
return fragment;
}
return elements;
} }
/** /**
* Internal method to compute the time to fade in / out * 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) { if (G_IS_DEV && globalConfig.debug.fastGameEnter) {
return 1; return 1;
} }

View File

@ -1,14 +1,14 @@
import type { Application } from "../application"; import type { Application } from "../application";
import type { InputReceiver, ReceiverId } from "./input_receiver"; import type { InputReceiver, ReceiverId } from "./input_receiver";
import { Signal, STOP_PROPAGATION } from "./signal";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { Signal, STOP_PROPAGATION } from "./signal";
import { arrayDeleteValue, fastArrayDeleteValue } from "./utils"; import { arrayDeleteValue, fastArrayDeleteValue } from "./utils";
const logger = createLogger("input_distributor"); const logger = createLogger("input_distributor");
export class InputDistributor { export class InputDistributor {
public recieverStack: InputReceiver[] = []; public receiverStack: InputReceiver[] = [];
public filters: ((arg: string) => boolean)[] = []; public filters: ((arg: string) => boolean)[] = [];
/** /**
@ -34,71 +34,71 @@ export class InputDistributor {
fastArrayDeleteValue(this.filters, filter); fastArrayDeleteValue(this.filters, filter);
} }
pushReciever(reciever: InputReceiver) { pushReceiver(receiver: InputReceiver) {
if (this.isRecieverAttached(reciever)) { if (this.isReceiverAttached(receiver)) {
assert(false, "Can not add reciever " + reciever.context + " twice"); assert(false, "Can not add receiver " + receiver.context + " twice");
logger.error("Can not add reciever", reciever.context, "twice"); logger.error("Can not add receiver", receiver.context, "twice");
return; return;
} }
this.recieverStack.push(reciever); this.receiverStack.push(receiver);
if (this.recieverStack.length > 10) { if (this.receiverStack.length > 10) {
logger.error( logger.error(
"Reciever stack is huge, probably some dead receivers arround:", "Receiver stack is huge, probably some dead receivers arround:",
this.recieverStack.map(x => x.context) this.receiverStack.map(x => x.context)
); );
} }
} }
popReciever(reciever: InputReceiver) { popReceiver(receiver: InputReceiver) {
if (this.recieverStack.indexOf(reciever) < 0) { if (this.receiverStack.indexOf(receiver) < 0) {
assert(false, "Can not pop reciever " + reciever.context + " since its not contained"); assert(false, "Can not pop receiver " + receiver.context + " since its not contained");
logger.error("Can not pop reciever", reciever.context, "since its not contained"); logger.error("Can not pop receiver", receiver.context, "since its not contained");
return; return;
} }
if (this.recieverStack[this.recieverStack.length - 1] !== reciever) { if (this.receiverStack[this.receiverStack.length - 1] !== receiver) {
logger.warn( logger.warn(
"Popping reciever", "Popping receiver",
reciever.context, receiver.context,
"which is not on top of the stack. Stack is: ", "which is not on top of the stack. Stack is: ",
this.recieverStack.map(x => x.context) this.receiverStack.map(x => x.context)
); );
} }
arrayDeleteValue(this.recieverStack, reciever); arrayDeleteValue(this.receiverStack, receiver);
} }
isRecieverAttached(reciever: InputReceiver) { isReceiverAttached(receiver: InputReceiver) {
return this.recieverStack.indexOf(reciever) >= 0; return this.receiverStack.indexOf(receiver) >= 0;
} }
isRecieverOnTop(reciever: InputReceiver) { isReceiverOnTop(receiver: InputReceiver) {
return ( return (
this.isRecieverAttached(reciever) && this.isReceiverAttached(receiver) &&
this.recieverStack[this.recieverStack.length - 1] === reciever this.receiverStack[this.receiverStack.length - 1] === receiver
); );
} }
makeSureAttachedAndOnTop(reciever: InputReceiver) { makeSureAttachedAndOnTop(receiver: InputReceiver) {
this.makeSureDetached(reciever); this.makeSureDetached(receiver);
this.pushReciever(reciever); this.pushReceiver(receiver);
} }
makeSureDetached(reciever: InputReceiver) { makeSureDetached(receiver: InputReceiver) {
if (this.isRecieverAttached(reciever)) { if (this.isReceiverAttached(receiver)) {
arrayDeleteValue(this.recieverStack, reciever); arrayDeleteValue(this.receiverStack, receiver);
} }
} }
destroyReceiver(reciever: InputReceiver) { destroyReceiver(receiver: InputReceiver) {
this.makeSureDetached(reciever); this.makeSureDetached(receiver);
reciever.cleanup(); receiver.cleanup();
} }
// Internal // Internal
getTopReciever() { getTopReceiver() {
if (this.recieverStack.length > 0) { if (this.receiverStack.length > 0) {
return this.recieverStack[this.recieverStack.length - 1]; return this.receiverStack[this.receiverStack.length - 1];
} }
return null; return null;
} }
@ -129,12 +129,12 @@ export class InputDistributor {
} }
} }
const reciever = this.getTopReciever(); const receiver = this.getTopReceiver();
if (!reciever) { if (!receiver) {
logger.warn("Dismissing event because not reciever was found:", eventId); logger.warn("Dismissing event because not receiver was found:", eventId);
return; return;
} }
const signal = reciever[eventId]; const signal = receiver[eventId];
assert(signal instanceof Signal, "Not a valid event id"); assert(signal instanceof Signal, "Not a valid event id");
// probably not possible to type properly, since the types of `signal` and `payload` are correlated // probably not possible to type properly, since the types of `signal` and `payload` are correlated
return signal.dispatch(payload as never); return signal.dispatch(payload as never);

View File

@ -1,15 +1,15 @@
import type { Application } from "../application"; import type { Application } from "../application";
import { getStringForKeyCode } from "../game/key_action_mapper";
import { SOUNDS } from "../platform/sound";
import { T } from "../translations";
import { ClickDetector, ClickDetectorConstructorArgs } from "./click_detector";
import { globalConfig } from "./config";
import { InputReceiver, KeydownEvent } from "./input_receiver";
import { createLogger } from "./logging";
import { FormElement } from "./modal_dialog_forms";
import { Signal, STOP_PROPAGATION } from "./signal"; import { Signal, STOP_PROPAGATION } from "./signal";
import { arrayDeleteValue, waitNextFrame } from "./utils"; import { arrayDeleteValue, waitNextFrame } from "./utils";
import { ClickDetector, ClickDetectorConstructorArgs } from "./click_detector";
import { SOUNDS } from "../platform/sound";
import { InputReceiver, KeydownEvent } from "./input_receiver";
import { FormElement } from "./modal_dialog_forms";
import { globalConfig } from "./config";
import { getStringForKeyCode } from "../game/key_action_mapper";
import { createLogger } from "./logging";
import { T } from "../translations";
/* /*
* *************************************************** * ***************************************************
@ -51,7 +51,7 @@ export class Dialog<T extends string = never, U extends unknown[] = []> {
public timeouts: number[] = []; public timeouts: number[] = [];
public clickDetectors: ClickDetector[] = []; public clickDetectors: ClickDetector[] = [];
public inputReciever: InputReceiver; public inputReceiver: InputReceiver;
public enterHandler: T = null; public enterHandler: T = null;
public escapeHandler: T = null; public escapeHandler: T = null;
@ -103,9 +103,9 @@ export class Dialog<T extends string = never, U extends unknown[] = []> {
this.buttonSignals[buttonId] = new Signal(); this.buttonSignals[buttonId] = new Signal();
} }
this.inputReciever = new InputReceiver("dialog-" + this.title); this.inputReceiver = new InputReceiver("dialog-" + this.title);
this.inputReciever.keydown.add(this.handleKeydown, this); this.inputReceiver.keydown.add(this.handleKeydown, this);
} }
/** /**
@ -124,7 +124,7 @@ export class Dialog<T extends string = never, U extends unknown[] = []> {
} }
internalButtonHandler(id: T | "close-button", ...payload: U | []) { internalButtonHandler(id: T | "close-button", ...payload: U | []) {
this.app.inputMgr.popReciever(this.inputReciever); this.app.inputMgr.popReceiver(this.inputReceiver);
if (id !== "close-button") { if (id !== "close-button") {
this.buttonSignals[id].dispatch(...payload); this.buttonSignals[id].dispatch(...payload);
@ -160,7 +160,7 @@ export class Dialog<T extends string = never, U extends unknown[] = []> {
}); });
title.appendChild(closeBtn); title.appendChild(closeBtn);
this.inputReciever.backButton.add(() => this.internalButtonHandler("close-button")); this.inputReceiver.backButton.add(() => this.internalButtonHandler("close-button"));
} }
const content = document.createElement("div"); const content = document.createElement("div");
@ -231,7 +231,7 @@ export class Dialog<T extends string = never, U extends unknown[] = []> {
} }
this.element = elem; this.element = elem;
this.app.inputMgr.pushReciever(this.inputReciever); this.app.inputMgr.pushReceiver(this.inputReceiver);
return this.element; return this.element;
} }
@ -248,7 +248,7 @@ export class Dialog<T extends string = never, U extends unknown[] = []> {
// We need to do this here, because if the backbutton event gets // We need to do this here, because if the backbutton event gets
// dispatched to the modal dialogs, it will not call the internalButtonHandler, // dispatched to the modal dialogs, it will not call the internalButtonHandler,
// and thus our receiver stays attached the whole time // and thus our receiver stays attached the whole time
this.app.inputMgr.destroyReceiver(this.inputReciever); this.app.inputMgr.destroyReceiver(this.inputReceiver);
for (let i = 0; i < this.clickDetectors.length; ++i) { for (let i = 0; i < this.clickDetectors.length; ++i) {
this.clickDetectors[i].cleanup(); this.clickDetectors[i].cleanup();
@ -300,8 +300,8 @@ export class DialogLoading extends Dialog {
}); });
// Loading dialog can not get closed with back button // Loading dialog can not get closed with back button
this.inputReciever.backButton.removeAll(); this.inputReceiver.backButton.removeAll();
this.inputReciever.context = "dialog-loading"; this.inputReceiver.context = "dialog-loading";
} }
createElement() { createElement() {
@ -322,7 +322,7 @@ export class DialogLoading extends Dialog {
loader.classList.add("loadingIndicator"); loader.classList.add("loadingIndicator");
elem.appendChild(loader); elem.appendChild(loader);
this.app.inputMgr.pushReciever(this.inputReciever); this.app.inputMgr.pushReceiver(this.inputReceiver);
return elem; return elem;
} }

View File

@ -2,10 +2,10 @@
import { Application } from "../application"; import { Application } from "../application";
/* typehints:end*/ /* typehints:end*/
import { MOD_SIGNALS } from "../mods/mod_signals";
import { GameState } from "./game_state"; import { GameState } from "./game_state";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
import { waitNextFrame, removeAllChildren } from "./utils"; import { removeAllChildren, waitNextFrame } from "./utils";
import { MOD_SIGNALS } from "../mods/mod_signals";
const logger = createLogger("state_manager"); const logger = createLogger("state_manager");
@ -34,7 +34,7 @@ export class StateManager {
// Create a dummy to retrieve the key // Create a dummy to retrieve the key
const dummy = new stateClass(); const dummy = new stateClass();
assert(dummy instanceof GameState, "Not a state!"); assert(dummy instanceof GameState, "Not a state!");
const key = dummy.getKey(); const key = dummy.key;
assert(!this.stateClasses[key], `State '${key}' is already registered!`); assert(!this.stateClasses[key], `State '${key}' is already registered!`);
this.stateClasses[key] = stateClass; this.stateClasses[key] = stateClass;
} }
@ -61,7 +61,7 @@ export class StateManager {
} }
if (this.currentState) { if (this.currentState) {
if (key === this.currentState.getKey()) { if (key === this.currentState.key) {
logger.error(`State '${key}' is already active!`); logger.error(`State '${key}' is already active!`);
return false; return false;
} }
@ -88,7 +88,8 @@ export class StateManager {
document.body.id = "state_" + key; document.body.id = "state_" + key;
if (this.currentState.getRemovePreviousContent()) { if (this.currentState.getRemovePreviousContent()) {
document.body.innerHTML = this.currentState.internalGetFullHtml(); const content = this.currentState.internalGetWrappedContent();
document.body.append(content);
} }
const dialogParent = document.createElement("div"); const dialogParent = document.createElement("div");

View File

@ -1,40 +1,79 @@
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { GameState } from "./game_state"; import { GameState } from "./game_state";
import { T } from "../translations";
/** /**
* Baseclass for all game states which are structured similary: A header with back button + some * Baseclass for all game states which are structured similary: A header with back button + some
* scrollable content. * scrollable content.
*/ */
export class TextualGameState extends GameState { export abstract class TextualGameState extends GameState {
///// INTERFACE //// 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 * Should return the states inner html. If not overriden, will create a scrollable container
* with the content of getMainContentHTML() * with the content of getMainContentHTML()
* @returns {string} * @deprecated
*/ */
getInnerHTML() { getInnerHTML(): string {
return ` return "";
<div class="content mainContent">
${this.getMainContentHTML()}
</div>
`;
} }
/** /**
* Should return the states HTML content. * Should return the states HTML content.
* @deprecated
*/ */
getMainContentHTML() { getMainContentHTML(): string {
return ""; return "";
} }
/**
* Should return the element(s) to be displayed in the state.
* If not overridden, a default layout consisting of a back button,
* title, and content returned by {@link getInitialContent}.
*/
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 = `
<div class="content mainContent">
${this.getMainContentHTML()}
</div>
`;
}
content = super.getContentLayout();
}
return (
<>
<div class="headerBar">
<h1>
<button class="backButton"></button>
{this.getStateHeaderTitle() ?? ""}
</h1>
</div>
<div class="container">{content}</div>
</>
);
}
protected getInitialContent(): Node {
return null;
}
/** /**
* Should return the title of the game state. If null, no title and back button will * Should return the title of the game state. If null, no title and back button will
* get created * get created
* @returns {string|null}
*/ */
getStateHeaderTitle() { protected getStateHeaderTitle(): string | null {
return null; return null;
} }
@ -44,7 +83,7 @@ export class TextualGameState extends GameState {
* Back button handler, can be overridden. Per default it goes back to the main menu, * 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. * or if coming from the game it moves back to the game again.
*/ */
onBackButton() { override onBackButton() {
if (this.backToStateId) { if (this.backToStateId) {
this.moveToState(this.backToStateId, this.backToStatePayload); this.moveToState(this.backToStateId, this.backToStatePayload);
} else { } else {
@ -61,9 +100,9 @@ export class TextualGameState extends GameState {
/** /**
* Goes to a new state, telling him to go back to this state later * 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, { this.moveToState(stateId, {
backToStateId: this.key, backToStateId: this.key,
backToStatePayload: { backToStatePayload: {
@ -89,43 +128,20 @@ export class TextualGameState extends GameState {
} }
} }
/**
* Overrides the GameState implementation to provide our own html
*/
internalGetFullHtml() {
let headerHtml = "";
if (this.getStateHeaderTitle()) {
headerHtml = `
<div class="headerBar">
<h1><button class="backButton"></button> ${this.getStateHeaderTitle()}</h1>
</div>`;
}
return `
${headerHtml}
<div class="container">
${this.getInnerHTML()}
</div>
`;
}
//// INTERNALS ///// //// INTERNALS /////
/** /**
* Overrides the GameState leave callback to cleanup stuff * Overrides the GameState leave callback to cleanup stuff
*/ */
internalLeaveCallback() { override internalLeaveCallback() {
super.internalLeaveCallback(); super.internalLeaveCallback();
this.dialogs.cleanup(); this.dialogs.cleanup();
} }
/** /**
* Overrides the GameState enter callback to setup required stuff * Overrides the GameState enter callback to setup required stuff
* @param {any} payload
*/ */
internalEnterCallback(payload) { override internalEnterCallback(payload: any) {
super.internalEnterCallback(payload, false); super.internalEnterCallback(payload, false);
if (payload.backToStateId) { if (payload.backToStateId) {
this.backToStateId = payload.backToStateId; this.backToStateId = payload.backToStateId;

View File

@ -57,7 +57,7 @@ export class UndergroundBeltComponent extends Component {
/** /**
* Used on both receiver and sender. * Used on both receiver and sender.
* Reciever: Used to store the next item to transfer, and to block input while doing this * Receiver: Used to store the next item to transfer, and to block input while doing this
* Sender: Used to store which items are currently "travelling" * Sender: Used to store which items are currently "travelling"
* @type {Array<[BaseItem, number]>} Format is [Item, ingame time to eject the item] * @type {Array<[BaseItem, number]>} Format is [Item, ingame time to eject the item]
*/ */

View File

@ -17,13 +17,16 @@ import { Rectangle } from "../core/rectangle";
import { ORIGINAL_SPRITE_SCALE } from "../core/sprites"; import { ORIGINAL_SPRITE_SCALE } from "../core/sprites";
import { lerp, randomInt, round2Digits } from "../core/utils"; import { lerp, randomInt, round2Digits } from "../core/utils";
import { Vector } from "../core/vector"; import { Vector } from "../core/vector";
import { MOD_SIGNALS } from "../mods/mod_signals";
import { Savegame } from "../savegame/savegame"; import { Savegame } from "../savegame/savegame";
import { SavegameSerializer } from "../savegame/savegame_serializer"; import { SavegameSerializer } from "../savegame/savegame_serializer";
import { AchievementProxy } from "./achievement_proxy";
import { AutomaticSave } from "./automatic_save"; import { AutomaticSave } from "./automatic_save";
import { MetaHubBuilding } from "./buildings/hub"; import { MetaHubBuilding } from "./buildings/hub";
import { Camera } from "./camera"; import { Camera } from "./camera";
import { DynamicTickrate } from "./dynamic_tickrate"; import { DynamicTickrate } from "./dynamic_tickrate";
import { EntityManager } from "./entity_manager"; import { EntityManager } from "./entity_manager";
import { GameMode } from "./game_mode";
import { GameSystemManager } from "./game_system_manager"; import { GameSystemManager } from "./game_system_manager";
import { HubGoals } from "./hub_goals"; import { HubGoals } from "./hub_goals";
import { GameHUD } from "./hud/hud"; import { GameHUD } from "./hud/hud";
@ -31,14 +34,11 @@ import { KeyActionMapper } from "./key_action_mapper";
import { GameLogic } from "./logic"; import { GameLogic } from "./logic";
import { MapView } from "./map_view"; import { MapView } from "./map_view";
import { defaultBuildingVariant } from "./meta_building"; import { defaultBuildingVariant } from "./meta_building";
import { GameMode } from "./game_mode";
import { ProductionAnalytics } from "./production_analytics"; import { ProductionAnalytics } from "./production_analytics";
import { GameRoot } from "./root"; import { GameRoot } from "./root";
import { ShapeDefinitionManager } from "./shape_definition_manager"; import { ShapeDefinitionManager } from "./shape_definition_manager";
import { AchievementProxy } from "./achievement_proxy";
import { SoundProxy } from "./sound_proxy"; import { SoundProxy } from "./sound_proxy";
import { GameTime } from "./time/game_time"; import { GameTime } from "./time/game_time";
import { MOD_SIGNALS } from "../mods/mod_signals";
const logger = createLogger("ingame/core"); const logger = createLogger("ingame/core");
@ -101,7 +101,7 @@ export class GameCore {
const root = this.root; const root = this.root;
// This isn't nice, but we need it right here // This isn't nice, but we need it right here
root.keyMapper = new KeyActionMapper(root, this.root.gameState.inputReciever); root.keyMapper = new KeyActionMapper(root, this.root.gameState.inputReceiver);
// Init game mode // Init game mode
root.gameMode = GameMode.create(root, gameModeId, parentState.creationPayload.gameModeParameters); root.gameMode = GameMode.create(root, gameModeId, parentState.creationPayload.gameModeParameters);
@ -142,7 +142,7 @@ export class GameCore {
// @todo Find better place // @todo Find better place
if (G_IS_DEV && globalConfig.debug.manualTickOnly) { if (G_IS_DEV && globalConfig.debug.manualTickOnly) {
this.root.gameState.inputReciever.keydown.add(key => { this.root.gameState.inputReceiver.keydown.add(key => {
if (key.keyCode === 84) { if (key.keyCode === 84) {
// 'T' // 'T'

View File

@ -1,20 +1,19 @@
import { globalConfig } from "../../../core/config";
import { gMetaBuildingRegistry } from "../../../core/global_registries"; import { gMetaBuildingRegistry } from "../../../core/global_registries";
import { Signal, STOP_PROPAGATION } from "../../../core/signal"; import { Signal, STOP_PROPAGATION } from "../../../core/signal";
import { TrackedState } from "../../../core/tracked_state"; import { TrackedState } from "../../../core/tracked_state";
import { safeModulo } from "../../../core/utils";
import { Vector } from "../../../core/vector"; import { Vector } from "../../../core/vector";
import { SOUNDS } from "../../../platform/sound";
import { getBuildingDataFromCode, getCodeFromBuildingData } from "../../building_codes";
import { MetaHubBuilding } from "../../buildings/hub";
import { enumMinerVariants, MetaMinerBuilding } from "../../buildings/miner";
import { enumMouseButton } from "../../camera"; import { enumMouseButton } from "../../camera";
import { StaticMapEntityComponent } from "../../components/static_map_entity"; import { StaticMapEntityComponent } from "../../components/static_map_entity";
import { Entity } from "../../entity"; import { Entity } from "../../entity";
import { KEYMAPPINGS } from "../../key_action_mapper"; import { KEYMAPPINGS } from "../../key_action_mapper";
import { defaultBuildingVariant, MetaBuilding } from "../../meta_building"; import { defaultBuildingVariant, MetaBuilding } from "../../meta_building";
import { BaseHUDPart } from "../base_hud_part";
import { SOUNDS } from "../../../platform/sound";
import { MetaMinerBuilding, enumMinerVariants } from "../../buildings/miner";
import { enumHubGoalRewards } from "../../tutorial_goals"; import { enumHubGoalRewards } from "../../tutorial_goals";
import { getBuildingDataFromCode, getCodeFromBuildingData } from "../../building_codes"; import { BaseHUDPart } from "../base_hud_part";
import { MetaHubBuilding } from "../../buildings/hub";
import { safeModulo } from "../../../core/utils";
/** /**
* Contains all logic for the building placer - this doesn't include the rendering * Contains all logic for the building placer - this doesn't include the rendering
@ -122,7 +121,7 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart {
.add(this.switchDirectionLockSide, this); .add(this.switchDirectionLockSide, this);
keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this); keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this);
keyActionMapper.getBinding(KEYMAPPINGS.placement.pipette).add(this.startPipette, this); keyActionMapper.getBinding(KEYMAPPINGS.placement.pipette).add(this.startPipette, this);
this.root.gameState.inputReciever.keyup.add(this.checkForDirectionLockSwitch, this); this.root.gameState.inputReceiver.keyup.add(this.checkForDirectionLockSwitch, this);
// BINDINGS TO GAME EVENTS // BINDINGS TO GAME EVENTS
this.root.hud.signals.buildingsSelectedForCopy.add(this.abortPlacement, this); this.root.hud.signals.buildingsSelectedForCopy.add(this.abortPlacement, this);

View File

@ -27,7 +27,7 @@ export class HUDEntityDebugger extends BaseHUDPart {
} }
initialize() { initialize() {
this.root.gameState.inputReciever.keydown.add(key => { this.root.gameState.inputReceiver.keydown.add(key => {
if (key.keyCode === 119) { if (key.keyCode === 119) {
// F8 // F8
this.pickEntity(); this.pickEntity();

View File

@ -24,7 +24,7 @@ export class HUDPuzzleCompleteNotification extends BaseHUDPart {
} }
createElements(parent) { createElements(parent) {
this.inputReciever = new InputReceiver("puzzle-complete"); this.inputReceiver = new InputReceiver("puzzle-complete");
this.element = makeDiv(parent, "ingame_HUD_PuzzleCompleteNotification", ["noBlur"]); this.element = makeDiv(parent, "ingame_HUD_PuzzleCompleteNotification", ["noBlur"]);
@ -86,13 +86,13 @@ export class HUDPuzzleCompleteNotification extends BaseHUDPart {
show() { show() {
this.root.soundProxy.playUi(SOUNDS.levelComplete); this.root.soundProxy.playUi(SOUNDS.levelComplete);
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReceiver);
this.visible = true; this.visible = true;
this.timeOfCompletion = this.root.time.now(); this.timeOfCompletion = this.root.time.now();
} }
cleanup() { cleanup() {
this.root.app.inputMgr.makeSureDetached(this.inputReciever); this.root.app.inputMgr.makeSureDetached(this.inputReceiver);
} }
isBlockingOverlay() { isBlockingOverlay() {

View File

@ -139,7 +139,7 @@ export class HUDSandboxController extends BaseHUDPart {
initialize() { initialize() {
// Allow toggling the controller overlay // Allow toggling the controller overlay
this.root.gameState.inputReciever.keydown.add(key => { this.root.gameState.inputReceiver.keydown.add(key => {
if (key.keyCode === 117) { if (key.keyCode === 117) {
// F6 // F6
this.toggle(); this.toggle();

View File

@ -1,11 +1,11 @@
import { BaseHUDPart } from "../base_hud_part";
import { makeDiv, formatBigNumberFull } from "../../../core/utils";
import { DynamicDomAttach } from "../dynamic_dom_attach";
import { InputReceiver } from "../../../core/input_receiver"; import { InputReceiver } from "../../../core/input_receiver";
import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; import { formatBigNumberFull, makeDiv } from "../../../core/utils";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { StaticMapEntityComponent } from "../../components/static_map_entity";
import { BeltComponent } from "../../components/belt"; import { BeltComponent } from "../../components/belt";
import { StaticMapEntityComponent } from "../../components/static_map_entity";
import { KEYMAPPINGS, KeyActionMapper } from "../../key_action_mapper";
import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach";
export class HUDSettingsMenu extends BaseHUDPart { export class HUDSettingsMenu extends BaseHUDPart {
createElements(parent) { createElements(parent) {
@ -83,8 +83,8 @@ export class HUDSettingsMenu extends BaseHUDPart {
attachClass: "visible", attachClass: "visible",
}); });
this.inputReciever = new InputReceiver("settingsmenu"); this.inputReceiver = new InputReceiver("settingsmenu");
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); this.keyActionMapper = new KeyActionMapper(this.root, this.inputReceiver);
this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this);
this.close(); this.close();
@ -92,7 +92,7 @@ export class HUDSettingsMenu extends BaseHUDPart {
show() { show() {
this.visible = true; this.visible = true;
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReceiver);
const totalMinutesPlayed = Math.ceil(this.root.time.now() / 60); const totalMinutesPlayed = Math.ceil(this.root.time.now() / 60);
@ -119,7 +119,7 @@ export class HUDSettingsMenu extends BaseHUDPart {
close() { close() {
this.visible = false; this.visible = false;
this.root.app.inputMgr.makeSureDetached(this.inputReciever); this.root.app.inputMgr.makeSureDetached(this.inputReceiver);
this.update(); this.update();
} }

View File

@ -1,7 +1,7 @@
import { InputReceiver } from "../../../core/input_receiver"; import { InputReceiver } from "../../../core/input_receiver";
import { makeDiv, removeAllChildren } from "../../../core/utils"; import { makeDiv, removeAllChildren } from "../../../core/utils";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; import { KEYMAPPINGS, KeyActionMapper } from "../../key_action_mapper";
import { ShapeDefinition } from "../../shape_definition"; import { ShapeDefinition } from "../../shape_definition";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
@ -38,8 +38,8 @@ export class HUDShapeViewer extends BaseHUDPart {
this.currentShapeKey = null; this.currentShapeKey = null;
this.inputReciever = new InputReceiver("shape_viewer"); this.inputReceiver = new InputReceiver("shape_viewer");
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); this.keyActionMapper = new KeyActionMapper(this.root, this.inputReceiver);
this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this);
@ -67,7 +67,7 @@ export class HUDShapeViewer extends BaseHUDPart {
*/ */
close() { close() {
this.visible = false; this.visible = false;
this.root.app.inputMgr.makeSureDetached(this.inputReciever); this.root.app.inputMgr.makeSureDetached(this.inputReceiver);
this.update(); this.update();
} }
@ -77,7 +77,7 @@ export class HUDShapeViewer extends BaseHUDPart {
*/ */
renderForShape(definition) { renderForShape(definition) {
this.visible = true; this.visible = true;
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReceiver);
removeAllChildren(this.renderArea); removeAllChildren(this.renderArea);

View File

@ -3,7 +3,7 @@ import { InputReceiver } from "../../../core/input_receiver";
import { formatBigNumber, getRomanNumber, makeDiv } from "../../../core/utils"; import { formatBigNumber, getRomanNumber, makeDiv } from "../../../core/utils";
import { SOUNDS } from "../../../platform/sound"; import { SOUNDS } from "../../../platform/sound";
import { T } from "../../../translations"; import { T } from "../../../translations";
import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; import { KEYMAPPINGS, KeyActionMapper } from "../../key_action_mapper";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
@ -193,8 +193,8 @@ export class HUDShop extends BaseHUDPart {
attachClass: "visible", attachClass: "visible",
}); });
this.inputReciever = new InputReceiver("shop"); this.inputReceiver = new InputReceiver("shop");
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); this.keyActionMapper = new KeyActionMapper(this.root, this.inputReceiver);
this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this);
this.keyActionMapper.getBinding(KEYMAPPINGS.ingame.menuClose).add(this.close, this); this.keyActionMapper.getBinding(KEYMAPPINGS.ingame.menuClose).add(this.close, this);
@ -224,13 +224,13 @@ export class HUDShop extends BaseHUDPart {
show() { show() {
this.visible = true; this.visible = true;
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReceiver);
this.rerenderFull(); this.rerenderFull();
} }
close() { close() {
this.visible = false; this.visible = false;
this.root.app.inputMgr.makeSureDetached(this.inputReciever); this.root.app.inputMgr.makeSureDetached(this.inputReceiver);
this.update(); this.update();
} }

View File

@ -1,11 +1,11 @@
import { InputReceiver } from "../../../core/input_receiver"; import { InputReceiver } from "../../../core/input_receiver";
import { makeButton, makeDiv, removeAllChildren } from "../../../core/utils"; import { makeButton, makeDiv, removeAllChildren } from "../../../core/utils";
import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; import { T } from "../../../translations";
import { KEYMAPPINGS, KeyActionMapper } from "../../key_action_mapper";
import { enumAnalyticsDataSource } from "../../production_analytics"; import { enumAnalyticsDataSource } from "../../production_analytics";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { enumDisplayMode, HUDShapeStatisticsHandle, statisticsUnitsSeconds } from "./statistics_handle"; import { HUDShapeStatisticsHandle, enumDisplayMode, statisticsUnitsSeconds } from "./statistics_handle";
import { T } from "../../../translations";
/** /**
* Capitalizes the first letter * Capitalizes the first letter
@ -115,8 +115,8 @@ export class HUDStatistics extends BaseHUDPart {
attachClass: "visible", attachClass: "visible",
}); });
this.inputReciever = new InputReceiver("statistics"); this.inputReceiver = new InputReceiver("statistics");
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); this.keyActionMapper = new KeyActionMapper(this.root, this.inputReceiver);
this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this);
this.keyActionMapper.getBinding(KEYMAPPINGS.ingame.menuClose).add(this.close, this); this.keyActionMapper.getBinding(KEYMAPPINGS.ingame.menuClose).add(this.close, this);
@ -157,14 +157,14 @@ export class HUDStatistics extends BaseHUDPart {
show() { show() {
this.visible = true; this.visible = true;
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReceiver);
this.rerenderFull(); this.rerenderFull();
this.update(); this.update();
} }
close() { close() {
this.visible = false; this.visible = false;
this.root.app.inputMgr.makeSureDetached(this.inputReciever); this.root.app.inputMgr.makeSureDetached(this.inputReceiver);
this.update(); this.update();
} }

View File

@ -1,10 +1,10 @@
import { InputReceiver } from "../../../core/input_receiver"; import { InputReceiver } from "../../../core/input_receiver";
import { TrackedState } from "../../../core/tracked_state"; import { TrackedState } from "../../../core/tracked_state";
import { makeDiv } from "../../../core/utils"; import { makeDiv } from "../../../core/utils";
import { T } from "../../../translations";
import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { DynamicDomAttach } from "../dynamic_dom_attach"; import { DynamicDomAttach } from "../dynamic_dom_attach";
import { T } from "../../../translations";
const tutorialVideos = [3, 4, 5, 6, 7, 9, 10, 11]; const tutorialVideos = [3, 4, 5, 6, 7, 9, 10, 11];
@ -46,8 +46,8 @@ export class HUDPartTutorialHints extends BaseHUDPart {
this.videoAttach.update(false); this.videoAttach.update(false);
this.enlarged = false; this.enlarged = false;
this.inputReciever = new InputReceiver("tutorial_hints"); this.inputReceiver = new InputReceiver("tutorial_hints");
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); this.keyActionMapper = new KeyActionMapper(this.root, this.inputReceiver);
this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this);
this.domAttach = new DynamicDomAttach(this.root, this.element); this.domAttach = new DynamicDomAttach(this.root, this.element);
@ -71,14 +71,14 @@ export class HUDPartTutorialHints extends BaseHUDPart {
close() { close() {
this.enlarged = false; this.enlarged = false;
this.element.classList.remove("enlarged", "noBlur"); this.element.classList.remove("enlarged", "noBlur");
this.root.app.inputMgr.makeSureDetached(this.inputReciever); this.root.app.inputMgr.makeSureDetached(this.inputReceiver);
this.update(); this.update();
} }
show() { show() {
this.element.classList.add("enlarged", "noBlur"); this.element.classList.add("enlarged", "noBlur");
this.enlarged = true; this.enlarged = true;
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReceiver);
this.update(); this.update();
this.videoElement.currentTime = 0; this.videoElement.currentTime = 0;

View File

@ -31,7 +31,7 @@ export class HUDUnlockNotification extends BaseHUDPart {
} }
createElements(parent) { createElements(parent) {
this.inputReciever = new InputReceiver("unlock-notification"); this.inputReceiver = new InputReceiver("unlock-notification");
this.element = makeDiv(parent, "ingame_HUD_UnlockNotification", ["noBlur"]); this.element = makeDiv(parent, "ingame_HUD_UnlockNotification", ["noBlur"]);
@ -67,7 +67,7 @@ export class HUDUnlockNotification extends BaseHUDPart {
return; return;
} }
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReceiver);
this.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace( this.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace(
"<level>", "<level>",
("" + level).padStart(2, "0") ("" + level).padStart(2, "0")
@ -118,7 +118,7 @@ export class HUDUnlockNotification extends BaseHUDPart {
} }
cleanup() { cleanup() {
this.root.app.inputMgr.makeSureDetached(this.inputReciever); this.root.app.inputMgr.makeSureDetached(this.inputReceiver);
if (this.buttonShowTimeout) { if (this.buttonShowTimeout) {
clearTimeout(this.buttonShowTimeout); clearTimeout(this.buttonShowTimeout);
this.buttonShowTimeout = null; this.buttonShowTimeout = null;
@ -158,7 +158,7 @@ export class HUDUnlockNotification extends BaseHUDPart {
} }
close() { close() {
this.root.app.inputMgr.makeSureDetached(this.inputReciever); this.root.app.inputMgr.makeSureDetached(this.inputReceiver);
if (this.buttonShowTimeout) { if (this.buttonShowTimeout) {
clearTimeout(this.buttonShowTimeout); clearTimeout(this.buttonShowTimeout);
this.buttonShowTimeout = null; this.buttonShowTimeout = null;

View File

@ -1,11 +1,11 @@
/* typehints:start */ /* typehints:start */
import { GameRoot } from "./root";
import { InputReceiver } from "../core/input_receiver";
import { Application } from "../application"; import { Application } from "../application";
import { InputReceiver } from "../core/input_receiver";
import { GameRoot } from "./root";
/* typehints:end */ /* typehints:end */
import { Signal, STOP_PROPAGATION } from "../core/signal";
import { IS_MOBILE } from "../core/config"; import { IS_MOBILE } from "../core/config";
import { Signal, STOP_PROPAGATION } from "../core/signal";
import { T } from "../translations"; import { T } from "../translations";
export function keyToKeyCode(str) { export function keyToKeyCode(str) {
@ -353,9 +353,9 @@ export class Keybinding {
get pressed() { get pressed() {
// Check if the key is down // Check if the key is down
if (this.app.inputMgr.keysDown.has(this.keyCode)) { if (this.app.inputMgr.keysDown.has(this.keyCode)) {
// Check if it is the top reciever // Check if it is the top receiver
const reciever = this.keyMapper.inputReceiver; const receiver = this.keyMapper.inputReceiver;
return this.app.inputMgr.getTopReciever() === reciever; return this.app.inputMgr.getTopReceiver() === receiver;
} }
return false; return false;
} }
@ -412,14 +412,14 @@ export class KeyActionMapper {
/** /**
* *
* @param {GameRoot} root * @param {GameRoot} root
* @param {InputReceiver} inputReciever * @param {InputReceiver} inputReceiver
*/ */
constructor(root, inputReciever) { constructor(root, inputReceiver) {
this.root = root; this.root = root;
this.inputReceiver = inputReciever; this.inputReceiver = inputReceiver;
inputReciever.keydown.add(this.handleKeydown, this); inputReceiver.keydown.add(this.handleKeydown, this);
inputReciever.keyup.add(this.handleKeyup, this); inputReceiver.keyup.add(this.handleKeyup, this);
/** @type {Object.<string, Keybinding>} */ /** @type {Object.<string, Keybinding>} */
this.keybindings = {}; this.keybindings = {};
@ -443,8 +443,8 @@ export class KeyActionMapper {
} }
} }
inputReciever.pageBlur.add(this.onPageBlur, this); inputReceiver.pageBlur.add(this.onPageBlur, this);
inputReciever.destroyed.add(this.cleanup, this); inputReceiver.destroyed.add(this.cleanup, this);
} }
/** /**

View File

@ -11,7 +11,7 @@ import {
enumDirectionToVector, enumDirectionToVector,
enumInvertedDirections, enumInvertedDirections,
} from "../../core/vector"; } from "../../core/vector";
import { enumUndergroundBeltMode, UndergroundBeltComponent } from "../components/underground_belt"; import { UndergroundBeltComponent, enumUndergroundBeltMode } from "../components/underground_belt";
import { Entity } from "../entity"; import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter"; import { GameSystemWithFilter } from "../game_system_with_filter";
@ -243,7 +243,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter {
* @param {Entity} entity * @param {Entity} entity
* @returns {import("../components/underground_belt").LinkedUndergroundBelt} * @returns {import("../components/underground_belt").LinkedUndergroundBelt}
*/ */
findRecieverForSender(entity) { findReceiverForSender(entity) {
const staticComp = entity.components.StaticMapEntity; const staticComp = entity.components.StaticMapEntity;
const undergroundComp = entity.components.UndergroundBelt; const undergroundComp = entity.components.UndergroundBelt;
const searchDirection = staticComp.localDirectionToWorld(enumDirection.top); const searchDirection = staticComp.localDirectionToWorld(enumDirection.top);
@ -299,7 +299,7 @@ export class UndergroundBeltSystem extends GameSystemWithFilter {
let cacheEntry = undergroundComp.cachedLinkedEntity; let cacheEntry = undergroundComp.cachedLinkedEntity;
if (!cacheEntry) { if (!cacheEntry) {
// Need to recompute cache // Need to recompute cache
cacheEntry = undergroundComp.cachedLinkedEntity = this.findRecieverForSender(entity); cacheEntry = undergroundComp.cachedLinkedEntity = this.findReceiverForSender(entity);
} }
if (!cacheEntry.entity) { if (!cacheEntry.entity) {

8
src/js/globals.d.ts vendored
View File

@ -149,8 +149,8 @@ declare namespace JSX {
*/ */
type IntrinsicElements = { type IntrinsicElements = {
[K in keyof HTMLElementTagNameMap]: { [K in keyof HTMLElementTagNameMap]: {
children?: Node | Node[]; children?: JSXNode | JSXNode[];
[k: string]: Node | Node[] | string | number | boolean; [k: string]: JSXNode | JSXNode[] | string | number | boolean;
}; };
}; };
/** /**
@ -164,14 +164,16 @@ declare namespace JSX {
* An attributes object. * An attributes object.
*/ */
type Props = { [k: string]: unknown }; type Props = { [k: string]: unknown };
/** /**
* A functional component requiring attributes to match `T`. * A functional component requiring attributes to match `T`.
*/ */
type Component<T extends Props> = { type Component<T extends Props> = {
(props: T): Element; (props: T): Element;
}; };
/** /**
* A child of a JSX element. * A child of a JSX element.
*/ */
type Node = Element | string | boolean | null | undefined; type JSXNode = Node | string | boolean | null | undefined;
} }

View File

@ -1,4 +1,4 @@
function isDisplayed(node: JSX.Node): node is Exclude<JSX.Node, boolean | null | undefined> { function isDisplayed(node: JSX.JSXNode): node is Exclude<JSX.JSXNode, boolean | null | undefined> {
return typeof node !== "boolean" && node != null; return typeof node !== "boolean" && node != null;
} }
@ -27,7 +27,7 @@ function jsx<U extends JSX.Props>(
} }
throw new TypeError("JSX element attribute assigned invalid type"); throw new TypeError("JSX element attribute assigned invalid type");
}); });
element.append(...([children].flat(Infinity) as JSX.Node[]).filter(isDisplayed)); element.append(...([children].flat(Infinity) as JSX.JSXNode[]).filter(isDisplayed));
return element; return element;
} }
@ -38,4 +38,4 @@ function jsx<U extends JSX.Props>(
const Fragment = (props: JSX.Props) => props.children as JSX.Element; const Fragment = (props: JSX.Props) => props.children as JSX.Element;
// jsxs is used when there are multiple children // jsxs is used when there are multiple children
export { jsx, jsx as jsxs, Fragment }; export { Fragment, jsx, jsx as jsxs };

View File

@ -2,10 +2,10 @@
import { Application } from "../application"; import { Application } from "../application";
/* typehints:end */ /* typehints:end */
import { DialogWithForm } from "root/core/modal_dialog_elements";
import { FormElementInput } from "root/core/modal_dialog_forms";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { compressX64 } from "../core/lzstring"; import { compressX64 } from "../core/lzstring";
import { DialogWithForm } from "../core/modal_dialog_elements";
import { FormElementInput } from "../core/modal_dialog_forms";
import { timeoutPromise } from "../core/utils"; import { timeoutPromise } from "../core/utils";
import { T } from "../translations"; import { T } from "../translations";

View File

@ -93,7 +93,7 @@ export class KeybindingsState extends TextualGameState {
type: "info", type: "info",
}); });
dialog.inputReciever.keydown.add(({ keyCode, shift, alt, event }) => { dialog.inputReceiver.keydown.add(({ keyCode, shift, alt, event }) => {
if (keyCode === 27) { if (keyCode === 27) {
this.dialogs.closeDialog(dialog); this.dialogs.closeDialog(dialog);
return; return;
@ -125,7 +125,7 @@ export class KeybindingsState extends TextualGameState {
this.updateKeybindings(); this.updateKeybindings();
}); });
dialog.inputReciever.backButton.add(() => {}); dialog.inputReceiver.backButton.add(() => {});
this.dialogs.internalShowDialog(dialog); this.dialogs.internalShowDialog(dialog);
this.app.sound.playUiSound(SOUNDS.dialogOk); this.app.sound.playUiSound(SOUNDS.dialogOk);

View File

@ -1,118 +0,0 @@
import { THIRDPARTY_URLS } from "../core/config";
import { TextualGameState } from "../core/textual_game_state";
import { MODS } from "../mods/modloader";
import { T } from "../translations";
export class ModsState extends TextualGameState {
constructor() {
super("ModsState");
}
getStateHeaderTitle() {
return T.mods.title;
}
get modsSupported() {
return true;
}
internalGetFullHtml() {
let headerHtml = `
<div class="headerBar">
<h1><button class="backButton"></button> ${this.getStateHeaderTitle()}</h1>
<div class="actions">
${
MODS.mods.length > 0
? `<button class="styledButton browseMods">${T.mods.browseMods}</button>`
: ""
}
<button class="styledButton openModsFolder">${T.mods.openFolder}</button>
</div>
</div>`;
return `
${headerHtml}
<div class="container">
${this.getInnerHTML()}
</div>
`;
}
getMainContentHTML() {
if (MODS.mods.length === 0) {
return `
<div class="modsStats noMods">
${T.mods.modsInfo}
<button class="styledButton browseMods">${T.mods.browseMods}</button>
</div>
`;
}
let modsHtml = ``;
MODS.mods.forEach(mod => {
modsHtml += `
<div class="mod">
<div class="mainInfo">
<span class="name">${mod.metadata.name}</span>
<span class="description">${mod.metadata.description}</span>
<a class="website" href="${mod.metadata.website}" target="_blank">${T.mods.modWebsite}</a>
</div>
<span class="version"><strong>${T.mods.version}</strong>${mod.metadata.version}</span>
<span class="author"><strong>${T.mods.author}</strong>${mod.metadata.author}</span>
<div class="value checkbox checked">
<span class="knob"></span>
</div>
</div>
`;
});
return `
<div class="modsStats">
${T.mods.modsInfo}
</div>
<div class="modsList">
${modsHtml}
</div>
`;
}
onEnter() {
const openModsFolder = this.htmlElement.querySelector(".openModsFolder");
if (openModsFolder) {
this.trackClicks(openModsFolder, this.openModsFolder);
}
const browseMods = this.htmlElement.querySelector(".browseMods");
if (browseMods) {
this.trackClicks(browseMods, this.openBrowseMods);
}
const checkboxes = this.htmlElement.querySelectorAll(".checkbox");
Array.from(checkboxes).forEach(checkbox => {
this.trackClicks(checkbox, this.showModTogglingComingSoon);
});
}
showModTogglingComingSoon() {
this.dialogs.showWarning(T.mods.togglingComingSoon.title, T.mods.togglingComingSoon.description);
}
openModsFolder() {
ipcRenderer.invoke("open-mods-folder");
}
openBrowseMods() {
this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.modBrowser);
}
getDefaultPreviousState() {
return "SettingsState";
}
}

49
src/js/states/mods.tsx Normal file
View File

@ -0,0 +1,49 @@
import { Mod } from "@/mods/mod";
import { MODS } from "@/mods/modloader";
import { TextualGameState } from "../core/textual_game_state";
import { T } from "../translations";
export class ModsState extends TextualGameState {
constructor() {
super("ModsState");
}
getStateHeaderTitle() {
return T.mods.title;
}
protected getInitialContent() {
const modElements = MODS.mods.map(mod => this.getModElement(mod));
return (
<div class="content">
<div class={`modsGrid${MODS.anyModsActive() ? "" : " noMods"}`}>
{MODS.anyModsActive() ? modElements : this.getNoModsMessage()}
</div>
</div>
);
}
private getModElement(mod: Mod): HTMLElement {
// TODO: Ensure proper design and localization once mods are reworked
return (
<div class="mod">
<div class="title">
<b>{mod.metadata.name}</b> by <i>{mod.metadata.author}</i>
</div>
<div class="description">{mod.metadata.description}</div>
<div class="advanced">
{mod.metadata.id} @ {mod.metadata.version}
</div>
</div>
);
}
private getNoModsMessage(): HTMLElement {
return <div class="noModsMessage">No mods are currently installed.</div>;
}
getDefaultPreviousState() {
return "SettingsState";
}
}