waves/public/assets/g/mario/References/InputWritr-0.2.0.ts
2025-04-09 17:11:14 -05:00

687 lines
24 KiB
TypeScript

declare module InputWritr {
export interface IInputWritrTriggerCallback {
(eventInformation: any, event: Event): void;
}
export interface IInputWritrTriggerContainer {
[i: string]: {
[i: string]: IInputWritrTriggerCallback;
[i: number]: IInputWritrTriggerCallback;
}
}
export interface IInputWriterBooleanGetter {
(...args: any[]): boolean;
}
export interface IInputWritrSettings {
/**
* The mapping of events to their key codes, to their callbacks.
*/
triggers: IInputWritrTriggerContainer;
/**
* The first argument to be passed to event callbacks.
*/
eventInformation?: any;
/**
* Function to generate a current timestamp, commonly performance.now.
*/
getTimestamp?: () => number;
/**
* Known, allowed aliases for triggers.
*/
aliases?: { [i: string]: any[] };
/**
* A quick lookup table of key aliases to their character codes.
*/
keyAliasesToCodes?: { [i: string]: number };
/**
* A quick lookup table of character codes to their key aliases.
*/
keyCodesToAliases?: { [i: number]: string };
/**
* Whether events are initially allowed to trigger (by default, true).
*/
canTrigger?: boolean | IInputWriterBooleanGetter;
/**
* Whether triggered inputs are initially allowed to be written to history
* (by default, true).
*/
isRecording?: boolean | IInputWriterBooleanGetter;
}
export interface IInputWritr {
restartHistory(keepHistory?: boolean): void;
getAliases(): any;
getAliasesAsKeyStrings(): { [i: string]: any };
getAliasAsKeyStrings(alias: any): string[];
convertAliasToKeyString(alias: any): string;
convertKeyStringToAlias(key: number | string): number | string;
getHistory(name?: string): any;
getHistories(): any;
getCanTrigger(): IInputWriterBooleanGetter;
getIsRecording(): IInputWriterBooleanGetter;
setCanTrigger(canTriggerNew: boolean | IInputWriterBooleanGetter): void;
setIsRecording(isRecordingNew: boolean | IInputWriterBooleanGetter): void;
setEventInformation(eventInformationNew: any): void;
addAliasValues(name: any, values: any[]): void;
removeAliasValues(name: string, values: any[]): void;
switchAliasValues(name: string, valuesOld: any[], valuesNew: any[]): void;
addAliases(aliasesRaw: any): void;
addEvent(trigger: string, label: any, callback: IInputWritrTriggerCallback): void;
removeEvent(trigger: string, label: any): void;
saveHistory(name?: string): void;
playHistory(): void;
playEvents(events: any): void;
callEvent(event: Function | string, keyCode?: number | string, sourceEvent?: Event): any;
makePipe(trigger: string, codeLabel: string, preventDefaults?: boolean): Function;
}
}
module InputWritr {
"use strict";
/**
* A general utility for automating interactions with user-called events linked
* with callbacks. Pipe functions are available that take in user input, switch
* on the event code, and call the appropriate callback. These Pipe functions
* can be made during runtime; further utilities allow for saving and playback
* of input histories in JSON format.
*/
export class InputWritr implements IInputWritr {
/**
* A mapping of events to their key codes, to their callbacks.
*/
private triggers: IInputWritrTriggerContainer;
/**
* Known, allowed aliases for triggers.
*/
private aliases: { [i: string]: any[] };
/**
* Recording of every action that has happened, with a timestamp.
*/
private history: any;
/**
* A listing of all histories, with indices set by this.saveHistory.
*/
private histories: any;
/**
* Function to generate a current timestamp, commonly performance.now.
*/
private getTimestamp: () => number;
/**
* A starting time used for calculating playback delays in playHistory.
*/
private startingTime: number;
/**
* An Object to be passed to event calls, commonly with key information,
* such as { "Down": 0 }.
*/
private eventInformation: any;
/**
* An optional Boolean callback to disable or enable input triggers.
*/
private canTrigger: IInputWriterBooleanGetter;
/**
* Whether to record events into history.
*/
private isRecording: IInputWriterBooleanGetter;
/**
* A quick lookup table of key aliases to their character codes.
*/
private keyAliasesToCodes: { [i: string]: number };
/**
* A quick lookup table of character codes to their key aliases.
*/
private keyCodesToAliases: { [i: number]: string };
/**
* @param {IInputWritrSettings} settings
*/
constructor(settings: IInputWritrSettings) {
if (typeof settings === "undefined") {
throw new Error("No settings object given to InputWritr.");
}
if (typeof settings.triggers === "undefined") {
throw new Error("No triggers given to InputWritr.");
}
this.triggers = settings.triggers;
// Headless browsers like PhantomJS might not contain the performance
// class, so Date.now is used as a backup
if (typeof settings.getTimestamp === "undefined") {
if (typeof performance === "undefined") {
this.getTimestamp = function (): number {
return Date.now();
};
} else {
this.getTimestamp = (
performance.now
|| (<any>performance).webkitNow
|| (<any>performance).mozNow
|| (<any>performance).msNow
|| (<any>performance).oNow
).bind(performance);
}
} else {
this.getTimestamp = settings.getTimestamp;
}
this.eventInformation = settings.eventInformation;
this.canTrigger = settings.hasOwnProperty("canTrigger")
? <IInputWriterBooleanGetter>settings.canTrigger
: function (): boolean {
return true;
};
this.isRecording = settings.hasOwnProperty("isRecording")
? <IInputWriterBooleanGetter>settings.isRecording
: function (): boolean {
return true;
};
this.history = {};
this.histories = {
"length": 0
};
this.aliases = {};
this.addAliases(settings.aliases || {});
this.keyAliasesToCodes = settings.keyAliasesToCodes || {
"shift": 16,
"ctrl": 17,
"space": 32,
"left": 37,
"up": 38,
"right": 39,
"down": 40
};
this.keyCodesToAliases = settings.keyCodesToAliases || {
"16": "shift",
"17": "ctrl",
"32": "space",
"37": "left",
"38": "up",
"39": "right",
"40": "down"
};
}
/**
* Clears the currently tracked inputs history and resets the starting time,
* and (optionally) saves the current history.
*
* @param {Boolean} [keepHistory] Whether the currently tracked history
* of inputs should be added to the master
* Array of histories (defaults to true).
*/
restartHistory(keepHistory: boolean = true): void {
if (keepHistory) {
this.saveHistory(this.history);
}
this.history = {};
this.startingTime = this.getTimestamp();
}
/* Simple gets
*/
/**
* @return {Object} The stored mapping of aliases to values.
*/
getAliases(): any {
return this.aliases;
}
/**
* @return {Object} The stored mapping of aliases to values, with values
* mapped to their equivalent key Strings.
*/
getAliasesAsKeyStrings(): { [i: string]: any } {
var output: any = {},
alias: string;
for (alias in this.aliases) {
if (this.aliases.hasOwnProperty(alias)) {
output[alias] = this.getAliasAsKeyStrings(alias);
}
}
return output;
}
/**
* @param {Mixed} alias An alias allowed to be passed in, typically a
* character code.
* @return {String[]} The mapped key Strings corresponding to that alias,
* typically the human-readable Strings representing
* input names, such as "a" or "left".
*/
getAliasAsKeyStrings(alias: any): string[] {
return this.aliases[alias].map<string>(this.convertAliasToKeyString.bind(this));
}
/**
* @param {Mixed} alias The alias of an input, typically a character
* code.
* @return {String} The human-readable String representing the input name,
* such as "a" or "left".
*/
convertAliasToKeyString(alias: any): string {
if (alias.constructor === String) {
return alias;
}
if (alias > 96 && alias < 105) {
return String.fromCharCode(alias - 48);
}
if (alias > 64 && alias < 97) {
return String.fromCharCode(alias);
}
return typeof this.keyCodesToAliases[alias] !== "undefined"
? this.keyCodesToAliases[alias] : "?";
}
/**
* @param {Mixed} key The number code of an input.
* @return {Number} The machine-usable character code of the input.
*
*/
convertKeyStringToAlias(key: number | string): number | string {
if (key.constructor === Number) {
return key;
}
if ((<string>key).length === 1) {
return (<string>key).charCodeAt(0) - 32;
}
return typeof this.keyAliasesToCodes[<string>key] !== "undefined" ? this.keyAliasesToCodes[<string>key] : -1;
}
/**
* Get function for a single history, either the current or a past one.
*
* @param {String} [name] The identifier for the old history to return. If
* none is provided, the current history is used.
* @return {Object} A history of inputs in JSON-friendly form.
*/
getHistory(name: string = undefined): any {
return arguments.length ? this.histories[name] : history;
}
/**
* @return {Object} All previously stored histories.
*/
getHistories(): any {
return this.histories;
}
/**
* @return {Boolean} Whether this is currently allowing inputs.
*/
getCanTrigger(): IInputWriterBooleanGetter {
return this.canTrigger;
}
/**
* @return {Boolean} Whether this is currently recording allowed inputs.
*/
getIsRecording(): IInputWriterBooleanGetter {
return this.isRecording;
}
/* Simple sets
*/
/**
* @param {Mixed} canTriggerNew Whether this is now allowing inputs. This
* may be either a Function (to be evaluated
* on each input) or a general Boolean.
*/
setCanTrigger(canTriggerNew: boolean | IInputWriterBooleanGetter): void {
if (canTriggerNew.constructor === Boolean) {
this.canTrigger = function (): boolean {
return <boolean>canTriggerNew;
};
} else {
this.canTrigger = <IInputWriterBooleanGetter>canTriggerNew;
}
}
/**
* @param {Boolean} isRecordingNew Whether this is now recording allowed
* inputs.
*/
setIsRecording(isRecordingNew: boolean | IInputWriterBooleanGetter): void {
if (isRecordingNew.constructor === Boolean) {
this.isRecording = function (): boolean {
return <boolean>isRecordingNew;
};
} else {
this.isRecording = <IInputWriterBooleanGetter>isRecordingNew;
}
}
/**
* @param {Mixed} eventInformationNew A new first argument for event
* callbacks.
*/
setEventInformation(eventInformationNew: any): void {
this.eventInformation = eventInformationNew;
}
/* Aliases
*/
/**
* Adds a list of values by which an event may be triggered.
*
* @param {String} name The name of the event that is being given
* aliases, such as "left".
* @param {Array} values An array of aliases by which the event will also
* be callable.
*/
addAliasValues(name: any, values: any[]): void {
var triggerName: any,
triggerGroup: any,
i: number;
if (!this.aliases.hasOwnProperty(name)) {
this.aliases[name] = values;
} else {
this.aliases[name].push.apply(this.aliases[name], values);
}
// triggerName = "onkeydown", "onkeyup", ...
for (triggerName in this.triggers) {
if (this.triggers.hasOwnProperty(triggerName)) {
// triggerGroup = { "left": function, ... }, ...
triggerGroup = this.triggers[triggerName];
if (triggerGroup.hasOwnProperty(name)) {
// values[i] = 37, 65, ...
for (i = 0; i < values.length; i += 1) {
triggerGroup[values[i]] = triggerGroup[name];
}
}
}
}
}
/**
* Removes a list of values by which an event may be triggered.
*
* @param {String} name The name of the event that is having aliases
* removed, such as "left".
* @param {Array} values An array of aliases by which the event will no
* longer be callable.
*/
removeAliasValues(name: string, values: any[]): void {
var triggerName: any,
triggerGroup: any,
i: number;
if (!this.aliases.hasOwnProperty(name)) {
return;
}
for (i = 0; i < values.length; i += 1) {
this.aliases[name].splice(this.aliases[name].indexOf(values[i], 1));
}
// triggerName = "onkeydown", "onkeyup", ...
for (triggerName in this.triggers) {
if (this.triggers.hasOwnProperty(triggerName)) {
// triggerGroup = { "left": function, ... }, ...
triggerGroup = this.triggers[triggerName];
if (triggerGroup.hasOwnProperty(name)) {
// values[i] = 37, 65, ...
for (i = 0; i < values.length; i += 1) {
if (triggerGroup.hasOwnProperty(values[i])) {
delete triggerGroup[values[i]];
}
}
}
}
}
}
/**
* Shortcut to remove old alias values and add new ones in.
*
*
* @param {String} name The name of the event that is having aliases
* added and removed, such as "left".
* @param {Array} valuesOld An array of aliases by which the event will no
* longer be callable.
* @param {Array} valuesNew An array of aliases by which the event will
* now be callable.
*/
switchAliasValues(name: string, valuesOld: any[], valuesNew: any[]): void {
this.removeAliasValues(name, valuesOld);
this.addAliasValues(name, valuesNew);
}
/**
* Adds a set of alises from an Object containing "name" => [values] pairs.
*
* @param {Object} aliasesRaw
*/
addAliases(aliasesRaw: any): void {
var aliasName: string;
for (aliasName in aliasesRaw) {
if (aliasesRaw.hasOwnProperty(aliasName)) {
this.addAliasValues(aliasName, aliasesRaw[aliasName]);
}
}
}
/* Functions
*/
/**
* Adds a triggerable event by marking a new callback under the trigger's
* triggers. Any aliases for the label are also given the callback.
*
* @param {String} trigger The name of the triggered event.
* @param {Mixed} label The code within the trigger to call within,
* typically either a character code or an alias.
* @param {Function} callback The callback Function to be triggered.
*/
addEvent(trigger: string, label: any, callback: IInputWritrTriggerCallback): void {
var i: number;
if (!this.triggers.hasOwnProperty(trigger)) {
throw new Error("Unknown trigger requested: '" + trigger + "'.");
}
this.triggers[trigger][label] = callback;
if (this.aliases.hasOwnProperty(label)) {
for (i = 0; i < this.aliases[label].length; i += 1) {
this.triggers[trigger][this.aliases[label][i]] = callback;
}
}
}
/**
* Removes a triggerable event by deleting any callbacks under the trigger's
* triggers. Any aliases for the label are also given the callback.
*
* @param {String} trigger The name of the triggered event.
* @param {Mixed} label The code within the trigger to call within,
* typically either a character code or an alias.
*/
removeEvent(trigger: string, label: any): void {
var i: number;
if (!this.triggers.hasOwnProperty(trigger)) {
throw new Error("Unknown trigger requested: '" + trigger + "'.");
}
delete this.triggers[trigger][label];
if (this.aliases.hasOwnProperty(label)) {
for (i = 0; i < this.aliases[label].length; i += 1) {
if (this.triggers[trigger][this.aliases[label][i]]) {
delete this.triggers[trigger][this.aliases[label][i]];
}
}
}
}
/**
* Stores the current history in the histories listing. this.restartHistory
* is typically called directly after.
*/
saveHistory(name: string = undefined): void {
this.histories[this.histories.length] = history;
this.histories.length += 1;
if (arguments.length) {
this.histories[name] = history;
}
}
/**
* Plays back the current history using this.playEvents.
*/
playHistory(): void {
this.playEvents(this.history);
}
/**
* "Plays" back an Array of event information by simulating each keystroke
* in a new call, timed by setTimeout.
*
* @param {Object} events The events history to play back.
* @remarks This will execute the same actions in the same order as before,
* but the arguments object may be different.
*/
playEvents(events: any): void {
var timeouts: any = {},
time: string,
call: Function;
for (time in events) {
if (events.hasOwnProperty(time)) {
call = this.makeEventCall(events[time]);
timeouts[time] = setTimeout(call, (Number(time) - this.startingTime) | 0);
}
}
}
/**
* Primary driver function to run an event. The event is chosen from the
* triggers object, and called with eventInformation as the input.
*
* @param {Function, String} event The event function (or string alias of
* it) that will be called.
* @param {Mixed} [keyCode] The alias of the event function under
* triggers[event], if event is a String.
* @param {Event} [sourceEvent] The raw event that caused the calling Pipe
* to be triggered, such as a MouseEvent.
* @return {Mixed}
*/
callEvent(event: Function | string, keyCode?: number | string, sourceEvent?: Event): any {
if (!this.canTrigger(event, keyCode)) {
return;
}
if (!event) {
throw new Error("Blank event given, ignoring it.");
}
if (event.constructor === String) {
event = this.triggers[<string>event][<string>keyCode];
}
return (<any>event)(this.eventInformation, sourceEvent);
}
/**
* Creates and returns a function to run a trigger.
*
* @param {String} trigger The label for the Array of functions that the
* pipe function should choose from.
* @param {String} codeLabel A mapping String for the alias to get the
* alias from the event.
* @param {Boolean} [preventDefaults] Whether the input to the pipe
* function will be an HTML-style
* event, where .preventDefault()
* should be clicked.
* @return {Function}
*/
makePipe(trigger: string, codeLabel: string, preventDefaults?: boolean): Function {
var functions: any = this.triggers[trigger],
InputWriter: InputWritr = this;
if (!functions) {
throw new Error("No trigger of label '" + trigger + "' defined.");
}
return function Pipe(event: Event): void {
var alias: any = event[codeLabel];
// Typical usage means alias will be an event from a key/mouse input
if (preventDefaults && event.preventDefault instanceof Function) {
event.preventDefault();
}
// If there's a function under that alias, run it
if (functions.hasOwnProperty(alias)) {
if (InputWriter.isRecording()) {
InputWriter.history[InputWriter.getTimestamp() | 0] = [trigger, alias];
}
InputWriter.callEvent(functions[alias], <number>alias, event);
}
};
}
/**
* Curry utility to create a closure that runs call() when called.
*
* @param {Array} info An array containing [alias, keyCode].
* @return {Function} A closure Function that activates a trigger
* when called.
*/
private makeEventCall(info: any[]): Function {
return function (): void {
this.callEvent(info[0], info[1]);
};
}
}
}