1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-12-09 16:21:51 +00:00

Add Most Useful TS files (#13)

* Update Signal

* Update modal_dialogs

* Inputs

* Update factories

* Update tracked state

* Changed let to const where possible

* Add HUD typings

* improvements to typings

* fix exports not being exposed to mods

* fix signal typings

* remove TypedSignal

* fix all reported type errors

---------

Co-authored-by: Thomas B <t.ferb1@gmail.com>
Co-authored-by: EmeraldBlock <yygengjunior@gmail.com>
This commit is contained in:
Bagel03 2023-11-17 17:02:08 -05:00 committed by GitHub
parent 76a00db34d
commit 6db782d66a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1169 additions and 1134 deletions

View File

@ -11,7 +11,9 @@ const resetDtMs = 16;
export class AnimationFrame { export class AnimationFrame {
constructor() { constructor() {
/** @type {Signal<[number]>} */
this.frameEmitted = new Signal(); this.frameEmitted = new Signal();
/** @type {Signal<[number]>} */
this.bgFrameEmitted = new Signal(); this.bgFrameEmitted = new Signal();
this.lastTime = performance.now(); this.lastTime = performance.now();

View File

@ -55,6 +55,7 @@ export class BackgroundResourcesLoader {
this.mainMenuPromise = null; this.mainMenuPromise = null;
this.ingamePromise = null; this.ingamePromise = null;
/** @type {Signal<[{ progress: number }]>} */
this.resourceStateChangedSignal = new Signal(); this.resourceStateChangedSignal = new Signal();
} }

View File

@ -83,16 +83,25 @@ export class ClickDetector {
this.preventClick = preventClick; this.preventClick = preventClick;
// Signals // Signals
/** @type {Signal<[Vector, TouchEvent | MouseEvent]>} */
this.click = new Signal(); this.click = new Signal();
/** @type {Signal<[Vector, MouseEvent]>} */
this.rightClick = new Signal(); this.rightClick = new Signal();
/** @type {Signal<[TouchEvent | MouseEvent]>} */
this.touchstart = new Signal(); this.touchstart = new Signal();
/** @type {Signal<[TouchEvent | MouseEvent]>} */
this.touchmove = new Signal(); this.touchmove = new Signal();
/** @type {Signal<[TouchEvent | MouseEvent]>} */
this.touchend = new Signal(); this.touchend = new Signal();
/** @type {Signal<[TouchEvent | MouseEvent]>} */
this.touchcancel = new Signal(); this.touchcancel = new Signal();
// Simple signals which just receive the touch position // Simple signals which just receive the touch position
/** @type {Signal<[number, number]>} */
this.touchstartSimple = new Signal(); this.touchstartSimple = new Signal();
/** @type {Signal<[number, number]>} */
this.touchmoveSimple = new Signal(); this.touchmoveSimple = new Signal();
/** @type {Signal<[(TouchEvent | MouseEvent)?]>} */
this.touchendSimple = new Signal(); this.touchendSimple = new Signal();
// Store time of touch start // Store time of touch start

View File

@ -3,21 +3,19 @@ import { createLogger } from "./logging";
const logger = createLogger("factory"); const logger = createLogger("factory");
// simple factory pattern // simple factory pattern
export class Factory { export class Factory<T> {
constructor(id) { // Store array as well as dictionary, to speed up lookups
this.id = id; public entries: Class<T>[] = [];
public entryIds: string[] = [];
public idToEntry: Record<string, Class<T>> = {};
// Store array as well as dictionary, to speed up lookups constructor(public id: string) {}
this.entries = [];
this.entryIds = [];
this.idToEntry = {};
}
getId() { getId() {
return this.id; return this.id;
} }
register(entry) { register(entry: Class<T> & { getId(): string }) {
// Extract id // Extract id
const id = entry.getId(); const id = entry.getId();
assert(id, "Factory: Invalid id for class: " + entry); assert(id, "Factory: Invalid id for class: " + entry);
@ -33,19 +31,15 @@ export class Factory {
/** /**
* Checks if a given id is registered * Checks if a given id is registered
* @param {string} id
* @returns {boolean}
*/ */
hasId(id) { hasId(id: string): boolean {
return !!this.idToEntry[id]; return !!this.idToEntry[id];
} }
/** /**
* Finds an instance by a given id * Finds an instance by a given id
* @param {string} id
* @returns {object}
*/ */
findById(id) { findById(id: string): Class<T> {
const entry = this.idToEntry[id]; const entry = this.idToEntry[id];
if (!entry) { if (!entry) {
logger.error("Object with id", id, "is not registered on factory", this.id, "!"); logger.error("Object with id", id, "is not registered on factory", this.id, "!");
@ -57,25 +51,22 @@ export class Factory {
/** /**
* Returns all entries * Returns all entries
* @returns {Array<object>}
*/ */
getEntries() { getEntries(): Class<T>[] {
return this.entries; return this.entries;
} }
/** /**
* Returns all registered ids * Returns all registered ids
* @returns {Array<string>}
*/ */
getAllIds() { getAllIds(): string[] {
return this.entryIds; return this.entryIds;
} }
/** /**
* Returns amount of stored entries * Returns amount of stored entries
* @returns {number}
*/ */
getNumEntries() { getNumEntries(): number {
return this.entries.length; return this.entries.length;
} }
} }

View File

@ -1,39 +0,0 @@
import { SingletonFactory } from "./singleton_factory";
import { Factory } from "./factory";
/**
* @typedef {import("../game/time/base_game_speed").BaseGameSpeed} BaseGameSpeed
* @typedef {import("../game/component").Component} Component
* @typedef {import("../game/base_item").BaseItem} BaseItem
* @typedef {import("../game/game_mode").GameMode} GameMode
* @typedef {import("../game/meta_building").MetaBuilding} MetaBuilding
// These factories are here to remove circular dependencies
/** @type {SingletonFactoryTemplate<MetaBuilding>} */
export let gMetaBuildingRegistry = new SingletonFactory();
/** @type {Object.<string, Array<Class<MetaBuilding>>>} */
export let gBuildingsByCategory = null;
/** @type {FactoryTemplate<Component>} */
export let gComponentRegistry = new Factory("component");
/** @type {FactoryTemplate<GameMode>} */
export let gGameModeRegistry = new Factory("gameMode");
/** @type {FactoryTemplate<BaseGameSpeed>} */
export let gGameSpeedRegistry = new Factory("gamespeed");
/** @type {FactoryTemplate<BaseItem>} */
export let gItemRegistry = new Factory("item");
// Helpers
/**
* @param {Object.<string, Array<Class<MetaBuilding>>>} buildings
*/
export function initBuildingsByCategory(buildings) {
gBuildingsByCategory = buildings;
}

View File

@ -0,0 +1,20 @@
import type { BaseGameSpeed } from "../game/time/base_game_speed";
import type { Component } from "../game/component";
import type { BaseItem } from "../game/base_item";
import type { GameMode } from "../game/game_mode";
import type { MetaBuilding } from "../game/meta_building";
import { SingletonFactory } from "./singleton_factory";
import { Factory } from "./factory";
// These factories are here to remove circular dependencies
export const gMetaBuildingRegistry = new SingletonFactory<MetaBuilding>("metaBuilding");
export const gComponentRegistry = new Factory<Component>("component");
export const gGameModeRegistry = new Factory<GameMode>("gameMode");
export const gGameSpeedRegistry = new Factory<BaseGameSpeed>("gameSpeed");
export const gItemRegistry = new Factory<BaseItem>("item");

View File

@ -1,7 +1,5 @@
/* typehints:start */ import type { Application } from "../application";
import { Application } from "../application"; import type { InputReceiver, ReceiverId } from "./input_receiver";
import { InputReceiver } from "./input_receiver";
/* typehints:end */
import { Signal, STOP_PROPAGATION } from "./signal"; import { Signal, STOP_PROPAGATION } from "./signal";
import { createLogger } from "./logging"; import { createLogger } from "./logging";
@ -10,47 +8,33 @@ 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 filters: ((arg: string) => boolean)[] = [];
/** /**
* * All keys which are currently down
* @param {Application} app
*/ */
constructor(app) { public keysDown = new Set<number>();
this.app = app;
/** @type {Array<InputReceiver>} */
this.recieverStack = [];
/** @type {Array<function(any) : boolean>} */
this.filters = [];
/**
* All keys which are currently down
*/
this.keysDown = new Set();
constructor(public app: Application) {
this.bindToEvents(); this.bindToEvents();
} }
/** /**
* Attaches a new filter which can filter and reject events * Attaches a new filter which can filter and reject events
* @param {function(any): boolean} filter
*/ */
installFilter(filter) { installFilter(filter: (arg: string) => boolean) {
this.filters.push(filter); this.filters.push(filter);
} }
/** /**
* Removes an attached filter * Removes an attached filter
* @param {function(any) : boolean} filter
*/ */
dismountFilter(filter) { dismountFilter(filter: (arg: string) => boolean) {
fastArrayDeleteValue(this.filters, filter); fastArrayDeleteValue(this.filters, filter);
} }
/** pushReciever(reciever: InputReceiver) {
* @param {InputReceiver} reciever
*/
pushReciever(reciever) {
if (this.isRecieverAttached(reciever)) { if (this.isRecieverAttached(reciever)) {
assert(false, "Can not add reciever " + reciever.context + " twice"); assert(false, "Can not add reciever " + reciever.context + " twice");
logger.error("Can not add reciever", reciever.context, "twice"); logger.error("Can not add reciever", reciever.context, "twice");
@ -66,10 +50,7 @@ export class InputDistributor {
} }
} }
/** popReciever(reciever: InputReceiver) {
* @param {InputReceiver} reciever
*/
popReciever(reciever) {
if (this.recieverStack.indexOf(reciever) < 0) { if (this.recieverStack.indexOf(reciever) < 0) {
assert(false, "Can not pop reciever " + reciever.context + " since its not contained"); assert(false, "Can not pop reciever " + reciever.context + " since its not contained");
logger.error("Can not pop reciever", reciever.context, "since its not contained"); logger.error("Can not pop reciever", reciever.context, "since its not contained");
@ -86,45 +67,29 @@ export class InputDistributor {
arrayDeleteValue(this.recieverStack, reciever); arrayDeleteValue(this.recieverStack, reciever);
} }
/** isRecieverAttached(reciever: InputReceiver) {
* @param {InputReceiver} reciever
*/
isRecieverAttached(reciever) {
return this.recieverStack.indexOf(reciever) >= 0; return this.recieverStack.indexOf(reciever) >= 0;
} }
/** isRecieverOnTop(reciever: InputReceiver) {
* @param {InputReceiver} reciever
*/
isRecieverOnTop(reciever) {
return ( return (
this.isRecieverAttached(reciever) && this.isRecieverAttached(reciever) &&
this.recieverStack[this.recieverStack.length - 1] === reciever this.recieverStack[this.recieverStack.length - 1] === reciever
); );
} }
/** makeSureAttachedAndOnTop(reciever: InputReceiver) {
* @param {InputReceiver} reciever
*/
makeSureAttachedAndOnTop(reciever) {
this.makeSureDetached(reciever); this.makeSureDetached(reciever);
this.pushReciever(reciever); this.pushReciever(reciever);
} }
/** makeSureDetached(reciever: InputReceiver) {
* @param {InputReceiver} reciever
*/
makeSureDetached(reciever) {
if (this.isRecieverAttached(reciever)) { if (this.isRecieverAttached(reciever)) {
arrayDeleteValue(this.recieverStack, reciever); arrayDeleteValue(this.recieverStack, reciever);
} }
} }
/** destroyReceiver(reciever: InputReceiver) {
*
* @param {InputReceiver} reciever
*/
destroyReceiver(reciever) {
this.makeSureDetached(reciever); this.makeSureDetached(reciever);
reciever.cleanup(); reciever.cleanup();
} }
@ -153,7 +118,10 @@ export class InputDistributor {
document.addEventListener("paste", this.handlePaste.bind(this)); document.addEventListener("paste", this.handlePaste.bind(this));
} }
forwardToReceiver(eventId, payload = null) { forwardToReceiver<T extends ReceiverId>(
eventId: T,
payload: Parameters<InputReceiver[T]["dispatch"]>[0] = null
) {
// Check filters // Check filters
for (let i = 0; i < this.filters.length; ++i) { for (let i = 0; i < this.filters.length; ++i) {
if (!this.filters[i](eventId)) { if (!this.filters[i](eventId)) {
@ -168,13 +136,11 @@ export class InputDistributor {
} }
const signal = reciever[eventId]; const signal = reciever[eventId];
assert(signal instanceof Signal, "Not a valid event id"); assert(signal instanceof Signal, "Not a valid event id");
return signal.dispatch(payload); // probably not possible to type properly, since the types of `signal` and `payload` are correlated
return signal.dispatch(payload as never);
} }
/** handleBackButton(event: Event) {
* @param {Event} event
*/
handleBackButton(event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.forwardToReceiver("backButton"); this.forwardToReceiver("backButton");
@ -184,21 +150,15 @@ export class InputDistributor {
* Handles when the page got blurred * Handles when the page got blurred
*/ */
handleBlur() { handleBlur() {
this.forwardToReceiver("pageBlur", {}); this.forwardToReceiver("pageBlur");
this.keysDown.clear(); this.keysDown.clear();
} }
/** handlePaste(ev: ClipboardEvent) {
*
*/
handlePaste(ev) {
this.forwardToReceiver("paste", ev); this.forwardToReceiver("paste", ev);
} }
/** handleKeyMouseDown(event: KeyboardEvent | MouseEvent) {
* @param {KeyboardEvent | MouseEvent} event
*/
handleKeyMouseDown(event) {
const keyCode = event instanceof MouseEvent ? event.button + 1 : event.keyCode; const keyCode = event instanceof MouseEvent ? event.button + 1 : event.keyCode;
if ( if (
keyCode === 4 || // MB4 keyCode === 4 || // MB4
@ -236,10 +196,7 @@ export class InputDistributor {
} }
} }
/** handleKeyMouseUp(event: KeyboardEvent | MouseEvent) {
* @param {KeyboardEvent | MouseEvent} event
*/
handleKeyMouseUp(event) {
const keyCode = event instanceof MouseEvent ? event.button + 1 : event.keyCode; const keyCode = event instanceof MouseEvent ? event.button + 1 : event.keyCode;
this.keysDown.delete(keyCode); this.keysDown.delete(keyCode);

View File

@ -1,27 +0,0 @@
import { Signal } from "./signal";
export class InputReceiver {
constructor(context = "unknown") {
this.context = context;
this.backButton = new Signal();
this.keydown = new Signal();
this.keyup = new Signal();
this.pageBlur = new Signal();
// Dispatched on destroy
this.destroyed = new Signal();
this.paste = new Signal();
}
cleanup() {
this.backButton.removeAll();
this.keydown.removeAll();
this.keyup.removeAll();
this.paste.removeAll();
this.destroyed.dispatch();
}
}

View File

@ -0,0 +1,47 @@
import { Signal } from "./signal";
export type KeydownEvent = {
keyCode: number;
shift: boolean;
alt: boolean;
ctrl: boolean;
initial: boolean;
event: KeyboardEvent | MouseEvent;
};
export type KeyupEvent = {
keyCode: number;
shift: boolean;
alt: boolean;
};
export class InputReceiver {
public backButton = new Signal();
public keydown = new Signal<[KeydownEvent]>();
public keyup = new Signal<[KeyupEvent]>();
public pageBlur = new Signal();
// Dispatched on destroy
public destroyed = new Signal();
public paste = new Signal<[ClipboardEvent]>();
constructor(public context: string = "unknown") {}
cleanup() {
this.backButton.removeAll();
this.keydown.removeAll();
this.keyup.removeAll();
this.paste.removeAll();
this.destroyed.dispatch();
}
}
export type ReceiverId = keyof {
[K in keyof InputReceiver as InputReceiver[K] extends Signal<any[]>
? K extends "destroyed"
? never
: K
: never]: unknown;
};

View File

@ -1,467 +1,502 @@
/* typehints:start */ import type { Application } from "../application";
import { Application } from "../application";
/* typehints:end */ import { Signal, STOP_PROPAGATION } from "./signal";
import { arrayDeleteValue, waitNextFrame } from "./utils";
import { Signal, STOP_PROPAGATION } from "./signal"; import { ClickDetector, ClickDetectorConstructorArgs } from "./click_detector";
import { arrayDeleteValue, waitNextFrame } from "./utils"; import { SOUNDS } from "../platform/sound";
import { ClickDetector } from "./click_detector"; import { InputReceiver, KeydownEvent } from "./input_receiver";
import { SOUNDS } from "../platform/sound"; import { FormElement } from "./modal_dialog_forms";
import { InputReceiver } from "./input_receiver"; import { globalConfig } from "./config";
import { FormElement } from "./modal_dialog_forms"; import { getStringForKeyCode } from "../game/key_action_mapper";
import { globalConfig } from "./config"; import { createLogger } from "./logging";
import { getStringForKeyCode } from "../game/key_action_mapper"; import { T } from "../translations";
import { createLogger } from "./logging";
import { T } from "../translations"; /*
* ***************************************************
/* *
* *************************************************** * LEGACY CODE WARNING
* *
* LEGACY CODE WARNING * This is old code from yorg3.io and needs to be refactored
* * @TODO
* This is old code from yorg3.io and needs to be refactored *
* @TODO * ***************************************************
* */
* ***************************************************
*/ const kbEnter = 13;
const kbCancel = 27;
const kbEnter = 13;
const kbCancel = 27; const logger = createLogger("dialogs");
const logger = createLogger("dialogs"); export type DialogButtonStr<T extends string> = `${T}:${string}` | T;
export type DialogButtonType = "info" | "loading" | "warning";
/**
* Basic text based dialog /**
*/ * Basic text based dialog
export class Dialog { */
/** export class Dialog<T extends string = never, U extends unknown[] = []> {
* public title: string;
* Constructs a new dialog with the given options public app: Application;
* @param {object} param0 public contentHTML: string;
* @param {Application} param0.app public type: string;
* @param {string} param0.title Title of the dialog public buttonIds: string[];
* @param {string} param0.contentHTML Inner dialog html public closeButton: boolean;
* @param {Array<string>} param0.buttons public dialogElem: HTMLDivElement;
* Button list, each button contains of up to 3 parts separated by ':'. public element: HTMLDivElement;
* Part 0: The id, one of the one defined in dialog_buttons.yaml
* Part 1: The style, either good, bad or misc public closeRequested = new Signal();
* Part 2 (optional): Additional parameters separated by '/', available are: public buttonSignals = {} as Record<T, Signal<U | []>>;
* timeout: This button is only available after some waiting time
* kb_enter: This button is triggered by the enter key public valueChosen = new Signal<[unknown]>();
* kb_escape This button is triggered by the escape key
* @param {string=} param0.type The dialog type, either "info" or "warn" public timeouts: number[] = [];
* @param {boolean=} param0.closeButton Whether this dialog has a close button public clickDetectors: ClickDetector[] = [];
*/
constructor({ app, title, contentHTML, buttons, type = "info", closeButton = false }) { public inputReciever: InputReceiver;
this.app = app; public enterHandler: T = null;
this.title = title; public escapeHandler: T = null;
this.contentHTML = contentHTML;
this.type = type; /**
this.buttonIds = buttons; *
this.closeButton = closeButton; * Constructs a new dialog with the given options
* @param param0
this.closeRequested = new Signal(); * @param param0.title Title of the dialog
this.buttonSignals = {}; * @param param0.contentHTML Inner dialog html
* @param param0.buttons
for (let i = 0; i < buttons.length; ++i) { * Button list, each button contains of up to 3 parts separated by ':'.
if (G_IS_DEV && globalConfig.debug.disableTimedButtons) { * Part 0: The id, one of the one defined in dialog_buttons.yaml
this.buttonIds[i] = this.buttonIds[i].replace(":timeout", ""); * Part 1: The style, either good, bad or misc
} * Part 2 (optional): Additional parameters separated by '/', available are:
* timeout: This button is only available after some waiting time
const buttonId = this.buttonIds[i].split(":")[0]; * kb_enter: This button is triggered by the enter key
this.buttonSignals[buttonId] = new Signal(); * kb_escape This button is triggered by the escape key
} * @param param0.type The dialog type, either "info", "warning", or "loading"
* @param param0.closeButton Whether this dialog has a close button
this.valueChosen = new Signal(); */
constructor({
this.timeouts = []; app,
this.clickDetectors = []; title,
contentHTML,
this.inputReciever = new InputReceiver("dialog-" + this.title); buttons,
type = "info",
this.inputReciever.keydown.add(this.handleKeydown, this); closeButton = false,
}: {
this.enterHandler = null; app: Application;
this.escapeHandler = null; title: string;
} contentHTML: string;
buttons?: DialogButtonStr<T>[];
/** type?: DialogButtonType;
* Internal keydown handler closeButton?: boolean;
* @param {object} param0 }) {
* @param {number} param0.keyCode this.app = app;
* @param {boolean} param0.shift this.title = title;
* @param {boolean} param0.alt this.contentHTML = contentHTML;
* @param {boolean} param0.ctrl this.type = type;
*/ this.buttonIds = buttons;
handleKeydown({ keyCode, shift, alt, ctrl }) { this.closeButton = closeButton;
if (keyCode === kbEnter && this.enterHandler) {
this.internalButtonHandler(this.enterHandler); for (let i = 0; i < buttons.length; ++i) {
return STOP_PROPAGATION; if (G_IS_DEV && globalConfig.debug.disableTimedButtons) {
} this.buttonIds[i] = this.buttonIds[i].replace(":timeout", "");
}
if (keyCode === kbCancel && this.escapeHandler) {
this.internalButtonHandler(this.escapeHandler); const buttonId = this.buttonIds[i].split(":")[0];
return STOP_PROPAGATION; this.buttonSignals[buttonId] = new Signal();
} }
}
this.inputReciever = new InputReceiver("dialog-" + this.title);
internalButtonHandler(id, ...payload) {
this.app.inputMgr.popReciever(this.inputReciever); this.inputReciever.keydown.add(this.handleKeydown, this);
}
if (id !== "close-button") {
this.buttonSignals[id].dispatch(...payload); /**
} * Internal keydown handler
this.closeRequested.dispatch(); */
} handleKeydown({ keyCode, shift, alt, ctrl }: KeydownEvent): void | STOP_PROPAGATION {
if (keyCode === kbEnter && this.enterHandler) {
createElement() { this.internalButtonHandler(this.enterHandler);
const elem = document.createElement("div"); return STOP_PROPAGATION;
elem.classList.add("ingameDialog"); }
this.dialogElem = document.createElement("div"); if (keyCode === kbCancel && this.escapeHandler) {
this.dialogElem.classList.add("dialogInner"); this.internalButtonHandler(this.escapeHandler);
return STOP_PROPAGATION;
if (this.type) { }
this.dialogElem.classList.add(this.type); }
}
elem.appendChild(this.dialogElem); internalButtonHandler(id: T | "close-button", ...payload: U | []) {
this.app.inputMgr.popReciever(this.inputReciever);
const title = document.createElement("h1");
title.innerText = this.title; if (id !== "close-button") {
title.classList.add("title"); this.buttonSignals[id].dispatch(...payload);
this.dialogElem.appendChild(title); }
this.closeRequested.dispatch();
if (this.closeButton) { }
this.dialogElem.classList.add("hasCloseButton");
createElement() {
const closeBtn = document.createElement("button"); const elem = document.createElement("div");
closeBtn.classList.add("closeButton"); elem.classList.add("ingameDialog");
this.trackClicks(closeBtn, () => this.internalButtonHandler("close-button"), { this.dialogElem = document.createElement("div");
applyCssClass: "pressedSmallElement", this.dialogElem.classList.add("dialogInner");
});
if (this.type) {
title.appendChild(closeBtn); this.dialogElem.classList.add(this.type); // @TODO: `this.type` seems unused
this.inputReciever.backButton.add(() => this.internalButtonHandler("close-button")); }
} elem.appendChild(this.dialogElem);
const content = document.createElement("div"); const title = document.createElement("h1");
content.classList.add("content"); title.innerText = this.title;
content.innerHTML = this.contentHTML; title.classList.add("title");
this.dialogElem.appendChild(content); this.dialogElem.appendChild(title);
if (this.buttonIds.length > 0) { if (this.closeButton) {
const buttons = document.createElement("div"); this.dialogElem.classList.add("hasCloseButton");
buttons.classList.add("buttons");
const closeBtn = document.createElement("button");
// Create buttons closeBtn.classList.add("closeButton");
for (let i = 0; i < this.buttonIds.length; ++i) {
const [buttonId, buttonStyle, rawParams] = this.buttonIds[i].split(":"); this.trackClicks(closeBtn, () => this.internalButtonHandler("close-button"), {
applyCssClass: "pressedSmallElement",
const button = document.createElement("button"); });
button.classList.add("button");
button.classList.add("styledButton"); title.appendChild(closeBtn);
button.classList.add(buttonStyle); this.inputReciever.backButton.add(() => this.internalButtonHandler("close-button"));
button.innerText = T.dialogs.buttons[buttonId]; }
const params = (rawParams || "").split("/"); const content = document.createElement("div");
const useTimeout = params.indexOf("timeout") >= 0; content.classList.add("content");
content.innerHTML = this.contentHTML;
const isEnter = params.indexOf("enter") >= 0; this.dialogElem.appendChild(content);
const isEscape = params.indexOf("escape") >= 0;
if (this.buttonIds.length > 0) {
if (isEscape && this.closeButton) { const buttons = document.createElement("div");
logger.warn("Showing dialog with close button, and additional cancel button"); buttons.classList.add("buttons");
}
// Create buttons
if (useTimeout) { for (let i = 0; i < this.buttonIds.length; ++i) {
button.classList.add("timedButton"); const [buttonId, buttonStyle, rawParams] = this.buttonIds[i].split(":") as [
const timeout = setTimeout(() => { T,
button.classList.remove("timedButton"); string,
arrayDeleteValue(this.timeouts, timeout); string?
}, 1000); ]; // @TODO: some button strings omit `buttonStyle`
this.timeouts.push(timeout);
} const button = document.createElement("button");
if (isEnter || isEscape) { button.classList.add("button");
// if (this.app.settings.getShowKeyboardShortcuts()) { button.classList.add("styledButton");
// Show keybinding button.classList.add(buttonStyle);
const spacer = document.createElement("code"); button.innerText = T.dialogs.buttons[buttonId as string];
spacer.classList.add("keybinding");
spacer.innerHTML = getStringForKeyCode(isEnter ? kbEnter : kbCancel); const params = (rawParams || "").split("/");
button.appendChild(spacer); const useTimeout = params.indexOf("timeout") >= 0;
// }
const isEnter = params.indexOf("enter") >= 0;
if (isEnter) { const isEscape = params.indexOf("escape") >= 0;
this.enterHandler = buttonId;
} if (isEscape && this.closeButton) {
if (isEscape) { logger.warn("Showing dialog with close button, and additional cancel button");
this.escapeHandler = buttonId; }
}
} if (useTimeout) {
button.classList.add("timedButton");
this.trackClicks(button, () => this.internalButtonHandler(buttonId)); const timeout = setTimeout(() => {
buttons.appendChild(button); button.classList.remove("timedButton");
} arrayDeleteValue(this.timeouts, timeout);
}, 1000) as unknown as number; // @TODO: @types/node should not be affecting this
this.dialogElem.appendChild(buttons); this.timeouts.push(timeout);
} else { }
this.dialogElem.classList.add("buttonless"); if (isEnter || isEscape) {
} // if (this.app.settings.getShowKeyboardShortcuts()) {
// Show keybinding
this.element = elem; const spacer = document.createElement("code");
this.app.inputMgr.pushReciever(this.inputReciever); spacer.classList.add("keybinding");
spacer.innerHTML = getStringForKeyCode(isEnter ? kbEnter : kbCancel);
return this.element; button.appendChild(spacer);
} // }
setIndex(index) { if (isEnter) {
this.element.style.zIndex = index; this.enterHandler = buttonId;
} }
if (isEscape) {
destroy() { this.escapeHandler = buttonId;
if (!this.element) { }
assert(false, "Tried to destroy dialog twice"); }
return;
} this.trackClicks(button, () => this.internalButtonHandler(buttonId));
// We need to do this here, because if the backbutton event gets buttons.appendChild(button);
// 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.dialogElem.appendChild(buttons);
} else {
for (let i = 0; i < this.clickDetectors.length; ++i) { this.dialogElem.classList.add("buttonless");
this.clickDetectors[i].cleanup(); }
}
this.clickDetectors = []; this.element = elem;
this.app.inputMgr.pushReciever(this.inputReciever);
this.element.remove();
this.element = null; return this.element;
}
for (let i = 0; i < this.timeouts.length; ++i) {
clearTimeout(this.timeouts[i]); setIndex(index: string) {
} this.element.style.zIndex = index;
this.timeouts = []; }
}
destroy() {
hide() { if (!this.element) {
this.element.classList.remove("visible"); assert(false, "Tried to destroy dialog twice");
} return;
}
show() { // We need to do this here, because if the backbutton event gets
this.element.classList.add("visible"); // 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);
/**
* Helper method to track clicks on an element for (let i = 0; i < this.clickDetectors.length; ++i) {
* @param {Element} elem this.clickDetectors[i].cleanup();
* @param {function():void} handler }
* @param {import("./click_detector").ClickDetectorConstructorArgs=} args this.clickDetectors = [];
* @returns {ClickDetector}
*/ this.element.remove();
trackClicks(elem, handler, args = {}) { this.element = null;
const detector = new ClickDetector(elem, args);
detector.click.add(handler, this); for (let i = 0; i < this.timeouts.length; ++i) {
this.clickDetectors.push(detector); clearTimeout(this.timeouts[i]);
return detector; }
} this.timeouts = [];
} }
/** hide() {
* Dialog which simply shows a loading spinner this.element.classList.remove("visible");
*/ }
export class DialogLoading extends Dialog {
constructor(app, text = "") { show() {
super({ this.element.classList.add("visible");
app, }
title: "",
contentHTML: "", /**
buttons: [], * Helper method to track clicks on an element
type: "loading", */
}); trackClicks(elem: Element, handler: () => void, args: ClickDetectorConstructorArgs = {}) {
const detector = new ClickDetector(elem, args);
// Loading dialog can not get closed with back button detector.click.add(handler, this);
this.inputReciever.backButton.removeAll(); this.clickDetectors.push(detector);
this.inputReciever.context = "dialog-loading"; return detector;
}
this.text = text; }
}
/**
createElement() { * Dialog which simply shows a loading spinner
const elem = document.createElement("div"); */
elem.classList.add("ingameDialog"); export class DialogLoading extends Dialog {
elem.classList.add("loadingDialog"); constructor(app: Application, public text = "") {
this.element = elem; super({
app,
if (this.text) { title: "",
const text = document.createElement("div"); contentHTML: "",
text.classList.add("text"); buttons: [],
text.innerText = this.text; type: "loading",
elem.appendChild(text); });
}
// Loading dialog can not get closed with back button
const loader = document.createElement("div"); this.inputReciever.backButton.removeAll();
loader.classList.add("prefab_LoadingTextWithAnim"); this.inputReciever.context = "dialog-loading";
loader.classList.add("loadingIndicator"); }
elem.appendChild(loader);
createElement() {
this.app.inputMgr.pushReciever(this.inputReciever); const elem = document.createElement("div");
elem.classList.add("ingameDialog");
return elem; elem.classList.add("loadingDialog");
} this.element = elem;
}
if (this.text) {
export class DialogOptionChooser extends Dialog { const text = document.createElement("div");
constructor({ app, title, options }) { text.classList.add("text");
let html = "<div class='optionParent'>"; text.innerText = this.text;
elem.appendChild(text);
options.options.forEach(({ value, text, desc = null, iconPrefix = null }) => { }
const descHtml = desc ? `<span class="desc">${desc}</span>` : "";
let iconHtml = iconPrefix ? `<span class="icon icon-${iconPrefix}-${value}"></span>` : ""; const loader = document.createElement("div");
html += ` loader.classList.add("prefab_LoadingTextWithAnim");
<div class='option ${value === options.active ? "active" : ""} ${ loader.classList.add("loadingIndicator");
iconPrefix ? "hasIcon" : "" elem.appendChild(loader);
}' data-optionvalue='${value}'>
${iconHtml} this.app.inputMgr.pushReciever(this.inputReciever);
<span class='title'>${text}</span>
${descHtml} return elem;
</div> }
`; }
});
type DialogOptionChooserOption = { value: string; text: string; desc?: string; iconPrefix?: string };
html += "</div>"; export class DialogOptionChooser extends Dialog<"optionSelected", [string]> {
super({ public options: {
app, options: DialogOptionChooserOption[];
title, active: string;
contentHTML: html, };
buttons: [], public initialOption: string;
type: "info",
closeButton: true, constructor({
}); app,
title,
this.options = options; options,
this.initialOption = options.active; }: {
app: Application;
this.buttonSignals.optionSelected = new Signal(); title: string;
} options: {
options: DialogOptionChooserOption[];
createElement() { active: string;
const div = super.createElement(); };
this.dialogElem.classList.add("optionChooserDialog"); }) {
let html = "<div class='optionParent'>";
div.querySelectorAll("[data-optionvalue]").forEach(handle => {
const value = handle.getAttribute("data-optionvalue"); options.options.forEach(({ value, text, desc = null, iconPrefix = null }) => {
if (!handle) { const descHtml = desc ? `<span class="desc">${desc}</span>` : "";
logger.error("Failed to bind option value in dialog:", value); const iconHtml = iconPrefix ? `<span class="icon icon-${iconPrefix}-${value}"></span>` : "";
return; html += `
} <div class='option ${value === options.active ? "active" : ""} ${
// Need click detector here to forward elements, otherwise scrolling does not work iconPrefix ? "hasIcon" : ""
const detector = new ClickDetector(handle, { }' data-optionvalue='${value}'>
consumeEvents: false, ${iconHtml}
preventDefault: false, <span class='title'>${text}</span>
clickSound: null, ${descHtml}
applyCssClass: "pressedOption", </div>
targetOnly: true, `;
}); });
this.clickDetectors.push(detector);
html += "</div>";
if (value !== this.initialOption) { super({
detector.click.add(() => { app,
const selected = div.querySelector(".option.active"); title,
if (selected) { contentHTML: html,
selected.classList.remove("active"); buttons: [],
} else { type: "info",
logger.warn("No selected option"); closeButton: true,
} });
handle.classList.add("active");
this.app.sound.playUiSound(SOUNDS.uiClick); this.options = options;
this.internalButtonHandler("optionSelected", value); this.initialOption = options.active;
});
} this.buttonSignals.optionSelected = new Signal();
}); }
return div;
} createElement() {
} const div = super.createElement();
this.dialogElem.classList.add("optionChooserDialog");
export class DialogWithForm extends Dialog {
/** div.querySelectorAll("[data-optionvalue]").forEach(handle => {
* const value = handle.getAttribute("data-optionvalue");
* @param {object} param0 if (!handle) {
* @param {Application} param0.app logger.error("Failed to bind option value in dialog:", value);
* @param {string} param0.title return;
* @param {string} param0.desc }
* @param {array=} param0.buttons // Need click detector here to forward elements, otherwise scrolling does not work
* @param {string=} param0.confirmButtonId const detector = new ClickDetector(handle, {
* @param {string=} param0.extraButton consumeEvents: false,
* @param {boolean=} param0.closeButton preventDefault: false,
* @param {Array<FormElement>} param0.formElements clickSound: null,
*/ applyCssClass: "pressedOption",
constructor({ targetOnly: true,
app, });
title, this.clickDetectors.push(detector);
desc,
formElements, if (value !== this.initialOption) {
buttons = ["cancel", "ok:good"], detector.click.add(() => {
confirmButtonId = "ok", const selected = div.querySelector(".option.active");
closeButton = true, if (selected) {
}) { selected.classList.remove("active");
let html = ""; } else {
html += desc + "<br>"; logger.warn("No selected option");
for (let i = 0; i < formElements.length; ++i) { }
html += formElements[i].getHtml(); handle.classList.add("active");
} this.app.sound.playUiSound(SOUNDS.uiClick);
this.internalButtonHandler("optionSelected", value);
super({ });
app, }
title: title, });
contentHTML: html, return div;
buttons: buttons, }
type: "info", }
closeButton,
}); export class DialogWithForm<T extends string = "cancel" | "ok"> extends Dialog<T> {
this.confirmButtonId = confirmButtonId; public confirmButtonId: string;
this.formElements = formElements; // `FormElement` is invariant so `unknown` and `never` don't work
public formElements: FormElement<any>[];
this.enterHandler = confirmButtonId;
} constructor({
app,
internalButtonHandler(id, ...payload) { title,
if (id === this.confirmButtonId) { desc,
if (this.hasAnyInvalid()) { formElements,
this.dialogElem.classList.remove("errorShake"); buttons = ["cancel", "ok:good"] as any,
waitNextFrame().then(() => { confirmButtonId = "ok" as any,
if (this.dialogElem) { closeButton = true,
this.dialogElem.classList.add("errorShake"); }: {
} app: Application;
}); title: string;
this.app.sound.playUiSound(SOUNDS.uiError); desc: string;
return; formElements: FormElement<any>[];
} buttons?: DialogButtonStr<T>[];
} confirmButtonId?: T;
closeButton?: boolean;
super.internalButtonHandler(id, payload); }) {
} let html = "";
html += desc + "<br>";
hasAnyInvalid() { for (let i = 0; i < formElements.length; ++i) {
for (let i = 0; i < this.formElements.length; ++i) { html += formElements[i].getHtml();
if (!this.formElements[i].isValid()) { }
return true;
} super({
} app,
return false; title: title,
} contentHTML: html,
buttons: buttons,
createElement() { type: "info",
const div = super.createElement(); closeButton,
});
for (let i = 0; i < this.formElements.length; ++i) { this.confirmButtonId = confirmButtonId;
const elem = this.formElements[i]; this.formElements = formElements;
elem.bindEvents(div, this.clickDetectors);
// elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested); this.enterHandler = confirmButtonId;
elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen); }
}
internalButtonHandler(id: T | "close-button", ...payload: []) {
waitNextFrame().then(() => { if (id === this.confirmButtonId) {
this.formElements[this.formElements.length - 1].focus(); if (this.hasAnyInvalid()) {
}); this.dialogElem.classList.remove("errorShake");
waitNextFrame().then(() => {
return div; if (this.dialogElem) {
} this.dialogElem.classList.add("errorShake");
} }
});
this.app.sound.playUiSound(SOUNDS.uiError);
return;
}
}
super.internalButtonHandler(id, ...payload);
}
hasAnyInvalid() {
for (let i = 0; i < this.formElements.length; ++i) {
if (!this.formElements[i].isValid()) {
return true;
}
}
return false;
}
createElement() {
const div = super.createElement();
for (let i = 0; i < this.formElements.length; ++i) {
const elem = this.formElements[i];
elem.bindEvents(div, this.clickDetectors);
// elem.valueChosen.add(this.closeRequested.dispatch, this.closeRequested);
elem.valueChosen.add(this.valueChosen.dispatch, this.valueChosen);
}
waitNextFrame().then(() => {
this.formElements[this.formElements.length - 1].focus();
});
return div;
}
}

View File

@ -1,238 +1,238 @@
import { BaseItem } from "../game/base_item"; import { BaseItem } from "../game/base_item";
import { ClickDetector } from "./click_detector"; import { ClickDetector } from "./click_detector";
import { Signal } from "./signal"; import { Signal } from "./signal";
/* /*
* *************************************************** * ***************************************************
* *
* LEGACY CODE WARNING * LEGACY CODE WARNING
* *
* This is old code from yorg3.io and needs to be refactored * This is old code from yorg3.io and needs to be refactored
* @TODO * @TODO
* *
* *************************************************** * ***************************************************
*/ */
export class FormElement { export abstract class FormElement<T = string> {
constructor(id, label) { public valueChosen = new Signal<[T]>();
this.id = id;
this.label = label; constructor(public id: string, public label: string) {}
this.valueChosen = new Signal(); abstract getHtml(): string;
}
getFormElement(parent: HTMLElement): HTMLElement {
getHtml() { return parent.querySelector("[data-formId='" + this.id + "']");
abstract; }
return "";
} abstract bindEvents(parent: HTMLDivElement, clickTrackers: ClickDetector[]): void;
getFormElement(parent) { focus() {}
return parent.querySelector("[data-formId='" + this.id + "']");
} isValid() {
return true;
bindEvents(parent, clickTrackers) { }
abstract;
} abstract getValue(): T;
}
focus() {}
export class FormElementInput extends FormElement {
isValid() { public placeholder: string;
return true; public defaultValue: string;
} public inputType: "text" | "email" | "token";
public validator: (value: string) => boolean;
/** @returns {any} */
getValue() { public element: HTMLInputElement = null;
abstract;
} constructor({
} id,
label = null,
export class FormElementInput extends FormElement { placeholder,
constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) { defaultValue = "",
super(id, label); inputType = "text",
this.placeholder = placeholder; validator = null,
this.defaultValue = defaultValue; }: {
this.inputType = inputType; id: string;
this.validator = validator; label?: string;
placeholder: string;
this.element = null; defaultValue?: string;
} inputType?: "text" | "email" | "token";
validator?: (value: string) => boolean;
getHtml() { }) {
let classes = []; super(id, label);
let inputType = "text"; this.placeholder = placeholder;
let maxlength = 256; this.defaultValue = defaultValue;
switch (this.inputType) { this.inputType = inputType;
case "text": { this.validator = validator;
classes.push("input-text"); }
break;
} getHtml() {
const classes = [];
case "email": { let inputType = "text";
classes.push("input-email"); let maxlength = 256;
inputType = "email"; // @TODO: `inputType` and these classes are unused
break; switch (this.inputType) {
} case "text": {
classes.push("input-text");
case "token": { break;
classes.push("input-token"); }
inputType = "text";
maxlength = 4; case "email": {
break; classes.push("input-email");
} inputType = "email";
} break;
}
return `
<div class="formElement input"> case "token": {
${this.label ? `<label>${this.label}</label>` : ""} classes.push("input-token");
<input inputType = "text";
type="${inputType}" maxlength = 4;
value="${this.defaultValue.replace(/["\\]+/gi, "")}" break;
maxlength="${maxlength}" }
autocomplete="off" }
autocorrect="off"
autocapitalize="off" return `
spellcheck="false" <div class="formElement input">
class="${classes.join(" ")}" ${this.label ? `<label>${this.label}</label>` : ""}
placeholder="${this.placeholder.replace(/["\\]+/gi, "")}" <input
data-formId="${this.id}"> type="${inputType}"
</div> value="${this.defaultValue.replace(/["\\]+/gi, "")}"
`; maxlength="${maxlength}"
} autocomplete="off"
autocorrect="off"
bindEvents(parent, clickTrackers) { autocapitalize="off"
this.element = this.getFormElement(parent); spellcheck="false"
this.element.addEventListener("input", event => this.updateErrorState()); class="${classes.join(" ")}"
this.updateErrorState(); placeholder="${this.placeholder.replace(/["\\]+/gi, "")}"
} data-formId="${this.id}">
</div>
updateErrorState() { `;
this.element.classList.toggle("errored", !this.isValid()); }
}
bindEvents(parent: HTMLDivElement, clickTrackers: ClickDetector[]) {
isValid() { this.element = this.getFormElement(parent) as HTMLInputElement;
return !this.validator || this.validator(this.element.value); this.element.addEventListener("input", event => this.updateErrorState());
} this.updateErrorState();
}
getValue() {
return this.element.value; updateErrorState() {
} this.element.classList.toggle("errored", !this.isValid());
}
setValue(value) {
this.element.value = value; isValid() {
this.updateErrorState(); return !this.validator || this.validator(this.element.value);
} }
focus() { getValue() {
this.element.focus(); return this.element.value;
this.element.select(); }
}
} setValue(value: string) {
this.element.value = value;
export class FormElementCheckbox extends FormElement { this.updateErrorState();
constructor({ id, label, defaultValue = true }) { }
super(id, label);
this.defaultValue = defaultValue; focus() {
this.value = this.defaultValue; this.element.focus();
this.element.select();
this.element = null; }
} }
getHtml() { export class FormElementCheckbox extends FormElement<boolean> {
return ` public defaultValue: boolean;
<div class="formElement checkBoxFormElem"> public value: boolean;
${this.label ? `<label>${this.label}</label>` : ""} public element: HTMLDivElement;
<div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'>
<span class="knob"></span > constructor({ id, label, defaultValue = true }) {
</div > super(id, label);
</div> this.defaultValue = defaultValue;
`; this.value = this.defaultValue;
}
this.element = null;
bindEvents(parent, clickTrackers) { }
this.element = this.getFormElement(parent);
const detector = new ClickDetector(this.element, { getHtml() {
consumeEvents: false, return `
preventDefault: false, <div class="formElement checkBoxFormElem">
}); ${this.label ? `<label>${this.label}</label>` : ""}
clickTrackers.push(detector); <div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'>
detector.click.add(this.toggle, this); <span class="knob"></span >
} </div >
</div>
getValue() { `;
return this.value; }
}
bindEvents(parent: HTMLDivElement, clickTrackers: ClickDetector[]) {
toggle() { this.element = this.getFormElement(parent) as HTMLDivElement;
this.value = !this.value; const detector = new ClickDetector(this.element, {
this.element.classList.toggle("checked", this.value); consumeEvents: false,
} preventDefault: false,
});
focus(parent) {} clickTrackers.push(detector);
} detector.click.add(this.toggle, this);
}
export class FormElementItemChooser extends FormElement {
/** getValue() {
* return this.value;
* @param {object} param0 }
* @param {string} param0.id
* @param {string=} param0.label toggle() {
* @param {Array<BaseItem>} param0.items this.value = !this.value;
*/ this.element.classList.toggle("checked", this.value);
constructor({ id, label, items = [] }) { }
super(id, label);
this.items = items; focus() {}
this.element = null; }
/** export class FormElementItemChooser extends FormElement<BaseItem> {
* @type {BaseItem} public items: BaseItem[];
*/ public element: HTMLDivElement = null;
this.chosenItem = null; public chosenItem: BaseItem = null;
}
constructor({ id, label, items = [] }: { id: string; label: string; items: BaseItem[] }) {
getHtml() { super(id, label);
let classes = []; this.items = items;
}
return `
<div class="formElement"> getHtml() {
${this.label ? `<label>${this.label}</label>` : ""} const classes = [];
<div class="ingameItemChooser input" data-formId="${this.id}"></div>
</div> return `
`; <div class="formElement">
} ${this.label ? `<label>${this.label}</label>` : ""}
<div class="ingameItemChooser input" data-formId="${this.id}"></div>
/** </div>
* @param {HTMLElement} parent `;
* @param {Array<ClickDetector>} clickTrackers }
*/
bindEvents(parent, clickTrackers) { bindEvents(parent: HTMLElement, clickTrackers: ClickDetector[]) {
this.element = this.getFormElement(parent); this.element = this.getFormElement(parent) as HTMLDivElement;
for (let i = 0; i < this.items.length; ++i) { for (let i = 0; i < this.items.length; ++i) {
const item = this.items[i]; const item = this.items[i];
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = 128; canvas.width = 128;
canvas.height = 128; canvas.height = 128;
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
item.drawFullSizeOnCanvas(context, 128); item.drawFullSizeOnCanvas(context, 128);
this.element.appendChild(canvas); this.element.appendChild(canvas);
const detector = new ClickDetector(canvas, {}); const detector = new ClickDetector(canvas, {});
clickTrackers.push(detector); clickTrackers.push(detector);
detector.click.add(() => { detector.click.add(() => {
this.chosenItem = item; this.chosenItem = item;
this.valueChosen.dispatch(item); this.valueChosen.dispatch(item);
}); });
} }
} }
isValid() { isValid() {
return true; return true;
} }
getValue() { getValue() {
return null; return null;
} }
focus() {} focus() {}
} }

View File

@ -1,17 +1,16 @@
export const STOP_PROPAGATION = "stop_propagation"; export const STOP_PROPAGATION = "stop_propagation" as const;
export type STOP_PROPAGATION = typeof STOP_PROPAGATION;
export class Signal { export type SignalReceiver<T extends unknown[]> = (...args: T) => STOP_PROPAGATION | void;
constructor() {
this.receivers = []; export class Signal<T extends unknown[] = []> {
this.modifyCount = 0; public receivers: { receiver: SignalReceiver<T>; scope: object }[] = [];
} public modifyCount: number = 0;
/** /**
* Adds a new signal listener * Adds a new signal listener
* @param {function} receiver
* @param {object} scope
*/ */
add(receiver, scope = null) { add(receiver: SignalReceiver<T>, scope: object = null) {
assert(receiver, "receiver is null"); assert(receiver, "receiver is null");
this.receivers.push({ receiver, scope }); this.receivers.push({ receiver, scope });
++this.modifyCount; ++this.modifyCount;
@ -19,10 +18,8 @@ export class Signal {
/** /**
* Adds a new signal listener * Adds a new signal listener
* @param {function} receiver
* @param {object} scope
*/ */
addToTop(receiver, scope = null) { addToTop(receiver: SignalReceiver<T>, scope: object = null) {
assert(receiver, "receiver is null"); assert(receiver, "receiver is null");
this.receivers.unshift({ receiver, scope }); this.receivers.unshift({ receiver, scope });
++this.modifyCount; ++this.modifyCount;
@ -30,15 +27,14 @@ export class Signal {
/** /**
* Dispatches the signal * Dispatches the signal
* @param {...any} payload
*/ */
dispatch() { dispatch(...payload: T): void | STOP_PROPAGATION {
const modifyState = this.modifyCount; const modifyState = this.modifyCount;
const n = this.receivers.length; const n = this.receivers.length;
for (let i = 0; i < n; ++i) { for (let i = 0; i < n; ++i) {
const { receiver, scope } = this.receivers[i]; const { receiver, scope } = this.receivers[i];
if (receiver.apply(scope, arguments) === STOP_PROPAGATION) { if (receiver.apply(scope, payload) === STOP_PROPAGATION) {
return STOP_PROPAGATION; return STOP_PROPAGATION;
} }
@ -51,9 +47,8 @@ export class Signal {
/** /**
* Removes a receiver * Removes a receiver
* @param {function} receiver
*/ */
remove(receiver) { remove(receiver: SignalReceiver<T>) {
let index = null; let index = null;
const n = this.receivers.length; const n = this.receivers.length;
for (let i = 0; i < n; ++i) { for (let i = 0; i < n; ++i) {

View File

@ -3,20 +3,18 @@ import { createLogger } from "./logging";
const logger = createLogger("singleton_factory"); const logger = createLogger("singleton_factory");
// simple factory pattern // simple factory pattern
export class SingletonFactory { export class SingletonFactory<T extends { getId(): string }> {
constructor(id) { // Store array as well as dictionary, to speed up lookups
this.id = id; public entries: T[] = [];
public idToEntry: Record<string, T> = {};
// Store array as well as dictionary, to speed up lookups constructor(public id: string) {}
this.entries = [];
this.idToEntry = {};
}
getId() { getId() {
return this.id; return this.id;
} }
register(classHandle) { register(classHandle: Class<T>) {
// First, construct instance // First, construct instance
const instance = new classHandle(); const instance = new classHandle();
@ -34,19 +32,15 @@ export class SingletonFactory {
/** /**
* Checks if a given id is registered * Checks if a given id is registered
* @param {string} id
* @returns {boolean}
*/ */
hasId(id) { hasId(id: string): boolean {
return !!this.idToEntry[id]; return !!this.idToEntry[id];
} }
/** /**
* Finds an instance by a given id * Finds an instance by a given id
* @param {string} id
* @returns {object}
*/ */
findById(id) { findById(id: string): T {
const entry = this.idToEntry[id]; const entry = this.idToEntry[id];
if (!entry) { if (!entry) {
logger.error("Object with id", id, "is not registered!"); logger.error("Object with id", id, "is not registered!");
@ -58,10 +52,8 @@ export class SingletonFactory {
/** /**
* Finds an instance by its constructor (The class handle) * Finds an instance by its constructor (The class handle)
* @param {object} classHandle
* @returns {object}
*/ */
findByClass(classHandle) { findByClass(classHandle: Class<T>): T {
for (let i = 0; i < this.entries.length; ++i) { for (let i = 0; i < this.entries.length; ++i) {
if (this.entries[i] instanceof classHandle) { if (this.entries[i] instanceof classHandle) {
return this.entries[i]; return this.entries[i];
@ -73,25 +65,22 @@ export class SingletonFactory {
/** /**
* Returns all entries * Returns all entries
* @returns {Array<object>}
*/ */
getEntries() { getEntries(): T[] {
return this.entries; return this.entries;
} }
/** /**
* Returns all registered ids * Returns all registered ids
* @returns {Array<string>}
*/ */
getAllIds() { getAllIds(): string[] {
return Object.keys(this.idToEntry); return Object.keys(this.idToEntry);
} }
/** /**
* Returns amount of stored entries * Returns amount of stored entries
* @returns {number}
*/ */
getNumEntries() { getNumEntries(): number {
return this.entries.length; return this.entries.length;
} }
} }

View File

@ -1,7 +1,10 @@
export class TrackedState { export type TrackedStateCallback<T> = (value: T) => void;
constructor(callbackMethod = null, callbackScope = null) {
this.lastSeenValue = null;
export class TrackedState<T> {
public lastSeenValue: T = null;
public callback: TrackedStateCallback<T>;
constructor(callbackMethod: TrackedStateCallback<T> = null, callbackScope: unknown = null) {
if (callbackMethod) { if (callbackMethod) {
this.callback = callbackMethod; this.callback = callbackMethod;
if (callbackScope) { if (callbackScope) {
@ -10,7 +13,7 @@ export class TrackedState {
} }
} }
set(value, changeHandler = null, changeScope = null) { set(value: T, changeHandler: TrackedStateCallback<T> = null, changeScope: unknown = null) {
if (value !== this.lastSeenValue) { if (value !== this.lastSeenValue) {
// Copy value since the changeHandler call could actually modify our lastSeenValue // Copy value since the changeHandler call could actually modify our lastSeenValue
const valueCopy = value; const valueCopy = value;
@ -29,11 +32,11 @@ export class TrackedState {
} }
} }
setSilent(value) { setSilent(value: T) {
this.lastSeenValue = value; this.lastSeenValue = value;
} }
get() { get(): T {
return this.lastSeenValue; return this.lastSeenValue;
} }
} }

View File

@ -11,6 +11,7 @@ import { GameRoot } from "./root";
const logger = createLogger("camera"); const logger = createLogger("camera");
// @TODO: unused signal
export const USER_INTERACT_MOVE = "move"; export const USER_INTERACT_MOVE = "move";
export const USER_INTERACT_ZOOM = "zoom"; export const USER_INTERACT_ZOOM = "zoom";
export const USER_INTERACT_TOUCHEND = "touchend"; export const USER_INTERACT_TOUCHEND = "touchend";
@ -60,6 +61,7 @@ export class Camera extends BasicSerializableObject {
this.keyboardForce = new Vector(); this.keyboardForce = new Vector();
// Signal which gets emitted once the user changed something // Signal which gets emitted once the user changed something
/** @type {Signal<[string]>} */
this.userInteraction = new Signal(); this.userInteraction = new Signal();
/** @type {Vector} */ /** @type {Vector} */
@ -84,10 +86,10 @@ export class Camera extends BasicSerializableObject {
this.touchPostMoveVelocity = new Vector(0, 0); this.touchPostMoveVelocity = new Vector(0, 0);
// Handlers // Handlers
this.downPreHandler = /** @type {TypedSignal<[Vector, enumMouseButton]>} */ (new Signal()); this.downPreHandler = /** @type {Signal<[Vector, enumMouseButton]>} */ (new Signal());
this.movePreHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); this.movePreHandler = /** @type {Signal<[Vector]>} */ (new Signal());
// this.pinchPreHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); // this.pinchPreHandler = /** @type {Signal<[Vector]>} */ (new Signal());
this.upPostHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); this.upPostHandler = /** @type {Signal<[Vector]>} */ (new Signal());
this.internalInitEvents(); this.internalInitEvents();
this.clampZoomLevel(); this.clampZoomLevel();

View File

@ -7,7 +7,6 @@ import { GameRoot } from "./root";
import { globalConfig } from "../core/config"; import { globalConfig } from "../core/config";
import { enumDirectionToVector, enumDirectionToAngle } from "../core/vector"; import { enumDirectionToVector, enumDirectionToAngle } from "../core/vector";
import { BasicSerializableObject, types } from "../savegame/serialization"; import { BasicSerializableObject, types } from "../savegame/serialization";
import { EntityComponentStorage } from "./entity_components";
import { Loader } from "../core/loader"; import { Loader } from "../core/loader";
import { drawRotatedSprite } from "../core/draw_utils"; import { drawRotatedSprite } from "../core/draw_utils";
import { gComponentRegistry } from "../core/global_registries"; import { gComponentRegistry } from "../core/global_registries";
@ -27,8 +26,9 @@ export class Entity extends BasicSerializableObject {
/** /**
* The components of the entity * The components of the entity
* @type {import("./entity_components").EntityComponentStorage}
*/ */
this.components = new EntityComponentStorage(); this.components = {};
/** /**
* Whether this entity was registered on the @see EntityManager so far * Whether this entity was registered on the @see EntityManager so far
@ -99,7 +99,7 @@ export class Entity extends BasicSerializableObject {
}); });
for (const key in this.components) { for (const key in this.components) {
/** @type {Component} */ (this.components[key]).copyAdditionalStateTo(clone.components[key]); this.components[key].copyAdditionalStateTo(clone.components[key]);
} }
return clone; return clone;

51
src/js/game/entity_components.d.ts vendored Normal file
View File

@ -0,0 +1,51 @@
import type { BeltComponent } from "./components/belt";
import type { BeltUnderlaysComponent } from "./components/belt_underlays";
import type { HubComponent } from "./components/hub";
import type { ItemAcceptorComponent } from "./components/item_acceptor";
import type { ItemEjectorComponent } from "./components/item_ejector";
import type { ItemProcessorComponent } from "./components/item_processor";
import type { MinerComponent } from "./components/miner";
import type { StaticMapEntityComponent } from "./components/static_map_entity";
import type { StorageComponent } from "./components/storage";
import type { UndergroundBeltComponent } from "./components/underground_belt";
import type { WiredPinsComponent } from "./components/wired_pins";
import type { WireComponent } from "./components/wire";
import type { ConstantSignalComponent } from "./components/constant_signal";
import type { LogicGateComponent } from "./components/logic_gate";
import type { LeverComponent } from "./components/lever";
import type { WireTunnelComponent } from "./components/wire_tunnel";
import type { DisplayComponent } from "./components/display";
import type { BeltReaderComponent } from "./components/belt_reader";
import type { FilterComponent } from "./components/filter";
import type { ItemProducerComponent } from "./components/item_producer";
import type { GoalAcceptorComponent } from "./components/goal_acceptor";
import type { Component } from "./component";
/**
* Typedefs for all entity components.
*/
export interface EntityComponentStorage {
StaticMapEntity?: StaticMapEntityComponent;
Belt?: BeltComponent;
ItemEjector?: ItemEjectorComponent;
ItemAcceptor?: ItemAcceptorComponent;
Miner?: MinerComponent;
ItemProcessor?: ItemProcessorComponent;
UndergroundBelt?: UndergroundBeltComponent;
Hub?: HubComponent;
Storage?: StorageComponent;
WiredPins?: WiredPinsComponent;
BeltUnderlays?: BeltUnderlaysComponent;
Wire?: WireComponent;
ConstantSignal?: ConstantSignalComponent;
LogicGate?: LogicGateComponent;
Lever?: LeverComponent;
WireTunnel?: WireTunnelComponent;
Display?: DisplayComponent;
BeltReader?: BeltReaderComponent;
Filter?: FilterComponent;
ItemProducer?: ItemProducerComponent;
GoalAcceptor?: GoalAcceptorComponent;
[k: string]: Component;
}

View File

@ -1,98 +0,0 @@
/* typehints:start */
import { BeltComponent } from "./components/belt";
import { BeltUnderlaysComponent } from "./components/belt_underlays";
import { HubComponent } from "./components/hub";
import { ItemAcceptorComponent } from "./components/item_acceptor";
import { ItemEjectorComponent } from "./components/item_ejector";
import { ItemProcessorComponent } from "./components/item_processor";
import { MinerComponent } from "./components/miner";
import { StaticMapEntityComponent } from "./components/static_map_entity";
import { StorageComponent } from "./components/storage";
import { UndergroundBeltComponent } from "./components/underground_belt";
import { WiredPinsComponent } from "./components/wired_pins";
import { WireComponent } from "./components/wire";
import { ConstantSignalComponent } from "./components/constant_signal";
import { LogicGateComponent } from "./components/logic_gate";
import { LeverComponent } from "./components/lever";
import { WireTunnelComponent } from "./components/wire_tunnel";
import { DisplayComponent } from "./components/display";
import { BeltReaderComponent } from "./components/belt_reader";
import { FilterComponent } from "./components/filter";
import { ItemProducerComponent } from "./components/item_producer";
import { GoalAcceptorComponent } from "./components/goal_acceptor";
/* typehints:end */
/**
* Typedefs for all entity components. These are not actually present on the entity,
* thus they are undefined by default
*/
export class EntityComponentStorage {
constructor() {
/* typehints:start */
/** @type {StaticMapEntityComponent} */
this.StaticMapEntity;
/** @type {BeltComponent} */
this.Belt;
/** @type {ItemEjectorComponent} */
this.ItemEjector;
/** @type {ItemAcceptorComponent} */
this.ItemAcceptor;
/** @type {MinerComponent} */
this.Miner;
/** @type {ItemProcessorComponent} */
this.ItemProcessor;
/** @type {UndergroundBeltComponent} */
this.UndergroundBelt;
/** @type {HubComponent} */
this.Hub;
/** @type {StorageComponent} */
this.Storage;
/** @type {WiredPinsComponent} */
this.WiredPins;
/** @type {BeltUnderlaysComponent} */
this.BeltUnderlays;
/** @type {WireComponent} */
this.Wire;
/** @type {ConstantSignalComponent} */
this.ConstantSignal;
/** @type {LogicGateComponent} */
this.LogicGate;
/** @type {LeverComponent} */
this.Lever;
/** @type {WireTunnelComponent} */
this.WireTunnel;
/** @type {DisplayComponent} */
this.Display;
/** @type {BeltReaderComponent} */
this.BeltReader;
/** @type {FilterComponent} */
this.Filter;
/** @type {ItemProducerComponent} */
this.ItemProducer;
/** @type {GoalAcceptorComponent} */
this.GoalAcceptor;
/* typehints:end */
}
}

View File

@ -101,7 +101,7 @@ export class BaseHUDPart {
/** /**
* Helper method to construct a new click detector * Helper method to construct a new click detector
* @param {Element} element The element to listen on * @param {Element} element The element to listen on
* @param {function} handler The handler to call on this object * @param {import("../../core/signal").SignalReceiver<[]>} handler The handler to call on this object
* @param {import("../../core/click_detector").ClickDetectorConstructorArgs=} args Click detector arguments * @param {import("../../core/click_detector").ClickDetectorConstructorArgs=} args Click detector arguments
* *
*/ */

View File

@ -34,17 +34,18 @@ export class GameHUD {
*/ */
initialize() { initialize() {
this.signals = { this.signals = {
buildingSelectedForPlacement: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()), buildingSelectedForPlacement: /** @type {Signal<[MetaBuilding|null]>} */ (new Signal()),
selectedPlacementBuildingChanged: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()), selectedPlacementBuildingChanged: /** @type {Signal<[MetaBuilding|null]>} */ (new Signal()),
shapePinRequested: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), shapePinRequested: /** @type {Signal<[ShapeDefinition]>} */ (new Signal()),
shapeUnpinRequested: /** @type {TypedSignal<[string]>} */ (new Signal()), shapeUnpinRequested: /** @type {Signal<[string]>} */ (new Signal()),
notification: /** @type {TypedSignal<[string, enumNotificationType]>} */ (new Signal()), notification: /** @type {Signal<[string, enumNotificationType]>} */ (new Signal()),
buildingsSelectedForCopy: /** @type {TypedSignal<[Array<number>]>} */ (new Signal()), buildingsSelectedForCopy: /** @type {Signal<[Array<number>]>} */ (new Signal()),
pasteBlueprintRequested: /** @type {TypedSignal<[]>} */ (new Signal()), pasteBlueprintRequested: /** @type {Signal<[]>} */ (new Signal()),
viewShapeDetailsRequested: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), viewShapeDetailsRequested: /** @type {Signal<[ShapeDefinition]>} */ (new Signal()),
unlockNotificationFinished: /** @type {TypedSignal<[]>} */ (new Signal()), unlockNotificationFinished: /** @type {Signal<[]>} */ (new Signal()),
}; };
/** @type {import("./hud_parts").HudParts} */
this.parts = { this.parts = {
buildingsToolbar: new HUDBuildingsToolbar(this.root), buildingsToolbar: new HUDBuildingsToolbar(this.root),

107
src/js/game/hud/hud_parts.d.ts vendored Normal file
View File

@ -0,0 +1,107 @@
import type { HUDBetaOverlay } from "./parts/beta_overlay.js";
import type { HUDBlueprintPlacer } from "./parts/blueprint_placer.js";
import type { HUDBuildingsToolbar } from "./parts/buildings_toolbar.js";
import type { HUDBuildingPlacer } from "./parts/building_placer.js";
import type { HUDColorBlindHelper } from "./parts/color_blind_helper.js";
import type { HUDConstantSignalEdit } from "./parts/constant_signal_edit.js";
import type { HUDChangesDebugger } from "./parts/debug_changes.js";
import type { HUDDebugInfo } from "./parts/debug_info.js";
import type { HUDEntityDebugger } from "./parts/entity_debugger.js";
import type { HUDGameMenu } from "./parts/game_menu.js";
import type { HUDInteractiveTutorial } from "./parts/interactive_tutorial.js";
import type { HUDKeybindingOverlay } from "./parts/keybinding_overlay.js";
import type { HUDLayerPreview } from "./parts/layer_preview.js";
import type { HUDLeverToggle } from "./parts/lever_toggle.js";
import type { HUDMassSelector } from "./parts/mass_selector.js";
import type { HUDMinerHighlight } from "./parts/miner_highlight.js";
import type { HUDModalDialogs } from "./parts/modal_dialogs.js";
import type { HUDPuzzleNextPuzzle } from "./parts/next_puzzle.js";
import type { HUDNotifications } from "./parts/notifications.js";
import type { HUDPinnedShapes } from "./parts/pinned_shapes.js";
import type { HUDPuzzleBackToMenu } from "./parts/puzzle_back_to_menu.js";
import type { HUDPuzzleCompleteNotification } from "./parts/puzzle_complete_notification.js";
import type { HUDPuzzleDLCLogo } from "./parts/puzzle_dlc_logo.js";
import type { HUDPuzzleEditorControls } from "./parts/puzzle_editor_controls.js";
import type { HUDPuzzleEditorReview } from "./parts/puzzle_editor_review.js";
import type { HUDPuzzleEditorSettings } from "./parts/puzzle_editor_settings.js";
import type { HUDPuzzlePlayMetadata } from "./parts/puzzle_play_metadata.js";
import type { HUDPuzzlePlaySettings } from "./parts/puzzle_play_settings.js";
import type { HUDScreenshotExporter } from "./parts/screenshot_exporter.js";
import type { HUDSettingsMenu } from "./parts/settings_menu.js";
import type { HUDShapeTooltip } from "./parts/shape_tooltip.js";
import type { HUDShapeViewer } from "./parts/shape_viewer.js";
import type { HUDShop } from "./parts/shop.js";
import type { HUDStandaloneAdvantages } from "./parts/standalone_advantages.js";
import type { HUDStatistics } from "./parts/statistics.js";
import type { HUDPartTutorialHints } from "./parts/tutorial_hints.js";
import type { HUDTutorialVideoOffer } from "./parts/tutorial_video_offer.js";
import type { HUDUnlockNotification } from "./parts/unlock_notification.js";
import type { HUDVignetteOverlay } from "./parts/vignette_overlay.js";
import type { HUDWatermark } from "./parts/watermark.js";
import type { HUDWaypoints } from "./parts/waypoints.js";
import type { HUDWiresOverlay } from "./parts/wires_overlay.js";
import type { HUDWiresToolbar } from "./parts/wires_toolbar.js";
import type { HUDWireInfo } from "./parts/wire_info.js";
export interface HudParts {
buildingsToolbar: HUDBuildingsToolbar;
blueprintPlacer: HUDBlueprintPlacer;
buildingPlacer: HUDBuildingPlacer;
shapeTooltip: HUDShapeTooltip;
// Must always exist
settingsMenu: HUDSettingsMenu;
debugInfo: HUDDebugInfo;
dialogs: HUDModalDialogs;
// Dev
entityDebugger?: HUDEntityDebugger;
changesDebugger?: HUDChangesDebugger;
vignetteOverlay?: HUDVignetteOverlay;
colorBlindHelper?: HUDColorBlindHelper;
betaOverlay?: HUDBetaOverlay;
// Additional Hud Parts
// Shared
massSelector?: HUDMassSelector;
constantSignalEdit?: HUDConstantSignalEdit;
// Regular
wiresToolbar?: HUDWiresToolbar;
unlockNotification?: HUDUnlockNotification;
shop?: HUDShop;
statistics?: HUDStatistics;
waypoints?: HUDWaypoints;
wireInfo?: HUDWireInfo;
leverToggle?: HUDLeverToggle;
pinnedShapes?: HUDPinnedShapes;
notifications?: HUDNotifications;
screenshotExporter?: HUDScreenshotExporter;
wiresOverlay?: HUDWiresOverlay;
shapeViewer?: HUDShapeViewer;
layerPreview?: HUDLayerPreview;
minerHighlight?: HUDMinerHighlight;
tutorialVideoOffer?: HUDTutorialVideoOffer;
gameMenu?: HUDGameMenu;
keybindingOverlay?: HUDKeybindingOverlay;
watermark?: HUDWatermark;
standaloneAdvantages?: HUDStandaloneAdvantages;
tutorialHints?: HUDPartTutorialHints;
interactiveTutorial?: HUDInteractiveTutorial;
// Puzzle mode
puzzleBackToMenu?: HUDPuzzleBackToMenu;
puzzleDlcLogo?: HUDPuzzleDLCLogo;
puzzleEditorControls?: HUDPuzzleEditorControls;
puzzleEditorReview?: HUDPuzzleEditorReview;
puzzleEditorSettings?: HUDPuzzleEditorSettings;
puzzlePlayMetadata?: HUDPuzzlePlayMetadata;
puzzlePlaySettings?: HUDPuzzlePlaySettings;
puzzleCompleteNotification?: HUDPuzzleCompleteNotification;
puzzleNext?: HUDPuzzleNextPuzzle;
}

View File

@ -68,7 +68,7 @@ export class HUDConstantSignalEdit extends BaseHUDPart {
label: fillInLinkIntoTranslation(T.dialogs.editSignal.descShortKey, THIRDPARTY_URLS.shapeViewer), label: fillInLinkIntoTranslation(T.dialogs.editSignal.descShortKey, THIRDPARTY_URLS.shapeViewer),
placeholder: "", placeholder: "",
defaultValue: signal ? signal.getAsCopyableKey() : "", defaultValue: signal ? signal.getAsCopyableKey() : "",
validator: val => this.parseSignalCode(entity, val), validator: val => this.parseSignalCode(entity, val) !== null,
}); });
const items = [...Object.values(COLOR_ITEM_SINGLETONS)]; const items = [...Object.values(COLOR_ITEM_SINGLETONS)];

View File

@ -58,7 +58,7 @@ export class HUDModalDialogs extends BaseHUDPart {
/** /**
* @param {string} title * @param {string} title
* @param {string} text * @param {string} text
* @param {Array<string>} buttons * @param {Array<`${string}:${string}`>} buttons
*/ */
showInfo(title, text, buttons = ["ok:good"]) { showInfo(title, text, buttons = ["ok:good"]) {
const dialog = new Dialog({ const dialog = new Dialog({
@ -80,7 +80,7 @@ export class HUDModalDialogs extends BaseHUDPart {
/** /**
* @param {string} title * @param {string} title
* @param {string} text * @param {string} text
* @param {Array<string>} buttons * @param {Array<import("../../../core/modal_dialog_elements").DialogButtonStr<string>>} buttons
*/ */
showWarning(title, text, buttons = ["ok:good"]) { showWarning(title, text, buttons = ["ok:good"]) {
const dialog = new Dialog({ const dialog = new Dialog({

View File

@ -146,7 +146,7 @@ export class HUDPuzzleEditorReview extends BaseHUDPart {
}); });
itemInput.valueChosen.add(value => { itemInput.valueChosen.add(value => {
shapeKeyInput.setValue(value.definition.getHash()); shapeKeyInput.setValue(/** @type {ShapeItem} */ (value).definition.getHash());
}); });
this.root.hud.parts.dialogs.internalShowDialog(dialog); this.root.hud.parts.dialogs.internalShowDialog(dialog);

View File

@ -150,9 +150,7 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
} }
for (const key in building.components) { for (const key in building.components) {
/** @type {import("../../../core/global_registries").Component} */ ( building.components[key].copyAdditionalStateTo(result.components[key]);
building.components[key]
).copyAdditionalStateTo(result.components[key]);
} }
} }
}); });

View File

@ -177,7 +177,7 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
} }
); );
return new Promise(resolve => { return new /** @type {typeof Promise<void>} */ (Promise)(resolve => {
optionSelected.add(option => { optionSelected.add(option => {
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog(); const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog();

View File

@ -145,53 +145,53 @@ export class GameRoot {
this.signals = { this.signals = {
// Entities // Entities
entityManuallyPlaced: /** @type {TypedSignal<[Entity]>} */ (new Signal()), entityManuallyPlaced: /** @type {Signal<[Entity]>} */ (new Signal()),
entityAdded: /** @type {TypedSignal<[Entity]>} */ (new Signal()), entityAdded: /** @type {Signal<[Entity]>} */ (new Signal()),
entityChanged: /** @type {TypedSignal<[Entity]>} */ (new Signal()), entityChanged: /** @type {Signal<[Entity]>} */ (new Signal()),
entityGotNewComponent: /** @type {TypedSignal<[Entity]>} */ (new Signal()), entityGotNewComponent: /** @type {Signal<[Entity]>} */ (new Signal()),
entityComponentRemoved: /** @type {TypedSignal<[Entity]>} */ (new Signal()), entityComponentRemoved: /** @type {Signal<[Entity]>} */ (new Signal()),
entityQueuedForDestroy: /** @type {TypedSignal<[Entity]>} */ (new Signal()), entityQueuedForDestroy: /** @type {Signal<[Entity]>} */ (new Signal()),
entityDestroyed: /** @type {TypedSignal<[Entity]>} */ (new Signal()), entityDestroyed: /** @type {Signal<[Entity]>} */ (new Signal()),
// Global // Global
resized: /** @type {TypedSignal<[number, number]>} */ (new Signal()), resized: /** @type {Signal<[number, number]>} */ (new Signal()),
readyToRender: /** @type {TypedSignal<[]>} */ (new Signal()), readyToRender: /** @type {Signal<[]>} */ (new Signal()),
aboutToDestruct: /** @type {TypedSignal<[]>} */ new Signal(), aboutToDestruct: /** @type {Signal<[]>} */ new Signal(),
// Game Hooks // Game Hooks
gameSaved: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got saved gameSaved: /** @type {Signal<[]>} */ (new Signal()), // Game got saved
gameRestored: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got restored gameRestored: /** @type {Signal<[]>} */ (new Signal()), // Game got restored
gameFrameStarted: /** @type {TypedSignal<[]>} */ (new Signal()), // New frame gameFrameStarted: /** @type {Signal<[]>} */ (new Signal()), // New frame
storyGoalCompleted: /** @type {TypedSignal<[number, string]>} */ (new Signal()), storyGoalCompleted: /** @type {Signal<[number, string]>} */ (new Signal()),
upgradePurchased: /** @type {TypedSignal<[string]>} */ (new Signal()), upgradePurchased: /** @type {Signal<[string]>} */ (new Signal()),
// Called right after game is initialized // Called right after game is initialized
postLoadHook: /** @type {TypedSignal<[]>} */ (new Signal()), postLoadHook: /** @type {Signal<[]>} */ (new Signal()),
shapeDelivered: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), shapeDelivered: /** @type {Signal<[ShapeDefinition]>} */ (new Signal()),
itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()), itemProduced: /** @type {Signal<[BaseItem]>} */ (new Signal()),
bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()), bulkOperationFinished: /** @type {Signal<[]>} */ (new Signal()),
immutableOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()), immutableOperationFinished: /** @type {Signal<[]>} */ (new Signal()),
editModeChanged: /** @type {TypedSignal<[Layer]>} */ (new Signal()), editModeChanged: /** @type {Signal<[Layer]>} */ (new Signal()),
// Called to check if an entity can be placed, second parameter is an additional offset. // Called to check if an entity can be placed, second parameter is an additional offset.
// Use to introduce additional placement checks // Use to introduce additional placement checks
prePlacementCheck: /** @type {TypedSignal<[Entity, Vector]>} */ (new Signal()), prePlacementCheck: /** @type {Signal<[Entity, Vector]>} */ (new Signal()),
// Called before actually placing an entity, use to perform additional logic // Called before actually placing an entity, use to perform additional logic
// for freeing space before actually placing. // for freeing space before actually placing.
freeEntityAreaBeforeBuild: /** @type {TypedSignal<[Entity]>} */ (new Signal()), freeEntityAreaBeforeBuild: /** @type {Signal<[Entity]>} */ (new Signal()),
// Called with an achievement key and necessary args to validate it can be unlocked. // Called with an achievement key and necessary args to validate it can be unlocked.
achievementCheck: /** @type {TypedSignal<[string, any]>} */ (new Signal()), achievementCheck: /** @type {Signal<[string, any]>} */ (new Signal()),
bulkAchievementCheck: /** @type {TypedSignal<(string|any)[]>} */ (new Signal()), bulkAchievementCheck: /** @type {Signal<(string|any)[]>} */ (new Signal()),
// Puzzle mode // Puzzle mode
puzzleComplete: /** @type {TypedSignal<[]>} */ (new Signal()), puzzleComplete: /** @type {Signal<[]>} */ (new Signal()),
}; };
// RNG's // RNG's

View File

@ -48,17 +48,17 @@ const MAX_QUEUED_CHARGES = 2;
*/ */
/** /**
* @type {Object<string, (ProcessorImplementationPayload) => void>} * @type {Object<string, (arg: ProcessorImplementationPayload) => void>}
*/ */
export const MOD_ITEM_PROCESSOR_HANDLERS = {}; export const MOD_ITEM_PROCESSOR_HANDLERS = {};
/** /**
* @type {Object<string, (ProccessingRequirementsImplementationPayload) => boolean>} * @type {Object<string, (arg: ProccessingRequirementsImplementationPayload) => boolean>}
*/ */
export const MODS_PROCESSING_REQUIREMENTS = {}; export const MODS_PROCESSING_REQUIREMENTS = {};
/** /**
* @type {Object<string, ({entity: Entity}) => boolean>} * @type {Object<string, (arg: {entity: Entity}) => boolean>}
*/ */
export const MODS_CAN_PROCESS = {}; export const MODS_CAN_PROCESS = {};
@ -67,7 +67,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
super(root, [ItemProcessorComponent]); super(root, [ItemProcessorComponent]);
/** /**
* @type {Object<enumItemProcessorTypes, function(ProcessorImplementationPayload) : string>} * @type {Object<enumItemProcessorTypes, (arg: ProcessorImplementationPayload) => void>}
*/ */
this.handlers = { this.handlers = {
[enumItemProcessorTypes.balancer]: this.process_BALANCER, [enumItemProcessorTypes.balancer]: this.process_BALANCER,

View File

@ -21,8 +21,7 @@ export class BaseGameSpeed extends BasicSerializableObject {
} }
getId() { getId() {
// @ts-ignore return /** @type {typeof BaseGameSpeed} */ (this.constructor).getId();
return this.constructor.getId();
} }
static getSchema() { static getSchema() {

View File

@ -180,7 +180,10 @@ export class GameTime extends BasicSerializableObject {
setSpeed(speed) { setSpeed(speed) {
assert(speed instanceof BaseGameSpeed, "Not a valid game speed"); assert(speed instanceof BaseGameSpeed, "Not a valid game speed");
if (this.speed.getId() === speed.getId()) { if (this.speed.getId() === speed.getId()) {
logger.warn("Same speed set than current one:", speed.constructor.getId()); logger.warn(
"Same speed set than current one:",
/** @type {typeof BaseGameSpeed} */ (speed.constructor).getId()
);
} }
this.speed = speed; this.speed = speed;
} }

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

@ -1,8 +1,11 @@
// Globals defined by webpack // Globals defined by webpack
declare const G_IS_DEV: boolean; declare const G_IS_DEV: boolean;
declare function assert(condition: boolean | object | string, ...errorMessage: string[]): void; declare function assert(condition: boolean | object | string, ...errorMessage: string[]): asserts condition;
declare function assertAlways(condition: boolean | object | string, ...errorMessage: string[]): void; declare function assertAlways(
condition: boolean | object | string,
...errorMessage: string[]
): asserts condition;
declare const abstract: void; declare const abstract: void;
@ -142,34 +145,6 @@ declare interface String {
padEnd(size: number, fill: string): string; padEnd(size: number, fill: string): string;
} }
declare interface FactoryTemplate<T> {
entries: Array<Class<T>>;
entryIds: Array<string>;
idToEntry: any;
getId(): string;
getAllIds(): Array<string>;
register(entry: Class<T>): void;
hasId(id: string): boolean;
findById(id: string): Class<T>;
getEntries(): Array<Class<T>>;
getNumEntries(): number;
}
declare interface SingletonFactoryTemplate<T> {
entries: Array<T>;
idToEntry: any;
getId(): string;
getAllIds(): Array<string>;
register(classHandle: Class<T>): void;
hasId(id: string): boolean;
findById(id: string): T;
findByClass(classHandle: Class<T>): T;
getEntries(): Array<T>;
getNumEntries(): number;
}
declare interface SignalTemplate0 { declare interface SignalTemplate0 {
add(receiver: () => string | void, scope: null | any); add(receiver: () => string | void, scope: null | any);
dispatch(): string | void; dispatch(): string | void;
@ -186,18 +161,6 @@ declare class TypedTrackedState<T> {
get(): T; get(): T;
} }
declare const STOP_PROPAGATION = "stop_propagation";
declare interface TypedSignal<T extends Array<any>> {
add(receiver: (...args: T) => /* STOP_PROPAGATION */ string | void, scope?: object);
addToTop(receiver: (...args: T) => /* STOP_PROPAGATION */ string | void, scope?: object);
remove(receiver: (...args: T) => /* STOP_PROPAGATION */ string | void);
dispatch(...args: T): /* STOP_PROPAGATION */ string | void;
removeAll();
}
declare type Layer = "regular" | "wires"; declare type Layer = "regular" | "wires";
declare type ItemType = "shape" | "color" | "boolean"; declare type ItemType = "shape" | "color" | "boolean";

View File

@ -13,27 +13,27 @@ export const MOD_SIGNALS = {
// Called when the application has booted and instances like the app settings etc are available // Called when the application has booted and instances like the app settings etc are available
appBooted: new Signal(), appBooted: new Signal(),
modifyLevelDefinitions: /** @type {TypedSignal<[Array[Object]]>} */ (new Signal()), modifyLevelDefinitions: /** @type {Signal<[Array[Object]]>} */ (new Signal()),
modifyUpgrades: /** @type {TypedSignal<[Object]>} */ (new Signal()), modifyUpgrades: /** @type {Signal<[Object]>} */ (new Signal()),
hudElementInitialized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()), hudElementInitialized: /** @type {Signal<[BaseHUDPart]>} */ (new Signal()),
hudElementFinalized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()), hudElementFinalized: /** @type {Signal<[BaseHUDPart]>} */ (new Signal()),
hudInitializer: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()), hudInitializer: /** @type {Signal<[GameRoot]>} */ (new Signal()),
gameInitialized: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()), gameInitialized: /** @type {Signal<[GameRoot]>} */ (new Signal()),
gameLoadingStageEntered: /** @type {TypedSignal<[InGameState, string]>} */ (new Signal()), gameLoadingStageEntered: /** @type {Signal<[InGameState, string]>} */ (new Signal()),
gameStarted: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()), gameStarted: /** @type {Signal<[GameRoot]>} */ (new Signal()),
stateEntered: /** @type {TypedSignal<[GameState]>} */ (new Signal()), stateEntered: /** @type {Signal<[GameState]>} */ (new Signal()),
gameSerialized: gameSerialized:
/** @type {TypedSignal<[GameRoot, import("../savegame/savegame_typedefs").SerializedGame]>} */ ( /** @type {Signal<[GameRoot, import("../savegame/savegame_typedefs").SerializedGame]>} */ (
new Signal() new Signal()
), ),
gameDeserialized: gameDeserialized:
/** @type {TypedSignal<[GameRoot, import("../savegame/savegame_typedefs").SerializedGame]>} */ ( /** @type {Signal<[GameRoot, import("../savegame/savegame_typedefs").SerializedGame]>} */ (
new Signal() new Signal()
), ),
}; };

View File

@ -107,9 +107,13 @@ export class ModLoader {
exposeExports() { exposeExports() {
if (G_IS_DEV || G_IS_STANDALONE) { if (G_IS_DEV || G_IS_STANDALONE) {
let exports = {}; let exports = {};
const modules = import.meta.webpackContext("../", { recursive: true, regExp: /\.js$/ }); const modules = import.meta.webpackContext("../", {
recursive: true,
regExp: /\.[jt]s$/,
exclude: /\.d\.ts$/,
});
Array.from(modules.keys()).forEach(key => { Array.from(modules.keys()).forEach(key => {
// @ts-ignore /** @type {object} */
const module = modules(key); const module = modules(key);
for (const member in module) { for (const member in module) {
if (member === "default" || member === "__$S__") { if (member === "default" || member === "__$S__") {

View File

@ -110,11 +110,6 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface {
initialize() { initialize() {
this.syncKey = null; this.syncKey = null;
window.setAbt = abt => {
this.app.storage.writeFileAsync("shapez_" + CURRENT_ABT + ".bin", String(abt));
window.location.reload();
};
// Retrieve sync key from player // Retrieve sync key from player
return this.fetchABVariant().then(() => { return this.fetchABVariant().then(() => {
setInterval(() => this.sendTimePoints(), 60 * 1000); setInterval(() => this.sendTimePoints(), 60 * 1000);

View File

@ -140,7 +140,7 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
performRestart() { performRestart() {
logger.log("Performing restart"); logger.log("Performing restart");
window.location.reload(true); window.location.reload();
} }
/** /**

View File

@ -54,7 +54,7 @@ export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser {
performRestart() { performRestart() {
logger.log(this, "Performing restart"); logger.log(this, "Performing restart");
window.location.reload(true); window.location.reload();
} }
initializeAdProvider() { initializeAdProvider() {

View File

@ -1,4 +1,4 @@
import { gMetaBuildingRegistry } from "../../core/global_registries.js"; import { gMetaBuildingRegistry } from "../../core/global_registries";
import { createLogger } from "../../core/logging.js"; import { createLogger } from "../../core/logging.js";
import { enumBalancerVariants, MetaBalancerBuilding } from "../../game/buildings/balancer.js"; import { enumBalancerVariants, MetaBalancerBuilding } from "../../game/buildings/balancer.js";
import { MetaBeltBuilding } from "../../game/buildings/belt.js"; import { MetaBeltBuilding } from "../../game/buildings/belt.js";

View File

@ -25,6 +25,15 @@ import {
TypePositiveIntegerOrString, TypePositiveIntegerOrString,
} from "./serialization_data_types"; } from "./serialization_data_types";
/**
* @typedef {import("../core/factory").Factory<T>} FactoryTemplate<T>
* @template T
*/
/**
* @typedef {import("../core/singleton_factory").SingletonFactory<T>} SingletonFactoryTemplate<T>
* @template {{ getId(): string }} T
*/
const logger = createLogger("serialization"); const logger = createLogger("serialization");
// Schema declarations // Schema declarations
@ -106,7 +115,7 @@ export const types = {
}, },
/** /**
* @param {SingletonFactoryTemplate<*>} innerType * @param {SingletonFactoryTemplate<*>} registry
*/ */
classRef(registry) { classRef(registry) {
return new TypeMetaClass(registry); return new TypeMetaClass(registry);

View File

@ -7,6 +7,15 @@ import { Vector } from "../core/vector";
import { round4Digits } from "../core/utils"; import { round4Digits } from "../core/utils";
export const globalJsonSchemaDefs = {}; export const globalJsonSchemaDefs = {};
/**
* @typedef {import("../core/factory").Factory<T>} FactoryTemplate<T>
* @template T
*/
/**
* @typedef {import("../core/singleton_factory").SingletonFactory<T>} SingletonFactoryTemplate<T>
* @template {{ getId(): string }} T
*/
/** /**
* *
* @param {import("./serialization").Schema} schema * @param {import("./serialization").Schema} schema
@ -48,6 +57,7 @@ export class BaseDataType {
/** /**
* Serializes a given raw value * Serializes a given raw value
* @param {any} value * @param {any} value
* @returns {unknown}
* @abstract * @abstract
*/ */
serialize(value) { serialize(value) {
@ -1034,7 +1044,8 @@ export class TypeKeyValueMap extends BaseDataType {
const serialized = this.valueType.serialize(value[key]); const serialized = this.valueType.serialize(value[key]);
if (!this.includeEmptyValues && typeof serialized === "object") { if (!this.includeEmptyValues && typeof serialized === "object") {
if ( if (
serialized.$ && "$" in serialized &&
"data" in serialized &&
typeof serialized.data === "object" && typeof serialized.data === "object" &&
Object.keys(serialized.data).length === 0 Object.keys(serialized.data).length === 0
) { ) {

View File

@ -103,7 +103,11 @@ export class KeybindingsState extends TextualGameState {
event.preventDefault(); event.preventDefault();
} }
if (event.target && event.target.tagName === "BUTTON" && keyCode === 1) { if (
event.target &&
/** @type {HTMLElement} */ (event.target).tagName === "BUTTON" &&
keyCode === 1
) {
return; return;
} }

View File

@ -54,7 +54,9 @@ export class LoginState extends GameState {
T.dialogs.offlineMode.desc, T.dialogs.offlineMode.desc,
["retry", "playOffline:bad"] ["retry", "playOffline:bad"]
); );
signals.retry.add(() => setTimeout(() => this.tryLogin(), 2000), this); signals.retry.add(() => {
setTimeout(() => this.tryLogin(), 2000);
}, this);
signals.playOffline.add(this.finishLoading, this); signals.playOffline.add(this.finishLoading, this);
} else { } else {
this.finishLoading(); this.finishLoading();

View File

@ -798,7 +798,7 @@ export class MainMenuState extends GameState {
"continue:bad", "continue:bad",
]); ]);
return new Promise(resolve => { return new /** @type {typeof Promise<void>} */ (Promise)(resolve => {
signals.continue.add(resolve); signals.continue.add(resolve);
}); });
} }

View File

@ -265,7 +265,7 @@ export class PreloadState extends GameState {
`; `;
} }
return new Promise(resolve => { return new /** @type {typeof Promise<void>} */ (Promise)(resolve => {
this.dialogs.showInfo(T.dialogs.updateSummary.title, dialogHtml).ok.add(resolve); this.dialogs.showInfo(T.dialogs.updateSummary.title, dialogHtml).ok.add(resolve);
}); });
}); });

View File

@ -30,6 +30,7 @@
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"types": ["webpack/module"] /* Type declaration files to be included in compilation. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"paths": { "paths": {
"root/*": ["./*"] "root/*": ["./*"]