1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2025-06-09 02:54:01 +00:00
tobspr_shapez.io/src/js/game/key_action_mapper.js
tobspr c41aaa1fc5
Mod Support - 1.5.0 Update (#1361)
* initial modloader draft

* modloader features

* Refactor mods to use signals

* Add support for modifying and registering new transltions

* Minor adjustments

* Support for string building ids for mods

* Initial support for adding new buildings

* Refactor how mods are loaded to resolve circular dependencies and prepare for future mod loading

* Lazy Load mods to make sure all dependencies are loaded

* Expose all exported members automatically to mods

* Fix duplicate exports

* Allow loading mods from standalone

* update changelog

* Fix mods folder incorrect path

* Fix modloading in standalone

* Fix sprites not getting replaced, update demo mod

* Load dev mod via raw loader

* Improve mod developing so mods are directly ready to be deployed, load mods from local file server

* Proper mods ui

* Allow mods to register game systems and draw stuff

* Change mods path

* Fix sprites not loading

* Minor adjustments, closes #1333

* Add support for loading atlases via mods

* Add support for loading mods from external sources in DEV

* Add confirmation when loading mods

* Fix circular dependency

* Minor Keybindings refactor, add support for keybindings to mods, add support for dialogs to mods

* Add some mod signals

* refactor game loading states

* Make shapez exports global

* Start to make mods safer

* Refactor file system electron event handling

* Properly isolate electron renderer process

* Update to latest electron

* Show errors when loading mods

* Update confirm dialgo

* Minor restructure, start to add mod examples

* Allow adding custom themesw

* Add more examples and allow defining custom item processor operations

* Add interface to register new buildings

* Fixed typescript type errors (#1335)

* Refactor building registry, make it easier for mods to add new buildings

* Allow overriding existing methods

* Add more examples and more features

* More mod examples

* Make mod loading simpler

* Add example how to add custom drawings

* Remove unused code

* Minor modloader adjustments

* Support for rotation variants in mods (was broken previously)

* Allow mods to replace builtin sub shapes

* Add helper methods to extend classes

* Fix menu bar on mac os

* Remember window state

* Add support for paste signals

* Add example how to add custom components and systems

* Support for mod settings

* Add example for adding a new item type

* Update class extensions

* Minor adjustments

* Fix typo

* Add notification blocks mod example

* Add small tutorial

* Update readme

* Add better instructions

* Update JSDoc for Replacing Methods (#1336)

* upgraded types for overriding methods

* updated comments

Co-authored-by: Edward Badel <you@example.com>

* Direction lock now indicates when there is a building inbetween

* Fix mod examples

* Fix linter error

* Game state register (#1341)

* Added a gamestate register helper

Added a gamestate register helper

* Update mod_interface.js

* export build options

* Fix runBeforeMethod and runAfterMethod

* Minor game system code cleanup

* Belt path drawing optimization

* Fix belt path optimization

* Belt drawing improvements, again

* Do not render belts in statics disabled view

* Allow external URL to load more than one mod (#1337)

* Allow external URL to load more than one mod

Instead of loading the text returned from the remote server, load a JSON object with a `mods` field, containing strings of all the mods. This lets us work on more than one mod at a time or without separate repos. This will break tooling such as `create-shapezio-mod` though.

* Update modloader.js

* Prettier fixes

* Added link to create-shapezio-mod npm page (#1339)

Added link to create-shapezio-mod npm page: https://www.npmjs.com/package/create-shapezio-mod

* allow command line switch to load more than one mod (#1342)

* Fixed class handle type (#1345)

* Fixed class handle type

* Fixed import game state

* Minor adjustments

* Refactor item acceptor to allow only single direction slots

* Allow specifying minimumGameVersion

* Add sandbox example

* Replaced concatenated strings with template literals (#1347)

* Mod improvements

* Make wired pins component optional on the storage

* Fix mod examples

* Bind `this` for method overriding JSDoc (#1352)

* fix entity debugger reaching HTML elements (#1353)

* Store mods in savegame and show warning when it differs

* Closes #1357

* Fix All Shapez Exports Being Const (#1358)

* Allowed setting of variables inside webpack modules

* remove console log

* Fix stringification of things inside of eval

Co-authored-by: Edward Badel <you@example.com>

* Fix building placer intersection warning

* Add example for storing data in the savegame

* Fix double painter bug (#1349)

* Add example on how to extend builtin buildings

* update readme

* Disable steam achievements when playing with mods

* Update translations

Co-authored-by: Thomas (DJ1TJOO) <44841260+DJ1TJOO@users.noreply.github.com>
Co-authored-by: Bagel03 <70449196+Bagel03@users.noreply.github.com>
Co-authored-by: Edward Badel <you@example.com>
Co-authored-by: Emerald Block <69981203+EmeraldBlock@users.noreply.github.com>
Co-authored-by: saile515 <63782477+saile515@users.noreply.github.com>
Co-authored-by: Sense101 <67970865+Sense101@users.noreply.github.com>
2022-02-01 16:35:49 +01:00

568 lines
16 KiB
JavaScript

/* typehints:start */
import { GameRoot } from "./root";
import { InputReceiver } from "../core/input_receiver";
import { Application } from "../application";
/* typehints:end */
import { Signal, STOP_PROPAGATION } from "../core/signal";
import { IS_MOBILE } from "../core/config";
import { T } from "../translations";
export function keyToKeyCode(str) {
return str.toUpperCase().charCodeAt(0);
}
export const KEYCODES = {
Tab: 9,
Enter: 13,
Shift: 16,
Ctrl: 17,
Alt: 18,
Escape: 27,
Space: 32,
ArrowLeft: 37,
ArrowUp: 38,
ArrowRight: 39,
ArrowDown: 40,
Delete: 46,
F1: 112,
F2: 113,
F3: 114,
F4: 115,
F5: 116,
F6: 117,
F7: 118,
F8: 119,
F9: 120,
F10: 121,
F11: 122,
F12: 123,
Plus: 187,
Minus: 189,
};
export const KEYMAPPINGS = {
// Make sure mods come first so they can override everything
mods: {},
general: {
confirm: { keyCode: KEYCODES.Enter },
back: { keyCode: KEYCODES.Escape, builtin: true },
},
ingame: {
menuOpenShop: { keyCode: keyToKeyCode("F") },
menuOpenStats: { keyCode: keyToKeyCode("G") },
menuClose: { keyCode: keyToKeyCode("Q") },
toggleHud: { keyCode: KEYCODES.F2 },
exportScreenshot: { keyCode: KEYCODES.F3 },
toggleFPSInfo: { keyCode: KEYCODES.F4 },
switchLayers: { keyCode: keyToKeyCode("E") },
showShapeTooltip: { keyCode: KEYCODES.Alt },
},
navigation: {
mapMoveUp: { keyCode: keyToKeyCode("W") },
mapMoveRight: { keyCode: keyToKeyCode("D") },
mapMoveDown: { keyCode: keyToKeyCode("S") },
mapMoveLeft: { keyCode: keyToKeyCode("A") },
mapMoveFaster: { keyCode: KEYCODES.Shift },
centerMap: { keyCode: KEYCODES.Space },
mapZoomIn: { keyCode: KEYCODES.Plus, repeated: true },
mapZoomOut: { keyCode: KEYCODES.Minus, repeated: true },
createMarker: { keyCode: keyToKeyCode("M") },
},
buildings: {
// Puzzle buildings
constant_producer: { keyCode: keyToKeyCode("H") },
goal_acceptor: { keyCode: keyToKeyCode("N") },
block: { keyCode: keyToKeyCode("4") },
// Primary Toolbar
belt: { keyCode: keyToKeyCode("1") },
balancer: { keyCode: keyToKeyCode("2") },
underground_belt: { keyCode: keyToKeyCode("3") },
miner: { keyCode: keyToKeyCode("4") },
cutter: { keyCode: keyToKeyCode("5") },
rotater: { keyCode: keyToKeyCode("6") },
stacker: { keyCode: keyToKeyCode("7") },
mixer: { keyCode: keyToKeyCode("8") },
painter: { keyCode: keyToKeyCode("9") },
trash: { keyCode: keyToKeyCode("0") },
// Sandbox
item_producer: { keyCode: keyToKeyCode("L") },
// Secondary toolbar
storage: { keyCode: keyToKeyCode("Y") },
reader: { keyCode: keyToKeyCode("U") },
lever: { keyCode: keyToKeyCode("I") },
filter: { keyCode: keyToKeyCode("O") },
display: { keyCode: keyToKeyCode("P") },
// Wires toolbar
wire: { keyCode: keyToKeyCode("1") },
wire_tunnel: { keyCode: keyToKeyCode("2") },
constant_signal: { keyCode: keyToKeyCode("3") },
logic_gate: { keyCode: keyToKeyCode("4") },
virtual_processor: { keyCode: keyToKeyCode("5") },
analyzer: { keyCode: keyToKeyCode("6") },
comparator: { keyCode: keyToKeyCode("7") },
transistor: { keyCode: keyToKeyCode("8") },
},
placement: {
pipette: { keyCode: keyToKeyCode("Q") },
rotateWhilePlacing: { keyCode: keyToKeyCode("R") },
rotateInverseModifier: { keyCode: KEYCODES.Shift },
rotateToUp: { keyCode: KEYCODES.ArrowUp },
rotateToDown: { keyCode: KEYCODES.ArrowDown },
rotateToRight: { keyCode: KEYCODES.ArrowRight },
rotateToLeft: { keyCode: KEYCODES.ArrowLeft },
cycleBuildingVariants: { keyCode: keyToKeyCode("T") },
cycleBuildings: { keyCode: KEYCODES.Tab },
switchDirectionLockSide: { keyCode: keyToKeyCode("R") },
copyWireValue: { keyCode: keyToKeyCode("Z") },
},
massSelect: {
massSelectStart: { keyCode: KEYCODES.Ctrl },
massSelectSelectMultiple: { keyCode: KEYCODES.Shift },
massSelectCopy: { keyCode: keyToKeyCode("C") },
massSelectCut: { keyCode: keyToKeyCode("X") },
massSelectClear: { keyCode: keyToKeyCode("B") },
confirmMassDelete: { keyCode: KEYCODES.Delete },
pasteLastBlueprint: { keyCode: keyToKeyCode("V") },
},
placementModifiers: {
lockBeltDirection: { keyCode: KEYCODES.Shift },
placementDisableAutoOrientation: { keyCode: KEYCODES.Ctrl },
placeMultiple: { keyCode: KEYCODES.Shift },
placeInverse: { keyCode: KEYCODES.Alt },
},
};
// Assign ids
for (const categoryId in KEYMAPPINGS) {
for (const mappingId in KEYMAPPINGS[categoryId]) {
KEYMAPPINGS[categoryId][mappingId].id = mappingId;
}
}
export const KEYCODE_LMB = 1;
export const KEYCODE_MMB = 2;
export const KEYCODE_RMB = 3;
/**
* Returns a keycode -> string
* @param {number} code
* @returns {string}
*/
export function getStringForKeyCode(code) {
// @todo: Refactor into dictionary
switch (code) {
case KEYCODE_LMB:
return "LMB";
case KEYCODE_MMB:
return "MMB";
case KEYCODE_RMB:
return "RMB";
case 4:
return "MB4";
case 5:
return "MB5";
case 8:
return "⌫";
case KEYCODES.Tab:
return T.global.keys.tab;
case KEYCODES.Enter:
return "⏎";
case KEYCODES.Shift:
return "⇪";
case KEYCODES.Ctrl:
return T.global.keys.control;
case KEYCODES.Alt:
return T.global.keys.alt;
case 19:
return "PAUSE";
case 20:
return "CAPS";
case KEYCODES.Escape:
return T.global.keys.escape;
case KEYCODES.Space:
return T.global.keys.space;
case 33:
return "PGUP";
case 34:
return "PGDOWN";
case 35:
return "END";
case 36:
return "HOME";
case KEYCODES.ArrowLeft:
return "⬅";
case KEYCODES.ArrowUp:
return "⬆";
case KEYCODES.ArrowRight:
return "➡";
case KEYCODES.ArrowDown:
return "⬇";
case 44:
return "PRNT";
case 45:
return "INS";
case 46:
return "DEL";
case 93:
return "SEL";
case 96:
return "NUM 0";
case 97:
return "NUM 1";
case 98:
return "NUM 2";
case 99:
return "NUM 3";
case 100:
return "NUM 4";
case 101:
return "NUM 5";
case 102:
return "NUM 6";
case 103:
return "NUM 7";
case 104:
return "NUM 8";
case 105:
return "NUM 9";
case 106:
return "*";
case 107:
return "+";
case 109:
return "-";
case 110:
return ".";
case 111:
return "/";
case KEYCODES.F1:
return "F1";
case KEYCODES.F2:
return "F2";
case KEYCODES.F3:
return "F3";
case KEYCODES.F4:
return "F4";
case KEYCODES.F5:
return "F5";
case KEYCODES.F6:
return "F6";
case KEYCODES.F7:
return "F7";
case KEYCODES.F8:
return "F8";
case KEYCODES.F9:
return "F9";
case KEYCODES.F10:
return "F10";
case KEYCODES.F11:
return "F11";
case KEYCODES.F12:
return "F12";
case 144:
return "NUMLOCK";
case 145:
return "SCRLOCK";
case 182:
return "COMP";
case 183:
return "CALC";
case 186:
return ";";
case 187:
return "+";
case 188:
return ",";
case 189:
return "-";
case 190:
return ".";
case 191:
return "/";
case 192:
return "`";
case 219:
return "[";
case 220:
return "\\";
case 221:
return "]";
case 222:
return "'";
}
return (48 <= code && code <= 57) || (65 <= code && code <= 90)
? String.fromCharCode(code)
: "[" + code + "]";
}
export class Keybinding {
/**
*
* @param {KeyActionMapper} keyMapper
* @param {Application} app
* @param {object} param0
* @param {number} param0.keyCode
* @param {boolean=} param0.builtin
* @param {boolean=} param0.repeated
* @param {{ shift?: boolean; alt?: boolean; ctrl?: boolean; }=} param0.modifiers
*/
constructor(keyMapper, app, { keyCode, builtin = false, repeated = false, modifiers = {} }) {
assert(keyCode && Number.isInteger(keyCode), "Invalid key code: " + keyCode);
this.keyMapper = keyMapper;
this.app = app;
this.keyCode = keyCode;
this.builtin = builtin;
this.repeated = repeated;
this.modifiers = modifiers;
this.signal = new Signal();
this.toggled = new Signal();
}
/**
* Returns whether this binding is currently pressed
* @returns {boolean}
*/
get pressed() {
// Check if the key is down
if (this.app.inputMgr.keysDown.has(this.keyCode)) {
// Check if it is the top reciever
const reciever = this.keyMapper.inputReceiver;
return this.app.inputMgr.getTopReciever() === reciever;
}
return false;
}
/**
* Adds an event listener
* @param {function() : void} receiver
* @param {object=} scope
*/
add(receiver, scope = null) {
this.signal.add(receiver, scope);
}
/**
* Adds an event listener
* @param {function() : void} receiver
* @param {object=} scope
*/
addToTop(receiver, scope = null) {
this.signal.addToTop(receiver, scope);
}
/**
* @param {Element} elem
* @returns {HTMLElement} the created element, or null if the keybindings are not shown
* */
appendLabelToElement(elem) {
if (IS_MOBILE) {
return null;
}
const spacer = document.createElement("code");
spacer.classList.add("keybinding");
spacer.innerHTML = getStringForKeyCode(this.keyCode);
elem.appendChild(spacer);
return spacer;
}
/**
* Returns the key code as a nice string
*/
getKeyCodeString() {
return getStringForKeyCode(this.keyCode);
}
/**
* Remvoes all signal receivers
*/
clearSignalReceivers() {
this.signal.removeAll();
}
}
export class KeyActionMapper {
/**
*
* @param {GameRoot} root
* @param {InputReceiver} inputReciever
*/
constructor(root, inputReciever) {
this.root = root;
this.inputReceiver = inputReciever;
inputReciever.keydown.add(this.handleKeydown, this);
inputReciever.keyup.add(this.handleKeyup, this);
/** @type {Object.<string, Keybinding>} */
this.keybindings = {};
const overrides = root.app.settings.getKeybindingOverrides();
for (const category in KEYMAPPINGS) {
for (const key in KEYMAPPINGS[category]) {
let payload = Object.assign({}, KEYMAPPINGS[category][key]);
if (overrides[key]) {
payload.keyCode = overrides[key];
}
this.keybindings[key] = new Keybinding(this, this.root.app, payload);
if (G_IS_DEV) {
// Sanity
if (!T.keybindings.mappings[key]) {
assertAlways(false, "Keybinding " + key + " has no translation!");
}
}
}
}
inputReciever.pageBlur.add(this.onPageBlur, this);
inputReciever.destroyed.add(this.cleanup, this);
}
/**
* Returns all keybindings starting with the given id
* @param {string} pattern
* @returns {Array<Keybinding>}
*/
getKeybindingsStartingWith(pattern) {
let result = [];
for (const key in this.keybindings) {
if (key.startsWith(pattern)) {
result.push(this.keybindings[key]);
}
}
return result;
}
/**
* Forwards the given events to the other mapper (used in tooltips)
* @param {KeyActionMapper} receiver
* @param {Array<string>} bindings
*/
forward(receiver, bindings) {
for (let i = 0; i < bindings.length; ++i) {
const key = bindings[i];
this.keybindings[key].signal.add((...args) => receiver.keybindings[key].signal.dispatch(...args));
}
}
cleanup() {
for (const key in this.keybindings) {
this.keybindings[key].signal.removeAll();
}
}
onPageBlur() {
// Reset all down states
// Find mapping
for (const key in this.keybindings) {
/** @type {Keybinding} */
const binding = this.keybindings[key];
}
}
/**
* Internal keydown handler
* @param {object} param0
* @param {number} param0.keyCode
* @param {boolean} param0.shift
* @param {boolean} param0.alt
* @param {boolean} param0.ctrl
* @param {boolean=} param0.initial
*/
handleKeydown({ keyCode, shift, alt, ctrl, initial }) {
let stop = false;
// Find mapping
for (const key in this.keybindings) {
/** @type {Keybinding} */
const binding = this.keybindings[key];
if (binding.keyCode === keyCode && (initial || binding.repeated)) {
if (binding.modifiers.shift && !shift) {
continue;
}
if (binding.modifiers.ctrl && !ctrl) {
continue;
}
if (binding.modifiers.alt && !alt) {
continue;
}
/** @type {Signal} */
const signal = this.keybindings[key].signal;
if (signal.dispatch() === STOP_PROPAGATION) {
return;
}
}
}
if (stop) {
return STOP_PROPAGATION;
}
}
/**
* Internal keyup handler
* @param {object} param0
* @param {number} param0.keyCode
* @param {boolean} param0.shift
* @param {boolean} param0.alt
*/
handleKeyup({ keyCode, shift, alt }) {
// Empty
}
/**
* Returns a given keybinding
* @param {{ keyCode: number }} binding
* @returns {Keybinding}
*/
getBinding(binding) {
// @ts-ignore
const id = binding.id;
assert(id, "Not a valid keybinding: " + JSON.stringify(binding));
assert(this.keybindings[id], "Keybinding " + id + " not known!");
return this.keybindings[id];
}
/**
* Returns a given keybinding
* @param {string} id
* @returns {Keybinding}
*/
getBindingById(id) {
assert(this.keybindings[id], "Keybinding " + id + " not known!");
return this.keybindings[id];
}
}