diff --git a/src/css/ingame_hud/building_placer.scss b/src/css/ingame_hud/building_placer.scss index c47e2792..9c038340 100644 --- a/src/css/ingame_hud/building_placer.scss +++ b/src/css/ingame_hud/building_placer.scss @@ -18,12 +18,11 @@ background-color: #55585a; } - pointer-events: all; - - &:hover { - opacity: 10%; + transition: opacity 0.1s ease-out; + &.hovered { + opacity: 0.1; .buildingImage { - opacity: 0%; + opacity: 0; } } @@ -79,6 +78,7 @@ @include S(height, 100px); background: top left / 100% 100% no-repeat; @include S(border-radius, $globalBorderRadius); + transition: opacity 0.1s ease-in-out; } @include StyleBelowWidth(700px) { diff --git a/src/css/ingame_hud/interactive_tutorial.scss b/src/css/ingame_hud/interactive_tutorial.scss index 0925e2ab..9d4aeffc 100644 --- a/src/css/ingame_hud/interactive_tutorial.scss +++ b/src/css/ingame_hud/interactive_tutorial.scss @@ -31,7 +31,8 @@ pointer-events: all; - &:hover { + transition: opacity 0.1s ease-out; + &.hovered { opacity: 10%; .helperGif { opacity: 0%; @@ -57,5 +58,6 @@ @include S(margin-top, 5px); @include S(height, 150px); background: center center / contain no-repeat; + transition: opacity 0.1s ease-out; } } diff --git a/src/css/ingame_hud/keybindings_overlay.scss b/src/css/ingame_hud/keybindings_overlay.scss index 9620d2c9..21d07b4b 100644 --- a/src/css/ingame_hud/keybindings_overlay.scss +++ b/src/css/ingame_hud/keybindings_overlay.scss @@ -1,69 +1,74 @@ -#ingame_HUD_KeybindingOverlay { - position: absolute; - @include S(top, 10px); - @include S(left, 10px); - - display: flex; - flex-direction: column; - align-items: flex-start; - color: #333438; - backdrop-filter: blur(D(2px)); - padding: D(3px); - - @include DarkThemeOverride { - color: #fff; - } - - > .binding { - &:not(.visible) { - display: none !important; - } - - display: inline-grid; - @include PlainText; - align-items: center; - @include S(margin-bottom, 3px); - grid-auto-flow: column; - @include S(grid-gap, 2px); - - i { - display: inline-block; - @include S(height, 10px); - width: 1px; - @include S(margin, 0, 3px); - background-color: #fff; - transform: rotate(10deg); - // @include S(margin, 0, 3px); - } - - code { - position: relative; - top: unset; - left: unset; - margin: 0; - &.rightMouse { - background: #fff uiResource("icons/mouse_right.png") center center / 85% no-repeat; - } - - &.leftMouse { - background: #fff uiResource("icons/mouse_left.png") center center / 85% no-repeat; - } - } - - label { - color: #333438; - @include SuperSmallText; - text-transform: uppercase; - // color: #fff; - @include DarkThemeOverride { - color: #fff; - } - - @include S(margin-left, 5px); - } - } -} - -body.uiHidden #ingame_HUD_KeybindingOverlay .binding:not(.hudToggle) { - display: none; -} +#ingame_HUD_KeybindingOverlay { + position: absolute; + @include S(top, 10px); + @include S(left, 10px); + + display: flex; + flex-direction: column; + align-items: flex-start; + color: #333438; + backdrop-filter: blur(D(2px)); + padding: D(3px); + + @include DarkThemeOverride { + color: #fff; + } + + transition: opacity 0.1s ease-out; + &.hovered { + opacity: 0.1; + } + + > .binding { + &:not(.visible) { + display: none !important; + } + + display: inline-grid; + @include PlainText; + align-items: center; + @include S(margin-bottom, 3px); + grid-auto-flow: column; + @include S(grid-gap, 2px); + + i { + display: inline-block; + @include S(height, 10px); + width: 1px; + @include S(margin, 0, 3px); + background-color: #fff; + transform: rotate(10deg); + // @include S(margin, 0, 3px); + } + + code { + position: relative; + top: unset; + left: unset; + margin: 0; + &.rightMouse { + background: #fff uiResource("icons/mouse_right.png") center center / 85% no-repeat; + } + + &.leftMouse { + background: #fff uiResource("icons/mouse_left.png") center center / 85% no-repeat; + } + } + + label { + color: #333438; + @include SuperSmallText; + text-transform: uppercase; + // color: #fff; + @include DarkThemeOverride { + color: #fff; + } + + @include S(margin-left, 5px); + } + } +} + +body.uiHidden #ingame_HUD_KeybindingOverlay .binding:not(.hudToggle) { + display: none; +} diff --git a/src/js/changelog.js b/src/js/changelog.js index 3ab28600..145dcd04 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -19,6 +19,7 @@ export const CHANGELOG = [ "Updated and added new translations (Thanks to all contributors!)", "Added setting to be able to delete buildings while placing (inspired by hexy)", "You can now adjust the sound and music volumes! (inspired by Yoshie2000)", + "Some hud elements now have reduced opacity when hovering, so you can see through (by mvb005)", "Mark pinned shapes in statistics dialog and show them first (inspired by davidburhans)", "Added setting to show chunk borders", "Quad painters have been reworked! They now are integrated with the wires, and only paint the shape when the value is 1 (inspired by dengr1605)", diff --git a/src/js/game/hud/dynamic_dom_attach.js b/src/js/game/hud/dynamic_dom_attach.js index 68f09052..2b150448 100644 --- a/src/js/game/hud/dynamic_dom_attach.js +++ b/src/js/game/hud/dynamic_dom_attach.js @@ -1,3 +1,4 @@ +import { TrackedState } from "../../core/tracked_state"; import { GameRoot } from "../root"; // Automatically attaches and detaches elements from the dom @@ -7,15 +8,28 @@ import { GameRoot } from "../root"; // Also attaches a class name if desired export class DynamicDomAttach { - constructor(root, element, { timeToKeepSeconds = 0, attachClass = null } = {}) { + /** + * + * @param {GameRoot} root + * @param {HTMLElement} element + * @param {object} param2 + * @param {number=} param2.timeToKeepSeconds How long to keep the element visible (in ms) after it should be hidden. + * Useful for fade-out effects + * @param {string=} param2.attachClass If set, attaches a class while the element is visible + * @param {boolean=} param2.trackHover If set, attaches the 'hovered' class if the cursor is above the element. Useful + * for fading out the element if its below the cursor for example. + */ + constructor(root, element, { timeToKeepSeconds = 0, attachClass = null, trackHover = false } = {}) { /** @type {GameRoot} */ this.root = root; /** @type {HTMLElement} */ this.element = element; this.parent = this.element.parentElement; + assert(this.parent, "Dom attach created without parent"); this.attachClass = attachClass; + this.trackHover = trackHover; this.timeToKeepSeconds = timeToKeepSeconds; this.lastVisibleTime = 0; @@ -26,8 +40,19 @@ export class DynamicDomAttach { this.internalIsClassAttached = false; this.classAttachTimeout = null; + + // Store the last bounds we computed + /** @type {DOMRect} */ + this.lastComputedBounds = null; + this.lastComputedBoundsTime = -1; + + // Track the 'hovered' class + this.trackedIsHovered = new TrackedState(this.setIsHoveredClass, this); } + /** + * Internal method to attach the element + */ internalAttach() { if (!this.attached) { this.parent.appendChild(this.element); @@ -36,6 +61,9 @@ export class DynamicDomAttach { } } + /** + * Internal method to detach the element + */ internalDetach() { if (this.attached) { assert(this.element.parentElement === this.parent, "Invalid parent #2"); @@ -44,14 +72,50 @@ export class DynamicDomAttach { } } + /** + * Returns whether the element is currently attached + */ isAttached() { return this.attached; } + /** + * Actually sets the 'hovered' class + * @param {boolean} isHovered + */ + setIsHoveredClass(isHovered) { + this.element.classList.toggle("hovered", isHovered); + } + + /** + * Call this every frame, and the dom attach class will take care of + * everything else + * @param {boolean} isVisible Whether the element should currently be visible or not + */ update(isVisible) { if (isVisible) { this.lastVisibleTime = this.root ? this.root.time.realtimeNow() : 0; this.internalAttach(); + + if (this.trackHover && this.root) { + let bounds = this.lastComputedBounds; + + // Recompute bounds only once in a while + if (!bounds || this.root.time.realtimeNow() - this.lastComputedBoundsTime > 1.0) { + bounds = this.lastComputedBounds = this.element.getBoundingClientRect(); + this.lastComputedBoundsTime = this.root.time.realtimeNow(); + } + + const mousePos = this.root.app.mousePosition; + if (mousePos) { + this.trackedIsHovered.set( + mousePos.x > bounds.left && + mousePos.x < bounds.right && + mousePos.y > bounds.top && + mousePos.y < bounds.bottom + ); + } + } } else { if (!this.root || this.root.time.realtimeNow() - this.lastVisibleTime >= this.timeToKeepSeconds) { this.internalDetach(); diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index ffe9b414..936774f4 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -55,7 +55,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { this.signals.variantChanged.add(this.rerenderVariants, this); this.root.hud.signals.buildingSelectedForPlacement.add(this.startSelection, this); - this.domAttach = new DynamicDomAttach(this.root, this.element, {}); + this.domAttach = new DynamicDomAttach(this.root, this.element, { trackHover: true }); this.variantsAttach = new DynamicDomAttach(this.root, this.variantsElement, {}); this.currentInterpolatedCornerTile = new Vector(); diff --git a/src/js/game/hud/parts/interactive_tutorial.js b/src/js/game/hud/parts/interactive_tutorial.js index b3d93dcd..ffebc639 100644 --- a/src/js/game/hud/parts/interactive_tutorial.js +++ b/src/js/game/hud/parts/interactive_tutorial.js @@ -1,81 +1,81 @@ -import { BaseHUDPart } from "../base_hud_part"; -import { makeDiv } from "../../../core/utils"; -import { GameRoot } from "../../root"; -import { MinerComponent } from "../../components/miner"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { TrackedState } from "../../../core/tracked_state"; -import { cachebust } from "../../../core/cachebust"; -import { T } from "../../../translations"; - -const tutorialsByLevel = [ - // Level 1 - [ - // 1.1. place an extractor - { - id: "1_1_extractor", - condition: /** @param {GameRoot} root */ root => { - return root.entityMgr.getAllWithComponent(MinerComponent).length === 0; - }, - }, - // 1.2. connect to hub - { - id: "1_2_conveyor", - condition: /** @param {GameRoot} root */ root => { - return root.hubGoals.getCurrentGoalDelivered() === 0; - }, - }, - // 1.3 wait for completion - { - id: "1_3_expand", - condition: () => true, - }, - ], -]; - -export class HUDInteractiveTutorial extends BaseHUDPart { - createElements(parent) { - this.element = makeDiv( - parent, - "ingame_HUD_InteractiveTutorial", - ["animEven"], - ` - ${T.ingame.interactiveTutorial.title} - ` - ); - - this.elementDescription = makeDiv(this.element, null, ["desc"]); - this.elementGif = makeDiv(this.element, null, ["helperGif"]); - } - - initialize() { - this.domAttach = new DynamicDomAttach(this.root, this.element); - this.currentHintId = new TrackedState(this.onHintChanged, this); - } - - onHintChanged(hintId) { - this.elementDescription.innerHTML = T.ingame.interactiveTutorial.hints[hintId]; - this.elementGif.style.backgroundImage = - "url('" + cachebust("res/ui/interactive_tutorial.noinline/" + hintId + ".gif") + "')"; - this.element.classList.toggle("animEven"); - this.element.classList.toggle("animOdd"); - } - - update() { - // Compute current hint - const thisLevelHints = tutorialsByLevel[this.root.hubGoals.level - 1]; - let targetHintId = null; - - if (thisLevelHints) { - for (let i = 0; i < thisLevelHints.length; ++i) { - const hint = thisLevelHints[i]; - if (hint.condition(this.root)) { - targetHintId = hint.id; - break; - } - } - } - - this.currentHintId.set(targetHintId); - this.domAttach.update(!!targetHintId); - } -} +import { BaseHUDPart } from "../base_hud_part"; +import { makeDiv } from "../../../core/utils"; +import { GameRoot } from "../../root"; +import { MinerComponent } from "../../components/miner"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { TrackedState } from "../../../core/tracked_state"; +import { cachebust } from "../../../core/cachebust"; +import { T } from "../../../translations"; + +const tutorialsByLevel = [ + // Level 1 + [ + // 1.1. place an extractor + { + id: "1_1_extractor", + condition: /** @param {GameRoot} root */ root => { + return root.entityMgr.getAllWithComponent(MinerComponent).length === 0; + }, + }, + // 1.2. connect to hub + { + id: "1_2_conveyor", + condition: /** @param {GameRoot} root */ root => { + return root.hubGoals.getCurrentGoalDelivered() === 0; + }, + }, + // 1.3 wait for completion + { + id: "1_3_expand", + condition: () => true, + }, + ], +]; + +export class HUDInteractiveTutorial extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv( + parent, + "ingame_HUD_InteractiveTutorial", + ["animEven"], + ` + ${T.ingame.interactiveTutorial.title} + ` + ); + + this.elementDescription = makeDiv(this.element, null, ["desc"]); + this.elementGif = makeDiv(this.element, null, ["helperGif"]); + } + + initialize() { + this.domAttach = new DynamicDomAttach(this.root, this.element, { trackHover: true }); + this.currentHintId = new TrackedState(this.onHintChanged, this); + } + + onHintChanged(hintId) { + this.elementDescription.innerHTML = T.ingame.interactiveTutorial.hints[hintId]; + this.elementGif.style.backgroundImage = + "url('" + cachebust("res/ui/interactive_tutorial.noinline/" + hintId + ".gif") + "')"; + this.element.classList.toggle("animEven"); + this.element.classList.toggle("animOdd"); + } + + update() { + // Compute current hint + const thisLevelHints = tutorialsByLevel[this.root.hubGoals.level - 1]; + let targetHintId = null; + + if (thisLevelHints) { + for (let i = 0; i < thisLevelHints.length; ++i) { + const hint = thisLevelHints[i]; + if (hint.condition(this.root)) { + targetHintId = hint.id; + break; + } + } + } + + this.currentHintId.set(targetHintId); + this.domAttach.update(!!targetHintId); + } +} diff --git a/src/js/game/hud/parts/keybinding_overlay.js b/src/js/game/hud/parts/keybinding_overlay.js index 995fc5d9..d31ee746 100644 --- a/src/js/game/hud/parts/keybinding_overlay.js +++ b/src/js/game/hud/parts/keybinding_overlay.js @@ -1,323 +1,330 @@ -import { makeDiv } from "../../../core/utils"; -import { T } from "../../../translations"; -import { - getStringForKeyCode, - KEYCODE_LMB, - KEYCODE_MMB, - KEYCODE_RMB, - KEYMAPPINGS, -} from "../../key_action_mapper"; -import { BaseHUDPart } from "../base_hud_part"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; - -const DIVIDER_TOKEN = "/"; -const ADDER_TOKEN = "+"; - -/** - * @typedef {{ keyCode: number }} KeyCode - */ - -/** - * @typedef {{ - * condition: () => boolean, - * keys: Array, - * label: string, - * cachedElement?: HTMLElement, - * cachedVisibility?: boolean - * }} KeyBinding - */ - -export class HUDKeybindingOverlay extends BaseHUDPart { - initialize() {} - - /** - * HELPER / Returns if there is a building selected for placement - * @returns {boolean} - */ - get buildingPlacementActive() { - const placer = this.root.hud.parts.buildingPlacer; - return !this.mapOverviewActive && placer && !!placer.currentMetaBuilding.get(); - } - - /** - * HELPER / Returns if there is a building selected for placement and - * it supports the belt planner - * @returns {boolean} - */ - get buildingPlacementSupportsBeltPlanner() { - const placer = this.root.hud.parts.buildingPlacer; - return ( - !this.mapOverviewActive && - placer && - placer.currentMetaBuilding.get() && - placer.currentMetaBuilding.get().getHasDirectionLockAvailable() - ); - } - - /** - * HELPER / Returns if there is a building selected for placement and - * it has multiplace enabled by default - * @returns {boolean} - */ - get buildingPlacementStaysInPlacement() { - const placer = this.root.hud.parts.buildingPlacer; - return ( - !this.mapOverviewActive && - placer && - placer.currentMetaBuilding.get() && - placer.currentMetaBuilding.get().getStayInPlacementMode() - ); - } - - /** - * HELPER / Returns if there is a blueprint selected for placement - * @returns {boolean} - */ - get blueprintPlacementActive() { - const placer = this.root.hud.parts.blueprintPlacer; - return placer && !!placer.currentBlueprint.get(); - } - - /** - * HELPER / Returns if the belt planner is currently active - * @returns {boolean} - */ - get beltPlannerActive() { - const placer = this.root.hud.parts.buildingPlacer; - return !this.mapOverviewActive && placer && placer.isDirectionLockActive; - } - - /** - * HELPER / Returns if there is a last blueprint available - * @returns {boolean} - */ - get lastBlueprintAvailable() { - const placer = this.root.hud.parts.blueprintPlacer; - return placer && !!placer.lastBlueprintUsed; - } - - /** - * HELPER / Returns if there is anything selected on the map - * @returns {boolean} - */ - get anythingSelectedOnMap() { - const selector = this.root.hud.parts.massSelector; - return selector && selector.selectedUids.size > 0; - } - - /** - * HELPER / Returns if there is a building or blueprint selected for placement - * @returns {boolean} - */ - get anyPlacementActive() { - return this.buildingPlacementActive || this.blueprintPlacementActive; - } - - /** - * HELPER / Returns if the map overview is active - * @returns {boolean} - */ - get mapOverviewActive() { - return this.root.camera.getIsMapOverlayActive(); - } - - /** - * Initializes the element - * @param {HTMLElement} parent - */ - createElements(parent) { - const mapper = this.root.keyMapper; - const k = KEYMAPPINGS; - - /** @type {Array} */ - this.keybindings = [ - { - // Move map - Including mouse - label: T.ingame.keybindingsOverlay.moveMap, - keys: [ - KEYCODE_LMB, - DIVIDER_TOKEN, - k.navigation.mapMoveUp, - k.navigation.mapMoveLeft, - k.navigation.mapMoveDown, - k.navigation.mapMoveRight, - ], - condition: () => !this.anyPlacementActive, - }, - - { - // Move map - No mouse - label: T.ingame.keybindingsOverlay.moveMap, - keys: [ - k.navigation.mapMoveUp, - k.navigation.mapMoveLeft, - k.navigation.mapMoveDown, - k.navigation.mapMoveRight, - ], - condition: () => this.anyPlacementActive, - }, - - { - // [OVERVIEW] Create marker with right click - label: T.ingame.keybindingsOverlay.createMarker, - keys: [KEYCODE_RMB], - condition: () => this.mapOverviewActive && !this.blueprintPlacementActive, - }, - - { - // Pipette - label: T.ingame.keybindingsOverlay.pipette, - keys: [k.placement.pipette], - condition: () => !this.mapOverviewActive && !this.blueprintPlacementActive, - }, - - { - // Cancel placement - label: T.ingame.keybindingsOverlay.stopPlacement, - keys: [KEYCODE_RMB], - condition: () => this.anyPlacementActive, - }, - - { - // Delete with right click - label: T.ingame.keybindingsOverlay.delete, - keys: [KEYCODE_RMB], - condition: () => - !this.anyPlacementActive && !this.mapOverviewActive && !this.anythingSelectedOnMap, - }, - - { - // Area select - label: T.ingame.keybindingsOverlay.selectBuildings, - keys: [k.massSelect.massSelectStart, ADDER_TOKEN, KEYCODE_LMB], - condition: () => !this.anyPlacementActive && !this.anythingSelectedOnMap, - }, - - { - // Place building - label: T.ingame.keybindingsOverlay.placeBuilding, - keys: [KEYCODE_LMB], - condition: () => this.anyPlacementActive, - }, - - { - // Rotate - label: T.ingame.keybindingsOverlay.rotateBuilding, - keys: [k.placement.rotateWhilePlacing], - condition: () => this.anyPlacementActive && !this.beltPlannerActive, - }, - - { - // [BELT PLANNER] Flip Side - label: T.ingame.keybindingsOverlay.plannerSwitchSide, - keys: [k.placement.switchDirectionLockSide], - condition: () => this.beltPlannerActive, - }, - - { - // Place last blueprint - label: T.ingame.keybindingsOverlay.pasteLastBlueprint, - keys: [k.massSelect.pasteLastBlueprint], - condition: () => !this.blueprintPlacementActive && this.lastBlueprintAvailable, - }, - - { - // Belt planner - label: T.ingame.keybindingsOverlay.lockBeltDirection, - keys: [k.placementModifiers.lockBeltDirection], - condition: () => this.buildingPlacementSupportsBeltPlanner && !this.beltPlannerActive, - }, - - { - // [SELECTION] Destroy - label: T.ingame.keybindingsOverlay.delete, - keys: [k.massSelect.confirmMassDelete], - condition: () => this.anythingSelectedOnMap, - }, - - { - // [SELECTION] Cancel - label: T.ingame.keybindingsOverlay.clearSelection, - keys: [k.general.back], - condition: () => this.anythingSelectedOnMap, - }, - { - // [SELECTION] Cut - label: T.ingame.keybindingsOverlay.cutSelection, - keys: [k.massSelect.massSelectCut], - condition: () => this.anythingSelectedOnMap, - }, - - { - // [SELECTION] Copy - label: T.ingame.keybindingsOverlay.copySelection, - keys: [k.massSelect.massSelectCopy], - condition: () => this.anythingSelectedOnMap, - }, - - { - // Switch layers - label: T.ingame.keybindingsOverlay.switchLayers, - keys: [k.ingame.switchLayers], - condition: () => true, - }, - ]; - - if (!this.root.app.settings.getAllSettings().alwaysMultiplace) { - this.keybindings.push({ - // Multiplace - label: T.ingame.keybindingsOverlay.placeMultiple, - keys: [k.placementModifiers.placeMultiple], - condition: () => this.anyPlacementActive && !this.buildingPlacementStaysInPlacement, - }); - } - - this.element = makeDiv(parent, "ingame_HUD_KeybindingOverlay", []); - - for (let i = 0; i < this.keybindings.length; ++i) { - let html = ""; - const handle = this.keybindings[i]; - - for (let k = 0; k < handle.keys.length; ++k) { - const key = handle.keys[k]; - - switch (key) { - case KEYCODE_LMB: - html += ``; - break; - case KEYCODE_RMB: - html += ``; - break; - case KEYCODE_MMB: - html += ``; - break; - case DIVIDER_TOKEN: - html += ``; - break; - case ADDER_TOKEN: - html += `+`; - break; - default: - html += `${getStringForKeyCode( - mapper.getBinding(/** @type {KeyCode} */ (key)).keyCode - )}`; - } - } - html += ``; - - handle.cachedElement = makeDiv(this.element, null, ["binding"], html); - handle.cachedVisibility = false; - } - } - - update() { - for (let i = 0; i < this.keybindings.length; ++i) { - const handle = this.keybindings[i]; - const visibility = handle.condition(); - if (visibility !== handle.cachedVisibility) { - handle.cachedVisibility = visibility; - handle.cachedElement.classList.toggle("visible", visibility); - } - } - } -} +import { makeDiv } from "../../../core/utils"; +import { T } from "../../../translations"; +import { + getStringForKeyCode, + KEYCODE_LMB, + KEYCODE_MMB, + KEYCODE_RMB, + KEYMAPPINGS, +} from "../../key_action_mapper"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; + +const DIVIDER_TOKEN = "/"; +const ADDER_TOKEN = "+"; + +/** + * @typedef {{ keyCode: number }} KeyCode + */ + +/** + * @typedef {{ + * condition: () => boolean, + * keys: Array, + * label: string, + * cachedElement?: HTMLElement, + * cachedVisibility?: boolean + * }} KeyBinding + */ + +export class HUDKeybindingOverlay extends BaseHUDPart { + /** + * HELPER / Returns if there is a building selected for placement + * @returns {boolean} + */ + get buildingPlacementActive() { + const placer = this.root.hud.parts.buildingPlacer; + return !this.mapOverviewActive && placer && !!placer.currentMetaBuilding.get(); + } + + /** + * HELPER / Returns if there is a building selected for placement and + * it supports the belt planner + * @returns {boolean} + */ + get buildingPlacementSupportsBeltPlanner() { + const placer = this.root.hud.parts.buildingPlacer; + return ( + !this.mapOverviewActive && + placer && + placer.currentMetaBuilding.get() && + placer.currentMetaBuilding.get().getHasDirectionLockAvailable() + ); + } + + /** + * HELPER / Returns if there is a building selected for placement and + * it has multiplace enabled by default + * @returns {boolean} + */ + get buildingPlacementStaysInPlacement() { + const placer = this.root.hud.parts.buildingPlacer; + return ( + !this.mapOverviewActive && + placer && + placer.currentMetaBuilding.get() && + placer.currentMetaBuilding.get().getStayInPlacementMode() + ); + } + + /** + * HELPER / Returns if there is a blueprint selected for placement + * @returns {boolean} + */ + get blueprintPlacementActive() { + const placer = this.root.hud.parts.blueprintPlacer; + return placer && !!placer.currentBlueprint.get(); + } + + /** + * HELPER / Returns if the belt planner is currently active + * @returns {boolean} + */ + get beltPlannerActive() { + const placer = this.root.hud.parts.buildingPlacer; + return !this.mapOverviewActive && placer && placer.isDirectionLockActive; + } + + /** + * HELPER / Returns if there is a last blueprint available + * @returns {boolean} + */ + get lastBlueprintAvailable() { + const placer = this.root.hud.parts.blueprintPlacer; + return placer && !!placer.lastBlueprintUsed; + } + + /** + * HELPER / Returns if there is anything selected on the map + * @returns {boolean} + */ + get anythingSelectedOnMap() { + const selector = this.root.hud.parts.massSelector; + return selector && selector.selectedUids.size > 0; + } + + /** + * HELPER / Returns if there is a building or blueprint selected for placement + * @returns {boolean} + */ + get anyPlacementActive() { + return this.buildingPlacementActive || this.blueprintPlacementActive; + } + + /** + * HELPER / Returns if the map overview is active + * @returns {boolean} + */ + get mapOverviewActive() { + return this.root.camera.getIsMapOverlayActive(); + } + + /** + * Initializes the element + * @param {HTMLElement} parent + */ + createElements(parent) { + const mapper = this.root.keyMapper; + const k = KEYMAPPINGS; + + /** @type {Array} */ + this.keybindings = [ + { + // Move map - Including mouse + label: T.ingame.keybindingsOverlay.moveMap, + keys: [ + KEYCODE_LMB, + DIVIDER_TOKEN, + k.navigation.mapMoveUp, + k.navigation.mapMoveLeft, + k.navigation.mapMoveDown, + k.navigation.mapMoveRight, + ], + condition: () => !this.anyPlacementActive, + }, + + { + // Move map - No mouse + label: T.ingame.keybindingsOverlay.moveMap, + keys: [ + k.navigation.mapMoveUp, + k.navigation.mapMoveLeft, + k.navigation.mapMoveDown, + k.navigation.mapMoveRight, + ], + condition: () => this.anyPlacementActive, + }, + + { + // [OVERVIEW] Create marker with right click + label: T.ingame.keybindingsOverlay.createMarker, + keys: [KEYCODE_RMB], + condition: () => this.mapOverviewActive && !this.blueprintPlacementActive, + }, + + { + // Pipette + label: T.ingame.keybindingsOverlay.pipette, + keys: [k.placement.pipette], + condition: () => !this.mapOverviewActive && !this.blueprintPlacementActive, + }, + + { + // Cancel placement + label: T.ingame.keybindingsOverlay.stopPlacement, + keys: [KEYCODE_RMB], + condition: () => this.anyPlacementActive, + }, + + { + // Delete with right click + label: T.ingame.keybindingsOverlay.delete, + keys: [KEYCODE_RMB], + condition: () => + !this.anyPlacementActive && !this.mapOverviewActive && !this.anythingSelectedOnMap, + }, + + { + // Area select + label: T.ingame.keybindingsOverlay.selectBuildings, + keys: [k.massSelect.massSelectStart, ADDER_TOKEN, KEYCODE_LMB], + condition: () => !this.anyPlacementActive && !this.anythingSelectedOnMap, + }, + + { + // Place building + label: T.ingame.keybindingsOverlay.placeBuilding, + keys: [KEYCODE_LMB], + condition: () => this.anyPlacementActive, + }, + + { + // Rotate + label: T.ingame.keybindingsOverlay.rotateBuilding, + keys: [k.placement.rotateWhilePlacing], + condition: () => this.anyPlacementActive && !this.beltPlannerActive, + }, + + { + // [BELT PLANNER] Flip Side + label: T.ingame.keybindingsOverlay.plannerSwitchSide, + keys: [k.placement.switchDirectionLockSide], + condition: () => this.beltPlannerActive, + }, + + { + // Place last blueprint + label: T.ingame.keybindingsOverlay.pasteLastBlueprint, + keys: [k.massSelect.pasteLastBlueprint], + condition: () => !this.blueprintPlacementActive && this.lastBlueprintAvailable, + }, + + { + // Belt planner + label: T.ingame.keybindingsOverlay.lockBeltDirection, + keys: [k.placementModifiers.lockBeltDirection], + condition: () => this.buildingPlacementSupportsBeltPlanner && !this.beltPlannerActive, + }, + + { + // [SELECTION] Destroy + label: T.ingame.keybindingsOverlay.delete, + keys: [k.massSelect.confirmMassDelete], + condition: () => this.anythingSelectedOnMap, + }, + + { + // [SELECTION] Cancel + label: T.ingame.keybindingsOverlay.clearSelection, + keys: [k.general.back], + condition: () => this.anythingSelectedOnMap, + }, + { + // [SELECTION] Cut + label: T.ingame.keybindingsOverlay.cutSelection, + keys: [k.massSelect.massSelectCut], + condition: () => this.anythingSelectedOnMap, + }, + + { + // [SELECTION] Copy + label: T.ingame.keybindingsOverlay.copySelection, + keys: [k.massSelect.massSelectCopy], + condition: () => this.anythingSelectedOnMap, + }, + + { + // Switch layers + label: T.ingame.keybindingsOverlay.switchLayers, + keys: [k.ingame.switchLayers], + condition: () => true, + }, + ]; + + if (!this.root.app.settings.getAllSettings().alwaysMultiplace) { + this.keybindings.push({ + // Multiplace + label: T.ingame.keybindingsOverlay.placeMultiple, + keys: [k.placementModifiers.placeMultiple], + condition: () => this.anyPlacementActive && !this.buildingPlacementStaysInPlacement, + }); + } + + this.element = makeDiv(parent, "ingame_HUD_KeybindingOverlay", []); + + for (let i = 0; i < this.keybindings.length; ++i) { + let html = ""; + const handle = this.keybindings[i]; + + for (let k = 0; k < handle.keys.length; ++k) { + const key = handle.keys[k]; + + switch (key) { + case KEYCODE_LMB: + html += ``; + break; + case KEYCODE_RMB: + html += ``; + break; + case KEYCODE_MMB: + html += ``; + break; + case DIVIDER_TOKEN: + html += ``; + break; + case ADDER_TOKEN: + html += `+`; + break; + default: + html += `${getStringForKeyCode( + mapper.getBinding(/** @type {KeyCode} */ (key)).keyCode + )}`; + } + } + html += ``; + + handle.cachedElement = makeDiv(this.element, null, ["binding"], html); + handle.cachedVisibility = false; + } + } + + initialize() { + this.domAttach = new DynamicDomAttach(this.root, this.element, { + trackHover: true, + }); + } + + update() { + for (let i = 0; i < this.keybindings.length; ++i) { + const handle = this.keybindings[i]; + const visibility = handle.condition(); + if (visibility !== handle.cachedVisibility) { + handle.cachedVisibility = visibility; + handle.cachedElement.classList.toggle("visible", visibility); + } + } + + // Required for hover + this.domAttach.update(true); + } +}