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

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

This commit is contained in:
tobspr 2022-01-14 19:01:38 +01:00
parent 3cfd4aaebd
commit 8a60acc6e3
13 changed files with 286 additions and 117 deletions

View File

@ -89,10 +89,10 @@
@include S(border-radius, $globalBorderRadius);
background: $accentColorBright;
@include S(margin-bottom, 4px);
@include S(padding, 7px);
@include S(grid-gap, 5px);
@include S(padding, 7px, 10px);
@include S(grid-gap, 15px);
display: grid;
grid-template-columns: 1fr D(100px) D(100px) D(100px);
grid-template-columns: 1fr D(100px) D(80px) D(50px);
.checkbox {
align-self: center;
@ -120,6 +120,7 @@
.author {
display: flex;
flex-direction: column;
align-self: center;
strong {
text-transform: uppercase;
color: $accentColorDark;

View File

@ -105,10 +105,6 @@
}
}
button.privacy {
@include S(margin-top, 4px);
}
.versionbar {
@include S(margin-top, 10px);

View File

@ -211,6 +211,7 @@ export class InputDistributor {
keyCode: keyCode,
shift: event.shiftKey,
alt: event.altKey,
ctrl: event.ctrlKey,
initial: isInitial,
event,
}) === STOP_PROPAGATION

View File

@ -90,8 +90,9 @@ export class Dialog {
* @param {number} param0.keyCode
* @param {boolean} param0.shift
* @param {boolean} param0.alt
* @param {boolean} param0.ctrl
*/
handleKeydown({ keyCode, shift, alt }) {
handleKeydown({ keyCode, shift, alt, ctrl }) {
if (keyCode === kbEnter && this.enterHandler) {
this.internalButtonHandler(this.enterHandler);
return STOP_PROPAGATION;

View File

@ -7,118 +7,152 @@ import { Application } from "../application";
import { Signal, STOP_PROPAGATION } from "../core/signal";
import { IS_MOBILE } from "../core/config";
import { T } from "../translations";
function key(str) {
export function keyToKeyCode(str) {
return str.toUpperCase().charCodeAt(0);
}
const KEYCODE_UP_ARROW = 38;
const KEYCODE_DOWN_ARROW = 40;
const KEYCODE_LEFT_ARROW = 37;
const KEYCODE_RIGHT_ARROW = 39;
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: 13 }, // enter
back: { keyCode: 27, builtin: true }, // escape
confirm: { keyCode: KEYCODES.Enter },
back: { keyCode: KEYCODES.Escape, builtin: true },
},
ingame: {
menuOpenShop: { keyCode: key("F") },
menuOpenStats: { keyCode: key("G") },
menuClose: { keyCode: key("Q") },
menuOpenShop: { keyCode: keyToKeyCode("F") },
menuOpenStats: { keyCode: keyToKeyCode("G") },
menuClose: { keyCode: keyToKeyCode("Q") },
toggleHud: { keyCode: 113 }, // F2
exportScreenshot: { keyCode: 114 }, // F3PS
toggleFPSInfo: { keyCode: 115 }, // F4
toggleHud: { keyCode: KEYCODES.F2 },
exportScreenshot: { keyCode: KEYCODES.F3 },
toggleFPSInfo: { keyCode: KEYCODES.F4 },
switchLayers: { keyCode: key("E") },
switchLayers: { keyCode: keyToKeyCode("E") },
showShapeTooltip: { keyCode: 18 }, // ALT
showShapeTooltip: { keyCode: KEYCODES.Alt },
},
navigation: {
mapMoveUp: { keyCode: key("W") },
mapMoveRight: { keyCode: key("D") },
mapMoveDown: { keyCode: key("S") },
mapMoveLeft: { keyCode: key("A") },
mapMoveFaster: { keyCode: 16 }, //shift
mapMoveUp: { keyCode: keyToKeyCode("W") },
mapMoveRight: { keyCode: keyToKeyCode("D") },
mapMoveDown: { keyCode: keyToKeyCode("S") },
mapMoveLeft: { keyCode: keyToKeyCode("A") },
mapMoveFaster: { keyCode: KEYCODES.Shift },
centerMap: { keyCode: 32 }, // SPACE
mapZoomIn: { keyCode: 187, repeated: true }, // "+"
mapZoomOut: { keyCode: 189, repeated: true }, // "-"
createMarker: { keyCode: key("M") },
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: key("H") },
goal_acceptor: { keyCode: key("N") },
block: { keyCode: key("4") },
constant_producer: { keyCode: keyToKeyCode("H") },
goal_acceptor: { keyCode: keyToKeyCode("N") },
block: { keyCode: keyToKeyCode("4") },
// Primary Toolbar
belt: { keyCode: key("1") },
balancer: { keyCode: key("2") },
underground_belt: { keyCode: key("3") },
miner: { keyCode: key("4") },
cutter: { keyCode: key("5") },
rotater: { keyCode: key("6") },
stacker: { keyCode: key("7") },
mixer: { keyCode: key("8") },
painter: { keyCode: key("9") },
trash: { keyCode: key("0") },
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: key("L") },
item_producer: { keyCode: keyToKeyCode("L") },
// Secondary toolbar
storage: { keyCode: key("Y") },
reader: { keyCode: key("U") },
lever: { keyCode: key("I") },
filter: { keyCode: key("O") },
display: { keyCode: key("P") },
storage: { keyCode: keyToKeyCode("Y") },
reader: { keyCode: keyToKeyCode("U") },
lever: { keyCode: keyToKeyCode("I") },
filter: { keyCode: keyToKeyCode("O") },
display: { keyCode: keyToKeyCode("P") },
// Wires toolbar
wire: { keyCode: key("1") },
wire_tunnel: { keyCode: key("2") },
constant_signal: { keyCode: key("3") },
logic_gate: { keyCode: key("4") },
virtual_processor: { keyCode: key("5") },
analyzer: { keyCode: key("6") },
comparator: { keyCode: key("7") },
transistor: { keyCode: key("8") },
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: key("Q") },
rotateWhilePlacing: { keyCode: key("R") },
rotateInverseModifier: { keyCode: 16 }, // SHIFT
rotateToUp: { keyCode: KEYCODE_UP_ARROW },
rotateToDown: { keyCode: KEYCODE_DOWN_ARROW },
rotateToRight: { keyCode: KEYCODE_RIGHT_ARROW },
rotateToLeft: { keyCode: KEYCODE_LEFT_ARROW },
cycleBuildingVariants: { keyCode: key("T") },
cycleBuildings: { keyCode: 9 }, // TAB
switchDirectionLockSide: { keyCode: key("R") },
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: key("Z") },
copyWireValue: { keyCode: keyToKeyCode("Z") },
},
massSelect: {
massSelectStart: { keyCode: 17 }, // CTRL
massSelectSelectMultiple: { keyCode: 16 }, // SHIFT
massSelectCopy: { keyCode: key("C") },
massSelectCut: { keyCode: key("X") },
massSelectClear: { keyCode: key("B") },
confirmMassDelete: { keyCode: 46 }, // DEL
pasteLastBlueprint: { keyCode: key("V") },
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: 16 }, // SHIFT
placementDisableAutoOrientation: { keyCode: 17 }, // CTRL
placeMultiple: { keyCode: 16 }, // SHIFT
placeInverse: { keyCode: 18 }, // ALT
lockBeltDirection: { keyCode: KEYCODES.Shift },
placementDisableAutoOrientation: { keyCode: KEYCODES.Ctrl },
placeMultiple: { keyCode: KEYCODES.Shift },
placeInverse: { keyCode: KEYCODES.Alt },
},
};
@ -153,23 +187,23 @@ export function getStringForKeyCode(code) {
return "MB5";
case 8:
return "⌫";
case 9:
case KEYCODES.Tab:
return T.global.keys.tab;
case 13:
case KEYCODES.Enter:
return "⏎";
case 16:
case KEYCODES.Shift:
return "⇪";
case 17:
case KEYCODES.Ctrl:
return T.global.keys.control;
case 18:
case KEYCODES.Alt:
return T.global.keys.alt;
case 19:
return "PAUSE";
case 20:
return "CAPS";
case 27:
case KEYCODES.Escape:
return T.global.keys.escape;
case 32:
case KEYCODES.Space:
return T.global.keys.space;
case 33:
return "PGUP";
@ -179,13 +213,13 @@ export function getStringForKeyCode(code) {
return "END";
case 36:
return "HOME";
case KEYCODE_LEFT_ARROW:
case KEYCODES.ArrowLeft:
return "⬅";
case KEYCODE_UP_ARROW:
case KEYCODES.ArrowUp:
return "⬆";
case KEYCODE_RIGHT_ARROW:
case KEYCODES.ArrowRight:
return "➡";
case KEYCODE_DOWN_ARROW:
case KEYCODES.ArrowDown:
return "⬇";
case 44:
return "PRNT";
@ -225,29 +259,29 @@ export function getStringForKeyCode(code) {
return ".";
case 111:
return "/";
case 112:
case KEYCODES.F1:
return "F1";
case 113:
case KEYCODES.F2:
return "F2";
case 114:
case KEYCODES.F3:
return "F3";
case 115:
case KEYCODES.F4:
return "F4";
case 116:
case KEYCODES.F5:
return "F5";
case 117:
case KEYCODES.F6:
return "F6";
case 118:
case KEYCODES.F7:
return "F7";
case 119:
case KEYCODES.F8:
return "F8";
case 120:
case KEYCODES.F9:
return "F9";
case 121:
case KEYCODES.F10:
return "F10";
case 122:
case KEYCODES.F11:
return "F11";
case 123:
case KEYCODES.F12:
return "F12";
case 144:
@ -296,8 +330,9 @@ export class Keybinding {
* @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 }) {
constructor(keyMapper, app, { keyCode, builtin = false, repeated = false, modifiers = {} }) {
assert(keyCode && Number.isInteger(keyCode), "Invalid key code: " + keyCode);
this.keyMapper = keyMapper;
this.app = app;
@ -305,6 +340,8 @@ export class Keybinding {
this.builtin = builtin;
this.repeated = repeated;
this.modifiers = modifiers;
this.signal = new Signal();
this.toggled = new Signal();
}
@ -395,7 +432,6 @@ export class KeyActionMapper {
if (overrides[key]) {
payload.keyCode = overrides[key];
}
this.keybindings[key] = new Keybinding(this, this.root.app, payload);
if (G_IS_DEV) {
@ -459,9 +495,10 @@ export class KeyActionMapper {
* @param {number} param0.keyCode
* @param {boolean} param0.shift
* @param {boolean} param0.alt
* @param {boolean} param0.ctrl
* @param {boolean=} param0.initial
*/
handleKeydown({ keyCode, shift, alt, initial }) {
handleKeydown({ keyCode, shift, alt, ctrl, initial }) {
let stop = false;
// Find mapping
@ -469,6 +506,18 @@ export class KeyActionMapper {
/** @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) {
@ -505,4 +554,14 @@ export class KeyActionMapper {
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];
}
}

View File

@ -194,6 +194,20 @@ registerMod(shapez => {
element.primaryBuildings.push(MetaDemoModBuilding);
}
});
// Register keybinding
this.modInterface.registerIngameKeybinding({
id: "demo_mod_binding",
keyCode: shapez.keyToKeyCode("F"),
translation: "mymod: Do something (always with SHIFT)",
modifiers: {
shift: true,
},
handler: root => {
this.dialogs.showInfo("Mod Message", "It worked!");
return shapez.STOP_PROPAGATION;
},
});
}
};
});

View File

@ -29,4 +29,8 @@ export class Mod {
}
init() {}
get dialogs() {
return this.modInterface.dialogs;
}
}

View File

@ -6,7 +6,6 @@ import { MetaBuilding } from "../game/meta_building";
/* typehints:end */
import { defaultBuildingVariant } from "../game/meta_building";
import { createLogger } from "../core/logging";
import { AtlasSprite, SpriteAtlasLink } from "../core/sprites";
import {
enumShortcodeToSubShape,
@ -22,8 +21,8 @@ import { gComponentRegistry, gMetaBuildingRegistry } from "../core/global_regist
import { MODS_ADDITIONAL_SHAPE_MAP_WEIGHTS } from "../game/map_chunk";
import { MODS_ADDITIONAL_SYSTEMS } from "../game/game_system_manager";
import { MOD_CHUNK_DRAW_HOOKS } from "../game/map_chunk_view";
const LOG = createLogger("mod-interface");
import { KEYMAPPINGS } from "../game/key_action_mapper";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
export class ModInterface {
/**
@ -32,9 +31,6 @@ export class ModInterface {
*/
constructor(modLoader) {
this.modLoader = modLoader;
/** @type {Map<string, AtlasSprite>} */
this.lazySprites = new Map();
}
registerCss(cssString) {
@ -249,6 +245,66 @@ export class ModInterface {
}
}
/**
*
* @param {Object} param0
* @param {string} param0.id
* @param {number} param0.keyCode
* @param {string} param0.translation
* @param {boolean=} param0.repeated
* @param {((GameRoot) => void)=} param0.handler
* @param {{shift?: boolean; alt?: boolean; ctrl?: boolean}=} param0.modifiers
* @param {boolean=} param0.builtin
*/
registerIngameKeybinding({
id,
keyCode,
translation,
modifiers = {},
repeated = false,
builtin = false,
handler = null,
}) {
if (!KEYMAPPINGS.mods) {
KEYMAPPINGS.mods = {};
}
const binding = (KEYMAPPINGS.mods[id] = {
keyCode,
id,
repeated,
modifiers,
builtin,
});
this.registerTranslations("en", {
keybindings: {
mappings: {
[id]: translation,
},
},
});
if (handler) {
this.modLoader.signals.gameStarted.add(root => {
root.keyMapper.getBindingById(id).addToTop(handler.bind(null, root));
});
}
return binding;
}
/**
* @returns {HUDModalDialogs}
*/
get dialogs() {
const state = this.modLoader.app.stateMgr.currentState;
// @ts-ignore
if (state.dialogs) {
// @ts-ignore
return state.dialogs;
}
throw new Error("Tried to access dialogs but current state doesn't support it");
}
setBuildingToolbarIcon(buildingId, iconBase64) {
this.registerCss(`
[data-icon="building_icons/${buildingId}.png"] .icon {

View File

@ -1,8 +1,10 @@
import { Signal } from "../core/signal";
/* typehints:start */
import { BaseHUDPart } from "../game/hud/base_hud_part";
import { GameRoot } from "../game/root";
/* typehints:end */
import { Signal } from "../core/signal";
// Single file to avoid circular deps
export const MOD_SIGNALS = {
@ -15,4 +17,6 @@ export const MOD_SIGNALS = {
hudElementInitialized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()),
hudElementFinalized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()),
gameStarted: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()),
};

View File

@ -1,3 +1,7 @@
/* typehints:start */
import { Application } from "../application";
/* typehints:end */
import { globalConfig } from "../core/config";
import { createLogger } from "../core/logging";
import { getIPCRenderer } from "../core/utils";
@ -11,6 +15,11 @@ export class ModLoader {
constructor() {
LOG.log("modloader created");
/**
* @type {Application}
*/
this.app = undefined;
/** @type {Mod[]} */
this.mods = [];

View File

@ -9,6 +9,7 @@ import { Savegame } from "../savegame/savegame";
import { GameCore } from "../game/core";
import { MUSIC } from "../platform/sound";
import { enumGameModeIds } from "../game/game_mode";
import { MOD_SIGNALS } from "../mods/mod_signals";
const logger = createLogger("state/ingame");
@ -82,6 +83,10 @@ export class InGameState extends GameState {
this.currentSavePromise = null;
}
get dialogs() {
return this.core.root.hud.parts.dialogs;
}
/**
* Switches the game into another sub-state
* @param {string} stage
@ -318,6 +323,8 @@ export class InGameState extends GameState {
// Initial resize, might have changed during loading (this is possible)
this.core.resize(this.app.screenWidth, this.app.screenHeight);
MOD_SIGNALS.gameStarted.dispatch(this.core.root);
}
}

View File

@ -34,6 +34,10 @@ export class KeybindingsState extends TextualGameState {
this.trackClicks(this.htmlElement.querySelector(".resetBindings"), this.resetBindings);
for (const category in KEYMAPPINGS) {
if (Object.keys(KEYMAPPINGS[category]).length === 0) {
continue;
}
const categoryDiv = document.createElement("div");
categoryDiv.classList.add("category");
keybindingsElem.appendChild(categoryDiv);
@ -138,7 +142,19 @@ export class KeybindingsState extends TextualGameState {
}
const mappingDiv = container.querySelector(".mapping");
mappingDiv.innerHTML = getStringForKeyCode(keyCode);
let modifiers = "";
if (mapped.modifiers && mapped.modifiers.shift) {
modifiers += "⇪ ";
}
if (mapped.modifiers && mapped.modifiers.alt) {
modifiers += T.global.keys.alt + " ";
}
if (mapped.modifiers && mapped.modifiers.ctrl) {
modifiers += T.global.keys.control + " ";
}
mappingDiv.innerHTML = modifiers + getStringForKeyCode(keyCode);
mappingDiv.classList.toggle("changed", !!overrides[keybindingId]);
const resetBtn = container.querySelector("button.resetKeybinding");

View File

@ -1328,6 +1328,7 @@ keybindings:
massSelect: Mass Select
buildings: Building Shortcuts
placementModifiers: Placement Modifiers
mods: Provided by Mods
mappings:
confirm: Confirm