1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-11 09:11:50 +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 },
alias: {
"global-compression": resolve("../src/js/core/lzstring.js"),
"root": resolve("../src/js/"),
"@": resolve("../src/js/"),
},
fullySpecified: false,
extensions: [".ts", ".js", ".tsx", ".jsx"],

View File

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

View File

@ -1,144 +1,34 @@
#state_ModsState {
.mainContent {
display: flex;
flex-direction: column;
}
> .headerBar {
.modsGrid {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
> h1 {
justify-self: start;
}
.openModsFolder {
background-color: $modsColor;
.noMods {
grid-template-columns: unset;
place-items: center;
}
}
.noModSupport {
.mod {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
flex-direction: column;
text-align: center;
max-width: 80%;
align-self: center;
.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 {
padding: 0.6em;
gap: 0.3em;
background-color: $mainBgColor;
@include S(border-radius, 0.75 * $globalBorderRadius);
@include PlainText;
color: $accentColorDark;
&.noMods {
@include S(width, 400px);
align-self: center;
justify-self: center;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
.title b {
@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 {
@include S(margin-top, 10px);
overflow-y: scroll;
pointer-events: all;
@include S(padding-right, 5px);
flex-grow: 1;
.advanced {
@include SuperSmallText;
}
.mod {
@include S(border-radius, $globalBorderRadius);
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;
}
}
@include DarkThemeOverride {
background-color: $darkModeGameBackground;
}
}
}

View File

@ -1,15 +1,12 @@
/* typehints:start */
import { Application } from "../application";
import { StateManager } from "./state_manager";
/* typehints:end */
import { globalConfig } from "./config";
import { MUSIC } from "@/platform/sound";
import type { Application } from "../application";
import { ClickDetector } from "./click_detector";
import { logSection, createLogger } from "./logging";
import { globalConfig } from "./config";
import { InputReceiver } from "./input_receiver";
import { waitNextFrame } from "./utils";
import { createLogger, logSection } from "./logging";
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");
@ -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<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();
this.inputReceiver = new InputReceiver("state-" + key);
this.inputReceiver.backButton.add(this.onBackButton, this);
}
//// 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,45 @@ 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 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
* "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
* @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 +243,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,12 +253,12 @@ 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.pushReciever(this.inputReciever);
this.app.inputMgr.pushReceiver(this.inputReceiver);
this.htmlElement = this.getDivElement();
this.htmlElement.classList.add("active");
@ -293,7 +284,7 @@ export class GameState {
this.onLeave();
this.htmlElement.classList.remove("active");
this.app.inputMgr.popReciever(this.inputReciever);
this.app.inputMgr.popReceiver(this.inputReceiver);
this.internalCleanUpClickDetectors();
this.asyncChannel.cancelAll();
}
@ -325,18 +316,27 @@ 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 (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
* @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;
}

View File

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

View File

@ -1,15 +1,15 @@
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 { 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 clickDetectors: ClickDetector[] = [];
public inputReciever: InputReceiver;
public inputReceiver: InputReceiver;
public enterHandler: 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.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 | []) {
this.app.inputMgr.popReciever(this.inputReciever);
this.app.inputMgr.popReceiver(this.inputReceiver);
if (id !== "close-button") {
this.buttonSignals[id].dispatch(...payload);
@ -160,7 +160,7 @@ export class Dialog<T extends string = never, U extends unknown[] = []> {
});
title.appendChild(closeBtn);
this.inputReciever.backButton.add(() => this.internalButtonHandler("close-button"));
this.inputReceiver.backButton.add(() => this.internalButtonHandler("close-button"));
}
const content = document.createElement("div");
@ -231,7 +231,7 @@ export class Dialog<T extends string = never, U extends unknown[] = []> {
}
this.element = elem;
this.app.inputMgr.pushReciever(this.inputReciever);
this.app.inputMgr.pushReceiver(this.inputReceiver);
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
// dispatched to the modal dialogs, it will not call the internalButtonHandler,
// 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) {
this.clickDetectors[i].cleanup();
@ -300,8 +300,8 @@ export class DialogLoading extends Dialog {
});
// Loading dialog can not get closed with back button
this.inputReciever.backButton.removeAll();
this.inputReciever.context = "dialog-loading";
this.inputReceiver.backButton.removeAll();
this.inputReceiver.context = "dialog-loading";
}
createElement() {
@ -322,7 +322,7 @@ export class DialogLoading extends Dialog {
loader.classList.add("loadingIndicator");
elem.appendChild(loader);
this.app.inputMgr.pushReciever(this.inputReciever);
this.app.inputMgr.pushReceiver(this.inputReceiver);
return elem;
}

View File

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

View File

@ -1,40 +1,79 @@
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 `
<div class="content mainContent">
${this.getMainContentHTML()}
</div>
`;
getInnerHTML(): string {
return "";
}
/**
* Should return the states HTML content.
* @deprecated
*/
getMainContentHTML() {
getMainContentHTML(): string {
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
* get created
* @returns {string|null}
*/
getStateHeaderTitle() {
protected getStateHeaderTitle(): string | 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,
* 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 +100,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 +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 /////
/**
* 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;

View File

@ -57,7 +57,7 @@ export class UndergroundBeltComponent extends Component {
/**
* 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"
* @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 { lerp, randomInt, round2Digits } from "../core/utils";
import { Vector } from "../core/vector";
import { MOD_SIGNALS } from "../mods/mod_signals";
import { Savegame } from "../savegame/savegame";
import { SavegameSerializer } from "../savegame/savegame_serializer";
import { AchievementProxy } from "./achievement_proxy";
import { AutomaticSave } from "./automatic_save";
import { MetaHubBuilding } from "./buildings/hub";
import { Camera } from "./camera";
import { DynamicTickrate } from "./dynamic_tickrate";
import { EntityManager } from "./entity_manager";
import { GameMode } from "./game_mode";
import { GameSystemManager } from "./game_system_manager";
import { HubGoals } from "./hub_goals";
import { GameHUD } from "./hud/hud";
@ -31,14 +34,11 @@ import { KeyActionMapper } from "./key_action_mapper";
import { GameLogic } from "./logic";
import { MapView } from "./map_view";
import { defaultBuildingVariant } from "./meta_building";
import { GameMode } from "./game_mode";
import { ProductionAnalytics } from "./production_analytics";
import { GameRoot } from "./root";
import { ShapeDefinitionManager } from "./shape_definition_manager";
import { AchievementProxy } from "./achievement_proxy";
import { SoundProxy } from "./sound_proxy";
import { GameTime } from "./time/game_time";
import { MOD_SIGNALS } from "../mods/mod_signals";
const logger = createLogger("ingame/core");
@ -101,7 +101,7 @@ export class GameCore {
const root = this.root;
// 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
root.gameMode = GameMode.create(root, gameModeId, parentState.creationPayload.gameModeParameters);
@ -142,7 +142,7 @@ export class GameCore {
// @todo Find better place
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) {
// 'T'

View File

@ -1,20 +1,19 @@
import { globalConfig } from "../../../core/config";
import { gMetaBuildingRegistry } from "../../../core/global_registries";
import { Signal, STOP_PROPAGATION } from "../../../core/signal";
import { TrackedState } from "../../../core/tracked_state";
import { safeModulo } from "../../../core/utils";
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 { StaticMapEntityComponent } from "../../components/static_map_entity";
import { Entity } from "../../entity";
import { KEYMAPPINGS } from "../../key_action_mapper";
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 { getBuildingDataFromCode, getCodeFromBuildingData } from "../../building_codes";
import { MetaHubBuilding } from "../../buildings/hub";
import { safeModulo } from "../../../core/utils";
import { BaseHUDPart } from "../base_hud_part";
/**
* 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);
keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, 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
this.root.hud.signals.buildingsSelectedForCopy.add(this.abortPlacement, this);

View File

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

View File

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

View File

@ -139,7 +139,7 @@ export class HUDSandboxController extends BaseHUDPart {
initialize() {
// Allow toggling the controller overlay
this.root.gameState.inputReciever.keydown.add(key => {
this.root.gameState.inputReceiver.keydown.add(key => {
if (key.keyCode === 117) {
// F6
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 { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper";
import { formatBigNumberFull, makeDiv } from "../../../core/utils";
import { T } from "../../../translations";
import { StaticMapEntityComponent } from "../../components/static_map_entity";
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 {
createElements(parent) {
@ -83,8 +83,8 @@ export class HUDSettingsMenu extends BaseHUDPart {
attachClass: "visible",
});
this.inputReciever = new InputReceiver("settingsmenu");
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever);
this.inputReceiver = new InputReceiver("settingsmenu");
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReceiver);
this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this);
this.close();
@ -92,7 +92,7 @@ export class HUDSettingsMenu extends BaseHUDPart {
show() {
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);
@ -119,7 +119,7 @@ export class HUDSettingsMenu extends BaseHUDPart {
close() {
this.visible = false;
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
this.root.app.inputMgr.makeSureDetached(this.inputReceiver);
this.update();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -149,8 +149,8 @@ declare namespace JSX {
*/
type IntrinsicElements = {
[K in keyof HTMLElementTagNameMap]: {
children?: Node | Node[];
[k: string]: Node | Node[] | string | number | boolean;
children?: JSXNode | JSXNode[];
[k: string]: JSXNode | JSXNode[] | string | number | boolean;
};
};
/**
@ -164,14 +164,16 @@ declare namespace JSX {
* An attributes object.
*/
type Props = { [k: string]: unknown };
/**
* A functional component requiring attributes to match `T`.
*/
type Component<T extends Props> = {
(props: T): 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;
}
@ -27,7 +27,7 @@ function jsx<U extends JSX.Props>(
}
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;
}
@ -38,4 +38,4 @@ function jsx<U extends JSX.Props>(
const Fragment = (props: JSX.Props) => props.children as JSX.Element;
// 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";
/* typehints:end */
import { DialogWithForm } from "root/core/modal_dialog_elements";
import { FormElementInput } from "root/core/modal_dialog_forms";
import { createLogger } from "../core/logging";
import { compressX64 } from "../core/lzstring";
import { DialogWithForm } from "../core/modal_dialog_elements";
import { FormElementInput } from "../core/modal_dialog_forms";
import { timeoutPromise } from "../core/utils";
import { T } from "../translations";

View File

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