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:
parent
76a00db34d
commit
6db782d66a
@ -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();
|
||||
|
||||
@ -55,6 +55,7 @@ export class BackgroundResourcesLoader {
|
||||
this.mainMenuPromise = null;
|
||||
this.ingamePromise = null;
|
||||
|
||||
/** @type {Signal<[{ progress: number }]>} */
|
||||
this.resourceStateChangedSignal = new Signal();
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
20
src/js/core/global_registries.ts
Normal file
20
src/js/core/global_registries.ts
Normal 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");
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
47
src/js/core/input_receiver.ts
Normal file
47
src/js/core/input_receiver.ts
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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() {}
|
||||
}
|
||||
@ -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) {
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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
51
src/js/game/entity_components.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@ -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 */
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
*
|
||||
*/
|
||||
|
||||
@ -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
107
src/js/game/hud/hud_parts.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@ -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)];
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
47
src/js/globals.d.ts
vendored
@ -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";
|
||||
|
||||
|
||||
@ -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()
|
||||
),
|
||||
};
|
||||
|
||||
@ -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__") {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -140,7 +140,7 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface {
|
||||
|
||||
performRestart() {
|
||||
logger.log("Performing restart");
|
||||
window.location.reload(true);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -54,7 +54,7 @@ export class PlatformWrapperImplElectron extends PlatformWrapperImplBrowser {
|
||||
|
||||
performRestart() {
|
||||
logger.log(this, "Performing restart");
|
||||
window.location.reload(true);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
initializeAdProvider() {
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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/*": ["./*"]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user