From 0377c6d58f23a0b5bcc679ca6ee1b352aa8e035e Mon Sep 17 00:00:00 2001 From: tobspr Date: Sun, 13 Sep 2020 18:39:06 +0200 Subject: [PATCH] Highlight connected miners, improve miner performance --- src/js/changelog.js | 1 + src/js/game/components/miner.js | 8 + src/js/game/hud/hud.js | 558 ++++++++++++----------- src/js/game/hud/parts/miner_highlight.js | 170 +++++++ src/js/game/map_view.js | 38 -- src/js/game/systems/miner.js | 84 +++- src/js/game/themes/dark.json | 7 + src/js/game/themes/light.json | 9 +- translations/base-en.yaml | 6 + 9 files changed, 548 insertions(+), 333 deletions(-) create mode 100644 src/js/game/hud/parts/miner_highlight.js diff --git a/src/js/changelog.js b/src/js/changelog.js index ad3ed34c..bbc4a4fa 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -17,6 +17,7 @@ export const CHANGELOG = [ "Added a button to the statistics dialog to disable the sorting (by squeek502)", "Tier 2 tunnels are now 9 tiles wide, so the gap between is 8 tiles (double the tier 1 range)", "Updated and added new translations (Thanks to all contributors!)", + "Show connected chained miners on hover", "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 (inspired by mvb005)", diff --git a/src/js/game/components/miner.js b/src/js/game/components/miner.js index d2f3ef2d..5b818afb 100644 --- a/src/js/game/components/miner.js +++ b/src/js/game/components/miner.js @@ -1,6 +1,7 @@ import { types } from "../../savegame/serialization"; import { BaseItem } from "../base_item"; import { Component } from "../component"; +import { Entity } from "../entity"; import { typeItemSingleton } from "../item_resolver"; const chainBufferSize = 6; @@ -40,6 +41,13 @@ export class MinerComponent extends Component { * @type {BaseItem} */ this.cachedMinedItem = null; + + /** + * Which miner this miner ejects to, in case its a chainable one. + * If the value is false, it means there is no entity, and we don't have to re-check + * @type {Entity|null|false} + */ + this.cachedChainedMiner = null; } /** diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index c1aa146e..3edc4e17 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -1,277 +1,281 @@ -/* typehints:start */ -import { GameRoot } from "../root"; -/* typehints:end */ - -/* dev:start */ -import { TrailerMaker } from "./trailer_maker"; -/* dev:end */ - -import { Signal } from "../../core/signal"; -import { DrawParameters } from "../../core/draw_parameters"; -import { HUDBuildingsToolbar } from "./parts/buildings_toolbar"; -import { HUDBuildingPlacer } from "./parts/building_placer"; -import { HUDBlueprintPlacer } from "./parts/blueprint_placer"; -import { HUDKeybindingOverlay } from "./parts/keybinding_overlay"; -import { HUDUnlockNotification } from "./parts/unlock_notification"; -import { HUDGameMenu } from "./parts/game_menu"; -import { HUDShop } from "./parts/shop"; -import { IS_MOBILE, globalConfig, IS_DEMO } from "../../core/config"; -import { HUDMassSelector } from "./parts/mass_selector"; -import { HUDVignetteOverlay } from "./parts/vignette_overlay"; -import { HUDStatistics } from "./parts/statistics"; -import { MetaBuilding } from "../meta_building"; -import { HUDPinnedShapes } from "./parts/pinned_shapes"; -import { ShapeDefinition } from "../shape_definition"; -import { HUDNotifications, enumNotificationType } from "./parts/notifications"; -import { HUDSettingsMenu } from "./parts/settings_menu"; -import { HUDDebugInfo } from "./parts/debug_info"; -import { HUDEntityDebugger } from "./parts/entity_debugger"; -import { KEYMAPPINGS } from "../key_action_mapper"; -import { HUDWatermark } from "./parts/watermark"; -import { HUDModalDialogs } from "./parts/modal_dialogs"; -import { HUDPartTutorialHints } from "./parts/tutorial_hints"; -import { HUDWaypoints } from "./parts/waypoints"; -import { HUDInteractiveTutorial } from "./parts/interactive_tutorial"; -import { HUDScreenshotExporter } from "./parts/screenshot_exporter"; -import { HUDColorBlindHelper } from "./parts/color_blind_helper"; -import { HUDShapeViewer } from "./parts/shape_viewer"; -import { HUDWiresOverlay } from "./parts/wires_overlay"; -import { HUDChangesDebugger } from "./parts/debug_changes"; -import { queryParamOptions } from "../../core/query_parameters"; -import { HUDSandboxController } from "./parts/sandbox_controller"; -import { HUDWiresToolbar } from "./parts/wires_toolbar"; -import { HUDWireInfo } from "./parts/wire_info"; -import { HUDLeverToggle } from "./parts/lever_toggle"; -import { HUDLayerPreview } from "./parts/layer_preview"; - -export class GameHUD { - /** - * @param {GameRoot} root - */ - constructor(root) { - this.root = root; - } - - /** - * Initializes the hud parts - */ - initialize() { - this.parts = { - buildingsToolbar: new HUDBuildingsToolbar(this.root), - wiresToolbar: new HUDWiresToolbar(this.root), - blueprintPlacer: new HUDBlueprintPlacer(this.root), - buildingPlacer: new HUDBuildingPlacer(this.root), - unlockNotification: new HUDUnlockNotification(this.root), - gameMenu: new HUDGameMenu(this.root), - massSelector: new HUDMassSelector(this.root), - shop: new HUDShop(this.root), - statistics: new HUDStatistics(this.root), - waypoints: new HUDWaypoints(this.root), - wireInfo: new HUDWireInfo(this.root), - leverToggle: new HUDLeverToggle(this.root), - - // Must always exist - pinnedShapes: new HUDPinnedShapes(this.root), - notifications: new HUDNotifications(this.root), - settingsMenu: new HUDSettingsMenu(this.root), - // betaOverlay: new HUDBetaOverlay(this.root), - debugInfo: new HUDDebugInfo(this.root), - dialogs: new HUDModalDialogs(this.root), - screenshotExporter: new HUDScreenshotExporter(this.root), - shapeViewer: new HUDShapeViewer(this.root), - - wiresOverlay: new HUDWiresOverlay(this.root), - layerPreview: new HUDLayerPreview(this.root), - - // Typing hints - /* typehints:start */ - /** @type {HUDChangesDebugger} */ - changesDebugger: null, - /* typehints:end */ - }; - - this.signals = { - buildingSelectedForPlacement: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()), - selectedPlacementBuildingChanged: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()), - shapePinRequested: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), - shapeUnpinRequested: /** @type {TypedSignal<[string]>} */ (new Signal()), - notification: /** @type {TypedSignal<[string, enumNotificationType]>} */ (new Signal()), - buildingsSelectedForCopy: /** @type {TypedSignal<[Array]>} */ (new Signal()), - pasteBlueprintRequested: /** @type {TypedSignal<[]>} */ (new Signal()), - viewShapeDetailsRequested: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), - }; - - if (!IS_MOBILE) { - this.parts.keybindingOverlay = new HUDKeybindingOverlay(this.root); - } - - if (G_IS_DEV && globalConfig.debug.enableEntityInspector) { - this.parts.entityDebugger = new HUDEntityDebugger(this.root); - } - - if (IS_DEMO) { - this.parts.watermark = new HUDWatermark(this.root); - } - - if (G_IS_DEV && globalConfig.debug.renderChanges) { - this.parts.changesDebugger = new HUDChangesDebugger(this.root); - } - - if (this.root.app.settings.getAllSettings().offerHints) { - this.parts.tutorialHints = new HUDPartTutorialHints(this.root); - this.parts.interactiveTutorial = new HUDInteractiveTutorial(this.root); - } - - if (this.root.app.settings.getAllSettings().vignette) { - this.parts.vignetteOverlay = new HUDVignetteOverlay(this.root); - } - - if (this.root.app.settings.getAllSettings().enableColorBlindHelper) { - this.parts.colorBlindHelper = new HUDColorBlindHelper(this.root); - } - - if (queryParamOptions.sandboxMode || G_IS_DEV) { - this.parts.sandboxController = new HUDSandboxController(this.root); - } - - const frag = document.createDocumentFragment(); - for (const key in this.parts) { - this.parts[key].createElements(frag); - } - - document.body.appendChild(frag); - - for (const key in this.parts) { - this.parts[key].initialize(); - } - - this.root.keyMapper.getBinding(KEYMAPPINGS.ingame.toggleHud).add(this.toggleUi, this); - - /* dev:start */ - if (G_IS_DEV && globalConfig.debug.renderForTrailer) { - this.trailerMaker = new TrailerMaker(this.root); - } - /* dev:end*/ - } - - /** - * Attempts to close all overlays - */ - closeAllOverlays() { - for (const key in this.parts) { - this.parts[key].close(); - } - } - - /** - * Returns true if the game logic should be paused - */ - shouldPauseGame() { - for (const key in this.parts) { - if (this.parts[key].shouldPauseGame()) { - return true; - } - } - return false; - } - - /** - * Returns true if the rendering can be paused - */ - shouldPauseRendering() { - for (const key in this.parts) { - if (this.parts[key].shouldPauseRendering()) { - return true; - } - } - return false; - } - - /** - * Returns true if the rendering can be paused - */ - hasBlockingOverlayOpen() { - if (this.root.camera.getIsMapOverlayActive()) { - return true; - } - for (const key in this.parts) { - if (this.parts[key].isBlockingOverlay()) { - return true; - } - } - return false; - } - - /** - * Toggles the ui - */ - toggleUi() { - document.body.classList.toggle("uiHidden"); - } - - /** - * Updates all parts - */ - update() { - if (!this.root.gameInitialized) { - return; - } - - for (const key in this.parts) { - this.parts[key].update(); - } - - /* dev:start */ - if (this.trailerMaker) { - this.trailerMaker.update(); - } - /* dev:end*/ - } - - /** - * Draws all parts - * @param {DrawParameters} parameters - */ - draw(parameters) { - const partsOrder = [ - "massSelector", - "buildingPlacer", - "blueprintPlacer", - "colorBlindHelper", - "changesDebugger", - ]; - - for (let i = 0; i < partsOrder.length; ++i) { - if (this.parts[partsOrder[i]]) { - this.parts[partsOrder[i]].draw(parameters); - } - } - } - - /** - * Draws all part overlays - * @param {DrawParameters} parameters - */ - drawOverlays(parameters) { - const partsOrder = ["waypoints", "watermark", "wireInfo"]; - - for (let i = 0; i < partsOrder.length; ++i) { - if (this.parts[partsOrder[i]]) { - this.parts[partsOrder[i]].drawOverlays(parameters); - } - } - } - - /** - * Cleans up everything - */ - cleanup() { - for (const key in this.parts) { - this.parts[key].cleanup(); - } - - for (const key in this.signals) { - this.signals[key].removeAll(); - } - } -} +/* typehints:start */ +import { GameRoot } from "../root"; +/* typehints:end */ + +/* dev:start */ +import { TrailerMaker } from "./trailer_maker"; +/* dev:end */ + +import { Signal } from "../../core/signal"; +import { DrawParameters } from "../../core/draw_parameters"; +import { HUDBuildingsToolbar } from "./parts/buildings_toolbar"; +import { HUDBuildingPlacer } from "./parts/building_placer"; +import { HUDBlueprintPlacer } from "./parts/blueprint_placer"; +import { HUDKeybindingOverlay } from "./parts/keybinding_overlay"; +import { HUDUnlockNotification } from "./parts/unlock_notification"; +import { HUDGameMenu } from "./parts/game_menu"; +import { HUDShop } from "./parts/shop"; +import { IS_MOBILE, globalConfig, IS_DEMO } from "../../core/config"; +import { HUDMassSelector } from "./parts/mass_selector"; +import { HUDVignetteOverlay } from "./parts/vignette_overlay"; +import { HUDStatistics } from "./parts/statistics"; +import { MetaBuilding } from "../meta_building"; +import { HUDPinnedShapes } from "./parts/pinned_shapes"; +import { ShapeDefinition } from "../shape_definition"; +import { HUDNotifications, enumNotificationType } from "./parts/notifications"; +import { HUDSettingsMenu } from "./parts/settings_menu"; +import { HUDDebugInfo } from "./parts/debug_info"; +import { HUDEntityDebugger } from "./parts/entity_debugger"; +import { KEYMAPPINGS } from "../key_action_mapper"; +import { HUDWatermark } from "./parts/watermark"; +import { HUDModalDialogs } from "./parts/modal_dialogs"; +import { HUDPartTutorialHints } from "./parts/tutorial_hints"; +import { HUDWaypoints } from "./parts/waypoints"; +import { HUDInteractiveTutorial } from "./parts/interactive_tutorial"; +import { HUDScreenshotExporter } from "./parts/screenshot_exporter"; +import { HUDColorBlindHelper } from "./parts/color_blind_helper"; +import { HUDShapeViewer } from "./parts/shape_viewer"; +import { HUDWiresOverlay } from "./parts/wires_overlay"; +import { HUDChangesDebugger } from "./parts/debug_changes"; +import { queryParamOptions } from "../../core/query_parameters"; +import { HUDSandboxController } from "./parts/sandbox_controller"; +import { HUDWiresToolbar } from "./parts/wires_toolbar"; +import { HUDWireInfo } from "./parts/wire_info"; +import { HUDLeverToggle } from "./parts/lever_toggle"; +import { HUDLayerPreview } from "./parts/layer_preview"; +import { HUDMinerHighlight } from "./parts/miner_highlight"; + +export class GameHUD { + /** + * @param {GameRoot} root + */ + constructor(root) { + this.root = root; + } + + /** + * Initializes the hud parts + */ + initialize() { + this.parts = { + buildingsToolbar: new HUDBuildingsToolbar(this.root), + wiresToolbar: new HUDWiresToolbar(this.root), + blueprintPlacer: new HUDBlueprintPlacer(this.root), + buildingPlacer: new HUDBuildingPlacer(this.root), + unlockNotification: new HUDUnlockNotification(this.root), + gameMenu: new HUDGameMenu(this.root), + massSelector: new HUDMassSelector(this.root), + shop: new HUDShop(this.root), + statistics: new HUDStatistics(this.root), + waypoints: new HUDWaypoints(this.root), + wireInfo: new HUDWireInfo(this.root), + leverToggle: new HUDLeverToggle(this.root), + + // Must always exist + pinnedShapes: new HUDPinnedShapes(this.root), + notifications: new HUDNotifications(this.root), + settingsMenu: new HUDSettingsMenu(this.root), + // betaOverlay: new HUDBetaOverlay(this.root), + debugInfo: new HUDDebugInfo(this.root), + dialogs: new HUDModalDialogs(this.root), + screenshotExporter: new HUDScreenshotExporter(this.root), + shapeViewer: new HUDShapeViewer(this.root), + + wiresOverlay: new HUDWiresOverlay(this.root), + layerPreview: new HUDLayerPreview(this.root), + + minerHighlight: new HUDMinerHighlight(this.root), + + // Typing hints + /* typehints:start */ + /** @type {HUDChangesDebugger} */ + changesDebugger: null, + /* typehints:end */ + }; + + this.signals = { + buildingSelectedForPlacement: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()), + selectedPlacementBuildingChanged: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()), + shapePinRequested: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), + shapeUnpinRequested: /** @type {TypedSignal<[string]>} */ (new Signal()), + notification: /** @type {TypedSignal<[string, enumNotificationType]>} */ (new Signal()), + buildingsSelectedForCopy: /** @type {TypedSignal<[Array]>} */ (new Signal()), + pasteBlueprintRequested: /** @type {TypedSignal<[]>} */ (new Signal()), + viewShapeDetailsRequested: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), + }; + + if (!IS_MOBILE) { + this.parts.keybindingOverlay = new HUDKeybindingOverlay(this.root); + } + + if (G_IS_DEV && globalConfig.debug.enableEntityInspector) { + this.parts.entityDebugger = new HUDEntityDebugger(this.root); + } + + if (IS_DEMO) { + this.parts.watermark = new HUDWatermark(this.root); + } + + if (G_IS_DEV && globalConfig.debug.renderChanges) { + this.parts.changesDebugger = new HUDChangesDebugger(this.root); + } + + if (this.root.app.settings.getAllSettings().offerHints) { + this.parts.tutorialHints = new HUDPartTutorialHints(this.root); + this.parts.interactiveTutorial = new HUDInteractiveTutorial(this.root); + } + + if (this.root.app.settings.getAllSettings().vignette) { + this.parts.vignetteOverlay = new HUDVignetteOverlay(this.root); + } + + if (this.root.app.settings.getAllSettings().enableColorBlindHelper) { + this.parts.colorBlindHelper = new HUDColorBlindHelper(this.root); + } + + if (queryParamOptions.sandboxMode || G_IS_DEV) { + this.parts.sandboxController = new HUDSandboxController(this.root); + } + + const frag = document.createDocumentFragment(); + for (const key in this.parts) { + this.parts[key].createElements(frag); + } + + document.body.appendChild(frag); + + for (const key in this.parts) { + this.parts[key].initialize(); + } + + this.root.keyMapper.getBinding(KEYMAPPINGS.ingame.toggleHud).add(this.toggleUi, this); + + /* dev:start */ + if (G_IS_DEV && globalConfig.debug.renderForTrailer) { + this.trailerMaker = new TrailerMaker(this.root); + } + /* dev:end*/ + } + + /** + * Attempts to close all overlays + */ + closeAllOverlays() { + for (const key in this.parts) { + this.parts[key].close(); + } + } + + /** + * Returns true if the game logic should be paused + */ + shouldPauseGame() { + for (const key in this.parts) { + if (this.parts[key].shouldPauseGame()) { + return true; + } + } + return false; + } + + /** + * Returns true if the rendering can be paused + */ + shouldPauseRendering() { + for (const key in this.parts) { + if (this.parts[key].shouldPauseRendering()) { + return true; + } + } + return false; + } + + /** + * Returns true if the rendering can be paused + */ + hasBlockingOverlayOpen() { + if (this.root.camera.getIsMapOverlayActive()) { + return true; + } + for (const key in this.parts) { + if (this.parts[key].isBlockingOverlay()) { + return true; + } + } + return false; + } + + /** + * Toggles the ui + */ + toggleUi() { + document.body.classList.toggle("uiHidden"); + } + + /** + * Updates all parts + */ + update() { + if (!this.root.gameInitialized) { + return; + } + + for (const key in this.parts) { + this.parts[key].update(); + } + + /* dev:start */ + if (this.trailerMaker) { + this.trailerMaker.update(); + } + /* dev:end*/ + } + + /** + * Draws all parts + * @param {DrawParameters} parameters + */ + draw(parameters) { + const partsOrder = [ + "massSelector", + "buildingPlacer", + "blueprintPlacer", + "colorBlindHelper", + "changesDebugger", + "minerHighlight", + ]; + + for (let i = 0; i < partsOrder.length; ++i) { + if (this.parts[partsOrder[i]]) { + this.parts[partsOrder[i]].draw(parameters); + } + } + } + + /** + * Draws all part overlays + * @param {DrawParameters} parameters + */ + drawOverlays(parameters) { + const partsOrder = ["waypoints", "watermark", "wireInfo"]; + + for (let i = 0; i < partsOrder.length; ++i) { + if (this.parts[partsOrder[i]]) { + this.parts[partsOrder[i]].drawOverlays(parameters); + } + } + } + + /** + * Cleans up everything + */ + cleanup() { + for (const key in this.parts) { + this.parts[key].cleanup(); + } + + for (const key in this.signals) { + this.signals[key].removeAll(); + } + } +} diff --git a/src/js/game/hud/parts/miner_highlight.js b/src/js/game/hud/parts/miner_highlight.js new file mode 100644 index 00000000..c2b23583 --- /dev/null +++ b/src/js/game/hud/parts/miner_highlight.js @@ -0,0 +1,170 @@ +import { globalConfig } from "../../../core/config"; +import { formatItemsPerSecond, round2Digits } from "../../../core/utils"; +import { Vector } from "../../../core/vector"; +import { T } from "../../../translations"; +import { Entity } from "../../entity"; +import { THEME } from "../../theme"; +import { BaseHUDPart } from "../base_hud_part"; + +export class HUDMinerHighlight extends BaseHUDPart { + initialize() {} + + /** + * + * @param {import("../../../core/draw_utils").DrawParameters} parameters + */ + draw(parameters) { + const mousePos = this.root.app.mousePosition; + if (!mousePos) { + // Mouse pos not ready + return; + } + + if (this.root.currentLayer !== "regular") { + // Not within the regular layer + return; + } + + if (this.root.camera.getIsMapOverlayActive()) { + // Not within the map overlay + return; + } + + const worldPos = this.root.camera.screenToWorld(mousePos); + const hoveredTile = worldPos.toTileSpace(); + + const contents = this.root.map.getTileContent(hoveredTile, "regular"); + if (!contents) { + // Empty tile + return; + } + + const minerComp = contents.components.Miner; + if (!minerComp || !minerComp.chainable) { + // Not a chainable miner + return; + } + + parameters.context.fillStyle = THEME.map.connectedMiners.overlay; + + const connectedEntities = this.findConnectedMiners(contents); + + for (let i = 0; i < connectedEntities.length; ++i) { + const entity = connectedEntities[i]; + const staticComp = entity.components.StaticMapEntity; + + parameters.context.beginRoundedRect( + staticComp.origin.x * globalConfig.tileSize + 5, + staticComp.origin.y * globalConfig.tileSize + 5, + globalConfig.tileSize - 10, + globalConfig.tileSize - 10, + 3 + ); + parameters.context.fill(); + } + + const throughput = round2Digits(connectedEntities.length * this.root.hubGoals.getMinerBaseSpeed()); + + const maxThroughput = this.root.hubGoals.getBeltBaseSpeed(); + + const screenPos = this.root.camera.screenToWorld(mousePos); + + const scale = (1 / this.root.camera.zoomLevel) * this.root.app.getEffectiveUiScale(); + + const isCapped = throughput > maxThroughput; + + // Background + parameters.context.fillStyle = THEME.map.connectedMiners.background; + parameters.context.beginRoundedRect( + screenPos.x + 5 * scale, + screenPos.y - 3 * scale, + (isCapped ? 100 : 65) * scale, + (isCapped ? 45 : 30) * scale, + 2 + ); + parameters.context.fill(); + + // Throughput + parameters.context.fillStyle = THEME.map.connectedMiners.textColor; + parameters.context.font = "bold " + scale * 10 + "px GameFont"; + parameters.context.fillText( + formatItemsPerSecond(throughput), + screenPos.x + 10 * scale, + screenPos.y + 10 * scale + ); + + // Amount of miners + parameters.context.globalAlpha = 0.6; + parameters.context.font = "bold " + scale * 8 + "px GameFont"; + parameters.context.fillText( + connectedEntities.length === 1 + ? T.ingame.connectedMiners.one_miner + : T.ingame.connectedMiners.n_miners.replace("", String(connectedEntities.length)), + screenPos.x + 10 * scale, + screenPos.y + 22 * scale + ); + + parameters.context.globalAlpha = 1; + + if (isCapped) { + parameters.context.fillStyle = THEME.map.connectedMiners.textColorCapped; + parameters.context.fillText( + T.ingame.connectedMiners.limited_items.replace( + "", + formatItemsPerSecond(maxThroughput) + ), + screenPos.x + 10 * scale, + screenPos.y + 34 * scale + ); + } + } + + /** + * Finds all connected miners to the given entity + * @param {Entity} entity + * @param {Set} seenUids Which entities have already been processed + * @returns {Array} The connected miners + */ + findConnectedMiners(entity, seenUids = new Set()) { + let results = []; + const origin = entity.components.StaticMapEntity.origin; + + if (!seenUids.has(entity.uid)) { + seenUids.add(entity.uid); + results.push(entity); + } + + // Check for the miner which we connect to + const connectedMiner = this.root.systemMgr.systems.miner.findChainedMiner(entity); + if (connectedMiner && !seenUids.has(connectedMiner.uid)) { + results.push(connectedMiner); + seenUids.add(connectedMiner.uid); + results.push(...this.findConnectedMiners(connectedMiner, seenUids)); + } + + // Search within a 1x1 grid - this assumes miners are always 1x1 + for (let dx = -1; dx <= 1; ++dx) { + for (let dy = -1; dy <= 1; ++dy) { + const contents = this.root.map.getTileContent( + new Vector(origin.x + dx, origin.y + dy), + "regular" + ); + if (contents) { + const minerComp = contents.components.Miner; + if (minerComp && minerComp.chainable) { + // Found a miner connected to this entity + if (!seenUids.has(contents.uid)) { + if (this.root.systemMgr.systems.miner.findChainedMiner(contents) === entity) { + results.push(contents); + seenUids.add(contents.uid); + results.push(...this.findConnectedMiners(contents, seenUids)); + } + } + } + } + } + } + + return results; + } +} diff --git a/src/js/game/map_view.js b/src/js/game/map_view.js index 491f8c32..0e0f3d5b 100644 --- a/src/js/game/map_view.js +++ b/src/js/game/map_view.js @@ -212,43 +212,5 @@ export class MapView extends BaseMap { } this.drawVisibleChunks(parameters, MapChunkView.prototype.drawBackgroundLayer); - - if (G_IS_DEV && globalConfig.debug.showChunkBorders) { - const cullRange = parameters.visibleRect.toTileCullRectangle(); - const top = cullRange.top(); - const right = cullRange.right(); - const bottom = cullRange.bottom(); - const left = cullRange.left(); - - const border = 1; - const minY = top - border; - const maxY = bottom + border; - const minX = left - border; - const maxX = right + border - 1; - - const chunkStartX = Math.floor(minX / globalConfig.mapChunkSize); - const chunkStartY = Math.floor(minY / globalConfig.mapChunkSize); - - const chunkEndX = Math.ceil(maxX / globalConfig.mapChunkSize); - const chunkEndY = Math.ceil(maxY / globalConfig.mapChunkSize); - - for (let chunkX = chunkStartX; chunkX <= chunkEndX; ++chunkX) { - for (let chunkY = chunkStartY; chunkY <= chunkEndY; ++chunkY) { - parameters.context.fillStyle = "#ffaaaa"; - parameters.context.fillRect( - chunkX * globalConfig.mapChunkWorldSize, - chunkY * globalConfig.mapChunkWorldSize, - globalConfig.mapChunkWorldSize, - 3 - ); - parameters.context.fillRect( - chunkX * globalConfig.mapChunkWorldSize, - chunkY * globalConfig.mapChunkWorldSize, - 3, - globalConfig.mapChunkWorldSize - ); - } - } - } } } diff --git a/src/js/game/systems/miner.js b/src/js/game/systems/miner.js index 9f966306..94f2791e 100644 --- a/src/js/game/systems/miner.js +++ b/src/js/game/systems/miner.js @@ -10,6 +10,24 @@ import { MapChunkView } from "../map_chunk_view"; export class MinerSystem extends GameSystemWithFilter { constructor(root) { super(root, [MinerComponent]); + + this.needsRecompute = true; + + this.root.signals.entityAdded.add(this.onEntityChanged, this); + this.root.signals.entityChanged.add(this.onEntityChanged, this); + this.root.signals.entityDestroyed.add(this.onEntityChanged, this); + } + + /** + * Called whenever an entity got changed + * @param {Entity} entity + */ + onEntityChanged(entity) { + const minerComp = entity.components.Miner; + if (minerComp && minerComp.chainable) { + // Miner component, need to recompute + this.needsRecompute = true; + } } update() { @@ -20,11 +38,15 @@ export class MinerSystem extends GameSystemWithFilter { for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; + const minerComp = entity.components.Miner; + + // Reset everything on recompute + if (this.needsRecompute) { + minerComp.cachedChainedMiner = null; + } // Check if miner is above an actual tile - const minerComp = entity.components.Miner; - if (!minerComp.cachedMinedItem) { const staticComp = entity.components.StaticMapEntity; const tileBelow = this.root.map.getLowerLayerContentXY( @@ -59,6 +81,36 @@ export class MinerSystem extends GameSystemWithFilter { } } } + + // After this frame we are done + this.needsRecompute = false; + } + + /** + * Finds the target chained miner for a given entity + * @param {Entity} entity + * @returns {Entity|false} The chained entity or null if not found + */ + findChainedMiner(entity) { + const ejectComp = entity.components.ItemEjector; + const staticComp = entity.components.StaticMapEntity; + + const ejectingSlot = ejectComp.slots[0]; + const ejectingPos = staticComp.localTileToWorld(ejectingSlot.pos); + const ejectingDirection = staticComp.localDirectionToWorld(ejectingSlot.direction); + + const targetTile = ejectingPos.add(enumDirectionToVector[ejectingDirection]); + const targetContents = this.root.map.getTileContent(targetTile, "regular"); + + // Check if we are connected to another miner and thus do not eject directly + if (targetContents) { + const targetMinerComp = targetContents.components.Miner; + if (targetMinerComp && targetMinerComp.chainable) { + return targetContents; + } + } + + return false; } /** @@ -69,26 +121,23 @@ export class MinerSystem extends GameSystemWithFilter { tryPerformMinerEject(entity, item) { const minerComp = entity.components.Miner; const ejectComp = entity.components.ItemEjector; - const staticComp = entity.components.StaticMapEntity; // Check if we are a chained miner if (minerComp.chainable) { - const ejectingSlot = ejectComp.slots[0]; - const ejectingPos = staticComp.localTileToWorld(ejectingSlot.pos); - const ejectingDirection = staticComp.localDirectionToWorld(ejectingSlot.direction); + const targetEntity = minerComp.cachedChainedMiner; - const targetTile = ejectingPos.add(enumDirectionToVector[ejectingDirection]); - const targetContents = this.root.map.getTileContent(targetTile, "regular"); + // Check if the cache has to get recomputed + if (targetEntity === null) { + minerComp.cachedChainedMiner = this.findChainedMiner(entity); + } - // Check if we are connected to another miner and thus do not eject directly - if (targetContents) { - const targetMinerComp = targetContents.components.Miner; - if (targetMinerComp) { - if (targetMinerComp.tryAcceptChainedItem(item)) { - return true; - } else { - return false; - } + // Check if we now have a target + if (targetEntity) { + const targetMinerComp = targetEntity.components.Miner; + if (targetMinerComp.tryAcceptChainedItem(item)) { + return true; + } else { + return false; } } } @@ -97,6 +146,7 @@ export class MinerSystem extends GameSystemWithFilter { if (ejectComp.tryEject(0, item)) { return true; } + return false; } diff --git a/src/js/game/themes/dark.json b/src/js/game/themes/dark.json index 28d00eba..7466201f 100644 --- a/src/js/game/themes/dark.json +++ b/src/js/game/themes/dark.json @@ -39,6 +39,13 @@ "overlayColor": "rgba(97, 161, 152, 0.75)", "previewColor": "rgb(97, 161, 152, 0.5)", "highlightColor": "rgba(0, 0, 255, 0.5)" + }, + + "connectedMiners": { + "overlay": "rgba(40, 50, 60, 0.5)", + "textColor": "#fff", + "textColorCapped": "#ef5072", + "background": "rgba(40, 50, 60, 0.8)" } }, diff --git a/src/js/game/themes/light.json b/src/js/game/themes/light.json index 2097bfc3..728135ab 100644 --- a/src/js/game/themes/light.json +++ b/src/js/game/themes/light.json @@ -39,7 +39,14 @@ "wires": { "overlayColor": "rgba(97, 161, 152, 0.75)", "previewColor": "rgb(97, 161, 152, 0.4)", - "highlightColor": "rgba(72, 137, 255, 0.8)" + "highlightColor": "rgba(72, 137, 255, 1)" + }, + + "connectedMiners": { + "overlay": "rgba(40, 50, 60, 0.5)", + "textColor": "#fff", + "textColorCapped": "#ef5072", + "background": "rgba(40, 50, 60, 0.8)" } }, diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 35ca0fba..28fef7e2 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -426,6 +426,12 @@ ingame: 1_3_expand: >- This is NOT an idle game! Build more extractors and belts to finish the goal quicker.

Tip: Hold SHIFT to place multiple extractors, and use R to rotate them. + # Connected miners + connectedMiners: + one_miner: 1 Miner + n_miners: Miners + limited_items: Limited to + # All shop upgrades shopUpgrades: belt: