Refactoring of the key action mapper, allow deselecting buildings, make sure stars always spawn in the start region (closes #7) (closes #9)
							
								
								
									
										
											BIN
										
									
								
								artwork/itch.io/background.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 906 KiB | 
							
								
								
									
										
											BIN
										
									
								
								artwork/itch.io/background.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.1 MiB | 
							
								
								
									
										
											BIN
										
									
								
								artwork/itch.io/banner.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 214 KiB | 
							
								
								
									
										
											BIN
										
									
								
								artwork/itch.io/banner.psd
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								artwork/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 23 KiB | 
| Before Width: | Height: | Size: 5.1 MiB After Width: | Height: | Size: 2.2 MiB | 
							
								
								
									
										1
									
								
								res/ui/get_on_itch_io.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 13 KiB | 
| @ -375,6 +375,7 @@ | ||||
|                 <false/> | ||||
|             </struct> | ||||
|             <key type="filename">sprites/blueprints/painter-double.png</key> | ||||
|             <key type="filename">sprites/blueprints/trash-storage.png</key> | ||||
|             <key type="filename">sprites/buildings/painter-double.png</key> | ||||
|             <struct type="IndividualSpriteSettings"> | ||||
|                 <key>pivotPoint</key> | ||||
| @ -475,6 +476,21 @@ | ||||
|                 <key>scale9FromFile</key> | ||||
|                 <false/> | ||||
|             </struct> | ||||
|             <key type="filename">sprites/misc/storage_overlay.png</key> | ||||
|             <struct type="IndividualSpriteSettings"> | ||||
|                 <key>pivotPoint</key> | ||||
|                 <point_f>0.5,0.5</point_f> | ||||
|                 <key>spriteScale</key> | ||||
|                 <double>1</double> | ||||
|                 <key>scale9Enabled</key> | ||||
|                 <false/> | ||||
|                 <key>scale9Borders</key> | ||||
|                 <rect>44,22,89,43</rect> | ||||
|                 <key>scale9Paddings</key> | ||||
|                 <rect>44,22,89,43</rect> | ||||
|                 <key>scale9FromFile</key> | ||||
|                 <false/> | ||||
|             </struct> | ||||
|         </map> | ||||
|         <key>fileList</key> | ||||
|         <array> | ||||
|  | ||||
| @ -71,8 +71,8 @@ ingame_HUD_BetaOverlay, | ||||
| ingame_HUD_UnlockNotification, | ||||
| ingame_HUD_Shop, | ||||
| ingame_HUD_Statistics, | ||||
| ingame_HUD_ModalDialogs, | ||||
| ingame_HUD_SettingsMenu; | ||||
| ingame_HUD_SettingsMenu, | ||||
| ingame_HUD_ModalDialogs; | ||||
| 
 | ||||
| $zindex: 100; | ||||
| 
 | ||||
|  | ||||
| @ -109,7 +109,7 @@ | ||||
|                 width: 100%; | ||||
|                 @include S(height, 50px); | ||||
| 
 | ||||
|                 background: uiResource("get_on_steam.png") center center / contain no-repeat; | ||||
|                 background: uiResource("get_on_itch_io.svg") center center / contain no-repeat; | ||||
|                 overflow: hidden; | ||||
|                 display: block; | ||||
|                 text-indent: -999em; | ||||
|  | ||||
| @ -5,8 +5,9 @@ export const IS_DEBUG = | ||||
|     (window.location.host.indexOf("localhost:") >= 0 || window.location.host.indexOf("192.168.0.") >= 0) && | ||||
|     window.location.search.indexOf("nodebug") < 0; | ||||
| 
 | ||||
| // export const IS_DEMO = G_IS_PROD;
 | ||||
| export const IS_DEMO = G_IS_RELEASE; | ||||
| export const IS_DEMO = | ||||
|     (G_IS_RELEASE && !G_IS_STANDALONE) || | ||||
|     (typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0); | ||||
| 
 | ||||
| const smoothCanvas = true; | ||||
| 
 | ||||
| @ -14,7 +15,8 @@ export const THIRDPARTY_URLS = { | ||||
|     discord: "https://discord.gg/HN7EVzV", | ||||
|     github: "https://github.com/tobspr/shapez.io", | ||||
| 
 | ||||
|     standaloneStorePage: "https://steam.shapez.io", | ||||
|     // standaloneStorePage: "https://steam.shapez.io",
 | ||||
|     standaloneStorePage: "https://tobspr.itch.io/shapez.io", | ||||
| }; | ||||
| 
 | ||||
| export const globalConfig = { | ||||
|  | ||||
| @ -330,7 +330,7 @@ export class Camera extends BasicSerializableObject { | ||||
|      * Binds the arrow keys | ||||
|      */ | ||||
|     bindKeys() { | ||||
|         const mapper = this.root.gameState.keyActionMapper; | ||||
|         const mapper = this.root.keyMapper; | ||||
|         mapper.getBinding(KEYMAPPINGS.ingame.mapMoveUp).add(() => (this.keyboardForce.y = -1)); | ||||
|         mapper.getBinding(KEYMAPPINGS.ingame.mapMoveDown).add(() => (this.keyboardForce.y = 1)); | ||||
|         mapper.getBinding(KEYMAPPINGS.ingame.mapMoveRight).add(() => (this.keyboardForce.x = 1)); | ||||
| @ -867,7 +867,7 @@ export class Camera extends BasicSerializableObject { | ||||
|             let forceX = 0; | ||||
|             let forceY = 0; | ||||
| 
 | ||||
|             const actionMapper = this.root.gameState.keyActionMapper; | ||||
|             const actionMapper = this.root.keyMapper; | ||||
|             if (actionMapper.getBinding(KEYMAPPINGS.ingame.mapMoveUp).currentlyDown) { | ||||
|                 forceY -= 1; | ||||
|             } | ||||
|  | ||||
| @ -75,6 +75,7 @@ export class GameCore { | ||||
|         // Construct the root element, this is the data representation of the game
 | ||||
|         this.root = new GameRoot(this.app); | ||||
|         this.root.gameState = parentState; | ||||
|         this.root.keyMapper = parentState.keyActionMapper; | ||||
|         this.root.savegame = savegame; | ||||
|         this.root.gameWidth = this.app.screenWidth; | ||||
|         this.root.gameHeight = this.app.screenHeight; | ||||
| @ -86,7 +87,7 @@ export class GameCore { | ||||
|         const root = this.root; | ||||
| 
 | ||||
|         // This isn't nice, but we need it right here
 | ||||
|         root.gameState.keyActionMapper = new KeyActionMapper(root, this.root.gameState.inputReciever); | ||||
|         root.keyMapper = new KeyActionMapper(root, this.root.gameState.inputReciever); | ||||
| 
 | ||||
|         // Needs to come first
 | ||||
|         root.dynamicTickrate = new DynamicTickrate(root); | ||||
|  | ||||
| @ -141,10 +141,7 @@ export class BaseHUDPart { | ||||
|      * @param {KeyActionMapper} sourceMapper | ||||
|      */ | ||||
|     forwardGameSpeedKeybindings(sourceMapper) { | ||||
|         sourceMapper.forward(this.root.gameState.keyActionMapper, [ | ||||
|             "gamespeed_pause", | ||||
|             "gamespeed_fastforward", | ||||
|         ]); | ||||
|         sourceMapper.forward(this.root.keyMapper, ["gamespeed_pause", "gamespeed_fastforward"]); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -153,7 +150,7 @@ export class BaseHUDPart { | ||||
|      * @param {KeyActionMapper} sourceMapper | ||||
|      */ | ||||
|     forwardMapMovementKeybindings(sourceMapper) { | ||||
|         sourceMapper.forward(this.root.gameState.keyActionMapper, [ | ||||
|         sourceMapper.forward(this.root.keyMapper, [ | ||||
|             "mapMoveUp", | ||||
|             "mapMoveRight", | ||||
|             "mapMoveDown", | ||||
|  | ||||
| @ -97,7 +97,7 @@ export class GameHUD { | ||||
|         } | ||||
|         this.internalInitSignalConnections(); | ||||
| 
 | ||||
|         this.root.gameState.keyActionMapper.getBinding(KEYMAPPINGS.ingame.toggleHud).add(this.toggleUi, this); | ||||
|         this.root.keyMapper.getBinding(KEYMAPPINGS.ingame.toggleHud).add(this.toggleUi, this); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -30,7 +30,7 @@ export class HUDBuildingPlacer extends BaseHUDPart { | ||||
|         /** @type {Entity} */ | ||||
|         this.fakeEntity = null; | ||||
| 
 | ||||
|         const keyActionMapper = this.root.gameState.keyActionMapper; | ||||
|         const keyActionMapper = this.root.keyMapper; | ||||
|         keyActionMapper | ||||
|             .getBinding(KEYMAPPINGS.placement.abortBuildingPlacement) | ||||
|             .add(this.abortPlacement, this); | ||||
| @ -284,9 +284,7 @@ export class HUDBuildingPlacer extends BaseHUDPart { | ||||
|         this.buildingInfoElements.label.innerHTML = T.buildings[metaBuilding.id].name; | ||||
|         this.buildingInfoElements.descText.innerHTML = T.buildings[metaBuilding.id].description; | ||||
| 
 | ||||
|         const binding = this.root.gameState.keyActionMapper.getBinding( | ||||
|             KEYMAPPINGS.buildings[metaBuilding.getId()] | ||||
|         ); | ||||
|         const binding = this.root.keyMapper.getBinding(KEYMAPPINGS.buildings[metaBuilding.getId()]); | ||||
|         this.buildingInfoElements.hotkey.innerHTML = T.ingame.buildingPlacement.hotkeyLabel.replace( | ||||
|             "<key>", | ||||
|             "<code class='keybinding'>" + binding.getKeyCodeString() + "</code>" | ||||
| @ -327,7 +325,7 @@ export class HUDBuildingPlacer extends BaseHUDPart { | ||||
|             T.ingame.buildingPlacement.cycleBuildingVariants.replace( | ||||
|                 "<key>", | ||||
|                 "<code class='keybinding'>" + | ||||
|                     this.root.gameState.keyActionMapper | ||||
|                     this.root.keyMapper | ||||
|                         .getBinding(KEYMAPPINGS.placement.cycleBuildingVariants) | ||||
|                         .getKeyCodeString() + | ||||
|                     "</code>" | ||||
|  | ||||
| @ -60,7 +60,7 @@ export class HUDBuildingsToolbar extends BaseHUDPart { | ||||
|     } | ||||
| 
 | ||||
|     initialize() { | ||||
|         const actionMapper = this.root.gameState.keyActionMapper; | ||||
|         const actionMapper = this.root.keyMapper; | ||||
| 
 | ||||
|         const items = makeDiv(this.element, null, ["buildings"]); | ||||
| 
 | ||||
| @ -143,6 +143,15 @@ export class HUDBuildingsToolbar extends BaseHUDPart { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Allow clicking an item again to deselect it
 | ||||
|         for (const buildingId in this.buildingHandles) { | ||||
|             const handle = this.buildingHandles[buildingId]; | ||||
|             if (handle.selected && handle.metaBuilding === metaBuilding) { | ||||
|                 metaBuilding = null; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.root.soundProxy.playUiClick(); | ||||
|         this.sigBuildingSelected.dispatch(metaBuilding); | ||||
|         this.onSelectedPlacementBuildingChanged(metaBuilding); | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import { BaseHUDPart } from "../base_hud_part"; | ||||
| import { makeDiv, round3Digits, round2Digits } from "../../../core/utils"; | ||||
| import { Math_round } from "../../../core/builtins"; | ||||
| import { DynamicDomAttach } from "../dynamic_dom_attach"; | ||||
| 
 | ||||
| export class HUDDebugInfo extends BaseHUDPart { | ||||
|     createElements(parent) { | ||||
| @ -13,6 +14,11 @@ export class HUDDebugInfo extends BaseHUDPart { | ||||
| 
 | ||||
|     initialize() { | ||||
|         this.lastTick = 0; | ||||
| 
 | ||||
|         this.visible = false; | ||||
|         this.domAttach = new DynamicDomAttach(this.root, this.element); | ||||
| 
 | ||||
|         // this.root.keyMapper
 | ||||
|     } | ||||
| 
 | ||||
|     update() { | ||||
|  | ||||
| @ -47,7 +47,7 @@ export class HUDGameMenu extends BaseHUDPart { | ||||
|             this.trackClicks(button, handler); | ||||
| 
 | ||||
|             if (keybinding) { | ||||
|                 const binding = this.root.gameState.keyActionMapper.getBinding(keybinding); | ||||
|                 const binding = this.root.keyMapper.getBinding(keybinding); | ||||
|                 binding.add(handler); | ||||
|                 binding.appendLabelToElement(button); | ||||
|             } | ||||
|  | ||||
| @ -20,7 +20,7 @@ export class HUDKeybindingOverlay extends BaseHUDPart { | ||||
|     } | ||||
| 
 | ||||
|     createElements(parent) { | ||||
|         const mapper = this.root.gameState.keyActionMapper; | ||||
|         const mapper = this.root.keyMapper; | ||||
| 
 | ||||
|         const getKeycode = id => { | ||||
|             return getStringForKeyCode(mapper.getBinding(id).keyCode); | ||||
|  | ||||
| @ -16,12 +16,10 @@ const logger = createLogger("hud/mass_selector"); | ||||
| 
 | ||||
| export class HUDMassSelector extends BaseHUDPart { | ||||
|     createElements(parent) { | ||||
|         const removalKeybinding = this.root.gameState.keyActionMapper | ||||
|         const removalKeybinding = this.root.keyMapper | ||||
|             .getBinding(KEYMAPPINGS.massSelect.confirmMassDelete) | ||||
|             .getKeyCodeString(); | ||||
|         const abortKeybinding = this.root.gameState.keyActionMapper | ||||
|             .getBinding(KEYMAPPINGS.general.back) | ||||
|             .getKeyCodeString(); | ||||
|         const abortKeybinding = this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).getKeyCodeString(); | ||||
| 
 | ||||
|         this.element = makeDiv( | ||||
|             parent, | ||||
| @ -46,8 +44,8 @@ export class HUDMassSelector extends BaseHUDPart { | ||||
|         this.root.camera.movePreHandler.add(this.onMouseMove, this); | ||||
|         this.root.camera.upPostHandler.add(this.onMouseUp, this); | ||||
| 
 | ||||
|         this.root.gameState.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.onBack, this); | ||||
|         this.root.gameState.keyActionMapper | ||||
|         this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).add(this.onBack, this); | ||||
|         this.root.keyMapper | ||||
|             .getBinding(KEYMAPPINGS.massSelect.confirmMassDelete) | ||||
|             .add(this.confirmDelete, this); | ||||
| 
 | ||||
|  | ||||
| @ -7,6 +7,7 @@ import { T } from "../../../translations"; | ||||
| import { StaticMapEntityComponent } from "../../components/static_map_entity"; | ||||
| import { ItemProcessorComponent } from "../../components/item_processor"; | ||||
| import { BeltComponent } from "../../components/belt"; | ||||
| import { IS_DEMO } from "../../../core/config"; | ||||
| 
 | ||||
| export class HUDSettingsMenu extends BaseHUDPart { | ||||
|     createElements(parent) { | ||||
| @ -56,7 +57,16 @@ export class HUDSettingsMenu extends BaseHUDPart { | ||||
|     } | ||||
| 
 | ||||
|     returnToMenu() { | ||||
|         this.root.gameState.goBackToMenu(); | ||||
|         if (IS_DEMO) { | ||||
|             const { cancel, deleteGame } = this.root.hud.parts.dialogs.showWarning( | ||||
|                 T.dialogs.leaveNotPossibleInDemo.title, | ||||
|                 T.dialogs.leaveNotPossibleInDemo.desc, | ||||
|                 ["cancel:good", "deleteGame:bad"] | ||||
|             ); | ||||
|             deleteGame.add(() => this.root.gameState.goBackToMenu()); | ||||
|         } else { | ||||
|             this.root.gameState.goBackToMenu(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     goToSettings() { | ||||
| @ -72,7 +82,7 @@ export class HUDSettingsMenu extends BaseHUDPart { | ||||
|     } | ||||
| 
 | ||||
|     initialize() { | ||||
|         this.root.gameState.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.show, this); | ||||
|         this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).add(this.show, this); | ||||
| 
 | ||||
|         this.domAttach = new DynamicDomAttach(this.root, this.background, { | ||||
|             attachClass: "visible", | ||||
|  | ||||
| @ -31,6 +31,7 @@ export const KEYMAPPINGS = { | ||||
|         menuOpenStats: { keyCode: key("G") }, | ||||
| 
 | ||||
|         toggleHud: { keyCode: 113 }, // F2
 | ||||
|         toggleFPSInfo: { keyCode: 112 }, // F1
 | ||||
|     }, | ||||
| 
 | ||||
|     buildings: { | ||||
| @ -362,7 +363,7 @@ export class KeyActionMapper { | ||||
|         for (const key in this.keybindings) { | ||||
|             /** @type {Keybinding} */ | ||||
|             const binding = this.keybindings[key]; | ||||
|             if (binding.keyCode === keyCode /* && binding.shift === shift && binding.alt === alt */) { | ||||
|             if (binding.keyCode === keyCode && !binding.currentlyDown) { | ||||
|                 binding.currentlyDown = true; | ||||
| 
 | ||||
|                 /** @type {Signal} */ | ||||
|  | ||||
| @ -163,17 +163,17 @@ export class MapChunk { | ||||
|             [enumSubShape.windmill]: Math_round(6 + clamp(distanceToOriginInChunks / 2, 0, 20)), | ||||
|         }; | ||||
| 
 | ||||
|         if (distanceToOriginInChunks < 4) { | ||||
|         if (distanceToOriginInChunks < 7) { | ||||
|             // Initial chunks can not spawn the good stuff
 | ||||
|             weights[enumSubShape.star] = 0; | ||||
|             weights[enumSubShape.windmill] = 0; | ||||
|         } | ||||
| 
 | ||||
|         if (distanceToOriginInChunks < 7) { | ||||
|         if (distanceToOriginInChunks < 10) { | ||||
|             // Initial chunk patches always have the same shape
 | ||||
|             const subShape = this.internalGenerateRandomSubShape(rng, weights); | ||||
|             subShapes = [subShape, subShape, subShape, subShape]; | ||||
|         } else if (distanceToOriginInChunks < 17) { | ||||
|         } else if (distanceToOriginInChunks < 15) { | ||||
|             // Later patches can also have mixed ones
 | ||||
|             const subShapeA = this.internalGenerateRandomSubShape(rng, weights); | ||||
|             const subShapeB = this.internalGenerateRandomSubShape(rng, weights); | ||||
| @ -269,22 +269,12 @@ export class MapChunk { | ||||
|             return true; | ||||
|         } | ||||
|         if (this.x === -1 && this.y === 0) { | ||||
|             const definition = this.root.shapeDefinitionMgr.getDefinitionFromSimpleShapes([ | ||||
|                 enumSubShape.circle, | ||||
|                 enumSubShape.circle, | ||||
|                 enumSubShape.circle, | ||||
|                 enumSubShape.circle, | ||||
|             ]); | ||||
|             const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey("CuCuCuCu"); | ||||
|             this.internalGeneratePatch(rng, 2, new ShapeItem(definition), globalConfig.mapChunkSize - 9, 7); | ||||
|             return true; | ||||
|         } | ||||
|         if (this.x === 0 && this.y === -1) { | ||||
|             const definition = this.root.shapeDefinitionMgr.getDefinitionFromSimpleShapes([ | ||||
|                 enumSubShape.rect, | ||||
|                 enumSubShape.rect, | ||||
|                 enumSubShape.rect, | ||||
|                 enumSubShape.rect, | ||||
|             ]); | ||||
|             const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey("RuRuRuRu"); | ||||
|             this.internalGeneratePatch(rng, 2, new ShapeItem(definition), 5, globalConfig.mapChunkSize - 7); | ||||
|             return true; | ||||
|         } | ||||
| @ -294,6 +284,12 @@ export class MapChunk { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (this.x === 5 && this.y === -2) { | ||||
|             const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey("SuSuSuSu"); | ||||
|             this.internalGeneratePatch(rng, 2, new ShapeItem(definition), 5, globalConfig.mapChunkSize - 7); | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -27,6 +27,7 @@ import { Entity } from "./entity"; | ||||
| import { ShapeDefinition } from "./shape_definition"; | ||||
| import { BaseItem } from "./base_item"; | ||||
| import { DynamicTickrate } from "./dynamic_tickrate"; | ||||
| import { KeyActionMapper } from "./key_action_mapper"; | ||||
| /* typehints:end */ | ||||
| 
 | ||||
| const logger = createLogger("game/root"); | ||||
| @ -50,6 +51,9 @@ export class GameRoot { | ||||
|         /** @type {InGameState} */ | ||||
|         this.gameState = null; | ||||
| 
 | ||||
|         /** @type {KeyActionMapper} */ | ||||
|         this.keyMapper = null; | ||||
| 
 | ||||
|         // Store game dimensions
 | ||||
|         this.gameWidth = 500; | ||||
|         this.gameHeight = 500; | ||||
|  | ||||
| @ -194,8 +194,7 @@ export class MainMenuState extends GameState { | ||||
| 
 | ||||
|     onSteamLinkClicked(event) { | ||||
|         this.app.analytics.trackUiClick("main_menu_steam_link"); | ||||
|         alert("The steam version will launch very soon! (Planned date: Begin of June 2020)"); | ||||
|         // window.open("https://steam.shapez.io");
 | ||||
|         window.open(THIRDPARTY_URLS.standaloneStorePage); | ||||
|         event.preventDefault(); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
| @ -59,7 +59,7 @@ demoBanners: | ||||
|     # This is the "advertisement" shown in the main menu and other various places | ||||
|     title: This is a demo version | ||||
|     intro: >- | ||||
|         Get <strong>shapez.io on steam</strong> to: | ||||
|         Get the <strong>shapez.io standalone</strong> to: | ||||
|     advantages: | ||||
|         - Save and resume your games. | ||||
|         - No advertisements. | ||||
| @ -84,6 +84,7 @@ dialogs: | ||||
|         restart: Restart | ||||
|         reset: Reset | ||||
|         getStandalone: Get Standalone | ||||
|         deleteGame: Delete Progress | ||||
| 
 | ||||
|     importSavegameError: | ||||
|         title: Import Error | ||||
| @ -134,6 +135,10 @@ dialogs: | ||||
|     saveNotPossibleInDemo: | ||||
|         desc: Your game has been saved, but restoring it is only possible in the standalone version. Consider to get the standalone for the full experience! | ||||
| 
 | ||||
|     leaveNotPossibleInDemo: | ||||
|         title: Saving not possible | ||||
|         desc: You can not save in the demo. Your game will be lost. Are you sure? | ||||
| 
 | ||||
| ingame: | ||||
|     # This is shown in the top left corner and displays useful keybindings in | ||||
|     # every situation | ||||
|  | ||||
 tobspr
						tobspr