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 {
constructor() {
/** @type {Signal<[number]>} */
this.frameEmitted = new Signal();
/** @type {Signal<[number]>} */
this.bgFrameEmitted = new Signal();
this.lastTime = performance.now();

View File

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

View File

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

View File

@ -3,21 +3,19 @@ import { createLogger } from "./logging";
const logger = createLogger("factory");
// simple factory pattern
export class Factory {
constructor(id) {
this.id = id;
export class Factory<T> {
// Store array as well as dictionary, to speed up lookups
public entries: Class<T>[] = [];
public entryIds: string[] = [];
public idToEntry: Record<string, Class<T>> = {};
// Store array as well as dictionary, to speed up lookups
this.entries = [];
this.entryIds = [];
this.idToEntry = {};
}
constructor(public id: string) {}
getId() {
return this.id;
}
register(entry) {
register(entry: Class<T> & { getId(): string }) {
// Extract id
const id = entry.getId();
assert(id, "Factory: Invalid id for class: " + entry);
@ -33,19 +31,15 @@ export class Factory {
/**
* Checks if a given id is registered
* @param {string} id
* @returns {boolean}
*/
hasId(id) {
hasId(id: string): boolean {
return !!this.idToEntry[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];
if (!entry) {
logger.error("Object with id", id, "is not registered on factory", this.id, "!");
@ -57,25 +51,22 @@ export class Factory {
/**
* Returns all entries
* @returns {Array<object>}
*/
getEntries() {
getEntries(): Class<T>[] {
return this.entries;
}
/**
* Returns all registered ids
* @returns {Array<string>}
*/
getAllIds() {
getAllIds(): string[] {
return this.entryIds;
}
/**
* Returns amount of stored entries
* @returns {number}
*/
getNumEntries() {
getNumEntries(): number {
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 { Application } from "../application";
import { InputReceiver } from "./input_receiver";
/* typehints:end */
import type { Application } from "../application";
import type { InputReceiver, ReceiverId } from "./input_receiver";
import { Signal, STOP_PROPAGATION } from "./signal";
import { createLogger } from "./logging";
@ -10,47 +8,33 @@ import { arrayDeleteValue, fastArrayDeleteValue } from "./utils";
const logger = createLogger("input_distributor");
export class InputDistributor {
public recieverStack: InputReceiver[] = [];
public filters: ((arg: string) => boolean)[] = [];
/**
*
* @param {Application} app
* All keys which are currently down
*/
constructor(app) {
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();
public keysDown = new Set<number>();
constructor(public app: Application) {
this.bindToEvents();
}
/**
* 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);
}
/**
* Removes an attached filter
* @param {function(any) : boolean} filter
*/
dismountFilter(filter) {
dismountFilter(filter: (arg: string) => boolean) {
fastArrayDeleteValue(this.filters, filter);
}
/**
* @param {InputReceiver} reciever
*/
pushReciever(reciever) {
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");
@ -66,10 +50,7 @@ export class InputDistributor {
}
}
/**
* @param {InputReceiver} reciever
*/
popReciever(reciever) {
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");
@ -86,45 +67,29 @@ export class InputDistributor {
arrayDeleteValue(this.recieverStack, reciever);
}
/**
* @param {InputReceiver} reciever
*/
isRecieverAttached(reciever) {
isRecieverAttached(reciever: InputReceiver) {
return this.recieverStack.indexOf(reciever) >= 0;
}
/**
* @param {InputReceiver} reciever
*/
isRecieverOnTop(reciever) {
isRecieverOnTop(reciever: InputReceiver) {
return (
this.isRecieverAttached(reciever) &&
this.recieverStack[this.recieverStack.length - 1] === reciever
);
}
/**
* @param {InputReceiver} reciever
*/
makeSureAttachedAndOnTop(reciever) {
makeSureAttachedAndOnTop(reciever: InputReceiver) {
this.makeSureDetached(reciever);
this.pushReciever(reciever);
}
/**
* @param {InputReceiver} reciever
*/
makeSureDetached(reciever) {
makeSureDetached(reciever: InputReceiver) {
if (this.isRecieverAttached(reciever)) {
arrayDeleteValue(this.recieverStack, reciever);
}
}
/**
*
* @param {InputReceiver} reciever
*/
destroyReceiver(reciever) {
destroyReceiver(reciever: InputReceiver) {
this.makeSureDetached(reciever);
reciever.cleanup();
}
@ -153,7 +118,10 @@ export class InputDistributor {
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
for (let i = 0; i < this.filters.length; ++i) {
if (!this.filters[i](eventId)) {
@ -168,13 +136,11 @@ export class InputDistributor {
}
const signal = reciever[eventId];
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);
}
/**
* @param {Event} event
*/
handleBackButton(event) {
handleBackButton(event: Event) {
event.preventDefault();
event.stopPropagation();
this.forwardToReceiver("backButton");
@ -184,21 +150,15 @@ export class InputDistributor {
* Handles when the page got blurred
*/
handleBlur() {
this.forwardToReceiver("pageBlur", {});
this.forwardToReceiver("pageBlur");
this.keysDown.clear();
}
/**
*
*/
handlePaste(ev) {
handlePaste(ev: ClipboardEvent) {
this.forwardToReceiver("paste", ev);
}
/**
* @param {KeyboardEvent | MouseEvent} event
*/
handleKeyMouseDown(event) {
handleKeyMouseDown(event: KeyboardEvent | MouseEvent) {
const keyCode = event instanceof MouseEvent ? event.button + 1 : event.keyCode;
if (
keyCode === 4 || // MB4
@ -236,10 +196,7 @@ export class InputDistributor {
}
}
/**
* @param {KeyboardEvent | MouseEvent} event
*/
handleKeyMouseUp(event) {
handleKeyMouseUp(event: KeyboardEvent | MouseEvent) {
const keyCode = event instanceof MouseEvent ? event.button + 1 : event.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 { Application } from "../application";
/* typehints:end */
import { Signal, STOP_PROPAGATION } from "./signal";
import { arrayDeleteValue, waitNextFrame } from "./utils";
import { ClickDetector } from "./click_detector";
import { SOUNDS } from "../platform/sound";
import { InputReceiver } 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";
/*
* ***************************************************
*
* LEGACY CODE WARNING
*
* This is old code from yorg3.io and needs to be refactored
* @TODO
*
* ***************************************************
*/
const kbEnter = 13;
const kbCancel = 27;
const logger = createLogger("dialogs");
/**
* Basic text based dialog
*/
export class Dialog {
/**
*
* Constructs a new dialog with the given options
* @param {object} param0
* @param {Application} param0.app
* @param {string} param0.title Title of the dialog
* @param {string} param0.contentHTML Inner dialog html
* @param {Array<string>} param0.buttons
* Button list, each button contains of up to 3 parts separated by ':'.
* Part 0: The id, one of the one defined in dialog_buttons.yaml
* 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
* kb_enter: This button is triggered by the enter key
* kb_escape This button is triggered by the escape key
* @param {string=} param0.type The dialog type, either "info" or "warn"
* @param {boolean=} param0.closeButton Whether this dialog has a close button
*/
constructor({ app, title, contentHTML, buttons, type = "info", closeButton = false }) {
this.app = app;
this.title = title;
this.contentHTML = contentHTML;
this.type = type;
this.buttonIds = buttons;
this.closeButton = closeButton;
this.closeRequested = new Signal();
this.buttonSignals = {};
for (let i = 0; i < buttons.length; ++i) {
if (G_IS_DEV && globalConfig.debug.disableTimedButtons) {
this.buttonIds[i] = this.buttonIds[i].replace(":timeout", "");
}
const buttonId = this.buttonIds[i].split(":")[0];
this.buttonSignals[buttonId] = new Signal();
}
this.valueChosen = new Signal();
this.timeouts = [];
this.clickDetectors = [];
this.inputReciever = new InputReceiver("dialog-" + this.title);
this.inputReciever.keydown.add(this.handleKeydown, this);
this.enterHandler = null;
this.escapeHandler = null;
}
/**
* Internal keydown handler
* @param {object} param0
* @param {number} param0.keyCode
* @param {boolean} param0.shift
* @param {boolean} param0.alt
* @param {boolean} param0.ctrl
*/
handleKeydown({ keyCode, shift, alt, ctrl }) {
if (keyCode === kbEnter && this.enterHandler) {
this.internalButtonHandler(this.enterHandler);
return STOP_PROPAGATION;
}
if (keyCode === kbCancel && this.escapeHandler) {
this.internalButtonHandler(this.escapeHandler);
return STOP_PROPAGATION;
}
}
internalButtonHandler(id, ...payload) {
this.app.inputMgr.popReciever(this.inputReciever);
if (id !== "close-button") {
this.buttonSignals[id].dispatch(...payload);
}
this.closeRequested.dispatch();
}
createElement() {
const elem = document.createElement("div");
elem.classList.add("ingameDialog");
this.dialogElem = document.createElement("div");
this.dialogElem.classList.add("dialogInner");
if (this.type) {
this.dialogElem.classList.add(this.type);
}
elem.appendChild(this.dialogElem);
const title = document.createElement("h1");
title.innerText = this.title;
title.classList.add("title");
this.dialogElem.appendChild(title);
if (this.closeButton) {
this.dialogElem.classList.add("hasCloseButton");
const closeBtn = document.createElement("button");
closeBtn.classList.add("closeButton");
this.trackClicks(closeBtn, () => this.internalButtonHandler("close-button"), {
applyCssClass: "pressedSmallElement",
});
title.appendChild(closeBtn);
this.inputReciever.backButton.add(() => this.internalButtonHandler("close-button"));
}
const content = document.createElement("div");
content.classList.add("content");
content.innerHTML = this.contentHTML;
this.dialogElem.appendChild(content);
if (this.buttonIds.length > 0) {
const buttons = document.createElement("div");
buttons.classList.add("buttons");
// Create buttons
for (let i = 0; i < this.buttonIds.length; ++i) {
const [buttonId, buttonStyle, rawParams] = this.buttonIds[i].split(":");
const button = document.createElement("button");
button.classList.add("button");
button.classList.add("styledButton");
button.classList.add(buttonStyle);
button.innerText = T.dialogs.buttons[buttonId];
const params = (rawParams || "").split("/");
const useTimeout = params.indexOf("timeout") >= 0;
const isEnter = params.indexOf("enter") >= 0;
const isEscape = params.indexOf("escape") >= 0;
if (isEscape && this.closeButton) {
logger.warn("Showing dialog with close button, and additional cancel button");
}
if (useTimeout) {
button.classList.add("timedButton");
const timeout = setTimeout(() => {
button.classList.remove("timedButton");
arrayDeleteValue(this.timeouts, timeout);
}, 1000);
this.timeouts.push(timeout);
}
if (isEnter || isEscape) {
// if (this.app.settings.getShowKeyboardShortcuts()) {
// Show keybinding
const spacer = document.createElement("code");
spacer.classList.add("keybinding");
spacer.innerHTML = getStringForKeyCode(isEnter ? kbEnter : kbCancel);
button.appendChild(spacer);
// }
if (isEnter) {
this.enterHandler = buttonId;
}
if (isEscape) {
this.escapeHandler = buttonId;
}
}
this.trackClicks(button, () => this.internalButtonHandler(buttonId));
buttons.appendChild(button);
}
this.dialogElem.appendChild(buttons);
} else {
this.dialogElem.classList.add("buttonless");
}
this.element = elem;
this.app.inputMgr.pushReciever(this.inputReciever);
return this.element;
}
setIndex(index) {
this.element.style.zIndex = index;
}
destroy() {
if (!this.element) {
assert(false, "Tried to destroy dialog twice");
return;
}
// 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);
for (let i = 0; i < this.clickDetectors.length; ++i) {
this.clickDetectors[i].cleanup();
}
this.clickDetectors = [];
this.element.remove();
this.element = null;
for (let i = 0; i < this.timeouts.length; ++i) {
clearTimeout(this.timeouts[i]);
}
this.timeouts = [];
}
hide() {
this.element.classList.remove("visible");
}
show() {
this.element.classList.add("visible");
}
/**
* Helper method to track clicks on an element
* @param {Element} elem
* @param {function():void} handler
* @param {import("./click_detector").ClickDetectorConstructorArgs=} args
* @returns {ClickDetector}
*/
trackClicks(elem, handler, args = {}) {
const detector = new ClickDetector(elem, args);
detector.click.add(handler, this);
this.clickDetectors.push(detector);
return detector;
}
}
/**
* Dialog which simply shows a loading spinner
*/
export class DialogLoading extends Dialog {
constructor(app, text = "") {
super({
app,
title: "",
contentHTML: "",
buttons: [],
type: "loading",
});
// Loading dialog can not get closed with back button
this.inputReciever.backButton.removeAll();
this.inputReciever.context = "dialog-loading";
this.text = text;
}
createElement() {
const elem = document.createElement("div");
elem.classList.add("ingameDialog");
elem.classList.add("loadingDialog");
this.element = elem;
if (this.text) {
const text = document.createElement("div");
text.classList.add("text");
text.innerText = this.text;
elem.appendChild(text);
}
const loader = document.createElement("div");
loader.classList.add("prefab_LoadingTextWithAnim");
loader.classList.add("loadingIndicator");
elem.appendChild(loader);
this.app.inputMgr.pushReciever(this.inputReciever);
return elem;
}
}
export class DialogOptionChooser extends Dialog {
constructor({ app, title, options }) {
let html = "<div class='optionParent'>";
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>` : "";
html += `
<div class='option ${value === options.active ? "active" : ""} ${
iconPrefix ? "hasIcon" : ""
}' data-optionvalue='${value}'>
${iconHtml}
<span class='title'>${text}</span>
${descHtml}
</div>
`;
});
html += "</div>";
super({
app,
title,
contentHTML: html,
buttons: [],
type: "info",
closeButton: true,
});
this.options = options;
this.initialOption = options.active;
this.buttonSignals.optionSelected = new Signal();
}
createElement() {
const div = super.createElement();
this.dialogElem.classList.add("optionChooserDialog");
div.querySelectorAll("[data-optionvalue]").forEach(handle => {
const value = handle.getAttribute("data-optionvalue");
if (!handle) {
logger.error("Failed to bind option value in dialog:", value);
return;
}
// Need click detector here to forward elements, otherwise scrolling does not work
const detector = new ClickDetector(handle, {
consumeEvents: false,
preventDefault: false,
clickSound: null,
applyCssClass: "pressedOption",
targetOnly: true,
});
this.clickDetectors.push(detector);
if (value !== this.initialOption) {
detector.click.add(() => {
const selected = div.querySelector(".option.active");
if (selected) {
selected.classList.remove("active");
} else {
logger.warn("No selected option");
}
handle.classList.add("active");
this.app.sound.playUiSound(SOUNDS.uiClick);
this.internalButtonHandler("optionSelected", value);
});
}
});
return div;
}
}
export class DialogWithForm extends Dialog {
/**
*
* @param {object} param0
* @param {Application} param0.app
* @param {string} param0.title
* @param {string} param0.desc
* @param {array=} param0.buttons
* @param {string=} param0.confirmButtonId
* @param {string=} param0.extraButton
* @param {boolean=} param0.closeButton
* @param {Array<FormElement>} param0.formElements
*/
constructor({
app,
title,
desc,
formElements,
buttons = ["cancel", "ok:good"],
confirmButtonId = "ok",
closeButton = true,
}) {
let html = "";
html += desc + "<br>";
for (let i = 0; i < formElements.length; ++i) {
html += formElements[i].getHtml();
}
super({
app,
title: title,
contentHTML: html,
buttons: buttons,
type: "info",
closeButton,
});
this.confirmButtonId = confirmButtonId;
this.formElements = formElements;
this.enterHandler = confirmButtonId;
}
internalButtonHandler(id, ...payload) {
if (id === this.confirmButtonId) {
if (this.hasAnyInvalid()) {
this.dialogElem.classList.remove("errorShake");
waitNextFrame().then(() => {
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;
}
}
import type { Application } from "../application";
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";
/*
* ***************************************************
*
* LEGACY CODE WARNING
*
* This is old code from yorg3.io and needs to be refactored
* @TODO
*
* ***************************************************
*/
const kbEnter = 13;
const kbCancel = 27;
const logger = createLogger("dialogs");
export type DialogButtonStr<T extends string> = `${T}:${string}` | T;
export type DialogButtonType = "info" | "loading" | "warning";
/**
* Basic text based dialog
*/
export class Dialog<T extends string = never, U extends unknown[] = []> {
public title: string;
public app: Application;
public contentHTML: string;
public type: string;
public buttonIds: string[];
public closeButton: boolean;
public dialogElem: HTMLDivElement;
public element: HTMLDivElement;
public closeRequested = new Signal();
public buttonSignals = {} as Record<T, Signal<U | []>>;
public valueChosen = new Signal<[unknown]>();
public timeouts: number[] = [];
public clickDetectors: ClickDetector[] = [];
public inputReciever: InputReceiver;
public enterHandler: T = null;
public escapeHandler: T = null;
/**
*
* Constructs a new dialog with the given options
* @param param0
* @param param0.title Title of the dialog
* @param param0.contentHTML Inner dialog html
* @param param0.buttons
* Button list, each button contains of up to 3 parts separated by ':'.
* Part 0: The id, one of the one defined in dialog_buttons.yaml
* 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
* kb_enter: This button is triggered by the enter key
* 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
*/
constructor({
app,
title,
contentHTML,
buttons,
type = "info",
closeButton = false,
}: {
app: Application;
title: string;
contentHTML: string;
buttons?: DialogButtonStr<T>[];
type?: DialogButtonType;
closeButton?: boolean;
}) {
this.app = app;
this.title = title;
this.contentHTML = contentHTML;
this.type = type;
this.buttonIds = buttons;
this.closeButton = closeButton;
for (let i = 0; i < buttons.length; ++i) {
if (G_IS_DEV && globalConfig.debug.disableTimedButtons) {
this.buttonIds[i] = this.buttonIds[i].replace(":timeout", "");
}
const buttonId = this.buttonIds[i].split(":")[0];
this.buttonSignals[buttonId] = new Signal();
}
this.inputReciever = new InputReceiver("dialog-" + this.title);
this.inputReciever.keydown.add(this.handleKeydown, this);
}
/**
* Internal keydown handler
*/
handleKeydown({ keyCode, shift, alt, ctrl }: KeydownEvent): void | STOP_PROPAGATION {
if (keyCode === kbEnter && this.enterHandler) {
this.internalButtonHandler(this.enterHandler);
return STOP_PROPAGATION;
}
if (keyCode === kbCancel && this.escapeHandler) {
this.internalButtonHandler(this.escapeHandler);
return STOP_PROPAGATION;
}
}
internalButtonHandler(id: T | "close-button", ...payload: U | []) {
this.app.inputMgr.popReciever(this.inputReciever);
if (id !== "close-button") {
this.buttonSignals[id].dispatch(...payload);
}
this.closeRequested.dispatch();
}
createElement() {
const elem = document.createElement("div");
elem.classList.add("ingameDialog");
this.dialogElem = document.createElement("div");
this.dialogElem.classList.add("dialogInner");
if (this.type) {
this.dialogElem.classList.add(this.type); // @TODO: `this.type` seems unused
}
elem.appendChild(this.dialogElem);
const title = document.createElement("h1");
title.innerText = this.title;
title.classList.add("title");
this.dialogElem.appendChild(title);
if (this.closeButton) {
this.dialogElem.classList.add("hasCloseButton");
const closeBtn = document.createElement("button");
closeBtn.classList.add("closeButton");
this.trackClicks(closeBtn, () => this.internalButtonHandler("close-button"), {
applyCssClass: "pressedSmallElement",
});
title.appendChild(closeBtn);
this.inputReciever.backButton.add(() => this.internalButtonHandler("close-button"));
}
const content = document.createElement("div");
content.classList.add("content");
content.innerHTML = this.contentHTML;
this.dialogElem.appendChild(content);
if (this.buttonIds.length > 0) {
const buttons = document.createElement("div");
buttons.classList.add("buttons");
// Create buttons
for (let i = 0; i < this.buttonIds.length; ++i) {
const [buttonId, buttonStyle, rawParams] = this.buttonIds[i].split(":") as [
T,
string,
string?
]; // @TODO: some button strings omit `buttonStyle`
const button = document.createElement("button");
button.classList.add("button");
button.classList.add("styledButton");
button.classList.add(buttonStyle);
button.innerText = T.dialogs.buttons[buttonId as string];
const params = (rawParams || "").split("/");
const useTimeout = params.indexOf("timeout") >= 0;
const isEnter = params.indexOf("enter") >= 0;
const isEscape = params.indexOf("escape") >= 0;
if (isEscape && this.closeButton) {
logger.warn("Showing dialog with close button, and additional cancel button");
}
if (useTimeout) {
button.classList.add("timedButton");
const timeout = setTimeout(() => {
button.classList.remove("timedButton");
arrayDeleteValue(this.timeouts, timeout);
}, 1000) as unknown as number; // @TODO: @types/node should not be affecting this
this.timeouts.push(timeout);
}
if (isEnter || isEscape) {
// if (this.app.settings.getShowKeyboardShortcuts()) {
// Show keybinding
const spacer = document.createElement("code");
spacer.classList.add("keybinding");
spacer.innerHTML = getStringForKeyCode(isEnter ? kbEnter : kbCancel);
button.appendChild(spacer);
// }
if (isEnter) {
this.enterHandler = buttonId;
}
if (isEscape) {
this.escapeHandler = buttonId;
}
}
this.trackClicks(button, () => this.internalButtonHandler(buttonId));
buttons.appendChild(button);
}
this.dialogElem.appendChild(buttons);
} else {
this.dialogElem.classList.add("buttonless");
}
this.element = elem;
this.app.inputMgr.pushReciever(this.inputReciever);
return this.element;
}
setIndex(index: string) {
this.element.style.zIndex = index;
}
destroy() {
if (!this.element) {
assert(false, "Tried to destroy dialog twice");
return;
}
// 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);
for (let i = 0; i < this.clickDetectors.length; ++i) {
this.clickDetectors[i].cleanup();
}
this.clickDetectors = [];
this.element.remove();
this.element = null;
for (let i = 0; i < this.timeouts.length; ++i) {
clearTimeout(this.timeouts[i]);
}
this.timeouts = [];
}
hide() {
this.element.classList.remove("visible");
}
show() {
this.element.classList.add("visible");
}
/**
* Helper method to track clicks on an element
*/
trackClicks(elem: Element, handler: () => void, args: ClickDetectorConstructorArgs = {}) {
const detector = new ClickDetector(elem, args);
detector.click.add(handler, this);
this.clickDetectors.push(detector);
return detector;
}
}
/**
* Dialog which simply shows a loading spinner
*/
export class DialogLoading extends Dialog {
constructor(app: Application, public text = "") {
super({
app,
title: "",
contentHTML: "",
buttons: [],
type: "loading",
});
// Loading dialog can not get closed with back button
this.inputReciever.backButton.removeAll();
this.inputReciever.context = "dialog-loading";
}
createElement() {
const elem = document.createElement("div");
elem.classList.add("ingameDialog");
elem.classList.add("loadingDialog");
this.element = elem;
if (this.text) {
const text = document.createElement("div");
text.classList.add("text");
text.innerText = this.text;
elem.appendChild(text);
}
const loader = document.createElement("div");
loader.classList.add("prefab_LoadingTextWithAnim");
loader.classList.add("loadingIndicator");
elem.appendChild(loader);
this.app.inputMgr.pushReciever(this.inputReciever);
return elem;
}
}
type DialogOptionChooserOption = { value: string; text: string; desc?: string; iconPrefix?: string };
export class DialogOptionChooser extends Dialog<"optionSelected", [string]> {
public options: {
options: DialogOptionChooserOption[];
active: string;
};
public initialOption: string;
constructor({
app,
title,
options,
}: {
app: Application;
title: string;
options: {
options: DialogOptionChooserOption[];
active: string;
};
}) {
let html = "<div class='optionParent'>";
options.options.forEach(({ value, text, desc = null, iconPrefix = null }) => {
const descHtml = desc ? `<span class="desc">${desc}</span>` : "";
const iconHtml = iconPrefix ? `<span class="icon icon-${iconPrefix}-${value}"></span>` : "";
html += `
<div class='option ${value === options.active ? "active" : ""} ${
iconPrefix ? "hasIcon" : ""
}' data-optionvalue='${value}'>
${iconHtml}
<span class='title'>${text}</span>
${descHtml}
</div>
`;
});
html += "</div>";
super({
app,
title,
contentHTML: html,
buttons: [],
type: "info",
closeButton: true,
});
this.options = options;
this.initialOption = options.active;
this.buttonSignals.optionSelected = new Signal();
}
createElement() {
const div = super.createElement();
this.dialogElem.classList.add("optionChooserDialog");
div.querySelectorAll("[data-optionvalue]").forEach(handle => {
const value = handle.getAttribute("data-optionvalue");
if (!handle) {
logger.error("Failed to bind option value in dialog:", value);
return;
}
// Need click detector here to forward elements, otherwise scrolling does not work
const detector = new ClickDetector(handle, {
consumeEvents: false,
preventDefault: false,
clickSound: null,
applyCssClass: "pressedOption",
targetOnly: true,
});
this.clickDetectors.push(detector);
if (value !== this.initialOption) {
detector.click.add(() => {
const selected = div.querySelector(".option.active");
if (selected) {
selected.classList.remove("active");
} else {
logger.warn("No selected option");
}
handle.classList.add("active");
this.app.sound.playUiSound(SOUNDS.uiClick);
this.internalButtonHandler("optionSelected", value);
});
}
});
return div;
}
}
export class DialogWithForm<T extends string = "cancel" | "ok"> extends Dialog<T> {
public confirmButtonId: string;
// `FormElement` is invariant so `unknown` and `never` don't work
public formElements: FormElement<any>[];
constructor({
app,
title,
desc,
formElements,
buttons = ["cancel", "ok:good"] as any,
confirmButtonId = "ok" as any,
closeButton = true,
}: {
app: Application;
title: string;
desc: string;
formElements: FormElement<any>[];
buttons?: DialogButtonStr<T>[];
confirmButtonId?: T;
closeButton?: boolean;
}) {
let html = "";
html += desc + "<br>";
for (let i = 0; i < formElements.length; ++i) {
html += formElements[i].getHtml();
}
super({
app,
title: title,
contentHTML: html,
buttons: buttons,
type: "info",
closeButton,
});
this.confirmButtonId = confirmButtonId;
this.formElements = formElements;
this.enterHandler = confirmButtonId;
}
internalButtonHandler(id: T | "close-button", ...payload: []) {
if (id === this.confirmButtonId) {
if (this.hasAnyInvalid()) {
this.dialogElem.classList.remove("errorShake");
waitNextFrame().then(() => {
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 { ClickDetector } from "./click_detector";
import { Signal } from "./signal";
/*
* ***************************************************
*
* LEGACY CODE WARNING
*
* This is old code from yorg3.io and needs to be refactored
* @TODO
*
* ***************************************************
*/
export class FormElement {
constructor(id, label) {
this.id = id;
this.label = label;
this.valueChosen = new Signal();
}
getHtml() {
abstract;
return "";
}
getFormElement(parent) {
return parent.querySelector("[data-formId='" + this.id + "']");
}
bindEvents(parent, clickTrackers) {
abstract;
}
focus() {}
isValid() {
return true;
}
/** @returns {any} */
getValue() {
abstract;
}
}
export class FormElementInput extends FormElement {
constructor({ id, label = null, placeholder, defaultValue = "", inputType = "text", validator = null }) {
super(id, label);
this.placeholder = placeholder;
this.defaultValue = defaultValue;
this.inputType = inputType;
this.validator = validator;
this.element = null;
}
getHtml() {
let classes = [];
let inputType = "text";
let maxlength = 256;
switch (this.inputType) {
case "text": {
classes.push("input-text");
break;
}
case "email": {
classes.push("input-email");
inputType = "email";
break;
}
case "token": {
classes.push("input-token");
inputType = "text";
maxlength = 4;
break;
}
}
return `
<div class="formElement input">
${this.label ? `<label>${this.label}</label>` : ""}
<input
type="${inputType}"
value="${this.defaultValue.replace(/["\\]+/gi, "")}"
maxlength="${maxlength}"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
class="${classes.join(" ")}"
placeholder="${this.placeholder.replace(/["\\]+/gi, "")}"
data-formId="${this.id}">
</div>
`;
}
bindEvents(parent, clickTrackers) {
this.element = this.getFormElement(parent);
this.element.addEventListener("input", event => this.updateErrorState());
this.updateErrorState();
}
updateErrorState() {
this.element.classList.toggle("errored", !this.isValid());
}
isValid() {
return !this.validator || this.validator(this.element.value);
}
getValue() {
return this.element.value;
}
setValue(value) {
this.element.value = value;
this.updateErrorState();
}
focus() {
this.element.focus();
this.element.select();
}
}
export class FormElementCheckbox extends FormElement {
constructor({ id, label, defaultValue = true }) {
super(id, label);
this.defaultValue = defaultValue;
this.value = this.defaultValue;
this.element = null;
}
getHtml() {
return `
<div class="formElement checkBoxFormElem">
${this.label ? `<label>${this.label}</label>` : ""}
<div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'>
<span class="knob"></span >
</div >
</div>
`;
}
bindEvents(parent, clickTrackers) {
this.element = this.getFormElement(parent);
const detector = new ClickDetector(this.element, {
consumeEvents: false,
preventDefault: false,
});
clickTrackers.push(detector);
detector.click.add(this.toggle, this);
}
getValue() {
return this.value;
}
toggle() {
this.value = !this.value;
this.element.classList.toggle("checked", this.value);
}
focus(parent) {}
}
export class FormElementItemChooser extends FormElement {
/**
*
* @param {object} param0
* @param {string} param0.id
* @param {string=} param0.label
* @param {Array<BaseItem>} param0.items
*/
constructor({ id, label, items = [] }) {
super(id, label);
this.items = items;
this.element = null;
/**
* @type {BaseItem}
*/
this.chosenItem = null;
}
getHtml() {
let classes = [];
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) {
this.element = this.getFormElement(parent);
for (let i = 0; i < this.items.length; ++i) {
const item = this.items[i];
const canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const context = canvas.getContext("2d");
item.drawFullSizeOnCanvas(context, 128);
this.element.appendChild(canvas);
const detector = new ClickDetector(canvas, {});
clickTrackers.push(detector);
detector.click.add(() => {
this.chosenItem = item;
this.valueChosen.dispatch(item);
});
}
}
isValid() {
return true;
}
getValue() {
return null;
}
focus() {}
}
import { BaseItem } from "../game/base_item";
import { ClickDetector } from "./click_detector";
import { Signal } from "./signal";
/*
* ***************************************************
*
* LEGACY CODE WARNING
*
* This is old code from yorg3.io and needs to be refactored
* @TODO
*
* ***************************************************
*/
export abstract class FormElement<T = string> {
public valueChosen = new Signal<[T]>();
constructor(public id: string, public label: string) {}
abstract getHtml(): string;
getFormElement(parent: HTMLElement): HTMLElement {
return parent.querySelector("[data-formId='" + this.id + "']");
}
abstract bindEvents(parent: HTMLDivElement, clickTrackers: ClickDetector[]): void;
focus() {}
isValid() {
return true;
}
abstract getValue(): T;
}
export class FormElementInput extends FormElement {
public placeholder: string;
public defaultValue: string;
public inputType: "text" | "email" | "token";
public validator: (value: string) => boolean;
public element: HTMLInputElement = null;
constructor({
id,
label = null,
placeholder,
defaultValue = "",
inputType = "text",
validator = null,
}: {
id: string;
label?: string;
placeholder: string;
defaultValue?: string;
inputType?: "text" | "email" | "token";
validator?: (value: string) => boolean;
}) {
super(id, label);
this.placeholder = placeholder;
this.defaultValue = defaultValue;
this.inputType = inputType;
this.validator = validator;
}
getHtml() {
const classes = [];
let inputType = "text";
let maxlength = 256;
// @TODO: `inputType` and these classes are unused
switch (this.inputType) {
case "text": {
classes.push("input-text");
break;
}
case "email": {
classes.push("input-email");
inputType = "email";
break;
}
case "token": {
classes.push("input-token");
inputType = "text";
maxlength = 4;
break;
}
}
return `
<div class="formElement input">
${this.label ? `<label>${this.label}</label>` : ""}
<input
type="${inputType}"
value="${this.defaultValue.replace(/["\\]+/gi, "")}"
maxlength="${maxlength}"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
class="${classes.join(" ")}"
placeholder="${this.placeholder.replace(/["\\]+/gi, "")}"
data-formId="${this.id}">
</div>
`;
}
bindEvents(parent: HTMLDivElement, clickTrackers: ClickDetector[]) {
this.element = this.getFormElement(parent) as HTMLInputElement;
this.element.addEventListener("input", event => this.updateErrorState());
this.updateErrorState();
}
updateErrorState() {
this.element.classList.toggle("errored", !this.isValid());
}
isValid() {
return !this.validator || this.validator(this.element.value);
}
getValue() {
return this.element.value;
}
setValue(value: string) {
this.element.value = value;
this.updateErrorState();
}
focus() {
this.element.focus();
this.element.select();
}
}
export class FormElementCheckbox extends FormElement<boolean> {
public defaultValue: boolean;
public value: boolean;
public element: HTMLDivElement;
constructor({ id, label, defaultValue = true }) {
super(id, label);
this.defaultValue = defaultValue;
this.value = this.defaultValue;
this.element = null;
}
getHtml() {
return `
<div class="formElement checkBoxFormElem">
${this.label ? `<label>${this.label}</label>` : ""}
<div class="checkbox ${this.defaultValue ? "checked" : ""}" data-formId='${this.id}'>
<span class="knob"></span >
</div >
</div>
`;
}
bindEvents(parent: HTMLDivElement, clickTrackers: ClickDetector[]) {
this.element = this.getFormElement(parent) as HTMLDivElement;
const detector = new ClickDetector(this.element, {
consumeEvents: false,
preventDefault: false,
});
clickTrackers.push(detector);
detector.click.add(this.toggle, this);
}
getValue() {
return this.value;
}
toggle() {
this.value = !this.value;
this.element.classList.toggle("checked", this.value);
}
focus() {}
}
export class FormElementItemChooser extends FormElement<BaseItem> {
public items: BaseItem[];
public element: HTMLDivElement = null;
public chosenItem: BaseItem = null;
constructor({ id, label, items = [] }: { id: string; label: string; items: BaseItem[] }) {
super(id, label);
this.items = items;
}
getHtml() {
const classes = [];
return `
<div class="formElement">
${this.label ? `<label>${this.label}</label>` : ""}
<div class="ingameItemChooser input" data-formId="${this.id}"></div>
</div>
`;
}
bindEvents(parent: HTMLElement, clickTrackers: ClickDetector[]) {
this.element = this.getFormElement(parent) as HTMLDivElement;
for (let i = 0; i < this.items.length; ++i) {
const item = this.items[i];
const canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
const context = canvas.getContext("2d");
item.drawFullSizeOnCanvas(context, 128);
this.element.appendChild(canvas);
const detector = new ClickDetector(canvas, {});
clickTrackers.push(detector);
detector.click.add(() => {
this.chosenItem = item;
this.valueChosen.dispatch(item);
});
}
}
isValid() {
return true;
}
getValue() {
return null;
}
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 {
constructor() {
this.receivers = [];
this.modifyCount = 0;
}
export type SignalReceiver<T extends unknown[]> = (...args: T) => STOP_PROPAGATION | void;
export class Signal<T extends unknown[] = []> {
public receivers: { receiver: SignalReceiver<T>; scope: object }[] = [];
public modifyCount: number = 0;
/**
* 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");
this.receivers.push({ receiver, scope });
++this.modifyCount;
@ -19,10 +18,8 @@ export class Signal {
/**
* 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");
this.receivers.unshift({ receiver, scope });
++this.modifyCount;
@ -30,15 +27,14 @@ export class Signal {
/**
* Dispatches the signal
* @param {...any} payload
*/
dispatch() {
dispatch(...payload: T): void | STOP_PROPAGATION {
const modifyState = this.modifyCount;
const n = this.receivers.length;
for (let i = 0; i < n; ++i) {
const { receiver, scope } = this.receivers[i];
if (receiver.apply(scope, arguments) === STOP_PROPAGATION) {
if (receiver.apply(scope, payload) === STOP_PROPAGATION) {
return STOP_PROPAGATION;
}
@ -51,9 +47,8 @@ export class Signal {
/**
* Removes a receiver
* @param {function} receiver
*/
remove(receiver) {
remove(receiver: SignalReceiver<T>) {
let index = null;
const n = this.receivers.length;
for (let i = 0; i < n; ++i) {

View File

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

View File

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

View File

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

View File

@ -7,7 +7,6 @@ import { GameRoot } from "./root";
import { globalConfig } from "../core/config";
import { enumDirectionToVector, enumDirectionToAngle } from "../core/vector";
import { BasicSerializableObject, types } from "../savegame/serialization";
import { EntityComponentStorage } from "./entity_components";
import { Loader } from "../core/loader";
import { drawRotatedSprite } from "../core/draw_utils";
import { gComponentRegistry } from "../core/global_registries";
@ -27,8 +26,9 @@ export class Entity extends BasicSerializableObject {
/**
* 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
@ -99,7 +99,7 @@ export class Entity extends BasicSerializableObject {
});
for (const key in this.components) {
/** @type {Component} */ (this.components[key]).copyAdditionalStateTo(clone.components[key]);
this.components[key].copyAdditionalStateTo(clone.components[key]);
}
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
* @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
*
*/

View File

@ -34,17 +34,18 @@ export class GameHUD {
*/
initialize() {
this.signals = {
buildingSelectedForPlacement: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()),
selectedPlacementBuildingChanged: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()),
shapePinRequested: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()),
shapeUnpinRequested: /** @type {TypedSignal<[string]>} */ (new Signal()),
notification: /** @type {TypedSignal<[string, enumNotificationType]>} */ (new Signal()),
buildingsSelectedForCopy: /** @type {TypedSignal<[Array<number>]>} */ (new Signal()),
pasteBlueprintRequested: /** @type {TypedSignal<[]>} */ (new Signal()),
viewShapeDetailsRequested: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()),
unlockNotificationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
buildingSelectedForPlacement: /** @type {Signal<[MetaBuilding|null]>} */ (new Signal()),
selectedPlacementBuildingChanged: /** @type {Signal<[MetaBuilding|null]>} */ (new Signal()),
shapePinRequested: /** @type {Signal<[ShapeDefinition]>} */ (new Signal()),
shapeUnpinRequested: /** @type {Signal<[string]>} */ (new Signal()),
notification: /** @type {Signal<[string, enumNotificationType]>} */ (new Signal()),
buildingsSelectedForCopy: /** @type {Signal<[Array<number>]>} */ (new Signal()),
pasteBlueprintRequested: /** @type {Signal<[]>} */ (new Signal()),
viewShapeDetailsRequested: /** @type {Signal<[ShapeDefinition]>} */ (new Signal()),
unlockNotificationFinished: /** @type {Signal<[]>} */ (new Signal()),
};
/** @type {import("./hud_parts").HudParts} */
this.parts = {
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),
placeholder: "",
defaultValue: signal ? signal.getAsCopyableKey() : "",
validator: val => this.parseSignalCode(entity, val),
validator: val => this.parseSignalCode(entity, val) !== null,
});
const items = [...Object.values(COLOR_ITEM_SINGLETONS)];

View File

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

View File

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

View File

@ -150,9 +150,7 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
}
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 => {
const closeLoading = this.root.hud.parts.dialogs.showLoadingDialog();

View File

@ -145,53 +145,53 @@ export class GameRoot {
this.signals = {
// Entities
entityManuallyPlaced: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityAdded: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityChanged: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityGotNewComponent: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityComponentRemoved: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityQueuedForDestroy: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityDestroyed: /** @type {TypedSignal<[Entity]>} */ (new Signal()),
entityManuallyPlaced: /** @type {Signal<[Entity]>} */ (new Signal()),
entityAdded: /** @type {Signal<[Entity]>} */ (new Signal()),
entityChanged: /** @type {Signal<[Entity]>} */ (new Signal()),
entityGotNewComponent: /** @type {Signal<[Entity]>} */ (new Signal()),
entityComponentRemoved: /** @type {Signal<[Entity]>} */ (new Signal()),
entityQueuedForDestroy: /** @type {Signal<[Entity]>} */ (new Signal()),
entityDestroyed: /** @type {Signal<[Entity]>} */ (new Signal()),
// Global
resized: /** @type {TypedSignal<[number, number]>} */ (new Signal()),
readyToRender: /** @type {TypedSignal<[]>} */ (new Signal()),
aboutToDestruct: /** @type {TypedSignal<[]>} */ new Signal(),
resized: /** @type {Signal<[number, number]>} */ (new Signal()),
readyToRender: /** @type {Signal<[]>} */ (new Signal()),
aboutToDestruct: /** @type {Signal<[]>} */ new Signal(),
// Game Hooks
gameSaved: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got saved
gameRestored: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got restored
gameSaved: /** @type {Signal<[]>} */ (new Signal()), // Game got saved
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()),
upgradePurchased: /** @type {TypedSignal<[string]>} */ (new Signal()),
storyGoalCompleted: /** @type {Signal<[number, string]>} */ (new Signal()),
upgradePurchased: /** @type {Signal<[string]>} */ (new Signal()),
// Called right after game is initialized
postLoadHook: /** @type {TypedSignal<[]>} */ (new Signal()),
postLoadHook: /** @type {Signal<[]>} */ (new Signal()),
shapeDelivered: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()),
itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()),
shapeDelivered: /** @type {Signal<[ShapeDefinition]>} */ (new Signal()),
itemProduced: /** @type {Signal<[BaseItem]>} */ (new Signal()),
bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
immutableOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()),
bulkOperationFinished: /** @type {Signal<[]>} */ (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.
// 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
// 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.
achievementCheck: /** @type {TypedSignal<[string, any]>} */ (new Signal()),
bulkAchievementCheck: /** @type {TypedSignal<(string|any)[]>} */ (new Signal()),
achievementCheck: /** @type {Signal<[string, any]>} */ (new Signal()),
bulkAchievementCheck: /** @type {Signal<(string|any)[]>} */ (new Signal()),
// Puzzle mode
puzzleComplete: /** @type {TypedSignal<[]>} */ (new Signal()),
puzzleComplete: /** @type {Signal<[]>} */ (new Signal()),
};
// 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 = {};
/**
* @type {Object<string, (ProccessingRequirementsImplementationPayload) => boolean>}
* @type {Object<string, (arg: ProccessingRequirementsImplementationPayload) => boolean>}
*/
export const MODS_PROCESSING_REQUIREMENTS = {};
/**
* @type {Object<string, ({entity: Entity}) => boolean>}
* @type {Object<string, (arg: {entity: Entity}) => boolean>}
*/
export const MODS_CAN_PROCESS = {};
@ -67,7 +67,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
super(root, [ItemProcessorComponent]);
/**
* @type {Object<enumItemProcessorTypes, function(ProcessorImplementationPayload) : string>}
* @type {Object<enumItemProcessorTypes, (arg: ProcessorImplementationPayload) => void>}
*/
this.handlers = {
[enumItemProcessorTypes.balancer]: this.process_BALANCER,

View File

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

View File

@ -180,7 +180,10 @@ export class GameTime extends BasicSerializableObject {
setSpeed(speed) {
assert(speed instanceof BaseGameSpeed, "Not a valid game speed");
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;
}

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

@ -1,8 +1,11 @@
// Globals defined by webpack
declare const G_IS_DEV: boolean;
declare function assert(condition: boolean | object | string, ...errorMessage: string[]): void;
declare function assertAlways(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[]
): asserts condition;
declare const abstract: void;
@ -142,34 +145,6 @@ declare interface 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 {
add(receiver: () => string | void, scope: null | any);
dispatch(): string | void;
@ -186,18 +161,6 @@ declare class TypedTrackedState<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 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
appBooted: new Signal(),
modifyLevelDefinitions: /** @type {TypedSignal<[Array[Object]]>} */ (new Signal()),
modifyUpgrades: /** @type {TypedSignal<[Object]>} */ (new Signal()),
modifyLevelDefinitions: /** @type {Signal<[Array[Object]]>} */ (new Signal()),
modifyUpgrades: /** @type {Signal<[Object]>} */ (new Signal()),
hudElementInitialized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()),
hudElementFinalized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()),
hudElementInitialized: /** @type {Signal<[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()),
gameLoadingStageEntered: /** @type {TypedSignal<[InGameState, string]>} */ (new Signal()),
gameInitialized: /** @type {Signal<[GameRoot]>} */ (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:
/** @type {TypedSignal<[GameRoot, import("../savegame/savegame_typedefs").SerializedGame]>} */ (
/** @type {Signal<[GameRoot, import("../savegame/savegame_typedefs").SerializedGame]>} */ (
new Signal()
),
gameDeserialized:
/** @type {TypedSignal<[GameRoot, import("../savegame/savegame_typedefs").SerializedGame]>} */ (
/** @type {Signal<[GameRoot, import("../savegame/savegame_typedefs").SerializedGame]>} */ (
new Signal()
),
};

View File

@ -107,9 +107,13 @@ export class ModLoader {
exposeExports() {
if (G_IS_DEV || G_IS_STANDALONE) {
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 => {
// @ts-ignore
/** @type {object} */
const module = modules(key);
for (const member in module) {
if (member === "default" || member === "__$S__") {

View File

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

View File

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

View File

@ -54,7 +54,7 @@ export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser {
performRestart() {
logger.log(this, "Performing restart");
window.location.reload(true);
window.location.reload();
}
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 { enumBalancerVariants, MetaBalancerBuilding } from "../../game/buildings/balancer.js";
import { MetaBeltBuilding } from "../../game/buildings/belt.js";

View File

@ -25,6 +25,15 @@ import {
TypePositiveIntegerOrString,
} 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");
// Schema declarations
@ -106,7 +115,7 @@ export const types = {
},
/**
* @param {SingletonFactoryTemplate<*>} innerType
* @param {SingletonFactoryTemplate<*>} registry
*/
classRef(registry) {
return new TypeMetaClass(registry);

View File

@ -7,6 +7,15 @@ import { Vector } from "../core/vector";
import { round4Digits } from "../core/utils";
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
@ -48,6 +57,7 @@ export class BaseDataType {
/**
* Serializes a given raw value
* @param {any} value
* @returns {unknown}
* @abstract
*/
serialize(value) {
@ -1034,7 +1044,8 @@ export class TypeKeyValueMap extends BaseDataType {
const serialized = this.valueType.serialize(value[key]);
if (!this.includeEmptyValues && typeof serialized === "object") {
if (
serialized.$ &&
"$" in serialized &&
"data" in serialized &&
typeof serialized.data === "object" &&
Object.keys(serialized.data).length === 0
) {

View File

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

View File

@ -54,7 +54,9 @@ export class LoginState extends GameState {
T.dialogs.offlineMode.desc,
["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);
} else {
this.finishLoading();

View File

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

View File

@ -30,6 +30,7 @@
/* Module Resolution Options */
"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'. */,
"paths": {
"root/*": ["./*"]