diff --git a/src/js/changelog.js b/src/js/changelog.js index 07902b80..72c4bf45 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -1,300 +1,302 @@ -export const CHANGELOG = [ - { - version: "1.2.0", - date: "unreleased", - entries: [ - "WIRES", - "Reworked menu UI design (by dengr1605)", - "Allow holding ALT in belt planner to reverse direction (by jakobhellermann)", - "Clear cursor when trying to pipette the same building twice (by hexy)", - "Fixed level 18 stacker bug: If you experienced it already, you know it, if not, I don't want to spoiler (by hexy)", - "Added keybinding to close menus (by isaisstillalive / Sandwichs-del)", - "Fix rare crash regarding the buildings toolbar (by isaisstillalive)", - "Fixed some phrases (by EnderDoom77)", - "Zoom towards mouse cursor (by Dimava)", - "Added multiple settings to optimize the performance", - "Updated the soundtrack again, it is now 40 minutes in total!", - "Updated and added new translations (Thanks to all contributors!)", - "Allow editing waypoints (by isaisstillalive)", - "Show confirmation when cutting area which is too expensive to get pasted again (by isaisstillalive)", - "Show mouse and camera tile on debug overlay (F4) (by dengr)", - "Fix tunnels entrances connecting to exits sometimes when they shouldn't", - "The initial belt planner direction is now based on the cursor movement (by MizardX)", - "Fix preferred variant not getting saved when clicking on the hud (by Danacus)", - ], - }, - { - version: "1.1.19", - date: "02.07.2020", - entries: [ - "There are now notifications every 15 minutes in the demo version to buy the full version (For further details and the reason, check the #surveys channel in the Discord)", - "I'm still working on the wires update, I hope to release it mid july!", - ], - }, - { - version: "1.1.18", - date: "27.06.2020", - entries: [ - "Huge performance improvements - up to double fps and tick-rate! This will wipe out all current items on belts.", - "Reduce story shapes required until unlocking blueprints", - "Allow clicking on variants to select them", - "Add 'copy key' button to shape viewer", - "Add more FPS to the belt animation and fix belt animation seeming to go 'backwards' on high belt speeds", - "Fix deconstruct sound being played when right clicking hub", - "Allow clicking 'Q' over a shape or color patch to automatically select the miner building (by Gerdon262)", - "Update belt placement performance on huge factories (by Phlosioneer)", - "Fix duplicate waypoints with a shape not rendering (by hexy)", - "Fix smart tunnel placement deleting wrong tunnels (by mordof)", - "Add setting (on by default) to store the last used rotation per building instead of globally storing it (by Magos)", - "Added chinese (traditional) translation", - "Updated translations", - ], - }, - { - version: "1.1.17", - date: "22.06.2020", - entries: [ - "Color blind mode! You can now activate it in the settings and it will show you which color is below your cursor (Either resource or on the belt)", - "Add info buttons to all shapes so you can figure out how they are built! (And also, which colors they have)", - "Allow configuring autosave interval and disabling it in the settings", - "The smart-tunnel placement has been reworked to properly replace belts. Thus the setting has been turned on again by default", - "The soundtrack now has a higher quality on the standalone version than the web version", - "Add setting to disable cut/delete warnings (by hexy)", - "Fix bug where belts in blueprints don't orient correctly (by hexy)", - "Fix camera moving weird after dragging and holding (by hexy)", - "Fix keybinding for pipette showing while pasting blueprints", - "Improve visibility of shape background in dark mode", - "Added sound when destroying a building", - "Added swedish translation", - "Update tutorial image for tier 2 tunnels to explain mix/match (by jimmyshadow1)", - ], - }, - { - version: "1.1.16", - date: "21.06.2020", - entries: [ - "You can now pickup buildings below your cursor with 'Q'!", - "The game soundtrack has been extended! There are now 4 songs with over 13 minutes of playtime from Peppsen!", - "Refactor keybindings overlay to show more appropriate keybindings", - "Show keybindings for area-select in the upper left instead", - "Automatically deselect area when selecting a new building", - "Raise markers limit from 14 characters to 71 (by Joker-vD)", - "Optimize performance by caching extractor items (by Phlosioneer)", - "Added setting to enable compact building infos, which only show ratios and hide the image / description", - "Apply dark theme to menu as well (by dengr1065)", - "Fix belt planner not placing the last belt", - "Fix buildings getting deleted when right clicking while placing a blueprint", - "Fix for exporting screenshots for huge bases (It was showing an empty file) (by xSparfuchs)", - "Fix buttons not responding when using right click directly after left click (by davidburhans)", - "Fix hub marker being hidden by building info panel", - "Disable dialog background blur since it can cause performance issues", - "Added simplified chinese translations", - "Update translations (Thanks to all translators!)", - ], - }, - { - version: "1.1.15", - date: "17.06.2020", - entries: [ - "You can now place straight belts (and tunnels) by holding SHIFT! (For you, @giantwaffle ❤️)", - "Added continue button to main menu and add seperate 'New game' button (by jaysc)", - "Added setting to disable smart tunnel placement introduced with the last update", - "Added setting to disable vignette", - "Update translations", - ], - }, - { - version: "1.1.14", - date: "16.06.2020", - entries: [ - "There is now an indicator (compass) to the HUB for the HUB Marker!", - "You can now include shape short keys in markers to render shape icons instead of text!", - "Added mirrored variant of the painter", - "When placing tunnels, unnecessary belts inbetween are now removed!", - "You can now drag tunnels and they will automatically expand! (Just try it out, its intuitive)", - ], - }, - { - version: "1.1.13", - date: "15.06.2020", - entries: [ - "Added shift modifier for faster pan (by jaysc)", - "Added Japanese translations", - "Added Portuguese (Portugal) translations", - "Updated icon for Spanish (Latin America) - It was showing a Spanish flag before", - "Updated existing translations", - ], - }, - { - version: "1.1.12", - date: "14.06.2020", - entries: [ - "Huge performance improvements! The game should now run up to 60% faster!", - "Added norwegian translation", - ], - }, - { - version: "1.1.11", - date: "13.06.2020", - entries: [ - "Pinned shapes are now smart, they dynamically update their goal and also unpin when no longer required. Completed objectives are now rendered transparent.", - "You can now cut areas, and also paste the last blueprint again! (by hexy)", - "You can now export your whole base as an image by pressing F3!", - "Improve upgrade number rounding, so there are no goals like '37.4k', instead it will now be '35k'", - "You can now configure the camera movement speed when using WASD (by mini-bomba)", - "Selecting an area now is relative to the world and thus does not move when moving the screen (by Dimava)", - "Allow higher tick-rates up to 500hz (This will burn your PC!)", - "Fix bug regarding number rounding", - "Fix dialog text being hardly readable in dark theme", - "Fix app not starting when the savegames were corrupted - there is now a better error message as well.", - "Further translation updates - Big thanks to all contributors!", - ], - }, - { - version: "1.1.10", - date: "12.06.2020", - entries: [ - "There are now linux builds on steam! Please report any issues in the Discord!", - "Steam cloud saves are now available!", - "Added and update more translations (Big thank you to all translators!)", - "Prevent invalid connection if existing underground tunnel entrance exists (by jaysc)", - ], - }, - { - version: "1.1.9", - date: "11.06.2020", - entries: [ - "Support for translations! Interested in helping out? Check out the translation guide!", - "Update stacker artwork to clarify how it works", - "Update keybinding hints on the top left to be more accurate", - "Make it more clear when blueprints are unlocked when trying to use them", - "Fix pinned shape icons not being visible in dark mode", - "Fix being able to select buildings via hotkeys in map overview mode", - "Make shapes unpinnable in the upgrades tab (By hexy)", - ], - }, - { - version: "1.1.8", - date: "07.06.2020", - entries: [ - "You can now purchase the standalone on steam! View steam page", - "Added ability to create markers in the demo, but only two.", - "Contest #01 has ended! I'll now work through the entries, select the 5 I like most and present them to the community to vote for!", - ], - }, - { - version: "1.1.7", - date: "04.06.2020", - entries: ["HOTFIX: Fix savegames not showing up on the standalone version"], - }, - { - version: "1.1.6", - date: "04.06.2020", - entries: [ - "The steam release will happen on the 7th of June - Be sure to add it to your wishlist! View on steam", - "Fixed level complete dialog being blurred when the shop was opened before", - "Standalone: Increased icon visibility for windows builds", - "Web version: Fixed firefox not loading the game when browsing in private mode", - ], - }, - - { - version: "1.1.5", - date: "03.06.2020", - entries: ["Added weekly contests!"], - }, - { - version: "1.1.4", - date: "01.06.2020", - entries: ["Add 'interactive' tutorial for the first level to improve onboarding experience"], - }, - { - version: "1.1.3", - date: "01.06.2020", - entries: [ - "Added setting to configure zoom / mouse wheel / touchpad sensitivity", - "Fix belts being too slow when copied via blueprint (by Dimava)", - "Allow binding mouse buttons to actions (by Dimava)", - "Increase readability of certain HUD elements", - ], - }, - { - version: "1.1.2", - date: "30.05.2020", - entries: [ - "The official trailer is now ready! Check it out here!", - "The steam page is now live!", - "Experimental linux builds are now available! Please give me feedback on them in the Discord", - "Allow hovering pinned shapes to enlarge them", - "Allow deselecting blueprints with right click and 'Q'", - "Move default key for deleting from 'X' to 'DEL'", - "Show confirmation when deleting more than 100 buildings", - "Reintroduce 'SPACE' keybinding to center on map", - "Improved keybinding hints", - "Fixed some keybindings showing as 'undefined'", - ], - }, - { - version: "1.1.1", - date: "28.05.2020", - entries: ["Fix crash when 'Show Hints' setting was turned off"], - }, - { - version: "1.1.0", - date: "28.05.2020", - entries: [ - "BLUEPRINTS! They are unlocked at level 12 and cost a special shape to build.", - "MAP MARKERS! Press 'M' to create a waypoint and be able to jump to it", - "Savegame levels are now shown in the main menu. For existing games, save them again to make the level show up.", - "Allow holding SHIFT to rotate counter clockwise", - "Added confirmation when deleting more than 500 buildings at a time", - "Added background to toolbar to increase contrast", - "Further decerase requirements of first levels", - "Pinned shapes now are saved", - "Allow placing extractors anywhere again, but they don't work at all if not placed on a resource", - "Show dialog explaining some keybindings after completing level 4", - "Fix keys being stuck when opening a dialog", - "Swapped shape order for painting upgrades", - "Allow changing all keybindings, including CTRL, ALT and SHIFT (by Dimava)", - "Fix cycling through keybindings selecting locked buildings as well (by Dimava)", - "There is now a github action, checking all pull requests with eslint. (by mrHedgehog)", - ], - }, - { - version: "1.0.4", - date: "26.05.2020", - entries: [ - "Reduce cost of first painting upgrade, and change 'Shape Processing' to 'Cutting, Rotating & Stacking'", - "Add dialog after completing level 2 to check out the upgrades tab.", - "Allow changing the keybindings in the demo version", - ], - }, - { - version: "1.0.3", - date: "24.05.2020", - entries: [ - "Reduced the amount of shapes required for the first 5 levels to make it easier to get into the game.", - ], - }, - { - version: "1.0.2", - date: "23.05.2020", - entries: [ - "Introduced changelog", - "Removed 'early access' label because the game isn't actually early access - its in a pretty good state already! (No worries, a lot more updates will follow!)", - "Added a 'Show hint' button which shows a small video for almost all levels to help out", - "Now showing proper descriptions when completing levels, with instructions on what the gained reward does.", - "Show a landing page on mobile devices about the game not being ready to be played on mobile yet", - "Fix painters and mixers being affected by the shape processors upgrade and not the painter one", - "Added 'multiplace' setting which is equivalent to holding SHIFT all the time", - "Added keybindings to zoom in / zoom out", - "Tunnels now also show connection lines to tunnel exits, instead of just tunnel entries", - "Lots of minor fixes and improvements", - ], - }, - { - version: "1.0.1", - date: "21.05.2020", - entries: ["Initial release!"], - }, -]; +export const CHANGELOG = [ + { + version: "1.2.0", + date: "unreleased", + entries: [ + "WIRES", + "Reworked menu UI design (by dengr1605)", + "Allow holding ALT in belt planner to reverse direction (by jakobhellermann)", + "Clear cursor when trying to pipette the same building twice (by hexy)", + "Fixed level 18 stacker bug: If you experienced it already, you know it, if not, I don't want to spoiler (by hexy)", + "Added keybinding to close menus (by isaisstillalive / Sandwichs-del)", + "Fix rare crash regarding the buildings toolbar (by isaisstillalive)", + "Fixed some phrases (by EnderDoom77)", + "Zoom towards mouse cursor (by Dimava)", + "Added multiple settings to optimize the performance", + "Updated the soundtrack again, it is now 40 minutes in total!", + "Added a button to the statistics dialog to disable the sorting (by squeek502)", + "Updated and added new translations (Thanks to all contributors!)", + "Added setting to be able to delete buildings while placing (inspired by hexy)", + "Allow editing waypoints (by isaisstillalive)", + "Show confirmation when cutting area which is too expensive to get pasted again (by isaisstillalive)", + "Show mouse and camera tile on debug overlay (F4) (by dengr)", + "Fix tunnels entrances connecting to exits sometimes when they shouldn't", + "The initial belt planner direction is now based on the cursor movement (by MizardX)", + "Fix preferred variant not getting saved when clicking on the hud (by Danacus)", + ], + }, + { + version: "1.1.19", + date: "02.07.2020", + entries: [ + "There are now notifications every 15 minutes in the demo version to buy the full version (For further details and the reason, check the #surveys channel in the Discord)", + "I'm still working on the wires update, I hope to release it mid july!", + ], + }, + { + version: "1.1.18", + date: "27.06.2020", + entries: [ + "Huge performance improvements - up to double fps and tick-rate! This will wipe out all current items on belts.", + "Reduce story shapes required until unlocking blueprints", + "Allow clicking on variants to select them", + "Add 'copy key' button to shape viewer", + "Add more FPS to the belt animation and fix belt animation seeming to go 'backwards' on high belt speeds", + "Fix deconstruct sound being played when right clicking hub", + "Allow clicking 'Q' over a shape or color patch to automatically select the miner building (by Gerdon262)", + "Update belt placement performance on huge factories (by Phlosioneer)", + "Fix duplicate waypoints with a shape not rendering (by hexy)", + "Fix smart tunnel placement deleting wrong tunnels (by mordof)", + "Add setting (on by default) to store the last used rotation per building instead of globally storing it (by Magos)", + "Added chinese (traditional) translation", + "Updated translations", + ], + }, + { + version: "1.1.17", + date: "22.06.2020", + entries: [ + "Color blind mode! You can now activate it in the settings and it will show you which color is below your cursor (Either resource or on the belt)", + "Add info buttons to all shapes so you can figure out how they are built! (And also, which colors they have)", + "Allow configuring autosave interval and disabling it in the settings", + "The smart-tunnel placement has been reworked to properly replace belts. Thus the setting has been turned on again by default", + "The soundtrack now has a higher quality on the standalone version than the web version", + "Add setting to disable cut/delete warnings (by hexy)", + "Fix bug where belts in blueprints don't orient correctly (by hexy)", + "Fix camera moving weird after dragging and holding (by hexy)", + "Fix keybinding for pipette showing while pasting blueprints", + "Improve visibility of shape background in dark mode", + "Added sound when destroying a building", + "Added swedish translation", + "Update tutorial image for tier 2 tunnels to explain mix/match (by jimmyshadow1)", + ], + }, + { + version: "1.1.16", + date: "21.06.2020", + entries: [ + "You can now pickup buildings below your cursor with 'Q'!", + "The game soundtrack has been extended! There are now 4 songs with over 13 minutes of playtime from Peppsen!", + "Refactor keybindings overlay to show more appropriate keybindings", + "Show keybindings for area-select in the upper left instead", + "Automatically deselect area when selecting a new building", + "Raise markers limit from 14 characters to 71 (by Joker-vD)", + "Optimize performance by caching extractor items (by Phlosioneer)", + "Added setting to enable compact building infos, which only show ratios and hide the image / description", + "Apply dark theme to menu as well (by dengr1065)", + "Fix belt planner not placing the last belt", + "Fix buildings getting deleted when right clicking while placing a blueprint", + "Fix for exporting screenshots for huge bases (It was showing an empty file) (by xSparfuchs)", + "Fix buttons not responding when using right click directly after left click (by davidburhans)", + "Fix hub marker being hidden by building info panel", + "Disable dialog background blur since it can cause performance issues", + "Added simplified chinese translations", + "Update translations (Thanks to all translators!)", + ], + }, + { + version: "1.1.15", + date: "17.06.2020", + entries: [ + "You can now place straight belts (and tunnels) by holding SHIFT! (For you, @giantwaffle ❤️)", + "Added continue button to main menu and add seperate 'New game' button (by jaysc)", + "Added setting to disable smart tunnel placement introduced with the last update", + "Added setting to disable vignette", + "Update translations", + ], + }, + { + version: "1.1.14", + date: "16.06.2020", + entries: [ + "There is now an indicator (compass) to the HUB for the HUB Marker!", + "You can now include shape short keys in markers to render shape icons instead of text!", + "Added mirrored variant of the painter", + "When placing tunnels, unnecessary belts inbetween are now removed!", + "You can now drag tunnels and they will automatically expand! (Just try it out, its intuitive)", + ], + }, + { + version: "1.1.13", + date: "15.06.2020", + entries: [ + "Added shift modifier for faster pan (by jaysc)", + "Added Japanese translations", + "Added Portuguese (Portugal) translations", + "Updated icon for Spanish (Latin America) - It was showing a Spanish flag before", + "Updated existing translations", + ], + }, + { + version: "1.1.12", + date: "14.06.2020", + entries: [ + "Huge performance improvements! The game should now run up to 60% faster!", + "Added norwegian translation", + ], + }, + { + version: "1.1.11", + date: "13.06.2020", + entries: [ + "Pinned shapes are now smart, they dynamically update their goal and also unpin when no longer required. Completed objectives are now rendered transparent.", + "You can now cut areas, and also paste the last blueprint again! (by hexy)", + "You can now export your whole base as an image by pressing F3!", + "Improve upgrade number rounding, so there are no goals like '37.4k', instead it will now be '35k'", + "You can now configure the camera movement speed when using WASD (by mini-bomba)", + "Selecting an area now is relative to the world and thus does not move when moving the screen (by Dimava)", + "Allow higher tick-rates up to 500hz (This will burn your PC!)", + "Fix bug regarding number rounding", + "Fix dialog text being hardly readable in dark theme", + "Fix app not starting when the savegames were corrupted - there is now a better error message as well.", + "Further translation updates - Big thanks to all contributors!", + ], + }, + { + version: "1.1.10", + date: "12.06.2020", + entries: [ + "There are now linux builds on steam! Please report any issues in the Discord!", + "Steam cloud saves are now available!", + "Added and update more translations (Big thank you to all translators!)", + "Prevent invalid connection if existing underground tunnel entrance exists (by jaysc)", + ], + }, + { + version: "1.1.9", + date: "11.06.2020", + entries: [ + "Support for translations! Interested in helping out? Check out the translation guide!", + "Update stacker artwork to clarify how it works", + "Update keybinding hints on the top left to be more accurate", + "Make it more clear when blueprints are unlocked when trying to use them", + "Fix pinned shape icons not being visible in dark mode", + "Fix being able to select buildings via hotkeys in map overview mode", + "Make shapes unpinnable in the upgrades tab (By hexy)", + ], + }, + { + version: "1.1.8", + date: "07.06.2020", + entries: [ + "You can now purchase the standalone on steam! View steam page", + "Added ability to create markers in the demo, but only two.", + "Contest #01 has ended! I'll now work through the entries, select the 5 I like most and present them to the community to vote for!", + ], + }, + { + version: "1.1.7", + date: "04.06.2020", + entries: ["HOTFIX: Fix savegames not showing up on the standalone version"], + }, + { + version: "1.1.6", + date: "04.06.2020", + entries: [ + "The steam release will happen on the 7th of June - Be sure to add it to your wishlist! View on steam", + "Fixed level complete dialog being blurred when the shop was opened before", + "Standalone: Increased icon visibility for windows builds", + "Web version: Fixed firefox not loading the game when browsing in private mode", + ], + }, + + { + version: "1.1.5", + date: "03.06.2020", + entries: ["Added weekly contests!"], + }, + { + version: "1.1.4", + date: "01.06.2020", + entries: ["Add 'interactive' tutorial for the first level to improve onboarding experience"], + }, + { + version: "1.1.3", + date: "01.06.2020", + entries: [ + "Added setting to configure zoom / mouse wheel / touchpad sensitivity", + "Fix belts being too slow when copied via blueprint (by Dimava)", + "Allow binding mouse buttons to actions (by Dimava)", + "Increase readability of certain HUD elements", + ], + }, + { + version: "1.1.2", + date: "30.05.2020", + entries: [ + "The official trailer is now ready! Check it out here!", + "The steam page is now live!", + "Experimental linux builds are now available! Please give me feedback on them in the Discord", + "Allow hovering pinned shapes to enlarge them", + "Allow deselecting blueprints with right click and 'Q'", + "Move default key for deleting from 'X' to 'DEL'", + "Show confirmation when deleting more than 100 buildings", + "Reintroduce 'SPACE' keybinding to center on map", + "Improved keybinding hints", + "Fixed some keybindings showing as 'undefined'", + ], + }, + { + version: "1.1.1", + date: "28.05.2020", + entries: ["Fix crash when 'Show Hints' setting was turned off"], + }, + { + version: "1.1.0", + date: "28.05.2020", + entries: [ + "BLUEPRINTS! They are unlocked at level 12 and cost a special shape to build.", + "MAP MARKERS! Press 'M' to create a waypoint and be able to jump to it", + "Savegame levels are now shown in the main menu. For existing games, save them again to make the level show up.", + "Allow holding SHIFT to rotate counter clockwise", + "Added confirmation when deleting more than 500 buildings at a time", + "Added background to toolbar to increase contrast", + "Further decerase requirements of first levels", + "Pinned shapes now are saved", + "Allow placing extractors anywhere again, but they don't work at all if not placed on a resource", + "Show dialog explaining some keybindings after completing level 4", + "Fix keys being stuck when opening a dialog", + "Swapped shape order for painting upgrades", + "Allow changing all keybindings, including CTRL, ALT and SHIFT (by Dimava)", + "Fix cycling through keybindings selecting locked buildings as well (by Dimava)", + "There is now a github action, checking all pull requests with eslint. (by mrHedgehog)", + ], + }, + { + version: "1.0.4", + date: "26.05.2020", + entries: [ + "Reduce cost of first painting upgrade, and change 'Shape Processing' to 'Cutting, Rotating & Stacking'", + "Add dialog after completing level 2 to check out the upgrades tab.", + "Allow changing the keybindings in the demo version", + ], + }, + { + version: "1.0.3", + date: "24.05.2020", + entries: [ + "Reduced the amount of shapes required for the first 5 levels to make it easier to get into the game.", + ], + }, + { + version: "1.0.2", + date: "23.05.2020", + entries: [ + "Introduced changelog", + "Removed 'early access' label because the game isn't actually early access - its in a pretty good state already! (No worries, a lot more updates will follow!)", + "Added a 'Show hint' button which shows a small video for almost all levels to help out", + "Now showing proper descriptions when completing levels, with instructions on what the gained reward does.", + "Show a landing page on mobile devices about the game not being ready to be played on mobile yet", + "Fix painters and mixers being affected by the shape processors upgrade and not the painter one", + "Added 'multiplace' setting which is equivalent to holding SHIFT all the time", + "Added keybindings to zoom in / zoom out", + "Tunnels now also show connection lines to tunnel exits, instead of just tunnel entries", + "Lots of minor fixes and improvements", + ], + }, + { + version: "1.0.1", + date: "21.05.2020", + entries: ["Initial release!"], + }, +]; diff --git a/src/js/game/hud/parts/building_placer_logic.js b/src/js/game/hud/parts/building_placer_logic.js index 898801c0..686e6d1c 100644 --- a/src/js/game/hud/parts/building_placer_logic.js +++ b/src/js/game/hud/parts/building_placer_logic.js @@ -1,775 +1,778 @@ -import { globalConfig } from "../../../core/config"; -import { gMetaBuildingRegistry } from "../../../core/global_registries"; -import { Signal, STOP_PROPAGATION } from "../../../core/signal"; -import { TrackedState } from "../../../core/tracked_state"; -import { Vector } from "../../../core/vector"; -import { enumMouseButton } from "../../camera"; -import { StaticMapEntityComponent } from "../../components/static_map_entity"; -import { Entity } from "../../entity"; -import { KEYMAPPINGS } from "../../key_action_mapper"; -import { defaultBuildingVariant, MetaBuilding } from "../../meta_building"; -import { BaseHUDPart } from "../base_hud_part"; -import { SOUNDS } from "../../../platform/sound"; -import { MetaMinerBuilding, enumMinerVariants } from "../../buildings/miner"; -import { enumHubGoalRewards } from "../../tutorial_goals"; -import { getBuildingDataFromCode, getCodeFromBuildingData } from "../../building_codes"; -import { MetaHubBuilding } from "../../buildings/hub"; - -/** - * Contains all logic for the building placer - this doesn't include the rendering - * of info boxes or drawing. - */ -export class HUDBuildingPlacerLogic extends BaseHUDPart { - /** - * Initializes the logic - * @see BaseHUDPart.initialize - */ - initialize() { - /** - * We use a fake entity to get information about how a building will look - * once placed - * @type {Entity} - */ - this.fakeEntity = null; - - // Signals - this.signals = { - variantChanged: new Signal(), - draggingStarted: new Signal(), - }; - - /** - * The current building - * @type {TypedTrackedState} - */ - this.currentMetaBuilding = new TrackedState(this.onSelectedMetaBuildingChanged, this); - - /** - * The current rotation - * @type {number} - */ - this.currentBaseRotationGeneral = 0; - - /** - * The current rotation preference for each building. - * @type{Object.} - */ - this.preferredBaseRotations = {}; - - /** - * Whether we are currently dragging - * @type {boolean} - */ - this.currentlyDragging = false; - - /** - * Current building variant - * @type {TypedTrackedState} - */ - this.currentVariant = new TrackedState(() => this.signals.variantChanged.dispatch()); - - /** - * Whether we are currently drag-deleting - * @type {boolean} - */ - this.currentlyDeleting = false; - - /** - * Stores which variants for each building we prefer, this is based on what - * the user last selected - * @type {Object.} - */ - this.preferredVariants = {}; - - /** - * The tile we last dragged from - * @type {Vector} - */ - this.lastDragTile = null; - - /** - * The side for direction lock - * @type {number} (0|1) - */ - this.currentDirectionLockSide = 0; - - /** - * Whether the side for direction lock has not yet been determined. - * @type {boolean} - */ - this.currentDirectionLockSideIndeterminate = true; - - this.initializeBindings(); - } - - /** - * Initializes all bindings - */ - initializeBindings() { - // KEYBINDINGS - const keyActionMapper = this.root.keyMapper; - keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.tryRotate, this); - keyActionMapper.getBinding(KEYMAPPINGS.placement.cycleBuildingVariants).add(this.cycleVariants, this); - keyActionMapper - .getBinding(KEYMAPPINGS.placement.switchDirectionLockSide) - .add(this.switchDirectionLockSide, this); - keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this); - keyActionMapper.getBinding(KEYMAPPINGS.placement.pipette).add(this.startPipette, this); - this.root.gameState.inputReciever.keyup.add(this.checkForDirectionLockSwitch, this); - - // BINDINGS TO GAME EVENTS - this.root.hud.signals.buildingsSelectedForCopy.add(this.abortPlacement, this); - this.root.hud.signals.pasteBlueprintRequested.add(this.abortPlacement, this); - this.root.signals.storyGoalCompleted.add(() => this.signals.variantChanged.dispatch()); - this.root.signals.upgradePurchased.add(() => this.signals.variantChanged.dispatch()); - this.root.signals.editModeChanged.add(this.onEditModeChanged, this); - - // MOUSE BINDINGS - this.root.camera.downPreHandler.add(this.onMouseDown, this); - this.root.camera.movePreHandler.add(this.onMouseMove, this); - this.root.camera.upPostHandler.add(this.onMouseUp, this); - } - - /** - * Called when the edit mode got changed - * @param {Layer} layer - */ - onEditModeChanged(layer) { - const metaBuilding = this.currentMetaBuilding.get(); - if (metaBuilding) { - if (metaBuilding.getLayer() !== layer) { - // This layer doesn't fit the edit mode anymore - this.currentMetaBuilding.set(null); - } - } - } - - /** - * Returns the current base rotation for the current meta-building. - * @returns {number} - */ - get currentBaseRotation() { - if (!this.root.app.settings.getAllSettings().rotationByBuilding) { - return this.currentBaseRotationGeneral; - } - const metaBuilding = this.currentMetaBuilding.get(); - if (metaBuilding && this.preferredBaseRotations.hasOwnProperty(metaBuilding.getId())) { - return this.preferredBaseRotations[metaBuilding.getId()]; - } else { - return this.currentBaseRotationGeneral; - } - } - - /** - * Sets the base rotation for the current meta-building. - * @param {number} rotation The new rotation/angle. - */ - set currentBaseRotation(rotation) { - if (!this.root.app.settings.getAllSettings().rotationByBuilding) { - this.currentBaseRotationGeneral = rotation; - } else { - const metaBuilding = this.currentMetaBuilding.get(); - if (metaBuilding) { - this.preferredBaseRotations[metaBuilding.getId()] = rotation; - } else { - this.currentBaseRotationGeneral = rotation; - } - } - } - - /** - * Returns if the direction lock is currently active - * @returns {boolean} - */ - get isDirectionLockActive() { - const metaBuilding = this.currentMetaBuilding.get(); - return ( - metaBuilding && - metaBuilding.getHasDirectionLockAvailable() && - this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.lockBeltDirection).pressed - ); - } - - /** - * Returns the current direction lock corner, that is, the corner between - * mouse and original start point - * @returns {Vector|null} - */ - get currentDirectionLockCorner() { - const mousePosition = this.root.app.mousePosition; - if (!mousePosition) { - // Not on screen - return null; - } - - if (!this.lastDragTile) { - // Haven't dragged yet - return null; - } - - // Figure which points the line visits - const worldPos = this.root.camera.screenToWorld(mousePosition); - const mouseTile = worldPos.toTileSpace(); - - // Figure initial direction - const dx = Math.abs(this.lastDragTile.x - mouseTile.x); - const dy = Math.abs(this.lastDragTile.y - mouseTile.y); - if (dx === 0 && dy === 0) { - // Back at the start. Try a new direction. - this.currentDirectionLockSideIndeterminate = true; - } else if (this.currentDirectionLockSideIndeterminate) { - this.currentDirectionLockSideIndeterminate = false; - this.currentDirectionLockSide = dx <= dy ? 0 : 1; - } - - if (this.currentDirectionLockSide === 0) { - return new Vector(this.lastDragTile.x, mouseTile.y); - } else { - return new Vector(mouseTile.x, this.lastDragTile.y); - } - } - - /** - * Aborts the placement - */ - abortPlacement() { - if (this.currentMetaBuilding.get()) { - this.currentMetaBuilding.set(null); - return STOP_PROPAGATION; - } - } - - /** - * Aborts any dragging - */ - abortDragging() { - this.currentlyDragging = true; - this.currentlyDeleting = false; - this.initialPlacementVector = null; - this.lastDragTile = null; - } - - /** - * @see BaseHUDPart.update - */ - update() { - // Always update since the camera might have moved - const mousePos = this.root.app.mousePosition; - if (mousePos) { - this.onMouseMove(mousePos); - } - - // Make sure we have nothing selected while in overview mode - if (this.root.camera.getIsMapOverlayActive()) { - if (this.currentMetaBuilding.get()) { - this.currentMetaBuilding.set(null); - } - } - } - - /** - * Tries to rotate the current building - */ - tryRotate() { - const selectedBuilding = this.currentMetaBuilding.get(); - if (selectedBuilding) { - if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).pressed) { - this.currentBaseRotation = (this.currentBaseRotation + 270) % 360; - } else { - this.currentBaseRotation = (this.currentBaseRotation + 90) % 360; - } - const staticComp = this.fakeEntity.components.StaticMapEntity; - staticComp.rotation = this.currentBaseRotation; - } - } - /** - * Tries to delete the building under the mouse - */ - deleteBelowCursor() { - const mousePosition = this.root.app.mousePosition; - if (!mousePosition) { - // Not on screen - return false; - } - - const worldPos = this.root.camera.screenToWorld(mousePosition); - const tile = worldPos.toTileSpace(); - const contents = this.root.map.getTileContent(tile, this.root.currentLayer); - if (contents) { - if (this.root.logic.tryDeleteBuilding(contents)) { - this.root.soundProxy.playUi(SOUNDS.destroyBuilding); - return true; - } - } - return false; - } - - /** - * Starts the pipette function - */ - startPipette() { - // Disable in overview - if (this.root.camera.getIsMapOverlayActive()) { - return; - } - - const mousePosition = this.root.app.mousePosition; - if (!mousePosition) { - // Not on screen - return; - } - - const worldPos = this.root.camera.screenToWorld(mousePosition); - const tile = worldPos.toTileSpace(); - - const contents = this.root.map.getTileContent(tile, this.root.currentLayer); - if (!contents) { - const tileBelow = this.root.map.getLowerLayerContentXY(tile.x, tile.y); - - // Check if there's a shape or color item below, if so select the miner - if (tileBelow) { - this.currentMetaBuilding.set(gMetaBuildingRegistry.findByClass(MetaMinerBuilding)); - - // Select chained miner if available, since thats always desired once unlocked - if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_miner_chainable)) { - this.currentVariant.set(enumMinerVariants.chainable); - } - } else { - this.currentMetaBuilding.set(null); - } - return; - } - - // Try to extract the building - const buildingCode = contents.components.StaticMapEntity.code; - const extracted = getBuildingDataFromCode(buildingCode); - - // Disable pipetting the hub - if (extracted.metaInstance.getId() === gMetaBuildingRegistry.findByClass(MetaHubBuilding).getId()) { - this.currentMetaBuilding.set(null); - return; - } - - // If the building we are picking is the same as the one we have, clear the cursor. - if ( - this.currentMetaBuilding.get() && - extracted.metaInstance.getId() === this.currentMetaBuilding.get().getId() && - extracted.variant === this.currentVariant.get() - ) { - this.currentMetaBuilding.set(null); - return; - } - - this.currentMetaBuilding.set(extracted.metaInstance); - this.currentVariant.set(extracted.variant); - this.currentBaseRotation = contents.components.StaticMapEntity.rotation; - } - - /** - * Switches the side for the direction lock manually - */ - switchDirectionLockSide() { - this.currentDirectionLockSide = 1 - this.currentDirectionLockSide; - } - - /** - * Checks if the direction lock key got released and if such, resets the placement - * @param {any} args - */ - checkForDirectionLockSwitch({ keyCode }) { - if ( - keyCode === - this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.lockBeltDirection).keyCode - ) { - this.abortDragging(); - } - } - - /** - * Tries to place the current building at the given tile - * @param {Vector} tile - */ - tryPlaceCurrentBuildingAt(tile) { - if (this.root.camera.zoomLevel < globalConfig.mapChunkOverviewMinZoom) { - // Dont allow placing in overview mode - return; - } - - const metaBuilding = this.currentMetaBuilding.get(); - const { rotation, rotationVariant } = metaBuilding.computeOptimalDirectionAndRotationVariantAtTile({ - root: this.root, - tile, - rotation: this.currentBaseRotation, - variant: this.currentVariant.get(), - layer: metaBuilding.getLayer(), - }); - - const entity = this.root.logic.tryPlaceBuilding({ - origin: tile, - rotation, - rotationVariant, - originalRotation: this.currentBaseRotation, - building: this.currentMetaBuilding.get(), - variant: this.currentVariant.get(), - }); - - if (entity) { - // Succesfully placed, find which entity we actually placed - this.root.signals.entityManuallyPlaced.dispatch(entity); - - // Check if we should flip the orientation (used for tunnels) - if ( - metaBuilding.getFlipOrientationAfterPlacement() && - !this.root.keyMapper.getBinding( - KEYMAPPINGS.placementModifiers.placementDisableAutoOrientation - ).pressed - ) { - this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; - } - - // Check if we should stop placement - if ( - !metaBuilding.getStayInPlacementMode() && - !this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeMultiple).pressed && - !this.root.app.settings.getAllSettings().alwaysMultiplace - ) { - // Stop placement - this.currentMetaBuilding.set(null); - } - return true; - } else { - return false; - } - } - - /** - * Cycles through the variants - */ - cycleVariants() { - const metaBuilding = this.currentMetaBuilding.get(); - if (!metaBuilding) { - this.currentVariant.set(defaultBuildingVariant); - } else { - const availableVariants = metaBuilding.getAvailableVariants(this.root); - const index = availableVariants.indexOf(this.currentVariant.get()); - assert( - index >= 0, - "Current variant was invalid: " + this.currentVariant.get() + " out of " + availableVariants - ); - const newIndex = (index + 1) % availableVariants.length; - const newVariant = availableVariants[newIndex]; - this.setVariant(newVariant); - } - } - - /** - * Sets the current variant to the given variant - * @param {string} variant - */ - setVariant(variant) { - const metaBuilding = this.currentMetaBuilding.get(); - this.currentVariant.set(variant); - - this.preferredVariants[metaBuilding.getId()] = variant; - } - - /** - * Performs the direction locked placement between two points after - * releasing the mouse - */ - executeDirectionLockedPlacement() { - const metaBuilding = this.currentMetaBuilding.get(); - if (!metaBuilding) { - // No active building - return; - } - - // Get path to place - const path = this.computeDirectionLockPath(); - - // Store if we placed anything - let anythingPlaced = false; - - // Perform this in bulk to avoid recalculations - this.root.logic.performBulkOperation(() => { - for (let i = 0; i < path.length; ++i) { - const { rotation, tile } = path[i]; - this.currentBaseRotation = rotation; - if (this.tryPlaceCurrentBuildingAt(tile)) { - anythingPlaced = true; - } - } - }); - - if (anythingPlaced) { - this.root.soundProxy.playUi(metaBuilding.getPlacementSound()); - } - } - - /** - * Finds the path which the current direction lock will use - * @returns {Array<{ tile: Vector, rotation: number }>} - */ - computeDirectionLockPath() { - const mousePosition = this.root.app.mousePosition; - if (!mousePosition) { - // Not on screen - return []; - } - - let result = []; - - // Figure which points the line visits - const worldPos = this.root.camera.screenToWorld(mousePosition); - let endTile = worldPos.toTileSpace(); - let startTile = this.lastDragTile; - - // if the alt key is pressed, reverse belt planner direction by switching start and end tile - if (this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeInverse).pressed) { - let tmp = startTile; - startTile = endTile; - endTile = tmp; - } - - // Place from start to corner - const pathToCorner = this.currentDirectionLockCorner.sub(startTile); - const deltaToCorner = pathToCorner.normalize().round(); - const lengthToCorner = Math.round(pathToCorner.length()); - let currentPos = startTile.copy(); - - let rotation = (Math.round(Math.degrees(deltaToCorner.angle()) / 90) * 90 + 360) % 360; - - if (lengthToCorner > 0) { - for (let i = 0; i < lengthToCorner; ++i) { - result.push({ - tile: currentPos.copy(), - rotation, - }); - currentPos.addInplace(deltaToCorner); - } - } - - // Place from corner to end - const pathFromCorner = endTile.sub(this.currentDirectionLockCorner); - const deltaFromCorner = pathFromCorner.normalize().round(); - const lengthFromCorner = Math.round(pathFromCorner.length()); - - if (lengthFromCorner > 0) { - rotation = (Math.round(Math.degrees(deltaFromCorner.angle()) / 90) * 90 + 360) % 360; - for (let i = 0; i < lengthFromCorner + 1; ++i) { - result.push({ - tile: currentPos.copy(), - rotation, - }); - currentPos.addInplace(deltaFromCorner); - } - } else { - // Finish last one - result.push({ - tile: currentPos.copy(), - rotation, - }); - } - return result; - } - - /** - * Selects a given building - * @param {MetaBuilding} metaBuilding - */ - startSelection(metaBuilding) { - this.currentMetaBuilding.set(metaBuilding); - } - - /** - * Called when the selected buildings changed - * @param {MetaBuilding} metaBuilding - */ - onSelectedMetaBuildingChanged(metaBuilding) { - this.abortDragging(); - this.root.hud.signals.selectedPlacementBuildingChanged.dispatch(metaBuilding); - if (metaBuilding) { - const variant = this.preferredVariants[metaBuilding.getId()] || defaultBuildingVariant; - this.currentVariant.set(variant); - - this.fakeEntity = new Entity(null); - metaBuilding.setupEntityComponents(this.fakeEntity, null); - - this.fakeEntity.addComponent( - new StaticMapEntityComponent({ - origin: new Vector(0, 0), - rotation: 0, - tileSize: metaBuilding.getDimensions(this.currentVariant.get()).copy(), - code: getCodeFromBuildingData(metaBuilding, variant, 0), - }) - ); - metaBuilding.updateVariants(this.fakeEntity, 0, this.currentVariant.get()); - } else { - this.fakeEntity = null; - } - - // Since it depends on both, rerender twice - this.signals.variantChanged.dispatch(); - } - - /** - * mouse down pre handler - * @param {Vector} pos - * @param {enumMouseButton} button - */ - onMouseDown(pos, button) { - if (this.root.camera.getIsMapOverlayActive()) { - // We do not allow dragging if the overlay is active - return; - } - - const metaBuilding = this.currentMetaBuilding.get(); - - // Placement - if (button === enumMouseButton.left && metaBuilding) { - this.currentlyDragging = true; - this.currentlyDeleting = false; - this.lastDragTile = this.root.camera.screenToWorld(pos).toTileSpace(); - - // Place initial building, but only if direction lock is not active - if (!this.isDirectionLockActive) { - if (this.tryPlaceCurrentBuildingAt(this.lastDragTile)) { - this.root.soundProxy.playUi(metaBuilding.getPlacementSound()); - } - } - return STOP_PROPAGATION; - } - - // Deletion - if (button === enumMouseButton.right && !metaBuilding) { - this.currentlyDragging = true; - this.currentlyDeleting = true; - this.lastDragTile = this.root.camera.screenToWorld(pos).toTileSpace(); - if (this.deleteBelowCursor()) { - return STOP_PROPAGATION; - } - } - - // Cancel placement - if (button === enumMouseButton.right && metaBuilding) { - this.currentMetaBuilding.set(null); - } - } - - /** - * mouse move pre handler - * @param {Vector} pos - */ - onMouseMove(pos) { - if (this.root.camera.getIsMapOverlayActive()) { - return; - } - - // Check for direction lock - if (this.isDirectionLockActive) { - return; - } - - const metaBuilding = this.currentMetaBuilding.get(); - if ((metaBuilding || this.currentlyDeleting) && this.lastDragTile) { - const oldPos = this.lastDragTile; - let newPos = this.root.camera.screenToWorld(pos).toTileSpace(); - - // Check if camera is moving, since then we do nothing - if (this.root.camera.desiredCenter) { - this.lastDragTile = newPos; - return; - } - - // Check if anything changed - if (!oldPos.equals(newPos)) { - // Automatic Direction - if ( - metaBuilding && - metaBuilding.getRotateAutomaticallyWhilePlacing(this.currentVariant.get()) && - !this.root.keyMapper.getBinding( - KEYMAPPINGS.placementModifiers.placementDisableAutoOrientation - ).pressed - ) { - const delta = newPos.sub(oldPos); - const angleDeg = Math.degrees(delta.angle()); - this.currentBaseRotation = (Math.round(angleDeg / 90) * 90 + 360) % 360; - - // Holding alt inverts the placement - if (this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeInverse).pressed) { - this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; - } - } - - // bresenham - let x0 = oldPos.x; - let y0 = oldPos.y; - let x1 = newPos.x; - let y1 = newPos.y; - - var dx = Math.abs(x1 - x0); - var dy = Math.abs(y1 - y0); - var sx = x0 < x1 ? 1 : -1; - var sy = y0 < y1 ? 1 : -1; - var err = dx - dy; - - let anythingPlaced = false; - let anythingDeleted = false; - - while (this.currentlyDeleting || this.currentMetaBuilding.get()) { - if (this.currentlyDeleting) { - // Deletion - const contents = this.root.map.getLayerContentXY(x0, y0, this.root.currentLayer); - if (contents && !contents.queuedForDestroy && !contents.destroyed) { - if (this.root.logic.tryDeleteBuilding(contents)) { - anythingDeleted = true; - } - } - } else { - // Placement - if (this.tryPlaceCurrentBuildingAt(new Vector(x0, y0))) { - anythingPlaced = true; - } - } - - if (x0 === x1 && y0 === y1) break; - var e2 = 2 * err; - if (e2 > -dy) { - err -= dy; - x0 += sx; - } - if (e2 < dx) { - err += dx; - y0 += sy; - } - } - - if (anythingPlaced) { - this.root.soundProxy.playUi(metaBuilding.getPlacementSound()); - } - if (anythingDeleted) { - this.root.soundProxy.playUi(SOUNDS.destroyBuilding); - } - } - - this.lastDragTile = newPos; - return STOP_PROPAGATION; - } - } - - /** - * Mouse up handler - */ - onMouseUp() { - if (this.root.camera.getIsMapOverlayActive()) { - return; - } - - // Check for direction lock - if (this.lastDragTile && this.currentlyDragging && this.isDirectionLockActive) { - this.executeDirectionLockedPlacement(); - } - - this.abortDragging(); - } -} +import { globalConfig } from "../../../core/config"; +import { gMetaBuildingRegistry } from "../../../core/global_registries"; +import { Signal, STOP_PROPAGATION } from "../../../core/signal"; +import { TrackedState } from "../../../core/tracked_state"; +import { Vector } from "../../../core/vector"; +import { enumMouseButton } from "../../camera"; +import { StaticMapEntityComponent } from "../../components/static_map_entity"; +import { Entity } from "../../entity"; +import { KEYMAPPINGS } from "../../key_action_mapper"; +import { defaultBuildingVariant, MetaBuilding } from "../../meta_building"; +import { BaseHUDPart } from "../base_hud_part"; +import { SOUNDS } from "../../../platform/sound"; +import { MetaMinerBuilding, enumMinerVariants } from "../../buildings/miner"; +import { enumHubGoalRewards } from "../../tutorial_goals"; +import { getBuildingDataFromCode, getCodeFromBuildingData } from "../../building_codes"; +import { MetaHubBuilding } from "../../buildings/hub"; + +/** + * Contains all logic for the building placer - this doesn't include the rendering + * of info boxes or drawing. + */ +export class HUDBuildingPlacerLogic extends BaseHUDPart { + /** + * Initializes the logic + * @see BaseHUDPart.initialize + */ + initialize() { + /** + * We use a fake entity to get information about how a building will look + * once placed + * @type {Entity} + */ + this.fakeEntity = null; + + // Signals + this.signals = { + variantChanged: new Signal(), + draggingStarted: new Signal(), + }; + + /** + * The current building + * @type {TypedTrackedState} + */ + this.currentMetaBuilding = new TrackedState(this.onSelectedMetaBuildingChanged, this); + + /** + * The current rotation + * @type {number} + */ + this.currentBaseRotationGeneral = 0; + + /** + * The current rotation preference for each building. + * @type{Object.} + */ + this.preferredBaseRotations = {}; + + /** + * Whether we are currently dragging + * @type {boolean} + */ + this.currentlyDragging = false; + + /** + * Current building variant + * @type {TypedTrackedState} + */ + this.currentVariant = new TrackedState(() => this.signals.variantChanged.dispatch()); + + /** + * Whether we are currently drag-deleting + * @type {boolean} + */ + this.currentlyDeleting = false; + + /** + * Stores which variants for each building we prefer, this is based on what + * the user last selected + * @type {Object.} + */ + this.preferredVariants = {}; + + /** + * The tile we last dragged from + * @type {Vector} + */ + this.lastDragTile = null; + + /** + * The side for direction lock + * @type {number} (0|1) + */ + this.currentDirectionLockSide = 0; + + /** + * Whether the side for direction lock has not yet been determined. + * @type {boolean} + */ + this.currentDirectionLockSideIndeterminate = true; + + this.initializeBindings(); + } + + /** + * Initializes all bindings + */ + initializeBindings() { + // KEYBINDINGS + const keyActionMapper = this.root.keyMapper; + keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.tryRotate, this); + keyActionMapper.getBinding(KEYMAPPINGS.placement.cycleBuildingVariants).add(this.cycleVariants, this); + keyActionMapper + .getBinding(KEYMAPPINGS.placement.switchDirectionLockSide) + .add(this.switchDirectionLockSide, this); + keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this); + keyActionMapper.getBinding(KEYMAPPINGS.placement.pipette).add(this.startPipette, this); + this.root.gameState.inputReciever.keyup.add(this.checkForDirectionLockSwitch, this); + + // BINDINGS TO GAME EVENTS + this.root.hud.signals.buildingsSelectedForCopy.add(this.abortPlacement, this); + this.root.hud.signals.pasteBlueprintRequested.add(this.abortPlacement, this); + this.root.signals.storyGoalCompleted.add(() => this.signals.variantChanged.dispatch()); + this.root.signals.upgradePurchased.add(() => this.signals.variantChanged.dispatch()); + this.root.signals.editModeChanged.add(this.onEditModeChanged, this); + + // MOUSE BINDINGS + this.root.camera.downPreHandler.add(this.onMouseDown, this); + this.root.camera.movePreHandler.add(this.onMouseMove, this); + this.root.camera.upPostHandler.add(this.onMouseUp, this); + } + + /** + * Called when the edit mode got changed + * @param {Layer} layer + */ + onEditModeChanged(layer) { + const metaBuilding = this.currentMetaBuilding.get(); + if (metaBuilding) { + if (metaBuilding.getLayer() !== layer) { + // This layer doesn't fit the edit mode anymore + this.currentMetaBuilding.set(null); + } + } + } + + /** + * Returns the current base rotation for the current meta-building. + * @returns {number} + */ + get currentBaseRotation() { + if (!this.root.app.settings.getAllSettings().rotationByBuilding) { + return this.currentBaseRotationGeneral; + } + const metaBuilding = this.currentMetaBuilding.get(); + if (metaBuilding && this.preferredBaseRotations.hasOwnProperty(metaBuilding.getId())) { + return this.preferredBaseRotations[metaBuilding.getId()]; + } else { + return this.currentBaseRotationGeneral; + } + } + + /** + * Sets the base rotation for the current meta-building. + * @param {number} rotation The new rotation/angle. + */ + set currentBaseRotation(rotation) { + if (!this.root.app.settings.getAllSettings().rotationByBuilding) { + this.currentBaseRotationGeneral = rotation; + } else { + const metaBuilding = this.currentMetaBuilding.get(); + if (metaBuilding) { + this.preferredBaseRotations[metaBuilding.getId()] = rotation; + } else { + this.currentBaseRotationGeneral = rotation; + } + } + } + + /** + * Returns if the direction lock is currently active + * @returns {boolean} + */ + get isDirectionLockActive() { + const metaBuilding = this.currentMetaBuilding.get(); + return ( + metaBuilding && + metaBuilding.getHasDirectionLockAvailable() && + this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.lockBeltDirection).pressed + ); + } + + /** + * Returns the current direction lock corner, that is, the corner between + * mouse and original start point + * @returns {Vector|null} + */ + get currentDirectionLockCorner() { + const mousePosition = this.root.app.mousePosition; + if (!mousePosition) { + // Not on screen + return null; + } + + if (!this.lastDragTile) { + // Haven't dragged yet + return null; + } + + // Figure which points the line visits + const worldPos = this.root.camera.screenToWorld(mousePosition); + const mouseTile = worldPos.toTileSpace(); + + // Figure initial direction + const dx = Math.abs(this.lastDragTile.x - mouseTile.x); + const dy = Math.abs(this.lastDragTile.y - mouseTile.y); + if (dx === 0 && dy === 0) { + // Back at the start. Try a new direction. + this.currentDirectionLockSideIndeterminate = true; + } else if (this.currentDirectionLockSideIndeterminate) { + this.currentDirectionLockSideIndeterminate = false; + this.currentDirectionLockSide = dx <= dy ? 0 : 1; + } + + if (this.currentDirectionLockSide === 0) { + return new Vector(this.lastDragTile.x, mouseTile.y); + } else { + return new Vector(mouseTile.x, this.lastDragTile.y); + } + } + + /** + * Aborts the placement + */ + abortPlacement() { + if (this.currentMetaBuilding.get()) { + this.currentMetaBuilding.set(null); + return STOP_PROPAGATION; + } + } + + /** + * Aborts any dragging + */ + abortDragging() { + this.currentlyDragging = true; + this.currentlyDeleting = false; + this.initialPlacementVector = null; + this.lastDragTile = null; + } + + /** + * @see BaseHUDPart.update + */ + update() { + // Always update since the camera might have moved + const mousePos = this.root.app.mousePosition; + if (mousePos) { + this.onMouseMove(mousePos); + } + + // Make sure we have nothing selected while in overview mode + if (this.root.camera.getIsMapOverlayActive()) { + if (this.currentMetaBuilding.get()) { + this.currentMetaBuilding.set(null); + } + } + } + + /** + * Tries to rotate the current building + */ + tryRotate() { + const selectedBuilding = this.currentMetaBuilding.get(); + if (selectedBuilding) { + if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).pressed) { + this.currentBaseRotation = (this.currentBaseRotation + 270) % 360; + } else { + this.currentBaseRotation = (this.currentBaseRotation + 90) % 360; + } + const staticComp = this.fakeEntity.components.StaticMapEntity; + staticComp.rotation = this.currentBaseRotation; + } + } + /** + * Tries to delete the building under the mouse + */ + deleteBelowCursor() { + const mousePosition = this.root.app.mousePosition; + if (!mousePosition) { + // Not on screen + return false; + } + + const worldPos = this.root.camera.screenToWorld(mousePosition); + const tile = worldPos.toTileSpace(); + const contents = this.root.map.getTileContent(tile, this.root.currentLayer); + if (contents) { + if (this.root.logic.tryDeleteBuilding(contents)) { + this.root.soundProxy.playUi(SOUNDS.destroyBuilding); + return true; + } + } + return false; + } + + /** + * Starts the pipette function + */ + startPipette() { + // Disable in overview + if (this.root.camera.getIsMapOverlayActive()) { + return; + } + + const mousePosition = this.root.app.mousePosition; + if (!mousePosition) { + // Not on screen + return; + } + + const worldPos = this.root.camera.screenToWorld(mousePosition); + const tile = worldPos.toTileSpace(); + + const contents = this.root.map.getTileContent(tile, this.root.currentLayer); + if (!contents) { + const tileBelow = this.root.map.getLowerLayerContentXY(tile.x, tile.y); + + // Check if there's a shape or color item below, if so select the miner + if (tileBelow) { + this.currentMetaBuilding.set(gMetaBuildingRegistry.findByClass(MetaMinerBuilding)); + + // Select chained miner if available, since thats always desired once unlocked + if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_miner_chainable)) { + this.currentVariant.set(enumMinerVariants.chainable); + } + } else { + this.currentMetaBuilding.set(null); + } + return; + } + + // Try to extract the building + const buildingCode = contents.components.StaticMapEntity.code; + const extracted = getBuildingDataFromCode(buildingCode); + + // Disable pipetting the hub + if (extracted.metaInstance.getId() === gMetaBuildingRegistry.findByClass(MetaHubBuilding).getId()) { + this.currentMetaBuilding.set(null); + return; + } + + // If the building we are picking is the same as the one we have, clear the cursor. + if ( + this.currentMetaBuilding.get() && + extracted.metaInstance.getId() === this.currentMetaBuilding.get().getId() && + extracted.variant === this.currentVariant.get() + ) { + this.currentMetaBuilding.set(null); + return; + } + + this.currentMetaBuilding.set(extracted.metaInstance); + this.currentVariant.set(extracted.variant); + this.currentBaseRotation = contents.components.StaticMapEntity.rotation; + } + + /** + * Switches the side for the direction lock manually + */ + switchDirectionLockSide() { + this.currentDirectionLockSide = 1 - this.currentDirectionLockSide; + } + + /** + * Checks if the direction lock key got released and if such, resets the placement + * @param {any} args + */ + checkForDirectionLockSwitch({ keyCode }) { + if ( + keyCode === + this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.lockBeltDirection).keyCode + ) { + this.abortDragging(); + } + } + + /** + * Tries to place the current building at the given tile + * @param {Vector} tile + */ + tryPlaceCurrentBuildingAt(tile) { + if (this.root.camera.zoomLevel < globalConfig.mapChunkOverviewMinZoom) { + // Dont allow placing in overview mode + return; + } + + const metaBuilding = this.currentMetaBuilding.get(); + const { rotation, rotationVariant } = metaBuilding.computeOptimalDirectionAndRotationVariantAtTile({ + root: this.root, + tile, + rotation: this.currentBaseRotation, + variant: this.currentVariant.get(), + layer: metaBuilding.getLayer(), + }); + + const entity = this.root.logic.tryPlaceBuilding({ + origin: tile, + rotation, + rotationVariant, + originalRotation: this.currentBaseRotation, + building: this.currentMetaBuilding.get(), + variant: this.currentVariant.get(), + }); + + if (entity) { + // Succesfully placed, find which entity we actually placed + this.root.signals.entityManuallyPlaced.dispatch(entity); + + // Check if we should flip the orientation (used for tunnels) + if ( + metaBuilding.getFlipOrientationAfterPlacement() && + !this.root.keyMapper.getBinding( + KEYMAPPINGS.placementModifiers.placementDisableAutoOrientation + ).pressed + ) { + this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; + } + + // Check if we should stop placement + if ( + !metaBuilding.getStayInPlacementMode() && + !this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeMultiple).pressed && + !this.root.app.settings.getAllSettings().alwaysMultiplace + ) { + // Stop placement + this.currentMetaBuilding.set(null); + } + return true; + } else { + return false; + } + } + + /** + * Cycles through the variants + */ + cycleVariants() { + const metaBuilding = this.currentMetaBuilding.get(); + if (!metaBuilding) { + this.currentVariant.set(defaultBuildingVariant); + } else { + const availableVariants = metaBuilding.getAvailableVariants(this.root); + const index = availableVariants.indexOf(this.currentVariant.get()); + assert( + index >= 0, + "Current variant was invalid: " + this.currentVariant.get() + " out of " + availableVariants + ); + const newIndex = (index + 1) % availableVariants.length; + const newVariant = availableVariants[newIndex]; + this.setVariant(newVariant); + } + } + + /** + * Sets the current variant to the given variant + * @param {string} variant + */ + setVariant(variant) { + const metaBuilding = this.currentMetaBuilding.get(); + this.currentVariant.set(variant); + + this.preferredVariants[metaBuilding.getId()] = variant; + } + + /** + * Performs the direction locked placement between two points after + * releasing the mouse + */ + executeDirectionLockedPlacement() { + const metaBuilding = this.currentMetaBuilding.get(); + if (!metaBuilding) { + // No active building + return; + } + + // Get path to place + const path = this.computeDirectionLockPath(); + + // Store if we placed anything + let anythingPlaced = false; + + // Perform this in bulk to avoid recalculations + this.root.logic.performBulkOperation(() => { + for (let i = 0; i < path.length; ++i) { + const { rotation, tile } = path[i]; + this.currentBaseRotation = rotation; + if (this.tryPlaceCurrentBuildingAt(tile)) { + anythingPlaced = true; + } + } + }); + + if (anythingPlaced) { + this.root.soundProxy.playUi(metaBuilding.getPlacementSound()); + } + } + + /** + * Finds the path which the current direction lock will use + * @returns {Array<{ tile: Vector, rotation: number }>} + */ + computeDirectionLockPath() { + const mousePosition = this.root.app.mousePosition; + if (!mousePosition) { + // Not on screen + return []; + } + + let result = []; + + // Figure which points the line visits + const worldPos = this.root.camera.screenToWorld(mousePosition); + let endTile = worldPos.toTileSpace(); + let startTile = this.lastDragTile; + + // if the alt key is pressed, reverse belt planner direction by switching start and end tile + if (this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeInverse).pressed) { + let tmp = startTile; + startTile = endTile; + endTile = tmp; + } + + // Place from start to corner + const pathToCorner = this.currentDirectionLockCorner.sub(startTile); + const deltaToCorner = pathToCorner.normalize().round(); + const lengthToCorner = Math.round(pathToCorner.length()); + let currentPos = startTile.copy(); + + let rotation = (Math.round(Math.degrees(deltaToCorner.angle()) / 90) * 90 + 360) % 360; + + if (lengthToCorner > 0) { + for (let i = 0; i < lengthToCorner; ++i) { + result.push({ + tile: currentPos.copy(), + rotation, + }); + currentPos.addInplace(deltaToCorner); + } + } + + // Place from corner to end + const pathFromCorner = endTile.sub(this.currentDirectionLockCorner); + const deltaFromCorner = pathFromCorner.normalize().round(); + const lengthFromCorner = Math.round(pathFromCorner.length()); + + if (lengthFromCorner > 0) { + rotation = (Math.round(Math.degrees(deltaFromCorner.angle()) / 90) * 90 + 360) % 360; + for (let i = 0; i < lengthFromCorner + 1; ++i) { + result.push({ + tile: currentPos.copy(), + rotation, + }); + currentPos.addInplace(deltaFromCorner); + } + } else { + // Finish last one + result.push({ + tile: currentPos.copy(), + rotation, + }); + } + return result; + } + + /** + * Selects a given building + * @param {MetaBuilding} metaBuilding + */ + startSelection(metaBuilding) { + this.currentMetaBuilding.set(metaBuilding); + } + + /** + * Called when the selected buildings changed + * @param {MetaBuilding} metaBuilding + */ + onSelectedMetaBuildingChanged(metaBuilding) { + this.abortDragging(); + this.root.hud.signals.selectedPlacementBuildingChanged.dispatch(metaBuilding); + if (metaBuilding) { + const variant = this.preferredVariants[metaBuilding.getId()] || defaultBuildingVariant; + this.currentVariant.set(variant); + + this.fakeEntity = new Entity(null); + metaBuilding.setupEntityComponents(this.fakeEntity, null); + + this.fakeEntity.addComponent( + new StaticMapEntityComponent({ + origin: new Vector(0, 0), + rotation: 0, + tileSize: metaBuilding.getDimensions(this.currentVariant.get()).copy(), + code: getCodeFromBuildingData(metaBuilding, variant, 0), + }) + ); + metaBuilding.updateVariants(this.fakeEntity, 0, this.currentVariant.get()); + } else { + this.fakeEntity = null; + } + + // Since it depends on both, rerender twice + this.signals.variantChanged.dispatch(); + } + + /** + * mouse down pre handler + * @param {Vector} pos + * @param {enumMouseButton} button + */ + onMouseDown(pos, button) { + if (this.root.camera.getIsMapOverlayActive()) { + // We do not allow dragging if the overlay is active + return; + } + + const metaBuilding = this.currentMetaBuilding.get(); + + // Placement + if (button === enumMouseButton.left && metaBuilding) { + this.currentlyDragging = true; + this.currentlyDeleting = false; + this.lastDragTile = this.root.camera.screenToWorld(pos).toTileSpace(); + + // Place initial building, but only if direction lock is not active + if (!this.isDirectionLockActive) { + if (this.tryPlaceCurrentBuildingAt(this.lastDragTile)) { + this.root.soundProxy.playUi(metaBuilding.getPlacementSound()); + } + } + return STOP_PROPAGATION; + } + + // Deletion + if ( + button === enumMouseButton.right && + (!metaBuilding || !this.root.app.settings.getAllSettings().clearCursorOnDeleteWhilePlacing) + ) { + this.currentlyDragging = true; + this.currentlyDeleting = true; + this.lastDragTile = this.root.camera.screenToWorld(pos).toTileSpace(); + if (this.deleteBelowCursor()) { + return STOP_PROPAGATION; + } + } + + // Cancel placement + if (button === enumMouseButton.right && metaBuilding) { + this.currentMetaBuilding.set(null); + } + } + + /** + * mouse move pre handler + * @param {Vector} pos + */ + onMouseMove(pos) { + if (this.root.camera.getIsMapOverlayActive()) { + return; + } + + // Check for direction lock + if (this.isDirectionLockActive) { + return; + } + + const metaBuilding = this.currentMetaBuilding.get(); + if ((metaBuilding || this.currentlyDeleting) && this.lastDragTile) { + const oldPos = this.lastDragTile; + let newPos = this.root.camera.screenToWorld(pos).toTileSpace(); + + // Check if camera is moving, since then we do nothing + if (this.root.camera.desiredCenter) { + this.lastDragTile = newPos; + return; + } + + // Check if anything changed + if (!oldPos.equals(newPos)) { + // Automatic Direction + if ( + metaBuilding && + metaBuilding.getRotateAutomaticallyWhilePlacing(this.currentVariant.get()) && + !this.root.keyMapper.getBinding( + KEYMAPPINGS.placementModifiers.placementDisableAutoOrientation + ).pressed + ) { + const delta = newPos.sub(oldPos); + const angleDeg = Math.degrees(delta.angle()); + this.currentBaseRotation = (Math.round(angleDeg / 90) * 90 + 360) % 360; + + // Holding alt inverts the placement + if (this.root.keyMapper.getBinding(KEYMAPPINGS.placementModifiers.placeInverse).pressed) { + this.currentBaseRotation = (180 + this.currentBaseRotation) % 360; + } + } + + // bresenham + let x0 = oldPos.x; + let y0 = oldPos.y; + let x1 = newPos.x; + let y1 = newPos.y; + + var dx = Math.abs(x1 - x0); + var dy = Math.abs(y1 - y0); + var sx = x0 < x1 ? 1 : -1; + var sy = y0 < y1 ? 1 : -1; + var err = dx - dy; + + let anythingPlaced = false; + let anythingDeleted = false; + + while (this.currentlyDeleting || this.currentMetaBuilding.get()) { + if (this.currentlyDeleting) { + // Deletion + const contents = this.root.map.getLayerContentXY(x0, y0, this.root.currentLayer); + if (contents && !contents.queuedForDestroy && !contents.destroyed) { + if (this.root.logic.tryDeleteBuilding(contents)) { + anythingDeleted = true; + } + } + } else { + // Placement + if (this.tryPlaceCurrentBuildingAt(new Vector(x0, y0))) { + anythingPlaced = true; + } + } + + if (x0 === x1 && y0 === y1) break; + var e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x0 += sx; + } + if (e2 < dx) { + err += dx; + y0 += sy; + } + } + + if (anythingPlaced) { + this.root.soundProxy.playUi(metaBuilding.getPlacementSound()); + } + if (anythingDeleted) { + this.root.soundProxy.playUi(SOUNDS.destroyBuilding); + } + } + + this.lastDragTile = newPos; + return STOP_PROPAGATION; + } + } + + /** + * Mouse up handler + */ + onMouseUp() { + if (this.root.camera.getIsMapOverlayActive()) { + return; + } + + // Check for direction lock + if (this.lastDragTile && this.currentlyDragging && this.isDirectionLockActive) { + this.executeDirectionLockedPlacement(); + } + + this.abortDragging(); + } +} diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index f3f61c4a..ba4a033e 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -1,593 +1,600 @@ -/* typehints:start */ -import { Application } from "../application"; -/* typehints:end */ - -import { ReadWriteProxy } from "../core/read_write_proxy"; -import { BoolSetting, EnumSetting, BaseSetting } from "./setting_types"; -import { createLogger } from "../core/logging"; -import { ExplainedResult } from "../core/explained_result"; -import { THEMES, THEME, applyGameTheme } from "../game/theme"; -import { IS_DEMO } from "../core/config"; -import { T } from "../translations"; -import { LANGUAGES } from "../languages"; - -const logger = createLogger("application_settings"); - -/** - * @enum {string} - */ -export const enumCategories = { - general: "general", - userInterface: "userInterface", - performance: "performance", - advanced: "advanced", -}; - -export const uiScales = [ - { - id: "super_small", - size: 0.6, - }, - { - id: "small", - size: 0.8, - }, - { - id: "regular", - size: 1, - }, - { - id: "large", - size: 1.05, - }, - { - id: "huge", - size: 1.1, - }, -]; - -export const scrollWheelSensitivities = [ - { - id: "super_slow", - scale: 0.25, - }, - { - id: "slow", - scale: 0.5, - }, - { - id: "regular", - scale: 1, - }, - { - id: "fast", - scale: 2, - }, - { - id: "super_fast", - scale: 4, - }, -]; - -export const movementSpeeds = [ - { - id: "super_slow", - multiplier: 0.25, - }, - { - id: "slow", - multiplier: 0.5, - }, - { - id: "regular", - multiplier: 1, - }, - { - id: "fast", - multiplier: 2, - }, - { - id: "super_fast", - multiplier: 4, - }, - { - id: "extremely_fast", - multiplier: 8, - }, -]; - -export const autosaveIntervals = [ - { - id: "one_minute", - seconds: 60, - }, - { - id: "two_minutes", - seconds: 120, - }, - { - id: "five_minutes", - seconds: 5 * 60, - }, - { - id: "ten_minutes", - seconds: 10 * 60, - }, - { - id: "twenty_minutes", - seconds: 20 * 60, - }, - { - id: "disabled", - seconds: null, - }, -]; - -/** @type {Array} */ -export const allApplicationSettings = [ - new EnumSetting("language", { - options: Object.keys(LANGUAGES), - valueGetter: key => key, - textGetter: key => LANGUAGES[key].name, - category: enumCategories.general, - restartRequired: true, - changeCb: (app, id) => null, - magicValue: "auto-detect", - }), - - new EnumSetting("uiScale", { - options: uiScales.sort((a, b) => a.size - b.size), - valueGetter: scale => scale.id, - textGetter: scale => T.settings.labels.uiScale.scales[scale.id], - category: enumCategories.userInterface, - restartRequired: false, - changeCb: - /** - * @param {Application} app - */ - (app, id) => app.updateAfterUiScaleChanged(), - }), - - new BoolSetting( - "soundsMuted", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => app.sound.setSoundsMuted(value) - ), - new BoolSetting( - "musicMuted", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => app.sound.setMusicMuted(value) - ), - - new BoolSetting( - "fullscreen", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => { - if (app.platformWrapper.getSupportsFullscreen()) { - app.platformWrapper.setFullscreen(value); - } - }, - !IS_DEMO - ), - - new BoolSetting( - "enableColorBlindHelper", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => null - ), - - new BoolSetting("offerHints", enumCategories.userInterface, (app, value) => {}), - - new EnumSetting("theme", { - options: Object.keys(THEMES), - valueGetter: theme => theme, - textGetter: theme => T.settings.labels.theme.themes[theme], - category: enumCategories.userInterface, - restartRequired: false, - changeCb: - /** - * @param {Application} app - */ - (app, id) => { - applyGameTheme(id); - document.documentElement.setAttribute("data-theme", id); - }, - enabled: !IS_DEMO, - }), - - new EnumSetting("autosaveInterval", { - options: autosaveIntervals, - valueGetter: interval => interval.id, - textGetter: interval => T.settings.labels.autosaveInterval.intervals[interval.id], - category: enumCategories.advanced, - restartRequired: false, - changeCb: - /** - * @param {Application} app - */ - (app, id) => null, - }), - - new EnumSetting("scrollWheelSensitivity", { - options: scrollWheelSensitivities.sort((a, b) => a.scale - b.scale), - valueGetter: scale => scale.id, - textGetter: scale => T.settings.labels.scrollWheelSensitivity.sensitivity[scale.id], - category: enumCategories.advanced, - restartRequired: false, - changeCb: - /** - * @param {Application} app - */ - (app, id) => app.updateAfterUiScaleChanged(), - }), - - new EnumSetting("movementSpeed", { - options: movementSpeeds.sort((a, b) => a.multiplier - b.multiplier), - valueGetter: multiplier => multiplier.id, - textGetter: multiplier => T.settings.labels.movementSpeed.speeds[multiplier.id], - category: enumCategories.advanced, - restartRequired: false, - changeCb: (app, id) => {}, - }), - - new BoolSetting("alwaysMultiplace", enumCategories.advanced, (app, value) => {}), - new BoolSetting("enableTunnelSmartplace", enumCategories.advanced, (app, value) => {}), - new BoolSetting("vignette", enumCategories.userInterface, (app, value) => {}), - new BoolSetting("compactBuildingInfo", enumCategories.userInterface, (app, value) => {}), - new BoolSetting("disableCutDeleteWarnings", enumCategories.advanced, (app, value) => {}), - new BoolSetting("rotationByBuilding", enumCategories.advanced, (app, value) => {}), - - new EnumSetting("refreshRate", { - options: ["60", "75", "100", "120", "144", "165", "250", "500"], - valueGetter: rate => rate, - textGetter: rate => rate + " Hz", - category: enumCategories.performance, - restartRequired: false, - changeCb: (app, id) => {}, - enabled: !IS_DEMO, - }), - - new BoolSetting("lowQualityMapResources", enumCategories.performance, (app, value) => {}), - new BoolSetting("disableTileGrid", enumCategories.performance, (app, value) => {}), - new BoolSetting("lowQualityTextures", enumCategories.performance, (app, value) => {}), -]; - -export function getApplicationSettingById(id) { - return allApplicationSettings.find(setting => setting.id === id); -} - -class SettingsStorage { - constructor() { - this.uiScale = "regular"; - this.fullscreen = G_IS_STANDALONE; - - this.soundsMuted = false; - this.musicMuted = false; - this.theme = "light"; - this.refreshRate = "60"; - this.scrollWheelSensitivity = "regular"; - this.movementSpeed = "regular"; - this.language = "auto-detect"; - this.autosaveInterval = "two_minutes"; - - this.alwaysMultiplace = false; - this.offerHints = true; - this.enableTunnelSmartplace = true; - this.vignette = true; - this.compactBuildingInfo = false; - this.disableCutDeleteWarnings = false; - this.rotationByBuilding = true; - - this.enableColorBlindHelper = false; - - this.lowQualityMapResources = false; - this.disableTileGrid = false; - this.lowQualityTextures = false; - - /** - * @type {Object.} - */ - this.keybindingOverrides = {}; - } -} - -export class ApplicationSettings extends ReadWriteProxy { - constructor(app) { - super(app, "app_settings.bin"); - } - - initialize() { - // Read and directly write latest data back - return this.readAsync() - .then(() => { - // Apply default setting callbacks - const settings = this.getAllSettings(); - for (let i = 0; i < allApplicationSettings.length; ++i) { - const handle = allApplicationSettings[i]; - handle.apply(this.app, settings[handle.id]); - } - }) - - .then(() => this.writeAsync()); - } - - save() { - return this.writeAsync(); - } - - // Getters - - /** - * @returns {SettingsStorage} - */ - getAllSettings() { - return this.getCurrentData().settings; - } - - /** - * @param {string} key - */ - getSetting(key) { - assert(this.getAllSettings().hasOwnProperty(key), "Setting not known: " + key); - return this.getAllSettings()[key]; - } - - getInterfaceScaleId() { - if (!this.currentData) { - // Not initialized yet - return "regular"; - } - return this.getAllSettings().uiScale; - } - - getDesiredFps() { - return parseInt(this.getAllSettings().refreshRate); - } - - getInterfaceScaleValue() { - const id = this.getInterfaceScaleId(); - for (let i = 0; i < uiScales.length; ++i) { - if (uiScales[i].id === id) { - return uiScales[i].size; - } - } - logger.error("Unknown ui scale id:", id); - return 1; - } - - getScrollWheelSensitivity() { - const id = this.getAllSettings().scrollWheelSensitivity; - for (let i = 0; i < scrollWheelSensitivities.length; ++i) { - if (scrollWheelSensitivities[i].id === id) { - return scrollWheelSensitivities[i].scale; - } - } - logger.error("Unknown scroll wheel sensitivity id:", id); - return 1; - } - - getMovementSpeed() { - const id = this.getAllSettings().movementSpeed; - for (let i = 0; i < movementSpeeds.length; ++i) { - if (movementSpeeds[i].id === id) { - return movementSpeeds[i].multiplier; - } - } - logger.error("Unknown movement speed id:", id); - return 1; - } - - getAutosaveIntervalSeconds() { - const id = this.getAllSettings().autosaveInterval; - for (let i = 0; i < autosaveIntervals.length; ++i) { - if (autosaveIntervals[i].id === id) { - return autosaveIntervals[i].seconds; - } - } - logger.error("Unknown autosave interval id:", id); - return 120; - } - - getIsFullScreen() { - return this.getAllSettings().fullscreen; - } - - getKeybindingOverrides() { - return this.getAllSettings().keybindingOverrides; - } - - getLanguage() { - return this.getAllSettings().language; - } - - // Setters - - updateLanguage(id) { - assert(LANGUAGES[id], "Language not known: " + id); - return this.updateSetting("language", id); - } - - /** - * @param {string} key - * @param {string|boolean} value - */ - updateSetting(key, value) { - for (let i = 0; i < allApplicationSettings.length; ++i) { - const setting = allApplicationSettings[i]; - if (setting.id === key) { - if (!setting.validate(value)) { - assertAlways(false, "Bad setting value: " + key); - } - this.getAllSettings()[key] = value; - if (setting.changeCb) { - setting.changeCb(this.app, value); - } - return this.writeAsync(); - } - } - assertAlways(false, "Unknown setting: " + key); - } - - /** - * Sets a new keybinding override - * @param {string} keybindingId - * @param {number} keyCode - */ - updateKeybindingOverride(keybindingId, keyCode) { - assert(Number.isInteger(keyCode), "Not a valid key code: " + keyCode); - this.getAllSettings().keybindingOverrides[keybindingId] = keyCode; - return this.writeAsync(); - } - - /** - * Resets a given keybinding override - * @param {string} id - */ - resetKeybindingOverride(id) { - delete this.getAllSettings().keybindingOverrides[id]; - return this.writeAsync(); - } - /** - * Resets all keybinding overrides - */ - resetKeybindingOverrides() { - this.getAllSettings().keybindingOverrides = {}; - return this.writeAsync(); - } - - // RW Proxy impl - verify(data) { - if (!data.settings) { - return ExplainedResult.bad("missing key 'settings'"); - } - if (typeof data.settings !== "object") { - return ExplainedResult.bad("Bad settings object"); - } - - const settings = data.settings; - for (let i = 0; i < allApplicationSettings.length; ++i) { - const setting = allApplicationSettings[i]; - const storedValue = settings[setting.id]; - if (!setting.validate(storedValue)) { - return ExplainedResult.bad("Bad setting value for " + setting.id + ": " + storedValue); - } - } - return ExplainedResult.good(); - } - - getDefaultData() { - return { - version: this.getCurrentVersion(), - settings: new SettingsStorage(), - }; - } - - getCurrentVersion() { - return 21; - } - - /** @param {{settings: SettingsStorage, version: number}} data */ - migrate(data) { - // Simply reset before - if (data.version < 5) { - data.settings = new SettingsStorage(); - data.version = this.getCurrentVersion(); - return ExplainedResult.good(); - } - - if (data.version < 6) { - data.settings.alwaysMultiplace = false; - data.version = 6; - } - - if (data.version < 7) { - data.settings.offerHints = true; - data.version = 7; - } - - if (data.version < 8) { - data.settings.scrollWheelSensitivity = "regular"; - data.version = 8; - } - - if (data.version < 9) { - data.settings.language = "auto-detect"; - data.version = 9; - } - - if (data.version < 10) { - data.settings.movementSpeed = "regular"; - data.version = 10; - } - - if (data.version < 11) { - data.settings.enableTunnelSmartplace = true; - data.version = 11; - } - - if (data.version < 12) { - data.settings.vignette = true; - data.version = 12; - } - - if (data.version < 13) { - data.settings.compactBuildingInfo = false; - data.version = 13; - } - - if (data.version < 14) { - data.settings.disableCutDeleteWarnings = false; - data.version = 14; - } - - if (data.version < 15) { - data.settings.autosaveInterval = "two_minutes"; - data.version = 15; - } - - if (data.version < 16) { - // RE-ENABLE this setting, it already existed - data.settings.enableTunnelSmartplace = true; - data.version = 16; - } - - if (data.version < 17) { - data.settings.enableColorBlindHelper = false; - data.version = 17; - } - - if (data.version < 18) { - data.settings.rotationByBuilding = true; - data.version = 18; - } - - if (data.version < 19) { - data.settings.lowQualityMapResources = false; - data.version = 19; - } - - if (data.version < 20) { - data.settings.disableTileGrid = false; - data.version = 20; - } - - if (data.version < 21) { - data.settings.lowQualityTextures = false; - data.version = 21; - } - - return ExplainedResult.good(); - } -} +/* typehints:start */ +import { Application } from "../application"; +/* typehints:end */ + +import { ReadWriteProxy } from "../core/read_write_proxy"; +import { BoolSetting, EnumSetting, BaseSetting } from "./setting_types"; +import { createLogger } from "../core/logging"; +import { ExplainedResult } from "../core/explained_result"; +import { THEMES, THEME, applyGameTheme } from "../game/theme"; +import { IS_DEMO } from "../core/config"; +import { T } from "../translations"; +import { LANGUAGES } from "../languages"; + +const logger = createLogger("application_settings"); + +/** + * @enum {string} + */ +export const enumCategories = { + general: "general", + userInterface: "userInterface", + performance: "performance", + advanced: "advanced", +}; + +export const uiScales = [ + { + id: "super_small", + size: 0.6, + }, + { + id: "small", + size: 0.8, + }, + { + id: "regular", + size: 1, + }, + { + id: "large", + size: 1.05, + }, + { + id: "huge", + size: 1.1, + }, +]; + +export const scrollWheelSensitivities = [ + { + id: "super_slow", + scale: 0.25, + }, + { + id: "slow", + scale: 0.5, + }, + { + id: "regular", + scale: 1, + }, + { + id: "fast", + scale: 2, + }, + { + id: "super_fast", + scale: 4, + }, +]; + +export const movementSpeeds = [ + { + id: "super_slow", + multiplier: 0.25, + }, + { + id: "slow", + multiplier: 0.5, + }, + { + id: "regular", + multiplier: 1, + }, + { + id: "fast", + multiplier: 2, + }, + { + id: "super_fast", + multiplier: 4, + }, + { + id: "extremely_fast", + multiplier: 8, + }, +]; + +export const autosaveIntervals = [ + { + id: "one_minute", + seconds: 60, + }, + { + id: "two_minutes", + seconds: 120, + }, + { + id: "five_minutes", + seconds: 5 * 60, + }, + { + id: "ten_minutes", + seconds: 10 * 60, + }, + { + id: "twenty_minutes", + seconds: 20 * 60, + }, + { + id: "disabled", + seconds: null, + }, +]; + +/** @type {Array} */ +export const allApplicationSettings = [ + new EnumSetting("language", { + options: Object.keys(LANGUAGES), + valueGetter: key => key, + textGetter: key => LANGUAGES[key].name, + category: enumCategories.general, + restartRequired: true, + changeCb: (app, id) => null, + magicValue: "auto-detect", + }), + + new EnumSetting("uiScale", { + options: uiScales.sort((a, b) => a.size - b.size), + valueGetter: scale => scale.id, + textGetter: scale => T.settings.labels.uiScale.scales[scale.id], + category: enumCategories.userInterface, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => app.updateAfterUiScaleChanged(), + }), + + new BoolSetting( + "soundsMuted", + enumCategories.general, + /** + * @param {Application} app + */ + (app, value) => app.sound.setSoundsMuted(value) + ), + new BoolSetting( + "musicMuted", + enumCategories.general, + /** + * @param {Application} app + */ + (app, value) => app.sound.setMusicMuted(value) + ), + + new BoolSetting( + "fullscreen", + enumCategories.general, + /** + * @param {Application} app + */ + (app, value) => { + if (app.platformWrapper.getSupportsFullscreen()) { + app.platformWrapper.setFullscreen(value); + } + }, + !IS_DEMO + ), + + new BoolSetting( + "enableColorBlindHelper", + enumCategories.general, + /** + * @param {Application} app + */ + (app, value) => null + ), + + new BoolSetting("offerHints", enumCategories.userInterface, (app, value) => {}), + + new EnumSetting("theme", { + options: Object.keys(THEMES), + valueGetter: theme => theme, + textGetter: theme => T.settings.labels.theme.themes[theme], + category: enumCategories.userInterface, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => { + applyGameTheme(id); + document.documentElement.setAttribute("data-theme", id); + }, + enabled: !IS_DEMO, + }), + + new EnumSetting("autosaveInterval", { + options: autosaveIntervals, + valueGetter: interval => interval.id, + textGetter: interval => T.settings.labels.autosaveInterval.intervals[interval.id], + category: enumCategories.advanced, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => null, + }), + + new EnumSetting("scrollWheelSensitivity", { + options: scrollWheelSensitivities.sort((a, b) => a.scale - b.scale), + valueGetter: scale => scale.id, + textGetter: scale => T.settings.labels.scrollWheelSensitivity.sensitivity[scale.id], + category: enumCategories.advanced, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => app.updateAfterUiScaleChanged(), + }), + + new EnumSetting("movementSpeed", { + options: movementSpeeds.sort((a, b) => a.multiplier - b.multiplier), + valueGetter: multiplier => multiplier.id, + textGetter: multiplier => T.settings.labels.movementSpeed.speeds[multiplier.id], + category: enumCategories.advanced, + restartRequired: false, + changeCb: (app, id) => {}, + }), + + new BoolSetting("alwaysMultiplace", enumCategories.advanced, (app, value) => {}), + new BoolSetting("clearCursorOnDeleteWhilePlacing", enumCategories.advanced, (app, value) => {}), + new BoolSetting("enableTunnelSmartplace", enumCategories.advanced, (app, value) => {}), + new BoolSetting("vignette", enumCategories.userInterface, (app, value) => {}), + new BoolSetting("compactBuildingInfo", enumCategories.userInterface, (app, value) => {}), + new BoolSetting("disableCutDeleteWarnings", enumCategories.advanced, (app, value) => {}), + new BoolSetting("rotationByBuilding", enumCategories.advanced, (app, value) => {}), + + new EnumSetting("refreshRate", { + options: ["60", "75", "100", "120", "144", "165", "250", "500"], + valueGetter: rate => rate, + textGetter: rate => rate + " Hz", + category: enumCategories.performance, + restartRequired: false, + changeCb: (app, id) => {}, + enabled: !IS_DEMO, + }), + + new BoolSetting("lowQualityMapResources", enumCategories.performance, (app, value) => {}), + new BoolSetting("disableTileGrid", enumCategories.performance, (app, value) => {}), + new BoolSetting("lowQualityTextures", enumCategories.performance, (app, value) => {}), +]; + +export function getApplicationSettingById(id) { + return allApplicationSettings.find(setting => setting.id === id); +} + +class SettingsStorage { + constructor() { + this.uiScale = "regular"; + this.fullscreen = G_IS_STANDALONE; + + this.soundsMuted = false; + this.musicMuted = false; + this.theme = "light"; + this.refreshRate = "60"; + this.scrollWheelSensitivity = "regular"; + this.movementSpeed = "regular"; + this.language = "auto-detect"; + this.autosaveInterval = "two_minutes"; + + this.alwaysMultiplace = false; + this.offerHints = true; + this.enableTunnelSmartplace = true; + this.vignette = true; + this.compactBuildingInfo = false; + this.disableCutDeleteWarnings = false; + this.rotationByBuilding = true; + this.clearCursorOnDeleteWhilePlacing = true; + + this.enableColorBlindHelper = false; + + this.lowQualityMapResources = false; + this.disableTileGrid = false; + this.lowQualityTextures = false; + + /** + * @type {Object.} + */ + this.keybindingOverrides = {}; + } +} + +export class ApplicationSettings extends ReadWriteProxy { + constructor(app) { + super(app, "app_settings.bin"); + } + + initialize() { + // Read and directly write latest data back + return this.readAsync() + .then(() => { + // Apply default setting callbacks + const settings = this.getAllSettings(); + for (let i = 0; i < allApplicationSettings.length; ++i) { + const handle = allApplicationSettings[i]; + handle.apply(this.app, settings[handle.id]); + } + }) + + .then(() => this.writeAsync()); + } + + save() { + return this.writeAsync(); + } + + // Getters + + /** + * @returns {SettingsStorage} + */ + getAllSettings() { + return this.getCurrentData().settings; + } + + /** + * @param {string} key + */ + getSetting(key) { + assert(this.getAllSettings().hasOwnProperty(key), "Setting not known: " + key); + return this.getAllSettings()[key]; + } + + getInterfaceScaleId() { + if (!this.currentData) { + // Not initialized yet + return "regular"; + } + return this.getAllSettings().uiScale; + } + + getDesiredFps() { + return parseInt(this.getAllSettings().refreshRate); + } + + getInterfaceScaleValue() { + const id = this.getInterfaceScaleId(); + for (let i = 0; i < uiScales.length; ++i) { + if (uiScales[i].id === id) { + return uiScales[i].size; + } + } + logger.error("Unknown ui scale id:", id); + return 1; + } + + getScrollWheelSensitivity() { + const id = this.getAllSettings().scrollWheelSensitivity; + for (let i = 0; i < scrollWheelSensitivities.length; ++i) { + if (scrollWheelSensitivities[i].id === id) { + return scrollWheelSensitivities[i].scale; + } + } + logger.error("Unknown scroll wheel sensitivity id:", id); + return 1; + } + + getMovementSpeed() { + const id = this.getAllSettings().movementSpeed; + for (let i = 0; i < movementSpeeds.length; ++i) { + if (movementSpeeds[i].id === id) { + return movementSpeeds[i].multiplier; + } + } + logger.error("Unknown movement speed id:", id); + return 1; + } + + getAutosaveIntervalSeconds() { + const id = this.getAllSettings().autosaveInterval; + for (let i = 0; i < autosaveIntervals.length; ++i) { + if (autosaveIntervals[i].id === id) { + return autosaveIntervals[i].seconds; + } + } + logger.error("Unknown autosave interval id:", id); + return 120; + } + + getIsFullScreen() { + return this.getAllSettings().fullscreen; + } + + getKeybindingOverrides() { + return this.getAllSettings().keybindingOverrides; + } + + getLanguage() { + return this.getAllSettings().language; + } + + // Setters + + updateLanguage(id) { + assert(LANGUAGES[id], "Language not known: " + id); + return this.updateSetting("language", id); + } + + /** + * @param {string} key + * @param {string|boolean} value + */ + updateSetting(key, value) { + for (let i = 0; i < allApplicationSettings.length; ++i) { + const setting = allApplicationSettings[i]; + if (setting.id === key) { + if (!setting.validate(value)) { + assertAlways(false, "Bad setting value: " + key); + } + this.getAllSettings()[key] = value; + if (setting.changeCb) { + setting.changeCb(this.app, value); + } + return this.writeAsync(); + } + } + assertAlways(false, "Unknown setting: " + key); + } + + /** + * Sets a new keybinding override + * @param {string} keybindingId + * @param {number} keyCode + */ + updateKeybindingOverride(keybindingId, keyCode) { + assert(Number.isInteger(keyCode), "Not a valid key code: " + keyCode); + this.getAllSettings().keybindingOverrides[keybindingId] = keyCode; + return this.writeAsync(); + } + + /** + * Resets a given keybinding override + * @param {string} id + */ + resetKeybindingOverride(id) { + delete this.getAllSettings().keybindingOverrides[id]; + return this.writeAsync(); + } + /** + * Resets all keybinding overrides + */ + resetKeybindingOverrides() { + this.getAllSettings().keybindingOverrides = {}; + return this.writeAsync(); + } + + // RW Proxy impl + verify(data) { + if (!data.settings) { + return ExplainedResult.bad("missing key 'settings'"); + } + if (typeof data.settings !== "object") { + return ExplainedResult.bad("Bad settings object"); + } + + const settings = data.settings; + for (let i = 0; i < allApplicationSettings.length; ++i) { + const setting = allApplicationSettings[i]; + const storedValue = settings[setting.id]; + if (!setting.validate(storedValue)) { + return ExplainedResult.bad("Bad setting value for " + setting.id + ": " + storedValue); + } + } + return ExplainedResult.good(); + } + + getDefaultData() { + return { + version: this.getCurrentVersion(), + settings: new SettingsStorage(), + }; + } + + getCurrentVersion() { + return 22; + } + + /** @param {{settings: SettingsStorage, version: number}} data */ + migrate(data) { + // Simply reset before + if (data.version < 5) { + data.settings = new SettingsStorage(); + data.version = this.getCurrentVersion(); + return ExplainedResult.good(); + } + + if (data.version < 6) { + data.settings.alwaysMultiplace = false; + data.version = 6; + } + + if (data.version < 7) { + data.settings.offerHints = true; + data.version = 7; + } + + if (data.version < 8) { + data.settings.scrollWheelSensitivity = "regular"; + data.version = 8; + } + + if (data.version < 9) { + data.settings.language = "auto-detect"; + data.version = 9; + } + + if (data.version < 10) { + data.settings.movementSpeed = "regular"; + data.version = 10; + } + + if (data.version < 11) { + data.settings.enableTunnelSmartplace = true; + data.version = 11; + } + + if (data.version < 12) { + data.settings.vignette = true; + data.version = 12; + } + + if (data.version < 13) { + data.settings.compactBuildingInfo = false; + data.version = 13; + } + + if (data.version < 14) { + data.settings.disableCutDeleteWarnings = false; + data.version = 14; + } + + if (data.version < 15) { + data.settings.autosaveInterval = "two_minutes"; + data.version = 15; + } + + if (data.version < 16) { + // RE-ENABLE this setting, it already existed + data.settings.enableTunnelSmartplace = true; + data.version = 16; + } + + if (data.version < 17) { + data.settings.enableColorBlindHelper = false; + data.version = 17; + } + + if (data.version < 18) { + data.settings.rotationByBuilding = true; + data.version = 18; + } + + if (data.version < 19) { + data.settings.lowQualityMapResources = false; + data.version = 19; + } + + if (data.version < 20) { + data.settings.disableTileGrid = false; + data.version = 20; + } + + if (data.version < 21) { + data.settings.lowQualityTextures = false; + data.version = 21; + } + + if (data.version < 22) { + data.settings.clearCursorOnDeleteWhilePlacing = true; + data.version = 22; + } + + return ExplainedResult.good(); + } +} diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 8703a14e..b90de4e9 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -841,6 +841,11 @@ settings: description: >- Disabling the tile grid can help with the performance. This also makes the game look cleaner! + clearCursorOnDeleteWhilePlacing: + title: Clear Cursor on Right Click + description: >- + Enabled by default, clears the cursor whenever you right click while you have a building selected for placement. If disabled, you can delete buildings by right-clicking while placing a building. + lowQualityTextures: title: Low quality textures (Ugly) description: >-