diff --git a/README.md b/README.md index 32e09d59..cc3e06bc 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ This project is based on ES5. Some ES2015 features are used but most of them are 5. Add a constructor. **The constructor must be called with optional parameters only!** `new MyFancyComponent({})` should always work. 6. Add any props you need in the constructor. 7. Add the component in `src/js/game/component_registry.js` -8. Add the componetn in `src/js/game/entity_components.js` +8. Add the component in `src/js/game/entity_components.js` 9. Done! You can use your component now #### Adding a new building @@ -96,6 +96,6 @@ This project is based on ES5. Some ES2015 features are used but most of them are For most assets I use Adobe Photoshop, you can find them in `assets/`. -You will need a Texture Packer license in order to regenerate the atlas. If you don't have one but want to contribute assets, let me know and I might compile it for you. I'm currently switching to an open source solution but I can't give an estimate when thats done. +You will need a Texture Packer license in order to regenerate the atlas. If you don't have one but want to contribute assets, let me know and I might compile it for you. I'm currently switching to an open source solution but I can't give an estimate when that's done. shapez.io Screenshot diff --git a/gulp/sounds.js b/gulp/sounds.js index c38e7a6e..aaae606f 100644 --- a/gulp/sounds.js +++ b/gulp/sounds.js @@ -16,6 +16,12 @@ function gulptasksSounds($, gulp, buildFolder) { cacheDirName: "shapezio-precompiled-sounds", }); + function getFileCacheValue(file) { + const { _isVinyl, base, cwd, contents, history, stat, path } = file; + const encodedContents = Buffer.from(contents).toString('base64'); + return { _isVinyl, base, cwd, contents: encodedContents, history, stat, path }; + } + // Encodes the game music gulp.task("sounds.music", () => { return gulp @@ -34,6 +40,7 @@ function gulptasksSounds($, gulp, buildFolder) { { name: "music", fileCache, + value: getFileCacheValue, } ) ) @@ -58,6 +65,7 @@ function gulptasksSounds($, gulp, buildFolder) { { name: "music-high-quality", fileCache, + value: getFileCacheValue, } ) ) diff --git a/res/ui/building_icons/balancer.png b/res/ui/building_icons/balancer.png new file mode 100644 index 00000000..4347d2ba Binary files /dev/null and b/res/ui/building_icons/balancer.png differ diff --git a/res/ui/building_icons/belt.png b/res/ui/building_icons/belt.png index 628480fb..566e971d 100644 Binary files a/res/ui/building_icons/belt.png and b/res/ui/building_icons/belt.png differ diff --git a/res/ui/building_icons/constant_signal.png b/res/ui/building_icons/constant_signal.png index b438fa29..c913837a 100644 Binary files a/res/ui/building_icons/constant_signal.png and b/res/ui/building_icons/constant_signal.png differ diff --git a/res/ui/building_icons/cutter.png b/res/ui/building_icons/cutter.png index 6d0fc4d1..84eafc30 100644 Binary files a/res/ui/building_icons/cutter.png and b/res/ui/building_icons/cutter.png differ diff --git a/res/ui/building_icons/display.png b/res/ui/building_icons/display.png index 14c48d7e..322d84e2 100644 Binary files a/res/ui/building_icons/display.png and b/res/ui/building_icons/display.png differ diff --git a/res/ui/building_icons/filter.png b/res/ui/building_icons/filter.png index 2e87af28..16215918 100644 Binary files a/res/ui/building_icons/filter.png and b/res/ui/building_icons/filter.png differ diff --git a/res/ui/building_icons/lever.png b/res/ui/building_icons/lever.png index 77eea9f3..e4514b98 100644 Binary files a/res/ui/building_icons/lever.png and b/res/ui/building_icons/lever.png differ diff --git a/res/ui/building_icons/logic_gate.png b/res/ui/building_icons/logic_gate.png index 81a0bdd6..d71ddbc5 100644 Binary files a/res/ui/building_icons/logic_gate.png and b/res/ui/building_icons/logic_gate.png differ diff --git a/res/ui/building_icons/miner.png b/res/ui/building_icons/miner.png index fc7050ea..9103750b 100644 Binary files a/res/ui/building_icons/miner.png and b/res/ui/building_icons/miner.png differ diff --git a/res/ui/building_icons/mixer.png b/res/ui/building_icons/mixer.png index 87409438..9c0fd11c 100644 Binary files a/res/ui/building_icons/mixer.png and b/res/ui/building_icons/mixer.png differ diff --git a/res/ui/building_icons/painter.png b/res/ui/building_icons/painter.png index 4aa888b6..e0b63af5 100644 Binary files a/res/ui/building_icons/painter.png and b/res/ui/building_icons/painter.png differ diff --git a/res/ui/building_icons/reader.png b/res/ui/building_icons/reader.png index 890a6ad7..8381c9b8 100644 Binary files a/res/ui/building_icons/reader.png and b/res/ui/building_icons/reader.png differ diff --git a/res/ui/building_icons/rotater.png b/res/ui/building_icons/rotater.png index 3fb355d6..00a4c75d 100644 Binary files a/res/ui/building_icons/rotater.png and b/res/ui/building_icons/rotater.png differ diff --git a/res/ui/building_icons/splitter.png b/res/ui/building_icons/splitter.png deleted file mode 100644 index fb889bab..00000000 Binary files a/res/ui/building_icons/splitter.png and /dev/null differ diff --git a/res/ui/building_icons/stacker.png b/res/ui/building_icons/stacker.png index 5a4dda42..ccafd591 100644 Binary files a/res/ui/building_icons/stacker.png and b/res/ui/building_icons/stacker.png differ diff --git a/res/ui/building_icons/trash.png b/res/ui/building_icons/trash.png index b6a34ae6..0f4238f4 100644 Binary files a/res/ui/building_icons/trash.png and b/res/ui/building_icons/trash.png differ diff --git a/res/ui/building_icons/underground_belt.png b/res/ui/building_icons/underground_belt.png index b52f4d8e..a8f121c1 100644 Binary files a/res/ui/building_icons/underground_belt.png and b/res/ui/building_icons/underground_belt.png differ diff --git a/res/ui/building_icons/virtual_processor.png b/res/ui/building_icons/virtual_processor.png index f5471999..310f130e 100644 Binary files a/res/ui/building_icons/virtual_processor.png and b/res/ui/building_icons/virtual_processor.png differ diff --git a/res/ui/building_icons/wire.png b/res/ui/building_icons/wire.png index 6bae2537..bc2a128d 100644 Binary files a/res/ui/building_icons/wire.png and b/res/ui/building_icons/wire.png differ diff --git a/res/ui/building_icons/wire_tunnel.png b/res/ui/building_icons/wire_tunnel.png index d0e185f9..97bfc3db 100644 Binary files a/res/ui/building_icons/wire_tunnel.png and b/res/ui/building_icons/wire_tunnel.png differ diff --git a/res/ui/building_tutorials/splitter-compact.png b/res/ui/building_tutorials/balancer-merger.png similarity index 100% rename from res/ui/building_tutorials/splitter-compact.png rename to res/ui/building_tutorials/balancer-merger.png diff --git a/res/ui/building_tutorials/balancer-splitter.png b/res/ui/building_tutorials/balancer-splitter.png new file mode 100644 index 00000000..d03103b2 Binary files /dev/null and b/res/ui/building_tutorials/balancer-splitter.png differ diff --git a/res/ui/building_tutorials/splitter.png b/res/ui/building_tutorials/balancer.png similarity index 100% rename from res/ui/building_tutorials/splitter.png rename to res/ui/building_tutorials/balancer.png diff --git a/res/ui/building_tutorials/splitter-compact-inverse.png b/res/ui/building_tutorials/splitter-compact-inverse.png deleted file mode 100644 index 38965f4d..00000000 Binary files a/res/ui/building_tutorials/splitter-compact-inverse.png and /dev/null differ diff --git a/res/ui/icons/delete.png b/res/ui/icons/delete.png index db1c86f1..114693a6 100644 Binary files a/res/ui/icons/delete.png and b/res/ui/icons/delete.png differ diff --git a/res/ui/icons/download.png b/res/ui/icons/download.png index 68ed3fb4..9bea1b85 100644 Binary files a/res/ui/icons/download.png and b/res/ui/icons/download.png differ diff --git a/res/ui/icons/enum_selector_white.png b/res/ui/icons/enum_selector_white.png new file mode 100644 index 00000000..c95a44cc Binary files /dev/null and b/res/ui/icons/enum_selector_white.png differ diff --git a/res/ui/icons/main_menu_exit.png b/res/ui/icons/main_menu_exit.png index 07a54c6c..b1918778 100644 Binary files a/res/ui/icons/main_menu_exit.png and b/res/ui/icons/main_menu_exit.png differ diff --git a/res/ui/icons/main_menu_settings.png b/res/ui/icons/main_menu_settings.png index eb99a2ef..88bd4987 100644 Binary files a/res/ui/icons/main_menu_settings.png and b/res/ui/icons/main_menu_settings.png differ diff --git a/res/ui/icons/save.png b/res/ui/icons/save.png index d48274bc..e75c08f4 100644 Binary files a/res/ui/icons/save.png and b/res/ui/icons/save.png differ diff --git a/res/ui/icons/settings_menu_exit.png b/res/ui/icons/settings_menu_exit.png new file mode 100644 index 00000000..3fa17330 Binary files /dev/null and b/res/ui/icons/settings_menu_exit.png differ diff --git a/res/ui/icons/settings_menu_play.png b/res/ui/icons/settings_menu_play.png new file mode 100644 index 00000000..358b5362 Binary files /dev/null and b/res/ui/icons/settings_menu_play.png differ diff --git a/res/ui/icons/settings_menu_settings.png b/res/ui/icons/settings_menu_settings.png new file mode 100644 index 00000000..8eb6efee Binary files /dev/null and b/res/ui/icons/settings_menu_settings.png differ diff --git a/res/ui/icons/shop.png b/res/ui/icons/shop.png index 29519103..94dbc6de 100644 Binary files a/res/ui/icons/shop.png and b/res/ui/icons/shop.png differ diff --git a/res/ui/icons/shop_active.png b/res/ui/icons/shop_active.png new file mode 100644 index 00000000..773d4c88 Binary files /dev/null and b/res/ui/icons/shop_active.png differ diff --git a/res/ui/icons/statistics.png b/res/ui/icons/statistics.png index c6b8e68a..e6349d88 100644 Binary files a/res/ui/icons/statistics.png and b/res/ui/icons/statistics.png differ diff --git a/res/ui/icons/toggle_unit.png b/res/ui/icons/toggle_unit.png new file mode 100644 index 00000000..e2126a97 Binary files /dev/null and b/res/ui/icons/toggle_unit.png differ diff --git a/res_built/atlas/atlas0_hq.json b/res_built/atlas/atlas0_hq.json index 9edcc70f..50304df8 100644 --- a/res_built/atlas/atlas0_hq.json +++ b/res_built/atlas/atlas0_hq.json @@ -2,7 +2,7 @@ "sprites/belt/built/forward_0.png": { - "frame": {"x":440,"y":742,"w":116,"h":144}, + "frame": {"x":1867,"y":593,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -10,7 +10,7 @@ }, "sprites/belt/built/forward_1.png": { - "frame": {"x":1815,"y":1070,"w":116,"h":144}, + "frame": {"x":1241,"y":1197,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -18,7 +18,7 @@ }, "sprites/belt/built/forward_2.png": { - "frame": {"x":1804,"y":1366,"w":116,"h":144}, + "frame": {"x":1558,"y":753,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -26,7 +26,7 @@ }, "sprites/belt/built/forward_3.png": { - "frame": {"x":1924,"y":1405,"w":116,"h":144}, + "frame": {"x":1555,"y":903,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -34,7 +34,7 @@ }, "sprites/belt/built/forward_4.png": { - "frame": {"x":439,"y":1038,"w":116,"h":144}, + "frame": {"x":1562,"y":1053,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -42,7 +42,7 @@ }, "sprites/belt/built/forward_5.png": { - "frame": {"x":1801,"y":1514,"w":116,"h":144}, + "frame": {"x":1680,"y":753,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -50,7 +50,7 @@ }, "sprites/belt/built/forward_6.png": { - "frame": {"x":1921,"y":1553,"w":116,"h":144}, + "frame": {"x":1677,"y":903,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -58,7 +58,7 @@ }, "sprites/belt/built/forward_7.png": { - "frame": {"x":1801,"y":1662,"w":116,"h":144}, + "frame": {"x":1684,"y":1053,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -66,7 +66,7 @@ }, "sprites/belt/built/forward_8.png": { - "frame": {"x":1921,"y":1701,"w":116,"h":144}, + "frame": {"x":1802,"y":743,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -74,7 +74,7 @@ }, "sprites/belt/built/forward_9.png": { - "frame": {"x":137,"y":1849,"w":116,"h":144}, + "frame": {"x":1924,"y":743,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -82,7 +82,7 @@ }, "sprites/belt/built/forward_10.png": { - "frame": {"x":1539,"y":1139,"w":116,"h":144}, + "frame": {"x":1363,"y":1195,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -90,7 +90,7 @@ }, "sprites/belt/built/forward_11.png": { - "frame": {"x":1538,"y":1287,"w":116,"h":144}, + "frame": {"x":845,"y":1548,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -98,7 +98,7 @@ }, "sprites/belt/built/forward_12.png": { - "frame": {"x":1806,"y":1218,"w":116,"h":144}, + "frame": {"x":1436,"y":753,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -106,7 +106,7 @@ }, "sprites/belt/built/forward_13.png": { - "frame": {"x":1926,"y":1257,"w":116,"h":144}, + "frame": {"x":1433,"y":903,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -114,7 +114,7 @@ }, "sprites/belt/built/left_0.png": { - "frame": {"x":3,"y":1182,"w":130,"h":130}, + "frame": {"x":569,"y":1845,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -122,7 +122,7 @@ }, "sprites/belt/built/left_1.png": { - "frame": {"x":137,"y":1182,"w":130,"h":130}, + "frame": {"x":1426,"y":1053,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -130,7 +130,7 @@ }, "sprites/belt/built/left_2.png": { - "frame": {"x":283,"y":1703,"w":130,"h":130}, + "frame": {"x":1893,"y":1189,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -138,7 +138,7 @@ }, "sprites/belt/built/left_3.png": { - "frame": {"x":389,"y":1849,"w":130,"h":130}, + "frame": {"x":1241,"y":1347,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -146,7 +146,7 @@ }, "sprites/belt/built/left_4.png": { - "frame": {"x":417,"y":1703,"w":130,"h":130}, + "frame": {"x":1377,"y":1345,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -154,7 +154,7 @@ }, "sprites/belt/built/left_5.png": { - "frame": {"x":523,"y":1849,"w":130,"h":130}, + "frame": {"x":1513,"y":1339,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -162,7 +162,7 @@ }, "sprites/belt/built/left_6.png": { - "frame": {"x":551,"y":1703,"w":130,"h":130}, + "frame": {"x":1649,"y":1339,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -170,7 +170,7 @@ }, "sprites/belt/built/left_7.png": { - "frame": {"x":657,"y":1849,"w":130,"h":130}, + "frame": {"x":1785,"y":1339,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -178,7 +178,7 @@ }, "sprites/belt/built/left_8.png": { - "frame": {"x":791,"y":1408,"w":130,"h":130}, + "frame": {"x":1377,"y":1481,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -186,7 +186,7 @@ }, "sprites/belt/built/left_9.png": { - "frame": {"x":685,"y":1703,"w":130,"h":130}, + "frame": {"x":1802,"y":1475,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -194,7 +194,7 @@ }, "sprites/belt/built/left_10.png": { - "frame": {"x":425,"y":1186,"w":130,"h":130}, + "frame": {"x":1485,"y":1203,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -202,7 +202,7 @@ }, "sprites/belt/built/left_11.png": { - "frame": {"x":425,"y":1320,"w":130,"h":130}, + "frame": {"x":1621,"y":1203,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -210,7 +210,7 @@ }, "sprites/belt/built/left_12.png": { - "frame": {"x":424,"y":1454,"w":130,"h":130}, + "frame": {"x":1757,"y":1203,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -218,7 +218,7 @@ }, "sprites/belt/built/left_13.png": { - "frame": {"x":144,"y":1465,"w":130,"h":130}, + "frame": {"x":1806,"y":1053,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -226,7 +226,7 @@ }, "sprites/belt/built/right_0.png": { - "frame": {"x":925,"y":1476,"w":130,"h":130}, + "frame": {"x":1799,"y":1611,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -234,7 +234,7 @@ }, "sprites/belt/built/right_1.png": { - "frame": {"x":565,"y":1542,"w":130,"h":130}, + "frame": {"x":294,"y":1885,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -242,7 +242,7 @@ }, "sprites/belt/built/right_2.png": { - "frame": {"x":1461,"y":1703,"w":130,"h":130}, + "frame": {"x":1239,"y":1483,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -250,7 +250,7 @@ }, "sprites/belt/built/right_3.png": { - "frame": {"x":791,"y":1849,"w":130,"h":130}, + "frame": {"x":997,"y":1785,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -258,7 +258,7 @@ }, "sprites/belt/built/right_4.png": { - "frame": {"x":925,"y":1610,"w":130,"h":130}, + "frame": {"x":1375,"y":1617,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -266,7 +266,7 @@ }, "sprites/belt/built/right_5.png": { - "frame": {"x":1059,"y":1740,"w":130,"h":130}, + "frame": {"x":1511,"y":1710,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -274,7 +274,7 @@ }, "sprites/belt/built/right_6.png": { - "frame": {"x":1193,"y":1805,"w":130,"h":130}, + "frame": {"x":1647,"y":1710,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -282,7 +282,7 @@ }, "sprites/belt/built/right_7.png": { - "frame": {"x":1327,"y":1827,"w":130,"h":130}, + "frame": {"x":1783,"y":1747,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -290,7 +290,7 @@ }, "sprites/belt/built/right_8.png": { - "frame": {"x":925,"y":1744,"w":130,"h":130}, + "frame": {"x":1111,"y":1635,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -298,7 +298,7 @@ }, "sprites/belt/built/right_9.png": { - "frame": {"x":1059,"y":1874,"w":130,"h":130}, + "frame": {"x":1133,"y":1771,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -306,7 +306,7 @@ }, "sprites/belt/built/right_10.png": { - "frame": {"x":699,"y":1542,"w":130,"h":130}, + "frame": {"x":430,"y":1885,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -314,7 +314,7 @@ }, "sprites/belt/built/right_11.png": { - "frame": {"x":1059,"y":1606,"w":130,"h":130}, + "frame": {"x":967,"y":1513,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -322,7 +322,7 @@ }, "sprites/belt/built/right_12.png": { - "frame": {"x":1193,"y":1671,"w":130,"h":130}, + "frame": {"x":975,"y":1649,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -330,15 +330,55 @@ }, "sprites/belt/built/right_13.png": { - "frame": {"x":1327,"y":1693,"w":130,"h":130}, + "frame": {"x":1103,"y":1444,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, "sourceSize": {"w":144,"h":144} }, +"sprites/blueprints/balancer-merger-inverse.png": +{ + "frame": {"x":993,"y":753,"w":142,"h":138}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":142,"h":138}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/balancer-merger.png": +{ + "frame": {"x":1281,"y":1051,"w":139,"h":138}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":2,"w":139,"h":138}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/balancer-splitter-inverse.png": +{ + "frame": {"x":1141,"y":753,"w":142,"h":138}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":142,"h":138}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/balancer-splitter.png": +{ + "frame": {"x":664,"y":1174,"w":139,"h":138}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":2,"w":139,"h":138}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/blueprints/balancer.png": +{ + "frame": {"x":298,"y":709,"w":257,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":17,"y":0,"w":257,"h":144}, + "sourceSize": {"w":288,"h":144} +}, "sprites/blueprints/belt_left.png": { - "frame": {"x":925,"y":1878,"w":130,"h":130}, + "frame": {"x":1168,"y":1907,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -346,7 +386,7 @@ }, "sprites/blueprints/belt_right.png": { - "frame": {"x":1461,"y":1849,"w":130,"h":130}, + "frame": {"x":1269,"y":1753,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -354,7 +394,7 @@ }, "sprites/blueprints/belt_top.png": { - "frame": {"x":3,"y":1318,"w":116,"h":144}, + "frame": {"x":1799,"y":903,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -362,7 +402,7 @@ }, "sprites/blueprints/constant_signal.png": { - "frame": {"x":1938,"y":847,"w":105,"h":127}, + "frame": {"x":1921,"y":1325,"w":105,"h":127}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":0,"w":105,"h":127}, @@ -370,7 +410,7 @@ }, "sprites/blueprints/cutter-quad.png": { - "frame": {"x":3,"y":151,"w":548,"h":144}, + "frame": {"x":4,"y":559,"w":548,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":17,"y":0,"w":548,"h":144}, @@ -378,7 +418,7 @@ }, "sprites/blueprints/cutter.png": { - "frame": {"x":847,"y":298,"w":256,"h":144}, + "frame": {"x":4,"y":1589,"w":256,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":17,"y":0,"w":256,"h":144}, @@ -386,7 +426,7 @@ }, "sprites/blueprints/display.png": { - "frame": {"x":257,"y":1849,"w":128,"h":136}, + "frame": {"x":841,"y":1698,"w":128,"h":136}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":8,"y":8,"w":128,"h":136}, @@ -394,7 +434,7 @@ }, "sprites/blueprints/filter.png": { - "frame": {"x":1107,"y":556,"w":268,"h":144}, + "frame": {"x":852,"y":304,"w":268,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":16,"y":0,"w":268,"h":144}, @@ -402,7 +442,7 @@ }, "sprites/blueprints/lever.png": { - "frame": {"x":1823,"y":732,"w":111,"h":129}, + "frame": {"x":1921,"y":893,"w":111,"h":129}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":17,"y":4,"w":111,"h":129}, @@ -410,7 +450,7 @@ }, "sprites/blueprints/logic_gate-not.png": { - "frame": {"x":1545,"y":843,"w":123,"h":144}, + "frame": {"x":1596,"y":603,"w":123,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":11,"y":0,"w":123,"h":144}, @@ -418,7 +458,7 @@ }, "sprites/blueprints/logic_gate-or.png": { - "frame": {"x":1249,"y":996,"w":144,"h":123}, + "frame": {"x":258,"y":1456,"w":144,"h":123}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":123}, @@ -426,15 +466,15 @@ }, "sprites/blueprints/logic_gate-transistor.png": { - "frame": {"x":708,"y":888,"w":101,"h":144}, + "frame": {"x":1568,"y":153,"w":102,"h":144}, "rotated": false, "trimmed": true, - "spriteSourceSize": {"x":0,"y":0,"w":101,"h":144}, + "spriteSourceSize": {"x":0,"y":0,"w":102,"h":144}, "sourceSize": {"w":144,"h":144} }, "sprites/blueprints/logic_gate-xor.png": { - "frame": {"x":291,"y":890,"w":144,"h":143}, + "frame": {"x":1676,"y":154,"w":144,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":143}, @@ -442,7 +482,7 @@ }, "sprites/blueprints/logic_gate.png": { - "frame": {"x":1397,"y":852,"w":144,"h":133}, + "frame": {"x":1821,"y":454,"w":144,"h":133}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":133}, @@ -450,7 +490,7 @@ }, "sprites/blueprints/miner-chainable.png": { - "frame": {"x":151,"y":1035,"w":136,"h":143}, + "frame": {"x":522,"y":1159,"w":136,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":136,"h":143}, @@ -458,7 +498,7 @@ }, "sprites/blueprints/miner.png": { - "frame": {"x":123,"y":1318,"w":136,"h":143}, + "frame": {"x":1725,"y":593,"w":136,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":136,"h":143}, @@ -466,7 +506,7 @@ }, "sprites/blueprints/mixer.png": { - "frame": {"x":1676,"y":584,"w":261,"h":144}, + "frame": {"x":298,"y":859,"w":261,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":13,"y":0,"w":261,"h":144}, @@ -474,7 +514,7 @@ }, "sprites/blueprints/painter-double.png": { - "frame": {"x":1683,"y":3,"w":288,"h":287}, + "frame": {"x":558,"y":304,"w":288,"h":287}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":288,"h":287}, @@ -482,7 +522,7 @@ }, "sprites/blueprints/painter-mirrored.png": { - "frame": {"x":555,"y":298,"w":288,"h":144}, + "frame": {"x":1124,"y":153,"w":288,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":288,"h":144}, @@ -490,7 +530,7 @@ }, "sprites/blueprints/painter-quad.png": { - "frame": {"x":3,"y":3,"w":560,"h":144}, + "frame": {"x":558,"y":4,"w":560,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":560,"h":144}, @@ -498,7 +538,7 @@ }, "sprites/blueprints/painter.png": { - "frame": {"x":3,"y":299,"w":288,"h":144}, + "frame": {"x":1678,"y":4,"w":288,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":288,"h":144}, @@ -506,7 +546,7 @@ }, "sprites/blueprints/reader.png": { - "frame": {"x":938,"y":1181,"w":141,"h":144}, + "frame": {"x":266,"y":1585,"w":141,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":0,"w":141,"h":144}, @@ -514,15 +554,15 @@ }, "sprites/blueprints/rotater-ccw.png": { - "frame": {"x":1245,"y":1123,"w":143,"h":144}, + "frame": {"x":1420,"y":303,"w":143,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":143,"h":144}, "sourceSize": {"w":144,"h":144} }, -"sprites/blueprints/rotater-fl.png": +"sprites/blueprints/rotater-rotate180.png": { - "frame": {"x":1658,"y":1325,"w":142,"h":144}, + "frame": {"x":271,"y":1735,"w":142,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":142,"h":144}, @@ -530,55 +570,15 @@ }, "sprites/blueprints/rotater.png": { - "frame": {"x":1097,"y":1171,"w":143,"h":144}, + "frame": {"x":1826,"y":154,"w":143,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":143,"h":144}, "sourceSize": {"w":144,"h":144} }, -"sprites/blueprints/splitter-compact-inverse.png": -{ - "frame": {"x":560,"y":1036,"w":142,"h":138}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":0,"y":2,"w":142,"h":138}, - "sourceSize": {"w":144,"h":144} -}, -"sprites/blueprints/splitter-compact-merge-inverse.png": -{ - "frame": {"x":792,"y":1118,"w":142,"h":138}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":0,"y":2,"w":142,"h":138}, - "sourceSize": {"w":144,"h":144} -}, -"sprites/blueprints/splitter-compact-merge.png": -{ - "frame": {"x":1373,"y":1409,"w":139,"h":138}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":5,"y":2,"w":139,"h":138}, - "sourceSize": {"w":144,"h":144} -}, -"sprites/blueprints/splitter-compact.png": -{ - "frame": {"x":1373,"y":1551,"w":139,"h":138}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":5,"y":2,"w":139,"h":138}, - "sourceSize": {"w":144,"h":144} -}, -"sprites/blueprints/splitter.png": -{ - "frame": {"x":295,"y":299,"w":256,"h":144}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":17,"y":0,"w":256,"h":144}, - "sourceSize": {"w":288,"h":144} -}, "sprites/blueprints/stacker.png": { - "frame": {"x":295,"y":594,"w":261,"h":144}, + "frame": {"x":4,"y":1739,"w":261,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":13,"y":0,"w":261,"h":144}, @@ -586,7 +586,7 @@ }, "sprites/blueprints/trash-storage.png": { - "frame": {"x":847,"y":593,"w":250,"h":288}, + "frame": {"x":4,"y":1001,"w":250,"h":288}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":21,"y":0,"w":250,"h":288}, @@ -594,7 +594,7 @@ }, "sprites/blueprints/trash.png": { - "frame": {"x":292,"y":742,"w":144,"h":144}, + "frame": {"x":1418,"y":153,"w":144,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":144}, @@ -602,7 +602,7 @@ }, "sprites/blueprints/underground_belt_entry-tier2.png": { - "frame": {"x":1516,"y":1574,"w":138,"h":125}, + "frame": {"x":954,"y":1188,"w":138,"h":125}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":19,"w":138,"h":125}, @@ -610,7 +610,7 @@ }, "sprites/blueprints/underground_belt_entry.png": { - "frame": {"x":283,"y":1182,"w":138,"h":112}, + "frame": {"x":954,"y":1319,"w":138,"h":112}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":32,"w":138,"h":112}, @@ -618,7 +618,7 @@ }, "sprites/blueprints/underground_belt_exit-tier2.png": { - "frame": {"x":1225,"y":1555,"w":139,"h":112}, + "frame": {"x":1513,"y":1475,"w":139,"h":112}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":139,"h":112}, @@ -626,7 +626,7 @@ }, "sprites/blueprints/underground_belt_exit.png": { - "frame": {"x":283,"y":1298,"w":138,"h":112}, + "frame": {"x":1658,"y":1475,"w":138,"h":112}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":138,"h":112}, @@ -634,15 +634,23 @@ }, "sprites/blueprints/virtual_processor-analyzer.png": { - "frame": {"x":3,"y":887,"w":144,"h":144}, + "frame": {"x":1419,"y":453,"w":144,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":144}, "sourceSize": {"w":144,"h":144} }, +"sprites/blueprints/virtual_processor-painter.png": +{ + "frame": {"x":711,"y":597,"w":130,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":14,"y":0,"w":130,"h":144}, + "sourceSize": {"w":144,"h":144} +}, "sprites/blueprints/virtual_processor-rotater.png": { - "frame": {"x":1545,"y":991,"w":118,"h":144}, + "frame": {"x":1162,"y":897,"w":118,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":118,"h":144}, @@ -650,7 +658,7 @@ }, "sprites/blueprints/virtual_processor-shapecompare.png": { - "frame": {"x":1397,"y":989,"w":144,"h":133}, + "frame": {"x":715,"y":897,"w":144,"h":133}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":133}, @@ -658,15 +666,15 @@ }, "sprites/blueprints/virtual_processor-stacker.png": { - "frame": {"x":961,"y":1033,"w":132,"h":144}, + "frame": {"x":569,"y":1695,"w":130,"h":144}, "rotated": false, "trimmed": true, - "spriteSourceSize": {"x":0,"y":0,"w":132,"h":144}, + "spriteSourceSize": {"x":14,"y":0,"w":130,"h":144}, "sourceSize": {"w":144,"h":144} }, "sprites/blueprints/virtual_processor-unstacker.png": { - "frame": {"x":1101,"y":704,"w":144,"h":144}, + "frame": {"x":1569,"y":303,"w":144,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":144}, @@ -674,7 +682,7 @@ }, "sprites/blueprints/virtual_processor.png": { - "frame": {"x":291,"y":1037,"w":144,"h":141}, + "frame": {"x":847,"y":604,"w":144,"h":141}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":3,"w":144,"h":141}, @@ -682,7 +690,7 @@ }, "sprites/blueprints/wire-cross.png": { - "frame": {"x":1249,"y":704,"w":144,"h":144}, + "frame": {"x":1569,"y":453,"w":144,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":144}, @@ -690,7 +698,7 @@ }, "sprites/blueprints/wire-split.png": { - "frame": {"x":1098,"y":1000,"w":144,"h":82}, + "frame": {"x":558,"y":597,"w":144,"h":82}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":62,"w":144,"h":82}, @@ -698,7 +706,7 @@ }, "sprites/blueprints/wire-turn.png": { - "frame": {"x":706,"y":1036,"w":82,"h":82}, + "frame": {"x":557,"y":1456,"w":82,"h":82}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":62,"y":62,"w":82,"h":82}, @@ -706,7 +714,7 @@ }, "sprites/blueprints/wire.png": { - "frame": {"x":1107,"y":151,"w":20,"h":144}, + "frame": {"x":1980,"y":1028,"w":20,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":62,"y":0,"w":20,"h":144}, @@ -714,7 +722,7 @@ }, "sprites/blueprints/wire_tunnel-coating.png": { - "frame": {"x":255,"y":677,"w":33,"h":134}, + "frame": {"x":526,"y":1009,"w":33,"h":134}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":56,"y":5,"w":33,"h":134}, @@ -722,15 +730,55 @@ }, "sprites/blueprints/wire_tunnel.png": { - "frame": {"x":1516,"y":1435,"w":138,"h":135}, + "frame": {"x":807,"y":1320,"w":138,"h":135}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":138,"h":135}, "sourceSize": {"w":144,"h":144} }, +"sprites/buildings/balancer-merger-inverse.png": +{ + "frame": {"x":864,"y":1036,"w":141,"h":136}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":3,"w":141,"h":136}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/balancer-merger.png": +{ + "frame": {"x":662,"y":1318,"w":139,"h":136}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":3,"w":139,"h":136}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/balancer-splitter-inverse.png": +{ + "frame": {"x":1014,"y":897,"w":142,"h":136}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":3,"w":142,"h":136}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/balancer-splitter.png": +{ + "frame": {"x":809,"y":1178,"w":139,"h":136}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":3,"w":139,"h":136}, + "sourceSize": {"w":144,"h":144} +}, +"sprites/buildings/balancer.png": +{ + "frame": {"x":260,"y":1158,"w":256,"h":143}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":17,"y":0,"w":256,"h":143}, + "sourceSize": {"w":288,"h":144} +}, "sprites/buildings/belt_left.png": { - "frame": {"x":3,"y":1182,"w":130,"h":130}, + "frame": {"x":569,"y":1845,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -738,7 +786,7 @@ }, "sprites/buildings/belt_right.png": { - "frame": {"x":925,"y":1476,"w":130,"h":130}, + "frame": {"x":1799,"y":1611,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -746,7 +794,7 @@ }, "sprites/buildings/belt_top.png": { - "frame": {"x":440,"y":742,"w":116,"h":144}, + "frame": {"x":1867,"y":593,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -754,7 +802,7 @@ }, "sprites/buildings/constant_signal.png": { - "frame": {"x":1941,"y":716,"w":104,"h":127}, + "frame": {"x":1938,"y":1458,"w":104,"h":127}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":0,"w":104,"h":127}, @@ -762,7 +810,7 @@ }, "sprites/buildings/cutter-quad.png": { - "frame": {"x":555,"y":151,"w":548,"h":143}, + "frame": {"x":1124,"y":4,"w":548,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":17,"y":0,"w":548,"h":143}, @@ -770,7 +818,7 @@ }, "sprites/buildings/cutter.png": { - "frame": {"x":847,"y":446,"w":256,"h":143}, + "frame": {"x":258,"y":1307,"w":256,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":17,"y":0,"w":256,"h":143}, @@ -778,7 +826,7 @@ }, "sprites/buildings/display.png": { - "frame": {"x":1545,"y":704,"w":126,"h":135}, + "frame": {"x":841,"y":1840,"w":126,"h":135}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":126,"h":135}, @@ -786,7 +834,7 @@ }, "sprites/buildings/filter.png": { - "frame": {"x":1379,"y":556,"w":267,"h":144}, + "frame": {"x":1146,"y":453,"w":267,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":17,"y":0,"w":267,"h":144}, @@ -794,7 +842,7 @@ }, "sprites/buildings/hub.png": { - "frame": {"x":1131,"y":3,"w":548,"h":549}, + "frame": {"x":4,"y":4,"w":548,"h":549}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":16,"w":548,"h":549}, @@ -802,7 +850,7 @@ }, "sprites/buildings/lever.png": { - "frame": {"x":1823,"y":865,"w":109,"h":127}, + "frame": {"x":1935,"y":1611,"w":109,"h":127}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":18,"y":5,"w":109,"h":127}, @@ -810,7 +858,7 @@ }, "sprites/buildings/logic_gate-not.png": { - "frame": {"x":972,"y":885,"w":122,"h":144}, + "frame": {"x":715,"y":747,"w":122,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":12,"y":0,"w":122,"h":144}, @@ -818,7 +866,7 @@ }, "sprites/buildings/logic_gate-or.png": { - "frame": {"x":1659,"y":1198,"w":143,"h":123}, + "frame": {"x":408,"y":1456,"w":143,"h":123}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":143,"h":123}, @@ -826,7 +874,7 @@ }, "sprites/buildings/logic_gate-transistor.png": { - "frame": {"x":151,"y":887,"w":100,"h":144}, + "frame": {"x":1719,"y":303,"w":100,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":100,"h":144}, @@ -834,7 +882,7 @@ }, "sprites/buildings/logic_gate-xor.png": { - "frame": {"x":1392,"y":1126,"w":143,"h":143}, + "frame": {"x":997,"y":604,"w":143,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":143,"h":143}, @@ -842,7 +890,7 @@ }, "sprites/buildings/logic_gate.png": { - "frame": {"x":1391,"y":1273,"w":143,"h":132}, + "frame": {"x":715,"y":1036,"w":143,"h":132}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":143,"h":132}, @@ -850,7 +898,7 @@ }, "sprites/buildings/miner-chainable.png": { - "frame": {"x":3,"y":1703,"w":136,"h":142}, + "frame": {"x":520,"y":1308,"w":136,"h":142}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":136,"h":142}, @@ -858,7 +906,7 @@ }, "sprites/buildings/miner.png": { - "frame": {"x":143,"y":1703,"w":136,"h":142}, + "frame": {"x":560,"y":1547,"w":136,"h":142}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":136,"h":142}, @@ -866,7 +914,7 @@ }, "sprites/buildings/mixer.png": { - "frame": {"x":560,"y":594,"w":260,"h":143}, + "frame": {"x":4,"y":1889,"w":260,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":260,"h":143}, @@ -874,7 +922,7 @@ }, "sprites/buildings/painter-double.png": { - "frame": {"x":1683,"y":294,"w":288,"h":286}, + "frame": {"x":4,"y":709,"w":288,"h":286}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":288,"h":286}, @@ -882,7 +930,7 @@ }, "sprites/buildings/painter-mirrored.png": { - "frame": {"x":555,"y":446,"w":288,"h":144}, + "frame": {"x":1126,"y":303,"w":288,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":288,"h":144}, @@ -890,7 +938,7 @@ }, "sprites/buildings/painter-quad.png": { - "frame": {"x":567,"y":3,"w":560,"h":144}, + "frame": {"x":558,"y":154,"w":560,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":560,"h":144}, @@ -898,7 +946,7 @@ }, "sprites/buildings/painter.png": { - "frame": {"x":3,"y":447,"w":288,"h":144}, + "frame": {"x":852,"y":454,"w":288,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":288,"h":144}, @@ -906,7 +954,7 @@ }, "sprites/buildings/reader.png": { - "frame": {"x":790,"y":1260,"w":141,"h":144}, + "frame": {"x":413,"y":1585,"w":141,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":0,"w":141,"h":144}, @@ -914,15 +962,15 @@ }, "sprites/buildings/rotater-ccw.png": { - "frame": {"x":1083,"y":1319,"w":141,"h":143}, + "frame": {"x":1289,"y":753,"w":141,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":0,"w":141,"h":143}, "sourceSize": {"w":144,"h":144} }, -"sprites/buildings/rotater-fl.png": +"sprites/buildings/rotater-rotate180.png": { - "frame": {"x":935,"y":1329,"w":141,"h":143}, + "frame": {"x":1286,"y":902,"w":141,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":141,"h":143}, @@ -930,55 +978,15 @@ }, "sprites/buildings/rotater.png": { - "frame": {"x":1228,"y":1408,"w":141,"h":143}, + "frame": {"x":1011,"y":1039,"w":141,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":0,"w":141,"h":143}, "sourceSize": {"w":144,"h":144} }, -"sprites/buildings/splitter-compact-inverse.png": -{ - "frame": {"x":1080,"y":1466,"w":141,"h":136}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":0,"y":3,"w":141,"h":136}, - "sourceSize": {"w":144,"h":144} -}, -"sprites/buildings/splitter-compact-merge-inverse.png": -{ - "frame": {"x":559,"y":1178,"w":142,"h":136}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":0,"y":3,"w":142,"h":136}, - "sourceSize": {"w":144,"h":144} -}, -"sprites/buildings/splitter-compact-merge.png": -{ - "frame": {"x":1658,"y":1473,"w":139,"h":136}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":5,"y":3,"w":139,"h":136}, - "sourceSize": {"w":144,"h":144} -}, -"sprites/buildings/splitter-compact.png": -{ - "frame": {"x":1658,"y":1613,"w":139,"h":136}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":5,"y":3,"w":139,"h":136}, - "sourceSize": {"w":144,"h":144} -}, -"sprites/buildings/splitter.png": -{ - "frame": {"x":295,"y":447,"w":256,"h":143}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":17,"y":0,"w":256,"h":143}, - "sourceSize": {"w":288,"h":144} -}, "sprites/buildings/stacker.png": { - "frame": {"x":560,"y":741,"w":260,"h":143}, + "frame": {"x":260,"y":1009,"w":260,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":260,"h":143}, @@ -986,7 +994,7 @@ }, "sprites/buildings/trash-storage.png": { - "frame": {"x":3,"y":595,"w":248,"h":288}, + "frame": {"x":4,"y":1295,"w":248,"h":288}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":22,"y":0,"w":248,"h":288}, @@ -994,7 +1002,7 @@ }, "sprites/buildings/trash.png": { - "frame": {"x":1397,"y":704,"w":144,"h":144}, + "frame": {"x":1825,"y":304,"w":144,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":144}, @@ -1002,7 +1010,7 @@ }, "sprites/buildings/underground_belt_entry-tier2.png": { - "frame": {"x":3,"y":1476,"w":137,"h":124}, + "frame": {"x":1098,"y":1197,"w":137,"h":124}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":20,"w":137,"h":124}, @@ -1010,7 +1018,7 @@ }, "sprites/buildings/underground_belt_entry.png": { - "frame": {"x":283,"y":1414,"w":137,"h":111}, + "frame": {"x":1098,"y":1327,"w":137,"h":111}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":33,"w":137,"h":111}, @@ -1018,7 +1026,7 @@ }, "sprites/buildings/underground_belt_exit-tier2.png": { - "frame": {"x":283,"y":1529,"w":137,"h":111}, + "frame": {"x":1513,"y":1593,"w":137,"h":111}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":137,"h":111}, @@ -1026,7 +1034,7 @@ }, "sprites/buildings/underground_belt_exit.png": { - "frame": {"x":424,"y":1588,"w":137,"h":111}, + "frame": {"x":1656,"y":1593,"w":137,"h":111}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":137,"h":111}, @@ -1034,15 +1042,23 @@ }, "sprites/buildings/virtual_processor-analyzer.png": { - "frame": {"x":1675,"y":732,"w":144,"h":144}, + "frame": {"x":419,"y":1735,"w":144,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":144}, "sourceSize": {"w":144,"h":144} }, +"sprites/buildings/virtual_processor-painter.png": +{ + "frame": {"x":705,"y":1688,"w":130,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":14,"y":0,"w":130,"h":144}, + "sourceSize": {"w":144,"h":144} +}, "sprites/buildings/virtual_processor-rotater.png": { - "frame": {"x":439,"y":890,"w":117,"h":144}, + "frame": {"x":1158,"y":1047,"w":117,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":117,"h":144}, @@ -1050,7 +1066,7 @@ }, "sprites/buildings/virtual_processor-shapecompare.png": { - "frame": {"x":1244,"y":1271,"w":143,"h":133}, + "frame": {"x":865,"y":897,"w":143,"h":133}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":143,"h":133}, @@ -1058,15 +1074,15 @@ }, "sprites/buildings/virtual_processor-stacker.png": { - "frame": {"x":3,"y":1849,"w":130,"h":144}, + "frame": {"x":705,"y":1838,"w":130,"h":144}, "rotated": false, "trimmed": true, - "spriteSourceSize": {"x":1,"y":0,"w":130,"h":144}, + "spriteSourceSize": {"x":14,"y":0,"w":130,"h":144}, "sourceSize": {"w":144,"h":144} }, "sprites/buildings/virtual_processor-unstacker.png": { - "frame": {"x":3,"y":1035,"w":144,"h":143}, + "frame": {"x":1446,"y":603,"w":144,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":1,"w":144,"h":143}, @@ -1074,7 +1090,7 @@ }, "sprites/buildings/virtual_processor.png": { - "frame": {"x":1249,"y":852,"w":144,"h":140}, + "frame": {"x":843,"y":751,"w":144,"h":140}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":144,"h":140}, @@ -1082,7 +1098,7 @@ }, "sprites/buildings/wire-cross.png": { - "frame": {"x":1672,"y":880,"w":144,"h":144}, + "frame": {"x":561,"y":685,"w":144,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":144}, @@ -1090,7 +1106,7 @@ }, "sprites/buildings/wire-split.png": { - "frame": {"x":1667,"y":1028,"w":144,"h":81}, + "frame": {"x":565,"y":985,"w":144,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":63,"w":144,"h":81}, @@ -1098,7 +1114,7 @@ }, "sprites/buildings/wire-turn.png": { - "frame": {"x":1941,"y":631,"w":81,"h":81}, + "frame": {"x":997,"y":1921,"w":81,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":63,"y":63,"w":81,"h":81}, @@ -1106,7 +1122,7 @@ }, "sprites/buildings/wire.png": { - "frame": {"x":2027,"y":272,"w":18,"h":144}, + "frame": {"x":2006,"y":1028,"w":18,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":63,"y":0,"w":18,"h":144}, @@ -1114,7 +1130,7 @@ }, "sprites/buildings/wire_tunnel-coating.png": { - "frame": {"x":255,"y":815,"w":32,"h":134}, + "frame": {"x":1942,"y":1028,"w":32,"h":134}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":56,"y":5,"w":32,"h":134}, @@ -1122,15 +1138,79 @@ }, "sprites/buildings/wire_tunnel.png": { - "frame": {"x":559,"y":1318,"w":137,"h":134}, + "frame": {"x":702,"y":1548,"w":137,"h":134}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":5,"w":137,"h":134}, "sourceSize": {"w":144,"h":144} }, +"sprites/colors/blue.png": +{ + "frame": {"x":1103,"y":1580,"w":54,"h":49}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":4,"w":54,"h":49}, + "sourceSize": {"w":54,"h":54} +}, +"sprites/colors/cyan.png": +{ + "frame": {"x":1163,"y":1580,"w":54,"h":49}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":4,"w":54,"h":49}, + "sourceSize": {"w":54,"h":54} +}, +"sprites/colors/green.png": +{ + "frame": {"x":705,"y":1988,"w":54,"h":49}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":4,"w":54,"h":49}, + "sourceSize": {"w":54,"h":54} +}, +"sprites/colors/purple.png": +{ + "frame": {"x":765,"y":1988,"w":54,"h":49}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":4,"w":54,"h":49}, + "sourceSize": {"w":54,"h":54} +}, +"sprites/colors/red.png": +{ + "frame": {"x":1492,"y":1846,"w":54,"h":49}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":4,"w":54,"h":49}, + "sourceSize": {"w":54,"h":54} +}, +"sprites/colors/uncolored.png": +{ + "frame": {"x":1552,"y":1846,"w":54,"h":49}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":4,"w":54,"h":49}, + "sourceSize": {"w":54,"h":54} +}, +"sprites/colors/white.png": +{ + "frame": {"x":1612,"y":1846,"w":54,"h":49}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":4,"w":54,"h":49}, + "sourceSize": {"w":54,"h":54} +}, +"sprites/colors/yellow.png": +{ + "frame": {"x":1672,"y":1846,"w":54,"h":49}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":4,"w":54,"h":49}, + "sourceSize": {"w":54,"h":54} +}, "sprites/debug/acceptor_slot.png": { - "frame": {"x":1107,"y":447,"w":12,"h":12}, + "frame": {"x":1269,"y":1889,"w":12,"h":12}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, @@ -1138,7 +1218,7 @@ }, "sprites/debug/ejector_slot.png": { - "frame": {"x":1107,"y":463,"w":12,"h":12}, + "frame": {"x":1719,"y":555,"w":12,"h":12}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":12,"h":12}, @@ -1146,7 +1226,7 @@ }, "sprites/misc/hub_direction_indicator.png": { - "frame": {"x":1975,"y":272,"w":48,"h":48}, + "frame": {"x":1247,"y":1619,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -1154,7 +1234,7 @@ }, "sprites/misc/processor_disabled.png": { - "frame": {"x":3,"y":1606,"w":78,"h":81}, + "frame": {"x":1084,"y":1921,"w":78,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":10,"y":10,"w":78,"h":81}, @@ -1162,7 +1242,7 @@ }, "sprites/misc/processor_disconnected.png": { - "frame": {"x":1975,"y":3,"w":65,"h":84}, + "frame": {"x":1304,"y":1619,"w":65,"h":84}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":17,"y":8,"w":65,"h":84}, @@ -1170,7 +1250,7 @@ }, "sprites/misc/reader_overlay.png": { - "frame": {"x":1820,"y":996,"w":104,"h":70}, + "frame": {"x":951,"y":1437,"w":104,"h":70}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":38,"w":104,"h":70}, @@ -1178,7 +1258,7 @@ }, "sprites/misc/slot_bad_arrow.png": { - "frame": {"x":255,"y":638,"w":35,"h":35}, + "frame": {"x":1999,"y":161,"w":35,"h":35}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":2,"w":35,"h":35}, @@ -1186,7 +1266,7 @@ }, "sprites/misc/slot_good_arrow.png": { - "frame": {"x":255,"y":595,"w":35,"h":39}, + "frame": {"x":1999,"y":116,"w":35,"h":39}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":0,"w":35,"h":39}, @@ -1194,7 +1274,7 @@ }, "sprites/misc/storage_overlay.png": { - "frame": {"x":1935,"y":1209,"w":89,"h":44}, + "frame": {"x":566,"y":1981,"w":89,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":1,"w":89,"h":44}, @@ -1202,7 +1282,7 @@ }, "sprites/misc/waypoint.png": { - "frame": {"x":48,"y":1997,"w":38,"h":48}, + "frame": {"x":661,"y":1981,"w":38,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":38,"h":48}, @@ -1210,7 +1290,7 @@ }, "sprites/wires/boolean_false.png": { - "frame": {"x":255,"y":953,"w":31,"h":41}, + "frame": {"x":1061,"y":1437,"w":31,"h":41}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":5,"w":31,"h":41}, @@ -1218,7 +1298,7 @@ }, "sprites/wires/boolean_true.png": { - "frame": {"x":1650,"y":556,"w":22,"h":41}, + "frame": {"x":1999,"y":202,"w":22,"h":41}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":11,"y":5,"w":22,"h":41}, @@ -1226,7 +1306,7 @@ }, "sprites/wires/display/blue.png": { - "frame": {"x":1975,"y":376,"w":47,"h":47}, + "frame": {"x":1391,"y":1927,"w":47,"h":47}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":1,"w":47,"h":47}, @@ -1234,7 +1314,7 @@ }, "sprites/wires/display/cyan.png": { - "frame": {"x":1975,"y":427,"w":47,"h":47}, + "frame": {"x":1444,"y":1927,"w":47,"h":47}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":1,"w":47,"h":47}, @@ -1242,7 +1322,7 @@ }, "sprites/wires/display/green.png": { - "frame": {"x":1975,"y":478,"w":47,"h":47}, + "frame": {"x":1370,"y":1980,"w":47,"h":47}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":1,"w":47,"h":47}, @@ -1250,7 +1330,7 @@ }, "sprites/wires/display/purple.png": { - "frame": {"x":1975,"y":529,"w":47,"h":47}, + "frame": {"x":1423,"y":1980,"w":47,"h":47}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":1,"w":47,"h":47}, @@ -1258,7 +1338,7 @@ }, "sprites/wires/display/red.png": { - "frame": {"x":1975,"y":580,"w":47,"h":47}, + "frame": {"x":1497,"y":1901,"w":47,"h":47}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":1,"w":47,"h":47}, @@ -1266,7 +1346,7 @@ }, "sprites/wires/display/white.png": { - "frame": {"x":90,"y":1997,"w":47,"h":47}, + "frame": {"x":1550,"y":1901,"w":47,"h":47}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":1,"w":47,"h":47}, @@ -1274,7 +1354,7 @@ }, "sprites/wires/display/yellow.png": { - "frame": {"x":141,"y":1997,"w":47,"h":47}, + "frame": {"x":1603,"y":1901,"w":47,"h":47}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":1,"w":47,"h":47}, @@ -1282,7 +1362,7 @@ }, "sprites/wires/lever_on.png": { - "frame": {"x":1936,"y":978,"w":109,"h":127}, + "frame": {"x":1935,"y":1744,"w":109,"h":127}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":18,"y":5,"w":109,"h":127}, @@ -1290,7 +1370,7 @@ }, "sprites/wires/logical_acceptor.png": { - "frame": {"x":1975,"y":91,"w":62,"h":106}, + "frame": {"x":1972,"y":4,"w":62,"h":106}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":43,"y":0,"w":62,"h":106}, @@ -1298,7 +1378,7 @@ }, "sprites/wires/logical_ejector.png": { - "frame": {"x":1975,"y":201,"w":60,"h":67}, + "frame": {"x":1304,"y":1976,"w":60,"h":67}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":44,"y":0,"w":60,"h":67}, @@ -1306,7 +1386,7 @@ }, "sprites/wires/network_conflict.png": { - "frame": {"x":192,"y":1997,"w":47,"h":44}, + "frame": {"x":1656,"y":1901,"w":47,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":2,"w":47,"h":44}, @@ -1314,7 +1394,7 @@ }, "sprites/wires/network_empty.png": { - "frame": {"x":3,"y":1997,"w":41,"h":48}, + "frame": {"x":1732,"y":1846,"w":41,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":41,"h":48}, @@ -1322,7 +1402,7 @@ }, "sprites/wires/overlay_tile.png": { - "frame": {"x":1935,"y":1109,"w":96,"h":96}, + "frame": {"x":1719,"y":453,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -1330,7 +1410,7 @@ }, "sprites/wires/sets/color_cross.png": { - "frame": {"x":1101,"y":852,"w":144,"h":144}, + "frame": {"x":565,"y":835,"w":144,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":144}, @@ -1338,7 +1418,7 @@ }, "sprites/wires/sets/color_forward.png": { - "frame": {"x":2026,"y":420,"w":18,"h":144}, + "frame": {"x":270,"y":1889,"w":18,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":63,"y":0,"w":18,"h":144}, @@ -1346,7 +1426,7 @@ }, "sprites/wires/sets/color_split.png": { - "frame": {"x":1667,"y":1113,"w":144,"h":81}, + "frame": {"x":565,"y":1072,"w":144,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":63,"w":144,"h":81}, @@ -1354,7 +1434,7 @@ }, "sprites/wires/sets/color_turn.png": { - "frame": {"x":706,"y":1122,"w":81,"h":81}, + "frame": {"x":1405,"y":1753,"w":81,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":63,"y":63,"w":81,"h":81}, @@ -1362,7 +1442,7 @@ }, "sprites/wires/sets/conflict_cross.png": { - "frame": {"x":824,"y":885,"w":144,"h":144}, + "frame": {"x":1146,"y":603,"w":144,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":144}, @@ -1370,7 +1450,7 @@ }, "sprites/wires/sets/conflict_forward.png": { - "frame": {"x":2026,"y":568,"w":18,"h":144}, + "frame": {"x":973,"y":1840,"w":18,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":63,"y":0,"w":18,"h":144}, @@ -1378,7 +1458,7 @@ }, "sprites/wires/sets/conflict_split.png": { - "frame": {"x":813,"y":1033,"w":144,"h":81}, + "frame": {"x":645,"y":1460,"w":144,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":63,"w":144,"h":81}, @@ -1386,7 +1466,7 @@ }, "sprites/wires/sets/conflict_turn.png": { - "frame": {"x":705,"y":1207,"w":81,"h":81}, + "frame": {"x":1304,"y":1889,"w":81,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":63,"y":63,"w":81,"h":81}, @@ -1394,7 +1474,7 @@ }, "sprites/wires/sets/regular_cross.png": { - "frame": {"x":1672,"y":880,"w":144,"h":144}, + "frame": {"x":561,"y":685,"w":144,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":144}, @@ -1402,7 +1482,7 @@ }, "sprites/wires/sets/regular_forward.png": { - "frame": {"x":2027,"y":272,"w":18,"h":144}, + "frame": {"x":2006,"y":1028,"w":18,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":63,"y":0,"w":18,"h":144}, @@ -1410,7 +1490,7 @@ }, "sprites/wires/sets/regular_split.png": { - "frame": {"x":1667,"y":1028,"w":144,"h":81}, + "frame": {"x":565,"y":985,"w":144,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":63,"w":144,"h":81}, @@ -1418,7 +1498,7 @@ }, "sprites/wires/sets/regular_turn.png": { - "frame": {"x":1941,"y":631,"w":81,"h":81}, + "frame": {"x":997,"y":1921,"w":81,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":63,"y":63,"w":81,"h":81}, @@ -1426,7 +1506,7 @@ }, "sprites/wires/sets/shape_cross.png": { - "frame": {"x":560,"y":888,"w":144,"h":144}, + "frame": {"x":1296,"y":603,"w":144,"h":144}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":144,"h":144}, @@ -1434,7 +1514,7 @@ }, "sprites/wires/sets/shape_forward.png": { - "frame": {"x":1107,"y":299,"w":18,"h":144}, + "frame": {"x":1975,"y":116,"w":18,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":63,"y":0,"w":18,"h":144}, @@ -1442,7 +1522,7 @@ }, "sprites/wires/sets/shape_split.png": { - "frame": {"x":1097,"y":1086,"w":144,"h":81}, + "frame": {"x":795,"y":1461,"w":144,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":63,"w":144,"h":81}, @@ -1450,7 +1530,7 @@ }, "sprites/wires/sets/shape_turn.png": { - "frame": {"x":705,"y":1292,"w":81,"h":81}, + "frame": {"x":1405,"y":1840,"w":81,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":63,"y":63,"w":81,"h":81}, @@ -1458,7 +1538,7 @@ }, "sprites/wires/wires_preview.png": { - "frame": {"x":1975,"y":324,"w":48,"h":48}, + "frame": {"x":1247,"y":1673,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -1471,6 +1551,6 @@ "format": "RGBA8888", "size": {"w":2048,"h":2048}, "scale": "0.75", - "smartupdate": "$TexturePacker:SmartUpdate:d21082eda6f288e04b0739186004794d:0912211652d1c400e2846013f9de057b:908b89f5ca8ff73e331a35a3b14d0604$" + "smartupdate": "$TexturePacker:SmartUpdate:c57f50d18c59efc0edbd4a3a732323a4:3fcf23da2ddc6370c437cf41f6d44ed0:908b89f5ca8ff73e331a35a3b14d0604$" } } diff --git a/res_built/atlas/atlas0_hq.png b/res_built/atlas/atlas0_hq.png index f56d35d0..f5b94051 100644 Binary files a/res_built/atlas/atlas0_hq.png and b/res_built/atlas/atlas0_hq.png differ diff --git a/res_built/atlas/atlas0_lq.json b/res_built/atlas/atlas0_lq.json index c5cdf2ad..6aed36ff 100644 --- a/res_built/atlas/atlas0_lq.json +++ b/res_built/atlas/atlas0_lq.json @@ -2,7 +2,7 @@ "sprites/belt/built/forward_0.png": { - "frame": {"x":466,"y":519,"w":40,"h":48}, + "frame": {"x":157,"y":505,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -10,7 +10,7 @@ }, "sprites/belt/built/forward_1.png": { - "frame": {"x":51,"y":961,"w":40,"h":48}, + "frame": {"x":154,"y":559,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -18,7 +18,7 @@ }, "sprites/belt/built/forward_2.png": { - "frame": {"x":352,"y":869,"w":40,"h":48}, + "frame": {"x":200,"y":582,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -26,7 +26,7 @@ }, "sprites/belt/built/forward_3.png": { - "frame": {"x":300,"y":891,"w":40,"h":48}, + "frame": {"x":150,"y":613,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -34,7 +34,7 @@ }, "sprites/belt/built/forward_4.png": { - "frame": {"x":249,"y":893,"w":40,"h":48}, + "frame": {"x":100,"y":660,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -42,7 +42,7 @@ }, "sprites/belt/built/forward_5.png": { - "frame": {"x":455,"y":864,"w":40,"h":48}, + "frame": {"x":50,"y":678,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -50,7 +50,7 @@ }, "sprites/belt/built/forward_6.png": { - "frame": {"x":396,"y":912,"w":40,"h":48}, + "frame": {"x":4,"y":717,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -58,7 +58,7 @@ }, "sprites/belt/built/forward_7.png": { - "frame": {"x":344,"y":921,"w":40,"h":48}, + "frame": {"x":776,"y":352,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -66,7 +66,7 @@ }, "sprites/belt/built/forward_8.png": { - "frame": {"x":227,"y":973,"w":40,"h":48}, + "frame": {"x":715,"y":402,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -74,7 +74,7 @@ }, "sprites/belt/built/forward_9.png": { - "frame": {"x":271,"y":973,"w":40,"h":48}, + "frame": {"x":665,"y":428,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -82,7 +82,7 @@ }, "sprites/belt/built/forward_10.png": { - "frame": {"x":95,"y":961,"w":40,"h":48}, + "frame": {"x":104,"y":606,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -90,7 +90,7 @@ }, "sprites/belt/built/forward_11.png": { - "frame": {"x":139,"y":941,"w":40,"h":48}, + "frame": {"x":54,"y":624,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -98,7 +98,7 @@ }, "sprites/belt/built/forward_12.png": { - "frame": {"x":183,"y":917,"w":40,"h":48}, + "frame": {"x":4,"y":663,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -106,7 +106,7 @@ }, "sprites/belt/built/forward_13.png": { - "frame": {"x":183,"y":969,"w":40,"h":48}, + "frame": {"x":253,"y":532,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -114,7 +114,7 @@ }, "sprites/belt/built/left_0.png": { - "frame": {"x":326,"y":499,"w":44,"h":44}, + "frame": {"x":487,"y":302,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -122,7 +122,7 @@ }, "sprites/belt/built/left_1.png": { - "frame": {"x":465,"y":571,"w":44,"h":44}, + "frame": {"x":487,"y":352,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -130,7 +130,7 @@ }, "sprites/belt/built/left_2.png": { - "frame": {"x":323,"y":699,"w":44,"h":44}, + "frame": {"x":565,"y":362,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -138,7 +138,7 @@ }, "sprites/belt/built/left_3.png": { - "frame": {"x":275,"y":707,"w":44,"h":44}, + "frame": {"x":615,"y":362,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -146,7 +146,7 @@ }, "sprites/belt/built/left_4.png": { - "frame": {"x":323,"y":747,"w":44,"h":44}, + "frame": {"x":487,"y":402,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -154,7 +154,7 @@ }, "sprites/belt/built/left_5.png": { - "frame": {"x":208,"y":759,"w":44,"h":44}, + "frame": {"x":431,"y":404,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -162,7 +162,7 @@ }, "sprites/belt/built/left_6.png": { - "frame": {"x":156,"y":773,"w":44,"h":44}, + "frame": {"x":381,"y":406,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -170,7 +170,7 @@ }, "sprites/belt/built/left_7.png": { - "frame": {"x":105,"y":797,"w":44,"h":44}, + "frame": {"x":313,"y":436,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -178,7 +178,7 @@ }, "sprites/belt/built/left_8.png": { - "frame": {"x":54,"y":817,"w":44,"h":44}, + "frame": {"x":205,"y":482,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -186,7 +186,7 @@ }, "sprites/belt/built/left_9.png": { - "frame": {"x":3,"y":843,"w":44,"h":44}, + "frame": {"x":104,"y":556,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -194,7 +194,7 @@ }, "sprites/belt/built/left_10.png": { - "frame": {"x":465,"y":619,"w":44,"h":44}, + "frame": {"x":437,"y":354,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -202,7 +202,7 @@ }, "sprites/belt/built/left_11.png": { - "frame": {"x":324,"y":651,"w":44,"h":44}, + "frame": {"x":381,"y":356,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -210,7 +210,7 @@ }, "sprites/belt/built/left_12.png": { - "frame": {"x":223,"y":611,"w":44,"h":44}, + "frame": {"x":733,"y":302,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -218,7 +218,7 @@ }, "sprites/belt/built/left_13.png": { - "frame": {"x":275,"y":659,"w":44,"h":44}, + "frame": {"x":676,"y":328,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -226,7 +226,7 @@ }, "sprites/belt/built/right_0.png": { - "frame": {"x":311,"y":795,"w":44,"h":44}, + "frame": {"x":54,"y":574,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -234,7 +234,7 @@ }, "sprites/belt/built/right_1.png": { - "frame": {"x":256,"y":797,"w":44,"h":44}, + "frame": {"x":4,"y":613,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -242,7 +242,7 @@ }, "sprites/belt/built/right_2.png": { - "frame": {"x":3,"y":891,"w":44,"h":44}, + "frame": {"x":947,"y":328,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -250,7 +250,7 @@ }, "sprites/belt/built/right_3.png": { - "frame": {"x":359,"y":821,"w":44,"h":44}, + "frame": {"x":726,"y":352,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -258,7 +258,7 @@ }, "sprites/belt/built/right_4.png": { - "frame": {"x":304,"y":843,"w":44,"h":44}, + "frame": {"x":665,"y":378,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -266,7 +266,7 @@ }, "sprites/belt/built/right_5.png": { - "frame": {"x":252,"y":845,"w":44,"h":44}, + "frame": {"x":565,"y":412,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -274,7 +274,7 @@ }, "sprites/belt/built/right_6.png": { - "frame": {"x":201,"y":855,"w":44,"h":44}, + "frame": {"x":615,"y":412,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -282,7 +282,7 @@ }, "sprites/belt/built/right_7.png": { - "frame": {"x":150,"y":869,"w":44,"h":44}, + "frame": {"x":481,"y":452,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -290,7 +290,7 @@ }, "sprites/belt/built/right_8.png": { - "frame": {"x":99,"y":893,"w":44,"h":44}, + "frame": {"x":431,"y":454,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -298,7 +298,7 @@ }, "sprites/belt/built/right_9.png": { - "frame": {"x":51,"y":913,"w":44,"h":44}, + "frame": {"x":377,"y":456,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -306,7 +306,7 @@ }, "sprites/belt/built/right_10.png": { - "frame": {"x":204,"y":807,"w":44,"h":44}, + "frame": {"x":255,"y":482,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -314,7 +314,7 @@ }, "sprites/belt/built/right_11.png": { - "frame": {"x":153,"y":821,"w":44,"h":44}, + "frame": {"x":783,"y":302,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -322,7 +322,7 @@ }, "sprites/belt/built/right_12.png": { - "frame": {"x":102,"y":845,"w":44,"h":44}, + "frame": {"x":833,"y":302,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -330,15 +330,55 @@ }, "sprites/belt/built/right_13.png": { - "frame": {"x":51,"y":865,"w":44,"h":44}, + "frame": {"x":897,"y":314,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, "sourceSize": {"w":48,"h":48} }, +"sprites/blueprints/balancer-merger-inverse.png": +{ + "frame": {"x":310,"y":112,"w":48,"h":48}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/balancer-merger.png": +{ + "frame": {"x":208,"y":376,"w":47,"h":47}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":47,"h":47}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/balancer-splitter-inverse.png": +{ + "frame": {"x":310,"y":166,"w":48,"h":48}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/balancer-splitter.png": +{ + "frame": {"x":261,"y":376,"w":47,"h":47}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":47,"h":47}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/blueprints/balancer.png": +{ + "frame": {"x":770,"y":58,"w":87,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":87,"h":48}, + "sourceSize": {"w":96,"h":48} +}, "sprites/blueprints/belt_left.png": { - "frame": {"x":3,"y":939,"w":44,"h":44}, + "frame": {"x":305,"y":486,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -346,7 +386,7 @@ }, "sprites/blueprints/belt_right.png": { - "frame": {"x":407,"y":864,"w":44,"h":44}, + "frame": {"x":203,"y":532,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -354,7 +394,7 @@ }, "sprites/blueprints/belt_top.png": { - "frame": {"x":315,"y":973,"w":40,"h":48}, + "frame": {"x":822,"y":352,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -362,7 +402,7 @@ }, "sprites/blueprints/constant_signal.png": { - "frame": {"x":426,"y":411,"w":36,"h":43}, + "frame": {"x":166,"y":268,"w":36,"h":43}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":6,"y":0,"w":36,"h":43}, @@ -370,7 +410,7 @@ }, "sprites/blueprints/cutter-quad.png": { - "frame": {"x":191,"y":55,"w":184,"h":48}, + "frame": {"x":4,"y":58,"w":184,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":184,"h":48}, @@ -378,7 +418,7 @@ }, "sprites/blueprints/cutter.png": { - "frame": {"x":95,"y":296,"w":87,"h":48}, + "frame": {"x":863,"y":58,"w":87,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":87,"h":48}, @@ -386,7 +426,7 @@ }, "sprites/blueprints/display.png": { - "frame": {"x":326,"y":399,"w":44,"h":46}, + "frame": {"x":4,"y":561,"w":44,"h":46}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":2,"w":44,"h":46}, @@ -394,7 +434,7 @@ }, "sprites/blueprints/filter.png": { - "frame": {"x":3,"y":244,"w":91,"h":48}, + "frame": {"x":582,"y":4,"w":91,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":91,"h":48}, @@ -402,7 +442,7 @@ }, "sprites/blueprints/lever.png": { - "frame": {"x":231,"y":467,"w":38,"h":44}, + "frame": {"x":906,"y":214,"w":38,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":1,"w":38,"h":44}, @@ -410,7 +450,7 @@ }, "sprites/blueprints/logic_gate-not.png": { - "frame": {"x":467,"y":311,"w":42,"h":48}, + "frame": {"x":316,"y":220,"w":42,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":42,"h":48}, @@ -418,7 +458,7 @@ }, "sprites/blueprints/logic_gate-or.png": { - "frame": {"x":423,"y":818,"w":48,"h":42}, + "frame": {"x":58,"y":322,"w":48,"h":42}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":42}, @@ -426,7 +466,7 @@ }, "sprites/blueprints/logic_gate-transistor.png": { - "frame": {"x":426,"y":534,"w":35,"h":48}, + "frame": {"x":166,"y":397,"w":35,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":35,"h":48}, @@ -434,7 +474,7 @@ }, "sprites/blueprints/logic_gate-xor.png": { - "frame": {"x":291,"y":159,"w":48,"h":48}, + "frame": {"x":965,"y":4,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -442,7 +482,7 @@ }, "sprites/blueprints/logic_gate.png": { - "frame": {"x":171,"y":622,"w":48,"h":45}, + "frame": {"x":208,"y":274,"w":48,"h":45}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":45}, @@ -450,7 +490,7 @@ }, "sprites/blueprints/miner-chainable.png": { - "frame": {"x":107,"y":648,"w":47,"h":48}, + "frame": {"x":680,"y":274,"w":47,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":47,"h":48}, @@ -458,7 +498,7 @@ }, "sprites/blueprints/miner.png": { - "frame": {"x":55,"y":672,"w":47,"h":48}, + "frame": {"x":570,"y":308,"w":47,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":47,"h":48}, @@ -466,7 +506,7 @@ }, "sprites/blueprints/mixer.png": { - "frame": {"x":98,"y":244,"w":89,"h":48}, + "frame": {"x":775,"y":4,"w":89,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":89,"h":48}, @@ -474,7 +514,7 @@ }, "sprites/blueprints/painter-double.png": { - "frame": {"x":387,"y":3,"w":96,"h":96}, + "frame": {"x":4,"y":112,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -482,7 +522,7 @@ }, "sprites/blueprints/painter-mirrored.png": { - "frame": {"x":191,"y":159,"w":96,"h":48}, + "frame": {"x":208,"y":112,"w":96,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":48}, @@ -490,7 +530,7 @@ }, "sprites/blueprints/painter-quad.png": { - "frame": {"x":3,"y":3,"w":188,"h":48}, + "frame": {"x":4,"y":4,"w":188,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":188,"h":48}, @@ -498,7 +538,7 @@ }, "sprites/blueprints/painter.png": { - "frame": {"x":375,"y":203,"w":96,"h":48}, + "frame": {"x":208,"y":166,"w":96,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":48}, @@ -506,7 +546,7 @@ }, "sprites/blueprints/reader.png": { - "frame": {"x":291,"y":211,"w":48,"h":48}, + "frame": {"x":956,"y":58,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -514,15 +554,15 @@ }, "sprites/blueprints/rotater-ccw.png": { - "frame": {"x":274,"y":399,"w":48,"h":48}, + "frame": {"x":950,"y":112,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, "sourceSize": {"w":48,"h":48} }, -"sprites/blueprints/rotater-fl.png": +"sprites/blueprints/rotater-rotate180.png": { - "frame": {"x":374,"y":411,"w":48,"h":48}, + "frame": {"x":950,"y":166,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -530,55 +570,15 @@ }, "sprites/blueprints/rotater.png": { - "frame": {"x":274,"y":451,"w":48,"h":48}, + "frame": {"x":582,"y":166,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, "sourceSize": {"w":48,"h":48} }, -"sprites/blueprints/splitter-compact-inverse.png": -{ - "frame": {"x":374,"y":463,"w":48,"h":48}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, - "sourceSize": {"w":48,"h":48} -}, -"sprites/blueprints/splitter-compact-merge-inverse.png": -{ - "frame": {"x":374,"y":515,"w":48,"h":48}, - "rotated": false, - "trimmed": false, - "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, - "sourceSize": {"w":48,"h":48} -}, -"sprites/blueprints/splitter-compact-merge.png": -{ - "frame": {"x":106,"y":700,"w":47,"h":47}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":1,"y":0,"w":47,"h":47}, - "sourceSize": {"w":48,"h":48} -}, -"sprites/blueprints/splitter-compact.png": -{ - "frame": {"x":54,"y":724,"w":47,"h":47}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":1,"y":0,"w":47,"h":47}, - "sourceSize": {"w":48,"h":48} -}, -"sprites/blueprints/splitter.png": -{ - "frame": {"x":186,"y":315,"w":87,"h":48}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":5,"y":0,"w":87,"h":48}, - "sourceSize": {"w":96,"h":48} -}, "sprites/blueprints/stacker.png": { - "frame": {"x":374,"y":307,"w":89,"h":48}, + "frame": {"x":870,"y":4,"w":89,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":89,"h":48}, @@ -586,7 +586,7 @@ }, "sprites/blueprints/trash-storage.png": { - "frame": {"x":285,"y":299,"w":85,"h":96}, + "frame": {"x":768,"y":112,"w":85,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":6,"y":0,"w":85,"h":96}, @@ -594,7 +594,7 @@ }, "sprites/blueprints/trash.png": { - "frame": {"x":3,"y":400,"w":48,"h":48}, + "frame": {"x":636,"y":166,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -602,7 +602,7 @@ }, "sprites/blueprints/underground_belt_entry-tier2.png": { - "frame": {"x":447,"y":771,"w":48,"h":43}, + "frame": {"x":4,"y":322,"w":48,"h":43}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":5,"w":48,"h":43}, @@ -610,7 +610,7 @@ }, "sprites/blueprints/underground_belt_entry.png": { - "frame": {"x":55,"y":556,"w":48,"h":38}, + "frame": {"x":58,"y":370,"w":48,"h":38}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":10,"w":48,"h":38}, @@ -618,7 +618,7 @@ }, "sprites/blueprints/underground_belt_exit-tier2.png": { - "frame": {"x":55,"y":598,"w":48,"h":38}, + "frame": {"x":4,"y":371,"w":48,"h":38}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":38}, @@ -626,7 +626,7 @@ }, "sprites/blueprints/underground_belt_exit.png": { - "frame": {"x":107,"y":606,"w":48,"h":38}, + "frame": {"x":112,"y":370,"w":48,"h":38}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":38}, @@ -634,15 +634,23 @@ }, "sprites/blueprints/virtual_processor-analyzer.png": { - "frame": {"x":55,"y":400,"w":48,"h":48}, + "frame": {"x":690,"y":166,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, "sourceSize": {"w":48,"h":48} }, +"sprites/blueprints/virtual_processor-painter.png": +{ + "frame": {"x":57,"y":466,"w":44,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":4,"y":0,"w":44,"h":48}, + "sourceSize": {"w":48,"h":48} +}, "sprites/blueprints/virtual_processor-rotater.png": { - "frame": {"x":466,"y":415,"w":41,"h":48}, + "frame": {"x":316,"y":274,"w":41,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":41,"h":48}, @@ -650,7 +658,7 @@ }, "sprites/blueprints/virtual_processor-shapecompare.png": { - "frame": {"x":223,"y":659,"w":48,"h":45}, + "frame": {"x":262,"y":274,"w":48,"h":45}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":45}, @@ -658,15 +666,15 @@ }, "sprites/blueprints/virtual_processor-stacker.png": { - "frame": {"x":325,"y":547,"w":45,"h":48}, + "frame": {"x":4,"y":507,"w":44,"h":48}, "rotated": false, "trimmed": true, - "spriteSourceSize": {"x":0,"y":0,"w":45,"h":48}, + "spriteSourceSize": {"x":4,"y":0,"w":44,"h":48}, "sourceSize": {"w":48,"h":48} }, "sprites/blueprints/virtual_processor-unstacker.png": { - "frame": {"x":107,"y":400,"w":48,"h":48}, + "frame": {"x":388,"y":195,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -674,7 +682,7 @@ }, "sprites/blueprints/virtual_processor.png": { - "frame": {"x":3,"y":452,"w":48,"h":48}, + "frame": {"x":442,"y":195,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -682,7 +690,7 @@ }, "sprites/blueprints/wire-cross.png": { - "frame": {"x":55,"y":452,"w":48,"h":48}, + "frame": {"x":496,"y":195,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -690,7 +698,7 @@ }, "sprites/blueprints/wire-split.png": { - "frame": {"x":285,"y":263,"w":48,"h":28}, + "frame": {"x":741,"y":268,"w":48,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":20,"w":48,"h":28}, @@ -698,7 +706,7 @@ }, "sprites/blueprints/wire-turn.png": { - "frame": {"x":479,"y":162,"w":28,"h":28}, + "frame": {"x":912,"y":364,"w":28,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":20,"w":28,"h":28}, @@ -706,7 +714,7 @@ }, "sprites/blueprints/wire.png": { - "frame": {"x":159,"y":520,"w":8,"h":48}, + "frame": {"x":1010,"y":79,"w":8,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":0,"w":8,"h":48}, @@ -714,7 +722,7 @@ }, "sprites/blueprints/wire_tunnel-coating.png": { - "frame": {"x":426,"y":655,"w":12,"h":46}, + "frame": {"x":363,"y":345,"w":12,"h":46}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":18,"y":1,"w":12,"h":46}, @@ -722,15 +730,55 @@ }, "sprites/blueprints/wire_tunnel.png": { - "frame": {"x":107,"y":504,"w":48,"h":47}, + "frame": {"x":388,"y":249,"w":48,"h":47}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":47}, "sourceSize": {"w":48,"h":48} }, +"sprites/buildings/balancer-merger-inverse.png": +{ + "frame": {"x":442,"y":249,"w":48,"h":47}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":47}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/balancer-merger.png": +{ + "frame": {"x":207,"y":429,"w":47,"h":47}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":47,"h":47}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/balancer-splitter-inverse.png": +{ + "frame": {"x":496,"y":249,"w":48,"h":47}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":48,"h":47}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/balancer-splitter.png": +{ + "frame": {"x":260,"y":429,"w":47,"h":47}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"y":0,"w":47,"h":47}, + "sourceSize": {"w":48,"h":48} +}, +"sprites/buildings/balancer.png": +{ + "frame": {"x":582,"y":112,"w":87,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":5,"y":0,"w":87,"h":48}, + "sourceSize": {"w":96,"h":48} +}, "sprites/buildings/belt_left.png": { - "frame": {"x":326,"y":499,"w":44,"h":44}, + "frame": {"x":487,"y":302,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -738,7 +786,7 @@ }, "sprites/buildings/belt_right.png": { - "frame": {"x":311,"y":795,"w":44,"h":44}, + "frame": {"x":54,"y":574,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -746,7 +794,7 @@ }, "sprites/buildings/belt_top.png": { - "frame": {"x":466,"y":519,"w":40,"h":48}, + "frame": {"x":157,"y":505,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -754,7 +802,7 @@ }, "sprites/buildings/constant_signal.png": { - "frame": {"x":426,"y":458,"w":36,"h":43}, + "frame": {"x":166,"y":317,"w":36,"h":43}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":6,"y":0,"w":36,"h":43}, @@ -762,7 +810,7 @@ }, "sprites/buildings/cutter-quad.png": { - "frame": {"x":191,"y":107,"w":184,"h":48}, + "frame": {"x":194,"y":58,"w":184,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":184,"h":48}, @@ -770,7 +818,7 @@ }, "sprites/buildings/cutter.png": { - "frame": {"x":3,"y":348,"w":87,"h":48}, + "frame": {"x":675,"y":112,"w":87,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":87,"h":48}, @@ -778,7 +826,7 @@ }, "sprites/buildings/display.png": { - "frame": {"x":326,"y":449,"w":44,"h":46}, + "frame": {"x":437,"y":302,"w":44,"h":46}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":2,"w":44,"h":46}, @@ -786,7 +834,7 @@ }, "sprites/buildings/filter.png": { - "frame": {"x":191,"y":263,"w":90,"h":48}, + "frame": {"x":679,"y":4,"w":90,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":90,"h":48}, @@ -794,7 +842,7 @@ }, "sprites/buildings/hub.png": { - "frame": {"x":3,"y":55,"w":184,"h":185}, + "frame": {"x":392,"y":4,"w":184,"h":185}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":184,"h":185}, @@ -802,7 +850,7 @@ }, "sprites/buildings/lever.png": { - "frame": {"x":231,"y":515,"w":38,"h":44}, + "frame": {"x":906,"y":264,"w":38,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":1,"w":38,"h":44}, @@ -810,7 +858,7 @@ }, "sprites/buildings/logic_gate-not.png": { - "frame": {"x":466,"y":363,"w":43,"h":48}, + "frame": {"x":314,"y":382,"w":43,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":43,"h":48}, @@ -818,7 +866,7 @@ }, "sprites/buildings/logic_gate-or.png": { - "frame": {"x":3,"y":556,"w":48,"h":42}, + "frame": {"x":112,"y":322,"w":48,"h":42}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":42}, @@ -826,7 +874,7 @@ }, "sprites/buildings/logic_gate-transistor.png": { - "frame": {"x":426,"y":586,"w":35,"h":48}, + "frame": {"x":164,"y":451,"w":35,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":35,"h":48}, @@ -834,7 +882,7 @@ }, "sprites/buildings/logic_gate-xor.png": { - "frame": {"x":107,"y":452,"w":48,"h":48}, + "frame": {"x":208,"y":220,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -842,7 +890,7 @@ }, "sprites/buildings/logic_gate.png": { - "frame": {"x":371,"y":723,"w":48,"h":45}, + "frame": {"x":208,"y":325,"w":48,"h":45}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":45}, @@ -850,7 +898,7 @@ }, "sprites/buildings/miner-chainable.png": { - "frame": {"x":3,"y":698,"w":47,"h":48}, + "frame": {"x":623,"y":308,"w":47,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":47,"h":48}, @@ -858,7 +906,7 @@ }, "sprites/buildings/miner.png": { - "frame": {"x":158,"y":671,"w":47,"h":48}, + "frame": {"x":384,"y":302,"w":47,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":47,"h":48}, @@ -866,7 +914,7 @@ }, "sprites/buildings/mixer.png": { - "frame": {"x":374,"y":359,"w":88,"h":48}, + "frame": {"x":582,"y":58,"w":88,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":88,"h":48}, @@ -874,7 +922,7 @@ }, "sprites/buildings/painter-double.png": { - "frame": {"x":379,"y":103,"w":96,"h":96}, + "frame": {"x":106,"y":112,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -882,7 +930,7 @@ }, "sprites/buildings/painter-mirrored.png": { - "frame": {"x":191,"y":211,"w":96,"h":48}, + "frame": {"x":4,"y":214,"w":96,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":48}, @@ -890,7 +938,7 @@ }, "sprites/buildings/painter-quad.png": { - "frame": {"x":195,"y":3,"w":188,"h":48}, + "frame": {"x":198,"y":4,"w":188,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":188,"h":48}, @@ -898,7 +946,7 @@ }, "sprites/buildings/painter.png": { - "frame": {"x":375,"y":255,"w":96,"h":48}, + "frame": {"x":106,"y":214,"w":96,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":48}, @@ -906,7 +954,7 @@ }, "sprites/buildings/reader.png": { - "frame": {"x":179,"y":467,"w":48,"h":48}, + "frame": {"x":262,"y":220,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -914,15 +962,15 @@ }, "sprites/buildings/rotater-ccw.png": { - "frame": {"x":273,"y":503,"w":48,"h":48}, + "frame": {"x":4,"y":268,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, "sourceSize": {"w":48,"h":48} }, -"sprites/buildings/rotater-fl.png": +"sprites/buildings/rotater-rotate180.png": { - "frame": {"x":374,"y":567,"w":48,"h":48}, + "frame": {"x":58,"y":268,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -930,55 +978,15 @@ }, "sprites/buildings/rotater.png": { - "frame": {"x":273,"y":555,"w":48,"h":48}, + "frame": {"x":112,"y":268,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, "sourceSize": {"w":48,"h":48} }, -"sprites/buildings/splitter-compact-inverse.png": -{ - "frame": {"x":107,"y":555,"w":48,"h":47}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":0,"y":0,"w":48,"h":47}, - "sourceSize": {"w":48,"h":48} -}, -"sprites/buildings/splitter-compact-merge-inverse.png": -{ - "frame": {"x":171,"y":571,"w":48,"h":47}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":0,"y":0,"w":48,"h":47}, - "sourceSize": {"w":48,"h":48} -}, -"sprites/buildings/splitter-compact-merge.png": -{ - "frame": {"x":3,"y":750,"w":47,"h":47}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":1,"y":0,"w":47,"h":47}, - "sourceSize": {"w":48,"h":48} -}, -"sprites/buildings/splitter-compact.png": -{ - "frame": {"x":209,"y":708,"w":47,"h":47}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":1,"y":0,"w":47,"h":47}, - "sourceSize": {"w":48,"h":48} -}, -"sprites/buildings/splitter.png": -{ - "frame": {"x":94,"y":348,"w":87,"h":48}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":5,"y":0,"w":87,"h":48}, - "sourceSize": {"w":96,"h":48} -}, "sprites/buildings/stacker.png": { - "frame": {"x":3,"y":296,"w":88,"h":48}, + "frame": {"x":676,"y":58,"w":88,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":88,"h":48}, @@ -986,7 +994,7 @@ }, "sprites/buildings/trash-storage.png": { - "frame": {"x":185,"y":367,"w":85,"h":96}, + "frame": {"x":859,"y":112,"w":85,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":6,"y":0,"w":85,"h":96}, @@ -994,7 +1002,7 @@ }, "sprites/buildings/trash.png": { - "frame": {"x":374,"y":619,"w":48,"h":48}, + "frame": {"x":744,"y":214,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -1002,7 +1010,7 @@ }, "sprites/buildings/underground_belt_entry-tier2.png": { - "frame": {"x":105,"y":751,"w":47,"h":42}, + "frame": {"x":4,"y":415,"w":47,"h":42}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":6,"w":47,"h":42}, @@ -1010,7 +1018,7 @@ }, "sprites/buildings/underground_belt_entry.png": { - "frame": {"x":54,"y":775,"w":47,"h":38}, + "frame": {"x":4,"y":463,"w":47,"h":38}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":10,"w":47,"h":38}, @@ -1018,7 +1026,7 @@ }, "sprites/buildings/underground_belt_exit-tier2.png": { - "frame": {"x":3,"y":801,"w":47,"h":38}, + "frame": {"x":111,"y":414,"w":47,"h":38}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":47,"h":38}, @@ -1026,7 +1034,7 @@ }, "sprites/buildings/underground_belt_exit.png": { - "frame": {"x":260,"y":755,"w":47,"h":38}, + "frame": {"x":111,"y":458,"w":47,"h":38}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":47,"h":38}, @@ -1034,15 +1042,23 @@ }, "sprites/buildings/virtual_processor-analyzer.png": { - "frame": {"x":178,"y":519,"w":48,"h":48}, + "frame": {"x":798,"y":214,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, "sourceSize": {"w":48,"h":48} }, +"sprites/buildings/virtual_processor-painter.png": +{ + "frame": {"x":107,"y":502,"w":44,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":4,"y":0,"w":44,"h":48}, + "sourceSize": {"w":48,"h":48} +}, "sprites/buildings/virtual_processor-rotater.png": { - "frame": {"x":466,"y":467,"w":41,"h":48}, + "frame": {"x":316,"y":328,"w":41,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":41,"h":48}, @@ -1050,7 +1066,7 @@ }, "sprites/buildings/virtual_processor-shapecompare.png": { - "frame": {"x":371,"y":772,"w":48,"h":45}, + "frame": {"x":262,"y":325,"w":48,"h":45}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":45}, @@ -1058,15 +1074,15 @@ }, "sprites/buildings/virtual_processor-stacker.png": { - "frame": {"x":325,"y":599,"w":45,"h":48}, + "frame": {"x":54,"y":520,"w":44,"h":48}, "rotated": false, "trimmed": true, - "spriteSourceSize": {"x":0,"y":0,"w":45,"h":48}, + "spriteSourceSize": {"x":4,"y":0,"w":44,"h":48}, "sourceSize": {"w":48,"h":48} }, "sprites/buildings/virtual_processor-unstacker.png": { - "frame": {"x":272,"y":607,"w":48,"h":48}, + "frame": {"x":852,"y":214,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -1074,7 +1090,7 @@ }, "sprites/buildings/virtual_processor.png": { - "frame": {"x":372,"y":671,"w":48,"h":48}, + "frame": {"x":950,"y":220,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -1082,7 +1098,7 @@ }, "sprites/buildings/wire-cross.png": { - "frame": {"x":458,"y":667,"w":48,"h":48}, + "frame": {"x":950,"y":274,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -1090,7 +1106,7 @@ }, "sprites/buildings/wire-split.png": { - "frame": {"x":3,"y":602,"w":48,"h":28}, + "frame": {"x":795,"y":268,"w":48,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":20,"w":48,"h":28}, @@ -1098,7 +1114,7 @@ }, "sprites/buildings/wire-turn.png": { - "frame": {"x":479,"y":194,"w":28,"h":28}, + "frame": {"x":912,"y":398,"w":28,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":20,"w":28,"h":28}, @@ -1106,7 +1122,7 @@ }, "sprites/buildings/wire.png": { - "frame": {"x":159,"y":572,"w":8,"h":48}, + "frame": {"x":537,"y":368,"w":8,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":0,"w":8,"h":48}, @@ -1114,7 +1130,7 @@ }, "sprites/buildings/wire_tunnel-coating.png": { - "frame": {"x":442,"y":655,"w":12,"h":46}, + "frame": {"x":547,"y":316,"w":12,"h":46}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":18,"y":1,"w":12,"h":46}, @@ -1122,15 +1138,79 @@ }, "sprites/buildings/wire_tunnel.png": { - "frame": {"x":157,"y":723,"w":47,"h":46}, + "frame": {"x":58,"y":414,"w":47,"h":46}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":1,"w":47,"h":46}, "sourceSize": {"w":48,"h":48} }, +"sprites/colors/blue.png": +{ + "frame": {"x":364,"y":141,"w":18,"h":18}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":18,"h":18}, + "sourceSize": {"w":18,"h":18} +}, +"sprites/colors/cyan.png": +{ + "frame": {"x":364,"y":165,"w":18,"h":18}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":18,"h":18}, + "sourceSize": {"w":18,"h":18} +}, +"sprites/colors/green.png": +{ + "frame": {"x":364,"y":189,"w":18,"h":18}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":18,"h":18}, + "sourceSize": {"w":18,"h":18} +}, +"sprites/colors/purple.png": +{ + "frame": {"x":744,"y":166,"w":18,"h":18}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":18,"h":18}, + "sourceSize": {"w":18,"h":18} +}, +"sprites/colors/red.png": +{ + "frame": {"x":744,"y":190,"w":18,"h":18}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":18,"h":18}, + "sourceSize": {"w":18,"h":18} +}, +"sprites/colors/uncolored.png": +{ + "frame": {"x":364,"y":213,"w":18,"h":18}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":18,"h":18}, + "sourceSize": {"w":18,"h":18} +}, +"sprites/colors/white.png": +{ + "frame": {"x":364,"y":237,"w":18,"h":18}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":18,"h":18}, + "sourceSize": {"w":18,"h":18} +}, +"sprites/colors/yellow.png": +{ + "frame": {"x":364,"y":261,"w":18,"h":18}, + "rotated": false, + "trimmed": false, + "spriteSourceSize": {"x":0,"y":0,"w":18,"h":18}, + "sourceSize": {"w":18,"h":18} +}, "sprites/debug/acceptor_slot.png": { - "frame": {"x":379,"y":55,"w":4,"h":4}, + "frame": {"x":537,"y":302,"w":4,"h":4}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":4,"h":4}, @@ -1138,7 +1218,7 @@ }, "sprites/debug/ejector_slot.png": { - "frame": {"x":379,"y":63,"w":4,"h":4}, + "frame": {"x":537,"y":312,"w":4,"h":4}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":4,"h":4}, @@ -1146,7 +1226,7 @@ }, "sprites/misc/hub_direction_indicator.png": { - "frame": {"x":487,"y":30,"w":16,"h":16}, + "frame": {"x":1004,"y":133,"w":16,"h":16}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":16,"h":16}, @@ -1154,7 +1234,7 @@ }, "sprites/misc/processor_disabled.png": { - "frame": {"x":479,"y":129,"w":28,"h":29}, + "frame": {"x":531,"y":452,"w":28,"h":29}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":2,"w":28,"h":29}, @@ -1162,7 +1242,7 @@ }, "sprites/misc/processor_disconnected.png": { - "frame": {"x":475,"y":258,"w":23,"h":29}, + "frame": {"x":550,"y":237,"w":23,"h":29}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":2,"w":23,"h":29}, @@ -1170,7 +1250,7 @@ }, "sprites/misc/reader_overlay.png": { - "frame": {"x":426,"y":505,"w":36,"h":25}, + "frame": {"x":166,"y":366,"w":36,"h":25}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":6,"y":12,"w":36,"h":25}, @@ -1178,7 +1258,7 @@ }, "sprites/misc/slot_bad_arrow.png": { - "frame": {"x":426,"y":638,"w":13,"h":13}, + "frame": {"x":363,"y":307,"w":13,"h":13}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, @@ -1186,7 +1266,7 @@ }, "sprites/misc/slot_good_arrow.png": { - "frame": {"x":443,"y":638,"w":13,"h":13}, + "frame": {"x":363,"y":326,"w":13,"h":13}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, @@ -1194,7 +1274,7 @@ }, "sprites/misc/storage_overlay.png": { - "frame": {"x":479,"y":110,"w":30,"h":15}, + "frame": {"x":984,"y":378,"w":30,"h":15}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":30,"h":15}, @@ -1202,7 +1282,7 @@ }, "sprites/misc/waypoint.png": { - "frame": {"x":495,"y":291,"w":14,"h":16}, + "frame": {"x":550,"y":294,"w":14,"h":16}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":14,"h":16}, @@ -1210,7 +1290,7 @@ }, "sprites/wires/boolean_false.png": { - "frame": {"x":424,"y":705,"w":12,"h":15}, + "frame": {"x":363,"y":397,"w":12,"h":15}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":1,"w":12,"h":15}, @@ -1218,7 +1298,7 @@ }, "sprites/wires/boolean_true.png": { - "frame": {"x":440,"y":705,"w":9,"h":15}, + "frame": {"x":1010,"y":58,"w":9,"h":15}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":1,"w":9,"h":15}, @@ -1226,7 +1306,7 @@ }, "sprites/wires/display/blue.png": { - "frame": {"x":487,"y":50,"w":16,"h":16}, + "frame": {"x":1004,"y":155,"w":16,"h":16}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":16,"h":16}, @@ -1234,7 +1314,7 @@ }, "sprites/wires/display/cyan.png": { - "frame": {"x":487,"y":70,"w":16,"h":16}, + "frame": {"x":1004,"y":177,"w":16,"h":16}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":16,"h":16}, @@ -1242,7 +1322,7 @@ }, "sprites/wires/display/green.png": { - "frame": {"x":487,"y":90,"w":16,"h":16}, + "frame": {"x":1004,"y":199,"w":16,"h":16}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":16,"h":16}, @@ -1250,7 +1330,7 @@ }, "sprites/wires/display/purple.png": { - "frame": {"x":475,"y":291,"w":16,"h":16}, + "frame": {"x":1004,"y":221,"w":16,"h":16}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":16,"h":16}, @@ -1258,7 +1338,7 @@ }, "sprites/wires/display/red.png": { - "frame": {"x":159,"y":400,"w":16,"h":16}, + "frame": {"x":1004,"y":243,"w":16,"h":16}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":16,"h":16}, @@ -1266,7 +1346,7 @@ }, "sprites/wires/display/white.png": { - "frame": {"x":159,"y":420,"w":16,"h":16}, + "frame": {"x":1004,"y":265,"w":16,"h":16}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":16,"h":16}, @@ -1274,7 +1354,7 @@ }, "sprites/wires/display/yellow.png": { - "frame": {"x":159,"y":440,"w":16,"h":16}, + "frame": {"x":1004,"y":287,"w":16,"h":16}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":16,"h":16}, @@ -1282,7 +1362,7 @@ }, "sprites/wires/lever_on.png": { - "frame": {"x":230,"y":563,"w":38,"h":44}, + "frame": {"x":868,"y":364,"w":38,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":1,"w":38,"h":44}, @@ -1290,7 +1370,7 @@ }, "sprites/wires/logical_acceptor.png": { - "frame": {"x":343,"y":259,"w":23,"h":36}, + "frame": {"x":550,"y":195,"w":23,"h":36}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":13,"y":0,"w":23,"h":36}, @@ -1298,7 +1378,7 @@ }, "sprites/wires/logical_ejector.png": { - "frame": {"x":487,"y":3,"w":22,"h":23}, + "frame": {"x":364,"y":112,"w":22,"h":23}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":22,"h":23}, @@ -1306,7 +1386,7 @@ }, "sprites/wires/network_conflict.png": { - "frame": {"x":159,"y":460,"w":16,"h":16}, + "frame": {"x":1004,"y":309,"w":16,"h":16}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":16,"h":16}, @@ -1314,7 +1394,7 @@ }, "sprites/wires/network_empty.png": { - "frame": {"x":159,"y":500,"w":15,"h":16}, + "frame": {"x":363,"y":285,"w":15,"h":16}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":15,"h":16}, @@ -1322,7 +1402,7 @@ }, "sprites/wires/overlay_tile.png": { - "frame": {"x":343,"y":159,"w":32,"h":32}, + "frame": {"x":946,"y":378,"w":32,"h":32}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":32,"h":32}, @@ -1330,7 +1410,7 @@ }, "sprites/wires/sets/color_cross.png": { - "frame": {"x":453,"y":719,"w":48,"h":48}, + "frame": {"x":579,"y":220,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -1338,7 +1418,7 @@ }, "sprites/wires/sets/color_forward.png": { - "frame": {"x":423,"y":724,"w":8,"h":48}, + "frame": {"x":551,"y":368,"w":8,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":0,"w":8,"h":48}, @@ -1346,7 +1426,7 @@ }, "sprites/wires/sets/color_split.png": { - "frame": {"x":3,"y":634,"w":48,"h":28}, + "frame": {"x":849,"y":268,"w":48,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":20,"w":48,"h":28}, @@ -1354,7 +1434,7 @@ }, "sprites/wires/sets/color_turn.png": { - "frame": {"x":343,"y":195,"w":28,"h":28}, + "frame": {"x":984,"y":399,"w":28,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":20,"w":28,"h":28}, @@ -1362,7 +1442,7 @@ }, "sprites/wires/sets/conflict_cross.png": { - "frame": {"x":3,"y":504,"w":48,"h":48}, + "frame": {"x":633,"y":220,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -1370,7 +1450,7 @@ }, "sprites/wires/sets/conflict_forward.png": { - "frame": {"x":435,"y":724,"w":8,"h":48}, + "frame": {"x":363,"y":418,"w":8,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":0,"w":8,"h":48}, @@ -1378,7 +1458,7 @@ }, "sprites/wires/sets/conflict_split.png": { - "frame": {"x":55,"y":640,"w":48,"h":28}, + "frame": {"x":572,"y":274,"w":48,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":20,"w":48,"h":28}, @@ -1386,7 +1466,7 @@ }, "sprites/wires/sets/conflict_turn.png": { - "frame": {"x":475,"y":226,"w":28,"h":28}, + "frame": {"x":946,"y":416,"w":28,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":20,"w":28,"h":28}, @@ -1394,7 +1474,7 @@ }, "sprites/wires/sets/regular_cross.png": { - "frame": {"x":458,"y":667,"w":48,"h":48}, + "frame": {"x":950,"y":274,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -1402,7 +1482,7 @@ }, "sprites/wires/sets/regular_forward.png": { - "frame": {"x":159,"y":572,"w":8,"h":48}, + "frame": {"x":537,"y":368,"w":8,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":0,"w":8,"h":48}, @@ -1410,7 +1490,7 @@ }, "sprites/wires/sets/regular_split.png": { - "frame": {"x":3,"y":602,"w":48,"h":28}, + "frame": {"x":795,"y":268,"w":48,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":20,"w":48,"h":28}, @@ -1418,7 +1498,7 @@ }, "sprites/wires/sets/regular_turn.png": { - "frame": {"x":479,"y":194,"w":28,"h":28}, + "frame": {"x":912,"y":398,"w":28,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":20,"w":28,"h":28}, @@ -1426,7 +1506,7 @@ }, "sprites/wires/sets/shape_cross.png": { - "frame": {"x":55,"y":504,"w":48,"h":48}, + "frame": {"x":687,"y":220,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -1434,7 +1514,7 @@ }, "sprites/wires/sets/shape_forward.png": { - "frame": {"x":499,"y":771,"w":8,"h":48}, + "frame": {"x":883,"y":302,"w":8,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":0,"w":8,"h":48}, @@ -1442,7 +1522,7 @@ }, "sprites/wires/sets/shape_split.png": { - "frame": {"x":3,"y":666,"w":48,"h":28}, + "frame": {"x":626,"y":274,"w":48,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":20,"w":48,"h":28}, @@ -1450,7 +1530,7 @@ }, "sprites/wires/sets/shape_turn.png": { - "frame": {"x":343,"y":227,"w":28,"h":28}, + "frame": {"x":980,"y":433,"w":28,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":20,"y":20,"w":28,"h":28}, @@ -1458,7 +1538,7 @@ }, "sprites/wires/wires_preview.png": { - "frame": {"x":159,"y":480,"w":16,"h":16}, + "frame": {"x":550,"y":272,"w":16,"h":16}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":16,"h":16}, @@ -1469,8 +1549,8 @@ "version": "1.0", "image": "atlas0_lq.png", "format": "RGBA8888", - "size": {"w":512,"h":1024}, + "size": {"w":1024,"h":1024}, "scale": "0.25", - "smartupdate": "$TexturePacker:SmartUpdate:d21082eda6f288e04b0739186004794d:0912211652d1c400e2846013f9de057b:908b89f5ca8ff73e331a35a3b14d0604$" + "smartupdate": "$TexturePacker:SmartUpdate:c57f50d18c59efc0edbd4a3a732323a4:3fcf23da2ddc6370c437cf41f6d44ed0:908b89f5ca8ff73e331a35a3b14d0604$" } } diff --git a/res_built/atlas/atlas0_lq.png b/res_built/atlas/atlas0_lq.png index 1c60156c..a5957fda 100644 Binary files a/res_built/atlas/atlas0_lq.png and b/res_built/atlas/atlas0_lq.png differ diff --git a/res_built/atlas/atlas0_mq.json b/res_built/atlas/atlas0_mq.json index c5d7d311..e9687dbb 100644 --- a/res_built/atlas/atlas0_mq.json +++ b/res_built/atlas/atlas0_mq.json @@ -2,7 +2,7 @@ "sprites/belt/built/forward_0.png": { - "frame": {"x":943,"y":803,"w":78,"h":96}, + "frame": {"x":942,"y":1319,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -10,7 +10,7 @@ }, "sprites/belt/built/forward_1.png": { - "frame": {"x":94,"y":1746,"w":78,"h":96}, + "frame": {"x":174,"y":1897,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -18,7 +18,7 @@ }, "sprites/belt/built/forward_2.png": { - "frame": {"x":754,"y":1555,"w":78,"h":96}, + "frame": {"x":280,"y":1754,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -26,7 +26,7 @@ }, "sprites/belt/built/forward_3.png": { - "frame": {"x":653,"y":1564,"w":78,"h":96}, + "frame": {"x":756,"y":1691,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -34,7 +34,7 @@ }, "sprites/belt/built/forward_4.png": { - "frame": {"x":556,"y":1601,"w":78,"h":96}, + "frame": {"x":656,"y":1723,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -42,7 +42,7 @@ }, "sprites/belt/built/forward_5.png": { - "frame": {"x":458,"y":1663,"w":78,"h":96}, + "frame": {"x":557,"y":1775,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -50,7 +50,7 @@ }, "sprites/belt/built/forward_6.png": { - "frame": {"x":359,"y":1711,"w":78,"h":96}, + "frame": {"x":459,"y":1834,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -58,7 +58,7 @@ }, "sprites/belt/built/forward_7.png": { - "frame": {"x":268,"y":1771,"w":78,"h":96}, + "frame": {"x":364,"y":1843,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -66,7 +66,7 @@ }, "sprites/belt/built/forward_8.png": { - "frame": {"x":176,"y":1835,"w":78,"h":96}, + "frame": {"x":258,"y":1945,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -74,7 +74,7 @@ }, "sprites/belt/built/forward_9.png": { - "frame": {"x":85,"y":1846,"w":78,"h":96}, + "frame": {"x":342,"y":1945,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -82,7 +82,7 @@ }, "sprites/belt/built/forward_10.png": { - "frame": {"x":3,"y":1789,"w":78,"h":96}, + "frame": {"x":672,"y":1621,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -90,7 +90,7 @@ }, "sprites/belt/built/forward_11.png": { - "frame": {"x":3,"y":1889,"w":78,"h":96}, + "frame": {"x":572,"y":1673,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -98,7 +98,7 @@ }, "sprites/belt/built/forward_12.png": { - "frame": {"x":856,"y":1469,"w":78,"h":96}, + "frame": {"x":473,"y":1732,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -106,7 +106,7 @@ }, "sprites/belt/built/forward_13.png": { - "frame": {"x":938,"y":1469,"w":78,"h":96}, + "frame": {"x":375,"y":1741,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -114,7 +114,7 @@ }, "sprites/belt/built/left_0.png": { - "frame": {"x":403,"y":911,"w":87,"h":87}, + "frame": {"x":200,"y":1441,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -122,7 +122,7 @@ }, "sprites/belt/built/left_1.png": { - "frame": {"x":403,"y":1002,"w":87,"h":87}, + "frame": {"x":101,"y":1516,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -130,7 +130,7 @@ }, "sprites/belt/built/left_2.png": { - "frame": {"x":3,"y":1516,"w":87,"h":87}, + "frame": {"x":544,"y":1453,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -138,7 +138,7 @@ }, "sprites/belt/built/left_3.png": { - "frame": {"x":785,"y":1191,"w":87,"h":87}, + "frame": {"x":837,"y":1412,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -146,7 +146,7 @@ }, "sprites/belt/built/left_4.png": { - "frame": {"x":876,"y":1196,"w":87,"h":87}, + "frame": {"x":930,"y":1421,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -154,7 +154,7 @@ }, "sprites/belt/built/left_5.png": { - "frame": {"x":785,"y":1282,"w":87,"h":87}, + "frame": {"x":735,"y":1435,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -162,7 +162,7 @@ }, "sprites/belt/built/left_6.png": { - "frame": {"x":685,"y":1287,"w":87,"h":87}, + "frame": {"x":637,"y":1487,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -170,7 +170,7 @@ }, "sprites/belt/built/left_7.png": { - "frame": {"x":583,"y":1325,"w":87,"h":87}, + "frame": {"x":828,"y":1505,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -178,7 +178,7 @@ }, "sprites/belt/built/left_8.png": { - "frame": {"x":482,"y":1381,"w":87,"h":87}, + "frame": {"x":730,"y":1528,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -186,7 +186,7 @@ }, "sprites/belt/built/left_9.png": { - "frame": {"x":383,"y":1429,"w":87,"h":87}, + "frame": {"x":921,"y":1514,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -194,7 +194,7 @@ }, "sprites/belt/built/left_10.png": { - "frame": {"x":391,"y":1338,"w":87,"h":87}, + "frame": {"x":4,"y":1520,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -202,7 +202,7 @@ }, "sprites/belt/built/left_11.png": { - "frame": {"x":292,"y":1398,"w":87,"h":87}, + "frame": {"x":849,"y":1319,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -210,7 +210,7 @@ }, "sprites/belt/built/left_12.png": { - "frame": {"x":196,"y":1462,"w":87,"h":87}, + "frame": {"x":744,"y":1342,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -218,7 +218,7 @@ }, "sprites/belt/built/left_13.png": { - "frame": {"x":99,"y":1473,"w":87,"h":87}, + "frame": {"x":642,"y":1394,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -226,7 +226,7 @@ }, "sprites/belt/built/right_0.png": { - "frame": {"x":287,"y":1489,"w":87,"h":87}, + "frame": {"x":823,"y":1598,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -234,7 +234,7 @@ }, "sprites/belt/built/right_1.png": { - "frame": {"x":190,"y":1553,"w":87,"h":87}, + "frame": {"x":916,"y":1607,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -242,7 +242,7 @@ }, "sprites/belt/built/right_2.png": { - "frame": {"x":674,"y":1378,"w":87,"h":87}, + "frame": {"x":4,"y":1613,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -250,7 +250,7 @@ }, "sprites/belt/built/right_3.png": { - "frame": {"x":573,"y":1416,"w":87,"h":87}, + "frame": {"x":486,"y":1546,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -258,7 +258,7 @@ }, "sprites/belt/built/right_4.png": { - "frame": {"x":474,"y":1472,"w":87,"h":87}, + "frame": {"x":386,"y":1555,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -266,7 +266,7 @@ }, "sprites/belt/built/right_5.png": { - "frame": {"x":378,"y":1520,"w":87,"h":87}, + "frame": {"x":287,"y":1564,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -274,7 +274,7 @@ }, "sprites/belt/built/right_6.png": { - "frame": {"x":281,"y":1580,"w":87,"h":87}, + "frame": {"x":190,"y":1627,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -282,7 +282,7 @@ }, "sprites/belt/built/right_7.png": { - "frame": {"x":185,"y":1644,"w":87,"h":87}, + "frame": {"x":97,"y":1702,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -290,7 +290,7 @@ }, "sprites/belt/built/right_8.png": { - "frame": {"x":94,"y":1655,"w":87,"h":87}, + "frame": {"x":4,"y":1706,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -298,7 +298,7 @@ }, "sprites/belt/built/right_9.png": { - "frame": {"x":3,"y":1698,"w":87,"h":87}, + "frame": {"x":579,"y":1580,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -306,7 +306,7 @@ }, "sprites/belt/built/right_10.png": { - "frame": {"x":94,"y":1564,"w":87,"h":87}, + "frame": {"x":393,"y":1462,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -314,7 +314,7 @@ }, "sprites/belt/built/right_11.png": { - "frame": {"x":3,"y":1607,"w":87,"h":87}, + "frame": {"x":293,"y":1471,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -322,7 +322,7 @@ }, "sprites/belt/built/right_12.png": { - "frame": {"x":876,"y":1287,"w":87,"h":87}, + "frame": {"x":194,"y":1534,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -330,15 +330,55 @@ }, "sprites/belt/built/right_13.png": { - "frame": {"x":776,"y":1373,"w":87,"h":87}, + "frame": {"x":97,"y":1609,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, "sourceSize": {"w":96,"h":96} }, +"sprites/blueprints/balancer-merger-inverse.png": +{ + "frame": {"x":453,"y":1170,"w":95,"h":93}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":1,"w":95,"h":93}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/balancer-merger.png": +{ + "frame": {"x":4,"y":1320,"w":93,"h":93}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":1,"w":93,"h":93}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/balancer-splitter-inverse.png": +{ + "frame": {"x":348,"y":1180,"w":95,"h":93}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":1,"w":95,"h":93}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/balancer-splitter.png": +{ + "frame": {"x":694,"y":1113,"w":93,"h":93}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":1,"w":93,"h":93}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/blueprints/balancer.png": +{ + "frame": {"x":184,"y":581,"w":172,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":11,"y":0,"w":172,"h":96}, + "sourceSize": {"w":192,"h":96} +}, "sprites/blueprints/belt_left.png": { - "frame": {"x":867,"y":1378,"w":87,"h":87}, + "frame": {"x":479,"y":1639,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -346,7 +386,7 @@ }, "sprites/blueprints/belt_right.png": { - "frame": {"x":765,"y":1464,"w":87,"h":87}, + "frame": {"x":380,"y":1648,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -354,7 +394,7 @@ }, "sprites/blueprints/belt_top.png": { - "frame": {"x":85,"y":1946,"w":78,"h":96}, + "frame": {"x":426,"y":1945,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -362,7 +402,7 @@ }, "sprites/blueprints/constant_signal.png": { - "frame": {"x":949,"y":396,"w":71,"h":85}, + "frame": {"x":945,"y":519,"w":71,"h":85}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":13,"y":0,"w":71,"h":85}, @@ -370,7 +410,7 @@ }, "sprites/blueprints/cutter-quad.png": { - "frame": {"x":373,"y":103,"w":366,"h":96}, + "frame": {"x":376,"y":106,"w":366,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":11,"y":0,"w":366,"h":96}, @@ -378,7 +418,7 @@ }, "sprites/blueprints/cutter.png": { - "frame": {"x":745,"y":594,"w":172,"h":96}, + "frame": {"x":362,"y":616,"w":172,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":11,"y":0,"w":172,"h":96}, @@ -386,7 +426,7 @@ }, "sprites/blueprints/display.png": { - "frame": {"x":664,"y":1469,"w":86,"h":91}, + "frame": {"x":283,"y":1657,"w":86,"h":91}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":5,"w":86,"h":91}, @@ -394,7 +434,7 @@ }, "sprites/blueprints/filter.png": { - "frame": {"x":569,"y":303,"w":180,"h":96}, + "frame": {"x":746,"y":603,"w":180,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":10,"y":0,"w":180,"h":96}, @@ -402,7 +442,7 @@ }, "sprites/blueprints/lever.png": { - "frame": {"x":167,"y":1935,"w":75,"h":86}, + "frame": {"x":945,"y":427,"w":75,"h":86}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":11,"y":3,"w":75,"h":86}, @@ -410,7 +450,7 @@ }, "sprites/blueprints/logic_gate-not.png": { - "frame": {"x":469,"y":1563,"w":83,"h":96}, + "frame": {"x":97,"y":1795,"w":83,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":7,"y":0,"w":83,"h":96}, @@ -418,7 +458,7 @@ }, "sprites/blueprints/logic_gate-or.png": { - "frame": {"x":303,"y":903,"w":96,"h":82}, + "frame": {"x":454,"y":980,"w":96,"h":82}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":82}, @@ -426,7 +466,7 @@ }, "sprites/blueprints/logic_gate-transistor.png": { - "frame": {"x":451,"y":703,"w":68,"h":96}, + "frame": {"x":461,"y":718,"w":68,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":68,"h":96}, @@ -434,7 +474,7 @@ }, "sprites/blueprints/logic_gate-xor.png": { - "frame": {"x":3,"y":674,"w":96,"h":96}, + "frame": {"x":572,"y":706,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -442,7 +482,7 @@ }, "sprites/blueprints/logic_gate.png": { - "frame": {"x":910,"y":1103,"w":96,"h":89}, + "frame": {"x":4,"y":887,"w":96,"h":89}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":89}, @@ -450,7 +490,7 @@ }, "sprites/blueprints/miner-chainable.png": { - "frame": {"x":100,"y":1373,"w":92,"h":96}, + "frame": {"x":653,"y":1212,"w":92,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":92,"h":96}, @@ -458,7 +498,7 @@ }, "sprites/blueprints/miner.png": { - "frame": {"x":3,"y":1416,"w":92,"h":96}, + "frame": {"x":548,"y":1271,"w":92,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":92,"h":96}, @@ -466,7 +506,7 @@ }, "sprites/blueprints/mixer.png": { - "frame": {"x":3,"y":474,"w":175,"h":96}, + "frame": {"x":4,"y":479,"w":175,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":8,"y":0,"w":175,"h":96}, @@ -474,7 +514,7 @@ }, "sprites/blueprints/painter-double.png": { - "frame": {"x":759,"y":3,"w":192,"h":192}, + "frame": {"x":764,"y":4,"w":192,"h":192}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":192,"h":192}, @@ -482,7 +522,7 @@ }, "sprites/blueprints/painter-mirrored.png": { - "frame": {"x":373,"y":303,"w":192,"h":96}, + "frame": {"x":376,"y":310,"w":192,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":192,"h":96}, @@ -490,7 +530,7 @@ }, "sprites/blueprints/painter-quad.png": { - "frame": {"x":3,"y":3,"w":374,"h":96}, + "frame": {"x":4,"y":4,"w":374,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":374,"h":96}, @@ -498,7 +538,7 @@ }, "sprites/blueprints/painter.png": { - "frame": {"x":753,"y":394,"w":192,"h":96}, + "frame": {"x":376,"y":412,"w":192,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":192,"h":96}, @@ -506,7 +546,7 @@ }, "sprites/blueprints/reader.png": { - "frame": {"x":3,"y":926,"w":95,"h":96}, + "frame": {"x":454,"y":1068,"w":95,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":95,"h":96}, @@ -514,15 +554,15 @@ }, "sprites/blueprints/rotater-ccw.png": { - "frame": {"x":103,"y":674,"w":96,"h":96}, + "frame": {"x":917,"y":814,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, "sourceSize": {"w":96,"h":96} }, -"sprites/blueprints/rotater-fl.png": +"sprites/blueprints/rotater-rotate180.png": { - "frame": {"x":102,"y":983,"w":95,"h":96}, + "frame": {"x":352,"y":1078,"w":95,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":95,"h":96}, @@ -530,55 +570,15 @@ }, "sprites/blueprints/rotater.png": { - "frame": {"x":203,"y":674,"w":96,"h":96}, + "frame": {"x":181,"y":683,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, "sourceSize": {"w":96,"h":96} }, -"sprites/blueprints/splitter-compact-inverse.png": -{ - "frame": {"x":300,"y":1047,"w":95,"h":93}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":0,"y":1,"w":95,"h":93}, - "sourceSize": {"w":96,"h":96} -}, -"sprites/blueprints/splitter-compact-merge-inverse.png": -{ - "frame": {"x":201,"y":1111,"w":95,"h":93}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":0,"y":1,"w":95,"h":93}, - "sourceSize": {"w":96,"h":96} -}, -"sprites/blueprints/splitter-compact-merge.png": -{ - "frame": {"x":300,"y":1144,"w":93,"h":93}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":3,"y":1,"w":93,"h":93}, - "sourceSize": {"w":96,"h":96} -}, -"sprites/blueprints/splitter-compact.png": -{ - "frame": {"x":201,"y":1208,"w":93,"h":93}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":3,"y":1,"w":93,"h":93}, - "sourceSize": {"w":96,"h":96} -}, -"sprites/blueprints/splitter.png": -{ - "frame": {"x":3,"y":574,"w":171,"h":96}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":11,"y":0,"w":171,"h":96}, - "sourceSize": {"w":192,"h":96} -}, "sprites/blueprints/stacker.png": { - "frame": {"x":182,"y":474,"w":175,"h":96}, + "frame": {"x":185,"y":479,"w":175,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":8,"y":0,"w":175,"h":96}, @@ -586,7 +586,7 @@ }, "sprites/blueprints/trash-storage.png": { - "frame": {"x":528,"y":603,"w":167,"h":192}, + "frame": {"x":574,"y":310,"w":167,"h":192}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":167,"h":192}, @@ -594,7 +594,7 @@ }, "sprites/blueprints/trash.png": { - "frame": {"x":351,"y":703,"w":96,"h":96}, + "frame": {"x":359,"y":718,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -602,7 +602,7 @@ }, "sprites/blueprints/underground_belt_entry-tier2.png": { - "frame": {"x":397,"y":1172,"w":93,"h":84}, + "frame": {"x":347,"y":1279,"w":93,"h":84}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":12,"w":93,"h":84}, @@ -610,7 +610,7 @@ }, "sprites/blueprints/underground_belt_entry.png": { - "frame": {"x":298,"y":1241,"w":93,"h":75}, + "frame": {"x":243,"y":1280,"w":93,"h":75}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":21,"w":93,"h":75}, @@ -618,7 +618,7 @@ }, "sprites/blueprints/underground_belt_exit-tier2.png": { - "frame": {"x":399,"y":1093,"w":94,"h":75}, + "frame": {"x":4,"y":1239,"w":94,"h":75}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":0,"w":94,"h":75}, @@ -626,7 +626,7 @@ }, "sprites/blueprints/underground_belt_exit.png": { - "frame": {"x":198,"y":1305,"w":93,"h":75}, + "frame": {"x":103,"y":1334,"w":93,"h":75}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":0,"w":93,"h":75}, @@ -634,15 +634,23 @@ }, "sprites/blueprints/virtual_processor-analyzer.png": { - "frame": {"x":523,"y":799,"w":96,"h":96}, + "frame": {"x":564,"y":808,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, "sourceSize": {"w":96,"h":96} }, +"sprites/blueprints/virtual_processor-painter.png": +{ + "frame": {"x":932,"y":610,"w":87,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":0,"w":87,"h":96}, + "sourceSize": {"w":96,"h":96} +}, "sprites/blueprints/virtual_processor-rotater.png": { - "frame": {"x":276,"y":1671,"w":79,"h":96}, + "frame": {"x":4,"y":1901,"w":79,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":79,"h":96}, @@ -650,7 +658,7 @@ }, "sprites/blueprints/virtual_processor-shapecompare.png": { - "frame": {"x":3,"y":774,"w":96,"h":89}, + "frame": {"x":4,"y":982,"w":96,"h":89}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":89}, @@ -658,15 +666,15 @@ }, "sprites/blueprints/virtual_processor-stacker.png": { - "frame": {"x":931,"y":903,"w":88,"h":96}, + "frame": {"x":931,"y":712,"w":87,"h":96}, "rotated": false, "trimmed": true, - "spriteSourceSize": {"x":0,"y":0,"w":88,"h":96}, + "spriteSourceSize": {"x":9,"y":0,"w":87,"h":96}, "sourceSize": {"w":96,"h":96} }, "sprites/blueprints/virtual_processor-unstacker.png": { - "frame": {"x":349,"y":803,"w":96,"h":96}, + "frame": {"x":359,"y":820,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -674,7 +682,7 @@ }, "sprites/blueprints/virtual_processor.png": { - "frame": {"x":710,"y":1090,"w":96,"h":94}, + "frame": {"x":250,"y":827,"w":96,"h":94}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":2,"w":96,"h":94}, @@ -682,7 +690,7 @@ }, "sprites/blueprints/wire-cross.png": { - "frame": {"x":522,"y":899,"w":96,"h":96}, + "frame": {"x":461,"y":878,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -690,7 +698,7 @@ }, "sprites/blueprints/wire-split.png": { - "frame": {"x":3,"y":867,"w":96,"h":55}, + "frame": {"x":352,"y":1017,"w":96,"h":55}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":41,"w":96,"h":55}, @@ -698,7 +706,7 @@ }, "sprites/blueprints/wire-turn.png": { - "frame": {"x":955,"y":105,"w":55,"h":55}, + "frame": {"x":962,"y":4,"w":55,"h":55}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":41,"y":41,"w":55,"h":55}, @@ -706,7 +714,7 @@ }, "sprites/blueprints/wire.png": { - "frame": {"x":699,"y":603,"w":14,"h":96}, + "frame": {"x":892,"y":909,"w":14,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":41,"y":0,"w":14,"h":96}, @@ -714,7 +722,7 @@ }, "sprites/blueprints/wire_tunnel-coating.png": { - "frame": {"x":921,"y":594,"w":23,"h":90}, + "frame": {"x":535,"y":782,"w":23,"h":90}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":37,"y":3,"w":23,"h":90}, @@ -722,15 +730,55 @@ }, "sprites/blueprints/wire_tunnel.png": { - "frame": {"x":101,"y":1278,"w":93,"h":91}, + "frame": {"x":886,"y":1222,"w":93,"h":91}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":2,"w":93,"h":91}, "sourceSize": {"w":96,"h":96} }, +"sprites/buildings/balancer-merger-inverse.png": +{ + "frame": {"x":105,"y":1237,"w":94,"h":91}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":94,"h":91}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/balancer-merger.png": +{ + "frame": {"x":554,"y":1174,"w":93,"h":91}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":2,"w":93,"h":91}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/balancer-splitter-inverse.png": +{ + "frame": {"x":246,"y":1183,"w":95,"h":91}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":95,"h":91}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/balancer-splitter.png": +{ + "frame": {"x":449,"y":1269,"w":93,"h":91}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":3,"y":2,"w":93,"h":91}, + "sourceSize": {"w":96,"h":96} +}, +"sprites/buildings/balancer.png": +{ + "frame": {"x":740,"y":807,"w":171,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":11,"y":0,"w":171,"h":96}, + "sourceSize": {"w":192,"h":96} +}, "sprites/buildings/belt_left.png": { - "frame": {"x":403,"y":911,"w":87,"h":87}, + "frame": {"x":200,"y":1441,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -738,7 +786,7 @@ }, "sprites/buildings/belt_right.png": { - "frame": {"x":287,"y":1489,"w":87,"h":87}, + "frame": {"x":823,"y":1598,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -746,7 +794,7 @@ }, "sprites/buildings/belt_top.png": { - "frame": {"x":943,"y":803,"w":78,"h":96}, + "frame": {"x":942,"y":1319,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -754,7 +802,7 @@ }, "sprites/buildings/constant_signal.png": { - "frame": {"x":949,"y":485,"w":70,"h":85}, + "frame": {"x":283,"y":683,"w":70,"h":85}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":13,"y":0,"w":70,"h":85}, @@ -762,7 +810,7 @@ }, "sprites/buildings/cutter-quad.png": { - "frame": {"x":373,"y":203,"w":366,"h":96}, + "frame": {"x":376,"y":208,"w":366,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":11,"y":0,"w":366,"h":96}, @@ -770,7 +818,7 @@ }, "sprites/buildings/cutter.png": { - "frame": {"x":178,"y":574,"w":171,"h":96}, + "frame": {"x":4,"y":683,"w":171,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":11,"y":0,"w":171,"h":96}, @@ -778,7 +826,7 @@ }, "sprites/buildings/display.png": { - "frame": {"x":565,"y":1507,"w":84,"h":90}, + "frame": {"x":190,"y":1720,"w":84,"h":90}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":6,"y":6,"w":84,"h":90}, @@ -786,7 +834,7 @@ }, "sprites/buildings/filter.png": { - "frame": {"x":569,"y":403,"w":179,"h":96}, + "frame": {"x":746,"y":705,"w":179,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":11,"y":0,"w":179,"h":96}, @@ -794,7 +842,7 @@ }, "sprites/buildings/hub.png": { - "frame": {"x":3,"y":103,"w":366,"h":367}, + "frame": {"x":4,"y":106,"w":366,"h":367}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":10,"w":366,"h":367}, @@ -802,7 +850,7 @@ }, "sprites/buildings/lever.png": { - "frame": {"x":948,"y":574,"w":73,"h":85}, + "frame": {"x":946,"y":245,"w":73,"h":85}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":12,"y":3,"w":73,"h":85}, @@ -810,7 +858,7 @@ }, "sprites/buildings/logic_gate-not.png": { - "frame": {"x":372,"y":1611,"w":82,"h":96}, + "frame": {"x":4,"y":1799,"w":82,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":8,"y":0,"w":82,"h":96}, @@ -818,7 +866,7 @@ }, "sprites/buildings/logic_gate-or.png": { - "frame": {"x":203,"y":866,"w":96,"h":83}, + "frame": {"x":106,"y":986,"w":96,"h":83}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":83}, @@ -826,7 +874,7 @@ }, "sprites/buildings/logic_gate-transistor.png": { - "frame": {"x":623,"y":799,"w":68,"h":96}, + "frame": {"x":666,"y":812,"w":68,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":68,"h":96}, @@ -834,7 +882,7 @@ }, "sprites/buildings/logic_gate-xor.png": { - "frame": {"x":610,"y":1033,"w":96,"h":95}, + "frame": {"x":106,"y":785,"w":96,"h":95}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":95}, @@ -842,7 +890,7 @@ }, "sprites/buildings/logic_gate.png": { - "frame": {"x":203,"y":774,"w":96,"h":88}, + "frame": {"x":250,"y":927,"w":96,"h":88}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":88}, @@ -850,7 +898,7 @@ }, "sprites/buildings/miner-chainable.png": { - "frame": {"x":690,"y":1188,"w":91,"h":95}, + "frame": {"x":103,"y":1415,"w":91,"h":95}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":91,"h":95}, @@ -858,7 +906,7 @@ }, "sprites/buildings/miner.png": { - "frame": {"x":590,"y":1226,"w":91,"h":95}, + "frame": {"x":4,"y":1419,"w":91,"h":95}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":91,"h":95}, @@ -866,7 +914,7 @@ }, "sprites/buildings/mixer.png": { - "frame": {"x":361,"y":503,"w":174,"h":96}, + "frame": {"x":366,"y":514,"w":174,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":174,"h":96}, @@ -874,7 +922,7 @@ }, "sprites/buildings/painter-double.png": { - "frame": {"x":759,"y":199,"w":192,"h":191}, + "frame": {"x":748,"y":202,"w":192,"h":191}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":192,"h":191}, @@ -882,7 +930,7 @@ }, "sprites/buildings/painter-mirrored.png": { - "frame": {"x":373,"y":403,"w":192,"h":96}, + "frame": {"x":747,"y":399,"w":192,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":192,"h":96}, @@ -890,7 +938,7 @@ }, "sprites/buildings/painter-quad.png": { - "frame": {"x":381,"y":3,"w":374,"h":96}, + "frame": {"x":384,"y":4,"w":374,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":374,"h":96}, @@ -898,7 +946,7 @@ }, "sprites/buildings/painter.png": { - "frame": {"x":752,"y":494,"w":192,"h":96}, + "frame": {"x":747,"y":501,"w":192,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":192,"h":96}, @@ -906,7 +954,7 @@ }, "sprites/buildings/reader.png": { - "frame": {"x":3,"y":1026,"w":95,"h":96}, + "frame": {"x":247,"y":1081,"w":95,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":95,"h":96}, @@ -914,15 +962,15 @@ }, "sprites/buildings/rotater-ccw.png": { - "frame": {"x":201,"y":1011,"w":95,"h":96}, + "frame": {"x":106,"y":1135,"w":95,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":95,"h":96}, "sourceSize": {"w":96,"h":96} }, -"sprites/buildings/rotater-fl.png": +"sprites/buildings/rotater-rotate180.png": { - "frame": {"x":102,"y":1083,"w":95,"h":96}, + "frame": {"x":4,"y":1137,"w":95,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":95,"h":96}, @@ -930,55 +978,15 @@ }, "sprites/buildings/rotater.png": { - "frame": {"x":3,"y":1126,"w":95,"h":96}, + "frame": {"x":555,"y":1072,"w":95,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":95,"h":96}, "sourceSize": {"w":96,"h":96} }, -"sprites/buildings/splitter-compact-inverse.png": -{ - "frame": {"x":3,"y":1226,"w":94,"h":91}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":0,"y":2,"w":94,"h":91}, - "sourceSize": {"w":96,"h":96} -}, -"sprites/buildings/splitter-compact-merge-inverse.png": -{ - "frame": {"x":102,"y":1183,"w":95,"h":91}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":0,"y":2,"w":95,"h":91}, - "sourceSize": {"w":96,"h":96} -}, -"sprites/buildings/splitter-compact-merge.png": -{ - "frame": {"x":3,"y":1321,"w":93,"h":91}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":3,"y":2,"w":93,"h":91}, - "sourceSize": {"w":96,"h":96} -}, -"sprites/buildings/splitter-compact.png": -{ - "frame": {"x":497,"y":1099,"w":93,"h":91}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":3,"y":2,"w":93,"h":91}, - "sourceSize": {"w":96,"h":96} -}, -"sprites/buildings/splitter.png": -{ - "frame": {"x":353,"y":603,"w":171,"h":96}, - "rotated": false, - "trimmed": true, - "spriteSourceSize": {"x":11,"y":0,"w":171,"h":96}, - "sourceSize": {"w":192,"h":96} -}, "sprites/buildings/stacker.png": { - "frame": {"x":539,"y":503,"w":174,"h":96}, + "frame": {"x":4,"y":581,"w":174,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":174,"h":96}, @@ -986,7 +994,7 @@ }, "sprites/buildings/trash-storage.png": { - "frame": {"x":736,"y":694,"w":166,"h":192}, + "frame": {"x":574,"y":508,"w":166,"h":192}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":166,"h":192}, @@ -994,7 +1002,7 @@ }, "sprites/buildings/trash.png": { - "frame": {"x":731,"y":890,"w":96,"h":96}, + "frame": {"x":563,"y":910,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -1002,7 +1010,7 @@ }, "sprites/buildings/underground_belt_entry-tier2.png": { - "frame": {"x":494,"y":1194,"w":92,"h":83}, + "frame": {"x":751,"y":1253,"w":92,"h":83}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":13,"w":92,"h":83}, @@ -1010,7 +1018,7 @@ }, "sprites/buildings/underground_belt_entry.png": { - "frame": {"x":395,"y":1260,"w":92,"h":74}, + "frame": {"x":646,"y":1314,"w":92,"h":74}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":22,"w":92,"h":74}, @@ -1018,7 +1026,7 @@ }, "sprites/buildings/underground_belt_exit-tier2.png": { - "frame": {"x":295,"y":1320,"w":92,"h":74}, + "frame": {"x":544,"y":1373,"w":92,"h":74}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":92,"h":74}, @@ -1026,7 +1034,7 @@ }, "sprites/buildings/underground_belt_exit.png": { - "frame": {"x":196,"y":1384,"w":92,"h":74}, + "frame": {"x":202,"y":1361,"w":92,"h":74}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":92,"h":74}, @@ -1034,15 +1042,23 @@ }, "sprites/buildings/virtual_processor-analyzer.png": { - "frame": {"x":622,"y":933,"w":96,"h":96}, + "frame": {"x":740,"y":909,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, "sourceSize": {"w":96,"h":96} }, +"sprites/buildings/virtual_processor-painter.png": +{ + "frame": {"x":793,"y":1151,"w":87,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":9,"y":0,"w":87,"h":96}, + "sourceSize": {"w":96,"h":96} +}, "sprites/buildings/virtual_processor-rotater.png": { - "frame": {"x":185,"y":1735,"w":79,"h":96}, + "frame": {"x":89,"y":1901,"w":79,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":79,"h":96}, @@ -1050,7 +1066,7 @@ }, "sprites/buildings/virtual_processor-shapecompare.png": { - "frame": {"x":103,"y":774,"w":96,"h":89}, + "frame": {"x":352,"y":922,"w":96,"h":89}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":89}, @@ -1058,15 +1074,15 @@ }, "sprites/buildings/virtual_processor-stacker.png": { - "frame": {"x":491,"y":1281,"w":88,"h":96}, + "frame": {"x":300,"y":1369,"w":87,"h":96}, "rotated": false, "trimmed": true, - "spriteSourceSize": {"x":0,"y":0,"w":88,"h":96}, + "spriteSourceSize": {"x":9,"y":0,"w":87,"h":96}, "sourceSize": {"w":96,"h":96} }, "sprites/buildings/virtual_processor-unstacker.png": { - "frame": {"x":510,"y":999,"w":96,"h":96}, + "frame": {"x":912,"y":916,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -1074,7 +1090,7 @@ }, "sprites/buildings/virtual_processor.png": { - "frame": {"x":810,"y":1093,"w":96,"h":94}, + "frame": {"x":106,"y":886,"w":96,"h":94}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":2,"w":96,"h":94}, @@ -1082,7 +1098,7 @@ }, "sprites/buildings/wire-cross.png": { - "frame": {"x":831,"y":893,"w":96,"h":96}, + "frame": {"x":908,"y":1018,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -1090,7 +1106,7 @@ }, "sprites/buildings/wire-split.png": { - "frame": {"x":103,"y":867,"w":96,"h":54}, + "frame": {"x":250,"y":1021,"w":96,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":42,"w":96,"h":54}, @@ -1098,7 +1114,7 @@ }, "sprites/buildings/wire-turn.png": { - "frame": {"x":955,"y":164,"w":54,"h":54}, + "frame": {"x":962,"y":65,"w":54,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":42,"y":42,"w":54,"h":54}, @@ -1106,7 +1122,7 @@ }, "sprites/buildings/wire.png": { - "frame": {"x":743,"y":103,"w":12,"h":96}, + "frame": {"x":890,"y":1011,"w":12,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":42,"y":0,"w":12,"h":96}, @@ -1114,7 +1130,7 @@ }, "sprites/buildings/wire_tunnel-coating.png": { - "frame": {"x":921,"y":688,"w":22,"h":90}, + "frame": {"x":546,"y":514,"w":22,"h":90}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":37,"y":3,"w":22,"h":90}, @@ -1122,15 +1138,79 @@ }, "sprites/buildings/wire_tunnel.png": { - "frame": {"x":594,"y":1132,"w":92,"h":90}, + "frame": {"x":446,"y":1366,"w":92,"h":90}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":3,"w":92,"h":90}, "sourceSize": {"w":96,"h":96} }, +"sprites/colors/blue.png": +{ + "frame": {"x":826,"y":1049,"w":36,"h":34}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":36,"h":34}, + "sourceSize": {"w":36,"h":36} +}, +"sprites/colors/cyan.png": +{ + "frame": {"x":826,"y":1089,"w":36,"h":34}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":36,"h":34}, + "sourceSize": {"w":36,"h":36} +}, +"sprites/colors/green.png": +{ + "frame": {"x":208,"y":785,"w":36,"h":34}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":36,"h":34}, + "sourceSize": {"w":36,"h":36} +}, +"sprites/colors/purple.png": +{ + "frame": {"x":208,"y":825,"w":36,"h":34}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":36,"h":34}, + "sourceSize": {"w":36,"h":36} +}, +"sprites/colors/red.png": +{ + "frame": {"x":208,"y":865,"w":36,"h":34}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":36,"h":34}, + "sourceSize": {"w":36,"h":36} +}, +"sprites/colors/uncolored.png": +{ + "frame": {"x":208,"y":905,"w":36,"h":34}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":36,"h":34}, + "sourceSize": {"w":36,"h":36} +}, +"sprites/colors/white.png": +{ + "frame": {"x":208,"y":945,"w":36,"h":34}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":36,"h":34}, + "sourceSize": {"w":36,"h":36} +}, +"sprites/colors/yellow.png": +{ + "frame": {"x":208,"y":985,"w":36,"h":34}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":2,"w":36,"h":34}, + "sourceSize": {"w":36,"h":36} +}, "sprites/debug/acceptor_slot.png": { - "frame": {"x":1013,"y":164,"w":8,"h":8}, + "frame": {"x":748,"y":106,"w":8,"h":8}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, @@ -1138,7 +1218,7 @@ }, "sprites/debug/ejector_slot.png": { - "frame": {"x":1013,"y":176,"w":8,"h":8}, + "frame": {"x":748,"y":120,"w":8,"h":8}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, @@ -1146,7 +1226,7 @@ }, "sprites/misc/hub_direction_indicator.png": { - "frame": {"x":695,"y":851,"w":32,"h":32}, + "frame": {"x":205,"y":1259,"w":32,"h":32}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":32,"h":32}, @@ -1154,7 +1234,7 @@ }, "sprites/misc/processor_disabled.png": { - "frame": {"x":449,"y":803,"w":53,"h":55}, + "frame": {"x":665,"y":974,"w":53,"h":55}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":6,"y":6,"w":53,"h":55}, @@ -1162,7 +1242,7 @@ }, "sprites/misc/processor_disconnected.png": { - "frame": {"x":303,"y":674,"w":44,"h":57}, + "frame": {"x":842,"y":909,"w":44,"h":57}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":11,"y":5,"w":44,"h":57}, @@ -1170,7 +1250,7 @@ }, "sprites/misc/reader_overlay.png": { - "frame": {"x":947,"y":752,"w":70,"h":47}, + "frame": {"x":283,"y":774,"w":70,"h":47}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":13,"y":25,"w":70,"h":47}, @@ -1178,7 +1258,7 @@ }, "sprites/misc/slot_bad_arrow.png": { - "frame": {"x":717,"y":605,"w":24,"h":24}, + "frame": {"x":535,"y":752,"w":24,"h":24}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":1,"w":24,"h":24}, @@ -1186,7 +1266,7 @@ }, "sprites/misc/slot_good_arrow.png": { - "frame": {"x":717,"y":575,"w":24,"h":26}, + "frame": {"x":535,"y":720,"w":24,"h":26}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":24,"h":26}, @@ -1194,7 +1274,7 @@ }, "sprites/misc/storage_overlay.png": { - "frame": {"x":955,"y":71,"w":60,"h":30}, + "frame": {"x":674,"y":776,"w":60,"h":30}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":60,"h":30}, @@ -1202,7 +1282,7 @@ }, "sprites/misc/waypoint.png": { - "frame": {"x":717,"y":539,"w":26,"h":32}, + "frame": {"x":540,"y":682,"w":26,"h":32}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":26,"h":32}, @@ -1210,7 +1290,7 @@ }, "sprites/wires/boolean_false.png": { - "frame": {"x":717,"y":633,"w":21,"h":28}, + "frame": {"x":546,"y":610,"w":21,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":6,"y":3,"w":21,"h":28}, @@ -1218,7 +1298,7 @@ }, "sprites/wires/boolean_true.png": { - "frame": {"x":717,"y":665,"w":15,"h":28}, + "frame": {"x":508,"y":820,"w":15,"h":28}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":7,"y":3,"w":15,"h":28}, @@ -1226,7 +1306,7 @@ }, "sprites/wires/display/blue.png": { - "frame": {"x":699,"y":703,"w":33,"h":33}, + "frame": {"x":208,"y":1025,"w":33,"h":33}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":33,"h":33}, @@ -1234,7 +1314,7 @@ }, "sprites/wires/display/cyan.png": { - "frame": {"x":699,"y":740,"w":33,"h":33}, + "frame": {"x":208,"y":1064,"w":33,"h":33}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":33,"h":33}, @@ -1242,7 +1322,7 @@ }, "sprites/wires/display/green.png": { - "frame": {"x":699,"y":777,"w":33,"h":33}, + "frame": {"x":208,"y":1103,"w":33,"h":33}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":33,"h":33}, @@ -1250,7 +1330,7 @@ }, "sprites/wires/display/purple.png": { - "frame": {"x":906,"y":782,"w":33,"h":33}, + "frame": {"x":207,"y":1142,"w":33,"h":33}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":33,"h":33}, @@ -1258,7 +1338,7 @@ }, "sprites/wires/display/red.png": { - "frame": {"x":906,"y":819,"w":33,"h":33}, + "frame": {"x":207,"y":1181,"w":33,"h":33}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":33,"h":33}, @@ -1266,7 +1346,7 @@ }, "sprites/wires/display/white.png": { - "frame": {"x":906,"y":856,"w":33,"h":33}, + "frame": {"x":658,"y":1035,"w":33,"h":33}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":33,"h":33}, @@ -1274,7 +1354,7 @@ }, "sprites/wires/display/yellow.png": { - "frame": {"x":695,"y":814,"w":33,"h":33}, + "frame": {"x":207,"y":1220,"w":33,"h":33}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":33,"h":33}, @@ -1282,7 +1362,7 @@ }, "sprites/wires/lever_on.png": { - "frame": {"x":948,"y":663,"w":73,"h":85}, + "frame": {"x":946,"y":336,"w":73,"h":85}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":12,"y":3,"w":73,"h":85}, @@ -1290,7 +1370,7 @@ }, "sprites/wires/logical_acceptor.png": { - "frame": {"x":303,"y":735,"w":42,"h":71}, + "frame": {"x":842,"y":972,"w":42,"h":71}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":28,"y":0,"w":42,"h":71}, @@ -1298,7 +1378,7 @@ }, "sprites/wires/logical_ejector.png": { - "frame": {"x":449,"y":862,"w":41,"h":45}, + "frame": {"x":461,"y":820,"w":41,"h":45}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":29,"y":0,"w":41,"h":45}, @@ -1306,7 +1386,7 @@ }, "sprites/wires/network_conflict.png": { - "frame": {"x":622,"y":899,"w":32,"h":30}, + "frame": {"x":656,"y":1112,"w":32,"h":30}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":1,"w":32,"h":30}, @@ -1314,7 +1394,7 @@ }, "sprites/wires/network_empty.png": { - "frame": {"x":717,"y":503,"w":28,"h":32}, + "frame": {"x":540,"y":644,"w":28,"h":32}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":28,"h":32}, @@ -1322,7 +1402,7 @@ }, "sprites/wires/overlay_tile.png": { - "frame": {"x":955,"y":3,"w":64,"h":64}, + "frame": {"x":674,"y":706,"w":64,"h":64}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":64,"h":64}, @@ -1330,7 +1410,7 @@ }, "sprites/wires/sets/color_cross.png": { - "frame": {"x":722,"y":990,"w":96,"h":96}, + "frame": {"x":724,"y":1011,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -1338,7 +1418,7 @@ }, "sprites/wires/sets/color_forward.png": { - "frame": {"x":743,"y":203,"w":12,"h":96}, + "frame": {"x":868,"y":1049,"w":12,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":42,"y":0,"w":12,"h":96}, @@ -1346,7 +1426,7 @@ }, "sprites/wires/sets/color_split.png": { - "frame": {"x":103,"y":925,"w":96,"h":54}, + "frame": {"x":106,"y":1075,"w":96,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":42,"w":96,"h":54}, @@ -1354,7 +1434,7 @@ }, "sprites/wires/sets/color_turn.png": { - "frame": {"x":955,"y":222,"w":54,"h":54}, + "frame": {"x":962,"y":125,"w":54,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":42,"y":42,"w":54,"h":54}, @@ -1362,7 +1442,7 @@ }, "sprites/wires/sets/conflict_cross.png": { - "frame": {"x":822,"y":993,"w":96,"h":96}, + "frame": {"x":904,"y":1120,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -1370,7 +1450,7 @@ }, "sprites/wires/sets/conflict_forward.png": { - "frame": {"x":506,"y":803,"w":12,"h":96}, + "frame": {"x":886,"y":1113,"w":12,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":42,"y":0,"w":12,"h":96}, @@ -1378,7 +1458,7 @@ }, "sprites/wires/sets/conflict_split.png": { - "frame": {"x":203,"y":953,"w":96,"h":54}, + "frame": {"x":4,"y":1077,"w":96,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":42,"w":96,"h":54}, @@ -1386,7 +1466,7 @@ }, "sprites/wires/sets/conflict_turn.png": { - "frame": {"x":955,"y":280,"w":54,"h":54}, + "frame": {"x":962,"y":185,"w":54,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":42,"y":42,"w":54,"h":54}, @@ -1394,7 +1474,7 @@ }, "sprites/wires/sets/regular_cross.png": { - "frame": {"x":831,"y":893,"w":96,"h":96}, + "frame": {"x":908,"y":1018,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -1402,7 +1482,7 @@ }, "sprites/wires/sets/regular_forward.png": { - "frame": {"x":743,"y":103,"w":12,"h":96}, + "frame": {"x":890,"y":1011,"w":12,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":42,"y":0,"w":12,"h":96}, @@ -1410,7 +1490,7 @@ }, "sprites/wires/sets/regular_split.png": { - "frame": {"x":103,"y":867,"w":96,"h":54}, + "frame": {"x":250,"y":1021,"w":96,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":42,"w":96,"h":54}, @@ -1418,7 +1498,7 @@ }, "sprites/wires/sets/regular_turn.png": { - "frame": {"x":955,"y":164,"w":54,"h":54}, + "frame": {"x":962,"y":65,"w":54,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":42,"y":42,"w":54,"h":54}, @@ -1426,7 +1506,7 @@ }, "sprites/wires/sets/shape_cross.png": { - "frame": {"x":922,"y":1003,"w":96,"h":96}, + "frame": {"x":4,"y":785,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -1434,7 +1514,7 @@ }, "sprites/wires/sets/shape_forward.png": { - "frame": {"x":494,"y":903,"w":12,"h":96}, + "frame": {"x":1006,"y":1120,"w":12,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":42,"y":0,"w":12,"h":96}, @@ -1442,7 +1522,7 @@ }, "sprites/wires/sets/shape_split.png": { - "frame": {"x":303,"y":989,"w":96,"h":54}, + "frame": {"x":556,"y":1012,"w":96,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":42,"w":96,"h":54}, @@ -1450,7 +1530,7 @@ }, "sprites/wires/sets/shape_turn.png": { - "frame": {"x":955,"y":338,"w":54,"h":54}, + "frame": {"x":665,"y":914,"w":54,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":42,"y":42,"w":54,"h":54}, @@ -1458,7 +1538,7 @@ }, "sprites/wires/wires_preview.png": { - "frame": {"x":695,"y":887,"w":32,"h":32}, + "frame": {"x":656,"y":1074,"w":32,"h":32}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":32,"h":32}, @@ -1471,6 +1551,6 @@ "format": "RGBA8888", "size": {"w":1024,"h":2048}, "scale": "0.5", - "smartupdate": "$TexturePacker:SmartUpdate:d21082eda6f288e04b0739186004794d:0912211652d1c400e2846013f9de057b:908b89f5ca8ff73e331a35a3b14d0604$" + "smartupdate": "$TexturePacker:SmartUpdate:c57f50d18c59efc0edbd4a3a732323a4:3fcf23da2ddc6370c437cf41f6d44ed0:908b89f5ca8ff73e331a35a3b14d0604$" } } diff --git a/res_built/atlas/atlas0_mq.png b/res_built/atlas/atlas0_mq.png index 9e9b97c6..36e71502 100644 Binary files a/res_built/atlas/atlas0_mq.png and b/res_built/atlas/atlas0_mq.png differ diff --git a/res_raw/atlas.tps b/res_raw/atlas.tps index 947bdb04..018591bd 100644 --- a/res_raw/atlas.tps +++ b/res_raw/atlas.tps @@ -197,7 +197,7 @@ scaleMode Smooth extrude - 2 + 3 trimThreshold 2 trimMargin @@ -269,7 +269,7 @@ sprites/blueprints/miner.png sprites/blueprints/reader.png sprites/blueprints/rotater-ccw.png - sprites/blueprints/rotater-fl.png + sprites/blueprints/rotater-rotate180.png sprites/blueprints/rotater.png sprites/blueprints/splitter-compact-inverse.png sprites/blueprints/splitter-compact-merge-inverse.png @@ -281,8 +281,10 @@ sprites/blueprints/underground_belt_exit-tier2.png sprites/blueprints/underground_belt_exit.png sprites/blueprints/virtual_processor-analyzer.png + sprites/blueprints/virtual_processor-painter.png sprites/blueprints/virtual_processor-rotater.png sprites/blueprints/virtual_processor-shapecompare.png + sprites/blueprints/virtual_processor-stacker.png sprites/blueprints/virtual_processor-unstacker.png sprites/blueprints/virtual_processor.png sprites/blueprints/wire_tunnel-coating.png @@ -298,7 +300,7 @@ sprites/buildings/miner-chainable.png sprites/buildings/reader.png sprites/buildings/rotater-ccw.png - sprites/buildings/rotater-fl.png + sprites/buildings/rotater-rotate180.png sprites/buildings/splitter-compact-inverse.png sprites/buildings/splitter-compact-merge-inverse.png sprites/buildings/splitter-compact-merge.png @@ -308,8 +310,10 @@ sprites/buildings/underground_belt_exit-tier2.png sprites/buildings/underground_belt_exit.png sprites/buildings/virtual_processor-analyzer.png + sprites/buildings/virtual_processor-painter.png sprites/buildings/virtual_processor-rotater.png sprites/buildings/virtual_processor-shapecompare.png + sprites/buildings/virtual_processor-stacker.png sprites/buildings/virtual_processor-unstacker.png sprites/buildings/virtual_processor.png sprites/buildings/wire_tunnel-coating.png @@ -503,6 +507,28 @@ scale9FromFile + sprites/colors/blue.png + sprites/colors/cyan.png + sprites/colors/green.png + sprites/colors/purple.png + sprites/colors/red.png + sprites/colors/uncolored.png + sprites/colors/white.png + sprites/colors/yellow.png + + pivotPoint + 0.5,0.5 + spriteScale + 1 + scale9Enabled + + scale9Borders + 18,18,36,36 + scale9Paddings + 18,18,36,36 + scale9FromFile + + sprites/debug/acceptor_slot.png sprites/debug/ejector_slot.png sprites/misc/hub_direction_indicator.png diff --git a/res_raw/sprites/blueprints/splitter-compact-inverse.png b/res_raw/sprites/blueprints/balancer-merger-inverse.png similarity index 100% rename from res_raw/sprites/blueprints/splitter-compact-inverse.png rename to res_raw/sprites/blueprints/balancer-merger-inverse.png diff --git a/res_raw/sprites/blueprints/splitter-compact.png b/res_raw/sprites/blueprints/balancer-merger.png similarity index 100% rename from res_raw/sprites/blueprints/splitter-compact.png rename to res_raw/sprites/blueprints/balancer-merger.png diff --git a/res_raw/sprites/blueprints/splitter-compact-merge-inverse.png b/res_raw/sprites/blueprints/balancer-splitter-inverse.png similarity index 100% rename from res_raw/sprites/blueprints/splitter-compact-merge-inverse.png rename to res_raw/sprites/blueprints/balancer-splitter-inverse.png diff --git a/res_raw/sprites/blueprints/splitter-compact-merge.png b/res_raw/sprites/blueprints/balancer-splitter.png similarity index 100% rename from res_raw/sprites/blueprints/splitter-compact-merge.png rename to res_raw/sprites/blueprints/balancer-splitter.png diff --git a/res_raw/sprites/blueprints/balancer.png b/res_raw/sprites/blueprints/balancer.png new file mode 100644 index 00000000..52473772 Binary files /dev/null and b/res_raw/sprites/blueprints/balancer.png differ diff --git a/res_raw/sprites/blueprints/logic_gate-transistor.png b/res_raw/sprites/blueprints/logic_gate-transistor.png index c3d3682d..b13aedea 100644 Binary files a/res_raw/sprites/blueprints/logic_gate-transistor.png and b/res_raw/sprites/blueprints/logic_gate-transistor.png differ diff --git a/res_raw/sprites/blueprints/rotater-fl.png b/res_raw/sprites/blueprints/rotater-rotate180.png similarity index 100% rename from res_raw/sprites/blueprints/rotater-fl.png rename to res_raw/sprites/blueprints/rotater-rotate180.png diff --git a/res_raw/sprites/blueprints/splitter.png b/res_raw/sprites/blueprints/splitter.png deleted file mode 100644 index 984a99a8..00000000 Binary files a/res_raw/sprites/blueprints/splitter.png and /dev/null differ diff --git a/res_raw/sprites/blueprints/trash-storage.png b/res_raw/sprites/blueprints/trash-storage.png index cc719a5a..b86519b9 100644 Binary files a/res_raw/sprites/blueprints/trash-storage.png and b/res_raw/sprites/blueprints/trash-storage.png differ diff --git a/res_raw/sprites/blueprints/virtual_processor-painter.png b/res_raw/sprites/blueprints/virtual_processor-painter.png new file mode 100644 index 00000000..243517db Binary files /dev/null and b/res_raw/sprites/blueprints/virtual_processor-painter.png differ diff --git a/res_raw/sprites/blueprints/virtual_processor-stacker.png b/res_raw/sprites/blueprints/virtual_processor-stacker.png index 1d691491..09363c3e 100644 Binary files a/res_raw/sprites/blueprints/virtual_processor-stacker.png and b/res_raw/sprites/blueprints/virtual_processor-stacker.png differ diff --git a/res_raw/sprites/buildings/splitter-compact-inverse.png b/res_raw/sprites/buildings/balancer-merger-inverse.png similarity index 100% rename from res_raw/sprites/buildings/splitter-compact-inverse.png rename to res_raw/sprites/buildings/balancer-merger-inverse.png diff --git a/res_raw/sprites/buildings/splitter-compact.png b/res_raw/sprites/buildings/balancer-merger.png similarity index 100% rename from res_raw/sprites/buildings/splitter-compact.png rename to res_raw/sprites/buildings/balancer-merger.png diff --git a/res_raw/sprites/buildings/splitter-compact-merge-inverse.png b/res_raw/sprites/buildings/balancer-splitter-inverse.png similarity index 100% rename from res_raw/sprites/buildings/splitter-compact-merge-inverse.png rename to res_raw/sprites/buildings/balancer-splitter-inverse.png diff --git a/res_raw/sprites/buildings/splitter-compact-merge.png b/res_raw/sprites/buildings/balancer-splitter.png similarity index 100% rename from res_raw/sprites/buildings/splitter-compact-merge.png rename to res_raw/sprites/buildings/balancer-splitter.png diff --git a/res_raw/sprites/buildings/balancer.png b/res_raw/sprites/buildings/balancer.png new file mode 100644 index 00000000..d1d12ad9 Binary files /dev/null and b/res_raw/sprites/buildings/balancer.png differ diff --git a/res_raw/sprites/buildings/logic_gate-transistor.png b/res_raw/sprites/buildings/logic_gate-transistor.png index 35952db4..e543d985 100644 Binary files a/res_raw/sprites/buildings/logic_gate-transistor.png and b/res_raw/sprites/buildings/logic_gate-transistor.png differ diff --git a/res_raw/sprites/buildings/rotater-fl.png b/res_raw/sprites/buildings/rotater-rotate180.png similarity index 100% rename from res_raw/sprites/buildings/rotater-fl.png rename to res_raw/sprites/buildings/rotater-rotate180.png diff --git a/res_raw/sprites/buildings/splitter.png b/res_raw/sprites/buildings/splitter.png deleted file mode 100644 index 7cd9a2ef..00000000 Binary files a/res_raw/sprites/buildings/splitter.png and /dev/null differ diff --git a/res_raw/sprites/buildings/trash-storage.png b/res_raw/sprites/buildings/trash-storage.png index 39a4df1f..56fbcc38 100644 Binary files a/res_raw/sprites/buildings/trash-storage.png and b/res_raw/sprites/buildings/trash-storage.png differ diff --git a/res_raw/sprites/buildings/virtual_processor-painter.png b/res_raw/sprites/buildings/virtual_processor-painter.png new file mode 100644 index 00000000..b952e9d7 Binary files /dev/null and b/res_raw/sprites/buildings/virtual_processor-painter.png differ diff --git a/res_raw/sprites/buildings/virtual_processor-stacker.png b/res_raw/sprites/buildings/virtual_processor-stacker.png index 15882e63..59c5bada 100644 Binary files a/res_raw/sprites/buildings/virtual_processor-stacker.png and b/res_raw/sprites/buildings/virtual_processor-stacker.png differ diff --git a/res_raw/sprites/colors/blue.png b/res_raw/sprites/colors/blue.png new file mode 100644 index 00000000..5dceb132 Binary files /dev/null and b/res_raw/sprites/colors/blue.png differ diff --git a/res_raw/sprites/colors/cyan.png b/res_raw/sprites/colors/cyan.png new file mode 100644 index 00000000..efd0ff1a Binary files /dev/null and b/res_raw/sprites/colors/cyan.png differ diff --git a/res_raw/sprites/colors/green.png b/res_raw/sprites/colors/green.png new file mode 100644 index 00000000..f719e0ce Binary files /dev/null and b/res_raw/sprites/colors/green.png differ diff --git a/res_raw/sprites/colors/purple.png b/res_raw/sprites/colors/purple.png new file mode 100644 index 00000000..ee3ebaf1 Binary files /dev/null and b/res_raw/sprites/colors/purple.png differ diff --git a/res_raw/sprites/colors/red.png b/res_raw/sprites/colors/red.png new file mode 100644 index 00000000..8e918371 Binary files /dev/null and b/res_raw/sprites/colors/red.png differ diff --git a/res_raw/sprites/colors/uncolored.png b/res_raw/sprites/colors/uncolored.png new file mode 100644 index 00000000..9ca5159e Binary files /dev/null and b/res_raw/sprites/colors/uncolored.png differ diff --git a/res_raw/sprites/colors/white.png b/res_raw/sprites/colors/white.png new file mode 100644 index 00000000..3f1c29f4 Binary files /dev/null and b/res_raw/sprites/colors/white.png differ diff --git a/res_raw/sprites/colors/yellow.png b/res_raw/sprites/colors/yellow.png new file mode 100644 index 00000000..6dc9c0ea Binary files /dev/null and b/res_raw/sprites/colors/yellow.png differ diff --git a/src/css/common.scss b/src/css/common.scss index 5ae13dda..0c1814bc 100644 --- a/src/css/common.scss +++ b/src/css/common.scss @@ -392,13 +392,18 @@ canvas { &::after { content: " "; background: uiResource("loading.svg") center center / contain no-repeat; - @include S(width, 15px); - @include S(height, 15px); - @include S(margin-top, 1px); - @include S(margin-left, 5px); + @include S(width, 35px); + @include S(height, 35px); display: inline-block; vertical-align: middle; } + + @include InlineAnimation(1.5s ease-in-out infinite) { + 50% { + transform: scale(1.2) rotate(160deg); + } + } + @include DarkThemeOverride { color: #fff; } @@ -463,18 +468,44 @@ canvas { justify-content: center; flex-direction: column; .loadingImage { - background: uiResource("loading.svg") center center / #{D(60px)} no-repeat; + background: uiResource("loading.svg") center center / #{D(40px)} no-repeat; width: 100%; display: flex; flex-grow: 1; + + @include InlineAnimation(1.5s ease-in-out infinite) { + 50% { + transform: scale(1.2) rotate(160deg); + } + } } + + .prefab_GameHint { + position: absolute; + @include S(left, 20px); + @include S(right, 20px); + @include S(bottom, 60px); + @include Text; + color: #666; + + @include DarkThemeOverride() { + color: lighten($darkModeGameBackground, 50); + } + } + .loadingStatus { position: absolute; @include S(left, 20px); @include S(right, 20px); @include S(bottom, 30px); @include Text; - @include TextShadow3D(#aaa); + @include PlainText; + color: #aaa; + + @include DarkThemeOverride { + color: lighten($darkModeGameBackground, 20); + } + display: flex; flex-direction: column; justify-content: center; @@ -568,6 +599,13 @@ canvas { background-color: lighten($themeColor, 15); } } + + @include DarkThemeOverride { + background-color: $darkModeGameBackground !important; + &.checked { + background-color: $colorBlueBright !important; + } + } } .rangeInputContainer { @@ -597,6 +635,16 @@ input.rangeInput { @include S(border-radius, 8px); } + @include DarkThemeOverride { + &::-webkit-slider-runnable-track { + background-color: $darkModeControlsBackground; + } + + &::-webkit-slider-thumb { + box-shadow: inset 0 0 0 D(10px) #eee; + } + } + &::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; diff --git a/src/css/icons.scss b/src/css/icons.scss index 841fccd9..9636059a 100644 --- a/src/css/icons.scss +++ b/src/css/icons.scss @@ -1,4 +1,4 @@ -$buildings: belt, cutter, miner, mixer, painter, rotater, splitter, stacker, trash, underground_belt, wire, +$buildings: belt, cutter, miner, mixer, painter, rotater, balancer, stacker, trash, underground_belt, wire, constant_signal, logic_gate, lever, filter, wire_tunnel, display, virtual_processor, reader; @each $building in $buildings { @@ -7,7 +7,7 @@ $buildings: belt, cutter, miner, mixer, painter, rotater, splitter, stacker, tra } } -$buildingsAndVariants: belt, splitter, splitter-compact, splitter-compact-inverse, underground_belt, +$buildingsAndVariants: belt, balancer, balancer-merger, balancer-splitter, underground_belt, underground_belt-tier2, miner, miner-chainable, cutter, cutter-quad, rotater, rotater-ccw, rotater-fl, stacker, mixer, painter, painter-double, painter-quad, trash, trash-storage; @each $building in $buildingsAndVariants { @@ -16,10 +16,18 @@ $buildingsAndVariants: belt, splitter, splitter-compact, splitter-compact-invers } } -// Special case +// @TODO: New buildings (balancer, wires, etc) + +// Special cases for mirrored vairants [data-icon="building_tutorials/painter-mirrored.png"] { background-image: uiResource("res/ui/building_tutorials/painter.png") !important; } +[data-icon="building_tutorials/balancer-merger-inverse.png"] { + background-image: uiResource("res/ui/building_tutorials/balancer-merger.png") !important; +} +[data-icon="building_tutorials/balancer-splitter-inverse.png"] { + background-image: uiResource("res/ui/building_tutorials/balancer-splitter.png") !important; +} $icons: notification_saved, notification_success, notification_upgrade; @each $icon in $icons { diff --git a/src/css/ingame_hud/buildings_toolbar.scss b/src/css/ingame_hud/buildings_toolbar.scss index d394106d..81f16906 100644 --- a/src/css/ingame_hud/buildings_toolbar.scss +++ b/src/css/ingame_hud/buildings_toolbar.scss @@ -1,107 +1,93 @@ -.ingame_buildingsToolbar { - position: fixed; - @include S(bottom, 0px); - left: 50%; - transform: translateX(-50%); - - // NOTE: This flex rule may not be necessary. Need to find out intent. - display: flex; - flex-direction: column; - background: transparent; - border-bottom-width: 0; - transition: transform 120ms ease-in-out; - will-change: transform; - - background-color: rgba(mix(#ddd, $colorBlueBright, 90%), 0.5); - backdrop-filter: blur(D(3px)); - - @include DarkThemeOverride { - background-color: #222428; - } - - &:not(.visible) { - transform: translateX(-50%) translateY(#{D(100px)}); - } - - @include S(border-top-left-radius, $globalBorderRadius); - @include S(border-top-right-radius, $globalBorderRadius); - - .buildings { - display: grid; - grid-auto-flow: column; - - .building { - color: $accentColorDark; - display: flex; - flex-direction: column; - position: relative; - align-items: center; - justify-content: center; - @include S(padding, 5px); - @include S(padding-bottom, 1px); - @include S(width, 35px); - @include S(height, 40px); - - background: center center / 65% no-repeat; - - &:not(.unlocked) { - @include S(width, 20px); - opacity: 0.15; - background-image: none !important; - - &::before { - content: " "; - background: uiResource("locked_building.png") center center / #{D(20px)} #{D(20px)} - no-repeat; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 4; - } - } - - @include S(border-radius, $globalBorderRadius); - - &.unlocked { - pointer-events: all; - transition: all 50ms ease-in-out; - transition-property: background-color, transform; - cursor: pointer; - will-change: transform; - - &::before { - content: ""; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background-color: $accentColorDark; - opacity: 0; - will-change: opacity; - } - - &:hover { - &::before { - opacity: 0.1; - } - } - - &.selected { - transform: scale(1.05); - - &::before { - background-color: $colorBlueBright; - opacity: 0.6; - } - - .keybinding { - color: #111; - } - } - } - } - } -} +.ingame_buildingsToolbar { + position: absolute; + @include S(bottom, 5px); + left: 50%; + transform: translateX(-50%); + + // NOTE: This flex rule may not be necessary. Need to find out intent. + display: flex; + flex-direction: column; + background: transparent; + transition: transform 120ms ease-in-out; + will-change: transform; + + backdrop-filter: blur(D(3px)); + background-color: rgba(0, 40, 80, 0.05); + @include S(border-radius, $globalBorderRadius); + + @include DarkThemeOverride { + background-color: rgba(darken($darkModeGameBackground, 15), 0.4); + + &#ingame_HUD_wires_toolbar { + background-color: rgba(darken($darkModeGameBackground, 5), 0.1); + } + } + + &:not(.visible) { + transform: translateX(-50%) translateY(#{D(100px)}); + } + + .buildings { + display: grid; + grid-auto-flow: column; + + .building { + color: $accentColorDark; + display: flex; + flex-direction: column; + position: relative; + align-items: center; + justify-content: center; + @include S(padding, 5px); + @include S(padding-bottom, 1px); + @include S(width, 35px); + @include S(height, 40px); + + background: center center / 70% no-repeat; + + &:not(.unlocked) { + @include S(width, 20px); + opacity: 0.15; + background-image: none !important; + + &::before { + content: " "; + background: uiResource("locked_building.png") center center / #{D(20px)} #{D(20px)} + no-repeat; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 4; + } + } + + @include S(border-radius, $globalBorderRadius); + + &.unlocked { + pointer-events: all; + transition: all 50ms ease-in-out; + transition-property: background-color, transform; + + cursor: pointer; + &:hover { + background-color: rgba(30, 40, 90, 0.1); + } + + &.pressed { + transform: scale(0.9) !important; + } + + &.selected { + // transform: scale(1.05); + background-color: rgba(lighten($colorBlueBright, 9), 0.4); + + .keybinding { + color: #111; + } + } + } + } + } +} diff --git a/src/css/ingame_hud/dialogs.scss b/src/css/ingame_hud/dialogs.scss index 9c9ce7a4..cee9bb92 100644 --- a/src/css/ingame_hud/dialogs.scss +++ b/src/css/ingame_hud/dialogs.scss @@ -1,231 +1,233 @@ -.ingameDialog { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: all; - background: $modalDialogBg; - - display: flex; - align-items: center; - justify-content: center; - - @include InlineAnimation(0.12s ease-in-out) { - 0% { - background-color: transparent; - opacity: 0.5; - } - 100% { - background-color: $modalDialogBg; - } - } - - $darkModeDialogBg: darken($darkModeGameBackground, 10); - - @include DarkThemeOverride { - background: rgba($darkModeDialogBg, 0.9); - @include InlineAnimation(0.12s ease-in-out) { - 0% { - background-color: transparent; - opacity: 0.5; - } - 100% { - background-color: rgba($darkModeDialogBg, 0.9); - } - } - - > .dialogInner.optionChooserDialog .optionParent { - .option { - background: #3d3f42; - - &:hover { - background-color: #424348; - } - - &.active { - background: $colorBlueBright; - color: #fff; - } - } - } - } - - &.visible { - .dialogInner { - opacity: 1; - } - backdrop-filter: blur(D(3px)); - } - - .dialogInner { - transition: opacity 0.2s ease-in-out; - opacity: 0; - } - - &.loadingDialog { - * { - color: #fff; - } - } - - > .dialogInner { - background: #fff; - max-height: calc(100vh - #{D(40px)}); - @include S(border-radius, $globalBorderRadius); - display: flex; - flex-direction: column; - @include S(padding, 12px); - pointer-events: all; - - @include DarkThemeOverride { - background: #333438; - } - - &.optionChooserDialog { - .optionParent { - display: grid; - @include S(grid-gap, 5px); - grid-template-columns: 1fr 1fr; - .option { - pointer-events: all; - cursor: pointer; - @include S(padding, 10px); - background: #eee; - transition: background-color 0.12s ease-in-out; - - &:hover { - background-color: #e7e7e7; - } - - &.active { - background-color: $colorBlueBright; - color: #fff; - } - } - } - } - - > .title { - @include Heading; - margin: 0; - text-transform: uppercase; - display: grid; - align-items: center; - grid-template-columns: 1fr auto; - @include S(margin-bottom, 10px); - - @include DarkThemeInvert(); - > .closeButton { - opacity: 0.7; - @include S(width, 20px); - @include S(height, 20px); - background: uiResource("icons/close.png") center center / 80% no-repeat; - cursor: pointer; - pointer-events: all; - transition: opacity 0.2s ease-in-out; - &:hover { - opacity: 0.4; - } - } - } - - > .content { - @include PlainText; - overflow-y: auto; - pointer-events: all; - @include S(width, 350px); - - @include DarkThemeOverride { - color: #aaa; - } - - a { - color: $colorBlueBright; - } - - strong { - font-weight: bold; - } - - .keybinding { - position: relative; - background: #eee; - @include PlainText; - height: unset; - margin: 1px 0; - } - - input { - background: #eee; - color: #333438; - width: 100%; - - &.errored { - background-color: rgb(250, 206, 206); - } - } - - ul.bucketList { - padding-left: 30px; - - li { - display: list-item; - } - } - } - - > .buttons { - @include S(margin-top, 15px); - display: flex; - justify-content: flex-end; - > button { - @include S(margin-left, 8px); - @include Text; - @include S(min-width, 60px); - @include S(padding, 5px, 15px); - - transition: opacity 0.12s ease-in-out; - &:hover { - opacity: 0.9; - } - - &.good { - background-color: $colorGreenBright; - color: #fff; - } - - &.bad { - background-color: $colorRedBright; - color: #fff; - } - - &.timedButton { - pointer-events: none; - cursor: default; - position: relative; - overflow: hidden; - &::after { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: unset; - z-index: 5; - content: " "; - display: inline-block; - background: rgba(#fff, 0.6); - @include InlineAnimation(5s linear) { - 0% { - width: 100%; - } - 100% { - width: 0%; - } - } - } - } - } - } - } -} +.ingameDialog { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: all; + background: $modalDialogBg; + + display: flex; + align-items: center; + justify-content: center; + + @include InlineAnimation(0.12s ease-in-out) { + 0% { + background-color: transparent; + opacity: 0.5; + } + 100% { + background-color: $modalDialogBg; + } + } + + $darkModeDialogBg: darken($darkModeGameBackground, 5); + + @include DarkThemeOverride { + background: rgba($darkModeDialogBg, 0.9); + @include InlineAnimation(0.12s ease-in-out) { + 0% { + background-color: transparent; + opacity: 0.5; + } + 100% { + background-color: rgba($darkModeDialogBg, 0.9); + } + } + + > .dialogInner.optionChooserDialog .optionParent { + .option { + background: $darkModeControlsBackground; + + &:hover { + background-color: lighten($darkModeControlsBackground, 5); + } + + &.active { + background: $colorBlueBright; + color: #fff; + } + } + } + } + + &.visible { + .dialogInner { + opacity: 1; + } + backdrop-filter: blur(D(3px)); + } + + .dialogInner { + transition: opacity 0.2s ease-in-out; + opacity: 0; + } + + &.loadingDialog { + * { + color: #fff; + } + } + + > .dialogInner { + background: #fff; + max-height: calc(100vh - #{D(40px)}); + @include S(border-radius, $globalBorderRadius); + display: flex; + flex-direction: column; + @include S(padding, 12px); + pointer-events: all; + + @include DarkThemeOverride { + background: darken($darkModeControlsBackground, 5); + } + + &.optionChooserDialog { + .optionParent { + display: grid; + @include S(grid-gap, 5px); + grid-template-columns: 1fr 1fr; + .option { + pointer-events: all; + cursor: pointer; + @include S(padding, 10px); + + background: #eee; + + transition: background-color 0.12s ease-in-out; + + &:hover { + background-color: #e7e7e7; + } + + &.active { + background-color: $colorBlueBright; + color: #fff; + } + } + } + } + + > .title { + @include Heading; + margin: 0; + text-transform: uppercase; + display: grid; + align-items: center; + grid-template-columns: 1fr auto; + @include S(margin-bottom, 10px); + + @include DarkThemeInvert(); + > .closeButton { + opacity: 0.7; + @include S(width, 20px); + @include S(height, 20px); + background: uiResource("icons/close.png") center center / 80% no-repeat; + cursor: pointer; + pointer-events: all; + transition: opacity 0.2s ease-in-out; + &:hover { + opacity: 0.4; + } + } + } + + > .content { + @include PlainText; + overflow-y: auto; + pointer-events: all; + @include S(width, 350px); + + @include DarkThemeOverride { + color: #aaa; + } + + a { + color: $colorBlueBright; + } + + strong { + font-weight: bold; + } + + .keybinding { + position: relative; + background: #eee; + @include PlainText; + height: unset; + margin: 1px 0; + } + + input { + background: #eee; + color: #333438; + width: 100%; + + &.errored { + background-color: rgb(250, 206, 206); + } + } + + ul.bucketList { + padding-left: 30px; + + li { + display: list-item; + } + } + } + + > .buttons { + @include S(margin-top, 15px); + display: flex; + justify-content: flex-end; + > button { + @include S(margin-left, 8px); + @include Text; + @include S(min-width, 60px); + @include S(padding, 5px, 15px); + + transition: opacity 0.12s ease-in-out; + &:hover { + opacity: 0.9; + } + + &.good { + background-color: $colorGreenBright; + color: #fff; + } + + &.bad { + background-color: $colorRedBright; + color: #fff; + } + + &.timedButton { + pointer-events: none; + cursor: default; + position: relative; + overflow: hidden; + &::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: unset; + z-index: 5; + content: " "; + display: inline-block; + background: rgba(#fff, 0.6); + @include InlineAnimation(5s linear) { + 0% { + width: 100%; + } + 100% { + width: 0%; + } + } + } + } + } + } + } +} diff --git a/src/css/ingame_hud/entity_debugger.scss b/src/css/ingame_hud/entity_debugger.scss index 15f03848..4cf7e5e9 100644 --- a/src/css/ingame_hud/entity_debugger.scss +++ b/src/css/ingame_hud/entity_debugger.scss @@ -1,43 +1,79 @@ #ingame_HUD_EntityDebugger { position: absolute; + background: $ingameHudBg; + @include S(padding, 5px); @include S(right, 30px); - @include S(top, 200px); - font-size: 14px; - line-height: 16px; - color: #fff; - background: rgba(0, 10, 20, 0.7); - padding: 10px; + top: 50%; + transform: translateY(-50%); + + @include SuperSmallText; + color: #eee; + display: flex; + flex-direction: column; + + > label { + text-transform: uppercase; + } + + .hint { + color: #aaa; + } + &, * { pointer-events: all; } - .flag { - display: inline-block; - background: #333438; - @include S(padding, 2px); - @include S(margin-right, 2px); - - u { - opacity: 0.5; - } + .propertyTable { + @include S(margin-top, 8px); } - .components { - @include S(margin-top, 4px); + .propertyTable, + .entityComponents, + .entityComponents .object > div { display: grid; - grid-template-columns: 1fr 1fr; - @include S(grid-gap, 3px); - .component { - @include S(padding, 2px); - background: #333; - display: flex; - flex-direction: column; + grid-template-columns: 1fr auto; + @include S(column-gap, 10px); + } - .data { - @include S(width, 150px); - @include S(height, 130px); + .entityComponents { + grid-column: 1 / 3; + @include S(margin-top, 5px); + + font-family: "Roboto Mono", "Fira Code", monospace; + font-size: 90%; + @include S(letter-spacing, -0.5px); + + label, + span { + line-height: 1.5em; + + &:not(span) { + opacity: 0.5; + } + } + &, + * { + @include SuperSmallText; + @include S(font-size, 7px, $important: true); + @include S(line-height, 12px, $important: true); + } + + .object { + grid-column: 1 / 3; + line-height: 1.5em; + + > summary { + transition: opacity 0.1s ease-in-out; + cursor: pointer; + &:hover { + opacity: 0.8; + } + } + > div { + @include S(margin-left, 4px); + cursor: pointer; } } } diff --git a/src/css/ingame_hud/game_menu.scss b/src/css/ingame_hud/game_menu.scss index f893904c..816b97f4 100644 --- a/src/css/ingame_hud/game_menu.scss +++ b/src/css/ingame_hud/game_menu.scss @@ -1,111 +1,95 @@ #ingame_HUD_GameMenu { position: absolute; - top: 0; - right: 0; - display: flex; - grid-auto-flow: column; + @include S(top, 10px); + @include S(right, 10px); + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + @include S(grid-gap, 6px); - > .menuButtons { + backdrop-filter: blur(D(1px)); + + > button, + > .button { + @include PlainText; + @include IncreasedClickArea(0px); + background: green; + @include S(width, 30px); + @include S(height, 30px); + + pointer-events: all; + cursor: pointer; position: relative; - display: flex; - flex-grow: 1; - @include S(padding, 5px, 4px); - justify-content: flex-end; - @include S(margin-left, 20px); + transition: all 0.12s ease-in-out; + transition-property: opacity, transform; - > .button { - @include S(width, 30px); - @include S(height, 30px); - display: inline-block; - background: center center / 60% no-repeat; - pointer-events: all; - cursor: pointer; - transition: all 0.12s ease-in-out; - transition-property: opacity, transform; - will-change: opacity; - opacity: 0.9; - @include S(margin-left, 5px); - position: relative; + display: inline-flex; + background: center center / 70% no-repeat; + grid-row: 1; - @include IncreasedClickArea(0px); + &.pressed { + transform: scale(0.9) !important; + } - @include DarkThemeInvert; + opacity: 0.7; + &:hover { + opacity: 0.9 !important; + } - &:hover { - opacity: 0.8; - } + @include DarkThemeInvert; - &.save { - background-image: uiResource("icons/save.png"); - @include MakeAnimationWrappedEvenOdd(0.5s ease-in-out) { - 0% { - transform: scale(1, 1); - } + &.shop { + background-image: uiResource("icons/shop.png"); + grid-column: 1; + } - 70% { - transform: scale(1.5, 1.5) rotate(20deg); - opacity: 0.2; - } + &.stats { + background-image: uiResource("icons/statistics.png"); + grid-column: 2; + } - 85% { - transform: scale(0.9, 0.9); - opacity: 1; - } + &.save { + background-image: uiResource("icons/save.png"); + grid-column: 3; + @include MakeAnimationWrappedEvenOdd(0.5s ease-in-out) { + 0% { + transform: scale(1, 1); + } - 90% { - transform: scale(1.1, 1.1); - } + 70% { + transform: scale(1.5, 1.5) rotate(20deg); + opacity: 0.2; + } + + 85% { + transform: scale(0.9, 0.9); + opacity: 1; + } + + 90% { + transform: scale(1.1, 1.1); } } - &.settings { - background-image: uiResource("icons/settings.png"); + &.saving { + @include InlineAnimation(0.4s ease-in-out infinite) { + 50% { + opacity: 0.5; + transform: scale(0.8); + } + } + pointer-events: none; + cursor: default; } } - } - .buttonContainer button { - @include PlainText; - color: #fff; - border-color: rgba(0, 0, 0, 0.1); - @include S(padding, 5px, 5px, 5px); - - @include S(padding-left, 30px); - @include S(margin-right, 3px); - @include IncreasedClickArea(0px); - @include ButtonText; - @include S(min-height, 40px); - transition: all 0.12s ease-in-out; - transition-property: opacity, transform; - display: inline-flex; - background: center #{D(13px)} / #{D(20px)} no-repeat; - background-color: $colorGreenBright; - - &[data-button-id="shop"] { - background-color: rgb(93, 103, 250); - background-image: uiResource("icons/shop.png"); - background-size: #{D(18px)}; - } - &[data-button-id="stats"] { - background-color: rgb(85, 199, 138); - background-image: uiResource("icons/statistics.png"); + &.settings { + background-image: uiResource("icons/settings_menu_settings.png"); + grid-column: 4; } &:hover { opacity: 0.9; - } - - .keybinding { - border: 0; - color: #fff; - border-top-left-radius: 0; - border-top-right-radius: 0; - bottom: unset; - background: transparent; - @include S(top, 0px); - right: unset; - left: 50%; - transform: translateX(-50%); + transform: translateY(0); } &:not(.hasBadge) .badge { @@ -113,34 +97,27 @@ } &.hasBadge { - transform-origin: 50% 0%; - @include InlineAnimation(1s ease-in-out infinite) { + &.shop { + filter: none; + background-image: uiResource("icons/shop_active.png"); + opacity: 0.9; + } + transform-origin: 50% 50%; + @include InlineAnimation(0.8s ease-in-out infinite) { 50% { - transform: scale(1.02); + transform: scale(1.3) rotate(6deg); } } .badge { position: absolute; - @include S(bottom, -8px); + top: 50%; left: 50%; - transform: translateX(-50%); - - background: #333; + transform: translate(-50%, -50%); @include PlainText; display: flex; justify-content: center; align-items: center; - @include S(min-width, 5px); - @include S(height, 10px); - @include S(padding, 1px, 3px, 2px); - @include S(border-radius, $globalBorderRadius); - border: #{D(1px)} solid #fff; - @include InlineAnimation(1s ease-in-out infinite) { - 50% { - transform: translateX(-50%) scale(1.05); - } - } } } } diff --git a/src/css/ingame_hud/keybindings_overlay.scss b/src/css/ingame_hud/keybindings_overlay.scss index 21d07b4b..e6845afe 100644 --- a/src/css/ingame_hud/keybindings_overlay.scss +++ b/src/css/ingame_hud/keybindings_overlay.scss @@ -7,7 +7,7 @@ flex-direction: column; align-items: flex-start; color: #333438; - backdrop-filter: blur(D(2px)); + backdrop-filter: blur(D(1px)); padding: D(3px); @include DarkThemeOverride { diff --git a/src/css/ingame_hud/pinned_shapes.scss b/src/css/ingame_hud/pinned_shapes.scss index 48e5b70e..753cce81 100644 --- a/src/css/ingame_hud/pinned_shapes.scss +++ b/src/css/ingame_hud/pinned_shapes.scss @@ -1,138 +1,137 @@ -#ingame_HUD_PinnedShapes { - position: absolute; - @include S(left, 9px); - @include S(top, 150px); - @include PlainText; - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: flex-start; - - > .shape { - position: relative; - display: grid; - align-items: center; - justify-content: center; - grid-template-columns: auto 1fr; - grid-template-rows: 1fr 1fr; - @include S(margin-bottom, 4px); - color: #333438; - // text-shadow: #{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.2); - filter: drop-shadow(#{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.2)); - - &.unpinable { - > canvas { - cursor: pointer; - pointer-events: all; - } - } - - > canvas { - @include S(width, 25px); - @include S(height, 25px); - grid-column: 1 / 2; - grid-row: 1 / 3; - pointer-events: all; - transition: transform 0.1s ease-in-out; - transform-origin: D(2px) center; - will-change: transform; - position: relative; - z-index: 20; - &:hover { - transform: scale(2); - z-index: 21; - } - } - - > .amountLabel, - > .goalLabel { - @include S(margin-left, 5px); - @include SuperSmallText; - font-weight: bold; - display: inline-flex; - align-items: center; - flex-direction: row; - grid-column: 2 / 3; - @include S(height, 9px); - - @include DarkThemeOverride { - color: #eee; - } - } - - > .goalLabel { - @include S(font-size, 7px); - opacity: 0.9; - align-self: start; - justify-self: start; - font-weight: normal; - grid-row: 2 / 3; - } - - > .amountLabel { - align-self: end; - justify-self: start; - grid-row: 1 / 2; - } - - > .infoButton { - @include S(width, 8px); - @include S(height, 8px); - background: uiResource("icons/info_button.png") center center / 95% no-repeat; - position: absolute; - opacity: 0.7; - @include S(top, 13px); - @include S(left, -7px); - @include DarkThemeInvert; - @include IncreasedClickArea(2px); - transition: opacity 0.12s ease-in-out; - z-index: 100; - - &:hover { - opacity: 0.8; - } - } - - &.goal, - &.blueprint { - .amountLabel::after { - content: " "; - position: absolute; - display: inline-block; - @include S(width, 8px); - @include S(height, 8px); - @include S(top, 4px); - @include S(left, -7px); - background: center center / contain no-repeat; - } - - &.goal .amountLabel { - &::after { - background-image: uiResource("icons/current_goal_marker.png"); - background-size: 90%; - } - @include DarkThemeOverride { - &::after { - background-image: uiResource("icons/current_goal_marker_inverted.png") !important; - } - } - } - - &.blueprint .amountLabel { - &::after { - background-image: uiResource("icons/blueprint_marker.png"); - background-size: 90%; - } - @include DarkThemeOverride { - &::after { - background-image: uiResource("icons/blueprint_marker_inverted.png") !important; - } - } - } - } - - &.completed { - opacity: 0.5; - } - } -} +#ingame_HUD_PinnedShapes { + position: absolute; + @include S(left, 9px); + @include S(top, 150px); + @include PlainText; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + > .shape { + position: relative; + display: grid; + align-items: center; + justify-content: center; + grid-template-columns: auto 1fr; + grid-template-rows: 1fr 1fr; + @include S(margin-bottom, 4px); + color: #333438; + // text-shadow: #{D(1px)} #{D(1px)} 0 rgba(0, 10, 20, 0.2); + + &.unpinable { + > canvas { + cursor: pointer; + pointer-events: all; + } + } + + > canvas { + @include S(width, 25px); + @include S(height, 25px); + grid-column: 1 / 2; + grid-row: 1 / 3; + pointer-events: all; + transition: transform 0.1s ease-in-out; + transform-origin: D(2px) center; + will-change: transform; + position: relative; + z-index: 20; + &:hover { + transform: scale(2); + z-index: 21; + } + } + + > .amountLabel, + > .goalLabel { + @include S(margin-left, 5px); + @include SuperSmallText; + font-weight: bold; + display: inline-flex; + align-items: center; + flex-direction: row; + grid-column: 2 / 3; + @include S(height, 9px); + + @include DarkThemeOverride { + color: #eee; + } + } + + > .goalLabel { + @include S(font-size, 7px); + opacity: 0.9; + align-self: start; + justify-self: start; + font-weight: normal; + grid-row: 2 / 3; + } + + > .amountLabel { + align-self: end; + justify-self: start; + grid-row: 1 / 2; + } + + > .infoButton { + @include S(width, 8px); + @include S(height, 8px); + background: uiResource("icons/info_button.png") center center / 95% no-repeat; + position: absolute; + opacity: 0.7; + @include S(top, 13px); + @include S(left, -7px); + @include DarkThemeInvert; + @include IncreasedClickArea(2px); + transition: opacity 0.12s ease-in-out; + z-index: 100; + + &:hover { + opacity: 0.8; + } + } + + &.goal, + &.blueprint { + .amountLabel::after { + content: " "; + position: absolute; + display: inline-block; + @include S(width, 8px); + @include S(height, 8px); + @include S(top, 4px); + @include S(left, -7px); + background: center center / contain no-repeat; + } + + &.goal .amountLabel { + &::after { + background-image: uiResource("icons/current_goal_marker.png"); + background-size: 90%; + } + @include DarkThemeOverride { + &::after { + background-image: uiResource("icons/current_goal_marker_inverted.png") !important; + } + } + } + + &.blueprint .amountLabel { + &::after { + background-image: uiResource("icons/blueprint_marker.png"); + background-size: 90%; + } + @include DarkThemeOverride { + &::after { + background-image: uiResource("icons/blueprint_marker_inverted.png") !important; + } + } + } + } + + &.completed { + opacity: 0.5; + } + } +} diff --git a/src/css/ingame_hud/sandbox_controller.scss b/src/css/ingame_hud/sandbox_controller.scss index 0202f5ef..e4680fe4 100644 --- a/src/css/ingame_hud/sandbox_controller.scss +++ b/src/css/ingame_hud/sandbox_controller.scss @@ -1,50 +1,50 @@ -#ingame_HUD_SandboxController { - position: absolute; - background: $ingameHudBg; - @include S(padding, 5px); - @include S(bottom, 10px); - @include S(left, 10px); - - @include SuperSmallText; - color: #eee; - display: flex; - flex-direction: column; - - > label { - text-transform: uppercase; - } - - .hint { - color: #aaa; - } - - .plusMinus { - @include S(margin-top, 4px); - display: grid; - grid-template-columns: 1fr auto auto; - align-items: center; - @include S(grid-gap, 4px); - - button { - @include PlainText; - @include S(padding, 0); - display: flex; - align-items: center; - justify-content: center; - @include S(width, 15px); - @include S(height, 15px); - @include IncreasedClickArea(0px); - } - } - - .additionalOptions { - display: flex; - flex-direction: column; - @include S(margin-top, 10px); - button { - @include S(margin-bottom, 2px); - @include IncreasedClickArea(0px); - @include SuperSmallText; - } - } -} +#ingame_HUD_SandboxController { + position: absolute; + background: $ingameHudBg; + @include S(padding, 5px); + @include S(bottom, 10px); + @include S(left, 10px); + + @include SuperSmallText; + color: #eee; + display: flex; + flex-direction: column; + + > label { + text-transform: uppercase; + } + + .sandboxHint { + color: #aaa; + } + + .plusMinus { + @include S(margin-top, 4px); + display: grid; + grid-template-columns: 1fr auto auto; + align-items: center; + @include S(grid-gap, 4px); + + button { + @include PlainText; + @include S(padding, 0); + display: flex; + align-items: center; + justify-content: center; + @include S(width, 15px); + @include S(height, 15px); + @include IncreasedClickArea(0px); + } + } + + .additionalOptions { + display: flex; + flex-direction: column; + @include S(margin-top, 10px); + button { + @include S(margin-bottom, 2px); + @include IncreasedClickArea(0px); + @include SuperSmallText; + } + } +} diff --git a/src/css/ingame_hud/settings_menu.scss b/src/css/ingame_hud/settings_menu.scss index e0cec1f6..3ba67358 100644 --- a/src/css/ingame_hud/settings_menu.scss +++ b/src/css/ingame_hud/settings_menu.scss @@ -1,41 +1,61 @@ -#ingame_HUD_SettingsMenu { - .statsElement { - position: absolute; - @include S(left, 30px); - @include S(top, 30px); - color: #fff; - display: flex; - grid-template-rows: 1fr auto; - flex-direction: column; - - strong { - text-transform: uppercase; - @include PlainText; - opacity: 0.5; - } - - span { - @include S(margin-bottom, 25px); - @include Heading; - } - } - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - - .buttons { - display: grid; - grid-auto-flow: row; - @include S(grid-gap, 10px); - background: rgba(0, 10, 20, 0.1); - @include S(padding, 10px); - @include S(border-radius, $globalBorderRadius); - - button { - background-color: #eee; - color: #55585a; - } - } -} +#ingame_HUD_SettingsMenu { + .statsElement { + position: absolute; + @include S(left, 30px); + @include S(right, 30px); + @include S(bottom, 30px); + color: #fff; + display: grid; + grid-template-rows: auto auto; + grid-auto-columns: 1fr; + align-items: center; + justify-items: center; + + strong { + text-transform: uppercase; + @include PlainText; + opacity: 0.5; + grid-row: 1; + } + + span { + @include Heading; + grid-row: 2; + } + } + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .buttons { + display: grid; + grid-auto-flow: column; + @include S(grid-gap, 50px); + @include S(margin-top, -10px); + + button { + background: transparent; + filter: invert(1); + + background: uiResource("icons/settings_menu_play.png") center top / contain no-repeat; + content: ""; + opacity: 0.8; + @include S(width, 35px); + @include S(height, 35px); + + &.settings { + background-image: uiResource("icons/settings_menu_settings.png"); + } + + &.menu { + background-image: uiResource("icons/settings_menu_exit.png"); + } + + &:hover { + opacity: 0.6; + } + } + } +} diff --git a/src/css/ingame_hud/shape_viewer.scss b/src/css/ingame_hud/shape_viewer.scss index 65491a5a..9ece9f35 100644 --- a/src/css/ingame_hud/shape_viewer.scss +++ b/src/css/ingame_hud/shape_viewer.scss @@ -1,161 +1,154 @@ -#ingame_HUD_ShapeViewer { - $dims: 170px; - - .content { - display: flex; - @include S(width, $dims); - width: 100%; - flex-direction: column; - overflow-x: hidden; - - &[data-layers="3"], - &[data-layers="4"] { - @include S(width, 2 * $dims); - .renderArea { - display: grid; - grid-template-columns: 1fr 1fr; - @include S(grid-row-gap, 15px); - } - } - - .renderArea { - display: grid; - width: 100%; - @include S(grid-row-gap, 10px); - align-items: center; - justify-items: center; - } - - .infoArea { - align-self: flex-end; - @include S(margin-top, 10px); - display: flex; - flex-direction: column; - overflow: hidden; - - button { - @include S(margin, 0); - @include PlainText; - } - } - - .seperator { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - } - - .layer { - position: relative; - background: #eee; - - @include DarkThemeOverride { - background: rgba(0, 10, 20, 0.2); - } - @include S(width, 150px); - @include S(height, 100px); - display: flex; - align-items: center; - justify-content: center; - - > canvas { - @include S(width, 50px); - @include S(height, 50px); - } - - .quad { - position: absolute; - width: 50%; - height: 50%; - display: flex; - justify-content: center; - align-items: center; - box-sizing: border-box; - - $arrowDims: 23px; - $spacing: 9px; - @include S(padding, 6px); - - .colorLabel { - text-transform: uppercase; - @include SuperSmallText; - @include S(font-size, 9px); - } - - .emptyLabel { - text-transform: uppercase; - @include SuperSmallText; - @include S(font-size, 9px); - } - - &::after { - content: " "; - background: rgba(0, 10, 20, 0.5); - @include S(width, $arrowDims); - @include S(height, 1px); - position: absolute; - transform: rotate(45deg); - transform-origin: 50% 50%; - } - @include DarkThemeOverride { - &::after { - background: rgba(255, 255, 255, 0.5); - } - } - - &.quad-0 { - right: 0; - top: 0; - align-items: flex-start; - justify-content: flex-end; - - &::after { - @include S(left, $spacing); - @include S(bottom, $arrowDims / 2 + $spacing); - transform: rotate(-45deg); - } - } - &.quad-1 { - bottom: 0; - right: 0; - - align-items: flex-end; - justify-content: flex-end; - - &::after { - @include S(left, $spacing); - @include S(top, $arrowDims / 2 + $spacing); - transform: rotate(45deg); - } - } - &.quad-2 { - bottom: 0; - left: 0; - - align-items: flex-end; - justify-content: flex-start; - - &::after { - @include S(right, $spacing); - @include S(top, $arrowDims / 2 + $spacing); - transform: rotate(135deg); - } - } - &.quad-3 { - top: 0; - left: 0; - - align-items: flex-start; - justify-content: flex-start; - - &::after { - @include S(right, $spacing); - @include S(bottom, $arrowDims / 2 + $spacing); - transform: rotate(225deg); - } - } - } - } - } -} +#ingame_HUD_ShapeViewer { + $dims: 170px; + + .content { + display: flex; + @include S(width, $dims); + width: 100%; + flex-direction: column; + overflow-x: hidden; + + &[data-layers="3"], + &[data-layers="4"] { + @include S(width, 2 * $dims); + .renderArea { + display: grid; + grid-template-columns: 1fr 1fr; + @include S(grid-row-gap, 15px); + } + } + + .renderArea { + display: grid; + width: 100%; + @include S(grid-row-gap, 10px); + align-items: center; + justify-items: center; + } + + .infoArea { + align-self: flex-end; + @include S(margin-top, 10px); + display: flex; + flex-direction: column; + overflow: hidden; + + button { + @include S(margin, 0); + @include PlainText; + } + } + + .layer { + position: relative; + background: #eee; + + @include DarkThemeOverride { + background: rgba(0, 10, 20, 0.2); + } + @include S(width, 150px); + @include S(height, 100px); + display: flex; + align-items: center; + justify-content: center; + + > canvas { + @include S(width, 50px); + @include S(height, 50px); + } + + .quad { + position: absolute; + width: 50%; + height: 50%; + display: flex; + justify-content: center; + align-items: center; + box-sizing: border-box; + + $arrowDims: 23px; + $spacing: 9px; + @include S(padding, 6px); + + .colorLabel { + text-transform: uppercase; + @include SuperSmallText; + @include S(font-size, 9px); + } + + .emptyLabel { + text-transform: uppercase; + @include SuperSmallText; + @include S(font-size, 9px); + } + + &::after { + content: " "; + background: rgba(0, 10, 20, 0.5); + @include S(width, $arrowDims); + @include S(height, 1px); + position: absolute; + transform: rotate(45deg); + transform-origin: 50% 50%; + } + @include DarkThemeOverride { + &::after { + background: rgba(255, 255, 255, 0.5); + } + } + + &.quad-0 { + right: 0; + top: 0; + align-items: flex-start; + justify-content: flex-end; + + &::after { + @include S(left, $spacing); + @include S(bottom, $arrowDims / 2 + $spacing); + transform: rotate(-45deg); + } + } + &.quad-1 { + bottom: 0; + right: 0; + + align-items: flex-end; + justify-content: flex-end; + + &::after { + @include S(left, $spacing); + @include S(top, $arrowDims / 2 + $spacing); + transform: rotate(45deg); + } + } + &.quad-2 { + bottom: 0; + left: 0; + + align-items: flex-end; + justify-content: flex-start; + + &::after { + @include S(right, $spacing); + @include S(top, $arrowDims / 2 + $spacing); + transform: rotate(135deg); + } + } + &.quad-3 { + top: 0; + left: 0; + + align-items: flex-start; + justify-content: flex-start; + + &::after { + @include S(right, $spacing); + @include S(bottom, $arrowDims / 2 + $spacing); + transform: rotate(225deg); + } + } + } + } + } +} diff --git a/src/css/ingame_hud/shop.scss b/src/css/ingame_hud/shop.scss index d8cc8a86..73727390 100644 --- a/src/css/ingame_hud/shop.scss +++ b/src/css/ingame_hud/shop.scss @@ -1,320 +1,320 @@ -#ingame_HUD_Shop { - .content { - @include S(padding-right, 10px); - display: flex; - flex-direction: column; - @include S(width, 500px); - - .upgrade { - display: grid; - grid-template-columns: auto 1fr auto; - background: #eee; - @include S(border-radius, $globalBorderRadius); - @include S(margin-bottom, 4px); - @include S(padding, 5px, 10px); - @include S(grid-row-gap, 1px); - @include S(height, 85px); - grid-template-rows: #{D(20px)} auto; - - &:last-child { - margin-bottom: 0; - } - - @include DarkThemeOverride { - background: #55585a; - } - - .title { - grid-column: 1 / 3; - grid-row: 1 / 2; - @include PlainText; - display: flex; - align-items: center; - flex-direction: row-reverse; - justify-content: flex-end; - - @include DarkThemeOverride { - color: #fff; - } - - .tier { - @include S(margin-right, 9px); - background: $colorGreenBright; - @include S(border-radius, $globalBorderRadius); - text-transform: uppercase; - @include PlainText; - color: #fff; - text-align: center; - font-weight: bold; - @include S(min-width, 50px); - @include S(padding, 0px, 5px); - - &[data-tier="0"] { - background-color: rgb(73, 186, 190); - } - &[data-tier="1"] { - background-color: rgb(88, 110, 207); - } - &[data-tier="2"] { - background-color: rgb(189, 100, 192); - } - &[data-tier="3"] { - background-color: rgb(117, 192, 98); - } - &[data-tier="4"] { - background-color: rgb(243, 77, 48); - } - &[data-tier="5"] { - background-color: rgb(255, 209, 6); - } - &[data-tier="6"] { - background-color: rgb(44, 41, 46); - } - } - } - - .icon { - @include S(width, 40px); - @include S(height, 40px); - background: center center / 80% no-repeat; - align-self: center; - justify-self: center; - grid-column: 1 / 2; - grid-row: 2 / 4; - @include S(margin-right, 30px); - @include S(margin-left, 10px); - opacity: 0.32; - display: none; - } - - .description { - grid-column: 2 / 4; - grid-row: 1 / 2; - @include PlainText; - color: #aaa; - align-self: start; - justify-self: end; - } - - .requirements { - grid-column: 2 / 3; - grid-row: 3 / 4; - display: grid; - grid-auto-flow: column; - @include S(grid-gap, 9px); - justify-content: start; - - .requirement { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - @include S(width, 70px); - overflow: hidden; - - button.pin { - @include S(width, 12px); - @include S(height, 12px); - background: uiResource("icons/pin.png") center center / 95% no-repeat; - position: absolute; - @include S(top, 2px); - @include S(right, 2px); - opacity: 0.6; - cursor: pointer; - pointer-events: all; - @include IncreasedClickArea(5px); - transition: opacity 0.12s ease-in-out; - - @include DarkThemeInvert; - - $disabledOpacity: 0.2; - $enabledOpacity: 0.6; - - &:hover { - opacity: $enabledOpacity + 0.1; - } - - &.alreadyPinned { - opacity: $disabledOpacity !important; - - &:hover { - opacity: $disabledOpacity + 0.1 !important; - } - } - - &.isGoal { - background: uiResource("icons/current_goal_marker.png") center center / 95% - no-repeat; - opacity: $disabledOpacity !important; - cursor: default; - pointer-events: none; - } - - &.pinned { - opacity: $disabledOpacity; - @include InlineAnimation(0.3s ease-in-out) { - 0% { - opacity: 1; - transform: scale(0.8); - } - - 30% { - opacity: 1; - transform: scale(1.2); - } - - 100% { - transform: scale(1); - } - } - &:hover { - opacity: $disabledOpacity + 0.1; - } - } - - &.unpinned { - opacity: $enabledOpacity; - @include InlineAnimation(0.3s ease-in-out) { - 0% { - opacity: 1; - transform: scale(0.8); - } - - 30% { - opacity: 1; - transform: scale(1.2); - } - - 100% { - transform: scale(1); - } - } - &:hover { - opacity: $enabledOpacity + 0.1; - } - } - } - - button.showInfo { - @include S(width, 11px); - @include S(height, 11px); - background: uiResource("icons/info_button.png") center center / 95% no-repeat; - position: absolute; - @include S(top, 17px); - @include S(right, 2.5px); - opacity: 0.5; - cursor: pointer; - pointer-events: all; - @include IncreasedClickArea(5px); - transition: opacity 0.12s ease-in-out; - @include DarkThemeInvert; - - &:hover { - opacity: 0.6; - } - } - - canvas { - @include S(width, 40px); - @include S(height, 40px); - } - - .amount { - @include S(margin-top, 4px); - z-index: 10; - @include SuperSmallText; - letter-spacing: 0; - background: #e2e4e6; - - @include S(line-height, 13px); - @include S(border-radius, $globalBorderRadius); - @include S(padding, 1px, 0px, 2px); - position: relative; - text-align: center; - @include S(min-width, 50px); - // @include S(max-width, 100px); - overflow: hidden; - width: 100%; - - @include DarkThemeOverride { - background: #333438; - color: #fff; - } - - .progressBar { - bottom: 0; - left: 0; - right: 0; - top: 0; - @include S(border-radius, $globalBorderRadius); - position: absolute; - display: inline-block; - z-index: -1; - transition: all 0.2s ease-in-out; - transition-property: width, background-color; - background: #bdbfca; - - @include DarkThemeOverride { - background: #8c8d96; - } - - &.complete { - background-color: $colorGreenBright; - - @include DarkThemeOverride { - background-color: $colorGreenBright; - } - } - } - } - } - } - - button.buy { - grid-column: 3 / 4; - grid-row: 3 / 4; - align-self: center; - justify-self: end; - // @include S(padding, 4px, 5px); - // @include PlainText; - background-color: $colorGreenBright; - color: #fff; - - transition: all 0.2s ease-in-out; - transition-property: background-color, opacity; - - &:not(.buyable) { - background-color: #aaa; - cursor: default; - pointer-events: none; - opacity: 0.3; - } - - &.buyable { - @include InlineAnimation(1s ease-in-out infinite) { - 0% { - } - - 50% { - background-color: lighten($colorGreenBright, 10); - } - 100% { - } - } - } - } - - &.maxLevel { - button.buy { - opacity: 0 !important; - } - .requirements { - display: none; - } - .description { - color: $colorGreenBright; - } - } - } - } -} +#ingame_HUD_Shop { + .content { + @include S(padding-right, 10px); + display: flex; + flex-direction: column; + @include S(width, 500px); + + .upgrade { + display: grid; + grid-template-columns: auto 1fr auto; + background: #eee; + @include S(border-radius, $globalBorderRadius); + @include S(margin-bottom, 4px); + @include S(padding, 5px, 10px); + @include S(grid-row-gap, 1px); + @include S(height, 85px); + grid-template-rows: #{D(20px)} auto; + + &:last-child { + margin-bottom: 0; + } + + @include DarkThemeOverride { + background: $darkModeControlsBackground; + } + + .title { + grid-column: 1 / 3; + grid-row: 1 / 2; + @include PlainText; + display: flex; + align-items: center; + flex-direction: row-reverse; + justify-content: flex-end; + + @include DarkThemeOverride { + color: #fff; + } + + .tier { + @include S(margin-right, 9px); + background: $colorGreenBright; + @include S(border-radius, $globalBorderRadius); + text-transform: uppercase; + @include PlainText; + color: #fff; + text-align: center; + font-weight: bold; + @include S(min-width, 50px); + @include S(padding, 0px, 5px); + + &[data-tier="0"] { + background-color: rgb(73, 186, 190); + } + &[data-tier="1"] { + background-color: rgb(88, 110, 207); + } + &[data-tier="2"] { + background-color: rgb(189, 100, 192); + } + &[data-tier="3"] { + background-color: rgb(117, 192, 98); + } + &[data-tier="4"] { + background-color: rgb(243, 77, 48); + } + &[data-tier="5"] { + background-color: rgb(255, 209, 6); + } + &[data-tier="6"] { + background-color: rgb(44, 41, 46); + } + } + } + + .icon { + @include S(width, 40px); + @include S(height, 40px); + background: center center / 80% no-repeat; + align-self: center; + justify-self: center; + grid-column: 1 / 2; + grid-row: 2 / 4; + @include S(margin-right, 30px); + @include S(margin-left, 10px); + opacity: 0.32; + display: none; + } + + .description { + grid-column: 2 / 4; + grid-row: 1 / 2; + @include PlainText; + color: #aaa; + align-self: start; + justify-self: end; + } + + .requirements { + grid-column: 2 / 3; + grid-row: 3 / 4; + display: grid; + grid-auto-flow: column; + @include S(grid-gap, 9px); + justify-content: start; + + .requirement { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + @include S(width, 70px); + overflow: hidden; + + button.pin { + @include S(width, 12px); + @include S(height, 12px); + background: uiResource("icons/pin.png") center center / 95% no-repeat; + position: absolute; + @include S(top, 2px); + @include S(right, 2px); + opacity: 0.6; + cursor: pointer; + pointer-events: all; + @include IncreasedClickArea(5px); + transition: opacity 0.12s ease-in-out; + + @include DarkThemeInvert; + + $disabledOpacity: 0.2; + $enabledOpacity: 0.6; + + &:hover { + opacity: $enabledOpacity + 0.1; + } + + &.alreadyPinned { + opacity: $disabledOpacity !important; + + &:hover { + opacity: $disabledOpacity + 0.1 !important; + } + } + + &.isGoal { + background: uiResource("icons/current_goal_marker.png") center center / 95% + no-repeat; + opacity: $disabledOpacity !important; + cursor: default; + pointer-events: none; + } + + &.pinned { + opacity: $disabledOpacity; + @include InlineAnimation(0.3s ease-in-out) { + 0% { + opacity: 1; + transform: scale(0.8); + } + + 30% { + opacity: 1; + transform: scale(1.2); + } + + 100% { + transform: scale(1); + } + } + &:hover { + opacity: $disabledOpacity + 0.1; + } + } + + &.unpinned { + opacity: $enabledOpacity; + @include InlineAnimation(0.3s ease-in-out) { + 0% { + opacity: 1; + transform: scale(0.8); + } + + 30% { + opacity: 1; + transform: scale(1.2); + } + + 100% { + transform: scale(1); + } + } + &:hover { + opacity: $enabledOpacity + 0.1; + } + } + } + + button.showInfo { + @include S(width, 11px); + @include S(height, 11px); + background: uiResource("icons/info_button.png") center center / 95% no-repeat; + position: absolute; + @include S(top, 17px); + @include S(right, 2.5px); + opacity: 0.5; + cursor: pointer; + pointer-events: all; + @include IncreasedClickArea(5px); + transition: opacity 0.12s ease-in-out; + @include DarkThemeInvert; + + &:hover { + opacity: 0.6; + } + } + + canvas { + @include S(width, 40px); + @include S(height, 40px); + } + + .amount { + @include S(margin-top, 4px); + z-index: 10; + @include SuperSmallText; + letter-spacing: 0; + background: #e2e4e6; + + @include S(line-height, 13px); + @include S(border-radius, $globalBorderRadius); + @include S(padding, 1px, 0px, 2px); + position: relative; + text-align: center; + @include S(min-width, 50px); + // @include S(max-width, 100px); + overflow: hidden; + width: 100%; + + @include DarkThemeOverride { + background: #333438; + color: #fff; + } + + .progressBar { + bottom: 0; + left: 0; + right: 0; + top: 0; + @include S(border-radius, $globalBorderRadius); + position: absolute; + display: inline-block; + z-index: -1; + transition: all 0.2s ease-in-out; + transition-property: width, background-color; + background: #bdbfca; + + @include DarkThemeOverride { + background: #8c8d96; + } + + &.complete { + background-color: $colorGreenBright; + + @include DarkThemeOverride { + background-color: $colorGreenBright; + } + } + } + } + } + } + + button.buy { + grid-column: 3 / 4; + grid-row: 3 / 4; + align-self: center; + justify-self: end; + // @include S(padding, 4px, 5px); + // @include PlainText; + background-color: $colorGreenBright; + color: #fff; + + transition: all 0.2s ease-in-out; + transition-property: background-color, opacity; + + &:not(.buyable) { + background-color: #aaa; + cursor: default; + pointer-events: none; + opacity: 0.3; + } + + &.buyable { + @include InlineAnimation(1s ease-in-out infinite) { + 0% { + } + + 50% { + background-color: lighten($colorGreenBright, 10); + } + 100% { + } + } + } + } + + &.maxLevel { + button.buy { + opacity: 0 !important; + } + .requirements { + display: none; + } + .description { + color: $colorGreenBright; + } + } + } + } +} diff --git a/src/css/ingame_hud/statistics.scss b/src/css/ingame_hud/statistics.scss index 12b8c1aa..41ed4467 100644 --- a/src/css/ingame_hud/statistics.scss +++ b/src/css/ingame_hud/statistics.scss @@ -33,22 +33,47 @@ &.displayIcons, &.displayDetailed, + &.displaySorted, + &.displayIterateUnit { + background: transparent center center / #{D(15px)} no-repeat; + } + + &.displayDetailed { + background-image: uiResource("icons/display_list.png"); + } + + &.displayIcons { + background-image: uiResource("icons/display_icons.png"); + background-size: #{D(11.5px)}; + } + + &.displayDetailed { + @include S(border-top-left-radius, $globalBorderRadius); + @include S(border-bottom-left-radius, $globalBorderRadius); + } + &.displaySorted { - background: uiResource("icons/display_list.png") center center / #{D(15px)} no-repeat; - &.displayIcons { - background-image: uiResource("icons/display_icons.png"); - background-size: #{D(11.5px)}; - } - &.displaySorted { - background-image: uiResource("icons/display_sorted.png"); - background-size: #{D(11.5px)}; - margin-right: 4px; - @include S(padding, 1px, 0); - } + background-image: uiResource("icons/display_sorted.png"); + background-size: #{D(11.5px)}; + margin-right: 5px; + @include S(border-top-right-radius, $globalBorderRadius); + @include S(border-bottom-right-radius, $globalBorderRadius); + + @include S(padding, 1px, 0); + } + + &.displayIterateUnit { + background-image: uiResource("icons/toggle_unit.png"); + opacity: 0.8; + @include S(padding, 1px, 0); } background-color: #44484a !important; transition: opacity 0.2s ease-in-out; + + @include DarkThemeOverride { + background-color: lighten($darkModeControlsBackground, 10) !important; + } } .filtersDataSource, @@ -110,10 +135,10 @@ } @include DarkThemeOverride { - background: #222428; + background: $darkModeControlsBackground; &.pinned { - background: darken(#222428, 10); + background: mix($darkModeControlsBackground, $colorBlueBright, 90%); } } @@ -159,6 +184,11 @@ grid-column: 1 / 2; grid-row: 2 / 3; justify-self: end; + color: #55595a; + + @include DarkThemeOverride { + color: #aaa; + } } } } @@ -176,6 +206,10 @@ align-self: center; text-align: right; color: #55595a; + + @include DarkThemeOverride { + color: #aaa; + } } canvas.graph { diff --git a/src/css/states/ingame.scss b/src/css/states/ingame.scss index 3f220a5d..d67ee00f 100644 --- a/src/css/states/ingame.scss +++ b/src/css/states/ingame.scss @@ -1,37 +1,52 @@ -#state_InGameState { - .gameLoadingOverlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 9999; - align-items: center; - justify-content: center; - pointer-events: all; - display: flex; - background: $mainBgColor; - flex-direction: column; - } - - #ingame_Canvas { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - } - #ingame_HUD_ModalDialogs { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - } - - @include DarkThemeOverride { - .gameLoadingOverlay { - background: $darkModeGameBackground; - } - } -} +#state_InGameState { + .gameLoadingOverlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + align-items: center; + justify-content: center; + pointer-events: all; + display: flex; + background: $mainBgColor; + flex-direction: column; + } + + .prefab_GameHint { + position: absolute; + @include S(bottom, 40px); + @include S(left, 20px); + @include S(right, 20px); + @include PlainText; + text-align: center; + + color: #666; + + @include DarkThemeOverride() { + color: lighten($darkModeGameBackground, 50); + } + } + + #ingame_Canvas { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + #ingame_HUD_ModalDialogs { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + + @include DarkThemeOverride { + .gameLoadingOverlay { + background: $darkModeGameBackground; + } + } +} diff --git a/src/css/states/keybindings.scss b/src/css/states/keybindings.scss index cf211403..09cc88dd 100644 --- a/src/css/states/keybindings.scss +++ b/src/css/states/keybindings.scss @@ -1,71 +1,71 @@ -#state_KeybindingsState { - .content { - .topEntries { - display: grid; - grid-template-columns: 1fr auto; - @include S(grid-gap, 5px); - @include S(margin-bottom, 10px); - } - - .hint { - display: block; - background: #eee; - @include S(padding, 4px); - @include PlainText; - } - - .category { - .entry { - display: grid; - @include S(margin-top, 2px); - @include S(padding-top, 2px); - @include S(grid-gap, 4px); - grid-template-columns: 1fr #{D(100px)} auto auto; - border-bottom: #{D(1px)} dotted #eee; - color: #888c8f; - .mapping { - color: $colorBlueBright; - text-align: center; - } - - button { - @include S(height, 15px); - @include S(width, 15px); - @include IncreasedClickArea(0px); - background: transparent center center / 40% no-repeat; - opacity: 0.9; - &.editKeybinding { - background-image: uiResource("icons/edit_key.png"); - } - - &.resetKeybinding { - background-image: uiResource("icons/reset_key.png"); - } - - &.disabled { - pointer-events: none; - cursor: default; - opacity: 0.1 !important; - } - } - } - } - } - - @include DarkThemeOverride { - .content { - .hint { - background: #3b3d40; - } - - .category .entry { - color: #c0c4c8; - border-bottom-color: #888; - - button { - filter: invert(1); - } - } - } - } -} +#state_KeybindingsState { + .content { + .topEntries { + display: grid; + grid-template-columns: 1fr auto; + @include S(grid-gap, 5px); + @include S(margin-bottom, 10px); + } + + .hint { + display: block; + background: #eee; + @include S(padding, 4px); + @include PlainText; + } + + .category { + .entry { + display: grid; + @include S(margin-top, 2px); + @include S(padding-top, 2px); + @include S(grid-gap, 4px); + grid-template-columns: 1fr #{D(100px)} auto auto; + border-bottom: #{D(1px)} dotted #eee; + color: #888c8f; + .mapping { + color: $colorBlueBright; + text-align: center; + } + + button { + @include S(height, 15px); + @include S(width, 15px); + @include IncreasedClickArea(0px); + background: transparent center center / 40% no-repeat; + opacity: 0.9; + &.editKeybinding { + background-image: uiResource("icons/edit_key.png"); + } + + &.resetKeybinding { + background-image: uiResource("icons/reset_key.png"); + } + + &.disabled { + pointer-events: none; + cursor: default; + opacity: 0.1 !important; + } + } + } + } + } + + @include DarkThemeOverride { + .content { + .hint { + background: darken($darkModeControlsBackground, 4); + } + + .category .entry { + color: #c0c4c8; + border-bottom-color: #888; + + button { + filter: invert(1); + } + } + } + } +} diff --git a/src/css/states/main_menu.scss b/src/css/states/main_menu.scss index 0d0a289e..e61df636 100644 --- a/src/css/states/main_menu.scss +++ b/src/css/states/main_menu.scss @@ -25,13 +25,15 @@ background: uiResource("icons/main_menu_settings.png") center center / contain no-repeat; transition: opacity 0.12s ease-in-out; @include IncreasedClickArea(2px); + opacity: 0.7; &:hover { - opacity: 0.9; + opacity: 1; } } .exitAppButton { background-image: uiResource("icons/main_menu_exit.png"); + background-size: 90%; } .languageChoose { @@ -40,6 +42,7 @@ background-color: #fff; @include S(border-width, 2px); background-size: cover; + opacity: 0.8; } } @@ -57,7 +60,7 @@ transform: translate(50%, 50%); filter: blur(D(3px)); - $opacity: 0.2; + $opacity: 0.07; &.loaded { display: block; opacity: $opacity; @@ -121,10 +124,13 @@ } .steamLink { + align-self: center; + justify-self: center; width: 100%; @include S(height, 40px); + @include S(width, 180px); - background: uiResource("get_on_steam.png") center center / contain no-repeat; + background: #171a23 uiResource("get_on_steam.png") center center / contain no-repeat; overflow: hidden; display: block; text-indent: -999em; @@ -134,8 +140,11 @@ transition: all 0.12s ease-in; transition-property: opacity, transform; transform: skewX(-0.5deg); + + @include S(border-radius, $globalBorderRadius); + &:hover { - transform: skewX(-1deg) scale(1.02); + transform: scale(1.02); opacity: 0.9; } } @@ -332,25 +341,39 @@ button.downloadGame { grid-column: 3 / 4; grid-row: 1 / 2; - background-color: $colorBlueBright; + background-color: transparent; background-image: uiResource("icons/download.png"); @include S(width, 15px); @include IncreasedClickArea(0px); @include S(height, 15px); - background-size: 60%; + background-size: 80%; align-self: start; + opacity: 0.4; + + &:hover { + opacity: 0.5; + } + + @include DarkThemeInvert; } button.deleteGame { grid-column: 3 / 4; grid-row: 2 / 3; - background-color: $colorRedBright; + background-color: transparent; @include IncreasedClickArea(0px); background-image: uiResource("icons/delete.png"); @include S(width, 15px); @include S(height, 15px); align-self: end; - background-size: 60%; + background-size: 80%; + opacity: 0.4; + + &:hover { + opacity: 0.5; + } + + @include DarkThemeInvert; } button.renameGame { @@ -363,11 +386,11 @@ justify-self: center; background-size: 90%; - opacity: 0.25; + opacity: 0.4; @include S(margin-left, 4px); &:hover { - opacity: 0.35; + opacity: 0.5; } @include DarkThemeInvert; @@ -379,6 +402,11 @@ margin: 0; @include S(width, 32px); height: 100%; + @include S(margin-left, 4px); + + @include DarkThemeOverride { + background-color: lighten($darkModeControlsBackground, 10); + } } } } @@ -407,13 +435,17 @@ @include S(padding, 15px); + $linkBg: #fdfdff; + $linkBgHover: darken($linkBg, 2); + $linkColor: #55586a; + > .boxLink { display: grid; align-items: center; grid-template-columns: 1fr auto; justify-content: center; - background: #fdfdfd uiResource("icons/link.png") top D(3px) right D(3px) / D(9px) no-repeat; + background: $linkBg uiResource("icons/link.png") top D(3px) right D(3px) / D(9px) no-repeat; @include S(padding, 5px); @include S(padding-left, 10px); @include S(border-radius, $globalBorderRadius); @@ -422,7 +454,7 @@ font-weight: bold; box-sizing: border-box; text-transform: uppercase; - color: #616266; + color: $linkColor; transition: background-color 0.12s ease-in-out; pointer-events: all; @@ -431,7 +463,7 @@ cursor: pointer; &:hover { - background-color: #f0f6ff; + background-color: $linkBgHover; } .thirdpartyLogo { @@ -458,12 +490,12 @@ @include S(height, 60px); > a { - color: #616266; - background: #fdfdfd; + color: $linkColor; + background: $linkBg; height: 100%; &:hover { - background-color: #f0f6ff; + background-color: $linkBgHover; } @include SuperSmallText; text-transform: uppercase; @@ -499,19 +531,11 @@ @include DarkThemeOverride { background: $darkModeGameBackground center center / cover !important; - .topButtons { - filter: invert(1); - - .languageChoose { - filter: invert(1); - } - } - .mainContainer { - background: darken($darkModeGameBackground, 10); + background: $darkModeControlsBackground; .savegames .savegame { - background: darken($darkModeGameBackground, 15); + background: darken($darkModeControlsBackground, 5); color: white; } } @@ -519,11 +543,11 @@ .footer { > a, .sidelinks > a { - background-color: darken($darkModeGameBackground, 10); + background-color: $darkModeControlsBackground; color: #eee; &:hover { - background-color: darken($darkModeGameBackground, 8); + background-color: darken($darkModeControlsBackground, 5); } } diff --git a/src/css/states/preload.scss b/src/css/states/preload.scss index ba0a372d..1187a2d4 100644 --- a/src/css/states/preload.scss +++ b/src/css/states/preload.scss @@ -1,145 +1,145 @@ -#state_PreloadState { - &.failure { - .loadingImage, - .loadingStatus { - display: none; - } - } - - .changelogDialogEntry { - margin-top: 10px; - width: 100%; - flex-direction: column; - text-align: left; - padding: 10px; - box-sizing: border-box; - background: #eef1f4; - - @include DarkThemeOverride { - background: #424242; - } - - .version { - @include Heading; - } - .date { - @include PlainText; - &::before { - content: " | "; - } - color: #aaabaf; - } - - .changes { - @include PlainText; - @include S(padding-left, 15px); - strong { - background: $colorBlueBright; - color: #fff; - text-transform: uppercase; - @include S(padding, 1px, 2px); - @include S(margin-right, 3px); - } - a { - color: $colorBlueBright; - } - li { - @include SuperSmallText; - @include S(margin-bottom, 10px); - } - } - } - - .failureBox { - .logo { - img { - @include S(width, 240px); - } - - @include S(margin-bottom, 30px); - } - - @include InlineAnimation(0.3s ease-in-out) { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } - } - - .failureInner { - // background: darken($mainBgColor, 6); - @include S(max-width, 350px); - margin: 0 20px; - text-align: left; - - @include BoxShadow3D(#fff); - @include S(padding, 15px); - @include S(border-radius, $globalBorderRadius); - @include DropShadow; - - .errorHeader { - color: #ef5072; - } - - .errorMessage { - @include PlainText; - display: block; - color: #666; - text-align: left; - @include BreakText; - hyphens: auto; - // border: dotted #666; - // @include S(border-width, 1px, 0); - @include S(padding, 10px, 0); - @include S(margin-top, 10px); - } - - .supportHelp { - @include S(margin-top, 10px); - @include PlainText; - - .email { - color: $themeColor; - cursor: pointer; - pointer-events: all; - } - } - - .lower { - display: flex; - align-items: center; - @include S(margin-top, 16px); - - i { - flex-grow: 1; - text-align: right; - color: #777; - @include PlainText; - } - - button.resetApp { - @include Button3D($colorRedBright); - @include PlainText; - @include S(padding, 5px, 8px, 4px); - color: #fff; - } - } - } - } - - /* Animations */ - .status { - transform: scale(0.7) $hardwareAcc; - opacity: 0; - @include StateAnim(transform, opacity); - } - - &.arrived { - .status { - opacity: 1; - transform: none; - } - } -} +#state_PreloadState { + &.failure { + .loadingImage, + .loadingStatus { + display: none; + } + } + + .changelogDialogEntry { + margin-top: 10px; + width: 100%; + flex-direction: column; + text-align: left; + padding: 10px; + box-sizing: border-box; + background: #eef1f4; + + @include DarkThemeOverride { + background: #424242; + } + + .version { + @include Heading; + } + .date { + @include PlainText; + &::before { + content: " | "; + } + color: #aaabaf; + } + + .changes { + @include PlainText; + @include S(padding-left, 15px); + strong { + background: $colorBlueBright; + color: #fff; + text-transform: uppercase; + @include S(padding, 1px, 2px); + @include S(margin-right, 3px); + } + a { + color: $colorBlueBright; + } + li { + @include SuperSmallText; + @include S(margin-bottom, 10px); + } + } + } + + .failureBox { + .logo { + img { + @include S(width, 240px); + } + + @include S(margin-bottom, 30px); + } + + @include InlineAnimation(0.3s ease-in-out) { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } + } + + .failureInner { + // background: darken($mainBgColor, 6); + @include S(max-width, 350px); + margin: 0 20px; + text-align: left; + + @include BoxShadow3D(#fff); + @include S(padding, 15px); + @include S(border-radius, $globalBorderRadius); + @include DropShadow; + + .errorHeader { + color: #ef5072; + } + + .errorMessage { + @include PlainText; + display: block; + color: #666; + text-align: left; + @include BreakText; + hyphens: auto; + // border: dotted #666; + // @include S(border-width, 1px, 0); + @include S(padding, 10px, 0); + @include S(margin-top, 10px); + } + + .supportHelp { + @include S(margin-top, 10px); + @include PlainText; + + .email { + color: $themeColor; + cursor: pointer; + pointer-events: all; + } + } + + .lower { + display: flex; + align-items: center; + @include S(margin-top, 16px); + + i { + flex-grow: 1; + text-align: right; + color: #777; + @include PlainText; + } + + button.resetApp { + @include Button3D($colorRedBright); + @include PlainText; + @include S(padding, 5px, 8px, 4px); + color: #fff; + } + } + } + } + + /* Animations */ + .status { + transform: scale(0.7) $hardwareAcc; + opacity: 0; + @include StateAnim(transform, opacity); + } + + &.arrived { + .status { + opacity: 1; + transform: none; + } + } +} diff --git a/src/css/states/settings.scss b/src/css/states/settings.scss index 9b7df8fb..9c4c2882 100644 --- a/src/css/states/settings.scss +++ b/src/css/states/settings.scss @@ -1,13 +1,95 @@ #state_SettingsState { - $colorCategoryButton: #eee; - $colorCategoryButtonSelected: #5f748b; + $colorCategoryButton: #eeeff5; + $colorCategoryButtonSelected: $colorBlueBright; + + $layoutBreak: 1000px; .container .content { - display: flex; - overflow-y: scroll; + display: grid; + grid-template-columns: auto 1fr; + @include S(grid-gap, 10px); + + @include StyleBelowWidth($layoutBreak) { + grid-template-columns: 1fr; + } + + .sidebar { + display: grid; + @include S(min-width, 210px); + @include S(max-width, 320px); + @include S(grid-gap, 3px); + grid-template-rows: auto auto auto auto auto 1fr; + + @include StyleBelowWidth($layoutBreak) { + grid-template-rows: 1fr 1fr; + grid-template-columns: auto auto; + max-width: unset !important; + } + + button { + text-align: left; + &::after { + content: unset; + } + width: 100%; + box-sizing: border-box; + + @include StyleBelowWidth($layoutBreak) { + text-align: center; + } + } + + .other { + @include S(margin-top, 10px); + align-self: end; + + @include StyleBelowWidth($layoutBreak) { + margin-top: 0; + } + } + + button.categoryButton, + button.about { + background-color: $colorCategoryButton; + color: #777a7f; + + &.active { + background-color: $colorCategoryButtonSelected; + color: #fff; + + &:hover { + opacity: 1; + } + } + + &.pressed { + transform: none !important; + } + } + + .versionbar { + @include S(margin-top, 10px); + + @include StyleBelowWidth($layoutBreak) { + display: none; + } + + @include SuperSmallText; + display: grid; + align-items: center; + grid-template-columns: 1fr auto; + .buildVersion { + display: flex; + flex-direction: column; + color: #aaadaf; + } + } + } .categoryContainer { - width: 100%; + overflow-y: scroll; + pointer-events: all; + @include S(padding-right, 10px); .category { display: none; @@ -88,65 +170,6 @@ } } } - - .sidebar { - display: flex; - flex-direction: column; - @include S(min-width, 210px); - @include S(max-width, 320px); - width: 30%; - height: 100%; - position: sticky; - top: 0; - @include S(margin-left, 20px); - @include S(margin-right, 32px); - - .other { - margin-top: auto; - } - - button { - @include S(margin-top, 4px); - width: calc(100% - #{D(20px)}); - text-align: start; - - &::after { - content: unset; - } - } - - button.categoryButton, - button.about { - background-color: $colorCategoryButton; - color: #777a7f; - - &.active { - background-color: $colorCategoryButtonSelected; - color: #fff; - - &:hover { - opacity: 1; - } - } - - &.pressed { - transform: none !important; - } - } - - .versionbar { - @include S(margin-top, 20px); - @include SuperSmallText; - display: grid; - align-items: center; - grid-template-columns: 1fr auto; - .buildVersion { - display: flex; - flex-direction: column; - color: #aaadaf; - } - } - } } @include DarkThemeOverride { @@ -154,10 +177,12 @@ .sidebar { button.categoryButton, button.about { - background-color: #3f3f47; + color: #ccc; + background-color: darken($darkModeControlsBackground, 5); &.active { - background-color: $colorBlueBright; + color: #fff; + background-color: $colorCategoryButtonSelected; } } } @@ -169,8 +194,13 @@ .value.enum { // dirty but works - filter: invert(0.78) sepia(40%) hue-rotate(190deg); - color: #222; + // color: #222; + background-color: $darkModeControlsBackground; + background-image: uiResource("icons/enum_selector_white.png"); + color: #ddd; + &:hover { + background-color: darken($darkModeControlsBackground, 2); + } } .value.checkbox { diff --git a/src/css/textual_game_state.scss b/src/css/textual_game_state.scss index 54c5dbb3..63b59cd0 100644 --- a/src/css/textual_game_state.scss +++ b/src/css/textual_game_state.scss @@ -1,81 +1,81 @@ -.gameState.textualState { - display: grid; - grid-template-rows: auto 1fr; - box-sizing: border-box; - @include S(padding, 32px); - height: 100vh; - - .headerBar { - display: flex; - - h1 { - display: grid; - grid-template-columns: auto 1fr; - align-items: center; - pointer-events: all; - cursor: pointer; - @include SuperHeading; - text-transform: uppercase; - color: #333438; - position: relative; - @include IncreasedClickArea(10px); - } - - .backButton { - @include S(width, 30px); - @include S(height, 30px); - @include S(margin-right, 10px); - @include S(margin-left, -5px); - background: uiResource("icons/state_back_button.png") center center / 70% no-repeat; - } - - @include S(margin-bottom, 20px); - } - - > .container { - display: flex; - justify-content: center; - width: 100%; - overflow-y: auto; - - > .content { - width: 100%; - background: #fff; - @include S(border-radius, $globalBorderRadius); - @include S(padding, 10px); - height: 100%; - overflow-y: auto; - box-sizing: border-box; - pointer-events: all; - - a { - color: $colorBlueBright; - } - - .categoryLabel { - display: block; - text-transform: uppercase; - @include S(margin-top, 15px); - @include S(margin-bottom, 15px); - @include Heading; - } - } - } - - @include DarkThemeOverride { - .headerBar { - h1 { - color: #e2e0db; - } - - .backButton { - filter: invert(1); - } - } - - > .container > .content { - background: darken($darkModeGameBackground, 3); - color: #eee; - } - } -} +.gameState.textualState { + display: grid; + grid-template-rows: auto 1fr; + box-sizing: border-box; + @include S(padding, 32px); + height: 100vh; + + .headerBar { + display: flex; + + h1 { + display: grid; + grid-template-columns: auto 1fr; + align-items: center; + pointer-events: all; + cursor: pointer; + @include SuperHeading; + text-transform: uppercase; + color: #333438; + position: relative; + @include IncreasedClickArea(10px); + } + + .backButton { + @include S(width, 30px); + @include S(height, 30px); + @include S(margin-right, 10px); + @include S(margin-left, -5px); + background: uiResource("icons/state_back_button.png") center center / 70% no-repeat; + } + + @include S(margin-bottom, 20px); + } + + > .container { + display: flex; + justify-content: center; + width: 100%; + overflow-y: auto; + + > .content { + width: 100%; + background: #fff; + @include S(border-radius, $globalBorderRadius); + @include S(padding, 10px); + height: 100%; + overflow-y: auto; + box-sizing: border-box; + pointer-events: all; + + a { + color: $colorBlueBright; + } + + .categoryLabel { + display: block; + text-transform: uppercase; + @include S(margin-top, 15px); + @include S(margin-bottom, 15px); + @include Heading; + } + } + } + + @include DarkThemeOverride { + .headerBar { + h1 { + color: #e2e0db; + } + + .backButton { + filter: invert(1); + } + } + + > .container > .content { + background: $darkModeControlsBackground; + color: #eee; + } + } +} diff --git a/src/css/variables.scss b/src/css/variables.scss index 7646e471..d2798f41 100644 --- a/src/css/variables.scss +++ b/src/css/variables.scss @@ -1,198 +1,199 @@ -$globalBorderRadius: 2px; - -// When to reduce control elements size for small devices -$layoutExpandMinWidth: 340px; - -// Font sizes and line heights -$superHeadingFontSize: 25px; -$superHeadingLineHeight: 24px; - -$breakTooltipShowStatsPx: 1023px; - -$headingFontSize: 19px; -$headingLineHeight: 21px; - -$textFontSize: 16px; -$textLineHeight: 21px; - -$plainTextFontSize: 13px; -$plainTextLineHeight: 17px; - -$supersmallTextFontSize: 10px; -$supersmallTextLineHeight: 13px; -$buttonFontSize: 14px; -$buttonLineHeight: 18px; - -// Main background color -$mainBgColor: #dee1ea; - -// Accent colors - -$accentColorBright: #e1e4ed; -$accentColorDark: #7d808a; -$colorGreenBright: #66bb6a; -$colorBlueBright: rgb(74, 163, 223); -$colorRedBright: #ef5072; -$themeColor: #393747; -$ingameHudBg: rgba(#333438, 0.9); - -$text3dColor: #f4ffff; - -$darkModeGameBackground: #5c606c; - -// Dialog properties -$modalDialogBg: rgba(160, 165, 180, 0.8); -$dialogBgColor: lighten($mainBgColor, 10); - -$lightFontWeight: normal; -$boldFontWeight: 600; - -$iconSizeSmall: 30px; -$iconSizeMedium: 40px; -$iconSizeLarge: 60px; - -// Poppins 500 -// Rubik 400 -// Cairo 400 -// Viga 400 -// Sniglet 400 - -$mainFont: "GameFont", sans-serif; -// $mainFont: "DK Canoodle"; -// $mainFont: "MADE Florence Sans"; -$numberFont: $mainFont; -$textFont: $mainFont; - -$mainFontWeight: 400; -$mainFontSpacing: 0.04em; -$mainFontScale: 1; - -@mixin DebugText($color) { - // font-size: 3px; - // &, - // * { - // color: $color !important; - // } -} - -@mixin SuperSmallText { - @include ScaleFont($supersmallTextFontSize, $supersmallTextLineHeight); - font-weight: $mainFontWeight; - font-family: $mainFont; - letter-spacing: $mainFontSpacing; - @include DebugText(green); -} - -@mixin PlainText { - @include ScaleFont($plainTextFontSize, $plainTextLineHeight); - font-weight: $mainFontWeight; - font-family: $mainFont; - letter-spacing: $mainFontSpacing; - - @include DebugText(red); -} - -@mixin Text { - @include ScaleFont($textFontSize, $textLineHeight); - font-weight: $mainFontWeight; - font-family: $mainFont; - - letter-spacing: $mainFontSpacing; - - @include DebugText(blue); -} - -@mixin Heading { - @include ScaleFont($headingFontSize, $headingLineHeight); - font-weight: $mainFontWeight; - font-family: $mainFont; - letter-spacing: $mainFontSpacing; - - @include DebugText(yellow); -} - -@mixin SuperHeading { - @include ScaleFont($superHeadingFontSize, $superHeadingLineHeight); - font-weight: $mainFontWeight; - font-family: $mainFont; - letter-spacing: $mainFontSpacing; - - @include DebugText(orange); -} - -@mixin ButtonText { - @include ScaleFont($buttonFontSize, $buttonLineHeight); - font-weight: $mainFontWeight; - font-family: $mainFont; - letter-spacing: $mainFontSpacing; - @include DebugText(purple); -} - -@function str-split($string, $separator) { - // empty array/list - $split-arr: (); - // first index of separator in string - $index: str-index($string, $separator); - // loop through string - @while $index != null { - // get the substring from the first character to the separator - $item: str-slice($string, 1, $index - 1); - // push item to array - $split-arr: append($split-arr, $item); - // remove item and separator from string - $string: str-slice($string, $index + 1); - // find new index of separator - $index: str-index($string, $separator); - } - // add the remaining string to list (the last item) - $split-arr: append($split-arr, $string); - - @return $split-arr; -} - -@function _first-index($string, $direction: "left") { - @for $i from 1 through str-length($string) { - $index: if($direction == "left", $i, -$i); - - @if str-slice($string, $index, $index) != " " { - @return $index; - } - } - - @return 0; -} - -@function trim($string) { - @return str-slice($string, _first-index($string, "left"), _first-index($string, "right")); -} - -@mixin AppendGlobal($prefix) { - $strSelector: quote(&); - $selectors: str-split($strSelector, ","); - - $builtSelector: null; - - @if (& == null) { - $builtSelector: "html" + $prefix; - } @else { - $builtSelector: (); - // @debug ($strSelector, "->>>", $selectors); - @each $srcSelector in $selectors { - $srcSelector: trim($srcSelector); - // @debug ("___", $srcSelector); - $selector: "html" + $prefix + " " + $srcSelector; - @if str-index($srcSelector, "html.") { - $selector: "html" + - $prefix + - "." + - str-slice($srcSelector, str-index($srcSelector, "html.") + 5); - } - // @debug ("_______", $selector); - $builtSelector: append($builtSelector, $selector, comma); - } - } - - @at-root #{$builtSelector} { - @content; - } -} +$globalBorderRadius: 2px; + +// When to reduce control elements size for small devices +$layoutExpandMinWidth: 340px; + +// Font sizes and line heights +$superHeadingFontSize: 25px; +$superHeadingLineHeight: 24px; + +$breakTooltipShowStatsPx: 1023px; + +$headingFontSize: 19px; +$headingLineHeight: 21px; + +$textFontSize: 16px; +$textLineHeight: 21px; + +$plainTextFontSize: 13px; +$plainTextLineHeight: 17px; + +$supersmallTextFontSize: 10px; +$supersmallTextLineHeight: 13px; +$buttonFontSize: 14px; +$buttonLineHeight: 18px; + +// Main background color +$mainBgColor: #dee1ea; + +// Accent colors + +$accentColorBright: #e1e4ed; +$accentColorDark: #7d808a; +$colorGreenBright: #66bb6a; +$colorBlueBright: rgb(74, 151, 223); +$colorRedBright: #ef5072; +$themeColor: #393747; +$ingameHudBg: rgba(#333438, 0.9); + +$text3dColor: #f4ffff; + +$darkModeGameBackground: #535866; +$darkModeControlsBackground: darken($darkModeGameBackground, 5); + +// Dialog properties +$modalDialogBg: rgba(160, 165, 180, 0.8); +$dialogBgColor: lighten($mainBgColor, 10); + +$lightFontWeight: normal; +$boldFontWeight: 600; + +$iconSizeSmall: 30px; +$iconSizeMedium: 40px; +$iconSizeLarge: 60px; + +// Poppins 500 +// Rubik 400 +// Cairo 400 +// Viga 400 +// Sniglet 400 + +$mainFont: "GameFont", sans-serif; +// $mainFont: "DK Canoodle"; +// $mainFont: "MADE Florence Sans"; +$numberFont: $mainFont; +$textFont: $mainFont; + +$mainFontWeight: 400; +$mainFontSpacing: 0.04em; +$mainFontScale: 1; + +@mixin DebugText($color) { + // font-size: 3px; + // &, + // * { + // color: $color !important; + // } +} + +@mixin SuperSmallText { + @include ScaleFont($supersmallTextFontSize, $supersmallTextLineHeight); + font-weight: $mainFontWeight; + font-family: $mainFont; + letter-spacing: $mainFontSpacing; + @include DebugText(green); +} + +@mixin PlainText { + @include ScaleFont($plainTextFontSize, $plainTextLineHeight); + font-weight: $mainFontWeight; + font-family: $mainFont; + letter-spacing: $mainFontSpacing; + + @include DebugText(red); +} + +@mixin Text { + @include ScaleFont($textFontSize, $textLineHeight); + font-weight: $mainFontWeight; + font-family: $mainFont; + + letter-spacing: $mainFontSpacing; + + @include DebugText(blue); +} + +@mixin Heading { + @include ScaleFont($headingFontSize, $headingLineHeight); + font-weight: $mainFontWeight; + font-family: $mainFont; + letter-spacing: $mainFontSpacing; + + @include DebugText(yellow); +} + +@mixin SuperHeading { + @include ScaleFont($superHeadingFontSize, $superHeadingLineHeight); + font-weight: $mainFontWeight; + font-family: $mainFont; + letter-spacing: $mainFontSpacing; + + @include DebugText(orange); +} + +@mixin ButtonText { + @include ScaleFont($buttonFontSize, $buttonLineHeight); + font-weight: $mainFontWeight; + font-family: $mainFont; + letter-spacing: $mainFontSpacing; + @include DebugText(purple); +} + +@function str-split($string, $separator) { + // empty array/list + $split-arr: (); + // first index of separator in string + $index: str-index($string, $separator); + // loop through string + @while $index != null { + // get the substring from the first character to the separator + $item: str-slice($string, 1, $index - 1); + // push item to array + $split-arr: append($split-arr, $item); + // remove item and separator from string + $string: str-slice($string, $index + 1); + // find new index of separator + $index: str-index($string, $separator); + } + // add the remaining string to list (the last item) + $split-arr: append($split-arr, $string); + + @return $split-arr; +} + +@function _first-index($string, $direction: "left") { + @for $i from 1 through str-length($string) { + $index: if($direction == "left", $i, -$i); + + @if str-slice($string, $index, $index) != " " { + @return $index; + } + } + + @return 0; +} + +@function trim($string) { + @return str-slice($string, _first-index($string, "left"), _first-index($string, "right")); +} + +@mixin AppendGlobal($prefix) { + $strSelector: quote(&); + $selectors: str-split($strSelector, ","); + + $builtSelector: null; + + @if (& == null) { + $builtSelector: "html" + $prefix; + } @else { + $builtSelector: (); + // @debug ($strSelector, "->>>", $selectors); + @each $srcSelector in $selectors { + $srcSelector: trim($srcSelector); + // @debug ("___", $srcSelector); + $selector: "html" + $prefix + " " + $srcSelector; + @if str-index($srcSelector, "html.") { + $selector: "html" + + $prefix + + "." + + str-slice($srcSelector, str-index($srcSelector, "html.") + 5); + } + // @debug ("_______", $selector); + $builtSelector: append($builtSelector, $selector, comma); + } + } + + @at-root #{$builtSelector} { + @content; + } +} diff --git a/src/js/application.js b/src/js/application.js index e5e22b60..1a8ca21f 100644 --- a/src/js/application.js +++ b/src/js/application.js @@ -1,408 +1,411 @@ -import { AnimationFrame } from "./core/animation_frame"; -import { BackgroundResourcesLoader } from "./core/background_resources_loader"; -import { IS_MOBILE } from "./core/config"; -import { GameState } from "./core/game_state"; -import { GLOBAL_APP, setGlobalApp } from "./core/globals"; -import { InputDistributor } from "./core/input_distributor"; -import { Loader } from "./core/loader"; -import { createLogger, logSection } from "./core/logging"; -import { StateManager } from "./core/state_manager"; -import { TrackedState } from "./core/tracked_state"; -import { getPlatformName, waitNextFrame } from "./core/utils"; -import { Vector } from "./core/vector"; -import { AdProviderInterface } from "./platform/ad_provider"; -import { NoAdProvider } from "./platform/ad_providers/no_ad_provider"; -import { AnalyticsInterface } from "./platform/analytics"; -import { GoogleAnalyticsImpl } from "./platform/browser/google_analytics"; -import { SoundImplBrowser } from "./platform/browser/sound"; -import { PlatformWrapperImplBrowser } from "./platform/browser/wrapper"; -import { PlatformWrapperImplElectron } from "./platform/electron/wrapper"; -import { PlatformWrapperInterface } from "./platform/wrapper"; -import { ApplicationSettings } from "./profile/application_settings"; -import { SavegameManager } from "./savegame/savegame_manager"; -import { AboutState } from "./states/about"; -import { ChangelogState } from "./states/changelog"; -import { InGameState } from "./states/ingame"; -import { KeybindingsState } from "./states/keybindings"; -import { MainMenuState } from "./states/main_menu"; -import { MobileWarningState } from "./states/mobile_warning"; -import { PreloadState } from "./states/preload"; -import { SettingsState } from "./states/settings"; -import { ShapezGameAnalytics } from "./platform/browser/game_analytics"; - -/** - * @typedef {import("./platform/game_analytics").GameAnalyticsInterface} GameAnalyticsInterface - * @typedef {import("./platform/sound").SoundInterface} SoundInterface - * @typedef {import("./platform/storage").StorageInterface} StorageInterface - */ - -const logger = createLogger("application"); - -// Set the name of the hidden property and the change event for visibility -let pageHiddenPropName, pageVisibilityEventName; -if (typeof document.hidden !== "undefined") { - // Opera 12.10 and Firefox 18 and later support - pageHiddenPropName = "hidden"; - pageVisibilityEventName = "visibilitychange"; - // @ts-ignore -} else if (typeof document.msHidden !== "undefined") { - pageHiddenPropName = "msHidden"; - pageVisibilityEventName = "msvisibilitychange"; - // @ts-ignore -} else if (typeof document.webkitHidden !== "undefined") { - pageHiddenPropName = "webkitHidden"; - pageVisibilityEventName = "webkitvisibilitychange"; -} - -export class Application { - constructor() { - assert(!GLOBAL_APP, "Tried to construct application twice"); - logger.log("Creating application, platform =", getPlatformName()); - setGlobalApp(this); - - this.unloaded = false; - - // Global stuff - this.settings = new ApplicationSettings(this); - this.ticker = new AnimationFrame(); - this.stateMgr = new StateManager(this); - this.savegameMgr = new SavegameManager(this); - this.inputMgr = new InputDistributor(this); - this.backgroundResourceLoader = new BackgroundResourcesLoader(this); - - // Platform dependent stuff - - /** @type {StorageInterface} */ - this.storage = null; - - /** @type {SoundInterface} */ - this.sound = null; - - /** @type {PlatformWrapperInterface} */ - this.platformWrapper = null; - - /** @type {AdProviderInterface} */ - this.adProvider = null; - - /** @type {AnalyticsInterface} */ - this.analytics = null; - - /** @type {GameAnalyticsInterface} */ - this.gameAnalytics = null; - - this.initPlatformDependentInstances(); - - // Track if the window is focused (only relevant for browser) - this.focused = true; - - // Track if the window is visible - this.pageVisible = true; - - // Track if the app is paused (cordova) - this.applicationPaused = false; - - /** @type {TypedTrackedState} */ - this.trackedIsRenderable = new TrackedState(this.onAppRenderableStateChanged, this); - - // Dimensions - this.screenWidth = 0; - this.screenHeight = 0; - - // Store the timestamp where we last checked for a screen resize, since orientationchange is unreliable with cordova - this.lastResizeCheck = null; - - // Store the mouse position, or null if not available - /** @type {Vector|null} */ - this.mousePosition = null; - } - - /** - * Initializes all platform instances - */ - initPlatformDependentInstances() { - logger.log("Creating platform dependent instances (standalone=", G_IS_STANDALONE, ")"); - - if (G_IS_STANDALONE) { - this.platformWrapper = new PlatformWrapperImplElectron(this); - } else { - this.platformWrapper = new PlatformWrapperImplBrowser(this); - } - - // Start with empty ad provider - this.adProvider = new NoAdProvider(this); - this.sound = new SoundImplBrowser(this); - this.analytics = new GoogleAnalyticsImpl(this); - this.gameAnalytics = new ShapezGameAnalytics(this); - } - - /** - * Registers all game states - */ - registerStates() { - /** @type {Array} */ - const states = [ - PreloadState, - MobileWarningState, - MainMenuState, - InGameState, - SettingsState, - KeybindingsState, - AboutState, - ChangelogState, - ]; - - for (let i = 0; i < states.length; ++i) { - this.stateMgr.register(states[i]); - } - } - - /** - * Registers all event listeners - */ - registerEventListeners() { - window.addEventListener("focus", this.onFocus.bind(this)); - window.addEventListener("blur", this.onBlur.bind(this)); - - window.addEventListener("resize", () => this.checkResize(), true); - window.addEventListener("orientationchange", () => this.checkResize(), true); - - if (!G_IS_MOBILE_APP && !IS_MOBILE) { - window.addEventListener("mousemove", this.handleMousemove.bind(this)); - } - - // Unload events - window.addEventListener("beforeunload", this.onBeforeUnload.bind(this), true); - window.addEventListener("unload", this.onUnload.bind(this), true); - - document.addEventListener(pageVisibilityEventName, this.handleVisibilityChange.bind(this), false); - - // Track touches so we can update the focus appropriately - document.addEventListener("touchstart", this.updateFocusAfterUserInteraction.bind(this), true); - document.addEventListener("touchend", this.updateFocusAfterUserInteraction.bind(this), true); - } - - /** - * Checks the focus after a touch - * @param {TouchEvent} event - */ - updateFocusAfterUserInteraction(event) { - const target = /** @type {HTMLElement} */ (event.target); - if (!target || !target.tagName) { - // Safety check - logger.warn("Invalid touchstart/touchend event:", event); - return; - } - - // When clicking an element which is not the currently focused one, defocus it - if (target !== document.activeElement) { - // @ts-ignore - if (document.activeElement.blur) { - // @ts-ignore - document.activeElement.blur(); - } - } - - // If we click an input field, focus it now - if (target.tagName.toLowerCase() === "input") { - // We *really* need the focus - waitNextFrame().then(() => target.focus()); - } - } - - /** - * Handles a page visibility change event - * @param {Event} event - */ - handleVisibilityChange(event) { - window.focus(); - const pageVisible = !document[pageHiddenPropName]; - if (pageVisible !== this.pageVisible) { - this.pageVisible = pageVisible; - logger.log("Visibility changed:", this.pageVisible); - this.trackedIsRenderable.set(this.isRenderable()); - } - } - - /** - * Handles a mouse move event - * @param {MouseEvent} event - */ - handleMousemove(event) { - this.mousePosition = new Vector(event.clientX, event.clientY); - } - - /** - * Internal on focus handler - */ - onFocus() { - this.focused = true; - } - - /** - * Internal blur handler - */ - onBlur() { - this.focused = false; - } - - /** - * Returns if the app is currently visible - */ - isRenderable() { - return !this.applicationPaused && this.pageVisible; - } - - onAppRenderableStateChanged(renderable) { - logger.log("Application renderable:", renderable); - window.focus(); - const currentState = this.stateMgr.getCurrentState(); - if (!renderable) { - if (currentState) { - currentState.onAppPause(); - } - } else { - if (currentState) { - currentState.onAppResume(); - } - this.checkResize(); - } - - this.sound.onPageRenderableStateChanged(renderable); - } - - /** - * Internal unload handler - */ - onUnload(event) { - if (!this.unloaded) { - logSection("UNLOAD HANDLER", "#f77"); - this.unloaded = true; - const currentState = this.stateMgr.getCurrentState(); - if (currentState) { - currentState.onBeforeExit(); - } - this.deinitialize(); - } - } - - /** - * Internal before-unload handler - */ - onBeforeUnload(event) { - logSection("BEFORE UNLOAD HANDLER", "#f77"); - const currentState = this.stateMgr.getCurrentState(); - - if (!G_IS_DEV && currentState && currentState.getHasUnloadConfirmation()) { - if (!G_IS_STANDALONE) { - // Need to show a "Are you sure you want to exit" - event.preventDefault(); - event.returnValue = "Are you sure you want to exit?"; - } - } - } - - /** - * Boots the application - */ - boot() { - console.log("Booting ..."); - this.registerStates(); - this.registerEventListeners(); - - Loader.linkAppAfterBoot(this); - - // Check for mobile - if (IS_MOBILE) { - this.stateMgr.moveToState("MobileWarningState"); - } else { - this.stateMgr.moveToState("PreloadState"); - } - - // Starting rendering - this.ticker.frameEmitted.add(this.onFrameEmitted, this); - this.ticker.bgFrameEmitted.add(this.onBackgroundFrame, this); - this.ticker.start(); - - window.focus(); - } - - /** - * Deinitializes the application - */ - deinitialize() { - return this.sound.deinitialize(); - } - - /** - * Background frame update callback - * @param {number} dt - */ - onBackgroundFrame(dt) { - if (this.isRenderable()) { - return; - } - - const currentState = this.stateMgr.getCurrentState(); - if (currentState) { - currentState.onBackgroundTick(dt); - } - } - - /** - * Frame update callback - * @param {number} dt - */ - onFrameEmitted(dt) { - if (!this.isRenderable()) { - return; - } - - const time = performance.now(); - - // Periodically check for resizes, this is expensive (takes 2-3ms so only do it once in a while!) - if (!this.lastResizeCheck || time - this.lastResizeCheck > 1000) { - this.checkResize(); - this.lastResizeCheck = time; - } - - const currentState = this.stateMgr.getCurrentState(); - if (currentState) { - currentState.onRender(dt); - } - } - - /** - * Checks if the app resized. Only does this once in a while - * @param {boolean} forceUpdate Forced update of the dimensions - */ - checkResize(forceUpdate = false) { - const w = window.innerWidth; - const h = window.innerHeight; - if (this.screenWidth !== w || this.screenHeight !== h || forceUpdate) { - this.screenWidth = w; - this.screenHeight = h; - const currentState = this.stateMgr.getCurrentState(); - if (currentState) { - currentState.onResized(this.screenWidth, this.screenHeight); - } - - const scale = this.getEffectiveUiScale(); - waitNextFrame().then(() => document.documentElement.style.setProperty("--ui-scale", `${scale}`)); - window.focus(); - } - } - - /** - * Returns the effective ui sclae - */ - getEffectiveUiScale() { - return this.platformWrapper.getUiScale() * this.settings.getInterfaceScaleValue(); - } - - /** - * Callback after ui scale has changed - */ - updateAfterUiScaleChanged() { - this.checkResize(true); - } -} +import { AnimationFrame } from "./core/animation_frame"; +import { BackgroundResourcesLoader } from "./core/background_resources_loader"; +import { IS_MOBILE } from "./core/config"; +import { GameState } from "./core/game_state"; +import { GLOBAL_APP, setGlobalApp } from "./core/globals"; +import { InputDistributor } from "./core/input_distributor"; +import { Loader } from "./core/loader"; +import { createLogger, logSection } from "./core/logging"; +import { StateManager } from "./core/state_manager"; +import { TrackedState } from "./core/tracked_state"; +import { getPlatformName, waitNextFrame } from "./core/utils"; +import { Vector } from "./core/vector"; +import { AdProviderInterface } from "./platform/ad_provider"; +import { NoAdProvider } from "./platform/ad_providers/no_ad_provider"; +import { AnalyticsInterface } from "./platform/analytics"; +import { GoogleAnalyticsImpl } from "./platform/browser/google_analytics"; +import { SoundImplBrowser } from "./platform/browser/sound"; +import { PlatformWrapperImplBrowser } from "./platform/browser/wrapper"; +import { PlatformWrapperImplElectron } from "./platform/electron/wrapper"; +import { PlatformWrapperInterface } from "./platform/wrapper"; +import { ApplicationSettings } from "./profile/application_settings"; +import { SavegameManager } from "./savegame/savegame_manager"; +import { AboutState } from "./states/about"; +import { ChangelogState } from "./states/changelog"; +import { InGameState } from "./states/ingame"; +import { KeybindingsState } from "./states/keybindings"; +import { MainMenuState } from "./states/main_menu"; +import { MobileWarningState } from "./states/mobile_warning"; +import { PreloadState } from "./states/preload"; +import { SettingsState } from "./states/settings"; +import { ShapezGameAnalytics } from "./platform/browser/game_analytics"; + +/** + * @typedef {import("./platform/game_analytics").GameAnalyticsInterface} GameAnalyticsInterface + * @typedef {import("./platform/sound").SoundInterface} SoundInterface + * @typedef {import("./platform/storage").StorageInterface} StorageInterface + */ + +const logger = createLogger("application"); + +// Set the name of the hidden property and the change event for visibility +let pageHiddenPropName, pageVisibilityEventName; +if (typeof document.hidden !== "undefined") { + // Opera 12.10 and Firefox 18 and later support + pageHiddenPropName = "hidden"; + pageVisibilityEventName = "visibilitychange"; + // @ts-ignore +} else if (typeof document.msHidden !== "undefined") { + pageHiddenPropName = "msHidden"; + pageVisibilityEventName = "msvisibilitychange"; + // @ts-ignore +} else if (typeof document.webkitHidden !== "undefined") { + pageHiddenPropName = "webkitHidden"; + pageVisibilityEventName = "webkitvisibilitychange"; +} + +export class Application { + constructor() { + assert(!GLOBAL_APP, "Tried to construct application twice"); + logger.log("Creating application, platform =", getPlatformName()); + setGlobalApp(this); + + this.unloaded = false; + + // Global stuff + this.settings = new ApplicationSettings(this); + this.ticker = new AnimationFrame(); + this.stateMgr = new StateManager(this); + this.savegameMgr = new SavegameManager(this); + this.inputMgr = new InputDistributor(this); + this.backgroundResourceLoader = new BackgroundResourcesLoader(this); + + // Platform dependent stuff + + /** @type {StorageInterface} */ + this.storage = null; + + /** @type {SoundInterface} */ + this.sound = null; + + /** @type {PlatformWrapperInterface} */ + this.platformWrapper = null; + + /** @type {AdProviderInterface} */ + this.adProvider = null; + + /** @type {AnalyticsInterface} */ + this.analytics = null; + + /** @type {GameAnalyticsInterface} */ + this.gameAnalytics = null; + + this.initPlatformDependentInstances(); + + // Track if the window is focused (only relevant for browser) + this.focused = true; + + // Track if the window is visible + this.pageVisible = true; + + // Track if the app is paused (cordova) + this.applicationPaused = false; + + /** @type {TypedTrackedState} */ + this.trackedIsRenderable = new TrackedState(this.onAppRenderableStateChanged, this); + + // Dimensions + this.screenWidth = 0; + this.screenHeight = 0; + + // Store the timestamp where we last checked for a screen resize, since orientationchange is unreliable with cordova + this.lastResizeCheck = null; + + // Store the mouse position, or null if not available + /** @type {Vector|null} */ + this.mousePosition = null; + } + + /** + * Initializes all platform instances + */ + initPlatformDependentInstances() { + logger.log("Creating platform dependent instances (standalone=", G_IS_STANDALONE, ")"); + + if (G_IS_STANDALONE) { + this.platformWrapper = new PlatformWrapperImplElectron(this); + } else { + this.platformWrapper = new PlatformWrapperImplBrowser(this); + } + + // Start with empty ad provider + this.adProvider = new NoAdProvider(this); + this.sound = new SoundImplBrowser(this); + this.analytics = new GoogleAnalyticsImpl(this); + this.gameAnalytics = new ShapezGameAnalytics(this); + } + + /** + * Registers all game states + */ + registerStates() { + /** @type {Array} */ + const states = [ + PreloadState, + MobileWarningState, + MainMenuState, + InGameState, + SettingsState, + KeybindingsState, + AboutState, + ChangelogState, + ]; + + for (let i = 0; i < states.length; ++i) { + this.stateMgr.register(states[i]); + } + } + + /** + * Registers all event listeners + */ + registerEventListeners() { + window.addEventListener("focus", this.onFocus.bind(this)); + window.addEventListener("blur", this.onBlur.bind(this)); + + window.addEventListener("resize", () => this.checkResize(), true); + window.addEventListener("orientationchange", () => this.checkResize(), true); + + if (!G_IS_MOBILE_APP && !IS_MOBILE) { + window.addEventListener("mousemove", this.handleMousemove.bind(this)); + window.addEventListener("mouseout", this.handleMousemove.bind(this)); + window.addEventListener("mouseover", this.handleMousemove.bind(this)); + window.addEventListener("mouseleave", this.handleMousemove.bind(this)); + } + + // Unload events + window.addEventListener("beforeunload", this.onBeforeUnload.bind(this), true); + window.addEventListener("unload", this.onUnload.bind(this), true); + + document.addEventListener(pageVisibilityEventName, this.handleVisibilityChange.bind(this), false); + + // Track touches so we can update the focus appropriately + document.addEventListener("touchstart", this.updateFocusAfterUserInteraction.bind(this), true); + document.addEventListener("touchend", this.updateFocusAfterUserInteraction.bind(this), true); + } + + /** + * Checks the focus after a touch + * @param {TouchEvent} event + */ + updateFocusAfterUserInteraction(event) { + const target = /** @type {HTMLElement} */ (event.target); + if (!target || !target.tagName) { + // Safety check + logger.warn("Invalid touchstart/touchend event:", event); + return; + } + + // When clicking an element which is not the currently focused one, defocus it + if (target !== document.activeElement) { + // @ts-ignore + if (document.activeElement.blur) { + // @ts-ignore + document.activeElement.blur(); + } + } + + // If we click an input field, focus it now + if (target.tagName.toLowerCase() === "input") { + // We *really* need the focus + waitNextFrame().then(() => target.focus()); + } + } + + /** + * Handles a page visibility change event + * @param {Event} event + */ + handleVisibilityChange(event) { + window.focus(); + const pageVisible = !document[pageHiddenPropName]; + if (pageVisible !== this.pageVisible) { + this.pageVisible = pageVisible; + logger.log("Visibility changed:", this.pageVisible); + this.trackedIsRenderable.set(this.isRenderable()); + } + } + + /** + * Handles a mouse move event + * @param {MouseEvent} event + */ + handleMousemove(event) { + this.mousePosition = new Vector(event.clientX, event.clientY); + } + + /** + * Internal on focus handler + */ + onFocus() { + this.focused = true; + } + + /** + * Internal blur handler + */ + onBlur() { + this.focused = false; + } + + /** + * Returns if the app is currently visible + */ + isRenderable() { + return !this.applicationPaused && this.pageVisible; + } + + onAppRenderableStateChanged(renderable) { + logger.log("Application renderable:", renderable); + window.focus(); + const currentState = this.stateMgr.getCurrentState(); + if (!renderable) { + if (currentState) { + currentState.onAppPause(); + } + } else { + if (currentState) { + currentState.onAppResume(); + } + this.checkResize(); + } + + this.sound.onPageRenderableStateChanged(renderable); + } + + /** + * Internal unload handler + */ + onUnload(event) { + if (!this.unloaded) { + logSection("UNLOAD HANDLER", "#f77"); + this.unloaded = true; + const currentState = this.stateMgr.getCurrentState(); + if (currentState) { + currentState.onBeforeExit(); + } + this.deinitialize(); + } + } + + /** + * Internal before-unload handler + */ + onBeforeUnload(event) { + logSection("BEFORE UNLOAD HANDLER", "#f77"); + const currentState = this.stateMgr.getCurrentState(); + + if (!G_IS_DEV && currentState && currentState.getHasUnloadConfirmation()) { + if (!G_IS_STANDALONE) { + // Need to show a "Are you sure you want to exit" + event.preventDefault(); + event.returnValue = "Are you sure you want to exit?"; + } + } + } + + /** + * Boots the application + */ + boot() { + console.log("Booting ..."); + this.registerStates(); + this.registerEventListeners(); + + Loader.linkAppAfterBoot(this); + + // Check for mobile + if (IS_MOBILE) { + this.stateMgr.moveToState("MobileWarningState"); + } else { + this.stateMgr.moveToState("PreloadState"); + } + + // Starting rendering + this.ticker.frameEmitted.add(this.onFrameEmitted, this); + this.ticker.bgFrameEmitted.add(this.onBackgroundFrame, this); + this.ticker.start(); + + window.focus(); + } + + /** + * Deinitializes the application + */ + deinitialize() { + return this.sound.deinitialize(); + } + + /** + * Background frame update callback + * @param {number} dt + */ + onBackgroundFrame(dt) { + if (this.isRenderable()) { + return; + } + + const currentState = this.stateMgr.getCurrentState(); + if (currentState) { + currentState.onBackgroundTick(dt); + } + } + + /** + * Frame update callback + * @param {number} dt + */ + onFrameEmitted(dt) { + if (!this.isRenderable()) { + return; + } + + const time = performance.now(); + + // Periodically check for resizes, this is expensive (takes 2-3ms so only do it once in a while!) + if (!this.lastResizeCheck || time - this.lastResizeCheck > 1000) { + this.checkResize(); + this.lastResizeCheck = time; + } + + const currentState = this.stateMgr.getCurrentState(); + if (currentState) { + currentState.onRender(dt); + } + } + + /** + * Checks if the app resized. Only does this once in a while + * @param {boolean} forceUpdate Forced update of the dimensions + */ + checkResize(forceUpdate = false) { + const w = window.innerWidth; + const h = window.innerHeight; + if (this.screenWidth !== w || this.screenHeight !== h || forceUpdate) { + this.screenWidth = w; + this.screenHeight = h; + const currentState = this.stateMgr.getCurrentState(); + if (currentState) { + currentState.onResized(this.screenWidth, this.screenHeight); + } + + const scale = this.getEffectiveUiScale(); + waitNextFrame().then(() => document.documentElement.style.setProperty("--ui-scale", `${scale}`)); + window.focus(); + } + } + + /** + * Returns the effective ui sclae + */ + getEffectiveUiScale() { + return this.platformWrapper.getUiScale() * this.settings.getInterfaceScaleValue(); + } + + /** + * Callback after ui scale has changed + */ + updateAfterUiScaleChanged() { + this.checkResize(true); + } +} diff --git a/src/js/changelog.js b/src/js/changelog.js index bbc4a4fa..6f38ae81 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -24,13 +24,17 @@ export const CHANGELOG = [ "Mark pinned shapes in statistics dialog and show them first (inspired by davidburhans)", "Added setting to show chunk borders", "Quad painters have been reworked! They now are integrated with the wires, and only paint the shape when the value is 1 (inspired by dengr1605)", - "There are now compact 1x1 splitters available to be unlocked!", + "There are now compact 1x1 balancers available to be unlocked!", "Replaced level completion sound to be less distracting", "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 belt planner placing the belt when a dialog opens in the meantime", + "Added confirmation when deleting a savegame", "Fixed tunnels entrances connecting to exits sometimes when they shouldn't", + "You can now pan the map with your mouse by moving the cursor to the edges of the screen!", "Added setting to auto select the extractor when pipetting a resource patch (by Exund)", + "You can now change the unit (seconds / minutes / hours) in the statistics dialog", "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)", ], @@ -109,7 +113,7 @@ export const CHANGELOG = [ 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 continue button to main menu and add separate 'New game' button (by jaysc)", "Added setting to disable smart tunnel placement introduced with the last update", "Added setting to disable vignette", "Update translations", diff --git a/src/js/core/buffer_maintainer.js b/src/js/core/buffer_maintainer.js index 3d466f14..1d506803 100644 --- a/src/js/core/buffer_maintainer.js +++ b/src/js/core/buffer_maintainer.js @@ -13,7 +13,7 @@ import { round1Digit } from "./utils"; const logger = createLogger("buffers"); -const bufferGcDurationSeconds = 5; +const bufferGcDurationSeconds = 0.5; export class BufferMaintainer { /** @@ -86,27 +86,29 @@ export class BufferMaintainer { // Make sure our backlog never gets too big clearBufferBacklog(); - const bufferStats = getBufferStats(); - const mbUsed = round1Digit(bufferStats.vramUsage / (1024 * 1024)); - logger.log( - "GC: Remove", - (deletedKeys + "").padStart(4), - ", Remain", - (totalKeys + "").padStart(4), - "(", - (bufferStats.bufferCount + "").padStart(4), - "total", - ")", + // if (G_IS_DEV) { + // const bufferStats = getBufferStats(); + // const mbUsed = round1Digit(bufferStats.vramUsage / (1024 * 1024)); + // logger.log( + // "GC: Remove", + // (deletedKeys + "").padStart(4), + // ", Remain", + // (totalKeys + "").padStart(4), + // "(", + // (bufferStats.bufferCount + "").padStart(4), + // "total", + // ")", - "(", - (bufferStats.backlog + "").padStart(4), - "backlog", - ")", + // "(", + // (bufferStats.backlogSize + "").padStart(4), + // "backlog", + // ")", - "VRAM:", - mbUsed, - "MB" - ); + // "VRAM:", + // mbUsed, + // "MB" + // ); + // } ++this.iterationIndex; } diff --git a/src/js/core/buffer_utils.js b/src/js/core/buffer_utils.js index 228560bc..310c315f 100644 --- a/src/js/core/buffer_utils.js +++ b/src/js/core/buffer_utils.js @@ -25,17 +25,43 @@ export function disableImageSmoothing(context) { context.webkitImageSmoothingEnabled = false; } -const registeredCanvas = []; -const freeCanvasList = []; +/** + * @typedef {{ + * canvas: HTMLCanvasElement, + * context: CanvasRenderingContext2D + * }} CanvasCacheEntry + */ -let vramUsage = 0; -let bufferCount = 0; +/** + * @type {Array} + */ +const registeredCanvas = []; + +/** + * Buckets for each width * height combination + * @type {Map>} + */ +const freeCanvasBuckets = new Map(); + +/** + * Track statistics + */ +const stats = { + vramUsage: 0, + backlogVramUsage: 0, + bufferCount: 0, + numReused: 0, + numCreated: 0, +}; /** * * @param {HTMLCanvasElement} canvas */ export function getBufferVramUsageBytes(canvas) { + assert(canvas, "no canvas given"); + assert(Number.isFinite(canvas.width), "bad canvas width: " + canvas.width); + assert(Number.isFinite(canvas.height), "bad canvas height" + canvas.height); return canvas.width * canvas.height * 4; } @@ -43,17 +69,31 @@ export function getBufferVramUsageBytes(canvas) { * Returns stats on the allocated buffers */ export function getBufferStats() { + let numBuffersFree = 0; + freeCanvasBuckets.forEach(bucket => { + numBuffersFree += bucket.length; + }); + return { - vramUsage, - bufferCount, - backlog: freeCanvasList.length, + ...stats, + backlogKeys: freeCanvasBuckets.size, + backlogSize: numBuffersFree, }; } +/** + * Clears the backlog buffers if they grew too much + */ export function clearBufferBacklog() { - while (freeCanvasList.length > 50) { - freeCanvasList.pop(); - } + freeCanvasBuckets.forEach(bucket => { + while (bucket.length > 500) { + const entry = bucket[bucket.length - 1]; + stats.backlogVramUsage -= getBufferVramUsageBytes(entry.canvas); + delete entry.canvas; + delete entry.context; + bucket.pop(); + } + }); } /** @@ -84,53 +124,29 @@ export function makeOffscreenBuffer(w, h, { smooth = true, reusable = true, labe let canvas = null; let context = null; - let bestMatchingOne = null; - let bestMatchingPixelsDiff = 1e50; - - const currentPixels = w * h; - // Ok, search in cache first - for (let i = 0; i < freeCanvasList.length; ++i) { - const { canvas: useableCanvas, context: useableContext } = freeCanvasList[i]; + const bucket = freeCanvasBuckets.get(w * h) || []; + + for (let i = 0; i < bucket.length; ++i) { + const { canvas: useableCanvas, context: useableContext } = bucket[i]; if (useableCanvas.width === w && useableCanvas.height === h) { // Ok we found one canvas = useableCanvas; context = useableContext; - fastArrayDelete(freeCanvasList, i); + // Restore past state + context.restore(); + context.save(); + context.clearRect(0, 0, canvas.width, canvas.height); + + delete canvas.style.width; + delete canvas.style.height; + + stats.numReused++; + stats.backlogVramUsage -= getBufferVramUsageBytes(canvas); + fastArrayDelete(bucket, i); break; } - - const otherPixels = useableCanvas.width * useableCanvas.height; - const diff = Math.abs(otherPixels - currentPixels); - if (diff < bestMatchingPixelsDiff) { - bestMatchingPixelsDiff = diff; - bestMatchingOne = { - canvas: useableCanvas, - context: useableContext, - index: i, - }; - } - } - - // Ok none matching, reuse one though - if (!canvas && bestMatchingOne) { - canvas = bestMatchingOne.canvas; - context = bestMatchingOne.context; - canvas.width = w; - canvas.height = h; - fastArrayDelete(freeCanvasList, bestMatchingOne.index); - } - - // Reset context - if (context) { - // Restore past state - context.restore(); - context.save(); - context.clearRect(0, 0, canvas.width, canvas.height); - - delete canvas.style.width; - delete canvas.style.height; } // None found , create new one @@ -138,6 +154,8 @@ export function makeOffscreenBuffer(w, h, { smooth = true, reusable = true, labe canvas = document.createElement("canvas"); context = canvas.getContext("2d" /*, { alpha } */); + stats.numCreated++; + canvas.width = w; canvas.height = h; @@ -145,6 +163,7 @@ export function makeOffscreenBuffer(w, h, { smooth = true, reusable = true, labe context.save(); } + // @ts-ignore canvas.label = label; if (smooth) { @@ -167,8 +186,9 @@ export function makeOffscreenBuffer(w, h, { smooth = true, reusable = true, labe export function registerCanvas(canvas, context) { registeredCanvas.push({ canvas, context }); - bufferCount += 1; - vramUsage += getBufferVramUsageBytes(canvas); + stats.bufferCount += 1; + const bytesUsed = getBufferVramUsageBytes(canvas); + stats.vramUsage += bytesUsed; } /** @@ -180,6 +200,7 @@ export function freeCanvas(canvas) { let index = -1; let data = null; + for (let i = 0; i < registeredCanvas.length; ++i) { if (registeredCanvas[i].canvas === canvas) { index = i; @@ -193,8 +214,18 @@ export function freeCanvas(canvas) { return; } fastArrayDelete(registeredCanvas, index); - freeCanvasList.push(data); - bufferCount -= 1; - vramUsage -= getBufferVramUsageBytes(canvas); + const key = canvas.width * canvas.height; + const bucket = freeCanvasBuckets.get(key); + if (bucket) { + bucket.push(data); + } else { + freeCanvasBuckets.set(key, [data]); + } + + stats.bufferCount -= 1; + + const bytesUsed = getBufferVramUsageBytes(canvas); + stats.vramUsage -= bytesUsed; + stats.backlogVramUsage += bytesUsed; } diff --git a/src/js/core/config.js b/src/js/core/config.js index a6d6ed63..b15fb672 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -68,7 +68,7 @@ export const globalConfig = { cutterQuad: 1 / 4, rotater: 1 / 1, rotaterCCW: 1 / 1, - rotaterFL: 1 / 1, + rotater180: 1 / 1, painter: 1 / 6, painterDouble: 1 / 8, painterQuad: 1 / 8, @@ -84,8 +84,8 @@ export const globalConfig = { // Global game speed gameSpeed: 1, - warmupTimeSecondsFast: 0.1, - warmupTimeSecondsRegular: 1, + warmupTimeSecondsFast: 0.5, + warmupTimeSecondsRegular: 3, smoothing: { smoothMainCanvas: smoothCanvas && true, @@ -132,5 +132,5 @@ if (G_IS_DEV && globalConfig.debug.renderForTrailer) { } if (globalConfig.debug.fastGameEnter) { - globalConfig.debug.noArtificalDelays = true; + globalConfig.debug.noArtificialDelays = true; } diff --git a/src/js/core/dpi_manager.js b/src/js/core/dpi_manager.js index b944d155..4fb792c0 100644 --- a/src/js/core/dpi_manager.js +++ b/src/js/core/dpi_manager.js @@ -15,14 +15,16 @@ export function getDeviceDPI() { * @returns {number} Smoothed dpi */ export function smoothenDpi(dpi) { - if (dpi < 0.02) { - return 0.02; - } else if (dpi < 0.1) { - return round2Digits(dpi); + if (dpi < 0.05) { + return 0.05; + } else if (dpi < 0.2) { + return round2Digits(Math.round(dpi / 0.04) * 0.04); } else if (dpi < 1) { - return round1Digit(dpi); - } else { + return round1Digit(Math.round(dpi / 0.1) * 0.1); + } else if (dpi < 4) { return round1Digit(Math.round(dpi / 0.5) * 0.5); + } else { + return 4; } } diff --git a/src/js/core/loader.js b/src/js/core/loader.js index d7f544e3..cadbc048 100644 --- a/src/js/core/loader.js +++ b/src/js/core/loader.js @@ -1,230 +1,230 @@ -import { makeOffscreenBuffer } from "./buffer_utils"; -import { AtlasSprite, BaseSprite, RegularSprite, SpriteAtlasLink } from "./sprites"; -import { cachebust } from "./cachebust"; -import { createLogger } from "./logging"; - -/** - * @typedef {import("../application").Application} Application - * @typedef {import("./atlas_definitions").AtlasDefinition} AtlasDefinition; - */ - -const logger = createLogger("loader"); - -const missingSpriteIds = {}; - -class LoaderImpl { - constructor() { - this.app = null; - - /** @type {Map} */ - this.sprites = new Map(); - - this.rawImages = []; - } - - /** - * @param {Application} app - */ - linkAppAfterBoot(app) { - this.app = app; - this.makeSpriteNotFoundCanvas(); - } - - /** - * Fetches a given sprite from the cache - * @param {string} key - * @returns {BaseSprite} - */ - getSpriteInternal(key) { - const sprite = this.sprites.get(key); - if (!sprite) { - if (!missingSpriteIds[key]) { - // Only show error once - missingSpriteIds[key] = true; - logger.error("Sprite '" + key + "' not found!"); - } - return this.spriteNotFoundSprite; - } - return sprite; - } - - /** - * Returns an atlas sprite from the cache - * @param {string} key - * @returns {AtlasSprite} - */ - getSprite(key) { - const sprite = this.getSpriteInternal(key); - assert(sprite instanceof AtlasSprite || sprite === this.spriteNotFoundSprite, "Not an atlas sprite"); - return /** @type {AtlasSprite} */ (sprite); - } - - /** - * Returns a regular sprite from the cache - * @param {string} key - * @returns {RegularSprite} - */ - getRegularSprite(key) { - const sprite = this.getSpriteInternal(key); - assert( - sprite instanceof RegularSprite || sprite === this.spriteNotFoundSprite, - "Not a regular sprite" - ); - return /** @type {RegularSprite} */ (sprite); - } - - /** - * - * @param {string} key - * @returns {Promise} - */ - internalPreloadImage(key) { - const url = cachebust("res/" + key); - const image = new Image(); - - let triesSoFar = 0; - - return Promise.race([ - new Promise((resolve, reject) => { - setTimeout(reject, G_IS_DEV ? 500 : 10000); - }), - - new Promise(resolve => { - image.onload = () => { - image.onerror = null; - image.onload = null; - - if (typeof image.decode === "function") { - // SAFARI: Image.decode() fails on safari with svgs -> we dont want to fail - // on that - // FIREFOX: Decode never returns if the image is in cache, so call it in background - image.decode().then( - () => null, - () => null - ); - } - resolve(image); - }; - - image.onerror = reason => { - logger.warn("Failed to load '" + url + "':", reason); - if (++triesSoFar < 4) { - logger.log("Retrying to load image from", url); - image.src = url + "?try=" + triesSoFar; - } else { - logger.warn("Failed to load", url, "after", triesSoFar, "tries with reason", reason); - image.onerror = null; - image.onload = null; - resolve(null); - } - }; - - image.src = url; - }), - ]); - } - - /** - * Preloads a sprite - * @param {string} key - * @returns {Promise} - */ - preloadCSSSprite(key) { - return this.internalPreloadImage(key).then(image => { - if (key.indexOf("game_misc") >= 0) { - // Allow access to regular sprites - this.sprites.set(key, new RegularSprite(image, image.width, image.height)); - } - this.rawImages.push(image); - }); - } - - /** - * Preloads an atlas - * @param {AtlasDefinition} atlas - * @returns {Promise} - */ - preloadAtlas(atlas) { - return this.internalPreloadImage(atlas.getFullSourcePath()).then(image => { - // @ts-ignore - image.label = atlas.sourceFileName; - return this.internalParseAtlas(atlas, image); - }); - } - - /** - * - * @param {AtlasDefinition} atlas - * @param {HTMLImageElement} loadedImage - */ - internalParseAtlas({ meta: { scale }, sourceData }, loadedImage) { - this.rawImages.push(loadedImage); - - for (const spriteName in sourceData) { - const { frame, sourceSize, spriteSourceSize } = sourceData[spriteName]; - - let sprite = /** @type {AtlasSprite} */ (this.sprites.get(spriteName)); - - if (!sprite) { - sprite = new AtlasSprite(spriteName); - this.sprites.set(spriteName, sprite); - } - - const link = new SpriteAtlasLink({ - packedX: frame.x, - packedY: frame.y, - packedW: frame.w, - packedH: frame.h, - packOffsetX: spriteSourceSize.x, - packOffsetY: spriteSourceSize.y, - atlas: loadedImage, - w: sourceSize.w, - h: sourceSize.h, - }); - sprite.linksByResolution[scale] = link; - } - } - - /** - * Makes the canvas which shows the question mark, shown when a sprite was not found - */ - makeSpriteNotFoundCanvas() { - const dims = 128; - - const [canvas, context] = makeOffscreenBuffer(dims, dims, { - smooth: false, - label: "not-found-sprite", - }); - context.fillStyle = "#f77"; - context.fillRect(0, 0, dims, dims); - - context.textAlign = "center"; - context.textBaseline = "middle"; - context.fillStyle = "#eee"; - context.font = "25px Arial"; - context.fillText("???", dims / 2, dims / 2); - - // TODO: Not sure why this is set here - // @ts-ignore - canvas.src = "not-found"; - - const sprite = new AtlasSprite("not-found"); - ["0.1", "0.25", "0.5", "0.75", "1"].forEach(resolution => { - sprite.linksByResolution[resolution] = new SpriteAtlasLink({ - packedX: 0, - packedY: 0, - w: dims, - h: dims, - packOffsetX: 0, - packOffsetY: 0, - packedW: dims, - packedH: dims, - atlas: canvas, - }); - }); - - this.spriteNotFoundSprite = sprite; - } -} - -export const Loader = new LoaderImpl(); +import { makeOffscreenBuffer } from "./buffer_utils"; +import { AtlasSprite, BaseSprite, RegularSprite, SpriteAtlasLink } from "./sprites"; +import { cachebust } from "./cachebust"; +import { createLogger } from "./logging"; + +/** + * @typedef {import("../application").Application} Application + * @typedef {import("./atlas_definitions").AtlasDefinition} AtlasDefinition; + */ + +const logger = createLogger("loader"); + +const missingSpriteIds = {}; + +class LoaderImpl { + constructor() { + this.app = null; + + /** @type {Map} */ + this.sprites = new Map(); + + this.rawImages = []; + } + + /** + * @param {Application} app + */ + linkAppAfterBoot(app) { + this.app = app; + this.makeSpriteNotFoundCanvas(); + } + + /** + * Fetches a given sprite from the cache + * @param {string} key + * @returns {BaseSprite} + */ + getSpriteInternal(key) { + const sprite = this.sprites.get(key); + if (!sprite) { + if (!missingSpriteIds[key]) { + // Only show error once + missingSpriteIds[key] = true; + logger.error("Sprite '" + key + "' not found!"); + } + return this.spriteNotFoundSprite; + } + return sprite; + } + + /** + * Returns an atlas sprite from the cache + * @param {string} key + * @returns {AtlasSprite} + */ + getSprite(key) { + const sprite = this.getSpriteInternal(key); + assert(sprite instanceof AtlasSprite || sprite === this.spriteNotFoundSprite, "Not an atlas sprite"); + return /** @type {AtlasSprite} */ (sprite); + } + + /** + * Returns a regular sprite from the cache + * @param {string} key + * @returns {RegularSprite} + */ + getRegularSprite(key) { + const sprite = this.getSpriteInternal(key); + assert( + sprite instanceof RegularSprite || sprite === this.spriteNotFoundSprite, + "Not a regular sprite" + ); + return /** @type {RegularSprite} */ (sprite); + } + + /** + * + * @param {string} key + * @returns {Promise} + */ + internalPreloadImage(key) { + const url = cachebust("res/" + key); + const image = new Image(); + + let triesSoFar = 0; + + return Promise.race([ + new Promise((resolve, reject) => { + setTimeout(reject, G_IS_DEV ? 500 : 10000); + }), + + new Promise(resolve => { + image.onload = () => { + image.onerror = null; + image.onload = null; + + if (typeof image.decode === "function") { + // SAFARI: Image.decode() fails on safari with svgs -> we dont want to fail + // on that + // FIREFOX: Decode never returns if the image is in cache, so call it in background + image.decode().then( + () => null, + () => null + ); + } + resolve(image); + }; + + image.onerror = reason => { + logger.warn("Failed to load '" + url + "':", reason); + if (++triesSoFar < 4) { + logger.log("Retrying to load image from", url); + image.src = url + "?try=" + triesSoFar; + } else { + logger.warn("Failed to load", url, "after", triesSoFar, "tries with reason", reason); + image.onerror = null; + image.onload = null; + resolve(null); + } + }; + + image.src = url; + }), + ]); + } + + /** + * Preloads a sprite + * @param {string} key + * @returns {Promise} + */ + preloadCSSSprite(key) { + return this.internalPreloadImage(key).then(image => { + if (key.indexOf("game_misc") >= 0) { + // Allow access to regular sprites + this.sprites.set(key, new RegularSprite(image, image.width, image.height)); + } + this.rawImages.push(image); + }); + } + + /** + * Preloads an atlas + * @param {AtlasDefinition} atlas + * @returns {Promise} + */ + preloadAtlas(atlas) { + return this.internalPreloadImage(atlas.getFullSourcePath()).then(image => { + // @ts-ignore + image.label = atlas.sourceFileName; + return this.internalParseAtlas(atlas, image); + }); + } + + /** + * + * @param {AtlasDefinition} atlas + * @param {HTMLImageElement} loadedImage + */ + internalParseAtlas({ meta: { scale }, sourceData }, loadedImage) { + this.rawImages.push(loadedImage); + + for (const spriteName in sourceData) { + const { frame, sourceSize, spriteSourceSize } = sourceData[spriteName]; + + let sprite = /** @type {AtlasSprite} */ (this.sprites.get(spriteName)); + + if (!sprite) { + sprite = new AtlasSprite(spriteName); + this.sprites.set(spriteName, sprite); + } + + const link = new SpriteAtlasLink({ + packedX: frame.x, + packedY: frame.y, + packedW: frame.w, + packedH: frame.h, + packOffsetX: spriteSourceSize.x, + packOffsetY: spriteSourceSize.y, + atlas: loadedImage, + w: sourceSize.w, + h: sourceSize.h, + }); + sprite.linksByResolution[scale] = link; + } + } + + /** + * Makes the canvas which shows the question mark, shown when a sprite was not found + */ + makeSpriteNotFoundCanvas() { + const dims = 128; + + const [canvas, context] = makeOffscreenBuffer(dims, dims, { + smooth: false, + label: "not-found-sprite", + }); + context.fillStyle = "#f77"; + context.fillRect(0, 0, dims, dims); + + context.textAlign = "center"; + context.textBaseline = "middle"; + context.fillStyle = "#eee"; + context.font = "25px Arial"; + context.fillText("???", dims / 2, dims / 2); + + // TODO: Not sure why this is set here + // @ts-ignore + canvas.src = "not-found"; + + const sprite = new AtlasSprite("not-found"); + ["0.1", "0.25", "0.5", "0.75", "1"].forEach(resolution => { + sprite.linksByResolution[resolution] = new SpriteAtlasLink({ + packedX: 0, + packedY: 0, + w: dims, + h: dims, + packOffsetX: 0, + packOffsetY: 0, + packedW: dims, + packedH: dims, + atlas: canvas, + }); + }); + + this.spriteNotFoundSprite = sprite; + } +} + +export const Loader = new LoaderImpl(); diff --git a/src/js/core/modal_dialog_elements.js b/src/js/core/modal_dialog_elements.js index 54b69402..124f51d4 100644 --- a/src/js/core/modal_dialog_elements.js +++ b/src/js/core/modal_dialog_elements.js @@ -30,10 +30,10 @@ export class Dialog { * @param {string} param0.title Title of the dialog * @param {string} param0.contentHTML Inner dialog html * @param {Array} param0.buttons - * Button list, each button contains of up to 3 parts seperated by ':'. + * Button list, each button contains of up to 3 parts separated by ':'. * Part 0: The id, one of the one defined in dialog_buttons.yaml * Part 1: The style, either good, bad or misc - * Part 2 (optional): Additional parameters seperated by '/', available are: + * Part 2 (optional): Additional parameters separated by '/', available are: * timeout: This button is only available after some waiting time * kb_enter: This button is triggered by the enter key * kb_escape This button is triggered by the escape key @@ -277,7 +277,6 @@ export class DialogLoading extends Dialog { const loader = document.createElement("div"); loader.classList.add("prefab_LoadingTextWithAnim"); loader.classList.add("loadingIndicator"); - loader.innerText = T.global.loading; elem.appendChild(loader); this.app.inputMgr.pushReciever(this.inputReciever); diff --git a/src/js/core/read_write_proxy.js b/src/js/core/read_write_proxy.js index 74b13efa..6d26fa2b 100644 --- a/src/js/core/read_write_proxy.js +++ b/src/js/core/read_write_proxy.js @@ -224,7 +224,7 @@ export class ReadWriteProxy { return rawData; }) - // Parse JSON, this could throw but thats fine + // Parse JSON, this could throw but that's fine .then(res => { try { return JSON.parse(res); diff --git a/src/js/core/sprites.js b/src/js/core/sprites.js index bdcc65b4..1019d8f7 100644 --- a/src/js/core/sprites.js +++ b/src/js/core/sprites.js @@ -1,360 +1,395 @@ -import { DrawParameters } from "./draw_parameters"; -import { Rectangle } from "./rectangle"; -import { round3Digits } from "./utils"; - -const floorSpriteCoordinates = false; - -export const ORIGINAL_SPRITE_SCALE = "0.75"; - -export class BaseSprite { - /** - * Returns the raw handle - * @returns {HTMLImageElement|HTMLCanvasElement} - */ - getRawTexture() { - abstract; - return null; - } - - /** - * Draws the sprite - * @param {CanvasRenderingContext2D} context - * @param {number} x - * @param {number} y - * @param {number} w - * @param {number} h - */ - draw(context, x, y, w, h) { - // eslint-disable-line no-unused-vars - abstract; - } -} - -/** - * Position of a sprite within an atlas - */ -export class SpriteAtlasLink { - /** - * - * @param {object} param0 - * @param {number} param0.packedX - * @param {number} param0.packedY - * @param {number} param0.packOffsetX - * @param {number} param0.packOffsetY - * @param {number} param0.packedW - * @param {number} param0.packedH - * @param {number} param0.w - * @param {number} param0.h - * @param {HTMLImageElement|HTMLCanvasElement} param0.atlas - */ - constructor({ w, h, packedX, packedY, packOffsetX, packOffsetY, packedW, packedH, atlas }) { - this.packedX = packedX; - this.packedY = packedY; - this.packedW = packedW; - this.packedH = packedH; - this.packOffsetX = packOffsetX; - this.packOffsetY = packOffsetY; - this.atlas = atlas; - this.w = w; - this.h = h; - } -} - -export class AtlasSprite extends BaseSprite { - /** - * - * @param {string} spriteName - */ - constructor(spriteName = "sprite") { - super(); - /** @type {Object.} */ - this.linksByResolution = {}; - this.spriteName = spriteName; - } - - getRawTexture() { - return this.linksByResolution[ORIGINAL_SPRITE_SCALE].atlas; - } - - /** - * Draws the sprite onto a regular context using no contexts - * @see {BaseSprite.draw} - */ - draw(context, x, y, w, h) { - if (G_IS_DEV) { - assert(context instanceof CanvasRenderingContext2D, "Not a valid context"); - } - - const link = this.linksByResolution[ORIGINAL_SPRITE_SCALE]; - - assert( - link, - "Link not known: " + - ORIGINAL_SPRITE_SCALE + - " (having " + - Object.keys(this.linksByResolution) + - ")" - ); - - const width = w || link.w; - const height = h || link.h; - - const scaleW = width / link.w; - const scaleH = height / link.h; - - context.drawImage( - link.atlas, - - link.packedX, - link.packedY, - link.packedW, - link.packedH, - - x + link.packOffsetX * scaleW, - y + link.packOffsetY * scaleH, - link.packedW * scaleW, - link.packedH * scaleH - ); - } - - /** - * - * @param {DrawParameters} parameters - * @param {number} x - * @param {number} y - * @param {number} size - * @param {boolean=} clipping - */ - drawCachedCentered(parameters, x, y, size, clipping = true) { - this.drawCached(parameters, x - size / 2, y - size / 2, size, size, clipping); - } - - /** - * - * @param {CanvasRenderingContext2D} context - * @param {number} x - * @param {number} y - * @param {number} size - */ - drawCentered(context, x, y, size) { - this.draw(context, x - size / 2, y - size / 2, size, size); - } - - /** - * Draws the sprite - * @param {DrawParameters} parameters - * @param {number} x - * @param {number} y - * @param {number} w - * @param {number} h - * @param {boolean=} clipping Whether to perform culling - */ - drawCached(parameters, x, y, w = null, h = null, clipping = true) { - if (G_IS_DEV) { - assert(parameters instanceof DrawParameters, "Not a valid context"); - assert(!!w && w > 0, "Not a valid width:" + w); - assert(!!h && h > 0, "Not a valid height:" + h); - } - - const visibleRect = parameters.visibleRect; - - const scale = parameters.desiredAtlasScale; - const link = this.linksByResolution[scale]; - - if (!link) { - assert(false, `Link not known: ${scale} (having ${Object.keys(this.linksByResolution)})`); - } - - const scaleW = w / link.w; - const scaleH = h / link.h; - - let destX = x + link.packOffsetX * scaleW; - let destY = y + link.packOffsetY * scaleH; - let destW = link.packedW * scaleW; - let destH = link.packedH * scaleH; - - let srcX = link.packedX; - let srcY = link.packedY; - let srcW = link.packedW; - let srcH = link.packedH; - - let intersection = null; - - if (clipping) { - const rect = new Rectangle(destX, destY, destW, destH); - intersection = rect.getIntersection(visibleRect); - if (!intersection) { - return; - } - - srcX += (intersection.x - destX) / scaleW; - srcY += (intersection.y - destY) / scaleH; - - srcW *= intersection.w / destW; - srcH *= intersection.h / destH; - - destX = intersection.x; - destY = intersection.y; - - destW = intersection.w; - destH = intersection.h; - } - - if (floorSpriteCoordinates) { - parameters.context.drawImage( - link.atlas, - - // atlas src pos - Math.floor(srcX), - Math.floor(srcY), - - // atlas src size - Math.floor(srcW), - Math.floor(srcH), - - // dest pos - Math.floor(destX), - Math.floor(destY), - - // dest size - Math.floor(destW), - Math.floor(destH) - ); - } else { - parameters.context.drawImage( - link.atlas, - - // atlas src pos - srcX, - srcY, - - // atlas src siize - srcW, - srcH, - - // dest pos and size - destX, - destY, - destW, - destH - ); - } - } - - /** - * Renders into an html element - * @param {HTMLElement} element - * @param {number} w - * @param {number} h - */ - renderToHTMLElement(element, w = 1, h = 1) { - element.style.position = "relative"; - element.innerHTML = this.getAsHTML(w, h); - } - - /** - * Returns the html to render as icon - * @param {number} w - * @param {number} h - */ - getAsHTML(w, h) { - const link = this.linksByResolution["0.5"]; - - // Find out how much we have to scale it so that it fits - const scaleX = w / link.w; - const scaleY = h / link.h; - - // Find out how big the scaled atlas is - const atlasW = link.atlas.width * scaleX; - const atlasH = link.atlas.height * scaleY; - - // @ts-ignore - const srcSafe = link.atlas.src.replaceAll("\\", "/"); - - // Find out how big we render the sprite - const widthAbsolute = scaleX * link.packedW; - const heightAbsolute = scaleY * link.packedH; - - // Compute the position in the relative container - const leftRelative = (link.packOffsetX * scaleX) / w; - const topRelative = (link.packOffsetY * scaleY) / h; - const widthRelative = widthAbsolute / w; - const heightRelative = heightAbsolute / h; - - // Scale the atlas relative to the width and height of the element - const bgW = atlasW / widthAbsolute; - const bgH = atlasH / heightAbsolute; - - // Figure out what the position of the atlas is - const bgX = link.packedX * scaleX; - const bgY = link.packedY * scaleY; - - // Fuck you, whoever thought its a good idea to make background-position work like it does now - const bgXRelative = -bgX / (widthAbsolute - atlasW); - const bgYRelative = -bgY / (heightAbsolute - atlasH); - - return ` - - `; - } -} - -export class RegularSprite extends BaseSprite { - constructor(sprite, w, h) { - super(); - this.w = w; - this.h = h; - this.sprite = sprite; - } - - getRawTexture() { - return this.sprite; - } - - /** - * Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing - * images into buffers - * @param {CanvasRenderingContext2D} context - * @param {number} x - * @param {number} y - * @param {number} w - * @param {number} h - */ - draw(context, x, y, w, h) { - assert(context, "No context given"); - assert(x !== undefined, "No x given"); - assert(y !== undefined, "No y given"); - assert(w !== undefined, "No width given"); - assert(h !== undefined, "No height given"); - context.drawImage(this.sprite, x, y, w, h); - } - - /** - * Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing - * images into buffers - * @param {CanvasRenderingContext2D} context - * @param {number} x - * @param {number} y - * @param {number} w - * @param {number} h - */ - drawCentered(context, x, y, w, h) { - assert(context, "No context given"); - assert(x !== undefined, "No x given"); - assert(y !== undefined, "No y given"); - assert(w !== undefined, "No width given"); - assert(h !== undefined, "No height given"); - context.drawImage(this.sprite, x - w / 2, y - h / 2, w, h); - } -} +import { DrawParameters } from "./draw_parameters"; +import { Rectangle } from "./rectangle"; +import { round3Digits } from "./utils"; + +export const ORIGINAL_SPRITE_SCALE = "0.75"; +export const FULL_CLIP_RECT = new Rectangle(0, 0, 1, 1); + +const EXTRUDE = 0.1; + +export class BaseSprite { + /** + * Returns the raw handle + * @returns {HTMLImageElement|HTMLCanvasElement} + */ + getRawTexture() { + abstract; + return null; + } + + /** + * Draws the sprite + * @param {CanvasRenderingContext2D} context + * @param {number} x + * @param {number} y + * @param {number} w + * @param {number} h + */ + draw(context, x, y, w, h) { + // eslint-disable-line no-unused-vars + abstract; + } +} + +/** + * Position of a sprite within an atlas + */ +export class SpriteAtlasLink { + /** + * + * @param {object} param0 + * @param {number} param0.packedX + * @param {number} param0.packedY + * @param {number} param0.packOffsetX + * @param {number} param0.packOffsetY + * @param {number} param0.packedW + * @param {number} param0.packedH + * @param {number} param0.w + * @param {number} param0.h + * @param {HTMLImageElement|HTMLCanvasElement} param0.atlas + */ + constructor({ w, h, packedX, packedY, packOffsetX, packOffsetY, packedW, packedH, atlas }) { + this.packedX = packedX; + this.packedY = packedY; + this.packedW = packedW; + this.packedH = packedH; + this.packOffsetX = packOffsetX; + this.packOffsetY = packOffsetY; + this.atlas = atlas; + this.w = w; + this.h = h; + } +} + +export class AtlasSprite extends BaseSprite { + /** + * + * @param {string} spriteName + */ + constructor(spriteName = "sprite") { + super(); + /** @type {Object.} */ + this.linksByResolution = {}; + this.spriteName = spriteName; + } + + getRawTexture() { + return this.linksByResolution[ORIGINAL_SPRITE_SCALE].atlas; + } + + /** + * Draws the sprite onto a regular context using no contexts + * @see {BaseSprite.draw} + */ + draw(context, x, y, w, h) { + if (G_IS_DEV) { + assert(context instanceof CanvasRenderingContext2D, "Not a valid context"); + } + + const link = this.linksByResolution[ORIGINAL_SPRITE_SCALE]; + + assert( + link, + "Link not known: " + + ORIGINAL_SPRITE_SCALE + + " (having " + + Object.keys(this.linksByResolution) + + ")" + ); + + const width = w || link.w; + const height = h || link.h; + + const scaleW = width / link.w; + const scaleH = height / link.h; + + context.drawImage( + link.atlas, + + link.packedX, + link.packedY, + link.packedW, + link.packedH, + + x + link.packOffsetX * scaleW, + y + link.packOffsetY * scaleH, + link.packedW * scaleW, + link.packedH * scaleH + ); + } + + /** + * + * @param {DrawParameters} parameters + * @param {number} x + * @param {number} y + * @param {number} size + * @param {boolean=} clipping + */ + drawCachedCentered(parameters, x, y, size, clipping = true) { + this.drawCached(parameters, x - size / 2, y - size / 2, size, size, clipping); + } + + /** + * + * @param {CanvasRenderingContext2D} context + * @param {number} x + * @param {number} y + * @param {number} size + */ + drawCentered(context, x, y, size) { + this.draw(context, x - size / 2, y - size / 2, size, size); + } + + /** + * Draws the sprite + * @param {DrawParameters} parameters + * @param {number} x + * @param {number} y + * @param {number} w + * @param {number} h + * @param {boolean=} clipping Whether to perform culling + */ + drawCached(parameters, x, y, w = null, h = null, clipping = true) { + if (G_IS_DEV) { + assert(parameters instanceof DrawParameters, "Not a valid context"); + assert(!!w && w > 0, "Not a valid width:" + w); + assert(!!h && h > 0, "Not a valid height:" + h); + } + + const visibleRect = parameters.visibleRect; + + const scale = parameters.desiredAtlasScale; + const link = this.linksByResolution[scale]; + + if (!link) { + assert(false, `Link not known: ${scale} (having ${Object.keys(this.linksByResolution)})`); + } + + const scaleW = w / link.w; + const scaleH = h / link.h; + + let destX = x + link.packOffsetX * scaleW; + let destY = y + link.packOffsetY * scaleH; + let destW = link.packedW * scaleW; + let destH = link.packedH * scaleH; + + let srcX = link.packedX; + let srcY = link.packedY; + let srcW = link.packedW; + let srcH = link.packedH; + + let intersection = null; + + if (clipping) { + const rect = new Rectangle(destX, destY, destW, destH); + intersection = rect.getIntersection(visibleRect); + if (!intersection) { + return; + } + + srcX += (intersection.x - destX) / scaleW; + srcY += (intersection.y - destY) / scaleH; + + srcW *= intersection.w / destW; + srcH *= intersection.h / destH; + + destX = intersection.x; + destY = intersection.y; + + destW = intersection.w; + destH = intersection.h; + } + + parameters.context.drawImage( + link.atlas, + + // atlas src pos + srcX, + srcY, + + // atlas src size + srcW, + srcH, + + // dest pos and size + destX - EXTRUDE, + destY - EXTRUDE, + destW + 2 * EXTRUDE, + destH + 2 * EXTRUDE + ); + } + + /** + * Draws a subset of the sprite. Does NO culling + * @param {DrawParameters} parameters + * @param {number} x + * @param {number} y + * @param {number} w + * @param {number} h + * @param {Rectangle=} clipRect The rectangle in local space (0 ... 1) to draw of the image + */ + drawCachedWithClipRect(parameters, x, y, w = null, h = null, clipRect = FULL_CLIP_RECT) { + if (G_IS_DEV) { + assert(parameters instanceof DrawParameters, "Not a valid context"); + assert(!!w && w > 0, "Not a valid width:" + w); + assert(!!h && h > 0, "Not a valid height:" + h); + assert(clipRect, "No clip rect given!"); + } + + const scale = parameters.desiredAtlasScale; + const link = this.linksByResolution[scale]; + + if (!link) { + assert(false, `Link not known: ${scale} (having ${Object.keys(this.linksByResolution)})`); + } + + const scaleW = w / link.w; + const scaleH = h / link.h; + + let destX = x + link.packOffsetX * scaleW + clipRect.x * w; + let destY = y + link.packOffsetY * scaleH + clipRect.y * h; + let destW = link.packedW * scaleW * clipRect.w; + let destH = link.packedH * scaleH * clipRect.h; + + let srcX = link.packedX + clipRect.x * link.packedW; + let srcY = link.packedY + clipRect.y * link.packedH; + let srcW = link.packedW * clipRect.w; + let srcH = link.packedH * clipRect.h; + + parameters.context.drawImage( + link.atlas, + + // atlas src pos + srcX, + srcY, + + // atlas src siize + srcW, + srcH, + + // dest pos and size + destX - EXTRUDE, + destY - EXTRUDE, + destW + 2 * EXTRUDE, + destH + 2 * EXTRUDE + ); + } + + /** + * Renders into an html element + * @param {HTMLElement} element + * @param {number} w + * @param {number} h + */ + renderToHTMLElement(element, w = 1, h = 1) { + element.style.position = "relative"; + element.innerHTML = this.getAsHTML(w, h); + } + + /** + * Returns the html to render as icon + * @param {number} w + * @param {number} h + */ + getAsHTML(w, h) { + const link = this.linksByResolution["0.5"]; + + // Find out how much we have to scale it so that it fits + const scaleX = w / link.w; + const scaleY = h / link.h; + + // Find out how big the scaled atlas is + const atlasW = link.atlas.width * scaleX; + const atlasH = link.atlas.height * scaleY; + + // @ts-ignore + const srcSafe = link.atlas.src.replaceAll("\\", "/"); + + // Find out how big we render the sprite + const widthAbsolute = scaleX * link.packedW; + const heightAbsolute = scaleY * link.packedH; + + // Compute the position in the relative container + const leftRelative = (link.packOffsetX * scaleX) / w; + const topRelative = (link.packOffsetY * scaleY) / h; + const widthRelative = widthAbsolute / w; + const heightRelative = heightAbsolute / h; + + // Scale the atlas relative to the width and height of the element + const bgW = atlasW / widthAbsolute; + const bgH = atlasH / heightAbsolute; + + // Figure out what the position of the atlas is + const bgX = link.packedX * scaleX; + const bgY = link.packedY * scaleY; + + // Fuck you, whoever thought its a good idea to make background-position work like it does now + const bgXRelative = -bgX / (widthAbsolute - atlasW); + const bgYRelative = -bgY / (heightAbsolute - atlasH); + + return ` + + `; + } +} + +export class RegularSprite extends BaseSprite { + constructor(sprite, w, h) { + super(); + this.w = w; + this.h = h; + this.sprite = sprite; + } + + getRawTexture() { + return this.sprite; + } + + /** + * Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing + * images into buffers + * @param {CanvasRenderingContext2D} context + * @param {number} x + * @param {number} y + * @param {number} w + * @param {number} h + */ + draw(context, x, y, w, h) { + assert(context, "No context given"); + assert(x !== undefined, "No x given"); + assert(y !== undefined, "No y given"); + assert(w !== undefined, "No width given"); + assert(h !== undefined, "No height given"); + context.drawImage(this.sprite, x, y, w, h); + } + + /** + * Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing + * images into buffers + * @param {CanvasRenderingContext2D} context + * @param {number} x + * @param {number} y + * @param {number} w + * @param {number} h + */ + drawCentered(context, x, y, w, h) { + assert(context, "No context given"); + assert(x !== undefined, "No x given"); + assert(y !== undefined, "No y given"); + assert(w !== undefined, "No width given"); + assert(h !== undefined, "No height given"); + context.drawImage(this.sprite, x - w / 2, y - h / 2, w, h); + } +} diff --git a/src/js/core/stale_area_detector.js b/src/js/core/stale_area_detector.js index f8e77f0c..5048ee37 100644 --- a/src/js/core/stale_area_detector.js +++ b/src/js/core/stale_area_detector.js @@ -1,50 +1,90 @@ -import { createLogger } from "./logging"; -import { Rectangle } from "./rectangle"; -import { globalConfig } from "./config"; - -const logger = createLogger("stale_areas"); - -export class StaleAreaDetector { - /** - * - * @param {object} param0 - * @param {import("../game/root").GameRoot} param0.root - * @param {string} param0.name The name for reference - * @param {(Rectangle) => void} param0.recomputeMethod Method which recomputes the given area - */ - constructor({ root, name, recomputeMethod }) { - this.root = root; - this.name = name; - this.recomputeMethod = recomputeMethod; - - /** @type {Rectangle} */ - this.staleArea = null; - } - - /** - * Invalidates the given area - * @param {Rectangle} area - */ - invalidate(area) { - // logger.log(this.name, "invalidated", area.toString()); - if (this.staleArea) { - this.staleArea = this.staleArea.getUnion(area); - } else { - this.staleArea = area.clone(); - } - } - - /** - * Updates the stale area - */ - update() { - if (this.staleArea) { - logger.log(this.name, "is recomputing", this.staleArea.toString()); - if (G_IS_DEV && globalConfig.debug.renderChanges) { - this.root.hud.parts.changesDebugger.renderChange(this.name, this.staleArea, "#fd145b"); - } - this.recomputeMethod(this.staleArea); - this.staleArea = null; - } - } -} +import { Component } from "../game/component"; +import { Entity } from "../game/entity"; +import { globalConfig } from "./config"; +import { createLogger } from "./logging"; +import { Rectangle } from "./rectangle"; + +const logger = createLogger("stale_areas"); + +export class StaleAreaDetector { + /** + * + * @param {object} param0 + * @param {import("../game/root").GameRoot} param0.root + * @param {string} param0.name The name for reference + * @param {(Rectangle) => void} param0.recomputeMethod Method which recomputes the given area + */ + constructor({ root, name, recomputeMethod }) { + this.root = root; + this.name = name; + this.recomputeMethod = recomputeMethod; + + /** @type {Rectangle} */ + this.staleArea = null; + } + + /** + * Invalidates the given area + * @param {Rectangle} area + */ + invalidate(area) { + // logger.log(this.name, "invalidated", area.toString()); + if (this.staleArea) { + this.staleArea = this.staleArea.getUnion(area); + } else { + this.staleArea = area.clone(); + } + } + + /** + * Makes this detector recompute the area of an entity whenever + * it changes in any way + * @param {Array} components + * @param {number} tilesAround How many tiles arround to expand the area + */ + recomputeOnComponentsChanged(components, tilesAround) { + const componentIds = components.map(component => component.getId()); + + /** + * Internal checker method + * @param {Entity} entity + */ + const checker = entity => { + if (!this.root.gameInitialized) { + return; + } + + // Check for all components + for (let i = 0; i < componentIds.length; ++i) { + if (entity.components[componentIds[i]]) { + // Entity is relevant, compute affected area + const area = entity.components.StaticMapEntity.getTileSpaceBounds().expandedInAllDirections( + tilesAround + ); + this.invalidate(area); + return; + } + } + }; + + this.root.signals.entityAdded.add(checker); + this.root.signals.entityChanged.add(checker); + this.root.signals.entityComponentRemoved.add(checker); + this.root.signals.entityGotNewComponent.add(checker); + this.root.signals.entityDestroyed.add(checker); + } + + /** + * Updates the stale area + */ + update() { + if (this.staleArea) { + logger.log(this.name, "is recomputing", this.staleArea.toString()); + if (G_IS_DEV && globalConfig.debug.renderChanges) { + this.root.hud.parts.changesDebugger.renderChange(this.name, this.staleArea, "#fd145b"); + } + this.recomputeMethod(this.staleArea); + this.staleArea = null; + } + } +} diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index b999e0ee..b162f09f 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -2,7 +2,7 @@ import { globalConfig } from "../core/config"; import { DrawParameters } from "../core/draw_parameters"; import { createLogger } from "../core/logging"; import { Rectangle } from "../core/rectangle"; -import { epsilonCompare, round4Digits, clamp } from "../core/utils"; +import { clamp, epsilonCompare, round4Digits } from "../core/utils"; import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector"; import { BasicSerializableObject, types } from "../savegame/serialization"; import { BaseItem } from "./base_item"; @@ -175,6 +175,11 @@ export class BeltPath extends BasicSerializableObject { */ onPathChanged() { this.acceptorTarget = this.computeAcceptingEntityAndSlot(); + + /** + * How many items past the first item are compressed + */ + this.numCompressedItemsAfterFirstItem = 0; } /** @@ -186,9 +191,12 @@ export class BeltPath extends BasicSerializableObject { /** * Finds the entity which accepts our items + * @param {boolean=} debug_Silent Whether debug output should be silent * @return {{ entity: Entity, slot: number, direction?: enumDirection }} */ - computeAcceptingEntityAndSlot() { + computeAcceptingEntityAndSlot(debug_Silent = false) { + DEBUG && !debug_Silent && logger.log("Recomputing acceptor target"); + const lastEntity = this.entityPath[this.entityPath.length - 1]; const lastStatic = lastEntity.components.StaticMapEntity; const lastBeltComp = lastEntity.components.Belt; @@ -207,12 +215,23 @@ export class BeltPath extends BasicSerializableObject { ); if (targetEntity) { + DEBUG && !debug_Silent && logger.log(" Found target entity", targetEntity.uid); const targetStaticComp = targetEntity.components.StaticMapEntity; const targetBeltComp = targetEntity.components.Belt; // Check for belts (special case) if (targetBeltComp) { const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top); + DEBUG && + !debug_Silent && + logger.log( + " Entity is accepting items from", + ejectSlotWsDirection, + "vs", + beltAcceptingDirection, + "Rotation:", + targetStaticComp.rotation + ); if (ejectSlotWsDirection === beltAcceptingDirection) { return { entity: targetEntity, @@ -377,6 +396,61 @@ export class BeltPath extends BasicSerializableObject { if (!actualBounds.equalsEpsilon(this.worldBounds, 0.01)) { return fail("Bounds are stale"); } + + // Check acceptor + const acceptor = this.computeAcceptingEntityAndSlot(true); + if (!!acceptor !== !!this.acceptorTarget) { + return fail("Acceptor target mismatch, acceptor", !!acceptor, "vs stored", !!this.acceptorTarget); + } + + if (acceptor) { + if (this.acceptorTarget.entity !== acceptor.entity) { + return fail( + "Mismatching entity on acceptor target:", + acceptor.entity.uid, + "vs", + this.acceptorTarget.entity.uid + ); + } + + if (this.acceptorTarget.slot !== acceptor.slot) { + return fail( + "Mismatching entity on acceptor target:", + acceptor.slot, + "vs stored", + this.acceptorTarget.slot + ); + } + + if (this.acceptorTarget.direction !== acceptor.direction) { + return fail( + "Mismatching direction on acceptor target:", + acceptor.direction, + "vs stored", + this.acceptorTarget.direction + ); + } + } + + // Check first nonzero offset + let firstNonzero = 0; + for (let i = this.items.length - 2; i >= 0; --i) { + if (this.items[i][_nextDistance] < globalConfig.itemSpacingOnBelts + 1e-5) { + ++firstNonzero; + } else { + break; + } + } + + // Should warn, but this check isn't actually accurate + // if (firstNonzero !== this.numCompressedItemsAfterFirstItem) { + // console.warn( + // "First nonzero index is " + + // firstNonzero + + // " but stored is " + + // this.numCompressedItemsAfterFirstItem + // ); + // } } /* dev:end */ @@ -989,11 +1063,15 @@ export class BeltPath extends BasicSerializableObject { // Store how much velocity (strictly its distance, not velocity) we have to distribute over all items let remainingVelocity = beltSpeed; - for (let i = this.items.length - 1; i >= 0; --i) { - const nextDistanceAndItem = this.items[i]; + // Store the last item we processed, so we can skip clashed ones + let lastItemProcessed; + + for (lastItemProcessed = this.items.length - 1; lastItemProcessed >= 0; --lastItemProcessed) { + const nextDistanceAndItem = this.items[lastItemProcessed]; // Compute how much spacing we need at least - const minimumSpacing = i === this.items.length - 1 ? 0 : globalConfig.itemSpacingOnBelts; + const minimumSpacing = + lastItemProcessed === this.items.length - 1 ? 0 : globalConfig.itemSpacingOnBelts; // Compute how much we can advance const clampedProgress = Math.max( @@ -1018,9 +1096,19 @@ export class BeltPath extends BasicSerializableObject { // Try to directly get rid of the item if (this.tryHandOverItem(nextDistanceAndItem[_item], excessVelocity)) { this.items.pop(); + + this.numCompressedItemsAfterFirstItem = Math.max( + 0, + this.numCompressedItemsAfterFirstItem - 1 + ); } } + if (isFirstItemProcessed) { + // Skip N null items after first items + lastItemProcessed -= this.numCompressedItemsAfterFirstItem; + } + isFirstItemProcessed = false; this.spacingToFirstItem += clampedProgress; if (remainingVelocity < 0.01) { @@ -1028,11 +1116,22 @@ export class BeltPath extends BasicSerializableObject { } } + // Compute compressed item count + this.numCompressedItemsAfterFirstItem = Math.max( + 0, + this.numCompressedItemsAfterFirstItem, + this.items.length - 2 - lastItemProcessed + ); + // Check if we have an item which is ready to be emitted const lastItem = this.items[this.items.length - 1]; if (lastItem && lastItem[_nextDistance] === 0 && this.acceptorTarget) { if (this.tryHandOverItem(lastItem[_item])) { this.items.pop(); + this.numCompressedItemsAfterFirstItem = Math.max( + 0, + this.numCompressedItemsAfterFirstItem - 1 + ); } } @@ -1069,12 +1168,14 @@ export class BeltPath extends BasicSerializableObject { // Trigger animation on the acceptor comp const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor; if (targetAcceptorComp) { - targetAcceptorComp.onItemAccepted( - this.acceptorTarget.slot, - this.acceptorTarget.direction, - item, - remainingProgress - ); + if (!this.root.app.settings.getAllSettings().simplifiedBelts) { + targetAcceptorComp.onItemAccepted( + this.acceptorTarget.slot, + this.acceptorTarget.direction, + item, + remainingProgress + ); + } } return true; @@ -1091,7 +1192,7 @@ export class BeltPath extends BasicSerializableObject { computePositionFromProgress(progress) { let currentLength = 0; - // floating point issuses .. + // floating point issues .. assert(progress <= this.totalLength + 0.02, "Progress too big: " + progress); for (let i = 0; i < this.entityPath.length; ++i) { @@ -1154,6 +1255,11 @@ export class BeltPath extends BasicSerializableObject { worldPos.y + 2 ); progress += nextDistanceAndItem[_nextDistance]; + + if (this.items.length - 1 - this.numCompressedItemsAfterFirstItem === i) { + parameters.context.fillStyle = "red"; + parameters.context.fillRect(worldPos.x + 5, worldPos.y, 20, 3); + } } for (let i = 0; i < this.entityPath.length; ++i) { @@ -1179,6 +1285,40 @@ export class BeltPath extends BasicSerializableObject { parameters.context.fillRect(firstItemIndicator.x - 3, firstItemIndicator.y - 1, 6, 2); } + /** + * Checks if this belt path should render simplified + */ + checkIsPotatoMode() { + // POTATO Mode: Only show items when belt is hovered + if (!this.root.app.settings.getAllSettings().simplifiedBelts) { + return false; + } + + if (this.root.currentLayer !== "regular") { + // Not in regular layer + return true; + } + + const mousePos = this.root.app.mousePosition; + if (!mousePos) { + // Mouse not registered + return true; + } + + const tile = this.root.camera.screenToWorld(mousePos).toTileSpace(); + const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "regular"); + if (!contents || !contents.components.Belt) { + // Nothing below + return true; + } + + if (contents.components.Belt.assignedPath !== this) { + // Not this path + return true; + } + return false; + } + /** * Draws the path * @param {DrawParameters} parameters @@ -1193,6 +1333,30 @@ export class BeltPath extends BasicSerializableObject { return; } + if (this.checkIsPotatoMode()) { + const firstItem = this.items[0]; + if (this.entityPath.length > 1 && firstItem) { + const medianBeltIndex = clamp( + Math.round(this.entityPath.length / 2 - 1), + 0, + this.entityPath.length - 1 + ); + const medianBelt = this.entityPath[medianBeltIndex]; + const beltComp = medianBelt.components.Belt; + const staticComp = medianBelt.components.StaticMapEntity; + const centerPosLocal = beltComp.transformBeltToLocalSpace( + this.entityPath.length % 2 === 0 ? beltComp.getEffectiveLengthTiles() : 0.5 + ); + const centerPos = staticComp.localTileToWorld(centerPosLocal).toWorldSpaceCenterOfTile(); + + parameters.context.globalAlpha = 0.5; + firstItem[_item].drawItemCenteredClipped(centerPos.x, centerPos.y, parameters); + parameters.context.globalAlpha = 1; + } + + return; + } + let currentItemPos = this.spacingToFirstItem; let currentItemIndex = 0; @@ -1206,7 +1370,7 @@ export class BeltPath extends BasicSerializableObject { // Check if the current items are on the belt while (trackPos + beltLength >= currentItemPos - 1e-5) { - // Its on the belt, render it now + // It's on the belt, render it now const staticComp = entity.components.StaticMapEntity; assert( currentItemPos - trackPos >= 0, diff --git a/src/js/game/blueprint.js b/src/js/game/blueprint.js index ccbbc248..a37ea20d 100644 --- a/src/js/game/blueprint.js +++ b/src/js/game/blueprint.js @@ -44,7 +44,7 @@ export class Blueprint { const entity = root.entityMgr.findByUid(uids[i]); assert(entity, "Entity for blueprint not found:" + uids[i]); - const clone = entity.duplicateWithoutContents(); + const clone = entity.clone(); newEntities.push(clone); const pos = entity.components.StaticMapEntity.getTileSpaceBounds().getCenter(); @@ -160,7 +160,7 @@ export class Blueprint { continue; } - const clone = entity.duplicateWithoutContents(); + const clone = entity.clone(); clone.components.StaticMapEntity.origin.addInplace(tile); root.logic.freeEntityAreaBeforeBuild(clone); root.map.placeStaticEntity(clone); diff --git a/src/js/game/building_codes.js b/src/js/game/building_codes.js index 05c27f57..0a3cbc36 100644 --- a/src/js/game/building_codes.js +++ b/src/js/game/building_codes.js @@ -1,83 +1,95 @@ -/* typehints:start */ -import { MetaBuilding } from "./meta_building"; -import { AtlasSprite } from "../core/sprites"; -import { Vector } from "../core/vector"; -/* typehints:end */ - -/** - * @typedef {{ - * metaClass: typeof MetaBuilding, - * metaInstance?: MetaBuilding, - * variant?: string, - * rotationVariant?: number, - * tileSize?: Vector, - * sprite?: AtlasSprite, - * blueprintSprite?: AtlasSprite, - * silhouetteColor?: string - * }} BuildingVariantIdentifier - */ - -/** - * Stores a lookup table for all building variants (for better performance) - * @type {Object} - */ -export const gBuildingVariants = { - // Set later -}; - -/** - * Registers a new variant - * @param {number} id - * @param {typeof MetaBuilding} meta - * @param {string} variant - * @param {number} rotationVariant - */ -export function registerBuildingVariant( - id, - meta, - variant = "default" /* FIXME: Circular dependency, actually its defaultBuildingVariant */, - rotationVariant = 0 -) { - assert(!gBuildingVariants[id], "Duplicate id: " + id); - gBuildingVariants[id] = { - metaClass: meta, - variant, - rotationVariant, - // @ts-ignore - tileSize: new meta().getDimensions(variant), - }; -} - -/** - * - * @param {number} code - * @returns {BuildingVariantIdentifier} - */ -export function getBuildingDataFromCode(code) { - assert(gBuildingVariants[code], "Invalid building code: " + code); - return gBuildingVariants[code]; -} - -/** - * Finds the code for a given variant - * @param {MetaBuilding} metaBuilding - * @param {string} variant - * @param {number} rotationVariant - */ -export function getCodeFromBuildingData(metaBuilding, variant, rotationVariant) { - for (const key in gBuildingVariants) { - const data = gBuildingVariants[key]; - if ( - data.metaInstance.getId() === metaBuilding.getId() && - data.variant === variant && - data.rotationVariant === rotationVariant - ) { - return +key; - } - } - assertAlways( - false, - "Building not found by data: " + metaBuilding.getId() + " / " + variant + " / " + rotationVariant - ); - return 0; -} +/* typehints:start */ +import { MetaBuilding } from "./meta_building"; +import { AtlasSprite } from "../core/sprites"; +import { Vector } from "../core/vector"; +/* typehints:end */ + +/** + * @typedef {{ + * metaClass: typeof MetaBuilding, + * metaInstance?: MetaBuilding, + * variant?: string, + * rotationVariant?: number, + * tileSize?: Vector, + * sprite?: AtlasSprite, + * blueprintSprite?: AtlasSprite, + * silhouetteColor?: string + * }} BuildingVariantIdentifier + */ + +/** + * Stores a lookup table for all building variants (for better performance) + * @type {Object} + */ +export const gBuildingVariants = { + // Set later +}; + +/** + * Mapping from 'metaBuildingId/variant/rotationVariant' to building code + * @type {Map} + */ +const variantsCache = new Map(); + +/** + * Registers a new variant + * @param {number} code + * @param {typeof MetaBuilding} meta + * @param {string} variant + * @param {number} rotationVariant + */ +export function registerBuildingVariant( + code, + meta, + variant = "default" /* FIXME: Circular dependency, actually its defaultBuildingVariant */, + rotationVariant = 0 +) { + assert(!gBuildingVariants[code], "Duplicate id: " + code); + gBuildingVariants[code] = { + metaClass: meta, + variant, + rotationVariant, + // @ts-ignore + tileSize: new meta().getDimensions(variant), + }; +} + +/** + * + * @param {number} code + * @returns {BuildingVariantIdentifier} + */ +export function getBuildingDataFromCode(code) { + assert(gBuildingVariants[code], "Invalid building code: " + code); + return gBuildingVariants[code]; +} + +/** + * Builds the cache for the codes + */ +export function buildBuildingCodeCache() { + for (const code in gBuildingVariants) { + const data = gBuildingVariants[code]; + const hash = data.metaInstance.getId() + "/" + data.variant + "/" + data.rotationVariant; + variantsCache.set(hash, +code); + } +} + +/** + * Finds the code for a given variant + * @param {MetaBuilding} metaBuilding + * @param {string} variant + * @param {number} rotationVariant + * @returns {number} + */ +export function getCodeFromBuildingData(metaBuilding, variant, rotationVariant) { + const hash = metaBuilding.getId() + "/" + variant + "/" + rotationVariant; + const result = variantsCache.get(hash); + if (G_IS_DEV) { + if (!result) { + console.warn("Known hashes:", Array.from(variantsCache.keys())); + assertAlways(false, "Building not found by data: " + hash); + } + } + return result; +} diff --git a/src/js/game/buildings/splitter.js b/src/js/game/buildings/balancer.js similarity index 77% rename from src/js/game/buildings/splitter.js rename to src/js/game/buildings/balancer.js index d512e002..06fbc965 100644 --- a/src/js/game/buildings/splitter.js +++ b/src/js/game/buildings/balancer.js @@ -11,29 +11,29 @@ import { formatItemsPerSecond } from "../../core/utils"; import { BeltUnderlaysComponent } from "../components/belt_underlays"; /** @enum {string} */ -export const enumSplitterVariants = { - compact: "compact", - compactInverse: "compact-inverse", - compactMerge: "compact-merge", - compactMergeInverse: "compact-merge-inverse", +export const enumBalancerVariants = { + merger: "merger", + mergerInverse: "merger-inverse", + splitter: "splitter", + splitterInverse: "splitter-inverse", }; -export class MetaSplitterBuilding extends MetaBuilding { +export class MetaBalancerBuilding extends MetaBuilding { constructor() { - super("splitter"); + super("balancer"); } getDimensions(variant) { switch (variant) { case defaultBuildingVariant: return new Vector(2, 1); - case enumSplitterVariants.compact: - case enumSplitterVariants.compactInverse: - case enumSplitterVariants.compactMerge: - case enumSplitterVariants.compactMergeInverse: + case enumBalancerVariants.merger: + case enumBalancerVariants.mergerInverse: + case enumBalancerVariants.splitter: + case enumBalancerVariants.splitterInverse: return new Vector(1, 1); default: - assertAlways(false, "Unknown splitter variant: " + variant); + assertAlways(false, "Unknown balancer variant: " + variant); } } @@ -43,7 +43,7 @@ export class MetaSplitterBuilding extends MetaBuilding { * @returns {Array<[string, string]>} */ getAdditionalStatistics(root, variant) { - const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.splitter); + const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.balancer); return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; } @@ -57,12 +57,12 @@ export class MetaSplitterBuilding extends MetaBuilding { getAvailableVariants(root) { let available = [defaultBuildingVariant]; - if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_splitter_compact)) { - available.push(enumSplitterVariants.compact, enumSplitterVariants.compactInverse); + if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_merger)) { + available.push(enumBalancerVariants.merger, enumBalancerVariants.mergerInverse); } - if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_merger_compact)) { - available.push(enumSplitterVariants.compactMerge, enumSplitterVariants.compactMergeInverse); + if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_splitter)) { + available.push(enumBalancerVariants.splitter, enumBalancerVariants.splitterInverse); } return available; @@ -72,7 +72,7 @@ export class MetaSplitterBuilding extends MetaBuilding { * @param {GameRoot} root */ getIsUnlocked(root) { - return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_splitter); + return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_balancer); } /** @@ -89,13 +89,14 @@ export class MetaSplitterBuilding extends MetaBuilding { entity.addComponent( new ItemProcessorComponent({ inputsPerCharge: 1, - processorType: enumItemProcessorTypes.splitter, + processorType: enumItemProcessorTypes.balancer, }) ); entity.addComponent( new ItemEjectorComponent({ slots: [], // set later + renderFloatingItems: false, }) ); @@ -134,8 +135,8 @@ export class MetaSplitterBuilding extends MetaBuilding { break; } - case enumSplitterVariants.compact: - case enumSplitterVariants.compactInverse: { + case enumBalancerVariants.merger: + case enumBalancerVariants.mergerInverse: { entity.components.ItemAcceptor.setSlots([ { pos: new Vector(0, 0), @@ -144,7 +145,7 @@ export class MetaSplitterBuilding extends MetaBuilding { { pos: new Vector(0, 0), directions: [ - variant === enumSplitterVariants.compactInverse + variant === enumBalancerVariants.mergerInverse ? enumDirection.left : enumDirection.right, ], @@ -161,8 +162,8 @@ export class MetaSplitterBuilding extends MetaBuilding { break; } - case enumSplitterVariants.compactMerge: - case enumSplitterVariants.compactMergeInverse: { + case enumBalancerVariants.splitter: + case enumBalancerVariants.splitterInverse: { entity.components.ItemAcceptor.setSlots([ { pos: new Vector(0, 0), @@ -178,7 +179,7 @@ export class MetaSplitterBuilding extends MetaBuilding { { pos: new Vector(0, 0), direction: - variant === enumSplitterVariants.compactMergeInverse + variant === enumBalancerVariants.splitterInverse ? enumDirection.left : enumDirection.right, }, @@ -191,7 +192,7 @@ export class MetaSplitterBuilding extends MetaBuilding { break; } default: - assertAlways(false, "Unknown splitter variant: " + variant); + assertAlways(false, "Unknown balancer variant: " + variant); } } } diff --git a/src/js/game/buildings/belt.js b/src/js/game/buildings/belt.js index 1fb80b88..562b47d5 100644 --- a/src/js/game/buildings/belt.js +++ b/src/js/game/buildings/belt.js @@ -1,52 +1,228 @@ -import { Loader } from "../../core/loader"; -import { enumDirection } from "../../core/vector"; -import { SOUNDS } from "../../platform/sound"; -import { arrayBeltVariantToRotation, MetaBeltBaseBuilding } from "./belt_base"; - -export class MetaBeltBuilding extends MetaBeltBaseBuilding { - constructor() { - super("belt"); - } - - getSilhouetteColor() { - return "#777"; - } - - getPlacementSound() { - return SOUNDS.placeBelt; - } - - getPreviewSprite(rotationVariant) { - switch (arrayBeltVariantToRotation[rotationVariant]) { - case enumDirection.top: { - return Loader.getSprite("sprites/buildings/belt_top.png"); - } - case enumDirection.left: { - return Loader.getSprite("sprites/buildings/belt_left.png"); - } - case enumDirection.right: { - return Loader.getSprite("sprites/buildings/belt_right.png"); - } - default: { - assertAlways(false, "Invalid belt rotation variant"); - } - } - } - - getBlueprintSprite(rotationVariant) { - switch (arrayBeltVariantToRotation[rotationVariant]) { - case enumDirection.top: { - return Loader.getSprite("sprites/blueprints/belt_top.png"); - } - case enumDirection.left: { - return Loader.getSprite("sprites/blueprints/belt_left.png"); - } - case enumDirection.right: { - return Loader.getSprite("sprites/blueprints/belt_right.png"); - } - default: { - assertAlways(false, "Invalid belt rotation variant"); - } - } - } -} +import { Loader } from "../../core/loader"; +import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils"; +import { enumAngleToDirection, enumDirection, Vector } from "../../core/vector"; +import { SOUNDS } from "../../platform/sound"; +import { T } from "../../translations"; +import { BeltComponent } from "../components/belt"; +import { Entity } from "../entity"; +import { MetaBuilding } from "../meta_building"; +import { GameRoot } from "../root"; + +export const arrayBeltVariantToRotation = [enumDirection.top, enumDirection.left, enumDirection.right]; + +export const beltOverlayMatrices = { + [enumDirection.top]: generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]), + [enumDirection.left]: generateMatrixRotations([0, 0, 0, 1, 1, 0, 0, 1, 0]), + [enumDirection.right]: generateMatrixRotations([0, 0, 0, 0, 1, 1, 0, 1, 0]), +}; + +export class MetaBeltBuilding extends MetaBuilding { + constructor() { + super("belt"); + } + + getSilhouetteColor() { + return "#777"; + } + + getPlacementSound() { + return SOUNDS.placeBelt; + } + + getHasDirectionLockAvailable() { + return true; + } + getStayInPlacementMode() { + return true; + } + + getRotateAutomaticallyWhilePlacing() { + return true; + } + + getSprite() { + return null; + } + + getIsReplaceable() { + return true; + } + + /** + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + const beltSpeed = root.hubGoals.getBeltBaseSpeed(); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]]; + } + + getPreviewSprite(rotationVariant) { + switch (arrayBeltVariantToRotation[rotationVariant]) { + case enumDirection.top: { + return Loader.getSprite("sprites/buildings/belt_top.png"); + } + case enumDirection.left: { + return Loader.getSprite("sprites/buildings/belt_left.png"); + } + case enumDirection.right: { + return Loader.getSprite("sprites/buildings/belt_right.png"); + } + default: { + assertAlways(false, "Invalid belt rotation variant"); + } + } + } + + getBlueprintSprite(rotationVariant) { + switch (arrayBeltVariantToRotation[rotationVariant]) { + case enumDirection.top: { + return Loader.getSprite("sprites/blueprints/belt_top.png"); + } + case enumDirection.left: { + return Loader.getSprite("sprites/blueprints/belt_left.png"); + } + case enumDirection.right: { + return Loader.getSprite("sprites/blueprints/belt_right.png"); + } + default: { + assertAlways(false, "Invalid belt rotation variant"); + } + } + } + + /** + * + * @param {number} rotation + * @param {number} rotationVariant + * @param {string} variant + * @param {Entity} entity + */ + getSpecialOverlayRenderMatrix(rotation, rotationVariant, variant, entity) { + return beltOverlayMatrices[entity.components.Belt.direction][rotation]; + } + + /** + * Creates the entity at the given location + * @param {Entity} entity + */ + setupEntityComponents(entity) { + entity.addComponent( + new BeltComponent({ + direction: enumDirection.top, // updated later + }) + ); + } + + /** + * + * @param {Entity} entity + * @param {number} rotationVariant + */ + updateVariants(entity, rotationVariant) { + entity.components.Belt.direction = arrayBeltVariantToRotation[rotationVariant]; + } + + /** + * Should compute the optimal rotation variant on the given tile + * @param {object} param0 + * @param {GameRoot} param0.root + * @param {Vector} param0.tile + * @param {number} param0.rotation + * @param {string} param0.variant + * @param {Layer} param0.layer + * @return {{ rotation: number, rotationVariant: number, connectedEntities?: Array }} + */ + computeOptimalDirectionAndRotationVariantAtTile({ root, tile, rotation, variant, layer }) { + const topDirection = enumAngleToDirection[rotation]; + const rightDirection = enumAngleToDirection[(rotation + 90) % 360]; + const bottomDirection = enumAngleToDirection[(rotation + 180) % 360]; + const leftDirection = enumAngleToDirection[(rotation + 270) % 360]; + + const { ejectors, acceptors } = root.logic.getEjectorsAndAcceptorsAtTile(tile); + + let hasBottomEjector = false; + let hasRightEjector = false; + let hasLeftEjector = false; + + let hasTopAcceptor = false; + let hasLeftAcceptor = false; + let hasRightAcceptor = false; + + // Check all ejectors + for (let i = 0; i < ejectors.length; ++i) { + const ejector = ejectors[i]; + + if (ejector.toDirection === topDirection) { + hasBottomEjector = true; + } else if (ejector.toDirection === leftDirection) { + hasRightEjector = true; + } else if (ejector.toDirection === rightDirection) { + hasLeftEjector = true; + } + } + + // Check all acceptors + for (let i = 0; i < acceptors.length; ++i) { + const acceptor = acceptors[i]; + if (acceptor.fromDirection === bottomDirection) { + hasTopAcceptor = true; + } else if (acceptor.fromDirection === rightDirection) { + hasLeftAcceptor = true; + } else if (acceptor.fromDirection === leftDirection) { + hasRightAcceptor = true; + } + } + + // Soo .. if there is any ejector below us we always prioritize + // this ejector + if (!hasBottomEjector) { + // When something ejects to us from the left and nothing from the right, + // do a curve from the left to the top + + if (hasRightEjector && !hasLeftEjector) { + return { + rotation: (rotation + 270) % 360, + rotationVariant: 2, + }; + } + + // When something ejects to us from the right and nothing from the left, + // do a curve from the right to the top + if (hasLeftEjector && !hasRightEjector) { + return { + rotation: (rotation + 90) % 360, + rotationVariant: 1, + }; + } + } + + // When there is a top acceptor, ignore sides + // NOTICE: This makes the belt prefer side turns *way* too much! + if (!hasTopAcceptor) { + // When there is an acceptor to the right but no acceptor to the left, + // do a turn to the right + if (hasRightAcceptor && !hasLeftAcceptor) { + return { + rotation, + rotationVariant: 2, + }; + } + + // When there is an acceptor to the left but no acceptor to the right, + // do a turn to the left + if (hasLeftAcceptor && !hasRightAcceptor) { + return { + rotation, + rotationVariant: 1, + }; + } + } + + return { + rotation, + rotationVariant: 0, + }; + } +} diff --git a/src/js/game/buildings/belt_base.js b/src/js/game/buildings/belt_base.js deleted file mode 100644 index 1aafa9e1..00000000 --- a/src/js/game/buildings/belt_base.js +++ /dev/null @@ -1,186 +0,0 @@ -import { formatItemsPerSecond, generateMatrixRotations } from "../../core/utils"; -import { enumAngleToDirection, enumDirection, Vector } from "../../core/vector"; -import { SOUNDS } from "../../platform/sound"; -import { T } from "../../translations"; -import { BeltComponent } from "../components/belt"; -import { Entity } from "../entity"; -import { MetaBuilding } from "../meta_building"; -import { GameRoot } from "../root"; - -export const arrayBeltVariantToRotation = [enumDirection.top, enumDirection.left, enumDirection.right]; - -export const beltOverlayMatrices = { - [enumDirection.top]: generateMatrixRotations([0, 1, 0, 0, 1, 0, 0, 1, 0]), - [enumDirection.left]: generateMatrixRotations([0, 0, 0, 1, 1, 0, 0, 1, 0]), - [enumDirection.right]: generateMatrixRotations([0, 0, 0, 0, 1, 1, 0, 1, 0]), -}; - -export class MetaBeltBaseBuilding extends MetaBuilding { - getHasDirectionLockAvailable() { - return true; - } - - /** - * @param {GameRoot} root - * @param {string} variant - * @returns {Array<[string, string]>} - */ - getAdditionalStatistics(root, variant) { - const beltSpeed = root.hubGoals.getBeltBaseSpeed(); - return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(beltSpeed)]]; - } - - getStayInPlacementMode() { - return true; - } - - getRotateAutomaticallyWhilePlacing() { - return true; - } - - getPlacementSound() { - return SOUNDS.placeBelt; - } - - getSprite() { - return null; - } - - getIsReplaceable() { - return true; - } - - /** - * - * @param {number} rotation - * @param {number} rotationVariant - * @param {string} variant - * @param {Entity} entity - */ - getSpecialOverlayRenderMatrix(rotation, rotationVariant, variant, entity) { - return beltOverlayMatrices[entity.components.Belt.direction][rotation]; - } - - /** - * Creates the entity at the given location - * @param {Entity} entity - */ - setupEntityComponents(entity) { - entity.addComponent( - new BeltComponent({ - direction: enumDirection.top, // updated later - }) - ); - } - - /** - * - * @param {Entity} entity - * @param {number} rotationVariant - */ - updateVariants(entity, rotationVariant) { - entity.components.Belt.direction = arrayBeltVariantToRotation[rotationVariant]; - } - - /** - * Should compute the optimal rotation variant on the given tile - * @param {object} param0 - * @param {GameRoot} param0.root - * @param {Vector} param0.tile - * @param {number} param0.rotation - * @param {string} param0.variant - * @param {Layer} param0.layer - * @return {{ rotation: number, rotationVariant: number, connectedEntities?: Array }} - */ - computeOptimalDirectionAndRotationVariantAtTile({ root, tile, rotation, variant, layer }) { - const topDirection = enumAngleToDirection[rotation]; - const rightDirection = enumAngleToDirection[(rotation + 90) % 360]; - const bottomDirection = enumAngleToDirection[(rotation + 180) % 360]; - const leftDirection = enumAngleToDirection[(rotation + 270) % 360]; - - const { ejectors, acceptors } = root.logic.getEjectorsAndAcceptorsAtTile(tile); - - let hasBottomEjector = false; - let hasRightEjector = false; - let hasLeftEjector = false; - - let hasTopAcceptor = false; - let hasLeftAcceptor = false; - let hasRightAcceptor = false; - - // Check all ejectors - for (let i = 0; i < ejectors.length; ++i) { - const ejector = ejectors[i]; - - if (ejector.toDirection === topDirection) { - hasBottomEjector = true; - } else if (ejector.toDirection === leftDirection) { - hasRightEjector = true; - } else if (ejector.toDirection === rightDirection) { - hasLeftEjector = true; - } - } - - // Check all acceptors - for (let i = 0; i < acceptors.length; ++i) { - const acceptor = acceptors[i]; - if (acceptor.fromDirection === bottomDirection) { - hasTopAcceptor = true; - } else if (acceptor.fromDirection === rightDirection) { - hasLeftAcceptor = true; - } else if (acceptor.fromDirection === leftDirection) { - hasRightAcceptor = true; - } - } - - // Soo .. if there is any ejector below us we always prioritize - // this ejector - if (!hasBottomEjector) { - // When something ejects to us from the left and nothing from the right, - // do a curve from the left to the top - - if (hasRightEjector && !hasLeftEjector) { - return { - rotation: (rotation + 270) % 360, - rotationVariant: 2, - }; - } - - // When something ejects to us from the right and nothing from the left, - // do a curve from the right to the top - if (hasLeftEjector && !hasRightEjector) { - return { - rotation: (rotation + 90) % 360, - rotationVariant: 1, - }; - } - } - - // When there is a top acceptor, ignore sides - // NOTICE: This makes the belt prefer side turns *way* too much! - if (!hasTopAcceptor) { - // When there is an acceptor to the right but no acceptor to the left, - // do a turn to the right - if (hasRightAcceptor && !hasLeftAcceptor) { - return { - rotation, - rotationVariant: 2, - }; - } - - // When there is an acceptor to the left but no acceptor to the right, - // do a turn to the left - if (hasLeftAcceptor && !hasRightAcceptor) { - return { - rotation, - rotationVariant: 1, - }; - } - } - - return { - rotation, - rotationVariant: 0, - }; - } -} diff --git a/src/js/game/buildings/filter.js b/src/js/game/buildings/filter.js index fb368fc7..37ad234a 100644 --- a/src/js/game/buildings/filter.js +++ b/src/js/game/buildings/filter.js @@ -1,11 +1,7 @@ import { enumDirection, Vector } from "../../core/vector"; +import { FilterComponent } from "../components/filter"; import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; -import { - enumItemProcessorRequirements, - enumItemProcessorTypes, - ItemProcessorComponent, -} from "../components/item_processor"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { Entity } from "../entity"; import { MetaBuilding } from "../meta_building"; @@ -79,12 +75,6 @@ export class MetaFilterBuilding extends MetaBuilding { }) ); - entity.addComponent( - new ItemProcessorComponent({ - processorType: enumItemProcessorTypes.filter, - inputsPerCharge: 1, - processingRequirement: enumItemProcessorRequirements.filter, - }) - ); + entity.addComponent(new FilterComponent()); } } diff --git a/src/js/game/buildings/rotater.js b/src/js/game/buildings/rotater.js index c278ef0d..7e5ef7d7 100644 --- a/src/js/game/buildings/rotater.js +++ b/src/js/game/buildings/rotater.js @@ -1,122 +1,122 @@ -import { formatItemsPerSecond } from "../../core/utils"; -import { enumDirection, Vector } from "../../core/vector"; -import { T } from "../../translations"; -import { ItemAcceptorComponent } from "../components/item_acceptor"; -import { ItemEjectorComponent } from "../components/item_ejector"; -import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; -import { Entity } from "../entity"; -import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; -import { GameRoot } from "../root"; -import { enumHubGoalRewards } from "../tutorial_goals"; - -/** @enum {string} */ -export const enumRotaterVariants = { ccw: "ccw", fl: "fl" }; - -export class MetaRotaterBuilding extends MetaBuilding { - constructor() { - super("rotater"); - } - - getSilhouetteColor() { - return "#7dc6cd"; - } - - /** - * @param {GameRoot} root - * @param {string} variant - * @returns {Array<[string, string]>} - */ - getAdditionalStatistics(root, variant) { - switch (variant) { - case defaultBuildingVariant: { - const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotater); - return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; - } - case enumRotaterVariants.ccw: { - const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotaterCCW); - return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; - } - case enumRotaterVariants.fl: { - const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotaterFL); - return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; - } - } - } - - /** - * - * @param {GameRoot} root - */ - getAvailableVariants(root) { - let variants = [defaultBuildingVariant]; - if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_rotater_ccw)) { - variants.push(enumRotaterVariants.ccw); - } - if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_rotater_fl)) { - variants.push(enumRotaterVariants.fl); - } - return variants; - } - - /** - * @param {GameRoot} root - */ - getIsUnlocked(root) { - return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_rotater); - } - - /** - * Creates the entity at the given location - * @param {Entity} entity - */ - setupEntityComponents(entity) { - entity.addComponent( - new ItemProcessorComponent({ - inputsPerCharge: 1, - processorType: enumItemProcessorTypes.rotater, - }) - ); - - entity.addComponent( - new ItemEjectorComponent({ - slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }], - }) - ); - entity.addComponent( - new ItemAcceptorComponent({ - slots: [ - { - pos: new Vector(0, 0), - directions: [enumDirection.bottom], - filter: "shape", - }, - ], - }) - ); - } - - /** - * - * @param {Entity} entity - * @param {number} rotationVariant - * @param {string} variant - */ - updateVariants(entity, rotationVariant, variant) { - switch (variant) { - case defaultBuildingVariant: { - entity.components.ItemProcessor.type = enumItemProcessorTypes.rotater; - break; - } - case enumRotaterVariants.ccw: { - entity.components.ItemProcessor.type = enumItemProcessorTypes.rotaterCCW; - break; - } - case enumRotaterVariants.fl: { - entity.components.ItemProcessor.type = enumItemProcessorTypes.rotaterFL; - break; - } - default: - assertAlways(false, "Unknown rotater variant: " + variant); - } - } -} +import { formatItemsPerSecond } from "../../core/utils"; +import { enumDirection, Vector } from "../../core/vector"; +import { T } from "../../translations"; +import { ItemAcceptorComponent } from "../components/item_acceptor"; +import { ItemEjectorComponent } from "../components/item_ejector"; +import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor"; +import { Entity } from "../entity"; +import { defaultBuildingVariant, MetaBuilding } from "../meta_building"; +import { GameRoot } from "../root"; +import { enumHubGoalRewards } from "../tutorial_goals"; + +/** @enum {string} */ +export const enumRotaterVariants = { ccw: "ccw", rotate180: "rotate180" }; + +export class MetaRotaterBuilding extends MetaBuilding { + constructor() { + super("rotater"); + } + + getSilhouetteColor() { + return "#7dc6cd"; + } + + /** + * @param {GameRoot} root + * @param {string} variant + * @returns {Array<[string, string]>} + */ + getAdditionalStatistics(root, variant) { + switch (variant) { + case defaultBuildingVariant: { + const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotater); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; + } + case enumRotaterVariants.ccw: { + const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotaterCCW); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; + } + case enumRotaterVariants.rotate180: { + const speed = root.hubGoals.getProcessorBaseSpeed(enumItemProcessorTypes.rotater180); + return [[T.ingame.buildingPlacement.infoTexts.speed, formatItemsPerSecond(speed)]]; + } + } + } + + /** + * + * @param {GameRoot} root + */ + getAvailableVariants(root) { + let variants = [defaultBuildingVariant]; + if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_rotater_ccw)) { + variants.push(enumRotaterVariants.ccw); + } + if (root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_rotater_180)) { + variants.push(enumRotaterVariants.rotate180); + } + return variants; + } + + /** + * @param {GameRoot} root + */ + getIsUnlocked(root) { + return root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_rotater); + } + + /** + * Creates the entity at the given location + * @param {Entity} entity + */ + setupEntityComponents(entity) { + entity.addComponent( + new ItemProcessorComponent({ + inputsPerCharge: 1, + processorType: enumItemProcessorTypes.rotater, + }) + ); + + entity.addComponent( + new ItemEjectorComponent({ + slots: [{ pos: new Vector(0, 0), direction: enumDirection.top }], + }) + ); + entity.addComponent( + new ItemAcceptorComponent({ + slots: [ + { + pos: new Vector(0, 0), + directions: [enumDirection.bottom], + filter: "shape", + }, + ], + }) + ); + } + + /** + * + * @param {Entity} entity + * @param {number} rotationVariant + * @param {string} variant + */ + updateVariants(entity, rotationVariant, variant) { + switch (variant) { + case defaultBuildingVariant: { + entity.components.ItemProcessor.type = enumItemProcessorTypes.rotater; + break; + } + case enumRotaterVariants.ccw: { + entity.components.ItemProcessor.type = enumItemProcessorTypes.rotaterCCW; + break; + } + case enumRotaterVariants.rotate180: { + entity.components.ItemProcessor.type = enumItemProcessorTypes.rotater180; + break; + } + default: + assertAlways(false, "Unknown rotater variant: " + variant); + } + } +} diff --git a/src/js/game/buildings/virtual_processor.js b/src/js/game/buildings/virtual_processor.js index dba8978a..acdb0dbe 100644 --- a/src/js/game/buildings/virtual_processor.js +++ b/src/js/game/buildings/virtual_processor.js @@ -11,6 +11,8 @@ export const enumVirtualProcessorVariants = { rotater: "rotater", unstacker: "unstacker", shapecompare: "shapecompare", + stacker: "stacker", + painter: "painter", }; /** @enum {string} */ @@ -20,6 +22,8 @@ export const enumVariantToGate = { [enumVirtualProcessorVariants.rotater]: enumLogicGateType.rotater, [enumVirtualProcessorVariants.unstacker]: enumLogicGateType.unstacker, [enumVirtualProcessorVariants.shapecompare]: enumLogicGateType.shapecompare, + [enumVirtualProcessorVariants.stacker]: enumLogicGateType.stacker, + [enumVirtualProcessorVariants.painter]: enumLogicGateType.painter, }; export class MetaVirtualProcessorBuilding extends MetaBuilding { @@ -54,6 +58,8 @@ export class MetaVirtualProcessorBuilding extends MetaBuilding { enumVirtualProcessorVariants.rotater, enumVirtualProcessorVariants.unstacker, enumVirtualProcessorVariants.analyzer, + enumVirtualProcessorVariants.stacker, + enumVirtualProcessorVariants.painter, enumVirtualProcessorVariants.shapecompare, ]; } @@ -130,6 +136,27 @@ export class MetaVirtualProcessorBuilding extends MetaBuilding { ]); break; } + case enumLogicGateType.stacker: + case enumLogicGateType.painter: { + pinComp.setSlots([ + { + pos: new Vector(0, 0), + direction: enumDirection.top, + type: enumPinSlotType.logicalEjector, + }, + { + pos: new Vector(0, 0), + direction: enumDirection.bottom, + type: enumPinSlotType.logicalAcceptor, + }, + { + pos: new Vector(0, 0), + direction: enumDirection.right, + type: enumPinSlotType.logicalAcceptor, + }, + ]); + break; + } default: assertAlways("unknown logic gate type: " + gateType); } diff --git a/src/js/game/camera.js b/src/js/game/camera.js index ab73fc83..66d7efa8 100644 --- a/src/js/game/camera.js +++ b/src/js/game/camera.js @@ -1,942 +1,1006 @@ -import { clickDetectorGlobals } from "../core/click_detector"; -import { globalConfig, SUPPORT_TOUCH } from "../core/config"; -import { createLogger } from "../core/logging"; -import { Rectangle } from "../core/rectangle"; -import { Signal, STOP_PROPAGATION } from "../core/signal"; -import { clamp } from "../core/utils"; -import { mixVector, Vector } from "../core/vector"; -import { BasicSerializableObject, types } from "../savegame/serialization"; -import { KEYMAPPINGS } from "./key_action_mapper"; -import { GameRoot } from "./root"; - -const logger = createLogger("camera"); - -export const USER_INTERACT_MOVE = "move"; -export const USER_INTERACT_ZOOM = "zoom"; -export const USER_INTERACT_TOUCHEND = "touchend"; - -const velocitySmoothing = 0.5; -const velocityFade = 0.98; -const velocityStrength = 0.4; -const velocityMax = 20; -const ticksBeforeErasingVelocity = 10; - -/** - * @enum {string} - */ -export const enumMouseButton = { - left: "left", - middle: "middle", - right: "right", -}; - -export class Camera extends BasicSerializableObject { - constructor(root) { - super(); - - /** @type {GameRoot} */ - this.root = root; - - // Zoom level, 2 means double size - - // Find optimal initial zoom - - this.zoomLevel = this.findInitialZoom(); - this.clampZoomLevel(); - - /** @type {Vector} */ - this.center = new Vector(0, 0); - - // Input handling - this.currentlyMoving = false; - this.lastMovingPosition = null; - this.lastMovingPositionLastTick = null; - this.numTicksStandingStill = null; - this.cameraUpdateTimeBucket = 0.0; - this.didMoveSinceTouchStart = false; - this.currentlyPinching = false; - this.lastPinchPositions = null; - - this.keyboardForce = new Vector(); - - // Signal which gets emitted once the user changed something - this.userInteraction = new Signal(); - - /** @type {Vector} */ - this.currentShake = new Vector(0, 0); - - /** @type {Vector} */ - this.currentPan = new Vector(0, 0); - - // Set desired pan (camera movement) - /** @type {Vector} */ - this.desiredPan = new Vector(0, 0); - - // Set desired camera center - /** @type {Vector} */ - this.desiredCenter = null; - - // Set desired camera zoom - /** @type {number} */ - this.desiredZoom = null; - - /** @type {Vector} */ - this.touchPostMoveVelocity = new Vector(0, 0); - - // Handlers - this.downPreHandler = /** @type {TypedSignal<[Vector, enumMouseButton]>} */ (new Signal()); - this.movePreHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); - // this.pinchPreHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); - this.upPostHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); - - this.internalInitEvents(); - this.clampZoomLevel(); - this.bindKeys(); - if (G_IS_DEV) { - window.addEventListener("keydown", ev => { - if (ev.key === "i") { - this.zoomLevel = 3; - } - }); - } - } - - // Serialization - static getId() { - return "Camera"; - } - - static getSchema() { - return { - zoomLevel: types.float, - center: types.vector, - }; - } - - deserialize(data) { - const errorCode = super.deserialize(data); - if (errorCode) { - return errorCode; - } - - // Safety - this.clampZoomLevel(); - } - - // Simple geters & setters - - addScreenShake(amount) { - const currentShakeAmount = this.currentShake.length(); - const scale = 1 / (1 + 3 * currentShakeAmount); - this.currentShake.x = this.currentShake.x + 2 * (Math.random() - 0.5) * scale * amount; - this.currentShake.y = this.currentShake.y + 2 * (Math.random() - 0.5) * scale * amount; - } - - /** - * Sets a point in world space to focus on - * @param {Vector} center - */ - setDesiredCenter(center) { - this.desiredCenter = center.copy(); - this.currentlyMoving = false; - } - - /** - * Sets a desired zoom level - * @param {number} zoom - */ - setDesiredZoom(zoom) { - this.desiredZoom = zoom; - } - - /** - * Returns if this camera is currently moving by a non-user interaction - */ - isCurrentlyMovingToDesiredCenter() { - return this.desiredCenter !== null; - } - - /** - * Sets the camera pan, every frame the camera will move by this amount - * @param {Vector} pan - */ - setPan(pan) { - this.desiredPan = pan.copy(); - } - - /** - * Finds a good initial zoom level - */ - findInitialZoom() { - const desiredWorldSpaceWidth = 15 * globalConfig.tileSize; - const zoomLevelX = this.root.gameWidth / desiredWorldSpaceWidth; - const zoomLevelY = this.root.gameHeight / desiredWorldSpaceWidth; - - const finalLevel = Math.min(zoomLevelX, zoomLevelY); - assert( - Number.isFinite(finalLevel) && finalLevel > 0, - "Invalid zoom level computed for initial zoom: " + finalLevel - ); - return finalLevel; - } - - /** - * Clears all animations - */ - clearAnimations() { - this.touchPostMoveVelocity.x = 0; - this.touchPostMoveVelocity.y = 0; - this.desiredCenter = null; - this.desiredPan.x = 0; - this.desiredPan.y = 0; - this.currentPan.x = 0; - this.currentPan.y = 0; - this.currentlyPinching = false; - this.currentlyMoving = false; - this.lastMovingPosition = null; - this.didMoveSinceTouchStart = false; - this.desiredZoom = null; - } - - /** - * Returns if the user is currently interacting with the camera - * @returns {boolean} true if the user interacts - */ - isCurrentlyInteracting() { - if (this.currentlyPinching) { - return true; - } - if (this.currentlyMoving) { - // Only interacting if moved at least once - return this.didMoveSinceTouchStart; - } - if (this.touchPostMoveVelocity.lengthSquare() > 1) { - return true; - } - return false; - } - - /** - * Returns if in the next frame the viewport will change - * @returns {boolean} true if it willchange - */ - viewportWillChange() { - return this.desiredCenter !== null || this.desiredZoom !== null || this.isCurrentlyInteracting(); - } - - /** - * Cancels all interactions, that is user interaction and non user interaction - */ - cancelAllInteractions() { - this.touchPostMoveVelocity = new Vector(0, 0); - this.desiredCenter = null; - this.currentlyMoving = false; - this.currentlyPinching = false; - this.desiredZoom = null; - } - - /** - * Returns effective viewport width - */ - getViewportWidth() { - return this.root.gameWidth / this.zoomLevel; - } - - /** - * Returns effective viewport height - */ - getViewportHeight() { - return this.root.gameHeight / this.zoomLevel; - } - - /** - * Returns effective world space viewport left - */ - getViewportLeft() { - return this.center.x - this.getViewportWidth() / 2 + (this.currentShake.x * 10) / this.zoomLevel; - } - - /** - * Returns effective world space viewport right - */ - getViewportRight() { - return this.center.x + this.getViewportWidth() / 2 + (this.currentShake.x * 10) / this.zoomLevel; - } - - /** - * Returns effective world space viewport top - */ - getViewportTop() { - return this.center.y - this.getViewportHeight() / 2 + (this.currentShake.x * 10) / this.zoomLevel; - } - - /** - * Returns effective world space viewport bottom - */ - getViewportBottom() { - return this.center.y + this.getViewportHeight() / 2 + (this.currentShake.x * 10) / this.zoomLevel; - } - - /** - * Returns the visible world space rect - * @returns {Rectangle} - */ - getVisibleRect() { - return Rectangle.fromTRBL( - Math.floor(this.getViewportTop()), - Math.ceil(this.getViewportRight()), - Math.ceil(this.getViewportBottom()), - Math.floor(this.getViewportLeft()) - ); - } - - getIsMapOverlayActive() { - return this.zoomLevel < globalConfig.mapChunkOverviewMinZoom; - } - - /** - * Attaches all event listeners - */ - internalInitEvents() { - this.eventListenerTouchStart = this.onTouchStart.bind(this); - this.eventListenerTouchEnd = this.onTouchEnd.bind(this); - this.eventListenerTouchMove = this.onTouchMove.bind(this); - this.eventListenerMousewheel = this.onMouseWheel.bind(this); - this.eventListenerMouseDown = this.onMouseDown.bind(this); - this.eventListenerMouseMove = this.onMouseMove.bind(this); - this.eventListenerMouseUp = this.onMouseUp.bind(this); - - if (SUPPORT_TOUCH) { - this.root.canvas.addEventListener("touchstart", this.eventListenerTouchStart); - this.root.canvas.addEventListener("touchend", this.eventListenerTouchEnd); - this.root.canvas.addEventListener("touchcancel", this.eventListenerTouchEnd); - this.root.canvas.addEventListener("touchmove", this.eventListenerTouchMove); - } - - this.root.canvas.addEventListener("wheel", this.eventListenerMousewheel); - this.root.canvas.addEventListener("mousedown", this.eventListenerMouseDown); - this.root.canvas.addEventListener("mousemove", this.eventListenerMouseMove); - window.addEventListener("mouseup", this.eventListenerMouseUp); - // this.root.canvas.addEventListener("mouseout", this.eventListenerMouseUp); - } - - /** - * Cleans up all event listeners - */ - cleanup() { - if (SUPPORT_TOUCH) { - this.root.canvas.removeEventListener("touchstart", this.eventListenerTouchStart); - this.root.canvas.removeEventListener("touchend", this.eventListenerTouchEnd); - this.root.canvas.removeEventListener("touchcancel", this.eventListenerTouchEnd); - this.root.canvas.removeEventListener("touchmove", this.eventListenerTouchMove); - } - - this.root.canvas.removeEventListener("wheel", this.eventListenerMousewheel); - this.root.canvas.removeEventListener("mousedown", this.eventListenerMouseDown); - this.root.canvas.removeEventListener("mousemove", this.eventListenerMouseMove); - window.removeEventListener("mouseup", this.eventListenerMouseUp); - // this.root.canvas.removeEventListener("mouseout", this.eventListenerMouseUp); - } - - /** - * Binds the arrow keys - */ - bindKeys() { - const mapper = this.root.keyMapper; - mapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).add(() => (this.keyboardForce.y = -1)); - mapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).add(() => (this.keyboardForce.y = 1)); - mapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).add(() => (this.keyboardForce.x = 1)); - mapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).add(() => (this.keyboardForce.x = -1)); - - mapper - .getBinding(KEYMAPPINGS.navigation.mapZoomIn) - .add(() => (this.desiredZoom = this.zoomLevel * 1.2)); - mapper - .getBinding(KEYMAPPINGS.navigation.mapZoomOut) - .add(() => (this.desiredZoom = this.zoomLevel * 0.8)); - - mapper.getBinding(KEYMAPPINGS.navigation.centerMap).add(() => this.centerOnMap()); - } - - centerOnMap() { - this.desiredCenter = new Vector(0, 0); - } - - /** - * Converts from screen to world space - * @param {Vector} screen - * @returns {Vector} world space - */ - screenToWorld(screen) { - const centerSpace = screen.subScalars(this.root.gameWidth / 2, this.root.gameHeight / 2); - return centerSpace.divideScalar(this.zoomLevel).add(this.center); - } - - /** - * Converts from world to screen space - * @param {Vector} world - * @returns {Vector} screen space - */ - worldToScreen(world) { - const screenSpace = world.sub(this.center).multiplyScalar(this.zoomLevel); - return screenSpace.addScalars(this.root.gameWidth / 2, this.root.gameHeight / 2); - } - - /** - * Returns if a point is on screen - * @param {Vector} point - * @returns {boolean} true if its on screen - */ - isWorldPointOnScreen(point) { - const rect = this.getVisibleRect(); - return rect.containsPoint(point.x, point.y); - } - - /** - * Returns if we can further zoom in - * @returns {boolean} - */ - canZoomIn() { - const maxLevel = this.root.app.platformWrapper.getMaximumZoom(); - return this.zoomLevel <= maxLevel - 0.01; - } - - /** - * Returns if we can further zoom out - * @returns {boolean} - */ - canZoomOut() { - const minLevel = this.root.app.platformWrapper.getMinimumZoom(); - return this.zoomLevel >= minLevel + 0.01; - } - - // EVENTS - - /** - * Checks if the mouse event is too close after a touch event and thus - * should get ignored - */ - checkPreventDoubleMouse() { - if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) { - return false; - } - return true; - } - - /** - * Mousedown handler - * @param {MouseEvent} event - */ - onMouseDown(event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - - if (!this.checkPreventDoubleMouse()) { - return; - } - - this.touchPostMoveVelocity = new Vector(0, 0); - if (event.button === 0) { - this.combinedSingleTouchStartHandler(event.clientX, event.clientY); - } else if (event.button === 1) { - this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.middle); - } else if (event.button === 2) { - this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.right); - } - return false; - } - - /** - * Mousemove handler - * @param {MouseEvent} event - */ - onMouseMove(event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - - if (!this.checkPreventDoubleMouse()) { - return; - } - - if (event.button === 0) { - this.combinedSingleTouchMoveHandler(event.clientX, event.clientY); - } - - // Clamp everything afterwards - this.clampZoomLevel(); - return false; - } - - /** - * Mouseup handler - * @param {MouseEvent=} event - */ - onMouseUp(event) { - if (event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - } - - if (!this.checkPreventDoubleMouse()) { - return; - } - - this.combinedSingleTouchStopHandler(event.clientX, event.clientY); - return false; - } - - /** - * Mousewheel event - * @param {WheelEvent} event - */ - onMouseWheel(event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - const prevZoom = this.zoomLevel; - - const delta = Math.sign(event.deltaY) * -0.15 * this.root.app.settings.getScrollWheelSensitivity(); - assert(Number.isFinite(delta), "Got invalid delta in mouse wheel event: " + event.deltaY); - assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *before* wheel: " + this.zoomLevel); - this.zoomLevel *= 1 + delta; - assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *after* wheel: " + this.zoomLevel); - - this.clampZoomLevel(); - this.desiredZoom = null; - - const mousePosition = this.root.app.mousePosition; - if (mousePosition) { - const worldPos = this.root.camera.screenToWorld(mousePosition); - const worldDelta = worldPos.sub(this.center); - const actualDelta = this.zoomLevel / prevZoom - 1; - this.center = this.center.add(worldDelta.multiplyScalar(actualDelta)); - this.desiredCenter = null; - } - - return false; - } - - /** - * Touch start handler - * @param {TouchEvent} event - */ - onTouchStart(event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - - clickDetectorGlobals.lastTouchTime = performance.now(); - this.touchPostMoveVelocity = new Vector(0, 0); - - if (event.touches.length === 1) { - const touch = event.touches[0]; - this.combinedSingleTouchStartHandler(touch.clientX, touch.clientY); - } else if (event.touches.length === 2) { - // if (this.pinchPreHandler.dispatch() === STOP_PROPAGATION) { - // // Something prevented pinching - // return false; - // } - - const touch1 = event.touches[0]; - const touch2 = event.touches[1]; - this.currentlyMoving = false; - this.currentlyPinching = true; - this.lastPinchPositions = [ - new Vector(touch1.clientX, touch1.clientY), - new Vector(touch2.clientX, touch2.clientY), - ]; - } - return false; - } - - /** - * Touch move handler - * @param {TouchEvent} event - */ - onTouchMove(event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - - clickDetectorGlobals.lastTouchTime = performance.now(); - - if (event.touches.length === 1) { - const touch = event.touches[0]; - this.combinedSingleTouchMoveHandler(touch.clientX, touch.clientY); - } else if (event.touches.length === 2) { - if (this.currentlyPinching) { - const touch1 = event.touches[0]; - const touch2 = event.touches[1]; - - const newPinchPositions = [ - new Vector(touch1.clientX, touch1.clientY), - new Vector(touch2.clientX, touch2.clientY), - ]; - - // Get distance of taps last time and now - const lastDistance = this.lastPinchPositions[0].distance(this.lastPinchPositions[1]); - const thisDistance = newPinchPositions[0].distance(newPinchPositions[1]); - - // IMPORTANT to do math max here to avoid NaN and causing an invalid zoom level - const difference = thisDistance / Math.max(0.001, lastDistance); - - // Find old center of zoom - let oldCenter = this.lastPinchPositions[0].centerPoint(this.lastPinchPositions[1]); - - // Find new center of zoom - let center = newPinchPositions[0].centerPoint(newPinchPositions[1]); - - // Compute movement - let movement = oldCenter.sub(center); - this.center.x += movement.x / this.zoomLevel; - this.center.y += movement.y / this.zoomLevel; - - // Compute zoom - center = center.sub(new Vector(this.root.gameWidth / 2, this.root.gameHeight / 2)); - - // Apply zoom - assert( - Number.isFinite(difference), - "Invalid pinch difference: " + - difference + - "(last=" + - lastDistance + - ", new = " + - thisDistance + - ")" - ); - this.zoomLevel *= difference; - - // Stick to pivot point - const correcture = center.multiplyScalar(difference - 1).divideScalar(this.zoomLevel); - - this.center = this.center.add(correcture); - this.lastPinchPositions = newPinchPositions; - this.userInteraction.dispatch(USER_INTERACT_MOVE); - - // Since we zoomed, abort any programmed zooming - if (this.desiredZoom) { - this.desiredZoom = null; - } - } - } - - // Clamp everything afterwards - this.clampZoomLevel(); - return false; - } - - /** - * Touch end and cancel handler - * @param {TouchEvent=} event - */ - onTouchEnd(event) { - if (event) { - if (event.cancelable) { - event.preventDefault(); - // event.stopPropagation(); - } - } - - clickDetectorGlobals.lastTouchTime = performance.now(); - if (event.changedTouches.length === 0) { - logger.warn("Touch end without changed touches"); - } - - const touch = event.changedTouches[0]; - this.combinedSingleTouchStopHandler(touch.clientX, touch.clientY); - return false; - } - - /** - * Internal touch start handler - * @param {number} x - * @param {number} y - */ - combinedSingleTouchStartHandler(x, y) { - const pos = new Vector(x, y); - if (this.downPreHandler.dispatch(pos, enumMouseButton.left) === STOP_PROPAGATION) { - // Somebody else captured it - return; - } - - this.touchPostMoveVelocity = new Vector(0, 0); - this.currentlyMoving = true; - this.lastMovingPosition = pos; - this.lastMovingPositionLastTick = null; - this.numTicksStandingStill = 0; - this.didMoveSinceTouchStart = false; - } - - /** - * Internal touch move handler - * @param {number} x - * @param {number} y - */ - combinedSingleTouchMoveHandler(x, y) { - const pos = new Vector(x, y); - if (this.movePreHandler.dispatch(pos) === STOP_PROPAGATION) { - // Somebody else captured it - return; - } - - if (!this.currentlyMoving) { - return false; - } - - let delta = this.lastMovingPosition.sub(pos).divideScalar(this.zoomLevel); - if (G_IS_DEV && globalConfig.debug.testCulling) { - // When testing culling, we see everything from the same distance - delta = delta.multiplyScalar(this.zoomLevel * -2); - } - - this.didMoveSinceTouchStart = this.didMoveSinceTouchStart || delta.length() > 0; - this.center = this.center.add(delta); - - this.touchPostMoveVelocity = this.touchPostMoveVelocity - .multiplyScalar(velocitySmoothing) - .add(delta.multiplyScalar(1 - velocitySmoothing)); - - this.lastMovingPosition = pos; - this.userInteraction.dispatch(USER_INTERACT_MOVE); - - // Since we moved, abort any programmed moving - if (this.desiredCenter) { - this.desiredCenter = null; - } - } - - /** - * Internal touch stop handler - */ - combinedSingleTouchStopHandler(x, y) { - if (this.currentlyMoving || this.currentlyPinching) { - this.currentlyMoving = false; - this.currentlyPinching = false; - this.lastMovingPosition = null; - this.lastMovingPositionLastTick = null; - this.numTicksStandingStill = 0; - this.lastPinchPositions = null; - this.userInteraction.dispatch(USER_INTERACT_TOUCHEND); - this.didMoveSinceTouchStart = false; - } - this.upPostHandler.dispatch(new Vector(x, y)); - } - - /** - * Clamps the camera zoom level within the allowed range - */ - clampZoomLevel() { - if (G_IS_DEV && globalConfig.debug.disableZoomLimits) { - return; - } - const wrapper = this.root.app.platformWrapper; - - assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *before* clamp: " + this.zoomLevel); - this.zoomLevel = clamp(this.zoomLevel, wrapper.getMinimumZoom(), wrapper.getMaximumZoom()); - assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *after* clamp: " + this.zoomLevel); - - if (this.desiredZoom) { - this.desiredZoom = clamp(this.desiredZoom, wrapper.getMinimumZoom(), wrapper.getMaximumZoom()); - } - } - - /** - * Updates the camera - * @param {number} dt Delta time in milliseconds - */ - update(dt) { - dt = Math.min(dt, 33); - this.cameraUpdateTimeBucket += dt; - - // Simulate movement of N FPS - const updatesPerFrame = 4; - const physicsStepSizeMs = 1000.0 / (60.0 * updatesPerFrame); - - let now = this.root.time.systemNow() - 3 * physicsStepSizeMs; - - while (this.cameraUpdateTimeBucket > physicsStepSizeMs) { - now += physicsStepSizeMs; - this.cameraUpdateTimeBucket -= physicsStepSizeMs; - - this.internalUpdatePanning(now, physicsStepSizeMs); - this.internalUpdateZooming(now, physicsStepSizeMs); - this.internalUpdateCentering(now, physicsStepSizeMs); - this.internalUpdateShake(now, physicsStepSizeMs); - this.internalUpdateKeyboardForce(now, physicsStepSizeMs); - } - this.clampZoomLevel(); - } - - /** - * Prepares a context to transform it - * @param {CanvasRenderingContext2D} context - */ - transform(context) { - if (G_IS_DEV && globalConfig.debug.testCulling) { - context.transform(1, 0, 0, 1, 100, 100); - return; - } - - this.clampZoomLevel(); - const zoom = this.zoomLevel; - - context.transform( - // Scale, skew, rotate - zoom, - 0, - 0, - zoom, - - // Translate - -zoom * this.getViewportLeft(), - -zoom * this.getViewportTop() - ); - } - - /** - * Internal shake handler - * @param {number} now Time now in seconds - * @param {number} dt Delta time - */ - internalUpdateShake(now, dt) { - this.currentShake = this.currentShake.multiplyScalar(0.92); - } - - /** - * Internal pan handler - * @param {number} now Time now in seconds - * @param {number} dt Delta time - */ - internalUpdatePanning(now, dt) { - const baseStrength = velocityStrength * this.root.app.platformWrapper.getTouchPanStrength(); - - this.touchPostMoveVelocity = this.touchPostMoveVelocity.multiplyScalar(velocityFade); - - // Check if the camera is being dragged but standing still: if not, zero out `touchPostMoveVelocity`. - if (this.currentlyMoving && this.desiredCenter === null) { - if ( - this.lastMovingPositionLastTick !== null && - this.lastMovingPositionLastTick.equalsEpsilon(this.lastMovingPosition) - ) { - this.numTicksStandingStill++; - } else { - this.numTicksStandingStill = 0; - } - this.lastMovingPositionLastTick = this.lastMovingPosition.copy(); - - if (this.numTicksStandingStill >= ticksBeforeErasingVelocity) { - this.touchPostMoveVelocity.x = 0; - this.touchPostMoveVelocity.y = 0; - } - } - // Check influence of past points - if (!this.currentlyMoving && !this.currentlyPinching) { - const len = this.touchPostMoveVelocity.length(); - if (len >= velocityMax) { - this.touchPostMoveVelocity.x = (this.touchPostMoveVelocity.x * velocityMax) / len; - this.touchPostMoveVelocity.y = (this.touchPostMoveVelocity.y * velocityMax) / len; - } - - this.center = this.center.add(this.touchPostMoveVelocity.multiplyScalar(baseStrength)); - - // Panning - this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06); - this.center = this.center.add(this.currentPan.multiplyScalar((0.5 * dt) / this.zoomLevel)); - } - } - - /** - * Updates the non user interaction zooming - * @param {number} now Time now in seconds - * @param {number} dt Delta time - */ - internalUpdateZooming(now, dt) { - if (!this.currentlyPinching && this.desiredZoom !== null) { - const diff = this.zoomLevel - this.desiredZoom; - if (Math.abs(diff) > 0.0001) { - let fade = 0.94; - if (diff > 0) { - // Zoom out faster than in - fade = 0.9; - } - - assert(Number.isFinite(this.desiredZoom), "Desired zoom is NaN: " + this.desiredZoom); - assert(Number.isFinite(fade), "Zoom fade is NaN: " + fade); - this.zoomLevel = this.zoomLevel * fade + this.desiredZoom * (1 - fade); - assert(Number.isFinite(this.zoomLevel), "Zoom level is NaN after fade: " + this.zoomLevel); - } else { - this.desiredZoom = null; - } - } - } - - /** - * Updates the non user interaction centering - * @param {number} now Time now in seconds - * @param {number} dt Delta time - */ - internalUpdateCentering(now, dt) { - if (!this.currentlyMoving && this.desiredCenter !== null) { - const diff = this.center.direction(this.desiredCenter); - const length = diff.length(); - const tolerance = 1 / this.zoomLevel; - if (length > tolerance) { - const movement = diff.multiplyScalar(Math.min(1, dt * 0.008)); - this.center.x += movement.x; - this.center.y += movement.y; - } else { - this.desiredCenter = null; - } - } - } - - /** - * Updates the keyboard forces - * @param {number} now - * @param {number} dt Delta time - */ - internalUpdateKeyboardForce(now, dt) { - if (!this.currentlyMoving && this.desiredCenter == null) { - const limitingDimension = Math.min(this.root.gameWidth, this.root.gameHeight); - - const moveAmount = ((limitingDimension / 2048) * dt) / this.zoomLevel; - - let forceX = 0; - let forceY = 0; - - const actionMapper = this.root.keyMapper; - if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).pressed) { - forceY -= 1; - } - - if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).pressed) { - forceY += 1; - } - - if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).pressed) { - forceX -= 1; - } - - if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).pressed) { - forceX += 1; - } - - let movementSpeed = - this.root.app.settings.getMovementSpeed() * - (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveFaster).pressed ? 4 : 1); - - this.center.x += moveAmount * forceX * movementSpeed; - this.center.y += moveAmount * forceY * movementSpeed; - } - } -} +import { clickDetectorGlobals } from "../core/click_detector"; +import { globalConfig, SUPPORT_TOUCH } from "../core/config"; +import { createLogger } from "../core/logging"; +import { Rectangle } from "../core/rectangle"; +import { Signal, STOP_PROPAGATION } from "../core/signal"; +import { clamp } from "../core/utils"; +import { mixVector, Vector } from "../core/vector"; +import { BasicSerializableObject, types } from "../savegame/serialization"; +import { KEYMAPPINGS } from "./key_action_mapper"; +import { GameRoot } from "./root"; + +const logger = createLogger("camera"); + +export const USER_INTERACT_MOVE = "move"; +export const USER_INTERACT_ZOOM = "zoom"; +export const USER_INTERACT_TOUCHEND = "touchend"; + +const velocitySmoothing = 0.5; +const velocityFade = 0.98; +const velocityStrength = 0.4; +const velocityMax = 20; +const ticksBeforeErasingVelocity = 10; + +/** + * @enum {string} + */ +export const enumMouseButton = { + left: "left", + middle: "middle", + right: "right", +}; + +export class Camera extends BasicSerializableObject { + constructor(root) { + super(); + + /** @type {GameRoot} */ + this.root = root; + + // Zoom level, 2 means double size + + // Find optimal initial zoom + + this.zoomLevel = this.findInitialZoom(); + this.clampZoomLevel(); + + /** @type {Vector} */ + this.center = new Vector(0, 0); + + // Input handling + this.currentlyMoving = false; + this.lastMovingPosition = null; + this.lastMovingPositionLastTick = null; + this.numTicksStandingStill = null; + this.cameraUpdateTimeBucket = 0.0; + this.didMoveSinceTouchStart = false; + this.currentlyPinching = false; + this.lastPinchPositions = null; + + this.keyboardForce = new Vector(); + + // Signal which gets emitted once the user changed something + this.userInteraction = new Signal(); + + /** @type {Vector} */ + this.currentShake = new Vector(0, 0); + + /** @type {Vector} */ + this.currentPan = new Vector(0, 0); + + // Set desired pan (camera movement) + /** @type {Vector} */ + this.desiredPan = new Vector(0, 0); + + // Set desired camera center + /** @type {Vector} */ + this.desiredCenter = null; + + // Set desired camera zoom + /** @type {number} */ + this.desiredZoom = null; + + /** @type {Vector} */ + this.touchPostMoveVelocity = new Vector(0, 0); + + // Handlers + this.downPreHandler = /** @type {TypedSignal<[Vector, enumMouseButton]>} */ (new Signal()); + this.movePreHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); + // this.pinchPreHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); + this.upPostHandler = /** @type {TypedSignal<[Vector]>} */ (new Signal()); + + this.internalInitEvents(); + this.clampZoomLevel(); + this.bindKeys(); + if (G_IS_DEV) { + window.addEventListener("keydown", ev => { + if (ev.key === "i") { + this.zoomLevel = 3; + } + }); + } + } + + // Serialization + static getId() { + return "Camera"; + } + + static getSchema() { + return { + zoomLevel: types.float, + center: types.vector, + }; + } + + deserialize(data) { + const errorCode = super.deserialize(data); + if (errorCode) { + return errorCode; + } + + // Safety + this.clampZoomLevel(); + } + + // Simple getters & setters + + addScreenShake(amount) { + const currentShakeAmount = this.currentShake.length(); + const scale = 1 / (1 + 3 * currentShakeAmount); + this.currentShake.x = this.currentShake.x + 2 * (Math.random() - 0.5) * scale * amount; + this.currentShake.y = this.currentShake.y + 2 * (Math.random() - 0.5) * scale * amount; + } + + /** + * Sets a point in world space to focus on + * @param {Vector} center + */ + setDesiredCenter(center) { + this.desiredCenter = center.copy(); + this.currentlyMoving = false; + } + + /** + * Sets a desired zoom level + * @param {number} zoom + */ + setDesiredZoom(zoom) { + this.desiredZoom = zoom; + } + + /** + * Returns if this camera is currently moving by a non-user interaction + */ + isCurrentlyMovingToDesiredCenter() { + return this.desiredCenter !== null; + } + + /** + * Sets the camera pan, every frame the camera will move by this amount + * @param {Vector} pan + */ + setPan(pan) { + this.desiredPan = pan.copy(); + } + + /** + * Finds a good initial zoom level + */ + findInitialZoom() { + const desiredWorldSpaceWidth = 15 * globalConfig.tileSize; + const zoomLevelX = this.root.gameWidth / desiredWorldSpaceWidth; + const zoomLevelY = this.root.gameHeight / desiredWorldSpaceWidth; + + const finalLevel = Math.min(zoomLevelX, zoomLevelY); + assert( + Number.isFinite(finalLevel) && finalLevel > 0, + "Invalid zoom level computed for initial zoom: " + finalLevel + ); + return finalLevel; + } + + /** + * Clears all animations + */ + clearAnimations() { + this.touchPostMoveVelocity.x = 0; + this.touchPostMoveVelocity.y = 0; + this.desiredCenter = null; + this.desiredPan.x = 0; + this.desiredPan.y = 0; + this.currentPan.x = 0; + this.currentPan.y = 0; + this.currentlyPinching = false; + this.currentlyMoving = false; + this.lastMovingPosition = null; + this.didMoveSinceTouchStart = false; + this.desiredZoom = null; + } + + /** + * Returns if the user is currently interacting with the camera + * @returns {boolean} true if the user interacts + */ + isCurrentlyInteracting() { + if (this.currentlyPinching) { + return true; + } + if (this.currentlyMoving) { + // Only interacting if moved at least once + return this.didMoveSinceTouchStart; + } + if (this.touchPostMoveVelocity.lengthSquare() > 1) { + return true; + } + return false; + } + + /** + * Returns if in the next frame the viewport will change + * @returns {boolean} true if it willchange + */ + viewportWillChange() { + return this.desiredCenter !== null || this.desiredZoom !== null || this.isCurrentlyInteracting(); + } + + /** + * Cancels all interactions, that is user interaction and non user interaction + */ + cancelAllInteractions() { + this.touchPostMoveVelocity = new Vector(0, 0); + this.desiredCenter = null; + this.currentlyMoving = false; + this.currentlyPinching = false; + this.desiredZoom = null; + } + + /** + * Returns effective viewport width + */ + getViewportWidth() { + return this.root.gameWidth / this.zoomLevel; + } + + /** + * Returns effective viewport height + */ + getViewportHeight() { + return this.root.gameHeight / this.zoomLevel; + } + + /** + * Returns effective world space viewport left + */ + getViewportLeft() { + return this.center.x - this.getViewportWidth() / 2 + (this.currentShake.x * 10) / this.zoomLevel; + } + + /** + * Returns effective world space viewport right + */ + getViewportRight() { + return this.center.x + this.getViewportWidth() / 2 + (this.currentShake.x * 10) / this.zoomLevel; + } + + /** + * Returns effective world space viewport top + */ + getViewportTop() { + return this.center.y - this.getViewportHeight() / 2 + (this.currentShake.x * 10) / this.zoomLevel; + } + + /** + * Returns effective world space viewport bottom + */ + getViewportBottom() { + return this.center.y + this.getViewportHeight() / 2 + (this.currentShake.x * 10) / this.zoomLevel; + } + + /** + * Returns the visible world space rect + * @returns {Rectangle} + */ + getVisibleRect() { + return Rectangle.fromTRBL( + Math.floor(this.getViewportTop()), + Math.ceil(this.getViewportRight()), + Math.ceil(this.getViewportBottom()), + Math.floor(this.getViewportLeft()) + ); + } + + getIsMapOverlayActive() { + return this.zoomLevel < globalConfig.mapChunkOverviewMinZoom; + } + + /** + * Attaches all event listeners + */ + internalInitEvents() { + this.eventListenerTouchStart = this.onTouchStart.bind(this); + this.eventListenerTouchEnd = this.onTouchEnd.bind(this); + this.eventListenerTouchMove = this.onTouchMove.bind(this); + this.eventListenerMousewheel = this.onMouseWheel.bind(this); + this.eventListenerMouseDown = this.onMouseDown.bind(this); + this.eventListenerMouseMove = this.onMouseMove.bind(this); + this.eventListenerMouseUp = this.onMouseUp.bind(this); + + if (SUPPORT_TOUCH) { + this.root.canvas.addEventListener("touchstart", this.eventListenerTouchStart); + this.root.canvas.addEventListener("touchend", this.eventListenerTouchEnd); + this.root.canvas.addEventListener("touchcancel", this.eventListenerTouchEnd); + this.root.canvas.addEventListener("touchmove", this.eventListenerTouchMove); + } + + this.root.canvas.addEventListener("wheel", this.eventListenerMousewheel); + this.root.canvas.addEventListener("mousedown", this.eventListenerMouseDown); + this.root.canvas.addEventListener("mousemove", this.eventListenerMouseMove); + window.addEventListener("mouseup", this.eventListenerMouseUp); + // this.root.canvas.addEventListener("mouseout", this.eventListenerMouseUp); + } + + /** + * Cleans up all event listeners + */ + cleanup() { + if (SUPPORT_TOUCH) { + this.root.canvas.removeEventListener("touchstart", this.eventListenerTouchStart); + this.root.canvas.removeEventListener("touchend", this.eventListenerTouchEnd); + this.root.canvas.removeEventListener("touchcancel", this.eventListenerTouchEnd); + this.root.canvas.removeEventListener("touchmove", this.eventListenerTouchMove); + } + + this.root.canvas.removeEventListener("wheel", this.eventListenerMousewheel); + this.root.canvas.removeEventListener("mousedown", this.eventListenerMouseDown); + this.root.canvas.removeEventListener("mousemove", this.eventListenerMouseMove); + window.removeEventListener("mouseup", this.eventListenerMouseUp); + // this.root.canvas.removeEventListener("mouseout", this.eventListenerMouseUp); + } + + /** + * Binds the arrow keys + */ + bindKeys() { + const mapper = this.root.keyMapper; + mapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).add(() => (this.keyboardForce.y = -1)); + mapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).add(() => (this.keyboardForce.y = 1)); + mapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).add(() => (this.keyboardForce.x = 1)); + mapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).add(() => (this.keyboardForce.x = -1)); + + mapper + .getBinding(KEYMAPPINGS.navigation.mapZoomIn) + .add(() => (this.desiredZoom = this.zoomLevel * 1.2)); + mapper + .getBinding(KEYMAPPINGS.navigation.mapZoomOut) + .add(() => (this.desiredZoom = this.zoomLevel * 0.8)); + + mapper.getBinding(KEYMAPPINGS.navigation.centerMap).add(() => this.centerOnMap()); + } + + centerOnMap() { + this.desiredCenter = new Vector(0, 0); + } + + /** + * Converts from screen to world space + * @param {Vector} screen + * @returns {Vector} world space + */ + screenToWorld(screen) { + const centerSpace = screen.subScalars(this.root.gameWidth / 2, this.root.gameHeight / 2); + return centerSpace.divideScalar(this.zoomLevel).add(this.center); + } + + /** + * Converts from world to screen space + * @param {Vector} world + * @returns {Vector} screen space + */ + worldToScreen(world) { + const screenSpace = world.sub(this.center).multiplyScalar(this.zoomLevel); + return screenSpace.addScalars(this.root.gameWidth / 2, this.root.gameHeight / 2); + } + + /** + * Returns if a point is on screen + * @param {Vector} point + * @returns {boolean} true if its on screen + */ + isWorldPointOnScreen(point) { + const rect = this.getVisibleRect(); + return rect.containsPoint(point.x, point.y); + } + + /** + * Returns if we can further zoom in + * @returns {boolean} + */ + canZoomIn() { + const maxLevel = this.root.app.platformWrapper.getMaximumZoom(); + return this.zoomLevel <= maxLevel - 0.01; + } + + /** + * Returns if we can further zoom out + * @returns {boolean} + */ + canZoomOut() { + const minLevel = this.root.app.platformWrapper.getMinimumZoom(); + return this.zoomLevel >= minLevel + 0.01; + } + + // EVENTS + + /** + * Checks if the mouse event is too close after a touch event and thus + * should get ignored + */ + checkPreventDoubleMouse() { + if (performance.now() - clickDetectorGlobals.lastTouchTime < 1000.0) { + return false; + } + return true; + } + + /** + * Mousedown handler + * @param {MouseEvent} event + */ + onMouseDown(event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + + if (!this.checkPreventDoubleMouse()) { + return; + } + + this.touchPostMoveVelocity = new Vector(0, 0); + if (event.button === 0) { + this.combinedSingleTouchStartHandler(event.clientX, event.clientY); + } else if (event.button === 1) { + this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.middle); + } else if (event.button === 2) { + this.downPreHandler.dispatch(new Vector(event.clientX, event.clientY), enumMouseButton.right); + } + return false; + } + + /** + * Mousemove handler + * @param {MouseEvent} event + */ + onMouseMove(event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + + if (!this.checkPreventDoubleMouse()) { + return; + } + + if (event.button === 0) { + this.combinedSingleTouchMoveHandler(event.clientX, event.clientY); + } + + // Clamp everything afterwards + this.clampZoomLevel(); + return false; + } + + /** + * Mouseup handler + * @param {MouseEvent=} event + */ + onMouseUp(event) { + if (event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + } + + if (!this.checkPreventDoubleMouse()) { + return; + } + + this.combinedSingleTouchStopHandler(event.clientX, event.clientY); + return false; + } + + /** + * Mousewheel event + * @param {WheelEvent} event + */ + onMouseWheel(event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + const prevZoom = this.zoomLevel; + + const delta = Math.sign(event.deltaY) * -0.15 * this.root.app.settings.getScrollWheelSensitivity(); + assert(Number.isFinite(delta), "Got invalid delta in mouse wheel event: " + event.deltaY); + assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *before* wheel: " + this.zoomLevel); + this.zoomLevel *= 1 + delta; + assert(Number.isFinite(this.zoomLevel), "Got invalid zoom level *after* wheel: " + this.zoomLevel); + + this.clampZoomLevel(); + this.desiredZoom = null; + + const mousePosition = this.root.app.mousePosition; + if (mousePosition) { + const worldPos = this.root.camera.screenToWorld(mousePosition); + const worldDelta = worldPos.sub(this.center); + const actualDelta = this.zoomLevel / prevZoom - 1; + this.center = this.center.add(worldDelta.multiplyScalar(actualDelta)); + this.desiredCenter = null; + } + + return false; + } + + /** + * Touch start handler + * @param {TouchEvent} event + */ + onTouchStart(event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + + clickDetectorGlobals.lastTouchTime = performance.now(); + this.touchPostMoveVelocity = new Vector(0, 0); + + if (event.touches.length === 1) { + const touch = event.touches[0]; + this.combinedSingleTouchStartHandler(touch.clientX, touch.clientY); + } else if (event.touches.length === 2) { + // if (this.pinchPreHandler.dispatch() === STOP_PROPAGATION) { + // // Something prevented pinching + // return false; + // } + + const touch1 = event.touches[0]; + const touch2 = event.touches[1]; + this.currentlyMoving = false; + this.currentlyPinching = true; + this.lastPinchPositions = [ + new Vector(touch1.clientX, touch1.clientY), + new Vector(touch2.clientX, touch2.clientY), + ]; + } + return false; + } + + /** + * Touch move handler + * @param {TouchEvent} event + */ + onTouchMove(event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + + clickDetectorGlobals.lastTouchTime = performance.now(); + + if (event.touches.length === 1) { + const touch = event.touches[0]; + this.combinedSingleTouchMoveHandler(touch.clientX, touch.clientY); + } else if (event.touches.length === 2) { + if (this.currentlyPinching) { + const touch1 = event.touches[0]; + const touch2 = event.touches[1]; + + const newPinchPositions = [ + new Vector(touch1.clientX, touch1.clientY), + new Vector(touch2.clientX, touch2.clientY), + ]; + + // Get distance of taps last time and now + const lastDistance = this.lastPinchPositions[0].distance(this.lastPinchPositions[1]); + const thisDistance = newPinchPositions[0].distance(newPinchPositions[1]); + + // IMPORTANT to do math max here to avoid NaN and causing an invalid zoom level + const difference = thisDistance / Math.max(0.001, lastDistance); + + // Find old center of zoom + let oldCenter = this.lastPinchPositions[0].centerPoint(this.lastPinchPositions[1]); + + // Find new center of zoom + let center = newPinchPositions[0].centerPoint(newPinchPositions[1]); + + // Compute movement + let movement = oldCenter.sub(center); + this.center.x += movement.x / this.zoomLevel; + this.center.y += movement.y / this.zoomLevel; + + // Compute zoom + center = center.sub(new Vector(this.root.gameWidth / 2, this.root.gameHeight / 2)); + + // Apply zoom + assert( + Number.isFinite(difference), + "Invalid pinch difference: " + + difference + + "(last=" + + lastDistance + + ", new = " + + thisDistance + + ")" + ); + this.zoomLevel *= difference; + + // Stick to pivot point + const correcture = center.multiplyScalar(difference - 1).divideScalar(this.zoomLevel); + + this.center = this.center.add(correcture); + this.lastPinchPositions = newPinchPositions; + this.userInteraction.dispatch(USER_INTERACT_MOVE); + + // Since we zoomed, abort any programmed zooming + if (this.desiredZoom) { + this.desiredZoom = null; + } + } + } + + // Clamp everything afterwards + this.clampZoomLevel(); + return false; + } + + /** + * Touch end and cancel handler + * @param {TouchEvent=} event + */ + onTouchEnd(event) { + if (event) { + if (event.cancelable) { + event.preventDefault(); + // event.stopPropagation(); + } + } + + clickDetectorGlobals.lastTouchTime = performance.now(); + if (event.changedTouches.length === 0) { + logger.warn("Touch end without changed touches"); + } + + const touch = event.changedTouches[0]; + this.combinedSingleTouchStopHandler(touch.clientX, touch.clientY); + return false; + } + + /** + * Internal touch start handler + * @param {number} x + * @param {number} y + */ + combinedSingleTouchStartHandler(x, y) { + const pos = new Vector(x, y); + if (this.downPreHandler.dispatch(pos, enumMouseButton.left) === STOP_PROPAGATION) { + // Somebody else captured it + return; + } + + this.touchPostMoveVelocity = new Vector(0, 0); + this.currentlyMoving = true; + this.lastMovingPosition = pos; + this.lastMovingPositionLastTick = null; + this.numTicksStandingStill = 0; + this.didMoveSinceTouchStart = false; + } + + /** + * Internal touch move handler + * @param {number} x + * @param {number} y + */ + combinedSingleTouchMoveHandler(x, y) { + const pos = new Vector(x, y); + if (this.movePreHandler.dispatch(pos) === STOP_PROPAGATION) { + // Somebody else captured it + return; + } + + if (!this.currentlyMoving) { + return false; + } + + let delta = this.lastMovingPosition.sub(pos).divideScalar(this.zoomLevel); + if (G_IS_DEV && globalConfig.debug.testCulling) { + // When testing culling, we see everything from the same distance + delta = delta.multiplyScalar(this.zoomLevel * -2); + } + + this.didMoveSinceTouchStart = this.didMoveSinceTouchStart || delta.length() > 0; + this.center = this.center.add(delta); + + this.touchPostMoveVelocity = this.touchPostMoveVelocity + .multiplyScalar(velocitySmoothing) + .add(delta.multiplyScalar(1 - velocitySmoothing)); + + this.lastMovingPosition = pos; + this.userInteraction.dispatch(USER_INTERACT_MOVE); + + // Since we moved, abort any programmed moving + if (this.desiredCenter) { + this.desiredCenter = null; + } + } + + /** + * Internal touch stop handler + */ + combinedSingleTouchStopHandler(x, y) { + if (this.currentlyMoving || this.currentlyPinching) { + this.currentlyMoving = false; + this.currentlyPinching = false; + this.lastMovingPosition = null; + this.lastMovingPositionLastTick = null; + this.numTicksStandingStill = 0; + this.lastPinchPositions = null; + this.userInteraction.dispatch(USER_INTERACT_TOUCHEND); + this.didMoveSinceTouchStart = false; + } + this.upPostHandler.dispatch(new Vector(x, y)); + } + + /** + * Clamps the camera zoom level within the allowed range + */ + clampZoomLevel() { + if (G_IS_DEV && globalConfig.debug.disableZoomLimits) { + return; + } + const wrapper = this.root.app.platformWrapper; + + assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *before* clamp: " + this.zoomLevel); + this.zoomLevel = clamp(this.zoomLevel, wrapper.getMinimumZoom(), wrapper.getMaximumZoom()); + assert(Number.isFinite(this.zoomLevel), "Invalid zoom level *after* clamp: " + this.zoomLevel); + + if (this.desiredZoom) { + this.desiredZoom = clamp(this.desiredZoom, wrapper.getMinimumZoom(), wrapper.getMaximumZoom()); + } + } + + /** + * Updates the camera + * @param {number} dt Delta time in milliseconds + */ + update(dt) { + dt = Math.min(dt, 33); + this.cameraUpdateTimeBucket += dt; + + // Simulate movement of N FPS + const updatesPerFrame = 4; + const physicsStepSizeMs = 1000.0 / (60.0 * updatesPerFrame); + + let now = this.root.time.systemNow() - 3 * physicsStepSizeMs; + + while (this.cameraUpdateTimeBucket > physicsStepSizeMs) { + now += physicsStepSizeMs; + this.cameraUpdateTimeBucket -= physicsStepSizeMs; + + this.internalUpdatePanning(now, physicsStepSizeMs); + this.internalUpdateMousePanning(now, physicsStepSizeMs); + this.internalUpdateZooming(now, physicsStepSizeMs); + this.internalUpdateCentering(now, physicsStepSizeMs); + this.internalUpdateShake(now, physicsStepSizeMs); + this.internalUpdateKeyboardForce(now, physicsStepSizeMs); + } + this.clampZoomLevel(); + } + + /** + * Prepares a context to transform it + * @param {CanvasRenderingContext2D} context + */ + transform(context) { + if (G_IS_DEV && globalConfig.debug.testCulling) { + context.transform(1, 0, 0, 1, 100, 100); + return; + } + + this.clampZoomLevel(); + const zoom = this.zoomLevel; + + context.transform( + // Scale, skew, rotate + zoom, + 0, + 0, + zoom, + + // Translate + -zoom * this.getViewportLeft(), + -zoom * this.getViewportTop() + ); + } + + /** + * Internal shake handler + * @param {number} now Time now in seconds + * @param {number} dt Delta time + */ + internalUpdateShake(now, dt) { + this.currentShake = this.currentShake.multiplyScalar(0.92); + } + + /** + * Internal pan handler + * @param {number} now Time now in seconds + * @param {number} dt Delta time + */ + internalUpdatePanning(now, dt) { + const baseStrength = velocityStrength * this.root.app.platformWrapper.getTouchPanStrength(); + + this.touchPostMoveVelocity = this.touchPostMoveVelocity.multiplyScalar(velocityFade); + + // Check if the camera is being dragged but standing still: if not, zero out `touchPostMoveVelocity`. + if (this.currentlyMoving && this.desiredCenter === null) { + if ( + this.lastMovingPositionLastTick !== null && + this.lastMovingPositionLastTick.equalsEpsilon(this.lastMovingPosition) + ) { + this.numTicksStandingStill++; + } else { + this.numTicksStandingStill = 0; + } + this.lastMovingPositionLastTick = this.lastMovingPosition.copy(); + + if (this.numTicksStandingStill >= ticksBeforeErasingVelocity) { + this.touchPostMoveVelocity.x = 0; + this.touchPostMoveVelocity.y = 0; + } + } + // Check influence of past points + if (!this.currentlyMoving && !this.currentlyPinching) { + const len = this.touchPostMoveVelocity.length(); + if (len >= velocityMax) { + this.touchPostMoveVelocity.x = (this.touchPostMoveVelocity.x * velocityMax) / len; + this.touchPostMoveVelocity.y = (this.touchPostMoveVelocity.y * velocityMax) / len; + } + + this.center = this.center.add(this.touchPostMoveVelocity.multiplyScalar(baseStrength)); + + // Panning + this.currentPan = mixVector(this.currentPan, this.desiredPan, 0.06); + this.center = this.center.add(this.currentPan.multiplyScalar((0.5 * dt) / this.zoomLevel)); + } + } + + /** + * Internal screen panning handler + * @param {number} now + * @param {number} dt + */ + internalUpdateMousePanning(now, dt) { + if (!this.root.app.focused) { + return; + } + + if (!this.root.app.settings.getAllSettings().enableMousePan) { + // Not enabled + return; + } + + const mousePos = this.root.app.mousePosition; + if (!mousePos) { + return; + } + + if (this.root.hud.shouldPauseGame() || this.root.hud.hasBlockingOverlayOpen()) { + return; + } + + if (this.desiredCenter || this.desiredZoom || this.currentlyMoving || this.currentlyPinching) { + // Performing another method of movement right now + return; + } + + if ( + mousePos.x < 0 || + mousePos.y < 0 || + mousePos.x > this.root.gameWidth || + mousePos.y > this.root.gameHeight + ) { + // Out of screen + return; + } + + const panAreaPixels = 2; + + const panVelocity = new Vector(); + if (mousePos.x < panAreaPixels) { + panVelocity.x -= 1; + } + if (mousePos.x > this.root.gameWidth - panAreaPixels) { + panVelocity.x += 1; + } + + if (mousePos.y < panAreaPixels) { + panVelocity.y -= 1; + } + if (mousePos.y > this.root.gameHeight - panAreaPixels) { + panVelocity.y += 1; + } + + this.center = this.center.add( + panVelocity.multiplyScalar( + ((0.5 * dt) / this.zoomLevel) * this.root.app.settings.getMovementSpeed() + ) + ); + } + + /** + * Updates the non user interaction zooming + * @param {number} now Time now in seconds + * @param {number} dt Delta time + */ + internalUpdateZooming(now, dt) { + if (!this.currentlyPinching && this.desiredZoom !== null) { + const diff = this.zoomLevel - this.desiredZoom; + if (Math.abs(diff) > 0.0001) { + let fade = 0.94; + if (diff > 0) { + // Zoom out faster than in + fade = 0.9; + } + + assert(Number.isFinite(this.desiredZoom), "Desired zoom is NaN: " + this.desiredZoom); + assert(Number.isFinite(fade), "Zoom fade is NaN: " + fade); + this.zoomLevel = this.zoomLevel * fade + this.desiredZoom * (1 - fade); + assert(Number.isFinite(this.zoomLevel), "Zoom level is NaN after fade: " + this.zoomLevel); + } else { + this.desiredZoom = null; + } + } + } + + /** + * Updates the non user interaction centering + * @param {number} now Time now in seconds + * @param {number} dt Delta time + */ + internalUpdateCentering(now, dt) { + if (!this.currentlyMoving && this.desiredCenter !== null) { + const diff = this.center.direction(this.desiredCenter); + const length = diff.length(); + const tolerance = 1 / this.zoomLevel; + if (length > tolerance) { + const movement = diff.multiplyScalar(Math.min(1, dt * 0.008)); + this.center.x += movement.x; + this.center.y += movement.y; + } else { + this.desiredCenter = null; + } + } + } + + /** + * Updates the keyboard forces + * @param {number} now + * @param {number} dt Delta time + */ + internalUpdateKeyboardForce(now, dt) { + if (!this.currentlyMoving && this.desiredCenter == null) { + const limitingDimension = Math.min(this.root.gameWidth, this.root.gameHeight); + + const moveAmount = ((limitingDimension / 2048) * dt) / this.zoomLevel; + + let forceX = 0; + let forceY = 0; + + const actionMapper = this.root.keyMapper; + if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveUp).pressed) { + forceY -= 1; + } + + if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveDown).pressed) { + forceY += 1; + } + + if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveLeft).pressed) { + forceX -= 1; + } + + if (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveRight).pressed) { + forceX += 1; + } + + let movementSpeed = + this.root.app.settings.getMovementSpeed() * + (actionMapper.getBinding(KEYMAPPINGS.navigation.mapMoveFaster).pressed ? 4 : 1); + + this.center.x += moveAmount * forceX * movementSpeed; + this.center.y += moveAmount * forceY * movementSpeed; + } + } +} diff --git a/src/js/game/component.js b/src/js/game/component.js index 7d30faff..46b1b545 100644 --- a/src/js/game/component.js +++ b/src/js/game/component.js @@ -18,12 +18,10 @@ export class Component extends BasicSerializableObject { } /** - * Should duplicate the component but without its contents - * @returns {object} + * Copy the current state to another component + * @param {Component} otherComponent */ - duplicateWithoutContents() { - abstract; - } + copyAdditionalStateTo(otherComponent) {} /* dev:start */ diff --git a/src/js/game/component_registry.js b/src/js/game/component_registry.js index b03c164f..c4ea6921 100644 --- a/src/js/game/component_registry.js +++ b/src/js/game/component_registry.js @@ -17,6 +17,7 @@ import { LeverComponent } from "./components/lever"; import { WireTunnelComponent } from "./components/wire_tunnel"; import { DisplayComponent } from "./components/display"; import { BeltReaderComponent } from "./components/belt_reader"; +import { FilterComponent } from "./components/filter"; export function initComponentRegistry() { gComponentRegistry.register(StaticMapEntityComponent); @@ -37,6 +38,7 @@ export function initComponentRegistry() { gComponentRegistry.register(WireTunnelComponent); gComponentRegistry.register(DisplayComponent); gComponentRegistry.register(BeltReaderComponent); + gComponentRegistry.register(FilterComponent); // IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS diff --git a/src/js/game/components/belt.js b/src/js/game/components/belt.js index 02197822..138c4775 100644 --- a/src/js/game/components/belt.js +++ b/src/js/game/components/belt.js @@ -40,10 +40,6 @@ export class BeltComponent extends Component { return "Belt"; } - duplicateWithoutContents() { - return new BeltComponent({ direction: this.direction }); - } - /** * * @param {object} param0 diff --git a/src/js/game/components/belt_reader.js b/src/js/game/components/belt_reader.js index d59feb9c..d451bab5 100644 --- a/src/js/game/components/belt_reader.js +++ b/src/js/game/components/belt_reader.js @@ -8,10 +8,6 @@ export class BeltReaderComponent extends Component { return "BeltReader"; } - duplicateWithoutContents() { - return new BeltReaderComponent(); - } - static getSchema() { return { lastItem: types.nullable(typeItemSingleton), diff --git a/src/js/game/components/belt_underlays.js b/src/js/game/components/belt_underlays.js index cb516b1a..63b265d0 100644 --- a/src/js/game/components/belt_underlays.js +++ b/src/js/game/components/belt_underlays.js @@ -1,33 +1,41 @@ -import { Component } from "../component"; -import { types } from "../../savegame/serialization"; -import { enumDirection, Vector } from "../../core/vector"; - -export class BeltUnderlaysComponent extends Component { - static getId() { - return "BeltUnderlays"; - } - - duplicateWithoutContents() { - const beltUnderlaysCopy = []; - for (let i = 0; i < this.underlays.length; ++i) { - const underlay = this.underlays[i]; - beltUnderlaysCopy.push({ - pos: underlay.pos.copy(), - direction: underlay.direction, - }); - } - - return new BeltUnderlaysComponent({ - underlays: beltUnderlaysCopy, - }); - } - - /** - * @param {object} param0 - * @param {Array<{pos: Vector, direction: enumDirection}>=} param0.underlays Where to render belt underlays - */ - constructor({ underlays }) { - super(); - this.underlays = underlays; - } -} +import { enumDirection, Vector } from "../../core/vector"; +import { Component } from "../component"; + +/** + * Store which type an underlay is, this is cached so we can easily + * render it. + * + * Full: Render underlay at top and bottom of tile + * Bottom Only: Only render underlay at the bottom half + * Top Only: + * @enum {string} + */ +export const enumClippedBeltUnderlayType = { + full: "full", + bottomOnly: "bottomOnly", + topOnly: "topOnly", + none: "none", +}; + +/** + * @typedef {{ + * pos: Vector, + * direction: enumDirection, + * cachedType?: enumClippedBeltUnderlayType + * }} BeltUnderlayTile + */ + +export class BeltUnderlaysComponent extends Component { + static getId() { + return "BeltUnderlays"; + } + + /** + * @param {object} param0 + * @param {Array=} param0.underlays Where to render belt underlays + */ + constructor({ underlays = [] }) { + super(); + this.underlays = underlays; + } +} diff --git a/src/js/game/components/constant_signal.js b/src/js/game/components/constant_signal.js index b51277a1..286108be 100644 --- a/src/js/game/components/constant_signal.js +++ b/src/js/game/components/constant_signal.js @@ -15,8 +15,12 @@ export class ConstantSignalComponent extends Component { }; } - duplicateWithoutContents() { - return new ConstantSignalComponent({ signal: this.signal }); + /** + * Copy the current state to another component + * @param {ConstantSignalComponent} otherComponent + */ + copyAdditionalStateTo(otherComponent) { + otherComponent.signal = this.signal; } /** diff --git a/src/js/game/components/display.js b/src/js/game/components/display.js index 720bf8c7..5a5b1b3b 100644 --- a/src/js/game/components/display.js +++ b/src/js/game/components/display.js @@ -4,8 +4,4 @@ export class DisplayComponent extends Component { static getId() { return "Display"; } - - duplicateWithoutContents() { - return new DisplayComponent(); - } } diff --git a/src/js/game/components/filter.js b/src/js/game/components/filter.js new file mode 100644 index 00000000..cffee969 --- /dev/null +++ b/src/js/game/components/filter.js @@ -0,0 +1,55 @@ +import { types } from "../../savegame/serialization"; +import { BaseItem } from "../base_item"; +import { Component } from "../component"; +import { typeItemSingleton } from "../item_resolver"; + +/** + * @typedef {{ + * item: BaseItem, + * progress: number + * }} PendingFilterItem + */ + +export class FilterComponent extends Component { + static getId() { + return "Filter"; + } + + duplicateWithoutContents() { + return new FilterComponent(); + } + + static getSchema() { + return { + pendingItemsToLeaveThrough: types.array( + types.structured({ + item: typeItemSingleton, + progress: types.ufloat, + }) + ), + + pendingItemsToReject: types.array( + types.structured({ + item: typeItemSingleton, + progress: types.ufloat, + }) + ), + }; + } + + constructor() { + super(); + + /** + * Items in queue to leave through + * @type {Array} + */ + this.pendingItemsToLeaveThrough = []; + + /** + * Items in queue to reject + * @type {Array} + */ + this.pendingItemsToReject = []; + } +} diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index 3885eb1f..7dbd9677 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -28,22 +28,6 @@ export class ItemAcceptorComponent extends Component { return "ItemAcceptor"; } - duplicateWithoutContents() { - const slotsCopy = []; - for (let i = 0; i < this.slots.length; ++i) { - const slot = this.slots[i]; - slotsCopy.push({ - pos: slot.pos.copy(), - directions: slot.directions.slice(), - filter: slot.filter, - }); - } - - return new ItemAcceptorComponent({ - slots: slotsCopy, - }); - } - /** * * @param {object} param0 diff --git a/src/js/game/components/item_ejector.js b/src/js/game/components/item_ejector.js index b9a23c38..4eda52f9 100644 --- a/src/js/game/components/item_ejector.js +++ b/src/js/game/components/item_ejector.js @@ -1,161 +1,143 @@ -import { enumDirection, enumDirectionToVector, Vector } from "../../core/vector"; -import { types } from "../../savegame/serialization"; -import { BaseItem } from "../base_item"; -import { BeltPath } from "../belt_path"; -import { Component } from "../component"; -import { Entity } from "../entity"; -import { typeItemSingleton } from "../item_resolver"; - -/** - * @typedef {{ - * pos: Vector, - * direction: enumDirection, - * item: BaseItem, - * progress: number?, - * cachedDestSlot?: import("./item_acceptor").ItemAcceptorLocatedSlot, - * cachedBeltPath?: BeltPath, - * cachedTargetEntity?: Entity - * }} ItemEjectorSlot - */ - -export class ItemEjectorComponent extends Component { - static getId() { - return "ItemEjector"; - } - - static getSchema() { - // The cachedDestSlot, cachedTargetEntity fields are not serialized. - return { - slots: types.array( - types.structured({ - item: types.nullable(typeItemSingleton), - progress: types.float, - }) - ), - }; - } - - duplicateWithoutContents() { - const slotsCopy = []; - for (let i = 0; i < this.slots.length; ++i) { - const slot = this.slots[i]; - slotsCopy.push({ - pos: slot.pos.copy(), - direction: slot.direction, - }); - } - - return new ItemEjectorComponent({ - slots: slotsCopy, - }); - } - - /** - * - * @param {object} param0 - * @param {Array<{pos: Vector, direction: enumDirection }>=} param0.slots The slots to eject on - */ - constructor({ slots = [] }) { - super(); - - this.setSlots(slots); - - /** - * Whether this ejector slot is enabled - */ - this.enabled = true; - } - - /** - * @param {Array<{pos: Vector, direction: enumDirection }>} slots The slots to eject on - */ - setSlots(slots) { - /** @type {Array} */ - this.slots = []; - for (let i = 0; i < slots.length; ++i) { - const slot = slots[i]; - this.slots.push({ - pos: slot.pos, - direction: slot.direction, - item: null, - progress: 0, - cachedDestSlot: null, - cachedTargetEntity: null, - }); - } - } - - /** - * Returns where this slot ejects to - * @param {ItemEjectorSlot} slot - * @returns {Vector} - */ - getSlotTargetLocalTile(slot) { - const directionVector = enumDirectionToVector[slot.direction]; - return slot.pos.add(directionVector); - } - - /** - * Returns whether any slot ejects to the given local tile - * @param {Vector} tile - */ - anySlotEjectsToLocalTile(tile) { - for (let i = 0; i < this.slots.length; ++i) { - if (this.getSlotTargetLocalTile(this.slots[i]).equals(tile)) { - return true; - } - } - return false; - } - - /** - * Returns if we can eject on a given slot - * @param {number} slotIndex - * @returns {boolean} - */ - canEjectOnSlot(slotIndex) { - assert(slotIndex >= 0 && slotIndex < this.slots.length, "Invalid ejector slot: " + slotIndex); - return !this.slots[slotIndex].item; - } - - /** - * Returns the first free slot on this ejector or null if there is none - * @returns {number?} - */ - getFirstFreeSlot() { - for (let i = 0; i < this.slots.length; ++i) { - if (this.canEjectOnSlot(i)) { - return i; - } - } - return null; - } - - /** - * Tries to eject a given item - * @param {number} slotIndex - * @param {BaseItem} item - * @returns {boolean} - */ - tryEject(slotIndex, item) { - if (!this.canEjectOnSlot(slotIndex)) { - return false; - } - this.slots[slotIndex].item = item; - this.slots[slotIndex].progress = 0; - return true; - } - - /** - * Clears the given slot and returns the item it had - * @param {number} slotIndex - * @returns {BaseItem|null} - */ - takeSlotItem(slotIndex) { - const slot = this.slots[slotIndex]; - const item = slot.item; - slot.item = null; - slot.progress = 0.0; - return item; - } -} +import { enumDirection, enumDirectionToVector, Vector } from "../../core/vector"; +import { types } from "../../savegame/serialization"; +import { BaseItem } from "../base_item"; +import { BeltPath } from "../belt_path"; +import { Component } from "../component"; +import { Entity } from "../entity"; +import { typeItemSingleton } from "../item_resolver"; + +/** + * @typedef {{ + * pos: Vector, + * direction: enumDirection, + * item: BaseItem, + * progress: number?, + * cachedDestSlot?: import("./item_acceptor").ItemAcceptorLocatedSlot, + * cachedBeltPath?: BeltPath, + * cachedTargetEntity?: Entity + * }} ItemEjectorSlot + */ + +export class ItemEjectorComponent extends Component { + static getId() { + return "ItemEjector"; + } + + static getSchema() { + // The cachedDestSlot, cachedTargetEntity fields are not serialized. + return { + slots: types.array( + types.structured({ + item: types.nullable(typeItemSingleton), + progress: types.float, + }) + ), + }; + } + + /** + * + * @param {object} param0 + * @param {Array<{pos: Vector, direction: enumDirection }>=} param0.slots The slots to eject on + * @param {boolean=} param0.renderFloatingItems Whether to render items even if they are not connected + */ + constructor({ slots = [], renderFloatingItems = true }) { + super(); + + this.setSlots(slots); + this.renderFloatingItems = renderFloatingItems; + } + + /** + * @param {Array<{pos: Vector, direction: enumDirection }>} slots The slots to eject on + */ + setSlots(slots) { + /** @type {Array} */ + this.slots = []; + for (let i = 0; i < slots.length; ++i) { + const slot = slots[i]; + this.slots.push({ + pos: slot.pos, + direction: slot.direction, + item: null, + progress: 0, + cachedDestSlot: null, + cachedTargetEntity: null, + }); + } + } + + /** + * Returns where this slot ejects to + * @param {ItemEjectorSlot} slot + * @returns {Vector} + */ + getSlotTargetLocalTile(slot) { + const directionVector = enumDirectionToVector[slot.direction]; + return slot.pos.add(directionVector); + } + + /** + * Returns whether any slot ejects to the given local tile + * @param {Vector} tile + */ + anySlotEjectsToLocalTile(tile) { + for (let i = 0; i < this.slots.length; ++i) { + if (this.getSlotTargetLocalTile(this.slots[i]).equals(tile)) { + return true; + } + } + return false; + } + + /** + * Returns if we can eject on a given slot + * @param {number} slotIndex + * @returns {boolean} + */ + canEjectOnSlot(slotIndex) { + assert(slotIndex >= 0 && slotIndex < this.slots.length, "Invalid ejector slot: " + slotIndex); + return !this.slots[slotIndex].item; + } + + /** + * Returns the first free slot on this ejector or null if there is none + * @returns {number?} + */ + getFirstFreeSlot() { + for (let i = 0; i < this.slots.length; ++i) { + if (this.canEjectOnSlot(i)) { + return i; + } + } + return null; + } + + /** + * Tries to eject a given item + * @param {number} slotIndex + * @param {BaseItem} item + * @returns {boolean} + */ + tryEject(slotIndex, item) { + if (!this.canEjectOnSlot(slotIndex)) { + return false; + } + this.slots[slotIndex].item = item; + this.slots[slotIndex].progress = 0; + return true; + } + + /** + * Clears the given slot and returns the item it had + * @param {number} slotIndex + * @returns {BaseItem|null} + */ + takeSlotItem(slotIndex) { + const slot = this.slots[slotIndex]; + const item = slot.item; + slot.item = null; + slot.progress = 0.0; + return item; + } +} diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index 5d51b4a3..fd466662 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -1,17 +1,15 @@ import { types } from "../../savegame/serialization"; import { BaseItem } from "../base_item"; import { Component } from "../component"; -import { typeItemSingleton } from "../item_resolver"; /** @enum {string} */ export const enumItemProcessorTypes = { - splitter: "splitter", - splitterWires: "splitterWires", + balancer: "balancer", cutter: "cutter", cutterQuad: "cutterQuad", rotater: "rotater", rotaterCCW: "rotaterCCW", - rotaterFL: "rotaterFL", + rotater180: "rotater180", stacker: "stacker", trash: "trash", mixer: "mixer", @@ -26,7 +24,6 @@ export const enumItemProcessorTypes = { /** @enum {string} */ export const enumItemProcessorRequirements = { painterQuad: "painterQuad", - filter: "filter", }; /** @typedef {{ @@ -51,14 +48,6 @@ export class ItemProcessorComponent extends Component { }; } - duplicateWithoutContents() { - return new ItemProcessorComponent({ - processorType: this.type, - processingRequirement: this.processingRequirement, - inputsPerCharge: this.inputsPerCharge, - }); - } - /** * * @param {object} param0 @@ -68,14 +57,14 @@ export class ItemProcessorComponent extends Component { * */ constructor({ - processorType = enumItemProcessorTypes.splitter, + processorType = enumItemProcessorTypes.balancer, processingRequirement = null, inputsPerCharge = 1, }) { super(); // Which slot to emit next, this is only a preference and if it can't emit - // it will take the other one. Some machines ignore this (e.g. the splitter) to make + // it will take the other one. Some machines ignore this (e.g. the balancer) to make // sure the outputs always match this.nextOutputSlot = 0; diff --git a/src/js/game/components/lever.js b/src/js/game/components/lever.js index e17df1e8..106cbbdd 100644 --- a/src/js/game/components/lever.js +++ b/src/js/game/components/lever.js @@ -12,8 +12,12 @@ export class LeverComponent extends Component { }; } - duplicateWithoutContents() { - return new LeverComponent({ toggled: this.toggled }); + /** + * Copy the current state to another component + * @param {LeverComponent} otherComponent + */ + copyAdditionalStateTo(otherComponent) { + otherComponent.toggled = this.toggled; } /** diff --git a/src/js/game/components/logic_gate.js b/src/js/game/components/logic_gate.js index fe151184..a19f70b6 100644 --- a/src/js/game/components/logic_gate.js +++ b/src/js/game/components/logic_gate.js @@ -13,6 +13,8 @@ export const enumLogicGateType = { unstacker: "unstacker", cutter: "cutter", shapecompare: "shapecompare", + stacker: "stacker", + painter: "painter", }; export class LogicGateComponent extends Component { @@ -20,10 +22,6 @@ export class LogicGateComponent extends Component { return "LogicGate"; } - duplicateWithoutContents() { - return new LogicGateComponent({ type: this.type }); - } - /** * * @param {object} param0 diff --git a/src/js/game/components/miner.js b/src/js/game/components/miner.js index 5b818afb..ab87760f 100644 --- a/src/js/game/components/miner.js +++ b/src/js/game/components/miner.js @@ -19,12 +19,6 @@ export class MinerComponent extends Component { }; } - duplicateWithoutContents() { - return new MinerComponent({ - chainable: this.chainable, - }); - } - constructor({ chainable = false }) { super(); this.lastMiningTime = 0; diff --git a/src/js/game/components/static_map_entity.js b/src/js/game/components/static_map_entity.js index 3d138e42..d5daa998 100644 --- a/src/js/game/components/static_map_entity.js +++ b/src/js/game/components/static_map_entity.js @@ -63,7 +63,11 @@ export class StaticMapEntityComponent extends Component { return getBuildingDataFromCode(this.code).metaInstance; } - duplicateWithoutContents() { + /** + * Copy the current state to another component + * @param {Component} otherComponent + */ + copyAdditionalStateTo(otherComponent) { return new StaticMapEntityComponent({ origin: this.origin.copy(), rotation: this.rotation, diff --git a/src/js/game/components/storage.js b/src/js/game/components/storage.js index 3b32f6a3..be243a44 100644 --- a/src/js/game/components/storage.js +++ b/src/js/game/components/storage.js @@ -17,10 +17,6 @@ export class StorageComponent extends Component { }; } - duplicateWithoutContents() { - return new StorageComponent({ maximumStorage: this.maximumStorage }); - } - /** * @param {object} param0 * @param {number=} param0.maximumStorage How much this storage can hold diff --git a/src/js/game/components/underground_belt.js b/src/js/game/components/underground_belt.js index 74351aac..a3e883ec 100644 --- a/src/js/game/components/underground_belt.js +++ b/src/js/game/components/underground_belt.js @@ -29,13 +29,6 @@ export class UndergroundBeltComponent extends Component { }; } - duplicateWithoutContents() { - return new UndergroundBeltComponent({ - mode: this.mode, - tier: this.tier, - }); - } - /** * * @param {object} param0 @@ -55,7 +48,7 @@ export class UndergroundBeltComponent extends Component { * Used on both receiver and sender. * Reciever: Used to store the next item to transfer, and to block input while doing this * Sender: Used to store which items are currently "travelling" - * @type {Array<[BaseItem, number]>} Format is [Item, remaining seconds until transfer/ejection] + * @type {Array<[BaseItem, number]>} Format is [Item, ingame time to eject the item] */ this.pendingItems = []; @@ -92,8 +85,9 @@ export class UndergroundBeltComponent extends Component { * @param {BaseItem} item * @param {number} travelDistance How many tiles this item has to travel * @param {number} beltSpeed How fast this item travels + * @param {number} now Current ingame time */ - tryAcceptTunneledItem(item, travelDistance, beltSpeed) { + tryAcceptTunneledItem(item, travelDistance, beltSpeed, now) { if (this.mode !== enumUndergroundBeltMode.receiver) { // Only receivers can accept tunneled items return false; @@ -112,11 +106,7 @@ export class UndergroundBeltComponent extends Component { // Additionally it takes 1 tile for the acceptor which we just add on top. const travelDuration = (travelDistance + 1.5) / beltSpeed / globalConfig.itemSpacingOnBelts; - this.pendingItems.push([item, travelDuration]); - - // Sort so we can only look at the first ones - this.pendingItems.sort((a, b) => a[1] - b[1]); - + this.pendingItems.push([item, now + travelDuration]); return true; } } diff --git a/src/js/game/components/wire.js b/src/js/game/components/wire.js index 88c56249..4bb1edca 100644 --- a/src/js/game/components/wire.js +++ b/src/js/game/components/wire.js @@ -13,10 +13,6 @@ export class WireComponent extends Component { return "Wire"; } - duplicateWithoutContents() { - return new WireComponent({ type: this.type }); - } - /** * @param {object} param0 * @param {enumWireType=} param0.type diff --git a/src/js/game/components/wire_tunnel.js b/src/js/game/components/wire_tunnel.js index dfb38f1f..e1be448b 100644 --- a/src/js/game/components/wire_tunnel.js +++ b/src/js/game/components/wire_tunnel.js @@ -5,10 +5,6 @@ export class WireTunnelComponent extends Component { return "WireTunnel"; } - duplicateWithoutContents() { - return new WireTunnelComponent({ multipleDirections: this.multipleDirections }); - } - /** * @param {object} param0 * @param {boolean=} param0.multipleDirections diff --git a/src/js/game/components/wired_pins.js b/src/js/game/components/wired_pins.js index 9a19c2b0..ff339b86 100644 --- a/src/js/game/components/wired_pins.js +++ b/src/js/game/components/wired_pins.js @@ -49,20 +49,6 @@ export class WiredPinsComponent extends Component { this.setSlots(slots); } - duplicateWithoutContents() { - const slots = []; - for (let i = 0; i < this.slots.length; ++i) { - const slot = this.slots[i]; - slots.push({ - pos: slot.pos.copy(), - type: slot.type, - direction: slot.direction, - }); - } - - return new WiredPinsComponent({ slots }); - } - /** * Sets the slots of this building * @param {Array} slots diff --git a/src/js/game/core.js b/src/js/game/core.js index 642d8d9d..306643f9 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -2,7 +2,12 @@ import { Application } from "../application"; /* typehints:end */ import { BufferMaintainer } from "../core/buffer_maintainer"; -import { disableImageSmoothing, enableImageSmoothing, registerCanvas } from "../core/buffer_utils"; +import { + disableImageSmoothing, + enableImageSmoothing, + getBufferStats, + registerCanvas, +} from "../core/buffer_utils"; import { globalConfig } from "../core/config"; import { getDeviceDPI, resizeHighDPICanvas } from "../core/dpi_manager"; import { DrawParameters } from "../core/draw_parameters"; @@ -219,9 +224,6 @@ export class GameCore { lastContext.clearRect(0, 0, lastCanvas.width, lastCanvas.height); } - // globalConfig.smoothing.smoothMainCanvas = getDeviceDPI() < 1.5; - // globalConfig.smoothing.smoothMainCanvas = true; - canvas.classList.toggle("smoothed", globalConfig.smoothing.smoothMainCanvas); // Oof, use :not() instead @@ -374,9 +376,9 @@ export class GameCore { (zoomLevel / globalConfig.assetsDpi) * getDeviceDPI() * globalConfig.assetsSharpness; let desiredAtlasScale = "0.25"; - if (effectiveZoomLevel > 0.8 && !lowQuality) { + if (effectiveZoomLevel > 0.5 && !lowQuality) { desiredAtlasScale = ORIGINAL_SPRITE_SCALE; - } else if (effectiveZoomLevel > 0.4 && !lowQuality) { + } else if (effectiveZoomLevel > 0.35 && !lowQuality) { desiredAtlasScale = "0.5"; } @@ -414,6 +416,11 @@ export class GameCore { const desiredOverlayAlpha = this.root.camera.getIsMapOverlayActive() ? 1 : 0; this.overlayAlpha = lerp(this.overlayAlpha, desiredOverlayAlpha, 0.25); + // On low performance, skip the fade + if (this.root.entityMgr.entities.length > 5000 || this.root.dynamicTickrate.averageFps < 50) { + this.overlayAlpha = desiredOverlayAlpha; + } + if (this.overlayAlpha < 0.99) { // Background (grid, resources, etc) root.map.drawBackground(params); @@ -500,18 +507,37 @@ export class GameCore { ); const stats = this.root.buffers.getStats(); + context.fillText( - "Buffers: " + + "Maintained Buffers: " + stats.rootKeys + - " root keys, " + + " root keys / " + stats.subKeys + - " sub keys / buffers / VRAM: " + + " buffers / VRAM: " + round2Digits(stats.vramBytes / (1024 * 1024)) + " MB", - 20, 620 ); + const internalStats = getBufferStats(); + context.fillText( + "Total Buffers: " + + internalStats.bufferCount + + " buffers / " + + internalStats.backlogSize + + " backlog / " + + internalStats.backlogKeys + + " keys in backlog / VRAM " + + round2Digits(internalStats.vramUsage / (1024 * 1024)) + + " MB / Backlog " + + round2Digits(internalStats.backlogVramUsage / (1024 * 1024)) + + " MB / Created " + + internalStats.numCreated + + " / Reused " + + internalStats.numReused, + 20, + 640 + ); } if (G_IS_DEV && globalConfig.debug.testClipping) { diff --git a/src/js/game/entity.js b/src/js/game/entity.js index ca21a16d..d7dd715e 100644 --- a/src/js/game/entity.js +++ b/src/js/game/entity.js @@ -1,229 +1,233 @@ -/* typehints:start */ -import { DrawParameters } from "../core/draw_parameters"; -import { Component } from "./component"; -/* typehints:end */ - -import { GameRoot } from "./root"; -import { globalConfig } from "../core/config"; -import { enumDirectionToVector, enumDirectionToAngle } from "../core/vector"; -import { BasicSerializableObject, types } from "../savegame/serialization"; -import { EntityComponentStorage } from "./entity_components"; -import { Loader } from "../core/loader"; -import { drawRotatedSprite } from "../core/draw_utils"; -import { gComponentRegistry } from "../core/global_registries"; - -export class Entity extends BasicSerializableObject { - /** - * @param {GameRoot} root - */ - constructor(root) { - super(); - - /** - * Handle to the global game root - */ - this.root = root; - - /** - * The components of the entity - */ - this.components = new EntityComponentStorage(); - - /** - * Whether this entity was registered on the @see EntityManager so far - */ - this.registered = false; - - /** - * On which layer this entity is - * @type {Layer} - */ - this.layer = "regular"; - - /** - * Internal entity unique id, set by the @see EntityManager - */ - this.uid = 0; - - /* typehints:start */ - - /** - * Stores if this entity is destroyed, set by the @see EntityManager - * @type {boolean} */ - this.destroyed; - - /** - * Stores if this entity is queued to get destroyed in the next tick - * of the @see EntityManager - * @type {boolean} */ - this.queuedForDestroy; - - /** - * Stores the reason why this entity was destroyed - * @type {string} */ - this.destroyReason; - - /* typehints:end */ - } - - static getId() { - return "Entity"; - } - - /** - * @see BasicSerializableObject.getSchema - * @returns {import("../savegame/serialization").Schema} - */ - static getSchema() { - return { - uid: types.uint, - components: types.keyValueMap(types.objData(gComponentRegistry), false), - }; - } - - /** - * Returns a clone of this entity without contents - */ - duplicateWithoutContents() { - const clone = new Entity(this.root); - for (const key in this.components) { - clone.components[key] = this.components[key].duplicateWithoutContents(); - } - clone.layer = this.layer; - return clone; - } - - /** - * Internal destroy callback - */ - internalDestroyCallback() { - assert(!this.destroyed, "Can not destroy entity twice"); - this.destroyed = true; - } - - /** - * Adds a new component, only possible until the entity is registered on the entity manager, - * after that use @see EntityManager.addDynamicComponent - * @param {Component} componentInstance - * @param {boolean} force Used by the entity manager. Internal parameter, do not change - */ - addComponent(componentInstance, force = false) { - if (!force && this.registered) { - this.root.entityMgr.attachDynamicComponent(this, componentInstance); - return; - } - assert(force || !this.registered, "Entity already registered, use EntityManager.addDynamicComponent"); - const id = /** @type {typeof Component} */ (componentInstance.constructor).getId(); - assert(!this.components[id], "Component already present"); - this.components[id] = componentInstance; - } - - /** - * Removes a given component, only possible until the entity is registered on the entity manager, - * after that use @see EntityManager.removeDynamicComponent - * @param {typeof Component} componentClass - * @param {boolean} force - */ - removeComponent(componentClass, force = false) { - if (!force && this.registered) { - this.root.entityMgr.removeDynamicComponent(this, componentClass); - return; - } - assert( - force || !this.registered, - "Entity already registered, use EntityManager.removeDynamicComponent" - ); - const id = componentClass.getId(); - assert(this.components[id], "Component does not exist on entity"); - delete this.components[id]; - } - - /** - * Draws the entity, to override use @see Entity.drawImpl - * @param {DrawParameters} parameters - */ - drawDebugOverlays(parameters) { - const context = parameters.context; - const staticComp = this.components.StaticMapEntity; - - if (G_IS_DEV && staticComp && globalConfig.debug.showEntityBounds) { - if (staticComp) { - const transformed = staticComp.getTileSpaceBounds(); - context.strokeStyle = "rgba(255, 0, 0, 0.5)"; - context.lineWidth = 2; - // const boundsSize = 20; - context.beginPath(); - context.rect( - transformed.x * globalConfig.tileSize, - transformed.y * globalConfig.tileSize, - transformed.w * globalConfig.tileSize, - transformed.h * globalConfig.tileSize - ); - context.stroke(); - } - } - - if (G_IS_DEV && staticComp && globalConfig.debug.showAcceptorEjectors) { - const ejectorComp = this.components.ItemEjector; - - if (ejectorComp) { - const ejectorSprite = Loader.getSprite("sprites/debug/ejector_slot.png"); - for (let i = 0; i < ejectorComp.slots.length; ++i) { - const slot = ejectorComp.slots[i]; - const slotTile = staticComp.localTileToWorld(slot.pos); - const direction = staticComp.localDirectionToWorld(slot.direction); - const directionVector = enumDirectionToVector[direction]; - const angle = Math.radians(enumDirectionToAngle[direction]); - - context.globalAlpha = slot.item ? 1 : 0.2; - drawRotatedSprite({ - parameters, - sprite: ejectorSprite, - x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize, - y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize, - angle, - size: globalConfig.tileSize * 0.25, - }); - } - } - const acceptorComp = this.components.ItemAcceptor; - - if (acceptorComp) { - const acceptorSprite = Loader.getSprite("sprites/misc/acceptor_slot.png"); - for (let i = 0; i < acceptorComp.slots.length; ++i) { - const slot = acceptorComp.slots[i]; - const slotTile = staticComp.localTileToWorld(slot.pos); - for (let k = 0; k < slot.directions.length; ++k) { - const direction = staticComp.localDirectionToWorld(slot.directions[k]); - const directionVector = enumDirectionToVector[direction]; - const angle = Math.radians(enumDirectionToAngle[direction] + 180); - context.globalAlpha = 0.4; - drawRotatedSprite({ - parameters, - sprite: acceptorSprite, - x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize, - y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize, - angle, - size: globalConfig.tileSize * 0.25, - }); - } - } - } - - context.globalAlpha = 1; - } - // this.drawImpl(parameters); - } - - ///// Helper interfaces - - ///// Interface to override by subclasses - - /** - * override, should draw the entity - * @param {DrawParameters} parameters - */ - drawImpl(parameters) { - abstract; - } -} +/* typehints:start */ +import { DrawParameters } from "../core/draw_parameters"; +import { Component } from "./component"; +/* typehints:end */ + +import { GameRoot } from "./root"; +import { globalConfig } from "../core/config"; +import { enumDirectionToVector, enumDirectionToAngle } from "../core/vector"; +import { BasicSerializableObject, types } from "../savegame/serialization"; +import { EntityComponentStorage } from "./entity_components"; +import { Loader } from "../core/loader"; +import { drawRotatedSprite } from "../core/draw_utils"; +import { gComponentRegistry } from "../core/global_registries"; +import { getBuildingDataFromCode } from "./building_codes"; + +export class Entity extends BasicSerializableObject { + /** + * @param {GameRoot} root + */ + constructor(root) { + super(); + + /** + * Handle to the global game root + */ + this.root = root; + + /** + * The components of the entity + */ + this.components = new EntityComponentStorage(); + + /** + * Whether this entity was registered on the @see EntityManager so far + */ + this.registered = false; + + /** + * On which layer this entity is + * @type {Layer} + */ + this.layer = "regular"; + + /** + * Internal entity unique id, set by the @see EntityManager + */ + this.uid = 0; + + /* typehints:start */ + + /** + * Stores if this entity is destroyed, set by the @see EntityManager + * @type {boolean} */ + this.destroyed; + + /** + * Stores if this entity is queued to get destroyed in the next tick + * of the @see EntityManager + * @type {boolean} */ + this.queuedForDestroy; + + /** + * Stores the reason why this entity was destroyed + * @type {string} */ + this.destroyReason; + + /* typehints:end */ + } + + static getId() { + return "Entity"; + } + + /** + * @see BasicSerializableObject.getSchema + * @returns {import("../savegame/serialization").Schema} + */ + static getSchema() { + return { + uid: types.uint, + components: types.keyValueMap(types.objData(gComponentRegistry), false), + }; + } + + /** + * Returns a clone of this entity + */ + clone() { + const staticComp = this.components.StaticMapEntity; + const buildingData = getBuildingDataFromCode(staticComp.code); + + const clone = buildingData.metaInstance.createEntity({ + root: this.root, + origin: staticComp.origin, + originalRotation: staticComp.originalRotation, + rotation: staticComp.rotation, + rotationVariant: buildingData.rotationVariant, + variant: buildingData.variant, + }); + + for (const key in this.components) { + /** @type {Component} */ (this.components[key]).copyAdditionalStateTo(clone.components[key]); + } + + return clone; + } + + /** + * Adds a new component, only possible until the entity is registered on the entity manager, + * after that use @see EntityManager.addDynamicComponent + * @param {Component} componentInstance + * @param {boolean} force Used by the entity manager. Internal parameter, do not change + */ + addComponent(componentInstance, force = false) { + if (!force && this.registered) { + this.root.entityMgr.attachDynamicComponent(this, componentInstance); + return; + } + assert(force || !this.registered, "Entity already registered, use EntityManager.addDynamicComponent"); + const id = /** @type {typeof Component} */ (componentInstance.constructor).getId(); + assert(!this.components[id], "Component already present"); + this.components[id] = componentInstance; + } + + /** + * Removes a given component, only possible until the entity is registered on the entity manager, + * after that use @see EntityManager.removeDynamicComponent + * @param {typeof Component} componentClass + * @param {boolean} force + */ + removeComponent(componentClass, force = false) { + if (!force && this.registered) { + this.root.entityMgr.removeDynamicComponent(this, componentClass); + return; + } + assert( + force || !this.registered, + "Entity already registered, use EntityManager.removeDynamicComponent" + ); + const id = componentClass.getId(); + assert(this.components[id], "Component does not exist on entity"); + delete this.components[id]; + } + + /** + * Draws the entity, to override use @see Entity.drawImpl + * @param {DrawParameters} parameters + */ + drawDebugOverlays(parameters) { + const context = parameters.context; + const staticComp = this.components.StaticMapEntity; + + if (G_IS_DEV && staticComp && globalConfig.debug.showEntityBounds) { + if (staticComp) { + const transformed = staticComp.getTileSpaceBounds(); + context.strokeStyle = "rgba(255, 0, 0, 0.5)"; + context.lineWidth = 2; + // const boundsSize = 20; + context.beginPath(); + context.rect( + transformed.x * globalConfig.tileSize, + transformed.y * globalConfig.tileSize, + transformed.w * globalConfig.tileSize, + transformed.h * globalConfig.tileSize + ); + context.stroke(); + } + } + + if (G_IS_DEV && staticComp && globalConfig.debug.showAcceptorEjectors) { + const ejectorComp = this.components.ItemEjector; + + if (ejectorComp) { + const ejectorSprite = Loader.getSprite("sprites/debug/ejector_slot.png"); + for (let i = 0; i < ejectorComp.slots.length; ++i) { + const slot = ejectorComp.slots[i]; + const slotTile = staticComp.localTileToWorld(slot.pos); + const direction = staticComp.localDirectionToWorld(slot.direction); + const directionVector = enumDirectionToVector[direction]; + const angle = Math.radians(enumDirectionToAngle[direction]); + + context.globalAlpha = slot.item ? 1 : 0.2; + drawRotatedSprite({ + parameters, + sprite: ejectorSprite, + x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize, + y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize, + angle, + size: globalConfig.tileSize * 0.25, + }); + } + } + const acceptorComp = this.components.ItemAcceptor; + + if (acceptorComp) { + const acceptorSprite = Loader.getSprite("sprites/misc/acceptor_slot.png"); + for (let i = 0; i < acceptorComp.slots.length; ++i) { + const slot = acceptorComp.slots[i]; + const slotTile = staticComp.localTileToWorld(slot.pos); + for (let k = 0; k < slot.directions.length; ++k) { + const direction = staticComp.localDirectionToWorld(slot.directions[k]); + const directionVector = enumDirectionToVector[direction]; + const angle = Math.radians(enumDirectionToAngle[direction] + 180); + context.globalAlpha = 0.4; + drawRotatedSprite({ + parameters, + sprite: acceptorSprite, + x: (slotTile.x + 0.5 + directionVector.x * 0.37) * globalConfig.tileSize, + y: (slotTile.y + 0.5 + directionVector.y * 0.37) * globalConfig.tileSize, + angle, + size: globalConfig.tileSize * 0.25, + }); + } + } + } + + context.globalAlpha = 1; + } + // this.drawImpl(parameters); + } + + ///// Helper interfaces + + ///// Interface to override by subclasses + + /** + * override, should draw the entity + * @param {DrawParameters} parameters + */ + drawImpl(parameters) { + abstract; + } +} diff --git a/src/js/game/entity_components.js b/src/js/game/entity_components.js index 4a2241e3..32870d37 100644 --- a/src/js/game/entity_components.js +++ b/src/js/game/entity_components.js @@ -17,6 +17,7 @@ import { LeverComponent } from "./components/lever"; import { WireTunnelComponent } from "./components/wire_tunnel"; import { DisplayComponent } from "./components/display"; import { BeltReaderComponent } from "./components/belt_reader"; +import { FilterComponent } from "./components/filter"; /* typehints:end */ /** @@ -81,6 +82,9 @@ export class EntityComponentStorage { /** @type {BeltReaderComponent} */ this.BeltReader; + /** @type {FilterComponent} */ + this.Filter; + /* typehints:end */ } } diff --git a/src/js/game/entity_manager.js b/src/js/game/entity_manager.js index 11bc709c..613ed12d 100644 --- a/src/js/game/entity_manager.js +++ b/src/js/game/entity_manager.js @@ -1,252 +1,258 @@ -import { arrayDeleteValue, newEmptyMap, fastArrayDeleteValue } from "../core/utils"; -import { Component } from "./component"; -import { GameRoot } from "./root"; -import { Entity } from "./entity"; -import { BasicSerializableObject, types } from "../savegame/serialization"; -import { createLogger } from "../core/logging"; - -const logger = createLogger("entity_manager"); - -// Manages all entities - -// NOTICE: We use arrayDeleteValue instead of fastArrayDeleteValue since that does not preserve the order -// This is slower but we need it for the street path generation - -export class EntityManager extends BasicSerializableObject { - constructor(root) { - super(); - - /** @type {GameRoot} */ - this.root = root; - - /** @type {Array} */ - this.entities = []; - - // We store a seperate list with entities to destroy, since we don't destroy - // them instantly - /** @type {Array} */ - this.destroyList = []; - - // Store a map from componentid to entities - This is used by the game system - // for faster processing - /** @type {Object.>} */ - this.componentToEntity = newEmptyMap(); - - // Store the next uid to use - this.nextUid = 10000; - } - - static getId() { - return "EntityManager"; - } - - static getSchema() { - return { - nextUid: types.uint, - }; - } - - getStatsText() { - return this.entities.length + " entities [" + this.destroyList.length + " to kill]"; - } - - // Main update - update() { - this.processDestroyList(); - } - - /** - * Registers a new entity - * @param {Entity} entity - * @param {number=} uid Optional predefined uid - */ - registerEntity(entity, uid = null) { - assert(this.entities.indexOf(entity) < 0, `RegisterEntity() called twice for entity ${entity}`); - assert(!entity.destroyed, `Attempting to register destroyed entity ${entity}`); - - if (G_IS_DEV && uid !== null) { - assert(!this.findByUid(uid, false), "Entity uid already taken: " + uid); - } - - if (uid !== null) { - assert(uid >= 0 && uid < Number.MAX_SAFE_INTEGER, "Invalid uid passed: " + uid); - } - - this.entities.push(entity); - - // Register into the componentToEntity map - for (const componentId in entity.components) { - if (entity.components[componentId]) { - if (this.componentToEntity[componentId]) { - this.componentToEntity[componentId].push(entity); - } else { - this.componentToEntity[componentId] = [entity]; - } - } - } - - // Give each entity a unique id - entity.uid = uid ? uid : this.generateUid(); - entity.registered = true; - - this.root.signals.entityAdded.dispatch(entity); - } - - /** - * Sorts all entitiy lists after a resync - */ - sortEntityLists() { - this.entities.sort((a, b) => a.uid - b.uid); - this.destroyList.sort((a, b) => a.uid - b.uid); - - for (const key in this.componentToEntity) { - this.componentToEntity[key].sort((a, b) => a.uid - b.uid); - } - } - - /** - * Generates a new uid - * @returns {number} - */ - generateUid() { - return this.nextUid++; - } - - /** - * Call to attach a new component after the creation of the entity - * @param {Entity} entity - * @param {Component} component - */ - attachDynamicComponent(entity, component) { - entity.addComponent(component, true); - const componentId = /** @type {typeof Component} */ (component.constructor).getId(); - if (this.componentToEntity[componentId]) { - this.componentToEntity[componentId].push(entity); - } else { - this.componentToEntity[componentId] = [entity]; - } - this.root.signals.entityGotNewComponent.dispatch(entity); - } - - /** - * Call to remove a component after the creation of the entity - * @param {Entity} entity - * @param {typeof Component} component - */ - removeDynamicComponent(entity, component) { - entity.removeComponent(component, true); - const componentId = /** @type {typeof Component} */ (component.constructor).getId(); - - fastArrayDeleteValue(this.componentToEntity[componentId], entity); - this.root.signals.entityComponentRemoved.dispatch(entity); - } - - /** - * Finds an entity buy its uid, kinda slow since it loops over all entities - * @param {number} uid - * @param {boolean=} errorWhenNotFound - * @returns {Entity} - */ - findByUid(uid, errorWhenNotFound = true) { - const arr = this.entities; - for (let i = 0, len = arr.length; i < len; ++i) { - const entity = arr[i]; - if (entity.uid === uid) { - if (entity.queuedForDestroy || entity.destroyed) { - if (errorWhenNotFound) { - logger.warn("Entity with UID", uid, "not found (destroyed)"); - } - return null; - } - return entity; - } - } - if (errorWhenNotFound) { - logger.warn("Entity with UID", uid, "not found"); - } - return null; - } - - /** - * Returns all entities having the given component - * @param {typeof Component} componentHandle - * @returns {Array} entities - */ - getAllWithComponent(componentHandle) { - return this.componentToEntity[componentHandle.getId()] || []; - } - - /** - * Return all of a given class. This is SLOW! - * @param {object} entityClass - * @returns {Array} entities - */ - getAllOfClass(entityClass) { - // FIXME: Slow - const result = []; - for (let i = 0; i < this.entities.length; ++i) { - const entity = this.entities[i]; - if (entity instanceof entityClass) { - result.push(entity); - } - } - return result; - } - - /** - * Unregisters all components of an entity from the component to entity mapping - * @param {Entity} entity - */ - unregisterEntityComponents(entity) { - for (const componentId in entity.components) { - if (entity.components[componentId]) { - arrayDeleteValue(this.componentToEntity[componentId], entity); - } - } - } - - // Processes the entities to destroy and actually destroys them - /* eslint-disable max-statements */ - processDestroyList() { - for (let i = 0; i < this.destroyList.length; ++i) { - const entity = this.destroyList[i]; - - // Remove from entities list - arrayDeleteValue(this.entities, entity); - - // Remove from componentToEntity list - this.unregisterEntityComponents(entity); - - entity.registered = false; - entity.internalDestroyCallback(); - - this.root.signals.entityDestroyed.dispatch(entity); - } - - this.destroyList = []; - } - - /** - * Queues an entity for destruction - * @param {Entity} entity - */ - destroyEntity(entity) { - if (entity.destroyed) { - logger.error("Tried to destroy already destroyed entity:", entity.uid); - return; - } - - if (entity.queuedForDestroy) { - logger.error("Trying to destroy entity which is already queued for destroy!", entity.uid); - return; - } - - if (this.destroyList.indexOf(entity) < 0) { - this.destroyList.push(entity); - entity.queuedForDestroy = true; - this.root.signals.entityQueuedForDestroy.dispatch(entity); - } else { - assert(false, "Trying to destroy entity twice"); - } - } -} +import { arrayDeleteValue, newEmptyMap, fastArrayDeleteValue } from "../core/utils"; +import { Component } from "./component"; +import { GameRoot } from "./root"; +import { Entity } from "./entity"; +import { BasicSerializableObject, types } from "../savegame/serialization"; +import { createLogger } from "../core/logging"; +import { globalConfig } from "../core/config"; + +const logger = createLogger("entity_manager"); + +// Manages all entities + +// NOTICE: We use arrayDeleteValue instead of fastArrayDeleteValue since that does not preserve the order +// This is slower but we need it for the street path generation + +export class EntityManager extends BasicSerializableObject { + constructor(root) { + super(); + + /** @type {GameRoot} */ + this.root = root; + + /** @type {Array} */ + this.entities = []; + + // We store a separate list with entities to destroy, since we don't destroy + // them instantly + /** @type {Array} */ + this.destroyList = []; + + // Store a map from componentid to entities - This is used by the game system + // for faster processing + /** @type {Object.>} */ + this.componentToEntity = newEmptyMap(); + + // Store the next uid to use + this.nextUid = 10000; + } + + static getId() { + return "EntityManager"; + } + + static getSchema() { + return { + nextUid: types.uint, + }; + } + + getStatsText() { + return this.entities.length + " entities [" + this.destroyList.length + " to kill]"; + } + + // Main update + update() { + this.processDestroyList(); + } + + /** + * Registers a new entity + * @param {Entity} entity + * @param {number=} uid Optional predefined uid + */ + registerEntity(entity, uid = null) { + if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts) { + assert(this.entities.indexOf(entity) < 0, `RegisterEntity() called twice for entity ${entity}`); + } + assert(!entity.destroyed, `Attempting to register destroyed entity ${entity}`); + + if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts && uid !== null) { + assert(!this.findByUid(uid, false), "Entity uid already taken: " + uid); + assert(uid >= 0 && uid < Number.MAX_SAFE_INTEGER, "Invalid uid passed: " + uid); + } + + this.entities.push(entity); + + // Register into the componentToEntity map + for (const componentId in entity.components) { + if (entity.components[componentId]) { + if (this.componentToEntity[componentId]) { + this.componentToEntity[componentId].push(entity); + } else { + this.componentToEntity[componentId] = [entity]; + } + } + } + + // Give each entity a unique id + entity.uid = uid ? uid : this.generateUid(); + entity.registered = true; + + this.root.signals.entityAdded.dispatch(entity); + } + + /** + * Generates a new uid + * @returns {number} + */ + generateUid() { + return this.nextUid++; + } + + /** + * Call to attach a new component after the creation of the entity + * @param {Entity} entity + * @param {Component} component + */ + attachDynamicComponent(entity, component) { + entity.addComponent(component, true); + const componentId = /** @type {typeof Component} */ (component.constructor).getId(); + if (this.componentToEntity[componentId]) { + this.componentToEntity[componentId].push(entity); + } else { + this.componentToEntity[componentId] = [entity]; + } + this.root.signals.entityGotNewComponent.dispatch(entity); + } + + /** + * Call to remove a component after the creation of the entity + * @param {Entity} entity + * @param {typeof Component} component + */ + removeDynamicComponent(entity, component) { + entity.removeComponent(component, true); + const componentId = /** @type {typeof Component} */ (component.constructor).getId(); + + fastArrayDeleteValue(this.componentToEntity[componentId], entity); + this.root.signals.entityComponentRemoved.dispatch(entity); + } + + /** + * Finds an entity buy its uid, kinda slow since it loops over all entities + * @param {number} uid + * @param {boolean=} errorWhenNotFound + * @returns {Entity} + */ + findByUid(uid, errorWhenNotFound = true) { + const arr = this.entities; + for (let i = 0, len = arr.length; i < len; ++i) { + const entity = arr[i]; + if (entity.uid === uid) { + if (entity.queuedForDestroy || entity.destroyed) { + if (errorWhenNotFound) { + logger.warn("Entity with UID", uid, "not found (destroyed)"); + } + return null; + } + return entity; + } + } + if (errorWhenNotFound) { + logger.warn("Entity with UID", uid, "not found"); + } + return null; + } + + /** + * Returns a map which gives a mapping from UID to Entity. + * This map is not updated. + * + * @returns {Map} + */ + getFrozenUidSearchMap() { + const result = new Map(); + const array = this.entities; + for (let i = 0, len = array.length; i < len; ++i) { + const entity = array[i]; + if (!entity.queuedForDestroy && !entity.destroyed) { + result.set(entity.uid, entity); + } + } + return result; + } + + /** + * Returns all entities having the given component + * @param {typeof Component} componentHandle + * @returns {Array} entities + */ + getAllWithComponent(componentHandle) { + return this.componentToEntity[componentHandle.getId()] || []; + } + + /** + * Return all of a given class. This is SLOW! + * @param {object} entityClass + * @returns {Array} entities + */ + getAllOfClass(entityClass) { + // FIXME: Slow + const result = []; + for (let i = 0; i < this.entities.length; ++i) { + const entity = this.entities[i]; + if (entity instanceof entityClass) { + result.push(entity); + } + } + return result; + } + + /** + * Unregisters all components of an entity from the component to entity mapping + * @param {Entity} entity + */ + unregisterEntityComponents(entity) { + for (const componentId in entity.components) { + if (entity.components[componentId]) { + arrayDeleteValue(this.componentToEntity[componentId], entity); + } + } + } + + // Processes the entities to destroy and actually destroys them + /* eslint-disable max-statements */ + processDestroyList() { + for (let i = 0; i < this.destroyList.length; ++i) { + const entity = this.destroyList[i]; + + // Remove from entities list + arrayDeleteValue(this.entities, entity); + + // Remove from componentToEntity list + this.unregisterEntityComponents(entity); + + entity.registered = false; + entity.destroyed = true; + + this.root.signals.entityDestroyed.dispatch(entity); + } + + this.destroyList = []; + } + + /** + * Queues an entity for destruction + * @param {Entity} entity + */ + destroyEntity(entity) { + if (entity.destroyed) { + logger.error("Tried to destroy already destroyed entity:", entity.uid); + return; + } + + if (entity.queuedForDestroy) { + logger.error("Trying to destroy entity which is already queued for destroy!", entity.uid); + return; + } + + if (this.destroyList.indexOf(entity) < 0) { + this.destroyList.push(entity); + entity.queuedForDestroy = true; + this.root.signals.entityQueuedForDestroy.dispatch(entity); + } else { + assert(false, "Trying to destroy entity twice"); + } + } +} diff --git a/src/js/game/game_loading_overlay.js b/src/js/game/game_loading_overlay.js index f1e9d6ce..d6bb79f0 100644 --- a/src/js/game/game_loading_overlay.js +++ b/src/js/game/game_loading_overlay.js @@ -1,6 +1,8 @@ /* typehints:start */ import { Application } from "../application"; /* typehints:end */ + +import { randomChoice } from "../core/utils"; import { T } from "../translations"; export class GameLoadingOverlay { @@ -43,6 +45,7 @@ export class GameLoadingOverlay { this.element.classList.add("gameLoadingOverlay"); this.parent.appendChild(this.element); this.internalAddSpinnerAndText(this.element); + this.internalAddHint(this.element); } /** @@ -52,7 +55,17 @@ export class GameLoadingOverlay { internalAddSpinnerAndText(element) { const inner = document.createElement("span"); inner.classList.add("prefab_LoadingTextWithAnim"); - inner.innerText = T.global.loading; element.appendChild(inner); } + + /** + * Adds a random hint + * @param {HTMLElement} element + */ + internalAddHint(element) { + const hint = document.createElement("span"); + hint.innerHTML = randomChoice(T.tips); + hint.classList.add("prefab_GameHint"); + element.appendChild(hint); + } } diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index b0ae46f2..2c81f899 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -22,6 +22,7 @@ import { LeverSystem } from "./systems/lever"; import { DisplaySystem } from "./systems/display"; import { ItemProcessorOverlaysSystem } from "./systems/item_processor_overlays"; import { BeltReaderSystem } from "./systems/belt_reader"; +import { FilterSystem } from "./systems/filter"; const logger = createLogger("game_system_manager"); @@ -92,6 +93,9 @@ export class GameSystemManager { /** @type {BeltReaderSystem} */ beltReader: null, + /** @type {FilterSystem} */ + filter: null, + /* typehints:end */ }; this.systemUpdateOrder = []; @@ -124,6 +128,8 @@ export class GameSystemManager { add("itemProcessor", ItemProcessorSystem); + add("filter", FilterSystem); + add("itemEjector", ItemEjectorSystem); add("mapResources", MapResourcesSystem); diff --git a/src/js/game/game_system_with_filter.js b/src/js/game/game_system_with_filter.js index 7b1ffbf0..a6efeffd 100644 --- a/src/js/game/game_system_with_filter.js +++ b/src/js/game/game_system_with_filter.js @@ -1,131 +1,137 @@ -/* typehints:start */ -import { Component } from "./component"; -import { Entity } from "./entity"; -/* typehints:end */ - -import { GameRoot } from "./root"; -import { GameSystem } from "./game_system"; -import { arrayDelete, arrayDeleteValue } from "../core/utils"; - -export class GameSystemWithFilter extends GameSystem { - /** - * Constructs a new game system with the given component filter. It will process - * all entities which have *all* of the passed components - * @param {GameRoot} root - * @param {Array} requiredComponents - */ - constructor(root, requiredComponents) { - super(root); - this.requiredComponents = requiredComponents; - this.requiredComponentIds = requiredComponents.map(component => component.getId()); - - /** - * All entities which match the current components - * @type {Array} - */ - this.allEntities = []; - - this.root.signals.entityAdded.add(this.internalPushEntityIfMatching, this); - this.root.signals.entityGotNewComponent.add(this.internalReconsiderEntityToAdd, this); - this.root.signals.entityComponentRemoved.add(this.internalCheckEntityAfterComponentRemoval, this); - this.root.signals.entityQueuedForDestroy.add(this.internalPopEntityIfMatching, this); - - this.root.signals.postLoadHook.add(this.internalPostLoadHook, this); - this.root.signals.bulkOperationFinished.add(this.refreshCaches, this); - } - - /** - * @param {Entity} entity - */ - internalPushEntityIfMatching(entity) { - for (let i = 0; i < this.requiredComponentIds.length; ++i) { - if (!entity.components[this.requiredComponentIds[i]]) { - return; - } - } - - assert(this.allEntities.indexOf(entity) < 0, "entity already in list: " + entity); - this.internalRegisterEntity(entity); - } - - /** - * - * @param {Entity} entity - */ - internalCheckEntityAfterComponentRemoval(entity) { - if (this.allEntities.indexOf(entity) < 0) { - // Entity wasn't interesting anyways - return; - } - - for (let i = 0; i < this.requiredComponentIds.length; ++i) { - if (!entity.components[this.requiredComponentIds[i]]) { - // Entity is not interesting anymore - arrayDeleteValue(this.allEntities, entity); - } - } - } - - /** - * - * @param {Entity} entity - */ - internalReconsiderEntityToAdd(entity) { - for (let i = 0; i < this.requiredComponentIds.length; ++i) { - if (!entity.components[this.requiredComponentIds[i]]) { - return; - } - } - if (this.allEntities.indexOf(entity) >= 0) { - return; - } - this.internalRegisterEntity(entity); - } - - refreshCaches() { - this.allEntities.sort((a, b) => a.uid - b.uid); - - // Remove all entities which are queued for destroy - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - if (entity.queuedForDestroy || entity.destroyed) { - this.allEntities.splice(i, 1); - } - } - } - - /** - * Recomputes all target entities after the game has loaded - */ - internalPostLoadHook() { - this.refreshCaches(); - } - - /** - * - * @param {Entity} entity - */ - internalRegisterEntity(entity) { - this.allEntities.push(entity); - - if (this.root.gameInitialized && !this.root.bulkOperationRunning) { - // Sort entities by uid so behaviour is predictable - this.allEntities.sort((a, b) => a.uid - b.uid); - } - } - - /** - * - * @param {Entity} entity - */ - internalPopEntityIfMatching(entity) { - if (this.root.bulkOperationRunning) { - // We do this in refreshCaches afterwards - return; - } - const index = this.allEntities.indexOf(entity); - if (index >= 0) { - arrayDelete(this.allEntities, index); - } - } -} +/* typehints:start */ +import { Component } from "./component"; +import { Entity } from "./entity"; +/* typehints:end */ + +import { GameRoot } from "./root"; +import { GameSystem } from "./game_system"; +import { arrayDelete, arrayDeleteValue } from "../core/utils"; +import { globalConfig } from "../core/config"; + +export class GameSystemWithFilter extends GameSystem { + /** + * Constructs a new game system with the given component filter. It will process + * all entities which have *all* of the passed components + * @param {GameRoot} root + * @param {Array} requiredComponents + */ + constructor(root, requiredComponents) { + super(root); + this.requiredComponents = requiredComponents; + this.requiredComponentIds = requiredComponents.map(component => component.getId()); + + /** + * All entities which match the current components + * @type {Array} + */ + this.allEntities = []; + + this.root.signals.entityAdded.add(this.internalPushEntityIfMatching, this); + this.root.signals.entityGotNewComponent.add(this.internalReconsiderEntityToAdd, this); + this.root.signals.entityComponentRemoved.add(this.internalCheckEntityAfterComponentRemoval, this); + this.root.signals.entityQueuedForDestroy.add(this.internalPopEntityIfMatching, this); + + this.root.signals.postLoadHook.add(this.internalPostLoadHook, this); + this.root.signals.bulkOperationFinished.add(this.refreshCaches, this); + } + + /** + * @param {Entity} entity + */ + internalPushEntityIfMatching(entity) { + for (let i = 0; i < this.requiredComponentIds.length; ++i) { + if (!entity.components[this.requiredComponentIds[i]]) { + return; + } + } + + // This is slow! + if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts) { + assert(this.allEntities.indexOf(entity) < 0, "entity already in list: " + entity); + } + + this.internalRegisterEntity(entity); + } + + /** + * + * @param {Entity} entity + */ + internalCheckEntityAfterComponentRemoval(entity) { + if (this.allEntities.indexOf(entity) < 0) { + // Entity wasn't interesting anyways + return; + } + + for (let i = 0; i < this.requiredComponentIds.length; ++i) { + if (!entity.components[this.requiredComponentIds[i]]) { + // Entity is not interesting anymore + arrayDeleteValue(this.allEntities, entity); + } + } + } + + /** + * + * @param {Entity} entity + */ + internalReconsiderEntityToAdd(entity) { + for (let i = 0; i < this.requiredComponentIds.length; ++i) { + if (!entity.components[this.requiredComponentIds[i]]) { + return; + } + } + if (this.allEntities.indexOf(entity) >= 0) { + return; + } + this.internalRegisterEntity(entity); + } + + refreshCaches() { + // Remove all entities which are queued for destroy + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + if (entity.queuedForDestroy || entity.destroyed) { + this.allEntities.splice(i, 1); + i -= 1; + } + } + + this.allEntities.sort((a, b) => a.uid - b.uid); + } + + /** + * Recomputes all target entities after the game has loaded + */ + internalPostLoadHook() { + this.refreshCaches(); + } + + /** + * + * @param {Entity} entity + */ + internalRegisterEntity(entity) { + this.allEntities.push(entity); + + if (this.root.gameInitialized && !this.root.bulkOperationRunning) { + // Sort entities by uid so behaviour is predictable + this.allEntities.sort((a, b) => a.uid - b.uid); + } + } + + /** + * + * @param {Entity} entity + */ + internalPopEntityIfMatching(entity) { + if (this.root.bulkOperationRunning) { + // We do this in refreshCaches afterwards + return; + } + const index = this.allEntities.indexOf(entity); + if (index >= 0) { + arrayDelete(this.allEntities, index); + } + } +} diff --git a/src/js/game/hints.js b/src/js/game/hints.js new file mode 100644 index 00000000..c2e7e4e5 --- /dev/null +++ b/src/js/game/hints.js @@ -0,0 +1,22 @@ +import { randomChoice } from "../core/utils"; +import { T } from "../translations"; + +const hintsShown = []; + +/** + * Finds a new hint to show about the game which the user hasn't seen within this session + */ +export function getRandomHint() { + let maxTries = 100 * T.tips.length; + + while (maxTries-- > 0) { + const hint = randomChoice(T.tips); + if (!hintsShown.includes(hint)) { + hintsShown.push(hint); + return hint; + } + } + + // All tips shown so far + return randomChoice(T.tips); +} diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index 71817ebd..16947f24 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -43,11 +43,11 @@ export class HubGoals extends BasicSerializableObject { // Compute upgrade improvements for (const upgradeId in UPGRADES) { - const upgradeHandle = UPGRADES[upgradeId]; + const tiers = UPGRADES[upgradeId]; const level = this.upgradeLevels[upgradeId] || 0; - let totalImprovement = upgradeHandle.baseValue || 1; + let totalImprovement = 1; for (let i = 0; i < level; ++i) { - totalImprovement += upgradeHandle.tiers[i].improvement; + totalImprovement += tiers[i].improvement; } this.upgradeImprovements[upgradeId] = totalImprovement; } @@ -98,7 +98,7 @@ export class HubGoals extends BasicSerializableObject { */ this.upgradeImprovements = {}; for (const key in UPGRADES) { - this.upgradeImprovements[key] = UPGRADES[key].baseValue || 1; + this.upgradeImprovements[key] = 1; } this.createNextGoal(); @@ -212,7 +212,7 @@ export class HubGoals extends BasicSerializableObject { this.currentGoal = { /** @type {ShapeDefinition} */ definition: this.createRandomShape(), - required: 10000 + findNiceIntegerValue(this.level * 2000), + required: findNiceIntegerValue(5000 + Math.pow(this.level * 2000, 0.75)), reward: enumHubGoalRewards.no_reward_freeplay, }; } @@ -243,10 +243,10 @@ export class HubGoals extends BasicSerializableObject { * @param {string} upgradeId */ canUnlockUpgrade(upgradeId) { - const handle = UPGRADES[upgradeId]; + const tiers = UPGRADES[upgradeId]; const currentLevel = this.getUpgradeLevel(upgradeId); - if (currentLevel >= handle.tiers.length) { + if (currentLevel >= tiers.length) { // Max level return false; } @@ -255,7 +255,7 @@ export class HubGoals extends BasicSerializableObject { return true; } - const tierData = handle.tiers[currentLevel]; + const tierData = tiers[currentLevel]; for (let i = 0; i < tierData.required.length; ++i) { const requirement = tierData.required[i]; @@ -290,10 +290,10 @@ export class HubGoals extends BasicSerializableObject { return false; } - const handle = UPGRADES[upgradeId]; + const upgradeTiers = UPGRADES[upgradeId]; const currentLevel = this.getUpgradeLevel(upgradeId); - const tierData = handle.tiers[currentLevel]; + const tierData = upgradeTiers[currentLevel]; if (!tierData) { return false; } @@ -396,15 +396,11 @@ export class HubGoals extends BasicSerializableObject { */ getProcessorBaseSpeed(processorType) { switch (processorType) { - case enumItemProcessorTypes.splitterWires: - return globalConfig.wiresSpeedItemsPerSecond * 2; - case enumItemProcessorTypes.trash: case enumItemProcessorTypes.hub: return 1e30; - case enumItemProcessorTypes.splitter: + case enumItemProcessorTypes.balancer: return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2; - case enumItemProcessorTypes.filter: case enumItemProcessorTypes.reader: return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt; @@ -427,7 +423,7 @@ export class HubGoals extends BasicSerializableObject { case enumItemProcessorTypes.cutterQuad: case enumItemProcessorTypes.rotater: case enumItemProcessorTypes.rotaterCCW: - case enumItemProcessorTypes.rotaterFL: + case enumItemProcessorTypes.rotater180: case enumItemProcessorTypes.stacker: { assert( globalConfig.buildingSpeeds[processorType], diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index 3edc4e17..e0ddfd9d 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -194,9 +194,6 @@ export class GameHUD { * Returns true if the rendering can be paused */ hasBlockingOverlayOpen() { - if (this.root.camera.getIsMapOverlayActive()) { - return true; - } for (const key in this.parts) { if (this.parts[key].isBlockingOverlay()) { return true; diff --git a/src/js/game/hud/parts/building_placer_logic.js b/src/js/game/hud/parts/building_placer_logic.js index 6031e555..8e8e72c3 100644 --- a/src/js/game/hud/parts/building_placer_logic.js +++ b/src/js/game/hud/parts/building_placer_logic.js @@ -253,6 +253,12 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart { * @see BaseHUDPart.update */ update() { + // Abort placement if a dialog was shown in the meantime + if (this.root.hud.hasBlockingOverlayOpen()) { + this.abortPlacement(); + return; + } + // Always update since the camera might have moved const mousePos = this.root.app.mousePosition; if (mousePos) { @@ -330,7 +336,7 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart { if (tileBelow && this.root.app.settings.getAllSettings().pickMinerOnPatch) { this.currentMetaBuilding.set(gMetaBuildingRegistry.findByClass(MetaMinerBuilding)); - // Select chained miner if available, since thats always desired once unlocked + // Select chained miner if available, since that's always desired once unlocked if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_miner_chainable)) { this.currentVariant.set(enumMinerVariants.chainable); } diff --git a/src/js/game/hud/parts/buildings_toolbar.js b/src/js/game/hud/parts/buildings_toolbar.js index f8953204..19754436 100644 --- a/src/js/game/hud/parts/buildings_toolbar.js +++ b/src/js/game/hud/parts/buildings_toolbar.js @@ -1,22 +1,22 @@ -import { MetaBeltBaseBuilding } from "../../buildings/belt_base"; +import { MetaBeltBuilding } from "../../buildings/belt"; import { MetaCutterBuilding } from "../../buildings/cutter"; +import { MetaDisplayBuilding } from "../../buildings/display"; +import { MetaFilterBuilding } from "../../buildings/filter"; +import { MetaLeverBuilding } from "../../buildings/lever"; import { MetaMinerBuilding } from "../../buildings/miner"; import { MetaMixerBuilding } from "../../buildings/mixer"; import { MetaPainterBuilding } from "../../buildings/painter"; +import { MetaReaderBuilding } from "../../buildings/reader"; import { MetaRotaterBuilding } from "../../buildings/rotater"; -import { MetaSplitterBuilding } from "../../buildings/splitter"; +import { MetaBalancerBuilding } from "../../buildings/balancer"; import { MetaStackerBuilding } from "../../buildings/stacker"; import { MetaTrashBuilding } from "../../buildings/trash"; import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt"; import { HUDBaseToolbar } from "./base_toolbar"; -import { MetaLeverBuilding } from "../../buildings/lever"; -import { MetaFilterBuilding } from "../../buildings/filter"; -import { MetaDisplayBuilding } from "../../buildings/display"; -import { MetaReaderBuilding } from "../../buildings/reader"; const supportedBuildings = [ - MetaBeltBaseBuilding, - MetaSplitterBuilding, + MetaBeltBuilding, + MetaBalancerBuilding, MetaUndergroundBeltBuilding, MetaMinerBuilding, MetaCutterBuilding, diff --git a/src/js/game/hud/parts/entity_debugger.js b/src/js/game/hud/parts/entity_debugger.js index 80f15eea..640ad4d6 100644 --- a/src/js/game/hud/parts/entity_debugger.js +++ b/src/js/game/hud/parts/entity_debugger.js @@ -1,7 +1,13 @@ -import { BaseHUDPart } from "../base_hud_part"; +/* dev:start */ import { makeDiv, removeAllChildren } from "../../../core/utils"; -import { globalConfig } from "../../../core/config"; +import { Vector } from "../../../core/vector"; +import { Entity } from "../../entity"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +/** + * Allows to inspect entities by pressing F8 while hovering them + */ export class HUDEntityDebugger extends BaseHUDPart { createElements(parent) { this.element = makeDiv( @@ -9,65 +15,147 @@ export class HUDEntityDebugger extends BaseHUDPart { "ingame_HUD_EntityDebugger", [], ` - Tile below cursor:
- Chunk below cursor:
-
+ + Use F8 to toggle this overlay + +
+
+
` ); - - /** @type {HTMLElement} */ - this.mousePosElem = this.element.querySelector(".mousePos"); - /** @type {HTMLElement} */ - this.chunkPosElem = this.element.querySelector(".chunkPos"); - this.entityInfoElem = this.element.querySelector(".entityInfo"); + this.componentsElem = this.element.querySelector(".entityComponents"); } initialize() { - this.root.camera.downPreHandler.add(this.onMouseDown, this); + this.root.gameState.inputReciever.keydown.add(key => { + if (key.keyCode === 119) { + // F8 + this.pickEntity(); + } + }); + + /** + * The currently selected entity + * @type {Entity} + */ + this.selectedEntity = null; + + this.lastUpdate = 0; + + this.domAttach = new DynamicDomAttach(this.root, this.element); } - update() { + pickEntity() { const mousePos = this.root.app.mousePosition; if (!mousePos) { return; } const worldPos = this.root.camera.screenToWorld(mousePos); const worldTile = worldPos.toTileSpace(); - - const chunk = worldTile.divideScalar(globalConfig.mapChunkSize).floor(); - this.mousePosElem.innerText = worldTile.x + " / " + worldTile.y; - this.chunkPosElem.innerText = chunk.x + " / " + chunk.y; - const entity = this.root.map.getTileContent(worldTile, this.root.currentLayer); + + this.selectedEntity = entity; if (entity) { - removeAllChildren(this.entityInfoElem); - let html = "Entity"; - - const flag = (name, val) => - `${name} ${val}`; - - html += "
"; - html += flag("registered", entity.registered); - html += flag("uid", entity.uid); - html += flag("destroyed", entity.destroyed); - html += "
"; - - html += "
"; - - for (const componentId in entity.components) { - const data = entity.components[componentId]; - html += "
"; - html += "" + componentId + ""; - html += ""; - - html += "
"; - } - - html += "
"; - - this.entityInfoElem.innerHTML = html; + this.rerenderFull(entity); } } - onMouseDown() {} + /** + * + * @param {string} name + * @param {any} val + * @param {number} indent + * @param {Array} recursion + */ + propertyToHTML(name, val, indent = 0, recursion = []) { + if (indent > 20) { + return; + } + + if (val !== null && typeof val === "object") { + // Array is displayed like object, with indexes + recursion.push(val); + + // Get type class name (like Array, Object, Vector...) + let typeName = `(${val.constructor ? val.constructor.name : "unknown"})`; + + if (Array.isArray(val)) { + typeName = `(Array[${val.length}])`; + } + + if (val instanceof Vector) { + typeName = `(Vector[${val.x}, ${val.y}])`; + } + + const colorStyle = `color: hsl(${30 * indent}, 100%, 80%)`; + + let html = `
+ ${name} ${typeName} +
`; + + for (const property in val) { + const isRoot = val[property] == this.root; + const isRecursive = recursion.includes(val[property]); + + let hiddenValue = isRoot ? "" : null; + if (isRecursive) { + // Avoid recursion by not "expanding" object more than once + hiddenValue = ""; + } + + html += this.propertyToHTML( + property, + hiddenValue ? hiddenValue : val[property], + indent + 1, + [...recursion] // still expand same value in other "branches" + ); + } + + html += "
"; + + return html; + } + + const displayValue = (val + "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">"); + return ` ${displayValue}`; + } + + /** + * Rerenders the whole container + * @param {Entity} entity + */ + rerenderFull(entity) { + removeAllChildren(this.componentsElem); + let html = ""; + + const property = (strings, val) => ` ${val}`; + + html += property`registered ${!!entity.registered}`; + html += property`uid ${entity.uid}`; + html += property`destroyed ${!!entity.destroyed}`; + + for (const componentId in entity.components) { + const data = entity.components[componentId]; + html += "
"; + html += "" + componentId + "
"; + + for (const property in data) { + // Put entity into recursion list, so it won't get "expanded" + html += this.propertyToHTML(property, data[property], 0, [entity]); + } + + html += "
"; + } + + this.componentsElem.innerHTML = html; + } + + update() { + this.domAttach.update(!!this.selectedEntity); + } } + +/* dev:end */ diff --git a/src/js/game/hud/parts/game_menu.js b/src/js/game/hud/parts/game_menu.js index 59ba0232..2a172ab2 100644 --- a/src/js/game/hud/parts/game_menu.js +++ b/src/js/game/hud/parts/game_menu.js @@ -5,6 +5,7 @@ import { enumNotificationType } from "./notifications"; import { T } from "../../../translations"; import { KEYMAPPINGS } from "../../key_action_mapper"; import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { TrackedState } from "../../../core/tracked_state"; export class HUDGameMenu extends BaseHUDPart { createElements(parent) { @@ -51,18 +52,15 @@ export class HUDGameMenu extends BaseHUDPart { * }>} */ this.visibilityToUpdate = []; - this.buttonsElement = makeDiv(this.element, null, ["buttonContainer"]); - buttons.forEach(({ id, label, handler, keybinding, badge, notification, visible }) => { const button = document.createElement("button"); - button.setAttribute("data-button-id", id); - this.buttonsElement.appendChild(button); + button.classList.add(id); + this.element.appendChild(button); this.trackClicks(button, handler); if (keybinding) { const binding = this.root.keyMapper.getBinding(keybinding); binding.add(handler); - binding.appendLabelToElement(button); } if (visible) { @@ -86,10 +84,8 @@ export class HUDGameMenu extends BaseHUDPart { } }); - const menuButtons = makeDiv(this.element, null, ["menuButtons"]); - - this.saveButton = makeDiv(menuButtons, null, ["button", "save", "animEven"]); - this.settingsButton = makeDiv(menuButtons, null, ["button", "settings"]); + this.saveButton = makeDiv(this.element, null, ["button", "save", "animEven"]); + this.settingsButton = makeDiv(this.element, null, ["button", "settings"]); this.trackClicks(this.saveButton, this.startSave); this.trackClicks(this.settingsButton, this.openSettings); @@ -97,12 +93,17 @@ export class HUDGameMenu extends BaseHUDPart { initialize() { this.root.signals.gameSaved.add(this.onGameSaved, this); + + this.trackedIsSaving = new TrackedState(this.onIsSavingChanged, this); } update() { let playSound = false; let notifications = new Set(); + // Check whether we are saving + this.trackedIsSaving.set(!!this.root.gameState.currentSavePromise); + // Update visibility of buttons for (let i = 0; i < this.visibilityToUpdate.length; ++i) { const { condition, domAttach } = this.visibilityToUpdate[i]; @@ -154,6 +155,10 @@ export class HUDGameMenu extends BaseHUDPart { }); } + onIsSavingChanged(isSaving) { + this.saveButton.classList.toggle("saving", isSaving); + } + onGameSaved() { this.saveButton.classList.toggle("animEven"); this.saveButton.classList.toggle("animOdd"); diff --git a/src/js/game/hud/parts/keybinding_overlay.js b/src/js/game/hud/parts/keybinding_overlay.js index d31ee746..834a7385 100644 --- a/src/js/game/hud/parts/keybinding_overlay.js +++ b/src/js/game/hud/parts/keybinding_overlay.js @@ -162,13 +162,6 @@ export class HUDKeybindingOverlay extends BaseHUDPart { condition: () => this.mapOverviewActive && !this.blueprintPlacementActive, }, - { - // Pipette - label: T.ingame.keybindingsOverlay.pipette, - keys: [k.placement.pipette], - condition: () => !this.mapOverviewActive && !this.blueprintPlacementActive, - }, - { // Cancel placement label: T.ingame.keybindingsOverlay.stopPlacement, @@ -184,6 +177,13 @@ export class HUDKeybindingOverlay extends BaseHUDPart { !this.anyPlacementActive && !this.mapOverviewActive && !this.anythingSelectedOnMap, }, + { + // Pipette + label: T.ingame.keybindingsOverlay.pipette, + keys: [k.placement.pipette], + condition: () => !this.mapOverviewActive && !this.blueprintPlacementActive, + }, + { // Area select label: T.ingame.keybindingsOverlay.selectBuildings, diff --git a/src/js/game/hud/parts/mass_selector.js b/src/js/game/hud/parts/mass_selector.js index a8972434..08a11769 100644 --- a/src/js/game/hud/parts/mass_selector.js +++ b/src/js/game/hud/parts/mass_selector.js @@ -48,6 +48,9 @@ export class HUDMassSelector extends BaseHUDPart { * @param {Entity} entity */ onEntityDestroyed(entity) { + if (this.root.bulkOperationRunning) { + return; + } this.selectedUids.delete(entity.uid); } @@ -90,14 +93,30 @@ export class HUDMassSelector extends BaseHUDPart { doDelete() { const entityUids = Array.from(this.selectedUids); - for (let i = 0; i < entityUids.length; ++i) { - const uid = entityUids[i]; - const entity = this.root.entityMgr.findByUid(uid); - if (!this.root.logic.tryDeleteBuilding(entity)) { - logger.error("Error in mass delete, could not remove building"); - this.selectedUids.delete(uid); + + // Build mapping from uid to entity + /** + * @type {Map} + */ + const mapUidToEntity = this.root.entityMgr.getFrozenUidSearchMap(); + + this.root.logic.performBulkOperation(() => { + for (let i = 0; i < entityUids.length; ++i) { + const uid = entityUids[i]; + const entity = mapUidToEntity.get(uid); + if (!entity) { + logger.error("Entity not found by uid:", uid); + continue; + } + + if (!this.root.logic.tryDeleteBuilding(entity)) { + logger.error("Error in mass delete, could not remove building"); + } } - } + }); + + // Clear uids later + this.selectedUids = new Set(); } startCopy() { diff --git a/src/js/game/hud/parts/miner_highlight.js b/src/js/game/hud/parts/miner_highlight.js index c2b23583..a0c6919d 100644 --- a/src/js/game/hud/parts/miner_highlight.js +++ b/src/js/game/hud/parts/miner_highlight.js @@ -45,6 +45,12 @@ export class HUDMinerHighlight extends BaseHUDPart { return; } + const lowerContents = this.root.map.getLowerLayerContentXY(hoveredTile.x, hoveredTile.y); + if (!lowerContents) { + // Not connected + return; + } + parameters.context.fillStyle = THEME.map.connectedMiners.overlay; const connectedEntities = this.findConnectedMiners(contents); @@ -67,7 +73,7 @@ export class HUDMinerHighlight extends BaseHUDPart { const maxThroughput = this.root.hubGoals.getBeltBaseSpeed(); - const screenPos = this.root.camera.screenToWorld(mousePos); + const tooltipLocation = this.root.camera.screenToWorld(mousePos); const scale = (1 / this.root.camera.zoomLevel) * this.root.app.getEffectiveUiScale(); @@ -76,8 +82,8 @@ export class HUDMinerHighlight extends BaseHUDPart { // Background parameters.context.fillStyle = THEME.map.connectedMiners.background; parameters.context.beginRoundedRect( - screenPos.x + 5 * scale, - screenPos.y - 3 * scale, + tooltipLocation.x + 5 * scale, + tooltipLocation.y - 3 * scale, (isCapped ? 100 : 65) * scale, (isCapped ? 45 : 30) * scale, 2 @@ -89,8 +95,8 @@ export class HUDMinerHighlight extends BaseHUDPart { parameters.context.font = "bold " + scale * 10 + "px GameFont"; parameters.context.fillText( formatItemsPerSecond(throughput), - screenPos.x + 10 * scale, - screenPos.y + 10 * scale + tooltipLocation.x + 10 * scale, + tooltipLocation.y + 10 * scale ); // Amount of miners @@ -100,8 +106,8 @@ export class HUDMinerHighlight extends BaseHUDPart { connectedEntities.length === 1 ? T.ingame.connectedMiners.one_miner : T.ingame.connectedMiners.n_miners.replace("", String(connectedEntities.length)), - screenPos.x + 10 * scale, - screenPos.y + 22 * scale + tooltipLocation.x + 10 * scale, + tooltipLocation.y + 22 * scale ); parameters.context.globalAlpha = 1; @@ -113,8 +119,8 @@ export class HUDMinerHighlight extends BaseHUDPart { "", formatItemsPerSecond(maxThroughput) ), - screenPos.x + 10 * scale, - screenPos.y + 34 * scale + tooltipLocation.x + 10 * scale, + tooltipLocation.y + 34 * scale ); } } diff --git a/src/js/game/hud/parts/modal_dialogs.js b/src/js/game/hud/parts/modal_dialogs.js index 95428691..06993616 100644 --- a/src/js/game/hud/parts/modal_dialogs.js +++ b/src/js/game/hud/parts/modal_dialogs.js @@ -1,211 +1,215 @@ -/* typehints:start */ -import { Application } from "../../../application"; -/* typehints:end */ - -import { SOUNDS } from "../../../platform/sound"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { BaseHUDPart } from "../base_hud_part"; -import { Dialog, DialogLoading, DialogOptionChooser } from "../../../core/modal_dialog_elements"; -import { makeDiv } from "../../../core/utils"; -import { T } from "../../../translations"; -import { THIRDPARTY_URLS } from "../../../core/config"; - -export class HUDModalDialogs extends BaseHUDPart { - constructor(root, app) { - // Important: Root is not always available here! Its also used in the main menu - super(root); - - /** @type {Application} */ - this.app = root ? root.app : app; - - this.dialogParent = null; - this.dialogStack = []; - } - - // For use inside of the game, implementation of base hud part - initialize() { - this.dialogParent = document.getElementById("ingame_HUD_ModalDialogs"); - this.domWatcher = new DynamicDomAttach(this.root, this.dialogParent); - } - - shouldPauseRendering() { - return this.dialogStack.length > 0; - } - - shouldPauseGame() { - return this.shouldPauseRendering(); - } - - createElements(parent) { - return makeDiv(parent, "ingame_HUD_ModalDialogs"); - } - - // For use outside of the game - initializeToElement(element) { - assert(element, "No element for dialogs given"); - this.dialogParent = element; - } - - // Methods - - /** - * @param {string} title - * @param {string} text - * @param {Array} buttons - */ - showInfo(title, text, buttons = ["ok:good"]) { - const dialog = new Dialog({ - app: this.app, - title: title, - contentHTML: text, - buttons: buttons, - type: "info", - }); - this.internalShowDialog(dialog); - - if (this.app) { - this.app.sound.playUiSound(SOUNDS.dialogOk); - } - - return dialog.buttonSignals; - } - - /** - * @param {string} title - * @param {string} text - * @param {Array} buttons - */ - showWarning(title, text, buttons = ["ok:good"]) { - const dialog = new Dialog({ - app: this.app, - title: title, - contentHTML: text, - buttons: buttons, - type: "warning", - }); - this.internalShowDialog(dialog); - - if (this.app) { - this.app.sound.playUiSound(SOUNDS.dialogError); - } - - return dialog.buttonSignals; - } - - /** - * @param {string} feature - * @param {string} textPrefab - */ - showFeatureRestrictionInfo(feature, textPrefab = T.dialogs.featureRestriction.desc) { - const dialog = new Dialog({ - app: this.app, - title: T.dialogs.featureRestriction.title, - contentHTML: textPrefab.replace("", feature), - buttons: ["cancel:bad", "getStandalone:good"], - type: "warning", - }); - this.internalShowDialog(dialog); - - if (this.app) { - this.app.sound.playUiSound(SOUNDS.dialogOk); - } - - this.app.analytics.trackUiClick("demo_dialog_show"); - - dialog.buttonSignals.cancel.add(() => { - this.app.analytics.trackUiClick("demo_dialog_cancel"); - }); - - dialog.buttonSignals.getStandalone.add(() => { - this.app.analytics.trackUiClick("demo_dialog_click"); - window.open(THIRDPARTY_URLS.standaloneStorePage); - }); - - return dialog.buttonSignals; - } - - showOptionChooser(title, options) { - const dialog = new DialogOptionChooser({ - app: this.app, - title, - options, - }); - this.internalShowDialog(dialog); - return dialog.buttonSignals; - } - - // Returns method to be called when laoding finishd - showLoadingDialog() { - const dialog = new DialogLoading(this.app); - this.internalShowDialog(dialog); - return this.closeDialog.bind(this, dialog); - } - - internalShowDialog(dialog) { - const elem = dialog.createElement(); - dialog.setIndex(this.dialogStack.length); - - // Hide last dialog in queue - if (this.dialogStack.length > 0) { - this.dialogStack[this.dialogStack.length - 1].hide(); - } - - this.dialogStack.push(dialog); - - // Append dialog - dialog.show(); - dialog.closeRequested.add(this.closeDialog.bind(this, dialog)); - - // Append to HTML - this.dialogParent.appendChild(elem); - - document.body.classList.toggle("modalDialogActive", this.dialogStack.length > 0); - - // IMPORTANT: Attach element directly, otherwise double submit is possible - this.update(); - } - - update() { - if (this.domWatcher) { - this.domWatcher.update(this.dialogStack.length > 0); - } - } - - closeDialog(dialog) { - dialog.destroy(); - - let index = -1; - for (let i = 0; i < this.dialogStack.length; ++i) { - if (this.dialogStack[i] === dialog) { - index = i; - break; - } - } - assert(index >= 0, "Dialog not in dialog stack"); - this.dialogStack.splice(index, 1); - - if (this.dialogStack.length > 0) { - // Show the dialog which was previously open - this.dialogStack[this.dialogStack.length - 1].show(); - } - - document.body.classList.toggle("modalDialogActive", this.dialogStack.length > 0); - } - - close() { - for (let i = 0; i < this.dialogStack.length; ++i) { - const dialog = this.dialogStack[i]; - dialog.destroy(); - } - this.dialogStack = []; - } - - cleanup() { - super.cleanup(); - for (let i = 0; i < this.dialogStack.length; ++i) { - this.dialogStack[i].destroy(); - } - this.dialogStack = []; - this.dialogParent = null; - } -} +/* typehints:start */ +import { Application } from "../../../application"; +/* typehints:end */ + +import { SOUNDS } from "../../../platform/sound"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { BaseHUDPart } from "../base_hud_part"; +import { Dialog, DialogLoading, DialogOptionChooser } from "../../../core/modal_dialog_elements"; +import { makeDiv } from "../../../core/utils"; +import { T } from "../../../translations"; +import { THIRDPARTY_URLS } from "../../../core/config"; + +export class HUDModalDialogs extends BaseHUDPart { + constructor(root, app) { + // Important: Root is not always available here! Its also used in the main menu + super(root); + + /** @type {Application} */ + this.app = root ? root.app : app; + + this.dialogParent = null; + this.dialogStack = []; + } + + // For use inside of the game, implementation of base hud part + initialize() { + this.dialogParent = document.getElementById("ingame_HUD_ModalDialogs"); + this.domWatcher = new DynamicDomAttach(this.root, this.dialogParent); + } + + shouldPauseRendering() { + return this.dialogStack.length > 0; + } + + shouldPauseGame() { + return this.shouldPauseRendering(); + } + + createElements(parent) { + return makeDiv(parent, "ingame_HUD_ModalDialogs"); + } + + // For use outside of the game + initializeToElement(element) { + assert(element, "No element for dialogs given"); + this.dialogParent = element; + } + + isBlockingOverlay() { + return this.dialogStack.length > 0; + } + + // Methods + + /** + * @param {string} title + * @param {string} text + * @param {Array} buttons + */ + showInfo(title, text, buttons = ["ok:good"]) { + const dialog = new Dialog({ + app: this.app, + title: title, + contentHTML: text, + buttons: buttons, + type: "info", + }); + this.internalShowDialog(dialog); + + if (this.app) { + this.app.sound.playUiSound(SOUNDS.dialogOk); + } + + return dialog.buttonSignals; + } + + /** + * @param {string} title + * @param {string} text + * @param {Array} buttons + */ + showWarning(title, text, buttons = ["ok:good"]) { + const dialog = new Dialog({ + app: this.app, + title: title, + contentHTML: text, + buttons: buttons, + type: "warning", + }); + this.internalShowDialog(dialog); + + if (this.app) { + this.app.sound.playUiSound(SOUNDS.dialogError); + } + + return dialog.buttonSignals; + } + + /** + * @param {string} feature + * @param {string} textPrefab + */ + showFeatureRestrictionInfo(feature, textPrefab = T.dialogs.featureRestriction.desc) { + const dialog = new Dialog({ + app: this.app, + title: T.dialogs.featureRestriction.title, + contentHTML: textPrefab.replace("", feature), + buttons: ["cancel:bad", "getStandalone:good"], + type: "warning", + }); + this.internalShowDialog(dialog); + + if (this.app) { + this.app.sound.playUiSound(SOUNDS.dialogOk); + } + + this.app.analytics.trackUiClick("demo_dialog_show"); + + dialog.buttonSignals.cancel.add(() => { + this.app.analytics.trackUiClick("demo_dialog_cancel"); + }); + + dialog.buttonSignals.getStandalone.add(() => { + this.app.analytics.trackUiClick("demo_dialog_click"); + window.open(THIRDPARTY_URLS.standaloneStorePage); + }); + + return dialog.buttonSignals; + } + + showOptionChooser(title, options) { + const dialog = new DialogOptionChooser({ + app: this.app, + title, + options, + }); + this.internalShowDialog(dialog); + return dialog.buttonSignals; + } + + // Returns method to be called when laoding finishd + showLoadingDialog() { + const dialog = new DialogLoading(this.app); + this.internalShowDialog(dialog); + return this.closeDialog.bind(this, dialog); + } + + internalShowDialog(dialog) { + const elem = dialog.createElement(); + dialog.setIndex(this.dialogStack.length); + + // Hide last dialog in queue + if (this.dialogStack.length > 0) { + this.dialogStack[this.dialogStack.length - 1].hide(); + } + + this.dialogStack.push(dialog); + + // Append dialog + dialog.show(); + dialog.closeRequested.add(this.closeDialog.bind(this, dialog)); + + // Append to HTML + this.dialogParent.appendChild(elem); + + document.body.classList.toggle("modalDialogActive", this.dialogStack.length > 0); + + // IMPORTANT: Attach element directly, otherwise double submit is possible + this.update(); + } + + update() { + if (this.domWatcher) { + this.domWatcher.update(this.dialogStack.length > 0); + } + } + + closeDialog(dialog) { + dialog.destroy(); + + let index = -1; + for (let i = 0; i < this.dialogStack.length; ++i) { + if (this.dialogStack[i] === dialog) { + index = i; + break; + } + } + assert(index >= 0, "Dialog not in dialog stack"); + this.dialogStack.splice(index, 1); + + if (this.dialogStack.length > 0) { + // Show the dialog which was previously open + this.dialogStack[this.dialogStack.length - 1].show(); + } + + document.body.classList.toggle("modalDialogActive", this.dialogStack.length > 0); + } + + close() { + for (let i = 0; i < this.dialogStack.length; ++i) { + const dialog = this.dialogStack[i]; + dialog.destroy(); + } + this.dialogStack = []; + } + + cleanup() { + super.cleanup(); + for (let i = 0; i < this.dialogStack.length; ++i) { + this.dialogStack[i].destroy(); + } + this.dialogStack = []; + this.dialogParent = null; + } +} diff --git a/src/js/game/hud/parts/notifications.js b/src/js/game/hud/parts/notifications.js index aef0cc75..bef8dd0f 100644 --- a/src/js/game/hud/parts/notifications.js +++ b/src/js/game/hud/parts/notifications.js @@ -1,56 +1,55 @@ -import { BaseHUDPart } from "../base_hud_part"; -import { makeDiv } from "../../../core/utils"; -import { T } from "../../../translations"; -import { IS_DEMO } from "../../../core/config"; - -/** @enum {string} */ -export const enumNotificationType = { - saved: "saved", - upgrade: "upgrade", - success: "success", -}; - -const notificationDuration = 3; - -export class HUDNotifications extends BaseHUDPart { - createElements(parent) { - this.element = makeDiv(parent, "ingame_HUD_Notifications", [], ``); - } - - initialize() { - this.root.hud.signals.notification.add(this.onNotification, this); - - /** @type {Array<{ element: HTMLElement, expireAt: number}>} */ - this.notificationElements = []; - - // Automatic notifications - this.root.signals.gameSaved.add(() => - this.onNotification(T.ingame.notifications.gameSaved, enumNotificationType.saved) - ); - } - - /** - * @param {string} message - * @param {enumNotificationType} type - */ - onNotification(message, type) { - const element = makeDiv(this.element, null, ["notification", "type-" + type], message); - element.setAttribute("data-icon", "icons/notification_" + type + ".png"); - - this.notificationElements.push({ - element, - expireAt: this.root.time.realtimeNow() + notificationDuration, - }); - } - - update() { - const now = this.root.time.realtimeNow(); - for (let i = 0; i < this.notificationElements.length; ++i) { - const handle = this.notificationElements[i]; - if (handle.expireAt <= now) { - handle.element.remove(); - this.notificationElements.splice(i, 1); - } - } - } -} +import { makeDiv } from "../../../core/utils"; +import { T } from "../../../translations"; +import { BaseHUDPart } from "../base_hud_part"; + +/** @enum {string} */ +export const enumNotificationType = { + saved: "saved", + upgrade: "upgrade", + success: "success", +}; + +const notificationDuration = 3; + +export class HUDNotifications extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_Notifications", [], ``); + } + + initialize() { + this.root.hud.signals.notification.add(this.onNotification, this); + + /** @type {Array<{ element: HTMLElement, expireAt: number}>} */ + this.notificationElements = []; + + // Automatic notifications + this.root.signals.gameSaved.add(() => + this.onNotification(T.ingame.notifications.gameSaved, enumNotificationType.saved) + ); + } + + /** + * @param {string} message + * @param {enumNotificationType} type + */ + onNotification(message, type) { + const element = makeDiv(this.element, null, ["notification", "type-" + type], message); + element.setAttribute("data-icon", "icons/notification_" + type + ".png"); + + this.notificationElements.push({ + element, + expireAt: this.root.time.realtimeNow() + notificationDuration, + }); + } + + update() { + const now = this.root.time.realtimeNow(); + for (let i = 0; i < this.notificationElements.length; ++i) { + const handle = this.notificationElements[i]; + if (handle.expireAt <= now) { + handle.element.remove(); + this.notificationElements.splice(i, 1); + } + } + } +} diff --git a/src/js/game/hud/parts/pinned_shapes.js b/src/js/game/hud/parts/pinned_shapes.js index 2f7dd11e..c54554bf 100644 --- a/src/js/game/hud/parts/pinned_shapes.js +++ b/src/js/game/hud/parts/pinned_shapes.js @@ -1,291 +1,291 @@ -import { ClickDetector } from "../../../core/click_detector"; -import { formatBigNumber, makeDiv, arrayDeleteValue } from "../../../core/utils"; -import { ShapeDefinition } from "../../shape_definition"; -import { BaseHUDPart } from "../base_hud_part"; -import { blueprintShape, UPGRADES } from "../../upgrades"; -import { enumHubGoalRewards } from "../../tutorial_goals"; - -/** - * Manages the pinned shapes on the left side of the screen - */ -export class HUDPinnedShapes extends BaseHUDPart { - constructor(root) { - super(root); - /** - * Store a list of pinned shapes - * @type {Array} - */ - this.pinnedShapes = []; - - /** - * Store handles to the currently rendered elements, so we can update them more - * convenient. Also allows for cleaning up handles. - * @type {Array<{ - * key: string, - * amountLabel: HTMLElement, - * lastRenderedValue: string, - * element: HTMLElement, - * detector?: ClickDetector, - * infoDetector?: ClickDetector - * }>} - */ - this.handles = []; - } - - createElements(parent) { - this.element = makeDiv(parent, "ingame_HUD_PinnedShapes", []); - } - - /** - * Serializes the pinned shapes - */ - serialize() { - return { - shapes: this.pinnedShapes, - }; - } - - /** - * Deserializes the pinned shapes - * @param {{ shapes: Array}} data - */ - deserialize(data) { - if (!data || !data.shapes || !Array.isArray(data.shapes)) { - return "Invalid pinned shapes data"; - } - this.pinnedShapes = data.shapes; - } - - /** - * Initializes the hud component - */ - initialize() { - // Connect to any relevant signals - this.root.signals.storyGoalCompleted.add(this.rerenderFull, this); - this.root.signals.upgradePurchased.add(this.updateShapesAfterUpgrade, this); - this.root.signals.postLoadHook.add(this.rerenderFull, this); - this.root.hud.signals.shapePinRequested.add(this.pinNewShape, this); - this.root.hud.signals.shapeUnpinRequested.add(this.unpinShape, this); - - // Perform initial render - this.updateShapesAfterUpgrade(); - } - - /** - * Updates all shapes after an upgrade has been purchased and removes the unused ones - */ - updateShapesAfterUpgrade() { - for (let i = 0; i < this.pinnedShapes.length; ++i) { - const key = this.pinnedShapes[i]; - if (key === blueprintShape) { - // Ignore blueprint shapes - continue; - } - let goal = this.findGoalValueForShape(key); - if (!goal) { - // Seems no longer relevant - this.pinnedShapes.splice(i, 1); - i -= 1; - } - } - - this.rerenderFull(); - } - - /** - * Finds the current goal for the given key. If the key is the story goal, returns - * the story goal. If its the blueprint shape, no goal is returned. Otherwise - * it's searched for upgrades. - * @param {string} key - */ - findGoalValueForShape(key) { - if (key === this.root.hubGoals.currentGoal.definition.getHash()) { - return this.root.hubGoals.currentGoal.required; - } - if (key === blueprintShape) { - return null; - } - - // Check if this shape is required for any upgrade - for (const upgradeId in UPGRADES) { - const { tiers } = UPGRADES[upgradeId]; - const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId); - const tierHandle = tiers[currentTier]; - - if (!tierHandle) { - // Max level - continue; - } - - for (let i = 0; i < tierHandle.required.length; ++i) { - const { shape, amount } = tierHandle.required[i]; - if (shape === key) { - return amount; - } - } - } - - return null; - } - - /** - * Returns whether a given shape is currently pinned - * @param {string} key - */ - isShapePinned(key) { - if (key === this.root.hubGoals.currentGoal.definition.getHash() || key === blueprintShape) { - // This is a "special" shape which is always pinned - return true; - } - - return this.pinnedShapes.indexOf(key) >= 0; - } - - /** - * Rerenders the whole component - */ - rerenderFull() { - const currentGoal = this.root.hubGoals.currentGoal; - const currentKey = currentGoal.definition.getHash(); - - // First, remove all old shapes - for (let i = 0; i < this.handles.length; ++i) { - this.handles[i].element.remove(); - const detector = this.handles[i].detector; - if (detector) { - detector.cleanup(); - } - const infoDetector = this.handles[i].infoDetector; - if (infoDetector) { - infoDetector.cleanup(); - } - } - this.handles = []; - - // Pin story goal - this.internalPinShape(currentKey, false, "goal"); - - // Pin blueprint shape as well - if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) { - this.internalPinShape(blueprintShape, false, "blueprint"); - } - - // Pin manually pinned shapes - for (let i = 0; i < this.pinnedShapes.length; ++i) { - const key = this.pinnedShapes[i]; - if (key !== currentKey) { - this.internalPinShape(key); - } - } - } - - /** - * Pins a new shape - * @param {string} key - * @param {boolean} canUnpin - * @param {string=} className - */ - internalPinShape(key, canUnpin = true, className = null) { - const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key); - - const element = makeDiv(this.element, null, ["shape"]); - const canvas = definition.generateAsCanvas(120); - element.appendChild(canvas); - - if (className) { - element.classList.add(className); - } - - let detector = null; - if (canUnpin) { - element.classList.add("unpinable"); - detector = new ClickDetector(element, { - consumeEvents: true, - preventDefault: true, - targetOnly: true, - }); - detector.click.add(() => this.unpinShape(key)); - } else { - element.classList.add("marked"); - } - - // Show small info icon - const infoButton = document.createElement("button"); - infoButton.classList.add("infoButton"); - element.appendChild(infoButton); - const infoDetector = new ClickDetector(infoButton, { - consumeEvents: true, - preventDefault: true, - targetOnly: true, - }); - infoDetector.click.add(() => this.root.hud.signals.viewShapeDetailsRequested.dispatch(definition)); - - const amountLabel = makeDiv(element, null, ["amountLabel"], ""); - - const goal = this.findGoalValueForShape(key); - if (goal) { - makeDiv(element, null, ["goalLabel"], "/" + formatBigNumber(goal)); - } - - this.handles.push({ - key, - element, - amountLabel, - lastRenderedValue: "", - detector, - infoDetector, - }); - } - - /** - * Updates all amount labels - */ - update() { - for (let i = 0; i < this.handles.length; ++i) { - const handle = this.handles[i]; - - const currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key); - const currentValueFormatted = formatBigNumber(currentValue); - if (currentValueFormatted !== handle.lastRenderedValue) { - handle.lastRenderedValue = currentValueFormatted; - handle.amountLabel.innerText = currentValueFormatted; - const goal = this.findGoalValueForShape(handle.key); - handle.element.classList.toggle("completed", goal && currentValue > goal); - } - } - } - - /** - * Unpins a shape - * @param {string} key - */ - unpinShape(key) { - arrayDeleteValue(this.pinnedShapes, key); - this.rerenderFull(); - } - - /** - * Requests to pin a new shape - * @param {ShapeDefinition} definition - */ - pinNewShape(definition) { - const key = definition.getHash(); - if (key === this.root.hubGoals.currentGoal.definition.getHash()) { - // Can not pin current goal - return; - } - - if (key === blueprintShape) { - // Can not pin the blueprint shape - return; - } - - // Check if its already pinned - if (this.pinnedShapes.indexOf(key) >= 0) { - return; - } - - this.pinnedShapes.push(key); - this.rerenderFull(); - } -} +import { ClickDetector } from "../../../core/click_detector"; +import { formatBigNumber, makeDiv, arrayDeleteValue } from "../../../core/utils"; +import { ShapeDefinition } from "../../shape_definition"; +import { BaseHUDPart } from "../base_hud_part"; +import { blueprintShape, UPGRADES } from "../../upgrades"; +import { enumHubGoalRewards } from "../../tutorial_goals"; + +/** + * Manages the pinned shapes on the left side of the screen + */ +export class HUDPinnedShapes extends BaseHUDPart { + constructor(root) { + super(root); + /** + * Store a list of pinned shapes + * @type {Array} + */ + this.pinnedShapes = []; + + /** + * Store handles to the currently rendered elements, so we can update them more + * convenient. Also allows for cleaning up handles. + * @type {Array<{ + * key: string, + * amountLabel: HTMLElement, + * lastRenderedValue: string, + * element: HTMLElement, + * detector?: ClickDetector, + * infoDetector?: ClickDetector + * }>} + */ + this.handles = []; + } + + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_PinnedShapes", []); + } + + /** + * Serializes the pinned shapes + */ + serialize() { + return { + shapes: this.pinnedShapes, + }; + } + + /** + * Deserializes the pinned shapes + * @param {{ shapes: Array}} data + */ + deserialize(data) { + if (!data || !data.shapes || !Array.isArray(data.shapes)) { + return "Invalid pinned shapes data"; + } + this.pinnedShapes = data.shapes; + } + + /** + * Initializes the hud component + */ + initialize() { + // Connect to any relevant signals + this.root.signals.storyGoalCompleted.add(this.rerenderFull, this); + this.root.signals.upgradePurchased.add(this.updateShapesAfterUpgrade, this); + this.root.signals.postLoadHook.add(this.rerenderFull, this); + this.root.hud.signals.shapePinRequested.add(this.pinNewShape, this); + this.root.hud.signals.shapeUnpinRequested.add(this.unpinShape, this); + + // Perform initial render + this.updateShapesAfterUpgrade(); + } + + /** + * Updates all shapes after an upgrade has been purchased and removes the unused ones + */ + updateShapesAfterUpgrade() { + for (let i = 0; i < this.pinnedShapes.length; ++i) { + const key = this.pinnedShapes[i]; + if (key === blueprintShape) { + // Ignore blueprint shapes + continue; + } + let goal = this.findGoalValueForShape(key); + if (!goal) { + // Seems no longer relevant + this.pinnedShapes.splice(i, 1); + i -= 1; + } + } + + this.rerenderFull(); + } + + /** + * Finds the current goal for the given key. If the key is the story goal, returns + * the story goal. If its the blueprint shape, no goal is returned. Otherwise + * it's searched for upgrades. + * @param {string} key + */ + findGoalValueForShape(key) { + if (key === this.root.hubGoals.currentGoal.definition.getHash()) { + return this.root.hubGoals.currentGoal.required; + } + if (key === blueprintShape) { + return null; + } + + // Check if this shape is required for any upgrade + for (const upgradeId in UPGRADES) { + const upgradeTiers = UPGRADES[upgradeId]; + const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId); + const tierHandle = upgradeTiers[currentTier]; + + if (!tierHandle) { + // Max level + continue; + } + + for (let i = 0; i < tierHandle.required.length; ++i) { + const { shape, amount } = tierHandle.required[i]; + if (shape === key) { + return amount; + } + } + } + + return null; + } + + /** + * Returns whether a given shape is currently pinned + * @param {string} key + */ + isShapePinned(key) { + if (key === this.root.hubGoals.currentGoal.definition.getHash() || key === blueprintShape) { + // This is a "special" shape which is always pinned + return true; + } + + return this.pinnedShapes.indexOf(key) >= 0; + } + + /** + * Rerenders the whole component + */ + rerenderFull() { + const currentGoal = this.root.hubGoals.currentGoal; + const currentKey = currentGoal.definition.getHash(); + + // First, remove all old shapes + for (let i = 0; i < this.handles.length; ++i) { + this.handles[i].element.remove(); + const detector = this.handles[i].detector; + if (detector) { + detector.cleanup(); + } + const infoDetector = this.handles[i].infoDetector; + if (infoDetector) { + infoDetector.cleanup(); + } + } + this.handles = []; + + // Pin story goal + this.internalPinShape(currentKey, false, "goal"); + + // Pin blueprint shape as well + if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) { + this.internalPinShape(blueprintShape, false, "blueprint"); + } + + // Pin manually pinned shapes + for (let i = 0; i < this.pinnedShapes.length; ++i) { + const key = this.pinnedShapes[i]; + if (key !== currentKey) { + this.internalPinShape(key); + } + } + } + + /** + * Pins a new shape + * @param {string} key + * @param {boolean} canUnpin + * @param {string=} className + */ + internalPinShape(key, canUnpin = true, className = null) { + const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key); + + const element = makeDiv(this.element, null, ["shape"]); + const canvas = definition.generateAsCanvas(120); + element.appendChild(canvas); + + if (className) { + element.classList.add(className); + } + + let detector = null; + if (canUnpin) { + element.classList.add("unpinable"); + detector = new ClickDetector(element, { + consumeEvents: true, + preventDefault: true, + targetOnly: true, + }); + detector.click.add(() => this.unpinShape(key)); + } else { + element.classList.add("marked"); + } + + // Show small info icon + const infoButton = document.createElement("button"); + infoButton.classList.add("infoButton"); + element.appendChild(infoButton); + const infoDetector = new ClickDetector(infoButton, { + consumeEvents: true, + preventDefault: true, + targetOnly: true, + }); + infoDetector.click.add(() => this.root.hud.signals.viewShapeDetailsRequested.dispatch(definition)); + + const amountLabel = makeDiv(element, null, ["amountLabel"], ""); + + const goal = this.findGoalValueForShape(key); + if (goal) { + makeDiv(element, null, ["goalLabel"], "/" + formatBigNumber(goal)); + } + + this.handles.push({ + key, + element, + amountLabel, + lastRenderedValue: "", + detector, + infoDetector, + }); + } + + /** + * Updates all amount labels + */ + update() { + for (let i = 0; i < this.handles.length; ++i) { + const handle = this.handles[i]; + + const currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key); + const currentValueFormatted = formatBigNumber(currentValue); + if (currentValueFormatted !== handle.lastRenderedValue) { + handle.lastRenderedValue = currentValueFormatted; + handle.amountLabel.innerText = currentValueFormatted; + const goal = this.findGoalValueForShape(handle.key); + handle.element.classList.toggle("completed", goal && currentValue > goal); + } + } + } + + /** + * Unpins a shape + * @param {string} key + */ + unpinShape(key) { + arrayDeleteValue(this.pinnedShapes, key); + this.rerenderFull(); + } + + /** + * Requests to pin a new shape + * @param {ShapeDefinition} definition + */ + pinNewShape(definition) { + const key = definition.getHash(); + if (key === this.root.hubGoals.currentGoal.definition.getHash()) { + // Can not pin current goal + return; + } + + if (key === blueprintShape) { + // Can not pin the blueprint shape + return; + } + + // Check if its already pinned + if (this.pinnedShapes.indexOf(key) >= 0) { + return; + } + + this.pinnedShapes.push(key); + this.rerenderFull(); + } +} diff --git a/src/js/game/hud/parts/sandbox_controller.js b/src/js/game/hud/parts/sandbox_controller.js index dd521655..c382cf84 100644 --- a/src/js/game/hud/parts/sandbox_controller.js +++ b/src/js/game/hud/parts/sandbox_controller.js @@ -1,158 +1,158 @@ -import { BaseHUDPart } from "../base_hud_part"; -import { makeDiv } from "../../../core/utils"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { blueprintShape, UPGRADES } from "../../upgrades"; -import { enumNotificationType } from "./notifications"; -import { tutorialGoals } from "../../tutorial_goals"; - -export class HUDSandboxController extends BaseHUDPart { - createElements(parent) { - this.element = makeDiv( - parent, - "ingame_HUD_SandboxController", - [], - ` - - Use F6 to toggle this overlay - -
-
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- -
- - -
-
- ` - ); - - const bind = (selector, handler) => this.trackClicks(this.element.querySelector(selector), handler); - - bind(".giveBlueprints", this.giveBlueprints); - bind(".maxOutAll", this.maxOutAll); - bind(".levelToggle .minus", () => this.modifyLevel(-1)); - bind(".levelToggle .plus", () => this.modifyLevel(1)); - - bind(".upgradesBelt .minus", () => this.modifyUpgrade("belt", -1)); - bind(".upgradesBelt .plus", () => this.modifyUpgrade("belt", 1)); - - bind(".upgradesExtraction .minus", () => this.modifyUpgrade("miner", -1)); - bind(".upgradesExtraction .plus", () => this.modifyUpgrade("miner", 1)); - - bind(".upgradesProcessing .minus", () => this.modifyUpgrade("processors", -1)); - bind(".upgradesProcessing .plus", () => this.modifyUpgrade("processors", 1)); - - bind(".upgradesPainting .minus", () => this.modifyUpgrade("painting", -1)); - bind(".upgradesPainting .plus", () => this.modifyUpgrade("painting", 1)); - } - - giveBlueprints() { - if (!this.root.hubGoals.storedShapes[blueprintShape]) { - this.root.hubGoals.storedShapes[blueprintShape] = 0; - } - this.root.hubGoals.storedShapes[blueprintShape] += 1e9; - } - - maxOutAll() { - this.modifyUpgrade("belt", 100); - this.modifyUpgrade("miner", 100); - this.modifyUpgrade("processors", 100); - this.modifyUpgrade("painting", 100); - } - - modifyUpgrade(id, amount) { - const handle = UPGRADES[id]; - const maxLevel = handle.tiers.length; - - this.root.hubGoals.upgradeLevels[id] = Math.max( - 0, - Math.min(maxLevel, (this.root.hubGoals.upgradeLevels[id] || 0) + amount) - ); - - // Compute improvement - let improvement = 1; - for (let i = 0; i < this.root.hubGoals.upgradeLevels[id]; ++i) { - improvement += handle.tiers[i].improvement; - } - this.root.hubGoals.upgradeImprovements[id] = improvement; - this.root.signals.upgradePurchased.dispatch(id); - this.root.hud.signals.notification.dispatch( - "Upgrade '" + id + "' is now at tier " + (this.root.hubGoals.upgradeLevels[id] + 1), - enumNotificationType.upgrade - ); - } - - modifyLevel(amount) { - const hubGoals = this.root.hubGoals; - hubGoals.level = Math.max(1, hubGoals.level + amount); - hubGoals.createNextGoal(); - - // Clear all shapes of this level - hubGoals.storedShapes[hubGoals.currentGoal.definition.getHash()] = 0; - - this.root.hud.parts.pinnedShapes.rerenderFull(); - - // Compute gained rewards - hubGoals.gainedRewards = {}; - for (let i = 0; i < hubGoals.level - 1; ++i) { - if (i < tutorialGoals.length) { - const reward = tutorialGoals[i].reward; - hubGoals.gainedRewards[reward] = (hubGoals.gainedRewards[reward] || 0) + 1; - } - } - - this.root.hud.signals.notification.dispatch( - "Changed level to " + hubGoals.level, - enumNotificationType.upgrade - ); - } - - initialize() { - // Allow toggling the controller overlay - this.root.gameState.inputReciever.keydown.add(key => { - if (key.keyCode === 117) { - // F6 - this.toggle(); - } - }); - - this.visible = !G_IS_DEV; - this.domAttach = new DynamicDomAttach(this.root, this.element); - } - - toggle() { - this.visible = !this.visible; - } - - update() { - this.domAttach.update(this.visible); - } -} +import { BaseHUDPart } from "../base_hud_part"; +import { makeDiv } from "../../../core/utils"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { blueprintShape, UPGRADES } from "../../upgrades"; +import { enumNotificationType } from "./notifications"; +import { tutorialGoals } from "../../tutorial_goals"; + +export class HUDSandboxController extends BaseHUDPart { + createElements(parent) { + this.element = makeDiv( + parent, + "ingame_HUD_SandboxController", + [], + ` + + Use F6 to toggle this overlay + +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+
+ ` + ); + + const bind = (selector, handler) => this.trackClicks(this.element.querySelector(selector), handler); + + bind(".giveBlueprints", this.giveBlueprints); + bind(".maxOutAll", this.maxOutAll); + bind(".levelToggle .minus", () => this.modifyLevel(-1)); + bind(".levelToggle .plus", () => this.modifyLevel(1)); + + bind(".upgradesBelt .minus", () => this.modifyUpgrade("belt", -1)); + bind(".upgradesBelt .plus", () => this.modifyUpgrade("belt", 1)); + + bind(".upgradesExtraction .minus", () => this.modifyUpgrade("miner", -1)); + bind(".upgradesExtraction .plus", () => this.modifyUpgrade("miner", 1)); + + bind(".upgradesProcessing .minus", () => this.modifyUpgrade("processors", -1)); + bind(".upgradesProcessing .plus", () => this.modifyUpgrade("processors", 1)); + + bind(".upgradesPainting .minus", () => this.modifyUpgrade("painting", -1)); + bind(".upgradesPainting .plus", () => this.modifyUpgrade("painting", 1)); + } + + giveBlueprints() { + if (!this.root.hubGoals.storedShapes[blueprintShape]) { + this.root.hubGoals.storedShapes[blueprintShape] = 0; + } + this.root.hubGoals.storedShapes[blueprintShape] += 1e9; + } + + maxOutAll() { + this.modifyUpgrade("belt", 100); + this.modifyUpgrade("miner", 100); + this.modifyUpgrade("processors", 100); + this.modifyUpgrade("painting", 100); + } + + modifyUpgrade(id, amount) { + const upgradeTiers = UPGRADES[id]; + const maxLevel = upgradeTiers.length; + + this.root.hubGoals.upgradeLevels[id] = Math.max( + 0, + Math.min(maxLevel, (this.root.hubGoals.upgradeLevels[id] || 0) + amount) + ); + + // Compute improvement + let improvement = 1; + for (let i = 0; i < this.root.hubGoals.upgradeLevels[id]; ++i) { + improvement += upgradeTiers[i].improvement; + } + this.root.hubGoals.upgradeImprovements[id] = improvement; + this.root.signals.upgradePurchased.dispatch(id); + this.root.hud.signals.notification.dispatch( + "Upgrade '" + id + "' is now at tier " + (this.root.hubGoals.upgradeLevels[id] + 1), + enumNotificationType.upgrade + ); + } + + modifyLevel(amount) { + const hubGoals = this.root.hubGoals; + hubGoals.level = Math.max(1, hubGoals.level + amount); + hubGoals.createNextGoal(); + + // Clear all shapes of this level + hubGoals.storedShapes[hubGoals.currentGoal.definition.getHash()] = 0; + + this.root.hud.parts.pinnedShapes.rerenderFull(); + + // Compute gained rewards + hubGoals.gainedRewards = {}; + for (let i = 0; i < hubGoals.level - 1; ++i) { + if (i < tutorialGoals.length) { + const reward = tutorialGoals[i].reward; + hubGoals.gainedRewards[reward] = (hubGoals.gainedRewards[reward] || 0) + 1; + } + } + + this.root.hud.signals.notification.dispatch( + "Changed level to " + hubGoals.level, + enumNotificationType.upgrade + ); + } + + initialize() { + // Allow toggling the controller overlay + this.root.gameState.inputReciever.keydown.add(key => { + if (key.keyCode === 117) { + // F6 + this.toggle(); + } + }); + + this.visible = !G_IS_DEV; + this.domAttach = new DynamicDomAttach(this.root, this.element); + } + + toggle() { + this.visible = !this.visible; + } + + update() { + this.domAttach.update(this.visible); + } +} diff --git a/src/js/game/hud/parts/screenshot_exporter.js b/src/js/game/hud/parts/screenshot_exporter.js index a3310204..59e76c63 100644 --- a/src/js/game/hud/parts/screenshot_exporter.js +++ b/src/js/game/hud/parts/screenshot_exporter.js @@ -87,7 +87,7 @@ export class HUDScreenshotExporter extends BaseHUDPart { const parameters = new DrawParameters({ context, visibleRect, - desiredAtlasScale: "1", + desiredAtlasScale: chunkScale, root: this.root, zoomLevel: chunkScale, }); diff --git a/src/js/game/hud/parts/settings_menu.js b/src/js/game/hud/parts/settings_menu.js index 391fde01..31afe348 100644 --- a/src/js/game/hud/parts/settings_menu.js +++ b/src/js/game/hud/parts/settings_menu.js @@ -1,127 +1,131 @@ -import { BaseHUDPart } from "../base_hud_part"; -import { makeDiv, formatBigNumberFull } from "../../../core/utils"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { InputReceiver } from "../../../core/input_receiver"; -import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; -import { T } from "../../../translations"; -import { StaticMapEntityComponent } from "../../components/static_map_entity"; -import { BeltComponent } from "../../components/belt"; - -export class HUDSettingsMenu extends BaseHUDPart { - createElements(parent) { - this.background = makeDiv(parent, "ingame_HUD_SettingsMenu", ["ingameDialog"]); - - this.menuElement = makeDiv(this.background, null, ["menuElement"]); - - this.statsElement = makeDiv( - this.background, - null, - ["statsElement"], - ` - ${T.ingame.settingsMenu.beltsPlaced} - ${T.ingame.settingsMenu.buildingsPlaced} - ${T.ingame.settingsMenu.playtime} - - ` - ); - - this.buttonContainer = makeDiv(this.menuElement, null, ["buttons"]); - - const buttons = [ - { - title: T.ingame.settingsMenu.buttons.continue, - action: () => this.close(), - }, - { - title: T.ingame.settingsMenu.buttons.settings, - action: () => this.goToSettings(), - }, - { - title: T.ingame.settingsMenu.buttons.menu, - action: () => this.returnToMenu(), - }, - ]; - - for (let i = 0; i < buttons.length; ++i) { - const { title, action } = buttons[i]; - - const element = document.createElement("button"); - element.classList.add("styledButton"); - element.innerText = title; - this.buttonContainer.appendChild(element); - - this.trackClicks(element, action); - } - } - - returnToMenu() { - this.root.gameState.goBackToMenu(); - } - - goToSettings() { - this.root.gameState.goToSettings(); - } - - shouldPauseGame() { - return this.visible; - } - - shouldPauseRendering() { - return this.visible; - } - - initialize() { - this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).add(this.show, this); - - this.domAttach = new DynamicDomAttach(this.root, this.background, { - attachClass: "visible", - }); - - this.inputReciever = new InputReceiver("settingsmenu"); - this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); - this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); - - this.close(); - } - - cleanup() { - document.body.classList.remove("ingameDialogOpen"); - } - - show() { - this.visible = true; - document.body.classList.add("ingameDialogOpen"); - this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); - - const totalMinutesPlayed = Math.ceil(this.root.time.now() / 60); - - /** @type {HTMLElement} */ - const playtimeElement = this.statsElement.querySelector(".playtime"); - /** @type {HTMLElement} */ - const buildingsPlacedElement = this.statsElement.querySelector(".buildingsPlaced"); - /** @type {HTMLElement} */ - const beltsPlacedElement = this.statsElement.querySelector(".beltsPlaced"); - - playtimeElement.innerText = T.global.time.xMinutes.replace("", `${totalMinutesPlayed}`); - - buildingsPlacedElement.innerText = formatBigNumberFull( - this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length - - this.root.entityMgr.getAllWithComponent(BeltComponent).length - ); - - beltsPlacedElement.innerText = formatBigNumberFull( - this.root.entityMgr.getAllWithComponent(BeltComponent).length - ); - } - - close() { - this.visible = false; - document.body.classList.remove("ingameDialogOpen"); - this.root.app.inputMgr.makeSureDetached(this.inputReciever); - this.update(); - } - - update() { - this.domAttach.update(this.visible); - } -} +import { BaseHUDPart } from "../base_hud_part"; +import { makeDiv, formatBigNumberFull } from "../../../core/utils"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { InputReceiver } from "../../../core/input_receiver"; +import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; +import { T } from "../../../translations"; +import { StaticMapEntityComponent } from "../../components/static_map_entity"; +import { BeltComponent } from "../../components/belt"; + +export class HUDSettingsMenu extends BaseHUDPart { + createElements(parent) { + this.background = makeDiv(parent, "ingame_HUD_SettingsMenu", ["ingameDialog"]); + + this.menuElement = makeDiv(this.background, null, ["menuElement"]); + + this.statsElement = makeDiv( + this.background, + null, + ["statsElement"], + ` + ${T.ingame.settingsMenu.beltsPlaced} + ${T.ingame.settingsMenu.buildingsPlaced} + ${T.ingame.settingsMenu.playtime} + + ` + ); + + this.buttonContainer = makeDiv(this.menuElement, null, ["buttons"]); + + const buttons = [ + { + id: "continue", + action: () => this.close(), + }, + { + id: "settings", + action: () => this.goToSettings(), + }, + { + id: "menu", + action: () => this.returnToMenu(), + }, + ]; + + for (let i = 0; i < buttons.length; ++i) { + const { title, action, id } = buttons[i]; + + const element = document.createElement("button"); + element.classList.add("styledButton"); + element.classList.add(id); + this.buttonContainer.appendChild(element); + + this.trackClicks(element, action); + } + } + + isBlockingOverlay() { + return this.visible; + } + + returnToMenu() { + this.root.gameState.goBackToMenu(); + } + + goToSettings() { + this.root.gameState.goToSettings(); + } + + shouldPauseGame() { + return this.visible; + } + + shouldPauseRendering() { + return this.visible; + } + + initialize() { + this.root.keyMapper.getBinding(KEYMAPPINGS.general.back).add(this.show, this); + + this.domAttach = new DynamicDomAttach(this.root, this.background, { + attachClass: "visible", + }); + + this.inputReciever = new InputReceiver("settingsmenu"); + this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); + this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); + + this.close(); + } + + cleanup() { + document.body.classList.remove("ingameDialogOpen"); + } + + show() { + this.visible = true; + document.body.classList.add("ingameDialogOpen"); + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); + + const totalMinutesPlayed = Math.ceil(this.root.time.now() / 60); + + /** @type {HTMLElement} */ + const playtimeElement = this.statsElement.querySelector(".playtime"); + /** @type {HTMLElement} */ + const buildingsPlacedElement = this.statsElement.querySelector(".buildingsPlaced"); + /** @type {HTMLElement} */ + const beltsPlacedElement = this.statsElement.querySelector(".beltsPlaced"); + + playtimeElement.innerText = T.global.time.xMinutes.replace("", `${totalMinutesPlayed}`); + + buildingsPlacedElement.innerText = formatBigNumberFull( + this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length - + this.root.entityMgr.getAllWithComponent(BeltComponent).length + ); + + beltsPlacedElement.innerText = formatBigNumberFull( + this.root.entityMgr.getAllWithComponent(BeltComponent).length + ); + } + + close() { + this.visible = false; + document.body.classList.remove("ingameDialogOpen"); + this.root.app.inputMgr.makeSureDetached(this.inputReciever); + this.update(); + } + + update() { + this.domAttach.update(this.visible); + } +} diff --git a/src/js/game/hud/parts/shape_viewer.js b/src/js/game/hud/parts/shape_viewer.js index ea4273aa..18f55c74 100644 --- a/src/js/game/hud/parts/shape_viewer.js +++ b/src/js/game/hud/parts/shape_viewer.js @@ -48,6 +48,10 @@ export class HUDShapeViewer extends BaseHUDPart { this.close(); } + isBlockingOverlay() { + return this.visible; + } + /** * Called when the copying of a key was requested */ diff --git a/src/js/game/hud/parts/shop.js b/src/js/game/hud/parts/shop.js index e2b8837b..cfa78c29 100644 --- a/src/js/game/hud/parts/shop.js +++ b/src/js/game/hud/parts/shop.js @@ -1,251 +1,255 @@ -import { ClickDetector } from "../../../core/click_detector"; -import { InputReceiver } from "../../../core/input_receiver"; -import { formatBigNumber, makeDiv } from "../../../core/utils"; -import { T } from "../../../translations"; -import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; -import { UPGRADES } from "../../upgrades"; -import { BaseHUDPart } from "../base_hud_part"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; - -export class HUDShop extends BaseHUDPart { - createElements(parent) { - this.background = makeDiv(parent, "ingame_HUD_Shop", ["ingameDialog"]); - - // DIALOG Inner / Wrapper - this.dialogInner = makeDiv(this.background, null, ["dialogInner"]); - this.title = makeDiv(this.dialogInner, null, ["title"], T.ingame.shop.title); - this.closeButton = makeDiv(this.title, null, ["closeButton"]); - this.trackClicks(this.closeButton, this.close); - this.contentDiv = makeDiv(this.dialogInner, null, ["content"]); - - this.upgradeToElements = {}; - - // Upgrades - for (const upgradeId in UPGRADES) { - const handle = {}; - handle.requireIndexToElement = []; - - // Wrapper - handle.elem = makeDiv(this.contentDiv, null, ["upgrade"]); - handle.elem.setAttribute("data-upgrade-id", upgradeId); - - // Title - const title = makeDiv(handle.elem, null, ["title"], T.shopUpgrades[upgradeId].name); - - // Title > Tier - handle.elemTierLabel = makeDiv(title, null, ["tier"]); - - // Icon - handle.icon = makeDiv(handle.elem, null, ["icon"]); - handle.icon.setAttribute("data-icon", "upgrades/" + upgradeId + ".png"); - - // Description - handle.elemDescription = makeDiv(handle.elem, null, ["description"], "??"); - handle.elemRequirements = makeDiv(handle.elem, null, ["requirements"]); - - // Buy button - handle.buyButton = document.createElement("button"); - handle.buyButton.classList.add("buy", "styledButton"); - handle.buyButton.innerText = T.ingame.shop.buttonUnlock; - handle.elem.appendChild(handle.buyButton); - - this.trackClicks(handle.buyButton, () => this.tryUnlockNextTier(upgradeId)); - - // Assign handle - this.upgradeToElements[upgradeId] = handle; - } - } - - rerenderFull() { - for (const upgradeId in this.upgradeToElements) { - const handle = this.upgradeToElements[upgradeId]; - const { tiers } = UPGRADES[upgradeId]; - - const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId); - const currentTierMultiplier = this.root.hubGoals.upgradeImprovements[upgradeId]; - const tierHandle = tiers[currentTier]; - - // Set tier - handle.elemTierLabel.innerText = T.ingame.shop.tier.replace( - "", - "" + T.ingame.shop.tierLabels[currentTier] - ); - - handle.elemTierLabel.setAttribute("data-tier", currentTier); - - // Cleanup detectors - for (let i = 0; i < handle.requireIndexToElement.length; ++i) { - const requiredHandle = handle.requireIndexToElement[i]; - requiredHandle.container.remove(); - requiredHandle.pinDetector.cleanup(); - requiredHandle.infoDetector.cleanup(); - } - - // Cleanup - handle.requireIndexToElement = []; - - handle.elem.classList.toggle("maxLevel", !tierHandle); - - if (!tierHandle) { - // Max level - handle.elemDescription.innerText = T.ingame.shop.maximumLevel.replace( - "", - currentTierMultiplier.toString() - ); - continue; - } - - // Set description - handle.elemDescription.innerText = T.shopUpgrades[upgradeId].description - .replace("", currentTierMultiplier.toString()) - .replace("", (currentTierMultiplier + tierHandle.improvement).toString()) - // Backwards compatibility - .replace("", (tierHandle.improvement * 100.0).toString()); - - tierHandle.required.forEach(({ shape, amount }) => { - const container = makeDiv(handle.elemRequirements, null, ["requirement"]); - - const shapeDef = this.root.shapeDefinitionMgr.getShapeFromShortKey(shape); - const shapeCanvas = shapeDef.generateAsCanvas(120); - shapeCanvas.classList.add(); - container.appendChild(shapeCanvas); - - const progressContainer = makeDiv(container, null, ["amount"]); - const progressBar = document.createElement("label"); - progressBar.classList.add("progressBar"); - progressContainer.appendChild(progressBar); - - const progressLabel = document.createElement("label"); - progressContainer.appendChild(progressLabel); - - const pinButton = document.createElement("button"); - pinButton.classList.add("pin"); - container.appendChild(pinButton); - - const viewInfoButton = document.createElement("button"); - viewInfoButton.classList.add("showInfo"); - container.appendChild(viewInfoButton); - - const currentGoalShape = this.root.hubGoals.currentGoal.definition.getHash(); - if (shape === currentGoalShape) { - pinButton.classList.add("isGoal"); - } else if (this.root.hud.parts.pinnedShapes.isShapePinned(shape)) { - pinButton.classList.add("alreadyPinned"); - } - - const pinDetector = new ClickDetector(pinButton, { - consumeEvents: true, - preventDefault: true, - }); - pinDetector.click.add(() => { - if (this.root.hud.parts.pinnedShapes.isShapePinned(shape)) { - this.root.hud.signals.shapeUnpinRequested.dispatch(shape); - pinButton.classList.add("unpinned"); - pinButton.classList.remove("pinned", "alreadyPinned"); - } else { - this.root.hud.signals.shapePinRequested.dispatch(shapeDef); - pinButton.classList.add("pinned"); - pinButton.classList.remove("unpinned"); - } - }); - - const infoDetector = new ClickDetector(viewInfoButton, { - consumeEvents: true, - preventDefault: true, - }); - infoDetector.click.add(() => - this.root.hud.signals.viewShapeDetailsRequested.dispatch(shapeDef) - ); - - handle.requireIndexToElement.push({ - container, - progressLabel, - progressBar, - definition: shapeDef, - required: amount, - pinDetector, - infoDetector, - }); - }); - } - } - - renderCountsAndStatus() { - for (const upgradeId in this.upgradeToElements) { - const handle = this.upgradeToElements[upgradeId]; - for (let i = 0; i < handle.requireIndexToElement.length; ++i) { - const { progressLabel, progressBar, definition, required } = handle.requireIndexToElement[i]; - - const haveAmount = this.root.hubGoals.getShapesStored(definition); - const progress = Math.min(haveAmount / required, 1.0); - - progressLabel.innerText = formatBigNumber(haveAmount) + " / " + formatBigNumber(required); - progressBar.style.width = progress * 100.0 + "%"; - progressBar.classList.toggle("complete", progress >= 1.0); - } - - handle.buyButton.classList.toggle("buyable", this.root.hubGoals.canUnlockUpgrade(upgradeId)); - } - } - - initialize() { - this.domAttach = new DynamicDomAttach(this.root, this.background, { - attachClass: "visible", - }); - - this.inputReciever = new InputReceiver("shop"); - this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); - - this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); - this.keyActionMapper.getBinding(KEYMAPPINGS.ingame.menuClose).add(this.close, this); - this.keyActionMapper.getBinding(KEYMAPPINGS.ingame.menuOpenShop).add(this.close, this); - - this.close(); - - this.rerenderFull(); - this.root.signals.upgradePurchased.add(this.rerenderFull, this); - } - - cleanup() { - document.body.classList.remove("ingameDialogOpen"); - - // Cleanup detectors - for (const upgradeId in this.upgradeToElements) { - const handle = this.upgradeToElements[upgradeId]; - for (let i = 0; i < handle.requireIndexToElement.length; ++i) { - const requiredHandle = handle.requireIndexToElement[i]; - requiredHandle.container.remove(); - requiredHandle.pinDetector.cleanup(); - requiredHandle.infoDetector.cleanup(); - } - handle.requireIndexToElement = []; - } - } - - show() { - this.visible = true; - document.body.classList.add("ingameDialogOpen"); - // this.background.classList.add("visible"); - this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); - this.rerenderFull(); - } - - close() { - this.visible = false; - document.body.classList.remove("ingameDialogOpen"); - this.root.app.inputMgr.makeSureDetached(this.inputReciever); - this.update(); - } - - update() { - this.domAttach.update(this.visible); - if (this.visible) { - this.renderCountsAndStatus(); - } - } - - tryUnlockNextTier(upgradeId) { - // Nothing - this.root.hubGoals.tryUnlockUpgrade(upgradeId); - } -} +import { ClickDetector } from "../../../core/click_detector"; +import { InputReceiver } from "../../../core/input_receiver"; +import { formatBigNumber, makeDiv } from "../../../core/utils"; +import { T } from "../../../translations"; +import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; +import { UPGRADES } from "../../upgrades"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; + +export class HUDShop extends BaseHUDPart { + createElements(parent) { + this.background = makeDiv(parent, "ingame_HUD_Shop", ["ingameDialog"]); + + // DIALOG Inner / Wrapper + this.dialogInner = makeDiv(this.background, null, ["dialogInner"]); + this.title = makeDiv(this.dialogInner, null, ["title"], T.ingame.shop.title); + this.closeButton = makeDiv(this.title, null, ["closeButton"]); + this.trackClicks(this.closeButton, this.close); + this.contentDiv = makeDiv(this.dialogInner, null, ["content"]); + + this.upgradeToElements = {}; + + // Upgrades + for (const upgradeId in UPGRADES) { + const handle = {}; + handle.requireIndexToElement = []; + + // Wrapper + handle.elem = makeDiv(this.contentDiv, null, ["upgrade"]); + handle.elem.setAttribute("data-upgrade-id", upgradeId); + + // Title + const title = makeDiv(handle.elem, null, ["title"], T.shopUpgrades[upgradeId].name); + + // Title > Tier + handle.elemTierLabel = makeDiv(title, null, ["tier"]); + + // Icon + handle.icon = makeDiv(handle.elem, null, ["icon"]); + handle.icon.setAttribute("data-icon", "upgrades/" + upgradeId + ".png"); + + // Description + handle.elemDescription = makeDiv(handle.elem, null, ["description"], "??"); + handle.elemRequirements = makeDiv(handle.elem, null, ["requirements"]); + + // Buy button + handle.buyButton = document.createElement("button"); + handle.buyButton.classList.add("buy", "styledButton"); + handle.buyButton.innerText = T.ingame.shop.buttonUnlock; + handle.elem.appendChild(handle.buyButton); + + this.trackClicks(handle.buyButton, () => this.tryUnlockNextTier(upgradeId)); + + // Assign handle + this.upgradeToElements[upgradeId] = handle; + } + } + + rerenderFull() { + for (const upgradeId in this.upgradeToElements) { + const handle = this.upgradeToElements[upgradeId]; + const upgradeTiers = UPGRADES[upgradeId]; + + const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId); + const currentTierMultiplier = this.root.hubGoals.upgradeImprovements[upgradeId]; + const tierHandle = upgradeTiers[currentTier]; + + // Set tier + handle.elemTierLabel.innerText = T.ingame.shop.tier.replace( + "", + "" + T.ingame.shop.tierLabels[currentTier] + ); + + handle.elemTierLabel.setAttribute("data-tier", currentTier); + + // Cleanup detectors + for (let i = 0; i < handle.requireIndexToElement.length; ++i) { + const requiredHandle = handle.requireIndexToElement[i]; + requiredHandle.container.remove(); + requiredHandle.pinDetector.cleanup(); + requiredHandle.infoDetector.cleanup(); + } + + // Cleanup + handle.requireIndexToElement = []; + + handle.elem.classList.toggle("maxLevel", !tierHandle); + + if (!tierHandle) { + // Max level + handle.elemDescription.innerText = T.ingame.shop.maximumLevel.replace( + "", + currentTierMultiplier.toString() + ); + continue; + } + + // Set description + handle.elemDescription.innerText = T.shopUpgrades[upgradeId].description + .replace("", currentTierMultiplier.toString()) + .replace("", (currentTierMultiplier + tierHandle.improvement).toString()) + // Backwards compatibility + .replace("", (tierHandle.improvement * 100.0).toString()); + + tierHandle.required.forEach(({ shape, amount }) => { + const container = makeDiv(handle.elemRequirements, null, ["requirement"]); + + const shapeDef = this.root.shapeDefinitionMgr.getShapeFromShortKey(shape); + const shapeCanvas = shapeDef.generateAsCanvas(120); + shapeCanvas.classList.add(); + container.appendChild(shapeCanvas); + + const progressContainer = makeDiv(container, null, ["amount"]); + const progressBar = document.createElement("label"); + progressBar.classList.add("progressBar"); + progressContainer.appendChild(progressBar); + + const progressLabel = document.createElement("label"); + progressContainer.appendChild(progressLabel); + + const pinButton = document.createElement("button"); + pinButton.classList.add("pin"); + container.appendChild(pinButton); + + const viewInfoButton = document.createElement("button"); + viewInfoButton.classList.add("showInfo"); + container.appendChild(viewInfoButton); + + const currentGoalShape = this.root.hubGoals.currentGoal.definition.getHash(); + if (shape === currentGoalShape) { + pinButton.classList.add("isGoal"); + } else if (this.root.hud.parts.pinnedShapes.isShapePinned(shape)) { + pinButton.classList.add("alreadyPinned"); + } + + const pinDetector = new ClickDetector(pinButton, { + consumeEvents: true, + preventDefault: true, + }); + pinDetector.click.add(() => { + if (this.root.hud.parts.pinnedShapes.isShapePinned(shape)) { + this.root.hud.signals.shapeUnpinRequested.dispatch(shape); + pinButton.classList.add("unpinned"); + pinButton.classList.remove("pinned", "alreadyPinned"); + } else { + this.root.hud.signals.shapePinRequested.dispatch(shapeDef); + pinButton.classList.add("pinned"); + pinButton.classList.remove("unpinned"); + } + }); + + const infoDetector = new ClickDetector(viewInfoButton, { + consumeEvents: true, + preventDefault: true, + }); + infoDetector.click.add(() => + this.root.hud.signals.viewShapeDetailsRequested.dispatch(shapeDef) + ); + + handle.requireIndexToElement.push({ + container, + progressLabel, + progressBar, + definition: shapeDef, + required: amount, + pinDetector, + infoDetector, + }); + }); + } + } + + renderCountsAndStatus() { + for (const upgradeId in this.upgradeToElements) { + const handle = this.upgradeToElements[upgradeId]; + for (let i = 0; i < handle.requireIndexToElement.length; ++i) { + const { progressLabel, progressBar, definition, required } = handle.requireIndexToElement[i]; + + const haveAmount = this.root.hubGoals.getShapesStored(definition); + const progress = Math.min(haveAmount / required, 1.0); + + progressLabel.innerText = formatBigNumber(haveAmount) + " / " + formatBigNumber(required); + progressBar.style.width = progress * 100.0 + "%"; + progressBar.classList.toggle("complete", progress >= 1.0); + } + + handle.buyButton.classList.toggle("buyable", this.root.hubGoals.canUnlockUpgrade(upgradeId)); + } + } + + initialize() { + this.domAttach = new DynamicDomAttach(this.root, this.background, { + attachClass: "visible", + }); + + this.inputReciever = new InputReceiver("shop"); + this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever); + + this.keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.close, this); + this.keyActionMapper.getBinding(KEYMAPPINGS.ingame.menuClose).add(this.close, this); + this.keyActionMapper.getBinding(KEYMAPPINGS.ingame.menuOpenShop).add(this.close, this); + + this.close(); + + this.rerenderFull(); + this.root.signals.upgradePurchased.add(this.rerenderFull, this); + } + + cleanup() { + document.body.classList.remove("ingameDialogOpen"); + + // Cleanup detectors + for (const upgradeId in this.upgradeToElements) { + const handle = this.upgradeToElements[upgradeId]; + for (let i = 0; i < handle.requireIndexToElement.length; ++i) { + const requiredHandle = handle.requireIndexToElement[i]; + requiredHandle.container.remove(); + requiredHandle.pinDetector.cleanup(); + requiredHandle.infoDetector.cleanup(); + } + handle.requireIndexToElement = []; + } + } + + show() { + this.visible = true; + document.body.classList.add("ingameDialogOpen"); + // this.background.classList.add("visible"); + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); + this.rerenderFull(); + } + + close() { + this.visible = false; + document.body.classList.remove("ingameDialogOpen"); + this.root.app.inputMgr.makeSureDetached(this.inputReciever); + this.update(); + } + + update() { + this.domAttach.update(this.visible); + if (this.visible) { + this.renderCountsAndStatus(); + } + } + + tryUnlockNextTier(upgradeId) { + // Nothing + this.root.hubGoals.tryUnlockUpgrade(upgradeId); + } + + isBlockingOverlay() { + return this.visible; + } +} diff --git a/src/js/game/hud/parts/statistics.js b/src/js/game/hud/parts/statistics.js index c5136312..910c49d0 100644 --- a/src/js/game/hud/parts/statistics.js +++ b/src/js/game/hud/parts/statistics.js @@ -4,7 +4,7 @@ import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; import { enumAnalyticsDataSource } from "../../production_analytics"; import { BaseHUDPart } from "../base_hud_part"; import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { enumDisplayMode, HUDShapeStatisticsHandle } from "./statistics_handle"; +import { enumDisplayMode, HUDShapeStatisticsHandle, statisticsUnitsSeconds } from "./statistics_handle"; import { T } from "../../../translations"; /** @@ -47,10 +47,12 @@ export class HUDStatistics extends BaseHUDPart { this.trackClicks(button, () => this.setDataSource(dataSource)); } + const buttonIterateUnit = makeButton(this.filtersDisplayMode, ["displayIterateUnit"]); const buttonDisplaySorted = makeButton(this.filtersDisplayMode, ["displaySorted"]); const buttonDisplayDetailed = makeButton(this.filtersDisplayMode, ["displayDetailed"]); const buttonDisplayIcons = makeButton(this.filtersDisplayMode, ["displayIcons"]); + this.trackClicks(buttonIterateUnit, () => this.iterateUnit()); this.trackClicks(buttonDisplaySorted, () => this.toggleSorted()); this.trackClicks(buttonDisplayIcons, () => this.setDisplayMode(enumDisplayMode.icons)); this.trackClicks(buttonDisplayDetailed, () => this.setDisplayMode(enumDisplayMode.detailed)); @@ -97,6 +99,17 @@ export class HUDStatistics extends BaseHUDPart { this.setSorted(!this.sorted); } + /** + * Chooses the next unit + */ + iterateUnit() { + const units = Array.from(Object.keys(statisticsUnitsSeconds)); + const newIndex = (units.indexOf(this.currentUnit) + 1) % units.length; + this.currentUnit = units[newIndex]; + + this.rerenderPartial(); + } + initialize() { this.domAttach = new DynamicDomAttach(this.root, this.background, { attachClass: "visible", @@ -112,6 +125,8 @@ export class HUDStatistics extends BaseHUDPart { /** @type {Object.} */ this.activeHandles = {}; + this.currentUnit = "second"; + this.setSorted(true); this.setDataSource(enumAnalyticsDataSource.produced); this.setDisplayMode(enumDisplayMode.detailed); @@ -140,6 +155,10 @@ export class HUDStatistics extends BaseHUDPart { document.body.classList.remove("ingameDialogOpen"); } + isBlockingOverlay() { + return this.visible; + } + show() { this.visible = true; document.body.classList.add("ingameDialogOpen"); @@ -173,7 +192,7 @@ export class HUDStatistics extends BaseHUDPart { rerenderPartial() { for (const key in this.activeHandles) { const handle = this.activeHandles[key]; - handle.update(this.displayMode, this.dataSource); + handle.update(this.displayMode, this.dataSource, this.currentUnit); } } diff --git a/src/js/game/hud/parts/statistics_handle.js b/src/js/game/hud/parts/statistics_handle.js index 6f49f8d3..a64a5f5b 100644 --- a/src/js/game/hud/parts/statistics_handle.js +++ b/src/js/game/hud/parts/statistics_handle.js @@ -12,6 +12,16 @@ export const enumDisplayMode = { detailed: "detailed", }; +/** + * Stores how many seconds one unit is + * @type {Object} + */ +export const statisticsUnitsSeconds = { + second: 1, + minute: 60, + hour: 3600, +}; + /** * Simple wrapper for a shape definition within the shape statistics */ @@ -64,9 +74,10 @@ export class HUDShapeStatisticsHandle { * * @param {enumDisplayMode} displayMode * @param {enumAnalyticsDataSource} dataSource + * @param {string} unit * @param {boolean=} forced */ - update(displayMode, dataSource, forced = false) { + update(displayMode, dataSource, unit, forced = false) { if (!this.element) { return; } @@ -89,12 +100,12 @@ export class HUDShapeStatisticsHandle { case enumAnalyticsDataSource.delivered: case enumAnalyticsDataSource.produced: { let rate = - (this.root.productionAnalytics.getCurrentShapeRate(dataSource, this.definition) / - globalConfig.analyticsSliceDurationSeconds) * - 60; - this.counter.innerText = T.ingame.statistics.shapesPerSecond.replace( + this.root.productionAnalytics.getCurrentShapeRate(dataSource, this.definition) / + globalConfig.analyticsSliceDurationSeconds; + + this.counter.innerText = T.ingame.statistics.shapesDisplayUnits[unit].replace( "", - formatBigNumber(rate / 60) + formatBigNumber(rate * statisticsUnitsSeconds[unit]) ); break; } diff --git a/src/js/game/hud/parts/unlock_notification.js b/src/js/game/hud/parts/unlock_notification.js index 7a5c923b..d88e2dbb 100644 --- a/src/js/game/hud/parts/unlock_notification.js +++ b/src/js/game/hud/parts/unlock_notification.js @@ -1,156 +1,160 @@ -import { globalConfig } from "../../../core/config"; -import { gMetaBuildingRegistry } from "../../../core/global_registries"; -import { makeDiv } from "../../../core/utils"; -import { SOUNDS } from "../../../platform/sound"; -import { T } from "../../../translations"; -import { defaultBuildingVariant } from "../../meta_building"; -import { enumHubGoalRewards } from "../../tutorial_goals"; -import { BaseHUDPart } from "../base_hud_part"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings"; -import { InputReceiver } from "../../../core/input_receiver"; - -export class HUDUnlockNotification extends BaseHUDPart { - initialize() { - this.visible = false; - - this.domAttach = new DynamicDomAttach(this.root, this.element, { - timeToKeepSeconds: 0, - }); - - if (!(G_IS_DEV && globalConfig.debug.disableUnlockDialog)) { - this.root.signals.storyGoalCompleted.add(this.showForLevel, this); - } - - this.buttonShowTimeout = null; - } - - createElements(parent) { - this.inputReciever = new InputReceiver("unlock-notification"); - - this.element = makeDiv(parent, "ingame_HUD_UnlockNotification", ["noBlur"]); - - const dialog = makeDiv(this.element, null, ["dialog"]); - - this.elemTitle = makeDiv(dialog, null, ["title"]); - this.elemSubTitle = makeDiv(dialog, null, ["subTitle"], T.ingame.levelCompleteNotification.completed); - - this.elemContents = makeDiv(dialog, null, ["contents"]); - - this.btnClose = document.createElement("button"); - this.btnClose.classList.add("close", "styledButton"); - this.btnClose.innerText = T.ingame.levelCompleteNotification.buttonNextLevel; - dialog.appendChild(this.btnClose); - - this.trackClicks(this.btnClose, this.requestClose); - } - - /** - * @param {number} level - * @param {enumHubGoalRewards} reward - */ - showForLevel(level, reward) { - this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); - this.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace( - "", - ("" + level).padStart(2, "0") - ); - - const rewardName = T.storyRewards[reward].title; - - let html = ` -
- ${T.ingame.levelCompleteNotification.unlockText.replace("", rewardName)} -
- -
- ${T.storyRewards[reward].desc} -
- - `; - - html += "
"; - const gained = enumHubGoalRewardsToContentUnlocked[reward]; - if (gained) { - gained.forEach(([metaBuildingClass, variant]) => { - const metaBuilding = gMetaBuildingRegistry.findByClass(metaBuildingClass); - html += `
`; - }); - } - html += "
"; - - this.elemContents.innerHTML = html; - this.visible = true; - this.root.soundProxy.playUi(SOUNDS.levelComplete); - - if (this.buttonShowTimeout) { - clearTimeout(this.buttonShowTimeout); - } - - this.element.querySelector("button.close").classList.remove("unlocked"); - - if (this.root.app.settings.getAllSettings().offerHints) { - this.buttonShowTimeout = setTimeout( - () => this.element.querySelector("button.close").classList.add("unlocked"), - G_IS_DEV ? 100 : 5000 - ); - } else { - this.element.querySelector("button.close").classList.add("unlocked"); - } - } - - cleanup() { - this.root.app.inputMgr.makeSureDetached(this.inputReciever); - if (this.buttonShowTimeout) { - clearTimeout(this.buttonShowTimeout); - this.buttonShowTimeout = null; - } - } - - requestClose() { - this.root.app.adProvider.showVideoAd().then(() => { - this.close(); - - if (!this.root.app.settings.getAllSettings().offerHints) { - return; - } - - if (this.root.hubGoals.level === 3) { - const { showUpgrades } = this.root.hud.parts.dialogs.showInfo( - T.dialogs.upgradesIntroduction.title, - T.dialogs.upgradesIntroduction.desc, - ["showUpgrades:good:timeout"] - ); - showUpgrades.add(() => this.root.hud.parts.shop.show()); - } - - if (this.root.hubGoals.level === 5) { - const { showKeybindings } = this.root.hud.parts.dialogs.showInfo( - T.dialogs.keybindingsIntroduction.title, - T.dialogs.keybindingsIntroduction.desc, - ["showKeybindings:misc", "ok:good:timeout"] - ); - showKeybindings.add(() => this.root.gameState.goToKeybindings()); - } - }); - } - - close() { - this.root.app.inputMgr.makeSureDetached(this.inputReciever); - if (this.buttonShowTimeout) { - clearTimeout(this.buttonShowTimeout); - this.buttonShowTimeout = null; - } - this.visible = false; - } - - update() { - this.domAttach.update(this.visible); - if (!this.visible && this.buttonShowTimeout) { - clearTimeout(this.buttonShowTimeout); - this.buttonShowTimeout = null; - } - } -} +import { globalConfig } from "../../../core/config"; +import { gMetaBuildingRegistry } from "../../../core/global_registries"; +import { makeDiv } from "../../../core/utils"; +import { SOUNDS } from "../../../platform/sound"; +import { T } from "../../../translations"; +import { defaultBuildingVariant } from "../../meta_building"; +import { enumHubGoalRewards } from "../../tutorial_goals"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings"; +import { InputReceiver } from "../../../core/input_receiver"; + +export class HUDUnlockNotification extends BaseHUDPart { + initialize() { + this.visible = false; + + this.domAttach = new DynamicDomAttach(this.root, this.element, { + timeToKeepSeconds: 0, + }); + + if (!(G_IS_DEV && globalConfig.debug.disableUnlockDialog)) { + this.root.signals.storyGoalCompleted.add(this.showForLevel, this); + } + + this.buttonShowTimeout = null; + } + + createElements(parent) { + this.inputReciever = new InputReceiver("unlock-notification"); + + this.element = makeDiv(parent, "ingame_HUD_UnlockNotification", ["noBlur"]); + + const dialog = makeDiv(this.element, null, ["dialog"]); + + this.elemTitle = makeDiv(dialog, null, ["title"]); + this.elemSubTitle = makeDiv(dialog, null, ["subTitle"], T.ingame.levelCompleteNotification.completed); + + this.elemContents = makeDiv(dialog, null, ["contents"]); + + this.btnClose = document.createElement("button"); + this.btnClose.classList.add("close", "styledButton"); + this.btnClose.innerText = T.ingame.levelCompleteNotification.buttonNextLevel; + dialog.appendChild(this.btnClose); + + this.trackClicks(this.btnClose, this.requestClose); + } + + /** + * @param {number} level + * @param {enumHubGoalRewards} reward + */ + showForLevel(level, reward) { + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); + this.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace( + "", + ("" + level).padStart(2, "0") + ); + + const rewardName = T.storyRewards[reward].title; + + let html = ` +
+ ${T.ingame.levelCompleteNotification.unlockText.replace("", rewardName)} +
+ +
+ ${T.storyRewards[reward].desc} +
+ + `; + + html += "
"; + const gained = enumHubGoalRewardsToContentUnlocked[reward]; + if (gained) { + gained.forEach(([metaBuildingClass, variant]) => { + const metaBuilding = gMetaBuildingRegistry.findByClass(metaBuildingClass); + html += `
`; + }); + } + html += "
"; + + this.elemContents.innerHTML = html; + this.visible = true; + this.root.soundProxy.playUi(SOUNDS.levelComplete); + + if (this.buttonShowTimeout) { + clearTimeout(this.buttonShowTimeout); + } + + this.element.querySelector("button.close").classList.remove("unlocked"); + + if (this.root.app.settings.getAllSettings().offerHints) { + this.buttonShowTimeout = setTimeout( + () => this.element.querySelector("button.close").classList.add("unlocked"), + G_IS_DEV ? 100 : 5000 + ); + } else { + this.element.querySelector("button.close").classList.add("unlocked"); + } + } + + cleanup() { + this.root.app.inputMgr.makeSureDetached(this.inputReciever); + if (this.buttonShowTimeout) { + clearTimeout(this.buttonShowTimeout); + this.buttonShowTimeout = null; + } + } + + isBlockingOverlay() { + return this.visible; + } + + requestClose() { + this.root.app.adProvider.showVideoAd().then(() => { + this.close(); + + if (!this.root.app.settings.getAllSettings().offerHints) { + return; + } + + if (this.root.hubGoals.level === 3) { + const { showUpgrades } = this.root.hud.parts.dialogs.showInfo( + T.dialogs.upgradesIntroduction.title, + T.dialogs.upgradesIntroduction.desc, + ["showUpgrades:good:timeout"] + ); + showUpgrades.add(() => this.root.hud.parts.shop.show()); + } + + if (this.root.hubGoals.level === 5) { + const { showKeybindings } = this.root.hud.parts.dialogs.showInfo( + T.dialogs.keybindingsIntroduction.title, + T.dialogs.keybindingsIntroduction.desc, + ["showKeybindings:misc", "ok:good:timeout"] + ); + showKeybindings.add(() => this.root.gameState.goToKeybindings()); + } + }); + } + + close() { + this.root.app.inputMgr.makeSureDetached(this.inputReciever); + if (this.buttonShowTimeout) { + clearTimeout(this.buttonShowTimeout); + this.buttonShowTimeout = null; + } + this.visible = false; + } + + update() { + this.domAttach.update(this.visible); + if (!this.visible && this.buttonShowTimeout) { + clearTimeout(this.buttonShowTimeout); + this.buttonShowTimeout = null; + } + } +} diff --git a/src/js/game/hud/parts/waypoints.js b/src/js/game/hud/parts/waypoints.js index abf05b1f..116cd087 100644 --- a/src/js/game/hud/parts/waypoints.js +++ b/src/js/game/hud/parts/waypoints.js @@ -1,627 +1,626 @@ -import { makeOffscreenBuffer } from "../../../core/buffer_utils"; -import { globalConfig, IS_DEMO } from "../../../core/config"; -import { DrawParameters } from "../../../core/draw_parameters"; -import { Loader } from "../../../core/loader"; -import { DialogWithForm } from "../../../core/modal_dialog_elements"; -import { FormElementInput } from "../../../core/modal_dialog_forms"; -import { Rectangle } from "../../../core/rectangle"; -import { STOP_PROPAGATION } from "../../../core/signal"; -import { arrayDeleteValue, lerp, makeDiv, removeAllChildren, clamp } from "../../../core/utils"; -import { Vector } from "../../../core/vector"; -import { T } from "../../../translations"; -import { enumMouseButton } from "../../camera"; -import { KEYMAPPINGS } from "../../key_action_mapper"; -import { BaseHUDPart } from "../base_hud_part"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { enumNotificationType } from "./notifications"; -import { ShapeDefinition } from "../../shape_definition"; -import { BaseItem } from "../../base_item"; -import { ShapeItem } from "../../items/shape_item"; - -/** @typedef {{ - * label: string | null, - * center: { x: number, y: number }, - * zoomLevel: number - * }} Waypoint */ - -/** - * Used when a shape icon is rendered instead - */ -const MAX_LABEL_LENGTH = 71; - -export class HUDWaypoints extends BaseHUDPart { - /** - * Creates the overview of waypoints - * @param {HTMLElement} parent - */ - createElements(parent) { - // Create the helper box on the lower right when zooming out - if (this.root.app.settings.getAllSettings().offerHints) { - this.hintElement = makeDiv( - parent, - "ingame_HUD_Waypoints_Hint", - [], - ` - ${T.ingame.waypoints.waypoints} - ${T.ingame.waypoints.description.replace( - "", - `${this.root.keyMapper - .getBinding(KEYMAPPINGS.navigation.createMarker) - .getKeyCodeString()}` - )} - ` - ); - } - - // Create the waypoint list on the upper right - this.waypointsListElement = makeDiv(parent, "ingame_HUD_Waypoints", [], "Waypoints"); - } - - /** - * Serializes the waypoints - */ - serialize() { - return { - waypoints: this.waypoints, - }; - } - - /** - * Deserializes the waypoints - * @param {{waypoints: Array}} data - */ - deserialize(data) { - if (!data || !data.waypoints || !Array.isArray(data.waypoints)) { - return "Invalid waypoints data"; - } - this.waypoints = data.waypoints; - this.rerenderWaypointList(); - } - - /** - * Initializes everything - */ - initialize() { - // Cache the sprite for the waypoints - this.waypointSprite = Loader.getSprite("sprites/misc/waypoint.png"); - this.directionIndicatorSprite = Loader.getSprite("sprites/misc/hub_direction_indicator.png"); - - /** @type {Array} - */ - this.waypoints = [ - { - label: null, - center: { x: 0, y: 0 }, - zoomLevel: 3, - }, - ]; - - // Create a buffer we can use to measure text - this.dummyBuffer = makeOffscreenBuffer(1, 1, { - reusable: false, - label: "waypoints-measure-canvas", - })[1]; - - // Dynamically attach/detach the lower right hint in the map overview - if (this.hintElement) { - this.domAttach = new DynamicDomAttach(this.root, this.hintElement); - } - - // Catch mouse and key events - this.root.camera.downPreHandler.add(this.onMouseDown, this); - this.root.keyMapper - .getBinding(KEYMAPPINGS.navigation.createMarker) - .add(() => this.requestSaveMarker({})); - - /** - * Stores at how much opacity the markers should be rendered on the map. - * This is interpolated over multiple frames so we have some sort of fade effect - */ - this.currentMarkerOpacity = 1; - this.currentCompassOpacity = 0; - - // Create buffer which is used to indicate the hub direction - const [canvas, context] = makeOffscreenBuffer(48, 48, { - smooth: true, - reusable: false, - label: "waypoints-compass", - }); - this.compassBuffer = { canvas, context }; - - /** - * Stores a cache from a shape short key to its canvas representation - */ - this.cachedKeyToCanvas = {}; - - /** - * Store cached text widths - * @type {Object} - */ - this.cachedTextWidths = {}; - - // Initial render - this.rerenderWaypointList(); - } - - /** - * Returns how long a text will be rendered - * @param {string} text - * @returns {number} - */ - getTextWidth(text) { - if (this.cachedTextWidths[text]) { - return this.cachedTextWidths[text]; - } - - this.dummyBuffer.font = "bold " + this.getTextScale() + "px GameFont"; - return (this.cachedTextWidths[text] = this.dummyBuffer.measureText(text).width); - } - - /** - * Returns how big the text should be rendered - */ - getTextScale() { - return this.getWaypointUiScale() * 12; - } - - /** - * Returns the scale for rendering waypoints - */ - getWaypointUiScale() { - return this.root.app.getEffectiveUiScale(); - } - - /** - * Re-renders the waypoint list to account for changes - */ - rerenderWaypointList() { - removeAllChildren(this.waypointsListElement); - this.cleanupClickDetectors(); - - for (let i = 0; i < this.waypoints.length; ++i) { - const waypoint = this.waypoints[i]; - const label = this.getWaypointLabel(waypoint); - - const element = makeDiv(this.waypointsListElement, null, ["waypoint"]); - - if (ShapeDefinition.isValidShortKey(label)) { - const canvas = this.getWaypointCanvas(waypoint); - /** - * Create a clone of the cached canvas, as calling appendElement when a canvas is - * already in the document will move the existing canvas to the new position. - */ - const [newCanvas, context] = makeOffscreenBuffer(48, 48, { - smooth: true, - label: label + "-waypoint-" + i, - }); - context.drawImage(canvas, 0, 0); - element.appendChild(newCanvas); - element.classList.add("shapeIcon"); - } else { - element.innerText = label; - } - - if (this.isWaypointDeletable(waypoint)) { - const editButton = makeDiv(element, null, ["editButton"]); - this.trackClicks(editButton, () => this.requestSaveMarker({ waypoint })); - } - - if (!waypoint.label) { - // This must be the hub label - element.classList.add("hub"); - element.insertBefore(this.compassBuffer.canvas, element.childNodes[0]); - } - - this.trackClicks(element, () => this.moveToWaypoint(waypoint), { - targetOnly: true, - }); - } - } - - /** - * Moves the camera to a given waypoint - * @param {Waypoint} waypoint - */ - moveToWaypoint(waypoint) { - this.root.camera.setDesiredCenter(new Vector(waypoint.center.x, waypoint.center.y)); - this.root.camera.setDesiredZoom(waypoint.zoomLevel); - } - - /** - * Deletes a waypoint from the list - * @param {Waypoint} waypoint - */ - deleteWaypoint(waypoint) { - arrayDeleteValue(this.waypoints, waypoint); - this.rerenderWaypointList(); - } - - /** - * Gets the canvas for a given waypoint - * @param {Waypoint} waypoint - * @returns {HTMLCanvasElement} - */ - getWaypointCanvas(waypoint) { - const key = waypoint.label; - if (this.cachedKeyToCanvas[key]) { - return this.cachedKeyToCanvas[key]; - } - - assert(ShapeDefinition.isValidShortKey(key), "Invalid short key: " + key); - const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key); - const preRendered = definition.generateAsCanvas(48); - return (this.cachedKeyToCanvas[key] = preRendered); - } - - /** - * Requests to save a marker at the current camera position. If worldPos is set, - * uses that position instead. - * @param {object} param0 - * @param {Vector=} param0.worldPos Override the world pos, otherwise it is the camera position - * @param {Waypoint=} param0.waypoint Waypoint to be edited. If omitted, create new - */ - requestSaveMarker({ worldPos = null, waypoint = null }) { - // Construct dialog with input field - const markerNameInput = new FormElementInput({ - id: "markerName", - label: null, - placeholder: "", - defaultValue: waypoint ? waypoint.label : "", - validator: val => - val.length > 0 && (val.length < MAX_LABEL_LENGTH || ShapeDefinition.isValidShortKey(val)), - }); - const dialog = new DialogWithForm({ - app: this.root.app, - title: waypoint ? T.dialogs.createMarker.titleEdit : T.dialogs.createMarker.title, - desc: T.dialogs.createMarker.desc, - formElements: [markerNameInput], - buttons: waypoint ? ["delete:bad", "cancel", "ok:good"] : ["cancel", "ok:good"], - }); - this.root.hud.parts.dialogs.internalShowDialog(dialog); - - // Edit marker - if (waypoint) { - dialog.buttonSignals.ok.add(() => { - // Actually rename the waypoint - this.renameWaypoint(waypoint, markerNameInput.getValue()); - }); - dialog.buttonSignals.delete.add(() => { - // Actually delete the waypoint - this.deleteWaypoint(waypoint); - }); - } else { - // Compute where to create the marker - const center = worldPos || this.root.camera.center; - - dialog.buttonSignals.ok.add(() => { - // Show info that you can have only N markers in the demo, - // actually show this *after* entering the name so you want the - // standalone even more (I'm evil :P) - if (IS_DEMO && this.waypoints.length > 2) { - this.root.hud.parts.dialogs.showFeatureRestrictionInfo( - "", - T.dialogs.markerDemoLimit.desc - ); - return; - } - - // Actually create the waypoint - this.addWaypoint(markerNameInput.getValue(), center); - }); - } - } - - /** - * Adds a new waypoint at the given location with the given label - * @param {string} label - * @param {Vector} position - */ - addWaypoint(label, position) { - this.waypoints.push({ - label, - center: { x: position.x, y: position.y }, - // Make sure the zoom is *just* a bit above the zoom level where the map overview - // starts, so you always see all buildings - zoomLevel: Math.max(this.root.camera.zoomLevel, globalConfig.mapChunkOverviewMinZoom + 0.05), - }); - - this.sortWaypoints(); - - // Show notification about creation - this.root.hud.signals.notification.dispatch( - T.ingame.waypoints.creationSuccessNotification, - enumNotificationType.success - ); - - // Re-render the list and thus add it - this.rerenderWaypointList(); - } - - /** - * Renames a waypoint with the given label - * @param {Waypoint} waypoint - * @param {string} label - */ - renameWaypoint(waypoint, label) { - waypoint.label = label; - - this.sortWaypoints(); - - // Show notification about renamed - this.root.hud.signals.notification.dispatch( - T.ingame.waypoints.creationSuccessNotification, - enumNotificationType.success - ); - - // Re-render the list and thus add it - this.rerenderWaypointList(); - } - - /** - * Called every frame to update stuff - */ - update() { - if (this.domAttach) { - this.domAttach.update(this.root.camera.getIsMapOverlayActive()); - } - } - - /** - * Sort waypoints by name - */ - sortWaypoints() { - this.waypoints.sort((a, b) => { - if (!a.label) { - return -1; - } - if (!b.label) { - return 1; - } - return this.getWaypointLabel(a) - .padEnd(MAX_LABEL_LENGTH, "0") - .localeCompare(this.getWaypointLabel(b).padEnd(MAX_LABEL_LENGTH, "0")); - }); - } - - /** - * Returns the label for a given waypoint - * @param {Waypoint} waypoint - * @returns {string} - */ - getWaypointLabel(waypoint) { - return waypoint.label || T.ingame.waypoints.hub; - } - - /** - * Returns if a waypoint is deletable - * @param {Waypoint} waypoint - * @returns {boolean} - */ - isWaypointDeletable(waypoint) { - return waypoint.label !== null; - } - - /** - * Returns the screen space bounds of the given waypoint or null - * if it couldn't be determined. Also returns wheter its a shape or not - * @param {Waypoint} waypoint - * @return {{ - * screenBounds: Rectangle - * item: BaseItem|null, - * text: string - * }} - */ - getWaypointScreenParams(waypoint) { - if (!this.root.camera.getIsMapOverlayActive()) { - return null; - } - - // Find parameters - const scale = this.getWaypointUiScale(); - const screenPos = this.root.camera.worldToScreen(new Vector(waypoint.center.x, waypoint.center.y)); - - // Distinguish between text and item waypoints -> Figure out parameters - const originalLabel = this.getWaypointLabel(waypoint); - let text, item, textWidth; - - if (ShapeDefinition.isValidShortKey(originalLabel)) { - // If the label is actually a key, render the shape icon - item = this.root.shapeDefinitionMgr.getShapeItemFromShortKey(originalLabel); - textWidth = 40; - } else { - // Otherwise render a regular waypoint - text = originalLabel; - textWidth = this.getTextWidth(text); - } - - return { - screenBounds: new Rectangle( - screenPos.x - 7 * scale, - screenPos.y - 12 * scale, - 15 * scale + textWidth, - 15 * scale - ), - item, - text, - }; - } - - /** - * Finds the currently intersected waypoint on the map overview under - * the cursor. - * - * @returns {Waypoint | null} - */ - findCurrentIntersectedWaypoint() { - const mousePos = this.root.app.mousePosition; - if (!mousePos) { - return; - } - - for (let i = 0; i < this.waypoints.length; ++i) { - const waypoint = this.waypoints[i]; - const params = this.getWaypointScreenParams(waypoint); - if (params && params.screenBounds.containsPoint(mousePos.x, mousePos.y)) { - return waypoint; - } - } - } - - /** - * Mouse-Down handler - * @param {Vector} pos - * @param {enumMouseButton} button - */ - onMouseDown(pos, button) { - const waypoint = this.findCurrentIntersectedWaypoint(); - if (waypoint) { - if (button === enumMouseButton.left) { - this.root.soundProxy.playUiClick(); - this.moveToWaypoint(waypoint); - } else if (button === enumMouseButton.right) { - if (this.isWaypointDeletable(waypoint)) { - this.root.soundProxy.playUiClick(); - this.requestSaveMarker({ waypoint }); - } else { - this.root.soundProxy.playUiError(); - } - } - - return STOP_PROPAGATION; - } else { - // Allow right click to create a marker - if (button === enumMouseButton.right) { - if (this.root.camera.getIsMapOverlayActive()) { - const worldPos = this.root.camera.screenToWorld(pos); - this.requestSaveMarker({ worldPos }); - return STOP_PROPAGATION; - } - } - } - } - - /** - * Rerenders the compass - */ - rerenderWaypointsCompass() { - const dims = 48; - const indicatorSize = 30; - const cameraPos = this.root.camera.center; - - const context = this.compassBuffer.context; - context.clearRect(0, 0, dims, dims); - - const distanceToHub = cameraPos.length(); - const compassVisible = distanceToHub > (10 * globalConfig.tileSize) / this.root.camera.zoomLevel; - const targetCompassAlpha = compassVisible ? 1 : 0; - - // Fade the compas in / out - this.currentCompassOpacity = lerp(this.currentCompassOpacity, targetCompassAlpha, 0.08); - - // Render the compass - if (this.currentCompassOpacity > 0.01) { - context.globalAlpha = this.currentCompassOpacity; - const angle = cameraPos.angle() + Math.radians(45) + Math.PI / 2; - context.translate(dims / 2, dims / 2); - context.rotate(angle); - this.directionIndicatorSprite.drawCentered(context, 0, 0, indicatorSize); - context.rotate(-angle); - context.translate(-dims / 2, -dims / 2); - context.globalAlpha = 1; - } - - // Render the regualr icon - const iconOpacity = 1 - this.currentCompassOpacity; - if (iconOpacity > 0.01) { - context.globalAlpha = iconOpacity; - this.waypointSprite.drawCentered(context, dims / 2, dims / 2, dims * 0.7); - context.globalAlpha = 1; - } - } - - /** - * Draws the waypoints on the map - * @param {DrawParameters} parameters - */ - drawOverlays(parameters) { - const mousePos = this.root.app.mousePosition; - const desiredOpacity = this.root.camera.getIsMapOverlayActive() ? 1 : 0; - this.currentMarkerOpacity = lerp(this.currentMarkerOpacity, desiredOpacity, 0.08); - - this.rerenderWaypointsCompass(); - - // Don't render with low opacity - if (this.currentMarkerOpacity < 0.01) { - return; - } - - // Determine rendering scale - const scale = this.getWaypointUiScale(); - - // Set the font size - const textSize = this.getTextScale(); - parameters.context.font = "bold " + textSize + "px GameFont"; - parameters.context.textBaseline = "middle"; - - // Loop over all waypoints - for (let i = 0; i < this.waypoints.length; ++i) { - const waypoint = this.waypoints[i]; - - const waypointData = this.getWaypointScreenParams(waypoint); - if (!waypointData) { - // Not relevant - continue; - } - - if (!parameters.visibleRect.containsRect(waypointData.screenBounds)) { - // Out of screen - continue; - } - - const bounds = waypointData.screenBounds; - const contentPaddingX = 7 * scale; - const isSelected = mousePos && bounds.containsPoint(mousePos.x, mousePos.y); - - // Render the background rectangle - parameters.context.globalAlpha = this.currentMarkerOpacity * (isSelected ? 1 : 0.7); - parameters.context.fillStyle = "rgba(255, 255, 255, 0.7)"; - parameters.context.fillRect(bounds.x, bounds.y, bounds.w, bounds.h); - - // Render the text - if (waypointData.item) { - const canvas = this.getWaypointCanvas(waypoint); - const itemSize = 14 * scale; - parameters.context.drawImage( - canvas, - bounds.x + contentPaddingX + 6 * scale, - bounds.y + bounds.h / 2 - itemSize / 2, - itemSize, - itemSize - ); - } else if (waypointData.text) { - // Render the text - parameters.context.fillStyle = "#000"; - parameters.context.textBaseline = "middle"; - parameters.context.fillText( - waypointData.text, - bounds.x + contentPaddingX + 6 * scale, - bounds.y + bounds.h / 2 - ); - parameters.context.textBaseline = "alphabetic"; - } else { - assertAlways(false, "Waypoint has no item and text"); - } - - // Render the small icon on the left - this.waypointSprite.drawCentered( - parameters.context, - bounds.x + contentPaddingX, - bounds.y + bounds.h / 2, - bounds.h * 0.7 - ); - } - - parameters.context.textBaseline = "alphabetic"; - parameters.context.globalAlpha = 1; - } -} +import { makeOffscreenBuffer } from "../../../core/buffer_utils"; +import { globalConfig, IS_DEMO } from "../../../core/config"; +import { DrawParameters } from "../../../core/draw_parameters"; +import { Loader } from "../../../core/loader"; +import { DialogWithForm } from "../../../core/modal_dialog_elements"; +import { FormElementInput } from "../../../core/modal_dialog_forms"; +import { Rectangle } from "../../../core/rectangle"; +import { STOP_PROPAGATION } from "../../../core/signal"; +import { arrayDeleteValue, lerp, makeDiv, removeAllChildren } from "../../../core/utils"; +import { Vector } from "../../../core/vector"; +import { T } from "../../../translations"; +import { BaseItem } from "../../base_item"; +import { enumMouseButton } from "../../camera"; +import { KEYMAPPINGS } from "../../key_action_mapper"; +import { ShapeDefinition } from "../../shape_definition"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { enumNotificationType } from "./notifications"; + +/** @typedef {{ + * label: string | null, + * center: { x: number, y: number }, + * zoomLevel: number + * }} Waypoint */ + +/** + * Used when a shape icon is rendered instead + */ +const MAX_LABEL_LENGTH = 71; + +export class HUDWaypoints extends BaseHUDPart { + /** + * Creates the overview of waypoints + * @param {HTMLElement} parent + */ + createElements(parent) { + // Create the helper box on the lower right when zooming out + if (this.root.app.settings.getAllSettings().offerHints) { + this.hintElement = makeDiv( + parent, + "ingame_HUD_Waypoints_Hint", + [], + ` + ${T.ingame.waypoints.waypoints} + ${T.ingame.waypoints.description.replace( + "", + `${this.root.keyMapper + .getBinding(KEYMAPPINGS.navigation.createMarker) + .getKeyCodeString()}` + )} + ` + ); + } + + // Create the waypoint list on the upper right + this.waypointsListElement = makeDiv(parent, "ingame_HUD_Waypoints", [], "Waypoints"); + } + + /** + * Serializes the waypoints + */ + serialize() { + return { + waypoints: this.waypoints, + }; + } + + /** + * Deserializes the waypoints + * @param {{waypoints: Array}} data + */ + deserialize(data) { + if (!data || !data.waypoints || !Array.isArray(data.waypoints)) { + return "Invalid waypoints data"; + } + this.waypoints = data.waypoints; + this.rerenderWaypointList(); + } + + /** + * Initializes everything + */ + initialize() { + // Cache the sprite for the waypoints + this.waypointSprite = Loader.getSprite("sprites/misc/waypoint.png"); + this.directionIndicatorSprite = Loader.getSprite("sprites/misc/hub_direction_indicator.png"); + + /** @type {Array} + */ + this.waypoints = [ + { + label: null, + center: { x: 0, y: 0 }, + zoomLevel: 3, + }, + ]; + + // Create a buffer we can use to measure text + this.dummyBuffer = makeOffscreenBuffer(1, 1, { + reusable: false, + label: "waypoints-measure-canvas", + })[1]; + + // Dynamically attach/detach the lower right hint in the map overview + if (this.hintElement) { + this.domAttach = new DynamicDomAttach(this.root, this.hintElement); + } + + // Catch mouse and key events + this.root.camera.downPreHandler.add(this.onMouseDown, this); + this.root.keyMapper + .getBinding(KEYMAPPINGS.navigation.createMarker) + .add(() => this.requestSaveMarker({})); + + /** + * Stores at how much opacity the markers should be rendered on the map. + * This is interpolated over multiple frames so we have some sort of fade effect + */ + this.currentMarkerOpacity = 1; + this.currentCompassOpacity = 0; + + // Create buffer which is used to indicate the hub direction + const [canvas, context] = makeOffscreenBuffer(48, 48, { + smooth: true, + reusable: false, + label: "waypoints-compass", + }); + this.compassBuffer = { canvas, context }; + + /** + * Stores a cache from a shape short key to its canvas representation + */ + this.cachedKeyToCanvas = {}; + + /** + * Store cached text widths + * @type {Object} + */ + this.cachedTextWidths = {}; + + // Initial render + this.rerenderWaypointList(); + } + + /** + * Returns how long a text will be rendered + * @param {string} text + * @returns {number} + */ + getTextWidth(text) { + if (this.cachedTextWidths[text]) { + return this.cachedTextWidths[text]; + } + + this.dummyBuffer.font = "bold " + this.getTextScale() + "px GameFont"; + return (this.cachedTextWidths[text] = this.dummyBuffer.measureText(text).width); + } + + /** + * Returns how big the text should be rendered + */ + getTextScale() { + return this.getWaypointUiScale() * 12; + } + + /** + * Returns the scale for rendering waypoints + */ + getWaypointUiScale() { + return this.root.app.getEffectiveUiScale(); + } + + /** + * Re-renders the waypoint list to account for changes + */ + rerenderWaypointList() { + removeAllChildren(this.waypointsListElement); + this.cleanupClickDetectors(); + + for (let i = 0; i < this.waypoints.length; ++i) { + const waypoint = this.waypoints[i]; + const label = this.getWaypointLabel(waypoint); + + const element = makeDiv(this.waypointsListElement, null, ["waypoint"]); + + if (ShapeDefinition.isValidShortKey(label)) { + const canvas = this.getWaypointCanvas(waypoint); + /** + * Create a clone of the cached canvas, as calling appendElement when a canvas is + * already in the document will move the existing canvas to the new position. + */ + const [newCanvas, context] = makeOffscreenBuffer(48, 48, { + smooth: true, + label: label + "-waypoint-" + i, + }); + context.drawImage(canvas, 0, 0); + element.appendChild(newCanvas); + element.classList.add("shapeIcon"); + } else { + element.innerText = label; + } + + if (this.isWaypointDeletable(waypoint)) { + const editButton = makeDiv(element, null, ["editButton"]); + this.trackClicks(editButton, () => this.requestSaveMarker({ waypoint })); + } + + if (!waypoint.label) { + // This must be the hub label + element.classList.add("hub"); + element.insertBefore(this.compassBuffer.canvas, element.childNodes[0]); + } + + this.trackClicks(element, () => this.moveToWaypoint(waypoint), { + targetOnly: true, + }); + } + } + + /** + * Moves the camera to a given waypoint + * @param {Waypoint} waypoint + */ + moveToWaypoint(waypoint) { + this.root.camera.setDesiredCenter(new Vector(waypoint.center.x, waypoint.center.y)); + this.root.camera.setDesiredZoom(waypoint.zoomLevel); + } + + /** + * Deletes a waypoint from the list + * @param {Waypoint} waypoint + */ + deleteWaypoint(waypoint) { + arrayDeleteValue(this.waypoints, waypoint); + this.rerenderWaypointList(); + } + + /** + * Gets the canvas for a given waypoint + * @param {Waypoint} waypoint + * @returns {HTMLCanvasElement} + */ + getWaypointCanvas(waypoint) { + const key = waypoint.label; + if (this.cachedKeyToCanvas[key]) { + return this.cachedKeyToCanvas[key]; + } + + assert(ShapeDefinition.isValidShortKey(key), "Invalid short key: " + key); + const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key); + const preRendered = definition.generateAsCanvas(48); + return (this.cachedKeyToCanvas[key] = preRendered); + } + + /** + * Requests to save a marker at the current camera position. If worldPos is set, + * uses that position instead. + * @param {object} param0 + * @param {Vector=} param0.worldPos Override the world pos, otherwise it is the camera position + * @param {Waypoint=} param0.waypoint Waypoint to be edited. If omitted, create new + */ + requestSaveMarker({ worldPos = null, waypoint = null }) { + // Construct dialog with input field + const markerNameInput = new FormElementInput({ + id: "markerName", + label: null, + placeholder: "", + defaultValue: waypoint ? waypoint.label : "", + validator: val => + val.length > 0 && (val.length < MAX_LABEL_LENGTH || ShapeDefinition.isValidShortKey(val)), + }); + const dialog = new DialogWithForm({ + app: this.root.app, + title: waypoint ? T.dialogs.createMarker.titleEdit : T.dialogs.createMarker.title, + desc: T.dialogs.createMarker.desc, + formElements: [markerNameInput], + buttons: waypoint ? ["delete:bad", "cancel", "ok:good"] : ["cancel", "ok:good"], + }); + this.root.hud.parts.dialogs.internalShowDialog(dialog); + + // Edit marker + if (waypoint) { + dialog.buttonSignals.ok.add(() => { + // Actually rename the waypoint + this.renameWaypoint(waypoint, markerNameInput.getValue()); + }); + dialog.buttonSignals.delete.add(() => { + // Actually delete the waypoint + this.deleteWaypoint(waypoint); + }); + } else { + // Compute where to create the marker + const center = worldPos || this.root.camera.center; + + dialog.buttonSignals.ok.add(() => { + // Show info that you can have only N markers in the demo, + // actually show this *after* entering the name so you want the + // standalone even more (I'm evil :P) + if (IS_DEMO && this.waypoints.length > 2) { + this.root.hud.parts.dialogs.showFeatureRestrictionInfo( + "", + T.dialogs.markerDemoLimit.desc + ); + return; + } + + // Actually create the waypoint + this.addWaypoint(markerNameInput.getValue(), center); + }); + } + } + + /** + * Adds a new waypoint at the given location with the given label + * @param {string} label + * @param {Vector} position + */ + addWaypoint(label, position) { + this.waypoints.push({ + label, + center: { x: position.x, y: position.y }, + // Make sure the zoom is *just* a bit above the zoom level where the map overview + // starts, so you always see all buildings + zoomLevel: Math.max(this.root.camera.zoomLevel, globalConfig.mapChunkOverviewMinZoom + 0.05), + }); + + this.sortWaypoints(); + + // Show notification about creation + this.root.hud.signals.notification.dispatch( + T.ingame.waypoints.creationSuccessNotification, + enumNotificationType.success + ); + + // Re-render the list and thus add it + this.rerenderWaypointList(); + } + + /** + * Renames a waypoint with the given label + * @param {Waypoint} waypoint + * @param {string} label + */ + renameWaypoint(waypoint, label) { + waypoint.label = label; + + this.sortWaypoints(); + + // Show notification about renamed + this.root.hud.signals.notification.dispatch( + T.ingame.waypoints.creationSuccessNotification, + enumNotificationType.success + ); + + // Re-render the list and thus add it + this.rerenderWaypointList(); + } + + /** + * Called every frame to update stuff + */ + update() { + if (this.domAttach) { + this.domAttach.update(this.root.camera.getIsMapOverlayActive()); + } + } + + /** + * Sort waypoints by name + */ + sortWaypoints() { + this.waypoints.sort((a, b) => { + if (!a.label) { + return -1; + } + if (!b.label) { + return 1; + } + return this.getWaypointLabel(a) + .padEnd(MAX_LABEL_LENGTH, "0") + .localeCompare(this.getWaypointLabel(b).padEnd(MAX_LABEL_LENGTH, "0")); + }); + } + + /** + * Returns the label for a given waypoint + * @param {Waypoint} waypoint + * @returns {string} + */ + getWaypointLabel(waypoint) { + return waypoint.label || T.ingame.waypoints.hub; + } + + /** + * Returns if a waypoint is deletable + * @param {Waypoint} waypoint + * @returns {boolean} + */ + isWaypointDeletable(waypoint) { + return waypoint.label !== null; + } + + /** + * Returns the screen space bounds of the given waypoint or null + * if it couldn't be determined. Also returns wheter its a shape or not + * @param {Waypoint} waypoint + * @return {{ + * screenBounds: Rectangle + * item: BaseItem|null, + * text: string + * }} + */ + getWaypointScreenParams(waypoint) { + if (!this.root.camera.getIsMapOverlayActive()) { + return null; + } + + // Find parameters + const scale = this.getWaypointUiScale(); + const screenPos = this.root.camera.worldToScreen(new Vector(waypoint.center.x, waypoint.center.y)); + + // Distinguish between text and item waypoints -> Figure out parameters + const originalLabel = this.getWaypointLabel(waypoint); + let text, item, textWidth; + + if (ShapeDefinition.isValidShortKey(originalLabel)) { + // If the label is actually a key, render the shape icon + item = this.root.shapeDefinitionMgr.getShapeItemFromShortKey(originalLabel); + textWidth = 40; + } else { + // Otherwise render a regular waypoint + text = originalLabel; + textWidth = this.getTextWidth(text); + } + + return { + screenBounds: new Rectangle( + screenPos.x - 7 * scale, + screenPos.y - 12 * scale, + 15 * scale + textWidth, + 15 * scale + ), + item, + text, + }; + } + + /** + * Finds the currently intersected waypoint on the map overview under + * the cursor. + * + * @returns {Waypoint | null} + */ + findCurrentIntersectedWaypoint() { + const mousePos = this.root.app.mousePosition; + if (!mousePos) { + return; + } + + for (let i = 0; i < this.waypoints.length; ++i) { + const waypoint = this.waypoints[i]; + const params = this.getWaypointScreenParams(waypoint); + if (params && params.screenBounds.containsPoint(mousePos.x, mousePos.y)) { + return waypoint; + } + } + } + + /** + * Mouse-Down handler + * @param {Vector} pos + * @param {enumMouseButton} button + */ + onMouseDown(pos, button) { + const waypoint = this.findCurrentIntersectedWaypoint(); + if (waypoint) { + if (button === enumMouseButton.left) { + this.root.soundProxy.playUiClick(); + this.moveToWaypoint(waypoint); + } else if (button === enumMouseButton.right) { + if (this.isWaypointDeletable(waypoint)) { + this.root.soundProxy.playUiClick(); + this.requestSaveMarker({ waypoint }); + } else { + this.root.soundProxy.playUiError(); + } + } + + return STOP_PROPAGATION; + } else { + // Allow right click to create a marker + if (button === enumMouseButton.right) { + if (this.root.camera.getIsMapOverlayActive()) { + const worldPos = this.root.camera.screenToWorld(pos); + this.requestSaveMarker({ worldPos }); + return STOP_PROPAGATION; + } + } + } + } + + /** + * Rerenders the compass + */ + rerenderWaypointsCompass() { + const dims = 48; + const indicatorSize = 30; + const cameraPos = this.root.camera.center; + + const context = this.compassBuffer.context; + context.clearRect(0, 0, dims, dims); + + const distanceToHub = cameraPos.length(); + const compassVisible = distanceToHub > (10 * globalConfig.tileSize) / this.root.camera.zoomLevel; + const targetCompassAlpha = compassVisible ? 1 : 0; + + // Fade the compas in / out + this.currentCompassOpacity = lerp(this.currentCompassOpacity, targetCompassAlpha, 0.08); + + // Render the compass + if (this.currentCompassOpacity > 0.01) { + context.globalAlpha = this.currentCompassOpacity; + const angle = cameraPos.angle() + Math.radians(45) + Math.PI / 2; + context.translate(dims / 2, dims / 2); + context.rotate(angle); + this.directionIndicatorSprite.drawCentered(context, 0, 0, indicatorSize); + context.rotate(-angle); + context.translate(-dims / 2, -dims / 2); + context.globalAlpha = 1; + } + + // Render the regualr icon + const iconOpacity = 1 - this.currentCompassOpacity; + if (iconOpacity > 0.01) { + context.globalAlpha = iconOpacity; + this.waypointSprite.drawCentered(context, dims / 2, dims / 2, dims * 0.7); + context.globalAlpha = 1; + } + } + + /** + * Draws the waypoints on the map + * @param {DrawParameters} parameters + */ + drawOverlays(parameters) { + const mousePos = this.root.app.mousePosition; + const desiredOpacity = this.root.camera.getIsMapOverlayActive() ? 1 : 0; + this.currentMarkerOpacity = lerp(this.currentMarkerOpacity, desiredOpacity, 0.08); + + this.rerenderWaypointsCompass(); + + // Don't render with low opacity + if (this.currentMarkerOpacity < 0.01) { + return; + } + + // Determine rendering scale + const scale = this.getWaypointUiScale(); + + // Set the font size + const textSize = this.getTextScale(); + parameters.context.font = "bold " + textSize + "px GameFont"; + parameters.context.textBaseline = "middle"; + + // Loop over all waypoints + for (let i = 0; i < this.waypoints.length; ++i) { + const waypoint = this.waypoints[i]; + + const waypointData = this.getWaypointScreenParams(waypoint); + if (!waypointData) { + // Not relevant + continue; + } + + if (!parameters.visibleRect.containsRect(waypointData.screenBounds)) { + // Out of screen + continue; + } + + const bounds = waypointData.screenBounds; + const contentPaddingX = 7 * scale; + const isSelected = mousePos && bounds.containsPoint(mousePos.x, mousePos.y); + + // Render the background rectangle + parameters.context.globalAlpha = this.currentMarkerOpacity * (isSelected ? 1 : 0.7); + parameters.context.fillStyle = "rgba(255, 255, 255, 0.7)"; + parameters.context.fillRect(bounds.x, bounds.y, bounds.w, bounds.h); + + // Render the text + if (waypointData.item) { + const canvas = this.getWaypointCanvas(waypoint); + const itemSize = 14 * scale; + parameters.context.drawImage( + canvas, + bounds.x + contentPaddingX + 6 * scale, + bounds.y + bounds.h / 2 - itemSize / 2, + itemSize, + itemSize + ); + } else if (waypointData.text) { + // Render the text + parameters.context.fillStyle = "#000"; + parameters.context.textBaseline = "middle"; + parameters.context.fillText( + waypointData.text, + bounds.x + contentPaddingX + 6 * scale, + bounds.y + bounds.h / 2 + ); + parameters.context.textBaseline = "alphabetic"; + } else { + assertAlways(false, "Waypoint has no item and text"); + } + + // Render the small icon on the left + this.waypointSprite.drawCentered( + parameters.context, + bounds.x + contentPaddingX, + bounds.y + bounds.h / 2, + bounds.h * 0.7 + ); + } + + parameters.context.textBaseline = "alphabetic"; + parameters.context.globalAlpha = 1; + } +} diff --git a/src/js/game/hud/trailer_maker.js b/src/js/game/hud/trailer_maker.js index 8655def4..e9193a93 100644 --- a/src/js/game/hud/trailer_maker.js +++ b/src/js/game/hud/trailer_maker.js @@ -1,125 +1,122 @@ -import { GameRoot } from "../root"; -import { globalConfig } from "../../core/config"; -import { Vector, mixVector } from "../../core/vector"; -import { lerp } from "../../core/utils"; - -/* dev:start */ -import trailerPoints from "./trailer_points"; -import { gMetaBuildingRegistry } from "../../core/global_registries"; -import { MetaBeltBaseBuilding } from "../buildings/belt_base"; -import { MinerComponent } from "../components/miner"; - -const tickrate = 1 / 165; - -export class TrailerMaker { - /** - * - * @param {GameRoot} root - */ - constructor(root) { - this.root = root; - - this.markers = []; - this.playbackMarkers = null; - this.currentPlaybackOrigin = new Vector(); - this.currentPlaybackZoom = 3; - - window.addEventListener("keydown", ev => { - if (ev.key === "j") { - console.log("Record"); - this.markers.push({ - pos: this.root.camera.center.copy(), - zoom: this.root.camera.zoomLevel, - time: 1, - wait: 0, - }); - } else if (ev.key === "k") { - console.log("Export"); - const json = JSON.stringify(this.markers); - const handle = window.open("about:blank"); - handle.document.write(json); - } else if (ev.key === "u") { - if (this.playbackMarkers && this.playbackMarkers.length > 0) { - this.playbackMarkers = []; - return; - } - console.log("Playback"); - this.playbackMarkers = trailerPoints.map(p => Object.assign({}, p)); - this.playbackMarkers.unshift(this.playbackMarkers[0]); - this.currentPlaybackOrigin = Vector.fromSerializedObject(this.playbackMarkers[0].pos); - - this.currentPlaybackZoom = this.playbackMarkers[0].zoom; - this.root.camera.center = this.currentPlaybackOrigin.copy(); - this.root.camera.zoomLevel = this.currentPlaybackZoom; - console.log("STart at", this.currentPlaybackOrigin); - - // this.root.entityMgr.getAllWithComponent(MinerComponent).forEach(miner => { - // miner.components.Miner.itemChainBuffer = []; - // miner.components.Miner.lastMiningTime = this.root.time.now() + 5; - // miner.components.ItemEjector.slots.forEach(slot => (slot.item = null)); - // }); - - // this.root.logic.tryPlaceBuilding({ - // origin: new Vector(-428, -15), - // building: gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding), - // originalRotation: 0, - // rotation: 0, - // variant: "default", - // rotationVariant: 0, - // }); - - // this.root.logic.tryPlaceBuilding({ - // origin: new Vector(-427, -15), - // building: gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding), - // originalRotation: 0, - // rotation: 0, - // variant: "default", - // rotationVariant: 0, - // }); - } - }); - } - - update() { - if (this.playbackMarkers && this.playbackMarkers.length > 0) { - const nextMarker = this.playbackMarkers[0]; - - if (!nextMarker.startTime) { - console.log("Starting to approach", nextMarker.pos); - nextMarker.startTime = performance.now() / 1000.0; - } - - const speed = - globalConfig.tileSize * - globalConfig.beltSpeedItemsPerSecond * - globalConfig.itemSpacingOnBelts; - // let time = - // this.currentPlaybackOrigin.distance(Vector.fromSerializedObject(nextMarker.pos)) / speed; - const time = nextMarker.time; - - const progress = (performance.now() / 1000.0 - nextMarker.startTime) / time; - - if (progress > 1.0) { - if (nextMarker.wait > 0) { - nextMarker.wait -= tickrate; - } else { - console.log("Approached"); - this.currentPlaybackOrigin = this.root.camera.center.copy(); - this.currentPlaybackZoom = this.root.camera.zoomLevel; - this.playbackMarkers.shift(); - } - return; - } - - const targetPos = Vector.fromSerializedObject(nextMarker.pos); - const targetZoom = nextMarker.zoom; - - const pos = mixVector(this.currentPlaybackOrigin, targetPos, progress); - const zoom = lerp(this.currentPlaybackZoom, targetZoom, progress); - this.root.camera.zoomLevel = zoom; - this.root.camera.center = pos; - } - } -} - -/* dev:end */ +import { GameRoot } from "../root"; +import { globalConfig } from "../../core/config"; +import { Vector, mixVector } from "../../core/vector"; +import { lerp } from "../../core/utils"; + +/* dev:start */ +import trailerPoints from "./trailer_points"; + +const tickrate = 1 / 165; + +export class TrailerMaker { + /** + * + * @param {GameRoot} root + */ + constructor(root) { + this.root = root; + + this.markers = []; + this.playbackMarkers = null; + this.currentPlaybackOrigin = new Vector(); + this.currentPlaybackZoom = 3; + + window.addEventListener("keydown", ev => { + if (ev.key === "j") { + console.log("Record"); + this.markers.push({ + pos: this.root.camera.center.copy(), + zoom: this.root.camera.zoomLevel, + time: 1, + wait: 0, + }); + } else if (ev.key === "k") { + console.log("Export"); + const json = JSON.stringify(this.markers); + const handle = window.open("about:blank"); + handle.document.write(json); + } else if (ev.key === "u") { + if (this.playbackMarkers && this.playbackMarkers.length > 0) { + this.playbackMarkers = []; + return; + } + console.log("Playback"); + this.playbackMarkers = trailerPoints.map(p => Object.assign({}, p)); + this.playbackMarkers.unshift(this.playbackMarkers[0]); + this.currentPlaybackOrigin = Vector.fromSerializedObject(this.playbackMarkers[0].pos); + + this.currentPlaybackZoom = this.playbackMarkers[0].zoom; + this.root.camera.center = this.currentPlaybackOrigin.copy(); + this.root.camera.zoomLevel = this.currentPlaybackZoom; + console.log("STart at", this.currentPlaybackOrigin); + + // this.root.entityMgr.getAllWithComponent(MinerComponent).forEach(miner => { + // miner.components.Miner.itemChainBuffer = []; + // miner.components.Miner.lastMiningTime = this.root.time.now() + 5; + // miner.components.ItemEjector.slots.forEach(slot => (slot.item = null)); + // }); + + // this.root.logic.tryPlaceBuilding({ + // origin: new Vector(-428, -15), + // building: gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding), + // originalRotation: 0, + // rotation: 0, + // variant: "default", + // rotationVariant: 0, + // }); + + // this.root.logic.tryPlaceBuilding({ + // origin: new Vector(-427, -15), + // building: gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding), + // originalRotation: 0, + // rotation: 0, + // variant: "default", + // rotationVariant: 0, + // }); + } + }); + } + + update() { + if (this.playbackMarkers && this.playbackMarkers.length > 0) { + const nextMarker = this.playbackMarkers[0]; + + if (!nextMarker.startTime) { + console.log("Starting to approach", nextMarker.pos); + nextMarker.startTime = performance.now() / 1000.0; + } + + const speed = + globalConfig.tileSize * + globalConfig.beltSpeedItemsPerSecond * + globalConfig.itemSpacingOnBelts; + // let time = + // this.currentPlaybackOrigin.distance(Vector.fromSerializedObject(nextMarker.pos)) / speed; + const time = nextMarker.time; + + const progress = (performance.now() / 1000.0 - nextMarker.startTime) / time; + + if (progress > 1.0) { + if (nextMarker.wait > 0) { + nextMarker.wait -= tickrate; + } else { + console.log("Approached"); + this.currentPlaybackOrigin = this.root.camera.center.copy(); + this.currentPlaybackZoom = this.root.camera.zoomLevel; + this.playbackMarkers.shift(); + } + return; + } + + const targetPos = Vector.fromSerializedObject(nextMarker.pos); + const targetZoom = nextMarker.zoom; + + const pos = mixVector(this.currentPlaybackOrigin, targetPos, progress); + const zoom = lerp(this.currentPlaybackZoom, targetZoom, progress); + this.root.camera.zoomLevel = zoom; + this.root.camera.center = pos; + } + } +} + +/* dev:end */ diff --git a/src/js/game/items/color_item.js b/src/js/game/items/color_item.js index 19d26286..02104282 100644 --- a/src/js/game/items/color_item.js +++ b/src/js/game/items/color_item.js @@ -1,122 +1,73 @@ -import { globalConfig } from "../../core/config"; -import { smoothenDpi } from "../../core/dpi_manager"; -import { DrawParameters } from "../../core/draw_parameters"; -import { types } from "../../savegame/serialization"; -import { BaseItem } from "../base_item"; -import { enumColors, enumColorsToHexCode } from "../colors"; -import { THEME } from "../theme"; -import { drawSpriteClipped } from "../../core/draw_utils"; - -export class ColorItem extends BaseItem { - static getId() { - return "color"; - } - - static getSchema() { - return types.enum(enumColors); - } - - serialize() { - return this.color; - } - - deserialize(data) { - this.color = data; - } - - /** @returns {"color"} **/ - getItemType() { - return "color"; - } - - /** - * @param {BaseItem} other - */ - equalsImpl(other) { - return this.color === /** @type {ColorItem} */ (other).color; - } - - /** - * @param {enumColors} color - */ - constructor(color) { - super(); - this.color = color; - this.bufferGenerator = null; - } - - getBackgroundColorAsResource() { - return THEME.map.resources[this.color]; - } - - /** - * @param {number} x - * @param {number} y - * @param {number} diameter - * @param {DrawParameters} parameters - */ - drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) { - if (!this.bufferGenerator) { - this.bufferGenerator = this.internalGenerateColorBuffer.bind(this); - } - - const realDiameter = diameter * 0.6; - const dpi = smoothenDpi(globalConfig.shapesSharpness * parameters.zoomLevel); - const key = realDiameter + "/" + dpi + "/" + this.color; - const canvas = parameters.root.buffers.getForKey({ - key: "coloritem", - subKey: key, - w: realDiameter, - h: realDiameter, - dpi, - redrawMethod: this.bufferGenerator, - }); - - drawSpriteClipped({ - parameters, - sprite: canvas, - x: x - realDiameter / 2, - y: y - realDiameter / 2, - w: realDiameter, - h: realDiameter, - originalW: realDiameter * dpi, - originalH: realDiameter * dpi, - }); - } - /** - * - * @param {HTMLCanvasElement} canvas - * @param {CanvasRenderingContext2D} context - * @param {number} w - * @param {number} h - * @param {number} dpi - */ - internalGenerateColorBuffer(canvas, context, w, h, dpi) { - context.translate((w * dpi) / 2, (h * dpi) / 2); - context.scale((dpi * w) / 12, (dpi * h) / 12); - - context.fillStyle = enumColorsToHexCode[this.color]; - context.strokeStyle = THEME.items.outline; - context.lineWidth = 2 * THEME.items.outlineWidth; - context.beginCircle(2, -1, 3); - context.stroke(); - context.fill(); - context.beginCircle(-2, -1, 3); - context.stroke(); - context.fill(); - context.beginCircle(0, 2, 3); - context.closePath(); - context.stroke(); - context.fill(); - } -} - -/** - * Singleton instances - * @type {Object} - */ -export const COLOR_ITEM_SINGLETONS = {}; - -for (const color in enumColors) { - COLOR_ITEM_SINGLETONS[color] = new ColorItem(color); -} +import { globalConfig } from "../../core/config"; +import { DrawParameters } from "../../core/draw_parameters"; +import { Loader } from "../../core/loader"; +import { types } from "../../savegame/serialization"; +import { BaseItem } from "../base_item"; +import { enumColors } from "../colors"; +import { THEME } from "../theme"; + +export class ColorItem extends BaseItem { + static getId() { + return "color"; + } + + static getSchema() { + return types.enum(enumColors); + } + + serialize() { + return this.color; + } + + deserialize(data) { + this.color = data; + } + + /** @returns {"color"} **/ + getItemType() { + return "color"; + } + + /** + * @param {BaseItem} other + */ + equalsImpl(other) { + return this.color === /** @type {ColorItem} */ (other).color; + } + + /** + * @param {enumColors} color + */ + constructor(color) { + super(); + this.color = color; + } + + getBackgroundColorAsResource() { + return THEME.map.resources[this.color]; + } + + /** + * @param {number} x + * @param {number} y + * @param {number} diameter + * @param {DrawParameters} parameters + */ + drawItemCenteredClipped(x, y, parameters, diameter = globalConfig.defaultItemDiameter) { + const realDiameter = diameter * 0.6; + if (!this.cachedSprite) { + this.cachedSprite = Loader.getSprite("sprites/colors/" + this.color + ".png"); + } + this.cachedSprite.drawCachedCentered(parameters, x, y, realDiameter); + } +} + +/** + * Singleton instances + * @type {Object} + */ +export const COLOR_ITEM_SINGLETONS = {}; + +for (const color in enumColors) { + COLOR_ITEM_SINGLETONS[color] = new ColorItem(color); +} diff --git a/src/js/game/key_action_mapper.js b/src/js/game/key_action_mapper.js index d5a758a5..074c4b84 100644 --- a/src/js/game/key_action_mapper.js +++ b/src/js/game/key_action_mapper.js @@ -45,7 +45,7 @@ export const KEYMAPPINGS = { buildings: { belt: { keyCode: key("1") }, - splitter: { keyCode: key("2") }, + balancer: { keyCode: key("2") }, underground_belt: { keyCode: key("3") }, miner: { keyCode: key("4") }, cutter: { keyCode: key("5") }, @@ -204,22 +204,20 @@ export function getStringForKeyCode(code) { case 115: return "F4"; case 116: - return "F4"; - case 117: return "F5"; - case 118: + case 117: return "F6"; - case 119: + case 118: return "F7"; - case 120: + case 119: return "F8"; - case 121: + case 120: return "F9"; - case 122: + case 121: return "F10"; - case 123: + case 122: return "F11"; - case 124: + case 123: return "F12"; case 144: diff --git a/src/js/game/map.js b/src/js/game/map.js index 5ff51ce8..a5ec8f21 100644 --- a/src/js/game/map.js +++ b/src/js/game/map.js @@ -1,236 +1,236 @@ -import { globalConfig } from "../core/config"; -import { Vector } from "../core/vector"; -import { BasicSerializableObject, types } from "../savegame/serialization"; -import { BaseItem } from "./base_item"; -import { Entity } from "./entity"; -import { MapChunkView } from "./map_chunk_view"; -import { GameRoot } from "./root"; - -export class BaseMap extends BasicSerializableObject { - static getId() { - return "Map"; - } - - static getSchema() { - return { - seed: types.uint, - }; - } - - /** - * - * @param {GameRoot} root - */ - constructor(root) { - super(); - this.root = root; - - this.seed = 0; - - /** - * Mapping of 'X|Y' to chunk - * @type {Map} */ - this.chunksById = new Map(); - } - - /** - * Returns the given chunk by index - * @param {number} chunkX - * @param {number} chunkY - */ - getChunk(chunkX, chunkY, createIfNotExistent = false) { - const chunkIdentifier = chunkX + "|" + chunkY; - let storedChunk; - - if ((storedChunk = this.chunksById.get(chunkIdentifier))) { - return storedChunk; - } - - if (createIfNotExistent) { - const instance = new MapChunkView(this.root, chunkX, chunkY); - this.chunksById.set(chunkIdentifier, instance); - return instance; - } - - return null; - } - - /** - * Gets or creates a new chunk if not existent for the given tile - * @param {number} tileX - * @param {number} tileY - * @returns {MapChunkView} - */ - getOrCreateChunkAtTile(tileX, tileY) { - const chunkX = Math.floor(tileX / globalConfig.mapChunkSize); - const chunkY = Math.floor(tileY / globalConfig.mapChunkSize); - return this.getChunk(chunkX, chunkY, true); - } - - /** - * Gets a chunk if not existent for the given tile - * @param {number} tileX - * @param {number} tileY - * @returns {MapChunkView?} - */ - getChunkAtTileOrNull(tileX, tileY) { - const chunkX = Math.floor(tileX / globalConfig.mapChunkSize); - const chunkY = Math.floor(tileY / globalConfig.mapChunkSize); - return this.getChunk(chunkX, chunkY, false); - } - - /** - * Checks if a given tile is within the map bounds - * @param {Vector} tile - * @returns {boolean} - */ - isValidTile(tile) { - if (G_IS_DEV) { - assert(tile instanceof Vector, "tile is not a vector"); - } - return Number.isInteger(tile.x) && Number.isInteger(tile.y); - } - - /** - * Returns the tile content of a given tile - * @param {Vector} tile - * @param {Layer} layer - * @returns {Entity} Entity or null - */ - getTileContent(tile, layer) { - if (G_IS_DEV) { - this.internalCheckTile(tile); - } - const chunk = this.getChunkAtTileOrNull(tile.x, tile.y); - return chunk && chunk.getLayerContentFromWorldCoords(tile.x, tile.y, layer); - } - - /** - * Returns the lower layers content of the given tile - * @param {number} x - * @param {number} y - * @returns {BaseItem=} - */ - getLowerLayerContentXY(x, y) { - return this.getOrCreateChunkAtTile(x, y).getLowerLayerFromWorldCoords(x, y); - } - - /** - * Returns the tile content of a given tile - * @param {number} x - * @param {number} y - * @param {Layer} layer - * @returns {Entity} Entity or null - */ - getLayerContentXY(x, y, layer) { - const chunk = this.getChunkAtTileOrNull(x, y); - return chunk && chunk.getLayerContentFromWorldCoords(x, y, layer); - } - - /** - * Returns the tile contents of a given tile - * @param {number} x - * @param {number} y - * @returns {Array} Entity or null - */ - getLayersContentsMultipleXY(x, y) { - const chunk = this.getChunkAtTileOrNull(x, y); - if (!chunk) { - return []; - } - return chunk.getLayersContentsMultipleFromWorldCoords(x, y); - } - - /** - * Checks if the tile is used - * @param {Vector} tile - * @param {Layer} layer - * @returns {boolean} - */ - isTileUsed(tile, layer) { - if (G_IS_DEV) { - this.internalCheckTile(tile); - } - const chunk = this.getChunkAtTileOrNull(tile.x, tile.y); - return chunk && chunk.getLayerContentFromWorldCoords(tile.x, tile.y, layer) != null; - } - - /** - * Checks if the tile is used - * @param {number} x - * @param {number} y - * @param {Layer} layer - * @returns {boolean} - */ - isTileUsedXY(x, y, layer) { - const chunk = this.getChunkAtTileOrNull(x, y); - return chunk && chunk.getLayerContentFromWorldCoords(x, y, layer) != null; - } - - /** - * Sets the tiles content - * @param {Vector} tile - * @param {Entity} entity - */ - setTileContent(tile, entity) { - if (G_IS_DEV) { - this.internalCheckTile(tile); - } - - this.getOrCreateChunkAtTile(tile.x, tile.y).setLayerContentFromWorldCords( - tile.x, - tile.y, - entity, - entity.layer - ); - - const staticComponent = entity.components.StaticMapEntity; - assert(staticComponent, "Can only place static map entities in tiles"); - } - - /** - * Places an entity with the StaticMapEntity component - * @param {Entity} entity - */ - placeStaticEntity(entity) { - assert(entity.components.StaticMapEntity, "Entity is not static"); - const staticComp = entity.components.StaticMapEntity; - const rect = staticComp.getTileSpaceBounds(); - for (let dx = 0; dx < rect.w; ++dx) { - for (let dy = 0; dy < rect.h; ++dy) { - const x = rect.x + dx; - const y = rect.y + dy; - this.getOrCreateChunkAtTile(x, y).setLayerContentFromWorldCords(x, y, entity, entity.layer); - } - } - } - - /** - * Removes an entity with the StaticMapEntity component - * @param {Entity} entity - */ - removeStaticEntity(entity) { - assert(entity.components.StaticMapEntity, "Entity is not static"); - const staticComp = entity.components.StaticMapEntity; - const rect = staticComp.getTileSpaceBounds(); - for (let dx = 0; dx < rect.w; ++dx) { - for (let dy = 0; dy < rect.h; ++dy) { - const x = rect.x + dx; - const y = rect.y + dy; - this.getOrCreateChunkAtTile(x, y).setLayerContentFromWorldCords(x, y, null, entity.layer); - } - } - } - - // Internal - - /** - * Checks a given tile for validty - * @param {Vector} tile - */ - internalCheckTile(tile) { - assert(tile instanceof Vector, "tile is not a vector: " + tile); - assert(tile.x % 1 === 0, "Tile X is not a valid integer: " + tile.x); - assert(tile.y % 1 === 0, "Tile Y is not a valid integer: " + tile.y); - } -} +import { globalConfig } from "../core/config"; +import { Vector } from "../core/vector"; +import { BasicSerializableObject, types } from "../savegame/serialization"; +import { BaseItem } from "./base_item"; +import { Entity } from "./entity"; +import { MapChunkView } from "./map_chunk_view"; +import { GameRoot } from "./root"; + +export class BaseMap extends BasicSerializableObject { + static getId() { + return "Map"; + } + + static getSchema() { + return { + seed: types.uint, + }; + } + + /** + * + * @param {GameRoot} root + */ + constructor(root) { + super(); + this.root = root; + + this.seed = 0; + + /** + * Mapping of 'X|Y' to chunk + * @type {Map} */ + this.chunksById = new Map(); + } + + /** + * Returns the given chunk by index + * @param {number} chunkX + * @param {number} chunkY + */ + getChunk(chunkX, chunkY, createIfNotExistent = false) { + const chunkIdentifier = chunkX + "|" + chunkY; + let storedChunk; + + if ((storedChunk = this.chunksById.get(chunkIdentifier))) { + return storedChunk; + } + + if (createIfNotExistent) { + const instance = new MapChunkView(this.root, chunkX, chunkY); + this.chunksById.set(chunkIdentifier, instance); + return instance; + } + + return null; + } + + /** + * Gets or creates a new chunk if not existent for the given tile + * @param {number} tileX + * @param {number} tileY + * @returns {MapChunkView} + */ + getOrCreateChunkAtTile(tileX, tileY) { + const chunkX = Math.floor(tileX / globalConfig.mapChunkSize); + const chunkY = Math.floor(tileY / globalConfig.mapChunkSize); + return this.getChunk(chunkX, chunkY, true); + } + + /** + * Gets a chunk if not existent for the given tile + * @param {number} tileX + * @param {number} tileY + * @returns {MapChunkView?} + */ + getChunkAtTileOrNull(tileX, tileY) { + const chunkX = Math.floor(tileX / globalConfig.mapChunkSize); + const chunkY = Math.floor(tileY / globalConfig.mapChunkSize); + return this.getChunk(chunkX, chunkY, false); + } + + /** + * Checks if a given tile is within the map bounds + * @param {Vector} tile + * @returns {boolean} + */ + isValidTile(tile) { + if (G_IS_DEV) { + assert(tile instanceof Vector, "tile is not a vector"); + } + return Number.isInteger(tile.x) && Number.isInteger(tile.y); + } + + /** + * Returns the tile content of a given tile + * @param {Vector} tile + * @param {Layer} layer + * @returns {Entity} Entity or null + */ + getTileContent(tile, layer) { + if (G_IS_DEV) { + this.internalCheckTile(tile); + } + const chunk = this.getChunkAtTileOrNull(tile.x, tile.y); + return chunk && chunk.getLayerContentFromWorldCoords(tile.x, tile.y, layer); + } + + /** + * Returns the lower layers content of the given tile + * @param {number} x + * @param {number} y + * @returns {BaseItem=} + */ + getLowerLayerContentXY(x, y) { + return this.getOrCreateChunkAtTile(x, y).getLowerLayerFromWorldCoords(x, y); + } + + /** + * Returns the tile content of a given tile + * @param {number} x + * @param {number} y + * @param {Layer} layer + * @returns {Entity} Entity or null + */ + getLayerContentXY(x, y, layer) { + const chunk = this.getChunkAtTileOrNull(x, y); + return chunk && chunk.getLayerContentFromWorldCoords(x, y, layer); + } + + /** + * Returns the tile contents of a given tile + * @param {number} x + * @param {number} y + * @returns {Array} Entity or null + */ + getLayersContentsMultipleXY(x, y) { + const chunk = this.getChunkAtTileOrNull(x, y); + if (!chunk) { + return []; + } + return chunk.getLayersContentsMultipleFromWorldCoords(x, y); + } + + /** + * Checks if the tile is used + * @param {Vector} tile + * @param {Layer} layer + * @returns {boolean} + */ + isTileUsed(tile, layer) { + if (G_IS_DEV) { + this.internalCheckTile(tile); + } + const chunk = this.getChunkAtTileOrNull(tile.x, tile.y); + return chunk && chunk.getLayerContentFromWorldCoords(tile.x, tile.y, layer) != null; + } + + /** + * Checks if the tile is used + * @param {number} x + * @param {number} y + * @param {Layer} layer + * @returns {boolean} + */ + isTileUsedXY(x, y, layer) { + const chunk = this.getChunkAtTileOrNull(x, y); + return chunk && chunk.getLayerContentFromWorldCoords(x, y, layer) != null; + } + + /** + * Sets the tiles content + * @param {Vector} tile + * @param {Entity} entity + */ + setTileContent(tile, entity) { + if (G_IS_DEV) { + this.internalCheckTile(tile); + } + + this.getOrCreateChunkAtTile(tile.x, tile.y).setLayerContentFromWorldCords( + tile.x, + tile.y, + entity, + entity.layer + ); + + const staticComponent = entity.components.StaticMapEntity; + assert(staticComponent, "Can only place static map entities in tiles"); + } + + /** + * Places an entity with the StaticMapEntity component + * @param {Entity} entity + */ + placeStaticEntity(entity) { + assert(entity.components.StaticMapEntity, "Entity is not static"); + const staticComp = entity.components.StaticMapEntity; + const rect = staticComp.getTileSpaceBounds(); + for (let dx = 0; dx < rect.w; ++dx) { + for (let dy = 0; dy < rect.h; ++dy) { + const x = rect.x + dx; + const y = rect.y + dy; + this.getOrCreateChunkAtTile(x, y).setLayerContentFromWorldCords(x, y, entity, entity.layer); + } + } + } + + /** + * Removes an entity with the StaticMapEntity component + * @param {Entity} entity + */ + removeStaticEntity(entity) { + assert(entity.components.StaticMapEntity, "Entity is not static"); + const staticComp = entity.components.StaticMapEntity; + const rect = staticComp.getTileSpaceBounds(); + for (let dx = 0; dx < rect.w; ++dx) { + for (let dy = 0; dy < rect.h; ++dy) { + const x = rect.x + dx; + const y = rect.y + dy; + this.getOrCreateChunkAtTile(x, y).setLayerContentFromWorldCords(x, y, null, entity.layer); + } + } + } + + // Internal + + /** + * Checks a given tile for validty + * @param {Vector} tile + */ + internalCheckTile(tile) { + assert(tile instanceof Vector, "tile is not a vector: " + tile); + assert(tile.x % 1 === 0, "Tile X is not a valid integer: " + tile.x); + assert(tile.y % 1 === 0, "Tile Y is not a valid integer: " + tile.y); + } +} diff --git a/src/js/game/map_chunk_view.js b/src/js/game/map_chunk_view.js index 7d74e224..58730f90 100644 --- a/src/js/game/map_chunk_view.js +++ b/src/js/game/map_chunk_view.js @@ -88,16 +88,17 @@ export class MapChunkView extends MapChunk { }); const dims = globalConfig.mapChunkWorldSize; + const extrude = 0.05; // Draw chunk "pixel" art parameters.context.imageSmoothingEnabled = false; drawSpriteClipped({ parameters, sprite, - x: this.x * dims, - y: this.y * dims, - w: dims, - h: dims, + x: this.x * dims - extrude, + y: this.y * dims - extrude, + w: dims + 2 * extrude, + h: dims + 2 * extrude, originalW: overlaySize, originalH: overlaySize, }); @@ -108,12 +109,12 @@ export class MapChunkView extends MapChunk { if (this.root.currentLayer === "regular") { for (let i = 0; i < this.patches.length; ++i) { const patch = this.patches[i]; - - const destX = this.x * dims + patch.pos.x * globalConfig.tileSize; - const destY = this.y * dims + patch.pos.y * globalConfig.tileSize; - const diameter = Math.min(80, 30 / parameters.zoomLevel); - - patch.item.drawItemCenteredClipped(destX, destY, parameters, diameter); + if (patch.item.getItemType() === "shape") { + const destX = this.x * dims + patch.pos.x * globalConfig.tileSize; + const destY = this.y * dims + patch.pos.y * globalConfig.tileSize; + const diameter = 80 / Math.pow(parameters.zoomLevel, 0.35); + patch.item.drawItemCenteredClipped(destX, destY, parameters, diameter); + } } } } diff --git a/src/js/game/map_view.js b/src/js/game/map_view.js index 0e0f3d5b..0f2ceb89 100644 --- a/src/js/game/map_view.js +++ b/src/js/game/map_view.js @@ -66,32 +66,34 @@ export class MapView extends BaseMap { * @param {DrawParameters} drawParameters */ drawStaticEntityDebugOverlays(drawParameters) { - const cullRange = drawParameters.visibleRect.toTileCullRectangle(); - const top = cullRange.top(); - const right = cullRange.right(); - const bottom = cullRange.bottom(); - const left = cullRange.left(); + if (G_IS_DEV && (globalConfig.debug.showAcceptorEjectors || globalConfig.debug.showEntityBounds)) { + const cullRange = drawParameters.visibleRect.toTileCullRectangle(); + const top = cullRange.top(); + const right = cullRange.right(); + const bottom = cullRange.bottom(); + const left = cullRange.left(); - const border = 1; + const border = 1; - const minY = top - border; - const maxY = bottom + border; - const minX = left - border; - const maxX = right + border - 1; + const minY = top - border; + const maxY = bottom + border; + const minX = left - border; + const maxX = right + border - 1; - // Render y from top down for proper blending - for (let y = minY; y <= maxY; ++y) { - for (let x = minX; x <= maxX; ++x) { - // const content = this.tiles[x][y]; - const chunk = this.getChunkAtTileOrNull(x, y); - if (!chunk) { - continue; - } - const content = chunk.getTileContentFromWorldCoords(x, y); - if (content) { - let isBorder = x <= left - 1 || x >= right + 1 || y <= top - 1 || y >= bottom + 1; - if (!isBorder) { - content.drawDebugOverlays(drawParameters); + // Render y from top down for proper blending + for (let y = minY; y <= maxY; ++y) { + for (let x = minX; x <= maxX; ++x) { + // const content = this.tiles[x][y]; + const chunk = this.getChunkAtTileOrNull(x, y); + if (!chunk) { + continue; + } + const content = chunk.getTileContentFromWorldCoords(x, y); + if (content) { + let isBorder = x <= left - 1 || x >= right + 1 || y <= top - 1 || y >= bottom + 1; + if (!isBorder) { + content.drawDebugOverlays(drawParameters); + } } } } diff --git a/src/js/game/meta_building_registry.js b/src/js/game/meta_building_registry.js index 647e55f5..4b7095df 100644 --- a/src/js/game/meta_building_registry.js +++ b/src/js/game/meta_building_registry.js @@ -1,19 +1,18 @@ import { gMetaBuildingRegistry } from "../core/global_registries"; import { createLogger } from "../core/logging"; import { MetaBeltBuilding } from "./buildings/belt"; -import { MetaBeltBaseBuilding } from "./buildings/belt_base"; import { enumCutterVariants, MetaCutterBuilding } from "./buildings/cutter"; import { MetaHubBuilding } from "./buildings/hub"; import { enumMinerVariants, MetaMinerBuilding } from "./buildings/miner"; import { MetaMixerBuilding } from "./buildings/mixer"; import { enumPainterVariants, MetaPainterBuilding } from "./buildings/painter"; import { enumRotaterVariants, MetaRotaterBuilding } from "./buildings/rotater"; -import { enumSplitterVariants, MetaSplitterBuilding } from "./buildings/splitter"; +import { enumBalancerVariants, MetaBalancerBuilding } from "./buildings/balancer"; import { MetaStackerBuilding } from "./buildings/stacker"; import { enumTrashVariants, MetaTrashBuilding } from "./buildings/trash"; import { enumUndergroundBeltVariants, MetaUndergroundBeltBuilding } from "./buildings/underground_belt"; import { MetaWireBuilding } from "./buildings/wire"; -import { gBuildingVariants, registerBuildingVariant } from "./building_codes"; +import { buildBuildingCodeCache, gBuildingVariants, registerBuildingVariant } from "./building_codes"; import { defaultBuildingVariant } from "./meta_building"; import { MetaConstantSignalBuilding } from "./buildings/constant_signal"; import { MetaLogicGateBuilding, enumLogicGateVariants } from "./buildings/logic_gate"; @@ -27,7 +26,7 @@ import { MetaReaderBuilding } from "./buildings/reader"; const logger = createLogger("building_registry"); export function initMetaBuildingRegistry() { - gMetaBuildingRegistry.register(MetaSplitterBuilding); + gMetaBuildingRegistry.register(MetaBalancerBuilding); gMetaBuildingRegistry.register(MetaMinerBuilding); gMetaBuildingRegistry.register(MetaCutterBuilding); gMetaBuildingRegistry.register(MetaRotaterBuilding); @@ -49,16 +48,16 @@ export function initMetaBuildingRegistry() { gMetaBuildingRegistry.register(MetaReaderBuilding); // Belt - registerBuildingVariant(1, MetaBeltBaseBuilding, defaultBuildingVariant, 0); - registerBuildingVariant(2, MetaBeltBaseBuilding, defaultBuildingVariant, 1); - registerBuildingVariant(3, MetaBeltBaseBuilding, defaultBuildingVariant, 2); + registerBuildingVariant(1, MetaBeltBuilding, defaultBuildingVariant, 0); + registerBuildingVariant(2, MetaBeltBuilding, defaultBuildingVariant, 1); + registerBuildingVariant(3, MetaBeltBuilding, defaultBuildingVariant, 2); - // Splitter - registerBuildingVariant(4, MetaSplitterBuilding); - registerBuildingVariant(5, MetaSplitterBuilding, enumSplitterVariants.compact); - registerBuildingVariant(6, MetaSplitterBuilding, enumSplitterVariants.compactInverse); - registerBuildingVariant(47, MetaSplitterBuilding, enumSplitterVariants.compactMerge); - registerBuildingVariant(48, MetaSplitterBuilding, enumSplitterVariants.compactMergeInverse); + // Balancer + registerBuildingVariant(4, MetaBalancerBuilding); + registerBuildingVariant(5, MetaBalancerBuilding, enumBalancerVariants.merger); + registerBuildingVariant(6, MetaBalancerBuilding, enumBalancerVariants.mergerInverse); + registerBuildingVariant(47, MetaBalancerBuilding, enumBalancerVariants.splitter); + registerBuildingVariant(48, MetaBalancerBuilding, enumBalancerVariants.splitterInverse); // Miner registerBuildingVariant(7, MetaMinerBuilding); @@ -71,7 +70,7 @@ export function initMetaBuildingRegistry() { // Rotater registerBuildingVariant(11, MetaRotaterBuilding); registerBuildingVariant(12, MetaRotaterBuilding, enumRotaterVariants.ccw); - registerBuildingVariant(13, MetaRotaterBuilding, enumRotaterVariants.fl); + registerBuildingVariant(13, MetaRotaterBuilding, enumRotaterVariants.rotate180); // Stacker registerBuildingVariant(14, MetaStackerBuilding); @@ -133,6 +132,8 @@ export function initMetaBuildingRegistry() { registerBuildingVariant(44, MetaVirtualProcessorBuilding, enumVirtualProcessorVariants.rotater); registerBuildingVariant(45, MetaVirtualProcessorBuilding, enumVirtualProcessorVariants.unstacker); registerBuildingVariant(46, MetaVirtualProcessorBuilding, enumVirtualProcessorVariants.shapecompare); + registerBuildingVariant(50, MetaVirtualProcessorBuilding, enumVirtualProcessorVariants.stacker); + registerBuildingVariant(51, MetaVirtualProcessorBuilding, enumVirtualProcessorVariants.painter); // Reader registerBuildingVariant(49, MetaReaderBuilding); @@ -175,4 +176,7 @@ export function initBuildingCodesAfterResourcesLoaded() { ); variant.silhouetteColor = variant.metaInstance.getSilhouetteColor(); } + + // Update caches + buildBuildingCodeCache(); } diff --git a/src/js/game/shape_definition.js b/src/js/game/shape_definition.js index 65b72a1a..9060a1b5 100644 --- a/src/js/game/shape_definition.js +++ b/src/js/game/shape_definition.js @@ -487,10 +487,10 @@ export class ShapeDefinition extends BasicSerializableObject { } /** - * Returns a definition which was rotated 180 degrees (flipped) + * Returns a definition which was rotated 180 degrees * @returns {ShapeDefinition} */ - cloneRotateFL() { + cloneRotate180() { const newLayers = this.internalCloneLayers(); for (let layerIndex = 0; layerIndex < newLayers.length; ++layerIndex) { const quadrants = newLayers[layerIndex]; diff --git a/src/js/game/shape_definition_manager.js b/src/js/game/shape_definition_manager.js index ef0d592f..86723fcd 100644 --- a/src/js/game/shape_definition_manager.js +++ b/src/js/game/shape_definition_manager.js @@ -1,259 +1,259 @@ -import { createLogger } from "../core/logging"; -import { BasicSerializableObject } from "../savegame/serialization"; -import { enumColors } from "./colors"; -import { ShapeItem } from "./items/shape_item"; -import { GameRoot } from "./root"; -import { enumSubShape, ShapeDefinition } from "./shape_definition"; - -const logger = createLogger("shape_definition_manager"); - -export class ShapeDefinitionManager extends BasicSerializableObject { - static getId() { - return "ShapeDefinitionManager"; - } - - /** - * - * @param {GameRoot} root - */ - constructor(root) { - super(); - this.root = root; - - /** - * Store a cache from key -> definition - * @type {Object} - */ - this.shapeKeyToDefinition = {}; - - /** - * Store a cache from key -> item - */ - this.shapeKeyToItem = {}; - - // Caches operations in the form of 'operation:def1[:def2]' - /** @type {Object.|ShapeDefinition>} */ - this.operationCache = {}; - } - - /** - * Returns a shape instance from a given short key - * @param {string} hash - * @returns {ShapeDefinition} - */ - getShapeFromShortKey(hash) { - const cached = this.shapeKeyToDefinition[hash]; - if (cached) { - return cached; - } - return (this.shapeKeyToDefinition[hash] = ShapeDefinition.fromShortKey(hash)); - } - - /** - * Returns a item instance from a given short key - * @param {string} hash - * @returns {ShapeItem} - */ - getShapeItemFromShortKey(hash) { - const cached = this.shapeKeyToItem[hash]; - if (cached) { - return cached; - } - const definition = this.getShapeFromShortKey(hash); - return (this.shapeKeyToItem[hash] = new ShapeItem(definition)); - } - - /** - * Returns a shape item for a given definition - * @param {ShapeDefinition} definition - * @returns {ShapeItem} - */ - getShapeItemFromDefinition(definition) { - return this.getShapeItemFromShortKey(definition.getHash()); - } - - /** - * Registers a new shape definition - * @param {ShapeDefinition} definition - */ - registerShapeDefinition(definition) { - const id = definition.getHash(); - assert(!this.shapeKeyToDefinition[id], "Shape Definition " + id + " already exists"); - this.shapeKeyToDefinition[id] = definition; - // logger.log("Registered shape with key", id); - } - - /** - * Generates a definition for splitting a shape definition in two halfs - * @param {ShapeDefinition} definition - * @returns {[ShapeDefinition, ShapeDefinition]} - */ - shapeActionCutHalf(definition) { - const key = "cut:" + definition.getHash(); - if (this.operationCache[key]) { - return /** @type {[ShapeDefinition, ShapeDefinition]} */ (this.operationCache[key]); - } - const rightSide = definition.cloneFilteredByQuadrants([2, 3]); - const leftSide = definition.cloneFilteredByQuadrants([0, 1]); - - return /** @type {[ShapeDefinition, ShapeDefinition]} */ (this.operationCache[key] = [ - this.registerOrReturnHandle(rightSide), - this.registerOrReturnHandle(leftSide), - ]); - } - - /** - * Generates a definition for splitting a shape definition in four quads - * @param {ShapeDefinition} definition - * @returns {[ShapeDefinition, ShapeDefinition, ShapeDefinition, ShapeDefinition]} - */ - shapeActionCutQuad(definition) { - const key = "cut-quad:" + definition.getHash(); - if (this.operationCache[key]) { - return /** @type {[ShapeDefinition, ShapeDefinition, ShapeDefinition, ShapeDefinition]} */ (this - .operationCache[key]); - } - - return /** @type {[ShapeDefinition, ShapeDefinition, ShapeDefinition, ShapeDefinition]} */ (this.operationCache[ - key - ] = [ - this.registerOrReturnHandle(definition.cloneFilteredByQuadrants([0])), - this.registerOrReturnHandle(definition.cloneFilteredByQuadrants([1])), - this.registerOrReturnHandle(definition.cloneFilteredByQuadrants([2])), - this.registerOrReturnHandle(definition.cloneFilteredByQuadrants([3])), - ]); - } - - /** - * Generates a definition for rotating a shape clockwise - * @param {ShapeDefinition} definition - * @returns {ShapeDefinition} - */ - shapeActionRotateCW(definition) { - const key = "rotate-cw:" + definition.getHash(); - if (this.operationCache[key]) { - return /** @type {ShapeDefinition} */ (this.operationCache[key]); - } - - const rotated = definition.cloneRotateCW(); - - return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( - rotated - )); - } - - /** - * Generates a definition for rotating a shape counter clockwise - * @param {ShapeDefinition} definition - * @returns {ShapeDefinition} - */ - shapeActionRotateCCW(definition) { - const key = "rotate-ccw:" + definition.getHash(); - if (this.operationCache[key]) { - return /** @type {ShapeDefinition} */ (this.operationCache[key]); - } - - const rotated = definition.cloneRotateCCW(); - - return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( - rotated - )); - } - - /** - * Generates a definition for rotating a shape counter clockwise - * @param {ShapeDefinition} definition - * @returns {ShapeDefinition} - */ - shapeActionRotateFL(definition) { - const key = "rotate-fl:" + definition.getHash(); - if (this.operationCache[key]) { - return /** @type {ShapeDefinition} */ (this.operationCache[key]); - } - - const rotated = definition.cloneRotateFL(); - - return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( - rotated - )); - } - - /** - * Generates a definition for stacking the upper definition onto the lower one - * @param {ShapeDefinition} lowerDefinition - * @param {ShapeDefinition} upperDefinition - * @returns {ShapeDefinition} - */ - shapeActionStack(lowerDefinition, upperDefinition) { - const key = "stack:" + lowerDefinition.getHash() + ":" + upperDefinition.getHash(); - if (this.operationCache[key]) { - return /** @type {ShapeDefinition} */ (this.operationCache[key]); - } - const stacked = lowerDefinition.cloneAndStackWith(upperDefinition); - return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( - stacked - )); - } - - /** - * Generates a definition for painting it with the given color - * @param {ShapeDefinition} definition - * @param {enumColors} color - * @returns {ShapeDefinition} - */ - shapeActionPaintWith(definition, color) { - const key = "paint:" + definition.getHash() + ":" + color; - if (this.operationCache[key]) { - return /** @type {ShapeDefinition} */ (this.operationCache[key]); - } - const colorized = definition.cloneAndPaintWith(color); - return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( - colorized - )); - } - - /** - * Generates a definition for painting it with the 4 colors - * @param {ShapeDefinition} definition - * @param {[enumColors, enumColors, enumColors, enumColors]} colors - * @returns {ShapeDefinition} - */ - shapeActionPaintWith4Colors(definition, colors) { - const key = "paint4:" + definition.getHash() + ":" + colors.join(","); - if (this.operationCache[key]) { - return /** @type {ShapeDefinition} */ (this.operationCache[key]); - } - const colorized = definition.cloneAndPaintWith4Colors(colors); - return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( - colorized - )); - } - - /** - * Checks if we already have cached this definition, and if so throws it away and returns the already - * cached variant - * @param {ShapeDefinition} definition - */ - registerOrReturnHandle(definition) { - const id = definition.getHash(); - if (this.shapeKeyToDefinition[id]) { - return this.shapeKeyToDefinition[id]; - } - this.shapeKeyToDefinition[id] = definition; - // logger.log("Registered shape with key (2)", id); - return definition; - } - - /** - * - * @param {[enumSubShape, enumSubShape, enumSubShape, enumSubShape]} subShapes - * @returns {ShapeDefinition} - */ - getDefinitionFromSimpleShapes(subShapes, color = enumColors.uncolored) { - const shapeLayer = /** @type {import("./shape_definition").ShapeLayer} */ (subShapes.map( - subShape => ({ subShape, color }) - )); - - return this.registerOrReturnHandle(new ShapeDefinition({ layers: [shapeLayer] })); - } -} +import { createLogger } from "../core/logging"; +import { BasicSerializableObject } from "../savegame/serialization"; +import { enumColors } from "./colors"; +import { ShapeItem } from "./items/shape_item"; +import { GameRoot } from "./root"; +import { enumSubShape, ShapeDefinition } from "./shape_definition"; + +const logger = createLogger("shape_definition_manager"); + +export class ShapeDefinitionManager extends BasicSerializableObject { + static getId() { + return "ShapeDefinitionManager"; + } + + /** + * + * @param {GameRoot} root + */ + constructor(root) { + super(); + this.root = root; + + /** + * Store a cache from key -> definition + * @type {Object} + */ + this.shapeKeyToDefinition = {}; + + /** + * Store a cache from key -> item + */ + this.shapeKeyToItem = {}; + + // Caches operations in the form of 'operation:def1[:def2]' + /** @type {Object.|ShapeDefinition>} */ + this.operationCache = {}; + } + + /** + * Returns a shape instance from a given short key + * @param {string} hash + * @returns {ShapeDefinition} + */ + getShapeFromShortKey(hash) { + const cached = this.shapeKeyToDefinition[hash]; + if (cached) { + return cached; + } + return (this.shapeKeyToDefinition[hash] = ShapeDefinition.fromShortKey(hash)); + } + + /** + * Returns a item instance from a given short key + * @param {string} hash + * @returns {ShapeItem} + */ + getShapeItemFromShortKey(hash) { + const cached = this.shapeKeyToItem[hash]; + if (cached) { + return cached; + } + const definition = this.getShapeFromShortKey(hash); + return (this.shapeKeyToItem[hash] = new ShapeItem(definition)); + } + + /** + * Returns a shape item for a given definition + * @param {ShapeDefinition} definition + * @returns {ShapeItem} + */ + getShapeItemFromDefinition(definition) { + return this.getShapeItemFromShortKey(definition.getHash()); + } + + /** + * Registers a new shape definition + * @param {ShapeDefinition} definition + */ + registerShapeDefinition(definition) { + const id = definition.getHash(); + assert(!this.shapeKeyToDefinition[id], "Shape Definition " + id + " already exists"); + this.shapeKeyToDefinition[id] = definition; + // logger.log("Registered shape with key", id); + } + + /** + * Generates a definition for splitting a shape definition in two halfs + * @param {ShapeDefinition} definition + * @returns {[ShapeDefinition, ShapeDefinition]} + */ + shapeActionCutHalf(definition) { + const key = "cut:" + definition.getHash(); + if (this.operationCache[key]) { + return /** @type {[ShapeDefinition, ShapeDefinition]} */ (this.operationCache[key]); + } + const rightSide = definition.cloneFilteredByQuadrants([2, 3]); + const leftSide = definition.cloneFilteredByQuadrants([0, 1]); + + return /** @type {[ShapeDefinition, ShapeDefinition]} */ (this.operationCache[key] = [ + this.registerOrReturnHandle(rightSide), + this.registerOrReturnHandle(leftSide), + ]); + } + + /** + * Generates a definition for splitting a shape definition in four quads + * @param {ShapeDefinition} definition + * @returns {[ShapeDefinition, ShapeDefinition, ShapeDefinition, ShapeDefinition]} + */ + shapeActionCutQuad(definition) { + const key = "cut-quad:" + definition.getHash(); + if (this.operationCache[key]) { + return /** @type {[ShapeDefinition, ShapeDefinition, ShapeDefinition, ShapeDefinition]} */ (this + .operationCache[key]); + } + + return /** @type {[ShapeDefinition, ShapeDefinition, ShapeDefinition, ShapeDefinition]} */ (this.operationCache[ + key + ] = [ + this.registerOrReturnHandle(definition.cloneFilteredByQuadrants([0])), + this.registerOrReturnHandle(definition.cloneFilteredByQuadrants([1])), + this.registerOrReturnHandle(definition.cloneFilteredByQuadrants([2])), + this.registerOrReturnHandle(definition.cloneFilteredByQuadrants([3])), + ]); + } + + /** + * Generates a definition for rotating a shape clockwise + * @param {ShapeDefinition} definition + * @returns {ShapeDefinition} + */ + shapeActionRotateCW(definition) { + const key = "rotate-cw:" + definition.getHash(); + if (this.operationCache[key]) { + return /** @type {ShapeDefinition} */ (this.operationCache[key]); + } + + const rotated = definition.cloneRotateCW(); + + return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( + rotated + )); + } + + /** + * Generates a definition for rotating a shape counter clockwise + * @param {ShapeDefinition} definition + * @returns {ShapeDefinition} + */ + shapeActionRotateCCW(definition) { + const key = "rotate-ccw:" + definition.getHash(); + if (this.operationCache[key]) { + return /** @type {ShapeDefinition} */ (this.operationCache[key]); + } + + const rotated = definition.cloneRotateCCW(); + + return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( + rotated + )); + } + + /** + * Generates a definition for rotating a shape FL + * @param {ShapeDefinition} definition + * @returns {ShapeDefinition} + */ + shapeActionRotate180(definition) { + const key = "rotate-fl:" + definition.getHash(); + if (this.operationCache[key]) { + return /** @type {ShapeDefinition} */ (this.operationCache[key]); + } + + const rotated = definition.cloneRotate180(); + + return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( + rotated + )); + } + + /** + * Generates a definition for stacking the upper definition onto the lower one + * @param {ShapeDefinition} lowerDefinition + * @param {ShapeDefinition} upperDefinition + * @returns {ShapeDefinition} + */ + shapeActionStack(lowerDefinition, upperDefinition) { + const key = "stack:" + lowerDefinition.getHash() + ":" + upperDefinition.getHash(); + if (this.operationCache[key]) { + return /** @type {ShapeDefinition} */ (this.operationCache[key]); + } + const stacked = lowerDefinition.cloneAndStackWith(upperDefinition); + return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( + stacked + )); + } + + /** + * Generates a definition for painting it with the given color + * @param {ShapeDefinition} definition + * @param {enumColors} color + * @returns {ShapeDefinition} + */ + shapeActionPaintWith(definition, color) { + const key = "paint:" + definition.getHash() + ":" + color; + if (this.operationCache[key]) { + return /** @type {ShapeDefinition} */ (this.operationCache[key]); + } + const colorized = definition.cloneAndPaintWith(color); + return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( + colorized + )); + } + + /** + * Generates a definition for painting it with the 4 colors + * @param {ShapeDefinition} definition + * @param {[enumColors, enumColors, enumColors, enumColors]} colors + * @returns {ShapeDefinition} + */ + shapeActionPaintWith4Colors(definition, colors) { + const key = "paint4:" + definition.getHash() + ":" + colors.join(","); + if (this.operationCache[key]) { + return /** @type {ShapeDefinition} */ (this.operationCache[key]); + } + const colorized = definition.cloneAndPaintWith4Colors(colors); + return /** @type {ShapeDefinition} */ (this.operationCache[key] = this.registerOrReturnHandle( + colorized + )); + } + + /** + * Checks if we already have cached this definition, and if so throws it away and returns the already + * cached variant + * @param {ShapeDefinition} definition + */ + registerOrReturnHandle(definition) { + const id = definition.getHash(); + if (this.shapeKeyToDefinition[id]) { + return this.shapeKeyToDefinition[id]; + } + this.shapeKeyToDefinition[id] = definition; + // logger.log("Registered shape with key (2)", id); + return definition; + } + + /** + * + * @param {[enumSubShape, enumSubShape, enumSubShape, enumSubShape]} subShapes + * @returns {ShapeDefinition} + */ + getDefinitionFromSimpleShapes(subShapes, color = enumColors.uncolored) { + const shapeLayer = /** @type {import("./shape_definition").ShapeLayer} */ (subShapes.map( + subShape => ({ subShape, color }) + )); + + return this.registerOrReturnHandle(new ShapeDefinition({ layers: [shapeLayer] })); + } +} diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 4d8151f6..10543e6c 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -1,520 +1,554 @@ -import { globalConfig } from "../../core/config"; -import { DrawParameters } from "../../core/draw_parameters"; -import { gMetaBuildingRegistry } from "../../core/global_registries"; -import { Loader } from "../../core/loader"; -import { createLogger } from "../../core/logging"; -import { AtlasSprite } from "../../core/sprites"; -import { fastArrayDeleteValue } from "../../core/utils"; -import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../../core/vector"; -import { BeltPath } from "../belt_path"; -import { arrayBeltVariantToRotation, MetaBeltBaseBuilding } from "../buildings/belt_base"; -import { BeltComponent } from "../components/belt"; -import { Entity } from "../entity"; -import { GameSystemWithFilter } from "../game_system_with_filter"; -import { MapChunkView } from "../map_chunk_view"; -import { defaultBuildingVariant } from "../meta_building"; -import { getCodeFromBuildingData } from "../building_codes"; - -export const BELT_ANIM_COUNT = 14; - -const logger = createLogger("belt"); - -/** - * Manages all belts - */ -export class BeltSystem extends GameSystemWithFilter { - constructor(root) { - super(root, [BeltComponent]); - /** - * @type {Object.>} - */ - this.beltSprites = { - [enumDirection.top]: Loader.getSprite("sprites/belt/built/forward_0.png"), - [enumDirection.left]: Loader.getSprite("sprites/belt/built/left_0.png"), - [enumDirection.right]: Loader.getSprite("sprites/belt/built/right_0.png"), - }; - - /** - * @type {Object.>} - */ - this.beltAnimations = { - [enumDirection.top]: [], - [enumDirection.left]: [], - [enumDirection.right]: [], - }; - - for (let i = 0; i < BELT_ANIM_COUNT; ++i) { - this.beltAnimations[enumDirection.top].push( - Loader.getSprite("sprites/belt/built/forward_" + i + ".png") - ); - this.beltAnimations[enumDirection.left].push( - Loader.getSprite("sprites/belt/built/left_" + i + ".png") - ); - this.beltAnimations[enumDirection.right].push( - Loader.getSprite("sprites/belt/built/right_" + i + ".png") - ); - } - - this.root.signals.entityDestroyed.add(this.onEntityDestroyed, this); - this.root.signals.entityDestroyed.add(this.updateSurroundingBeltPlacement, this); - - // Notice: These must come *after* the entity destroyed signals - this.root.signals.entityAdded.add(this.onEntityAdded, this); - this.root.signals.entityAdded.add(this.updateSurroundingBeltPlacement, this); - - /** @type {Array} */ - this.beltPaths = []; - } - - /** - * Serializes all belt paths - * @returns {Array} - */ - serializePaths() { - let data = []; - for (let i = 0; i < this.beltPaths.length; ++i) { - data.push(this.beltPaths[i].serialize()); - } - return data; - } - - /** - * Deserializes all belt paths - * @param {Array} data - */ - deserializePaths(data) { - if (!Array.isArray(data)) { - return "Belt paths are not an array: " + typeof data; - } - - for (let i = 0; i < data.length; ++i) { - const path = BeltPath.fromSerialized(this.root, data[i]); - // If path is a string, that means its an error - if (!(path instanceof BeltPath)) { - return "Failed to create path from belt data: " + path; - } - this.beltPaths.push(path); - } - - if (this.beltPaths.length === 0) { - // Old savegames might not have paths yet - logger.warn("Recomputing belt paths (most likely the savegame is old or empty)"); - this.recomputeAllBeltPaths(); - } else { - logger.warn("Restored", this.beltPaths.length, "belt paths"); - } - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - } - - /** - * Updates the belt placement after an entity has been added / deleted - * @param {Entity} entity - */ - updateSurroundingBeltPlacement(entity) { - if (!this.root.gameInitialized) { - return; - } - - const staticComp = entity.components.StaticMapEntity; - if (!staticComp) { - return; - } - - const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBaseBuilding); - // Compute affected area - const originalRect = staticComp.getTileSpaceBounds(); - const affectedArea = originalRect.expandedInAllDirections(1); - - /** @type {Set} */ - const changedPaths = new Set(); - - for (let x = affectedArea.x; x < affectedArea.right(); ++x) { - for (let y = affectedArea.y; y < affectedArea.bottom(); ++y) { - if (originalRect.containsPoint(x, y)) { - // Make sure we don't update the original entity - continue; - } - - const targetEntities = this.root.map.getLayersContentsMultipleXY(x, y); - for (let i = 0; i < targetEntities.length; ++i) { - const targetEntity = targetEntities[i]; - - const targetBeltComp = targetEntity.components.Belt; - const targetStaticComp = targetEntity.components.StaticMapEntity; - - if (!targetBeltComp) { - // Not a belt - continue; - } - - const { - rotation, - rotationVariant, - } = metaBelt.computeOptimalDirectionAndRotationVariantAtTile({ - root: this.root, - tile: new Vector(x, y), - rotation: targetStaticComp.originalRotation, - variant: defaultBuildingVariant, - layer: targetEntity.layer, - }); - - // Compute delta to see if anything changed - const newDirection = arrayBeltVariantToRotation[rotationVariant]; - - if (targetStaticComp.rotation !== rotation || newDirection !== targetBeltComp.direction) { - // Ok, first remove it from its current path - this.deleteEntityFromPath(targetBeltComp.assignedPath, targetEntity); - - // Change stuff - targetStaticComp.rotation = rotation; - metaBelt.updateVariants(targetEntity, rotationVariant, defaultBuildingVariant); - - // Update code as well - targetStaticComp.code = getCodeFromBuildingData( - metaBelt, - defaultBuildingVariant, - rotationVariant - ); - - // Now add it again - this.addEntityToPaths(targetEntity); - - // Sanity - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - - // Make sure the chunks know about the update - this.root.signals.entityChanged.dispatch(targetEntity); - } - - if (targetBeltComp.assignedPath) { - changedPaths.add(targetBeltComp.assignedPath); - } - } - } - } - - // notify all paths *afterwards* to avoid multi-updates - changedPaths.forEach(path => path.onSurroundingsChanged()); - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - } - - /** - * Called when an entity got destroyed - * @param {Entity} entity - */ - onEntityDestroyed(entity) { - if (!this.root.gameInitialized) { - return; - } - - if (!entity.components.Belt) { - return; - } - - const assignedPath = entity.components.Belt.assignedPath; - assert(assignedPath, "Entity has no belt path assigned"); - this.deleteEntityFromPath(assignedPath, entity); - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - } - - /** - * Attempts to delete the belt from its current path - * @param {BeltPath} path - * @param {Entity} entity - */ - deleteEntityFromPath(path, entity) { - if (path.entityPath.length === 1) { - // This is a single entity path, easy to do, simply erase whole path - fastArrayDeleteValue(this.beltPaths, path); - return; - } - - // Notice: Since there might be circular references, it is important to check - // which role the entity has - if (path.isStartEntity(entity)) { - // We tried to delete the start - path.deleteEntityOnStart(entity); - } else if (path.isEndEntity(entity)) { - // We tried to delete the end - path.deleteEntityOnEnd(entity); - } else { - // We tried to delete something inbetween - const newPath = path.deleteEntityOnPathSplitIntoTwo(entity); - this.beltPaths.push(newPath); - } - - // Sanity - entity.components.Belt.assignedPath = null; - } - - /** - * Adds the given entity to the appropriate paths - * @param {Entity} entity - */ - addEntityToPaths(entity) { - const fromEntity = this.findSupplyingEntity(entity); - const toEntity = this.findFollowUpEntity(entity); - - // Check if we can add the entity to the previous path - if (fromEntity) { - const fromPath = fromEntity.components.Belt.assignedPath; - fromPath.extendOnEnd(entity); - - // Check if we now can extend the current path by the next path - if (toEntity) { - const toPath = toEntity.components.Belt.assignedPath; - - if (fromPath === toPath) { - // This is a circular dependency -> Ignore - } else { - fromPath.extendByPath(toPath); - - // Delete now obsolete path - fastArrayDeleteValue(this.beltPaths, toPath); - } - } - } else { - if (toEntity) { - // Prepend it to the other path - const toPath = toEntity.components.Belt.assignedPath; - toPath.extendOnBeginning(entity); - } else { - // This is an empty belt path - const path = new BeltPath(this.root, [entity]); - this.beltPaths.push(path); - } - } - } - - /** - * Called when an entity got added - * @param {Entity} entity - */ - onEntityAdded(entity) { - if (!this.root.gameInitialized) { - return; - } - - if (!entity.components.Belt) { - return; - } - - this.addEntityToPaths(entity); - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - } - - /** - * Draws all belt paths - * @param {DrawParameters} parameters - */ - drawBeltItems(parameters) { - for (let i = 0; i < this.beltPaths.length; ++i) { - this.beltPaths[i].draw(parameters); - } - } - - /** - * Verifies all belt paths - */ - debug_verifyBeltPaths() { - for (let i = 0; i < this.beltPaths.length; ++i) { - this.beltPaths[i].debug_checkIntegrity("general-verify"); - } - - const belts = this.root.entityMgr.getAllWithComponent(BeltComponent); - for (let i = 0; i < belts.length; ++i) { - const path = belts[i].components.Belt.assignedPath; - if (!path) { - throw new Error("Belt has no path: " + belts[i].uid); - } - if (this.beltPaths.indexOf(path) < 0) { - throw new Error("Path of entity not contained: " + belts[i].uid); - } - } - } - - /** - * Finds the follow up entity for a given belt. Used for building the dependencies - * @param {Entity} entity - * @returns {Entity|null} - */ - findFollowUpEntity(entity) { - const staticComp = entity.components.StaticMapEntity; - const beltComp = entity.components.Belt; - - const followUpDirection = staticComp.localDirectionToWorld(beltComp.direction); - const followUpVector = enumDirectionToVector[followUpDirection]; - - const followUpTile = staticComp.origin.add(followUpVector); - const followUpEntity = this.root.map.getLayerContentXY(followUpTile.x, followUpTile.y, entity.layer); - - // Check if theres a belt at the tile we point to - if (followUpEntity) { - const followUpBeltComp = followUpEntity.components.Belt; - if (followUpBeltComp) { - const followUpStatic = followUpEntity.components.StaticMapEntity; - - const acceptedDirection = followUpStatic.localDirectionToWorld(enumDirection.top); - if (acceptedDirection === followUpDirection) { - return followUpEntity; - } - } - } - - return null; - } - - /** - * Finds the supplying belt for a given belt. Used for building the dependencies - * @param {Entity} entity - * @returns {Entity|null} - */ - findSupplyingEntity(entity) { - const staticComp = entity.components.StaticMapEntity; - - const supplyDirection = staticComp.localDirectionToWorld(enumDirection.bottom); - const supplyVector = enumDirectionToVector[supplyDirection]; - - const supplyTile = staticComp.origin.add(supplyVector); - const supplyEntity = this.root.map.getLayerContentXY(supplyTile.x, supplyTile.y, entity.layer); - - // Check if theres a belt at the tile we point to - if (supplyEntity) { - const supplyBeltComp = supplyEntity.components.Belt; - if (supplyBeltComp) { - const supplyStatic = supplyEntity.components.StaticMapEntity; - const otherDirection = supplyStatic.localDirectionToWorld( - enumInvertedDirections[supplyBeltComp.direction] - ); - - if (otherDirection === supplyDirection) { - return supplyEntity; - } - } - } - - return null; - } - - /** - * Recomputes the belt path network. Only required for old savegames - */ - recomputeAllBeltPaths() { - logger.warn("Recomputing all belt paths"); - const visitedUids = new Set(); - - const result = []; - - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - if (visitedUids.has(entity.uid)) { - continue; - } - - // Mark entity as visited - visitedUids.add(entity.uid); - - // Compute path, start with entity and find precedors / successors - const path = [entity]; - - // Prevent infinite loops - let maxIter = 99999; - - // Find precedors - let prevEntity = this.findSupplyingEntity(entity); - while (prevEntity && --maxIter > 0) { - if (visitedUids.has(prevEntity.uid)) { - break; - } - path.unshift(prevEntity); - visitedUids.add(prevEntity.uid); - prevEntity = this.findSupplyingEntity(prevEntity); - } - - // Find succedors - let nextEntity = this.findFollowUpEntity(entity); - while (nextEntity && --maxIter > 0) { - if (visitedUids.has(nextEntity.uid)) { - break; - } - - path.push(nextEntity); - visitedUids.add(nextEntity.uid); - nextEntity = this.findFollowUpEntity(nextEntity); - } - - assert(maxIter > 1, "Ran out of iterations"); - result.push(new BeltPath(this.root, path)); - } - - logger.log("Found", this.beltPaths.length, "belt paths"); - this.beltPaths = result; - } - - /** - * Updates all belts - */ - update() { - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - - for (let i = 0; i < this.beltPaths.length; ++i) { - this.beltPaths[i].update(); - } - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_verifyBeltPaths(); - } - } - - /** - * Draws a given chunk - * @param {DrawParameters} parameters - * @param {MapChunkView} chunk - */ - drawChunk(parameters, chunk) { - // Limit speed to avoid belts going backwards - const speedMultiplier = Math.min(this.root.hubGoals.getBeltBaseSpeed(), 10); - - // SYNC with systems/item_acceptor.js:drawEntityUnderlays! - // 126 / 42 is the exact animation speed of the png animation - const animationIndex = Math.floor( - ((this.root.time.realtimeNow() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42) * - globalConfig.itemSpacingOnBelts - ); - const contents = chunk.containedEntitiesByLayer.regular; - for (let i = 0; i < contents.length; ++i) { - const entity = contents[i]; - if (entity.components.Belt) { - const direction = entity.components.Belt.direction; - const sprite = this.beltAnimations[direction][animationIndex % BELT_ANIM_COUNT]; - - // Culling happens within the static map entity component - entity.components.StaticMapEntity.drawSpriteOnBoundsClipped(parameters, sprite, 0); - } - } - } - - /** - * Draws the belt path debug overlays - * @param {DrawParameters} parameters - */ - drawBeltPathDebug(parameters) { - for (let i = 0; i < this.beltPaths.length; ++i) { - this.beltPaths[i].drawDebug(parameters); - } - } -} +import { globalConfig } from "../../core/config"; +import { DrawParameters } from "../../core/draw_parameters"; +import { gMetaBuildingRegistry } from "../../core/global_registries"; +import { Loader } from "../../core/loader"; +import { createLogger } from "../../core/logging"; +import { AtlasSprite } from "../../core/sprites"; +import { fastArrayDeleteValue } from "../../core/utils"; +import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../../core/vector"; +import { BeltPath } from "../belt_path"; +import { arrayBeltVariantToRotation, MetaBeltBuilding } from "../buildings/belt"; +import { getCodeFromBuildingData } from "../building_codes"; +import { BeltComponent } from "../components/belt"; +import { Entity } from "../entity"; +import { GameSystemWithFilter } from "../game_system_with_filter"; +import { MapChunkView } from "../map_chunk_view"; +import { defaultBuildingVariant } from "../meta_building"; + +export const BELT_ANIM_COUNT = 14; + +const logger = createLogger("belt"); + +/** + * Manages all belts + */ +export class BeltSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [BeltComponent]); + /** + * @type {Object.>} + */ + this.beltSprites = { + [enumDirection.top]: Loader.getSprite("sprites/belt/built/forward_0.png"), + [enumDirection.left]: Loader.getSprite("sprites/belt/built/left_0.png"), + [enumDirection.right]: Loader.getSprite("sprites/belt/built/right_0.png"), + }; + + /** + * @type {Object.>} + */ + this.beltAnimations = { + [enumDirection.top]: [], + [enumDirection.left]: [], + [enumDirection.right]: [], + }; + + for (let i = 0; i < BELT_ANIM_COUNT; ++i) { + this.beltAnimations[enumDirection.top].push( + Loader.getSprite("sprites/belt/built/forward_" + i + ".png") + ); + this.beltAnimations[enumDirection.left].push( + Loader.getSprite("sprites/belt/built/left_" + i + ".png") + ); + this.beltAnimations[enumDirection.right].push( + Loader.getSprite("sprites/belt/built/right_" + i + ".png") + ); + } + + this.root.signals.entityDestroyed.add(this.onEntityDestroyed, this); + this.root.signals.entityDestroyed.add(this.updateSurroundingBeltPlacement, this); + + // Notice: These must come *after* the entity destroyed signals + this.root.signals.entityAdded.add(this.onEntityAdded, this); + this.root.signals.entityAdded.add(this.updateSurroundingBeltPlacement, this); + + /** @type {Array} */ + this.beltPaths = []; + } + + /** + * Serializes all belt paths + * @returns {Array} + */ + serializePaths() { + let data = []; + for (let i = 0; i < this.beltPaths.length; ++i) { + data.push(this.beltPaths[i].serialize()); + } + return data; + } + + /** + * Deserializes all belt paths + * @param {Array} data + */ + deserializePaths(data) { + if (!Array.isArray(data)) { + return "Belt paths are not an array: " + typeof data; + } + + for (let i = 0; i < data.length; ++i) { + const path = BeltPath.fromSerialized(this.root, data[i]); + // If path is a string, that means its an error + if (!(path instanceof BeltPath)) { + return "Failed to create path from belt data: " + path; + } + this.beltPaths.push(path); + } + + if (this.beltPaths.length === 0) { + // Old savegames might not have paths yet + logger.warn("Recomputing belt paths (most likely the savegame is old or empty)"); + this.recomputeAllBeltPaths(); + } else { + logger.warn("Restored", this.beltPaths.length, "belt paths"); + } + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + } + + /** + * Updates the belt placement after an entity has been added / deleted + * @param {Entity} entity + */ + updateSurroundingBeltPlacement(entity) { + if (!this.root.gameInitialized) { + return; + } + + const staticComp = entity.components.StaticMapEntity; + if (!staticComp) { + return; + } + + const metaBelt = gMetaBuildingRegistry.findByClass(MetaBeltBuilding); + // Compute affected area + const originalRect = staticComp.getTileSpaceBounds(); + const affectedArea = originalRect.expandedInAllDirections(1); + + /** @type {Set} */ + const changedPaths = new Set(); + + for (let x = affectedArea.x; x < affectedArea.right(); ++x) { + for (let y = affectedArea.y; y < affectedArea.bottom(); ++y) { + if (originalRect.containsPoint(x, y)) { + // Make sure we don't update the original entity + continue; + } + + const targetEntities = this.root.map.getLayersContentsMultipleXY(x, y); + for (let i = 0; i < targetEntities.length; ++i) { + const targetEntity = targetEntities[i]; + + const targetBeltComp = targetEntity.components.Belt; + const targetStaticComp = targetEntity.components.StaticMapEntity; + + if (!targetBeltComp) { + // Not a belt + continue; + } + + const { + rotation, + rotationVariant, + } = metaBelt.computeOptimalDirectionAndRotationVariantAtTile({ + root: this.root, + tile: new Vector(x, y), + rotation: targetStaticComp.originalRotation, + variant: defaultBuildingVariant, + layer: targetEntity.layer, + }); + + // Compute delta to see if anything changed + const newDirection = arrayBeltVariantToRotation[rotationVariant]; + + if (targetStaticComp.rotation !== rotation || newDirection !== targetBeltComp.direction) { + const originalPath = targetBeltComp.assignedPath; + + // Ok, first remove it from its current path + this.deleteEntityFromPath(targetBeltComp.assignedPath, targetEntity); + + // Change stuff + targetStaticComp.rotation = rotation; + metaBelt.updateVariants(targetEntity, rotationVariant, defaultBuildingVariant); + + // Update code as well + targetStaticComp.code = getCodeFromBuildingData( + metaBelt, + defaultBuildingVariant, + rotationVariant + ); + + // Update the original path since it might have picked up the entit1y + originalPath.onPathChanged(); + + // Now add it again + this.addEntityToPaths(targetEntity); + + // Sanity + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + + // Make sure the chunks know about the update + this.root.signals.entityChanged.dispatch(targetEntity); + } + + if (targetBeltComp.assignedPath) { + changedPaths.add(targetBeltComp.assignedPath); + } + } + } + } + + // notify all paths *afterwards* to avoid multi-updates + changedPaths.forEach(path => path.onSurroundingsChanged()); + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + } + + /** + * Called when an entity got destroyed + * @param {Entity} entity + */ + onEntityDestroyed(entity) { + if (!this.root.gameInitialized) { + return; + } + + if (!entity.components.Belt) { + return; + } + + const assignedPath = entity.components.Belt.assignedPath; + assert(assignedPath, "Entity has no belt path assigned"); + this.deleteEntityFromPath(assignedPath, entity); + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + } + + /** + * Attempts to delete the belt from its current path + * @param {BeltPath} path + * @param {Entity} entity + */ + deleteEntityFromPath(path, entity) { + if (path.entityPath.length === 1) { + // This is a single entity path, easy to do, simply erase whole path + fastArrayDeleteValue(this.beltPaths, path); + return; + } + + // Notice: Since there might be circular references, it is important to check + // which role the entity has + if (path.isStartEntity(entity)) { + // We tried to delete the start + path.deleteEntityOnStart(entity); + } else if (path.isEndEntity(entity)) { + // We tried to delete the end + path.deleteEntityOnEnd(entity); + } else { + // We tried to delete something inbetween + const newPath = path.deleteEntityOnPathSplitIntoTwo(entity); + this.beltPaths.push(newPath); + } + + // Sanity + entity.components.Belt.assignedPath = null; + } + + /** + * Adds the given entity to the appropriate paths + * @param {Entity} entity + */ + addEntityToPaths(entity) { + const fromEntity = this.findSupplyingEntity(entity); + const toEntity = this.findFollowUpEntity(entity); + + // Check if we can add the entity to the previous path + if (fromEntity) { + const fromPath = fromEntity.components.Belt.assignedPath; + fromPath.extendOnEnd(entity); + + // Check if we now can extend the current path by the next path + if (toEntity) { + const toPath = toEntity.components.Belt.assignedPath; + + if (fromPath === toPath) { + // This is a circular dependency -> Ignore + } else { + fromPath.extendByPath(toPath); + + // Delete now obsolete path + fastArrayDeleteValue(this.beltPaths, toPath); + } + } + } else { + if (toEntity) { + // Prepend it to the other path + const toPath = toEntity.components.Belt.assignedPath; + toPath.extendOnBeginning(entity); + } else { + // This is an empty belt path + const path = new BeltPath(this.root, [entity]); + this.beltPaths.push(path); + } + } + } + + /** + * Called when an entity got added + * @param {Entity} entity + */ + onEntityAdded(entity) { + if (!this.root.gameInitialized) { + return; + } + + if (!entity.components.Belt) { + return; + } + + this.addEntityToPaths(entity); + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + } + + /** + * Draws all belt paths + * @param {DrawParameters} parameters + */ + drawBeltItems(parameters) { + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].draw(parameters); + } + } + + /** + * Verifies all belt paths + */ + debug_verifyBeltPaths() { + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].debug_checkIntegrity("general-verify"); + } + + const belts = this.root.entityMgr.getAllWithComponent(BeltComponent); + for (let i = 0; i < belts.length; ++i) { + const path = belts[i].components.Belt.assignedPath; + if (!path) { + throw new Error("Belt has no path: " + belts[i].uid); + } + if (this.beltPaths.indexOf(path) < 0) { + throw new Error("Path of entity not contained: " + belts[i].uid); + } + } + } + + /** + * Finds the follow up entity for a given belt. Used for building the dependencies + * @param {Entity} entity + * @returns {Entity|null} + */ + findFollowUpEntity(entity) { + const staticComp = entity.components.StaticMapEntity; + const beltComp = entity.components.Belt; + + const followUpDirection = staticComp.localDirectionToWorld(beltComp.direction); + const followUpVector = enumDirectionToVector[followUpDirection]; + + const followUpTile = staticComp.origin.add(followUpVector); + const followUpEntity = this.root.map.getLayerContentXY(followUpTile.x, followUpTile.y, entity.layer); + + // Check if there's a belt at the tile we point to + if (followUpEntity) { + const followUpBeltComp = followUpEntity.components.Belt; + if (followUpBeltComp) { + const followUpStatic = followUpEntity.components.StaticMapEntity; + + const acceptedDirection = followUpStatic.localDirectionToWorld(enumDirection.top); + if (acceptedDirection === followUpDirection) { + return followUpEntity; + } + } + } + + return null; + } + + /** + * Finds the supplying belt for a given belt. Used for building the dependencies + * @param {Entity} entity + * @returns {Entity|null} + */ + findSupplyingEntity(entity) { + const staticComp = entity.components.StaticMapEntity; + + const supplyDirection = staticComp.localDirectionToWorld(enumDirection.bottom); + const supplyVector = enumDirectionToVector[supplyDirection]; + + const supplyTile = staticComp.origin.add(supplyVector); + const supplyEntity = this.root.map.getLayerContentXY(supplyTile.x, supplyTile.y, entity.layer); + + // Check if there's a belt at the tile we point to + if (supplyEntity) { + const supplyBeltComp = supplyEntity.components.Belt; + if (supplyBeltComp) { + const supplyStatic = supplyEntity.components.StaticMapEntity; + const otherDirection = supplyStatic.localDirectionToWorld( + enumInvertedDirections[supplyBeltComp.direction] + ); + + if (otherDirection === supplyDirection) { + return supplyEntity; + } + } + } + + return null; + } + + /** + * Recomputes the belt path network. Only required for old savegames + */ + recomputeAllBeltPaths() { + logger.warn("Recomputing all belt paths"); + const visitedUids = new Set(); + + const result = []; + + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + if (visitedUids.has(entity.uid)) { + continue; + } + + // Mark entity as visited + visitedUids.add(entity.uid); + + // Compute path, start with entity and find precedors / successors + const path = [entity]; + + // Prevent infinite loops + let maxIter = 99999; + + // Find precedors + let prevEntity = this.findSupplyingEntity(entity); + while (prevEntity && --maxIter > 0) { + if (visitedUids.has(prevEntity.uid)) { + break; + } + path.unshift(prevEntity); + visitedUids.add(prevEntity.uid); + prevEntity = this.findSupplyingEntity(prevEntity); + } + + // Find succedors + let nextEntity = this.findFollowUpEntity(entity); + while (nextEntity && --maxIter > 0) { + if (visitedUids.has(nextEntity.uid)) { + break; + } + + path.push(nextEntity); + visitedUids.add(nextEntity.uid); + nextEntity = this.findFollowUpEntity(nextEntity); + } + + assert(maxIter > 1, "Ran out of iterations"); + result.push(new BeltPath(this.root, path)); + } + + logger.log("Found", this.beltPaths.length, "belt paths"); + this.beltPaths = result; + } + + /** + * Updates all belts + */ + update() { + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].update(); + } + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_verifyBeltPaths(); + } + } + + /** + * Draws a given chunk + * @param {DrawParameters} parameters + * @param {MapChunkView} chunk + */ + drawChunk(parameters, chunk) { + // Limit speed to avoid belts going backwards + const speedMultiplier = Math.min(this.root.hubGoals.getBeltBaseSpeed(), 10); + + // SYNC with systems/item_acceptor.js:drawEntityUnderlays! + // 126 / 42 is the exact animation speed of the png animation + const animationIndex = Math.floor( + ((this.root.time.realtimeNow() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42) * + globalConfig.itemSpacingOnBelts + ); + const contents = chunk.containedEntitiesByLayer.regular; + + if (this.root.app.settings.getAllSettings().simplifiedBelts) { + // POTATO Mode: Only show items when belt is hovered + let hoveredBeltPath = null; + const mousePos = this.root.app.mousePosition; + if (mousePos && this.root.currentLayer === "regular") { + const tile = this.root.camera.screenToWorld(mousePos).toTileSpace(); + const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "regular"); + if (contents && contents.components.Belt) { + hoveredBeltPath = contents.components.Belt.assignedPath; + } + } + + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + if (entity.components.Belt) { + const direction = entity.components.Belt.direction; + let sprite = this.beltAnimations[direction][0]; + + if (entity.components.Belt.assignedPath === hoveredBeltPath) { + sprite = this.beltAnimations[direction][animationIndex % BELT_ANIM_COUNT]; + } + + // Culling happens within the static map entity component + entity.components.StaticMapEntity.drawSpriteOnBoundsClipped(parameters, sprite, 0); + } + } + } else { + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + if (entity.components.Belt) { + const direction = entity.components.Belt.direction; + const sprite = this.beltAnimations[direction][animationIndex % BELT_ANIM_COUNT]; + + // Culling happens within the static map entity component + entity.components.StaticMapEntity.drawSpriteOnBoundsClipped(parameters, sprite, 0); + } + } + } + } + + /** + * Draws the belt path debug overlays + * @param {DrawParameters} parameters + */ + drawBeltPathDebug(parameters) { + for (let i = 0; i < this.beltPaths.length; ++i) { + this.beltPaths[i].drawDebug(parameters); + } + } +} diff --git a/src/js/game/systems/belt_reader.js b/src/js/game/systems/belt_reader.js index abddd999..4ce75af4 100644 --- a/src/js/game/systems/belt_reader.js +++ b/src/js/game/systems/belt_reader.js @@ -48,7 +48,7 @@ export class BeltReaderSystem extends GameSystemWithFilter { throughput = 1 / (averageSpacing / averageSpacingNum); } - readerComp.lastThroughput = throughput; + readerComp.lastThroughput = Math.min(30, throughput); } } } diff --git a/src/js/game/systems/belt_underlays.js b/src/js/game/systems/belt_underlays.js index 5bdf2331..c5c69d26 100644 --- a/src/js/game/systems/belt_underlays.js +++ b/src/js/game/systems/belt_underlays.js @@ -1,84 +1,300 @@ -import { globalConfig } from "../../core/config"; -import { drawRotatedSprite } from "../../core/draw_utils"; -import { Loader } from "../../core/loader"; -import { enumDirectionToAngle } from "../../core/vector"; -import { BeltUnderlaysComponent } from "../components/belt_underlays"; -import { GameSystemWithFilter } from "../game_system_with_filter"; -import { BELT_ANIM_COUNT } from "./belt"; -import { MapChunkView } from "../map_chunk_view"; -import { DrawParameters } from "../../core/draw_parameters"; - -export class BeltUnderlaysSystem extends GameSystemWithFilter { - constructor(root) { - super(root, [BeltUnderlaysComponent]); - - this.underlayBeltSprites = []; - - for (let i = 0; i < BELT_ANIM_COUNT; ++i) { - this.underlayBeltSprites.push(Loader.getSprite("sprites/belt/built/forward_" + i + ".png")); - } - } - - /** - * Draws a given chunk - * @param {DrawParameters} parameters - * @param {MapChunkView} chunk - */ - drawChunk(parameters, chunk) { - // Limit speed to avoid belts going backwards - const speedMultiplier = Math.min(this.root.hubGoals.getBeltBaseSpeed(), 10); - - const contents = chunk.containedEntitiesByLayer.regular; - for (let i = 0; i < contents.length; ++i) { - const entity = contents[i]; - const underlayComp = entity.components.BeltUnderlays; - if (!underlayComp) { - continue; - } - - const staticComp = entity.components.StaticMapEntity; - const underlays = underlayComp.underlays; - for (let i = 0; i < underlays.length; ++i) { - const { pos, direction } = underlays[i]; - const transformedPos = staticComp.localTileToWorld(pos); - - // Culling - if (!chunk.tileSpaceRectangle.containsPoint(transformedPos.x, transformedPos.y)) { - continue; - } - - const destX = transformedPos.x * globalConfig.tileSize; - const destY = transformedPos.y * globalConfig.tileSize; - - // Culling, #2 - if ( - !parameters.visibleRect.containsRect4Params( - destX, - destY, - globalConfig.tileSize, - globalConfig.tileSize - ) - ) { - continue; - } - - const angle = enumDirectionToAngle[staticComp.localDirectionToWorld(direction)]; - - // SYNC with systems/belt.js:drawSingleEntity! - const animationIndex = Math.floor( - ((this.root.time.realtimeNow() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42) * - globalConfig.itemSpacingOnBelts - ); - - drawRotatedSprite({ - parameters, - sprite: this.underlayBeltSprites[animationIndex % this.underlayBeltSprites.length], - x: destX + globalConfig.halfTileSize, - y: destY + globalConfig.halfTileSize, - angle: Math.radians(angle), - size: globalConfig.tileSize, - }); - } - } - } -} +import { globalConfig } from "../../core/config"; +import { DrawParameters } from "../../core/draw_parameters"; +import { Loader } from "../../core/loader"; +import { Rectangle } from "../../core/rectangle"; +import { FULL_CLIP_RECT } from "../../core/sprites"; +import { StaleAreaDetector } from "../../core/stale_area_detector"; +import { + enumDirection, + enumDirectionToAngle, + enumDirectionToVector, + enumInvertedDirections, + Vector, +} from "../../core/vector"; +import { BeltComponent } from "../components/belt"; +import { BeltUnderlaysComponent, enumClippedBeltUnderlayType } from "../components/belt_underlays"; +import { ItemAcceptorComponent } from "../components/item_acceptor"; +import { ItemEjectorComponent } from "../components/item_ejector"; +import { Entity } from "../entity"; +import { GameSystemWithFilter } from "../game_system_with_filter"; +import { MapChunkView } from "../map_chunk_view"; +import { BELT_ANIM_COUNT } from "./belt"; + +/** + * Mapping from underlay type to clip rect + * @type {Object} + */ +const enumUnderlayTypeToClipRect = { + [enumClippedBeltUnderlayType.none]: null, + [enumClippedBeltUnderlayType.full]: FULL_CLIP_RECT, + [enumClippedBeltUnderlayType.topOnly]: new Rectangle(0, 0, 1, 0.5), + [enumClippedBeltUnderlayType.bottomOnly]: new Rectangle(0, 0.5, 1, 0.5), +}; + +export class BeltUnderlaysSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [BeltUnderlaysComponent]); + + this.underlayBeltSprites = []; + + for (let i = 0; i < BELT_ANIM_COUNT; ++i) { + this.underlayBeltSprites.push(Loader.getSprite("sprites/belt/built/forward_" + i + ".png")); + } + + // Automatically recompute areas + this.staleArea = new StaleAreaDetector({ + root, + name: "belt-underlay", + recomputeMethod: this.recomputeStaleArea.bind(this), + }); + + this.staleArea.recomputeOnComponentsChanged( + [BeltUnderlaysComponent, BeltComponent, ItemAcceptorComponent, ItemEjectorComponent], + 1 + ); + } + + update() { + this.staleArea.update(); + } + + /** + * Called when an area changed - Resets all caches in the given area + * @param {Rectangle} area + */ + recomputeStaleArea(area) { + for (let x = 0; x < area.w; ++x) { + for (let y = 0; y < area.h; ++y) { + const tileX = area.x + x; + const tileY = area.y + y; + const entity = this.root.map.getLayerContentXY(tileX, tileY, "regular"); + if (entity) { + const underlayComp = entity.components.BeltUnderlays; + if (underlayComp) { + for (let i = 0; i < underlayComp.underlays.length; ++i) { + underlayComp.underlays[i].cachedType = null; + } + } + } + } + } + } + + /** + * Checks if a given tile is connected and has an acceptor + * @param {Vector} tile + * @param {enumDirection} fromDirection + * @returns {boolean} + */ + checkIsAcceptorConnected(tile, fromDirection) { + const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "regular"); + if (!contents) { + return false; + } + + const staticComp = contents.components.StaticMapEntity; + + // Check if its a belt, since then its simple + const beltComp = contents.components.Belt; + if (beltComp) { + return staticComp.localDirectionToWorld(enumDirection.bottom) === fromDirection; + } + + // Check if there's an item acceptor + const acceptorComp = contents.components.ItemAcceptor; + if (acceptorComp) { + // Check each slot to see if its connected + for (let i = 0; i < acceptorComp.slots.length; ++i) { + const slot = acceptorComp.slots[i]; + const slotTile = staticComp.localTileToWorld(slot.pos); + + // Step 1: Check if the tile matches + if (!slotTile.equals(tile)) { + continue; + } + + // Step 2: Check if any of the directions matches + for (let j = 0; j < slot.directions.length; ++j) { + const slotDirection = staticComp.localDirectionToWorld(slot.directions[j]); + if (slotDirection === fromDirection) { + return true; + } + } + } + } + + return false; + } + + /** + * Checks if a given tile is connected and has an ejector + * @param {Vector} tile + * @param {enumDirection} toDirection + * @returns {boolean} + */ + checkIsEjectorConnected(tile, toDirection) { + const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "regular"); + if (!contents) { + return false; + } + + const staticComp = contents.components.StaticMapEntity; + + // Check if its a belt, since then its simple + const beltComp = contents.components.Belt; + if (beltComp) { + return staticComp.localDirectionToWorld(beltComp.direction) === toDirection; + } + + // Check for an ejector + const ejectorComp = contents.components.ItemEjector; + if (ejectorComp) { + // Check each slot to see if its connected + for (let i = 0; i < ejectorComp.slots.length; ++i) { + const slot = ejectorComp.slots[i]; + const slotTile = staticComp.localTileToWorld(slot.pos); + + // Step 1: Check if the tile matches + if (!slotTile.equals(tile)) { + continue; + } + + // Step 2: Check if the direction matches + const slotDirection = staticComp.localDirectionToWorld(slot.direction); + if (slotDirection === toDirection) { + return true; + } + } + } + + return false; + } + + /** + * Computes the flag for a given tile + * @param {Entity} entity + * @param {import("../components/belt_underlays").BeltUnderlayTile} underlayTile + * @returns {enumClippedBeltUnderlayType} The type of the underlay + */ + computeBeltUnderlayType(entity, underlayTile) { + if (underlayTile.cachedType) { + return underlayTile.cachedType; + } + + const staticComp = entity.components.StaticMapEntity; + + const transformedPos = staticComp.localTileToWorld(underlayTile.pos); + const destX = transformedPos.x * globalConfig.tileSize; + const destY = transformedPos.y * globalConfig.tileSize; + + // Extract direction and angle + const worldDirection = staticComp.localDirectionToWorld(underlayTile.direction); + const worldDirectionVector = enumDirectionToVector[worldDirection]; + + // Figure out if there is anything connected at the top + const connectedTop = this.checkIsAcceptorConnected( + transformedPos.add(worldDirectionVector), + enumInvertedDirections[worldDirection] + ); + + // Figure out if there is anything connected at the bottom + const connectedBottom = this.checkIsEjectorConnected( + transformedPos.sub(worldDirectionVector), + worldDirection + ); + + let flag = enumClippedBeltUnderlayType.none; + + if (connectedTop && connectedBottom) { + flag = enumClippedBeltUnderlayType.full; + } else if (connectedTop) { + flag = enumClippedBeltUnderlayType.topOnly; + } else if (connectedBottom) { + flag = enumClippedBeltUnderlayType.bottomOnly; + } + + return (underlayTile.cachedType = flag); + } + + /** + * Draws a given chunk + * @param {DrawParameters} parameters + * @param {MapChunkView} chunk + */ + drawChunk(parameters, chunk) { + // Limit speed to avoid belts going backwards + const speedMultiplier = Math.min(this.root.hubGoals.getBeltBaseSpeed(), 10); + + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + const underlayComp = entity.components.BeltUnderlays; + if (!underlayComp) { + continue; + } + + const staticComp = entity.components.StaticMapEntity; + const underlays = underlayComp.underlays; + for (let i = 0; i < underlays.length; ++i) { + // Extract underlay parameters + const { pos, direction } = underlays[i]; + const transformedPos = staticComp.localTileToWorld(pos); + const destX = transformedPos.x * globalConfig.tileSize; + const destY = transformedPos.y * globalConfig.tileSize; + + // Culling, Part 1: Check if the chunk contains the tile + if (!chunk.tileSpaceRectangle.containsPoint(transformedPos.x, transformedPos.y)) { + continue; + } + + // Culling, Part 2: Check if the overlay is visible + if ( + !parameters.visibleRect.containsRect4Params( + destX, + destY, + globalConfig.tileSize, + globalConfig.tileSize + ) + ) { + continue; + } + + // Extract direction and angle + const worldDirection = staticComp.localDirectionToWorld(direction); + const angle = enumDirectionToAngle[worldDirection]; + + const underlayType = this.computeBeltUnderlayType(entity, underlays[i]); + const clipRect = enumUnderlayTypeToClipRect[underlayType]; + if (!clipRect) { + // Empty + continue; + } + + // Actually draw the sprite + const x = destX + globalConfig.halfTileSize; + const y = destY + globalConfig.halfTileSize; + const angleRadians = Math.radians(angle); + + // SYNC with systems/belt.js:drawSingleEntity! + const animationIndex = Math.floor( + ((this.root.time.realtimeNow() * speedMultiplier * BELT_ANIM_COUNT * 126) / 42) * + globalConfig.itemSpacingOnBelts + ); + parameters.context.translate(x, y); + parameters.context.rotate(angleRadians); + this.underlayBeltSprites[ + animationIndex % this.underlayBeltSprites.length + ].drawCachedWithClipRect( + parameters, + -globalConfig.halfTileSize, + -globalConfig.halfTileSize, + globalConfig.tileSize, + globalConfig.tileSize, + clipRect + ); + parameters.context.rotate(-angleRadians); + parameters.context.translate(-x, -y); + } + } + } +} diff --git a/src/js/game/systems/display.js b/src/js/game/systems/display.js index 2ad551f0..f11091b9 100644 --- a/src/js/game/systems/display.js +++ b/src/js/game/systems/display.js @@ -65,7 +65,7 @@ export class DisplaySystem extends GameSystemWithFilter { const pinsComp = entity.components.WiredPins; const network = pinsComp.slots[0].linkedNetwork; - if (!network || !network.currentValue) { + if (!network || !network.hasValue()) { continue; } diff --git a/src/js/game/systems/filter.js b/src/js/game/systems/filter.js new file mode 100644 index 00000000..a6442b41 --- /dev/null +++ b/src/js/game/systems/filter.js @@ -0,0 +1,85 @@ +import { globalConfig } from "../../core/config"; +import { BaseItem } from "../base_item"; +import { FilterComponent } from "../components/filter"; +import { Entity } from "../entity"; +import { GameSystemWithFilter } from "../game_system_with_filter"; +import { BOOL_TRUE_SINGLETON } from "../items/boolean_item"; + +const MAX_ITEMS_IN_QUEUE = 2; + +export class FilterSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [FilterComponent]); + } + + update() { + const progress = + this.root.dynamicTickrate.deltaSeconds * + this.root.hubGoals.getBeltBaseSpeed() * + globalConfig.itemSpacingOnBelts; + + const requiredProgress = 1 - progress; + + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + const filterComp = entity.components.Filter; + const ejectorComp = entity.components.ItemEjector; + + // Process payloads + const slotsAndLists = [filterComp.pendingItemsToLeaveThrough, filterComp.pendingItemsToReject]; + for (let slotIndex = 0; slotIndex < slotsAndLists.length; ++slotIndex) { + const pendingItems = slotsAndLists[slotIndex]; + + for (let j = 0; j < pendingItems.length; ++j) { + const nextItem = pendingItems[j]; + // Advance next item + nextItem.progress = Math.min(requiredProgress, nextItem.progress + progress); + // Check if it's ready to eject + if (nextItem.progress >= requiredProgress - 1e-5) { + if (ejectorComp.tryEject(slotIndex, nextItem.item)) { + pendingItems.shift(); + } + } + } + } + } + } + + /** + * + * @param {Entity} entity + * @param {number} slot + * @param {BaseItem} item + */ + tryAcceptItem(entity, slot, item) { + const network = entity.components.WiredPins.slots[0].linkedNetwork; + if (!network || !network.hasValue()) { + // Filter is not connected + return false; + } + + const value = network.currentValue; + const filterComp = entity.components.Filter; + assert(filterComp, "entity is no filter"); + + // Figure out which list we have to check + let listToCheck; + if (value.equals(BOOL_TRUE_SINGLETON) || value.equals(item)) { + listToCheck = filterComp.pendingItemsToLeaveThrough; + } else { + listToCheck = filterComp.pendingItemsToReject; + } + + if (listToCheck.length >= MAX_ITEMS_IN_QUEUE) { + // Busy + return false; + } + + // Actually accept item + listToCheck.push({ + item, + progress: 0.0, + }); + return true; + } +} diff --git a/src/js/game/systems/item_acceptor.js b/src/js/game/systems/item_acceptor.js index 6d6fec77..780b4abd 100644 --- a/src/js/game/systems/item_acceptor.js +++ b/src/js/game/systems/item_acceptor.js @@ -9,14 +9,35 @@ import { MapChunkView } from "../map_chunk_view"; export class ItemAcceptorSystem extends GameSystemWithFilter { constructor(root) { super(root, [ItemAcceptorComponent]); + + // Well ... it's better to be verbose I guess? + this.accumulatedTicksWhileInMapOverview = 0; } update() { + if (this.root.app.settings.getAllSettings().simplifiedBelts) { + // Disabled in potato mode + return; + } + + // This system doesn't render anything while in map overview, + // so simply accumulate ticks + if (this.root.camera.getIsMapOverlayActive()) { + ++this.accumulatedTicksWhileInMapOverview; + return; + } + + // Compute how much ticks we missed + const numTicks = 1 + this.accumulatedTicksWhileInMapOverview; const progress = this.root.dynamicTickrate.deltaSeconds * 2 * this.root.hubGoals.getBeltBaseSpeed() * - globalConfig.itemSpacingOnBelts; // * 2 because its only a half tile + globalConfig.itemSpacingOnBelts * // * 2 because its only a half tile + numTicks; + + // Reset accumulated ticks + this.accumulatedTicksWhileInMapOverview = 0; for (let i = 0; i < this.allEntities.length; ++i) { const entity = this.allEntities[i]; @@ -40,6 +61,11 @@ export class ItemAcceptorSystem extends GameSystemWithFilter { * @param {MapChunkView} chunk */ drawChunk(parameters, chunk) { + if (this.root.app.settings.getAllSettings().simplifiedBelts) { + // Disabled in potato mode + return; + } + const contents = chunk.containedEntitiesByLayer.regular; for (let i = 0; i < contents.length; ++i) { const entity = contents[i]; diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index 925dcc2e..4f7d29be 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -2,8 +2,11 @@ import { globalConfig } from "../../core/config"; import { DrawParameters } from "../../core/draw_parameters"; import { createLogger } from "../../core/logging"; import { Rectangle } from "../../core/rectangle"; +import { StaleAreaDetector } from "../../core/stale_area_detector"; import { enumDirection, enumDirectionToVector } from "../../core/vector"; import { BaseItem } from "../base_item"; +import { BeltComponent } from "../components/belt"; +import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; @@ -15,102 +18,52 @@ export class ItemEjectorSystem extends GameSystemWithFilter { constructor(root) { super(root, [ItemEjectorComponent]); - this.root.signals.entityAdded.add(this.checkForCacheInvalidation, this); - this.root.signals.entityDestroyed.add(this.checkForCacheInvalidation, this); - this.root.signals.postLoadHook.add(this.recomputeCache, this); + this.staleAreaDetector = new StaleAreaDetector({ + root: this.root, + name: "item-ejector", + recomputeMethod: this.recomputeArea.bind(this), + }); - /** - * @type {Rectangle} - */ - this.areaToRecompute = null; + this.staleAreaDetector.recomputeOnComponentsChanged( + [ItemEjectorComponent, ItemAcceptorComponent, BeltComponent], + 1 + ); + + this.root.signals.postLoadHook.add(this.recomputeCacheFull, this); } /** - * - * @param {Entity} entity + * Recomputes an area after it changed + * @param {Rectangle} area */ - checkForCacheInvalidation(entity) { - if (!this.root.gameInitialized) { - return; - } - if (!entity.components.StaticMapEntity) { - return; - } - - // Optimize for the common case: adding or removing one building at a time. Clicking - // and dragging can cause up to 4 add/remove signals. - const staticComp = entity.components.StaticMapEntity; - const bounds = staticComp.getTileSpaceBounds(); - const expandedBounds = bounds.expandedInAllDirections(2); - - if (this.areaToRecompute) { - this.areaToRecompute = this.areaToRecompute.getUnion(expandedBounds); - } else { - this.areaToRecompute = expandedBounds; - } - } - - /** - * Precomputes the cache, which makes up for a huge performance improvement - */ - recomputeCache() { - if (this.areaToRecompute) { - logger.log("Recomputing cache using rectangle"); - if (G_IS_DEV && globalConfig.debug.renderChanges) { - this.root.hud.parts.changesDebugger.renderChange( - "ejector-area", - this.areaToRecompute, - "#fe50a6" - ); - } - this.recomputeAreaCache(); - this.areaToRecompute = null; - } else { - logger.log("Full cache recompute"); - if (G_IS_DEV && globalConfig.debug.renderChanges) { - this.root.hud.parts.changesDebugger.renderChange( - "ejector-full", - new Rectangle(-1000, -1000, 2000, 2000), - "#fe50a6" - ); - } - - // Try to find acceptors for every ejector - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - this.recomputeSingleEntityCache(entity); - } - } - } - - /** - * Recomputes the cache in the given area - */ - recomputeAreaCache() { - const area = this.areaToRecompute; - let entryCount = 0; - - logger.log("Recomputing area:", area.x, area.y, "/", area.w, area.h); - - // Store the entities we already recomputed, so we don't do work twice - const recomputedEntities = new Set(); - - for (let x = area.x; x < area.right(); ++x) { - for (let y = area.y; y < area.bottom(); ++y) { - const entities = this.root.map.getLayersContentsMultipleXY(x, y); - for (let i = 0; i < entities.length; ++i) { - const entity = entities[i]; - - // Recompute the entity in case its relevant for this system and it - // hasn't already been computed - if (!recomputedEntities.has(entity.uid) && entity.components.ItemEjector) { - recomputedEntities.add(entity.uid); - this.recomputeSingleEntityCache(entity); + recomputeArea(area) { + /** @type {Set} */ + const seenUids = new Set(); + for (let x = 0; x < area.w; ++x) { + for (let y = 0; y < area.h; ++y) { + const tileX = area.x + x; + const tileY = area.y + y; + // @NOTICE: Item ejector currently only supports regular layer + const contents = this.root.map.getLayerContentXY(tileX, tileY, "regular"); + if (contents && contents.components.ItemEjector) { + if (!seenUids.has(contents.uid)) { + seenUids.add(contents.uid); + this.recomputeSingleEntityCache(contents); } } } } - return entryCount; + } + + /** + * Recomputes the whole cache after the game has loaded + */ + recomputeCacheFull() { + logger.log("Full cache recompute in post load hook"); + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + this.recomputeSingleEntityCache(entity); + } } /** @@ -183,9 +136,7 @@ export class ItemEjectorSystem extends GameSystemWithFilter { } update() { - if (this.areaToRecompute) { - this.recomputeCache(); - } + this.staleAreaDetector.update(); // Precompute effective belt speed let progressGrowth = 2 * this.root.dynamicTickrate.deltaSeconds; @@ -198,9 +149,6 @@ export class ItemEjectorSystem extends GameSystemWithFilter { for (let i = 0; i < this.allEntities.length; ++i) { const sourceEntity = this.allEntities[i]; const sourceEjectorComp = sourceEntity.components.ItemEjector; - if (!sourceEjectorComp.enabled) { - continue; - } const slots = sourceEjectorComp.slots; for (let j = 0; j < slots.length; ++j) { @@ -211,8 +159,6 @@ export class ItemEjectorSystem extends GameSystemWithFilter { continue; } - const targetEntity = sourceSlot.cachedTargetEntity; - // Advance items on the slot sourceSlot.progress = Math.min( 1, @@ -245,17 +191,24 @@ export class ItemEjectorSystem extends GameSystemWithFilter { } // Check if the target acceptor can actually accept this item + const destEntity = sourceSlot.cachedTargetEntity; const destSlot = sourceSlot.cachedDestSlot; if (destSlot) { - const targetAcceptorComp = targetEntity.components.ItemAcceptor; + const targetAcceptorComp = destEntity.components.ItemAcceptor; if (!targetAcceptorComp.canAcceptItem(destSlot.index, item)) { continue; } // Try to hand over the item - if (this.tryPassOverItem(item, targetEntity, destSlot.index)) { + if (this.tryPassOverItem(item, destEntity, destSlot.index)) { // Handover successful, clear slot - targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.acceptedDirection, item); + if (!this.root.app.settings.getAllSettings().simplifiedBelts) { + targetAcceptorComp.onItemAccepted( + destSlot.index, + destSlot.acceptedDirection, + item + ); + } sourceSlot.item = null; continue; } @@ -329,6 +282,15 @@ export class ItemEjectorSystem extends GameSystemWithFilter { return false; } + const filterComp = receiver.components.Filter; + if (filterComp) { + // It's a filter! Unfortunately the filter has to know a lot about it's + // surrounding state and components, so it can't be within the component itself. + if (this.root.systemMgr.systems.filter.tryAcceptItem(receiver, slotIndex, item)) { + return true; + } + } + return false; } @@ -337,6 +299,11 @@ export class ItemEjectorSystem extends GameSystemWithFilter { * @param {MapChunkView} chunk */ drawChunk(parameters, chunk) { + if (this.root.app.settings.getAllSettings().simplifiedBelts) { + // Disabled in potato mode + return; + } + const contents = chunk.containedEntitiesByLayer.regular; for (let i = 0; i < contents.length; ++i) { @@ -357,6 +324,11 @@ export class ItemEjectorSystem extends GameSystemWithFilter { continue; } + if (!ejectorComp.renderFloatingItems && !slot.cachedTargetEntity) { + // Not connected to any building + continue; + } + const realPosition = staticComp.localTileToWorld(slot.pos); if (!chunk.tileSpaceRectangle.containsPoint(realPosition.x, realPosition.y)) { // Not within this chunk diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index d58aa697..7fc2819b 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -16,9 +16,55 @@ import { ShapeItem } from "../items/shape_item"; */ const MAX_QUEUED_CHARGES = 2; +/** + * Whole data for a produced item + * + * @typedef {{ + * item: BaseItem, + * preferredSlot?: number, + * requiredSlot?: number, + * doNotTrack?: boolean + * }} ProducedItem + */ + +/** + * Type of a processor implementation + * @typedef {{ + * entity: Entity, + * items: Array<{ item: BaseItem, sourceSlot: number }>, + * itemsBySlot: Object, + * outItems: Array + * }} ProcessorImplementationPayload + */ + export class ItemProcessorSystem extends GameSystemWithFilter { constructor(root) { super(root, [ItemProcessorComponent]); + + /** + * @type {Object} + */ + this.handlers = { + [enumItemProcessorTypes.balancer]: this.process_BALANCER, + [enumItemProcessorTypes.cutter]: this.process_CUTTER, + [enumItemProcessorTypes.cutterQuad]: this.process_CUTTER_QUAD, + [enumItemProcessorTypes.rotater]: this.process_ROTATER, + [enumItemProcessorTypes.rotaterCCW]: this.process_ROTATER_CCW, + [enumItemProcessorTypes.rotater180]: this.process_ROTATER_180, + [enumItemProcessorTypes.stacker]: this.process_STACKER, + [enumItemProcessorTypes.trash]: this.process_TRASH, + [enumItemProcessorTypes.mixer]: this.process_MIXER, + [enumItemProcessorTypes.painter]: this.process_PAINTER, + [enumItemProcessorTypes.painterDouble]: this.process_PAINTER_DOUBLE, + [enumItemProcessorTypes.painterQuad]: this.process_PAINTER_QUAD, + [enumItemProcessorTypes.hub]: this.process_HUB, + [enumItemProcessorTypes.reader]: this.process_READER, + }; + + // Bind all handlers + for (const key in this.handlers) { + this.handlers[key] = this.handlers[key].bind(this); + } } update() { @@ -115,24 +161,13 @@ export class ItemProcessorSystem extends GameSystemWithFilter { // Check the network value at the given slot const network = pinsComp.slots[slotIndex - 1].linkedNetwork; - const slotIsEnabled = network && isTruthyItem(network.currentValue); + const slotIsEnabled = network && network.hasValue() && isTruthyItem(network.currentValue); if (!slotIsEnabled) { return false; } return true; } - case enumItemProcessorRequirements.filter: { - const network = pinsComp.slots[0].linkedNetwork; - if (!network || !network.currentValue) { - // Item filter is not connected - return false; - } - - // Otherwise, all good - return true; - } - // By default, everything is accepted default: return true; @@ -175,9 +210,8 @@ export class ItemProcessorSystem extends GameSystemWithFilter { // Check which slots are enabled for (let i = 0; i < 4; ++i) { // Extract the network value on the Nth pin - const networkValue = pinsComp.slots[i].linkedNetwork - ? pinsComp.slots[i].linkedNetwork.currentValue - : null; + const network = pinsComp.slots[i].linkedNetwork; + const networkValue = network && network.hasValue() ? network.currentValue : null; // If there is no "1" on that slot, don't paint there if (!isTruthyItem(networkValue)) { @@ -210,18 +244,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter { return true; } - // FILTER - // Double check with linked network - case enumItemProcessorRequirements.filter: { - const network = entity.components.WiredPins.slots[0].linkedNetwork; - if (!network || !network.currentValue) { - // Item filter is not connected - return false; - } - - return processorComp.inputSlots.length >= processorComp.inputsPerCharge; - } - default: assertAlways(false, "Unknown requirement for " + processorComp.processingRequirement); } @@ -238,310 +260,30 @@ export class ItemProcessorSystem extends GameSystemWithFilter { const items = processorComp.inputSlots; processorComp.inputSlots = []; - /** @type {Object.} */ + /** @type {Object} */ const itemsBySlot = {}; for (let i = 0; i < items.length; ++i) { - itemsBySlot[items[i].sourceSlot] = items[i]; + itemsBySlot[items[i].sourceSlot] = items[i].item; } - /** @type {Array<{item: BaseItem, requiredSlot?: number, preferredSlot?: number}>} */ + /** @type {Array} */ const outItems = []; - // Whether to track the production towards the analytics - let trackProduction = true; + /** @type {function(ProcessorImplementationPayload) : void} */ + const handler = this.handlers[processorComp.type]; + assert(handler, "No handler for processor type defined: " + processorComp.type); - // DO SOME MAGIC - - switch (processorComp.type) { - // SPLITTER - case enumItemProcessorTypes.splitterWires: - case enumItemProcessorTypes.splitter: { - trackProduction = false; - const availableSlots = entity.components.ItemEjector.slots.length; - - let nextSlot = processorComp.nextOutputSlot++ % availableSlots; - for (let i = 0; i < items.length; ++i) { - outItems.push({ - item: items[i].item, - preferredSlot: (nextSlot + i) % availableSlots, - }); - } - break; - } - - // CUTTER - case enumItemProcessorTypes.cutter: { - const inputItem = /** @type {ShapeItem} */ (items[0].item); - assert(inputItem instanceof ShapeItem, "Input for cut is not a shape"); - const inputDefinition = inputItem.definition; - - const cutDefinitions = this.root.shapeDefinitionMgr.shapeActionCutHalf(inputDefinition); - - for (let i = 0; i < cutDefinitions.length; ++i) { - const definition = cutDefinitions[i]; - if (!definition.isEntirelyEmpty()) { - outItems.push({ - item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(definition), - requiredSlot: i, - }); - } - } - - break; - } - - // CUTTER (Quad) - case enumItemProcessorTypes.cutterQuad: { - const inputItem = /** @type {ShapeItem} */ (items[0].item); - assert(inputItem instanceof ShapeItem, "Input for cut is not a shape"); - const inputDefinition = inputItem.definition; - - const cutDefinitions = this.root.shapeDefinitionMgr.shapeActionCutQuad(inputDefinition); - - for (let i = 0; i < cutDefinitions.length; ++i) { - const definition = cutDefinitions[i]; - if (!definition.isEntirelyEmpty()) { - outItems.push({ - item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(definition), - requiredSlot: i, - }); - } - } - - break; - } - - // ROTATER - case enumItemProcessorTypes.rotater: { - const inputItem = /** @type {ShapeItem} */ (items[0].item); - assert(inputItem instanceof ShapeItem, "Input for rotation is not a shape"); - const inputDefinition = inputItem.definition; - - const rotatedDefinition = this.root.shapeDefinitionMgr.shapeActionRotateCW(inputDefinition); - outItems.push({ - item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(rotatedDefinition), - }); - break; - } - - // ROTATER (CCW) - case enumItemProcessorTypes.rotaterCCW: { - const inputItem = /** @type {ShapeItem} */ (items[0].item); - assert(inputItem instanceof ShapeItem, "Input for rotation is not a shape"); - const inputDefinition = inputItem.definition; - - const rotatedDefinition = this.root.shapeDefinitionMgr.shapeActionRotateCCW(inputDefinition); - outItems.push({ - item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(rotatedDefinition), - }); - break; - } - - // ROTATER (FL) - case enumItemProcessorTypes.rotaterFL: { - const inputItem = /** @type {ShapeItem} */ (items[0].item); - assert(inputItem instanceof ShapeItem, "Input for rotation is not a shape"); - const inputDefinition = inputItem.definition; - - const rotatedDefinition = this.root.shapeDefinitionMgr.shapeActionRotateFL(inputDefinition); - outItems.push({ - item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(rotatedDefinition), - }); - break; - } - - // STACKER - - case enumItemProcessorTypes.stacker: { - const lowerItem = /** @type {ShapeItem} */ (itemsBySlot[0].item); - const upperItem = /** @type {ShapeItem} */ (itemsBySlot[1].item); - - assert(lowerItem instanceof ShapeItem, "Input for lower stack is not a shape"); - assert(upperItem instanceof ShapeItem, "Input for upper stack is not a shape"); - - const stackedDefinition = this.root.shapeDefinitionMgr.shapeActionStack( - lowerItem.definition, - upperItem.definition - ); - outItems.push({ - item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(stackedDefinition), - }); - break; - } - - // TRASH - - case enumItemProcessorTypes.trash: { - // Well this one is easy .. simply do nothing with the item - break; - } - - // MIXER - - case enumItemProcessorTypes.mixer: { - // Find both colors and combine them - const item1 = /** @type {ColorItem} */ (items[0].item); - const item2 = /** @type {ColorItem} */ (items[1].item); - assert(item1 instanceof ColorItem, "Input for color mixer is not a color"); - assert(item2 instanceof ColorItem, "Input for color mixer is not a color"); - - const color1 = item1.color; - const color2 = item2.color; - - // Try finding mixer color, and if we can't mix it we simply return the same color - const mixedColor = enumColorMixingResults[color1][color2]; - let resultColor = color1; - if (mixedColor) { - resultColor = mixedColor; - } - outItems.push({ - item: COLOR_ITEM_SINGLETONS[resultColor], - }); - - break; - } - - // PAINTER - - case enumItemProcessorTypes.painter: { - const shapeItem = /** @type {ShapeItem} */ (itemsBySlot[0].item); - const colorItem = /** @type {ColorItem} */ (itemsBySlot[1].item); - - const colorizedDefinition = this.root.shapeDefinitionMgr.shapeActionPaintWith( - shapeItem.definition, - colorItem.color - ); - - outItems.push({ - item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(colorizedDefinition), - }); - - break; - } - - // PAINTER (DOUBLE) - - case enumItemProcessorTypes.painterDouble: { - const shapeItem1 = /** @type {ShapeItem} */ (itemsBySlot[0].item); - const shapeItem2 = /** @type {ShapeItem} */ (itemsBySlot[1].item); - const colorItem = /** @type {ColorItem} */ (itemsBySlot[2].item); - - assert(shapeItem1 instanceof ShapeItem, "Input for painter is not a shape"); - assert(shapeItem2 instanceof ShapeItem, "Input for painter is not a shape"); - assert(colorItem instanceof ColorItem, "Input for painter is not a color"); - - const colorizedDefinition1 = this.root.shapeDefinitionMgr.shapeActionPaintWith( - shapeItem1.definition, - colorItem.color - ); - - const colorizedDefinition2 = this.root.shapeDefinitionMgr.shapeActionPaintWith( - shapeItem2.definition, - colorItem.color - ); - outItems.push({ - item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(colorizedDefinition1), - }); - - outItems.push({ - item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(colorizedDefinition2), - }); - - break; - } - - // PAINTER (QUAD) - - case enumItemProcessorTypes.painterQuad: { - const shapeItem = /** @type {ShapeItem} */ (itemsBySlot[0].item); - assert(shapeItem instanceof ShapeItem, "Input for painter is not a shape"); - - /** @type {Array} */ - const colors = [null, null, null, null]; - for (let i = 0; i < 4; ++i) { - if (itemsBySlot[i + 1]) { - colors[i] = /** @type {ColorItem} */ (itemsBySlot[i + 1].item).color; - } - } - - const colorizedDefinition = this.root.shapeDefinitionMgr.shapeActionPaintWith4Colors( - shapeItem.definition, - /** @type {[string, string, string, string]} */ (colors) - ); - - outItems.push({ - item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(colorizedDefinition), - }); - break; - } - - // FILTER - case enumItemProcessorTypes.filter: { - // TODO - trackProduction = false; - - const item = itemsBySlot[0].item; - - const network = entity.components.WiredPins.slots[0].linkedNetwork; - if (!network || !network.currentValue) { - outItems.push({ - item, - requiredSlot: 1, - }); - break; - } - - const value = network.currentValue; - if (value.equals(BOOL_TRUE_SINGLETON) || value.equals(item)) { - outItems.push({ - item, - requiredSlot: 0, - }); - } else { - outItems.push({ - item, - requiredSlot: 1, - }); - } - - break; - } - - // READER - case enumItemProcessorTypes.reader: { - // Pass through the item - const item = itemsBySlot[0].item; - outItems.push({ item }); - - // Track the item - const readerComp = entity.components.BeltReader; - readerComp.lastItemTimes.push(this.root.time.now()); - readerComp.lastItem = item; - break; - } - - // HUB - case enumItemProcessorTypes.hub: { - trackProduction = false; - - const hubComponent = entity.components.Hub; - assert(hubComponent, "Hub item processor has no hub component"); - - for (let i = 0; i < items.length; ++i) { - const item = /** @type {ShapeItem} */ (items[i].item); - this.root.hubGoals.handleDefinitionDelivered(item.definition); - } - - break; - } - - default: - assertAlways(false, "Unkown item processor type: " + processorComp.type); - } + // Call implementation + handler({ + entity, + items, + itemsBySlot, + outItems, + }); // Track produced items - if (trackProduction) { - for (let i = 0; i < outItems.length; ++i) { + for (let i = 0; i < outItems.length; ++i) { + if (!outItems[i].doNotTrack) { this.root.signals.itemProduced.dispatch(outItems[i].item); } } @@ -553,28 +295,265 @@ export class ItemProcessorSystem extends GameSystemWithFilter { const bonusTimeToApply = Math.min(originalTime, processorComp.bonusTime); const timeToProcess = originalTime - bonusTimeToApply; - // Substract one tick because we already process it this frame - // if (processorComp.bonusTime > originalTime) { - // if (processorComp.type === enumItemProcessorTypes.reader) { - // console.log( - // "Bonus time", - // round4Digits(processorComp.bonusTime), - // "Original time", - // round4Digits(originalTime), - // "Overcomit by", - // round4Digits(processorComp.bonusTime - originalTime), - // "->", - // round4Digits(timeToProcess), - // "reduced by", - // round4Digits(bonusTimeToApply) - // ); - // } - // } processorComp.bonusTime -= bonusTimeToApply; - processorComp.ongoingCharges.push({ items: outItems, remainingTime: timeToProcess, }); } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_BALANCER(payload) { + const availableSlots = payload.entity.components.ItemEjector.slots.length; + const processorComp = payload.entity.components.ItemProcessor; + + const nextSlot = processorComp.nextOutputSlot++ % availableSlots; + + for (let i = 0; i < payload.items.length; ++i) { + payload.outItems.push({ + item: payload.items[i].item, + preferredSlot: (nextSlot + i) % availableSlots, + doNotTrack: true, + }); + } + return true; + } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_CUTTER(payload) { + const inputItem = /** @type {ShapeItem} */ (payload.items[0].item); + assert(inputItem instanceof ShapeItem, "Input for cut is not a shape"); + const inputDefinition = inputItem.definition; + + const cutDefinitions = this.root.shapeDefinitionMgr.shapeActionCutHalf(inputDefinition); + + for (let i = 0; i < cutDefinitions.length; ++i) { + const definition = cutDefinitions[i]; + if (!definition.isEntirelyEmpty()) { + payload.outItems.push({ + item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(definition), + requiredSlot: i, + }); + } + } + } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_CUTTER_QUAD(payload) { + const inputItem = /** @type {ShapeItem} */ (payload.items[0].item); + assert(inputItem instanceof ShapeItem, "Input for cut is not a shape"); + const inputDefinition = inputItem.definition; + + const cutDefinitions = this.root.shapeDefinitionMgr.shapeActionCutQuad(inputDefinition); + + for (let i = 0; i < cutDefinitions.length; ++i) { + const definition = cutDefinitions[i]; + if (!definition.isEntirelyEmpty()) { + payload.outItems.push({ + item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(definition), + requiredSlot: i, + }); + } + } + } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_ROTATER(payload) { + const inputItem = /** @type {ShapeItem} */ (payload.items[0].item); + assert(inputItem instanceof ShapeItem, "Input for rotation is not a shape"); + const inputDefinition = inputItem.definition; + + const rotatedDefinition = this.root.shapeDefinitionMgr.shapeActionRotateCW(inputDefinition); + payload.outItems.push({ + item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(rotatedDefinition), + }); + } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_ROTATER_CCW(payload) { + const inputItem = /** @type {ShapeItem} */ (payload.items[0].item); + assert(inputItem instanceof ShapeItem, "Input for rotation is not a shape"); + const inputDefinition = inputItem.definition; + + const rotatedDefinition = this.root.shapeDefinitionMgr.shapeActionRotateCCW(inputDefinition); + payload.outItems.push({ + item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(rotatedDefinition), + }); + } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_ROTATER_180(payload) { + const inputItem = /** @type {ShapeItem} */ (payload.items[0].item); + assert(inputItem instanceof ShapeItem, "Input for rotation is not a shape"); + const inputDefinition = inputItem.definition; + + const rotatedDefinition = this.root.shapeDefinitionMgr.shapeActionRotate180(inputDefinition); + payload.outItems.push({ + item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(rotatedDefinition), + }); + } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_STACKER(payload) { + const lowerItem = /** @type {ShapeItem} */ (payload.itemsBySlot[0]); + const upperItem = /** @type {ShapeItem} */ (payload.itemsBySlot[1]); + + assert(lowerItem instanceof ShapeItem, "Input for lower stack is not a shape"); + assert(upperItem instanceof ShapeItem, "Input for upper stack is not a shape"); + + const stackedDefinition = this.root.shapeDefinitionMgr.shapeActionStack( + lowerItem.definition, + upperItem.definition + ); + payload.outItems.push({ + item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(stackedDefinition), + }); + } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_TRASH(payload) { + // Do nothing .. + } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_MIXER(payload) { + // Find both colors and combine them + const item1 = /** @type {ColorItem} */ (payload.items[0].item); + const item2 = /** @type {ColorItem} */ (payload.items[1].item); + assert(item1 instanceof ColorItem, "Input for color mixer is not a color"); + assert(item2 instanceof ColorItem, "Input for color mixer is not a color"); + + const color1 = item1.color; + const color2 = item2.color; + + // Try finding mixer color, and if we can't mix it we simply return the same color + const mixedColor = enumColorMixingResults[color1][color2]; + let resultColor = color1; + if (mixedColor) { + resultColor = mixedColor; + } + payload.outItems.push({ + item: COLOR_ITEM_SINGLETONS[resultColor], + }); + } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_PAINTER(payload) { + const shapeItem = /** @type {ShapeItem} */ (payload.itemsBySlot[0]); + const colorItem = /** @type {ColorItem} */ (payload.itemsBySlot[1]); + + const colorizedDefinition = this.root.shapeDefinitionMgr.shapeActionPaintWith( + shapeItem.definition, + colorItem.color + ); + + payload.outItems.push({ + item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(colorizedDefinition), + }); + } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_PAINTER_DOUBLE(payload) { + const shapeItem1 = /** @type {ShapeItem} */ (payload.itemsBySlot[0]); + const shapeItem2 = /** @type {ShapeItem} */ (payload.itemsBySlot[1]); + const colorItem = /** @type {ColorItem} */ (payload.itemsBySlot[2]); + + assert(shapeItem1 instanceof ShapeItem, "Input for painter is not a shape"); + assert(shapeItem2 instanceof ShapeItem, "Input for painter is not a shape"); + assert(colorItem instanceof ColorItem, "Input for painter is not a color"); + + const colorizedDefinition1 = this.root.shapeDefinitionMgr.shapeActionPaintWith( + shapeItem1.definition, + colorItem.color + ); + + const colorizedDefinition2 = this.root.shapeDefinitionMgr.shapeActionPaintWith( + shapeItem2.definition, + colorItem.color + ); + payload.outItems.push({ + item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(colorizedDefinition1), + }); + + payload.outItems.push({ + item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(colorizedDefinition2), + }); + } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_PAINTER_QUAD(payload) { + const shapeItem = /** @type {ShapeItem} */ (payload.itemsBySlot[0]); + assert(shapeItem instanceof ShapeItem, "Input for painter is not a shape"); + + /** @type {Array} */ + const colors = [null, null, null, null]; + for (let i = 0; i < 4; ++i) { + if (payload.itemsBySlot[i + 1]) { + colors[i] = /** @type {ColorItem} */ (payload.itemsBySlot[i + 1]).color; + } + } + + const colorizedDefinition = this.root.shapeDefinitionMgr.shapeActionPaintWith4Colors( + shapeItem.definition, + /** @type {[string, string, string, string]} */ (colors) + ); + + payload.outItems.push({ + item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(colorizedDefinition), + }); + } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_READER(payload) { + // Pass through the item + const item = payload.itemsBySlot[0]; + payload.outItems.push({ + item, + doNotTrack: true, + }); + + // Track the item + const readerComp = payload.entity.components.BeltReader; + readerComp.lastItemTimes.push(this.root.time.now()); + readerComp.lastItem = item; + } + + /** + * @param {ProcessorImplementationPayload} payload + */ + process_HUB(payload) { + const hubComponent = payload.entity.components.Hub; + assert(hubComponent, "Hub item processor has no hub component"); + + for (let i = 0; i < payload.items.length; ++i) { + const item = /** @type {ShapeItem} */ (payload.items[i].item); + this.root.hubGoals.handleDefinitionDelivered(item.definition); + } + } } diff --git a/src/js/game/systems/item_processor_overlays.js b/src/js/game/systems/item_processor_overlays.js index 2ec91b88..3ba44c7b 100644 --- a/src/js/game/systems/item_processor_overlays.js +++ b/src/js/game/systems/item_processor_overlays.js @@ -1,6 +1,6 @@ import { globalConfig } from "../../core/config"; import { Loader } from "../../core/loader"; -import { smoothPulse, round4Digits } from "../../core/utils"; +import { smoothPulse } from "../../core/utils"; import { enumItemProcessorRequirements, enumItemProcessorTypes } from "../components/item_processor"; import { Entity } from "../entity"; import { GameSystem } from "../game_system"; @@ -17,7 +17,6 @@ export class ItemProcessorOverlaysSystem extends GameSystem { this.readerOverlaySprite = Loader.getSprite("sprites/misc/reader_overlay.png"); this.drawnUids = new Set(); - this.root.signals.gameFrameStarted.add(this.clearDrawnUids, this); } @@ -35,35 +34,40 @@ export class ItemProcessorOverlaysSystem extends GameSystem { for (let i = 0; i < contents.length; ++i) { const entity = contents[i]; const processorComp = entity.components.ItemProcessor; - if (!processorComp) { - continue; - } + const filterComp = entity.components.Filter; - const requirement = processorComp.processingRequirement; - - if (!requirement && processorComp.type !== enumItemProcessorTypes.reader) { - continue; - } - - if (this.drawnUids.has(entity.uid)) { - continue; - } - - this.drawnUids.add(entity.uid); - - switch (requirement) { - case enumItemProcessorRequirements.painterQuad: { - this.drawConnectedSlotRequirement(parameters, entity, { drawIfFalse: true }); - break; + // Draw processor overlays + if (processorComp) { + const requirement = processorComp.processingRequirement; + if (!requirement && processorComp.type !== enumItemProcessorTypes.reader) { + continue; } - case enumItemProcessorRequirements.filter: { - this.drawConnectedSlotRequirement(parameters, entity, { drawIfFalse: false }); - break; + + if (this.drawnUids.has(entity.uid)) { + continue; + } + this.drawnUids.add(entity.uid); + + switch (requirement) { + case enumItemProcessorRequirements.painterQuad: { + this.drawConnectedSlotRequirement(parameters, entity, { drawIfFalse: true }); + break; + } + } + + if (processorComp.type === enumItemProcessorTypes.reader) { + this.drawReaderOverlays(parameters, entity); } } - if (processorComp.type === enumItemProcessorTypes.reader) { - this.drawReaderOverlays(parameters, entity); + // Draw filter overlays + else if (filterComp) { + if (this.drawnUids.has(entity.uid)) { + continue; + } + this.drawnUids.add(entity.uid); + + this.drawConnectedSlotRequirement(parameters, entity, { drawIfFalse: false }); } } } @@ -113,7 +117,7 @@ export class ItemProcessorOverlaysSystem extends GameSystem { for (let i = 0; i < pinsComp.slots.length; ++i) { const slot = pinsComp.slots[i]; const network = slot.linkedNetwork; - if (network && network.currentValue) { + if (network && network.hasValue()) { anySlotConnected = true; if (isTruthyItem(network.currentValue) || !drawIfFalse) { diff --git a/src/js/game/systems/logic_gate.js b/src/js/game/systems/logic_gate.js index 3bfc20cd..cd2d7dfb 100644 --- a/src/js/game/systems/logic_gate.js +++ b/src/js/game/systems/logic_gate.js @@ -7,6 +7,7 @@ import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON, isTruthyItem, BooleanItem } import { COLOR_ITEM_SINGLETONS, ColorItem } from "../items/color_item"; import { ShapeDefinition } from "../shape_definition"; import { ShapeItem } from "../items/shape_item"; +import { enumInvertedDirections } from "../../core/vector"; export class LogicGateSystem extends GameSystemWithFilter { constructor(root) { @@ -24,6 +25,8 @@ export class LogicGateSystem extends GameSystemWithFilter { [enumLogicGateType.cutter]: this.compute_CUT.bind(this), [enumLogicGateType.unstacker]: this.compute_UNSTACK.bind(this), [enumLogicGateType.shapecompare]: this.compute_SHAPECOMPARE.bind(this), + [enumLogicGateType.stacker]: this.compute_STACKER.bind(this), + [enumLogicGateType.painter]: this.compute_PAINTER.bind(this), }; } @@ -44,13 +47,13 @@ export class LogicGateSystem extends GameSystemWithFilter { if (slot.type !== enumPinSlotType.logicalAcceptor) { continue; } - if (slot.linkedNetwork) { - if (slot.linkedNetwork.valueConflict) { + const network = slot.linkedNetwork; + if (network) { + if (network.valueConflict) { anyConflict = true; break; } - - slotValues.push(slot.linkedNetwork.currentValue); + slotValues.push(network.currentValue); } else { slotValues.push(null); } @@ -259,6 +262,58 @@ export class LogicGateSystem extends GameSystemWithFilter { ]; } + /** + * @param {Array} parameters + * @returns {BaseItem} + */ + compute_STACKER(parameters) { + const lowerItem = parameters[0]; + const upperItem = parameters[1]; + + if (!lowerItem || !upperItem) { + // Empty + return null; + } + + if (lowerItem.getItemType() !== "shape" || upperItem.getItemType() !== "shape") { + // Bad type + return null; + } + + const stackedShape = this.root.shapeDefinitionMgr.shapeActionStack( + /** @type {ShapeItem} */ (lowerItem).definition, + /** @type {ShapeItem} */ (upperItem).definition + ); + + return this.root.shapeDefinitionMgr.getShapeItemFromDefinition(stackedShape); + } + + /** + * @param {Array} parameters + * @returns {BaseItem} + */ + compute_PAINTER(parameters) { + const shape = parameters[0]; + const color = parameters[1]; + + if (!shape || !color) { + // Empty + return null; + } + + if (shape.getItemType() !== "shape" || color.getItemType() !== "color") { + // Bad type + return null; + } + + const coloredShape = this.root.shapeDefinitionMgr.shapeActionPaintWith( + /** @type {ShapeItem} */ (shape).definition, + /** @type {ColorItem} */ (color).color + ); + + return this.root.shapeDefinitionMgr.getShapeItemFromDefinition(coloredShape); + } + /** * @param {Array} parameters * @returns {BaseItem} @@ -269,7 +324,7 @@ export class LogicGateSystem extends GameSystemWithFilter { if (!itemA || !itemB) { // Empty - return BOOL_FALSE_SINGLETON; + return null; } if (itemA.getItemType() !== itemB.getItemType()) { diff --git a/src/js/game/systems/map_resources.js b/src/js/game/systems/map_resources.js index c368e684..807afb36 100644 --- a/src/js/game/systems/map_resources.js +++ b/src/js/game/systems/map_resources.js @@ -49,11 +49,20 @@ export class MapResourcesSystem extends GameSystem { } else { // HIGH QUALITY: Draw all items const layer = chunk.lowerLayer; + const layerEntities = chunk.contents; for (let x = 0; x < globalConfig.mapChunkSize; ++x) { const row = layer[x]; + const rowEntities = layerEntities[x]; const worldX = (chunk.tileX + x) * globalConfig.tileSize; for (let y = 0; y < globalConfig.mapChunkSize; ++y) { const lowerItem = row[y]; + + const entity = rowEntities[y]; + if (entity) { + // Don't draw if there is an entity above + continue; + } + if (lowerItem) { const worldY = (chunk.tileY + y) * globalConfig.tileSize; diff --git a/src/js/game/systems/miner.js b/src/js/game/systems/miner.js index 94f2791e..4ebc6f7b 100644 --- a/src/js/game/systems/miner.js +++ b/src/js/game/systems/miner.js @@ -5,6 +5,7 @@ import { BaseItem } from "../base_item"; import { MinerComponent } from "../components/miner"; import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; +import { statisticsUnitsSeconds } from "../hud/parts/statistics_handle"; import { MapChunkView } from "../map_chunk_view"; export class MinerSystem extends GameSystemWithFilter { @@ -46,7 +47,6 @@ export class MinerSystem extends GameSystemWithFilter { } // Check if miner is above an actual tile - if (!minerComp.cachedMinedItem) { const staticComp = entity.components.StaticMapEntity; const tileBelow = this.root.map.getLowerLayerContentXY( @@ -94,6 +94,11 @@ export class MinerSystem extends GameSystemWithFilter { findChainedMiner(entity) { const ejectComp = entity.components.ItemEjector; const staticComp = entity.components.StaticMapEntity; + const contentsBelow = this.root.map.getLowerLayerContentXY(staticComp.origin.x, staticComp.origin.y); + if (!contentsBelow) { + // This miner has no contents + return null; + } const ejectingSlot = ejectComp.slots[0]; const ejectingPos = staticComp.localTileToWorld(ejectingSlot.pos); @@ -106,7 +111,10 @@ export class MinerSystem extends GameSystemWithFilter { if (targetContents) { const targetMinerComp = targetContents.components.Miner; if (targetMinerComp && targetMinerComp.chainable) { - return targetContents; + const targetLowerLayer = this.root.map.getLowerLayerContentXY(targetTile.x, targetTile.y); + if (targetLowerLayer) { + return targetContents; + } } } @@ -171,7 +179,7 @@ export class MinerSystem extends GameSystemWithFilter { } // Draw the item background - this is to hide the ejected item animation from - // the item ejecto + // the item ejector const padding = 3; const destX = staticComp.origin.x * globalConfig.tileSize + padding; diff --git a/src/js/game/systems/storage.js b/src/js/game/systems/storage.js index 5a2b57bb..80affac9 100644 --- a/src/js/game/systems/storage.js +++ b/src/js/game/systems/storage.js @@ -1,101 +1,101 @@ -import { GameSystemWithFilter } from "../game_system_with_filter"; -import { StorageComponent } from "../components/storage"; -import { DrawParameters } from "../../core/draw_parameters"; -import { formatBigNumber, lerp } from "../../core/utils"; -import { Loader } from "../../core/loader"; -import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item"; -import { MapChunkView } from "../map_chunk_view"; - -export class StorageSystem extends GameSystemWithFilter { - constructor(root) { - super(root, [StorageComponent]); - - this.storageOverlaySprite = Loader.getSprite("sprites/misc/storage_overlay.png"); - - /** - * Stores which uids were already drawn to avoid drawing entities twice - * @type {Set} - */ - this.drawnUids = new Set(); - - this.root.signals.gameFrameStarted.add(this.clearDrawnUids, this); - } - - clearDrawnUids() { - this.drawnUids.clear(); - } - - update() { - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - const storageComp = entity.components.Storage; - const pinsComp = entity.components.WiredPins; - - // Eject from storage - if (storageComp.storedItem && storageComp.storedCount > 0) { - const ejectorComp = entity.components.ItemEjector; - - const nextSlot = ejectorComp.getFirstFreeSlot(); - if (nextSlot !== null) { - if (ejectorComp.tryEject(nextSlot, storageComp.storedItem)) { - storageComp.storedCount--; - - if (storageComp.storedCount === 0) { - storageComp.storedItem = null; - } - } - } - } - - let targetAlpha = storageComp.storedCount > 0 ? 1 : 0; - storageComp.overlayOpacity = lerp(storageComp.overlayOpacity, targetAlpha, 0.05); - - pinsComp.slots[0].value = storageComp.storedItem; - pinsComp.slots[1].value = storageComp.getIsFull() ? BOOL_TRUE_SINGLETON : BOOL_FALSE_SINGLETON; - } - } - - /** - * @param {DrawParameters} parameters - * @param {MapChunkView} chunk - */ - drawChunk(parameters, chunk) { - const contents = chunk.containedEntitiesByLayer.regular; - for (let i = 0; i < contents.length; ++i) { - const entity = contents[i]; - const storageComp = entity.components.Storage; - if (!storageComp) { - continue; - } - - const storedItem = storageComp.storedItem; - if (!storedItem) { - continue; - } - - if (this.drawnUids.has(entity.uid)) { - continue; - } - - this.drawnUids.add(entity.uid); - - const staticComp = entity.components.StaticMapEntity; - - const context = parameters.context; - context.globalAlpha = storageComp.overlayOpacity; - const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); - storedItem.drawItemCenteredClipped(center.x, center.y, parameters, 30); - - this.storageOverlaySprite.drawCached(parameters, center.x - 15, center.y + 15, 30, 15); - - if (parameters.visibleRect.containsCircle(center.x, center.y + 25, 20)) { - context.font = "bold 10px GameFont"; - context.textAlign = "center"; - context.fillStyle = "#64666e"; - context.fillText(formatBigNumber(storageComp.storedCount), center.x, center.y + 25.5); - context.textAlign = "left"; - } - context.globalAlpha = 1; - } - } -} +import { GameSystemWithFilter } from "../game_system_with_filter"; +import { StorageComponent } from "../components/storage"; +import { DrawParameters } from "../../core/draw_parameters"; +import { formatBigNumber, lerp } from "../../core/utils"; +import { Loader } from "../../core/loader"; +import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item"; +import { MapChunkView } from "../map_chunk_view"; + +export class StorageSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [StorageComponent]); + + this.storageOverlaySprite = Loader.getSprite("sprites/misc/storage_overlay.png"); + + /** + * Stores which uids were already drawn to avoid drawing entities twice + * @type {Set} + */ + this.drawnUids = new Set(); + + this.root.signals.gameFrameStarted.add(this.clearDrawnUids, this); + } + + clearDrawnUids() { + this.drawnUids.clear(); + } + + update() { + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + const storageComp = entity.components.Storage; + const pinsComp = entity.components.WiredPins; + + // Eject from storage + if (storageComp.storedItem && storageComp.storedCount > 0) { + const ejectorComp = entity.components.ItemEjector; + + const nextSlot = ejectorComp.getFirstFreeSlot(); + if (nextSlot !== null) { + if (ejectorComp.tryEject(nextSlot, storageComp.storedItem)) { + storageComp.storedCount--; + + if (storageComp.storedCount === 0) { + storageComp.storedItem = null; + } + } + } + } + + let targetAlpha = storageComp.storedCount > 0 ? 1 : 0; + storageComp.overlayOpacity = lerp(storageComp.overlayOpacity, targetAlpha, 0.05); + + pinsComp.slots[0].value = storageComp.storedItem; + pinsComp.slots[1].value = storageComp.getIsFull() ? BOOL_TRUE_SINGLETON : BOOL_FALSE_SINGLETON; + } + } + + /** + * @param {DrawParameters} parameters + * @param {MapChunkView} chunk + */ + drawChunk(parameters, chunk) { + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + const storageComp = entity.components.Storage; + if (!storageComp) { + continue; + } + + const storedItem = storageComp.storedItem; + if (!storedItem) { + continue; + } + + if (this.drawnUids.has(entity.uid)) { + continue; + } + + this.drawnUids.add(entity.uid); + + const staticComp = entity.components.StaticMapEntity; + + const context = parameters.context; + context.globalAlpha = storageComp.overlayOpacity; + const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); + storedItem.drawItemCenteredClipped(center.x, center.y, parameters, 30); + + this.storageOverlaySprite.drawCached(parameters, center.x - 15, center.y + 15, 30, 15); + + if (parameters.visibleRect.containsCircle(center.x, center.y + 25, 20)) { + context.font = "bold 10px GameFont"; + context.textAlign = "center"; + context.fillStyle = "#64666e"; + context.fillText(formatBigNumber(storageComp.storedCount), center.x, center.y + 25.5); + context.textAlign = "left"; + } + context.globalAlpha = 1; + } + } +} diff --git a/src/js/game/systems/underground_belt.js b/src/js/game/systems/underground_belt.js index 90d29e50..7a7609f8 100644 --- a/src/js/game/systems/underground_belt.js +++ b/src/js/game/systems/underground_belt.js @@ -1,406 +1,349 @@ -import { globalConfig } from "../../core/config"; -import { Loader } from "../../core/loader"; -import { createLogger } from "../../core/logging"; -import { Rectangle } from "../../core/rectangle"; -import { - enumAngleToDirection, - enumDirection, - enumDirectionToAngle, - enumDirectionToVector, - enumInvertedDirections, -} from "../../core/vector"; -import { enumUndergroundBeltMode, UndergroundBeltComponent } from "../components/underground_belt"; -import { Entity } from "../entity"; -import { GameSystemWithFilter } from "../game_system_with_filter"; -import { fastArrayDelete } from "../../core/utils"; - -const logger = createLogger("tunnels"); - -export class UndergroundBeltSystem extends GameSystemWithFilter { - constructor(root) { - super(root, [UndergroundBeltComponent]); - - this.beltSprites = { - [enumUndergroundBeltMode.sender]: Loader.getSprite( - "sprites/buildings/underground_belt_entry.png" - ), - [enumUndergroundBeltMode.receiver]: Loader.getSprite( - "sprites/buildings/underground_belt_exit.png" - ), - }; - - this.root.signals.entityManuallyPlaced.add(this.onEntityManuallyPlaced, this); - - /** - * @type {Rectangle} - */ - this.areaToRecompute = null; - - this.root.signals.entityAdded.add(this.onEntityChanged, this); - this.root.signals.entityDestroyed.add(this.onEntityChanged, this); - } - - /** - * Called when an entity got added or removed - * @param {Entity} entity - */ - onEntityChanged(entity) { - if (!this.root.gameInitialized) { - return; - } - const undergroundComp = entity.components.UndergroundBelt; - if (!undergroundComp) { - return; - } - - const affectedArea = entity.components.StaticMapEntity.getTileSpaceBounds().expandedInAllDirections( - globalConfig.undergroundBeltMaxTilesByTier[ - globalConfig.undergroundBeltMaxTilesByTier.length - 1 - ] + 1 - ); - - if (this.areaToRecompute) { - this.areaToRecompute = this.areaToRecompute.getUnion(affectedArea); - } else { - this.areaToRecompute = affectedArea; - } - } - - /** - * Callback when an entity got placed, used to remove belts between underground belts - * @param {Entity} entity - */ - onEntityManuallyPlaced(entity) { - if (!this.root.app.settings.getAllSettings().enableTunnelSmartplace) { - // Smart-place disabled - return; - } - - const undergroundComp = entity.components.UndergroundBelt; - if (undergroundComp && undergroundComp.mode === enumUndergroundBeltMode.receiver) { - const staticComp = entity.components.StaticMapEntity; - const tile = staticComp.origin; - - const direction = enumAngleToDirection[staticComp.rotation]; - const inverseDirection = enumInvertedDirections[direction]; - const offset = enumDirectionToVector[inverseDirection]; - - let currentPos = tile.copy(); - - const tier = undergroundComp.tier; - const range = globalConfig.undergroundBeltMaxTilesByTier[tier]; - - // FIND ENTRANCE - // Search for the entrance which is furthes apart (this is why we can't reuse logic here) - let matchingEntrance = null; - for (let i = 0; i < range; ++i) { - currentPos.addInplace(offset); - const contents = this.root.map.getTileContent(currentPos, entity.layer); - if (!contents) { - continue; - } - - const contentsUndergroundComp = contents.components.UndergroundBelt; - const contentsStaticComp = contents.components.StaticMapEntity; - if ( - contentsUndergroundComp && - contentsUndergroundComp.tier === undergroundComp.tier && - contentsUndergroundComp.mode === enumUndergroundBeltMode.sender && - enumAngleToDirection[contentsStaticComp.rotation] === direction - ) { - matchingEntrance = { - entity: contents, - range: i, - }; - } - } - - if (!matchingEntrance) { - // Nothing found - return; - } - - // DETECT OBSOLETE BELTS BETWEEN - // Remove any belts between entrance and exit which have the same direction, - // but only if they *all* have the right direction - currentPos = tile.copy(); - let allBeltsMatch = true; - for (let i = 0; i < matchingEntrance.range; ++i) { - currentPos.addInplace(offset); - - const contents = this.root.map.getTileContent(currentPos, entity.layer); - if (!contents) { - allBeltsMatch = false; - break; - } - - const contentsStaticComp = contents.components.StaticMapEntity; - const contentsBeltComp = contents.components.Belt; - if (!contentsBeltComp) { - allBeltsMatch = false; - break; - } - - // It's a belt - if ( - contentsBeltComp.direction !== enumDirection.top || - enumAngleToDirection[contentsStaticComp.rotation] !== direction - ) { - allBeltsMatch = false; - break; - } - } - - currentPos = tile.copy(); - if (allBeltsMatch) { - // All belts between this are obsolete, so drop them - for (let i = 0; i < matchingEntrance.range; ++i) { - currentPos.addInplace(offset); - const contents = this.root.map.getTileContent(currentPos, entity.layer); - assert(contents, "Invalid smart underground belt logic"); - this.root.logic.tryDeleteBuilding(contents); - } - } - - // REMOVE OBSOLETE TUNNELS - // Remove any double tunnels, by checking the tile plus the tile above - currentPos = tile.copy().add(offset); - for (let i = 0; i < matchingEntrance.range - 1; ++i) { - const posBefore = currentPos.copy(); - currentPos.addInplace(offset); - - const entityBefore = this.root.map.getTileContent(posBefore, entity.layer); - const entityAfter = this.root.map.getTileContent(currentPos, entity.layer); - - if (!entityBefore || !entityAfter) { - continue; - } - - const undergroundBefore = entityBefore.components.UndergroundBelt; - const undergroundAfter = entityAfter.components.UndergroundBelt; - - if (!undergroundBefore || !undergroundAfter) { - // Not an underground belt - continue; - } - - if ( - // Both same tier - undergroundBefore.tier !== undergroundAfter.tier || - // And same tier as our original entity - undergroundBefore.tier !== undergroundComp.tier - ) { - // Mismatching tier - continue; - } - - if ( - undergroundBefore.mode !== enumUndergroundBeltMode.sender || - undergroundAfter.mode !== enumUndergroundBeltMode.receiver - ) { - // Not the right mode - continue; - } - - // Check rotations - const staticBefore = entityBefore.components.StaticMapEntity; - const staticAfter = entityAfter.components.StaticMapEntity; - - if ( - enumAngleToDirection[staticBefore.rotation] !== direction || - enumAngleToDirection[staticAfter.rotation] !== direction - ) { - // Wrong rotation - continue; - } - - // All good, can remove - this.root.logic.tryDeleteBuilding(entityBefore); - this.root.logic.tryDeleteBuilding(entityAfter); - } - } - } - - /** - * Recomputes the cache in the given area, invalidating all entries there - */ - recomputeArea() { - const area = this.areaToRecompute; - logger.log("Recomputing area:", area.x, area.y, "/", area.w, area.h); - if (G_IS_DEV && globalConfig.debug.renderChanges) { - this.root.hud.parts.changesDebugger.renderChange("tunnels", this.areaToRecompute, "#fc03be"); - } - - for (let x = area.x; x < area.right(); ++x) { - for (let y = area.y; y < area.bottom(); ++y) { - const entities = this.root.map.getLayersContentsMultipleXY(x, y); - for (let i = 0; i < entities.length; ++i) { - const entity = entities[i]; - const undergroundComp = entity.components.UndergroundBelt; - if (!undergroundComp) { - continue; - } - - undergroundComp.cachedLinkedEntity = null; - } - } - } - } - - update() { - if (this.areaToRecompute) { - this.recomputeArea(); - this.areaToRecompute = null; - } - - const delta = this.root.dynamicTickrate.deltaSeconds; - - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - const undergroundComp = entity.components.UndergroundBelt; - const pendingItems = undergroundComp.pendingItems; - - // Decrease remaining time of all items in belt - for (let k = 0; k < pendingItems.length; ++k) { - const item = pendingItems[k]; - item[1] = Math.max(0, item[1] - delta); - if (G_IS_DEV && globalConfig.debug.instantBelts) { - item[1] = 0; - } - } - if (undergroundComp.mode === enumUndergroundBeltMode.sender) { - this.handleSender(entity); - } else { - this.handleReceiver(entity); - } - } - } - - /** - * Finds the receiver for a given sender - * @param {Entity} entity - * @returns {import("../components/underground_belt").LinkedUndergroundBelt} - */ - findRecieverForSender(entity) { - const staticComp = entity.components.StaticMapEntity; - const undergroundComp = entity.components.UndergroundBelt; - const searchDirection = staticComp.localDirectionToWorld(enumDirection.top); - const searchVector = enumDirectionToVector[searchDirection]; - const targetRotation = enumDirectionToAngle[searchDirection]; - let currentTile = staticComp.origin; - - // Search in the direction of the tunnel - for ( - let searchOffset = 0; - searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier]; - ++searchOffset - ) { - currentTile = currentTile.add(searchVector); - - const potentialReceiver = this.root.map.getTileContent(currentTile, "regular"); - if (!potentialReceiver) { - // Empty tile - continue; - } - const receiverUndergroundComp = potentialReceiver.components.UndergroundBelt; - if (!receiverUndergroundComp || receiverUndergroundComp.tier !== undergroundComp.tier) { - // Not a tunnel, or not on the same tier - continue; - } - - const receiverStaticComp = potentialReceiver.components.StaticMapEntity; - if (receiverStaticComp.rotation !== targetRotation) { - // Wrong rotation - continue; - } - - if (receiverUndergroundComp.mode !== enumUndergroundBeltMode.receiver) { - // Not a receiver, but a sender -> Abort to make sure we don't deliver double - break; - } - - return { entity: potentialReceiver, distance: searchOffset }; - } - - // None found - return { entity: null, distance: 0 }; - } - - /** - * - * @param {Entity} entity - */ - handleSender(entity) { - const undergroundComp = entity.components.UndergroundBelt; - - // Find the current receiver - let receiver = undergroundComp.cachedLinkedEntity; - if (!receiver) { - // We don't have a receiver, compute it - receiver = undergroundComp.cachedLinkedEntity = this.findRecieverForSender(entity); - - if (G_IS_DEV && globalConfig.debug.renderChanges) { - this.root.hud.parts.changesDebugger.renderChange( - "sender", - entity.components.StaticMapEntity.getTileSpaceBounds(), - "#fc03be" - ); - } - } - - if (!receiver.entity) { - // If there is no connection to a receiver, ignore this one - return; - } - - // Check if we have any item - if (undergroundComp.pendingItems.length > 0) { - assert(undergroundComp.pendingItems.length === 1, "more than 1 pending"); - const nextItemAndDuration = undergroundComp.pendingItems[0]; - const remainingTime = nextItemAndDuration[1]; - const nextItem = nextItemAndDuration[0]; - - // Check if the item is ready to be emitted - if (remainingTime === 0) { - // Check if the receiver can accept it - if ( - receiver.entity.components.UndergroundBelt.tryAcceptTunneledItem( - nextItem, - receiver.distance, - this.root.hubGoals.getUndergroundBeltBaseSpeed() - ) - ) { - // Drop this item - fastArrayDelete(undergroundComp.pendingItems, 0); - } - } - } - } - - /** - * - * @param {Entity} entity - */ - handleReceiver(entity) { - const undergroundComp = entity.components.UndergroundBelt; - - // Try to eject items, we only check the first one because it is sorted by remaining time - const items = undergroundComp.pendingItems; - if (items.length > 0) { - const nextItemAndDuration = undergroundComp.pendingItems[0]; - const remainingTime = nextItemAndDuration[1]; - const nextItem = nextItemAndDuration[0]; - - if (remainingTime <= 0) { - const ejectorComp = entity.components.ItemEjector; - - const nextSlotIndex = ejectorComp.getFirstFreeSlot(); - if (nextSlotIndex !== null) { - if (ejectorComp.tryEject(nextSlotIndex, nextItem)) { - items.shift(); - } - } - } - } - } -} +import { globalConfig } from "../../core/config"; +import { Loader } from "../../core/loader"; +import { createLogger } from "../../core/logging"; +import { Rectangle } from "../../core/rectangle"; +import { StaleAreaDetector } from "../../core/stale_area_detector"; +import { fastArrayDelete } from "../../core/utils"; +import { + enumAngleToDirection, + enumDirection, + enumDirectionToAngle, + enumDirectionToVector, + enumInvertedDirections, +} from "../../core/vector"; +import { enumUndergroundBeltMode, UndergroundBeltComponent } from "../components/underground_belt"; +import { Entity } from "../entity"; +import { GameSystemWithFilter } from "../game_system_with_filter"; + +const logger = createLogger("tunnels"); + +export class UndergroundBeltSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [UndergroundBeltComponent]); + + this.beltSprites = { + [enumUndergroundBeltMode.sender]: Loader.getSprite( + "sprites/buildings/underground_belt_entry.png" + ), + [enumUndergroundBeltMode.receiver]: Loader.getSprite( + "sprites/buildings/underground_belt_exit.png" + ), + }; + + this.staleAreaWatcher = new StaleAreaDetector({ + root: this.root, + name: "underground-belt", + recomputeMethod: this.recomputeArea.bind(this), + }); + + this.root.signals.entityManuallyPlaced.add(this.onEntityManuallyPlaced, this); + + // NOTICE: Once we remove a tunnel, we need to update the whole area to + // clear outdated handles + this.staleAreaWatcher.recomputeOnComponentsChanged( + [UndergroundBeltComponent], + globalConfig.undergroundBeltMaxTilesByTier[globalConfig.undergroundBeltMaxTilesByTier.length - 1] + ); + } + + /** + * Callback when an entity got placed, used to remove belts between underground belts + * @param {Entity} entity + */ + onEntityManuallyPlaced(entity) { + if (!this.root.app.settings.getAllSettings().enableTunnelSmartplace) { + // Smart-place disabled + return; + } + + const undergroundComp = entity.components.UndergroundBelt; + if (undergroundComp && undergroundComp.mode === enumUndergroundBeltMode.receiver) { + const staticComp = entity.components.StaticMapEntity; + const tile = staticComp.origin; + + const direction = enumAngleToDirection[staticComp.rotation]; + const inverseDirection = enumInvertedDirections[direction]; + const offset = enumDirectionToVector[inverseDirection]; + + let currentPos = tile.copy(); + + const tier = undergroundComp.tier; + const range = globalConfig.undergroundBeltMaxTilesByTier[tier]; + + // FIND ENTRANCE + // Search for the entrance which is farthest apart (this is why we can't reuse logic here) + let matchingEntrance = null; + for (let i = 0; i < range; ++i) { + currentPos.addInplace(offset); + const contents = this.root.map.getTileContent(currentPos, entity.layer); + if (!contents) { + continue; + } + + const contentsUndergroundComp = contents.components.UndergroundBelt; + const contentsStaticComp = contents.components.StaticMapEntity; + if ( + contentsUndergroundComp && + contentsUndergroundComp.tier === undergroundComp.tier && + contentsUndergroundComp.mode === enumUndergroundBeltMode.sender && + enumAngleToDirection[contentsStaticComp.rotation] === direction + ) { + matchingEntrance = { + entity: contents, + range: i, + }; + } + } + + if (!matchingEntrance) { + // Nothing found + return; + } + + // DETECT OBSOLETE BELTS BETWEEN + // Remove any belts between entrance and exit which have the same direction, + // but only if they *all* have the right direction + currentPos = tile.copy(); + let allBeltsMatch = true; + for (let i = 0; i < matchingEntrance.range; ++i) { + currentPos.addInplace(offset); + + const contents = this.root.map.getTileContent(currentPos, entity.layer); + if (!contents) { + allBeltsMatch = false; + break; + } + + const contentsStaticComp = contents.components.StaticMapEntity; + const contentsBeltComp = contents.components.Belt; + if (!contentsBeltComp) { + allBeltsMatch = false; + break; + } + + // It's a belt + if ( + contentsBeltComp.direction !== enumDirection.top || + enumAngleToDirection[contentsStaticComp.rotation] !== direction + ) { + allBeltsMatch = false; + break; + } + } + + currentPos = tile.copy(); + if (allBeltsMatch) { + // All belts between this are obsolete, so drop them + for (let i = 0; i < matchingEntrance.range; ++i) { + currentPos.addInplace(offset); + const contents = this.root.map.getTileContent(currentPos, entity.layer); + assert(contents, "Invalid smart underground belt logic"); + this.root.logic.tryDeleteBuilding(contents); + } + } + + // REMOVE OBSOLETE TUNNELS + // Remove any double tunnels, by checking the tile plus the tile above + currentPos = tile.copy().add(offset); + for (let i = 0; i < matchingEntrance.range - 1; ++i) { + const posBefore = currentPos.copy(); + currentPos.addInplace(offset); + + const entityBefore = this.root.map.getTileContent(posBefore, entity.layer); + const entityAfter = this.root.map.getTileContent(currentPos, entity.layer); + + if (!entityBefore || !entityAfter) { + continue; + } + + const undergroundBefore = entityBefore.components.UndergroundBelt; + const undergroundAfter = entityAfter.components.UndergroundBelt; + + if (!undergroundBefore || !undergroundAfter) { + // Not an underground belt + continue; + } + + if ( + // Both same tier + undergroundBefore.tier !== undergroundAfter.tier || + // And same tier as our original entity + undergroundBefore.tier !== undergroundComp.tier + ) { + // Mismatching tier + continue; + } + + if ( + undergroundBefore.mode !== enumUndergroundBeltMode.sender || + undergroundAfter.mode !== enumUndergroundBeltMode.receiver + ) { + // Not the right mode + continue; + } + + // Check rotations + const staticBefore = entityBefore.components.StaticMapEntity; + const staticAfter = entityAfter.components.StaticMapEntity; + + if ( + enumAngleToDirection[staticBefore.rotation] !== direction || + enumAngleToDirection[staticAfter.rotation] !== direction + ) { + // Wrong rotation + continue; + } + + // All good, can remove + this.root.logic.tryDeleteBuilding(entityBefore); + this.root.logic.tryDeleteBuilding(entityAfter); + } + } + } + + /** + * Recomputes the cache in the given area, invalidating all entries there + * @param {Rectangle} area + */ + recomputeArea(area) { + for (let x = area.x; x < area.right(); ++x) { + for (let y = area.y; y < area.bottom(); ++y) { + const entities = this.root.map.getLayersContentsMultipleXY(x, y); + for (let i = 0; i < entities.length; ++i) { + const entity = entities[i]; + const undergroundComp = entity.components.UndergroundBelt; + if (!undergroundComp) { + continue; + } + undergroundComp.cachedLinkedEntity = null; + } + } + } + } + + update() { + this.staleAreaWatcher.update(); + + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + const undergroundComp = entity.components.UndergroundBelt; + if (undergroundComp.mode === enumUndergroundBeltMode.sender) { + this.handleSender(entity); + } else { + this.handleReceiver(entity); + } + } + } + + /** + * Finds the receiver for a given sender + * @param {Entity} entity + * @returns {import("../components/underground_belt").LinkedUndergroundBelt} + */ + findRecieverForSender(entity) { + const staticComp = entity.components.StaticMapEntity; + const undergroundComp = entity.components.UndergroundBelt; + const searchDirection = staticComp.localDirectionToWorld(enumDirection.top); + const searchVector = enumDirectionToVector[searchDirection]; + const targetRotation = enumDirectionToAngle[searchDirection]; + let currentTile = staticComp.origin; + + // Search in the direction of the tunnel + for ( + let searchOffset = 0; + searchOffset < globalConfig.undergroundBeltMaxTilesByTier[undergroundComp.tier]; + ++searchOffset + ) { + currentTile = currentTile.add(searchVector); + + const potentialReceiver = this.root.map.getTileContent(currentTile, "regular"); + if (!potentialReceiver) { + // Empty tile + continue; + } + const receiverUndergroundComp = potentialReceiver.components.UndergroundBelt; + if (!receiverUndergroundComp || receiverUndergroundComp.tier !== undergroundComp.tier) { + // Not a tunnel, or not on the same tier + continue; + } + + const receiverStaticComp = potentialReceiver.components.StaticMapEntity; + if (receiverStaticComp.rotation !== targetRotation) { + // Wrong rotation + continue; + } + + if (receiverUndergroundComp.mode !== enumUndergroundBeltMode.receiver) { + // Not a receiver, but a sender -> Abort to make sure we don't deliver double + break; + } + + return { entity: potentialReceiver, distance: searchOffset }; + } + + // None found + return { entity: null, distance: 0 }; + } + + /** + * + * @param {Entity} entity + */ + handleSender(entity) { + const undergroundComp = entity.components.UndergroundBelt; + + // Find the current receiver + let cacheEntry = undergroundComp.cachedLinkedEntity; + if (!cacheEntry) { + // Need to recompute cache + cacheEntry = undergroundComp.cachedLinkedEntity = this.findRecieverForSender(entity); + } + + if (!cacheEntry.entity) { + // If there is no connection to a receiver, ignore this one + return; + } + + // Check if we have any items to eject + const nextItemAndDuration = undergroundComp.pendingItems[0]; + if (nextItemAndDuration) { + assert(undergroundComp.pendingItems.length === 1, "more than 1 pending"); + + // Check if the receiver can accept it + if ( + cacheEntry.entity.components.UndergroundBelt.tryAcceptTunneledItem( + nextItemAndDuration[0], + cacheEntry.distance, + this.root.hubGoals.getUndergroundBeltBaseSpeed(), + this.root.time.now() + ) + ) { + // Drop this item + fastArrayDelete(undergroundComp.pendingItems, 0); + } + } + } + + /** + * + * @param {Entity} entity + */ + handleReceiver(entity) { + const undergroundComp = entity.components.UndergroundBelt; + + // Try to eject items, we only check the first one because it is sorted by remaining time + const nextItemAndDuration = undergroundComp.pendingItems[0]; + if (nextItemAndDuration) { + if (this.root.time.now() > nextItemAndDuration[1]) { + const ejectorComp = entity.components.ItemEjector; + + const nextSlotIndex = ejectorComp.getFirstFreeSlot(); + if (nextSlotIndex !== null) { + if (ejectorComp.tryEject(nextSlotIndex, nextItemAndDuration[0])) { + undergroundComp.pendingItems.shift(); + } + } + } + } + } +} diff --git a/src/js/game/systems/wire.js b/src/js/game/systems/wire.js index fa9287e1..4168edc4 100644 --- a/src/js/game/systems/wire.js +++ b/src/js/game/systems/wire.js @@ -79,6 +79,14 @@ export class WireNetwork { */ this.uid = ++networkUidCounter; } + + /** + * Returns whether this network currently has a value + * @returns {boolean} + */ + hasValue() { + return !!this.currentValue && !this.valueConflict; + } } export class WireSystem extends GameSystemWithFilter { @@ -162,7 +170,7 @@ export class WireSystem extends GameSystemWithFilter { const tunnelEntities = this.root.entityMgr.getAllWithComponent(WireTunnelComponent); const pinEntities = this.root.entityMgr.getAllWithComponent(WiredPinsComponent); - // Clear all network references, but not on the first update since thats the deserializing one + // Clear all network references, but not on the first update since that's the deserializing one if (!this.isFirstRecompute) { for (let i = 0; i < wireEntities.length; ++i) { wireEntities[i].components.Wire.linkedNetwork = null; @@ -432,7 +440,7 @@ export class WireSystem extends GameSystemWithFilter { continue; } - // Check if its a tunnel, if so, go to the forwarded item + // Check if it's a tunnel, if so, go to the forwarded item const tunnelComp = entity.components.WireTunnel; if (tunnelComp) { if (visitedTunnels.has(entity.uid)) { diff --git a/src/js/game/systems/wired_pins.js b/src/js/game/systems/wired_pins.js index 202691b8..e8bc1882 100644 --- a/src/js/game/systems/wired_pins.js +++ b/src/js/game/systems/wired_pins.js @@ -149,8 +149,6 @@ export class WiredPinsSystem extends GameSystemWithFilter { } } - update() {} - /** * Draws a given entity * @param {DrawParameters} parameters diff --git a/src/js/game/themes/dark.json b/src/js/game/themes/dark.json index 7466201f..2f03b767 100644 --- a/src/js/game/themes/dark.json +++ b/src/js/game/themes/dark.json @@ -1,7 +1,7 @@ { "uiStyle": "dark", "map": { - "background": "#2e2f37", + "background": "#3e3f47", "grid": "rgba(255, 255, 255, 0.02)", "gridLineWidth": 0.5, @@ -25,10 +25,10 @@ "colorBlindPickerTile": "rgba(255, 255, 255, 0.5)", "resources": { - "shape": "#3d3f4a", - "red": "#4a3d3f", - "green": "#3e4a3d", - "blue": "#35384a" + "shape": "#5d5f6a", + "red": "#854f56", + "green": "#667964", + "blue": "#5e7ca4" }, "chunkOverview": { "empty": "#444856", diff --git a/src/js/game/tutorial_goals.js b/src/js/game/tutorial_goals.js index 9084f508..3a5cf807 100644 --- a/src/js/game/tutorial_goals.js +++ b/src/js/game/tutorial_goals.js @@ -11,21 +11,19 @@ export const enumHubGoalRewards = { reward_painter: "reward_painter", reward_mixer: "reward_mixer", reward_stacker: "reward_stacker", - reward_splitter: "reward_splitter", + reward_balancer: "reward_balancer", reward_tunnel: "reward_tunnel", reward_rotater_ccw: "reward_rotater_ccw", - reward_rotater_fl: "reward_rotater_fl", + reward_rotater_180: "reward_rotater_180", reward_miner_chainable: "reward_miner_chainable", reward_underground_belt_tier_2: "reward_underground_belt_tier_2", - reward_splitter_compact: "reward_splitter_compact", + reward_splitter: "reward_splitter", reward_cutter_quad: "reward_cutter_quad", reward_painter_double: "reward_painter_double", reward_painter_quad: "reward_painter_quad", reward_storage: "reward_storage", - - // @todo: unlock - reward_merger_compact: "reward_compact_merger", + reward_merger: "reward_merger", reward_blueprints: "reward_blueprints", reward_freeplay: "reward_freeplay", @@ -55,14 +53,14 @@ export const tutorialGoals = [ // Rectangle { shape: "RuRuRuRu", // miners t1 - required: 100, - reward: enumHubGoalRewards.reward_splitter, + required: 85, + reward: enumHubGoalRewards.reward_balancer, }, // 4 { shape: "RuRu----", // processors t2 - required: 120, + required: 100, reward: enumHubGoalRewards.reward_rotater, }, @@ -70,14 +68,14 @@ export const tutorialGoals = [ // Rotater { shape: "Cu----Cu", // belts t2 - required: 200, + required: 175, reward: enumHubGoalRewards.reward_tunnel, }, // 6 { shape: "Cu------", // miners t2 - required: 400, + required: 250, reward: enumHubGoalRewards.reward_painter, }, @@ -85,14 +83,14 @@ export const tutorialGoals = [ // Painter { shape: "CrCrCrCr", // unused - required: 800, + required: 500, reward: enumHubGoalRewards.reward_rotater_ccw, }, // 8 { shape: "RbRb----", // painter t2 - required: 1000, + required: 700, reward: enumHubGoalRewards.reward_mixer, }, @@ -100,15 +98,15 @@ export const tutorialGoals = [ // Mixing (purple) { shape: "CpCpCpCp", // belts t3 - required: 1400, - reward: enumHubGoalRewards.reward_splitter_compact, + required: 800, + reward: enumHubGoalRewards.reward_splitter, }, // 10 // Star shape + cyan { shape: "ScScScSc", // miners t3 - required: 1600, + required: 900, reward: enumHubGoalRewards.reward_stacker, }, @@ -116,7 +114,7 @@ export const tutorialGoals = [ // Stacker { shape: "CgScScCg", // processors t3 - required: 1800, + required: 1000, reward: enumHubGoalRewards.reward_miner_chainable, }, @@ -124,49 +122,56 @@ export const tutorialGoals = [ // Blueprints { shape: "CbCbCbRb:CwCwCwCw", - required: 2000, + required: 1250, reward: enumHubGoalRewards.reward_blueprints, }, // 13 { shape: "RpRpRpRp:CwCwCwCw", // painting t3 - required: 12000, + required: 5000, reward: enumHubGoalRewards.reward_underground_belt_tier_2, }, // 14 { shape: "SrSrSrSr:CyCyCyCy", // unused - required: 16000, + required: 7500, reward: enumHubGoalRewards.reward_storage, }, // 15 { shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", // belts t4 (two variants) - required: 25000, + required: 15000, reward: enumHubGoalRewards.reward_cutter_quad, }, // 16 { shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants) - required: 50000, + required: 20000, reward: enumHubGoalRewards.reward_painter_double, }, // 17 { - shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", // processors t4 (two variants) - required: 120000, - reward: enumHubGoalRewards.reward_painter_quad, + shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // rotater 180 + required: 25000, + reward: enumHubGoalRewards.reward_rotater_180, }, // 18 + { + shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", // processors t4 (two variants) + required: 30000, + reward: enumHubGoalRewards.reward_painter_quad, + }, + + // 19 { shape: finalGameShape, - required: 250000, + required: 50000, reward: enumHubGoalRewards.reward_freeplay, }, ]; diff --git a/src/js/game/tutorial_goals_mappings.js b/src/js/game/tutorial_goals_mappings.js index 923c2814..1257cad9 100644 --- a/src/js/game/tutorial_goals_mappings.js +++ b/src/js/game/tutorial_goals_mappings.js @@ -1,52 +1,51 @@ -import { MetaBuilding, defaultBuildingVariant } from "./meta_building"; -import { MetaCutterBuilding, enumCutterVariants } from "./buildings/cutter"; -import { MetaRotaterBuilding, enumRotaterVariants } from "./buildings/rotater"; -import { MetaPainterBuilding, enumPainterVariants } from "./buildings/painter"; -import { MetaMixerBuilding } from "./buildings/mixer"; -import { MetaStackerBuilding } from "./buildings/stacker"; -import { MetaSplitterBuilding, enumSplitterVariants } from "./buildings/splitter"; -import { MetaUndergroundBeltBuilding, enumUndergroundBeltVariants } from "./buildings/underground_belt"; -import { MetaMinerBuilding, enumMinerVariants } from "./buildings/miner"; -import { MetaTrashBuilding, enumTrashVariants } from "./buildings/trash"; - -/** @typedef {Array<[typeof MetaBuilding, string]>} TutorialGoalReward */ - -import { enumHubGoalRewards } from "./tutorial_goals"; - -/** - * Helper method for proper types - * @returns {TutorialGoalReward} - */ -const typed = x => x; - -/** - * Stores which reward unlocks what - * @enum {TutorialGoalReward?} - */ -export const enumHubGoalRewardsToContentUnlocked = { - [enumHubGoalRewards.reward_cutter_and_trash]: typed([[MetaCutterBuilding, defaultBuildingVariant]]), - [enumHubGoalRewards.reward_rotater]: typed([[MetaRotaterBuilding, defaultBuildingVariant]]), - [enumHubGoalRewards.reward_painter]: typed([[MetaPainterBuilding, defaultBuildingVariant]]), - [enumHubGoalRewards.reward_mixer]: typed([[MetaMixerBuilding, defaultBuildingVariant]]), - [enumHubGoalRewards.reward_stacker]: typed([[MetaStackerBuilding, defaultBuildingVariant]]), - [enumHubGoalRewards.reward_splitter]: typed([[MetaSplitterBuilding, defaultBuildingVariant]]), - [enumHubGoalRewards.reward_tunnel]: typed([[MetaUndergroundBeltBuilding, defaultBuildingVariant]]), - - [enumHubGoalRewards.reward_rotater_ccw]: typed([[MetaRotaterBuilding, enumRotaterVariants.ccw]]), - [enumHubGoalRewards.reward_rotater_fl]: typed([[MetaRotaterBuilding, enumRotaterVariants.fl]]), - [enumHubGoalRewards.reward_miner_chainable]: typed([[MetaMinerBuilding, enumMinerVariants.chainable]]), - [enumHubGoalRewards.reward_underground_belt_tier_2]: typed([ - [MetaUndergroundBeltBuilding, enumUndergroundBeltVariants.tier2], - ]), - [enumHubGoalRewards.reward_splitter_compact]: typed([ - [MetaSplitterBuilding, enumSplitterVariants.compact], - ]), - [enumHubGoalRewards.reward_cutter_quad]: typed([[MetaCutterBuilding, enumCutterVariants.quad]]), - [enumHubGoalRewards.reward_painter_double]: typed([[MetaPainterBuilding, enumPainterVariants.double]]), - [enumHubGoalRewards.reward_painter_quad]: typed([[MetaPainterBuilding, enumPainterVariants.quad]]), - [enumHubGoalRewards.reward_storage]: typed([[MetaTrashBuilding, enumTrashVariants.storage]]), - - [enumHubGoalRewards.reward_freeplay]: null, - [enumHubGoalRewards.no_reward]: null, - [enumHubGoalRewards.no_reward_freeplay]: null, -}; +import { MetaBuilding, defaultBuildingVariant } from "./meta_building"; +import { MetaCutterBuilding, enumCutterVariants } from "./buildings/cutter"; +import { MetaRotaterBuilding, enumRotaterVariants } from "./buildings/rotater"; +import { MetaPainterBuilding, enumPainterVariants } from "./buildings/painter"; +import { MetaMixerBuilding } from "./buildings/mixer"; +import { MetaStackerBuilding } from "./buildings/stacker"; +import { MetaBalancerBuilding, enumBalancerVariants } from "./buildings/balancer"; +import { MetaUndergroundBeltBuilding, enumUndergroundBeltVariants } from "./buildings/underground_belt"; +import { MetaMinerBuilding, enumMinerVariants } from "./buildings/miner"; +import { MetaTrashBuilding, enumTrashVariants } from "./buildings/trash"; + +/** @typedef {Array<[typeof MetaBuilding, string]>} TutorialGoalReward */ + +import { enumHubGoalRewards } from "./tutorial_goals"; + +/** + * Helper method for proper types + * @returns {TutorialGoalReward} + */ +const typed = x => x; + +/** + * Stores which reward unlocks what + * @enum {TutorialGoalReward?} + */ +export const enumHubGoalRewardsToContentUnlocked = { + [enumHubGoalRewards.reward_cutter_and_trash]: typed([[MetaCutterBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_rotater]: typed([[MetaRotaterBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_painter]: typed([[MetaPainterBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_mixer]: typed([[MetaMixerBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_stacker]: typed([[MetaStackerBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_balancer]: typed([[MetaBalancerBuilding, defaultBuildingVariant]]), + [enumHubGoalRewards.reward_tunnel]: typed([[MetaUndergroundBeltBuilding, defaultBuildingVariant]]), + + [enumHubGoalRewards.reward_rotater_ccw]: typed([[MetaRotaterBuilding, enumRotaterVariants.ccw]]), + [enumHubGoalRewards.reward_rotater_180]: typed([[MetaRotaterBuilding, enumRotaterVariants.rotate180]]), + [enumHubGoalRewards.reward_miner_chainable]: typed([[MetaMinerBuilding, enumMinerVariants.chainable]]), + [enumHubGoalRewards.reward_underground_belt_tier_2]: typed([ + [MetaUndergroundBeltBuilding, enumUndergroundBeltVariants.tier2], + ]), + [enumHubGoalRewards.reward_splitter]: typed([[MetaBalancerBuilding, enumBalancerVariants.splitter]]), + [enumHubGoalRewards.reward_merger]: typed([[MetaBalancerBuilding, enumBalancerVariants.merger]]), + [enumHubGoalRewards.reward_cutter_quad]: typed([[MetaCutterBuilding, enumCutterVariants.quad]]), + [enumHubGoalRewards.reward_painter_double]: typed([[MetaPainterBuilding, enumPainterVariants.double]]), + [enumHubGoalRewards.reward_painter_quad]: typed([[MetaPainterBuilding, enumPainterVariants.quad]]), + [enumHubGoalRewards.reward_storage]: typed([[MetaTrashBuilding, enumTrashVariants.storage]]), + + [enumHubGoalRewards.reward_freeplay]: null, + [enumHubGoalRewards.no_reward]: null, + [enumHubGoalRewards.no_reward_freeplay]: null, +}; diff --git a/src/js/game/upgrades.js b/src/js/game/upgrades.js index 6e0c7c64..4735592b 100644 --- a/src/js/game/upgrades.js +++ b/src/js/game/upgrades.js @@ -1,175 +1,160 @@ -import { findNiceIntegerValue } from "../core/utils"; -import { ShapeDefinition } from "./shape_definition"; - -export const finalGameShape = "RuCw--Cw:----Ru--"; -export const blueprintShape = "CbCbCbRb:CwCwCwCw"; - -export const UPGRADES = { - belt: { - tiers: [ - { - required: [{ shape: "CuCuCuCu", amount: 150 }], - improvement: 1, - }, - { - required: [{ shape: "--CuCu--", amount: 1200 }], - improvement: 2, - }, - { - required: [{ shape: "CpCpCpCp", amount: 15000 }], - improvement: 2, - }, - { - required: [{ shape: "SrSrSrSr:CyCyCyCy", amount: 40000 }], - improvement: 2, - }, - { - required: [{ shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", amount: 40000 }], - improvement: 2, - }, - { - required: [{ shape: finalGameShape, amount: 150000 }], - improvement: 5, - excludePrevious: true, - }, - ], - }, - - miner: { - tiers: [ - { - required: [{ shape: "RuRuRuRu", amount: 400 }], - improvement: 1, - }, - { - required: [{ shape: "Cu------", amount: 4000 }], - improvement: 2, - }, - { - required: [{ shape: "ScScScSc", amount: 20000 }], - improvement: 2, - }, - { - required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 40000 }], - improvement: 2, - }, - { - required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 40000 }], - improvement: 2, - }, - { - required: [{ shape: finalGameShape, amount: 150000 }], - improvement: 5, - excludePrevious: true, - }, - ], - }, - - processors: { - tiers: [ - { - required: [{ shape: "SuSuSuSu", amount: 1000 }], - improvement: 1, - }, - { - required: [{ shape: "RuRu----", amount: 2000 }], - improvement: 2, - }, - { - required: [{ shape: "CgScScCg", amount: 25000 }], - improvement: 2, - }, - { - required: [{ shape: "CwCrCwCr:SgSgSgSg", amount: 40000 }], - improvement: 2, - }, - { - required: [{ shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", amount: 40000 }], - improvement: 2, - }, - { - required: [{ shape: finalGameShape, amount: 150000 }], - improvement: 5, - excludePrevious: true, - }, - ], - }, - - painting: { - tiers: [ - { - required: [{ shape: "RbRb----", amount: 1500 }], - improvement: 2, - }, - { - required: [{ shape: "WrWrWrWr", amount: 4000 }], - improvement: 1, - }, - { - required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 30000 }], - improvement: 2, - }, - { - required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 40000 }], - improvement: 2, - }, - { - required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp:CwCwCwCw", amount: 40000 }], - improvement: 2, - }, - { - required: [{ shape: finalGameShape, amount: 150000 }], - improvement: 5, - excludePrevious: true, - }, - ], - }, -}; - -// Tiers need % of the previous tier as requirement too -const tierGrowth = 2.5; - -// Automatically generate tier levels -for (const upgradeId in UPGRADES) { - const upgrade = UPGRADES[upgradeId]; - - let currentTierRequirements = []; - for (let i = 0; i < upgrade.tiers.length; ++i) { - const tierHandle = upgrade.tiers[i]; - const originalRequired = tierHandle.required.slice(); - - for (let k = currentTierRequirements.length - 1; k >= 0; --k) { - const oldTierRequirement = currentTierRequirements[k]; - if (!tierHandle.excludePrevious) { - tierHandle.required.unshift({ - shape: oldTierRequirement.shape, - amount: oldTierRequirement.amount, - }); - } - } - currentTierRequirements.push( - ...originalRequired.map(req => ({ - amount: req.amount, - shape: req.shape, - })) - ); - currentTierRequirements.forEach(tier => { - tier.amount = findNiceIntegerValue(tier.amount * tierGrowth); - }); - } -} - -if (G_IS_DEV) { - for (const upgradeId in UPGRADES) { - const upgrade = UPGRADES[upgradeId]; - upgrade.tiers.forEach(tier => { - tier.required.forEach(({ shape }) => { - try { - ShapeDefinition.fromShortKey(shape); - } catch (ex) { - throw new Error("Invalid upgrade goal: '" + ex + "' for shape" + shape); - } - }); - }); - } -} +import { findNiceIntegerValue } from "../core/utils"; +import { ShapeDefinition } from "./shape_definition"; + +export const finalGameShape = "RuCw--Cw:----Ru--"; +export const blueprintShape = "CbCbCbRb:CwCwCwCw"; + +const fixedImprovements = [0.5, 0.5, 1, 1, 2, 2]; + +/** @typedef {{ + * shape: string, + * amount: number + * }} UpgradeRequirement */ + +/** @typedef {{ + * required: Array + * improvement?: number, + * excludePrevious?: boolean + * }} TierRequirement */ + +/** @typedef {Array} UpgradeTiers */ + +/** @type {Object} */ +export const UPGRADES = { + belt: [ + { + required: [{ shape: "CuCuCuCu", amount: 150 }], + }, + { + required: [{ shape: "--CuCu--", amount: 1000 }], + }, + { + required: [{ shape: "CpCpCpCp", amount: 5000 }], + }, + { + required: [{ shape: "SrSrSrSr:CyCyCyCy", amount: 12000 }], + }, + { + required: [{ shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", amount: 20000 }], + }, + { + required: [{ shape: finalGameShape, amount: 75000 }], + excludePrevious: true, + }, + ], + + miner: [ + { + required: [{ shape: "RuRuRuRu", amount: 400 }], + }, + { + required: [{ shape: "Cu------", amount: 3000 }], + }, + { + required: [{ shape: "ScScScSc", amount: 7000 }], + }, + { + required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 15000 }], + }, + { + required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 30000 }], + }, + { + required: [{ shape: finalGameShape, amount: 85000 }], + excludePrevious: true, + }, + ], + + processors: [ + { + required: [{ shape: "SuSuSuSu", amount: 600 }], + }, + { + required: [{ shape: "RuRu----", amount: 2000 }], + }, + { + required: [{ shape: "CgScScCg", amount: 15000 }], + }, + { + required: [{ shape: "CwCrCwCr:SgSgSgSg", amount: 20000 }], + }, + { + required: [{ shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", amount: 30000 }], + }, + { + required: [{ shape: finalGameShape, amount: 100000 }], + excludePrevious: true, + }, + ], + + painting: [ + { + required: [{ shape: "RbRb----", amount: 1000 }], + }, + { + required: [{ shape: "WrWrWrWr", amount: 3000 }], + }, + { + required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 15000 }], + }, + { + required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 20000 }], + }, + { + required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp:CwCwCwCw", amount: 30000 }], + }, + { + required: [{ shape: finalGameShape, amount: 125000 }], + excludePrevious: true, + }, + ], +}; + +// Tiers need % of the previous tier as requirement too +const tierGrowth = 1.8; + +// Automatically generate tier levels +for (const upgradeId in UPGRADES) { + const upgradeTiers = UPGRADES[upgradeId]; + + let currentTierRequirements = []; + for (let i = 0; i < upgradeTiers.length; ++i) { + const tierHandle = upgradeTiers[i]; + tierHandle.improvement = fixedImprovements[i]; + const originalRequired = tierHandle.required.slice(); + + for (let k = currentTierRequirements.length - 1; k >= 0; --k) { + const oldTierRequirement = currentTierRequirements[k]; + if (!tierHandle.excludePrevious) { + tierHandle.required.unshift({ + shape: oldTierRequirement.shape, + amount: oldTierRequirement.amount, + }); + } + } + currentTierRequirements.push( + ...originalRequired.map(req => ({ + amount: req.amount, + shape: req.shape, + })) + ); + currentTierRequirements.forEach(tier => { + tier.amount = findNiceIntegerValue(tier.amount * tierGrowth); + }); + } +} + +// VALIDATE +if (G_IS_DEV) { + for (const upgradeId in UPGRADES) { + UPGRADES[upgradeId].forEach(tier => { + tier.required.forEach(({ shape }) => { + try { + ShapeDefinition.fromShortKey(shape); + } catch (ex) { + throw new Error("Invalid upgrade goal: '" + ex + "' for shape" + shape); + } + }); + }); + } +} diff --git a/src/js/jsconfig.json b/src/js/jsconfig.json index e28a1c04..99d65145 100644 --- a/src/js/jsconfig.json +++ b/src/js/jsconfig.json @@ -2,5 +2,6 @@ "compilerOptions": { "target": "es6", "checkJs": true - } + }, + "include": ["./**/*.js"] } diff --git a/src/js/platform/browser/game_analytics.js b/src/js/platform/browser/game_analytics.js index 7336ffd9..52497ef7 100644 --- a/src/js/platform/browser/game_analytics.js +++ b/src/js/platform/browser/game_analytics.js @@ -1,261 +1,260 @@ -import { globalConfig } from "../../core/config"; -import { createLogger } from "../../core/logging"; -import { GameRoot } from "../../game/root"; -import { InGameState } from "../../states/ingame"; -import { GameAnalyticsInterface } from "../game_analytics"; -import { FILE_NOT_FOUND } from "../storage"; -import { blueprintShape, UPGRADES } from "../../game/upgrades"; -import { tutorialGoals } from "../../game/tutorial_goals"; -import { BeltComponent } from "../../game/components/belt"; -import { StaticMapEntityComponent } from "../../game/components/static_map_entity"; - -const logger = createLogger("game_analytics"); - -const analyticsUrl = G_IS_DEV ? "http://localhost:8001" : "https://analytics.shapez.io"; - -// Be sure to increment the ID whenever it changes to make sure all -// users are tracked -const analyticsLocalFile = "shapez_token_123.bin"; - -export class ShapezGameAnalytics extends GameAnalyticsInterface { - get environment() { - if (G_IS_DEV) { - return "dev"; - } - - if (G_IS_STANDALONE) { - return "steam"; - } - - if (G_IS_RELEASE) { - return "prod"; - } - - return "beta"; - } - - /** - * @returns {Promise} - */ - initialize() { - this.syncKey = null; - - setInterval(() => this.sendTimePoints(), 60 * 1000); - - // Retrieve sync key from player - return this.app.storage.readFileAsync(analyticsLocalFile).then( - syncKey => { - this.syncKey = syncKey; - logger.log("Player sync key read:", this.syncKey); - }, - error => { - // File was not found, retrieve new key - if (error === FILE_NOT_FOUND) { - logger.log("Retrieving new player key"); - - // Perform call to get a new key from the API - this.sendToApi("/v1/register", { - environment: this.environment, - }) - .then(res => { - // Try to read and parse the key from the api - if (res.key && typeof res.key === "string" && res.key.length === 40) { - this.syncKey = res.key; - logger.log("Key retrieved:", this.syncKey); - this.app.storage.writeFileAsync(analyticsLocalFile, res.key); - } else { - throw new Error("Bad response from analytics server: " + res); - } - }) - .catch(err => { - logger.error("Failed to register on analytics api:", err); - }); - } else { - logger.error("Failed to read ga key:", error); - } - return; - } - ); - } - - /** - * Sends a request to the api - * @param {string} endpoint Endpoint without base url - * @param {object} data payload - * @returns {Promise} - */ - sendToApi(endpoint, data) { - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject("Request to " + endpoint + " timed out"), 20000); - - fetch(analyticsUrl + endpoint, { - method: "POST", - mode: "cors", - cache: "no-cache", - referrer: "no-referrer", - credentials: "omit", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "x-api-key": globalConfig.info.analyticsApiKey, - }, - body: JSON.stringify(data), - }) - .then(res => { - clearTimeout(timeout); - if (!res.ok || res.status !== 200) { - reject("Fetch error: Bad status " + res.status); - } else { - return res.json(); - } - }) - .then(resolve) - .catch(reason => { - clearTimeout(timeout); - reject(reason); - }); - }); - } - - /** - * Sends a game event to the analytics - * @param {string} category - * @param {string} value - */ - sendGameEvent(category, value) { - if (!this.syncKey) { - logger.warn("Can not send event due to missing sync key"); - return; - } - - const gameState = this.app.stateMgr.currentState; - if (!(gameState instanceof InGameState)) { - logger.warn("Trying to send analytics event outside of ingame state"); - return; - } - - const savegame = gameState.savegame; - if (!savegame) { - logger.warn("Ingame state has empty savegame"); - return; - } - - const savegameId = savegame.internalId; - if (!gameState.core) { - logger.warn("Game state has no core"); - return; - } - const root = gameState.core.root; - if (!root) { - logger.warn("Root is not initialized"); - return; - } - - logger.log("Sending event", category, value); - - this.sendToApi("/v1/game-event", { - playerKey: this.syncKey, - gameKey: savegameId, - ingameTime: root.time.now(), - environment: this.environment, - category, - value, - version: G_BUILD_VERSION, - level: root.hubGoals.level, - gameDump: this.generateGameDump(root), - }); - } - - sendTimePoints() { - const gameState = this.app.stateMgr.currentState; - if (gameState instanceof InGameState) { - logger.log("Syncing analytics"); - this.sendGameEvent("sync", ""); - } - } - - /** - * Returns true if the shape is interesting - * @param {string} key - */ - isInterestingShape(key) { - if (key === blueprintShape) { - return true; - } - - // Check if its a story goal - for (let i = 0; i < tutorialGoals.length; ++i) { - if (key === tutorialGoals[i].shape) { - return true; - } - } - - // Check if its required to unlock an upgrade - for (const upgradeKey in UPGRADES) { - const handle = UPGRADES[upgradeKey]; - const tiers = handle.tiers; - for (let i = 0; i < tiers.length; ++i) { - const tier = tiers[i]; - const required = tier.required; - for (let k = 0; k < required.length; ++k) { - if (required[k].shape === key) { - return true; - } - } - } - } - - return false; - } - - /** - * Generates a game dump - * @param {GameRoot} root - */ - generateGameDump(root) { - const shapeIds = Object.keys(root.hubGoals.storedShapes).filter(this.isInterestingShape.bind(this)); - let shapes = {}; - for (let i = 0; i < shapeIds.length; ++i) { - shapes[shapeIds[i]] = root.hubGoals.storedShapes[shapeIds[i]]; - } - return { - shapes, - upgrades: root.hubGoals.upgradeLevels, - belts: root.entityMgr.getAllWithComponent(BeltComponent).length, - buildings: - root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length - - root.entityMgr.getAllWithComponent(BeltComponent).length, - }; - } - - /** - */ - handleGameStarted() { - this.sendGameEvent("game_start", ""); - } - - /** - */ - handleGameResumed() { - this.sendTimePoints(); - } - - /** - * Handles the given level completed - * @param {number} level - */ - handleLevelCompleted(level) { - logger.log("Complete level", level); - this.sendGameEvent("level_complete", "" + level); - } - - /** - * Handles the given upgrade completed - * @param {string} id - * @param {number} level - */ - handleUpgradeUnlocked(id, level) { - logger.log("Unlock upgrade", id, level); - this.sendGameEvent("upgrade_unlock", id + "@" + level); - } -} +import { globalConfig } from "../../core/config"; +import { createLogger } from "../../core/logging"; +import { GameRoot } from "../../game/root"; +import { InGameState } from "../../states/ingame"; +import { GameAnalyticsInterface } from "../game_analytics"; +import { FILE_NOT_FOUND } from "../storage"; +import { blueprintShape, UPGRADES } from "../../game/upgrades"; +import { tutorialGoals } from "../../game/tutorial_goals"; +import { BeltComponent } from "../../game/components/belt"; +import { StaticMapEntityComponent } from "../../game/components/static_map_entity"; + +const logger = createLogger("game_analytics"); + +const analyticsUrl = G_IS_DEV ? "http://localhost:8001" : "https://analytics.shapez.io"; + +// Be sure to increment the ID whenever it changes to make sure all +// users are tracked +const analyticsLocalFile = "shapez_token_123.bin"; + +export class ShapezGameAnalytics extends GameAnalyticsInterface { + get environment() { + if (G_IS_DEV) { + return "dev"; + } + + if (G_IS_STANDALONE) { + return "steam"; + } + + if (G_IS_RELEASE) { + return "prod"; + } + + return "beta"; + } + + /** + * @returns {Promise} + */ + initialize() { + this.syncKey = null; + + setInterval(() => this.sendTimePoints(), 60 * 1000); + + // Retrieve sync key from player + return this.app.storage.readFileAsync(analyticsLocalFile).then( + syncKey => { + this.syncKey = syncKey; + logger.log("Player sync key read:", this.syncKey); + }, + error => { + // File was not found, retrieve new key + if (error === FILE_NOT_FOUND) { + logger.log("Retrieving new player key"); + + // Perform call to get a new key from the API + this.sendToApi("/v1/register", { + environment: this.environment, + }) + .then(res => { + // Try to read and parse the key from the api + if (res.key && typeof res.key === "string" && res.key.length === 40) { + this.syncKey = res.key; + logger.log("Key retrieved:", this.syncKey); + this.app.storage.writeFileAsync(analyticsLocalFile, res.key); + } else { + throw new Error("Bad response from analytics server: " + res); + } + }) + .catch(err => { + logger.error("Failed to register on analytics api:", err); + }); + } else { + logger.error("Failed to read ga key:", error); + } + return; + } + ); + } + + /** + * Sends a request to the api + * @param {string} endpoint Endpoint without base url + * @param {object} data payload + * @returns {Promise} + */ + sendToApi(endpoint, data) { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject("Request to " + endpoint + " timed out"), 20000); + + fetch(analyticsUrl + endpoint, { + method: "POST", + mode: "cors", + cache: "no-cache", + referrer: "no-referrer", + credentials: "omit", + headers: { + "Content-Type": "application/json", + "Accept": "application/json", + "x-api-key": globalConfig.info.analyticsApiKey, + }, + body: JSON.stringify(data), + }) + .then(res => { + clearTimeout(timeout); + if (!res.ok || res.status !== 200) { + reject("Fetch error: Bad status " + res.status); + } else { + return res.json(); + } + }) + .then(resolve) + .catch(reason => { + clearTimeout(timeout); + reject(reason); + }); + }); + } + + /** + * Sends a game event to the analytics + * @param {string} category + * @param {string} value + */ + sendGameEvent(category, value) { + if (!this.syncKey) { + logger.warn("Can not send event due to missing sync key"); + return; + } + + const gameState = this.app.stateMgr.currentState; + if (!(gameState instanceof InGameState)) { + logger.warn("Trying to send analytics event outside of ingame state"); + return; + } + + const savegame = gameState.savegame; + if (!savegame) { + logger.warn("Ingame state has empty savegame"); + return; + } + + const savegameId = savegame.internalId; + if (!gameState.core) { + logger.warn("Game state has no core"); + return; + } + const root = gameState.core.root; + if (!root) { + logger.warn("Root is not initialized"); + return; + } + + logger.log("Sending event", category, value); + + this.sendToApi("/v1/game-event", { + playerKey: this.syncKey, + gameKey: savegameId, + ingameTime: root.time.now(), + environment: this.environment, + category, + value, + version: G_BUILD_VERSION, + level: root.hubGoals.level, + gameDump: this.generateGameDump(root), + }); + } + + sendTimePoints() { + const gameState = this.app.stateMgr.currentState; + if (gameState instanceof InGameState) { + logger.log("Syncing analytics"); + this.sendGameEvent("sync", ""); + } + } + + /** + * Returns true if the shape is interesting + * @param {string} key + */ + isInterestingShape(key) { + if (key === blueprintShape) { + return true; + } + + // Check if its a story goal + for (let i = 0; i < tutorialGoals.length; ++i) { + if (key === tutorialGoals[i].shape) { + return true; + } + } + + // Check if its required to unlock an upgrade + for (const upgradeKey in UPGRADES) { + const upgradeTiers = UPGRADES[upgradeKey]; + for (let i = 0; i < upgradeTiers.length; ++i) { + const tier = upgradeTiers[i]; + const required = tier.required; + for (let k = 0; k < required.length; ++k) { + if (required[k].shape === key) { + return true; + } + } + } + } + + return false; + } + + /** + * Generates a game dump + * @param {GameRoot} root + */ + generateGameDump(root) { + const shapeIds = Object.keys(root.hubGoals.storedShapes).filter(this.isInterestingShape.bind(this)); + let shapes = {}; + for (let i = 0; i < shapeIds.length; ++i) { + shapes[shapeIds[i]] = root.hubGoals.storedShapes[shapeIds[i]]; + } + return { + shapes, + upgrades: root.hubGoals.upgradeLevels, + belts: root.entityMgr.getAllWithComponent(BeltComponent).length, + buildings: + root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length - + root.entityMgr.getAllWithComponent(BeltComponent).length, + }; + } + + /** + */ + handleGameStarted() { + this.sendGameEvent("game_start", ""); + } + + /** + */ + handleGameResumed() { + this.sendTimePoints(); + } + + /** + * Handles the given level completed + * @param {number} level + */ + handleLevelCompleted(level) { + logger.log("Complete level", level); + this.sendGameEvent("level_complete", "" + level); + } + + /** + * Handles the given upgrade completed + * @param {string} id + * @param {number} level + */ + handleUpgradeUnlocked(id, level) { + logger.log("Unlock upgrade", id, level); + this.sendGameEvent("upgrade_unlock", id + "@" + level); + } +} diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index 084a6fe7..ace30eff 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -253,6 +253,7 @@ export const allApplicationSettings = [ changeCb: (app, id) => {}, }), + new BoolSetting("enableMousePan", enumCategories.advanced, (app, value) => {}), new BoolSetting("alwaysMultiplace", enumCategories.advanced, (app, value) => {}), new BoolSetting("clearCursorOnDeleteWhilePlacing", enumCategories.advanced, (app, value) => {}), new BoolSetting("enableTunnelSmartplace", enumCategories.advanced, (app, value) => {}), @@ -276,6 +277,7 @@ export const allApplicationSettings = [ new BoolSetting("lowQualityMapResources", enumCategories.performance, (app, value) => {}), new BoolSetting("disableTileGrid", enumCategories.performance, (app, value) => {}), new BoolSetting("lowQualityTextures", enumCategories.performance, (app, value) => {}), + new BoolSetting("simplifiedBelts", enumCategories.performance, (app, value) => {}), ]; export function getApplicationSettingById(id) { @@ -307,12 +309,14 @@ class SettingsStorage { this.clearCursorOnDeleteWhilePlacing = true; this.displayChunkBorders = false; this.pickMinerOnPatch = true; + this.enableMousePan = true; this.enableColorBlindHelper = false; this.lowQualityMapResources = false; this.disableTileGrid = false; this.lowQualityTextures = false; + this.simplifiedBelts = false; /** * @type {Object.} @@ -523,7 +527,7 @@ export class ApplicationSettings extends ReadWriteProxy { } getCurrentVersion() { - return 26; + return 28; } /** @param {{settings: SettingsStorage, version: number}} data */ @@ -646,6 +650,16 @@ export class ApplicationSettings extends ReadWriteProxy { data.version = 26; } + if (data.version < 27) { + data.settings.simplifiedBelts = false; + data.version = 27; + } + + if (data.version < 28) { + data.settings.enableMousePan = true; + data.version = 28; + } + return ExplainedResult.good(); } } diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index 2a7102a9..0ad630f6 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -1,273 +1,279 @@ -import { ReadWriteProxy } from "../core/read_write_proxy"; -import { ExplainedResult } from "../core/explained_result"; -import { SavegameSerializer } from "./savegame_serializer"; -import { BaseSavegameInterface } from "./savegame_interface"; -import { createLogger } from "../core/logging"; -import { globalConfig } from "../core/config"; -import { getSavegameInterface, savegameInterfaces } from "./savegame_interface_registry"; -import { SavegameInterface_V1001 } from "./schemas/1001"; -import { SavegameInterface_V1002 } from "./schemas/1002"; -import { SavegameInterface_V1003 } from "./schemas/1003"; -import { SavegameInterface_V1004 } from "./schemas/1004"; -import { SavegameInterface_V1005 } from "./schemas/1005"; - -const logger = createLogger("savegame"); - -/** - * @typedef {import("../application").Application} Application - * @typedef {import("../game/root").GameRoot} GameRoot - * @typedef {import("./savegame_typedefs").SavegameData} SavegameData - * @typedef {import("./savegame_typedefs").SavegameMetadata} SavegameMetadata - * @typedef {import("./savegame_typedefs").SavegameStats} SavegameStats - * @typedef {import("./savegame_typedefs").SerializedGame} SerializedGame - */ - -export class Savegame extends ReadWriteProxy { - /** - * - * @param {Application} app - * @param {object} param0 - * @param {string} param0.internalId - * @param {SavegameMetadata} param0.metaDataRef Handle to the meta data - */ - constructor(app, { internalId, metaDataRef }) { - super(app, "savegame-" + internalId + ".bin"); - this.internalId = internalId; - this.metaDataRef = metaDataRef; - - /** @type {SavegameData} */ - this.currentData = this.getDefaultData(); - - assert( - savegameInterfaces[Savegame.getCurrentVersion()], - "Savegame interface not defined: " + Savegame.getCurrentVersion() - ); - } - - //////// RW Proxy Impl ////////// - - /** - * @returns {number} - */ - static getCurrentVersion() { - return 1005; - } - - /** - * @returns {typeof BaseSavegameInterface} - */ - static getReaderClass() { - return savegameInterfaces[Savegame.getCurrentVersion()]; - } - - /** - * @returns {number} - */ - getCurrentVersion() { - return /** @type {typeof Savegame} */ (this.constructor).getCurrentVersion(); - } - - /** - * Returns the savegames default data - * @returns {SavegameData} - */ - getDefaultData() { - return { - version: this.getCurrentVersion(), - dump: null, - stats: {}, - lastUpdate: Date.now(), - }; - } - - /** - * Migrates the savegames data - * @param {SavegameData} data - */ - migrate(data) { - if (data.version < 1000) { - return ExplainedResult.bad("Can not migrate savegame, too old"); - } - - if (data.version === 1000) { - SavegameInterface_V1001.migrate1000to1001(data); - data.version = 1001; - } - - if (data.version === 1001) { - SavegameInterface_V1002.migrate1001to1002(data); - data.version = 1002; - } - - if (data.version === 1002) { - SavegameInterface_V1003.migrate1002to1003(data); - data.version = 1003; - } - - if (data.version === 1003) { - SavegameInterface_V1004.migrate1003to1004(data); - data.version = 1004; - } - - if (data.version === 1004) { - SavegameInterface_V1005.migrate1004to1005(data); - data.version = 1005; - } - - return ExplainedResult.good(); - } - - /** - * Verifies the savegames data - * @param {SavegameData} data - */ - verify(data) { - if (!data.dump) { - // Well, guess that works - return ExplainedResult.good(); - } - - if (!this.getDumpReaderForExternalData(data).validate()) { - return ExplainedResult.bad("dump-reader-failed-validation"); - } - return ExplainedResult.good(); - } - - //////// Subclasses interface //////// - - /** - * Returns if this game can be saved on disc - * @returns {boolean} - */ - isSaveable() { - return true; - } - /** - * Returns the statistics of the savegame - * @returns {SavegameStats} - */ - getStatistics() { - return this.currentData.stats; - } - - /** - * Returns the *real* last update of the savegame, not the one of the metadata - * which could also be the servers one - */ - getRealLastUpdate() { - return this.currentData.lastUpdate; - } - - /** - * Returns if this game has a serialized game dump - */ - hasGameDump() { - return !!this.currentData.dump && this.currentData.dump.entities.length > 0; - } - - /** - * Returns the current game dump - * @returns {SerializedGame} - */ - getCurrentDump() { - return this.currentData.dump; - } - - /** - * Returns a reader to access the data - * @returns {BaseSavegameInterface} - */ - getDumpReader() { - if (!this.currentData.dump) { - logger.warn("Getting reader on null-savegame dump"); - } - - const cls = /** @type {typeof Savegame} */ (this.constructor).getReaderClass(); - return new cls(this.currentData); - } - - /** - * Returns a reader to access external data - * @returns {BaseSavegameInterface} - */ - getDumpReaderForExternalData(data) { - assert(data.version, "External data contains no version"); - return getSavegameInterface(data); - } - - ///////// Public Interface /////////// - - /** - * Updates the last update field so we can send the savegame to the server, - * WITHOUT Saving! - */ - setLastUpdate(time) { - this.currentData.lastUpdate = time; - } - - /** - * - * @param {GameRoot} root - */ - updateData(root) { - // Construct a new serializer - const serializer = new SavegameSerializer(); - - // let timer = performance.now(); - const dump = serializer.generateDumpFromGameRoot(root); - if (!dump) { - return false; - } - - const shadowData = Object.assign({}, this.currentData); - shadowData.dump = dump; - shadowData.lastUpdate = new Date().getTime(); - shadowData.version = this.getCurrentVersion(); - - const reader = this.getDumpReaderForExternalData(shadowData); - - // Validate (not in prod though) - if (!G_IS_RELEASE) { - const validationResult = reader.validate(); - if (!validationResult) { - return false; - } - } - - // Save data - this.currentData = shadowData; - } - - /** - * Writes the savegame as well as its metadata - */ - writeSavegameAndMetadata() { - return this.writeAsync().then(() => this.saveMetadata()); - } - - /** - * Updates the savegames metadata - */ - saveMetadata() { - this.metaDataRef.lastUpdate = new Date().getTime(); - this.metaDataRef.version = this.getCurrentVersion(); - if (!this.hasGameDump()) { - this.metaDataRef.level = 0; - } else { - this.metaDataRef.level = this.currentData.dump.hubGoals.level; - } - - return this.app.savegameMgr.writeAsync(); - } - - /** - * @see ReadWriteProxy.writeAsync - * @returns {Promise} - */ - writeAsync() { - if (G_IS_DEV && globalConfig.debug.disableSavegameWrite) { - return Promise.resolve(); - } - return super.writeAsync(); - } -} +import { ReadWriteProxy } from "../core/read_write_proxy"; +import { ExplainedResult } from "../core/explained_result"; +import { SavegameSerializer } from "./savegame_serializer"; +import { BaseSavegameInterface } from "./savegame_interface"; +import { createLogger } from "../core/logging"; +import { globalConfig } from "../core/config"; +import { getSavegameInterface, savegameInterfaces } from "./savegame_interface_registry"; +import { SavegameInterface_V1001 } from "./schemas/1001"; +import { SavegameInterface_V1002 } from "./schemas/1002"; +import { SavegameInterface_V1003 } from "./schemas/1003"; +import { SavegameInterface_V1004 } from "./schemas/1004"; +import { SavegameInterface_V1005 } from "./schemas/1005"; +import { SavegameInterface_V1006 } from "./schemas/1006"; + +const logger = createLogger("savegame"); + +/** + * @typedef {import("../application").Application} Application + * @typedef {import("../game/root").GameRoot} GameRoot + * @typedef {import("./savegame_typedefs").SavegameData} SavegameData + * @typedef {import("./savegame_typedefs").SavegameMetadata} SavegameMetadata + * @typedef {import("./savegame_typedefs").SavegameStats} SavegameStats + * @typedef {import("./savegame_typedefs").SerializedGame} SerializedGame + */ + +export class Savegame extends ReadWriteProxy { + /** + * + * @param {Application} app + * @param {object} param0 + * @param {string} param0.internalId + * @param {SavegameMetadata} param0.metaDataRef Handle to the meta data + */ + constructor(app, { internalId, metaDataRef }) { + super(app, "savegame-" + internalId + ".bin"); + this.internalId = internalId; + this.metaDataRef = metaDataRef; + + /** @type {SavegameData} */ + this.currentData = this.getDefaultData(); + + assert( + savegameInterfaces[Savegame.getCurrentVersion()], + "Savegame interface not defined: " + Savegame.getCurrentVersion() + ); + } + + //////// RW Proxy Impl ////////// + + /** + * @returns {number} + */ + static getCurrentVersion() { + return 1006; + } + + /** + * @returns {typeof BaseSavegameInterface} + */ + static getReaderClass() { + return savegameInterfaces[Savegame.getCurrentVersion()]; + } + + /** + * @returns {number} + */ + getCurrentVersion() { + return /** @type {typeof Savegame} */ (this.constructor).getCurrentVersion(); + } + + /** + * Returns the savegames default data + * @returns {SavegameData} + */ + getDefaultData() { + return { + version: this.getCurrentVersion(), + dump: null, + stats: {}, + lastUpdate: Date.now(), + }; + } + + /** + * Migrates the savegames data + * @param {SavegameData} data + */ + migrate(data) { + if (data.version < 1000) { + return ExplainedResult.bad("Can not migrate savegame, too old"); + } + + if (data.version === 1000) { + SavegameInterface_V1001.migrate1000to1001(data); + data.version = 1001; + } + + if (data.version === 1001) { + SavegameInterface_V1002.migrate1001to1002(data); + data.version = 1002; + } + + if (data.version === 1002) { + SavegameInterface_V1003.migrate1002to1003(data); + data.version = 1003; + } + + if (data.version === 1003) { + SavegameInterface_V1004.migrate1003to1004(data); + data.version = 1004; + } + + if (data.version === 1004) { + SavegameInterface_V1005.migrate1004to1005(data); + data.version = 1005; + } + + if (data.version === 1005) { + SavegameInterface_V1006.migrate1005to1006(data); + data.version = 1006; + } + + return ExplainedResult.good(); + } + + /** + * Verifies the savegames data + * @param {SavegameData} data + */ + verify(data) { + if (!data.dump) { + // Well, guess that works + return ExplainedResult.good(); + } + + if (!this.getDumpReaderForExternalData(data).validate()) { + return ExplainedResult.bad("dump-reader-failed-validation"); + } + return ExplainedResult.good(); + } + + //////// Subclasses interface //////// + + /** + * Returns if this game can be saved on disc + * @returns {boolean} + */ + isSaveable() { + return true; + } + /** + * Returns the statistics of the savegame + * @returns {SavegameStats} + */ + getStatistics() { + return this.currentData.stats; + } + + /** + * Returns the *real* last update of the savegame, not the one of the metadata + * which could also be the servers one + */ + getRealLastUpdate() { + return this.currentData.lastUpdate; + } + + /** + * Returns if this game has a serialized game dump + */ + hasGameDump() { + return !!this.currentData.dump && this.currentData.dump.entities.length > 0; + } + + /** + * Returns the current game dump + * @returns {SerializedGame} + */ + getCurrentDump() { + return this.currentData.dump; + } + + /** + * Returns a reader to access the data + * @returns {BaseSavegameInterface} + */ + getDumpReader() { + if (!this.currentData.dump) { + logger.warn("Getting reader on null-savegame dump"); + } + + const cls = /** @type {typeof Savegame} */ (this.constructor).getReaderClass(); + return new cls(this.currentData); + } + + /** + * Returns a reader to access external data + * @returns {BaseSavegameInterface} + */ + getDumpReaderForExternalData(data) { + assert(data.version, "External data contains no version"); + return getSavegameInterface(data); + } + + ///////// Public Interface /////////// + + /** + * Updates the last update field so we can send the savegame to the server, + * WITHOUT Saving! + */ + setLastUpdate(time) { + this.currentData.lastUpdate = time; + } + + /** + * + * @param {GameRoot} root + */ + updateData(root) { + // Construct a new serializer + const serializer = new SavegameSerializer(); + + // let timer = performance.now(); + const dump = serializer.generateDumpFromGameRoot(root); + if (!dump) { + return false; + } + + const shadowData = Object.assign({}, this.currentData); + shadowData.dump = dump; + shadowData.lastUpdate = new Date().getTime(); + shadowData.version = this.getCurrentVersion(); + + const reader = this.getDumpReaderForExternalData(shadowData); + + // Validate (not in prod though) + if (!G_IS_RELEASE) { + const validationResult = reader.validate(); + if (!validationResult) { + return false; + } + } + + // Save data + this.currentData = shadowData; + } + + /** + * Writes the savegame as well as its metadata + */ + writeSavegameAndMetadata() { + return this.writeAsync().then(() => this.saveMetadata()); + } + + /** + * Updates the savegames metadata + */ + saveMetadata() { + this.metaDataRef.lastUpdate = new Date().getTime(); + this.metaDataRef.version = this.getCurrentVersion(); + if (!this.hasGameDump()) { + this.metaDataRef.level = 0; + } else { + this.metaDataRef.level = this.currentData.dump.hubGoals.level; + } + + return this.app.savegameMgr.writeAsync(); + } + + /** + * @see ReadWriteProxy.writeAsync + * @returns {Promise} + */ + writeAsync() { + if (G_IS_DEV && globalConfig.debug.disableSavegameWrite) { + return Promise.resolve(); + } + return super.writeAsync(); + } +} diff --git a/src/js/savegame/savegame_interface_registry.js b/src/js/savegame/savegame_interface_registry.js index fb1df52f..07b5353c 100644 --- a/src/js/savegame/savegame_interface_registry.js +++ b/src/js/savegame/savegame_interface_registry.js @@ -1,45 +1,47 @@ -import { BaseSavegameInterface } from "./savegame_interface"; -import { SavegameInterface_V1000 } from "./schemas/1000"; -import { createLogger } from "../core/logging"; -import { SavegameInterface_V1001 } from "./schemas/1001"; -import { SavegameInterface_V1002 } from "./schemas/1002"; -import { SavegameInterface_V1003 } from "./schemas/1003"; -import { SavegameInterface_V1004 } from "./schemas/1004"; -import { SavegameInterface_V1005 } from "./schemas/1005"; - -/** @type {Object.} */ -export const savegameInterfaces = { - 1000: SavegameInterface_V1000, - 1001: SavegameInterface_V1001, - 1002: SavegameInterface_V1002, - 1003: SavegameInterface_V1003, - 1004: SavegameInterface_V1004, - 1005: SavegameInterface_V1005, -}; - -const logger = createLogger("savegame_interface_registry"); - -/** - * Returns if the given savegame has any supported interface - * @param {any} savegame - * @returns {BaseSavegameInterface|null} - */ -export function getSavegameInterface(savegame) { - if (!savegame || !savegame.version) { - logger.warn("Savegame does not contain a valid version (undefined)"); - return null; - } - const version = savegame.version; - if (!Number.isInteger(version)) { - logger.warn("Savegame does not contain a valid version (non-integer):", version); - return null; - } - - const interfaceClass = savegameInterfaces[version]; - if (!interfaceClass) { - logger.warn("Version", version, "has no implemented interface!"); - return null; - } - - return new interfaceClass(savegame); -} +import { BaseSavegameInterface } from "./savegame_interface"; +import { SavegameInterface_V1000 } from "./schemas/1000"; +import { createLogger } from "../core/logging"; +import { SavegameInterface_V1001 } from "./schemas/1001"; +import { SavegameInterface_V1002 } from "./schemas/1002"; +import { SavegameInterface_V1003 } from "./schemas/1003"; +import { SavegameInterface_V1004 } from "./schemas/1004"; +import { SavegameInterface_V1005 } from "./schemas/1005"; +import { SavegameInterface_V1006 } from "./schemas/1006"; + +/** @type {Object.} */ +export const savegameInterfaces = { + 1000: SavegameInterface_V1000, + 1001: SavegameInterface_V1001, + 1002: SavegameInterface_V1002, + 1003: SavegameInterface_V1003, + 1004: SavegameInterface_V1004, + 1005: SavegameInterface_V1005, + 1006: SavegameInterface_V1006, +}; + +const logger = createLogger("savegame_interface_registry"); + +/** + * Returns if the given savegame has any supported interface + * @param {any} savegame + * @returns {BaseSavegameInterface|null} + */ +export function getSavegameInterface(savegame) { + if (!savegame || !savegame.version) { + logger.warn("Savegame does not contain a valid version (undefined)"); + return null; + } + const version = savegame.version; + if (!Number.isInteger(version)) { + logger.warn("Savegame does not contain a valid version (non-integer):", version); + return null; + } + + const interfaceClass = savegameInterfaces[version]; + if (!interfaceClass) { + logger.warn("Version", version, "has no implemented interface!"); + return null; + } + + return new interfaceClass(savegame); +} diff --git a/src/js/savegame/savegame_serializer.js b/src/js/savegame/savegame_serializer.js index 92db738b..552bc35c 100644 --- a/src/js/savegame/savegame_serializer.js +++ b/src/js/savegame/savegame_serializer.js @@ -1,146 +1,146 @@ -import { ExplainedResult } from "../core/explained_result"; -import { createLogger } from "../core/logging"; -import { gComponentRegistry } from "../core/global_registries"; -import { SerializerInternal } from "./serializer_internal"; - -/** - * @typedef {import("../game/component").Component} Component - * @typedef {import("../game/component").StaticComponent} StaticComponent - * @typedef {import("../game/entity").Entity} Entity - * @typedef {import("../game/root").GameRoot} GameRoot - * @typedef {import("../savegame/savegame_typedefs").SerializedGame} SerializedGame - */ - -const logger = createLogger("savegame_serializer"); - -/** - * Serializes a savegame - */ -export class SavegameSerializer { - constructor() { - this.internal = new SerializerInternal(); - } - - /** - * Serializes the game root into a dump - * @param {GameRoot} root - * @param {boolean=} sanityChecks Whether to check for validity - * @returns {object} - */ - generateDumpFromGameRoot(root, sanityChecks = true) { - /** @type {SerializedGame} */ - const data = { - camera: root.camera.serialize(), - time: root.time.serialize(), - map: root.map.serialize(), - entityMgr: root.entityMgr.serialize(), - hubGoals: root.hubGoals.serialize(), - pinnedShapes: root.hud.parts.pinnedShapes.serialize(), - waypoints: root.hud.parts.waypoints.serialize(), - entities: this.internal.serializeEntityArray(root.entityMgr.entities), - beltPaths: root.systemMgr.systems.belt.serializePaths(), - }; - - if (!G_IS_RELEASE) { - if (sanityChecks) { - // Sanity check - const sanity = this.verifyLogicalErrors(data); - if (!sanity.result) { - logger.error("Created invalid savegame:", sanity.reason, "savegame:", data); - return null; - } - } - } - return data; - } - - /** - * Verifies if there are logical errors in the savegame - * @param {SerializedGame} savegame - * @returns {ExplainedResult} - */ - verifyLogicalErrors(savegame) { - if (!savegame.entities) { - return ExplainedResult.bad("Savegame has no entities"); - } - - const seenUids = []; - - // Check for duplicate UIDS - for (let i = 0; i < savegame.entities.length; ++i) { - /** @type {Entity} */ - const entity = savegame.entities[i]; - - const uid = entity.uid; - if (!Number.isInteger(uid)) { - return ExplainedResult.bad("Entity has invalid uid: " + uid); - } - if (seenUids.indexOf(uid) >= 0) { - return ExplainedResult.bad("Duplicate uid " + uid); - } - seenUids.push(uid); - - // Verify components - if (!entity.components) { - return ExplainedResult.bad("Entity is missing key 'components': " + JSON.stringify(entity)); - } - - const components = entity.components; - for (const componentId in components) { - const componentClass = gComponentRegistry.findById(componentId); - - // Check component id is known - if (!componentClass) { - return ExplainedResult.bad("Unknown component id: " + componentId); - } - - // Verify component data - const componentData = components[componentId]; - const componentVerifyError = /** @type {StaticComponent} */ (componentClass).verify( - componentData - ); - - // Check component data is ok - if (componentVerifyError) { - return ExplainedResult.bad( - "Component " + componentId + " has invalid data: " + componentVerifyError - ); - } - } - } - - return ExplainedResult.good(); - } - - /** - * Tries to load the savegame from a given dump - * @param {SerializedGame} savegame - * @param {GameRoot} root - * @returns {ExplainedResult} - */ - deserialize(savegame, root) { - // Sanity - const verifyResult = this.verifyLogicalErrors(savegame); - if (!verifyResult.result) { - return ExplainedResult.bad(verifyResult.reason); - } - let errorReason = null; - - errorReason = errorReason || root.entityMgr.deserialize(savegame.entityMgr); - errorReason = errorReason || root.time.deserialize(savegame.time); - errorReason = errorReason || root.camera.deserialize(savegame.camera); - errorReason = errorReason || root.map.deserialize(savegame.map); - errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals); - errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes); - errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints); - errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities); - errorReason = errorReason || root.systemMgr.systems.belt.deserializePaths(savegame.beltPaths); - - // Check for errors - if (errorReason) { - return ExplainedResult.bad(errorReason); - } - - return ExplainedResult.good(); - } -} +import { ExplainedResult } from "../core/explained_result"; +import { createLogger } from "../core/logging"; +import { gComponentRegistry } from "../core/global_registries"; +import { SerializerInternal } from "./serializer_internal"; + +/** + * @typedef {import("../game/component").Component} Component + * @typedef {import("../game/component").StaticComponent} StaticComponent + * @typedef {import("../game/entity").Entity} Entity + * @typedef {import("../game/root").GameRoot} GameRoot + * @typedef {import("../savegame/savegame_typedefs").SerializedGame} SerializedGame + */ + +const logger = createLogger("savegame_serializer"); + +/** + * Serializes a savegame + */ +export class SavegameSerializer { + constructor() { + this.internal = new SerializerInternal(); + } + + /** + * Serializes the game root into a dump + * @param {GameRoot} root + * @param {boolean=} sanityChecks Whether to check for validity + * @returns {object} + */ + generateDumpFromGameRoot(root, sanityChecks = true) { + /** @type {SerializedGame} */ + const data = { + camera: root.camera.serialize(), + time: root.time.serialize(), + map: root.map.serialize(), + entityMgr: root.entityMgr.serialize(), + hubGoals: root.hubGoals.serialize(), + pinnedShapes: root.hud.parts.pinnedShapes.serialize(), + waypoints: root.hud.parts.waypoints.serialize(), + entities: this.internal.serializeEntityArray(root.entityMgr.entities), + beltPaths: root.systemMgr.systems.belt.serializePaths(), + }; + + if (G_IS_DEV) { + if (sanityChecks) { + // Sanity check + const sanity = this.verifyLogicalErrors(data); + if (!sanity.result) { + logger.error("Created invalid savegame:", sanity.reason, "savegame:", data); + return null; + } + } + } + return data; + } + + /** + * Verifies if there are logical errors in the savegame + * @param {SerializedGame} savegame + * @returns {ExplainedResult} + */ + verifyLogicalErrors(savegame) { + if (!savegame.entities) { + return ExplainedResult.bad("Savegame has no entities"); + } + + const seenUids = new Set(); + + // Check for duplicate UIDS + for (let i = 0; i < savegame.entities.length; ++i) { + /** @type {Entity} */ + const entity = savegame.entities[i]; + + const uid = entity.uid; + if (!Number.isInteger(uid)) { + return ExplainedResult.bad("Entity has invalid uid: " + uid); + } + if (seenUids.has(uid)) { + return ExplainedResult.bad("Duplicate uid " + uid); + } + seenUids.add(uid); + + // Verify components + if (!entity.components) { + return ExplainedResult.bad("Entity is missing key 'components': " + JSON.stringify(entity)); + } + + const components = entity.components; + for (const componentId in components) { + const componentClass = gComponentRegistry.findById(componentId); + + // Check component id is known + if (!componentClass) { + return ExplainedResult.bad("Unknown component id: " + componentId); + } + + // Verify component data + const componentData = components[componentId]; + const componentVerifyError = /** @type {StaticComponent} */ (componentClass).verify( + componentData + ); + + // Check component data is ok + if (componentVerifyError) { + return ExplainedResult.bad( + "Component " + componentId + " has invalid data: " + componentVerifyError + ); + } + } + } + + return ExplainedResult.good(); + } + + /** + * Tries to load the savegame from a given dump + * @param {SerializedGame} savegame + * @param {GameRoot} root + * @returns {ExplainedResult} + */ + deserialize(savegame, root) { + // Sanity + const verifyResult = this.verifyLogicalErrors(savegame); + if (!verifyResult.result) { + return ExplainedResult.bad(verifyResult.reason); + } + let errorReason = null; + + errorReason = errorReason || root.entityMgr.deserialize(savegame.entityMgr); + errorReason = errorReason || root.time.deserialize(savegame.time); + errorReason = errorReason || root.camera.deserialize(savegame.camera); + errorReason = errorReason || root.map.deserialize(savegame.map); + errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals); + errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes); + errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints); + errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities); + errorReason = errorReason || root.systemMgr.systems.belt.deserializePaths(savegame.beltPaths); + + // Check for errors + if (errorReason) { + return ExplainedResult.bad(errorReason); + } + + return ExplainedResult.good(); + } +} diff --git a/src/js/savegame/schemas/1006.js b/src/js/savegame/schemas/1006.js new file mode 100644 index 00000000..422814c1 --- /dev/null +++ b/src/js/savegame/schemas/1006.js @@ -0,0 +1,285 @@ +import { gMetaBuildingRegistry } from "../../core/global_registries.js"; +import { createLogger } from "../../core/logging.js"; +import { MetaBeltBuilding } from "../../game/buildings/belt.js"; +import { enumCutterVariants, MetaCutterBuilding } from "../../game/buildings/cutter.js"; +import { MetaHubBuilding } from "../../game/buildings/hub.js"; +import { enumMinerVariants, MetaMinerBuilding } from "../../game/buildings/miner.js"; +import { MetaMixerBuilding } from "../../game/buildings/mixer.js"; +import { enumPainterVariants, MetaPainterBuilding } from "../../game/buildings/painter.js"; +import { enumRotaterVariants, MetaRotaterBuilding } from "../../game/buildings/rotater.js"; +import { enumBalancerVariants, MetaBalancerBuilding } from "../../game/buildings/balancer.js"; +import { MetaStackerBuilding } from "../../game/buildings/stacker.js"; +import { enumTrashVariants, MetaTrashBuilding } from "../../game/buildings/trash.js"; +import { + enumUndergroundBeltVariants, + MetaUndergroundBeltBuilding, +} from "../../game/buildings/underground_belt.js"; +import { getCodeFromBuildingData } from "../../game/building_codes.js"; +import { StaticMapEntityComponent } from "../../game/components/static_map_entity.js"; +import { Entity } from "../../game/entity.js"; +import { defaultBuildingVariant, MetaBuilding } from "../../game/meta_building.js"; +import { SavegameInterface_V1005 } from "./1005.js"; + +const schema = require("./1006.json"); +const logger = createLogger("savegame_interface/1006"); + +/** + * + * @param {typeof MetaBuilding} metaBuilding + * @param {string=} variant + * @param {number=} rotationVariant + */ +function findCode(metaBuilding, variant = defaultBuildingVariant, rotationVariant = 0) { + return getCodeFromBuildingData(gMetaBuildingRegistry.findByClass(metaBuilding), variant, rotationVariant); +} + +/** + * Rebalances a value from the old balancing to the new one + * @param {number} value + * @returns {number} + */ +function rebalance(value) { + return Math.round(Math.pow(value, 0.75)); +} + +export class SavegameInterface_V1006 extends SavegameInterface_V1005 { + getVersion() { + return 1006; + } + + getSchemaUncached() { + return schema; + } + + static computeSpriteMapping() { + return { + // Belt + "sprites/blueprints/belt_top.png": findCode(MetaBeltBuilding, defaultBuildingVariant, 0), + "sprites/blueprints/belt_left.png": findCode(MetaBeltBuilding, defaultBuildingVariant, 1), + "sprites/blueprints/belt_right.png": findCode(MetaBeltBuilding, defaultBuildingVariant, 2), + + // Splitter (=Balancer) + "sprites/blueprints/splitter.png": findCode(MetaBalancerBuilding), + "sprites/blueprints/splitter-compact.png": findCode( + MetaBalancerBuilding, + enumBalancerVariants.merger + ), + "sprites/blueprints/splitter-compact-inverse.png": findCode( + MetaBalancerBuilding, + enumBalancerVariants.mergerInverse + ), + + // Underground belt + "sprites/blueprints/underground_belt_entry.png": findCode( + MetaUndergroundBeltBuilding, + defaultBuildingVariant, + 0 + ), + "sprites/blueprints/underground_belt_exit.png": findCode( + MetaUndergroundBeltBuilding, + defaultBuildingVariant, + 1 + ), + + "sprites/blueprints/underground_belt_entry-tier2.png": findCode( + MetaUndergroundBeltBuilding, + enumUndergroundBeltVariants.tier2, + 0 + ), + "sprites/blueprints/underground_belt_exit-tier2.png": findCode( + MetaUndergroundBeltBuilding, + enumUndergroundBeltVariants.tier2, + 1 + ), + + // Miner + "sprites/blueprints/miner.png": findCode(MetaMinerBuilding), + "sprites/blueprints/miner-chainable.png": findCode( + MetaMinerBuilding, + enumMinerVariants.chainable, + 0 + ), + + // Cutter + "sprites/blueprints/cutter.png": findCode(MetaCutterBuilding), + "sprites/blueprints/cutter-quad.png": findCode(MetaCutterBuilding, enumCutterVariants.quad), + + // Rotater + "sprites/blueprints/rotater.png": findCode(MetaRotaterBuilding), + "sprites/blueprints/rotater-ccw.png": findCode(MetaRotaterBuilding, enumRotaterVariants.ccw), + + // Stacker + "sprites/blueprints/stacker.png": findCode(MetaStackerBuilding), + + // Mixer + "sprites/blueprints/mixer.png": findCode(MetaMixerBuilding), + + // Painter + "sprites/blueprints/painter.png": findCode(MetaPainterBuilding), + "sprites/blueprints/painter-mirrored.png": findCode( + MetaPainterBuilding, + enumPainterVariants.mirrored + ), + "sprites/blueprints/painter-double.png": findCode( + MetaPainterBuilding, + enumPainterVariants.double + ), + "sprites/blueprints/painter-quad.png": findCode(MetaPainterBuilding, enumPainterVariants.quad), + + // Trash / Storage + "sprites/blueprints/trash.png": findCode(MetaTrashBuilding), + "sprites/blueprints/trash-storage.png": findCode(MetaTrashBuilding, enumTrashVariants.storage), + }; + } + + /** + * @param {import("../savegame_typedefs.js").SavegameData} data + */ + static migrate1005to1006(data) { + logger.log("Migrating 1005 to 1006"); + const dump = data.dump; + if (!dump) { + return true; + } + + // Reduce stored shapes + const stored = dump.hubGoals.storedShapes; + for (const shapeKey in stored) { + stored[shapeKey] = rebalance(stored[shapeKey]); + } + + // Reduce goals + if (dump.hubGoals.currentGoal) { + dump.hubGoals.currentGoal.required = rebalance(dump.hubGoals.currentGoal.required); + } + + // Update entities + const entities = dump.entities; + for (let i = 0; i < entities.length; ++i) { + const entity = entities[i]; + const components = entity.components; + this.migrateStaticComp1005to1006(entity); + + // HUB + if (components.Hub) { + // @ts-ignore + components.Hub = {}; + } + + // Item Processor + if (components.ItemProcessor) { + // @ts-ignore + components.ItemProcessor = { + nextOutputSlot: 0, + }; + } + + // OLD: Unremovable component + // @ts-ignore + if (components.Unremovable) { + // @ts-ignore + delete components.Unremovable; + } + + // OLD: ReplaceableMapEntity + // @ts-ignore + if (components.ReplaceableMapEntity) { + // @ts-ignore + delete components.ReplaceableMapEntity; + } + + // ItemAcceptor + if (components.ItemAcceptor) { + // @ts-ignore + components.ItemAcceptor = {}; + } + + // Belt + if (components.Belt) { + // @ts-ignore + components.Belt = {}; + } + + // Item Ejector + if (components.ItemEjector) { + // @ts-ignore + components.ItemEjector = { + slots: [], + }; + } + + // UndergroundBelt + if (components.UndergroundBelt) { + // @ts-ignore + components.UndergroundBelt = { + pendingItems: [], + }; + } + + // Miner + if (components.Miner) { + // @ts-ignore + delete components.Miner.chainable; + + components.Miner.lastMiningTime = 0; + components.Miner.itemChainBuffer = []; + } + + // Storage + if (components.Storage) { + // @ts-ignore + components.Storage = { + storedCount: 0, + storedItem: null, + }; + } + } + } + + /** + * + * @param {Entity} entity + */ + static migrateStaticComp1005to1006(entity) { + const spriteMapping = this.computeSpriteMapping(); + const staticComp = entity.components.StaticMapEntity; + + /** @type {StaticMapEntityComponent} */ + const newStaticComp = {}; + newStaticComp.origin = staticComp.origin; + newStaticComp.originalRotation = staticComp.originalRotation; + newStaticComp.rotation = staticComp.rotation; + + // @ts-ignore + newStaticComp.code = spriteMapping[staticComp.blueprintSpriteKey]; + + // Hub special case + if (entity.components.Hub) { + newStaticComp.code = findCode(MetaHubBuilding); + } + + // Belt special case + if (entity.components.Belt) { + const actualCode = { + top: findCode(MetaBeltBuilding, defaultBuildingVariant, 0), + left: findCode(MetaBeltBuilding, defaultBuildingVariant, 1), + right: findCode(MetaBeltBuilding, defaultBuildingVariant, 2), + }[entity.components.Belt.direction]; + if (actualCode !== newStaticComp.code) { + if (G_IS_DEV) { + console.warn("Belt mismatch"); + } + newStaticComp.code = actualCode; + } + } + + if (!newStaticComp.code) { + throw new Error( + // @ts-ignore + "1006 Migration: Could not reconstruct code for " + staticComp.blueprintSpriteKey + ); + } + + entity.components.StaticMapEntity = newStaticComp; + } +} diff --git a/src/js/savegame/schemas/1006.json b/src/js/savegame/schemas/1006.json new file mode 100644 index 00000000..b0916986 --- /dev/null +++ b/src/js/savegame/schemas/1006.json @@ -0,0 +1,5 @@ +{ + "type": "object", + "required": [], + "additionalProperties": true +} diff --git a/src/js/savegame/serializer_internal.js b/src/js/savegame/serializer_internal.js index 6e5dfbc2..fa02a437 100644 --- a/src/js/savegame/serializer_internal.js +++ b/src/js/savegame/serializer_internal.js @@ -1,3 +1,4 @@ +import { globalConfig } from "../core/config"; import { createLogger } from "../core/logging"; import { Vector } from "../core/vector"; import { getBuildingDataFromCode } from "../game/building_codes"; @@ -78,7 +79,9 @@ export class SerializerInternal { deserializeComponents(root, entity, data) { for (const componentId in data) { if (!entity.components[componentId]) { - logger.warn("Entity no longer has component:", componentId); + if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts) { + logger.warn("Entity no longer has component:", componentId); + } continue; } diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js index 3eea38b7..2dd2db76 100644 --- a/src/js/states/ingame.js +++ b/src/js/states/ingame.js @@ -1,438 +1,458 @@ -import { APPLICATION_ERROR_OCCURED } from "../core/error_handler"; -import { GameState } from "../core/game_state"; -import { logSection, createLogger } from "../core/logging"; -import { waitNextFrame } from "../core/utils"; -import { globalConfig } from "../core/config"; -import { GameLoadingOverlay } from "../game/game_loading_overlay"; -import { KeyActionMapper } from "../game/key_action_mapper"; -import { Savegame } from "../savegame/savegame"; -import { GameCore } from "../game/core"; -import { MUSIC } from "../platform/sound"; - -const logger = createLogger("state/ingame"); - -// Different sub-states -const stages = { - s3_createCore: "🌈 3: Create core", - s4_A_initEmptyGame: "🌈 4/A: Init empty game", - s4_B_resumeGame: "🌈 4/B: Resume game", - - s5_firstUpdate: "🌈 5: First game update", - s6_postLoadHook: "🌈 6: Post load hook", - s7_warmup: "🌈 7: Warmup", - - s10_gameRunning: "🌈 10: Game finally running", - - leaving: "🌈 Saving, then leaving the game", - destroyed: "🌈 DESTROYED: Core is empty and waits for state leave", - initFailed: "🌈 ERROR: Initialization failed!", -}; - -export const gameCreationAction = { - new: "new-game", - resume: "resume-game", -}; - -// Typehints -export class GameCreationPayload { - constructor() { - /** @type {boolean|undefined} */ - this.fastEnter; - - /** @type {Savegame} */ - this.savegame; - } -} - -export class InGameState extends GameState { - constructor() { - super("InGameState"); - - /** @type {GameCreationPayload} */ - this.creationPayload = null; - - // Stores current stage - this.stage = ""; - - /** @type {GameCore} */ - this.core = null; - - /** @type {KeyActionMapper} */ - this.keyActionMapper = null; - - /** @type {GameLoadingOverlay} */ - this.loadingOverlay = null; - - /** @type {Savegame} */ - this.savegame; - - this.boundInputFilter = this.filterInput.bind(this); - } - - /** - * Switches the game into another sub-state - * @param {string} stage - */ - switchStage(stage) { - assert(stage, "Got empty stage"); - if (stage !== this.stage) { - this.stage = stage; - logger.log(this.stage); - return true; - } else { - // log(this, "Re entering", stage); - return false; - } - } - - // GameState implementation - getInnerHTML() { - return ""; - } - - getThemeMusic() { - return MUSIC.theme; - } - - onBeforeExit() { - // logger.log("Saving before quitting"); - // return this.doSave().then(() => { - // logger.log(this, "Successfully saved"); - // // this.stageDestroyed(); - // }); - } - - onAppPause() { - // if (this.stage === stages.s10_gameRunning) { - // logger.log("Saving because app got paused"); - // this.doSave(); - // } - } - - getHasFadeIn() { - return false; - } - - getPauseOnFocusLost() { - return false; - } - - getHasUnloadConfirmation() { - return true; - } - - onLeave() { - if (this.core) { - this.stageDestroyed(); - } - this.app.inputMgr.dismountFilter(this.boundInputFilter); - } - - onResized(w, h) { - super.onResized(w, h); - if (this.stage === stages.s10_gameRunning) { - this.core.resize(w, h); - } - } - - // ---- End of GameState implementation - - /** - * Goes back to the menu state - */ - goBackToMenu() { - this.saveThenGoToState("MainMenuState"); - } - - /** - * Goes back to the settings state - */ - goToSettings() { - this.saveThenGoToState("SettingsState", { - backToStateId: this.key, - backToStatePayload: this.creationPayload, - }); - } - - /** - * Goes back to the settings state - */ - goToKeybindings() { - this.saveThenGoToState("KeybindingsState", { - backToStateId: this.key, - backToStatePayload: this.creationPayload, - }); - } - - /** - * Moves to a state outside of the game - * @param {string} stateId - * @param {any=} payload - */ - saveThenGoToState(stateId, payload) { - if (this.stage === stages.leaving || this.stage === stages.destroyed) { - logger.warn( - "Tried to leave game twice or during destroy:", - this.stage, - "(attempted to move to", - stateId, - ")" - ); - return; - } - this.stageLeavingGame(); - this.doSave().then(() => { - this.stageDestroyed(); - this.moveToState(stateId, payload); - }); - } - - onBackButton() { - // do nothing - } - - /** - * Called when the game somehow failed to initialize. Resets everything to basic state and - * then goes to the main menu, showing the error - * @param {string} err - */ - onInitializationFailure(err) { - if (this.switchStage(stages.initFailed)) { - logger.error("Init failure:", err); - this.stageDestroyed(); - this.moveToState("MainMenuState", { loadError: err }); - } - } - - // STAGES - - /** - * Creates the game core instance, and thus the root - */ - stage3CreateCore() { - if (this.switchStage(stages.s3_createCore)) { - logger.log("Creating new game core"); - this.core = new GameCore(this.app); - - this.core.initializeRoot(this, this.savegame); - - if (this.savegame.hasGameDump()) { - this.stage4bResumeGame(); - } else { - this.app.gameAnalytics.handleGameStarted(); - this.stage4aInitEmptyGame(); - } - } - } - - /** - * Initializes a new empty game - */ - stage4aInitEmptyGame() { - if (this.switchStage(stages.s4_A_initEmptyGame)) { - this.core.initNewGame(); - this.stage5FirstUpdate(); - } - } - - /** - * Resumes an existing game - */ - stage4bResumeGame() { - if (this.switchStage(stages.s4_B_resumeGame)) { - if (!this.core.initExistingGame()) { - this.onInitializationFailure("Savegame is corrupt and can not be restored."); - return; - } - this.app.gameAnalytics.handleGameResumed(); - this.stage5FirstUpdate(); - } - } - - /** - * Performs the first game update on the game which initializes most caches - */ - stage5FirstUpdate() { - if (this.switchStage(stages.s5_firstUpdate)) { - this.core.root.logicInitialized = true; - this.core.updateLogic(); - this.stage6PostLoadHook(); - } - } - - /** - * Call the post load hook, this means that we have loaded the game, and all systems - * can operate and start to work now. - */ - stage6PostLoadHook() { - if (this.switchStage(stages.s6_postLoadHook)) { - logger.log("Post load hook"); - this.core.postLoadHook(); - this.stage7Warmup(); - } - } - - /** - * This makes the game idle and draw for a while, because we run most code this way - * the V8 engine can already start to optimize it. Also this makes sure the resources - * are in the VRAM and we have a smooth experience once we start. - */ - stage7Warmup() { - if (this.switchStage(stages.s7_warmup)) { - if (G_IS_DEV && globalConfig.debug.noArtificialDelays) { - this.warmupTimeSeconds = 0.05; - } else { - if (this.creationPayload.fastEnter) { - this.warmupTimeSeconds = globalConfig.warmupTimeSecondsFast; - } else { - this.warmupTimeSeconds = globalConfig.warmupTimeSecondsRegular; - } - } - } - } - - /** - * The final stage where this game is running and updating regulary. - */ - stage10GameRunning() { - if (this.switchStage(stages.s10_gameRunning)) { - this.core.root.signals.readyToRender.dispatch(); - - logSection("GAME STARTED", "#26a69a"); - - // Initial resize, might have changed during loading (this is possible) - this.core.resize(this.app.screenWidth, this.app.screenHeight); - } - } - - /** - * This stage destroys the whole game, used to cleanup - */ - stageDestroyed() { - if (this.switchStage(stages.destroyed)) { - // Cleanup all api calls - this.cancelAllAsyncOperations(); - - if (this.syncer) { - this.syncer.cancelSync(); - this.syncer = null; - } - - // Cleanup core - if (this.core) { - this.core.destruct(); - this.core = null; - } - } - } - - /** - * When leaving the game - */ - stageLeavingGame() { - if (this.switchStage(stages.leaving)) { - // ... - } - } - - // END STAGES - - /** - * Filters the input (keybindings) - */ - filterInput() { - return this.stage === stages.s10_gameRunning; - } - - /** - * @param {GameCreationPayload} payload - */ - onEnter(payload) { - this.app.inputMgr.installFilter(this.boundInputFilter); - - this.creationPayload = payload; - this.savegame = payload.savegame; - - this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement()); - this.loadingOverlay.showBasic(); - - // Remove unneded default element - document.body.querySelector(".modalDialogParent").remove(); - - this.asyncChannel.watch(waitNextFrame()).then(() => this.stage3CreateCore()); - } - - /** - * Render callback - * @param {number} dt - */ - onRender(dt) { - if (APPLICATION_ERROR_OCCURED) { - // Application somehow crashed, do not do anything - return; - } - - if (this.stage === stages.s7_warmup) { - this.core.draw(); - this.warmupTimeSeconds -= dt / 1000.0; - if (this.warmupTimeSeconds < 0) { - logger.log("Warmup completed"); - this.stage10GameRunning(); - } - } - - if (this.stage === stages.s10_gameRunning) { - this.core.tick(dt); - } - - // If the stage is still active (This might not be the case if tick() moved us to game over) - if (this.stage === stages.s10_gameRunning) { - // Only draw if page visible - if (this.app.pageVisible) { - this.core.draw(); - } - - this.loadingOverlay.removeIfAttached(); - } else { - if (!this.loadingOverlay.isAttached()) { - this.loadingOverlay.showBasic(); - } - } - } - - onBackgroundTick(dt) { - this.onRender(dt); - } - - /** - * Saves the game - */ - - doSave() { - if (!this.savegame || !this.savegame.isSaveable()) { - return Promise.resolve(); - } - - if (APPLICATION_ERROR_OCCURED) { - logger.warn("skipping save because application crashed"); - return Promise.resolve(); - } - - if ( - this.stage !== stages.s10_gameRunning && - this.stage !== stages.s7_warmup && - this.stage !== stages.leaving - ) { - logger.warn("Skipping save because game is not ready"); - return Promise.resolve(); - } - - // First update the game data - logger.log("Starting to save game ..."); - this.core.root.signals.gameSaved.dispatch(); - this.savegame.updateData(this.core.root); - return this.savegame.writeSavegameAndMetadata().catch(err => { - logger.warn("Failed to save:", err); - }); - } -} +import { APPLICATION_ERROR_OCCURED } from "../core/error_handler"; +import { GameState } from "../core/game_state"; +import { logSection, createLogger } from "../core/logging"; +import { waitNextFrame } from "../core/utils"; +import { globalConfig } from "../core/config"; +import { GameLoadingOverlay } from "../game/game_loading_overlay"; +import { KeyActionMapper } from "../game/key_action_mapper"; +import { Savegame } from "../savegame/savegame"; +import { GameCore } from "../game/core"; +import { MUSIC } from "../platform/sound"; + +const logger = createLogger("state/ingame"); + +// Different sub-states +const stages = { + s3_createCore: "🌈 3: Create core", + s4_A_initEmptyGame: "🌈 4/A: Init empty game", + s4_B_resumeGame: "🌈 4/B: Resume game", + + s5_firstUpdate: "🌈 5: First game update", + s6_postLoadHook: "🌈 6: Post load hook", + s7_warmup: "🌈 7: Warmup", + + s10_gameRunning: "🌈 10: Game finally running", + + leaving: "🌈 Saving, then leaving the game", + destroyed: "🌈 DESTROYED: Core is empty and waits for state leave", + initFailed: "🌈 ERROR: Initialization failed!", +}; + +export const gameCreationAction = { + new: "new-game", + resume: "resume-game", +}; + +// Typehints +export class GameCreationPayload { + constructor() { + /** @type {boolean|undefined} */ + this.fastEnter; + + /** @type {Savegame} */ + this.savegame; + } +} + +export class InGameState extends GameState { + constructor() { + super("InGameState"); + + /** @type {GameCreationPayload} */ + this.creationPayload = null; + + // Stores current stage + this.stage = ""; + + /** @type {GameCore} */ + this.core = null; + + /** @type {KeyActionMapper} */ + this.keyActionMapper = null; + + /** @type {GameLoadingOverlay} */ + this.loadingOverlay = null; + + /** @type {Savegame} */ + this.savegame = null; + + this.boundInputFilter = this.filterInput.bind(this); + + /** + * Whether we are currently saving the game + * @TODO: This doesn't realy fit here + */ + this.currentSavePromise = null; + } + + /** + * Switches the game into another sub-state + * @param {string} stage + */ + switchStage(stage) { + assert(stage, "Got empty stage"); + if (stage !== this.stage) { + this.stage = stage; + logger.log(this.stage); + return true; + } else { + // log(this, "Re entering", stage); + return false; + } + } + + // GameState implementation + getInnerHTML() { + return ""; + } + + getThemeMusic() { + return MUSIC.theme; + } + + onBeforeExit() { + // logger.log("Saving before quitting"); + // return this.doSave().then(() => { + // logger.log(this, "Successfully saved"); + // // this.stageDestroyed(); + // }); + } + + onAppPause() { + // if (this.stage === stages.s10_gameRunning) { + // logger.log("Saving because app got paused"); + // this.doSave(); + // } + } + + getHasFadeIn() { + return false; + } + + getPauseOnFocusLost() { + return false; + } + + getHasUnloadConfirmation() { + return true; + } + + onLeave() { + if (this.core) { + this.stageDestroyed(); + } + this.app.inputMgr.dismountFilter(this.boundInputFilter); + } + + onResized(w, h) { + super.onResized(w, h); + if (this.stage === stages.s10_gameRunning) { + this.core.resize(w, h); + } + } + + // ---- End of GameState implementation + + /** + * Goes back to the menu state + */ + goBackToMenu() { + this.saveThenGoToState("MainMenuState"); + } + + /** + * Goes back to the settings state + */ + goToSettings() { + this.saveThenGoToState("SettingsState", { + backToStateId: this.key, + backToStatePayload: this.creationPayload, + }); + } + + /** + * Goes back to the settings state + */ + goToKeybindings() { + this.saveThenGoToState("KeybindingsState", { + backToStateId: this.key, + backToStatePayload: this.creationPayload, + }); + } + + /** + * Moves to a state outside of the game + * @param {string} stateId + * @param {any=} payload + */ + saveThenGoToState(stateId, payload) { + if (this.stage === stages.leaving || this.stage === stages.destroyed) { + logger.warn( + "Tried to leave game twice or during destroy:", + this.stage, + "(attempted to move to", + stateId, + ")" + ); + return; + } + this.stageLeavingGame(); + this.doSave().then(() => { + this.stageDestroyed(); + this.moveToState(stateId, payload); + }); + } + + onBackButton() { + // do nothing + } + + /** + * Called when the game somehow failed to initialize. Resets everything to basic state and + * then goes to the main menu, showing the error + * @param {string} err + */ + onInitializationFailure(err) { + if (this.switchStage(stages.initFailed)) { + logger.error("Init failure:", err); + this.stageDestroyed(); + this.moveToState("MainMenuState", { loadError: err }); + } + } + + // STAGES + + /** + * Creates the game core instance, and thus the root + */ + stage3CreateCore() { + if (this.switchStage(stages.s3_createCore)) { + logger.log("Creating new game core"); + this.core = new GameCore(this.app); + + this.core.initializeRoot(this, this.savegame); + + if (this.savegame.hasGameDump()) { + this.stage4bResumeGame(); + } else { + this.app.gameAnalytics.handleGameStarted(); + this.stage4aInitEmptyGame(); + } + } + } + + /** + * Initializes a new empty game + */ + stage4aInitEmptyGame() { + if (this.switchStage(stages.s4_A_initEmptyGame)) { + this.core.initNewGame(); + this.stage5FirstUpdate(); + } + } + + /** + * Resumes an existing game + */ + stage4bResumeGame() { + if (this.switchStage(stages.s4_B_resumeGame)) { + if (!this.core.initExistingGame()) { + this.onInitializationFailure("Savegame is corrupt and can not be restored."); + return; + } + this.app.gameAnalytics.handleGameResumed(); + this.stage5FirstUpdate(); + } + } + + /** + * Performs the first game update on the game which initializes most caches + */ + stage5FirstUpdate() { + if (this.switchStage(stages.s5_firstUpdate)) { + this.core.root.logicInitialized = true; + this.core.updateLogic(); + this.stage6PostLoadHook(); + } + } + + /** + * Call the post load hook, this means that we have loaded the game, and all systems + * can operate and start to work now. + */ + stage6PostLoadHook() { + if (this.switchStage(stages.s6_postLoadHook)) { + logger.log("Post load hook"); + this.core.postLoadHook(); + this.stage7Warmup(); + } + } + + /** + * This makes the game idle and draw for a while, because we run most code this way + * the V8 engine can already start to optimize it. Also this makes sure the resources + * are in the VRAM and we have a smooth experience once we start. + */ + stage7Warmup() { + if (this.switchStage(stages.s7_warmup)) { + if (G_IS_DEV && globalConfig.debug.noArtificialDelays) { + this.warmupTimeSeconds = 0.05; + } else { + if (this.creationPayload.fastEnter) { + this.warmupTimeSeconds = globalConfig.warmupTimeSecondsFast; + } else { + this.warmupTimeSeconds = globalConfig.warmupTimeSecondsRegular; + } + } + } + } + + /** + * The final stage where this game is running and updating regulary. + */ + stage10GameRunning() { + if (this.switchStage(stages.s10_gameRunning)) { + this.core.root.signals.readyToRender.dispatch(); + + logSection("GAME STARTED", "#26a69a"); + + // Initial resize, might have changed during loading (this is possible) + this.core.resize(this.app.screenWidth, this.app.screenHeight); + } + } + + /** + * This stage destroys the whole game, used to cleanup + */ + stageDestroyed() { + if (this.switchStage(stages.destroyed)) { + // Cleanup all api calls + this.cancelAllAsyncOperations(); + + if (this.syncer) { + this.syncer.cancelSync(); + this.syncer = null; + } + + // Cleanup core + if (this.core) { + this.core.destruct(); + this.core = null; + } + } + } + + /** + * When leaving the game + */ + stageLeavingGame() { + if (this.switchStage(stages.leaving)) { + // ... + } + } + + // END STAGES + + /** + * Filters the input (keybindings) + */ + filterInput() { + return this.stage === stages.s10_gameRunning; + } + + /** + * @param {GameCreationPayload} payload + */ + onEnter(payload) { + this.app.inputMgr.installFilter(this.boundInputFilter); + + this.creationPayload = payload; + this.savegame = payload.savegame; + + this.loadingOverlay = new GameLoadingOverlay(this.app, this.getDivElement()); + this.loadingOverlay.showBasic(); + + // Remove unneded default element + document.body.querySelector(".modalDialogParent").remove(); + + this.asyncChannel.watch(waitNextFrame()).then(() => this.stage3CreateCore()); + } + + /** + * Render callback + * @param {number} dt + */ + onRender(dt) { + if (APPLICATION_ERROR_OCCURED) { + // Application somehow crashed, do not do anything + return; + } + + if (this.stage === stages.s7_warmup) { + this.core.draw(); + this.warmupTimeSeconds -= dt / 1000.0; + if (this.warmupTimeSeconds < 0) { + logger.log("Warmup completed"); + this.stage10GameRunning(); + } + } + + if (this.stage === stages.s10_gameRunning) { + this.core.tick(dt); + } + + // If the stage is still active (This might not be the case if tick() moved us to game over) + if (this.stage === stages.s10_gameRunning) { + // Only draw if page visible + if (this.app.pageVisible) { + this.core.draw(); + } + + this.loadingOverlay.removeIfAttached(); + } else { + if (!this.loadingOverlay.isAttached()) { + this.loadingOverlay.showBasic(); + } + } + } + + onBackgroundTick(dt) { + this.onRender(dt); + } + + /** + * Saves the game + */ + + doSave() { + if (!this.savegame || !this.savegame.isSaveable()) { + return Promise.resolve(); + } + + if (APPLICATION_ERROR_OCCURED) { + logger.warn("skipping save because application crashed"); + return Promise.resolve(); + } + + if ( + this.stage !== stages.s10_gameRunning && + this.stage !== stages.s7_warmup && + this.stage !== stages.leaving + ) { + logger.warn("Skipping save because game is not ready"); + return Promise.resolve(); + } + + if (this.currentSavePromise) { + logger.warn("Skipping double save and returning same promise"); + return this.currentSavePromise; + } + logger.log("Starting to save game ..."); + this.savegame.updateData(this.core.root); + + this.currentSavePromise = this.savegame + .writeSavegameAndMetadata() + .catch(err => { + // Catch errors + logger.warn("Failed to save:", err); + }) + .then(() => { + // Clear promise + logger.log("Saved!"); + this.core.root.signals.gameSaved.dispatch(); + this.currentSavePromise = null; + }); + + return this.currentSavePromise; + } +} diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index bea209a8..02c1e690 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -17,6 +17,8 @@ import { getApplicationSettingById } from "../profile/application_settings"; import { FormElementInput } from "../core/modal_dialog_forms"; import { DialogWithForm } from "../core/modal_dialog_elements"; +const trim = require("trim"); + /** * @typedef {import("../savegame/savegame_typedefs").SavegameMetadata} SavegameMetadata * @typedef {import("../profile/setting_types").EnumSetting} EnumSetting @@ -133,7 +135,7 @@ export class MainMenuState extends GameState { !this.app.platformWrapper.getHasUnlimitedSavegames() ) { this.app.analytics.trackUiClick("importgame_slot_limit_show"); - this.dialogs.showWarning(T.dialogs.oneSavegameLimit.title, T.dialogs.oneSavegameLimit.desc); + this.showSavegameSlotLimit(); return; } @@ -436,7 +438,7 @@ export class MainMenuState extends GameState { label: null, placeholder: "", defaultValue: game.name || "", - validator: val => val.match(regex), + validator: val => val.match(regex) && trim(val).length > 0, }); const dialog = new DialogWithForm({ app: this.app, @@ -449,7 +451,7 @@ export class MainMenuState extends GameState { // When confirmed, save the name dialog.buttonSignals.ok.add(() => { - game.name = nameInput.getValue(); + game.name = trim(nameInput.getValue()); this.app.savegameMgr.writeAsync(); this.renderSavegames(); }); @@ -488,8 +490,10 @@ export class MainMenuState extends GameState { const signals = this.dialogs.showWarning( T.dialogs.confirmSavegameDelete.title, - T.dialogs.confirmSavegameDelete.text, - ["delete:bad", "cancel:good"] + T.dialogs.confirmSavegameDelete.text + .replace("", game.name || T.mainMenu.savegameUnnamed) + .replace("", String(game.level)), + ["cancel:good", "delete:bad:timeout"] ); signals.delete.add(() => { @@ -522,6 +526,21 @@ export class MainMenuState extends GameState { }); } + /** + * Shows a hint that the slot limit has been reached + */ + showSavegameSlotLimit() { + const { getStandalone } = this.dialogs.showWarning( + T.dialogs.oneSavegameLimit.title, + T.dialogs.oneSavegameLimit.desc, + ["cancel:bad", "getStandalone:good"] + ); + getStandalone.add(() => { + this.app.analytics.trackUiClick("visit_steampage_from_slot_limit"); + this.app.platformWrapper.openExternalLink(THIRDPARTY_URLS.standaloneStorePage); + }); + } + onSettingsButtonClicked() { this.moveToState("SettingsState"); } @@ -540,7 +559,7 @@ export class MainMenuState extends GameState { !this.app.platformWrapper.getHasUnlimitedSavegames() ) { this.app.analytics.trackUiClick("startgame_slot_limit_show"); - this.dialogs.showWarning(T.dialogs.oneSavegameLimit.title, T.dialogs.oneSavegameLimit.desc); + this.showSavegameSlotLimit(); return; } diff --git a/src/js/states/preload.js b/src/js/states/preload.js index 0f47e8d6..b35b369d 100644 --- a/src/js/states/preload.js +++ b/src/js/states/preload.js @@ -1,293 +1,331 @@ -import { GameState } from "../core/game_state"; -import { createLogger } from "../core/logging"; -import { findNiceValue } from "../core/utils"; -import { cachebust } from "../core/cachebust"; -import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; -import { T, autoDetectLanguageId, updateApplicationLanguage } from "../translations"; -import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; -import { CHANGELOG } from "../changelog"; -import { globalConfig } from "../core/config"; - -const logger = createLogger("state/preload"); - -export class PreloadState extends GameState { - constructor() { - super("PreloadState"); - } - - getInnerHTML() { - return ` -
-
- Booting - - - 0% - -
- - `; - } - - getThemeMusic() { - return null; - } - - getHasFadeIn() { - return false; - } - - onEnter() { - this.htmlElement.classList.add("prefab_LoadingState"); - - const elementsToRemove = ["#loadingPreload", "#fontPreload"]; - for (let i = 0; i < elementsToRemove.length; ++i) { - const elem = document.querySelector(elementsToRemove[i]); - if (elem) { - elem.remove(); - } - } - - this.dialogs = new HUDModalDialogs(null, this.app); - const dialogsElement = document.body.querySelector(".modalDialogParent"); - this.dialogs.initializeToElement(dialogsElement); - - /** @type {HTMLElement} */ - this.statusText = this.htmlElement.querySelector(".loadingStatus > .desc"); - /** @type {HTMLElement} */ - this.statusBar = this.htmlElement.querySelector(".loadingStatus > .bar > .inner"); - /** @type {HTMLElement} */ - this.statusBarText = this.htmlElement.querySelector(".loadingStatus > .bar > .status"); - - this.currentStatus = "booting"; - this.currentIndex = 0; - - this.startLoading(); - } - - onLeave() { - // this.dialogs.cleanup(); - } - - startLoading() { - this.setStatus("Booting") - - .then(() => this.setStatus("Creating platform wrapper")) - .then(() => this.app.platformWrapper.initialize()) - - .then(() => this.setStatus("Initializing local storage")) - .then(() => { - const wrapper = this.app.platformWrapper; - if (wrapper instanceof PlatformWrapperImplBrowser) { - try { - window.localStorage.setItem("local_storage_test", "1"); - window.localStorage.removeItem("local_storage_test"); - } catch (ex) { - logger.error("Failed to read/write local storage:", ex); - return new Promise(() => { - alert(`Your brower does not support thirdparty cookies or you have disabled it in your security settings.\n\n - In Chrome this setting is called "Block third-party cookies and site data".\n\n - Please allow third party cookies and then reload the page.`); - // Never return - }); - } - } - }) - - .then(() => this.setStatus("Creating storage")) - .then(() => { - return this.app.storage.initialize(); - }) - - .then(() => this.setStatus("Initializing libraries")) - .then(() => this.app.analytics.initialize()) - .then(() => this.app.gameAnalytics.initialize()) - - .then(() => this.setStatus("Initializing settings")) - .then(() => { - return this.app.settings.initialize(); - }) - - .then(() => { - // Initialize fullscreen - if (this.app.platformWrapper.getSupportsFullscreen()) { - this.app.platformWrapper.setFullscreen(this.app.settings.getIsFullScreen()); - } - }) - - .then(() => this.setStatus("Initializing language")) - .then(() => { - if (this.app.settings.getLanguage() === "auto-detect") { - const language = autoDetectLanguageId(); - logger.log("Setting language to", language); - return this.app.settings.updateLanguage(language); - } - }) - .then(() => { - const language = this.app.settings.getLanguage(); - updateApplicationLanguage(language); - }) - - .then(() => this.setStatus("Initializing sounds")) - .then(() => { - // Notice: We don't await the sounds loading itself - return this.app.sound.initialize(); - }) - - .then(() => { - this.app.backgroundResourceLoader.startLoading(); - }) - - .then(() => this.setStatus("Initializing savegame")) - .then(() => { - return this.app.savegameMgr.initialize().catch(err => { - logger.error("Failed to initialize savegames:", err); - alert( - "Your savegames failed to load, it seems your data files got corrupted. I'm so sorry!\n\n(This can happen if your pc crashed while a game was saved).\n\nYou can try re-importing your savegames." - ); - return this.app.savegameMgr.writeAsync(); - }); - }) - - .then(() => this.setStatus("Downloading resources")) - .then(() => { - return this.app.backgroundResourceLoader.getPromiseForBareGame(); - }) - - .then(() => this.setStatus("Checking changelog")) - .then(() => { - if (G_IS_DEV && globalConfig.debug.disableUpgradeNotification) { - return; - } - - return this.app.storage - .readFileAsync("lastversion.bin") - .catch(err => { - logger.warn("Failed to read lastversion:", err); - return G_BUILD_VERSION; - }) - .then(version => { - logger.log("Last version:", version, "App version:", G_BUILD_VERSION); - this.app.storage.writeFileAsync("lastversion.bin", G_BUILD_VERSION); - return version; - }) - .then(version => { - let changelogEntries = []; - logger.log("Last seen version:", version); - - for (let i = 0; i < CHANGELOG.length; ++i) { - if (CHANGELOG[i].version === version) { - break; - } - changelogEntries.push(CHANGELOG[i]); - } - if (changelogEntries.length === 0) { - return; - } - - let dialogHtml = T.dialogs.updateSummary.desc; - for (let i = 0; i < changelogEntries.length; ++i) { - const entry = changelogEntries[i]; - dialogHtml += ` -
- ${entry.version} - ${entry.date} -
    - ${entry.entries.map(text => `
  • ${text}
  • `).join("")} -
-
- `; - } - - return new Promise(resolve => { - this.dialogs.showInfo(T.dialogs.updateSummary.title, dialogHtml).ok.add(resolve); - }); - }); - }) - - .then(() => this.setStatus("Launching")) - .then( - () => { - this.moveToState("MainMenuState"); - }, - err => { - this.showFailMessage(err); - } - ); - } - - setStatus(text) { - logger.log("✅ " + text); - this.currentIndex += 1; - this.currentStatus = text; - this.statusText.innerText = text; - - const numSteps = 10; // FIXME - - const percentage = (this.currentIndex / numSteps) * 100.0; - this.statusBar.style.width = percentage + "%"; - this.statusBarText.innerText = findNiceValue(percentage) + "%"; - - return Promise.resolve(); - } - - showFailMessage(text) { - logger.error("App init failed:", text); - - const email = "bugs@shapez.io"; - - const subElement = document.createElement("div"); - subElement.classList.add("failureBox"); - - subElement.innerHTML = ` - -
-
- Failed to initialize application! -
-
- ${this.currentStatus} failed:
- ${text} -
- -
- Please send me an email with steps to reproduce and what you did before this happened: -
-
- -
- - Build ${G_BUILD_VERSION} @ ${G_BUILD_COMMIT_HASH} -
-
- `; - - this.htmlElement.classList.add("failure"); - this.htmlElement.appendChild(subElement); - - const resetBtn = subElement.querySelector("button.resetApp"); - this.trackClicks(resetBtn, this.showResetConfirm); - } - - showResetConfirm() { - if (confirm("Are you sure you want to reset the app? This will delete all your savegames")) { - this.resetApp(); - } - } - - resetApp() { - this.app.settings - .resetEverythingAsync() - .then(() => { - this.app.savegameMgr.resetEverythingAsync(); - }) - .then(() => { - this.app.settings.resetEverythingAsync(); - }) - .then(() => { - window.location.reload(); - }); - } -} +import { CHANGELOG } from "../changelog"; +import { cachebust } from "../core/cachebust"; +import { globalConfig } from "../core/config"; +import { GameState } from "../core/game_state"; +import { createLogger } from "../core/logging"; +import { findNiceValue } from "../core/utils"; +import { getRandomHint } from "../game/hints"; +import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; +import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper"; +import { autoDetectLanguageId, T, updateApplicationLanguage } from "../translations"; + +const logger = createLogger("state/preload"); + +export class PreloadState extends GameState { + constructor() { + super("PreloadState"); + } + + getInnerHTML() { + return ` +
+
+ Booting + + + 0% + +
+ + + `; + } + + getThemeMusic() { + return null; + } + + getHasFadeIn() { + return false; + } + + onEnter() { + this.htmlElement.classList.add("prefab_LoadingState"); + + const elementsToRemove = ["#loadingPreload", "#fontPreload"]; + for (let i = 0; i < elementsToRemove.length; ++i) { + const elem = document.querySelector(elementsToRemove[i]); + if (elem) { + elem.remove(); + } + } + + this.dialogs = new HUDModalDialogs(null, this.app); + const dialogsElement = document.body.querySelector(".modalDialogParent"); + this.dialogs.initializeToElement(dialogsElement); + + /** @type {HTMLElement} */ + this.statusText = this.htmlElement.querySelector(".loadingStatus > .desc"); + /** @type {HTMLElement} */ + this.statusBar = this.htmlElement.querySelector(".loadingStatus > .bar > .inner"); + /** @type {HTMLElement} */ + this.statusBarText = this.htmlElement.querySelector(".loadingStatus > .bar > .status"); + + /** @type {HTMLElement} */ + this.hintsText = this.htmlElement.querySelector(".prefab_GameHint"); + this.lastHintShown = -1000; + this.nextHintDuration = 0; + + this.currentStatus = "booting"; + this.currentIndex = 0; + + this.startLoading(); + } + + onLeave() { + // this.dialogs.cleanup(); + } + + startLoading() { + this.setStatus("Booting") + + .then(() => this.setStatus("Creating platform wrapper")) + .then(() => this.app.platformWrapper.initialize()) + + .then(() => this.setStatus("Initializing local storage")) + .then(() => { + const wrapper = this.app.platformWrapper; + if (wrapper instanceof PlatformWrapperImplBrowser) { + try { + window.localStorage.setItem("local_storage_test", "1"); + window.localStorage.removeItem("local_storage_test"); + } catch (ex) { + logger.error("Failed to read/write local storage:", ex); + return new Promise(() => { + alert(`Your brower does not support thirdparty cookies or you have disabled it in your security settings.\n\n + In Chrome this setting is called "Block third-party cookies and site data".\n\n + Please allow third party cookies and then reload the page.`); + // Never return + }); + } + } + }) + + .then(() => this.setStatus("Creating storage")) + .then(() => { + return this.app.storage.initialize(); + }) + + .then(() => this.setStatus("Initializing libraries")) + .then(() => this.app.analytics.initialize()) + .then(() => this.app.gameAnalytics.initialize()) + + .then(() => this.setStatus("Initializing settings")) + .then(() => { + return this.app.settings.initialize(); + }) + + .then(() => { + // Initialize fullscreen + if (this.app.platformWrapper.getSupportsFullscreen()) { + this.app.platformWrapper.setFullscreen(this.app.settings.getIsFullScreen()); + } + }) + + .then(() => this.setStatus("Initializing language")) + .then(() => { + if (this.app.settings.getLanguage() === "auto-detect") { + const language = autoDetectLanguageId(); + logger.log("Setting language to", language); + return this.app.settings.updateLanguage(language); + } + }) + .then(() => { + const language = this.app.settings.getLanguage(); + updateApplicationLanguage(language); + }) + + .then(() => this.setStatus("Initializing sounds")) + .then(() => { + // Notice: We don't await the sounds loading itself + return this.app.sound.initialize(); + }) + + .then(() => { + this.app.backgroundResourceLoader.startLoading(); + }) + + .then(() => this.setStatus("Initializing savegame")) + .then(() => { + return this.app.savegameMgr.initialize().catch(err => { + logger.error("Failed to initialize savegames:", err); + alert( + "Your savegames failed to load, it seems your data files got corrupted. I'm so sorry!\n\n(This can happen if your pc crashed while a game was saved).\n\nYou can try re-importing your savegames." + ); + return this.app.savegameMgr.writeAsync(); + }); + }) + + .then(() => this.setStatus("Downloading resources")) + .then(() => { + return this.app.backgroundResourceLoader.getPromiseForBareGame(); + }) + + .then(() => this.setStatus("Checking changelog")) + .then(() => { + if (G_IS_DEV && globalConfig.debug.disableUpgradeNotification) { + return; + } + + return this.app.storage + .readFileAsync("lastversion.bin") + .catch(err => { + logger.warn("Failed to read lastversion:", err); + return G_BUILD_VERSION; + }) + .then(version => { + logger.log("Last version:", version, "App version:", G_BUILD_VERSION); + this.app.storage.writeFileAsync("lastversion.bin", G_BUILD_VERSION); + return version; + }) + .then(version => { + let changelogEntries = []; + logger.log("Last seen version:", version); + + for (let i = 0; i < CHANGELOG.length; ++i) { + if (CHANGELOG[i].version === version) { + break; + } + changelogEntries.push(CHANGELOG[i]); + } + if (changelogEntries.length === 0) { + return; + } + + let dialogHtml = T.dialogs.updateSummary.desc; + for (let i = 0; i < changelogEntries.length; ++i) { + const entry = changelogEntries[i]; + dialogHtml += ` +
+ ${entry.version} + ${entry.date} +
    + ${entry.entries.map(text => `
  • ${text}
  • `).join("")} +
+
+ `; + } + + return new Promise(resolve => { + this.dialogs.showInfo(T.dialogs.updateSummary.title, dialogHtml).ok.add(resolve); + }); + }); + }) + + .then(() => this.setStatus("Launching")) + .then( + () => { + this.moveToState("MainMenuState"); + }, + err => { + this.showFailMessage(err); + } + ); + } + + update() { + const now = performance.now(); + if (now - this.lastHintShown > this.nextHintDuration) { + this.lastHintShown = now; + const hintText = getRandomHint(); + + this.hintsText.innerHTML = hintText; + + /** + * Compute how long the user will need to read the hint. + * We calculate with 130 words per minute, with an average of 5 chars + * that is 650 characters / minute + */ + this.nextHintDuration = Math.max(2500, (hintText.length / 650) * 60 * 1000); + } + } + + onRender() { + this.update(); + } + + onBackgroundTick() { + this.update(); + } + + /** + * + * @param {string} text + */ + setStatus(text) { + logger.log("✅ " + text); + this.currentIndex += 1; + this.currentStatus = text; + this.statusText.innerText = text; + + const numSteps = 10; // FIXME + + const percentage = (this.currentIndex / numSteps) * 100.0; + this.statusBar.style.width = percentage + "%"; + this.statusBarText.innerText = findNiceValue(percentage) + "%"; + + return Promise.resolve(); + } + + showFailMessage(text) { + logger.error("App init failed:", text); + + const email = "bugs@shapez.io"; + + const subElement = document.createElement("div"); + subElement.classList.add("failureBox"); + + subElement.innerHTML = ` + +
+
+ Failed to initialize application! +
+
+ ${this.currentStatus} failed:
+ ${text} +
+ +
+ Please send me an email with steps to reproduce and what you did before this happened: +
+
+ +
+ + Build ${G_BUILD_VERSION} @ ${G_BUILD_COMMIT_HASH} +
+
+ `; + + this.htmlElement.classList.add("failure"); + this.htmlElement.appendChild(subElement); + + const resetBtn = subElement.querySelector("button.resetApp"); + this.trackClicks(resetBtn, this.showResetConfirm); + + this.hintsText.remove(); + } + + showResetConfirm() { + if (confirm("Are you sure you want to reset the app? This will delete all your savegames!")) { + this.resetApp(); + } + } + + resetApp() { + this.app.settings + .resetEverythingAsync() + .then(() => { + this.app.savegameMgr.resetEverythingAsync(); + }) + .then(() => { + this.app.settings.resetEverythingAsync(); + }) + .then(() => { + window.location.reload(); + }); + } +} diff --git a/translations/README.md b/translations/README.md index 7695f022..596da8d8 100644 --- a/translations/README.md +++ b/translations/README.md @@ -1,80 +1,82 @@ -# Translations - -The base language is English and can be found [here](base-en.yaml). - -## Languages - -- [German](base-de.yaml) -- [French](base-fr.yaml) -- [Korean](base-kor.yaml) -- [Dutch](base-nl.yaml) -- [Polish](base-pl.yaml) -- [Portuguese (Brazil)](base-pt-BR.yaml) -- [Portuguese (Portugal)](base-pt-PT.yaml) -- [Russian](base-ru.yaml) -- [Greek](base-el.yaml) -- [Italian](base-it.yaml) -- [Romanian](base-ro.yaml) -- [Swedish](base-sv.yaml) -- [Chinese (Simplified)](base-zh-CN.yaml) -- [Chinese (Traditional)](base-zh-TW.yaml) -- [Spanish](base-es.yaml) -- [Hungarian](base-hu.yaml) -- [Turkish](base-tr.yaml) -- [Japanese](base-ja.yaml) -- [Lithuanian](base-lt.yaml) -- [Arabic](base-ar.yaml) -- [Norwegian](base-no.yaml) -- [Kroatian](base-hr.yaml) -- [Danish](base-da.yaml) -- [Finnish](base-fi.yaml) -- [Catalan](base-cat.yaml) -- [Slovenian](base-sl.yaml) -- [Ukrainian](base-uk.yaml) -- [Indonesian](base-ind.yaml) -- [Serbian](base-sr.yaml) - -(If you want to translate into a new language, see below!) - -## Editing existing translations - -If you want to edit an existing translation (Fixing typos, Updating it to a newer version, etc), you can just use the github file editor to edit the file. - -- Click the language you want to edit from the list above -- Click the small "edit" symbol on the top right - -edit symbol - -- Do the changes you wish to do (Be sure **not** to translate placeholders! For example, ` minutes` should get ` Minuten` and **not** ` Minuten`!) - -- Click "Propose Changes" - -propose changes - -- Click "Create pull request" - -create pull request - -- I will review your changes and make comments, and eventually merge them so they will be in the next release! Be sure to regulary check the created pull request for comments. - -## Adding a new language - -Please DM me on Discord (tobspr#5407), so I can add the language template for you. - -Please use the following template: - -``` -Hey, could you add a new translation? - -Language: -Short code: -Local Name: -``` - -You can find the short code [here](https://www.science.co.il/language/Codes.php) (In column `Code 2`). - -PS: I'm super busy, but I'll give my best to do it quickly! - -## Updating a language to the latest version - -Run `yarn syncTranslations` in the root directory to synchronize all translations to the latest version! This will remove obsolete keys and add newly added keys. (Run `yarn` before to install packes). +# Translations + +The base language is English and can be found [here](base-en.yaml). + +## Languages + +- [German](base-de.yaml) +- [French](base-fr.yaml) +- [Korean](base-kor.yaml) +- [Dutch](base-nl.yaml) +- [Polish](base-pl.yaml) +- [Portuguese (Brazil)](base-pt-BR.yaml) +- [Portuguese (Portugal)](base-pt-PT.yaml) +- [Russian](base-ru.yaml) +- [Greek](base-el.yaml) +- [Italian](base-it.yaml) +- [Romanian](base-ro.yaml) +- [Swedish](base-sv.yaml) +- [Chinese (Simplified)](base-zh-CN.yaml) +- [Chinese (Traditional)](base-zh-TW.yaml) +- [Spanish](base-es.yaml) +- [Hungarian](base-hu.yaml) +- [Turkish](base-tr.yaml) +- [Japanese](base-ja.yaml) +- [Lithuanian](base-lt.yaml) +- [Arabic](base-ar.yaml) +- [Norwegian](base-no.yaml) +- [Kroatian](base-hr.yaml) +- [Danish](base-da.yaml) +- [Finnish](base-fi.yaml) +- [Catalan](base-cat.yaml) +- [Slovenian](base-sl.yaml) +- [Ukrainian](base-uk.yaml) +- [Indonesian](base-ind.yaml) +- [Serbian](base-sr.yaml) + +(If you want to translate into a new language, see below!) + +## Editing existing translations + +If you want to edit an existing translation (Fixing typos, updating it to a newer version, etc), you can just use the github file editor to edit the file. + +- Click the language you want to edit from the list above +- Click the small "edit" symbol on the top right + +edit symbol + +- Do the changes you wish to do (Be sure **not** to translate placeholders! For example, ` minutes` should get ` Minuten` and **not** ` Minuten`!) + +- Click "Propose Changes" + +propose changes + +- Click "Create pull request" + +create pull request + +- I will review your changes and make comments, and eventually merge them so they will be in the next release! Be sure to regulary check the created pull request for comments. + +## Adding a new language + +Please DM me on Discord (tobspr#5407), so I can add the language template for you. + +**Important: I am currently not accepting new languages until the wires update is out!** + +Please use the following template: + +``` +Hey, could you add a new translation? + +Language: +Short code: +Local Name: +``` + +You can find the short code [here](https://www.science.co.il/language/Codes.php) (In column `Code 2`). + +PS: I'm super busy, but I'll give my best to do it quickly! + +## Updating a language to the latest version + +Run `yarn syncTranslations` in the root directory to synchronize all translations to the latest version! This will remove obsolete keys and add newly added keys. (Run `yarn` before to install packages). diff --git a/translations/base-de.yaml b/translations/base-de.yaml index 84c8a57e..fee9615a 100644 --- a/translations/base-de.yaml +++ b/translations/base-de.yaml @@ -66,7 +66,7 @@ steamPage: [*] Verschiedene Karten und Herausforderungen (z.B. Karten mit Hindernissen) [*] Puzzle (Liefere die geforderte Form mit begrenztem Platz/limitierten Gebäuden) [*] Eine Kampagne mit Gebäudekosten - [*] Konfigurierbarer Kartengenerator (Ändere die Grösse/Anzahl/Dichte der Ressourcenflecken, den Seed und viel mehr) + [*] Konfigurierbarer Kartengenerator (Ändere die Grösse/Anzahl/Dichte der Ressourcenflecken, den Seed und vieles mehr) [*] Mehr Formentypen [*] Performanceverbesserungen (Das Spiel läuft bereits sehr gut!) [*] Und vieles mehr! @@ -256,7 +256,7 @@ dialogs: title: Nützliche Hotkeys desc: >- Dieses Spiel hat viele Hotkeys, die den Bau von Fabriken vereinfachen und beschleunigen. - Hier sind ein paar, aber prüfe am besten die Tastenbelegung-Einstellungen!

+ Hier sind ein paar Beispiele, aber prüfe am besten die Tastenbelegung-Einstellungen!

STRG + Ziehen: Wähle Areal aus.
UMSCH: Halten, um mehrere Gebäude zu platzieren.
ALT: Invertiere die Platzierungsrichtung der Förderbänder.
@@ -635,7 +635,7 @@ storyRewards: no_reward: title: Nächstes Level desc: >- - Dieses Level hat dir keine Belohnung gegeben, aber dafür das Nächste schon!

PS: Denke daran, deine alten Fabriken nicht zu zerstören - Du wirst sie später alle noch brauchen, um Upgrades freizuschalten! + Dieses Level hat dir keine Belohnung gegeben, aber im Nächsten gibt es eine!

PS: Denke daran, deine alten Fabriken nicht zu zerstören - Du wirst sie später alle noch brauchen, um Upgrades freizuschalten! no_reward_freeplay: title: Nächstes Level @@ -694,7 +694,7 @@ settings: movementSpeed: title: Bewegungsgeschwindigkeit description: >- - Ändert die Geschwindigkeit, mit der der Bildschirm durch die Pfeiltasten bewegt wird. + Ändert die Geschwindigkeit, mit welcher der Bildschirm durch die Pfeiltasten bewegt wird. speeds: super_slow: Sehr langsam slow: Langsam @@ -711,7 +711,7 @@ settings: enableColorBlindHelper: title: Modus für Farbenblinde description: >- - Aktiviert verschiedene Werkzeuge, die dir das Spielen trotz Farbenblindheit ermöglichen. + Aktiviert verschiedene Werkzeuge, welche dir das Spielen trotz Farbenblindheit ermöglichen. fullscreen: title: Vollbild @@ -775,7 +775,7 @@ settings: rotationByBuilding: title: Rotation pro Gebäudetyp description: >- - Jeder Gebäudetyp merkt sich einzeln, in welche Richtung er zeigt. + Jeder Gebäudetyp merkt sich eigenständig, in welche Richtung er zeigt. Das fühlt sich möglicherweise besser an, wenn du häufig zwischen verschiedenen Gebäudetypen wechselst. compactBuildingInfo: @@ -786,7 +786,7 @@ settings: disableCutDeleteWarnings: title: Deaktiviere Warnungsdialog beim Löschen description: >- - Deaktiviert die Warnung, die beim Löschen und Ausschneiden von mehr als 100 Feldern angezeigt wird. + Deaktiviert die Warnung, welche beim Löschen und Ausschneiden von mehr als 100 Feldern angezeigt wird. keybindings: title: Tastenbelegung diff --git a/translations/base-el.yaml b/translations/base-el.yaml index a8c8b241..30f825e5 100644 --- a/translations/base-el.yaml +++ b/translations/base-el.yaml @@ -22,7 +22,7 @@ --- steamPage: # This is the short text appearing on the steam page - shortText: shapez.io is a game about building factories to automate the creation and combination of increasingly complex shapes within an infinite map. + shortText: Στο shapez.io χτήζεις εργοστάσια για να αυτοματοποιήσεις την δημιουργία και τον συνδιασμό σχημάτων αυξανόμενης πολυπλοκότητας σε έναν ατέλειωτο χάρτη. # This is the long description for the steam page - It is contained here so you can help to translate it, and I will regulary update the store page. # NOTICE: @@ -31,63 +31,63 @@ steamPage: longText: >- [img]{STEAM_APP_IMAGE}/extras/store_page_gif.gif[/img] - shapez.io is a game about building factories to automate the creation and processing of increasingly complex shapes across an infinitely expanding map. - Upon delivering the requested shapes you will progress within the game and unlock upgrades to speed up your factory. + Στο shapez.io χτήζεις εργοστάσια για να αυτοματοποιήσεις την δημιουργία και τον συνδιασμό σχημάτων αυξανόμενης πολυπλοκότητας σε έναν ατέλειωτο χάρτη. + Όταν παραδώσεις τα απαιτούμενα σχήματα, θα προχωρήσεις στο παιχνίδι και θα ξεκλειδώσεις αναβαθμήσεις για να επιταχύνεις το εργοστάσιό σου. - As the demand for shapes increases, you will have to scale up your factory to meet the demand - Don't forget about resources though, you will have to expand across the [b]infinite map[/b]! + Επειδή η ζήτηση για σχήματα αυξάνεται συνεχώς, θα πρέπει να επεκτείνεις το εργοστάσιό σου για να την ικανοποιήσεις - Μήν ξεχάσεις, για να βρείς όλους τους πόρους που χρειάζεσαι θα πρέπει να εξαπλωθείς στον [b]άτελείωτο χάρτη[/b]! - Soon you will have to mix colors and paint your shapes with them - Combine red, green and blue color resources to produce different colors and paint shapes with it to satisfy the demand. + Σύντομα θα πρέπει να αναμείξεις χρώματα και να βάψεις σχήματα - Ανάμειξε κόκκινο, πράσινο και μπλέ για να φτιάξεις διαφορετικά χρώματα και να βάψεις σχήματα ώστε να ικανοποιήσεις την ζήτηση. - This game features 18 progressive levels (Which should keep you busy for hours already!) but I'm constantly adding new content - There is a lot planned! + Το παιχνίδι έχει 18 βαθμιαία επίπεδα (που ήδη θα σε απασχολήσουν για ώρες!) και συνεχίζω να προσθέτω περιεχόμενο - Έχω ακόμα πολλά σχέδια! - Purchasing the game gives you access to the standalone version which has additional features and you'll also receive access to newly developed features. + Αγόράζοντας το παιχνίδι λαμβάνεις την αυτόνομη έκδοση (standalone) με πολλές επιπλέον δυνατότητες, καθώς και πρόσβαση σε νέες δυνατότητες που ακόμα αναπτύσσονται. - [b]Standalone Advantages[/b] + [b]Πλεονεκτήματα Αυτόνομης Έκδοσης[/b] [list] - [*] Dark Mode - [*] Unlimited Waypoints - [*] Unlimited Savegames - [*] Additional settings - [*] Coming soon: Wires & Energy! Aiming for (roughly) end of July 2020. - [*] Coming soon: More Levels - [*] Allows me to further develop shapez.io ❤️ + [*] Σκοτεινή λειτουργία + [*] Απεριόριστα σημεία γρήγορης κίνησης της κάμερας στον χάρτη + [*] Απεριόριστα αποθηκευμένα παιχνίδια + [*] Πρόσθετες ρυθμίσεις + [*] Στο κοντινό μέλλον: Καλώδια και ενέργεια! Aναμένεται (περίπου) τέλος Ιουλίου 2020. + [*] Στο κοντινό μέλλον: Περισσότερα επίπεδα + [*] Μου επιτρέπει να αναπτήξω περεταίρω το shapez.io ❤️ [/list] - [b]Future Updates[/b] + [b]Μελλοντικές ενημερώσεις[/b] - I am updating the game very often and trying to push an update at least every week! + Ενημερώνω το παιχνίδι πολύ συχνά και προσπαθώ να προωγώ ενημερώσεις σχεδόν κάθε εβδομάδα! [list] - [*] Different maps and challenges (e.g. maps with obstacles) - [*] Puzzles (Deliver the requested shape with a restricted area / set of buildings) - [*] A story mode where buildings have a cost - [*] Configurable map generator (Configure resource/shape size/density, seed and more) - [*] Additional types of shapes - [*] Performance improvements (The game already runs pretty well!) - [*] And much more! + [*] Διαφορετικοί χάρτες και προκλήσεις (π.χ. χάρτες με εμπόδια) + [*] Πάζλ (Παράδωσε το αιτούμενο σχήμα με περιορισμένο χώρο / κτήρια) + [*] Ένα σενάριο όπου τα κτήρια έχουν κόστος + [*] Διαμορφώσιμη δημιουργία χάρτη (Με επιλογές για το μέγεθος και την πυκνότητα των πόρων και άλλα) + [*] Επιπλέον σχήματα! + [*] Βελτιώσεις απόδοσης (Το παιχνίδι λειτουργεί ήδη πολύ καλά!) + [*] Και πολλά άλλα! [/list] - [b]This game is open source![/b] + [b]Αυτό το παιχνίδι είναι ανοιχτού κώδικα![/b] - Anybody can contribute, I'm actively involved in the community and attempt to review all suggestions and take feedback into consideration where possible. - Be sure to check out my trello board for the full roadmap! + Όλοι μπορούν να συνεισφέρουν, συμμετέχω ενεργά στην κοινότητα και προσπαθώ να εξετάσω όλες τις προτάσεις και να λάβω υπόψη τα σχόλια όπου είναι δυνατόν. + Δείτε το trello board μου για τον πλήρη χάρτη πορείας! - [b]Links[/b] + [b]Σύνδεσμοι[/b] [list] - [*] [url=https://discord.com/invite/HN7EVzV]Official Discord[/url] - [*] [url=https://trello.com/b/ISQncpJP/shapezio]Roadmap[/url] + [*] [url=https://discord.com/invite/HN7EVzV]Επίσημο Discord[/url] + [*] [url=https://trello.com/b/ISQncpJP/shapezio]Χάρτης πορείας[/url] [*] [url=https://www.reddit.com/r/shapezio]Subreddit[/url] - [*] [url=https://github.com/tobspr/shapez.io]Source code (GitHub)[/url] - [*] [url=https://github.com/tobspr/shapez.io/blob/master/translations/README.md]Help translate[/url] + [*] [url=https://github.com/tobspr/shapez.io]Πηγαίος κώδικας (GitHub)[/url] + [*] [url=https://github.com/tobspr/shapez.io/blob/master/translations/README.md]Βοήθησε με μεταφράσεις[/url] [/list] - discordLink: Official Discord - Chat with me! + discordLink: Επίσημο Discord - Συνομίλησε μαζί μου! global: - loading: Loading - error: Error + loading: Φόρτωση + error: Σφάλμα # How big numbers are rendered, e.g. "10,000" thousandsDivider: "," @@ -97,31 +97,31 @@ global: # The suffix for large numbers, e.g. 1.3k, 400.2M, etc. suffix: - thousands: k - millions: M - billions: B - trillions: T + thousands: χλ. + millions: εκ. + billions: δισ. + trillions: τρισ. # Shown for infinitely big numbers - infinite: inf + infinite: άπειρο time: # Used for formatting past time dates - oneSecondAgo: one second ago - xSecondsAgo: seconds ago - oneMinuteAgo: one minute ago - xMinutesAgo: minutes ago - oneHourAgo: one hour ago - xHoursAgo: hours ago - oneDayAgo: one day ago - xDaysAgo: days ago + oneSecondAgo: πριν ένα δευτερόλεπτο + xSecondsAgo: πριν δευτερόλεπτα + oneMinuteAgo: πριν ένα λεπτό + xMinutesAgo: πριν λεπτά + oneHourAgo: πριν μία ώρα + xHoursAgo: πριν ώρες + oneDayAgo: πριν μία ημέρα + xDaysAgo: πριν ημέρες # Short formats for times, e.g. '5h 23m' - secondsShort: s - minutesAndSecondsShort: m s - hoursAndMinutesShort: h s + secondsShort: δ + minutesAndSecondsShort: λ δ + hoursAndMinutesShort: ω λ - xMinutes: minutes + xMinutes: λεπτά keys: tab: TAB @@ -138,506 +138,504 @@ demoBanners: Get the standalone to unlock all features! mainMenu: - play: Play + play: Παίξε changelog: Changelog - importSavegame: Import - openSourceHint: This game is open source! - discordLink: Official Discord Server - helpTranslate: Help translate! + importSavegame: Εισαγωγή αποθηκευμένου παιχνιδιού + openSourceHint: Αυτό το παιχνίδι είναι ανοιχτού κώδικα! + discordLink: Επίσημο Discord Server + helpTranslate: Βοήθησε με μεταφράσεις! # This is shown when using firefox and other browsers which are not supported. browserWarning: >- - Sorry, but the game is known to run slow on your browser! Get the standalone version or download chrome for the full experience. + Δυστυχώς, το παιχνίδι τρέχει αργά στο πρόγραμμα περιήγησής σας! Αποκτήστε την αυτόνομη έκδοση ή κατεβάστε το chrome για την πλήρη εμπειρία. - savegameLevel: Level - savegameLevelUnknown: Unknown Level + savegameLevel: Επίπεδο + savegameLevelUnknown: Άγνωστο Επίπεδο - continue: Continue - newGame: New Game + continue: Συνέχεια + newGame: Καινούριο παιχνίδι madeBy: Made by subreddit: Reddit dialogs: buttons: ok: OK - delete: Delete - cancel: Cancel - later: Later - restart: Restart - reset: Reset - getStandalone: Get Standalone - deleteGame: I know what I do - viewUpdate: View Update - showUpgrades: Show Upgrades - showKeybindings: Show Keybindings + delete: Διαγραφή + cancel: Άκυρο + later: Αργότερα + restart: Επανεκκίνηση + reset: Επαναφορά + getStandalone: Απόκτησε την Αυτόνομη έκδοση + deleteGame: Ξέρω τί κάνω + viewUpdate: Προβολή ενημέρωσης + showUpgrades: Εμφάνιση αναβαθμίσεων + showKeybindings: Συνδυασμοί πλήκτρων importSavegameError: - title: Import Error + title: Σφάλμα εισαγωγής text: >- - Failed to import your savegame: + Αποτυχία εισαγωγής του αποθηκευμένου παιχνιδιού: importSavegameSuccess: - title: Savegame Imported + title: Εισαγωγή αποθηκευμένου παιχνιδιού text: >- - Your savegame has been successfully imported. + Η Εισαγωγή του αποθηκευμένου παιχνιδιού ήταν επιτυχής. gameLoadFailure: - title: Game is broken + title: Το παιχνίδι είναι κατεστραμμένο text: >- - Failed to load your savegame: + Η φώρτοση του αποθηκευμένου παιχνιδιού ήταν αποτυχής: confirmSavegameDelete: - title: Confirm deletion + title: Επιβεβαίωση διαγραφής text: >- - Are you sure you want to delete the game? + Είσαι βέβαιος/η ότι θέλεις να διαγράψεις το παιχνίδι; savegameDeletionError: - title: Failed to delete + title: Αποτυχία διαγραφής text: >- - Failed to delete the savegame: + Η διαγραφή του αποθηκευμένου παιχνιδιού ήταν αποτυχής: restartRequired: - title: Restart required + title: Χρειάζεται επανεκκίνηση text: >- - You need to restart the game to apply the settings. + Πρέπει να επανεκκινήσεις το παιχνίδι για να εφαρμόσεις τις ρυθμίσεις. editKeybinding: - title: Change Keybinding - desc: Press the key or mouse button you want to assign, or escape to cancel. + title: Αλλαγή συνδιασμών πλήκτρων + desc: Πάτησε το πλήκτρο ή το κουμπί του ποντικιού που θέλεις να αντιστοιχίσεις, ή escape για ακύρωση. resetKeybindingsConfirmation: - title: Reset keybindings - desc: This will reset all keybindings to their default values. Please confirm. + title: Επαναφορά συνδιασμών πλήκτρων + desc: Όλες οι συνδιασμών πλήκτρων θα επαναφερθούν στις προεπιλεγμένες τιμές τους. Επιβεβαίωση; keybindingsResetOk: - title: Keybindings reset - desc: The keybindings have been reset to their respective defaults! + title: Συνδιασμοί πλήκτρων επαναφέρθηκαν + desc: Οι συνδιασμών πλήκτρων επαναφέρθηκαν στις προεπιλεγμένες τιμές τους! featureRestriction: - title: Demo Version - desc: You tried to access a feature () which is not available in the demo. Consider to get the standalone for the full experience! + title: Έκδοση Demo + desc: Προσπάθησες να χρησιμοποιήσεις μία δυνατότητα που δεν είναι διαθέσιμη στην έκδοση demo. Αποκτήστε την αυτόνομη έκδοση για την ολοκληρομένη εμπειρία! oneSavegameLimit: - title: Limited savegames - desc: You can only have one savegame at a time in the demo version. Please remove the existing one or get the standalone! + title: Περιορισμένα αποθηκευμένα παιχνίδια + desc: Στην demo έκδοση μπορείς να έχεις μόνο ένα αποθηκευμένο παιχνίδι. Παρακαλώ διάγραψε το υπάρχον αποθηκευμένο παιχνίδι ή απόκτησε την αθτόνομη έκδοση! updateSummary: - title: New update! + title: Νέα αναβάθμιση! desc: >- - Here are the changes since you last played: + Αυτές είναι οι αλλαγές από την τελευταία φορά που έπαιξες: upgradesIntroduction: - title: Unlock Upgrades + title: Ξεκλείδωμα αναβαθμίσεων desc: >- - All shapes you produce can be used to unlock upgrades - Don't destroy your old factories! - The upgrades tab can be found on the top right corner of the screen. + Όλα τα σχήματα που παράγεις μπορούν να χρησιμοποιηθούν για να ξεκλειδώσεις αναβαθμίσεις - Μην καταστρέψεις τα παλιά σου εργοστάσια! + Η καρτέλα αναβαθμίσεων βρίσκεται στην επάνω δεξιά γωνία της οθόνης. massDeleteConfirm: - title: Confirm delete + title: Επιβεβαίωση διαγραφής desc: >- - You are deleting a lot of buildings ( to be exact)! Are you sure you want to do this? + Ετοιμάζεσαι να διαγράψεις πολλά κτήρια ( για την ακρίβεια)! Είσαι βέβαιος/η ότι θέλεις να το κάνεις αυτό; blueprintsNotUnlocked: - title: Not unlocked yet + title: Αυτή η δυνατότητα δεν έχει ξεκλειδωθεί ακόμα desc: >- - Blueprints have not been unlocked yet! Complete more levels to unlock them. + Τα σχεδιαγράμματα δεν έχουν ξεκλειδωθεί ακόμα! Ολοκλήρωσε περισσότερα επίπεδα για να τα ξεκλειδώσεις. keybindingsIntroduction: - title: Useful keybindings + title: Χρήσιμοι Συνδιασμοί πλήκτρων desc: >- - This game has a lot of keybindings which make it easier to build big factories. - Here are a few, but be sure to check out the keybindings!

- CTRL + Drag: Select area to copy / delete.
- SHIFT: Hold to place multiple of one building.
- ALT: Invert orientation of placed belts.
+ Αυτό το παιχνίδι έχει πολλούς συνδιασμούς πλήκτρων που διευκολύνουν στο χτήσιμο μεγάλων εργοστασίων. + Εδώ είναι μερικοί από αυτούς. Για τους υπόλοιπους αναφέρσου στην ενότητα Συνδιασμοί πλήκτρων

+ CTRL + Σύρε: Επιλογή περιοχής για αντιγραφή / διαγραφή.
+ SHIFT: Κράτα πατημένο για να χτίσεις πάνω από ένα κτήριο.
+ ALT: Αντιστρέφει τον προσανατολισμό των τοποθετούμενων ιμάντων.
createMarker: - title: New Marker - desc: Give it a meaningful name, you can also include a short key of a shape (Which you can generate here) - titleEdit: Edit Marker + title: Νέο Σημάδι + desc: Δώσ' του ένα όνομα με νόημα. Μπορείς επίσης να χρησημοποιήσεις και τον σύντομο κώδικα ενός σχήματος (τον οποίο μπορείς να βρείς εδώ) + titleEdit: Επεξεργασία Σημαδιού markerDemoLimit: - desc: You can only create two custom markers in the demo. Get the standalone for unlimited markers! + desc: Στην έκδωση demo μπορείς να βάλεις μέχρι δύο σημάδια στον χάρτη. Αποκτήστε την αυτόνομη έκδοση για να μπορείς να βάλεις απεριόριστα σημάδια στον χάρτη! massCutConfirm: - title: Confirm cut + title: Επιβεβαίωση αποκοπής desc: >- - You are cutting a lot of buildings ( to be exact)! Are you sure you - want to do this? + Ετοιμάζεσαι να αποκόψεις πολλά κτήρια ( φια την ακρίβεια)! Είσαι βέβαιος/η ότι θέλεις να το κάνεις αυτό; exportScreenshotWarning: - title: Export screenshot + title: Εξαγωγή στιγμιότυπου οθόνης desc: >- - You requested to export your base as a screenshot. Please note that this can - be quite slow for a big base and even crash your game! + Ζήτησες να εξαγάγεις τη βάση σου ως στιγμιότυπο οθόνης. Λάβε υπόψη ότι αυτό μπορεί να είναι αρκετά αργό για μια μεγάλη βάση και μπορεί ακόμη και να διακόψει (crash) το παιχνίδι σου! massCutInsufficientConfirm: - title: Confirm cut - desc: You can not afford to paste this area! Are you sure you want to cut it? + title: Επιβεβαίωση αποκοπής + desc: Δεν έχεις τους πόρους να επικολλήσεις αυτήν την περιοχή! Είσαι βέβαιος/η ότι θέλεις να την αποκόψεις; ingame: # This is shown in the top left corner and displays useful keybindings in # every situation keybindingsOverlay: - moveMap: Move - selectBuildings: Select area - stopPlacement: Stop placement - rotateBuilding: Rotate building - placeMultiple: Place multiple - reverseOrientation: Reverse orientation - disableAutoOrientation: Disable auto orientation - toggleHud: Toggle HUD - placeBuilding: Place building - createMarker: Create Marker - delete: Destroy - pasteLastBlueprint: Paste last blueprint - lockBeltDirection: Enable belt planner - plannerSwitchSide: Flip planner side - cutSelection: Cut - copySelection: Copy - clearSelection: Clear Selection - pipette: Pipette - switchLayers: Switch layers + moveMap: Κίνηση + selectBuildings: Επιλογή περιοχής + stopPlacement: Διακοπή τοποθέτησης + rotateBuilding: Περιστροφή κτηρίου + placeMultiple: Τοποθέτηση πολλαπλών κτηρίων + reverseOrientation: Αντιστροφή προσανατολισμού ιμάντα + disableAutoOrientation: Απενεργοποίηση αυτόματου προσανατολισμού + toggleHud: Εναλλαγή HUD + placeBuilding: Τοποθέτηση κτηρίου + createMarker: Δημιουργία σημαδιού + delete: Διαγραφή + pasteLastBlueprint: Επικόλληση τελευταίου σχεδιαγράμματος + lockBeltDirection: Ενεργοποίηση σχεδιαστή ιμάντα + plannerSwitchSide: Αλλαγή πλευράς σχεδιαστή + cutSelection: Αποκοπή + copySelection: Αντιγραφή + clearSelection: Εκκαθαρισμός επιλογής + pipette: Σταγονόμετρο + switchLayers: Εναλλαγή στρώματος # Everything related to placing buildings (I.e. as soon as you selected a building # from the toolbar) buildingPlacement: # Buildings can have different variants which are unlocked at later levels, # and this is the hint shown when there are multiple variants available. - cycleBuildingVariants: Press to cycle variants. + cycleBuildingVariants: Πάτησε για εναλλαγή μεταξύ παραλλαγών. # Shows the hotkey in the ui, e.g. "Hotkey: Q" hotkeyLabel: >- Hotkey: infoTexts: - speed: Speed - range: Range - storage: Storage - oneItemPerSecond: 1 item / second - itemsPerSecond: items / s + speed: Ταχύτητα + range: Απόσταση + storage: Αποθηκευτικός χώρος + oneItemPerSecond: 1 είδος / δευτερόλεπτο + itemsPerSecond: είδη / δ itemsPerSecondDouble: (x2) - tiles: tiles + tiles: πλακάκια # The notification when completing a level levelCompleteNotification: # is replaced by the actual level, so this gets 'Level 03' for example. - levelTitle: Level - completed: Completed - unlockText: Unlocked ! - buttonNextLevel: Next Level + levelTitle: Επίπεδο + completed: Ολοκληρώθηκε + unlockText: Ξεκλειδώθηκε ! + buttonNextLevel: Επόμενο επίπεδο # Notifications on the lower right notifications: - newUpgrade: A new upgrade is available! - gameSaved: Your game has been saved. + newUpgrade: Μια νέα αναβάθμιση είναι διαθέσιμη! + gameSaved: Το παιχνίδι έχει αποθηκευτεί. # The "Upgrades" window shop: - title: Upgrades - buttonUnlock: Upgrade + title: Αναβαθμίσεις + buttonUnlock: Αναβάθμιση # Gets replaced to e.g. "Tier IX" - tier: Tier + tier: Βαθμίδα # The roman number for each tier tierLabels: [I, II, III, IV, V, VI, VII, VIII, IX, X] - maximumLevel: MAXIMUM LEVEL (Speed x) + maximumLevel: ΜΕΓΙΣΤΟ ΕΠΙΠΕΔΟ (Ταχύτητα x) # The "Statistics" window statistics: - title: Statistics + title: Στατιστικά dataSources: stored: - title: Stored - description: Displaying amount of stored shapes in your central building. + title: Αποθηκευμένα + description: Εμφάνιση ποσού αποθηκευμένων σχημάτων στο κεντρικό σου κτήριο. produced: - title: Produced - description: Displaying all shapes your whole factory produces, including intermediate products. + title: Παραγμένα + description: Εμφάνιση όλων των σχημάτων που παράγει ολόκληρο το εργοστάσιό σου, συμπεριλαμβανομένων των ενδιάμεσων προϊόντων. delivered: - title: Delivered - description: Displaying shapes which are delivered to your central building. - noShapesProduced: No shapes have been produced so far. + title: Παραδωμένα + description: Εμφάνιση σχημάτων που παραδίδονται στο κεντρικό σου κτήριο. + noShapesProduced: Δεν έχεις παράξει σχήματα ακόμα. # Displays the shapes per minute, e.g. '523 / m' - shapesPerMinute: / m + shapesPerMinute: / λ # Settings menu, when you press "ESC" settingsMenu: - playtime: Playtime + playtime: Χρόνος που έπαιξες - buildingsPlaced: Buildings - beltsPlaced: Belts + buildingsPlaced: Κτήρια + beltsPlaced: Ιμάντες buttons: - continue: Continue - settings: Settings - menu: Return to menu + continue: Συνέχεια + settings: Ρυθμίσεις + menu: Επιστροφή στο μενού # Bottom left tutorial hints tutorialHints: - title: Need help? - showHint: Show hint - hideHint: Close + title: Χρειάζεσε βοήθεια; + showHint: Εμφάνιση υπόδειξης + hideHint: Kλείσιμο # When placing a blueprint blueprintPlacer: - cost: Cost + cost: Κόστος # Map markers waypoints: - waypoints: Markers + waypoints: Σημάδι hub: HUB - description: Left-click a marker to jump to it, right-click to delete it.

Press to create a marker from the current view, or right-click to create a marker at the selected location. - creationSuccessNotification: Marker has been created. + description: Κάνε αριστερό κλικ σε ένα σημάδι για να μεταβείς σε αυτό, κάνε δεξί κλικ για να το διαγράψεις.

Πάτησε για να δημιουργήσεις ένα σημάδι από την τρέχουσα προβολή ή δεξί κλικ για να δημιουργήσεις ένα σημάδι στην επιλεγμένη τοποθεσία. + creationSuccessNotification: Το σημάδι δημιουργήθηκε. # Interactive tutorial interactiveTutorial: title: Tutorial hints: - 1_1_extractor: Place an extractor on top of a circle shape to extract it! + 1_1_extractor: Τοποθέτησε έναν αποσπαστή πάνω από ένα σχήμα κύκλου για να το αποσπάεις! 1_2_conveyor: >- - Connect the extractor with a conveyor belt to your hub!

Tip: Click and drag the belt with your mouse! + Σύνδεσε τον αποσπαστή με έναν μεταφορικό ιμάντα στο κεντρικό σου κτήριο!

Συμβουλή: Κάνε κλικ και σύρε τη ζώνη με το ποντίκι σου! 1_3_expand: >- - This is NOT an idle game! Build more extractors and belts to finish the goal quicker.

Tip: Hold SHIFT to place multiple extractors, and use R to rotate them. + Αυτό ΔΕΝ είναι ένα αδρανές παιχνίδι! Δημιούργησε περισσότερους αποσπαστές και ιμάντες για να ολοκληρώσεις τον στόχο σου πιο γρήγορα.

Συμβουλή: Κράτησε το πλήκτρο SHIFT για να τοποθετήσεις πολλούς αποσπαστές και χρησιμοποιήστε το R για να τους περιστρέψεις. colors: - red: Red - green: Green - blue: Blue - yellow: Yellow - purple: Purple - cyan: Cyan - white: White - uncolored: No color - black: Black + red: Κόκκινο + green: Πράσινο + blue: Μπλε + yellow: Κίτρινο + purple: Μωβ + cyan: Γαλάζιο + white: Λευκό + uncolored: Χωρίς χρώμα + black: Μαύρο shapeViewer: - title: Layers - empty: Empty - copyKey: Copy Key + title: Στρώματα + empty: Κενό + copyKey: Αντιγραφή κώδικα # All shop upgrades shopUpgrades: belt: - name: Belts, Distributor & Tunnels - description: Speed x → x + name: Ιμάντες, Διανομείς & Σήραγγες + description: Ταχύτητα x → x miner: - name: Extraction - description: Speed x → x + name: Απόσπαση + description: Ταχύτητα x → x processors: - name: Cutting, Rotating & Stacking - description: Speed x → x + name: Κοπή, Περιστροφή & Στοίβαξη + description: Ταχύτητα x → x painting: - name: Mixing & Painting - description: Speed x → x + name: Ανάμειξη & Βαφή + description: Ταχύτητα x → x # Buildings and their name / description buildings: belt: default: - name: &belt Conveyor Belt - description: Transports items, hold and drag to place multiple. + name: &belt Μεταφορικός Ιμάντας + description: Μεταφέρει είδη. Κράτα πατημένο και σύρε για να τοποθετήσεις πολλαπλούς ιμάντες. miner: # Internal name for the Extractor default: - name: &miner Extractor - description: Place over a shape or color to extract it. + name: &miner Αποσπαστής + description: Τοποθέτησε πάνω από ένα σχήμα ή χρώμα για να το αποσπάσεις. chainable: - name: Extractor (Chain) - description: Place over a shape or color to extract it. Can be chained. + name: Αποσπαστής (Αλυσιδωτός) + description: Τοποθέτησε πάνω από ένα σχήμα ή χρώμα για να το αποσπάσεις. Μπορούν να συνδεθούν σε σειρά. underground_belt: # Internal name for the Tunnel default: - name: &underground_belt Tunnel - description: Allows to tunnel resources under buildings and belts. + name: &underground_belt Σήραγγα + description: Μεταφέρει είδη κάτω από κτήρια και μεταφορικούς ιμάντες. tier2: - name: Tunnel Tier II - description: Allows to tunnel resources under buildings and belts. + name: Σήραγγα Βαθμίδα II + description: Μεταφέρει είδη κάτω από κτήρια και μεταφορικούς ιμάντες. splitter: # Internal name for the Balancer default: - name: &splitter Balancer - description: Multifunctional - Evenly distributes all inputs onto all outputs. + name: &splitter Ισορροπηστής + description: Πολυλειτουργικό - κατανέμει ομοιόμορφα είδη από όλες τις εισόδους σε όλες τις εξόδους. compact: - name: Merger (compact) - description: Merges two conveyor belts into one. + name: Συγχωνευτής (συμπαγής) + description: Συγχωνεύει δύο μεταφορικούς ιμάντες σε έναν. compact-inverse: - name: Merger (compact) - description: Merges two conveyor belts into one. + name: Συγχωνευτής (συμπαγής) + description: Συγχωνεύει δύο μεταφορικούς ιμάντες σε έναν. cutter: default: - name: &cutter Cutter - description: Cuts shapes from top to bottom and outputs both halfs. If you use only one part, be sure to destroy the other part or it will stall! + name: &cutter Κόπτης + description: Κόβει σχήματα από πάνω προς τα κάτω και παράγει και τα δύο μισά. Εάν χρησιμοποιείς μόνο το ένα κομμάτι, φρόντισε να καταστρέψεις το άλλο κομμάτι, διαφορετικά η λειτουργία θα σταματήσει! quad: - name: Cutter (Quad) - description: Cuts shapes into four parts. If you use only one part, be sure to destroy the other part or it will stall! + name: Κόπτης (Τετάρτων) + description: Κόβει σχήματα σε τέσσερα κομμάτια. Εάν χρησιμοποιείς μόνο το ένα κομμάτι, φρόντισε να καταστρέψεις τα άλλα κομμάτια, διαφορετικά η λειτουργία θα σταματήσει! rotater: default: - name: &rotater Rotate - description: Rotates shapes clockwise by 90 degrees. + name: &rotater Περιστροφέας + description: Περιστρέφει τα σχήματα δεξιόστροφα κατά 90 μοίρες. ccw: - name: Rotate (CCW) - description: Rotates shapes counter clockwise by 90 degrees. + name: Περιστροφέας (Αρστ.) + description: Περιστρέφει τα σχήματα αριστερόστροφα κατά 90 μοίρες. fl: - name: Rotate (180) - description: Rotates shapes by 180 degrees. + name: Περιστροφέας (180) + description: Περιστρέφει τα σχήματα κατά 180 μοίρες. stacker: default: - name: &stacker Stacker - description: Stacks both items. If they can not be merged, the right item is placed above the left item. + name: &stacker Στοίβαχτής + description: Στοιβάζει και τα δύο είδη. Εάν δεν μπορούν να συγχωνευτούν, το δεξί είδος τοποθετείται πάνω από το αριστερό είδος. mixer: default: - name: &mixer Color Mixer - description: Mixes two colors using additive blending. + name: &mixer Αναμείκτης Χρωμάτων + description: Αναμειγνύει δύο χρώματα χρησιμοποιώντας ανάμιξη πρόσθετων. painter: default: - name: &painter Painter - description: &painter_desc Colors the whole shape on the left input with the color from the right input. + name: &painter Βαφέας + description: &painter_desc Χρωματίζει ολόκληρο το σχήμα στην αριστερή είσοδο με το χρώμα από τη δεξιά είσοδο. double: - name: Painter (Double) - description: Colors the shapes on the left inputs with the color from the top input. + name: Βαφέας (Διπλός) + description: Χρωματίζει τα σχήματα από τις αριστερές εισόδους με το χρώμα από την επάνω είσοδο. quad: - name: Painter (Quad) - description: Allows to color each quadrant of the shape with a different color. + name: Βαφέας (Τετάρτων) + description: Επιτρέπει να χρωματίσει κάθε τεταρτημόριο του σχήματος με διαφορετικό χρώμα. mirrored: name: *painter description: *painter_desc trash: default: - name: &trash Trash - description: Accepts inputs from all sides and destroys them. Forever. + name: &trash Κάδος απορριμμάτων + description: Δέχεται είδη από όλες τις πλευρές και τα καταστρέφει. Για πάντα. storage: - name: Storage - description: Stores excess items, up to a given capacity. Can be used as an overflow gate. + name: Αποθήκη + description: Αποθηκεύει επιπλέον είδη, έως μια δεδομένη χωρητικότητα. Μπορεί να χρησιμοποιηθεί ως πύλη υπερχείλισης. hub: - deliver: Deliver - toUnlock: to unlock + deliver: Παράδωσε + toUnlock: για να ξεκλειδώσεις levelShortcut: LVL wire: default: - name: Energy Wire - description: Allows you to transport energy. + name: Καλώδιο ενέργειας + description: Σου επιτρέπει να μεταφέρεις ενέργεια. advanced_processor: default: - name: Color Inverter - description: Accepts a color or shape and inverts it. + name: Μετατροπέας χρώματος + description: Δέχεται ένα χρώμα ή σχήμα και το αντιστρέφει. energy_generator: - deliver: Deliver - toGenerateEnergy: For + deliver: Παράδωσε + toGenerateEnergy: Για default: - name: Energy Generator - description: Generates energy by consuming shapes. + name: Γεννήτρια ενέργειας + description: Παράγει ενέργεια καταναλώνοντας σχήματα. wire_crossings: default: - name: Wire Splitter - description: Splits a energy wire into two. + name: Διαχωριστής καλωδίων + description: Χωρίζει ένα καλώδιο ενέργειας σε δύο. merger: - name: Wire Merger - description: Merges two energy wires into one. + name: Συγχωνευτής καλωδίων + description: Συγχωνεύει δύο καλώδια ενέργειας σε ένα. storyRewards: # Those are the rewards gained from completing the store reward_cutter_and_trash: - title: Cutting Shapes - desc: You just unlocked the cutter - it cuts shapes half from top to bottom regardless of its orientation!

Be sure to get rid of the waste, or otherwise it will stall - For this purpose I gave you a trash, which destroys everything you put into it! + title: Κοπή σχημάτων + desc: Μόλις ξεκλείδωσες τον κόπτη - κόβει σχήματα κατά το ήμισυ από πάνω προς τα κάτω ανεξάρτητα από τον προσανατολισμό του!

Φρόντισε να καταστρέψεις κομμάτια που δεω χρησημοποιείς αλλιώς η λειτουργία θα σταματήσει - Για το σκοπό αυτό σου έδωσα τον κάδο απορριμμάτων, ο οποίος καταστρέφει ό,τι είδη μεταφερθούν εκεί! reward_rotater: - title: Rotating - desc: The rotater has been unlocked! It rotates shapes clockwise by 90 degrees. + title: Περιστροφή + desc: Ο Περιστροφέας ξεκλειδώθηκε! Το κτήριο αυτό περιστρέφει τα σχήματα δεξιόστροφα κατά 90 μοίρες. reward_painter: - title: Painting + title: Βαφή desc: >- - The painter has been unlocked - Extract some color veins (just as you do with shapes) and combine it with a shape in the painter to color them!

PS: If you are colorblind, there is a color blind mode in the settings! + Μόλις ξεκλειδώθηκε ο Βαφέας - Απόσπασε μερικά χρώματα (όπως κάνεις με τα σχήματα) και συνδύασέ τα με ένα σχήμα στον βαφέα για να τα χρωματίσεις!

PS: Εάν πάσχεις από αχρωματοψία, τυφλοί, υπάρχει η λειτουργία αχρωματοψίας στις ρυθμίσεις! reward_mixer: - title: Color Mixing - desc: The mixer has been unlocked - Combine two colors using additive blending with this building! + title: Ανάμιξη Χρωμάτων + desc: Ο Αναμείκτης χρωμάτων είναι διαθέσιμος - Συνδύαστε δύο χρώματα ακολουθόντας ανάμιξη πρόσθετων με αυτό το κτήριο! reward_stacker: - title: Combiner - desc: You can now combine shapes with the combiner! Both inputs are combined, and if they can be put next to each other, they will be fused. If not, the right input is stacked on top of the left input! + title: Στοίβαχτής + desc: Τώρα μπορείς να συνδυάσεις σχήματα με τον Στοίβαχτής! Εαν τα σχήματα από τις δύο εισόδους μπορούν να τοποθετηθούν το ένα δίπλα στο άλλο θα συμπτυχθούν. Εάν όχι, το σχήμα της δεηιάς εισώδου στοιβάζεται πάνω από το σχήμα της αριστερής! reward_splitter: - title: Splitter/Merger - desc: The multifunctional balancer has been unlocked - It can be used to build bigger factories by splitting and merging items onto multiple belts!

+ title: Διαχωρισμός/Συγχώνευση + desc: Ο πολυλειτουργικός Ισορροπηστής είναι πλέον διαθέσιμος - Μπορεί να χρησιμοποιηθεί για την κατασκευή μεγαλύτερων εργοστασίων με διαχωρισμό και συγχώνευση ειδών σε/από πολλούς μεταφορικούς ιμάντες!

reward_tunnel: - title: Tunnel - desc: The tunnel has been unlocked - You can now pipe items through belts and buildings with it! + title: Σήραγγα + desc: Το Σήραγγα είναι πλέον διαθέσιμο - Τώρα μπορείς να διοχετεύσεις είδη κάτω από ιμάντες και κτήρια! reward_rotater_ccw: - title: CCW Rotating - desc: You have unlocked a variant of the rotater - It allows to rotate counter clockwise! To build it, select the rotater and press 'T' to cycle its variants! + title: Περιστροφή (Αρστ.) + desc: Ξεκλείδωσες μια παραλλαγή του Περιστροφέα - Επιτρέπει αριστερόστροφη περιστροφή! Για να τον τοποθετήσεις, επίλεξε τον περιστροφέα και πάτησε 'T' για να κυλίσεις ανάμεσα στις παραλλαγές του! reward_miner_chainable: - title: Chaining Extractor - desc: You have unlocked the chaining extractor! It can forward its resources to other extractors so you can more efficiently extract resources! + title: Αλυσιδωτός Αποσπαστής + desc: Ξεκλείδωσες τον Αλυσιδωτός Αποσπαστής! Μπορεί να προωθήσει τους αποσπασμένους πόρους σε άλλους αποσπαστές, ώστε να μπορείτε να αποσπάσεις πιο αποτελεσματικά πόρους! reward_underground_belt_tier_2: - title: Tunnel Tier II - desc: You have unlocked a new variant of the tunnel - It has a bigger range, and you can also mix-n-match those tunnels now! + title: Σήραγγα Βαθμίδα II + desc: Ξεκλείδωσες μια νέα παραλλαγή της Σήραγγας - Καλύπτει μεγαλύτερη απόσταση και επιτρέπει το "πλέξιμο" διαφορετικών βαθμίδων σήραγγας! reward_splitter_compact: - title: Compact Balancer + title: Συμπαγής Ισορροπηστής desc: >- - You have unlocked a compact variant of the balancer - It accepts two inputs and merges them into one! + Ξεκλείδωσες μια συμπαγή παραλλαγή του Ισορροπηστή. Δέχεται είδη από δύο ιμάντες και τους συγχωνεύει σε έναν! reward_cutter_quad: - title: Quad Cutting - desc: You have unlocked a variant of the cutter - It allows you to cut shapes in four parts instead of just two! + title: Κόπτης Τετάρτων + desc: Ξεκλείδωσες μια παραλλαγή του Κόπτη - Σου επιτρέπει να κόψεις σχήματα σε τέσσερα κομμάτια αντί για δύο! reward_painter_double: - title: Double Painting - desc: You have unlocked a variant of the painter - It works as the regular painter but processes two shapes at once consuming just one color instead of two! + title: Διπλός Βαφέας + desc: Ξεκλείδωσες μια παραλλαγή του Βαφέα - Λειτουργεί όπως ο κανονικός βαφέας, αλλά επεξεργάζεται δύο σχήματα ταυτόχρονα, καταναλώνοντας μόνο ένα χρώμα αντί για δύο! reward_painter_quad: - title: Quad Painting - desc: You have unlocked a variant of the painter - It allows to paint each part of the shape individually! + title: Βαφέας Τετάρτων + desc: Ξεκλείδωσες μια παραλλαγή του Βαφέα - Σου επιτρέπει να βάψεις κάθε τεταρτημόριο του σχήματος ξεχωριστά! reward_storage: - title: Storage Buffer - desc: You have unlocked a variant of the trash - It allows to store items up to a given capacity! + title: Αποθηκευτικός χώρος + desc: Ξεκλείδωσες μια παραλλαγή του Κάδου Απορριμμάτων - Επιτρέπει την αποθήκευση ειδών έως μια δεδομένη χωρητικότητα! reward_freeplay: - title: Freeplay - desc: You did it! You unlocked the free-play mode! This means that shapes are now randomly generated! (No worries, more content is planned for the standalone!) + title: Ελεύθερο παιχνίδι + desc: Τα κατάφερες! Ξεκλείδωσες την λειτουργία ελεύθερου παιχνιδιού! Από εδώ και πέρα τα σχήματα δημιουργούνται τυχαία! (Μην ανυσηχείς, περισσότερο περιεχόμενο έρχεται σύντομα στην αυτόνομη έκδοση!) reward_blueprints: - title: Blueprints - desc: You can now copy and paste parts of your factory! Select an area (Hold CTRL, then drag with your mouse), and press 'C' to copy it.

Pasting it is not free, you need to produce blueprint shapes to afford it! (Those you just delivered). + title: Σχεδιαγράμματα + desc: Μπορείς πλέον να κάνεις αντιγραφή και επικόλληση στα μέρη του εργοστασίου σου! Επίλεξε μια περιοχή (Κράτα πατημένο το CTRL, και τράβα το ποντίκι σου), και πάτησε 'C' για να αντιγράψεις τα κτήρια στην επιλεγμένη περιοχή.

Η επικόλληση δεν είναι δωρεάν, θα πρέπει να παράγεις σχήματα σχεδιαγράμματος για να χρησημοποιήσεις αυτή την δυνατότητα! (Τα σχήματα που μόλις παρέδωσες). # Special reward, which is shown when there is no reward actually no_reward: - title: Next level + title: Επόμενο Επίπεδο desc: >- - This level gave you no reward, but the next one will!

PS: Better don't destroy your existing factory - You need all those shapes later again to unlock upgrades! + Αυτό το επίπεδο δεν σου παρήχε κάποια αμοιβή, άλλα το επόμενο επίπεδο θα σου δώσει!

Υ.Γ.: Καλύτερα μην καταστρέψεις το υπάρχον σου εργοστάσιο - θα χρειαστείς όλα αυτά τα σχήματα ξανά αργότερα για να ξεκλειδώσεις αναβαθμίσεις! no_reward_freeplay: - title: Next level + title: Επόμενο Επίπεδο desc: >- - Congratulations! By the way, more content is planned for the standalone! + Συγχαριτήρια! Παρεμπιπτόντως, περισσότερο περιεχόμενο θα έρθει σύντομα στην αυτόνομη έκδοση! settings: - title: Settings + title: Ρυθμίσεις categories: - general: General - userInterface: User Interface - advanced: Advanced + general: Γενικές + userInterface: Περιβάλλον χρήστη + advanced: Προχωρημένα versionBadges: dev: Development @@ -647,161 +645,162 @@ settings: labels: uiScale: - title: Interface scale + title: Κλίμακα περιβάλλοντος χρήστη description: >- - Changes the size of the user interface. The interface will still scale based on your device resolution, but this setting controls the amount of scale. + Αλλάζει το μέγεθος του περιβάλλοντος χρήστη. Το περιβάλλον χρήστη θα κλιμακωθεί με βάση την ανάλυση της συσκευής σας, αλλά αυτή η ρύθμιση ελέγχει το μέγεθος εντώς αυτής της κλίμακας. scales: - super_small: Super small - small: Small - regular: Regular - large: Large - huge: Huge + super_small: Πολύ μικρό + small: Μικρό + regular: Κανονικό + large: Μεγάλο + huge: Πολύ μεγάλο scrollWheelSensitivity: - title: Zoom sensitivity + title: Ευαισθησία ζουμ description: >- - Changes how sensitive the zoom is (Either mouse wheel or trackpad). + Αλλάζει πόσο ευαίσθητο είναι το ζουμ (Είτε με τον τροχό ποντικιού ή με trackpad). sensitivity: - super_slow: Super slow - slow: Slow - regular: Regular - fast: Fast - super_fast: Super fast + super_slow: Πολύ αργό + slow: Αργό + regular: Κανονικό + fast: Γρήγορο + super_fast: Πολύ γρήγορο language: - title: Language + title: Γλώσσα description: >- - Change the language. All translations are user contributed and might be incomplete! + Αλλάξτε τη γλώσσα. Γιά όλες τις μεταφράσεις έχουν συνεισφέρει χρήστες. Μερικές μεταφράσεις ενδέχεται να είναι ελλιπείς! fullscreen: - title: Fullscreen + title: Λειτουργία πλήρους οθόνης description: >- - It is recommended to play the game in fullscreen to get the best experience. Only available in the standalone. + Για την καλύτερη δυνατή εμπειρία συνιστάται η λειτουργία πλήρους οθόνης. Διατίθεται μόνο στην αυτόνομη έκδοση. soundsMuted: - title: Mute Sounds + title: Σίγαση ήχων description: >- - If enabled, mutes all sound effects. + Εάν είναι ενεργοποιημένο, σβήνει όλα τα ηχητικά εφέ. musicMuted: - title: Mute Music + title: Σίγαση μουσικής description: >- - If enabled, mutes all music. + Εάν είναι ενεργοποιημένο, σβήνει την μουσική. theme: - title: Game theme + title: Λειτουργία παιχνιδιού description: >- - Choose the game theme (light / dark). + Επίλεξε το την λειτουργία του παιχνιδιού (Φωτεινή / Σκοτεινή). themes: - dark: Dark - light: Light + dark: Σκοτεινή + light: Φωτεινή refreshRate: - title: Simulation Target + title: Στόχος Προσομοίωσης description: >- - If you have a 144hz monitor, change the refresh rate here so the game will properly simulate at higher refresh rates. This might actually decrease the FPS if your computer is too slow. + Εάν έχεις οθόνη 144hz, άλλαξε τον ρυθμό ανανέωσης εδώ, ώστε το παιχνίδι να προσομοιωθεί σωστά σε υψηλότερους ρυθμούς ανανέωσης. Μπορεί να μειώσει τα FPS εάν ο υπολογιστής σου είναι πολύ αργός. alwaysMultiplace: - title: Multiplace + title: Πολλαπλή τοποθέτηση description: >- - If enabled, all buildings will stay selected after placement until you cancel it. This is equivalent to holding SHIFT permanently. + Εάν είναι ενεργοποιημένο, όλα τα κτίρια θα παραμείνουν επιλεγμένα μετά την τοποθέτηση έως ότου ακυρώσεις την επιλογή. Αυτό ισοδυναμεί με μόνιμο κράτημα του SHIFT. offerHints: - title: Hints & Tutorials + title: Συμβουλές & Οδηγίες description: >- - Whether to offer hints and tutorials while playing. Also hides certain UI elements onto a given level to make it easier to get into the game. + Αν θέλεις να προσφέρονται συμβουλές και οδηγίες ενώ παίζεις. Επίσης κρύβει ορισμένα στοιχεία του περιβάλλοντος χρήστη έως ένα δεδομένο επίπεδο, για να διευκολύνει το παιχνίδι για νέους χρήστες. movementSpeed: - title: Movement speed - description: Changes how fast the view moves when using the keyboard. + title: Ταχύτητα κίνησης + description: Αλλάζει την ταχύτητα κίνησης όταν χρησιμοποιείς το πληκτρολόγιο. speeds: - super_slow: Super slow - slow: Slow - regular: Regular - fast: Fast - super_fast: Super Fast - extremely_fast: Extremely Fast + super_slow: Πολύ αργή + slow: Αργή + regular: Κανονική + fast: Γρήγορη + super_fast: Πολύ γρήγορη + extremely_fast: Πάρα πολύ γρήγορη enableTunnelSmartplace: - title: Smart Tunnels + title: Έξυπνες σήραγγες description: >- - When enabled, placing tunnels will automatically remove unnecessary belts. - This also enables to drag tunnels and excess tunnels will get removed. + Όταν ενεργοποιηθεί, η τοποθέτηση σηράγγων θα αφαιρέσει αυτόματα τις περιττές ζώνες. + Αυτό σου επιτρέπει επίσης να σύρεις το ποντίκι με την σήραγγα επιλεγμένη, και οι περιττές σήραγγες θα αφαιρεθούν. + vignette: - title: Vignette + title: Βινιέτα description: >- - Enables the vignette which darkens the screen corners and makes text easier - to read. + Ενεργοποιεί την Βιωιέτα, που σκουραίνει τις γωνίες της οθόνης και διευκολύνει την ανάγνωση του κειμένου. autosaveInterval: - title: Autosave Interval + title: Διάστημα μεταξύ αυτόματων αποθηκεύσεων description: >- - Controls how often the game saves automatically. You can also disable it - entirely here. + Ελέγχει πόσο συχνά το παιχνίδι αποθηκεύεται αυτόματα. + Εδώ μπορείς επίσης να απενεργοποιήσεις την λειτουργία εντελώς. + intervals: - one_minute: 1 Minute - two_minutes: 2 Minutes - five_minutes: 5 Minutes - ten_minutes: 10 Minutes - twenty_minutes: 20 Minutes - disabled: Disabled + one_minute: 1 Λεπτό + two_minutes: 2 Λεπτά + five_minutes: 5 Λεπτά + ten_minutes: 10 Λεπτά + twenty_minutes: 20 Λεπτά + disabled: Απενεργοποιημένο compactBuildingInfo: - title: Compact Building Infos + title: Σύντομες πληροφορίες κτηρίων description: >- - Shortens info boxes for buildings by only showing their ratios. Otherwise a - description and image is shown. + Συντομεύει τα πλαίσια πληροφοριών για κτίρια δείχνοντας μόνο την ταχύτητα λειτουργίας τους. + Διαφορετικά εμφανίζεται μια περιγραφή και εικόνα. disableCutDeleteWarnings: - title: Disable Cut/Delete Warnings + title: Απενεργοποίηση προειδοποιήσεων αποκοπής / διαγραφής description: >- - Disable the warning dialogs brought up when cutting/deleting more than 100 - entities. + Απενεργοποιεί τους διαλόγους προειδοποίησης που εμφανίζονται όταν κόβεις / διαγράφεις περισσότερα από 100 κτήρια. enableColorBlindHelper: - title: Color Blind Mode - description: Enables various tools which allow to play the game if you are color blind. + title: Λειτουργία αχρωματοψίας + description: Ενεργοποιεί διάφορα εργαλεία που επιτρέπουν να παίξεiw το παιχνίδι αν πάσχεις από αχρωματοψία. rotationByBuilding: - title: Rotation by building type + title: Περιστροφή ανά τύπο κτιρίου description: >- - Each building type remembers the rotation you last set it to individually. - This may be more comfortable if you frequently switch between placing - different building types. + Κάθε τύπος κτιρίου θυμάται την περιστροφή που όρισες στην τελευταία χρήση. + Μπορεί να είναι πιο άνετο εάν κάνεις εναλλαγή κτηρίων μεταξύ τοποθέτησης διαφορετικών τύπων κτηρίων. keybindings: - title: Keybindings + title: Συνδιασμοί πλήκτρων hint: >- - Tip: Be sure to make use of CTRL, SHIFT and ALT! They enable different placement options. + Συμβουλή: Φρόντισε να χρησιμοποιήσεις τα πλήκτρα CTRL, SHIFT και ALT! Ενεργοποιούν διαφορετικές επιλογές τοποθέτησης. - resetKeybindings: Reset Keyinbindings + resetKeybindings: Επαναφορά συνδιασμών πλήκτρων categoryLabels: - general: Application - ingame: Game - navigation: Navigating - placement: Placement - massSelect: Mass Select - buildings: Building Shortcuts - placementModifiers: Placement Modifiers + general: Εφαρμογή + ingame: Παιχνίδι + navigation: Πλοήγηση + placement: Τοποθέτηση + massSelect: Μαζική Επιλογή + buildings: Συντομεύσεις Κτηρίων + placementModifiers: Τροποποιητές τοποθέτησης mappings: - confirm: Confirm - back: Back - mapMoveUp: Move Up - mapMoveRight: Move Right - mapMoveDown: Move Down - mapMoveLeft: Move Left - centerMap: Center Map + confirm: Επιβεβαίωση + back: Πίσω + mapMoveUp: Κίνηση προς τα Πάνω + mapMoveRight: Κίνηση προς τα Δεξιά + mapMoveDown: Κίνηση προς τα Κάτω + mapMoveLeft: Κίνηση προς τα Αριστερά + centerMap: Kεντράρισμα του χάρτη - mapZoomIn: Zoom in - mapZoomOut: Zoom out - createMarker: Create Marker + mapZoomIn: Μεγέθυνση + mapZoomOut: Σμίκρυνση + createMarker: Δημηουργία Σημαδιού - menuOpenShop: Upgrades - menuOpenStats: Statistics + menuOpenShop: Αναβαθμίσεις + menuOpenStats: Στατιστικές - toggleHud: Toggle HUD - toggleFPSInfo: Toggle FPS and Debug Info + toggleHud: Εναλλαγή HUD + toggleFPSInfo: Εναλλαγή FPS και Πληροφοριών εντοπισμού σφαλμάτων + + # --- Do not translate the values in this section belt: *belt splitter: *splitter underground_belt: *underground_belt @@ -812,63 +811,67 @@ keybindings: mixer: *mixer painter: *painter trash: *trash + # --- - rotateWhilePlacing: Rotate + rotateWhilePlacing: Περιστροφή rotateInverseModifier: >- - Modifier: Rotate CCW instead - cycleBuildingVariants: Cycle Variants - confirmMassDelete: Confirm Mass Delete - cycleBuildings: Cycle Buildings + Modifier: Αριστερόστροφη περιστροφή + cycleBuildingVariants: Επιλογή Παραλλαγής + confirmMassDelete: Επιβεβαίωση μαζικής διαγραφής + cycleBuildings: Επιλογή Κτηρίου - massSelectStart: Hold and drag to start - massSelectSelectMultiple: Select multiple areas - massSelectCopy: Copy area + massSelectStart: Κράτησε πατημένο και σείρε για να ξεκινήσεις + massSelectSelectMultiple: Επίλεξε πολλές περιοχές + massSelectCopy: Αντιγραφή περιοχής - placementDisableAutoOrientation: Disable automatic orientation - placeMultiple: Stay in placement mode - placeInverse: Invert automatic belt orientation - pasteLastBlueprint: Paste last blueprint - massSelectCut: Cut area - exportScreenshot: Export whole Base as Image - mapMoveFaster: Move Faster - lockBeltDirection: Enable belt planner - switchDirectionLockSide: "Planner: Switch side" - pipette: Pipette - menuClose: Close Menu - switchLayers: Switch layers - advanced_processor: Color Inverter - energy_generator: Energy Generator - wire: Energy Wire + placementDisableAutoOrientation: Απενεργοποίηση αυτόματου προσανατολισμού + placeMultiple: Παραμονή σε λειτουργία τοποθέτησης + placeInverse: Αντιστροφή αυτόματου προσανατολισμό του ιμάντα + pasteLastBlueprint: Επικόλληση τελευταίου σχεδιαγράμματος + massSelectCut: Αποκοπή περιοχής + exportScreenshot: Εξαγωγή ολόκληρης της βάσης ως εικόνα + mapMoveFaster: Ταχλυτερη κίνηση + lockBeltDirection: Ενεργοποίηση σχεδιαστή ιμάντα + switchDirectionLockSide: >- + Σχεδιαστής: Αλλαγή πλευράς + + pipette: Σταγονόμετρο + menuClose: Κλείσιμο μενού + switchLayers: Εναλλαγή επιπέδων + advanced_processor: Μετατροπέας χρώματος + energy_generator: Γεννήτρια ενέργειας + wire: Καλώδιο ενέργειας about: - title: About this Game + title: Σχετικά με αυτό το παιχνίδι body: >- - This game is open source and developed by Tobias Springer (this is me).

+ Αυτό το παιχνίδι είναι ανοιχτού κώδικα και αναπτύχθηκε από τους Tobias Springer (αυτός είμαι εγώ).

- If you want to contribute, check out shapez.io on github.

+ Αν θέλεις να συνεισφέρεις, δες το shapez.io στο github.

- This game wouldn't have been possible without the great Discord community - around my games - You should really join the discord server!

+ Αυτό το παιχνίδι δεν θα ήταν δυνατό χωρίς τη μεγάλη κοινότητα Discord + γύρω από τα παιχνίδια μου - Σας συνιστώ πραγματικά να εγγραφείτε στον + discord server!

- The soundtrack was made by Peppsen - He's awesome.

+ Το soundtrack δημιουργήθηκε από τους Peppsen - Είναι φοβεροί.

- Finally, huge thanks to my best friend Niklas - Without our - factorio sessions this game would never have existed. + Τέλος, ευχαριστώ πολύ τον καλύτερο μου φίλο Niklas - Χωρίς + τις συνεδρίες μασ στο Factorio αυτό το παιχνίδι δεν θα υπήρχε. changelog: title: Changelog demo: features: - restoringGames: Restoring savegames - importingGames: Importing savegames - oneGameLimit: Limited to one savegame - customizeKeybindings: Customizing Keybindings - exportingBase: Exporting whole Base as Image + restoringGames: Επαναφορά αποθηκευμένων παιχνιδιών + importingGames: Εισαγωγή αποθηκευμένων παιχνιδιών + oneGameLimit: Περιορίζεται σε ένα αποθηκευμένο παιχνίδι + customizeKeybindings: Προσαρμογή συνδιασμών πλήκτρων + exportingBase: Εξαγωγή ολόκληρης της βάσης ως εικόνα - settingNotAvailable: Not available in the demo. + settingNotAvailable: Δεν είναι διαθέσιμο στο demo. diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 28fef7e2..193a2d04 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -191,7 +191,9 @@ dialogs: confirmSavegameDelete: title: Confirm deletion text: >- - Are you sure you want to delete the game? + Are you sure you want to delete the following game?

+ '' at level

+ This can not be undone! savegameDeletionError: title: Failed to delete @@ -378,7 +380,10 @@ ingame: noShapesProduced: No shapes have been produced so far. # Displays the shapes per second, e.g. '523 / s' - shapesPerSecond: / s + shapesDisplayUnits: + second: / s + minute: / m + hour: / h # Settings menu, when you press "ESC" settingsMenu: @@ -390,7 +395,7 @@ ingame: buttons: continue: Continue settings: Settings - menu: Return to menu + menu: Menu # Bottom left tutorial hints tutorialHints: @@ -479,25 +484,25 @@ buildings: name: Tunnel Tier II description: Allows you to tunnel resources under buildings and belts. - # Internal name for the Balancer - splitter: + # Balancer + balancer: default: - name: &splitter Balancer + name: &balancer Balancer description: Multifunctional - Evenly distributes all inputs onto all outputs. - compact: + merger: name: Merger (compact) description: Merges two conveyor belts into one. - compact-inverse: + merger-inverse: name: Merger (compact) description: Merges two conveyor belts into one. - compact-merge: + splitter: name: Splitter (compact) description: Splits one conveyor belt into two. - compact-merge-inverse: + splitter-inverse: name: Splitter (compact) description: Splits one conveyor belt into two. @@ -516,7 +521,7 @@ buildings: ccw: name: Rotate (CCW) description: Rotates shapes counter-clockwise by 90 degrees. - fl: + rotate180: name: Rotate (180) description: Rotates shapes by 180 degrees. @@ -637,6 +642,14 @@ buildings: name: Compare description: Returns true if both items are exactly equal. Can compare shapes, items and booleans. + stacker: + name: Virtual Stacker + description: Virtually stacks the right shape onto the left. + + painter: + name: Virtual Painter + description: Virtually paints the shape from the bottom input with the shape on the right input. + storyRewards: # Those are the rewards gained from completing the store reward_cutter_and_trash: @@ -660,8 +673,8 @@ storyRewards: title: Combiner desc: You can now combine shapes with the combiner! Both inputs are combined, and if they can be put next to each other, they will be fused. If not, the right input is stacked on top of the left input! - reward_splitter: - title: Splitter/Merger + reward_balancer: + title: Balancer desc: The multifunctional balancer has been unlocked - It can be used to build bigger factories by splitting and merging items onto multiple belts!

reward_tunnel: @@ -680,10 +693,15 @@ storyRewards: title: Tunnel Tier II desc: You have unlocked a new variant of the tunnel - It has a bigger range, and you can also mix-n-match those tunnels now! - reward_splitter_compact: - title: Compact Balancer + reward_merger: + title: Compact Merger desc: >- - You have unlocked a compact variant of the balancer - It accepts two inputs and merges them into one belt! + You have unlocked a merger variant of the balancer - It accepts two inputs and merges them into one belt! + + reward_splitter: + title: Compact Splitter + desc: >- + You have unlocked a merger variant of the balancer - It accepts one input and splits them into two! reward_cutter_quad: title: Quad Cutting @@ -709,6 +727,10 @@ storyRewards: title: Blueprints desc: You can now copy and paste parts of your factory! Select an area (Hold CTRL, then drag with your mouse), and press 'C' to copy it.

Pasting it is not free, you need to produce blueprint shapes to afford it! (Those you just delivered). + reward_rotater_180: + title: Rotater (180 degrees) + desc: You just unlocked the 180 degress rotater! - It allows you to rotate a shape by 180 degress (Surprise! :D) + # Special reward, which is shown when there is no reward actually no_reward: title: Next level @@ -775,7 +797,7 @@ settings: movementSpeed: title: Movement speed description: >- - Changes how fast the view moves when using the keyboard. + Changes how fast the view moves when using the keyboard or moving the mouse to the screen borders. speeds: super_slow: Super slow slow: Slow @@ -830,7 +852,7 @@ settings: refreshRate: title: Tick Rate description: >- - The game will automatically adjust the tickrate to be between this target tickrate and half of it. For example, with a tickrate of 60hz, the game will try to stay at 60hz, and if your computer can't handle it it will go down until it eventually reaches 30hz. + This determines how many game ticks happen per second. In general, a higher tick rate means better precision but also worse performance. On lower tickrates, the throughput may not be exact. alwaysMultiplace: title: Multiplace @@ -898,12 +920,22 @@ settings: description: >- Enabled by default, selects the miner if you use the pipette when hovering a resource patch. + simplifiedBelts: + title: Simplified Belts (Ugly) + description: >- + Does not render belt items except when hovering the belt to save performance. I do not recommend to play with this setting if you do not absolutely need the performance. + + enableMousePan: + title: Enable Mouse Pan + description: >- + Allows to move the map by moving the cursor to the edges of the screen. The speed depends on the Movement Speed setting. + keybindings: title: Keybindings hint: >- Tip: Be sure to make use of CTRL, SHIFT and ALT! They enable different placement options. - resetKeybindings: Reset Keybindings + resetKeybindings: Reset categoryLabels: general: Application @@ -939,7 +971,7 @@ keybindings: # --- Do not translate the values in this section belt: *belt - splitter: *splitter + balancer: *balancer underground_belt: *underground_belt miner: *miner cutter: *cutter @@ -1004,3 +1036,56 @@ demo: exportingBase: Exporting whole Base as Image settingNotAvailable: Not available in the demo. + +tips: + - The hub accepts input of any kind, not just the current shape! + - Make sure your factories are modular - it will pay out! + - Don't build too close to the hub, or it will be a huge chaos! + - If stacking does not work, try switching the inputs. + - You can toggle the belt planner direction by pressing R. + - Holding CTRL allows dragging of belts without auto-orientation. + - Ratios stay the same, as long as all upgrades are on the same Tier. + - Serial execution is more efficient than parallel. + - You will unlock more variants of buildings later in the game! + - You can use T to switch between different variants. + - Symmetry is key! + - You can weave different tiers of tunnels. + - Try to build compact factories - it will pay out! + - The painter has a mirrored variant which you can select with T + - Having the right building ratios will maximize efficiency. + - At maximum level, 5 extractors will fill a single belt. + - Don't forget about tunnels! + - You don't need to divide up items evenly for full efficiency. + - Holding SHIFT will activate the belt planner, letting you place long lines of belts easily. + - Cutters always cut vertically, regardless of their orientation. + - To get white mix all three colors. + - The storage buffer priorities the first output. + - Invest time to build repeatable designs - it's worth it! + - Holding CTRL allows to place multiple buildings. + - You can hold ALT to invert the direction of placed belts. + - Efficiency is key! + - Shape patches that are further away from the hub are more complex. + - Machines have a limited speed, divide them up for maximum efficiency. + - Use balancers to maximize your efficiency. + - Organization is important. Try not to cross conveyors too much. + - Plan in advance, or it will be a huge chaos! + - Don't remove your old factories! You'll need them to unlock upgrades. + - Try beating level 18 on your own before seeking for help! + - Don't complicate things, try to stay simple and you'll go far. + - You may need to re-use factories later in the game. Plan your factories to be re-usable. + - Sometimes, you can find a needed shape in the map without creating it with stackers. + - Full windmills / pinwheels can never spawn naturally. + - Color your shapes before cutting for maximum efficiency. + - With modules, space is merely a perception; a concern for mortal men. + - Make a separate blueprint factory. They're important for modules. + - Have a closer look on the color mixer, and your questions will be answered. + - Use CTRL + Click to select an area. + - Building too close to the hub can get in the way of later projects. + - The pin icon next to each shape in the upgrade list pins it to the screen. + - Mix all primary colours together to make white! + - You have an infinite map, don't cramp your factory, expand! + - Also try Factorio! It's my favourite game. + - The quad cutter cuts clockwise starting from the top right! + - You can download your savegames in the main menu! + - This game has a lot of useful keybindings! Be sure to check out the settings page. + - This game has a lot of settings, be sure to check them out! diff --git a/translations/base-fi.yaml b/translations/base-fi.yaml index 5acc58cf..17ad35d6 100644 --- a/translations/base-fi.yaml +++ b/translations/base-fi.yaml @@ -168,7 +168,7 @@ dialogs: deleteGame: Tiedän mitä olen tekemässä viewUpdate: Näytä päivitys showUpgrades: Näytä Päivitykset - showKeybindings: Show Keybindings + showKeybindings: Näytä pikanäppäimet importSavegameError: title: Tuonti Virhe @@ -258,7 +258,7 @@ dialogs: createMarker: title: Uusi Merkki desc: Anna merkille kuvaava nimi, voit myös sisällyttää muodon lyhyen avaimen siihen. (Lyhyen avaimen voit luoda täällä) - titleEdit: Edit Marker + titleEdit: Muokkaa merkkiä markerDemoLimit: desc: Voit tehdä vain kaksi mukautettua merkkiä demoversiossa. Hanki itsenäinen versio saadaksesi loputtoman määrän merkkejä! @@ -267,8 +267,8 @@ dialogs: title: Vie kuvakaappaus desc: Pyysit tukikohtasi viemistä kuvakaappauksena. Huomaa, että tämä voi olla melko hidasta isolla tukikohdalla ja voi jopa kaataa pelisi! massCutInsufficientConfirm: - title: Confirm cut - desc: You can not afford to paste this area! Are you sure you want to cut it? + title: Vahvista leikkaus + desc: Sinulla ei ole varaa leikata tätä aluetta! Oletko varma että haluat leikata sen? ingame: # This is shown in the top left corner and displays useful keybindings in @@ -303,8 +303,8 @@ ingame: purple: Violetti cyan: Syaani white: Valkoinen - uncolored: Ei Väriä - black: Black + uncolored: Väritön + black: Musta # Everything related to placing buildings (I.e. as soon as you selected a building # from the toolbar) @@ -465,7 +465,7 @@ buildings: tier2: name: Tunneli Taso II - description: Sallii resurssien kuljetuksen rakennuksien ja hihnojen alta. + description: Sallii resurssien kuljetuksen rakennuksien ja hihnojen alta pidemmältä kantamalta. splitter: # Internal name for the Balancer default: @@ -498,11 +498,11 @@ buildings: name: &rotater Kääntäjä description: Kääntää muotoja 90 astetta myötäpäivään. ccw: - name: Rotate (Vastapäivään) + name: Kääntäjä (Vastapäivään) description: Kääntää muotoja 90 astetta vastapäivään. fl: - name: Rotate (180) - description: Rotates shapes by 180 degrees. + name: Kääntäkä (180) + description: Kääntää muotoja 180 astetta. stacker: default: @@ -550,11 +550,11 @@ buildings: description: Tuottaa sähköä kuluttamalla muotoja. Jokainen sähkögeneraattori vaatii eri muotoja. wire_crossings: default: - name: Wire Splitter - description: Splits a energy wire into two. + name: Johdon jakaja + description: Jakaa energiajohdon kahteen. merger: - name: Wire Merger - description: Merges two energy wires into one. + name: Johtojen yhdistäjä + description: Yhdistää kaksi energiajohtoa yhteen. storyRewards: # Those are the rewards gained from completing the store @@ -642,9 +642,9 @@ storyRewards: settings: title: Asetukset categories: - general: General - userInterface: User Interface - advanced: Advanced + general: Yleinen + userInterface: Käyttöliittyma + advanced: Kehittynyt versionBadges: dev: Kehitys @@ -736,17 +736,17 @@ settings: refreshRate: title: Simulaatiotavoite description: >- - Jos sinulla on 144hz näyttö, muuta virkistystaajuus täällä jotta pelin simulaatio toimii oikein isommilla virkistystaajuuksilla. Tämä voi laskea FPS nopeutta jos tietokoneesi on liian hidas. + Jos sinulla on 144hz näyttö, muuta virkistystaajuus täällä jotta pelin simulaatio toimii oikein isommilla virkistystaajuuksilla. Tämä voi laskea FPS nopeutta, jos tietokoneesi on liian hidas. alwaysMultiplace: title: Monisijoitus description: >- - Jos käytössä, kaikki rakennukset pysyvät valittuina sijoittamisen jälkeen kunnes peruutat sen. Tämä vastaa SHIFT:in pitämistö pohjassa ikuisesti. + Jos käytössä, kaikki rakennukset pysyvät valittuina sijoittamisen jälkeen kunnes peruutat sen. Tämä vastaa SHIFT:in pitämistä pohjassa ikuisesti. offerHints: title: Vihjeet & Oppaat description: >- - Tarjotaanko pelaamisen aikana vihjeitä ja oppaita. Myös piilottaa tietyt käyttöliittymäelementit tietyn tason mukaan, jotta alkuunpääseminen olisi helpompaa. + Tarjoaa pelaamisen aikana vihjeitä ja oppaita. Myös piilottaa tietyt käyttöliittymäelementit tietyn tason mukaan, jotta alkuunpääseminen olisi helpompaa. enableTunnelSmartplace: title: Älykkäät Tunnelit @@ -771,12 +771,12 @@ settings: disableCutDeleteWarnings: title: Poista Leikkaus/Poisto Varoitukset description: >- - Poista varoitusikkunat dialogs brought up when cutting/deleting more than 100 entities. + Poista varoitusikkunat jotka ilmestyy kun leikkaat/poistat enemmän kuin 100 entiteettiä keybindings: title: Pikanäppäimet hint: >- - Tip: Muista käyttää CTRL, VAIHTO and ALT! Ne ottavat käyttöön erilaisia sijoitteluvaihtoehtoja. + Tip: Muista käyttää CTRL, VAIHTO ja ALT! Ne ottavat käyttöön erilaisia sijoitteluvaihtoehtoja. resetKeybindings: Nollaa Pikanäppäimet @@ -843,13 +843,13 @@ keybindings: placementDisableAutoOrientation: Poista automaattinen suunta käytöstä placeMultiple: Pysy sijoittamistilassa - placeInverse: Käännä automaattinen hihnan suunta - menuClose: Close Menu + placeInverse: Käännä automaattinen hihnan suunta päinvastoin + menuClose: Sulje valikko about: title: Tietoja tästä pelistä body: >- - Tämä peli on avoimen lähdekoodin ja kehitettä on Tobias Springer (tämä on minä).

+ Tämä peli on avointa lähdekoodia ja kehittäjä on Tobias Springer (tämä on minä).

Jos haluat osallistua, tarkista shapez.io githubissa.

diff --git a/translations/base-fr.yaml b/translations/base-fr.yaml index 0504956a..704f8896 100644 --- a/translations/base-fr.yaml +++ b/translations/base-fr.yaml @@ -32,15 +32,15 @@ steamPage: [img]{STEAM_APP_IMAGE}/extras/store_page_gif.gif[/img] shapez.io est un jeu dans lequel vous devrez construire des usines pour automatiser la création et la combinaison de formes de plus en plus complexes sur une carte infinie. - Lors de la livraison des formes requises vous progresserez et débloquerez des améliorations pour accélerer votre usine. + En livrant les formes requises, vous progresserez et débloquerez des améliorations pour accélérer votre usine. - Au vu de l'augmentation des demandes de formes, vous devrez agrandir votre usine pour répondre à la forte demande - Mais n'oubliez pas les ressources, vous drevrez vous étendre au milieu de cette [b]carte infinie[/b] ! + Vous devrez agrandir votre usine pour répondre à l’augmentation de la demande en formes — Mais n’oubliez pas les ressources, vous devrez vous étendre au milieu de cette [b]carte infinie[/b] ! - Bientôt vous devrez mixer les couleurs et peindre vos formes avec - Combinez les ressources de couleurs rouge, verte et bleue pour produire différentes couleurs et peindre les formes avec pour satisfaire la demande. + Bientôt, vous devrez mélanger les couleurs et peindre vos formes avec — Combinez les ressources de couleurs rouge, verte et bleue pour produire différentes couleurs et peindre les formes avec pour satisfaire la demande. - Ce jeu propose 18 niveaux progressifs (qui devraient déjà vous occuper quelques heures !) mais j'ajoute constamment de nouveau contenus - Il y en a beaucoup de prévus ! + Ce jeu propose 18 niveaux progressifs (qui devraient déjà vous occuper quelques heures !) mais je développe constamment plus de contenu — Il y a beaucoup de choses prévues ! - Acheter le jeu vous donne accès à la version complète qui a des fonctionnalités additionnelles et vous recevrez aussi un accès à des fonctionnalités fraîchement développées. + Acheter le jeu vous donne accès à la version complète qui a des fonctionnalités supplémentaires, et vous pourrez aussi accéder aux fonctionnalités fraîchement développées. [b]Avantages de la version complète (standalone)[/b] @@ -48,30 +48,30 @@ steamPage: [*] Mode sombre [*] Balises infinies [*] Parties infinies - [*] Plus d'options - [*] Prochainement: Câbles et énergie ! Prévu pour (environ) fin Juillet 2020. - [*] Prochainement: Plus de niveaux - [*] Aidez moi à continuer de développer shapez.io ❤️ + [*] Plus d’options + [*] Prochainement : Câbles et énergie ! Prévu pour (environ) fin juillet 2020. + [*] Prochainement : Plus de niveaux + [*] Aidez-moi à continuer de développer shapez.io ❤️ [/list] - [b]Mises à jour futures[/b] + [b]Mises à jour à venir[/b] - Je fais souvent des mises à jour et essaye d'en sortir une par semaine! + Je fais souvent des mises à jour et j’essaye d’en sortir une par semaine ! [list] [*] Différentes cartes et challenges (e.g. carte avec obstacles) - [*] Puzzles (Délivrer la forme requise avec une zone limitée/jeu de bâtiments) + [*] Casse-tête (Livrer la forme requise avec une zone limitée / jeu de bâtiments) [*] Un mode histoire où les bâtiments ont un coût - [*] Générateur de carte configurable (configuration des ressources/formes/taille/densitée, seed et plus) - [*] Plus de formes - [*] Amélioration des performances (Le jeu tourne déjà plutot bien !) - [*] Et bien plus ! + [*] Générateur de carte configurable (configuration des ressources / formes / taille / densité, graine aléatoire et plus) + [*] Plus de niveaux + [*] Amélioration des performances (Le jeu tourne déjà plutôt bien !) + [*] Et bien plus ! [/list] - [b]Ce jeu est open source ![/b] + [b]Ce jeu est open source ![/b] - Tout le monde peut contribuer, je suis très impliqué dans la communauté et j'essaye de répondre à toutes les suggestions et prendre en compte vos retours si possible. - Jetez un coup d'œil à mon Trello pour le suivi du projet et la planification du développement ! + Tout le monde peut contribuer, je suis très impliqué dans la communauté et j’essaye de répondre à toutes les suggestions et prendre en compte vos retours si possible. + Jetez un coup d’œil à mon Trello pour le suivi du projet et les plans de développement ! [b]Liens[/b] [list] @@ -82,19 +82,20 @@ steamPage: [*] [url=https://github.com/tobspr/shapez.io/blob/master/translations/README.md]Aidez à traduire[/url] [/list] - discordLink: Discord officiel - Parlez avec moi! + discordLink: Discord officiel — Parlez avec moi ! global: loading: Chargement error: Erreur # How big numbers are rendered, e.g. "10,000" - thousandsDivider: " " + # En français, le séparateur des milliers est l’espace (fine) insécable + thousandsDivider: " " # What symbol to use to seperate the integer part from the fractional part of a number, e.g. "0.4" decimalSeparator: "," - # The suffix for large numbers, e.g. 1.3k, 400.2M, etc. cf wikipedia système international d'unité + # The suffix for large numbers, e.g. 1.3k, 400.2M, etc. cf wikipedia système international d’unité # For french: https://fr.wikipedia.org/wiki/Pr%C3%A9fixes_du_Syst%C3%A8me_international_d%27unit%C3%A9s suffix: thousands: k @@ -108,20 +109,20 @@ global: time: # Used for formatting past time dates oneSecondAgo: il y a une seconde - xSecondsAgo: il y a secondes + xSecondsAgo: il y a  secondes oneMinuteAgo: il y a une minute - xMinutesAgo: il y a minutes + xMinutesAgo: il y a  minutes oneHourAgo: il y a une heure - xHoursAgo: il y a heures + xHoursAgo: il y a  heures oneDayAgo: il y a un jour - xDaysAgo: il y a jours + xDaysAgo: il y a  jours # Short formats for times, e.g. '5h 23m' - secondsShort: s - minutesAndSecondsShort: m s - hoursAndMinutesShort: h m + secondsShort:  s + minutesAndSecondsShort:  m  s + hoursAndMinutesShort:  h  m - xMinutes: minutes + xMinutes:  minutes keys: tab: TAB @@ -135,21 +136,21 @@ demoBanners: # This is the "advertisement" shown in the main menu and other various places title: Version démo intro: >- - Achetez la version complète pour débloquer toutes les fonctionnalités ! + Achetez la version complète pour débloquer toutes les fonctionnalités ! mainMenu: play: Jouer changelog: Historique importSavegame: Importer - openSourceHint: Ce jeu est open source ! + openSourceHint: Ce jeu est open source ! discordLink: Serveur Discord officiel - helpTranslate: Contribuez à la traduction ! + helpTranslate: Contribuez à la traduction ! # This is shown when using firefox and other browsers which are not supported. browserWarning: >- - Désolé, ce jeu est connu pour tourner lentement sur votre navigateur web ! Procurez-vous la version complète ou téléchargez Chrome pour une meilleure expérience. + Désolé, ce jeu sera lent sur votre navigateur web ! Procurez-vous la version complète ou téléchargez Chrome pour une meilleure expérience. - savegameLevel: Niveau + savegameLevel: Niveau  savegameLevelUnknown: Niveau inconnu continue: Continuer @@ -165,16 +166,16 @@ dialogs: later: Plus tard restart: Relancer reset: Réinitialiser - getStandalone: Se procurer la version complète + getStandalone: Obtenir la version complète deleteGame: Je sais ce que je fais viewUpdate: Voir les mises à jour showUpgrades: Montrer les améliorations showKeybindings: Montrer les raccourcis importSavegameError: - title: Erreur d'importation + title: Erreur d’importation text: >- - Impossible d'importer votre sauvegarde: + Impossible d’importer votre sauvegarde : importSavegameSuccess: title: Sauvegarde importée @@ -184,17 +185,17 @@ dialogs: gameLoadFailure: title: La sauvegarde est corrompue text: >- - Impossible de charger votre sauvegarde: + Impossible de charger votre sauvegarde : confirmSavegameDelete: title: Confirmez la suppression text: >- - Êtes-vous certains de vouloir supprimer votre partie ? + Êtes-vous sûr de vouloir supprimer votre partie ? savegameDeletionError: title: Impossible de supprimer text: >- - Impossible de supprimer votre sauvegarde: + Impossible de supprimer votre sauvegarde : restartRequired: title: Redémarrage requis @@ -211,91 +212,91 @@ dialogs: keybindingsResetOk: title: Réinitialisation des contrôles - desc: Les contrôles ont été réinitialisés dans leur état par défaut respectifs ! + desc: Les contrôles ont été remis à défaut ! featureRestriction: title: Version démo - desc: Vous avez essayé d'accéder à la fonction () qui n'est pas disponible dans la démo. Considérez l'achat de la version complète pour une expérience optimale ! + desc: Vous avez essayé d’accéder à la fonction () qui n’est pas disponible dans la démo. Pensez à acheter la version complète pour une expérience optimale ! oneSavegameLimit: title: Sauvegardes limitées - desc: Vous ne pouvez avoir qu'une seule sauvegarde en même temps dans la version démo. Merci d'effacer celle en cours ou alternativement de vous procurer la version complète ! + desc: Vous ne pouvez avoir qu’une seule sauvegarde en même temps dans la version démo. Merci d’effacer celle en cours ou bien de vous procurer la version complète ! updateSummary: - title: Nouvelle mise à jour ! + title: Nouvelle mise à jour ! desc: >- - Voici les modifications depuis votre dernière session: + Voici les changements depuis votre dernière session : upgradesIntroduction: title: Débloquer les améliorations desc: >- - Toutes les formes que vous produisez peuvent être utilisées pour débloquer des améliorations - Ne détruisez pas vos anciennes usines ! - L'onglet des améliorations se trouve dans le coin supérieur droit de l'écran. + Toutes les formes que vous produisez peuvent être utilisées pour débloquer des améliorations — Ne détruisez pas vos anciennes usines ! + L’onglet des améliorations se trouve dans le coin supérieur droit de l’écran. massDeleteConfirm: title: Confirmation de suppression desc: >- - Vous allez supprimer pas mal de bâtiments ( pour être exact) ! Êtes vous certains de vouloir faire ça ? + Vous allez supprimer beaucoup de bâtiments ( pour être précis) ! Êtes-vous sûr de vouloir faire ça ? massCutConfirm: title: Confirmer la coupure desc: >- - Vous vous apprêtez à couper beaucoup de bâtiments ( pour être précis) ! Êtes-vous certains de vouloir faire ça ? + Vous allez couper beaucoup de bâtiments ( pour être précis) ! Êtes-vous sûr de vouloir faire ça ? blueprintsNotUnlocked: title: Pas encore débloqué desc: >- - Les patrons n'ont pas encore étés débloqués ! Terminez encore quelques niveaux pour y avoir accès. + Les patrons n’ont pas encore été débloqués ! Terminez le niveau 12 pour y avoir accès. keybindingsIntroduction: title: Raccourcis utiles desc: >- Le jeu a de nombreux raccourcis facilitant la construction de grandes usines. - En voici quelques uns, n'hésitez pas à aller découvrir les raccourcis !

- CTRL + Glisser: Sélectionne une zone à copier / effacer.
- SHIFT: Laissez appuyé pour placer plusieurs fois le même bâtiment.
- ALT: Inverse l'orientation des convoyeurs placés.
+ En voici quelques-uns, n’hésitez pas à aller découvrir les raccourcis !

+ CTRL + glisser : Sélectionne une zone à copier / effacer.
+ MAJ : Laissez appuyé pour placer plusieurs fois le même bâtiment.
+ ALT : Inverse l’orientation des convoyeurs placés.
createMarker: title: Nouvelle balise - desc: Donnez-lui un nom, vous pouvez aussi inclure le raccourci d'une forme (Que vous pouvez générer ici) - titleEdit: Éditer cette balise + desc: Donnez-lui un nom, vous pouvez aussi inclure le raccourci d’une forme (que vous pouvez générer ici). + titleEdit: Modifier cette balise markerDemoLimit: - desc: Vous ne pouvez créer que deux balises dans la démo. Achetez la version complète pour en faire autant que vous voulez ! + desc: Vous ne pouvez créer que deux balises dans la démo. Achetez la version complète pour en placer autant que vous voulez ! exportScreenshotWarning: - title: Exporter une capture d'écran + title: Exporter une capture d’écran desc: >- - Vous avez demandé à exporter votre base sous la forme d'une capture d'écran. Soyez conscient que cela peut s'avérer passablement lent pour une grande base, voire même faire planter votre jeu ! + Vous avez demandé à exporter une capture d’écran de votre base. Soyez conscient que cela peut s’avérer passablement lent pour une grande base, voire faire planter votre jeu ! massCutInsufficientConfirm: title: Confirmer la coupe - desc: Vous n'avez pas les moyens de copier cette zone ! Etes vous certain de vouloir la couper ? + desc: Vous n’avez pas les moyens de copier cette zone ! Êtes-vous sûr de vouloir la couper ? ingame: # This is shown in the top left corner and displays useful keybindings in # every situation keybindingsOverlay: moveMap: Déplacer - selectBuildings: Sélection d'une zone - stopPlacement: Arrêter le placement + selectBuildings: Sélection d’une zone + stopPlacement: Arrêter de placer rotateBuilding: Tourner le bâtiment placeMultiple: Placement multiple - reverseOrientation: Changer l'orientation - disableAutoOrientation: Désactiver l'orientation automatique - toggleHud: Basculer l'affichage tête haute (ATH) + reverseOrientation: Changer l’orientation + disableAutoOrientation: Désactiver l’orientation automatique + toggleHud: Basculer l’affichage tête haute (ATH) placeBuilding: Placer un bâtiment createMarker: Créer une balise delete: Supprimer pasteLastBlueprint: Copier le dernier patron - lockBeltDirection: Utiliser le plannificateur de convoyeurs - plannerSwitchSide: Échanger la direction du plannificateur + lockBeltDirection: Utiliser le planificateur de convoyeurs + plannerSwitchSide: Inverser la direction du planificateur cutSelection: Couper copySelection: Copier clearSelection: Effacer la sélection pipette: Pipette - switchLayers: Échanger les calques + switchLayers: Changer de calque # Everything related to placing buildings (I.e. as soon as you selected a building # from the toolbar) @@ -306,29 +307,29 @@ ingame: # Shows the hotkey in the ui, e.g. "Hotkey: Q" hotkeyLabel: >- - Raccourci: + Raccourci : infoTexts: speed: Vitesse range: Portée storage: Espace de stockage - oneItemPerSecond: 1 forme / s - itemsPerSecond: formes / s - itemsPerSecondDouble: (x2) + oneItemPerSecond: 1 forme ⁄ s + itemsPerSecond:  formes ⁄ s + itemsPerSecondDouble: (×2) - tiles: cases + tiles:  cases # The notification when completing a level levelCompleteNotification: # is replaced by the actual level, so this gets 'Level 03' for example. - levelTitle: Niveau + levelTitle: Niveau  completed: Terminé - unlockText: débloqué ! + unlockText: débloqué ! buttonNextLevel: Niveau suivant # Notifications on the lower right notifications: - newUpgrade: Une nouvelle amélioration est disponible ! + newUpgrade: Une nouvelle amélioration est disponible ! gameSaved: Votre partie a été sauvegardée. # The "Upgrades" window @@ -337,11 +338,11 @@ ingame: buttonUnlock: Améliorer # Gets replaced to e.g. "Tier IX" - tier: Niveau + tier: Niveau  # The roman number for each tier tierLabels: [I, II, III, IV, V, VI, VII, VIII, IX, X] - maximumLevel: NIVEAU MAXIMAL (Vitesse x) + maximumLevel: NIVEAU MAXIMAL (Vitesse ×) # The "Statistics" window statistics: @@ -352,14 +353,14 @@ ingame: description: Affiche le nombre de formes stockées dans votre bâtiment central. produced: title: Produit - description: Affiche tous les formes que votre usine produit, en incluant les formes intermédiaires. + description: Affiche toutes les formes que votre usine produit, y compris les formes intermédiaires. delivered: - title: Délivré + title: Livré description: Affiche les formes qui ont été livrées dans votre bâtiment central. - noShapesProduced: Aucune forme n'a été produite jusqu'à présent. + noShapesProduced: Aucune forme produite pour le moment. # Displays the shapes per minute, e.g. '523 / m' - shapesPerMinute: / m + shapesPerMinute:  ⁄ m # Settings menu, when you press "ESC" settingsMenu: @@ -375,7 +376,7 @@ ingame: # Bottom left tutorial hints tutorialHints: - title: Besoin d'aide ? + title: Besoin d’aide ? showHint: Indice hideHint: Fermer @@ -387,19 +388,19 @@ ingame: waypoints: waypoints: Balise hub: Centre - description: Cliquez une balise pour vous y rendre, clic-droit pour l'effacer.

Appuyez sur pour créer une balise sur la vue actuelle, ou clic-droit pour en créer une sur l'endroit pointé. + description: Cliquez sur une balise pour vous y rendre, clic-droit pour l’effacer.

Appuyez sur pour créer une balise sur la vue actuelle, ou clic-droit pour en créer une sur l’endroit pointé. creationSuccessNotification: La balise a été créée. # Interactive tutorial interactiveTutorial: title: Tutoriel hints: - 1_1_extractor: Placez un extracteur sur une forme en cercle pour l'extraire ! + 1_1_extractor: Placez un extracteur sur une forme en cercle pour l’extraire ! 1_2_conveyor: >- - Connectez l'extracteur avec un convoyeur vers votre centre !

Astuce: Cliquez et faites glisser le convoyeur avec votre souris ! + Connectez l’extracteur avec un convoyeur vers votre centre !

Astuce : Cliquez et faites glisser le convoyeur avec votre souris ! 1_3_expand: >- - Ceci n'est PAS un jeu incrémental et inactif ! Construisez plus d'extracteurs et de convoyeurs pour atteindre plus vite votre votre but.

Astuce: Gardez MAJ enfoncé pour placer plusieurs extracteurs, et utilisez R pour les faire pivoter. + Ceci n’est PAS un jeu incrémental et inactif ! Construisez plus d’extracteurs et de convoyeurs pour atteindre plus vite votre but.

Astuce : Gardez MAJ enfoncé pour placer plusieurs extracteurs, et utilisez R pour les faire pivoter. colors: red: Rouge @@ -409,92 +410,92 @@ ingame: purple: Violet cyan: Cyan white: Blanc - uncolored: Non coloré + uncolored: Sans couleur black: Noir shapeViewer: title: Calques empty: Vide - copyKey: Copier la clé de forme + copyKey: Copier le raccourci de la forme # All shop upgrades shopUpgrades: belt: - name: Convoyeurs, Distributeurs et Tunnels - description: Vitesse x → x + name: Convoyeurs, distributeurs et tunnels + description: Vitesse × → × miner: name: Extraction - description: Vitesse x → x + description: Vitesse × → × processors: - name: Découpage, Rotation et Empilage - description: Vitesse x → x + name: Découpage, rotation et empilage + description: Vitesse × → × painting: - name: Mélange et Peinture - description: Vitesse x → x + name: Mélange et peinture + description: Vitesse × → × # Buildings and their name / description buildings: belt: default: name: &belt Convoyeur - description: Transporte les objects, maintenez et faites glisser pour en placer plusieurs. + description: Transporte les objets, maintenez et faites glisser pour en placer plusieurs. miner: # Internal name for the Extractor default: name: &miner Extracteur - description: Placez-le au dessus d'une forme ou couleur pour l'extraire. + description: Placez-le au-dessus d’une forme ou couleur pour l’extraire. chainable: name: Extracteur en série - description: Placez-le au dessus d'une forme ou couleur pour l'extraire. Peut être mis en série. + description: Placez-le au-dessus d’une forme ou couleur pour l’extraire. Peut être mis en série. underground_belt: # Internal name for the Tunnel default: name: &underground_belt Tunnel - description: Permet de faire passer des ressources en dessous de bâtiment et de convoyeurs. + description: Permet de faire passer des ressources sous les bâtiments et les convoyeurs. tier2: - name: Tunnel Niveau II - description: Permet de faire passer des ressources en dessous de bâtiment et de convoyeurs. + name: Tunnel niveau II + description: Permet de faire passer des ressources sous les bâtiments et les convoyeurs. splitter: # Internal name for the Balancer default: name: &splitter Répartiteur - description: Multifonctionnel - Distribue de manière équitable toutes les entrées vers toutes les sorties. + description: Multifonctions — Distribue équitablement toutes les entrées vers toutes les sorties. compact: name: Fusionneur (compact) - description: Fusionne deux convoyeurs en un. + description: Fusionne deux convoyeurs en un seul. compact-inverse: name: Fusionneur (compact) - description: Fusionne deux convoyeurs en un. + description: Fusionne deux convoyeurs en un seul. cutter: default: name: &cutter Découpeur - description: Coupe une forme de haut en bas et sort les deux parties. Si vous n'utilisez qu'une seule partie, assurez-vous de détruite l'autre ou sinon, gare au blocage ! + description: Coupe une forme de haut en bas et sort les deux parties. Si vous n’utilisez qu’une seule partie, assurez-vous de détruire l’autre ou sinon, gare au blocage ! quad: - name: Découpeur (Quatre) - description: Coupe une forme en quatre parties. Si vous n'utilisez pas toutes les parties, assurez-vous de détruite les autres ou sinon, gare au blocage ! + name: Découpeur (quadruple) + description: Coupe une forme en quatre parties. Si vous n’utilisez pas toutes les parties, assurez-vous de détruire les autres ou sinon, gare au blocage ! rotater: default: name: &rotater Pivoteur - description: Fait pivoter une forme de 90 degrés vers la droite. + description: Fait pivoter une forme de 90 degrés vers la droite. ccw: name: Pivoteur inversé - description: Fait pivoter une forme de 90 degrés vers la gauche. + description: Fait pivoter une forme de 90 degrés vers la gauche. fl: name: Retourneur - description: Tourne la forme de 180 degrés. + description: Tourne une forme de 180 degrés. stacker: default: name: &stacker Combineur - description: Combine deux formes. Si elles ne peuvent pas êtres combinées, la forme de droite est placée sur la forme de gauche. + description: Combine deux formes. Si elles ne peuvent pas être combinées, la forme de droite est placée sur la forme de gauche. mixer: default: @@ -506,11 +507,11 @@ buildings: name: &painter Peintre description: &painter_desc Colorie entièrement la forme de gauche avec la couleur de droite. double: - name: Peintre (Double) + name: Peintre (double) description: Colorie les deux formes de gauche avec la couleur de droite. quad: - name: Peintre (Quadruple) - description: Permet de colorier chaque quadrant d'une forme avec une couleur différente. + name: Peintre (quadruple) + description: Colorie chaque quadrant d’une forme avec une couleur différente. mirrored: name: *painter description: *painter_desc @@ -518,125 +519,125 @@ buildings: trash: default: name: &trash Poubelle - description: Accepte des formes de n'importe quel côté et les détruit... pour toujours. + description: Accepte des formes de n’importe quel côté et les détruit… pour toujours. storage: name: Stockage - description: Stocke les formes en trop jusqu'à une certaine capacité. Peut être utilisé comme tampon. + description: Stocke les formes en trop jusqu’à une certaine capacité. Peut être utilisé pour absorber un surplus. hub: - deliver: Délivrez + deliver: Livrez toUnlock: pour débloquer levelShortcut: NV wire: default: - name: Ligne énergétique - description: Permet de transporter de l'énergie. + name: Câble + description: Permet de transporter de l’énergie. advanced_processor: default: name: Inverseur de couleur - description: Accepte une couleur ou une forme et l'inverse. + description: Accepte une couleur ou une forme, et l’inverse. energy_generator: - deliver: Délivrer + deliver: Livrer toGenerateEnergy: Pour default: - name: Générateur d'énergie - description: Genère de l'énergie en consommant des formes. + name: Générateur d’énergie + description: Génère de l’énergie en consommant des formes. wire_crossings: default: - name: Duplicateur de ligne - description: Sépare une ligne énergétique en deux. + name: Duplicateur de câble + description: Sépare un câble en deux. merger: - name: Fusionneur de ligne - description: Fusionne deux lignes énergétiques en une seule. + name: Fusionneur de câble + description: Fusionne deux câbles en un seul. storyRewards: # Those are the rewards gained from completing the store reward_cutter_and_trash: - title: Découper des formes - desc: Vous venez de débloquer le découpeur - il coupe des formes en deux de haut en bas quel que soit son orientation !

Assurez-vous de vous débarasser des déchets, sinon gare au blocage - À cet effet, je mets à votre disposition la poubelle, qui détruit tout ce que vous y mettez ! + title: Découpage de formes + desc: Vous venez de débloquer le découpeur — il coupe des formes en deux de haut en bas quelle que soit son orientation !

Assurez-vous de vous débarrasser des déchets, sinon gare au blocage — À cet effet, je mets à votre disposition la poubelle, qui détruit tout ce que vous y mettez ! reward_rotater: title: Rotation - desc: Le pivoteur a été débloqué ! Il pivote les formes de 90 degrés vers la droite. + desc: Le pivoteur a été débloqué ! Il pivote les formes de 90 degrés vers la droite. reward_painter: title: Peintre desc: >- - Le peintre a été débloqué - Extrayez des pigments de couleur (comme vous le faites avec les formes) et combinez les avec une forme dans un peintre pour les colorier !

PS: Si vous êtes daltonien, il y a un mode daltonien paramétrable dans les préférences ! + Le peintre a été débloqué — Extrayez des pigments de couleur (comme vous le faites avec les formes) et combinez-les avec une forme dans un peintre pour les colorier !

PS : Si vous êtes daltonien, il y a un mode daltonien paramétrable dans les préférences ! reward_mixer: title: Mélangeur de couleurs - desc: Le mélangeur a été débloqué - Combinez deux couleurs en utilisant la synthèse additive des couleurs avec ce bâtiment ! + desc: Le mélangeur a été débloqué — Combinez deux couleurs en utilisant la synthèse additive des couleurs avec ce bâtiment ! reward_stacker: title: Combineur - desc: Vous pouvez maintenant combiner deux formes avec le combineur ! Les deux entrées sont combinées et si elles ne peuvent êtres mises l'une à côté de l'autre, elles sont fusionnées. Sinon, la forme de droite est placée au dessus de la forme de gauche après avoir été légèrement réduite. + desc: Vous pouvez maintenant combiner deux formes avec le combineur ! Les deux entrées sont combinées et si elles peuvent être mises l’une à côté de l’autre, elles sont fusionnées. Sinon, la forme de droite est placée au-dessus de la forme de gauche. reward_splitter: - title: Distributeur/Rassembleur - desc: Le répartiteur multifonctionnel a été débloqué - Il peut être utilisé pour construire de plus grandes usines en distribuant équitablement et rassemblant les formes entre plusieurs convoyeurs !

+ title: Distributeur / rassembleur + desc: Le répartiteur multifonctionnel a été débloqué — Il peut être utilisé pour construire de plus grandes usines en distribuant équitablement et rassemblant les formes entre plusieurs convoyeurs !

reward_tunnel: title: Tunnel - desc: Le tunnel a été débloqué - À présent il devient possible de faire passer des formes sous les convoyeurs et les bâtiments ! + desc: Le tunnel a été débloqué — Vous pouvez maintenant faire passer des formes sous les convoyeurs et les bâtiments ! reward_rotater_ccw: title: Pivoteur inversé - desc: Vous avez débloqué une variante du pivoteur - Elle permet de faire pivoter vers la gauche ! Pour le construire, sélectionnez le pivoteur et appuyez sur 'T' pour alterner entre les variantes ! + desc: Vous avez débloqué une variante du pivoteur — Elle permet de faire pivoter vers la gauche ! Pour le construire, sélectionnez le pivoteur et appuyez sur 'T' pour alterner entre les variantes ! reward_miner_chainable: title: Extracteur en série - desc: Vous avez débloqué l'extracteur en série ! Il permet de transférer ses resources à d'autres extracteurs pour augmenter le débit sortant ! + desc: Vous avez débloqué l’extracteur en série ! Il permet de transférer ses ressources à d’autres extracteurs pour augmenter le débit sortant ! reward_underground_belt_tier_2: title: Tunnel niveau II - desc: Vous avez débloqué une nouvelle variante du tunnel - Elle a une portée plus grande, et vous pouvez à présent superposer les deux variantes de tunnels ! + desc: Vous avez débloqué une nouvelle variante du tunnel — Elle a une portée plus grande, et vous pouvez superposer les deux variantes de tunnels ! reward_splitter_compact: title: Répartiteur compact desc: >- - Vous avez débloqué une variante compacte du répartiteur - Elle accepte deux entrées et les rassemble en une sortie ! + Vous avez débloqué une variante compacte du répartiteur — Elle accepte deux entrées et les rassemble en une sortie ! reward_cutter_quad: title: Quadruple découpeur - desc: Vous avez débloqué une variante du découpeur - Elle permet de découper les formes en quatre parties à la place de simplement deux ! + desc: Vous avez débloqué une variante du découpeur — Elle permet de découper les formes en quatre parties à la place de simplement deux ! reward_painter_double: title: Double peintre - desc: Vous avez débloqué une variante du peintre - Elle fonctionne comme le peintre de base, mais elle permet de traiter deux formes à la fois en ne consommant qu'une couleur au lieu de deux ! + desc: Vous avez débloqué une variante du peintre — Elle fonctionne comme le peintre de base, mais elle permet de traiter deux formes à la fois en ne consommant qu’une couleur au lieu de deux ! reward_painter_quad: title: Quadruple peintre - desc: Vous avez débloqué une variante du peintre - Elle permet de colorier chaque partie d'une forme individuellement ! + desc: Vous avez débloqué une variante du peintre — Elle permet de colorier chaque partie d’une forme individuellement ! reward_storage: title: Tampon de stockage - desc: Vous avez débloqué une variante de la poubelle - Elle permet de stocker des formes jusqu'à une certaine limite ! + desc: Vous avez débloqué une variante de la poubelle — Elle permet de stocker des formes jusqu’à une certaine limite ! reward_freeplay: title: Mode libre - desc: Vous y êtes arrivé ! Vous avez débloqué le mode libre ! Cela veut dire que dorénavant, les formes sont générées aléatoirement ! (Ne vous en faites pas, plus de contenu est prévu pour la version complète !) + desc: Vous y êtes arrivé ! Vous avez débloqué le mode libre ! Cela veut dire que dorénavant, les formes sont générées aléatoirement ! (Ne vous en faites pas, encore plus de contenu est prévu pour la version complète !) reward_blueprints: title: Patrons - desc: Vous pouvez maintenant copier et coller des parties de votre usines ! Sélectionnez une zone (Appuyez sur CTRL, et sélectionnez avec votre souris), et appuyez sur 'C' pour la copier.

Coller n'est pas gratuit, vous devez produire des formes de patrons pour vous le payer (les mêmes que celles que vous venez de livrer). + desc: Vous pouvez maintenant copier et coller des parties de votre usine ! Sélectionnez une zone (Appuyez sur CTRL, et sélectionnez avec votre souris), et appuyez sur 'C' pour la copier.

Coller n’est pas gratuit, vous devez produire des formes de patrons pour vous le payer (les mêmes que celles que vous venez de livrer). # Special reward, which is shown when there is no reward actually no_reward: title: Niveau suivant desc: >- - Ce niveau n'a pas de récompense mais le prochain, oui !

PS: Vous ne devriez pas détruire votre usine actuelle - Vous aurez besoin de toutes ces formes plus tard pour débloquer des améliorations + Ce niveau n’a pas de récompense mais le prochain, si !

PS : Ne détruisez pas votre usine actuelle — Vous aurez besoin de toutes ces formes plus tard pour débloquer des améliorations. no_reward_freeplay: title: Niveau suivant desc: >- - Bravo ! À propos, plus de contenu est prévu pour la version complète ! + Bravo ! À propos, plus de contenu est prévu pour la version complète ! settings: title: Options categories: general: Général - userInterface: Interface Utilisateur + userInterface: Interface utilisateur advanced: Avancé performance: Performance @@ -644,30 +645,30 @@ settings: dev: Développement staging: Test prod: Production - buildDate: Créé le + buildDate: Créé labels: uiScale: - title: Taille de l'interface + title: Taille de l’interface description: >- - Change la taille de l'interface utilisateur. Cette interface se redimensionnera suivant la résolution de votre appareil, mais cette option contrôle le facteur de résolution. + Change la taille de l’interface utilisateur. Cette interface se redimensionnera suivant la résolution de votre écran, mais cette option contrôle le facteur de résolution. scales: super_small: Très petite small: Petite regular: Normale - large: Large - huge: Très large + large: Grande + huge: Très grande scrollWheelSensitivity: title: Sensibilité du zoom description: >- - Change la sensibilité du zoom (aussi bien de la roulette de la souris que du pavé tactile). + Change la sensibilité du zoom (roulette de la souris et pavé tactile). sensitivity: - super_slow: Super lent + super_slow: Très lent slow: Lent regular: Normal fast: Rapide - super_fast: Super rapide + super_fast: Très rapide fullscreen: title: Plein écran @@ -687,7 +688,7 @@ settings: theme: title: Thème description: >- - Choisissez votre thème (clair / sombre). + Choisissez votre thème (clair / sombre). themes: dark: Sombre @@ -701,7 +702,7 @@ settings: alwaysMultiplace: title: Placement multiple description: >- - Si activé, tous les bâtiments resterons sélectionnés tant que vous n'aurez pas annulé. Ceci revient à garder la touche SHIFT appuyée en permanence. + Si activé, tous les bâtiments resteront sélectionnés tant que vous n’aurez pas annulé. Ceci revient à garder la touche MAJ appuyée en permanence. offerHints: title: Indices @@ -711,13 +712,13 @@ settings: language: title: Langue description: >- - Change la langue. Toutes les traductions sont des contributions des utilisateurs et pourraient être partiellement incomplètes ! + Change la langue. Les traductions sont une contribution des utilisateurs et peuvent être incomplètes ! movementSpeed: title: Vitesse de déplacement - description: Change la vitesse à laquelle l'écran se déplace lors de l'utilisation du clavier. + description: Change la vitesse de déplacement de l’écran avec les touches clavier. speeds: - super_slow: Super lent + super_slow: Très lent slow: Lent regular: Normal fast: Rapide @@ -728,75 +729,75 @@ settings: title: Tunnels intelligents description: >- Si cette option est sélectionnée, placer des tunnels effacera automatiquement les convoyeurs inutiles. - Cela permet aussi d'étirer les tunnels et les tunnels en surnombre seront effacés. + Cela permet aussi d’étirer les tunnels, et les tunnels en surnombre seront effacés. vignette: title: Effet de vignette description: >- - Permet l'affichage de l'effet de vignette qui assombrit les coins de l'écran afin de rendre le texte plus facile à lire. + Permet l’affichage de l’effet de vignette qui assombrit les coins de l’écran afin de rendre le texte plus facile à lire. autosaveInterval: title: Fréquence des sauvegardes automatiques description: >- Contrôle avec quelle fréquence le jeu sera sauvegardé automatiquement. Vous pouvez aussi entièrement désactiver cette fonctionnalité ici. intervals: - one_minute: 1 Minute - two_minutes: 2 Minutes - five_minutes: 5 Minutes - ten_minutes: 10 Minutes - twenty_minutes: 20 Minutes + one_minute: 1 minute + two_minutes: 2 minutes + five_minutes: 5 minutes + ten_minutes: 10 minutes + twenty_minutes: 20 minutes disabled: Désactivé compactBuildingInfo: title: Informations réduites sur les bâtiments description: >- - Raccourcit les panneaux d'information sur les bâtiments en n'affichant que les ratios. Dans le cas contraire, une description et une imagine sont présentés. + Raccourcit les panneaux d’information sur les bâtiments en n’affichant que les ratios. Si désactivé, montre une description et une image. disableCutDeleteWarnings: - title: Désactive les avertissement pour Couper/Effacer + title: Désactive les avertissements pour Couper / Effacer description: >- - Désactive la boîte de dialogue qui s'affiche lorsque vous vous apprêtez à couper/effacer plus de 100 entités. + Désactive la boîte de dialogue qui s’affiche lorsque vous vous apprêtez à couper / effacer plus de 100 entités. enableColorBlindHelper: - title: Mode Daltonien + title: Mode daltonien description: Active divers outils qui permettent de jouer à ce jeu si vous êtes daltonien. rotationByBuilding: title: Rotation par catégorie de bâtiment description: >- Chaque catégorie de bâtiment enregistre le sens de rotation que vous lui avez assigné la dernière fois, de manière individuelle. - Cela sera sans doute plus confortable si vous alternez fréquemment entre le placement de différents types de bâtiments. + Cela sera sans doute plus agréable si vous alternez fréquemment entre le placement de différents types de bâtiments. lowQualityMapResources: title: Ressources de la carte de plus basse qualité description: >- - Simplifie le rendu des ressources sur la carte lorsqu'elle est zoomée opur améliorer les performances. - C'est encore plus clean, n'oubliez pas d'essayer ! + Simplifie le rendu des ressources sur la carte lorsqu’elle est zoomée pour améliorer les performances. + Ça donne un rendu encore plus propre, alors essayez-le ! disableTileGrid: - title: Desactiver la grille de placement + title: Désactiver la grille de placement description: >- - Desactiver la grille de placement peut aider les performances. Ça rend aussi le jeu encore plus uni! + Désactiver la grille de placement peut améliorer les performances. Ça rend aussi l’apparence plus unie ! clearCursorOnDeleteWhilePlacing: - title: Effacer le curseur avec clic droit + title: Déselectionner avec le clic droit description: >- - Activé par défaut, efface le curseur lorsque vous faites un clic droit en ayant un bâtiment selectioné pour la constructio. Si desactivé, vous pouvez detruire les bâtiments en faisant un clic droit tout en placant un bâtiment. + Activé par défaut. Désélectionne le bâtiment choisi pour la construction lorsque vous faites un clic droit sur un bâtiment existant. Si désactivé, vous pouvez détruire des bâtiments avec un clic droit puis continuer de placer le bâtiment sélectionné. lowQualityTextures: - title: Textures de basse résolution (Moche) + title: Textures de basse résolution (moche) description: >- - Utilise des textures de basse qualité pour augmenter les performances. Cela va rendre le jeu moche! + Utilise des textures de basse qualité pour améliorer les performances. Rend le jeu très moche ! displayChunkBorders: - title: Monter les bordures de chunks + title: Monter les secteurs description: >- - Le jeu est divisé en parties de 16x16 cases, si ce réglage est activé, les bordures de chaque partie sont affichées. + Le jeu est divisé en secteurs de 16×16 cases. Si ce réglage est activé, les limites de chaque secteur sont affichées. keybindings: title: Contrôles hint: >- - Astuce: Soyez sûr d'utiliser CTRL, SHIFT et ALT ! Ces touches activent différentes options de placement. + Astuce : N’oubliez pas d’utiliser CTRL, MAJ et ALT ! Ces touches activent différentes options de placement. resetKeybindings: Réinitialiser les contrôles @@ -805,7 +806,7 @@ keybindings: ingame: Jeu navigation: Navigation placement: Placement - massSelect: Suppression de zone + massSelect: Sélection d’une zone buildings: Raccourcis bâtiment placementModifiers: Modificateurs de placement @@ -825,8 +826,8 @@ keybindings: menuOpenShop: Améliorations menuOpenStats: Statistiques - toggleHud: Basculer l'affichage tête haute (ATH) - toggleFPSInfo: Basculer l'affichage des IPS (itérations par seconde) et des informations de débogage + toggleHud: Basculer l’affichage tête haute (ATH) + toggleFPSInfo: Basculer l’affichage des IPS (itérations par seconde) et des informations de débogage belt: *belt splitter: *splitter underground_belt: *underground_belt @@ -840,49 +841,49 @@ keybindings: rotateWhilePlacing: Pivoter rotateInverseModifier: >- - Variante: Pivote à gauche + Variante : Pivote à gauche cycleBuildingVariants: Alterner entre les variantes confirmMassDelete: Confirmer la suppression de la sélection cycleBuildings: Alterner entre les bâtiments - massSelectStart: Cliquez et maintenez pour commencer + massSelectStart: Cliquez et glissez pour commencer massSelectSelectMultiple: Sélectionner plusieurs zones massSelectCopy: Copier la sélection - placementDisableAutoOrientation: Désactiver l'orientation automatique + placementDisableAutoOrientation: Désactiver l’orientation automatique placeMultiple: Rester en mode placement - placeInverse: Inverser le mode d'orientation automatique + placeInverse: Inverser le mode d’orientation automatique pasteLastBlueprint: Copier le dernier patron massSelectCut: Couper la sélection - exportScreenshot: Exporter toute la base en tant qu'image. + exportScreenshot: Exporter une image de toute la base mapMoveFaster: Se déplacer plus vite - lockBeltDirection: Utiliser le plannificateur de convoyeurs - switchDirectionLockSide: "Plannificateur: changer de côté" + lockBeltDirection: Utiliser le planificateur de convoyeurs + switchDirectionLockSide: "Planificateur : changer de côté" pipette: Pipette menuClose: Fermer le menu - switchLayers: Échanger les calques + switchLayers: Basculer le calque advanced_processor: Inverseur de couleur - energy_generator: Générateur d'énergie - wire: Ligne énergétique + energy_generator: Générateur d’énergie + wire: Câble about: title: À propos de ce jeu body: >- Ce jeu est open source et développé par Tobias Springer (c'est moi).

+ target="_blank">Tobias Springer (c’est moi).

Si vous souhaitez contribuer, allez voir shapez.io sur github.

- Ce jeu n'aurait pu être réalisé sans la précieuse communauté Discord autour de - mes jeux - Vous devriez vraiment envisager de joindre le serveur Discord !

+ Ce jeu n’aurait pas pu être réalisé sans la précieuse communauté Discord autour de + mes jeux — Vous devriez vraiment rejoindre le serveur Discord !

La bande son a été créée par Peppsen - Il est impressionnant !

+ target="_blank">Peppsen — Il est génial !

- Pour terminer, un immense merci à mon meilleur amis Niklas - Sans nos sessions sur factorio, ce jeu n'aurait jamais existé. + Pour terminer, un immense merci à mon meilleur ami Niklas — Sans nos sessions sur Factorio, ce jeu n’aurait jamais existé. changelog: title: Historique @@ -893,7 +894,7 @@ demo: importingGames: Importer des sauvegardes oneGameLimit: Limité à une sauvegarde customizeKeybindings: Personnalisation des contrôles - exportingBase: Exporter toute la base en tant qu'image + exportingBase: Exporter une image de toute la base settingNotAvailable: Indisponible dans la démo. # diff --git a/translations/base-ind.yaml b/translations/base-ind.yaml index 80e1238a..22dc0a08 100644 --- a/translations/base-ind.yaml +++ b/translations/base-ind.yaml @@ -22,10 +22,10 @@ --- steamPage: # This is the short text appearing on the steam page - shortText: shapez.io adalah permainan membangun pabrik-pabrik dengan tujuan untuk mengautomatiskan pembentukan dan pemrosesan bentuk-bentuk yang bertambah semakin kompleks di dalam area permainan yang meluas secara tak terhingga. + shortText: Shapez.io adalah game tentang membangun pabrik untuk mengotomatiskan pembuatan dan pemrosesan bentuk-bentuk yang semakin kompleks di peta yang meluas tanpa batas. # This is the text shown above the discord link - discordLink: Tautan Resmi Discord – Obrol dengan saya! + discordLink: Tautan Resmi Discord – Mari mengobrol dengan saya! # This is the long description for the steam page - It is contained here so you can help to translate it, and I will regulary update the store page. # NOTICE: # - Do not translate the first line (This is the gif image at the start of the store) @@ -33,13 +33,13 @@ steamPage: longText: >- [img]{STEAM_APP_IMAGE}/extras/store_page_gif.gif[/img] - shapez.io adalah permainan membangun pabrik-pabrik dengan tujuan untuk mengautomatiskan pembentukan dan pemrosesan bentuk-bentuk yang bertambah semakin kompleks di dalam area permainan yang meluas secara tak terhingga. + shapez.io adalah permainan membangun pabrik dengan tujuan untuk mengautomatiskan pembentukan dan pemrosesan bentuk-bentuk yang bertambah semakin kompleks di peta yang meluas tanpa batas. Setelah pengiriman bentuk-bentuk yang diminta, Anda akan maju dalam permainan dan membuka tingkatan versi-versi mesin selanjutnya untuk mempercepat pabrik Anda. - Seiring meningkatnya kesulitan dari bentuk-bentuk yang diminta, Anda harus meningkatkan pabrik Anda untuk mengatasi kesulitan tersebut – Jangan lupa dengan sumber-sumber daya, Anda harus memperluas ke seluruh [b]area yang tidak terbatas[/b]! + Seiring meningkatnya kesulitan dari bentuk-bentuk yang diminta, Anda harus meningkatkan pabrik Anda untuk mengatasi kesulitan tersebut – Jangan lupa dengan sumber daya, Anda harus memperluas ke seluruh [b]area yang tidak terbatas[/b]! - Kemudian Anda harus mencampurkan warna-warna dan mencat bentuk-bentuk dengannya – Campurkan merah, hijau, dan biru untuk memproduksi warna-warna lain dan mencat bentuk-bentuk dengannya untuk memenuhi permintaan. + Kemudian Anda harus mencampurkan warna-warna dan mencat bentuk-bentuk tersebut – Campurkan merah, hijau, dan biru untuk memproduksi warna-warna lain dan mencat bentuk-bentuk tersebut untuk memenuhi permintaan. Permainan ini mempunyai 18 level-level progresif (yang mana akan membuat Anda sibuk berjam-jam!), akan tetapi saya akan terus menambahkan konten-konten baru – Ada banyak yang direncanakan! @@ -48,16 +48,16 @@ steamPage: [b]Keuntungan Versi Penuh[/b] [list] - [*] Versi Permainan Gelap + [*] Mode Malam [*] Titik Arah Tak Terhingga [*] Penyimpanan Permainan Tak Terhingga [*] Pengaturan-pengaturan Tambahan - [*] Akan datang: Kawat & Energi! Akan dicoba untuk dicapai untuk (kira-kira) akhir Juli 2020. + [*] Akan datang: Kawat & Energi! Akan dicoba untuk dicapai sekitar akhir Juli 2020. [*] Akan datang: Level-level tambahan - [*] Memperkenankan saya untuk terus mengembangkan shapez.io ❤️ + [*] Mendukung saya untuk terus mengembangkan shapez.io ❤️ [/list] - [b]Pembaruan di Masa Depan[/b] + [b]Pembaruan di masa yang akan datang[/b] Saya seringkali membarui permainan ini dan terus mencoba untuk menciptakan pembaruan paling sedikit sekali seminggu! @@ -87,8 +87,8 @@ steamPage: [/list] global: - loading: Memuat - error: Terdapat kesalahan + loading: Sedang memuat + error: Terjadi kesalahan # How big numbers are rendered, e.g. "10,000" thousandsDivider: "," @@ -98,9 +98,9 @@ global: # The suffix for large numbers, e.g. 1.3k, 400.2M, etc. suffix: - thousands: k - millions: M - billions: B + thousands: K + millions: J + billions: M trillions: T # Shown for infinitely big numbers @@ -108,13 +108,13 @@ global: time: # Used for formatting past time dates - oneSecondAgo: satu detik yang lalu + oneSecondAgo: sedetik yang lalu xSecondsAgo: detik yang lalu - oneMinuteAgo: satu menit yang lalu + oneMinuteAgo: semenit yang lalu xMinutesAgo: menit yang lalu - oneHourAgo: satu jam yang lalu + oneHourAgo: sejam yang lalu xHoursAgo: jam yang lalu - oneDayAgo: satu hari yang lalu + oneDayAgo: sehari yang lalu xDaysAgo: hari yang lalu # Short formats for times, e.g. '5h 23m' @@ -142,7 +142,7 @@ mainMenu: play: Mulai Permainan continue: Lanjutkan Permainan newGame: Permainan Baru - changelog: Ganti Data Log + changelog: Catatan Perubahan subreddit: Reddit importSavegame: Impor Data Simpanan openSourceHint: Permainan ini bekerja secara open source! @@ -166,7 +166,7 @@ dialogs: restart: Mulai Ulang reset: Setel Ulang getStandalone: Dapatkan Versi Penuh - deleteGame: Saya tahu apa yang saya kerjakan + deleteGame: Saya tahu apa yang saya lakukan viewUpdate: Tampilkan Pembaruan showUpgrades: Tunjukkan Tingkatan showKeybindings: Tunjukan Tombol Pintas diff --git a/translations/base-nl.yaml b/translations/base-nl.yaml index 2f061268..01dd79ee 100644 --- a/translations/base-nl.yaml +++ b/translations/base-nl.yaml @@ -87,7 +87,7 @@ steamPage: global: loading: Laden - error: Error + error: Fout # How big numbers are rendered, e.g. "10,000" thousandsDivider: "." @@ -172,7 +172,7 @@ dialogs: showKeybindings: Zie Sneltoetsen importSavegameError: - title: Importeer error + title: Importeerfout text: >- Het importeren van je savegame is mislukt: @@ -322,7 +322,7 @@ ingame: # is replaced by the actual level, so this gets 'Level 03' for example. levelTitle: Level completed: Voltooid - unlockText: Ontgrendeld ! + unlockText: ontgrendeld! buttonNextLevel: Volgende Level # Notifications on the lower right @@ -633,9 +633,9 @@ storyRewards: settings: title: Opties categories: - general: General - userInterface: User Interface - advanced: Advanced + general: Algemeen + userInterface: Opmaak + advanced: Geavanceerd versionBadges: dev: Ontwikkeling @@ -795,8 +795,8 @@ keybindings: menuOpenShop: Upgrades menuOpenStats: Statistieken - toggleHud: Toggle HUD - toggleFPSInfo: Toggle FPS en Debug Info + toggleHud: Schakel HUD + toggleFPSInfo: Schakel FPS en Debug Info belt: *belt splitter: *splitter underground_belt: *underground_belt diff --git a/translations/base-pt-BR.yaml b/translations/base-pt-BR.yaml index bf269558..3a4fef2c 100644 --- a/translations/base-pt-BR.yaml +++ b/translations/base-pt-BR.yaml @@ -22,7 +22,7 @@ --- steamPage: # This is the short text appearing on the steam page - shortText: shapez.io é um jogo sobre construir fábricas, automatizando a criação e combinação de formas cada vez mais complexas num mapa infinito. + shortText: Shapez.io é um jogo sobre construir fábricas, automatizando a criação e combinação de formas cada vez mais complexas num mapa infinito. # This is the text shown above the Discord link discordLink: Discord Oficial - Converse comigo! @@ -34,17 +34,17 @@ steamPage: longText: >- [img]{STEAM_APP_IMAGE}/extras/store_page_gif.gif[/img] - shapez.io é um jogo sobre construir fábricas, automatizando a criação e combinação de formas cada vez mais complexas num mapa infinito. + Shapez.io é um jogo sobre construir fábricas, automatizando a criação e combinação de formas cada vez mais complexas num mapa infinito. - Após a entrega das formas requisitadas você progredirá no jogo e desbloqueará melhorias para acelerar sua fábrica. + Após a entrega das formas requisitadas, você avançará no jogo e desbloqueará melhorias para acelerar sua produção. - Conforme sua demanda por formas aumenta, você irá que aumentar sua fábrica para alcançar-la - Mas não se esqueça dos recursos, você precisará expandir pelo [b]mapa infinito[/b]! + Conforme sua demanda por formas aumenta, você terá que aumentar sua fábrica para alcançá-la - Mas não se esqueça dos recursos, você precisará expandir pelo [b]mapa infinito[/b]! - Rapidamente você vai ter que misturar cores e pintar suas formas com elas - Combine recursos vermelhos, verdes e azuis para produzir cores diferentes e pintar formas com elas para satisfazer a demanda. + Em pouco tempo você terá que misturar cores e pintar suas formas com elas - Combine recursos vermelhos, verdes e azuis para produzir cores diferentes e pintar formas com elas para satisfazer a demanda. - O jogo contém 18 níveis progressivos (Que já devem manter você ocupado por horas!) mas eu adiciono novo contéudo constantemente - Tem bastante coisa já planejada! + O jogo contém 18 níveis progressivos (que já devem manter você ocupado por horas!) mas eu adiciono novo contéudo constantemente - Tem bastante coisa já planejada! - Comprando o jogo você terá acesso à versão completa, que contém recursos adicionais, e além disso você também terá acesso aos recursos que seram desenvolvidos. + Comprando o jogo você terá acesso à versão completa, que contém recursos adicionais, e além disso você também terá acesso aos recursos que serão desenvolvidos. [b]Vantagens da versão completa[/b] @@ -60,15 +60,15 @@ steamPage: [b]Atualizações Futuras[/b] - Eu lanço atualizações frequentemente e estou tentando lançar pelo menos um por semana! + Eu lanço atualizações frequentemente e estou tentando lançar pelo menos uma por semana! [list] [*] Mapas diferentes e desafios (por exemplo mapas com obstáculos) [*] Puzzles (Entregue a forma pedida com uma área restringida ou um certo conjunto de construções) [*] Um modo história onde as construções têm um custo - [*] Um geredor de mapa customizável (Configure recursos, forma, tamanho, densidade, semente e mais) + [*] Um gerador de mapa customizável (configure recursos, formas, tamanho, densidade, semente e mais) [*] Mais tipos de formas - [*] Melhorias de desempenho (O jogo já roda bem!) + [*] Melhorias de desempenho (o jogo já roda bem!) [*] E muito mais! [/list] @@ -130,8 +130,8 @@ global: control: CTRL alt: ALT escape: ESC - shift: SHIFT - space: ESPAÇO + shift: Shift + space: Espaço demoBanners: # This is the "advertisement" shown in the main menu and other various places @@ -145,7 +145,7 @@ mainMenu: newGame: Novo jogo changelog: Changelog subreddit: Reddit - importSavegame: Importar + importSavegame: Importar save openSourceHint: Esse jogo tem código aberto! discordLink: Discord oficial helpTranslate: Ajude a traduzir! @@ -183,7 +183,7 @@ dialogs: Seu jogo salvo foi importado com sucesso. gameLoadFailure: - title: Jogo salvo quebrado + title: Jogo salvo corrompido text: >- Houve uma falha ao carregar seu jogo salvo: @@ -198,13 +198,13 @@ dialogs: Houve uma falha ao deletar seu jogo salvo: restartRequired: - title: Ação necessária + title: Reinicialização necessária text: >- Você precisa reiniciar o jogo para aplicar as mudanças. editKeybinding: title: Alterar tecla - desc: Pressiona a tecla que deseja vincular, ou ESC para cancelar. + desc: Pressione a tecla que deseja vincular, ou ESC para cancelar. resetKeybindingsConfirmation: title: Resetar controles @@ -234,17 +234,17 @@ dialogs: O guia de melhorias pode ser encontrado no canto superior direito da tela. massDeleteConfirm: - title: Deletar + title: Deletar? desc: >- Você está deletando vários objetos ( para ser exato)! Você quer continuar? massCutConfirm: - title: Confirmar corte + title: Confirmar corte? desc: >- Você está cortando vários objetos ( para ser exato)! Você quer continuar? massCutInsufficientConfirm: - title: Confirmar Corte + title: Confirmar Corte? desc: >- You can not afford to paste this area! Are you sure you want to cut it? @@ -290,8 +290,8 @@ ingame: createMarker: Criar marcador delete: Destruir pasteLastBlueprint: Colar último projeto - lockBeltDirection: Ativar Planejador de Esteiras - plannerSwitchSide: Girar Planejador + lockBeltDirection: Ativar Planejamento de Esteiras + plannerSwitchSide: Girar Planejamento cutSelection: Cortar copySelection: Copiar clearSelection: Limpar Seleção @@ -363,13 +363,13 @@ ingame: dataSources: stored: title: Estoque - description: Exibindo a quantidade de formas armazenadas em sua construção central. + description: Exibindo a quantidade de formas armazenadas no seu HUB. produced: title: Produção description: Exibindo todas as formas que toda a sua fábrica produz, incluindo produtos intermediários.. delivered: title: Entregue - description: Exibindo formas entregues na sua construção central. + description: Exibindo formas entregues no seu HUB. noShapesProduced: Nenhuma forma foi produzida até o momento. # Displays the shapes per minute, e.g. '523 / m' @@ -384,14 +384,14 @@ ingame: buttons: continue: Continuar - settings: Definições + settings: Configurações menu: Voltar ao menu # Bottom left tutorial hints tutorialHints: title: Quer ajuda? showHint: Mostrar dica - hideHint: Fechar + hideHint: Esconder dica # When placing a blueprint blueprintPlacer: @@ -401,7 +401,7 @@ ingame: waypoints: waypoints: Marcadores hub: HUB - description: Clique com o botão esquerdo do mouse em um marcador para pular, clique com o botão direito do mouse para excluí-lo.

Pressione para criar um marcador a partir da exibição atual ou clique com o botão direito do mouse para criar um marcador no local selecionado. + description: Clique com o botão esquerdo do mouse em um marcador para pular, clique com o botão direito do mouse para excluí-lo.

Pressione para criar um marcador à partir da exibição atual ou clique com o botão direito do mouse para criar um marcador no local selecionado. creationSuccessNotification: Marcador criado. # Shape viewer @@ -419,7 +419,7 @@ ingame: Conecte o extrator com uma esteira transportadora até a sua base!

Dica, clique e arraste a esteira com o mouse! 1_3_expand: >- - Este NÃO é um jogo inativo! Construa mais extratores e esteiras para concluir o objetivo mais rapidamente.

Dica, segure SHIFT para colocar vários extratores e use R para girá-los. + Este NÃO é um jogo idle! Construa mais extratores e esteiras para concluir o objetivo mais rapidamente.

Dica, segure SHIFT para colocar vários extratores e use R para girá-los. # All shop upgrades shopUpgrades: @@ -433,7 +433,7 @@ shopUpgrades: name: Corte, Rotação e Montagem description: Velocidade x → x painting: - name: Mistura e Pintura + name: Mistura de cores e Pintura description: Velocidade x → x # Buildings and their name / description @@ -469,7 +469,7 @@ buildings: tier2: name: Túnel Classe II - description: Permite transportar recursos por baixo de construções e esteiras. + description: Permite transportar recursos por baixo de construções e outras esteiras. splitter: # Internal name for the Balancer default: @@ -541,7 +541,7 @@ buildings: storage: name: Estoque - description: Armazena itens em excesso, até uma determinada capacidade. Pode ser usado como uma porta de transbordamento. + description: Armazena itens em excesso, até uma determinada capacidade. Pode ser usado como uma eclusa. energy_generator: deliver: Entregar @@ -575,7 +575,7 @@ storyRewards: reward_painter: title: Pintura desc: >- - O Pintor foi desbloqueado - Extrai alguns pigmentos coloridos (assim como você fez com as formas) e combina-os com uma forma no pintor para os colorir!

PS: Se for daltônico, existe um modo daltônico nas definições! + O Pintor foi desbloqueado - Extraia alguns pigmentos coloridos (assim como você fez com as formas) e combine-os com uma forma no pintor para colorí-las!

PS: Se for daltônico, existe um modo daltônico nas definições! reward_mixer: title: Misturando cores @@ -591,7 +591,7 @@ storyRewards: reward_tunnel: title: Túnel - desc: O túnel foi desbloqueado - Agora você pode canalizar itens sob construções! + desc: O túnel foi desbloqueado - Agora você pode transportar itens abaixo do solo! reward_rotater_ccw: title: Rotação anti-horária @@ -646,7 +646,7 @@ storyRewards: Parabéns! Aliás, mais conteúdo vindo na versão completa! settings: - title: opções + title: Opções categories: general: Geral userInterface: Interface de Usuário @@ -671,7 +671,7 @@ settings: huge: Gigante autosaveInterval: - title: Intervalo de gravação automática + title: Intervalo de save automático description: >- Controla a frequência com que o jogo salva automaticamente. Você também pode desativá-lo totalmente aqui. @@ -714,7 +714,7 @@ settings: enableColorBlindHelper: title: Modo daltônico. description: >- - Permite várias ferramentas que permitem jogar se você é daltônico. + Habilita várias ferramentas que te permitem jogar se você é daltônico. fullscreen: title: Tela Cheia @@ -745,24 +745,24 @@ settings: Se você possui um monitor de 144 hz, altere a taxa de atualização aqui para que o jogo seja simulado corretamente com taxas de atualização mais altas. Isso diminuir o FPS consideravelmente se o computador for muito lento. alwaysMultiplace: - title: Multiplicidade + title: Posicionamento Múltiplo description: >- Se ativado, todas as construções permanecerão selecionadas após o posicionamento até que você a cancele. Isso é equivalente a pressionar SHIFT permanentemente. offerHints: - title: Dicas e tutoriais + title: Dicas e Tutoriais description: >- Se ativado, oferece dicas e tutoriais enquanto se joga. Além disso, esconde certos elementos da interface até certo ponto, para facilitar o começo do jogo. enableTunnelSmartplace: - title: Túneis inteligentes + title: Túneis Inteligentes description: >- Quando colocados, irão remover automaticamente esteiras desnecessárias. Isso também permite arrastar túneis e túneis em excesso serão removidos. vignette: title: Vinheta description: >- - Permite o modo vinheta que escurece os cantos da tela e facilita a leitura do texto. + Habilita o modo vinheta que escurece os cantos da tela e facilita a leitura do texto. rotationByBuilding: title: Rotação por tipo de construção @@ -777,7 +777,7 @@ settings: disableCutDeleteWarnings: title: Desativar avisos de recorte / exclusão description: >- - Desative as caixas de diálogo de aviso exibidas ao cortar / excluir mais de 100 entidades. + Desativa as caixas de diálogo de aviso exibidas ao cortar / excluir mais de 100 entidades. keybindings: title: Controles @@ -790,9 +790,9 @@ keybindings: general: Geral ingame: Jogo navigation: Navegação - placement: Construção - massSelect: Seleção - buildings: Atalhos de objetos + placement: Posicionamento + massSelect: Seleção em Massa + buildings: Construções placementModifiers: Modificadores mappings: @@ -807,7 +807,7 @@ keybindings: mapZoomIn: Aproximar mapZoomOut: Distanciar - createMarker: Criar marcação + createMarker: Criar marcador menuOpenShop: Melhorias menuOpenStats: Estatísticas @@ -839,7 +839,7 @@ keybindings: confirmMassDelete: Confirmar exclusão em massa pasteLastBlueprint: Colar último projeto cycleBuildings: Trocar de construção - lockBeltDirection: Ativar planejador de correia + lockBeltDirection: Ativar planejamento de esteira switchDirectionLockSide: >- Planejador: Mudar de lado