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: >-